physics and rendering fixes

This commit is contained in:
2026-07-17 21:06:21 -05:00
parent 5f08337726
commit 8fb1c9fb42
25 changed files with 491 additions and 78 deletions
+1
View File
@@ -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();
+42
View File
@@ -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;
}
+19 -2
View File
@@ -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
);
+145 -29
View File
@@ -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,
+52 -5
View File
@@ -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
);
+1 -3
View File
@@ -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();
}
+1 -2
View File
@@ -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();
}
-2
View File
@@ -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();
}
-2
View File
@@ -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();
}
-2
View File
@@ -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();
}
-2
View File
@@ -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();
}
+1 -1
View File
@@ -85,5 +85,5 @@ errorret_t uiCropDraw(void) {
}
};
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
+1 -1
View File
@@ -75,7 +75,7 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
}
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
void uiFullboxTransition(
+1 -2
View File
@@ -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(
-2
View File
@@ -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();
}
-1
View File
@@ -79,7 +79,6 @@ errorret_t uiTextboxMainDraw(void) {
&FONT_DEFAULT
);
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
errorChain(spriteBatchFlush());
errorOk();
}
-1
View File
@@ -110,7 +110,6 @@ errorret_t uiEmojiDraw(void) {
}
};
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, mat));
errorChain(spriteBatchFlush());
errorOk();
}
+1 -1
View File
@@ -52,5 +52,5 @@ errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) {
}
};
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
return spriteBatchFlush();
errorOk();
}
+1 -6
View File
@@ -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();
}
-3
View File
@@ -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());
}
}
-1
View File
@@ -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));
+4 -6
View File
@@ -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
+1
View File
@@ -7,5 +7,6 @@ include(dusktest)
# Tests
dusktest(test_maparea.c)
dusktest(test_tileshape.c)
# Subdirs
+140
View File
@@ -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);
}
+80 -4
View File
@@ -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),