|
|
@@ -20,6 +20,7 @@ import {
|
|
|
CANDLE_THROW_SPEED,
|
|
|
CAKE_CHECK_INTERVAL_S,
|
|
|
CHERRY_DROP_SPEED,
|
|
|
+ CHERRY_HOLDER_JUMP_MULT,
|
|
|
CHERRY_POINTS_PER_S,
|
|
|
CHERRY_RADIUS,
|
|
|
CHERRY_REGRAB_DELAY,
|
|
|
@@ -72,6 +73,10 @@ import {
|
|
|
STUN_GUARD_KNOCKBACK,
|
|
|
STUN_GUARD_S,
|
|
|
TICK_DT,
|
|
|
+ WALL_JUMP_PUSH,
|
|
|
+ WALL_JUMP_UP,
|
|
|
+ WALL_NORMAL_X,
|
|
|
+ WALL_SLIDE_SPEED,
|
|
|
WHISK_CRACK_POWER,
|
|
|
WHISK_CRACK_RADIUS,
|
|
|
WHISK_CRACKS,
|
|
|
@@ -123,6 +128,9 @@ interface ServerPlayer {
|
|
|
facing: number;
|
|
|
grounded: boolean;
|
|
|
sinceGrounded: number;
|
|
|
+ /** Wall touched in the air: -1 on the left, 1 on the right, 0 none; and how long ago. */
|
|
|
+ wallSide: number;
|
|
|
+ sinceWall: number;
|
|
|
jumpBuffer: number;
|
|
|
attackCooldown: number;
|
|
|
specialCooldown: number;
|
|
|
@@ -390,6 +398,8 @@ export class Game {
|
|
|
facing: 1,
|
|
|
grounded: false,
|
|
|
sinceGrounded: Infinity,
|
|
|
+ wallSide: 0,
|
|
|
+ sinceWall: Infinity,
|
|
|
jumpBuffer: 0,
|
|
|
attackCooldown: 0,
|
|
|
specialCooldown: 0,
|
|
|
@@ -748,10 +758,22 @@ export class Game {
|
|
|
}
|
|
|
|
|
|
p.jumpBuffer = pressed & Button.Jump ? JUMP_BUFFER_TIME : Math.max(0, p.jumpBuffer - TICK_DT);
|
|
|
+ const jumpSpeed =
|
|
|
+ stats.jump * Math.sqrt(mobility) * (p.soaked > 0 ? SOAKED_JUMP_MULT : 1) * (this.cherry?.holder === p.info.id ? CHERRY_HOLDER_JUMP_MULT : 1);
|
|
|
+ const towardWall = !locked && p.wallSide !== 0 && isDown(input, p.wallSide > 0 ? Button.Right : Button.Left);
|
|
|
if (p.jumpBuffer > 0 && p.sinceGrounded <= COYOTE_TIME) {
|
|
|
- vy = -stats.jump * Math.sqrt(mobility) * (p.soaked > 0 ? SOAKED_JUMP_MULT : 1);
|
|
|
+ vy = -jumpSpeed;
|
|
|
p.jumpBuffer = 0;
|
|
|
p.sinceGrounded = Infinity;
|
|
|
+ } else if (p.jumpBuffer > 0 && p.sinceWall <= COYOTE_TIME && p.wallSide !== 0) {
|
|
|
+ // Wall jump: kick off the wall, up and a little away.
|
|
|
+ vy = -jumpSpeed * WALL_JUMP_UP;
|
|
|
+ vx = -p.wallSide * WALL_JUMP_PUSH;
|
|
|
+ p.jumpBuffer = 0;
|
|
|
+ p.sinceWall = Infinity;
|
|
|
+ } else if (towardWall && p.sinceWall === 0 && vy > WALL_SLIDE_SPEED) {
|
|
|
+ // Holding toward a wall slows the fall.
|
|
|
+ vy = WALL_SLIDE_SPEED;
|
|
|
} else if (released & Button.Jump && vy < 0) {
|
|
|
vy *= JUMP_CUT;
|
|
|
}
|
|
|
@@ -786,19 +808,28 @@ export class Game {
|
|
|
private postStep(p: ServerPlayer): void {
|
|
|
if (p.dead > 0) return;
|
|
|
p.grounded = false;
|
|
|
+ let wall = 0;
|
|
|
for (let edge = p.body.getContactList(); edge; edge = edge.next) {
|
|
|
const contact = edge.contact;
|
|
|
if (!contact.isTouching()) continue;
|
|
|
const normal = contact.getWorldManifold(null)?.normal;
|
|
|
if (!normal) continue;
|
|
|
// The manifold normal points from fixture A to fixture B; flip it so it points toward the player.
|
|
|
- const ny = contact.getFixtureA().getBody() === p.body ? -normal.y : normal.y;
|
|
|
- if (ny < GROUND_NORMAL_Y) {
|
|
|
- p.grounded = true;
|
|
|
- break;
|
|
|
- }
|
|
|
+ const flip = contact.getFixtureA().getBody() === p.body ? -1 : 1;
|
|
|
+ const nx = normal.x * flip;
|
|
|
+ const ny = normal.y * flip;
|
|
|
+ if (ny < GROUND_NORMAL_Y) p.grounded = true;
|
|
|
+ // A wall on the left pushes the player right (+x), and the other way round.
|
|
|
+ else if (Math.abs(nx) > WALL_NORMAL_X) wall = nx > 0 ? -1 : 1;
|
|
|
}
|
|
|
p.sinceGrounded = p.grounded ? 0 : p.sinceGrounded + TICK_DT;
|
|
|
+ if (!p.grounded && wall !== 0) {
|
|
|
+ p.wallSide = wall;
|
|
|
+ p.sinceWall = 0;
|
|
|
+ } else {
|
|
|
+ p.sinceWall += TICK_DT;
|
|
|
+ if (p.grounded) p.wallSide = 0;
|
|
|
+ }
|
|
|
// Landing ends the spoon flight; brushing an edge on the way up does not.
|
|
|
if (p.grounded && p.body.getLinearVelocity().y >= 0) p.flight = 0;
|
|
|
const y = p.body.getPosition().y * PPM;
|
|
|
@@ -1015,6 +1046,7 @@ export class Game {
|
|
|
vx: vel.x * PPM,
|
|
|
vy: vel.y * PPM,
|
|
|
grounded: p.grounded,
|
|
|
+ carryingCherry: this.cherry?.holder === p.info.id,
|
|
|
facing: p.facing,
|
|
|
input,
|
|
|
emitOp: (op, onApplied) => this.queueOp(op, onApplied, p.info.id),
|
|
|
@@ -1318,8 +1350,11 @@ export class Game {
|
|
|
const pos = cherry.body.getPosition();
|
|
|
if (pos.y * PPM > this.custardY + CUSTARD_SINK_PX || pos.y * PPM > this.terrain.height + OUT_OF_MAP_MARGIN) {
|
|
|
this.fx.push({ kind: 'splash', id: 0, x: pos.x * PPM, y: this.custardY });
|
|
|
- const [x, y] = this.map.cherrySpawn;
|
|
|
+ // Back on the cake, anywhere it can rest (not always at the top: diggers get their chance).
|
|
|
+ const spots = this.landingSpots(CHERRY_RADIUS, Math.min(this.custardY, this.terrain.height) - CHERRY_RADIUS - 4);
|
|
|
+ const [x, y] = spots.length > 0 ? spots[this.rng.int(0, spots.length - 1)] : this.map.cherrySpawn;
|
|
|
cherry.body.setPosition({ x: x / PPM, y: y / PPM });
|
|
|
+ this.fx.push({ kind: 'pickup', id: 0, item: 'cherry', x, y }); // shows everyone where it is now
|
|
|
cherry.body.setLinearVelocity({ x: 0, y: 0 });
|
|
|
return;
|
|
|
}
|