refactor: reduce kart rotation frames from 16 to 8 and add racer artwork

- Add new racer spritesheets (8 angles × 64×64) for all 9 racers
- Introduce KART_ROT_FRAMES constant as single source of truth for rotation frame count
- Update SuperKartMode7.angleFrame(), SuperKartGame frame math, and procedural fallback
  to derive from KART_ROT_FRAMES instead of magic numbers
- Update data/superkart-artwork.json with asset paths and 8-angle metadata
- Update sprites.md documentation to reflect 8-view-angle contract
This commit is contained in:
Brian Fertig 2026-07-17 21:38:18 -06:00
parent aabf8aa9f3
commit 29d18c9c5f
32 changed files with 48 additions and 36 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

View File

@ -5,13 +5,14 @@
"supplied - set the path once the PNG exists and the game uses it with no",
"code changes. Loaded lazily on game entry via src/data/assetManifest.js.",
"",
"racerSheets - one spritesheet per racer, 18 frames of 64x64 in a single row",
" (1152x64 png). Frames 0..15 are the kart seen from 16 view angles:",
"racerSheets - one spritesheet per racer, 10 frames of 64x64 in a single row",
" (640x64 png). Frames 0..7 are the kart seen from 8 view angles:",
" frame 0 = viewed square from BEHIND, rotating CLOCKWISE (seen from above)",
" in 22.5-degree steps - frame 4 = kart's left side, frame 8 = head-on,",
" frame 12 = right side. Frames 16/17 are optional player-view lean frames",
" (16 = leaning left, 17 = leaning right); if the sheet is only 16 frames",
" wide the game reuses frame 0 with a tilt instead.",
" in 45-degree steps - frame 2 = kart's left side, frame 4 = head-on,",
" frame 6 = right side (1/3/5/7 are the diagonal in-between views). Frames",
" 8/9 are optional player-view lean frames (8 = leaning left, 9 = leaning",
" right); if the sheet is only 8 frames wide the game reuses frame 0 with",
" a tilt instead.",
" Transparent background, kart centered, wheels touching the frame bottom.",
"",
"themeSheets - one 8-frame sheet per location theme, 64x64 frames in a row",
@ -29,15 +30,15 @@
" overdrive, emp, coins), then frame 7 = item box, frame 8 = coin."
],
"racerSheets": {
"mario": { "key": "superkart-kart-mario", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"kona": { "key": "superkart-kart-kona", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"fireball": { "key": "superkart-kart-fireball", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"smasher": { "key": "superkart-kart-smasher", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"croc": { "key": "superkart-kart-croc", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"gerome": { "key": "superkart-kart-gerome", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"blackwind": { "key": "superkart-kart-blackwind", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"dv-8-2303": { "key": "superkart-kart-dv-8-2303", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 },
"zanthor": { "key": "superkart-kart-zanthor", "path": null, "frameWidth": 64, "frameHeight": 64, "angles": 16 }
"mario": { "key": "superkart-kart-mario", "path": "assets/images/superkart/racer-mario.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"kona": { "key": "superkart-kart-kona", "path": "assets/images/superkart/racer-kona.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"fireball": { "key": "superkart-kart-fireball", "path": "assets/images/superkart/racer-fireball.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"smasher": { "key": "superkart-kart-smasher", "path": "assets/images/superkart/racer-smasher.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"croc": { "key": "superkart-kart-croc", "path": "assets/images/superkart/racer-croc.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"gerome": { "key": "superkart-kart-gerome", "path": "assets/images/superkart/racer-gerome.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"blackwind": { "key": "superkart-kart-blackwind", "path": "assets/images/superkart/racer-blackwind.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"dv-8-2303": { "key": "superkart-kart-dv-8-2303", "path": "assets/images/superkart/racer-dv-8-2303.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 },
"zanthor": { "key": "superkart-kart-zanthor", "path": "assets/images/superkart/racer-zanthor.png", "frameWidth": 64, "frameHeight": 64, "angles": 8 }
},
"themeSheets": {
"beach": { "key": "superkart-theme-beach", "path": null, "frameWidth": 64, "frameHeight": 64 },

View File

@ -27,7 +27,7 @@ import {
rasterizeTrack, drawMinimapCanvas, buildKartSheetCanvas, buildItemSheetCanvas,
buildBackdropCanvas, buildDecorSheetCanvas,
} from './SuperKartRaster.js';
import { SuperKartMode7, Mode7Backdrop, MODE7 } from './SuperKartMode7.js';
import { SuperKartMode7, Mode7Backdrop, MODE7, KART_ROT_FRAMES } from './SuperKartMode7.js';
const FONT = 'm6x11, "Julius Sans One"';
const TIMES_KEY = 'superkart-times';
@ -646,7 +646,7 @@ export default class SuperKartGame extends Phaser.Scene {
let frame = null;
if (tag === 'kart') {
wx = ref.x; wy = ref.y;
frame = this.mode7.angleFrame(ref.spinMs > 0 ? ref.heading + ref.spinSpin : ref.heading);
frame = this.mode7.angleFrame(ref.spinMs > 0 ? ref.heading + ref.spinSpin : ref.heading, KART_ROT_FRAMES);
visible = ref.rescueMs <= 0;
} else if (tag === 'box') {
visible = ref.respawnAt <= state.timeMs;
@ -706,17 +706,17 @@ export default class SuperKartGame extends Phaser.Scene {
renderPlayerKart(kart) {
const s = this.playerSprite;
const sheet = this.textures.get(this.kartTexKey(kart.racer.id));
const hasLean = sheet.frameTotal > 17;
const hasLean = sheet.frameTotal > KART_ROT_FRAMES + 1;
const inputs = this.readInputs();
let frame = 0;
let angle = 0;
if (kart.spinMs > 0) {
frame = Math.floor((this.raceState.timeMs / 60)) % 16;
frame = Math.floor((this.raceState.timeMs / 60)) % KART_ROT_FRAMES;
} else if (kart.drifting) {
frame = kart.driftDir > 0 ? 1 : 15;
frame = kart.driftDir > 0 ? 1 : KART_ROT_FRAMES - 1;
angle = kart.driftDir * 6;
} else if (inputs.steer !== 0 && hasLean) {
frame = inputs.steer < 0 ? 16 : 17;
frame = inputs.steer < 0 ? KART_ROT_FRAMES : KART_ROT_FRAMES + 1;
} else if (inputs.steer !== 0) {
angle = inputs.steer * 5;
}

View File

@ -29,6 +29,12 @@ export const MODE7 = {
spriteBase: 34, // world size (units) a 1.0-scale 64px sprite frame represents
};
// Racer spritesheet rotation-frame count — the single source of truth for
// the 8-view-angle kart art contract (data/superkart-artwork.json, the
// procedural fallback in SuperKartRaster.js, and the frame math below all
// derive from this instead of a repeated magic number).
export const KART_ROT_FRAMES = 8;
const FRAG = `
precision mediump float;
@ -153,8 +159,8 @@ export class SuperKartMode7 {
};
}
// View-angle frame for a kart heading (16-frame sheets, frame 0 = rear view).
angleFrame(heading, angles = 16) {
// View-angle frame for a kart heading (8-frame sheets, frame 0 = rear view).
angleFrame(heading, angles = KART_ROT_FRAMES) {
let rel = (heading - this.cam.angle) % TAU;
if (rel < 0) rel += TAU;
return Math.round((rel / TAU) * angles) % angles;

View File

@ -9,6 +9,7 @@
// data/superkart-artwork.json are still path:null.
import { sampleAt, STRIPE_LEN, CURB_WIDTH } from './SuperKartTrack.js';
import { KART_ROT_FRAMES } from './SuperKartMode7.js';
export const TRACK_TEXTURE_SIZE = 2048;
@ -275,18 +276,18 @@ export function drawMinimapCanvas(model, size = 220) {
// ── Procedural fallback sprites ─────────────────────────────────────────────
// 18-frame kart rotation strip (16 view angles + 2 lean frames) drawn as a
// 10-frame kart rotation strip (8 view angles + 2 lean frames) drawn as a
// simple tinted kart. Frame 0 = seen from behind, rotating clockwise — the
// same contract as drop-in racer sheets in data/superkart-artwork.json.
export function buildKartSheetCanvas(color, frame = 64) {
const angles = 16;
const angles = KART_ROT_FRAMES;
const canvas = makeCanvas(frame * (angles + 2), frame);
const ctx = canvas.getContext('2d');
for (let i = 0; i < angles; i += 1) {
drawKart(ctx, i * frame + frame / 2, frame / 2, (i * Math.PI * 2) / angles, color, frame, 0);
}
drawKart(ctx, 16 * frame + frame / 2, frame / 2, 0, color, frame, -0.35);
drawKart(ctx, 17 * frame + frame / 2, frame / 2, 0, color, frame, 0.35);
drawKart(ctx, angles * frame + frame / 2, frame / 2, 0, color, frame, -0.35);
drawKart(ctx, (angles + 1) * frame + frame / 2, frame / 2, 0, color, frame, 0.35);
return canvas;
}

View File

@ -26,17 +26,21 @@ All racer/theme/item art needs a **transparent background**.
| `zanthor` | Zanthor |
- **Frame size:** 64×64
- **Layout:** 18 frames in a single row → **1152×64 px**
- **Layout:** 10 frames in a single row → **640×64 px**
- **File:** set `racerSheets.<id>.path` in `data/superkart-artwork.json`
- **Frames 015** — the kart viewed from 16 angles, rotating **clockwise as
seen from above**, in 22.5° steps:
- **Frames 07** — the kart viewed from 8 angles, rotating **clockwise as
seen from above**, in 45° steps:
- frame 0 = dead-on from **behind**
- frame 4 = kart's left side
- frame 8 = head-on (facing the viewer)
- frame 12 = kart's right side
- **Frames 1617** — optional player-view lean frames (16 = leaning left,
17 = leaning right), used only for the player's fixed on-screen kart while
steering. If the sheet is only 16 frames wide, the game reuses frame 0 with
- frame 1 = diagonal, between behind and left
- frame 2 = kart's left side
- frame 3 = diagonal, between left and head-on
- frame 4 = head-on (facing the viewer)
- frame 5 = diagonal, between head-on and right
- frame 6 = kart's right side
- frame 7 = diagonal, between right and behind
- **Frames 89** — optional player-view lean frames (8 = leaning left,
9 = leaning right), used only for the player's fixed on-screen kart while
steering. If the sheet is only 8 frames wide, the game reuses frame 0 with
a programmatic tilt instead — fine to skip.
- Kart centered horizontally in each frame, wheels touching the bottom edge
(world sprites are anchored a bit above the frame's bottom, not dead