Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion backend/src/rooms/MainRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
COLOR_STATIONS,
DOOR_IDS,
DOORS,
ELECTRIC_LINK_RANGE,
ELECTRIC_SOURCE,
PLATES,
PLAYER_RADIUS,
SPAWN_POINT,
Expand Down Expand Up @@ -201,14 +203,37 @@ export class MainRoom extends Room {
const occupants: PlateOccupant[] = [];
for (const player of this.state.players.values()) {
if (this.playerOverlaps(player, definition)) {
occupants.push({ entityKind: "player", color: player.color });
occupants.push({ entityKind: "player", color: player.color, charged: player.charged });
}
}
occupants.push(...this.world.occupantsInRect(definition));
return occupants;
}

private updateElectricity() {
const players = [...this.state.players.values()];
const charged = new Set<Player>();
for (const player of players) {
if (this.playerOverlaps(player, ELECTRIC_SOURCE)) charged.add(player);
}
for (let grew = true; grew;) {
grew = false;
for (const a of players) {
if (!charged.has(a)) continue;
for (const b of players) {
if (charged.has(b) || a === b) continue;
if (Math.hypot(a.x - b.x, a.y - b.y) <= ELECTRIC_LINK_RANGE) {
charged.add(b);
grew = true;
}
}
}
}
for (const player of players) player.charged = charged.has(player);
}

private updateGameplay() {
this.updateElectricity();
const now = Date.now();
const totalPlayers = this.state.players.size;

Expand Down
1 change: 1 addition & 0 deletions backend/src/rooms/schema/GeneralSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class Player extends Schema {
@type("string") color = "#4ade80";
@type("number") x = 1600;
@type("number") y = 1200;
@type("boolean") charged = false;
}

export class Door extends Schema {
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/entities/BallView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,13 @@ function parseColor(color: string) {
return Number.isFinite(parsed) ? parsed : 0xffffff;
}

/**
* Client-side prediction with the shared physics: local hits react instantly,
* while a "ghost" body (hard-set to each server snapshot, then advanced by the
* same physics without local input) is what the prediction reconciles against.
*/
export class BallView implements EntityView {
readonly radius: number;
private config: EntityKindConfig;
private predicted: CircleBody;
private ghost: CircleBody;
private container: Phaser.GameObjects.Container;

private body: Phaser.GameObjects.Arc;
private fillColor: number;

Expand All @@ -40,8 +36,6 @@ export class BallView implements EntityView {
this.ghost = { ...this.predicted };

this.fillColor = parseColor(state.color);
// A colour disc under the ink barrel: the pack art is black linework, and
// setTint multiplies, so tinting the sprite itself would do nothing.
this.body = scene.add.circle(0, 0, state.radius * 0.82, this.fillColor, 0.85);
const barrel = scene.add
.image(0, 0, "barrel")
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/entities/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { BallView } from "./BallView";
import type { Rect, Sweep } from "@shared";
import type { EntityState } from "../network/state";

/** Per-frame prediction inputs: local cursor sweep + currently solid rects. */
export interface PredictionContext {
sweep: Sweep | null;
solids: readonly Rect[];
Expand All @@ -17,7 +16,6 @@ export interface EntityView {

type ViewFactory = (scene: Phaser.Scene, state: EntityState) => EntityView;

/** Register new entity kinds here; unknown kinds fall back to a plain ball. */
const VIEWS: Record<string, ViewFactory> = {
ball: (scene, state) => new BallView(scene, state),
boulder: (scene, state) => new BallView(scene, state),
Expand Down
1 change: 0 additions & 1 deletion frontend/src/gameplay/collision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Phaser from "phaser";
import { moveCircle } from "@shared";
import type { Rect } from "@shared";

/** Cursor movement uses the same shared collision code as simulated entities. */
export function moveAndCollide(
cursor: { x: number; y: number },
velocity: Phaser.Math.Vector2,
Expand Down
59 changes: 59 additions & 0 deletions frontend/src/gameplay/electric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Phaser from "phaser";
import type { Rect } from "@shared";
import { ACCENT_SPARK } from "./palette";

function drawBolt(scene: Phaser.Scene, x: number, y: number) {
const angle =
(Math.random() < 0.5 ? -1 : 1) * (Math.PI / 2) + Phaser.Math.FloatBetween(-0.5, 0.5);
const length = Phaser.Math.Between(18, 32);
const segments = 4;
const bolt = scene.add.graphics().setPosition(x, y).setDepth(20).lineStyle(3, ACCENT_SPARK, 1);
bolt.beginPath();
bolt.moveTo(0, 0);
for (let i = 1; i <= segments; i++) {
const along = (length * i) / segments;
const across = i === segments ? 0 : Phaser.Math.Between(-7, 7);
bolt.lineTo(
Math.cos(angle) * along - Math.sin(angle) * across,
Math.sin(angle) * along + Math.cos(angle) * across,
);
}
bolt.strokePath();
scene.tweens.add({ targets: bolt, alpha: 0, duration: 160, onComplete: () => bolt.destroy() });
}

function sparkLoop(scene: Phaser.Scene, getPos: () => { x: number; y: number } | null) {
const timer = scene.time.addEvent({
delay: 70,
loop: true,
callback: () => {
const pos = getPos();
if (!pos) return timer.remove();
drawBolt(scene, pos.x, pos.y);
},
});
return timer;
}

export function buildElectricSource(scene: Phaser.Scene, source: Rect) {
const cx = source.x + source.width / 2;
const cy = source.y + source.height / 2;
sparkLoop(scene, () => ({ x: cx, y: cy }));
}

export function setElectrified(
scene: Phaser.Scene,
body: Phaser.GameObjects.Image,
charged: boolean,
) {
if (charged === (body.getData("charged") ?? false)) return;
body.setData("charged", charged);
if (!charged) {
(body.getData("sparks") as Phaser.Time.TimerEvent | undefined)?.remove();
return;
}
body.setData(
"sparks",
sparkLoop(scene, () => (body.isDestroyed ? null : { x: body.x, y: body.y })),
);
}
Loading
Loading