physics and rendering fixes
This commit is contained in:
@@ -21,6 +21,7 @@ errorret_t shaderInit(shader_t *shader, const shaderdefinition_t *def) {
|
||||
|
||||
errorret_t shaderBind(shader_t *shader) {
|
||||
assertNotNull(shader, "Shader cannot be null");
|
||||
if(bound == shader) errorOk();
|
||||
errorChain(shaderBindPlatform(shader));
|
||||
bound = shader;
|
||||
errorOk();
|
||||
|
||||
@@ -6,6 +6,28 @@
|
||||
*/
|
||||
|
||||
#include "tileshape.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
// Per-shape corner heights as [sw, se, ne, nw] offsets (0 or 1) from the
|
||||
// tile's base Z layer - transcribed verbatim from the mesh generator's
|
||||
// ramp tables (tools/asset/chunk/__main__.py _RAMP_CORNERS,
|
||||
// editor/client/public/common/chunkterrain.js RAMP_CORNERS) so the
|
||||
// walkable surface always matches what's rendered.
|
||||
static const float_t TILE_SHAPE_RAMP_CORNERS[TILE_SHAPE_COUNT][4] = {
|
||||
[TILE_SHAPE_GROUND] = { 0.0f, 0.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTH] = { 0.0f, 0.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTH] = { 1.0f, 1.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_EAST] = { 0.0f, 1.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_WEST] = { 1.0f, 0.0f, 0.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHEAST] = { 0.0f, 0.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHWEST] = { 0.0f, 0.0f, 0.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST] = { 0.0f, 1.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST] = { 1.0f, 0.0f, 0.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHEAST_INNER] = { 0.0f, 1.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_NORTHWEST_INNER] = { 1.0f, 0.0f, 1.0f, 1.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHEAST_INNER] = { 1.0f, 1.0f, 1.0f, 0.0f },
|
||||
[TILE_SHAPE_RAMP_SOUTHWEST_INNER] = { 1.0f, 1.0f, 0.0f, 1.0f },
|
||||
};
|
||||
|
||||
bool_t tileShapeIsWalkable(const tileshape_t shape) {
|
||||
switch(shape) {
|
||||
@@ -36,4 +58,24 @@ bool_t tileShapeIsRamp(const tileshape_t shape) {
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
float_t tileShapeGetRampHeight(
|
||||
const tileshape_t shape, const float_t localX, const float_t localY
|
||||
) {
|
||||
assertTrue(localX >= 0.0f && localX <= 1.0f, "localX must be in [0,1]");
|
||||
assertTrue(localY >= 0.0f && localY <= 1.0f, "localY must be in [0,1]");
|
||||
|
||||
const float_t sw = TILE_SHAPE_RAMP_CORNERS[shape][0];
|
||||
const float_t se = TILE_SHAPE_RAMP_CORNERS[shape][1];
|
||||
const float_t ne = TILE_SHAPE_RAMP_CORNERS[shape][2];
|
||||
const float_t nw = TILE_SHAPE_RAMP_CORNERS[shape][3];
|
||||
|
||||
// Planar interpolation per triangle, split along the SW-NE diagonal -
|
||||
// matches the two-triangle quad the mesh generator emits exactly,
|
||||
// rather than a smooth (but unrendered) bilinear blend.
|
||||
if(localY <= localX) {
|
||||
return sw + (se - sw) * localX + (ne - se) * localY;
|
||||
}
|
||||
return sw + (ne - nw) * localX + (nw - sw) * localY;
|
||||
}
|
||||
@@ -38,8 +38,25 @@ bool_t tileShapeIsRamp(const tileshape_t shape);
|
||||
|
||||
/**
|
||||
* Returns whether or not the given tile shape is walkable.
|
||||
*
|
||||
*
|
||||
* @param shape The tile shape to check.
|
||||
* @return bool_t True if walkable, false if not.
|
||||
*/
|
||||
bool_t tileShapeIsWalkable(const tileshape_t shape);
|
||||
bool_t tileShapeIsWalkable(const tileshape_t shape);
|
||||
|
||||
/**
|
||||
* Returns the floor height offset (0 to 1, above the tile's base Z
|
||||
* layer) for the given tile shape at a local position within the tile.
|
||||
* Flat ground is always 0. Ramp shapes vary linearly across the tile,
|
||||
* matching the two-triangle mesh generated for rendering (split along
|
||||
* the SW-NE diagonal) exactly, so the walkable surface and the
|
||||
* rendered surface never disagree.
|
||||
*
|
||||
* @param shape The tile shape to query.
|
||||
* @param localX Local X position within the tile, in [0, 1].
|
||||
* @param localY Local Y position within the tile, in [0, 1].
|
||||
* @return float_t The height offset above the tile's base Z layer.
|
||||
*/
|
||||
float_t tileShapeGetRampHeight(
|
||||
const tileshape_t shape, const float_t localX, const float_t localY
|
||||
);
|
||||
@@ -81,8 +81,13 @@ void physicsWorldResolveAxisX(
|
||||
for(worldunit_t col = oldColMax + 1; col <= newColMax; col++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
const worldpos_t pos = { col, y, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
col, y, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
@@ -107,8 +112,13 @@ void physicsWorldResolveAxisX(
|
||||
for(worldunit_t col = oldColMin - 1; col >= newColMin; col--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t y = yStart; y <= yEnd; y++) {
|
||||
const worldpos_t pos = { col, y, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
col, y, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
@@ -159,8 +169,13 @@ void physicsWorldResolveAxisY(
|
||||
for(worldunit_t row = oldRowMax + 1; row <= newRowMax; row++) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
const worldpos_t pos = { x, row, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, row, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
@@ -185,8 +200,13 @@ void physicsWorldResolveAxisY(
|
||||
for(worldunit_t row = oldRowMin - 1; row >= newRowMin; row--) {
|
||||
bool_t blocked = false;
|
||||
for(worldunit_t x = xStart; x <= xEnd; x++) {
|
||||
const worldpos_t pos = { x, row, layerZ };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t unusedShape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, row, layerZ, true, &unusedBase, &unusedShape
|
||||
)
|
||||
) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
@@ -216,9 +236,6 @@ void physicsWorldResolveAxisZ(
|
||||
assertNotNull(world, "world must not be null");
|
||||
assertNotNull(body, "body must not be null");
|
||||
|
||||
const float_t vz = body->velocity[2];
|
||||
if(vz == 0.0f) return;
|
||||
|
||||
vec3 min, max;
|
||||
physicsBodyGetBounds(body, min, max);
|
||||
|
||||
@@ -226,22 +243,86 @@ void physicsWorldResolveAxisZ(
|
||||
const worldunit_t xEnd = (worldunit_t)floorf(max[0] - PHYSICS_EPSILON);
|
||||
const worldunit_t yStart = (worldunit_t)floorf(min[1] + PHYSICS_EPSILON);
|
||||
const worldunit_t yEnd = (worldunit_t)floorf(max[1] - PHYSICS_EPSILON);
|
||||
const worldunit_t layer =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
const float_t vz = body->velocity[2];
|
||||
|
||||
if(vz < 0.0f) {
|
||||
const worldunit_t layer =
|
||||
(worldunit_t)floorf(body->position[2] + PHYSICS_EPSILON);
|
||||
// Whether a column not directly at the current layer should still
|
||||
// count as walkable if its own tile sits one layer above or below -
|
||||
// each column only ever has one real tile, so this is what lets a
|
||||
// column just crossed into horizontally (see the +/-1 layer check in
|
||||
// ResolveAxisX/Y) be recognised here too, whether its tile sits one
|
||||
// layer above (finishing a climb) or below (starting a descent).
|
||||
// Gating on horizontal movement (rather than vz's sign) keeps this
|
||||
// from ever mistaking a genuine ceiling directly above a stationary
|
||||
// body - e.g. the frame right after being blocked by one, when vz has
|
||||
// just been zeroed - for ground to snap up onto.
|
||||
const bool_t movingHorizontally =
|
||||
body->velocity[0] != 0.0f || body->velocity[1] != 0.0f;
|
||||
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
const worldpos_t pos = { x, y, layer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) solid = false;
|
||||
// Walkability across the whole footprint - every spanned column must
|
||||
// have a walkable tile (at the current layer, or the +/-1 fallback
|
||||
// above) for this layer to count as solid, same as before ramps.
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
worldunit_t unusedBase;
|
||||
tileshape_t shape;
|
||||
if(
|
||||
!physicsWorldFindColumnTile(
|
||||
x, y, layer, movingHorizontally, &unusedBase, &shape
|
||||
)
|
||||
) {
|
||||
solid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ground/ramp height comes from a single reference column - the
|
||||
// footprint's center, not the tallest of every spanned column. A 1-
|
||||
// wide body sitting at a non-integer position always straddles two
|
||||
// columns (e.g. [1.01, 2.01)); using the tallest of them would yank a
|
||||
// body barely touching a taller neighbouring column onto its full
|
||||
// height, rather than the column it's actually standing on.
|
||||
float_t groundHeight = (float_t)layer;
|
||||
if(solid) {
|
||||
const float_t centerX = body->position[0] + body->extents[0] * 0.5f;
|
||||
const float_t centerY = body->position[1] + body->extents[1] * 0.5f;
|
||||
const worldunit_t centerCol =
|
||||
(worldunit_t)floorf(centerX + PHYSICS_EPSILON);
|
||||
const worldunit_t centerRow =
|
||||
(worldunit_t)floorf(centerY + PHYSICS_EPSILON);
|
||||
|
||||
worldunit_t tileBase;
|
||||
tileshape_t shape;
|
||||
physicsWorldFindColumnTile(
|
||||
centerCol, centerRow, layer, movingHorizontally, &tileBase, &shape
|
||||
);
|
||||
|
||||
const float_t localX = mathClamp(centerX - centerCol, 0.0f, 1.0f);
|
||||
const float_t localY = mathClamp(centerY - centerRow, 0.0f, 1.0f);
|
||||
groundHeight =
|
||||
(float_t)tileBase + tileShapeGetRampHeight(shape, localX, localY);
|
||||
}
|
||||
|
||||
// Standing on, or embedded below, the ground/ramp surface - snap up to
|
||||
// it regardless of vertical velocity direction. Gravity only ever
|
||||
// pulls down, so walking onto a rising ramp needs this explicit lift
|
||||
// rather than just a fall-clamp.
|
||||
if(solid && body->position[2] < groundHeight) {
|
||||
body->position[2] = groundHeight;
|
||||
if(vz <= 0.0f) body->velocity[2] = 0.0f;
|
||||
body->grounded = true;
|
||||
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if(vz == 0.0f) return;
|
||||
|
||||
if(vz < 0.0f) {
|
||||
const float_t newZ = body->position[2] + vz * dt;
|
||||
if(solid && newZ < (float_t)layer) {
|
||||
body->position[2] = (float_t)layer;
|
||||
if(solid && newZ < groundHeight) {
|
||||
body->position[2] = groundHeight;
|
||||
body->velocity[2] = 0.0f;
|
||||
body->grounded = true;
|
||||
} else {
|
||||
@@ -250,19 +331,19 @@ void physicsWorldResolveAxisZ(
|
||||
}
|
||||
} else {
|
||||
const float_t head = max[2];
|
||||
const worldunit_t layer = (worldunit_t)ceilf(head - PHYSICS_EPSILON);
|
||||
const worldunit_t ceilLayer = (worldunit_t)ceilf(head - PHYSICS_EPSILON);
|
||||
|
||||
bool_t solid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && solid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && solid; y++) {
|
||||
const worldpos_t pos = { x, y, layer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) solid = false;
|
||||
bool_t ceilingSolid = true;
|
||||
for(worldunit_t x = xStart; x <= xEnd && ceilingSolid; x++) {
|
||||
for(worldunit_t y = yStart; y <= yEnd && ceilingSolid; y++) {
|
||||
const worldpos_t pos = { x, y, ceilLayer };
|
||||
if(!tileShapeIsWalkable(mapGetTile(pos).shape)) ceilingSolid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const float_t newHead = head + vz * dt;
|
||||
if(solid && newHead > (float_t)layer) {
|
||||
body->position[2] = (float_t)layer - body->extents[2];
|
||||
if(ceilingSolid && newHead > (float_t)ceilLayer) {
|
||||
body->position[2] = (float_t)ceilLayer - body->extents[2];
|
||||
body->velocity[2] = 0.0f;
|
||||
} else {
|
||||
body->position[2] += vz * dt;
|
||||
@@ -272,6 +353,41 @@ void physicsWorldResolveAxisZ(
|
||||
physicsWorldResolveBodyOverlap(body, 2, others, othersCount);
|
||||
}
|
||||
|
||||
bool_t physicsWorldFindColumnTile(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t layer,
|
||||
const bool_t movingHorizontally,
|
||||
worldunit_t *outBase,
|
||||
tileshape_t *outShape
|
||||
) {
|
||||
assertNotNull(outBase, "outBase must not be null");
|
||||
assertNotNull(outShape, "outShape must not be null");
|
||||
|
||||
tile_t tile = mapGetTile((worldpos_t){ x, y, layer });
|
||||
worldunit_t base = layer;
|
||||
|
||||
if(!tileShapeIsWalkable(tile.shape) && movingHorizontally) {
|
||||
const tile_t tileAbove =
|
||||
mapGetTile((worldpos_t){ x, y, (worldunit_t)(layer + 1) });
|
||||
if(tileShapeIsWalkable(tileAbove.shape)) {
|
||||
tile = tileAbove;
|
||||
base = (worldunit_t)(layer + 1);
|
||||
} else {
|
||||
const tile_t tileBelow =
|
||||
mapGetTile((worldpos_t){ x, y, (worldunit_t)(layer - 1) });
|
||||
if(tileShapeIsWalkable(tileBelow.shape)) {
|
||||
tile = tileBelow;
|
||||
base = (worldunit_t)(layer - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*outBase = base;
|
||||
*outShape = tile.shape;
|
||||
return tileShapeIsWalkable(tile.shape);
|
||||
}
|
||||
|
||||
void physicsWorldResolveBodyOverlap(
|
||||
physicsbody_t *body,
|
||||
const uint8_t axis,
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
#include "physicsbody.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
|
||||
#define PHYSICS_WORLD_GRAVITY_DEFAULT 20.0f
|
||||
#define PHYSICS_WORLD_TERMINAL_VELOCITY_DEFAULT 40.0f
|
||||
@@ -46,12 +48,30 @@ void physicsWorldInit(
|
||||
* bodies (see physicsWorldResolveBodyOverlap) - so another body is
|
||||
* treated as solid too, not just the tile map.
|
||||
*
|
||||
* Ramp tiles are walkable at a continuously-varying height (see
|
||||
* tileShapeGetRampHeight) rather than a flat Z layer - walking across one
|
||||
* lifts or drops the body smoothly to match the tile's sloped surface,
|
||||
* snapping upward as needed since gravity alone only ever pulls down.
|
||||
*
|
||||
* Known limitations, deliberately out of scope for this very basic pass:
|
||||
* - No ramp/slope support - ramp tiles are treated as flat walkable
|
||||
* ground at whatever Z layer they are queried at.
|
||||
* - No wall/hole distinction - horizontal movement treats any
|
||||
* non-walkable (or unloaded) column at the body's current Z layer as
|
||||
* a solid wall, rather than an edge to fall from.
|
||||
* - Ceiling/head-bump checks are still flat and ramp-unaware - only the
|
||||
* walking surface accounts for slope, not the underside of a ramp
|
||||
* above the body.
|
||||
* - Descending a fast-dropping ramp relies on gravity to close the gap
|
||||
* each step rather than an explicit "snap down" - imperceptible at
|
||||
* normal walk/run speeds given the default gravity, but a real
|
||||
* asymmetry with the "snap up" case.
|
||||
* - A body whose footprint spans multiple tile columns (unused by any
|
||||
* entity today - all default to a 1x1 footprint) queries each
|
||||
* column's ramp height using its own clamped local coordinate
|
||||
* independently, rather than blending a single surface across tiles.
|
||||
* - Limited wall/hole distinction - horizontal movement checks the
|
||||
* body's current Z layer plus one layer above and below (each column
|
||||
* only ever stores a single tile at one Z layer, so this covers
|
||||
* stepping onto an adjacent ascending or descending ramp/ledge
|
||||
* whose own layer differs from the body's current one). A column
|
||||
* with nothing walkable within one layer either way is still treated
|
||||
* as a solid wall rather than a deeper edge to fall from.
|
||||
* - A single step can tunnel through an intervening solid Z layer if
|
||||
* velocity.z * dt exceeds one grid unit; terminalVelocity bounds this
|
||||
* but does not eliminate it for very small/thin floors.
|
||||
@@ -152,3 +172,30 @@ void physicsWorldResolveBodyOverlap(
|
||||
physicsbody_t * const *others,
|
||||
const uint32_t othersCount
|
||||
);
|
||||
|
||||
/**
|
||||
* Finds the walkable tile for a single tile column (x, y) near the given
|
||||
* layer: checks the layer itself first, and - only if movingHorizontally
|
||||
* is true, so a stationary body never mistakes a genuine ceiling above
|
||||
* it for ground - falls back to one layer above then one layer below.
|
||||
* Each column only ever has one real tile, so this lets a column whose
|
||||
* own tile sits adjacent to the current layer (e.g. finishing a climb
|
||||
* onto, or starting a descent from, an adjacent ramp/ledge) be found.
|
||||
*
|
||||
* @param x Column X coordinate.
|
||||
* @param y Column Y coordinate.
|
||||
* @param layer The layer to check first.
|
||||
* @param movingHorizontally Whether to also try one layer above/below.
|
||||
* @param outBase Output, set to the layer the found tile actually sits
|
||||
* at (layer, layer + 1, or layer - 1).
|
||||
* @param outShape Output, set to the found tile's shape.
|
||||
* @return true if a walkable tile was found.
|
||||
*/
|
||||
bool_t physicsWorldFindColumnTile(
|
||||
const worldunit_t x,
|
||||
const worldunit_t y,
|
||||
const worldunit_t layer,
|
||||
const bool_t movingHorizontally,
|
||||
worldunit_t *outBase,
|
||||
tileshape_t *outShape
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include "uifps.h"
|
||||
#include "time/time.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "engine/engine.h"
|
||||
@@ -56,7 +55,6 @@ errorret_t uiFPSDraw() {
|
||||
fpsText, textColor,
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
int32_t versionWidth, versionHeight;
|
||||
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
|
||||
@@ -68,5 +66,5 @@ errorret_t uiFPSDraw() {
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "uiplayerpos.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "rpg/entity/entity.h"
|
||||
#include "rpg/entity/entitytype.h"
|
||||
#include "rpg/overworld/worldpos.h"
|
||||
@@ -60,5 +59,5 @@ errorret_t uiPlayerPosDraw() {
|
||||
(float_t)SCREEN.scanX, velocityY, velocityText, COLOR_GREEN, &FONT_DEFAULT
|
||||
));
|
||||
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "rpg/item/backpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "assert/assert.h"
|
||||
@@ -95,7 +94,6 @@ errorret_t uiBackpackDraw(void) {
|
||||
uiItemListDraw(&UI_BACKPACK.itemList, contentX, listY, contentWidth)
|
||||
);
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "ui/frame/settings/uisettings.h"
|
||||
#include "ui/frame/backpack/uibackpack.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
@@ -85,7 +84,6 @@ errorret_t uiGameMenuDraw(void) {
|
||||
height - (UI_FRAME_START_Y * 2)
|
||||
));
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "ui/frame/uiconfirm.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "assert/assert.h"
|
||||
@@ -174,7 +173,6 @@ errorret_t uiSettingsDraw(void) {
|
||||
));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ errorret_t uiConfirmDraw(void) {
|
||||
errorChain(
|
||||
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||
);
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
int32_t textW, textH;
|
||||
textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH);
|
||||
@@ -94,7 +93,6 @@ errorret_t uiConfirmDraw(void) {
|
||||
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
||||
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,5 +85,5 @@ errorret_t uiCropDraw(void) {
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiFullboxTransition(
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "time/time.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "uifullbox.h"
|
||||
|
||||
#define UI_LOADING_TEXT "loading"
|
||||
@@ -64,7 +63,7 @@ errorret_t uiLoadingDraw(void) {
|
||||
color.a = (uint8_t)(alpha * 255.0f);
|
||||
|
||||
errorChain(textDraw(x, y, UI_LOADING_TEXT, color, &FONT_DEFAULT));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
static void uiLoadingTransition(
|
||||
|
||||
@@ -170,7 +170,6 @@ errorret_t uiTextboxDraw(
|
||||
}
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
||||
|
||||
@@ -210,7 +209,6 @@ errorret_t uiTextboxDraw(
|
||||
charsLeft -= visible;
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ errorret_t uiTextboxMainDraw(void) {
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ errorret_t uiEmojiDraw(void) {
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, mat));
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,5 +52,5 @@ errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) {
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
+1
-6
@@ -12,7 +12,6 @@
|
||||
#include "display/screen/screen.h"
|
||||
#include "ui/uielement.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "log/log.h"
|
||||
|
||||
ui_t UI;
|
||||
|
||||
@@ -45,14 +44,10 @@ errorret_t uiRender(void) {
|
||||
const uielement_t *element = &UI_ELEMENTS[0];
|
||||
while(!uiElementIsNull(element)) {
|
||||
errorChain(uiElementDraw(element));
|
||||
|
||||
if(SPRITEBATCH.spriteCount > 0) {
|
||||
logDebug("Finished UI element but unflushed sprites remain.\n");
|
||||
}
|
||||
|
||||
element++;
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ errorret_t uiSliderDraw(
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
||||
if(fillWidth > 0.0f) {
|
||||
@@ -187,7 +186,6 @@ errorret_t uiSliderDraw(
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
|
||||
errorChain(spriteBatchFlush());
|
||||
}
|
||||
|
||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||
@@ -218,7 +216,6 @@ errorret_t uiSliderDraw(
|
||||
errorChain(
|
||||
spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial)
|
||||
);
|
||||
errorChain(spriteBatchFlush());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ errorret_t uiTabDraw(
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT));
|
||||
|
||||
|
||||
@@ -93,13 +93,11 @@ errorret_t meshFlushGL(
|
||||
#else
|
||||
glBindBuffer(GL_ARRAY_BUFFER, mesh->vboId);
|
||||
errorChain(errorGLCheck());
|
||||
glBufferData(
|
||||
glBufferSubData(
|
||||
GL_ARRAY_BUFFER,
|
||||
mesh->vertexCount * sizeof(meshvertex_t),
|
||||
mesh->vertices,
|
||||
// vertCount * sizeof(meshvertex_t),
|
||||
// &mesh->vertices[vertOffset],
|
||||
GL_DYNAMIC_DRAW
|
||||
vertOffset * sizeof(meshvertex_t),
|
||||
vertCount * sizeof(meshvertex_t),
|
||||
&mesh->vertices[vertOffset]
|
||||
);
|
||||
errorChain(errorGLCheck());
|
||||
#endif
|
||||
|
||||
@@ -7,5 +7,6 @@ include(dusktest)
|
||||
|
||||
# Tests
|
||||
dusktest(test_maparea.c)
|
||||
dusktest(test_tileshape.c)
|
||||
|
||||
# Subdirs
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "util/memory.h"
|
||||
#include "rpg/overworld/tileshape.h"
|
||||
|
||||
static void test_tileShapeGetRampHeightGroundIsAlwaysFlat(void **state) {
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.0f, 0.0f), 0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 1.0f, 1.0f), 0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_GROUND, 0.5f, 0.5f), 0.0f, 0.0001f
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_tileShapeGetRampHeightCardinalRamps(void **state) {
|
||||
// RAMP_NORTH: rises going north (+y) - height equals localY.
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 0.0f), 0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.5f, 1.0f), 1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTH, 0.0f, 0.5f), 0.5f, 0.0001f
|
||||
);
|
||||
|
||||
// RAMP_SOUTH: rises going south (-y) - height equals 1 - localY.
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 0.0f), 1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTH, 0.5f, 1.0f), 0.0f, 0.0001f
|
||||
);
|
||||
|
||||
// RAMP_EAST: rises going east (+x) - height equals localX.
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 0.0f, 0.5f), 0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_EAST, 1.0f, 0.5f), 1.0f, 0.0001f
|
||||
);
|
||||
|
||||
// RAMP_WEST: rises going west (-x) - height equals 1 - localX.
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 0.0f, 0.5f), 1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_WEST, 1.0f, 0.5f), 0.0f, 0.0001f
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_tileShapeGetRampHeightOuterCornerRamps(void **state) {
|
||||
// RAMP_NORTHEAST: only the NE corner raised - a hip shape, height
|
||||
// equal to min(localX, localY) since the mesh is split along the
|
||||
// SW-NE diagonal into two flat triangles.
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.0f, 0.0f),
|
||||
0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 1.0f, 1.0f),
|
||||
1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.3f, 0.7f),
|
||||
0.3f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST, 0.7f, 0.3f),
|
||||
0.3f, 0.0001f
|
||||
);
|
||||
|
||||
// RAMP_SOUTHWEST: only the SW corner raised - height equal to
|
||||
// min(1 - localX, 1 - localY).
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 0.0f, 0.0f),
|
||||
1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 1.0f),
|
||||
0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_SOUTHWEST, 1.0f, 0.0f),
|
||||
0.0f, 0.0001f
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_tileShapeGetRampHeightInnerCornerRamp(void **state) {
|
||||
// RAMP_NORTHEAST_INNER: only the opposite (SW) corner lowered, rest of
|
||||
// the tile raised - height equal to max(localX, localY).
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.0f, 0.0f),
|
||||
0.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 1.0f),
|
||||
1.0f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.3f, 0.7f),
|
||||
0.7f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 0.7f, 0.3f),
|
||||
0.7f, 0.0001f
|
||||
);
|
||||
assert_float_equal(
|
||||
tileShapeGetRampHeight(TILE_SHAPE_RAMP_NORTHEAST_INNER, 1.0f, 0.0f),
|
||||
1.0f, 0.0001f
|
||||
);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_tileShapeGetRampHeightGroundIsAlwaysFlat),
|
||||
cmocka_unit_test(test_tileShapeGetRampHeightCardinalRamps),
|
||||
cmocka_unit_test(test_tileShapeGetRampHeightOuterCornerRamps),
|
||||
cmocka_unit_test(test_tileShapeGetRampHeightInnerCornerRamp),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -224,16 +224,91 @@ static void test_physicsWorldStepMultiColumnFootprint(void **state) {
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
|
||||
static void test_physicsWorldStepClimbsRampGoingUp(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 0, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_EAST);
|
||||
for(worldunit_t x = 2; x < 6; x++) {
|
||||
testMapSetTile(x, 0, 1, TILE_SHAPE_GROUND);
|
||||
}
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 1.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 400; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
|
||||
|
||||
// Ground height is driven by the body's center (not its corner
|
||||
// position), so a 1-wide body barely touching a taller neighbouring
|
||||
// column isn't yanked onto that column's full height - see
|
||||
// physicsWorldResolveAxisZ. While the center is over the ramp tile,
|
||||
// height should track how far across it smoothly, not snap flat.
|
||||
const float_t centerX = body.position[0] + 0.5f;
|
||||
if(centerX >= 1.0f && centerX < 2.0f) {
|
||||
const float_t expected = centerX - 1.0f;
|
||||
assert_float_equal(body.position[2], expected, 0.005f);
|
||||
}
|
||||
}
|
||||
|
||||
// Fully across, resting on the elevated ground one layer up.
|
||||
assert_float_equal(body.position[0], 5.0f, 0.001f);
|
||||
assert_float_equal(body.position[2], 1.0f, 0.005f);
|
||||
assert_true(body.grounded);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepDescendsRampGoingDown(void **state) {
|
||||
testMapReset();
|
||||
testMapSetTile(0, 0, 1, TILE_SHAPE_GROUND);
|
||||
testMapSetTile(1, 0, 0, TILE_SHAPE_RAMP_WEST);
|
||||
for(worldunit_t x = 2; x < 6; x++) {
|
||||
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
||||
}
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 0.0f, 40.0f);
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 1.0f };
|
||||
const vec3 extents = { 1.0f, 1.0f, 1.0f };
|
||||
physicsBodyInit(&body, position, extents);
|
||||
body.velocity[0] = 1.0f;
|
||||
|
||||
for(uint32_t i = 0; i < 400; i++) {
|
||||
physicsWorldStep(&world, &body, DUSK_TIME_STEP, NULL, 0);
|
||||
|
||||
// Never falls through as a hole while crossing the ramp - descent
|
||||
// may lag slightly behind the ideal slope (gravity closes the gap
|
||||
// each step, see physicsWorldStep's documented limitation) but must
|
||||
// stay close to it, never dropping toward the void below.
|
||||
assert_true(body.position[2] >= -0.2f);
|
||||
}
|
||||
|
||||
// Fully across, resting on the lower ground.
|
||||
assert_float_equal(body.position[0], 5.0f, 0.001f);
|
||||
assert_float_equal(body.position[2], 0.0f, 0.005f);
|
||||
assert_true(body.grounded);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
}
|
||||
|
||||
static void test_physicsWorldStepFlatGroundUnaffectedByRampLogic(
|
||||
void **state
|
||||
) {
|
||||
testMapReset();
|
||||
for(worldunit_t x = 0; x < 6; x++) {
|
||||
testMapSetTile(x, 0, 0, TILE_SHAPE_GROUND);
|
||||
}
|
||||
|
||||
physicsworld_t world;
|
||||
physicsWorldInit(&world, 20.0f, 40.0f);
|
||||
|
||||
physicsbody_t body;
|
||||
const vec3 position = { 0.0f, 0.0f, 0.0f };
|
||||
@@ -246,7 +321,6 @@ static void test_physicsWorldStepRampTreatedAsFlatGround(void **state) {
|
||||
assert_float_equal(body.position[2], 0.0f, 0.0001f);
|
||||
}
|
||||
|
||||
assert_float_equal(body.velocity[0], 5.0f, 0.0001f);
|
||||
assert_true(body.position[0] > 1.5f);
|
||||
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
@@ -346,7 +420,9 @@ int main(void) {
|
||||
cmocka_unit_test(test_physicsWorldStepTerminalVelocityClamp),
|
||||
cmocka_unit_test(test_physicsWorldStepBlockedByCeiling),
|
||||
cmocka_unit_test(test_physicsWorldStepMultiColumnFootprint),
|
||||
cmocka_unit_test(test_physicsWorldStepRampTreatedAsFlatGround),
|
||||
cmocka_unit_test(test_physicsWorldStepClimbsRampGoingUp),
|
||||
cmocka_unit_test(test_physicsWorldStepDescendsRampGoingDown),
|
||||
cmocka_unit_test(test_physicsWorldStepFlatGroundUnaffectedByRampLogic),
|
||||
cmocka_unit_test(test_physicsWorldStepBlockedByOtherBody),
|
||||
cmocka_unit_test(test_physicsWorldStepBlockedByNearestOfMultipleBodies),
|
||||
cmocka_unit_test(test_physicsWorldResolveBodyOverlapIgnoresSelfAndNull),
|
||||
|
||||
Reference in New Issue
Block a user