New ramp types
Build Dusk / run-tests (push) Failing after 5m33s
Build Dusk / build-linux (push) Successful in 5m3s
Build Dusk / build-psp (push) Successful in 1m17s
Build Dusk / build-knulli (push) Successful in 4m45s
Build Dusk / build-gamecube (push) Successful in 3m49s
Build Dusk / build-gamecube-iso (push) Successful in 3m50s
Build Dusk / build-wii (push) Successful in 3m29s
Build Dusk / build-wii-iso (push) Successful in 4m4s

This commit is contained in:
2026-07-01 16:05:02 -05:00
parent a2d0a12c1a
commit 874c6258ab
10 changed files with 168 additions and 28 deletions
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1086,7 +1086,7 @@
9, 9,
0 0
], ],
"type": 8, "type": 12,
"tile": 0 "tile": 0
}, },
{ {
+13 -1
View File
@@ -27,10 +27,16 @@ const ChunkTerrain = (() => {
const TILE_SHAPE_RAMP_NORTHWEST = 7; const TILE_SHAPE_RAMP_NORTHWEST = 7;
const TILE_SHAPE_RAMP_SOUTHEAST = 8; const TILE_SHAPE_RAMP_SOUTHEAST = 8;
const TILE_SHAPE_RAMP_SOUTHWEST = 9; const TILE_SHAPE_RAMP_SOUTHWEST = 9;
const TILE_SHAPE_RAMP_NORTHEAST_INNER = 10;
const TILE_SHAPE_RAMP_NORTHWEST_INNER = 11;
const TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12;
const TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13;
// Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z. // Per-shape corner heights as [sw, se, ne, nw] offsets from tile base Z.
// NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1. // NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1.
// Diagonal ramps: opposite corner at 0/1, adjacent corners at 0.5. // Diagonal outer-corner ramps: only the named corner raised, rest at 0.
// Diagonal inner-corner ramps: only the corner opposite the named one is
// lowered to 0, the rest (including the named corner) stay raised at 1.
const RAMP_CORNERS = { const RAMP_CORNERS = {
[TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0], [TILE_SHAPE_GROUND]: [0.0, 0.0, 0.0, 0.0],
[TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0], [TILE_SHAPE_RAMP_NORTH]: [0.0, 0.0, 1.0, 1.0],
@@ -41,6 +47,10 @@ const ChunkTerrain = (() => {
[TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0], [TILE_SHAPE_RAMP_NORTHWEST]: [0.0, 0.0, 0.0, 1.0],
[TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0], [TILE_SHAPE_RAMP_SOUTHEAST]: [0.0, 1.0, 0.0, 0.0],
[TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0], [TILE_SHAPE_RAMP_SOUTHWEST]: [1.0, 0.0, 0.0, 0.0],
[TILE_SHAPE_RAMP_NORTHEAST_INNER]: [0.0, 1.0, 1.0, 1.0],
[TILE_SHAPE_RAMP_NORTHWEST_INNER]: [1.0, 0.0, 1.0, 1.0],
[TILE_SHAPE_RAMP_SOUTHEAST_INNER]: [1.0, 1.0, 1.0, 0.0],
[TILE_SHAPE_RAMP_SOUTHWEST_INNER]: [1.0, 1.0, 0.0, 1.0],
}; };
function tileIndex(x, y, z) { function tileIndex(x, y, z) {
@@ -92,6 +102,8 @@ const ChunkTerrain = (() => {
TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST, TILE_SHAPE_RAMP_SOUTH, TILE_SHAPE_RAMP_WEST,
TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST, TILE_SHAPE_RAMP_NORTHEAST, TILE_SHAPE_RAMP_NORTHWEST,
TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST, TILE_SHAPE_RAMP_SOUTHEAST, TILE_SHAPE_RAMP_SOUTHWEST,
TILE_SHAPE_RAMP_NORTHEAST_INNER, TILE_SHAPE_RAMP_NORTHWEST_INNER,
TILE_SHAPE_RAMP_SOUTHEAST_INNER, TILE_SHAPE_RAMP_SOUTHWEST_INNER,
RAMP_CORNERS, tileIndex, buildTerrainVerts, RAMP_CORNERS, tileIndex, buildTerrainVerts,
}); });
})(); })();
+1 -3
View File
@@ -52,10 +52,8 @@
<span id="zLevelLabel">0</span> <span id="zLevelLabel">0</span>
</div> </div>
<div class="d-flex gap-3"> <div class="d-flex gap-3">
<div class="d-flex flex-column gap-2"> <div id="toolPalette" class="tile-palette"></div>
<button type="button" id="fillToggle" class="btn btn-sm btn-outline-secondary">Fill: Off</button>
<div id="tilePalette" class="tile-palette"></div> <div id="tilePalette" class="tile-palette"></div>
</div>
<canvas id="gridCanvas" class="border tile-grid" width="512" height="512"></canvas> <canvas id="gridCanvas" class="border tile-grid" width="512" height="512"></canvas>
</div> </div>
+96 -10
View File
@@ -17,6 +17,10 @@
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" }, { type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST, name: "Ramp NW", color: "#ff5722" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" }, { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST, name: "Ramp SE", color: "#9c27b0" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" }, { type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST, name: "Ramp SW", color: "#e91e63" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER, name: "Ramp NE Inner", color: "#795548" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER, name: "Ramp NW Inner", color: "#607d8b" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER, name: "Ramp SE Inner", color: "#8bc34a" },
{ type: ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER, name: "Ramp SW Inner", color: "#ffc107" },
]; ];
const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color])); const SHAPE_COLORS = Object.fromEntries(SHAPES.map((s) => [s.type, s.color]));
@@ -31,6 +35,10 @@
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 }, [ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST]: { dx: -1, dy: -1 },
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 }, [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST]: { dx: 1, dy: 1 },
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 }, [ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST]: { dx: -1, dy: 1 },
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHEAST_INNER]: { dx: 1, dy: -1 },
[ChunkTerrain.TILE_SHAPE_RAMP_NORTHWEST_INNER]: { dx: -1, dy: -1 },
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHEAST_INNER]: { dx: 1, dy: 1 },
[ChunkTerrain.TILE_SHAPE_RAMP_SOUTHWEST_INNER]: { dx: -1, dy: 1 },
}; };
// Draws an arrow centered at (cx, cy) pointing toward the ramp's high // Draws an arrow centered at (cx, cy) pointing toward the ramp's high
@@ -65,6 +73,66 @@
ctx.fill(); ctx.fill();
} }
// Draws a small pencil icon centered in a `size`x`size` canvas.
function drawPencilIcon(ctx, size) {
ctx.save();
ctx.translate(size / 2, size / 2);
ctx.rotate(-Math.PI / 4);
const shaftW = size * 0.18;
const shaftH = size * 0.62;
ctx.fillStyle = "#e0e0e0";
ctx.fillRect(-shaftW / 2, shaftH * 0.28, shaftW, shaftH * 0.22);
ctx.fillStyle = "#f0c419";
ctx.fillRect(-shaftW / 2, -shaftH * 0.5, shaftW, shaftH * 0.78);
ctx.fillStyle = "#8d6e63";
ctx.beginPath();
ctx.moveTo(-shaftW / 2, -shaftH * 0.5);
ctx.lineTo(shaftW / 2, -shaftH * 0.5);
ctx.lineTo(0, -shaftH * 0.5 - shaftW);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Draws a small paint bucket icon centered in a `size`x`size` canvas.
function drawBucketIcon(ctx, size) {
ctx.save();
ctx.translate(size / 2, size / 2);
const w = size * 0.5, h = size * 0.34;
ctx.strokeStyle = "#e0e0e0";
ctx.lineWidth = Math.max(1, size * 0.06);
ctx.beginPath();
ctx.arc(0, -h * 0.4, w * 0.4, Math.PI, 0);
ctx.stroke();
ctx.fillStyle = "#03a9f4";
ctx.beginPath();
ctx.moveTo(-w / 2, -h * 0.2);
ctx.lineTo(w / 2, -h * 0.2);
ctx.lineTo(w * 0.32, h * 0.6);
ctx.lineTo(-w * 0.32, h * 0.6);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(-w * 0.55, h * 0.75, size * 0.06, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
const TOOLS = [
{ id: "pencil", name: "Pencil", draw: drawPencilIcon },
{ id: "bucket", name: "Paint Bucket", draw: drawBucketIcon },
];
let coord = { x: 0, y: 0, z: 0 }; let coord = { x: 0, y: 0, z: 0 };
let tiles = new Int32Array(ChunkTerrain.TILE_COUNT); let tiles = new Int32Array(ChunkTerrain.TILE_COUNT);
let meshes = []; let meshes = [];
@@ -74,7 +142,7 @@
let zoomWorldH = 20; let zoomWorldH = 20;
let dirty = false; let dirty = false;
let hoverTile = null; let hoverTile = null;
let fillMode = false; let activeTool = "pencil";
let mapRenderer = null; let mapRenderer = null;
let terrainMesh = null; let terrainMesh = null;
@@ -372,6 +440,31 @@
} }
} }
function buildToolPalette() {
const el = document.getElementById("toolPalette");
el.innerHTML = "";
for(const tool of TOOLS) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "tile-swatch" + (tool.id === activeTool ? " active" : "");
btn.style.background = "#2a2d33";
btn.title = tool.name;
btn.addEventListener("click", () => {
activeTool = tool.id;
buildToolPalette();
});
const icon = document.createElement("canvas");
icon.width = 32;
icon.height = 32;
icon.className = "tile-swatch-icon";
tool.draw(icon.getContext("2d"), 32);
btn.appendChild(icon);
el.appendChild(btn);
}
}
function canvasToTile(e, canvas) { function canvasToTile(e, canvas) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH; const cell = canvas.width / ChunkTerrain.CHUNK_WIDTH;
@@ -655,7 +748,7 @@
let draggingMeshIndex = -1; let draggingMeshIndex = -1;
gridCanvas.addEventListener("mousedown", (e) => { gridCanvas.addEventListener("mousedown", (e) => {
if(fillMode) { if(activeTool === "bucket") {
fillAt(e); fillAt(e);
return; return;
} }
@@ -719,14 +812,6 @@
document.getElementById("btnSave").addEventListener("click", runSave); document.getElementById("btnSave").addEventListener("click", runSave);
document.getElementById("fillToggle").addEventListener("click", () => {
fillMode = !fillMode;
const btn = document.getElementById("fillToggle");
btn.textContent = fillMode ? "Fill: On" : "Fill: Off";
btn.classList.toggle("btn-secondary", fillMode);
btn.classList.toggle("btn-outline-secondary", !fillMode);
});
document.getElementById("btnAddMesh").addEventListener("click", () => { document.getElementById("btnAddMesh").addEventListener("click", () => {
const base = document.getElementById("meshPicker").value; const base = document.getElementById("meshPicker").value;
if(!base) return; if(!base) return;
@@ -766,6 +851,7 @@
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
buildPalette(); buildPalette();
buildToolPalette();
mapRenderer = MapRenderer.create(document.getElementById("previewCanvas")); mapRenderer = MapRenderer.create(document.getElementById("previewCanvas"));
bindEvents(); bindEvents();
await Promise.all([buildChunkBrowseList(), buildMeshPicker()]); await Promise.all([buildChunkBrowseList(), buildMeshPicker()]);
+36 -10
View File
@@ -97,22 +97,35 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
( (
// Can only walk UP the direction the ramp faces. // Can only walk UP the direction the ramp faces.
(direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent || (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent ||
// If diagonal ramp, can go up one of two ways only. // If diagonal ramp, can go up one of two ways only. Inner ramps
// share the same allowed directions as their outer counterparts.
( (
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST && (
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST && (
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST && (
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST && (
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST ||
tileCurrent == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) )
) )
@@ -141,22 +154,35 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
( (
// This handles regular cardinal ramps // This handles regular cardinal ramps
(entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow || (entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow ||
// This handles diagonal ramps // This handles diagonal ramps. Inner ramps share the same
// allowed directions as their outer counterparts.
( (
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST && (
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHEAST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST && (
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST ||
tileBelow == TILE_SHAPE_RAMP_SOUTHWEST_INNER
) &&
(direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST)
) || ) ||
( (
tileBelow == TILE_SHAPE_RAMP_NORTHEAST && (
tileBelow == TILE_SHAPE_RAMP_NORTHEAST ||
tileBelow == TILE_SHAPE_RAMP_NORTHEAST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST)
) || ) ||
( (
tileBelow == TILE_SHAPE_RAMP_NORTHWEST && (
tileBelow == TILE_SHAPE_RAMP_NORTHWEST ||
tileBelow == TILE_SHAPE_RAMP_NORTHWEST_INNER
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
) )
) )
+4
View File
@@ -27,6 +27,10 @@ bool_t tileIsRamp(const tile_t tile) {
case TILE_SHAPE_RAMP_NORTHWEST: case TILE_SHAPE_RAMP_NORTHWEST:
case TILE_SHAPE_RAMP_SOUTHEAST: case TILE_SHAPE_RAMP_SOUTHEAST:
case TILE_SHAPE_RAMP_SOUTHWEST: case TILE_SHAPE_RAMP_SOUTHWEST:
case TILE_SHAPE_RAMP_NORTHEAST_INNER:
case TILE_SHAPE_RAMP_NORTHWEST_INNER:
case TILE_SHAPE_RAMP_SOUTHEAST_INNER:
case TILE_SHAPE_RAMP_SOUTHWEST_INNER:
return true; return true;
default: default:
+4
View File
@@ -21,6 +21,10 @@ typedef enum {
TILE_SHAPE_RAMP_NORTHWEST = 7, TILE_SHAPE_RAMP_NORTHWEST = 7,
TILE_SHAPE_RAMP_SOUTHEAST = 8, TILE_SHAPE_RAMP_SOUTHEAST = 8,
TILE_SHAPE_RAMP_SOUTHWEST = 9, TILE_SHAPE_RAMP_SOUTHWEST = 9,
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10,
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11,
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12,
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13,
TILE_SHAPE_COUNT TILE_SHAPE_COUNT
} tile_t; } tile_t;
+11 -1
View File
@@ -74,6 +74,10 @@ TILE_SHAPE_RAMP_NORTHEAST = 6
TILE_SHAPE_RAMP_NORTHWEST = 7 TILE_SHAPE_RAMP_NORTHWEST = 7
TILE_SHAPE_RAMP_SOUTHEAST = 8 TILE_SHAPE_RAMP_SOUTHEAST = 8
TILE_SHAPE_RAMP_SOUTHWEST = 9 TILE_SHAPE_RAMP_SOUTHWEST = 9
TILE_SHAPE_RAMP_NORTHEAST_INNER = 10
TILE_SHAPE_RAMP_NORTHWEST_INNER = 11
TILE_SHAPE_RAMP_SOUTHEAST_INNER = 12
TILE_SHAPE_RAMP_SOUTHWEST_INNER = 13
FILE_MAGIC = b'DCF' FILE_MAGIC = b'DCF'
DMF_MAGIC = b'DMF\x00' DMF_MAGIC = b'DMF\x00'
@@ -186,7 +190,9 @@ def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
# Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z. # Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z.
# NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1. # NORTH=+Y, EAST=+X. Cardinal ramps: low face at 0, high face at 1.
# Diagonal ramps: opposite corner at 0/1, adjacent corners at 0.5. # Diagonal outer-corner ramps: only the named corner raised, rest at 0.
# Diagonal inner-corner ramps: only the corner opposite the named one is
# lowered to 0, the rest (including the named corner) stay raised at 1.
_RAMP_CORNERS = { _RAMP_CORNERS = {
TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0), TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0),
TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high
@@ -197,6 +203,10 @@ _RAMP_CORNERS = {
TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised
TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised
TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised
TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low
TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low
TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low
TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low
} }