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(); } }
}
},
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;


