mirror of
https://github.com/tiennm99/try-claudekit.git
synced 2026-04-17 15:21:21 +00:00
fix: physics tunneling and update fruit stats to community standard
Fix fruits falling through the floor by replacing single-step physics with fixed-timestep sub-stepping (16.67ms steps, max 5 per frame). Add escaped-body cleanup as safety net. Tune physics constants for better feel (higher density, more bounce, less float). Update fruit radii to match the community-standard ratios (TomboFry/ moonfloof clones) scaled to our 400px container. Update colors to match real fruit appearances. Fix "Grapes" → "Grape" per wiki.
This commit is contained in:
@@ -7,20 +7,22 @@
|
|||||||
|
|
||||||
## Fruit Chain
|
## Fruit Chain
|
||||||
|
|
||||||
Cherry → Strawberry → Grapes → Dekopon → Persimmon → Apple → Pear → Peach → Pineapple → Melon → Watermelon
|
Cherry → Strawberry → Grape → Dekopon → Persimmon → Apple → Pear → Peach → Pineapple → Melon → Watermelon
|
||||||
|
|
||||||
| Tier | Fruit | Radius | Color | Merge Points |
|
| Tier | Fruit | Radius | Color | Merge Points |
|
||||||
|------|-------|--------|-------|-------------|
|
|------|-------|--------|-------|-------------|
|
||||||
| 0 | Cherry | 12px | Red | 1 |
|
| 0 | Cherry | 12px | Red | 1 |
|
||||||
| 1 | Strawberry | 16px | Light Red | 3 |
|
| 1 | Strawberry | 17px | Pink-Red | 3 |
|
||||||
| 2 | Grapes | 20px | Purple | 6 |
|
| 2 | Grape | 21px | Purple | 6 |
|
||||||
| 3 | Dekopon | 26px | Orange | 10 |
|
| 3 | Dekopon | 29px | Orange | 10 |
|
||||||
| 4 | Persimmon | 32px | Dark Orange | 15 |
|
| 4 | Persimmon | 33px | Orange-Red | 15 |
|
||||||
| 5 | Apple | 36px | Red | 21 |
|
| 5 | Apple | 37px | Red | 21 |
|
||||||
| 6 | Pear | 42px | Green-Yellow | 28 |
|
| 6 | Pear | 44px | Yellow-Green | 28 |
|
||||||
| 7 | Peach | 48px | Peach | 36 |
|
| 7 | Peach | 50px | Peach | 36 |
|
||||||
| 8 | Pineapple | 57px | Yellow | 45 |
|
| 8 | Pineapple | 67px | Yellow | 45 |
|
||||||
| 9 | Melon | 64px | Green | 55 |
|
| 9 | Melon | 83px | Green | 55 |
|
||||||
| 10 | Watermelon | 77px | Dark Green | 66 |
|
| 10 | Watermelon | 100px | Dark Green | 66 |
|
||||||
|
|
||||||
|
Radii are scaled from the [community-standard values](https://github.com/TomboFry/suika-game) (24–192px) to fit the 400px container. Points match the Nintendo Switch scoring.
|
||||||
|
|
||||||
Only the 5 smallest fruits (Cherry through Persimmon) appear as drop candidates. When two Watermelons merge, they disappear and award 66 bonus points.
|
Only the 5 smallest fruits (Cherry through Persimmon) appear as drop candidates. When two Watermelons merge, they disappear and award 66 bonus points.
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ export const DANGER_LINE_Y = CONTAINER_Y + 50;
|
|||||||
// Physics
|
// Physics
|
||||||
export const GRAVITY = { x: 0, y: 1.5 };
|
export const GRAVITY = { x: 0, y: 1.5 };
|
||||||
export const FRUIT_BODY_OPTIONS = {
|
export const FRUIT_BODY_OPTIONS = {
|
||||||
restitution: 0.2,
|
restitution: 0.3,
|
||||||
friction: 0.5,
|
friction: 0.3,
|
||||||
frictionAir: 0.01,
|
frictionAir: 0.02,
|
||||||
density: 0.001,
|
frictionStatic: 0.5,
|
||||||
|
density: 0.003,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fixed physics step: run sub-steps at this interval to prevent tunneling
|
||||||
|
export const PHYSICS_STEP_MS = 1000 / 60; // ~16.67ms
|
||||||
|
export const MAX_SUB_STEPS = 5; // cap sub-steps to avoid spiral of death
|
||||||
|
|
||||||
// Timing
|
// Timing
|
||||||
export const DROP_COOLDOWN_MS = 500;
|
export const DROP_COOLDOWN_MS = 500;
|
||||||
export const NEW_FRUIT_GRACE_MS = 1000;
|
export const NEW_FRUIT_GRACE_MS = 1000;
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
// Community-standard fruit stats (TomboFry/moonfloof clones, Suika Game Wiki).
|
||||||
|
// Radii scaled from the standard (24..192) by 0.52 to fit our 400px container.
|
||||||
|
// Points match the Nintendo Switch scoring: 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66.
|
||||||
export const FRUITS = [
|
export const FRUITS = [
|
||||||
{ tier: 0, name: 'Cherry', radius: 12, color: '#E74C3C', points: 1 },
|
{ tier: 0, name: 'Cherry', radius: 12, color: '#E8373B', points: 1 },
|
||||||
{ tier: 1, name: 'Strawberry', radius: 16, color: '#FF6B6B', points: 3 },
|
{ tier: 1, name: 'Strawberry', radius: 17, color: '#FF6B6B', points: 3 },
|
||||||
{ tier: 2, name: 'Grapes', radius: 20, color: '#9B59B6', points: 6 },
|
{ tier: 2, name: 'Grape', radius: 21, color: '#7B52AB', points: 6 },
|
||||||
{ tier: 3, name: 'Dekopon', radius: 26, color: '#F39C12', points: 10 },
|
{ tier: 3, name: 'Dekopon', radius: 29, color: '#FF8C00', points: 10 },
|
||||||
{ tier: 4, name: 'Persimmon', radius: 32, color: '#E67E22', points: 15 },
|
{ tier: 4, name: 'Persimmon', radius: 33, color: '#E2611C', points: 15 },
|
||||||
{ tier: 5, name: 'Apple', radius: 36, color: '#E74C3C', points: 21 },
|
{ tier: 5, name: 'Apple', radius: 37, color: '#C0392B', points: 21 },
|
||||||
{ tier: 6, name: 'Pear', radius: 42, color: '#A8D648', points: 28 },
|
{ tier: 6, name: 'Pear', radius: 44, color: '#C8D44E', points: 28 },
|
||||||
{ tier: 7, name: 'Peach', radius: 48, color: '#FDCB6E', points: 36 },
|
{ tier: 7, name: 'Peach', radius: 50, color: '#FFBF8C', points: 36 },
|
||||||
{ tier: 8, name: 'Pineapple', radius: 57, color: '#F1C40F', points: 45 },
|
{ tier: 8, name: 'Pineapple', radius: 67, color: '#F5C518', points: 45 },
|
||||||
{ tier: 9, name: 'Melon', radius: 64, color: '#2ECC71', points: 55 },
|
{ tier: 9, name: 'Melon', radius: 83, color: '#90C040', points: 55 },
|
||||||
{ tier: 10, name: 'Watermelon', radius: 77, color: '#27AE60', points: 66 },
|
{ tier: 10, name: 'Watermelon', radius: 100, color: '#3A7D44', points: 66 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Only the 5 smallest fruits can be randomly selected for dropping
|
// Only the 5 smallest fruits can be randomly selected for dropping
|
||||||
|
|||||||
40
src/game.js
40
src/game.js
@@ -3,6 +3,7 @@ import {
|
|||||||
createWalls,
|
createWalls,
|
||||||
createFruitBody,
|
createFruitBody,
|
||||||
addToWorld,
|
addToWorld,
|
||||||
|
removeFromWorld,
|
||||||
getAllBodies,
|
getAllBodies,
|
||||||
stepEngine,
|
stepEngine,
|
||||||
} from './physics.js';
|
} from './physics.js';
|
||||||
@@ -12,10 +13,15 @@ import { clampX } from './input.js';
|
|||||||
import { FRUITS, getRandomDroppableTier } from './fruits.js';
|
import { FRUITS, getRandomDroppableTier } from './fruits.js';
|
||||||
import {
|
import {
|
||||||
CANVAS_WIDTH,
|
CANVAS_WIDTH,
|
||||||
|
CONTAINER_X,
|
||||||
|
CONTAINER_WIDTH,
|
||||||
CONTAINER_Y,
|
CONTAINER_Y,
|
||||||
|
CONTAINER_HEIGHT,
|
||||||
DANGER_LINE_Y,
|
DANGER_LINE_Y,
|
||||||
DROP_COOLDOWN_MS,
|
DROP_COOLDOWN_MS,
|
||||||
NEW_FRUIT_GRACE_MS,
|
NEW_FRUIT_GRACE_MS,
|
||||||
|
PHYSICS_STEP_MS,
|
||||||
|
MAX_SUB_STEPS,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
@@ -25,6 +31,7 @@ export class Game {
|
|||||||
this.mergeHandler = null;
|
this.mergeHandler = null;
|
||||||
this.state = null;
|
this.state = null;
|
||||||
this.lastTime = 0;
|
this.lastTime = 0;
|
||||||
|
this.accumulator = 0;
|
||||||
this.animFrameId = null;
|
this.animFrameId = null;
|
||||||
this.cooldownTimer = null;
|
this.cooldownTimer = null;
|
||||||
this.init();
|
this.init();
|
||||||
@@ -60,8 +67,21 @@ export class Game {
|
|||||||
this.lastTime = time;
|
this.lastTime = time;
|
||||||
|
|
||||||
if (!this.state.isGameOver) {
|
if (!this.state.isGameOver) {
|
||||||
stepEngine(this.engine, delta);
|
// Fixed-step sub-stepping: accumulate time and run physics in
|
||||||
this.mergeHandler.flushMerges();
|
// consistent small steps to prevent tunneling through walls/floor.
|
||||||
|
this.accumulator += delta;
|
||||||
|
const maxAccumulated = PHYSICS_STEP_MS * MAX_SUB_STEPS;
|
||||||
|
if (this.accumulator > maxAccumulated) {
|
||||||
|
this.accumulator = maxAccumulated;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this.accumulator >= PHYSICS_STEP_MS) {
|
||||||
|
stepEngine(this.engine, PHYSICS_STEP_MS);
|
||||||
|
this.mergeHandler.flushMerges();
|
||||||
|
this.accumulator -= PHYSICS_STEP_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeEscapedBodies();
|
||||||
this.checkGameOver();
|
this.checkGameOver();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +113,22 @@ export class Game {
|
|||||||
}, DROP_COOLDOWN_MS);
|
}, DROP_COOLDOWN_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeEscapedBodies() {
|
||||||
|
const bodies = getAllBodies(this.engine);
|
||||||
|
const margin = 100;
|
||||||
|
const minX = CONTAINER_X - margin;
|
||||||
|
const maxX = CONTAINER_X + CONTAINER_WIDTH + margin;
|
||||||
|
const maxY = CONTAINER_Y + CONTAINER_HEIGHT + margin;
|
||||||
|
|
||||||
|
for (const body of bodies) {
|
||||||
|
if (body.fruitTier === undefined || body.isStatic) continue;
|
||||||
|
const { x, y } = body.position;
|
||||||
|
if (x < minX || x > maxX || y > maxY) {
|
||||||
|
removeFromWorld(this.engine, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkGameOver() {
|
checkGameOver() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const bodies = getAllBodies(this.engine);
|
const bodies = getAllBodies(this.engine);
|
||||||
|
|||||||
Reference in New Issue
Block a user