بنقرة واحدة
boss-fight
How to build boss fight encounters in SIGNAL — combat loop, visual effects, jeopardy, and content authoring
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
How to build boss fight encounters in SIGNAL — combat loop, visual effects, jeopardy, and content authoring
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Use this skill whenever introducing a new Go programming concept that needs to be explained visually to beginners. Triggers when the user asks to explain, illustrate, animate, or teach any Go concept — including but not limited to: variables, functions, packages, imports, structs, interfaces, goroutines, channels, error handling, maps, slices, pointers, closures, or methods. Also triggers when the user says things like "add a new scene", "introduce X concept", "animate how X works", or "show the next concept". Always use this skill before writing any animation code — it defines the entire design system, analogy model, and scene structure that all Go animations must follow.
How to write and extend beginner mode content — pre-level concept briefings for new players
How to add new game content (chapters, bosses, story) to SIGNAL
Audit engine output patterns for common Go pitfalls — ensures every step has targeted feedback for Printf/Println, whitespace, format mismatches, and other gotchas
How to write and tune AI system prompts for Maya's 3-tier LLM backend
How to write zen rules for new SIGNAL levels — idiomatic Go detection, jolts, suggestions, testing
| name | boss-fight |
| description | How to build boss fight encounters in SIGNAL — combat loop, visual effects, jeopardy, and content authoring |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
Boss fights are full-screen combat encounters where the player fixes weapon system code while a boss entity attacks Maya. They're structurally different from regular chapters — no chat panel, no level timer, no step progression. The jeopardy comes entirely from boss attacks draining Maya's hearts.
src/
├── data/challenges/boss-XX.ts # Config: tabs, turns, test harnesses, XP
├── lib/game/boss-combat.ts # Pure combat state machine (no React)
├── hooks/useBossFight.ts # React hook: orchestrates combat + comms
├── components/boss/
│ ├── BossArena.tsx # Full-screen canvas + DOM overlay (main component)
│ ├── BossHUD.tsx # Top bar: hearts, boss HP, turn counter
│ └── BossComms.tsx # Maya comms feed (bottom-left)
├── lib/sprites/
│ ├── boss-painter.ts # Boss entity sprite (64x80 base, Canvas 2D)
│ ├── weapon-painter.ts # Weapon effects: targeting, ammo, charge, projectiles, blood
│ └── scene-painter.ts # paintBossFPS() — first-person server room background
Same as regular game logic — pure functions, React state, rendering:
| Layer | Files | Responsibility |
|---|---|---|
| Pure logic | boss-combat.ts | State machine, turn resolution, XP calculation |
| React state | useBossFight.ts | Orchestrate combat flow, Maya comms, persistence |
| Rendering | BossArena.tsx, weapon-painter.ts, boss-painter.ts | 60fps canvas + DOM overlays |
Phases flow linearly per turn:
ready → telegraph → player_window → hit/miss → telegraph → ... → victory/boss_retreats/gameover
BossCombatPhase)| Phase | What happens |
|---|---|
ready | Pre-fight. ENGAGE button or Maya intro briefing. |
telegraph | Boss announces next move. 3.5s duration. Red warning on canvas. |
player_window | Player codes. No timer — stays open until player executes. Boss attacks periodically. |
hit | Player's code was correct. Beam + explosion animation. 2.5s, then auto-advance. |
miss | Player's code was wrong or didn't compile. Spark effect. 2.5s, then auto-advance. |
victory | Boss HP reached 0. XP breakdown shown. |
boss_retreats | All turns exhausted, boss still alive. Partial win. |
gameover | Maya's hearts reached 0 from boss attacks. |
Boss fights have no countdown timer. The only jeopardy is periodic boss attacks that drain Maya's hearts. This creates organic urgency — the longer you take, the more shots Maya absorbs. Never add a timer to boss fights.
Boss attacks are the sole source of danger. They fire semi-randomly during player_window phase.
// Semi-randomized interval: 8-14 seconds between attacks
function nextAttackInterval(): number {
return 8000 + Math.random() * 6000;
}
// In the RAF draw loop:
if (state.phase === "player_window" && !anim.bossAttackActive) {
if (time - anim.lastBossAttackMs > anim.nextAttackIntervalMs) {
anim.bossAttackActive = true;
anim.bossAttackProgress = 0;
anim.bossAttackLanded = false;
// Randomize where projectile hits (different every time)
const target = randomAttackTarget();
anim.attackTargetX = target.x;
anim.attackTargetY = target.y;
}
}
// When projectile lands (progress > 0.85):
if (anim.bossAttackProgress > 0.85 && !anim.bossAttackLanded) {
anim.bossAttackLanded = true;
anim.heartsLost += 1; // Update in anim state for RAF
bossAttackHitRef.current(); // Update React state via ref
}
The canvas runs at 60fps via requestAnimationFrame. React state closures go stale. The solution:
// Store the React callback in a ref, update it every render
const bossAttackHitRef = useRef(actions.bossAttackHit);
bossAttackHitRef.current = actions.bossAttackHit;
// RAF calls the ref (always gets latest function)
bossAttackHitRef.current();
The same pattern applies to audio:
const audioRef = useRef(audio);
audioRef.current = audio;
// In RAF: audioRef.current.playSfx("boss-hit", 0.45);
AnimState tracks everything the canvas needs at 60fps — weapon effects, boss projectile progress, blood splatter count. This state is mutated directly (no setState) for performance:
interface AnimState {
weaponProgress: number; // Targeting/ammo/charge animation cycle
bossAttackActive: boolean; // Is a boss projectile in flight?
bossAttackProgress: number; // 0-1, projectile flight progress
lastBossAttackMs: number; // performance.now() timestamp
nextAttackIntervalMs: number; // Randomized delay until next attack
bossAttackLanded: boolean; // Prevent double-damage per projectile
attackTargetX: number; // Randomized target (0-1 normalized)
attackTargetY: number;
hitBeamProgress: number; // Player's weapon beam (-1 = inactive)
explosionProgress: number; // Boss explosion on hit
missProgress: number; // Miss sparks
shakeIntensity: number; // Screen shake (decays over time)
telegraphProgress: number; // Warning pulse
heartsLost: number; // Mirror of React state for RAF access
}
Key rule: heartsLost must be tracked in AnimState. React state is stale inside RAF closures. The RAF loop reads anim.heartsLost for blood rendering; bossAttackHitRef.current() updates React state for the HUD.
drawBloodSplatters(ctx, w, h, heartsLost, maxHearts) paints progressive camera damage:
splatSeed hash)Each boss attack picks a random screen position via randomAttackTarget():
function randomAttackTarget(): { x: number; y: number } {
const angle = Math.random() * Math.PI * 2;
const dist = 0.15 + Math.random() * 0.3;
return {
x: 0.5 + Math.cos(angle) * dist,
y: 0.5 + Math.sin(angle) * dist,
};
}
Never hardcode projectile targets — the whole point is unpredictability.
Maya communicates through the BossComms panel (bottom-left). Messages are typed sequentially:
Before combat begins, Maya types 4 lines explaining the situation. This gives the player context before chaos starts:
const MAYA_INTRO = [
"i just got in... i can see the lockmaster's core from here.",
"i've got the main weapon system online but the firmware's corrupted.",
"i need you to fix each module — aim, load, fire — while i keep us alive.",
"the lockmaster will shoot back. every hit takes me down. don't let me die in here.",
];
MAYA_TELEGRAPH[turnIndex] gives tactical context for each turn. Must reference the specific tab and task.
On hit/miss/malfunction/timeout/boss-attack, Maya reacts with in-character lines. Keep them short, lowercase, no exclamation marks.
src/data/challenges/boss-XX.ts)export const bossXX: Challenge = {
id: "boss-XX",
act: N,
chapter: N,
title: "BOSS_NAME",
location: "LOCATION",
concepts: ["concept1", "concept2"],
steps: [], // Boss fights don't use the step system
events: [],
timer: { timeLimitSeconds: 0, gameOverOnExpiry: false },
isBoss: true,
parTimeSeconds: 0,
};
export const bossXXConfig: BossFightConfig = {
bossName: "BOSS_NAME",
bossHP: 100,
defeatXP: 500, // XP for full defeat
survivalXP: 200, // XP if survived but didn't fully defeat
retreatThreshold: 30, // HP at which boss retreats
perHitXP: 50, // XP per successful turn
flawlessBonus: 250, // XP if Maya took no damage
speedBonus: 150, // XP if avg turn time < 5s
tabs: [...], // Weapon subsystem files (see below)
turns: [...], // Combat sequence (see below)
};
Each tab is a Go file the player edits. 2-4 tabs is ideal. Each needs:
{
id: "aim", // Unique tab ID
filename: "aim.go", // Display filename
label: "AIM", // Tab button text
functionSignature: "func Aim(sector int) (int, int)", // Status bar hint
starterCode: `func Aim(sector int) (int, int) {
\t// Comments explaining what to do
\treturn 0, 0
}`,
}
Starter code rules:
\t for indentation (matches editor tab behavior)package main or import — the build system adds thoseEach turn targets one tab and has a test harness:
{
id: 1, // 1-based turn number
telegraph: "BOSS activating sector 3 lock...", // Boss announcement
hint: "aim at sector 3 — check the grid", // Player guidance
activeTab: "aim", // Which tab auto-focuses
windowSeconds: 25, // Not used as timer, but kept for pacing reference
damage: 20, // HP damage on hit
testHarness: `func main() {
\tx, y := Aim(3)
\tfmt.Println(x, y)
}`,
expectedOutput: "384 160", // Exact match (trimmed)
bossCharge: "charge-sector", // Visual effect key (for boss-painter)
hitEffect: "node-explode", // Visual effect on hit
missEffect: "emp-hit", // Visual effect on miss
}
Test harness rules:
func main() that calls the player's functionscheckOutput() — exact string match after trimfmt.Println for output (matches Go Playground behavior)buildSource() and sent to Go PlaygroundGood boss fights escalate:
| Turn | Pattern |
|---|---|
| 1-2 | Single-tab, simple task. Teach the mechanic. |
| 3 | Same tab, harder variant. Time pressure from boss attacks. |
| 4 | Cross-tab integration. Wire multiple functions together. |
| 5 | Rule change. Modify earlier code (e.g., grid shifts). |
| 6 | Everything together. The kill shot. |
In useBossFight.ts, add:
MAYA_INTRO — 3-4 lines of context before the fightMAYA_TELEGRAPH[turnIndex] — tactical callout per turn (reference specific tabs/tasks)MAYA_HIT / MAYA_MISS / MAYA_TIMEOUT / MAYA_MALFUNCTION — reaction linesAll Maya lines: lowercase, no exclamation marks, short, in-character.
Add to CHAPTERS array:
{
challenge: bossXX,
bossFightConfig: bossXXConfig,
introScenes: BOSS_XX_INTRO_SCENES,
completeScenes: BOSS_XX_COMPLETE_SCENES,
introTitle: "BOSS FIGHT",
introSubtitle: "BOSS_NAME",
completeTitle: "BOSS DEFEATED",
completeSubtitle: "description",
ctaLabel: "CONTINUE",
}
And add camera config:
"boss-XX": { scene: "boss-arena", animation: "hack", rushAnimation: "hack" },
All weapon effects are in weapon-painter.ts — pure Canvas 2D rendering functions:
| Function | When | What |
|---|---|---|
drawTargetingGrid | activeTab === "aim" | 3x3 sector grid with crosshair on active sector |
drawAmmoRack | activeTab === "load" | Vertical ammo slots that fill based on progress |
drawChargeBar | activeTab === "fire" | Horizontal charge meter (0-100%) |
drawFiringBeam | On hit | Beam from weapon position to boss |
drawExplosion | On hit (delayed) | Radial explosion at boss position |
drawBossProjectile | Boss attacking | Red energy bolt flying toward randomized target |
drawImpactFlash | Projectile lands | Screen-wide red flash |
drawMissEffect | On miss | Sparks from weapon position |
drawBloodSplatters | heartsLost > 0 | Progressive blood on camera lens |
drawTelegraphWarning | Telegraph phase | Pulsing red border + "INCOMING" text |
drawWeaponStatus | Player window | Bottom-left AIM/LOAD/FIRE indicator |
getShakeOffset | Any shake | Screen displacement for impact feel |
boss-painter.ts)The boss is a wall-mounted security mainframe (not humanoid):
Boss fights use these audio cues:
| Event | SFX | Volume |
|---|---|---|
| Fight starts | alert-beep | 0.4 |
| Telegraph | warning-beep + weapon-charge | 0.35 / 0.25 |
| Player window opens | target-lock | 0.3 |
| Player hits | laser-fire → explosion-small → hit-confirm | 0.5 / 0.4 / 0.3 |
| Player misses | dread-sting → boss-hit | 0.45 / 0.35 |
| Boss attacks Maya | boss-hit + explosion-small | 0.45 / 0.3 |
| Boss attack dodge | grunt-dodge-1 or grunt-dodge-2 | 0.5 |
| Boss attack hit | grunt-hit-1/2/3 (80ms delay) | 0.55 |
| Victory | handshake-confirm | 0.6 |
| Game over | captured-impact → game-over-slam | 0.6 / 0.5 |
Loops during combat:
boss-loop at 0.45 (intro), ramped to 0.55 (combat), 0.65 (low HP), 0.70 (low hearts)facility-hum at 0.15tension-drone at 0.12heartbeat-fast at 0.25-0.3 when boss HP low or Maya at 1 heartMusic loops must start in the ENGAGE button's click handler — synchronously, as the very first calls. This ensures the browser recognises el.play() as user-gesture-initiated (autoplay policy). Use setLoopVolume() to ramp volume on phase transitions — never stopLoop+startLoop (that kills/recreates the element).
onClick={() => {
// Music FIRST — synchronous in click handler for autoplay policy
audio.startLoop("boss-loop", 0.45);
audio.startLoop("facility-hum", 0.15);
audio.startLoop("tension-drone", 0.12);
audio.playSfx("terminal-beep", 0); // unlock Web Audio context
setShowIntro(true);
actions.startFight();
audio.preload([...]); // fire-and-forget
}}
Boss fights reuse the regular CodeEditor component from src/components/game/CodeEditor. It's wrapped in a tab bar for switching between weapon files:
<div className="flex-1 min-h-0">
<CodeEditor
code={state.tabCode[selectedTab] ?? ""}
onCodeChange={(code) => actions.setTabCode(selectedTab, code)}
onSubmit={actions.execute}
busy={state.busy}
attempts={0} inRush={false} baseXP={0} rushBonus={0}
vimEnabled={vimEnabled}
/>
</div>
Never build a custom editor for boss fights. The proven CodeEditor handles syntax highlighting, vim mode, tab indentation, and paste prevention. Just wrap it with a tab bar.
Boss fights have extensive camera motion and visual feedback:
When Maya dodges a boss attack, the camera lurches using damped spring oscillation:
When Maya takes a hit: 45px backward slam, 20px lateral stagger, ±2.3° tilt, 1.2s decay.
Two layered sine waves for organic drift (always active):
const idleX = Math.sin(t * 0.7) * 6 + Math.sin(t * 1.3) * 3;
const idleY = Math.sin(t * 0.9 + 1.0) * 3 + Math.cos(t * 1.6) * 1.5;
drawScreenShatter() — persistent glass cracks from each hit impact point. Severity scales with total hits.
anim.* or *Ref.current — never state.* inside the draw loop.const xRef = useRef(x); xRef.current = x; pattern.async in startLoop. startLoop must be synchronous — async breaks the user gesture call stack, causing browsers to reject el.play() under autoplay policy. Use .catch() instead of await.useAudio return in effect deps. useAudio() returns a memoized object (via useMemo). If you add [audio] as a useEffect dependency, verify audio is stable — an unstable ref triggers cleanup on every render, killing all loops. For unmount-only cleanup, use [].stopLoop+startLoop to change volume. This kills and recreates the Audio element. Use setLoopVolume() to ramp an existing loop — avoids autoplay issues and audible gaps.CodeEditor. It works. Custom editors break.Date.now() in RAF. RAF provides performance.now()-scale timestamps. Use performance.now() for RAF-compatible timing.ctx.globalAlpha = 1. Always reset after transparent draws in Canvas 2D.bossAttackLanded guard. Without this flag, a single projectile triggers multiple damage events per frame.el.src = "" on Audio elements. Firefox throws NS_ERROR_DOM_INVALID_STATE_ERR. Just call el.pause() and delete from the map — no src manipulation.Calculated by calculateBossXP() in boss-combat.ts:
| Component | Formula |
|---|---|
| Hit XP | hits * config.perHitXP |
| Defeat bonus | config.defeatXP if boss HP = 0, else config.survivalXP |
| Flawless bonus | config.flawlessBonus if heartsLost === 0 |
| Speed bonus | config.speedBonus if avg hit time < 5s |
Typical values for a 6-turn boss: 500 defeat + 300 hits + 250 flawless + 150 speed = 1200 max XP.