feat(superkart): add beach theme and AI rubber-banding

- Add beach theme artwork and register paths in artwork JSON
- Implement AI rubber-banding that scales AI top speed based on gap to player
- Configure engine classes with varying rubber-band strength (Cruiser most forgiving, Turbo least)
- Balance racer stats: boost Kona top speed, reduce heavy racer accel/handling
- Update verification script to validate new rubber-banding configuration
This commit is contained in:
Brian Fertig 2026-07-17 23:45:46 -06:00
parent a0316bf467
commit 65cc0a2c79
8 changed files with 29 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

View File

@ -41,7 +41,7 @@
"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 },
"beach": { "key": "superkart-theme-beach", "path": "assets/images/superkart/theme-beach.png", "frameWidth": 64, "frameHeight": 64 },
"volcano": { "key": "superkart-theme-volcano", "path": null, "frameWidth": 64, "frameHeight": 64 },
"scrapyard": { "key": "superkart-theme-scrapyard", "path": null, "frameWidth": 64, "frameHeight": 64 },
"swamp": { "key": "superkart-theme-swamp", "path": null, "frameWidth": 64, "frameHeight": 64 },
@ -49,7 +49,7 @@
"nightcity": { "key": "superkart-theme-nightcity", "path": null, "frameWidth": 64, "frameHeight": 64 }
},
"backdrops": {
"beach": { "key": "superkart-backdrop-beach", "path": null },
"beach": { "key": "superkart-backdrop-beach", "path": "assets/images/superkart/backdrop-beach.png" },
"volcano": { "key": "superkart-backdrop-volcano", "path": null },
"scrapyard": { "key": "superkart-backdrop-scrapyard", "path": null },
"swamp": { "key": "superkart-backdrop-swamp", "path": null },

View File

@ -35,7 +35,7 @@
"id": "kona",
"archetype": "Featherweight",
"stats": {
"topSpeed": 0.35,
"topSpeed": 0.45,
"accel": 0.7,
"handling": 0.95,
"weight": 0.15,
@ -96,8 +96,8 @@
"archetype": "Speed Demon",
"stats": {
"topSpeed": 1.0,
"accel": 0.45,
"handling": 0.4,
"accel": 0.10,
"handling": 0.2,
"weight": 0.55,
"offroad": 0.3,
"drift": 0.45

View File

@ -6,9 +6,9 @@
"After editing, run `node tools/verifySuperKart.js` to re-check invariants."
],
"engineClasses": [
{ "id": "cruiser", "label": "CRUISER CLASS", "desc": "Relaxed speeds, forgiving rivals.", "speedMult": 0.8, "aiSkillMult": 0.75 },
{ "id": "racer", "label": "RACER CLASS", "desc": "The intended Super Kart experience.", "speedMult": 1.0, "aiSkillMult": 1.0 },
{ "id": "turbo", "label": "TURBO CLASS", "desc": "Everything faster, rivals ruthless.", "speedMult": 1.15, "aiSkillMult": 1.25 }
{ "id": "cruiser", "label": "CRUISER CLASS", "desc": "Relaxed speeds, forgiving rivals.", "speedMult": 0.8, "aiSkillMult": 0.75, "rubberBandMult": 1.75 },
{ "id": "racer", "label": "RACER CLASS", "desc": "The intended Super Kart experience.", "speedMult": 1.0, "aiSkillMult": 1.0, "rubberBandMult": 1.0 },
{ "id": "turbo", "label": "TURBO CLASS", "desc": "Everything faster, rivals ruthless.", "speedMult": 1.15, "aiSkillMult": 1.25, "rubberBandMult": 0.25 }
],
"physics": {
"kartRadius": 14,

View File

@ -175,9 +175,26 @@ function emit(state, type, data) {
function classSpeedMult(state) { return state.engineClass?.speedMult ?? 1; }
// Gap-to-player rubber-banding: AI karts trailing the player get a top-speed
// boost, karts ahead get held back, scaled to 0 at the player's own
// progress and saturating at +/-rubberBandRange world units of gap. Engine
// class scales the whole effect (rubberBandMult) — Cruiser leans on this
// hard for forgiving rivals, Turbo barely uses it to stay ruthless.
function rubberBandMultOf(state, kart) {
if (kart.isPlayer) return 1;
const player = state.karts[state.playerIndex];
if (!player?.isPlayer) return 1;
const { physics } = state;
const gain = physics.rubberBandGain * (state.engineClass?.rubberBandMult ?? 1);
if (gain <= 0) return 1;
const gap = player.progress - kart.progress; // + = kart trails the player
const t = Math.max(-1, Math.min(1, gap / physics.rubberBandRange));
return 1 + gain * t;
}
function effTopSpeed(state, kart) {
const { physics } = state;
let mult = classSpeedMult(state);
let mult = classSpeedMult(state) * rubberBandMultOf(state, kart);
mult *= 1 + kart.coins * physics.coinTopSpeedBonus;
if (kart.starMs > 0) mult *= itemParams(state, 'overdrive').mult ?? 1.2;
if (kart.empMs > 0) mult *= itemParams(state, 'emp').mult ?? 0.6;

View File

@ -57,6 +57,8 @@ for (const key of statKeys) {
check('3 engine classes', rules.engineClasses.length === 3);
check('classes ordered by speed', rules.engineClasses.every((c, i, a) => i === 0 || c.speedMult > a[i - 1].speedMult));
check('classes ordered by inverse rubber-band strength (cruiser most forgiving)',
rules.engineClasses.every((c, i, a) => i === 0 || c.rubberBandMult < a[i - 1].rubberBandMult));
check('points table has 9 entries', rules.pointsTable.length === 9);
check('points table descending', rules.pointsTable.every((p, i, a) => i === 0 || p <= a[i - 1]));
for (const item of rules.items) {
@ -68,7 +70,7 @@ for (let col = 0; col < 9; col += 1) {
check(`weight column ${col + 1} sums > 0`, total > 0);
}
const physKeys = ['baseTopSpeed', 'baseAccel', 'turnRateBase', 'hopMs', 'offroadMultMin',
'boostPadMult', 'spinMs', 'gridSpacing', 'rouletteMs', 'rubberBandGain'];
'boostPadMult', 'spinMs', 'gridSpacing', 'rouletteMs', 'rubberBandGain', 'rubberBandRange'];
for (const k of physKeys) check(`physics.${k} present`, typeof rules.physics[k] === 'number');
check('3 cups', cups.cups.length === 3);