Add spectacle-first visual effects infrastructure for viral promo clips (#3)

Wire SPECTACLE_* EventBus hooks into the template, skill docs, design
audit, and make-game pipeline so every new game starts with entrance
animations, combo/streak effects, and intensity targets calibrated for
13-second silent video capture.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
glitchtrend
2026-02-23 04:12:46 -08:00
parent f31e524dbf
commit 7459a72443
5 changed files with 530 additions and 19 deletions
+157
View File
@@ -23,6 +23,27 @@ A scaffolded game is functional but visually flat. A designed game has:
- **Satisfying feedback**: Every action has a visible (and audible) reaction
- **Smooth transitions**: Scenes flow into each other, not jump-cut
## Viral Spectacle Philosophy
The design target is not just the player — it's a **viewer scrolling a social feed with sound off**. Games are captured as 13-second silent video clips. Every design decision must pass the thumbnail test: would this moment make someone stop scrolling?
**Five principles:**
1. **Every frame must have motion** — No static moments. Background particles, color shifts, trails, bobbing idle animations. A paused screenshot should still look dynamic.
2. **Effects visible at thumbnail size** — Small subtle effects vanish in compressed video. Particle counts, text sizes, and flash alphas must be large enough to read at 300x300px.
3. **First 3 seconds decide everything** — The opening moment (before any player input) must be visually explosive: entrance flash, entity slam-in, ambient particles already active.
4. **Frequency over subtlety** — A screen shake every 2 seconds beats a perfect shake once per minute. More effects at moderate intensity > fewer effects at high intensity.
5. **Silent communication** — Text slams ("COMBO!", "ON FIRE!"), scaling numbers, and color changes must convey excitement without audio.
### Opening Moment
These elements fire in `create()` before any player input:
- **Entrance flash** — `cameras.main.flash(300)` on scene start
- **Entity slam-in** — Player drops from above with `Bounce.easeOut`, landing shake + particle burst
- **Ambient motion** — Background particles, color cycling, or parallax drift active from frame 1
- **Optional flavor text** — Short text like "GO!", "DODGE!", or "FIGHT!" that scales in and fades. Use only when it naturally fits the game's vibe — not every game needs it
## Design Process
When invoked, follow this process:
@@ -53,9 +74,12 @@ Evaluate these areas and score each 1-5:
| **Safe Zone** | Are all UI elements (text, buttons, score panels) positioned below `SAFE_ZONE.TOP`? Does any UI get hidden behind the Play.fun widget bar (~75px at top)? |
| **Entity Prominence** | Is the player character large enough to read? Character-driven games need 12-15% of GAME.WIDTH. Are entities proportionally sized (`GAME.WIDTH * ratio`), not fixed pixels? |
| **Character Prominence** | Is the main character the visually dominant element? Does it occupy 30%+ of screen height? Larger than all other entities? |
| **First Impression / Viral Appeal** | Does the game explode visually in the first 3 seconds? Entrance animation, ambient particles active, background in motion? Would a 13-second silent clip stop a scroller? |
Present the scores as a table, then list the top improvements ranked by visual impact.
**Mandatory threshold**: Any area scoring below 4 MUST be improved before the design pass is complete. **First Impression / Viral Appeal is the most critical category** — it directly determines whether the promo clip converts viewers.
### Step 3: Implement improvements
After presenting the report, implement the improvements. Follow these rules:
@@ -68,6 +92,139 @@ After presenting the report, implement the improvements. Follow these rules:
6. **Create new files** in the appropriate directories (`systems/`, `entities/`, `ui/`)
7. **Respect the safe zone** — Verify all UI text, buttons, and interactive elements are below `SAFE_ZONE.TOP` from Constants.js. If any UI element is positioned in the top 8% of the screen, shift it down. Use `SAFE_ZONE.TOP + usableH * ratio` for proportional positioning (where `usableH = GAME.HEIGHT - SAFE_ZONE.TOP`).
### Spectacle Effects (Viral-Critical)
These effects are the highest priority for promo clip impact. Wire them to `SPECTACLE_*` EventBus events.
#### Combo Text with Scaling
```js
// Wire to SPECTACLE_COMBO — grows with consecutive hits
eventBus.on(Events.SPECTACLE_COMBO, ({ combo }) => {
const size = Math.min(32 + combo * 4, 72);
const text = scene.add.text(GAME.WIDTH / 2, GAME.HEIGHT * 0.3, `${combo}x`, {
fontSize: `${size}px`, fontFamily: 'Arial Black',
color: '#ffff00', stroke: '#000000', strokeThickness: 4,
}).setOrigin(0.5).setScale(1.8).setDepth(400);
scene.tweens.add({
targets: text,
scale: 1, y: text.y - 30, alpha: 0,
duration: 700, ease: 'Elastic.easeOut',
onComplete: () => text.destroy(),
});
});
```
#### Hit Freeze Frame
```js
// 60ms physics pause on destruction — makes hits feel powerful
function hitFreeze(scene) {
scene.physics.world.pause();
scene.time.delayedCall(60, () => scene.physics.world.resume());
}
```
#### Rainbow / Color Cycling Background
```js
// Hue shifts over time in update() — ambient visual energy
let bgHue = 0;
function updateBgHue(delta, bgGraphics) {
bgHue = (bgHue + delta * 0.02) % 360;
const color = Phaser.Display.Color.HSLToColor(bgHue / 360, 0.6, 0.15);
bgGraphics.clear();
bgGraphics.fillStyle(color.color, 1);
bgGraphics.fillRect(0, 0, GAME.WIDTH, GAME.HEIGHT);
}
```
#### Pulsing Background on Score
```js
// Additive blend overlay that flashes on score events
const scorePulse = scene.add.rectangle(
GAME.WIDTH / 2, GAME.HEIGHT / 2, GAME.WIDTH, GAME.HEIGHT,
PALETTE.ACCENT, 0,
).setDepth(-50).setBlendMode(Phaser.BlendModes.ADD);
eventBus.on(Events.SCORE_CHANGED, () => {
scorePulse.setAlpha(0.15);
scene.tweens.add({
targets: scorePulse, alpha: 0, duration: 300, ease: 'Quad.easeOut',
});
});
```
#### Entity Entrance Animations
```js
// Pop-in: entity appears from scale 0
function popIn(scene, target, delay = 0) {
target.setScale(0);
scene.tweens.add({
targets: target, scale: 1, duration: 300, delay, ease: 'Back.easeOut',
});
}
// Slam-in: entity drops from above with bounce
function slamIn(scene, target, targetY, delay = 0) {
target.y = -50;
scene.tweens.add({
targets: target, y: targetY, duration: 350, delay, ease: 'Bounce.easeOut',
onComplete: () => scene.cameras.main.shake(80, 0.006),
});
}
```
#### Persistent Player Trail
```js
// Continuous particle spawn behind the player
const trail = scene.add.particles(0, 0, 'particle', {
follow: player,
scale: { start: 0.6, end: 0 },
alpha: { start: 0.5, end: 0 },
speed: { min: 5, max: 15 },
lifespan: 400,
frequency: 30,
blendMode: 'ADD',
tint: PALETTE.ACCENT,
});
```
#### Streak Milestone Announcements
```js
// Full-screen text slam at milestones (5x, 10x, 25x)
eventBus.on(Events.SPECTACLE_STREAK, ({ streak }) => {
const labels = { 5: 'ON FIRE!', 10: 'UNSTOPPABLE!', 25: 'LEGENDARY!' };
const label = labels[streak] || `${streak}x STREAK`;
const text = scene.add.text(GAME.WIDTH / 2, GAME.HEIGHT / 2, label, {
fontSize: '80px', fontFamily: 'Arial Black',
color: '#ffffff', stroke: '#000000', strokeThickness: 8,
}).setOrigin(0.5).setScale(3).setAlpha(0).setDepth(500);
scene.tweens.add({
targets: text, scale: 1, alpha: 1, duration: 300,
ease: 'Back.easeOut', hold: 400, yoyo: true,
onComplete: () => text.destroy(),
});
scene.cameras.main.shake(200, 0.02);
emitBurst(scene, GAME.WIDTH / 2, GAME.HEIGHT / 2, 40, PALETTE.HIGHLIGHT);
});
```
#### SPECTACLE Constants Example
```js
// In Constants.js — spectacle tuning values
export const SPECTACLE = {
ENTRANCE_FLASH_DURATION: 300,
ENTRANCE_SLAM_DURATION: 400,
HIT_FREEZE_MS: 60,
COMBO_TEXT_BASE_SIZE: 32,
COMBO_TEXT_MAX_SIZE: 72,
COMBO_TEXT_GROWTH: 4,
STREAK_MILESTONES: [5, 10, 25, 50],
PARTICLE_BURST_MIN: 12,
PARTICLE_BURST_MAX: 30,
SCORE_PULSE_ALPHA: 0.15,
BG_HUE_SPEED: 0.02,
};
```
## When NOT to Change
- **Physics values** (gravity, velocity, collision boxes) — those are gameplay, not design
+39 -13
View File
@@ -256,7 +256,7 @@ Launch a `Task` subagent with these instructions:
> 4. Scoring
> 5. Restart flow (GameState.reset() → clean slate)
>
> Keep scope small: **1 scene, 1 mechanic, 1 fail condition**. Get the gameplay loop working before any polish.
> Keep scope small: **1 scene, 1 mechanic, 1 fail condition**. Wire spectacle EventBus hooks alongside the core loop — they are scaffolding, not polish.
>
> Transform the template into the game concept:
> - Rename entities, scenes/systems, and events to match the concept
@@ -267,6 +267,9 @@ Launch a `Task` subagent with these instructions:
> - **No title screen** — the template boots directly into gameplay. Do not create a MenuScene or title screen. Only add one if the user explicitly asks.
> - **No in-game score HUD** — the Play.fun widget displays score in a deadzone at the top of the game. Do not create a UIScene or HUD overlay for score display.
> - **Mobile-first input**: Choose the best mobile input scheme for the game concept (tap zones, virtual joystick, gyroscope tilt, swipe). Implement touch + keyboard from the start — never keyboard-only. Use the unified analog InputSystem pattern (moveX/moveZ) so game logic is input-source-agnostic.
> - Wire spectacle events: emit `SPECTACLE_ENTRANCE` in `create()`, `SPECTACLE_ACTION` on every player input, `SPECTACLE_HIT` on score/destroy, `SPECTACLE_COMBO` on consecutive hits (pass `{ combo }` ), `SPECTACLE_STREAK` at milestones (5, 10, 25 — pass `{ streak }`), `SPECTACLE_NEAR_MISS` on close calls
> - Add entrance sequence in `create()`: player starts off-screen, tweens into position with `Bounce.easeOut`, landing shake + particle burst
> - Add combo tracking to GameState: `combo` (current streak, resets on miss), `bestCombo` (session high), both reset in `reset()`
> - Ensure restart is clean — test mentally that 3 restarts in a row would work identically
> - Add `isMuted` to GameState for audio mute support
>
@@ -432,7 +435,7 @@ Mark task 3 as `in_progress`.
Launch a `Task` subagent with these instructions:
> You are implementing Step 2 (Visual Design) of the game creation pipeline.
> You are implementing Step 2 (Visual Design — Spectacle-First) of the game creation pipeline.
>
> **Project path**: `<project-dir>`
> **Engine**: `<2d|3d>`
@@ -440,17 +443,40 @@ Launch a `Task` subagent with these instructions:
>
> **Read `progress.md`** at the project root before starting. It describes the game's entities, events, constants, and what previous steps have done.
>
> Apply the game-designer skill:
> 1. Audit the current visuals — read Constants.js, all scenes, entities, EventBus
> 2. Score each visual area (background, palette, animations, particles, transitions, typography, game feel, game over) on a 1-5 scale
> 3. Implement the highest-impact improvements:
> - Sky gradients or environment backgrounds
> - Particle effects for key gameplay moments
> - Screen shake, flash, or slow-mo for impact
> - Smooth scene transitions
> - UI juice: button hover, text shadows, floating score text
> 4. All new values go in Constants.js, use EventBus for triggering effects
> 5. Don't alter gameplay mechanics
> Apply the game-designer skill with spectacle as the top priority. Work in this order:
>
> **1. Opening Moment (CRITICAL — this determines promo clip success):**
> - Entrance flash: `cameras.main.flash(300)` on scene start
> - Player slam-in: player starts off-screen, tweens in with `Bounce.easeOut`, landing shake (0.012) + particle burst (20 particles)
> - Ambient particles active from frame 1 (drifting motes, dust, sparkles)
> - Optional flavor text (e.g., "GO!", "DODGE!") — only when it naturally fits the game's vibe
> - Verify: the first 3 seconds have zero static frames
>
> **2. Every-Action Effects (wire to SPECTACLE_* events from Step 1):**
> - Particle burst (12-20 particles) on `SPECTACLE_ACTION` and `SPECTACLE_HIT`
> - Floating score text (28px, scale 1.8, `Elastic.easeOut`) on `SCORE_CHANGED`
> - Background pulse (additive blend, alpha 0.15) on `SCORE_CHANGED`
> - Persistent player trail (particle emitter following player, `blendMode: ADD`)
> - Screen shake (0.008-0.015) on hits
>
> **3. Combo & Streak System (wire to SPECTACLE_COMBO / SPECTACLE_STREAK):**
> - Combo counter text that scales with combo count (32px base, +4px per combo)
> - Streak milestone announcements at 5x, 10x, 25x (full-screen text slam + 40-particle burst)
> - Hit freeze frame (60ms physics pause) on destruction events
> - Shake intensity scales with combo (0.008 + combo * 0.002, capped at 0.025)
>
> **4. Standard Design Audit:**
> - Full 10-area audit (background, palette, animations, particles, transitions, typography, game feel, game over, character prominence, first impression / viral appeal)
> - **Every area must score 4 or higher** — improve any that fall below
> - First Impression / Viral Appeal is the most critical category
>
> **5. Intensity Calibration:**
> - Particle bursts: 12-30 per event (never fewer than 10)
> - Screen shake: 0.008 (light) to 0.025 (heavy)
> - Floating text: 28px minimum, starting scale 1.8
> - Flash overlays: alpha 0.3-0.5
> - All new values go in Constants.js, use EventBus for triggering effects
> - Don't alter gameplay mechanics
>
> **After completing your work**, append a `## Step 2: Design` section to `progress.md` with: improvements applied, new effects added, any color or layout changes.
>
+17 -1
View File
@@ -13,7 +13,7 @@ You are an expert Phaser game developer building games with the game-creator plu
## Core Principles
1. **Core loop first** — Implement the minimum gameplay loop before any polish: boot → preload → create → update. Add the win/lose condition and scoring **before** visuals, audio, or juice. Keep initial scope small: 1 scene, 1 mechanic, 1 fail condition.
1. **Core loop first** — Implement the minimum gameplay loop before any polish: boot → preload → create → update. Add the win/lose condition and scoring **before** visuals, audio, or juice. Keep initial scope small: 1 scene, 1 mechanic, 1 fail condition. Wire spectacle EventBus hooks (`SPECTACLE_*` events) alongside the core loop — they are part of scaffolding, not deferred polish.
2. **TypeScript-first** — Always use TypeScript for type safety and IDE support
3. **Scene-based architecture** — Each game screen is a Scene; keep them focused
4. **Vite bundling** — Use the official `phaserjs/template-vite-ts` template
@@ -22,6 +22,21 @@ You are an expert Phaser game developer building games with the game-creator plu
7. **Event-driven communication** — All cross-scene/system communication via EventBus
8. **Restart-safe** — Gameplay must be fully restart-safe and deterministic. `GameState.reset()` must restore a clean slate. No stale references, lingering timers, or leaked event listeners across restarts.
## Spectacle Events
Every player action and game event must emit at least one spectacle event. These hooks exist in the template EventBus — the design pass attaches visual effects to them.
| Event | Constant | When to Emit |
|-------|----------|--------------|
| `spectacle:entrance` | `SPECTACLE_ENTRANCE` | In `create()` when the player/entities first appear on screen |
| `spectacle:action` | `SPECTACLE_ACTION` | On every player input (tap, jump, shoot, swipe) |
| `spectacle:hit` | `SPECTACLE_HIT` | When player hits/destroys an enemy, collects an item, or scores |
| `spectacle:combo` | `SPECTACLE_COMBO` | When consecutive hits/scores happen without a miss. Pass `{ combo: n }` |
| `spectacle:streak` | `SPECTACLE_STREAK` | When combo reaches milestones (5, 10, 25, 50). Pass `{ streak: n }` |
| `spectacle:near_miss` | `SPECTACLE_NEAR_MISS` | When player narrowly avoids danger (within ~20% of collision radius) |
**Rule**: If a gameplay moment has no spectacle event, add one. The design pass cannot polish what it cannot hook into.
## Mandatory Conventions
All games MUST follow the [game-creator conventions](conventions.md):
@@ -359,6 +374,7 @@ Before considering a game complete, verify:
- [ ] **Object pooling** — Frequently created/destroyed objects use Groups with `maxSize`
- [ ] **Delta-based movement** — All motion uses `delta`, not frame count
- [ ] **Mute toggle** — Audio can be muted/unmuted; `isMuted` state is respected
- [ ] **Spectacle hooks wired** — Every player action and game event emits a `SPECTACLE_*` event; entrance sequence fires in `create()`
- [ ] **Build passes**`npm run build` succeeds with no errors
- [ ] **No console errors** — Game runs without uncaught exceptions or WebGL failures
+309 -5
View File
@@ -287,14 +287,17 @@ update(): void {
### Screen Shake
On impacts, deaths, explosions:
On impacts, deaths, explosions. Use intensities that are visible in video — subtle shakes disappear in compression.
```typescript
// Light shake
this.cameras.main.shake(100, 0.005);
// Light shake (score, small hit)
this.cameras.main.shake(100, 0.008);
// Heavy shake
this.cameras.main.shake(200, 0.015);
// Medium shake (enemy destroyed, combo)
this.cameras.main.shake(150, 0.015);
// Heavy shake (death, streak milestone, big explosion)
this.cameras.main.shake(200, 0.025);
```
### Trail Effects
@@ -371,6 +374,276 @@ private popScore(text: Phaser.GameObjects.Text): void {
}
```
## Spectacle Patterns
These patterns make games visually compelling in short video clips. Wire them to `SPECTACLE_*` events from the EventBus so the design pass can plug them in without touching gameplay code.
### Opening Entrance Animation
Fires in `create()` before any player input. The first 3 seconds decide whether a viewer keeps watching.
```typescript
// Flash + player slam-in
private playEntrance(): void {
this.cameras.main.flash(300, 255, 255, 255, true);
// Player starts above screen, slams into position
const targetY = this.player.y;
this.player.y = -100 * PX;
this.tweens.add({
targets: this.player,
y: targetY,
duration: 400,
ease: 'Bounce.easeOut',
onComplete: () => {
this.cameras.main.shake(150, 0.012);
emitBurst(this, this.player.x, this.player.y, 20, PALETTE.ACCENT);
eventBus.emit(Events.SPECTACLE_ENTRANCE);
},
});
// Optional flavor text — use only when it fits the game's vibe
// (e.g., "GO!" for racing, "DODGE!" for avoidance, "FIGHT!" for combat)
const goText = this.add.text(GAME.WIDTH / 2, GAME.HEIGHT / 2, 'GO!', {
fontSize: `${64 * PX}px`, fontFamily: 'Arial Black',
color: '#ffffff', stroke: '#000000', strokeThickness: 6 * PX,
}).setOrigin(0.5).setScale(0).setDepth(500);
this.tweens.add({
targets: goText,
scale: 1.8,
alpha: 0,
duration: 600,
ease: 'Back.easeOut',
onComplete: () => goText.destroy(),
});
}
```
### Combo Counter with Scaling Text
Grows with consecutive hits. Wire to `SPECTACLE_COMBO`.
```typescript
private showCombo(combo: number): void {
const size = Math.min(32 + combo * 4, 72);
const comboText = this.add.text(GAME.WIDTH / 2, GAME.HEIGHT * 0.3, `${combo}x COMBO`, {
fontSize: `${size * PX}px`, fontFamily: 'Arial Black',
color: '#ffff00', stroke: '#000000', strokeThickness: 4 * PX,
}).setOrigin(0.5).setScale(1.8).setDepth(400);
this.tweens.add({
targets: comboText,
scale: 1,
y: comboText.y - 30 * PX,
alpha: 0,
duration: 700,
ease: 'Elastic.easeOut',
onComplete: () => comboText.destroy(),
});
}
```
### Hit Freeze Frame (Hit Stop)
60ms physics pause on impact. Makes hits feel powerful.
```typescript
private hitFreeze(): void {
this.physics.world.pause();
this.time.delayedCall(60, () => {
this.physics.world.resume();
});
}
```
### Screen-Wide Flash Burst
Colored flashes for different event types.
```typescript
private flashBurst(color: number, alpha = 0.4): void {
const overlay = this.add.rectangle(
GAME.WIDTH / 2, GAME.HEIGHT / 2,
GAME.WIDTH, GAME.HEIGHT, color, alpha,
).setDepth(900).setBlendMode(Phaser.BlendModes.ADD);
this.tweens.add({
targets: overlay,
alpha: 0,
duration: 150,
onComplete: () => overlay.destroy(),
});
}
```
### Color Cycling Background
Hue shifts over time for ambient visual energy.
```typescript
private bgHue = 0;
private bgGraphics: Phaser.GameObjects.Graphics;
private updateBackgroundHue(delta: number): void {
this.bgHue = (this.bgHue + delta * 0.02) % 360;
const color = Phaser.Display.Color.HSLToColor(this.bgHue / 360, 0.6, 0.15);
this.bgGraphics.clear();
this.bgGraphics.fillStyle(color.color, 1);
this.bgGraphics.fillRect(0, 0, GAME.WIDTH, GAME.HEIGHT);
}
```
### Pulsing Background on Score
Additive blend overlay that flashes on score events.
```typescript
private createScorePulse(): void {
this.scorePulse = this.add.rectangle(
GAME.WIDTH / 2, GAME.HEIGHT / 2,
GAME.WIDTH, GAME.HEIGHT, PALETTE.ACCENT, 0,
).setDepth(-50).setBlendMode(Phaser.BlendModes.ADD);
eventBus.on(Events.SCORE_CHANGED, () => {
this.scorePulse.setAlpha(0.15);
this.tweens.add({
targets: this.scorePulse,
alpha: 0,
duration: 300,
ease: 'Quad.easeOut',
});
});
}
```
### Entity Entrance Animations
Pop-in and slam-in patterns for spawning entities.
```typescript
// Pop-in: entity appears from scale 0
private popIn(target: Phaser.GameObjects.GameObject, delay = 0): void {
(target as any).setScale(0);
this.tweens.add({
targets: target,
scale: 1,
duration: 300,
delay,
ease: 'Back.easeOut',
});
}
// Slam-in: entity drops from above with bounce
private slamIn(target: Phaser.GameObjects.GameObject, targetY: number, delay = 0): void {
(target as any).y = -50 * PX;
this.tweens.add({
targets: target,
y: targetY,
duration: 350,
delay,
ease: 'Bounce.easeOut',
onComplete: () => {
this.cameras.main.shake(80, 0.006);
},
});
}
```
### Persistent Player Trail
Continuous particle spawn behind the player.
```typescript
private createPlayerTrail(): void {
this.playerTrail = this.add.particles(0, 0, 'particle', {
follow: this.player,
scale: { start: 0.6, end: 0 },
alpha: { start: 0.5, end: 0 },
speed: { min: 5, max: 15 },
lifespan: 400,
frequency: 30,
blendMode: 'ADD',
tint: PALETTE.ACCENT,
});
}
```
### Particle Ring Burst
Expanding ring for milestones. Higher visual impact than a random burst.
```typescript
private ringBurst(x: number, y: number, color: number, count = 24): void {
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count;
const dist = 80 * PX + Math.random() * 20 * PX;
const particle = this.add.circle(x, y, 4 * PX, color, 1);
this.tweens.add({
targets: particle,
x: x + Math.cos(angle) * dist,
y: y + Math.sin(angle) * dist,
alpha: 0,
scale: 0.3,
duration: 500,
ease: 'Quad.easeOut',
onComplete: () => particle.destroy(),
});
}
}
```
### Large Spectacle Burst
Higher count, multi-color variant for big moments (streaks, game over).
```typescript
private spectacleBurst(x: number, y: number, colors: number[], count = 30): void {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 80 + Math.random() * 120;
const color = colors[i % colors.length];
const size = (3 + Math.random() * 4) * PX;
const particle = this.add.circle(x, y, size, color, 1);
this.tweens.add({
targets: particle,
x: x + Math.cos(angle) * speed * PX,
y: y + Math.sin(angle) * speed * PX,
alpha: 0,
scale: 0.1,
duration: 500 + Math.random() * 300,
ease: 'Quad.easeOut',
onComplete: () => particle.destroy(),
});
}
}
```
### Streak Milestone Announcements
Full-screen text slam for streak milestones (5x, 10x, 25x).
```typescript
private announceStreak(streak: number): void {
const labels: Record<number, string> = { 5: 'ON FIRE!', 10: 'UNSTOPPABLE!', 25: 'LEGENDARY!' };
const label = labels[streak] || `${streak}x STREAK`;
const text = this.add.text(GAME.WIDTH / 2, GAME.HEIGHT / 2, label, {
fontSize: `${80 * PX}px`, fontFamily: 'Arial Black',
color: '#ffffff', stroke: '#000000', strokeThickness: 8 * PX,
}).setOrigin(0.5).setScale(3).setAlpha(0).setDepth(500);
this.tweens.add({
targets: text,
scale: 1,
alpha: 1,
duration: 300,
ease: 'Back.easeOut',
hold: 400,
yoyo: true,
onComplete: () => text.destroy(),
});
this.cameras.main.shake(200, 0.02);
this.spectacleBurst(GAME.WIDTH / 2, GAME.HEIGHT / 2,
[PALETTE.ACCENT, PALETTE.HIGHLIGHT, 0xffffff], 40);
}
```
## Drawing Game Entities with Graphics
### Simple Sprite-Like Entity
@@ -423,3 +696,34 @@ When building an asset-free game, verify:
- [ ] Score/text pop on change
- [ ] Scene transitions (fade, flash, or slide)
- [ ] Consistent shape language (all rounded, or all angular — pick one)
## Viral Clip Checklist
Every game is captured as a 13-second silent video clip for social media. Design for a viewer scrolling with sound off.
### First 3 seconds (before player input)
- [ ] **Screen flash** on scene start (white or accent color, 200-300ms)
- [ ] **Player entrance animation** — slam-in or pop-in, not a static spawn
- [ ] **Landing particles** — burst of 15-20 particles at spawn position
- [ ] **Ambient motion** — background particles, color cycling, or parallax drift active immediately
- [ ] **Optional flavor text** — "GO!", "DODGE!", etc. only when it naturally fits the game's theme
### Every action (seconds 3-13)
- [ ] **Particle burst** on every player action (minimum 12 particles per burst)
- [ ] **Floating text** on every score event (28px+ font, scale 1.8 start with Elastic.easeOut)
- [ ] **Screen shake** on every hit/score (minimum intensity 0.008)
- [ ] **Background pulse** on score change (additive blend flash, alpha 0.15)
- [ ] **Player trail** — continuous particle spawn behind the player
- [ ] **Combo text** visible at 2x combo and above (scaling with combo count)
- [ ] **Streak announcement** at milestones (5x, 10x, 25x — full-screen text slam)
- [ ] **Hit freeze** on destruction events (60ms physics pause)
### Intensity targets
- Particle bursts: 12-30 count per event (never fewer than 10)
- Screen shake range: 0.008 (light) to 0.025 (heavy)
- Floating text: 28px minimum, starting scale 1.8
- Flash overlays: alpha 0.3-0.5 for visibility in compressed video
- At least one visual effect firing every 0.5 seconds during active gameplay
+8
View File
@@ -15,6 +15,14 @@ export const Events = {
// Particles
PARTICLES_EMIT: 'particles:emit',
// Spectacle (visual effects hooks — emit during gameplay, design pass attaches effects)
SPECTACLE_ENTRANCE: 'spectacle:entrance',
SPECTACLE_ACTION: 'spectacle:action',
SPECTACLE_HIT: 'spectacle:hit',
SPECTACLE_COMBO: 'spectacle:combo',
SPECTACLE_STREAK: 'spectacle:streak',
SPECTACLE_NEAR_MISS: 'spectacle:near_miss',
// Audio (used by /add-audio)
AUDIO_INIT: 'audio:init',
MUSIC_MENU: 'music:menu',