From 95571c6296efac35cc69ab0d77e0862d1d6dcc7b Mon Sep 17 00:00:00 2001 From: Surya Teja Date: Thu, 29 Jan 2026 01:43:50 -0500 Subject: [PATCH] added crowd --- src/client/game/GameScene.ts | 48 ++-- src/client/game/PhaserGame.tsx | 3 +- src/client/game/backgrounds/addons/crowd.ts | 213 ++++++++++++++++++ .../game/backgrounds/screens/stadium.ts | 10 + 4 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 src/client/game/backgrounds/addons/crowd.ts create mode 100644 src/client/game/backgrounds/screens/stadium.ts diff --git a/src/client/game/GameScene.ts b/src/client/game/GameScene.ts index eb27a5b..09e9f47 100644 --- a/src/client/game/GameScene.ts +++ b/src/client/game/GameScene.ts @@ -1,4 +1,6 @@ import Phaser from 'phaser'; +import { stadiumScreen } from './backgrounds/screens/stadium'; +import { CrowdAddon } from './backgrounds/addons/crowd'; export default class GameScene extends Phaser.Scene { public declare add: Phaser.GameObjects.GameObjectFactory; @@ -25,6 +27,8 @@ export default class GameScene extends Phaser.Scene { private fireworksTriggered: boolean = false; private lastDistractionScore: number = 0; private dailyModifiers = { gravityMultiplier: 1, kickMultiplier: 1, windX: 0 }; + private crowdAddon?: CrowdAddon; + private crowdCheerTriggered: boolean = false; private onGameOverCallback?: (score: number) => void; private onScoreUpdateCallback?: (score: number) => void; @@ -56,6 +60,8 @@ export default class GameScene extends Phaser.Scene { this.confettiTriggered = false; this.fireworksTriggered = false; this.lastDistractionScore = 0; + this.crowdCheerTriggered = false; + this.crowdAddon?.reset(); } create() { @@ -145,9 +151,10 @@ export default class GameScene extends Phaser.Scene { } } - this.cameras.main.setBackgroundColor('#1e293b'); + this.cameras.main.setBackgroundColor(stadiumScreen.background); - this.createCrowdBackdrop(width, height); + this.crowdAddon = new CrowdAddon(this, stadiumScreen.crowd); + this.crowdAddon.create(width, height); // Score BG this.scoreText = this.add @@ -196,6 +203,11 @@ export default class GameScene extends Phaser.Scene { this.handleBallTap(pointer); } }); + this.input.keyboard?.on('keydown-SPACE', () => { + if (this.isGameOver) return; + const syntheticPointer = { x: this.ball.x, y: this.ball.y } as Phaser.Input.Pointer; + this.handleBallTap(syntheticPointer); + }); const instr = this.add .text(width / 2, height - 100, 'TAP BALL TO KICK OFF', { @@ -272,6 +284,10 @@ export default class GameScene extends Phaser.Scene { if (this.score >= 10 && this.score <= 100 && this.score % 10 === 0) { this.blastConfetti(); } + if (this.score === 10 && !this.crowdCheerTriggered) { + this.crowdCheerTriggered = true; + this.crowdAddon?.cheer(3000); + } if (this.score >= 5 && !this.fireworksTriggered) { this.fireworksTriggered = true; this.launchFireworks(); @@ -291,33 +307,6 @@ export default class GameScene extends Phaser.Scene { }); } - private createCrowdBackdrop(width: number, height: number) { - const rows = 3; - const rowHeight = Math.max(40, height * 0.07); - const baseY = height * 0.12; - const colors = [0x0b1220, 0x111827, 0x1f2937]; - - for (let row = 0; row < rows; row++) { - const y = baseY + row * (rowHeight + 8); - const headCount = Math.max(10, Math.floor(width / 60)); - const headWidth = width / headCount; - - for (let i = 0; i < headCount; i++) { - const x = i * headWidth + headWidth / 2; - const bodyHeight = Phaser.Math.Between(18, 28); - const bodyWidth = Phaser.Math.Between(18, 30); - const color = colors[row % colors.length]; - - const body = this.add.rectangle(x, y + rowHeight / 2, bodyWidth, bodyHeight, color, 0.9); - body.setDepth(1); - } - } - - // Soft overlay to blend the crowd into the background - const overlay = this.add.rectangle(width / 2, baseY + rowHeight, width, rowHeight * rows, 0x0f172a, 0.35); - overlay.setDepth(2); - } - private shouldTriggerDistraction() { return ( this.score >= 25 && @@ -614,4 +603,5 @@ export default class GameScene extends Phaser.Scene { delayMs += clusterGapMs + Phaser.Math.Between(-clusterJitterMs, clusterJitterMs); } } + } diff --git a/src/client/game/PhaserGame.tsx b/src/client/game/PhaserGame.tsx index 9df5a08..1d8ce95 100644 --- a/src/client/game/PhaserGame.tsx +++ b/src/client/game/PhaserGame.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import Phaser from 'phaser'; import GameScene from './GameScene'; import type { DailyModifiers } from '../services/daily'; +import { stadiumScreen } from './backgrounds/screens/stadium'; interface PhaserGameProps { onGameOver: (score: number) => void; @@ -45,7 +46,7 @@ const PhaserGame: React.FC = ({ onGameOver, onScoreUpdate, dail }, }, scene: GameScene, - backgroundColor: '#1e293b', + backgroundColor: stadiumScreen.background, transparent: true, antialias: true, scale: { diff --git a/src/client/game/backgrounds/addons/crowd.ts b/src/client/game/backgrounds/addons/crowd.ts new file mode 100644 index 0000000..83f489b --- /dev/null +++ b/src/client/game/backgrounds/addons/crowd.ts @@ -0,0 +1,213 @@ +import Phaser from 'phaser'; + +export type CrowdColors = { + band: number; + frontBand: number; + haze: number; + bodyPalette: number[]; + head: number; +}; + +type CrowdPerson = { + container: Phaser.GameObjects.Container; + leftArm: Phaser.GameObjects.Rectangle; + rightArm: Phaser.GameObjects.Rectangle; + seatedY: number; + standingY: number; + idleLeftRot: number; + idleRightRot: number; +}; + +type CrowdOptions = { + groundOffset?: number; + vDepth?: number; + bandHeight?: number; + count?: number; +}; + +export class CrowdAddon { + private scene: Phaser.Scene; + private colors: CrowdColors; + private people: CrowdPerson[] = []; + private clapTweens: Phaser.Tweens.Tween[] = []; + private backgroundObjects: Phaser.GameObjects.GameObject[] = []; + + constructor(scene: Phaser.Scene, colors: CrowdColors) { + this.scene = scene; + this.colors = colors; + } + + create(width: number, height: number, options: CrowdOptions = {}) { + this.reset(); + this.people = []; + + const bandHeight = options.bandHeight ?? Math.max(60, height * 0.16); + const groundOffset = options.groundOffset ?? 40; + const baseY = height - groundOffset; + const vDepth = options.vDepth ?? Math.max(18, bandHeight * 0.35); + const count = options.count ?? Math.max(14, Math.floor(width / 44)); + const standOffset = Math.max(16, bandHeight * 0.25); + + const bandBottom = baseY + bandHeight * 0.2; + const backBand = this.scene.add.rectangle( + width / 2, + bandBottom - bandHeight / 2, + width, + bandHeight, + this.colors.band, + 0.7 + ); + backBand.setDepth(1); + + const frontBand = this.scene.add.rectangle( + width / 2, + bandBottom - bandHeight * 0.1, + width, + bandHeight * 0.45, + this.colors.frontBand, + 0.8 + ); + frontBand.setDepth(2); + + const haze = this.scene.add.rectangle( + width / 2, + bandBottom - bandHeight * 0.8, + width, + bandHeight, + this.colors.haze, + 0.25 + ); + haze.setDepth(4); + + this.backgroundObjects.push(backBand, frontBand, haze); + + const gap = width / count; + const center = (count - 1) / 2; + + for (let i = 0; i < count; i++) { + const x = i * gap + gap / 2; + const t = center > 0 ? Math.abs(i - center) / center : 0; + const seatY = baseY - vDepth * t; + + const torsoWidth = Phaser.Math.Between(14, 18); + const torsoHeight = Phaser.Math.Between(14, 18); + const armLength = Phaser.Math.Between(10, 14); + const armWidth = 4; + const legLength = Phaser.Math.Between(10, 12); + const legWidth = 4; + const headRadius = Phaser.Math.Between(5, 7); + const bodyColor = this.colors.bodyPalette[i % this.colors.bodyPalette.length]; + + const container = this.scene.add.container(x, seatY); + container.setDepth(3); + + const head = this.scene.add.circle(0, -20, headRadius, this.colors.head, 1); + const torso = this.scene.add.rectangle(0, -6, torsoWidth, torsoHeight, bodyColor, 0.95); + const leftArm = this.scene.add.rectangle(-torsoWidth / 2, -10, armLength, armWidth, bodyColor, 0.95); + leftArm.setOrigin(1, 0.5); + const rightArm = this.scene.add.rectangle(torsoWidth / 2, -10, armLength, armWidth, bodyColor, 0.95); + rightArm.setOrigin(0, 0.5); + + const leftLeg = this.scene.add.rectangle( + -torsoWidth / 4, + torsoHeight / 2 + 2, + legWidth, + legLength, + bodyColor, + 0.95 + ); + leftLeg.setOrigin(0.5, 0); + leftLeg.setRotation(0.9); + const rightLeg = this.scene.add.rectangle( + torsoWidth / 4, + torsoHeight / 2 + 2, + legWidth, + legLength, + bodyColor, + 0.95 + ); + rightLeg.setOrigin(0.5, 0); + rightLeg.setRotation(-0.9); + + const idleLeftRot = -0.7; + const idleRightRot = 0.7; + leftArm.setRotation(idleLeftRot); + rightArm.setRotation(idleRightRot); + + container.add([head, torso, leftArm, rightArm, leftLeg, rightLeg]); + + this.people.push({ + container, + leftArm, + rightArm, + seatedY: seatY, + standingY: seatY - standOffset, + idleLeftRot, + idleRightRot, + }); + } + } + + cheer(durationMs: number = 3000) { + if (this.people.length === 0) return; + + this.clapTweens.forEach((tween) => tween.stop()); + this.clapTweens = []; + + this.people.forEach((person, index) => { + this.scene.tweens.add({ + targets: person.container, + y: person.standingY, + duration: 260, + ease: 'Back.easeOut', + delay: index * 10, + }); + + const clapLeft = this.scene.tweens.add({ + targets: person.leftArm, + rotation: { from: person.idleLeftRot, to: -0.2 }, + duration: 140, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut', + delay: Phaser.Math.Between(0, 220), + }); + const clapRight = this.scene.tweens.add({ + targets: person.rightArm, + rotation: { from: person.idleRightRot, to: 0.2 }, + duration: 140, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut', + delay: Phaser.Math.Between(0, 220), + }); + this.clapTweens.push(clapLeft, clapRight); + }); + + this.scene.time.delayedCall(durationMs, () => { + this.clapTweens.forEach((tween) => tween.stop()); + this.clapTweens = []; + + this.people.forEach((person, index) => { + person.leftArm.setRotation(person.idleLeftRot); + person.rightArm.setRotation(person.idleRightRot); + this.scene.tweens.add({ + targets: person.container, + y: person.seatedY, + duration: 240, + ease: 'Sine.easeInOut', + delay: index * 8, + }); + }); + }); + } + + reset() { + this.clapTweens.forEach((tween) => tween.stop()); + this.clapTweens = []; + this.people.forEach((person) => person.container.destroy(true)); + this.people = []; + this.backgroundObjects.forEach((obj) => obj.destroy()); + this.backgroundObjects = []; + } +} diff --git a/src/client/game/backgrounds/screens/stadium.ts b/src/client/game/backgrounds/screens/stadium.ts new file mode 100644 index 0000000..12d5694 --- /dev/null +++ b/src/client/game/backgrounds/screens/stadium.ts @@ -0,0 +1,10 @@ +export const stadiumScreen = { + background: '#1e293b', + crowd: { + band: 0x0f172a, + frontBand: 0x111827, + haze: 0x0b1220, + bodyPalette: [0x0b1220, 0x111827, 0x1f2937, 0x0f172a], + head: 0x1f2937, + }, +};