top of page
Search

Infinite Fish Game

  • 55 minutes ago
  • 29 min read

Copy the prompt below into Gemini 3 with pro and canvas selected. Chatgpt and claude are capable of running this from an intellgience perspective but have a limit on how much code they are allowed to load into artifacts so it won't work in those. It will work in coding editors like claude code and codex but might need slight modification:

COPY AND PASTE THIS PROMPT: 🧬 FISH GAME - MASTER PROTOCOL v3.2 (TITANIUM + COMPLETE EDITION)IDENTITYRole: 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 COMPLIANCESVG 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 STANDARDSA. THE "NO LAZY MUTATION" LAWWhen 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 SYNCThe 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 LOOP1. 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)Visual:// 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)Visual:// 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)Visual:// 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)Visual:// 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 MANDATESA. The "Legacy Support" RuleAlways include BIO_GEO.misc.mouth and BIO_GEO.misc.deadX.B. The "Input" ProtocolDesktop: 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" BlueprintWrite 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" ProtocolUI container must fade (opacity: 0.2) when the player is near (dist < 300px) to prevent visual obstruction.E. Environmental Powerup LawEvery 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 RobustnessScrollable 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 Visualsctx.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.mp3Level 1 (Tropical): https://static.wixstatic.com/mp3/1fd518_af7ca187a0294ca8b88f0d7746b77e75.mp3Level 2 (Volcanic): https://static.wixstatic.com/mp3/1fd518_a3b38fbbb8344974b189bd48b6d3e727.mp3Level 3 (Deep): https://static.wixstatic.com/mp3/1fd518_98bfe85e7a6c499ebe626aedea8aba67.mp3Boss Theme: https://static.wixstatic.com/mp3/1fd518_6acb100e13304fd09baafef15dcb4f27.mp3Victory: https://static.wixstatic.com/mp3/1fd518_08454420d54049e1a4b8250fa8e15275.mp3Game Over: https://static.wixstatic.com/mp3/1fd518_795df267296b4daca2bb3c370bc7e7c4.mp3Extended Library (Pick whatever fits best):Tension/Sinking: https://static.wixstatic.com/mp3/1fd518_5288d7780fae4e62aa330b97e30272b0.mp3Upbeat/Easy: https://static.wixstatic.com/mp3/1fd518_6ac130eb22c94456a3830c2cf18c25fd.mp3Level Win: https://static.wixstatic.com/mp3/1fd518_08454420d54049e1a4b8250fa8e15275.mp3Title Screen: https://static.wixstatic.com/mp3/1fd518_f938740eb75642cf9f695746d94559f5.mp3Game Over: https://static.wixstatic.com/mp3/1fd518_795df267296b4daca2bb3c370bc7e7c4.mp3Level 1 Theme: https://static.wixstatic.com/mp3/1fd518_af7ca187a0294ca8b88f0d7746b77e75.mp3Tropical Chill: https://static.wixstatic.com/mp3/1fd518_fd787dfff04f469884c9e2399e0e2051.mp3Tropical Driven: https://static.wixstatic.com/mp3/1fd518_65d549655b16413c882d157aa546cfd0.mp3Calm Underwater: https://static.wixstatic.com/mp3/1fd518_abd5d5f6a7924d5d9e01aef1ad7a8039.mp3Major/Urgent: https://static.wixstatic.com/mp3/1fd518_605ddee8ee2e4826b33f5868aedc1cf3.mp3Industrial/City: https://static.wixstatic.com/mp3/1fd518_52398ca35355421aa0ae2c1788ad7736.mp3Heating Up: https://static.wixstatic.com/mp3/1fd518_044263f469df405da24debd2886bd3c3.mp3Melancholy Depths: https://static.wixstatic.com/mp3/1fd518_98bfe85e7a6c499ebe626aedea8aba67.mp3Fast Industrial: https://static.wixstatic.com/mp3/1fd518_770d979675f64e0ab7c55be83135afcd.mp3Deep Sinking: https://static.wixstatic.com/mp3/1fd518_90508a0df39e48eb9081198b2d0f6eec.mp3Resting/Possibility: https://static.wixstatic.com/mp3/1fd518_4cab648a3211463393bf91c196f782ba.mp3BOSS (Robot Shark): https://static.wixstatic.com/mp3/1fd518_6acb100e13304fd09baafef15dcb4f27.mp3Pirate: https://static.wixstatic.com/mp3/1fd518_af090a725b5144b7b60f7fef4de6c717.mp3Penultimate: https://static.wixstatic.com/mp3/1fd518_2ba4c8f87b9b44e9b2b3925438446e62.mp3Ocean Ruin: https://static.wixstatic.com/mp3/1fd518_afc6fddef81c4f61a55015adca1fe1c3.mp3SECRET BOSS (Cyber Shark): https://static.wixstatic.com/mp3/1fd518_011620c363514bf5a8eb91aa3017725a.mp3Mysterious Deep: https://static.wixstatic.com/mp3/1fd518_ebdf707c3f884e75a1c791deef575866.mp3Winter/Arctic: https://static.wixstatic.com/mp3/1fd518_e4abb959d69c4e98902450c9277abf6f.mp3Credits: https://static.wixstatic.com/mp3/1fd518_826f8f0ea6ac47bab7fab5d46d90c39b.mp3Epic Upbeat: https://static.wixstatic.com/mp3/1fd518_06b9b48c12d642779d87b2281eab8256.mp3Lava/Magma: https://static.wixstatic.com/mp3/1fd518_a3b38fbbb8344974b189bd48b6d3e727.mp3Adventure: https://static.wixstatic.com/mp3/1fd518_fee032b994a14c3b9d8f919ded4e289e.mp3Hard Level: https://static.wixstatic.com/mp3/1fd518_84fc310b85c84db4a99935680bfbfe58.mp3Midgame Sinking: https://static.wixstatic.com/mp3/1fd518_a64bce10248c40a782e27231801ed41c.mp3Haunted/Ghost Ship: https://static.wixstatic.com/mp3/1fd518_d3a426c6b4f84ca985d3225b692b4fd7.mp3Fast Paced: https://static.wixstatic.com/mp3/1fd518_d16c96132aad4809a4e103bca271d644.mp3Kelp Forest: https://static.wixstatic.com/mp3/1fd518_a3cd423ee432474380045b7c6079c58f.mp3Pirate Shanty: https://static.wixstatic.com/mp3/1fd518_b138b0b2b5114e018d14c89ef75d130c.mp3Shallow Reef: https://static.wixstatic.com/mp3/1fd518_4c3b03ae556543c58da7e2db9b49cbd1.mp3Ironic Game Over: https://static.wixstatic.com/mp3/1fd518_55336ab76785455ab8d041e1f3f1ad5b.mp3Unserious/Easy: https://static.wixstatic.com/mp3/1fd518_b019e4857d1b45c085feda6a6b4c7d6c.mp3Sky/Clouds: https://static.wixstatic.com/mp3/1fd518_5ff0ed46643b4e6fbed0bd9bc72d9639.mp3Arctic Calm: https://static.wixstatic.com/mp3/1fd518_eda8e0a64e8e464589376e5690346f91.mp3Sinking/Drop: https://static.wixstatic.com/mp3/1fd518_d85900ebd3a344809a1dbe96408f557e.mp3Bottom of Ocean: https://static.wixstatic.com/mp3/1fd518_ceed5059597b4340abd0fd7f98e10b08.mp3Cave/Trench: https://static.wixstatic.com/mp3/1fd518_347b017c1a074fcfa00dcb924318ec0b.mp3Standard Ocean: https://static.wixstatic.com/mp3/1fd518_f3db423cb3e841cc91f2d13ae59778be.mp3Galactic/Space: https://static.wixstatic.com/mp3/1fd518_86fc01e72b2e4f00b47ac39482ca617c.mp3Abyss/Caving: https://static.wixstatic.com/mp3/1fd518_c912b6b9e5a54cbcaa8969a723be4ba8.mp3Intense Conflict: https://static.wixstatic.com/mp3/1fd518_1fd825ef9aa24eebad0f38ce98ebfa82.mp3Late Game Intense: https://static.wixstatic.com/mp3/1fd518_73c5a106e2f54f8a856422f59e2bf518.mp3Mountain/Trench: https://static.wixstatic.com/mp3/1fd518_d59cec5483ee47aa8e5cd3bc7a35ffbe.mp3Ghost Ship (Ominous): https://static.wixstatic.com/mp3/1fd518_c1986b45e9c44675b8d737e5dbaa7dcf.mp3High Seas: https://static.wixstatic.com/mp3/1fd518_7b9734f4968643cc861c6bb750ddc32b.mp3Mysterious Forest: https://static.wixstatic.com/mp3/1fd518_e5dc86cff6604d16a031d93f08233b4b.mp3Beach: https://static.wixstatic.com/mp3/1fd518_5957db4ee5694c9e9cd9509b189c29df.mp3RESPONSE 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;


 
 
 
SUBSCRIBE TODAY!
  • Youtube
  • Twitter
  • Reddit

© 2023 Space Kangaroo Inc.

bottom of page