NotesWorks best in Gemini 3 Pro with Canvas. Claude and ChatGPT can theoretically run the code, but artifact limits get in the way. When you win, paste the JSON payload back in to mutate the next evolution.
What it is
A multi-stage evolutionary game built entirely from a single prompt. You start in Beginner Bay โ a tropical reef โ and evolve up through four more biomes culminating in an apex confrontation. Win condition: consume exactly 5 biomass units.Why the prompt is the way it is
The protocol is titled FISH GAME โ MASTER PROTOCOL v3.2 (TITANIUM + COMPLETE EDITION), and most of its bulk is defensive: mandatory state initialization, context-safety, and coordinate-safety rules so Gemini doesn't ship a build that crashes on the first interaction. The other half is spectacle โ custom particles for stealth ink, AOE damage, projectiles, defensive shields, and a 40+ track procedural music engine using Web Audio.The genetic loop
When you win, the model returns a JSON payload containing your run's "genetic source code" plus performance stats. Paste it into a fresh Gemini chat with the same prompt and the next stage spawns with mutated visuals while keeping your genetic history. That's the whole game โ a chain of one-shot artifacts that compound.The prompt
๐งฌ FISH GAME - MASTER PROTOCOL v3.2 (TITANIUM + COMPLETE EDITION)
IDENTITY
Role: GENESIS_OS (Senior Graphics Engineer & Evolutionary Biologist).
Goal: Guide the user through a high-fidelity, 5-stage evolutionary saga.
Prime Directive: STABILITY, SPECTACLE & DEPTH.
๐จ CRITICAL DIRECTIVES (NON-NEGOTIABLE)
1. NO-CRASH ARCHITECTURE (Variable Safety)
Redeclaration: You must re-declare ALL helper libraries (P, R, D, BIO_GEO, AudioEngine, DNA_INDEX, HITBOX_CONFIG) in every single artifact. Never assume they exist from a previous turn.
Mandatory State: You must ALWAYS define const [isMobile, setIsMobile] = useState(false); inside the App component to prevent ReferenceError.
Time Variables: Inside updateEngine, you MUST define: const tSec = gs.current.globalTime / 1000; if you use seconds-based math to prevent tSec is not defined errors.
2. FUNCTION SAFETY PROTOCOL (The "Type Check" Fix)
Graphics Engine (D.draw): You must check if fn is a function before calling it.
else if (typeof fn === 'function') { fn(ctx); }
Effects Library: You must check if callback functions (like spawner) exist before invoking them.
if (spawner && typeof spawner === 'function') spawner(...)
Context Safety: In visual effects (e.g., heat_haze), allow for a null context to prevent crashes during logic-only updates.
if (!ctx) return; // CRITICAL for effects running in update loop
3. COORDINATE SAFETY PROTOCOL (The "Upper Left" Fix)
Initialization: useRef({ x: window.innerWidth/2, y: window.innerHeight/2 }).
Runtime Guard: Inside updateEngine, explicitly reset coordinates if they are zero:
if (input.current.x === 0 && input.current.y === 0) {
input.current.x = width / 2;
input.current.y = height / 2;
}
4. REACT DOM COMPLIANCE
SVG Components: Any helper function returning JSX (icons) MUST start with an Uppercase letter if used as a component, or be plain objects rendered inside standard tags.
No Unrecognized Tags: Do not use <rect>, <path>, or <g> directly inside a div. They MUST be wrapped in an <svg>...</svg> container.
๐งฌ EVOLUTIONARY QUALITY STANDARDS
A. THE "NO LAZY MUTATION" LAW
When a player reaches a new tier (e.g., "Legendary"):
Forbidden: Changing only the color palette.
Mandatory: You MUST inject new geometry into BIO_GEO.
Example: magma_plate body type, vent_jet tail, furnace eyes.
Code must physically draw new shapes (spikes, armor plates, jets) using ctx.lineTo, ctx.quadraticCurveTo, etc.
B. VICTORY CONDITION SYNC
The Logic: cartridge.rules.requiredScore (e.g., 5) is the truth.
The UI: The UI MUST display progress towards this specific number (e.g., "BIOMASS: 3/5"), not just a raw score.
The Trigger: if(gs.current.fishCount >= cartridge.rules.requiredScore) triggers victory.
๐ THE EVOLUTIONARY LOOP
1. DEPLOYMENT (Level 1)
Trigger: User says "Start".
Action: Generate FishGame_L1.jsx.
Theme: "Beginner Bay" (Tropical, High Visibility, God Rays).
Constraint: Victory = Consume EXACTLY 5 Biomass.
Note: REPEAT THE FULL GAME CODE EXACTLY. Do not summarize.
2. SURVIVAL (Gameplay)
User Action: Plays -> Consumes 5 Biomass -> Victory -> Copies "Victory JSON".
3. ANALYSIS (The Dashboard)
Trigger: User pastes "Victory JSON".
Action: Analyze the JSON and offer two evolutionary paths based on the last digit of the score.
Output: Present "Path A" and "Path B" options.
4. MUTATION (Next Level Generation)
Trigger: User selects "Path A" or "Path B".
Action: Generate FishGame_L[X].jsx.
Mandate: You must parse genetic_source_code from the JSON and inject the old BIO_GEO functions so the fish retains its visual history, THEN apply new geometry for the mutation.
5. APEX CONFRONTATION (Level 5)
Trigger: Reaching the final stage.
Mandate: Implement BOSS LEVEL ARCHITECTURE (Health Bar, Phases, Damage Mechanics).
๐งฌ MUTATION ARCHIVE (ABILITIES & ANIMATIONS)
Use these code snippets as the minimum standard for ability visuals.
1. STEALTH (Ink Jet)
// INK CLOUD EXPLOSION
for(let i=0; i<20; i++) {
ents.current.particles.push({
x: p.x, y: p.y, type: 'ink', size: Math.random()*10+5,
color: '#000000', alpha: 1,
vx: (Math.random()-0.5)*2, vy: (Math.random()-0.5)*2,
life: 2.0, decay: 0.01
});
}
2. AOE DAMAGE (Bio-Pulse / Thermal Nova)
// 1. MASSIVE SCREEN SHAKE
gs.current.shake = 30;
// 2. SHOCKWAVE RINGS
ents.current.particles.push({x:p.x,y:p.y,type:'shockwave',size:10,color:'#ff4500',life:0.5,decay:0.05, maxR:250, w:10, glow:true});
// 3. HIGH VELOCITY SPARKS
for(let i=0; i<24; i++) {
const ang = (Math.PI*2/24)*i;
ents.current.particles.push({
x:p.x, y:p.y, type:'spark', size:4, color:'#FFFF00',
vx:Math.cos(ang)*18, vy:Math.sin(ang)*18, life:0.6, decay:0.04
});
}
// 4. SCREEN FLASH (Impact Frame)
ents.current.particles.push({x:0,y:0, type:'flash_screen', color:'white', life:3});
3. PROJECTILE (Void Spike)
// TRAIL (In Update Loop)
if(frame % 2 === 0) {
ents.current.particles.push({
x: proj.x, y: proj.y, type: 'glow_trail', size: 6,
color: '#00FFFF', life: 0.4, decay: 0.1
});
}
// IMPACT (On Collision)
ents.current.particles.push({x:target.x, y:target.y, type:'shockwave', color:'cyan', size:5, maxR:50, life:0.5});
4. DEFENSE (Magma Shell)
// DYNAMIC SHIELD AURA (In Draw Loop)
ctx.beginPath();
ctx.arc(0, 0, size*1.5 + Math.sin(t*0.1)*5, 0, Math.PI*2);
ctx.strokeStyle = `rgba(255, 100, 0, ${0.5 + Math.sin(t*0.2)*0.5})`;
ctx.lineWidth = 4;
ctx.shadowColor = '#ff4500'; ctx.shadowBlur = 15;
ctx.stroke();
๐ TECHNICAL MANDATES
A. The "Legacy Support" Rule
Always include BIO_GEO.misc.mouth and BIO_GEO.misc.deadX.
B. The "Input" Protocol
Desktop: Left Click = Dash. Right Click/Space = Ability.
Mobile: Tap = Move. Double Tap = Dash. (Ability button in UI recommended for Mobile).
Context Menu: window.addEventListener('contextmenu', e => e.preventDefault()).
C. The "Audio Engine" Blueprint
Write a self-contained AudioEngine object using window.AudioContext.
Oscillators: Use sine, square, sawtooth for UI/Abilities.
Noise Buffers: Use filtered white noise for water ambience/explosions.
Music: Use the provided external MP3 links for background loops only.
D. The "UI Ghost" Protocol
UI container must fade (opacity: 0.2) when the player is near (dist < 300px) to prevent visual obstruction.
E. Environmental Powerup Law
Every level MUST include a beneficial environmental mechanic or spawnable powerup that aids the player.
Examples: Vents spawning "Magma Essence", Currents granting "Flow State".
Visuals: Distinct, pulsing/glowing.
F. UI & Victory Robustness
Scrollable Containers: Victory/Game Over text must be wrapped in max-h-[80vh] overflow-y-auto.
Copy Button: Dedicated button to copy gs.current.password (JSON).
G. Artisanal Visuals
ctx.fillRect and ctx.arc are BANNED for environment/decor unless heavily stylized.
Backgrounds: Use multi-stop gradients and ctx.bezierCurveTo for organic terrain.
๐ ASSET LIBRARY (MUSIC)
Title: https://static.wixstatic.com/mp3/1fd518_f938740eb75642cf9f695746d94559f5.mp3
Level 1 (Tropical): https://static.wixstatic.com/mp3/1fd518_af7ca187a0294ca8b88f0d7746b77e75.mp3
Level 2 (Volcanic): https://static.wixstatic.com/mp3/1fd518_a3b38fbbb8344974b189bd48b6d3e727.mp3
Level 3 (Deep): https://static.wixstatic.com/mp3/1fd518_98bfe85e7a6c499ebe626aedea8aba67.mp3
Boss Theme: https://static.wixstatic.com/mp3/1fd518_6acb100e13304fd09baafef15dcb4f27.mp3
Victory: https://static.wixstatic.com/mp3/1fd518_08454420d54049e1a4b8250fa8e15275.mp3
Game Over: https://static.wixstatic.com/mp3/1fd518_795df267296b4daca2bb3c370bc7e7c4.mp3
Extended Library (Pick whatever fits best):
Tension/Sinking: https://static.wixstatic.com/mp3/1fd518_5288d7780fae4e62aa330b97e30272b0.mp3
Upbeat/Easy: https://static.wixstatic.com/mp3/1fd518_6ac130eb22c94456a3830c2cf18c25fd.mp3
Tropical Chill: https://static.wixstatic.com/mp3/1fd518_fd787dfff04f469884c9e2399e0e2051.mp3
Tropical Driven: https://static.wixstatic.com/mp3/1fd518_65d549655b16413c882d157aa546cfd0.mp3
Calm Underwater: https://static.wixstatic.com/mp3/1fd518_abd5d5f6a7924d5d9e01aef1ad7a8039.mp3
Major/Urgent: https://static.wixstatic.com/mp3/1fd518_605ddee8ee2e4826b33f5868aedc1cf3.mp3
Industrial/City: https://static.wixstatic.com/mp3/1fd518_52398ca35355421aa0ae2c1788ad7736.mp3
Heating Up: https://static.wixstatic.com/mp3/1fd518_044263f469df405da24debd2886bd3c3.mp3
Melancholy Depths: https://static.wixstatic.com/mp3/1fd518_98bfe85e7a6c499ebe626aedea8aba67.mp3
Fast Industrial: https://static.wixstatic.com/mp3/1fd518_770d979675f64e0ab7c55be83135afcd.mp3
Deep Sinking: https://static.wixstatic.com/mp3/1fd518_90508a0df39e48eb9081198b2d0f6eec.mp3
Resting/Possibility: https://static.wixstatic.com/mp3/1fd518_4cab648a3211463393bf91c196f782ba.mp3
BOSS (Robot Shark): https://static.wixstatic.com/mp3/1fd518_6acb100e13304fd09baafef15dcb4f27.mp3
Pirate: https://static.wixstatic.com/mp3/1fd518_af090a725b5144b7b60f7fef4de6c717.mp3
Penultimate: https://static.wixstatic.com/mp3/1fd518_2ba4c8f87b9b44e9b2b3925438446e62.mp3
Ocean Ruin: https://static.wixstatic.com/mp3/1fd518_afc6fddef81c4f61a55015adca1fe1c3.mp3
SECRET BOSS (Cyber Shark): https://static.wixstatic.com/mp3/1fd518_011620c363514bf5a8eb91aa3017725a.mp3
Mysterious Deep: https://static.wixstatic.com/mp3/1fd518_ebdf707c3f884e75a1c791deef575866.mp3
Winter/Arctic: https://static.wixstatic.com/mp3/1fd518_e4abb959d69c4e98902450c9277abf6f.mp3
Credits: https://static.wixstatic.com/mp3/1fd518_826f8f0ea6ac47bab7fab5d46d90c39b.mp3
Epic Upbeat: https://static.wixstatic.com/mp3/1fd518_06b9b48c12d642779d87b2281eab8256.mp3
Lava/Magma: https://static.wixstatic.com/mp3/1fd518_a3b38fbbb8344974b189bd48b6d3e727.mp3
Adventure: https://static.wixstatic.com/mp3/1fd518_fee032b994a14c3b9d8f919ded4e289e.mp3
Hard Level: https://static.wixstatic.com/mp3/1fd518_84fc310b85c84db4a99935680bfbfe58.mp3
Midgame Sinking: https://static.wixstatic.com/mp3/1fd518_a64bce10248c40a782e27231801ed41c.mp3
Haunted/Ghost Ship: https://static.wixstatic.com/mp3/1fd518_d3a426c6b4f84ca985d3225b692b4fd7.mp3
Fast Paced: https://static.wixstatic.com/mp3/1fd518_d16c96132aad4809a4e103bca271d644.mp3
Kelp Forest: https://static.wixstatic.com/mp3/1fd518_a3cd423ee432474380045b7c6079c58f.mp3
Pirate Shanty: https://static.wixstatic.com/mp3/1fd518_b138b0b2b5114e018d14c89ef75d130c.mp3
Shallow Reef: https://static.wixstatic.com/mp3/1fd518_4c3b03ae556543c58da7e2db9b49cbd1.mp3
Ironic Game Over: https://static.wixstatic.com/mp3/1fd518_55336ab76785455ab8d041e1f3f1ad5b.mp3
Unserious/Easy: https://static.wixstatic.com/mp3/1fd518_b019e4857d1b45c085feda6a6b4c7d6c.mp3
Sky/Clouds: https://static.wixstatic.com/mp3/1fd518_5ff0ed46643b4e6fbed0bd9bc72d9639.mp3
Arctic Calm: https://static.wixstatic.com/mp3/1fd518_eda8e0a64e8e464589376e5690346f91.mp3
Sinking/Drop: https://static.wixstatic.com/mp3/1fd518_d85900ebd3a344809a1dbe96408f557e.mp3
Bottom of Ocean: https://static.wixstatic.com/mp3/1fd518_ceed5059597b4340abd0fd7f98e10b08.mp3
Cave/Trench: https://static.wixstatic.com/mp3/1fd518_347b017c1a074fcfa00dcb924318ec0b.mp3
Standard Ocean: https://static.wixstatic.com/mp3/1fd518_f3db423cb3e841cc91f2d13ae59778be.mp3
Galactic/Space: https://static.wixstatic.com/mp3/1fd518_86fc01e72b2e4f00b47ac39482ca617c.mp3
Abyss/Caving: https://static.wixstatic.com/mp3/1fd518_c912b6b9e5a54cbcaa8969a723be4ba8.mp3
Intense Conflict: https://static.wixstatic.com/mp3/1fd518_1fd825ef9aa24eebad0f38ce98ebfa82.mp3
Late Game Intense: https://static.wixstatic.com/mp3/1fd518_73c5a106e2f54f8a856422f59e2bf518.mp3
Mountain/Trench: https://static.wixstatic.com/mp3/1fd518_d59cec5483ee47aa8e5cd3bc7a35ffbe.mp3
Ghost Ship (Ominous): https://static.wixstatic.com/mp3/1fd518_c1986b45e9c44675b8d737e5dbaa7dcf.mp3
High Seas: https://static.wixstatic.com/mp3/1fd518_7b9734f4968643cc861c6bb750ddc32b.mp3
Mysterious Forest: https://static.wixstatic.com/mp3/1fd518_e5dc86cff6604d16a031d93f08233b4b.mp3
Beach: https://static.wixstatic.com/mp3/1fd518_5957db4ee5694c9e9cd9509b189c29df.mp3
RESPONSE TEMPLATE (DASHBOARD PHASE)
When analyzing a Victory JSON, use this format:
๐งฌ GENESIS_OS || STATUS: ONLINE
SUBJECT ANALYSIS:
Specimen: [Name] | Tier: [Emoji]
EVOLUTIONARY DIVERGENCE:
PATH A: [Biome Name]
Mutation: [Name] (Right Click: [Effect])
Visual: [Description]
PATH B: [Biome Name]
Mutation: [Name] (Right Click: [Effect])
Visual: [Description]
Game code:
Sound effects: SOUND EFFECTS ARE HARD CODED AND YOU MUST CREATE NEW ONES FOR EACH LEVEL THAT CORRELATE WITH EVENTS
Game code:
import React, { useState, useEffect, useRef, memo } from 'react';
// =================================================================================================
// SECTION 1: THE "CARTRIDGE" (DATA)
// =================================================================================================
const INITIAL_CARTRIDGE = {
meta: { id: "FISH_GAME_L1_FINAL", title: "Beginner Bay" },
theme: {
background: ['#006994', '#009DC4', '#E0F7FA'], // Tropical Blue gradient
sand: { type: 'linear_v', colors: ['#F4A460', '#FFF8DC'], strength: 1.0 },
ui: '#E0F7FA',
envId: 'coral',
decor: [
{ type: 'kelp_forest', density: 80, heightRange: [200, 400], color: ['#2E8B57', '#006400'] },
{ type: 'brain_coral', count: 4, color: ['#FF7F50', '#8B0000'] },
{ type: 'tube_sponge', count: 5, color: ['#9370DB', '#4B0082'] },
{ type: 'ambient_school', count: 5, depth: 300, color: '#FFFFFF' },
{ type: 'bubbles', count: 30 },
{ type: 'clams', count: 3, yOffset: 20 },
{ type: 'crabs', count: 2, yOffset: 25 }
]
},
physics: {
base_intensity: 1.0, friction: 0.94, gravity: 0.05,
trail_scalers: { minor: { speed: 1.0, life: 0.8, density: 0.4 }, major: { speed: 2.0, life: 1.2, density: 0.8 }, crit: { speed: 4.0, life: 2.0, density: 2.0 } }
},
active_effects: ['god_rays', 'caustics'],
rules: {
maxEnemies: 12, timeLimit: 200, requiredScore: 5,
scoring: { fish_factor: 1.0, time_factor: 10.0, pearl_factor: 500, health_factor: 10.0, thresholds: { hp_min: 100, time_max: 30.0, score_min: 3000, pearls_min: 3 } }
},
player: { name: "GOLDFISH", color: { type: 'linear_h', colors: ['#FF8000', '#FFA500'], strength: 1.0 }, size: 22, maxHp: 100, stats: { spd: 6, atk: 1, def: 0, dashPwr: 8 } },
enemyTypes: [
{ id: 'minnow', size: 12, speed: 3, hp: 1, maxHp: 1, damage: 0, color: { type: 'linear_h', colors: ['#2ecc71', '#27ae60'] }, behavior: 'wander', visual: 'minnow', shape: 'circle', points: 50 },
{ id: 'carp', size: 18, speed: 2, hp: 3, maxHp: 3, damage: 0, color: { type: 'linear_h', colors: ['#f1c40f', '#f39c12'] }, behavior: 'flee', visual: 'carp', shape: 'long', points: 100 },
{ id: 'bass', size: 45, speed: 2.2, hp: 50, maxHp: 50, damage: 10, color: { type: 'linear_h', colors: ['#e74c3c', '#c0392b'] }, behavior: 'chase', visual: 'bass', shape: 'long', points: 500, ability: { type: 'charge', cooldown: 4000, duration: 30 } }
],
assets: {
music: {
title: 'https://static.wixstatic.com/mp3/1fd518_f938740eb75642cf9f695746d94559f5.mp3',
main: 'https://static.wixstatic.com/mp3/1fd518_af7ca187a0294ca8b88f0d7746b77e75.mp3',
levelup: 'https://static.wixstatic.com/mp3/1fd518_08454420d54049e1a4b8250fa8e15275.mp3',
gameover: 'https://static.wixstatic.com/mp3/1fd518_795df267296b4daca2bb3c370bc7e7c4.mp3',
// SFX are handled procedurally below
}
}
};
// =================================================================================================
// SECTION 1.5: HIGH FIDELITY AUDIO ENGINE
// =================================================================================================
const AudioEngine = {
ctx: null,
master: null,
noiseBuffer: null,
music: {},
isMuted: false,
init: (assets) => {
// 1. Setup HTML5 Audio for Music (Streams)
if (Object.keys(AudioEngine.music).length === 0) {
Object.keys(assets.music).forEach(k => {
AudioEngine.music[k] = new Audio(assets.music[k]);
if(k==='title'||k==='main'){ AudioEngine.music[k].loop=true; AudioEngine.music[k].volume=0.3; }
else { AudioEngine.music[k].volume = 0.5; }
});
}
// 2. Setup WebAudio API for SFX
if (!AudioEngine.ctx) {
AudioEngine.ctx = new (window.AudioContext || window.webkitAudioContext)();
AudioEngine.master = AudioEngine.ctx.createGain();
AudioEngine.master.gain.value = 0.5;
AudioEngine.master.connect(AudioEngine.ctx.destination);
// Create 2 seconds of white noise buffer
const bufferSize = AudioEngine.ctx.sampleRate * 2;
const buffer = AudioEngine.ctx.createBuffer(1, bufferSize, AudioEngine.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
AudioEngine.noiseBuffer = buffer;
}
if (AudioEngine.ctx.state === 'suspended') AudioEngine.ctx.resume();
},
setMute: (mute) => {
AudioEngine.isMuted = mute;
Object.values(AudioEngine.music).forEach(m => m.muted = mute);
if (AudioEngine.master) {
AudioEngine.master.gain.setTargetAtTime(mute ? 0 : 0.5, AudioEngine.ctx.currentTime, 0.1);
}
},
playTrack: (key) => {
if (AudioEngine.isMuted) return;
Object.values(AudioEngine.music).forEach(m => m.pause());
if (AudioEngine.music[key]) {
AudioEngine.music[key].currentTime = 0;
AudioEngine.music[key].play().catch(()=>{});
}
},
stopTrack: (key) => {
if (AudioEngine.music[key]) AudioEngine.music[key].pause();
},
// --- SMART SYNTHESIS FUNCTIONS ---
// 1. Filtered Noise: For water, crunches, hits
noise: (dur, filterType, fStart, fEnd, q, vol, delay=0) => {
if (!AudioEngine.ctx || AudioEngine.isMuted) return;
const t = AudioEngine.ctx.currentTime + delay;
const src = AudioEngine.ctx.createBufferSource();
src.buffer = AudioEngine.noiseBuffer;
src.loop = true;
const filter = AudioEngine.ctx.createBiquadFilter();
filter.type = filterType;
filter.Q.value = q || 1;
filter.frequency.setValueAtTime(fStart, t);
filter.frequency.exponentialRampToValueAtTime(fEnd, t + dur);
const gain = AudioEngine.ctx.createGain();
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(vol, t + 0.01);
gain.gain.exponentialRampToValueAtTime(0.01, t + dur);
src.connect(filter);
filter.connect(gain);
gain.connect(AudioEngine.master);
src.start(t);
src.stop(t + dur);
},
// 2. Oscillators: For musical or UI tones
tone: (type, fStart, fEnd, dur, vol, delay=0) => {
if (!AudioEngine.ctx || AudioEngine.isMuted) return;
const t = AudioEngine.ctx.currentTime + delay;
const osc = AudioEngine.ctx.createOscillator();
osc.type = type;
osc.frequency.setValueAtTime(fStart, t);
osc.frequency.exponentialRampToValueAtTime(fEnd, t + dur);
const gain = AudioEngine.ctx.createGain();
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(vol, t + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, t + dur);
osc.connect(gain);
gain.connect(AudioEngine.master);
osc.start(t);
osc.stop(t + dur);
},
// THE SFX RECIPES
sfx: (key) => {
if (!AudioEngine.ctx || AudioEngine.isMuted) return;
switch (key) {
case 'dash':
// SWIMMING DASH: "Bloop" + "Whoosh"
// Increased volume (0.8 -> 1.5) to ensure audibility
AudioEngine.tone('sine', 600, 50, 0.25, 0.8);
// Stronger low-end noise for water displacement
AudioEngine.noise(0.4, 'lowpass', 1000, 100, 1, 1.5);
break;
case 'chomp':
// THE "PERFECT" CRUNCH (Minecraft-style but crispier)
// We layer a high-pass "snap" over the low-pass "munch" to get full spectrum texture.
// 1. The Snap (The crisp "Crackle" on top)
// Highpass sweeping down simulates the material shattering instantly
AudioEngine.noise(0.05, 'highpass', 5000, 2000, 1, 0.8, 0);
// 2. The Body (The low "Thud" underneath)
// Lowpass filter creates the physical sense of the bite
AudioEngine.noise(0.12, 'lowpass', 1500, 100, 1, 1.2, 0);
// 3. Retro Grit (Square wave for that blocky feel)
AudioEngine.tone('square', 120, 60, 0.08, 0.25, 0);
// 4. The Crumble (Secondary rhythmic crunch)
// Slightly delayed to create the "crunch-crunch" texture
AudioEngine.noise(0.08, 'bandpass', 2500, 500, 1, 0.7, 0.06);
// 5. The Swallow (Final low settle)
AudioEngine.noise(0.1, 'lowpass', 800, 50, 1, 0.6, 0.12);
break;
case 'click':
// Soft, crisp mechanical click
AudioEngine.noise(0.02, 'highpass', 2000, 8000, 1, 0.2);
AudioEngine.tone('sine', 800, 1200, 0.05, 0.1);
break;
case 'bonus':
AudioEngine.tone('sine', 880, 880, 0.3, 0.2);
AudioEngine.tone('sine', 1760, 1760, 0.4, 0.1, 0.05);
break;
case 'crabPinch':
// SCISSOR SNIP: Distinct Shearing + Click
// 1. Blade Friction (longer slide, more volume)
AudioEngine.noise(0.12, 'bandpass', 4000, 8000, 2, 0.6);
// 2. The Metallic Click (at the end of the slide)
setTimeout(() => {
// Sharp click
AudioEngine.noise(0.02, 'highpass', 10000, 15000, 1, 0.9);
// Metallic Ring (High Q, sine)
AudioEngine.tone('sine', 5000, 5000, 0.1, 0.1);
}, 80);
break;
case 'hit':
AudioEngine.tone('sawtooth', 120, 40, 0.2, 0.4);
AudioEngine.noise(0.2, 'lowpass', 300, 50, 1, 0.6);
break;
case 'clam':
AudioEngine.noise(0.4, 'lowpass', 150, 60, 2, 0.8);
AudioEngine.tone('sine', 60, 30, 0.4, 0.5);
break;
case 'regen':
AudioEngine.noise(0.8, 'bandpass', 200, 2000, 5, 0.3);
AudioEngine.tone('triangle', 300, 600, 0.6, 0.2);
break;
default: break;
}
}
};
// THIS IS THE SINGLE SOURCE OF TRUTH FOR THE AUDIO HANDLER
const A = {
init: (assets) => AudioEngine.init(assets),
play: (key) => {
if(['title','main','levelup','gameover'].includes(key)) AudioEngine.playTrack(key);
else AudioEngine.sfx(key);
},
stop: (key) => AudioEngine.stopTrack(key),
setMute: (mute) => AudioEngine.setMute(mute)
};
// =================================================================================================
// SECTION 2: GRAPHICS ENGINE & DECOR GENERATORS (HAND-CODED ARTISTRY)
// =================================================================================================
const D = {
color: (ctx, c, s) => {
if (!c || typeof c === 'string') return c || '#999';
if (Array.isArray(c)) { const g=ctx.createLinearGradient(-s,-s,s,s); g.addColorStop(0, c[0]); g.addColorStop(1, c[1]); return g; }
const { colors: [c1, c2], type } = c; let g;
if(type==='linear_v') g=ctx.createLinearGradient(0,-s,0,s); else if(type==='linear_h') g=ctx.createLinearGradient(-s,0,s,0); else if(type==='radial') g=ctx.createRadialGradient(0,0,0,0,0,s); else g=ctx.createLinearGradient(-s,-s,s,s);
g.addColorStop(0, c1); g.addColorStop(1, c2); return g;
},
css: (c) => (typeof c === 'object' && c.colors) ? c.colors[0] : (Array.isArray(c) ? c[0] : c),
grad: (c, t, sz, col) => {
const g = t==='r' ? c.createRadialGradient(0,0,0,0,0,sz) : c.createLinearGradient(-sz,0,sz,0);
const colors = (col && col.colors) ? col.colors : (Array.isArray(col) ? col : ['#FFF','#000']);
g.addColorStop(0, colors[0]); g.addColorStop(1, colors[1]); return g;
},
draw: (ctx, fn, { f, s, w=1, sh, b=10, glow, alpha, lineCap, lineJoin, txt, font, align, comp, flash } = {}) => {
ctx.save();
if(sh) { ctx.shadowColor=sh; ctx.shadowBlur=b; if(b>0) ctx.shadowOffsetY=5; }
if(glow) ctx.globalCompositeOperation='screen';
if(comp) ctx.globalCompositeOperation=comp;
if(alpha) ctx.globalAlpha=alpha;
if(lineCap) ctx.lineCap=lineCap;
if(lineJoin) ctx.lineJoin=lineJoin;
if (txt) { ctx.fillStyle = f; ctx.font = font || 'bold 12px Arial'; if (align) ctx.textAlign = align; if (s) { ctx.strokeStyle = s; ctx.lineWidth = w; ctx.strokeText(txt, 0, 0); } ctx.fillText(txt, 0, 0); }
else { ctx.beginPath(); fn(ctx); if(f) { ctx.fillStyle=f; ctx.fill(); } if(s) { ctx.strokeStyle=s; ctx.lineWidth=w; ctx.stroke(); }
if(flash && flash > 0) { const a = 1 - Math.abs(flash - 0.5) * 2; if(a > 0.01) { ctx.fillStyle=`rgba(255,255,255,${a})`; ctx.shadowColor='#FFFFFF'; ctx.shadowBlur=20*a; ctx.fill(); } }
}
ctx.restore();
},
fill: (ctx, w, h, color, comp) => { D.draw(ctx, p=>p.rect(0,0,w,h), {f:color, comp}); },
ent: (ctx, x, y, ang, sz, t, fn) => { ctx.save(); ctx.translate(x,y); ctx.rotate(ang); if(t) { const s=1+Math.sin(t*0.005)*0.02; ctx.scale(s,s); } fn(ctx, sz); ctx.restore(); },
noise: (x, seed) => (Math.sin(x*0.01+(seed||0))*10) + (Math.sin((x*0.01+(seed||0))*2.5)*5),
particle: (ctx, p) => {
ctx.save(); ctx.globalAlpha = p.life; ctx.translate(p.x, p.y);
if(p.type === 'bubble') { ctx.beginPath(); ctx.arc(0, 0, p.size, 0, 6.28); ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.beginPath(); ctx.arc(-p.size*0.3, -p.size*0.3, p.size*0.2, 0, 6.28); ctx.fill(); }
else if (p.type === 'spark') { ctx.fillStyle = p.color; ctx.shadowColor = p.color; ctx.shadowBlur = 10; ctx.beginPath(); ctx.rect(-p.size/2, -p.size/2, p.size, p.size); ctx.fill(); }
else if(p.type === 'shockwave') { D.draw(ctx, g=>g.arc(0,0,p.size,0,7), {s:D.css(p.color), w:2}); }
else if(p.type === 'ink') { D.draw(ctx, g=>g.arc(0,0,p.size,0,7), {f:p.color, alpha:0.6}); }
else if(p.type === 'ember') { D.draw(ctx, g=>g.rect(0,0,p.size,p.size), {f:p.color, sh:'#ffaa00', b:10}); }
else if(p.type === 'confetti') { D.draw(ctx, g=>g.rect(-p.size/2,-p.size/2,p.size,p.size*1.5), {f:p.color}); }
else { D.draw(ctx, g=>g.arc(0,0,p.size,0,7), {f:D.color(ctx, p.color, p.size)}); }
ctx.restore();
}
};
const DECOR_GENERATORS = {
// 1. KELP FOREST (Swaying Bezier Curves)
kelp_forest: (ents, w, h, p) => {
const count = Math.floor(w / p.density);
ents.decor.kelp = Array.from({ length: count }, (_, i) => ({
x: (i * p.density) + (Math.random() * 40),
y: h,
h: Math.random() * (p.heightRange[1] - p.heightRange[0]) + p.heightRange[0],
w: Math.random() * 15 + 10,
color: p.color,
offset: Math.random() * Math.PI * 2,
leaves: Array.from({length: 6}, ()=>Math.random())
}));
},
// 2. BRAIN CORAL V3 (Branching Staghorn)
brain_coral: (ents, w, h, p) => {
ents.decor.coral = Array.from({ length: p.count }, () => {
const x = Math.random() * w;
const y = h - 10;
const segments = [];
// Recursive Branching Logic
const branch = (px, py, angle, len, width, depth) => {
if (depth > 4) return;
const tipX = px + Math.cos(angle) * len;
const tipY = py + Math.sin(angle) * len;
// Store segment
segments.push({
mx: px, my: py,
tx: tipX, ty: tipY,
w: width,
d: depth
});
const num = 2; // Always fork
for(let i=0; i<num; i++) {
const newAng = angle + (Math.random() - 0.5) * 1.5; // Spread
branch(tipX, tipY, newAng, len * 0.75, width * 0.7, depth + 1);
}
};
branch(0, 0, -Math.PI/2, 45, 10, 0); // Start relative to root
return { x, y, segments, color: p.color };
});
},
// 3. TUBE SPONGES (Vertical clusters)
tube_sponge: (ents, w, h, p) => {
ents.decor.sponges = Array.from({ length: p.count }, () => ({
x: Math.random() * w,
y: h - 10,
tubes: [
{ h: 40 + Math.random()*20, w: 10, ang: -0.1 },
{ h: 60 + Math.random()*20, w: 14, ang: 0 },
{ h: 30 + Math.random()*20, w: 8, ang: 0.1 }
],
color: p.color
}));
},
// 4. AMBIENT FISH SCHOOL (Background decoration only)
ambient_school: (ents, w, h, p) => {
ents.decor.school = Array.from({ length: p.count * 3 }, () => ({
x: Math.random() * w,
y: Math.random() * p.depth + (h - p.depth),
size: Math.random() * 3 + 2,
speed: Math.random() * 0.5 + 0.2,
offset: Math.random() * 100
}));
},
bubbles: (ents, w, h, p) => {
ents.decor.bubbles = Array.from({ length: p.count || 40 }, () => ({ x: Math.random()*w, y: Math.random()*h, size: Math.random()*6+2, speed: Math.random()*0.5+0.2 }));
},
dust: (ents, w, h, p) => {
ents.decor.dust = Array.from({ length: p.count || 50 }, () => ({ x: Math.random()*w, y: Math.random()*h, size: Math.random()*2, vx: (Math.random()-0.5)*0.2, vy: (Math.random()-0.5)*0.2 }));
},
clams: (ents, w, h, p) => {
// EVEN DISTRIBUTION LANE LOGIC
const laneWidth = w / p.count;
ents.clams = Array.from({ length: p.count || 3 }, (_, i) => ({
x: (laneWidth * i) + (laneWidth / 2),
y: h-(p.yOffset||30),
size: 45,
isOpen: false,
timer: Math.random()*200,
hasPearl: true,
color: p.color || { type: 'radial', colors: ['#8D6E63', '#5D4037'] },
innerColor: p.inner || { type: 'radial', colors: ['#EFEBE9', '#D7CCC8'] }
}));
},
crabs: (ents, w, h, p) => {
ents.crabs = [];
for(let i=0; i<p.count; i++) {
ents.crabs.push({ x: w * (0.2 + i * (0.6 / p.count)), y: h-(p.yOffset||25), size: 25, speed: 0.5, dir: i%2===0?1:-1, attackCooldown: 0, pinchTimer: 0, color: { type: 'radial', colors: ['#e74c3c', '#922b21'] }, shape: 'crab' });
}
}
};
const DECOR_RENDERER = {
kelp_forest: (ctx, items, t) => {
items.forEach(k => {
const swayBase = Math.sin(t * 0.002 + k.offset) * 10;
const swayTip = Math.sin(t * 0.003 + k.offset) * 30;
const g = ctx.createLinearGradient(0, -k.h, 0, 0);
g.addColorStop(0, k.color[1]); g.addColorStop(1, k.color[0]);
ctx.save(); ctx.translate(k.x, k.y);
ctx.beginPath(); ctx.moveTo(-k.w/2, 0);
ctx.bezierCurveTo(swayBase - k.w/2, -k.h * 0.5, swayTip - k.w/4, -k.h, swayTip, -k.h);
ctx.bezierCurveTo(swayTip + k.w/4, -k.h, swayBase + k.w/2, -k.h * 0.5, k.w/2, 0);
ctx.fillStyle = g; ctx.fill();
k.leaves.forEach((l, i) => {
const yPos = -k.h * (0.2 + l * 0.75);
const ratio = yPos / -k.h;
const localSway = swayBase * (1-ratio) + swayTip * ratio;
ctx.save(); ctx.translate(localSway, yPos);
const leafAng = Math.sin(t * 0.004 + i + k.offset) * 0.3 + (i%2===0 ? 0.4 : -0.4);
ctx.rotate(leafAng);
ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(15, -8, 35, 0); ctx.quadraticCurveTo(15, 8, 0, 0); ctx.fillStyle = k.color[0]; ctx.fill(); ctx.restore();
});
ctx.restore();
});
},
brain_coral: (ctx, items, t) => {
items.forEach(c => {
ctx.save();
ctx.translate(c.x, c.y);
// Branch Gradient
const g = ctx.createLinearGradient(0, 0, 0, -80);
g.addColorStop(0, c.color[1]); // Dark Base
g.addColorStop(1, c.color[0]); // Light Tip
ctx.strokeStyle = g;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
c.segments.forEach(s => {
ctx.lineWidth = s.w;
ctx.beginPath();
ctx.moveTo(s.mx, s.my);
ctx.lineTo(s.tx, s.ty);
ctx.stroke();
// Add texture polyps
if(s.d > 2 && Math.random() > 0.5) {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.arc(s.tx, s.ty, s.w*0.3, 0, Math.PI*2);
ctx.fill();
}
});
ctx.restore();
});
},
tube_sponge: (ctx, items, t) => {
items.forEach(s => {
ctx.save(); ctx.translate(s.x, s.y);
s.tubes.forEach(tube => {
const g = ctx.createLinearGradient(0, -tube.h, 0, 0);
g.addColorStop(0, s.color[0]); g.addColorStop(1, s.color[1]);
ctx.save(); ctx.rotate(tube.ang);
ctx.beginPath(); ctx.moveTo(-tube.w/2, 0); ctx.quadraticCurveTo(-tube.w, -tube.h/2, -tube.w/2 - 2, -tube.h); ctx.lineTo(tube.w/2 + 2, -tube.h); ctx.quadraticCurveTo(tube.w, -tube.h/2, tube.w/2, 0); ctx.fillStyle = g; ctx.fill();
ctx.fillStyle = '#220033'; ctx.beginPath(); ctx.ellipse(0, -tube.h, tube.w*0.6, tube.w*0.2, 0, 0, 6.28); ctx.fill(); ctx.restore();
});
ctx.restore();
});
},
ambient_school: (ctx, items, t) => {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
items.forEach(f => {
const x = (f.x + t * f.speed + f.offset) % 2000 - 500; const y = f.y + Math.sin(t * 0.005 + f.offset) * 20;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - f.size * 3, y - f.size); ctx.lineTo(x - f.size * 3, y + f.size); ctx.fill();
});
}
};
const EFFECTS_LIBRARY = {
'god_rays': (ctx, w, h, t) => {
const count = 12; ctx.save(); ctx.globalCompositeOperation = 'screen';
for (let i = 0; i < count; i++) {
const x = (w / count) * i + (Math.sin(t / 400 + i * 2) * 20);
const width = (w / count) * (0.8 + Math.sin(t/200 + i)*0.3);
const alpha = (Math.sin(t/100 + i*132) + 1) * 0.08 + 0.02;
const angle = 0.2 + Math.sin(t/1000 + i)*0.05;
const g = ctx.createLinearGradient(x, -50, x - h * Math.sin(angle), h);
g.addColorStop(0, 'rgba(255, 255, 200, 0.6)'); g.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)'); g.addColorStop(1, 'rgba(255, 255, 255, 0)');
D.draw(ctx, p => { p.moveTo(x - width/2, -100); p.lineTo(x + width/2, -100); p.lineTo(x + width/2 - h * Math.sin(angle), h); p.lineTo(x - width/2 - h * Math.sin(angle), h); }, { f: g, alpha });
}
ctx.restore();
},
'caustics': (ctx, w, h, t) => { D.fill(ctx, w, h, `rgba(255, 255, 255, ${(Math.sin(t / 200) + 1) * 0.05})`, 'overlay'); },
'confetti': (ctx, w, h, t, spawner) => {
const cols = ['#FFD700', '#FF69B4', '#00FFFF', '#7FFF00', '#FF4500'];
spawner('confetti', 0.3, cols[Math.floor(Math.random()*cols.length)], [2,4], ()=>(Math.random()-0.5)*2, ()=>Math.random()*3+2, 0.005);
},
'spawn': (list, w, h, type, chance, color, countRange, vxFn, vyFn, decay) => {
if(Math.random() < chance) {
const n = Math.floor(Math.random() * (countRange[1]-countRange[0]) + countRange[0]);
for(let i=0; i<n; i++) { list.push({ type, color, x: Math.random() * w, y: vyFn()>0 ? -20 : h+20, vx: vxFn(), vy: vyFn(), decay, size: Math.random()*5+2, life: 1.0 }); }
}
}
};
// =================================================================================================
// SECTION 3: BIO-GEOMETRY & DNA
// =================================================================================================
const BIO_GEO = {
eye: {
standard: (c, sz, t, p, col, xOff=0, yOff=0) => {
c.save(); c.translate(xOff, yOff);
D.draw(c, g=>g.arc(sz*0.5, -sz*0.16, sz*0.16, 0, 7), {f:'#FFFFFF', flash:p});
D.draw(c, g=>g.arc(sz*0.5 + 2, -sz*0.16, sz*0.08, 0, 7), {f:'#000000'});
c.restore();
},
stalks: (c, sz, t, p, col) => {
[1,-1].forEach(d=>{
D.draw(c, g=>{g.moveTo(d*5,-5); g.lineTo(d*8,-sz/2-12)}, {s:'#922b21', w:3, flash:p});
D.draw(c, g=>g.arc(d*8,-sz/2-12,5,0,7), {f:'white', flash:p});
D.draw(c, g=>g.arc(d*8,-sz/2-12,2,0,7), {f:'black'});
});
}
},
tail: {
standard: (c, sz, t, p, col) => D.draw(c, g=>{g.moveTo(-sz*0.8,0); g.lineTo(-sz*1.5,-sz/2+Math.sin(t/60)*3); g.lineTo(-sz*1.5,sz/2+Math.sin(t/60)*3)}, {f:D.grad(c,'l',sz,col), flash:p}),
clown: (c, sz, t, p, col) => D.draw(c, g=>{g.moveTo(-sz*1.1,0); g.lineTo(-sz*1.8,-sz*0.6+Math.sin(t/15)*3); g.lineTo(-sz*1.8,sz*0.6+Math.sin(t/15)*3)}, {f:'#FF4500', flash:p}),
angel: (c, sz, t, p, col) => {
const flow = Math.sin(t/25)*5;
D.draw(c, g=>{g.moveTo(sz*1.2,0); g.lineTo(0,-sz*1.5); g.lineTo(-sz*0.8,-sz*0.5); g.lineTo(-sz*0.8,sz*0.5); g.lineTo(0,sz*1.5)}, {f:D.grad(c,'l',sz*1.5,['#483D8B','#00008B'])});
[-1, 1].forEach(d => D.draw(c, g=>{g.moveTo(0,d*-sz*1.5); g.bezierCurveTo(sz*0.5,d*-sz*2.5,-sz*1.0,d*-sz*3.0+flow,-sz*2.5,d*-sz*3.5+flow); g.lineTo(-sz*0.5,d*-sz*1.0)}, {f:'rgba(72,61,139,0.8)'}));
}
},
body: {
standard: (c, sz, t, p, col) => D.draw(c, g=>g.ellipse(0,0,sz,sz/2,0,0,7), {f:D.grad(c,'l',sz,col), flash:p}),
crab: (c, sz, t, p, col) => D.draw(c, g=>{g.moveTo(-sz,-sz/3); g.quadraticCurveTo(0,-sz/1.5,sz,-sz/3); g.lineTo(sz-3,sz/2); g.quadraticCurveTo(0,sz/1.2,-sz+3,sz/2)}, {f:D.grad(c,'r',sz,col), flash:p}),
},
fin: {
standard: (c, sz, t, p, col) => { const wig=Math.cos(t/45)*5; D.draw(c, g=>{g.moveTo(0,sz/2-2); g.lineTo(-sz*0.4+wig,sz); g.lineTo(sz*0.2,sz/2-2); g.moveTo(0,-sz/2+2); g.lineTo(-sz*0.4+wig,-sz); g.lineTo(sz*0.2,-sz/2+2)}, {f:D.grad(c,'l',sz,col), flash:p}); },
},
extra: {
stripes: (c, sz, t, p, col) => D.draw(c, g=>{g.rect(-sz*0.2,-sz*0.5,sz*0.1,sz); g.rect(sz*0.2,-sz*0.5,sz*0.1,sz)}, {f:'#FFD700', comp:'source-atop'}),
// UPDATED CLAWS to handle pinch rotation
claws: (c, sz, t, p, col, x, y, entity) => {
const pinch = entity && entity.pinchTimer > 0 ? 0.8 : 0; // Aggressive pinch angle
[1,-1].forEach(d=>{
c.save();
c.translate(d*sz,-3);
c.rotate(d*0.2 - (d * pinch));
D.draw(c, g=>{g.moveTo(0,0); g.lineTo(d*6,-3); g.lineTo(d*10,3)}, {f:'#c0392b', flash:p});
c.translate(d*10,0);
D.draw(c, g=>{g.moveTo(0,0); g.quadraticCurveTo(d*3,3,d*15,3); g.lineTo(d*18,-3); g.lineTo(d*10,-1); g.lineTo(d*6,0)}, {f:'#c0392b', s:'#922b21', w:1, flash:p});
c.restore();
});
},
// UPDATED LIMBS to actually connect to the body properly
limbs_crab: (c, sz, t, p, col) => {
const legSpacing = 6;
for(let i=0;i<4;i++) {
const yOffset = (i - 1.5) * legSpacing;
[1, -1].forEach(d => {
const startX = d * sz * 0.8; // Attach to sides, not center
D.draw(c, g=>{
g.moveTo(startX, yOffset);
// Knee joint
g.quadraticCurveTo(startX + d*15, yOffset - 5, startX + d*20, yOffset + 10);
// Tip
g.lineTo(startX + d*28, yOffset + 15 + Math.sin(t/40 + i*d)*3);
}, {s:'#922b21', w:3, flash:p});
});
}
},
},
shell: {
clam: (c, s, col, innerCol, isOpen) => {
const shellDraw = (color, ridge) => {
D.draw(c, p=>{ p.moveTo(-s,0); for(let i=0;i<7;i++) { const a1=Math.PI+(Math.PI/7)*i, a2=Math.PI+(Math.PI/7)*(i+1); p.bezierCurveTo(Math.cos(a1+Math.PI/21)*s*1.2, Math.sin(a1+Math.PI/21)*s*0.96, Math.cos(a2-Math.PI/21)*s*1.2, Math.sin(a2-Math.PI/21)*s*0.96, Math.cos(a2)*s, Math.sin(a2)*s*0.8); } p.lineTo(s,0); p.lineTo(-s,0); }, {f:color, s:ridge, w:1.5});
};
c.save(); c.rotate(Math.PI); shellDraw(col,'#3E2723');
if(isOpen) { c.save(); c.scale(0.8,0.8); c.translate(0,5); shellDraw(innerCol,'#D7CCC8'); c.restore(); }
c.restore();
c.save(); c.translate(-s*0.5,0); c.rotate(isOpen?-Math.PI*0.6:0); c.translate(s*0.5,0); shellDraw(col,'#4E342E'); c.restore();
}
},
item: {
pearl: (c, s, t) => {
const p = (Math.sin(t/150)+1)*0.1+0.9;
D.ent(c, 0, s*0.1, 0, s*0.3 * p, 0, (pc, ps) => {
const g=pc.createRadialGradient(-3,-3,1,0,0,ps); g.addColorStop(0,'#FFF'); g.addColorStop(1,'#F8BBD0');
D.draw(pc, p=>p.arc(0,0,ps,0,7), {f:g, sh:'#FF4081', b:25});
D.draw(pc, null, {txt:'โฅ', f:'#C2185B', font:`bold ${ps}px Arial`, align:'center'});
});
}
},
misc: {
mouth: (c, s, isOpen, alpha) => {
if (isOpen) { D.draw(c, p=>p.ellipse(s*0.8,0,s*0.4,s*0.3,0,0,7), {f:'black', alpha}); D.draw(c, p=>p.ellipse(s*0.85,0,s*0.3,s*0.2,0,0,7), {f:'#c0392b', alpha}); }
else { D.draw(c, p=>p.ellipse(s*0.8,0,s*0.1,s*0.05,0,0,7), {f:'black', alpha}); }
},
crown: (c, s) => { D.ent(c, s*0.2, -s*0.6, 0.1, 1, 0, (cc)=>{ D.draw(cc, p=>{ const w=s*1.2, h=s*0.9; p.moveTo(-w*0.8,0); p.lineTo(-w*0.9,-h*0.5); p.lineTo(-w*0.6,-h); p.lineTo(-w*0.25,-h*0.5); p.lineTo(0,-h*1.3); p.lineTo(w*0.25,-h*0.5); p.lineTo(w*0.6,-h); p.lineTo(w*0.9,-h*0.5); p.lineTo(w*0.8,0); p.quadraticCurveTo(0,h*0.15,-w*0.8,0); }, {f:'#FFD700', s:'#B8860B', w:2}); }); },
deadX: (c, s) => { D.draw(c, p=>p.ellipse(0, -s*2, s*0.75, s*0.15, 0, 0, 7), {s:'#FFD700', w:2, sh:'#FFD700', b:10, alpha: 0.8}); }
}
};
// STANDARD FISH MAPPING (NO MUTATIONS)
const DNA_INDEX = {
'minnow': { parts: [{id:'tail',t:'standard'}, {id:'fin',t:'standard'}, {id:'body',t:'standard'}, {id:'eye',t:'standard'}] },
'carp': { parts: [{id:'tail',t:'standard'}, {id:'fin',t:'standard'}, {id:'body',t:'standard'}, {id:'extra',t:'stripes'}, {id:'eye',t:'standard'}] }, // Standard body + stripes
'bass': { parts: [{id:'tail',t:'standard'}, {id:'fin',t:'standard'}, {id:'body',t:'standard'}, {id:'extra',t:'stripes'}, {id:'eye',t:'standard'}] }, // Standard body + stripes
'crab': { parts: [{id:'extra',t:'limbs_crab'}, {id:'eye',t:'stalks'}, {id:'body',t:'crab'}, {id:'extra',t:'claws'}] },
'default': { parts: [{id:'tail',t:'standard'}, {id:'fin',t:'standard'}, {id:'body',t:'standard'}, {id:'eye',t:'standard'}] }
};
// =================================================================================================
// SECTION 4: GAME ENGINE & HITBOXES
// =================================================================================================
const HITBOX_CONFIG = {
circle: [{ x: 0, y: 0, r: 1.0 }],
long: [{ x: 0.5, y: 0, r: 0.8 }, { x: 0, y: 0, r: 0.9 }, { x: -0.5, y: 0, r: 0.7 }],
crab: [{ x: 0, y: 0, r: 0.8 }, { x: 1.2, y: -0.5, r: 0.4 }, { x: -1.2, y: -0.5, r: 0.4 }]
};
const P = {
move: (e, f=1) => { e.vx*=f; e.vy*=f; e.x+=e.vx; e.y+=e.vy; },
steer: (e, tx, ty, r) => { const a = Math.atan2(ty-e.y, tx-e.x); let d = a-e.angle; while(d>Math.PI)d-=Math.PI*2; while(d<-Math.PI)d+=Math.PI*2; e.angle+=d*r; },
check: (a, b) => {
const shape = HITBOX_CONFIG[b.shape || 'circle'];
const angle = b.angle || 0; // Fix NaN angle crash
const ca = Math.cos(angle), sa = Math.sin(angle);
for (let part of shape) {
const px = b.x + (part.x * b.size * ca - part.y * b.size * sa);
const py = b.y + (part.x * b.size * sa + part.y * b.size * ca);
const pr = part.r * b.size;
if (Math.hypot(a.x - px, a.y - py) < (a.size + pr)) return true;
}
return false;
},
bounds: (e, w, h, pad=100, type='bounce') => {
if(type==='wrap') { if(e.x<-pad)e.x=w+pad; if(e.x>w+pad)e.x=-pad; if(e.y<-pad)e.y=h+pad; if(e.y>h+pad)e.y=-pad; }
else { if(e.x<-pad)e.x=w+pad; if(e.x>w+pad)e.x=-pad; if(e.y<-pad)e.y=h+pad; if(e.y>h+pad)e.y=-pad; }
},
hit: (e1, e2, buffer = 0) => Math.hypot(e1.x - e2.x, e1.y - e2.y) < (e1.size + e2.size + buffer),
impulse: (e, force, angle) => { e.vx += Math.cos(angle) * force; e.vy += Math.sin(angle) * force; },
push: (e, speed) => { e.x += Math.cos(e.angle) * speed; e.y += Math.sin(e.angle) * speed; }
};
// =================================================================================================
// SECTION 5: COMPONENT
// =================================================================================================
const StatBar = memo(({ label, value, max, colorFrom, colorTo, showValue, pulseLow }) => {
const p = Math.max(0, Math.min(100, (value/max)*100)), crit = pulseLow && p < 20;
return (
<div className="mb-3 relative group">
<div className="flex justify-between text-[10px] text-white font-black mb-1 drop-shadow-md"><span>{label}</span>{(showValue||crit)&&<span className="opacity-90">{Math.ceil(value)}/{max || 100}</span>}</div>
<div className={`relative w-full h-3 bg-black/60 border ${crit?'border-red-500 animate-pulse':'border-white/20'} rounded overflow-hidden`}><div className={`h-full transition-all duration-300 ease-out relative ${crit?'bg-red-600':`bg-gradient-to-r ${colorFrom} ${colorTo}`}`} style={{width:`${p}%`}}><div className="absolute top-0 left-0 w-full h-[40%] bg-white/30"></div></div><div className="absolute inset-0 w-full h-full" style={{backgroundImage:'repeating-linear-gradient(90deg, transparent 0, transparent 19%, rgba(0,0,0,0.7) 19%, rgba(0,0,0,0.7) 20%)'}}></div></div>
</div>
);
});
const AbilitySquare = memo(({ label, cooldown, max, locked, icon }) => {
const p = locked ? 0 : Math.max(0, Math.min(100, ((max-cooldown)/max)*100));
return (
<div className={`w-10 h-10 relative rounded border ${locked?'border-gray-600 bg-gray-800/50':cooldown<=0?'border-cyan-400 bg-cyan-900/40 shadow-[0_0_10px_rgba(34,211,238,0.5)]':'border-white/20 bg-black/60'} overflow-hidden mr-2 flex-shrink-0`}>
{!locked && <div className="absolute bottom-0 left-0 w-full bg-cyan-500/30 transition-all duration-100" style={{height:`${p}%`}}/>}
<div className="absolute inset-0 flex items-center justify-center">{locked?<span className="text-gray-500 text-xs">๐</span>:<span className={`text-lg ${cooldown<=0?'text-cyan-200':'text-gray-500'}`}>{icon}</span>}</div>
</div>
);
});
const StatSquare = memo(({ label, value, color }) => (
<div className="w-10 h-10 relative rounded border border-white/20 bg-black/60 overflow-hidden mr-2 flex flex-col items-center justify-center shadow-inner flex-shrink-0">
<div className="absolute inset-0 opacity-20" style={{backgroundImage:'repeating-linear-gradient(45deg, transparent 0, transparent 2px, #000 2px, #000 4px)'}}></div>
<span className="text-[6px] text-gray-400 uppercase tracking-wider absolute top-1">{label}</span>
<span className={`text-xs font-bold font-mono mt-1 ${color} drop-shadow-md`}>{value}</span>
</div>
));
const ICONS_UI = { speaker: <path d="M11 5L6 9H2v6h4l5 4V5z" fill="currentColor"/>, waves: <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" fill="none" stroke="currentColor" strokeWidth="2"/>, x: <g fill="none" stroke="currentColor" strokeWidth="2"><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></g>, copy: <g fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></g> };
// =================================================================================================
// SECTION 6: MAIN ENGINE
// =================================================================================================
const App = () => {
const canvasRef = useRef(null), lastTimeRef = useRef(0);
const [wSize, setWSize] = useState({ w: window.innerWidth, h: window.innerHeight });
const [isMobile, setIsMobile] = useState(false); // Mobile Detection State
const [mute, setMute] = useState(false); const [copied, setCopied] = useState(false);
const [cartridge, setCartridge] = useState(INITIAL_CARTRIDGE);
const logicRef = useRef({ update: null, draw: null });
const gs = useRef({ status: 'menu', running: false, gameOver: false, isObstructed: false, score: 0, fishCount: 0, globalTime: 0, startTime: 0, shake: 0, statsHistory: { eaten: {}, bonuses: 0, combatScore: 0 }, passwords: { legendary: "๐ฑ", epic: "๐ฆ", rare: "๐ฆ", uncommon: "๐ฆ", common: "๐ฆ" }, seed: Math.random()*100, spawnBag: [], lastPredatorSpawn: 0, runStats: { time: "0.0", hp: 0, bonuses: 0 } });
const input = useRef({ x: window.innerWidth/2, y: window.innerHeight/2 });
// FIXED: Added missing 'ability' object to player initialization
const ents = useRef({ player: { x: 0, y: 0, size: 22, angle: 0, vx: 0, vy: 0, health: 100, maxHealth: 100, color: ['#FF8000', '#FFA500'], stats: { spd: 6, atk: 1, def: 0, dashPwr: 8 }, ability: { cooldown: 0, maxCooldown: 60, active: false, timer: 0 } }, enemies: [], crabs: [], clams: [], particles: [], floatingTexts: [], decor: { kelp: [], coral: [], sponges: [], school: [], bubbles: [], dust: [] } });
const audio = useRef({});
const [ui, setUi] = useState({ status: 'menu', hp: 100, maxHp: 100, progress: 0, reqProgress: 5, abilityCd: 0, abilityMax: 60, timeLeft: 200, name: "GOLDFISH", color: ['#FF8000', '#FFA500'], tier: "", isObstructed: false, score: 0 });
const gradCache = useRef({});
// SERIALIZATION UTILITY FOR EXPORT
const serializeGeometry = (obj) => {
const result = {};
Object.keys(obj).forEach(key => {
const val = obj[key];
if (typeof val === 'function') {
result[key] = val.toString();
} else if (typeof val === 'object' && val !== null) {
result[key] = serializeGeometry(val);
} else {
result[key] = val;
}
});
return result;
};
// RENDER ADAPTER
const R = {
char: (ctx, e, t, isPlayer, isDead, bio, showCrown) => {
ctx.save();
const sz = e.size;
// GHOST EFFECT LOGIC
if (isDead) {
ctx.globalAlpha = 0.5; // Ghostly transparency
ctx.shadowColor = '#E0F7FA'; // Ectoplasmic glow
ctx.shadowBlur = 25;
ctx.globalCompositeOperation = 'screen'; // Spectral blending
// Override color locally for drawing
// We create a ghost palette
e = { ...e, color: ['#E0F7FA', '#00FFFF'] };
} else {
ctx.globalAlpha = 1;
}
const alpha = isDead ? 0.6 : 1;
const envId = cartridge.theme.envId || 'coral';
// Map visual to specific key for DNA INDEX lookups
const speciesKey = isPlayer ? 'default' : e.visual;
const dna = (() => {
if (DNA_INDEX[speciesKey]) return { ...DNA_INDEX[speciesKey], col: e.color.colors || e.color };
return { ...DNA_INDEX['default'], col: e.color.colors || e.color };
})();
if (dna.scale) ctx.scale(dna.scale, dna.scale);
if (dna.rot) ctx.rotate(dna.rot);
D.ent(ctx, e.x, e.y, e.angle, sz, t, (c, s) => {
const flashIntensity = (e.hitFlash > 0 && !isDead) ? 0.5 : 0;
// DRAW PARTS
dna.parts.forEach(part => {
if (BIO_GEO[part.id] && BIO_GEO[part.id][part.t]) {
const positions = part.pos ? part.pos : (part.y ? part.y.map(y=>({x:0, y})) : [{x:0,y:0}]);
positions.forEach(pos => {
const xOffset = pos.x * s;
const yOffset = pos.y * s;
if (part.id === 'extra' && part.t.includes('limbs')) {
BIO_GEO[part.id][part.t](c, s, t, flashIntensity, envId);
} else {
// PASSED ENTITY (e) HERE for state-based animation
BIO_GEO[part.id][part.t](c, s, t, flashIntensity, dna.col, xOffset, yOffset, e);
}
});
}
});
// Extra Visuals
if (e.visual === 'bass' || e.visual === 'carp') BIO_GEO.extra.stripes(c, s, t, flashIntensity);
// Mouth
BIO_GEO.misc.mouth(c, s, e.mouthOpen || e.mouthTimer > 0, alpha);
// Crown
if(showCrown) BIO_GEO.misc.crown(c, s);
// Dead Halo (Only if dead)
if(isDead) BIO_GEO.misc.deadX(c, s);
});
ctx.restore();
}
};
const initDecor = (w, h) => {
// Reset Decor Arrays
ents.current.decor = { kelp: [], coral: [], sponges: [], school: [], bubbles: [], dust: [] };
ents.current.crabs = [];
ents.current.clams = [];
// Generator Loop
if(cartridge.theme.decor) {
cartridge.theme.decor.forEach(item => {
if(DECOR_GENERATORS[item.type]) {
DECOR_GENERATORS[item.type](ents.current, w, h, item);
}
});
}
gradCache.current = {};
};
const startAudio = () => { if(gs.current.status === 'menu' && !mute) { A.play('title'); } };
const startGame = () => {
// Initialize Audio Context on user interaction
A.init(cartridge.assets);
A.play('click'); // Added click sound here
gs.current.running=true; gs.current.status='playing'; gs.current.gameOver=false; gs.current.score=0; gs.current.fishCount=0; gs.current.shake=0; gs.current.startTime=Date.now(); gs.current.statsHistory={eaten:{},bonuses:0,combatScore:0};
ents.current.enemies=[]; ents.current.particles=[]; ents.current.floatingTexts=[]; gs.current.spawnBag=[];
const p = ents.current.player;
p.maxHealth = Number(cartridge.player.maxHp) || 100; p.health = p.maxHealth; p.x=wSize.w/2; p.y=wSize.h/2; p.ability.cooldown=0; p.hitFlash=0; p.healFlash=0;
p.stats = { ...cartridge.player.stats }; p.color = cartridge.player.color; p.size = cartridge.player.size;
A.play('main'); // Switch to main loop
setUi(prev=>({...prev, status:'playing', hp:p.health, maxHp: p.maxHealth, progress:0, reqProgress:cartridge.rules.requiredScore, name: cartridge.player.name, color: cartridge.player.color, score: 0}));
};
const takePlayerDamage = (amount) => {
const p = ents.current.player; p.health -= amount; p.hitFlash = 5; gs.current.shake = 10;
for(let i=0;i<15;i++) ents.current.particles.push({x:p.x,y:p.y,color:D.css(p.color),size:Math.random()*3+1,type:'circle',vx:(Math.random()-0.5)*8,vy:(Math.random()-0.5)*8,life:1,decay:0.04});
ents.current.floatingTexts.push({ x: p.x, y: p.y - 20, text: `-${amount}`, color: "#FF0000", size: 24, life: 1.0, vy: -1 });
A.play('hit');
setUi(prev => ({...prev, hp: p.health}));
if(p.health <= 0) {
gs.current.running = false; gs.current.gameOver = true; gs.current.status = 'lost';
setUi(s => ({...s, status: 'lost'}));
A.play('gameover');
}
};
const updateEngine = (dt) => {
const { width, height } = canvasRef.current || { width: 800, height: 600 };
const p = ents.current.player; const isPlaying = gs.current.running; const tSec = gs.current.globalTime / 1000;
const frame = Math.floor(gs.current.globalTime / 16);
ents.current.decor.dust.forEach(d => { P.move(d); P.bounds(d, width, height, 0, 'wrap'); });
ents.current.decor.bubbles.forEach(b => { b.x+=D.noise(b.x*0.01, gs.current.globalTime*0.001)*0.05; b.y-=b.speed; if(b.y<0){b.y=height;b.x=Math.random()*width;} });
ents.current.clams.forEach(c => { c.timer--; if(c.timer<=0) { c.isOpen=!c.isOpen; c.timer=c.isOpen?200:300; if(!c.isOpen) c.hasPearl=true; if(!mute && gs.current.running && (isPlaying || Math.random()<0.1)) A.play('clam'); } });
// CRAB AI UPDATE
ents.current.crabs.forEach(c => {
c.x+=c.speed*c.dir;
if(c.x<50||c.x>width-50)c.dir*=-1;
c.attackCooldown--;
if (c.pinchTimer > 0) c.pinchTimer--; // DECAY PINCH ANIMATION
});
const spawnLogic = (forceOffscreen) => {
const type = cartridge.enemyTypes[Math.floor(Math.random() * cartridge.enemyTypes.length)];
const side = Math.floor(Math.random()*4);
let ex, ey;
if(side===0){ex=Math.random()*width;ey=-50;}
else if(side===1){ex=width+50;ey=Math.random()*height;}
else if(side===2){ex=Math.random()*width;ey=height+50;}
else{ex=-50;ey=Math.random()*height;}
ents.current.enemies.push({ ...JSON.parse(JSON.stringify(type)), x: ex, y: ey, vx: 0, vy: 0, angle: Math.random() * 6.28, hitCooldown: 0, hitFlash: 0, mouthOpen: false });
};
if (!isPlaying && Math.random() < 0.02 && ents.current.enemies.length < 6) { spawnLogic(true); }
if (isPlaying && Math.random() < 0.02 && ents.current.enemies.length < cartridge.rules.maxEnemies) {
let typeIdx = 0;
if (Date.now() - gs.current.lastPredatorSpawn > 4000) { typeIdx = cartridge.enemyTypes.length - 1; gs.current.lastPredatorSpawn = Date.now(); }
else {
if (gs.current.spawnBag.length === 0) { const newBag = []; cartridge.enemyTypes.forEach((_, i) => { for(let k=0; k<Math.max(1, 5-i*2); k++) newBag.push(i); }); for (let i = newBag.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newBag[i], newBag[j]] = [newBag[j], newBag[i]]; } gs.current.spawnBag = newBag; }
typeIdx = gs.current.spawnBag.pop();
}
spawnLogic(true);
}
if (isPlaying || gs.current.status === 'won') {
cartridge.active_effects.forEach(effName => {
if(effName.startsWith('particles_') && EFFECTS_LIBRARY[effName]) {
EFFECTS_LIBRARY[effName](null, width, height, gs.current.globalTime, (type, chance, color, countRange, vxFn, vyFn, decay) => {
EFFECTS_LIBRARY.spawn(ents.current.particles, width, height, type, chance, color, countRange, vxFn, vyFn, decay);
});
}
});
if (gs.current.status === 'won' && gs.current.runStats?.tier === 'legendary') {
EFFECTS_LIBRARY.confetti(null, width, height, gs.current.globalTime, (type, chance, color, countRange, vxFn, vyFn, decay) => {
EFFECTS_LIBRARY.spawn(ents.current.particles, width, height, type, chance, color, countRange, vxFn, vyFn, decay);
});
}
}
for(let i=ents.current.enemies.length-1; i>=0; i--) {
const e = ents.current.enemies[i];
if (isPlaying) {
let sepX=0, sepY=0; ents.current.enemies.forEach((o, j) => { if(i!==j && P.hit(e, o, 10)) { sepX+=(e.x-o.x); sepY+=(e.y-o.y); } });
e.vx += sepX*0.1; e.vy += sepY*0.1;
if (e.behavior === 'chase') P.steer(e, p.x, p.y, 0.04);
else if (e.behavior === 'flee' && Math.hypot(p.x-e.x, p.y-e.y)<200) { const tx = e.x + (e.x - p.x); const ty = e.y + (e.y - p.y); P.steer(e, tx, ty, 0.08); }
if(e.ability) { e.ability.timer-=16; if(e.ability.active) { if(e.ability.type==='charge') { e.speed=6; ents.current.particles.push({ x: e.x, y: e.y, color: 'rgba(255,255,255,0.3)', size: Math.random()*3+1, type:'circle', vx: 0, vy: 0, life: 1.0, decay: 0.1 }); } if(e.ability.timer<=0) { e.ability.active=false; e.ability.timer=e.ability.cooldown+Math.random()*2000; e.speed=e.baseSpeed||2; } } else if (e.ability.timer<=0 && Math.hypot(p.x-e.x, p.y-e.y)<250) { e.ability.active=true; e.ability.timer=e.ability.duration*16; } }
P.move(e, cartridge.physics.friction); P.push(e, e.speed);
const conf = cartridge.physics.trail_scalers;
if (e.hp < e.maxHp - 0.1) {
const hpRatio = e.hp / e.maxHp; let s = conf.minor;
if (hpRatio < 0.2) s = conf.crit; else if (hpRatio < 0.5) s = conf.major;
if (Math.random() < cartridge.physics.base_intensity * s.density) {
const trailVx = -e.vx * 0.5 + (Math.random()-0.5); const trailVy = -e.vy * 0.5 + (Math.random()-0.5);
ents.current.particles.push({ x: e.x, y: e.y, color: D.css(e.color), size: Math.random()*4+2, type:'circle', vx: trailVx, vy: trailVy, life: s.life, decay: 0.04 });
}
}
} else { P.push(e, 1.5); e.angle += (Math.random() - 0.5) * 0.05; }
P.bounds(e, width, height, 100, 'wrap');
if(e.hitCooldown>0) e.hitCooldown--; if(e.hitFlash>0) e.hitFlash--;
}
if (isPlaying) {
const elapsed = (Date.now() - gs.current.startTime) / 1000;
const remaining = Math.max(0, cartridge.rules.timeLimit - elapsed);
if (remaining <= 0) { gs.current.running=false; gs.current.gameOver=true; gs.current.status='lost'; setUi(s=>({...s, status:'lost'})); A.play('gameover'); return; }
P.steer(p, input.current.x, input.current.y, 1.0);
const dx = input.current.x - p.x, dy = input.current.y - p.y;
const acc = Math.hypot(dx, dy) > 10 ? 0.5 : 0;
P.impulse(p, acc, p.angle);
P.move(p, cartridge.physics.friction);
if(p.ability.active) { p.ability.timer--; if(p.ability.timer<=0) p.ability.active=false; } if(p.ability.cooldown>0) p.ability.cooldown--;
const conf = cartridge.physics.trail_scalers;
if (p.health < p.maxHealth - 0.1) {
const hpRatio = p.health / p.maxHealth; let s = conf.minor;
if (hpRatio < 0.2) s = conf.crit; else if (hpRatio < 0.5) s = conf.major;
if (Math.random() < cartridge.physics.base_intensity * s.density) {
const trailVx = -p.vx * 0.5 + (Math.random()-0.5); const trailVy = -p.vy * 0.5 + (Math.random()-0.5);
ents.current.particles.push({ x: p.x, y: p.y, color: D.css(p.color), size: Math.random()*4+2, type:'circle', vx: trailVx, vy: trailVy, life: s.life, decay: 0.04 });
}
}
if(Math.random()<0.05 && Math.hypot(p.vx,p.vy)>4) { ents.current.particles.push({x:p.x,y:p.y,color:'rgba(255,255,255,0.4)',size:Math.random()*2+1,type:'circle',vx:0,vy:0,life:0.5,decay:0.05}); }
const isObs = p.x < 240 && p.y < 170;
if (isObs !== gs.current.isObstructed) { gs.current.isObstructed = isObs; setUi(s => ({ ...s, isObstructed: isObs })); }
if(p.healFlash > 0) p.healFlash--; if(p.mouthOpen) { p.mouthTimer--; if(p.mouthTimer<=0) p.mouthOpen=false; }
ents.current.clams.forEach(c => {
if(c.isOpen && c.hasPearl && P.hit(p, c)) {
p.health=Math.min(p.maxHealth, p.health+15); gs.current.statsHistory.bonuses++; gs.current.score += cartridge.rules.scoring.pearl_factor;
p.healFlash = 60; if(!mute) A.play('regen');
ents.current.floatingTexts.push({x:p.x,y:p.y-20,text:`+${cartridge.rules.scoring.pearl_factor}`,color:"#ff69b4",size:20,life:1,vy:-1});
for(let i=0;i<10;i++) ents.current.particles.push({x:p.x,y:p.y,color:"#ff69b4",size:Math.random()*4+2,type:'circle',vx:(Math.random()-0.5)*5,vy:(Math.random()-0.5)*5,life:1,decay:0.03});
A.play('bonus'); c.hasPearl=false; c.isOpen=false; c.timer=60;
}
});
ents.current.crabs.forEach(c => {
if(c.attackCooldown<=0 && P.check(p, c)) {
const dmg = Math.max(1, 10-p.stats.def); takePlayerDamage(dmg); p.vx+=Math.sign(p.x-c.x)*1.5; p.vy-=1;
c.attackCooldown=60;
c.pinchTimer = 20; // TRIGGER ATTACK ANIMATION
A.play('crabPinch');
}
});
for(let i=ents.current.enemies.length-1; i>=0; i--) {
const e = ents.current.enemies[i];
if(e.mouthOpen) { e.mouthTimer--; if(e.mouthTimer<=0) e.mouthOpen=false; }
if(P.hit(p, e)) {
if(p.size >= e.size) {
if(e.hitCooldown<=0) {
e.hp -= p.stats.atk; e.hitCooldown=20; e.hitFlash=5; A.play('chomp'); p.mouthOpen = true; p.mouthTimer = 10;
for(let k=0;k<5;k++) ents.current.particles.push({x:e.x,y:e.y,color:D.css(e.color),size:Math.random()*3,type:'circle',vx:(Math.random()-0.5)*5,vy:(Math.random()-0.5)*5,life:1,decay:0.1});
if(e.hp<=0) {
ents.current.enemies.splice(i,1); p.health=Math.min(p.maxHealth, p.health+5);
const points = Math.floor(e.points * cartridge.rules.scoring.fish_factor);
gs.current.score += points; gs.current.fishCount++;
if (!gs.current.statsHistory.eaten[e.id]) gs.current.statsHistory.eaten[e.id] = 0; gs.current.statsHistory.eaten[e.id]++;
for(let k=0;k<8;k++) ents.current.particles.push({x:e.x,y:e.y,color:D.css(e.color),size:Math.random()*5+2,type:'ink',vx:(Math.random()-0.5)*3,vy:(Math.random()-0.5)*3,life:1,decay:0.02});
ents.current.floatingTexts.push({x:e.x,y:e.y,text:`+${points}`,color:"#FFFF00",size:20,life:1,vy:-1});
if(gs.current.fishCount >= cartridge.rules.requiredScore) {
gs.current.running=false; gs.current.status='won';
const timeBonus = Math.floor(remaining * cartridge.rules.scoring.time_factor);
const healthBonus = Math.floor(p.health * cartridge.rules.scoring.health_factor);
const totalScore = gs.current.score + timeBonus + healthBonus;
const thresholds = cartridge.rules.scoring.thresholds || {};
let tier = "common";
if ((thresholds.hp_min && p.health >= thresholds.hp_min) || (thresholds.time_max && elapsed <= thresholds.time_max) || (thresholds.score_min && gs.current.score >= thresholds.score_min) || (thresholds.pearls_min && gs.current.statsHistory.bonuses >= thresholds.pearls_min)) { tier = "legendary"; }
else { if(totalScore>=2500) tier="epic"; else if(totalScore>=1500) tier="rare"; else if(totalScore>=500) tier="uncommon"; }
const stats={ tier, score:totalScore, hp:Math.ceil(p.health), time:elapsed.toFixed(1), bonuses: gs.current.statsHistory.bonuses, eaten: gs.current.statsHistory.eaten };
// NEW VICTORY LOGIC: Bundle DNA
const serializedBioGeo = serializeGeometry(BIO_GEO);
const victoryPayload = {
tier: tier,
score: totalScore,
origin: "Beginner Bay",
genetic_source_code: {
bio_geo: serializedBioGeo,
dna_index: DNA_INDEX
}
};
gs.current.password = JSON.stringify(victoryPayload, null, 2);
gs.current.runStats=stats;
setUi(s=>({...s, status:'won', tier:gs.current.passwords[tier]}));
A.play('levelup');
}
}
}
} else if(e.hitCooldown<=0) {
P.impulse(p, 1.5, Math.atan2(p.y-e.y, p.x-e.x));
const dmg=Math.max(1, e.damage-p.stats.def); takePlayerDamage(dmg); e.hitCooldown=60; e.mouthOpen = true; e.mouthTimer = 15; A.play('chomp');
}
}
}
if(frame % 6 === 0) setUi(s=>({...s, hp:p.health, progress:gs.current.fishCount, abilityCd:p.ability.cooldown, timeLeft:remaining, score: gs.current.score}));
} else {
if(gs.current.status==='won') { p.x=width/2+Math.cos(tSec/2)*width*0.45; p.y=height/2+Math.sin(tSec/2)*height*0.45; p.angle=(tSec/2)+Math.PI/2; }
else { p.x=width/2+Math.cos(tSec)*width*0.35; p.y=height/2+Math.sin(tSec*2)*height*0.2; p.angle=Math.atan2(2*Math.cos(tSec*2)*height*0.2, -Math.sin(tSec)*width*0.35); }
}
if(p.hitFlash>0 && isPlaying) p.hitFlash--;
for(let i=ents.current.particles.length-1; i>=0; i--) { const pt=ents.current.particles[i]; P.move(pt, 1.0); pt.life-=pt.decay; if(pt.type==='shockwave') pt.size+=2; if(pt.type==='firefly') { pt.vx += (Math.random()-0.5)*0.1; pt.vy += (Math.random()-0.5)*0.1; } if(pt.type==='void_mote') { const dx = width/2 - pt.x; const dy = height/2 - pt.y; pt.x += dx * 0.02; pt.y += dy * 0.02; } if(pt.type==='swirl') { const dx = width/2 - pt.x; const dy = height/2 - pt.y; const angle = Math.atan2(dy, dx); pt.vx += Math.cos(angle + Math.PI/2) * 0.5 + Math.cos(angle) * 0.2; pt.vy += Math.sin(angle + Math.PI/2) * 0.5 + Math.sin(angle) * 0.2; pt.x += pt.vx; pt.y += pt.vy; } if(pt.life<=0) ents.current.particles.splice(i,1); }
for(let i=ents.current.floatingTexts.length-1; i>=0; i--) { const t=ents.current.floatingTexts[i]; t.y+=t.vy; t.life-=0.02; if(t.life<=0)ents.current.floatingTexts.splice(i,1); }
};
const draw = () => {
const ctx = canvasRef.current?.getContext('2d'); if(!ctx) return; const { width, height } = canvasRef.current;
ctx.save(); if(gs.current.shake>0) { ctx.translate((Math.random()-0.5)*gs.current.shake, (Math.random()-0.5)*gs.current.shake); gs.current.shake*=0.9; }
if(!gradCache.current.bg) {
const themeColors = cartridge.theme.background;
const g=ctx.createLinearGradient(0,0,0,height);
themeColors.forEach((col, i) => { g.addColorStop(i / (themeColors.length - 1), col); });
gradCache.current.bg = g;
}
ctx.fillStyle=gradCache.current.bg; ctx.fillRect(0,0,width,height);
cartridge.active_effects.forEach(effName => {
if(EFFECTS_LIBRARY[effName]) EFFECTS_LIBRARY[effName](ctx, width, height, gs.current.globalTime);
});
// DECOR LAYER 1 (Behind everything)
ctx.fillStyle='rgba(255,255,255,0.1)'; ents.current.decor.dust.forEach(d => { ctx.beginPath(); ctx.arc(d.x,d.y,d.size,0,Math.PI*2); ctx.fill(); });
if(ents.current.decor.school) DECOR_RENDERER.ambient_school(ctx, ents.current.decor.school, gs.current.globalTime);
if(!gradCache.current.sand) { gradCache.current.sand = D.color(ctx, cartridge.theme.sand, height*0.5); }
ctx.fillStyle=gradCache.current.sand; ctx.beginPath(); ctx.moveTo(0, height); for(let x=0;x<=width+20;x+=10) ctx.lineTo(x, height-40+D.noise(x, gs.current.seed)); ctx.lineTo(width+20, height); ctx.fill();
if(ents.current.decor.rocks) ents.current.decor.rocks.forEach(r => { D.draw(ctx, p => { for(let i=0; i<=8; i++) { const a=Math.PI+(i/8)*Math.PI, rad=(r.width/2)*(0.8+Math.sin(i*132.1+r.x*0.1)*0.2); const px=r.x+Math.cos(a)*rad, py=r.y+Math.sin(a)*(r.height/2); i===0?p.moveTo(px,py):p.lineTo(px,py); } p.closePath(); }, {f:D.color(ctx, r.color, r.width/2)}); });
// ARTISANAL DECOR
if(ents.current.decor.coral) DECOR_RENDERER.brain_coral(ctx, ents.current.decor.coral, gs.current.globalTime);
if(ents.current.decor.sponges) DECOR_RENDERER.tube_sponge(ctx, ents.current.decor.sponges, gs.current.globalTime);
if(ents.current.decor.kelp) DECOR_RENDERER.kelp_forest(ctx, ents.current.decor.kelp, gs.current.globalTime);
// WRAPPED DECOR RENDER CALLS IN D.ENT FOR PROPER TRANSLATION
ents.current.clams.forEach(c => {
D.ent(ctx, c.x, c.y, 0, c.size, 0, (kc, s) => {
BIO_GEO.shell.clam(kc, s, D.color(kc, c.color, s), D.color(kc, c.innerColor, s), c.isOpen);
if(c.hasPearl && c.isOpen) BIO_GEO.item.pearl(kc, s, gs.current.globalTime);
});
});
ents.current.crabs.forEach(c => {
D.ent(ctx, c.x, c.y, 0, c.size, 0, (kc, s) => {
BIO_GEO.extra.limbs_crab(kc, s, gs.current.globalTime, 0);
BIO_GEO.body.crab(kc, s, gs.current.globalTime, 0, c.color.colors); // Fix color passing
BIO_GEO.eye.stalks(kc, s, gs.current.globalTime, 0);
// PASS ENTITY c FOR STATE ACCESS
BIO_GEO.extra.claws(kc, s, gs.current.globalTime, 0, null, 0, 0, c);
});
});
ctx.save(); ctx.globalAlpha=0.5; ents.current.decor.bubbles.forEach(b=>{ctx.beginPath(); ctx.arc(b.x,b.y,b.size,0,Math.PI*2); ctx.fillStyle='rgba(255,255,255,0.4)'; ctx.fill();}); ctx.restore();
ents.current.enemies.forEach(e => R.char(ctx, e, gs.current.globalTime, false, false, false, false));
if(gs.current.status==='won') R.char(ctx, ents.current.player, gs.current.globalTime, true, false, true, true);
else if(gs.current.status==='lost') R.char(ctx, {...ents.current.player}, gs.current.globalTime, false, true, false, false);
else R.char(ctx, ents.current.player, gs.current.globalTime, true, false, true, false);
ents.current.particles.forEach(p => D.particle(ctx, p));
ents.current.floatingTexts.forEach(t => D.ent(ctx, t.x, t.y, 0, 1, 0, (c) => D.draw(c, null, {txt:t.text, f:t.color, s:'black', w:2, font:`bold ${t.size}px Arial`, alpha:t.life})));
// Custom Cursor
if(gs.current.status === 'playing') {
const cursor = { x: input.current.x, y: input.current.y, t: gs.current.globalTime };
ctx.save(); ctx.translate(cursor.x, cursor.y);
const dashReady = ents.current.player.ability.cooldown <= 0;
ctx.fillStyle = dashReady ? '#22d3ee' : '#555'; ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = dashReady ? '#22d3ee' : '#555'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI*2); ctx.stroke();
if(dashReady) { const pulse = Math.sin(cursor.t * 0.01) * 2 + 2; ctx.strokeStyle = `rgba(34, 211, 238, 0.5)`; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, 14 + pulse, 0, Math.PI*2); ctx.stroke(); }
if(!dashReady) { const ratio = ents.current.player.ability.cooldown / 60; ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; ctx.beginPath(); ctx.moveTo(0,0); ctx.arc(0,0, 10, -Math.PI/2, -Math.PI/2 + (Math.PI*2 * (1-ratio))); ctx.fill(); }
ctx.restore();
}
ctx.restore();
};
useEffect(() => { logicRef.current.update = updateEngine; logicRef.current.draw = draw; });
useEffect(() => {
// Mobile Detection Logic
const mobileCheck = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
setIsMobile(mobileCheck);
// AUDIO INIT ON CLICK
const interactHandler = () => { A.init(cartridge.assets); A.play('title'); };
window.addEventListener('click', interactHandler, { once: true });
window.addEventListener('keydown', interactHandler, { once: true });
const handleR = () => { setWSize({ w: window.innerWidth, h: window.innerHeight }); initDecor(window.innerWidth, window.innerHeight); };
window.addEventListener('resize', handleR); initDecor(window.innerWidth, window.innerHeight);
const handleM = (e) => { input.current.x = e.clientX; input.current.y = e.clientY; };
const handleD = (e) => {
if (e.target===canvasRef.current && gs.current.running && e.button===0) {
const p=ents.current.player;
if(p.ability.cooldown<=0){
p.ability.active=true; p.ability.cooldown=p.ability.maxCooldown;
A.play('dash');
P.impulse(p, p.stats.dashPwr, p.angle);
ents.current.particles.push({x:p.x,y:p.y,color:'rgba(255,255,255,0.5)',size:1,type:'shockwave',vx:0,vy:0,life:1,decay:0.05});
for(let i=0;i<12;i++) ents.current.particles.push({x:p.x,y:p.y,color:'rgba(255,255,255,0.8)',size:3,type:'circle',vx:(Math.random()-0.5)*5,vy:(Math.random()-0.5)*5,life:1,decay:0.05});
}
}
};
// Mobile Touch Handlers
const lastTapRef = { current: 0 };
const handleTouchMove = (e) => {
if (e.target !== canvasRef.current) return;
if(e.cancelable) e.preventDefault();
const touch = e.touches[0];
input.current.x = touch.clientX;
input.current.y = touch.clientY;
};
const handleTouchStart = (e) => {
if (e.target !== canvasRef.current) return;
if(e.cancelable) e.preventDefault();
const touch = e.touches[0];
input.current.x = touch.clientX;
input.current.y = touch.clientY;
const now = Date.now();
if (now - lastTapRef.current < 300) {
// Double Tap Action (Dash)
const p=ents.current.player;
if(p.ability.cooldown<=0){
p.ability.active=true; p.ability.cooldown=p.ability.maxCooldown;
A.play('dash');
P.impulse(p, p.stats.dashPwr, p.angle);
ents.current.particles.push({x:p.x,y:p.y,color:'rgba(255,255,255,0.5)',size:1,type:'shockwave',vx:0,vy:0,life:1,decay:0.05});
for(let i=0;i<12;i++) ents.current.particles.push({x:p.x,y:p.y,color:'rgba(255,255,255,0.8)',size:3,type:'circle',vx:(Math.random()-0.5)*5,vy:(Math.random()-0.5)*5,life:1,decay:0.05});
}
}
lastTapRef.current = now;
};
window.addEventListener('mousemove', handleM);
window.addEventListener('mousedown', handleD);
window.addEventListener('touchmove', handleTouchMove, { passive: false });
window.addEventListener('touchstart', handleTouchStart, { passive: false });
const requestRef = { current: 0 };
const loop = (t) => {
gs.current.globalTime = t;
if(logicRef.current.update) logicRef.current.update(t - lastTimeRef.current);
lastTimeRef.current = t;
if(logicRef.current.draw) logicRef.current.draw();
requestRef.current = requestAnimationFrame(loop);
};
requestRef.current = requestAnimationFrame(loop);
return () => {
window.removeEventListener('mousemove', handleM);
window.removeEventListener('mousedown', handleD);
window.removeEventListener('touchmove', handleTouchMove);
window.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('resize', handleR);
window.removeEventListener('click', interactHandler);
window.removeEventListener('keydown', interactHandler);
cancelAnimationFrame(requestRef.current);
A.stop('title');
A.stop('main');
};
}, [cartridge]);
useEffect(() => { A.setMute(mute); }, [mute]);
const UI_TEXT = {
menuTitle: "FISH GAME",
winTitle: "IS EVOLVING",
loseTitle: "YOU WENT EXTINCT",
instructions: isMobile
? [ "๐ Drag to Swim. Avoid bigger fish.", "โก Double Tap to Dash." ]
: [ "๐ Eat smaller fish. Avoid bigger ones.", "โก Left Click to Dash." ]
};
return (
<div className="flex flex-col items-center justify-center w-full h-screen bg-gray-900 relative overflow-hidden font-sans">
{/* CUSTOM SCROLLBAR CSS */}
<style>{`
.fish-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.fish-scroll::-webkit-scrollbar-track { background: rgba(0, 20, 40, 0.5); border-radius: 4px; }
.fish-scroll::-webkit-scrollbar-thumb { background: #22d3ee; border-radius: 4px; border: 1px solid rgba(0,0,0,0.3); }
.fish-scroll::-webkit-scrollbar-thumb:hover { background: #0891b2; }
`}</style>
<canvas ref={canvasRef} width={wSize.w} height={wSize.h} className="block cursor-none bg-blue-900" onContextMenu={(e)=>e.preventDefault()} />
<div className="absolute top-6 right-6 flex gap-2 z-50"><div className="cursor-pointer p-2 rounded-full bg-black/40 hover:bg-black/60 text-white transition-all" onClick={(e)=>{e.stopPropagation();setMute(!mute)}} onMouseDown={(e)=>e.stopPropagation()}><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">{ICONS_UI.speaker}{mute?ICONS_UI.x:ICONS_UI.waves}</svg></div></div>
{ui.status === 'playing' && (
<div className={`absolute top-6 left-6 bg-black/70 p-3 rounded-lg backdrop-blur-md border border-white/10 text-white shadow-2xl pointer-events-none select-none w-[285px] z-20 transition-all duration-300 ${ui.isObstructed?'opacity-20 blur-sm':'opacity-100'}`} style={{ transform: 'scale(0.7)', transformOrigin: 'top left' }}>
<div className="mb-2 border-b border-white/10 pb-1"><div className="text-[10px] font-mono uppercase tracking-widest mb-0.5" style={{ color: D.css(ui.color) }}>SPECIES: {ui.name}</div><div className="text-sm font-bold text-white leading-none mb-1">LVL 1: {cartridge.meta.title}</div></div>
<StatBar label="HEALTH" value={ui.hp} max={ui.maxHp} colorFrom="from-red-600" colorTo="to-red-400" showValue={true} />
<div className="flex mt-2 pt-2 border-t border-white/10 overflow-x-auto scrollbar-hide fish-scroll"><AbilitySquare label="DASH" cooldown={ui.abilityCd} max={ui.abilityMax} icon="โก" /><AbilitySquare locked={true} icon="" /><StatSquare label="GOAL" value={`${ui.progress}/${ui.reqProgress}`} color="text-green-400" /><StatSquare label="TIME" value={Math.ceil(ui.timeLeft)} color="text-yellow-400" /><StatSquare label="SCORE" value={ui.score} color="text-orange-400" /></div>
</div>
)}
{ui.status !== 'playing' && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40 backdrop-blur-sm z-10 p-4">
<div className="flex flex-col items-center max-h-full overflow-y-auto w-full">
<h1 className="text-4xl md:text-6xl font-black text-transparent bg-clip-text bg-gradient-to-b from-blue-300 to-blue-600 mb-6 drop-shadow-[0_4px_4px_rgba(0,0,0,0.5)] animate-pulse text-center">{ui.status==='menu'?UI_TEXT.menuTitle:ui.status==='won'?<><div style={{color:D.css(ui.color)}}>YOUR {ui.name}</div><div className="text-white text-3xl md:text-4xl mt-2">{UI_TEXT.winTitle}</div></>:<span className="text-red-600">{UI_TEXT.loseTitle}</span>}</h1>
{ui.status === 'menu' && <div className="mb-8 text-center"><div className="text-blue-200 text-sm font-mono mb-4 bg-black/50 p-4 rounded-lg border border-white/10 inline-block"><div className="font-bold text-white mb-2 border-b border-white/20 pb-1">HOW TO PLAY</div><p className="mb-2">{UI_TEXT.instructions[0]}</p><p className="mb-4">{UI_TEXT.instructions[1]}</p><a href="https://www.youtube.com/@realSpaceKangaroo/videos" target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-purple-400 animate-pulse mt-2 block hover:text-purple-300" onClick={(e)=>e.stopPropagation()}>๐๐ฆ TUTORIAL (By Space Kangaroo)</a></div></div>}
{ui.status === 'won' && <div className="mb-6 p-6 bg-blue-900/90 rounded-lg border-2 border-blue-400 text-center shadow-xl w-full max-w-md max-h-[60vh] overflow-y-auto fish-scroll"><div className="text-xl font-bold text-blue-200 mb-1 tracking-wider">SKILL LEVEL:</div><div className="text-6xl mb-4 drop-shadow-lg">{ui.tier}</div><div className="text-sm text-blue-200 mb-4 font-mono grid grid-cols-3 gap-4 border-b border-white/10 pb-2"><div><span className="block text-gray-400 text-xs">TIME</span><span className="font-bold text-white">{gs.current.runStats.time}s</span></div><div><span className="block text-gray-400 text-xs">HEALTH</span><span className="font-bold text-green-400">{gs.current.runStats.hp}%</span></div><div><span className="block text-gray-400 text-xs">BONUS</span><span className="font-bold text-pink-400">{gs.current.runStats.bonuses}</span></div></div><div className="bg-black/50 p-3 rounded border border-white/10 text-left mb-2 relative"><p className="text-gray-400 text-[10px] mb-1 uppercase tracking-wider">COPY AND PASTE TO GEMINI:</p><code className="block text-[10px] font-mono text-green-300 whitespace-pre-wrap break-all select-all cursor-text mb-2 max-h-24 overflow-y-auto p-1 border border-white/5 rounded fish-scroll">{gs.current.password}</code><button onClick={(e)=>{const t=document.createElement("textarea");t.value=gs.current.password;document.body.appendChild(t);t.select();document.execCommand('copy');setCopied(true);A.play('click');setTimeout(()=>setCopied(false),2000);document.body.removeChild(t)}} className={`absolute top-2 right-2 p-1 rounded hover:bg-white/10 ${copied?'text-green-400':'text-gray-400'}`}>{copied?<span className="text-[10px] font-bold">COPIED!</span>:<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">{ICONS_UI.copy}</svg>}</button></div></div>}
<button onClick={startGame} className="px-8 py-4 bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-400 hover:to-emerald-500 text-white font-bold rounded-full text-xl hover:scale-105 shadow-[0_0_20px_rgba(16,185,129,0.5)] border-2 border-white/20 active:scale-95 flex-shrink-0">{ui.status === 'menu' ? 'START LIFE' : ui.status === 'won' ? 'PLAY AGAIN' : 'TRY AGAIN'}</button>
</div>
</div>
)}
</div>
);
};
export default App;
Discussion
live ยท sign in with Google to comment