| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- import { Container, Graphics, Text } from 'pixi.js';
- import { Anim } from '../../shared/characters';
- import type { PlayerInfo, PlayerState } from '../../shared/protocol';
- import { PAINTERS, paintGeneric, type Painter, type Pose } from '../characters/painters';
- import { paintItem } from './RoundViews';
- import { OUTLINE_COLOR, OUTLINE_WIDTH, PLAYER_COLORS, RUN_LEAN, SQUASH_RECOVERY } from '../config';
- /** Max size the fat ratio is normalized against (castor maxSize is in its JSON; this only drives the cheeks). */
- const FAT_RANGE = 26;
- export interface PlayerEvents {
- landed: boolean;
- landingSpeed: number;
- stunnedNow: boolean;
- }
- /** A character on screen: painter-drawn body with squash & stretch, walk cycle, and a name tag. */
- export class PlayerView {
- readonly container = new Container();
- private readonly body = new Graphics();
- /** Soft contact shadow on the ground. */
- private readonly shadow = new Graphics();
- /** Carried item (candle) held up next to the character. */
- private readonly held = new Graphics();
- private readonly name: Text;
- private readonly paint: Painter;
- private readonly color: number;
- private time = 0;
- private walk = 0;
- private squash = 0;
- private wasGrounded = true;
- private wasStunned = false;
- private lastVy = 0;
- constructor(readonly info: PlayerInfo) {
- this.paint = PAINTERS[info.characterId] ?? paintGeneric;
- this.color = PLAYER_COLORS[(info.id - 1) % PLAYER_COLORS.length];
- this.name = new Text({
- text: info.name,
- style: { fontFamily: 'Comic Sans MS', fontSize: 15, fill: this.color, fontWeight: 'bold', stroke: { color: OUTLINE_COLOR, width: 4 } },
- });
- this.name.anchor.set(0.5, 1);
- this.held.scale.set(0.8);
- this.container.addChild(this.shadow, this.body, this.held, this.name);
- }
- /** Draws a static pose (menu portraits). */
- drawIdle(): void {
- this.draw(this.info.radius, 1, 0.6, -0.2, { moving: false, grounded: true, attack: false, special: false, stunned: false, busy: false, underground: false });
- this.shadow.clear();
- }
- update(state: PlayerState, aimX: number, aimY: number, dt: number): PlayerEvents {
- this.time += dt;
- const size = state.size;
- this.container.position.set(state.x, state.y);
- // In the custard, waiting for the spoon.
- this.container.visible = (state.anim & Anim.Dead) === 0;
- this.held.clear();
- if (state.item) {
- paintItem(this.held, state.item, false, this.time);
- this.held.position.set(-state.facing * size * 0.95, -size * 0.35);
- this.held.rotation = -state.facing * 0.4;
- }
- // Landing squashes, jumping stretches.
- const events: PlayerEvents = { landed: false, landingSpeed: 0, stunnedNow: false };
- if (state.grounded && !this.wasGrounded) {
- this.squash = Math.min(0.45, Math.abs(this.lastVy) / 1600);
- events.landed = true;
- events.landingSpeed = Math.abs(this.lastVy);
- } else if (!state.grounded && this.wasGrounded && state.vy < -100) this.squash = -0.3;
- this.wasGrounded = state.grounded;
- this.lastVy = state.vy;
- const stunned = (state.anim & Anim.Stunned) !== 0;
- events.stunnedNow = stunned && !this.wasStunned;
- this.wasStunned = stunned;
- this.squash += (0 - this.squash) * Math.min(1, SQUASH_RECOVERY * dt);
- const moving = Math.abs(state.vx) > 20;
- this.walk += dt * (moving ? Math.abs(state.vx) / 18 : 0);
- // Look direction in local (facing-right) space.
- const dx = (aimX - state.x) * state.facing;
- const dy = aimY - state.y;
- const len = Math.hypot(dx, dy) || 1;
- // Leans into the run.
- this.body.rotation = Math.max(-RUN_LEAN, Math.min(RUN_LEAN, (state.vx / 600) * RUN_LEAN));
- this.shadow.clear();
- if (state.grounded) this.shadow.ellipse(0, size * 0.98, size * 0.85, size * 0.18).fill({ color: OUTLINE_COLOR, alpha: 0.18 });
- this.draw(size, state.facing, dx / len, dy / len, {
- moving,
- grounded: state.grounded,
- attack: (state.anim & Anim.Attack) !== 0,
- special: (state.anim & Anim.Special) !== 0,
- stunned,
- busy: (state.anim & Anim.Busy) !== 0,
- underground: (state.anim & Anim.Underground) !== 0,
- });
- return events;
- }
- private draw(
- size: number,
- facing: number,
- lookX: number,
- lookY: number,
- flags: Pick<Pose, 'moving' | 'grounded' | 'attack' | 'special' | 'stunned' | 'busy' | 'underground'>,
- ): void {
- const base = this.info.radius;
- const scale = size / base;
- const sx = 1 + this.squash;
- const sy = 1 - this.squash;
- // Wobble while eating.
- const wobble = flags.special ? 1 + Math.sin(this.time * 28) * 0.04 : 1;
- this.body.scale.set(facing * scale * sx * wobble, scale * sy / wobble);
- // Keep the feet on the ground while squashed.
- this.body.y = size * (1 - sy);
- this.body.clear();
- this.paint(this.body, base, {
- time: this.time,
- walk: this.walk,
- ...flags,
- lookX,
- lookY,
- fat: Math.min(1, Math.max(0, (size - base) / FAT_RANGE)),
- color: this.color,
- ink: OUTLINE_WIDTH / scale,
- });
- this.name.y = -size * 1.15 - 10;
- }
- destroy(): void {
- this.container.destroy({ children: true });
- }
- }
|