diff --git a/assets/images/superkart/backdrop-beach.png b/assets/images/superkart/backdrop-beach.png new file mode 100644 index 0000000..170b74c Binary files /dev/null and b/assets/images/superkart/backdrop-beach.png differ diff --git a/assets/images/superkart/theme-beach.png b/assets/images/superkart/theme-beach.png new file mode 100644 index 0000000..f8894ae Binary files /dev/null and b/assets/images/superkart/theme-beach.png differ diff --git a/assets/images/superkart/theme-beach.psd b/assets/images/superkart/theme-beach.psd new file mode 100644 index 0000000..ef7b5f6 Binary files /dev/null and b/assets/images/superkart/theme-beach.psd differ diff --git a/data/superkart-artwork.json b/data/superkart-artwork.json index d6cf33f..a84ee62 100644 --- a/data/superkart-artwork.json +++ b/data/superkart-artwork.json @@ -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 }, diff --git a/data/superkart-racers.json b/data/superkart-racers.json index f22d555..c1716b8 100644 --- a/data/superkart-racers.json +++ b/data/superkart-racers.json @@ -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 diff --git a/data/superkart-rules.json b/data/superkart-rules.json index 2fecb8f..cb3de30 100644 --- a/data/superkart-rules.json +++ b/data/superkart-rules.json @@ -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, diff --git a/src/games/superkart/SuperKartLogic.js b/src/games/superkart/SuperKartLogic.js index eb3860c..f8e3a68 100644 --- a/src/games/superkart/SuperKartLogic.js +++ b/src/games/superkart/SuperKartLogic.js @@ -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; diff --git a/tools/verifySuperKart.js b/tools/verifySuperKart.js index 233d987..9c8ae0c 100644 --- a/tools/verifySuperKart.js +++ b/tools/verifySuperKart.js @@ -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);