Compare commits
2 Commits
29d18c9c5f
...
65cc0a2c79
| Author | SHA1 | Date |
|---|---|---|
|
|
65cc0a2c79 | |
|
|
a0316bf467 |
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -25,6 +25,11 @@
|
|||
"driftTurnMult": 1.6,
|
||||
"driftSlipRate": 240,
|
||||
"driftGripSpread": 170,
|
||||
"miniTurboMinMs": 500,
|
||||
"miniTurboMaxMs": 1800,
|
||||
"miniTurboBoostMs": 650,
|
||||
"miniTurboBoostMultMin": 1.1,
|
||||
"miniTurboBoostMultMax": 1.4,
|
||||
"slipDecay": 3.2,
|
||||
"hopMs": 260,
|
||||
"hopCooldownMs": 160,
|
||||
|
|
|
|||
|
|
@ -713,8 +713,7 @@ export default class SuperKartGame extends Phaser.Scene {
|
|||
if (kart.spinMs > 0) {
|
||||
frame = Math.floor((this.raceState.timeMs / 60)) % KART_ROT_FRAMES;
|
||||
} else if (kart.drifting) {
|
||||
frame = kart.driftDir > 0 ? 1 : KART_ROT_FRAMES - 1;
|
||||
angle = kart.driftDir * 6;
|
||||
angle = kart.driftDir * 14;
|
||||
} else if (inputs.steer !== 0 && hasLean) {
|
||||
frame = inputs.steer < 0 ? KART_ROT_FRAMES : KART_ROT_FRAMES + 1;
|
||||
} else if (inputs.steer !== 0) {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ function makeKart(index, racer, isPlayer, slot, physics) {
|
|||
airborne: false,
|
||||
drifting: false,
|
||||
driftDir: 0,
|
||||
driftChargeMs: 0, // mini-turbo charge accrued while committed to the slide
|
||||
prevHop: false,
|
||||
prevItem: false,
|
||||
surface: SURFACE.ROAD,
|
||||
|
|
@ -174,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;
|
||||
|
|
@ -201,6 +219,7 @@ function respawnAtCheckpoint(state, kart) {
|
|||
kart.velAngle = kart.heading;
|
||||
kart.speed = 0;
|
||||
kart.drifting = false;
|
||||
kart.driftChargeMs = 0;
|
||||
kart.airborne = false;
|
||||
kart.hopMs = 0;
|
||||
kart.offroadMs = 0;
|
||||
|
|
@ -214,6 +233,7 @@ function startSpin(state, kart, cause) {
|
|||
kart.spinMs = state.physics.spinMs;
|
||||
kart.spinSpin = 0;
|
||||
kart.drifting = false;
|
||||
kart.driftChargeMs = 0;
|
||||
const dropped = Math.min(kart.coins, state.physics.coinsLostOnHit);
|
||||
kart.coins -= dropped;
|
||||
emit(state, 'spin', { kart: kart.index, cause });
|
||||
|
|
@ -359,14 +379,30 @@ function stepKart(state, kart, inputs, dt) {
|
|||
if (inputs.hop && steer !== 0 && kart.speed > effTopSpeed(state, kart) * 0.3) {
|
||||
kart.drifting = true;
|
||||
kart.driftDir = Math.sign(steer);
|
||||
kart.driftChargeMs = 0;
|
||||
emit(state, 'drift-start', { kart: kart.index });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kart.drifting) {
|
||||
kart.driftChargeMs += STEP_MS;
|
||||
const top = effTopSpeed(state, kart);
|
||||
if (!inputs.hop || kart.speed < top * 0.25 || (steer !== 0 && Math.sign(steer) !== kart.driftDir)) {
|
||||
const speedDropped = kart.speed < top * 0.25;
|
||||
const steerReversed = steer !== 0 && Math.sign(steer) !== kart.driftDir;
|
||||
if (!inputs.hop || speedDropped || steerReversed) {
|
||||
kart.drifting = false;
|
||||
// Mini-turbo: only a clean release (hop let go, not a stall/reversal
|
||||
// cutting the slide short) with enough committed charge pays out —
|
||||
// stronger the longer the slide, capped at miniTurboMaxMs.
|
||||
if (!speedDropped && !steerReversed && kart.driftChargeMs >= physics.miniTurboMinMs) {
|
||||
const span = Math.max(1, physics.miniTurboMaxMs - physics.miniTurboMinMs);
|
||||
const t = Math.min(1, (kart.driftChargeMs - physics.miniTurboMinMs) / span);
|
||||
kart.boostMs = physics.miniTurboBoostMs;
|
||||
kart.boostMult = physics.miniTurboBoostMultMin
|
||||
+ (physics.miniTurboBoostMultMax - physics.miniTurboBoostMultMin) * t;
|
||||
emit(state, 'boost', { kart: kart.index, pad: false });
|
||||
}
|
||||
kart.driftChargeMs = 0;
|
||||
emit(state, 'drift-end', { kart: kart.index });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue