Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>SLIMERA – Devour & Evolve</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:#1a3a1a;font-family:'Nunito',sans-serif}
#gameCanvas{display:block;touch-action:none}
#ui{position:fixed;top:0;left:0;width:100%;pointer-events:none;z-index:10}
.hud{display:flex;align-items:flex-start;justify-content:space-between;padding:10px 14px;gap:8px}
.hud-left{display:flex;flex-direction:column;gap:5px;pointer-events:all}
.hud-right{display:flex;flex-direction:column;align-items:flex-end;gap:5px;pointer-events:all}
.stat-bar-wrap{background:rgba(0,0,0,.45);border-radius:20px;padding:4px 8px;display:flex;align-items:center;gap:6px;min-width:160px}
.stat-icon{font-size:13px}
.stat-bar-bg{flex:1;height:10px;background:rgba(255,255,255,.18);border-radius:6px;overflow:hidden}
.stat-bar-fill{height:100%;border-radius:6px;transition:width .3s}
.bar-hp{background:linear-gradient(90deg,#e74c3c,#ff6b6b)}
.bar-xp{background:linear-gradient(90deg,#f39c12,#f9ca24)}
.stat-val{font-size:11px;color:#fff;font-weight:700;min-width:32px;text-align:right}
.level-badge{background:rgba(0,0,0,.5);border:2px solid #f9ca24;border-radius:10px;padding:3px 10px;color:#f9ca24;font-family:'Fredoka One',sans-serif;font-size:14px}
.ability-bar{display:flex;gap:5px;pointer-events:all}
.ability-slot{width:42px;height:42px;background:rgba(0,0,0,.5);border:2px solid rgba(255,255,255,.25);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;position:relative;cursor:pointer;transition:border-color .2s}
.ability-slot.active{border-color:#00e5ff;box-shadow:0 0 8px #00e5ff88}
.ability-slot.ready{border-color:#2ecc71}
.ability-slot .cd-overlay{position:absolute;inset:0;background:rgba(0,0,0,.65);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:800}
.ability-slot .key-hint{position:absolute;bottom:2px;right:4px;font-size:8px;color:rgba(255,255,255,.5);font-weight:700}
.msg-popup{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.78);border:2px solid #f9ca24;border-radius:14px;padding:14px 22px;color:#fff;text-align:center;z-index:50;pointer-events:none;transition:opacity .5s;max-width:260px}
.msg-popup h2{font-family:'Fredoka One',sans-serif;font-size:22px;color:#f9ca24;margin-bottom:4px}
.msg-popup p{font-size:13px;color:#ddd;line-height:1.5}
.msg-popup.hidden{opacity:0;pointer-events:none}
/* Mobile controls */
#mobileControls{position:fixed;bottom:10px;left:0;width:100%;display:flex;justify-content:space-between;align-items:flex-end;padding:0 14px;z-index:20;pointer-events:none}
.joystick-area{position:relative;width:110px;height:110px;pointer-events:all}
#joystickBase{width:110px;height:110px;background:rgba(0,0,0,.3);border:2px solid rgba(255,255,255,.2);border-radius:50%;position:absolute}
#joystickKnob{width:46px;height:46px;background:rgba(255,255,255,.35);border:2px solid rgba(255,255,255,.6);border-radius:50%;position:absolute;top:32px;left:32px;transition:background .1s}
.action-btns{display:flex;flex-direction:column;gap:8px;pointer-events:all}
.act-btn{width:58px;height:58px;border-radius:50%;border:3px solid rgba(255,255,255,.35);background:rgba(0,0,0,.4);color:#fff;font-size:11px;font-weight:800;font-family:'Fredoka One',sans-serif;text-align:center;line-height:1.2;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-tap-highlight-color:transparent;user-select:none;letter-spacing:.5px}
.act-btn:active{filter:brightness(1.4)}
#btnDevour{border-color:#e74c3c;background:rgba(180,20,20,.4);font-size:13px}
#btnAbility{border-color:#00e5ff;background:rgba(0,80,140,.4);font-size:22px}
/* Overlays */
.screen{position:fixed;inset:0;z-index:100;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(10,25,10,.92);backdrop-filter:blur(6px)}
.screen.hidden{display:none}
.screen-title{font-family:'Fredoka One',sans-serif;font-size:clamp(36px,10vw,58px);letter-spacing:.05em;margin-bottom:6px}
.screen-sub{font-size:14px;color:#aed6a0;margin-bottom:24px;text-align:center;max-width:300px;line-height:1.6}
.big-btn{font-family:'Fredoka One',sans-serif;font-size:18px;padding:13px 38px;border:none;border-radius:50px;cursor:pointer;letter-spacing:.05em;transition:all .15s;margin:5px}
.big-btn:hover,.big-btn:active{filter:brightness(1.15);transform:scale(1.03)}
.btn-green{background:linear-gradient(135deg,#27ae60,#2ecc71);color:#fff;box-shadow:0 4px 16px #27ae6066}
.btn-red{background:linear-gradient(135deg,#c0392b,#e74c3c);color:#fff;box-shadow:0 4px 16px #c0392b66}
/* Ability unlock popup */
#abilityUnlock{position:fixed;top:18%;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,#1a3a4a,#0d2233);border:3px solid #00e5ff;border-radius:18px;padding:16px 22px;z-index:60;pointer-events:none;text-align:center;min-width:220px;max-width:280px}
#abilityUnlock h3{font-family:'Fredoka One',sans-serif;font-size:18px;color:#00e5ff;margin-bottom:4px}
#abilityUnlock p{font-size:12px;color:#adf;line-height:1.5}
#abilityUnlock.hidden{display:none}
/* minimap */
#minimap{position:fixed;top:10px;right:10px;z-index:15;pointer-events:none}
#minimapCanvas{border:2px solid rgba(255,255,255,.25);border-radius:6px;background:rgba(0,0,0,.3)}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div class="hud">
<div class="hud-left">
<div class="level-badge" id="levelBadge">LV 1 – Micro Slime</div>
<div class="stat-bar-wrap"><span class="stat-icon">❤️</span><div class="stat-bar-bg"><div class="stat-bar-fill bar-hp" id="hpBar" style="width:100%"></div></div><span class="stat-val" id="hpVal">100</span></div>
<div class="stat-bar-wrap"><span class="stat-icon">⭐</span><div class="stat-bar-bg"><div class="stat-bar-fill bar-xp" id="xpBar" style="width:0%"></div></div><span class="stat-val" id="xpVal">0</span></div>
</div>
<div class="hud-right">
<div class="ability-bar" id="abilityBar"></div>
</div>
</div>
</div>
<div id="mobileControls">
<div class="joystick-area" id="joystickArea">
<div id="joystickBase"></div>
<div id="joystickKnob"></div>
</div>
<div class="action-btns">
<div class="act-btn" id="btnAbility">✨</div>
<div class="act-btn" id="btnDevour">EAT</div>
</div>
</div>
<div class="msg-popup hidden" id="msgPopup"><h2 id="msgTitle"></h2><p id="msgBody"></p></div>
<div id="abilityUnlock" class="hidden"><h3 id="unlockName"></h3><p id="unlockDesc"></p></div>
<!-- START SCREEN -->
<div class="screen" id="startScreen">
<div class="screen-title" style="color:#7dff7a;text-shadow:0 0 20px #7dff7a88">SLIMERA</div>
<div class="screen-sub">You are a mysterious slime. Devour creatures, absorb their powers,<br>grow enormous & conquer the wild!</div>
<button class="big-btn btn-green" id="startBtn">🌿 BEGIN JOURNEY</button>
<div style="font-size:12px;color:#668866;margin-top:12px">WASD / Arrow keys or on-screen joystick<br>SPACE / EAT button to devour nearby creatures</div>
</div>
<!-- GAME OVER SCREEN -->
<div class="screen hidden" id="gameOverScreen">
<div class="screen-title" style="color:#e74c3c">DEVOURED</div>
<div class="screen-sub" id="gameOverText">You were overcome by a stronger creature.</div>
<button class="big-btn btn-green" id="restartBtn">🔄 Try Again</button>
</div>
<script>
"use strict";
// ─────────────────────────────────────────────
// CONSTANTS & CONFIG
// ─────────────────────────────────────────────
const WORLD_W = 3200, WORLD_H = 3200;
const TILE = 64;
const COLS = WORLD_W / TILE, ROWS = WORLD_H / TILE;
// Level thresholds (XP needed)
const LEVEL_XP = [0,30,80,160,280,450,680,980,1360,1830,2400,3100,4000,5200,6700];
const LEVEL_NAMES = [
"Micro Slime","Tiny Slime","Small Slime","Blob","Plump Blob",
"Pudding Slime","Bouncy Slime","Jelly Beast","Ooze Crawler","Gooey Horror",
"Slime Titan","Ancient Ooze","Omega Blob","Void Slime","SLIMERA"
];
// ─────────────────────────────────────────────
// CREATURE DEFINITIONS
// ─────────────────────────────────────────────
const CREATURES = [
// id, name, emoji-style (drawn via canvas), level, xp, hp, speed, size, color, rarity, zone, ability
{ id:"ant", name:"Ant", lv:1, xp:5, hp:8, spd:1.4, sz:6, col:"#a0522d",col2:"#7b3b1e", rare:false, zone:"grass", abilityId:null },
{ id:"fly", name:"Fly", lv:1, xp:6, hp:6, spd:2.2, sz:5, col:"#555", col2:"#333", rare:false, zone:"grass", abilityId:"dash" },
{ id:"beetle", name:"Beetle", lv:2, xp:12, hp:18, spd:0.9, sz:9, col:"#1a5c1a",col2:"#0d3d0d", rare:false, zone:"grass", abilityId:null },
{ id:"butterfly", name:"Butterfly", lv:2, xp:10, hp:7, spd:2.0, sz:8, col:"#e056a0",col2:"#b0206a", rare:false, zone:"grass", abilityId:null },
{ id:"worm", name:"Earthworm", lv:1, xp:8, hp:10, spd:0.7, sz:7, col:"#d27b5c",col2:"#b05a40", rare:false, zone:"grass", abilityId:null },
{ id:"spider", name:"Spider", lv:3, xp:18, hp:22, spd:1.8, sz:10, col:"#1a1a2e",col2:"#0d0d1a", rare:false, zone:"forest",abilityId:"web" },
{ id:"frog", name:"Frog", lv:3, xp:20, hp:28, spd:1.5, sz:12, col:"#3cb371",col2:"#267350", rare:false, zone:"water", abilityId:"jump" },
{ id:"snail", name:"Snail", lv:2, xp:14, hp:20, spd:0.5, sz:10, col:"#c8a060",col2:"#8b5e20", rare:false, zone:"grass", abilityId:"shell" },
{ id:"grasshopper",name:"Grasshopper",lv:3,xp:16, hp:16, spd:2.5, sz:11, col:"#5a8f2a",col2:"#3a6010", rare:false, zone:"grass", abilityId:"jump" },
{ id:"ladybug", name:"Ladybug", lv:2, xp:11, hp:14, spd:1.2, sz:8, col:"#e02020",col2:"#a01010", rare:false, zone:"grass", abilityId:null },
{ id:"mouse", name:"Mouse", lv:4, xp:28, hp:35, spd:2.0, sz:13, col:"#b0a090",col2:"#807060", rare:false, zone:"forest",abilityId:null },
{ id:"rabbit", name:"Rabbit", lv:5, xp:40, hp:50, spd:2.8, sz:16, col:"#e8dcc8",col2:"#c4b090", rare:false, zone:"grass", abilityId:"dash" },
{ id:"lizard", name:"Lizard", lv:5, xp:38, hp:55, spd:1.8, sz:15, col:"#5a9e40",col2:"#3a6e28", rare:false, zone:"forest",abilityId:null },
{ id:"fish", name:"Goldfish", lv:3, xp:22, hp:24, spd:2.2, sz:11, col:"#ff7f00",col2:"#cc5500", rare:false, zone:"water", abilityId:null },
{ id:"crab", name:"Crab", lv:5, xp:36, hp:60, spd:1.1, sz:15, col:"#e04020",col2:"#a02010", rare:false, zone:"water", abilityId:"pinch" },
{ id:"turtle", name:"Turtle", lv:6, xp:50, hp:80, spd:0.7, sz:18, col:"#4a8040",col2:"#2a5020", rare:false, zone:"water", abilityId:"shell" },
{ id:"snake_e", name:"Snake", lv:6, xp:55, hp:70, spd:2.0, sz:14, col:"#78a030",col2:"#4a6820", rare:false, zone:"forest",abilityId:"poison" },
{ id:"crow", name:"Crow", lv:7, xp:65, hp:75, spd:3.0, sz:16, col:"#222", col2:"#111", rare:false, zone:"forest",abilityId:"dive" },
{ id:"fox", name:"Fox", lv:8, xp:90, hp:100, spd:2.5, sz:20, col:"#d86820",col2:"#a04010", rare:false, zone:"forest",abilityId:"dash" },
{ id:"owl", name:"Owl", lv:8, xp:85, hp:90, spd:2.0, sz:18, col:"#8b6b40",col2:"#5a4020", rare:false, zone:"forest",abilityId:"night" },
{ id:"deer", name:"Deer", lv:9, xp:110, hp:130, spd:2.8, sz:22, col:"#c8a060",col2:"#906030", rare:false, zone:"forest",abilityId:null },
{ id:"boar", name:"Boar", lv:10,xp:140, hp:160, spd:2.0, sz:24, col:"#7a5a40",col2:"#503a28", rare:false, zone:"forest",abilityId:"charge" },
// WATER SPECIALS
{ id:"jellyfish", name:"Jellyfish", lv:4, xp:30, hp:30, spd:0.8, sz:14, col:"#9b59b6",col2:"#7b39a0", rare:true, zone:"water", abilityId:"zap" },
{ id:"octopus", name:"Octopus", lv:7, xp:80, hp:85, spd:1.5, sz:18, col:"#8e44ad",col2:"#5d2d80", rare:true, zone:"water", abilityId:"ink" },
// RARE
{ id:"firefly", name:"Firefly", lv:3, xp:25, hp:12, spd:2.5, sz:7, col:"#f0e040",col2:"#c0b000", rare:true, zone:"grass", abilityId:"glow" },
{ id:"scorpion", name:"Scorpion", lv:6, xp:60, hp:65, spd:1.6, sz:14, col:"#c8a020",col2:"#907010", rare:true, zone:"grass", abilityId:"sting" },
{ id:"chameleon", name:"Chameleon", lv:7, xp:75, hp:70, spd:1.3, sz:17, col:"#40c080",col2:"#208050", rare:true, zone:"forest",abilityId:"camouflage" },
{ id:"dragonfly", name:"Dragonfly", lv:4, xp:32, hp:20, spd:3.5, sz:10, col:"#20aaff",col2:"#0080cc", rare:true, zone:"water", abilityId:"dash" },
{ id:"mantis", name:"Mantis", lv:5, xp:45, hp:48, spd:2.0, sz:13, col:"#58b840",col2:"#308020", rare:true, zone:"grass", abilityId:"strike" },
{ id:"axolotl", name:"Axolotl", lv:5, xp:50, hp:55, spd:1.4, sz:16, col:"#ff9ec8",col2:"#e06898", rare:true, zone:"water", abilityId:"regen" },
{ id:"bearCub", name:"Bear Cub", lv:11,xp:180, hp:220, spd:1.8, sz:28, col:"#8b5e2c",col2:"#5a3a18", rare:false, zone:"forest",abilityId:"roar" },
{ id:"wolf", name:"Wolf", lv:12,xp:220, hp:260, spd:3.0, sz:26, col:"#888", col2:"#555", rare:false, zone:"forest",abilityId:"howl" },
{ id:"eagle", name:"Eagle", lv:13,xp:280, hp:300, spd:3.5, sz:28, col:"#8b6020",col2:"#5a3e10", rare:true, zone:"forest",abilityId:"dive" },
];
// ─────────────────────────────────────────────
// ABILITY DEFINITIONS
// ─────────────────────────────────────────────
const ABILITIES = {
dash: { name:"Speed Dash", emoji:"💨", desc:"Burst forward at 3× speed for 1.5s", cd:8, duration:1500, type:"move" },
web: { name:"Sticky Web", emoji:"🕸️", desc:"Slow nearby enemies for 3s", cd:12, duration:3000, type:"area" },
jump: { name:"Leap", emoji:"🦘", desc:"Leap over obstacles & stun on land", cd:7, duration:800, type:"move" },
shell: { name:"Hard Shell", emoji:"🛡️", desc:"Block 80% damage for 2s", cd:15, duration:2000, type:"defend" },
poison: { name:"Venom Spit", emoji:"☠️", desc:"Spit poison, DoT 3 dmg/s for 4s", cd:10, duration:4000, type:"attack" },
zap: { name:"Zap", emoji:"⚡", desc:"Electric shock stuns nearest foe 2s", cd:9, duration:2000, type:"attack" },
ink: { name:"Ink Cloud", emoji:"🌑", desc:"Blind nearby enemies for 3s", cd:14, duration:3000, type:"area" },
glow: { name:"Glow Lure", emoji:"✨", desc:"Lure nearby creatures toward you", cd:11, duration:3000, type:"area" },
sting: { name:"Venom Sting", emoji:"🦂", desc:"Instant 30 damage to target", cd:8, duration:500, type:"attack" },
camouflage: { name:"Camouflage", emoji:"👻", desc:"Turn invisible to enemies for 4s", cd:18, duration:4000, type:"defend" },
strike: { name:"Power Strike", emoji:"⚔️", desc:"Double devour success chance", cd:10, duration:3000, type:"attack" },
regen: { name:"Regen", emoji:"💚", desc:"Regenerate 30 HP over 5s", cd:20, duration:5000, type:"heal" },
pinch: { name:"Crab Pinch", emoji:"🦀", desc:"Deal 25 damage + stun for 1s", cd:9, duration:1000, type:"attack" },
night: { name:"Night Vision", emoji:"🦉", desc:"Reveal all creatures on map for 8s", cd:25, duration:8000, type:"util" },
charge: { name:"Boar Charge", emoji:"🐗", desc:"Charge forward, stun & deal 40 dmg", cd:12, duration:1200, type:"attack" },
roar: { name:"Bear Roar", emoji:"🐻", desc:"Frighten all nearby creatures away", cd:20, duration:3000, type:"area" },
howl: { name:"Wolf Howl", emoji:"🐺", desc:"+50% speed & attack for 5s", cd:25, duration:5000, type:"buff" },
dive: { name:"Eagle Dive", emoji:"🦅", desc:"Instantly devour any creature ≤ your lv",cd:30, duration:1000, type:"attack" },
};
// ─────────────────────────────────────────────
// GAME STATE
// ─────────────────────────────────────────────
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H;
function resize(){
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
// Camera
const cam = { x:0, y:0 };
// Player
const player = {
x: WORLD_W/2, y: WORLD_H/2,
vx:0, vy:0,
hp:100, maxHp:100,
xp:0, level:1,
size:14, baseSpeed:2.2,
color:"#7dff7a", color2:"#4acc45",
abilities:[], activeAbilityIdx:0,
abilityCooldowns:{},
abilityEndTimes:{},
activeEffects:{}, // effectId -> endTime
wobble:0, wobbleV:0,
facing:0, // angle
devourAnim:0,
dead:false,
invincible:0, // ms
};
// World objects
let creatures = [];
let particles = [];
let floatingTexts = [];
let tiles = []; // tile types
let decorations = [];
let msgTimer = 0;
let gameRunning = false;
let lastTime = 0;
let keys = {};
let joystick = { active:false, dx:0, dy:0 };
let minimapCanvas, minimapCtx;
// ─────────────────────────────────────────────
// TILE / WORLD GENERATION
// ─────────────────────────────────────────────
function genWorld(){
tiles = [];
for(let r=0;r<ROWS;r++){
tiles[r]=[];
for(let c=0;c<COLS;c++){
// Create zones: center=grass, pockets of water and forest
const wx = c/COLS, wy = r/ROWS;
// noise-ish
const n = Math.sin(c*0.3)*Math.cos(r*0.2) + Math.sin(c*0.07+r*0.11)*1.5;
let t;
if(n > 1.1) t = 'water';
else if(n > 0.4) t = 'deep_grass';
else if(n < -0.9) t = 'forest';
else t = 'grass';
// border water
if(c<2||c>COLS-3||r<2||r>ROWS-3) t='water';
tiles[r][c]=t;
}
}
// Build decorations (trees, rocks, flowers, lily pads)
decorations = [];
for(let r=0;r<ROWS;r++){
for(let c=0;c<COLS;c++){
const t = tiles[r][c];
if(t==='forest' && Math.random()<0.35){
decorations.push({type:'tree',x:c*TILE+Math.random()*TILE,y:r*TILE+Math.random()*TILE,s:18+Math.random()*12,variant:Math.floor(Math.random()*3)});
} else if(t==='deep_grass' && Math.random()<0.25){
decorations.push({type:'bush',x:c*TILE+Math.random()*TILE,y:r*TILE+Math.random()*TILE,s:8+Math.random()*6});
} else if(t==='grass' && Math.random()<0.08){
decorations.push({type:'flower',x:c*TILE+Math.random()*TILE,y:r*TILE+Math.random()*TILE,col:['#ff69b4','#ffd700','#ff4500','#da70d6'][Math.floor(Math.random()*4)]});
} else if(t==='water' && Math.random()<0.06){
decorations.push({type:'lily',x:c*TILE+Math.random()*TILE,y:r*TILE+Math.random()*TILE,s:10+Math.random()*8});
} else if(t==='forest' && Math.random()<0.05){
decorations.push({type:'rock',x:c*TILE+Math.random()*TILE,y:r*TILE+Math.random()*TILE,s:7+Math.random()*8});
}
}
}
}
function tileAt(wx,wy){
const c = Math.floor(wx/TILE), r = Math.floor(wy/TILE);
if(c<0||c>=COLS||r<0||r>=ROWS) return 'water';
return tiles[r][c];
}
// ─────────────────────────────────────────────
// SPAWN CREATURES
// ─────────────────────────────────────────────
function spawnCreatures(){
creatures = [];
const total = 280;
for(let i=0;i<total;i++){
spawnRandomCreature();
}
}
function spawnRandomCreature(){
// pick random creature def
const defs = CREATURES;
// bias toward lower level creatures
let def;
const r = Math.random();
if(r < 0.02) {
// rare
const rares = defs.filter(d=>d.rare);
def = rares[Math.floor(Math.random()*rares.length)];
} else {
// weighted by inv level
const pool = defs.filter(d=>!d.rare);
const weights = pool.map(d=> Math.max(0.2, 1/(d.lv)));
const total = weights.reduce((a,b)=>a+b,0);
let pick = Math.random()*total;
def = pool[0];
for(let i=0;i<pool.length;i++){
pick -= weights[i];
if(pick<=0){def=pool[i];break;}
}
}
// find valid position in matching zone
let x,y,tries=0;
do {
x = 80 + Math.random()*(WORLD_W-160);
y = 80 + Math.random()*(WORLD_H-160);
tries++;
} while(tileAt(x,y)!==def.zone && tileAt(x,y)!=='grass' && tries<30);
const c = {
def,
x, y,
vx: (Math.random()-0.5)*def.spd,
vy: (Math.random()-0.5)*def.spd,
hp: def.hp,
maxHp: def.hp,
size: def.sz,
angle: Math.random()*Math.PI*2,
wobble: Math.random()*Math.PI*2,
wanderTimer: Math.random()*180,
fleeing: false,
fleeTimer: 0,
stunned: 0,
poisoned: 0,
id: Math.random().toString(36).substr(2,8),
highlighted: false,
};
creatures.push(c);
return c;
}
// ─────────────────────────────────────────────
// DRAWING HELPERS
// ─────────────────────────────────────────────
const TILE_COLORS = {
grass: ['#5db347','#6dc050','#4ea03a'],
deep_grass: ['#3a8030','#44952e','#2e6820'],
forest: ['#2a5c20','#346625','#1e4a18'],
water: ['#3a8fbf','#4499cc','#2a7aaa'],
};
function drawWorld(){
const startC = Math.max(0, Math.floor(cam.x/TILE)-1);
const endC = Math.min(COLS, Math.ceil((cam.x+W)/TILE)+1);
const startR = Math.max(0, Math.floor(cam.y/TILE)-1);
const endR = Math.min(ROWS, Math.ceil((cam.y+H)/TILE)+1);
for(let r=startR;r<endR;r++){
for(let c=startC;c<endC;c++){
const t = tiles[r][c];
const cols = TILE_COLORS[t] || TILE_COLORS.grass;
// slight checkerboard
const ci = (r+c)%3;
ctx.fillStyle = cols[ci];
ctx.fillRect(c*TILE - cam.x, r*TILE - cam.y, TILE, TILE);
}
}
}
function drawDecorations(){
for(const d of decorations){
const sx = d.x - cam.x, sy = d.y - cam.y;
if(sx < -60 || sx > W+60 || sy < -60 || sy > H+60) continue;
ctx.save();
ctx.translate(sx, sy);
if(d.type==='tree'){
// trunk
ctx.fillStyle = '#6b4226';
ctx.fillRect(-3, 0, 6, 12);
// canopy layers
const greens = ['#1a5c10','#227a16','#2a9920'];
ctx.fillStyle = greens[d.variant%3];
ctx.shadowColor='rgba(0,0,0,.25)';ctx.shadowBlur=6;
ctx.beginPath();ctx.arc(0,-d.s*0.4,d.s,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#2eb820';
ctx.beginPath();ctx.arc(-d.s*0.25,-d.s*0.7,d.s*0.7,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(d.s*0.2,-d.s*0.65,d.s*0.65,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
} else if(d.type==='bush'){
ctx.fillStyle='#3d8a28';
ctx.beginPath();ctx.ellipse(0,0,d.s*1.2,d.s*0.8,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#4da830';
ctx.beginPath();ctx.ellipse(-d.s*0.3,-d.s*0.3,d.s*0.7,d.s*0.6,0,0,Math.PI*2);ctx.fill();
} else if(d.type==='flower'){
const petals = 5;
for(let i=0;i<petals;i++){
const a = i/petals*Math.PI*2;
ctx.fillStyle = d.col;
ctx.beginPath();ctx.ellipse(Math.cos(a)*4,Math.sin(a)*4,3,2,a,0,Math.PI*2);ctx.fill();
}
ctx.fillStyle='#ffd700';
ctx.beginPath();ctx.arc(0,0,2.5,0,Math.PI*2);ctx.fill();
} else if(d.type==='lily'){
ctx.fillStyle='rgba(50,160,50,.7)';
ctx.beginPath();ctx.ellipse(0,0,d.s,d.s*0.7,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#ff69b4';
ctx.beginPath();ctx.arc(0,0,3,0,Math.PI*2);ctx.fill();
} else if(d.type==='rock'){
ctx.fillStyle='#888';
ctx.beginPath();ctx.ellipse(0,0,d.s,d.s*0.7,0.3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#aaa';
ctx.beginPath();ctx.ellipse(-d.s*0.1,-d.s*0.2,d.s*0.5,d.s*0.35,0.5,0,Math.PI*2);ctx.fill();
}
ctx.restore();
}
}
function drawCreature(c){
const sx = c.x - cam.x, sy = c.y - cam.y;
if(sx<-80||sx>W+80||sy<-80||sy>H+80) return;
const d = c.def;
ctx.save();
ctx.translate(sx, sy);
// stunned flash
if(c.stunned > 0){ ctx.globalAlpha = 0.5 + 0.5*Math.sin(Date.now()*0.02); }
const s = c.size;
const wobble = Math.sin(c.wobble)*0.15;
// Draw based on creature type
const id = d.id;
// === INSECT TYPES ===
if(id==='ant'){
drawAnt(ctx, s, d.col, d.col2);
} else if(id==='fly'){
drawFly(ctx, s, d.col, d.col2);
} else if(id==='beetle'){
drawBeetle(ctx, s, d.col, d.col2);
} else if(id==='butterfly'){
drawButterfly(ctx, s, d.col, d.col2, c.wobble);
} else if(id==='worm'){
drawWorm(ctx, s, d.col, d.col2, wobble);
} else if(id==='spider'){
drawSpider(ctx, s, d.col, d.col2);
} else if(id==='grasshopper'){
drawGrasshopper(ctx, s, d.col, d.col2);
} else if(id==='ladybug'){
drawLadybug(ctx, s, d.col, d.col2);
} else if(id==='snail'){
drawSnail(ctx, s, d.col, d.col2);
} else if(id==='firefly'){
drawFirefly(ctx, s, d.col, d.col2);
} else if(id==='scorpion'){
drawScorpion(ctx, s, d.col, d.col2);
} else if(id==='mantis'){
drawMantis(ctx, s, d.col, d.col2);
} else if(id==='dragonfly'){
drawDragonfly(ctx, s, d.col, d.col2);
// === AMPHIBIAN / REPTILE ===
} else if(id==='frog'){
drawFrog(ctx, s, d.col, d.col2);
} else if(id==='snake_e'){
drawSnakeCreature(ctx, s, d.col, d.col2, wobble);
} else if(id==='lizard'){
drawLizard(ctx, s, d.col, d.col2);
} else if(id==='turtle'){
drawTurtle(ctx, s, d.col, d.col2);
} else if(id==='chameleon'){
drawChameleon(ctx, s, d.col, d.col2);
} else if(id==='axolotl'){
drawAxolotl(ctx, s, d.col, d.col2);
// === FISH / WATER ===
} else if(id==='fish'){
drawFish(ctx, s, d.col, d.col2);
} else if(id==='jellyfish'){
drawJellyfish(ctx, s, d.col, d.col2, c.wobble);
} else if(id==='crab'){
drawCrab(ctx, s, d.col, d.col2);
} else if(id==='octopus'){
drawOctopus(ctx, s, d.col, d.col2, c.wobble);
// === MAMMALS / BIRDS ===
} else if(id==='mouse'){
drawMouse(ctx, s, d.col, d.col2);
} else if(id==='rabbit'){
drawRabbit(ctx, s, d.col, d.col2);
} else if(id==='crow'){
drawCrow(ctx, s, d.col, d.col2);
} else if(id==='fox'){
drawFox(ctx, s, d.col, d.col2);
} else if(id==='owl'){
drawOwl(ctx, s, d.col, d.col2);
} else if(id==='deer'){
drawDeer(ctx, s, d.col, d.col2);
} else if(id==='boar'){
drawBoar(ctx, s, d.col, d.col2);
} else if(id==='bearCub'){
drawBear(ctx, s, d.col, d.col2);
} else if(id==='wolf'){
drawWolf(ctx, s, d.col, d.col2);
} else if(id==='eagle'){
drawEagle(ctx, s, d.col, d.col2);
} else {
// generic
ctx.fillStyle = d.col;
ctx.beginPath(); ctx.arc(0,0,s,0,Math.PI*2); ctx.fill();
}
// HP bar (only if damaged)
if(c.hp < c.maxHp){
const bw = s*2.4, bh = 4;
ctx.fillStyle='rgba(0,0,0,.5)';
ctx.fillRect(-bw/2, -s-10, bw, bh);
ctx.fillStyle = c.hp/c.maxHp > 0.5 ? '#2ecc71' : c.hp/c.maxHp>0.25?'#f39c12':'#e74c3c';
ctx.fillRect(-bw/2, -s-10, bw*(c.hp/c.maxHp), bh);
}
// RARE badge
if(d.rare){
ctx.fillStyle='rgba(0,0,0,.5)';
ctx.beginPath();ctx.arc(s+2,-s+2,5,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#f9ca24';
ctx.font='bold 7px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText('★',s+2,-s+2);
}
ctx.restore();
}
// ─────────────────────────────────────────────
// CREATURE DRAW FUNCTIONS
// ─────────────────────────────────────────────
function drawAnt(ctx,s,c1,c2){
ctx.fillStyle=c1;
// 3 body segments
ctx.beginPath();ctx.ellipse(0,-s*0.8,s*0.4,s*0.35,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(0,0,s*0.55,s*0.45,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,s*0.9,s*0.7,s*0.6,0,0,Math.PI*2);ctx.fill();
// legs
ctx.strokeStyle=c2;ctx.lineWidth=1.2;
for(let i=-1;i<=1;i+=1){
ctx.beginPath();ctx.moveTo(-s*0.5,i*s*0.3);ctx.lineTo(-s*1.3,i*s*0.5+s*0.2);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.5,i*s*0.3);ctx.lineTo(s*1.3,i*s*0.5+s*0.2);ctx.stroke();
}
// antennae
ctx.beginPath();ctx.moveTo(-s*0.2,-s*1.1);ctx.lineTo(-s*0.6,-s*1.8);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.2,-s*1.1);ctx.lineTo(s*0.6,-s*1.8);ctx.stroke();
}
function drawFly(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.55,s*0.7,0,0,Math.PI*2);ctx.fill();
// wings
ctx.fillStyle='rgba(180,220,255,.55)';
ctx.beginPath();ctx.ellipse(-s*1.0,-s*0.3,s*0.9,s*0.45,-.4,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*1.0,-s*0.3,s*0.9,s*0.45,.4,0,Math.PI*2);ctx.fill();
// eyes
ctx.fillStyle='#e00';
ctx.beginPath();ctx.arc(-s*0.25,-s*0.5,s*0.22,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.25,-s*0.5,s*0.22,0,Math.PI*2);ctx.fill();
}
function drawBeetle(ctx,s,c1,c2){
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,0,s,s*0.85,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c1;
// wing cases
ctx.beginPath();ctx.ellipse(-s*0.4,s*0.1,s*0.5,s*0.75,-0.1,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.4,s*0.1,s*0.5,s*0.75,0.1,0,Math.PI*2);ctx.fill();
// head
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,-s*0.8,s*0.4,s*0.35,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle=c2;ctx.lineWidth=1;
// legs
for(let i=0;i<3;i++){
const y = -s*0.3 + i*s*0.35;
ctx.beginPath();ctx.moveTo(-s,y);ctx.lineTo(-s*1.5,y+s*0.4);ctx.stroke();
ctx.beginPath();ctx.moveTo(s,y);ctx.lineTo(s*1.5,y+s*0.4);ctx.stroke();
}
}
function drawButterfly(ctx,s,c1,c2,t){
const fw = Math.abs(Math.sin(t*0.1))*0.4+0.6;
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(-s*fw,-s*0.5,s*1.1*fw,s*0.8,-.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*fw,-s*0.5,s*1.1*fw,s*0.8,.5,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*fw*.6,s*0.2,s*0.7*fw,s*0.55,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*fw*.6,s*0.2,s*0.7*fw,s*0.55,.3,0,Math.PI*2);ctx.fill();
// body
ctx.fillStyle='#333';
ctx.beginPath();ctx.ellipse(0,0,s*0.22,s*0.95,0,0,Math.PI*2);ctx.fill();
// wing dots
ctx.fillStyle='rgba(255,255,255,.5)';
ctx.beginPath();ctx.arc(-s*fw*0.5,-s*0.3,s*0.2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*fw*0.5,-s*0.3,s*0.2,0,Math.PI*2);ctx.fill();
}
function drawWorm(ctx,s,c1,c2,w){
ctx.strokeStyle=c1;ctx.lineWidth=s*0.9;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(-s*1.2+w*s*0.5,0);
ctx.bezierCurveTo(-s*0.4,s*w*3,s*0.4,-s*w*3,s*1.2+w*s*0.3,0);
ctx.stroke();
ctx.fillStyle=c2;
ctx.beginPath();ctx.arc(-s*1.2+w*s*0.5,0,s*0.5,0,Math.PI*2);ctx.fill();
}
function drawSpider(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.7,s*0.6,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,-s*0.85,s*0.5,s*0.45,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle=c2;ctx.lineWidth=1.2;
// 8 legs
for(let i=0;i<4;i++){
const a = -0.3 + i*0.22;
ctx.beginPath();ctx.moveTo(-s*0.6,a*s*2);ctx.quadraticCurveTo(-s*1.4,a*s*2-s*0.8,-s*1.8,a*s*2+s*0.3);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.6,a*s*2);ctx.quadraticCurveTo(s*1.4,a*s*2-s*0.8,s*1.8,a*s*2+s*0.3);ctx.stroke();
}
ctx.fillStyle='#e00';
ctx.beginPath();ctx.arc(-s*0.2,-s*0.25,s*0.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.2,-s*0.25,s*0.15,0,Math.PI*2);ctx.fill();
}
function drawGrasshopper(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.5,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.7,-s*0.35,s*0.4,s*0.38,-.4,0,Math.PI*2);ctx.fill();
// big back legs
ctx.strokeStyle=c1;ctx.lineWidth=2.2;
ctx.beginPath();ctx.moveTo(-s*0.3,s*0.4);ctx.lineTo(-s*0.9,s*1.3);ctx.lineTo(-s*0.2,s*1.8);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.3,s*0.4);ctx.lineTo(s*0.9,s*1.3);ctx.lineTo(s*0.2,s*1.8);ctx.stroke();
// antennae
ctx.strokeStyle=c2;ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(-s*0.8,-s*0.6);ctx.lineTo(-s*1.4,-s*1.5);ctx.stroke();
ctx.beginPath();ctx.moveTo(-s*0.6,-s*0.6);ctx.lineTo(-s*0.9,-s*1.5);ctx.stroke();
}
function drawLadybug(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.arc(0,0,s,0,Math.PI*2);ctx.fill();
// split line
ctx.strokeStyle='#111';ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(0,-s);ctx.lineTo(0,s);ctx.stroke();
// dots
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-s*0.45,-s*0.1,s*0.22,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.45,-s*0.1,s*0.22,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(-s*0.35,s*0.5,s*0.18,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.35,s*0.5,s*0.18,0,Math.PI*2);ctx.fill();
// head
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(0,-s*0.88,s*0.38,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(-s*0.15,-s*0.88,s*0.1,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.15,-s*0.88,s*0.1,0,Math.PI*2);ctx.fill();
}
function drawSnail(ctx,s,c1,c2){
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*0.2,s*0.3,s*0.9,s*0.45,0,0,Math.PI*2);ctx.fill();
// shell spiral
ctx.fillStyle=c1;
ctx.beginPath();ctx.arc(-s*0.2,-s*0.15,s*0.8,0,Math.PI*2);ctx.fill();
ctx.strokeStyle=c2;ctx.lineWidth=2.5;
ctx.beginPath();ctx.arc(-s*0.2,-s*0.15,s*0.5,0,Math.PI*1.8);ctx.stroke();
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
ctx.beginPath();ctx.arc(-s*0.2,-s*0.15,s*0.25,0,Math.PI*1.5);ctx.stroke();
// antennae
ctx.strokeStyle='#a06030';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(s*0.7,s*0.1);ctx.lineTo(s*0.9,-s*0.4);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.9,s*0.1);ctx.lineTo(s*1.1,-s*0.35);ctx.stroke();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(s*0.9,-s*0.4,2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*1.1,-s*0.35,2,0,Math.PI*2);ctx.fill();
}
function drawFirefly(ctx,s,c1,c2){
ctx.fillStyle='#333';
ctx.beginPath();ctx.ellipse(0,0,s*0.5,s*0.75,0,0,Math.PI*2);ctx.fill();
// glow abdomen
ctx.fillStyle=c1;
ctx.shadowColor=c1;ctx.shadowBlur=10;
ctx.beginPath();ctx.ellipse(0,s*0.4,s*0.38,s*0.38,0,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
// wings
ctx.fillStyle='rgba(200,240,180,.4)';
ctx.beginPath();ctx.ellipse(-s*0.7,-s*0.2,s*0.6,s*0.35,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.7,-s*0.2,s*0.6,s*0.35,.3,0,Math.PI*2);ctx.fill();
}
function drawScorpion(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.75,s*0.6,0,0,Math.PI*2);ctx.fill();
// tail segments
ctx.fillStyle=c2;
for(let i=0;i<4;i++){
const ty = s*0.7+i*s*0.45;
ctx.beginPath();ctx.ellipse(i*s*0.12,ty,s*0.3-i*s*0.04,s*0.27-i*s*0.03,0,0,Math.PI*2);ctx.fill();
}
// stinger
ctx.fillStyle='#f0d020';
ctx.beginPath();ctx.arc(s*0.45,s*2.4,s*0.2,0,Math.PI*2);ctx.fill();
// claws
ctx.strokeStyle=c1;ctx.lineWidth=2.5;
ctx.beginPath();ctx.moveTo(-s*0.7,-s*0.2);ctx.lineTo(-s*1.4,-s*0.8);ctx.stroke();
ctx.beginPath();ctx.arc(-s*1.4,-s*0.8,s*0.32,0,Math.PI*2);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.7,-s*0.2);ctx.lineTo(s*1.4,-s*0.8);ctx.stroke();
ctx.beginPath();ctx.arc(s*1.4,-s*0.8,s*0.32,0,Math.PI*2);ctx.stroke();
// legs
ctx.strokeStyle=c2;ctx.lineWidth=1;
for(let i=0;i<4;i++){
const y = -s*0.2+i*s*0.25;
ctx.beginPath();ctx.moveTo(-s*0.7,y);ctx.lineTo(-s*1.3,y+s*0.3);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.7,y);ctx.lineTo(s*1.3,y+s*0.3);ctx.stroke();
}
}
function drawMantis(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,s*0.3,s*0.5,s*0.8,0,0,Math.PI*2);ctx.fill();
// head
ctx.beginPath();ctx.ellipse(0,-s*0.6,s*0.38,s*0.4,0,0,Math.PI*2);ctx.fill();
// triangular body
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(-s*0.3,-s*0.2);ctx.lineTo(s*0.3,-s*0.2);ctx.lineTo(0,s*0.5);ctx.closePath();ctx.fill();
// raptorial arms
ctx.strokeStyle=c2;ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(-s*0.3,-s*0.3);ctx.lineTo(-s*1.0,-s*1.1);ctx.lineTo(-s*0.5,-s*0.5);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.3,-s*0.3);ctx.lineTo(s*1.0,-s*1.1);ctx.lineTo(s*0.5,-s*0.5);ctx.stroke();
// eyes
ctx.fillStyle='#90ee00';
ctx.beginPath();ctx.arc(-s*0.15,-s*0.65,s*0.14,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.15,-s*0.65,s*0.14,0,Math.PI*2);ctx.fill();
}
function drawDragonfly(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.28,s*1.2,0,0,Math.PI*2);ctx.fill();
// wings 4
ctx.fillStyle='rgba(150,220,255,.5)';
ctx.beginPath();ctx.ellipse(-s*1.1,-s*0.2,s*1.0,s*0.4,-.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*1.1,-s*0.2,s*1.0,s*0.4,.15,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgba(150,200,255,.4)';
ctx.beginPath();ctx.ellipse(-s*1.0,s*0.3,s*0.8,s*0.3,-.1,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*1.0,s*0.3,s*0.8,s*0.3,.1,0,Math.PI*2);ctx.fill();
// head + big eyes
ctx.fillStyle=c2;
ctx.beginPath();ctx.arc(0,-s*1.0,s*0.4,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#99ddff';
ctx.beginPath();ctx.arc(-s*0.22,-s*1.0,s*0.22,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.22,-s*1.0,s*0.22,0,Math.PI*2);ctx.fill();
}
function drawFrog(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s,s*0.8,0,0,Math.PI*2);ctx.fill();
// back legs
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.9,s*0.6,s*0.35,s*0.7,-.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.9,s*0.6,s*0.35,s*0.7,.5,0,Math.PI*2);ctx.fill();
// front legs
ctx.beginPath();ctx.ellipse(-s*0.85,-s*0.1,s*0.25,s*0.5,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.85,-s*0.1,s*0.25,s*0.5,.3,0,Math.PI*2);ctx.fill();
// belly
ctx.fillStyle='#c8f0a8';
ctx.beginPath();ctx.ellipse(0,s*0.1,s*0.6,s*0.45,0,0,Math.PI*2);ctx.fill();
// eyes on top
ctx.fillStyle=c2;
ctx.beginPath();ctx.arc(-s*0.45,-s*0.7,s*0.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.45,-s*0.7,s*0.3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-s*0.45,-s*0.7,s*0.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.45,-s*0.7,s*0.15,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(-s*0.5,-s*0.73,s*0.07,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.5,-s*0.73,s*0.07,0,Math.PI*2);ctx.fill();
}
function drawSnakeCreature(ctx,s,c1,c2,w){
ctx.strokeStyle=c1;ctx.lineWidth=s*0.7;ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(-s*1.5+w*s,0);
ctx.bezierCurveTo(-s*0.5,s*w*4,s*0.5,-s*w*4,s*1.5+w*s*0.5,0);
ctx.stroke();
// scales pattern
ctx.strokeStyle=c2;ctx.lineWidth=s*0.15;
for(let i=-1;i<=1;i+=0.5){
ctx.beginPath();ctx.arc(i*s,0,s*0.25,0,Math.PI);ctx.stroke();
}
// head
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*1.5+w*s*0.5,0,s*0.5,s*0.38,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#f0d030';
ctx.beginPath();ctx.arc(s*1.6+w*s*0.5,-s*0.15,s*0.12,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*1.6+w*s*0.5,s*0.15,s*0.12,0,Math.PI*2);ctx.fill();
}
function drawLizard(ctx,s,c1,c2){
// body
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.9,s*0.5,0,0,Math.PI*2);ctx.fill();
// tail
ctx.strokeStyle=c2;ctx.lineWidth=s*0.4;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(-s*0.8,0);ctx.bezierCurveTo(-s*1.3,s*0.7,-s*2.0,s*0.5,-s*2.4,s*0.2);ctx.stroke();
// head
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*0.9,0,s*0.55,s*0.38,0,0,Math.PI*2);ctx.fill();
// legs
ctx.strokeStyle=c2;ctx.lineWidth=1.8;
ctx.beginPath();ctx.moveTo(-s*0.4,s*0.4);ctx.lineTo(-s*0.7,s*1.0);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.3,s*0.4);ctx.lineTo(s*0.6,s*1.0);ctx.stroke();
ctx.beginPath();ctx.moveTo(-s*0.5,-s*0.4);ctx.lineTo(-s*0.9,-s*1.0);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.2,-s*0.4);ctx.lineTo(s*0.5,-s*1.0);ctx.stroke();
// eye
ctx.fillStyle='#f0a020';
ctx.beginPath();ctx.arc(s*1.1,-s*0.12,s*0.15,0,Math.PI*2);ctx.fill();
}
function drawTurtle(ctx,s,c1,c2){
// shell
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,0,s*0.9,s*0.85,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.75,s*0.7,0,0,Math.PI*2);ctx.fill();
// shell pattern
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(0,-s*0.7);ctx.lineTo(0,s*0.7);ctx.stroke();
ctx.beginPath();ctx.moveTo(-s*0.7,0);ctx.lineTo(s*0.7,0);ctx.stroke();
ctx.beginPath();ctx.arc(0,0,s*0.38,0,Math.PI*2);ctx.stroke();
// head
ctx.fillStyle='#5a8040';
ctx.beginPath();ctx.ellipse(s*0.85,0,s*0.35,s*0.28,0,0,Math.PI*2);ctx.fill();
// legs
ctx.fillStyle='#5a8040';
ctx.beginPath();ctx.ellipse(-s*0.6,s*0.75,s*0.25,s*0.2,.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.5,s*0.75,s*0.25,s*0.2,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(-s*0.7,-s*0.7,s*0.2,s*0.18,.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.4,-s*0.7,s*0.2,s*0.18,-.5,0,Math.PI*2);ctx.fill();
}
function drawChameleon(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.85,s*0.55,0,0,Math.PI*2);ctx.fill();
// curly tail
ctx.strokeStyle=c2;ctx.lineWidth=s*0.35;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(-s*0.7,0);ctx.bezierCurveTo(-s*1.4,-s*0.5,-s*1.8,s*0.5,-s*1.4,s*1.0);ctx.stroke();
// crest
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
for(let i=0;i<5;i++){
const x=-s*0.5+i*s*0.25,y=-s*0.5;
ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(x,y-s*0.3);ctx.stroke();
}
// head
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*1.1,0,s*0.55,s*0.4,0.2,0,Math.PI*2);ctx.fill();
// turret eye
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(s*1.2,-s*0.1,s*0.2,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.22,-s*0.1,s*0.1,0,Math.PI*2);ctx.fill();
// legs
ctx.strokeStyle=c2;ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(-s*0.2,s*0.5);ctx.lineTo(-s*0.5,s*1.1);ctx.lineTo(-s*0.2,s*1.3);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.4,s*0.5);ctx.lineTo(s*0.7,s*1.1);ctx.lineTo(s*0.4,s*1.3);ctx.stroke();
}
function drawAxolotl(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.65,0,0,Math.PI*2);ctx.fill();
// gills
ctx.strokeStyle=c2;ctx.lineWidth=s*0.2;ctx.lineCap='round';
for(let i=0;i<3;i++){
const a = (-0.4+i*0.4)*Math.PI;
ctx.beginPath();ctx.moveTo(-s*0.6,-s*0.45);
ctx.lineTo(-s*0.6+Math.cos(a)*s*0.5,-s*0.45+Math.sin(a)*s*0.5);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.6,-s*0.45);
ctx.lineTo(s*0.6-Math.cos(a)*s*0.5,-s*0.45+Math.sin(a)*s*0.5);ctx.stroke();
}
// tail
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(-s*1.0,0);ctx.quadraticCurveTo(-s*1.7,s*0.4,-s*2.0,0);ctx.quadraticCurveTo(-s*1.7,-s*0.4,-s*1.0,0);ctx.fill();
// legs stubby
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.5,s*0.6,s*0.25,s*0.18,.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.3,s*0.6,s*0.25,s*0.18,-.3,0,Math.PI*2);ctx.fill();
// face
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(-s*0.6,-s*0.1,s*0.12,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.6,-s*0.1,s*0.12,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='#111';ctx.lineWidth=1.5;
ctx.beginPath();ctx.arc(0,s*0.1,s*0.2,0.1*Math.PI,0.9*Math.PI);ctx.stroke();
}
function drawFish(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.65,0,0,Math.PI*2);ctx.fill();
// tail
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(-s*1.0,0);ctx.lineTo(-s*1.6,s*0.6);ctx.lineTo(-s*1.6,-s*0.6);ctx.closePath();ctx.fill();
// fin top
ctx.beginPath();ctx.moveTo(-s*0.3,-s*0.6);ctx.lineTo(s*0.1,-s*1.1);ctx.lineTo(s*0.5,-s*0.6);ctx.closePath();ctx.fill();
// scales hint
ctx.strokeStyle=c2;ctx.lineWidth=1;ctx.globalAlpha=0.5;
ctx.beginPath();ctx.arc(s*0.1,0,s*0.4,Math.PI*0.3,Math.PI*0.7);ctx.stroke();
ctx.beginPath();ctx.arc(-s*0.3,0,s*0.4,Math.PI*0.3,Math.PI*0.7);ctx.stroke();
ctx.globalAlpha=1;
// eye
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(s*0.7,-s*0.1,s*0.22,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*0.73,-s*0.1,s*0.11,0,Math.PI*2);ctx.fill();
}
function drawJellyfish(ctx,s,c1,c2,t){
const bob = Math.sin(t*0.05)*s*0.2;
// bell
const g = ctx.createRadialGradient(0,-s*0.2+bob,s*0.1,0,-s*0.2+bob,s);
g.addColorStop(0,c1+'dd');g.addColorStop(1,c2+'66');
ctx.fillStyle=g;
ctx.beginPath();ctx.ellipse(0,-s*0.2+bob,s,s*0.75,0,0,Math.PI);ctx.fill();
// tentacles
ctx.strokeStyle=c1+'99';ctx.lineWidth=1.5;ctx.lineCap='round';
for(let i=0;i<7;i++){
const tx = -s*0.8+i*s*0.27;
const wave = Math.sin(t*0.08+i)*s*0.3;
ctx.beginPath();ctx.moveTo(tx,s*0.5+bob);ctx.bezierCurveTo(tx+wave,s*1.0+bob,tx-wave,s*1.5+bob,tx+wave*0.5,s*2.0+bob);ctx.stroke();
}
// inner glow
ctx.fillStyle='rgba(255,255,255,.15)';
ctx.beginPath();ctx.ellipse(0,-s*0.35+bob,s*0.55,s*0.4,0,0,Math.PI*2);ctx.fill();
}
function drawCrab(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.2,s*0.85,0,0,Math.PI*2);ctx.fill();
// carapace texture
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(0,0,s*0.7,s*0.5,0,0,Math.PI*2);ctx.fill();
// big claws
ctx.strokeStyle=c1;ctx.lineWidth=s*0.4;
ctx.beginPath();ctx.moveTo(-s*1.1,-s*0.1);ctx.lineTo(-s*1.9,-s*0.7);ctx.stroke();
ctx.beginPath();ctx.arc(-s*1.9,-s*0.7,s*0.45,0,Math.PI*2);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*1.1,-s*0.1);ctx.lineTo(s*1.9,-s*0.7);ctx.stroke();
ctx.beginPath();ctx.arc(s*1.9,-s*0.7,s*0.45,0,Math.PI*2);ctx.stroke();
// legs
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
for(let i=0;i<4;i++){
const y = -s*0.3+i*s*0.22;
ctx.beginPath();ctx.moveTo(-s,y);ctx.lineTo(-s*1.6,y+s*0.5);ctx.stroke();
ctx.beginPath();ctx.moveTo(s,y);ctx.lineTo(s*1.6,y+s*0.5);ctx.stroke();
}
// eyes on stalks
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(-s*0.3,-s*0.8);ctx.lineTo(-s*0.4,-s*1.2);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.3,-s*0.8);ctx.lineTo(s*0.4,-s*1.2);ctx.stroke();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-s*0.4,-s*1.2,s*0.14,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.4,-s*1.2,s*0.14,0,Math.PI*2);ctx.fill();
}
function drawOctopus(ctx,s,c1,c2,t){
ctx.fillStyle=c1;
ctx.beginPath();ctx.arc(0,0,s,0,Math.PI*2);ctx.fill();
// 8 arms
ctx.strokeStyle=c2;ctx.lineWidth=s*0.32;ctx.lineCap='round';
for(let i=0;i<8;i++){
const a = i/8*Math.PI*2;
const wave = Math.sin(t*0.06+i)*s*0.4;
const ex = Math.cos(a)*s*2.2, ey = Math.sin(a)*s*2.2;
ctx.beginPath();
ctx.moveTo(Math.cos(a)*s*0.8,Math.sin(a)*s*0.8);
ctx.bezierCurveTo(
Math.cos(a)*s*1.4+wave*Math.sin(a),Math.sin(a)*s*1.4+wave*Math.cos(a),
ex,ey, ex,ey
);
ctx.stroke();
}
// suckers
ctx.fillStyle='rgba(255,255,255,.3)';
for(let i=0;i<8;i++){
const a = i/8*Math.PI*2;
ctx.beginPath();ctx.arc(Math.cos(a)*s*1.3,Math.sin(a)*s*1.3,s*0.1,0,Math.PI*2);ctx.fill();
}
// eyes
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(-s*0.35,-s*0.25,s*0.28,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.35,-s*0.25,s*0.28,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-s*0.35,-s*0.25,s*0.14,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.35,-s*0.25,s*0.14,0,Math.PI*2);ctx.fill();
}
function drawMouse(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.85,s*0.65,0,0,Math.PI*2);ctx.fill();
// tail
ctx.strokeStyle=c2;ctx.lineWidth=s*0.2;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(-s*0.7,0);ctx.bezierCurveTo(-s*1.2,s*0.5,-s*1.7,s*0.2,-s*1.9,-s*0.1);ctx.stroke();
// head
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(s*0.85,0,s*0.5,s*0.42,0,0,Math.PI*2);ctx.fill();
// ears
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*0.6,-s*0.6,s*0.3,s*0.35,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*1.0,-s*0.55,s*0.3,s*0.35,.2,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#ffb0b0';
ctx.beginPath();ctx.ellipse(s*0.6,-s*0.6,s*0.18,s*0.22,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*1.0,-s*0.55,s*0.18,s*0.22,.2,0,Math.PI*2);ctx.fill();
// eye & nose
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.1,-s*0.1,s*0.11,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#ffb0b0';ctx.beginPath();ctx.arc(s*1.3,s*0.05,s*0.1,0,Math.PI*2);ctx.fill();
}
function drawRabbit(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.9,s*0.8,0,0,Math.PI*2);ctx.fill();
// ears
ctx.beginPath();ctx.ellipse(-s*0.35,-s*1.3,s*0.22,s*0.65,-.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.35,-s*1.3,s*0.22,s*0.65,.15,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#ffb0b0';
ctx.beginPath();ctx.ellipse(-s*0.35,-s*1.3,s*0.11,s*0.5,-.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.35,-s*1.3,s*0.11,s*0.5,.15,0,Math.PI*2);ctx.fill();
// face
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*0.1,-s*0.1,s*0.6,s*0.52,0,0,Math.PI*2);ctx.fill();
// eyes
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(-s*0.25,-s*0.1,s*0.14,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.35,-s*0.1,s*0.14,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-s*0.2,-s*0.14,s*0.06,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.4,-s*0.14,s*0.06,0,Math.PI*2);ctx.fill();
// nose
ctx.fillStyle='#ffb0b0';ctx.beginPath();ctx.arc(s*0.1,s*0.18,s*0.1,0,Math.PI*2);ctx.fill();
// tail
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-s*0.85,s*0.4,s*0.25,0,Math.PI*2);ctx.fill();
}
function drawCrow(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.85,s*0.6,0,0,Math.PI*2);ctx.fill();
// wings folded
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.2,s*0.1,s*0.75,s*0.4,-0.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.2,s*0.1,s*0.75,s*0.4,0.3,0,Math.PI*2);ctx.fill();
// tail feathers
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(-s*0.8,s*0.2);ctx.lineTo(-s*1.5,s*0.6);ctx.lineTo(-s*0.6,s*0.5);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(-s*0.8,s*0.1);ctx.lineTo(-s*1.6,s*0.3);ctx.lineTo(-s*0.6,s*0.4);ctx.closePath();ctx.fill();
// head
ctx.fillStyle=c1;
ctx.beginPath();ctx.arc(s*0.65,-s*0.35,s*0.42,0,Math.PI*2);ctx.fill();
// beak
ctx.fillStyle='#888';
ctx.beginPath();ctx.moveTo(s*1.0,-s*0.35);ctx.lineTo(s*1.5,-s*0.28);ctx.lineTo(s*1.0,-s*0.22);ctx.closePath();ctx.fill();
// eye
ctx.fillStyle='#333';ctx.beginPath();ctx.arc(s*0.75,-s*0.38,s*0.13,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgba(100,180,255,.4)';ctx.beginPath();ctx.arc(s*0.77,-s*0.37,s*0.05,0,Math.PI*2);ctx.fill();
}
function drawFox(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s,s*0.65,0,0,Math.PI*2);ctx.fill();
// bushy tail
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(-s*1.3,s*0.2,s*0.75,s*0.45,.4,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';
ctx.beginPath();ctx.ellipse(-s*1.3,s*0.3,s*0.4,s*0.25,.4,0,Math.PI*2);ctx.fill();
// head
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(s*0.9,-s*0.1,s*0.55,s*0.45,0,0,Math.PI*2);ctx.fill();
// pointy ears
ctx.beginPath();ctx.moveTo(s*0.65,-s*0.5);ctx.lineTo(s*0.5,-s*1.1);ctx.lineTo(s*0.85,-s*0.55);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*1.05,-s*0.5);ctx.lineTo(s*1.2,-s*1.05);ctx.lineTo(s*1.25,-s*0.5);ctx.closePath();ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.moveTo(s*0.68,-s*0.56);ctx.lineTo(s*0.56,-s*1.0);ctx.lineTo(s*0.82,-s*0.58);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*1.07,-s*0.54);ctx.lineTo(s*1.18,-s*0.95);ctx.lineTo(s*1.22,-s*0.54);ctx.closePath();ctx.fill();
// face white
ctx.fillStyle='#fff';
ctx.beginPath();ctx.ellipse(s*1.1,s*0.05,s*0.3,s*0.25,0,0,Math.PI*2);ctx.fill();
// nose
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.35,s*0.0,s*0.1,0,Math.PI*2);ctx.fill();
// eye
ctx.beginPath();ctx.arc(s*0.92,-s*0.15,s*0.1,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#f0d020';ctx.beginPath();ctx.arc(s*0.92,-s*0.15,s*0.05,0,Math.PI*2);ctx.fill();
// legs
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(-s*0.3,s*0.7,s*0.18,s*0.28,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.4,s*0.7,s*0.18,s*0.28,0,0,Math.PI*2);ctx.fill();
}
function drawOwl(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.8,s*1.0,0,0,Math.PI*2);ctx.fill();
// ear tufts
ctx.beginPath();ctx.moveTo(-s*0.35,-s*0.9);ctx.lineTo(-s*0.55,-s*1.4);ctx.lineTo(-s*0.15,-s*0.85);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*0.35,-s*0.9);ctx.lineTo(s*0.55,-s*1.4);ctx.lineTo(s*0.15,-s*0.85);ctx.closePath();ctx.fill();
// wing pattern
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.35,s*0.2,s*0.45,s*0.7,0.2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.35,s*0.2,s*0.45,s*0.7,-0.2,0,Math.PI*2);ctx.fill();
// face disc
ctx.fillStyle='#e8d090';
ctx.beginPath();ctx.ellipse(0,-s*0.2,s*0.6,s*0.55,0,0,Math.PI*2);ctx.fill();
// big eyes
ctx.fillStyle='#f0c030';
ctx.beginPath();ctx.arc(-s*0.28,-s*0.25,s*0.28,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.28,-s*0.25,s*0.28,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(-s*0.28,-s*0.25,s*0.15,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.28,-s*0.25,s*0.15,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(-s*0.22,-s*0.3,s*0.06,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.22,-s*0.3,s*0.06,0,Math.PI*2);ctx.fill();
// beak
ctx.fillStyle='#d4a020';
ctx.beginPath();ctx.moveTo(-s*0.12,s*0.0);ctx.lineTo(s*0.12,s*0.0);ctx.lineTo(0,s*0.18);ctx.closePath();ctx.fill();
}
function drawDeer(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,s*0.1,s*0.9,s*0.65,0,0,Math.PI*2);ctx.fill();
// neck & head
ctx.beginPath();ctx.ellipse(s*0.65,-s*0.35,s*0.28,s*0.5,0.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.9,-s*0.75,s*0.35,s*0.3,0,0,Math.PI*2);ctx.fill();
// antlers
ctx.strokeStyle='#8b6020';ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(s*0.75,-s*1.0);ctx.lineTo(s*0.5,-s*1.7);ctx.lineTo(s*0.3,-s*1.4);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.5,-s*1.55);ctx.lineTo(s*0.7,-s*1.45);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*1.05,-s*1.0);ctx.lineTo(s*1.2,-s*1.7);ctx.lineTo(s*1.35,-s*1.4);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*1.2,-s*1.55);ctx.lineTo(s*1.05,-s*1.45);ctx.stroke();
// belly
ctx.fillStyle='#e8d0a0';
ctx.beginPath();ctx.ellipse(0,s*0.2,s*0.5,s*0.38,0,0,Math.PI*2);ctx.fill();
// legs
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.5,s*0.9,s*0.15,s*0.38,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.2,s*0.9,s*0.15,s*0.38,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(-s*0.65,s*0.8,s*0.12,s*0.3,.2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.55,s*0.8,s*0.12,s*0.3,-.2,0,Math.PI*2);ctx.fill();
// white rump
ctx.fillStyle='#fff';ctx.beginPath();ctx.ellipse(-s*0.75,s*0.2,s*0.35,s*0.28,0,0,Math.PI*2);ctx.fill();
// eye
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.0,-s*0.78,s*0.1,0,Math.PI*2);ctx.fill();
}
function drawBoar(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*0.75,0,0,Math.PI*2);ctx.fill();
// bristles/mane
ctx.strokeStyle=c2;ctx.lineWidth=1.5;
for(let i=0;i<8;i++){
const x = -s*0.8+i*s*0.22;
ctx.beginPath();ctx.moveTo(x,-s*0.7);ctx.lineTo(x,-s*1.1);ctx.stroke();
}
// head
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*1.0,0,s*0.6,s*0.5,0,0,Math.PI*2);ctx.fill();
// snout
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(s*1.45,s*0.1,s*0.3,s*0.25,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.42,s*0.1,s*0.08,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*1.52,s*0.1,s*0.08,0,Math.PI*2);ctx.fill();
// tusks
ctx.strokeStyle='#f0e0a0';ctx.lineWidth=2.5;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(s*1.35,s*0.3);ctx.lineTo(s*1.6,s*0.5);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*1.45,s*0.3);ctx.lineTo(s*1.7,s*0.5);ctx.stroke();
// eye
ctx.fillStyle='#c00';ctx.beginPath();ctx.arc(s*0.95,-s*0.15,s*0.12,0,Math.PI*2);ctx.fill();
// legs
ctx.fillStyle=c2;
for(let i=0;i<4;i++){
const x=-s*0.5+i*s*0.35,y=s*0.75;
ctx.beginPath();ctx.ellipse(x,y,s*0.15,s*0.32,0,0,Math.PI*2);ctx.fill();
}
}
function drawBear(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*1.1,s*1.0,0,0,Math.PI*2);ctx.fill();
// ears
ctx.beginPath();ctx.arc(-s*0.7,-s*0.8,s*0.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.7,-s*0.8,s*0.3,0,Math.PI*2);ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.arc(-s*0.7,-s*0.8,s*0.18,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.7,-s*0.8,s*0.18,0,Math.PI*2);ctx.fill();
// muzzle
ctx.fillStyle='#c8a070';
ctx.beginPath();ctx.ellipse(0,s*0.15,s*0.55,s*0.45,0,0,Math.PI*2);ctx.fill();
// nose
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(0,-s*0.1,s*0.18,0,Math.PI*2);ctx.fill();
// eyes
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(-s*0.4,-s*0.35,s*0.14,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.4,-s*0.35,s*0.14,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(-s*0.37,-s*0.37,s*0.05,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(s*0.37,-s*0.37,s*0.05,0,Math.PI*2);ctx.fill();
// paws
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(-s*0.9,s*0.8,s*0.3,s*0.22,.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.9,s*0.8,s*0.3,s*0.22,-.3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(-s*0.95,-s*0.7,s*0.28,s*0.2,.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.95,-s*0.7,s*0.28,s*0.2,-.5,0,Math.PI*2);ctx.fill();
}
function drawWolf(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s,s*0.7,0,0,Math.PI*2);ctx.fill();
// tail
ctx.strokeStyle=c1;ctx.lineWidth=s*0.35;ctx.lineCap='round';
ctx.beginPath();ctx.moveTo(-s*0.9,0);ctx.bezierCurveTo(-s*1.5,-s*0.3,-s*1.8,s*0.2,-s*2.1,s*0.5);ctx.stroke();
ctx.strokeStyle='#fff';ctx.lineWidth=s*0.12;
ctx.beginPath();ctx.moveTo(-s*1.6,s*0.1);ctx.lineTo(-s*2.0,s*0.45);ctx.stroke();
// head
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(s*0.85,-s*0.1,s*0.55,s*0.42,0,0,Math.PI*2);ctx.fill();
// ears pointy
ctx.beginPath();ctx.moveTo(s*0.6,-s*0.45);ctx.lineTo(s*0.45,-s*1.0);ctx.lineTo(s*0.85,-s*0.48);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*1.0,-s*0.42);ctx.lineTo(s*1.15,-s*0.95);ctx.lineTo(s*1.25,-s*0.42);ctx.closePath();ctx.fill();
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(s*0.63,-s*0.49);ctx.lineTo(s*0.52,-s*0.88);ctx.lineTo(s*0.82,-s*0.52);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*1.02,-s*0.46);ctx.lineTo(s*1.13,-s*0.84);ctx.lineTo(s*1.21,-s*0.46);ctx.closePath();ctx.fill();
// snout
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(s*1.28,s*0.05,s*0.28,s*0.22,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*1.3,s*0.0,s*0.1,0,Math.PI*2);ctx.fill();
// eye
ctx.fillStyle='#f0c020';ctx.beginPath();ctx.arc(s*0.9,-s*0.18,s*0.13,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*0.9,-s*0.18,s*0.07,0,Math.PI*2);ctx.fill();
// legs
ctx.fillStyle=c2;
ctx.beginPath();ctx.ellipse(-s*0.4,s*0.8,s*0.17,s*0.32,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(s*0.3,s*0.8,s*0.17,s*0.32,0,0,Math.PI*2);ctx.fill();
}
function drawEagle(ctx,s,c1,c2){
ctx.fillStyle=c1;
ctx.beginPath();ctx.ellipse(0,0,s*0.8,s*0.55,0,0,Math.PI*2);ctx.fill();
// wide wings spread
ctx.fillStyle=c2;
ctx.beginPath();ctx.moveTo(-s*0.6,-s*0.1);ctx.bezierCurveTo(-s*1.5,-s*0.8,-s*2.5,-s*0.2,-s*2.8,s*0.2);ctx.lineTo(-s*2.4,s*0.6);ctx.bezierCurveTo(-s*1.8,s*0.3,-s*0.8,s*0.4,-s*0.2,s*0.3);ctx.closePath();ctx.fill();
ctx.beginPath();ctx.moveTo(s*0.6,-s*0.1);ctx.bezierCurveTo(s*1.5,-s*0.8,s*2.5,-s*0.2,s*2.8,s*0.2);ctx.lineTo(s*2.4,s*0.6);ctx.bezierCurveTo(s*1.8,s*0.3,s*0.8,s*0.4,s*0.2,s*0.3);ctx.closePath();ctx.fill();
// wing feather lines
ctx.strokeStyle='#5a3010';ctx.lineWidth=1;
for(let i=1;i<5;i++){
ctx.beginPath();ctx.moveTo(-s*0.5+i*-s*0.4,-s*0.2);ctx.lineTo(-s*0.5+i*-s*0.55,s*0.4);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.5+i*s*0.4,-s*0.2);ctx.lineTo(s*0.5+i*s*0.55,s*0.4);ctx.stroke();
}
// white head
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(s*0.3,-s*0.35,s*0.42,0,Math.PI*2);ctx.fill();
// beak
ctx.fillStyle='#f0c020';
ctx.beginPath();ctx.moveTo(s*0.65,-s*0.3);ctx.lineTo(s*1.15,-s*0.45);ctx.lineTo(s*0.75,-s*0.15);ctx.closePath();ctx.fill();
// eye
ctx.fillStyle='#f0c020';ctx.beginPath();ctx.arc(s*0.42,-s*0.42,s*0.14,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(s*0.42,-s*0.42,s*0.07,0,Math.PI*2);ctx.fill();
// talons
ctx.strokeStyle='#c09020';ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(-s*0.3,s*0.5);ctx.lineTo(-s*0.5,s*0.9);ctx.stroke();
ctx.beginPath();ctx.moveTo(s*0.3,s*0.5);ctx.lineTo(s*0.5,s*0.9);ctx.stroke();
}
// ─────────────────────────────────────────────
// DRAW PLAYER SLIME
// ─────────────────────────────────────────────
function drawPlayer(){
const sx = player.x - cam.x, sy = player.y - cam.y;
const s = player.size;
const t = Date.now();
ctx.save();
ctx.translate(sx, sy);
// camouflage
if(player.activeEffects.camouflage && player.activeEffects.camouflage > t){
ctx.globalAlpha = 0.3 + 0.2*Math.sin(t*0.01);
}
// glow effect
if(player.activeEffects.glow && player.activeEffects.glow > t){
ctx.shadowColor='#ffd700';ctx.shadowBlur=20;
} else {
ctx.shadowColor=player.color;ctx.shadowBlur=12;
}
// howl buff
if(player.activeEffects.howl && player.activeEffects.howl > t){
ctx.shadowBlur=25;ctx.shadowColor='#ff4444';
}
// Wobbly blob shape
const w = player.wobble;
ctx.fillStyle = player.color;
ctx.beginPath();
const pts = 12;
for(let i=0;i<pts;i++){
const a = i/pts*Math.PI*2;
const r = s + Math.sin(a*3+w)*s*0.18 + Math.cos(a*5+w*1.3)*s*0.1;
if(i===0) ctx.moveTo(Math.cos(a)*r, Math.sin(a)*r);
else ctx.lineTo(Math.cos(a)*r, Math.sin(a)*r);
}
ctx.closePath();ctx.fill();
// inner highlight
ctx.fillStyle = player.color2 || '#fff';
ctx.globalAlpha *= 0.4;
ctx.beginPath();ctx.ellipse(-s*0.2,-s*0.2,s*0.5,s*0.4,0,0,Math.PI*2);ctx.fill();
ctx.globalAlpha = 1;
// eyes
ctx.shadowBlur = 0;
const eyeA = player.facing;
const e1x = Math.cos(eyeA-0.4)*s*0.55, e1y = Math.sin(eyeA-0.4)*s*0.55;
const e2x = Math.cos(eyeA+0.4)*s*0.55, e2y = Math.sin(eyeA+0.4)*s*0.55;
ctx.fillStyle = '#fff';
ctx.beginPath();ctx.arc(e1x,e1y,s*0.22,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(e2x,e2y,s*0.22,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(e1x+Math.cos(eyeA)*s*0.07,e1y+Math.sin(eyeA)*s*0.07,s*0.12,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(e2x+Math.cos(eyeA)*s*0.07,e2y+Math.sin(eyeA)*s*0.07,s*0.12,0,Math.PI*2);ctx.fill();
// devour animation - open mouth
if(player.devourAnim > 0){
const ma = player.devourAnim/20 * 0.8;
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(Math.cos(eyeA)*s*0.7,Math.sin(eyeA)*s*0.7,s*0.35*ma,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgba(255,80,80,.6)';
ctx.beginPath();ctx.arc(Math.cos(eyeA)*s*0.7,Math.sin(eyeA)*s*0.7,s*0.25*ma,0,Math.PI*2);ctx.fill();
}
// shell shield
if(player.activeEffects.shell && player.activeEffects.shell > t){
ctx.strokeStyle='rgba(180,220,255,.6)';ctx.lineWidth=3;
ctx.beginPath();ctx.arc(0,0,s+6,0,Math.PI*2);ctx.stroke();
}
ctx.restore();
}
// ─────────────────────────────────────────────
// PARTICLES
// ─────────────────────────────────────────────
function spawnParticles(x,y,color,count,type='circle'){
for(let i=0;i<count;i++){
const a = Math.random()*Math.PI*2;
const sp = 0.8+Math.random()*2.5;
particles.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,life:1,col:color,type,size:2+Math.random()*4});
}
}
function spawnFloatingText(x,y,text,color){
floatingTexts.push({x,y,text,color,life:1,vy:-1.2});
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.x+=p.vx; p.y+=p.vy; p.vy+=0.06; p.life-=0.025;
if(p.life<=0) particles.splice(i,1);
}
for(let i=floatingTexts.length-1;i>=0;i--){
const t=floatingTexts[i];
t.y+=t.vy; t.life-=0.018;
if(t.life<=0) floatingTexts.splice(i,1);
}
}
function drawParticlesAndTexts(){
for(const p of particles){
const sx=p.x-cam.x, sy=p.y-cam.y;
ctx.save();ctx.globalAlpha=p.life;ctx.fillStyle=p.col;
ctx.shadowColor=p.col;ctx.shadowBlur=4;
ctx.beginPath();ctx.arc(sx,sy,p.size*p.life,0,Math.PI*2);ctx.fill();
ctx.restore();
}
for(const t of floatingTexts){
const sx=t.x-cam.x, sy=t.y-cam.y;
ctx.save();ctx.globalAlpha=t.life;
ctx.font=`bold ${12+Math.round(t.life*4)}px 'Nunito',sans-serif`;
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillStyle=t.col;ctx.shadowColor=t.col;ctx.shadowBlur=6;
ctx.fillText(t.text,sx,sy);
ctx.restore();
}
}
// ─────────────────────────────────────────────
// CREATURE AI
// ─────────────────────────────────────────────
function updateCreatureAI(c, dt){
if(c.stunned > 0){ c.stunned -= dt; return; }
const d = c.def;
const dist = Math.hypot(player.x-c.x, player.y-c.y);
const stronger = d.lv > player.level + 1;
const weaker = d.lv <= player.level - 1;
const edible = dist < (player.size + c.size)*2.5;
const cam_eff = player.activeEffects.camouflage && player.activeEffects.camouflage > Date.now();
// flee if outmatched
if(weaker && dist < 160 && !cam_eff){
c.fleeing = true; c.fleeTimer = 180;
}
if(c.fleeTimer > 0) c.fleeTimer--;
else c.fleeing = false;
// hunt if stronger
if(stronger && dist < 200 && !cam_eff && d.zone!=='water'){
const a = Math.atan2(player.y-c.y, player.x-c.x);
c.vx += Math.cos(a)*d.spd*0.08;
c.vy += Math.sin(a)*d.spd*0.08;
} else if(c.fleeing){
const a = Math.atan2(c.y-player.y, c.x-player.x);
c.vx += Math.cos(a)*d.spd*0.12;
c.vy += Math.sin(a)*d.spd*0.12;
} else {
// wander
c.wanderTimer--;
if(c.wanderTimer <= 0){
const a = Math.random()*Math.PI*2;
c.vx = Math.cos(a)*d.spd*(0.4+Math.random()*0.6);
c.vy = Math.sin(a)*d.spd*(0.4+Math.random()*0.6);
c.wanderTimer = 80+Math.random()*140;
}
}
// glow lure
if(player.activeEffects.glow && player.activeEffects.glow > Date.now() && dist < 200){
const a = Math.atan2(player.y-c.y, player.x-c.x);
c.vx += Math.cos(a)*0.08;
c.vy += Math.sin(a)*0.08;
}
// clamp speed
const spd = Math.hypot(c.vx,c.vy);
if(spd > d.spd){ c.vx=c.vx/spd*d.spd; c.vy=c.vy/spd*d.spd; }
// friction
c.vx *= 0.9; c.vy *= 0.9;
c.x += c.vx; c.y += c.vy;
c.wobble += 0.08;
// wall bounce
if(c.x<c.size){c.x=c.size;c.vx=Math.abs(c.vx);}
if(c.x>WORLD_W-c.size){c.x=WORLD_W-c.size;c.vx=-Math.abs(c.vx);}
if(c.y<c.size){c.y=c.size;c.vy=Math.abs(c.vy);}
if(c.y>WORLD_H-c.size){c.y=WORLD_H-c.size;c.vy=-Math.abs(c.vy);}
// stronger creature attacks player
if(stronger && dist < (player.size+c.size)*1.2 && !cam_eff){
const now = Date.now();
if(!c.lastAttack || now-c.lastAttack > 800){
c.lastAttack = now;
let dmg = Math.max(2, d.lv*3 - player.level*1.5);
// shell
if(player.activeEffects.shell && player.activeEffects.shell > now) dmg *= 0.2;
player.hp -= dmg;
player.invincible = 400;
spawnParticles(player.x, player.y,'#ff4444',8);
spawnFloatingText(player.x, player.y-player.size-10, `-${Math.round(dmg)}`, '#ff4444');
if(player.hp <= 0){ player.hp=0; endGame(); }
}
}
// poisoned tick
if(c.poisoned > 0){
c.poisoned -= dt;
const now = Date.now();
if(!c.lastPoison || now-c.lastPoison > 500){
c.lastPoison=now;
c.hp -= 3;
spawnParticles(c.x,c.y,'#50c030',3);
if(c.hp <= 0) killCreature(c);
}
}
}
// ─────────────────────────────────────────────
// PLAYER LOGIC
// ─────────────────────────────────────────────
function updatePlayer(dt){
if(player.dead) return;
const now = Date.now();
// input
let mx=0,my=0;
if(keys['ArrowLeft']||keys['a']||keys['A']) mx-=1;
if(keys['ArrowRight']||keys['d']||keys['D']) mx+=1;
if(keys['ArrowUp']||keys['w']||keys['W']) my-=1;
if(keys['ArrowDown']||keys['s']||keys['S']) my+=1;
if(joystick.active){ mx=joystick.dx; my=joystick.dy; }
let spd = player.baseSpeed * (1 - (player.size-14)*0.008);
spd = Math.max(0.8, spd);
// dash
if(player.activeEffects.dash && player.activeEffects.dash > now) spd*=3;
if(player.activeEffects.howl && player.activeEffects.howl > now) spd*=1.5;
const mag = Math.hypot(mx,my);
if(mag>0){ mx/=mag; my/=mag; }
player.vx += mx*spd*0.35; player.vy += my*spd*0.35;
player.vx *= 0.82; player.vy *= 0.82;
const ps = Math.hypot(player.vx,player.vy);
if(ps>spd){ player.vx=player.vx/ps*spd; player.vy=player.vy/ps*spd; }
if(mag>0.05) player.facing = Math.atan2(my,mx);
player.x += player.vx; player.y += player.vy;
player.x = Math.max(player.size, Math.min(WORLD_W-player.size, player.x));
player.y = Math.max(player.size, Math.min(WORLD_H-player.size, player.y));
player.wobble += 0.05 + ps*0.02;
if(player.devourAnim>0) player.devourAnim--;
// regen
if(player.activeEffects.regen && player.activeEffects.regen > now){
player.hp = Math.min(player.maxHp, player.hp + 0.006*dt);
}
// web effect on nearby creatures
if(player.activeEffects.web && player.activeEffects.web > now){
for(const c of creatures){
if(Math.hypot(player.x-c.x,player.y-c.y) < 120){
c.vx *= 0.85; c.vy *= 0.85;
}
}
}
// camera
cam.x = player.x - W/2;
cam.y = player.y - H/2;
cam.x = Math.max(0, Math.min(WORLD_W-W, cam.x));
cam.y = Math.max(0, Math.min(WORLD_H-H, cam.y));
updateHUD();
}
function tryDevour(){
if(player.dead) return;
const now = Date.now();
player.devourAnim = 20;
// eagle dive – instant devour ≤ player level
const eagleDive = player.activeEffects.eagleDive && player.activeEffects.eagleDive > now;
// find closest creature in range
const range = player.size * 2.8;
let closest=null, closestDist=Infinity;
for(const c of creatures){
const dist = Math.hypot(player.x-c.x, player.y-c.y);
if(dist < range + c.size && dist < closestDist){
closestDist = dist; closest = c;
}
}
if(!closest) return;
const c = closest;
const d = c.def;
// success chance
let chance = 1.0;
if(d.lv > player.level){
const diff = d.lv - player.level;
chance = Math.max(0.05, 1 - diff*0.28);
}
if(player.activeEffects.strike && player.activeEffects.strike > now) chance = Math.min(1, chance*2);
if(eagleDive && d.lv <= player.level) chance = 1.0;
if(Math.random() < chance){
// SUCCESS
killCreature(c, true);
} else {
// FAIL
spawnFloatingText(player.x, player.y-player.size-15, 'TOO STRONG!', '#ff6b6b');
spawnParticles(player.x, player.y, '#ff8844', 6);
// recoil damage
const dmg = Math.ceil(d.lv*2.5);
player.hp -= dmg;
spawnFloatingText(player.x, player.y-player.size-28, `-${dmg}`, '#ff4444');
if(player.hp <= 0){ player.hp=0; endGame(); }
}
}
function killCreature(c, devoured=false){
const d = c.def;
spawnParticles(c.x, c.y, d.col, 16, 'circle');
if(devoured){
// XP
player.xp += d.xp;
spawnFloatingText(c.x, c.y-c.size-10, `+${d.xp} XP`, '#f9ca24');
// Grow
player.size = Math.min(55, player.size + d.sz*0.18);
// Color shift
morphPlayerColor(d.col);
// Level up?
checkLevelUp();
// Grant ability
if(d.abilityId){
const abil = ABILITIES[d.abilityId];
if(abil && !player.abilities.includes(d.abilityId)){
player.abilities.push(d.abilityId);
showAbilityUnlock(d.abilityId, d.name);
spawnParticles(player.x,player.y,'#00e5ff',20);
}
}
// Rare bonus
if(d.rare){
showMsg('✨ RARE CREATURE!', `Absorbed ${d.name}'s power!`);
}
// Sting: instant damage already used ability
if(d.abilityId==='sting'){
// visual only
}
}
// remove
const idx = creatures.indexOf(c);
if(idx>-1) creatures.splice(idx,1);
// respawn
setTimeout(()=> spawnRandomCreature(), 4000+Math.random()*6000);
}
function morphPlayerColor(targetCol){
// blend toward target color
const parse = (hex) => {
const n = parseInt(hex.replace('#',''),16);
return [(n>>16)&255,(n>>8)&255,n&255];
};
const blend = (a,b,t) => Math.round(a+(b-a)*t);
const pc = parse(player.color||'#7dff7a');
const tc = parse(targetCol);
const nc = [blend(pc[0],tc[0],0.25),blend(pc[1],tc[1],0.25),blend(pc[2],tc[2],0.25)];
player.color = '#'+nc.map(v=>v.toString(16).padStart(2,'0')).join('');
const nc2 = [blend(pc[0],tc[0],0.12),blend(pc[1],tc[1],0.12),blend(pc[2],tc[2],0.12)];
player.color2 = '#'+nc2.map(v=>Math.max(0,v-30).toString(16).padStart(2,'0')).join('');
}
function checkLevelUp(){
const maxLv = LEVEL_XP.length;
while(player.level < maxLv && player.xp >= LEVEL_XP[player.level]){
player.level++;
player.maxHp += 20;
player.hp = Math.min(player.maxHp, player.hp + 30);
const name = LEVEL_NAMES[player.level-1] || 'Slime Lord';
showMsg(`LEVEL UP! Lv.${player.level}`, `You are now a ${name}!`);
spawnParticles(player.x, player.y, '#ffd700', 30);
spawnFloatingText(player.x, player.y-player.size-20, `LEVEL ${player.level}!`, '#ffd700');
}
}
function useAbility(idx){
if(!player.abilities[idx]) return;
const id = player.abilities[idx];
const abil = ABILITIES[id];
if(!abil) return;
const now = Date.now();
const cdEnd = player.abilityCooldowns[id] || 0;
if(now < cdEnd) return;
player.abilityCooldowns[id] = now + abil.cd*1000;
player.abilityEndTimes[id] = now + abil.duration;
player.activeEffects[id] = now + abil.duration;
spawnFloatingText(player.x, player.y-player.size-22, abil.emoji+' '+abil.name+'!', '#00e5ff');
spawnParticles(player.x, player.y, '#00e5ff', 12);
// Instant effects
if(id==='sting'||id==='zap'||id==='charge'||id==='pinch'){
// Deal damage to nearest creature
let nearest=null,nd=Infinity;
for(const c of creatures){
const dist=Math.hypot(player.x-c.x,player.y-c.y);
if(dist<nd&&dist<200){nd=dist;nearest=c;}
}
if(nearest){
const dmg = id==='sting'?30:id==='zap'?25:id==='charge'?40:25;
nearest.hp -= dmg;
nearest.stunned = id==='zap'||id==='charge'?2000:1000;
spawnParticles(nearest.x,nearest.y,'#ffaa00',10);
spawnFloatingText(nearest.x,nearest.y-nearest.size-10,`-${dmg}`,nearest.def.col);
if(nearest.hp<=0) killCreature(nearest);
}
}
if(id==='roar'){
for(const c of creatures){
const dist=Math.hypot(player.x-c.x,player.y-c.y);
if(dist<180){
const a=Math.atan2(c.y-player.y,c.x-player.x);
c.vx+=Math.cos(a)*5; c.vy+=Math.sin(a)*5;
c.fleeing=true; c.fleeTimer=300;
}
}
}
if(id==='regen'){
// just activates via activeEffects
}
if(id==='poison'){
let nearest=null,nd=Infinity;
for(const c of creatures){
const dist=Math.hypot(player.x-c.x,player.y-c.y);
if(dist<nd&&dist<180){nd=dist;nearest=c;}
}
if(nearest) nearest.poisoned=4000;
}
if(id==='ink'){
spawnParticles(player.x,player.y,'#330044',30);
// blind effect: creatures wander randomly
for(const c of creatures){
if(Math.hypot(player.x-c.x,player.y-c.y)<160){ c.stunned=1500; }
}
}
updateAbilityBar();
}
// ─────────────────────────────────────────────
// HUD UPDATE
// ─────────────────────────────────────────────
function updateHUD(){
const hp = document.getElementById('hpBar');
const xpBar = document.getElementById('xpBar');
const hpVal = document.getElementById('hpVal');
const xpVal = document.getElementById('xpVal');
const lvBadge = document.getElementById('levelBadge');
const hpPct = Math.max(0,(player.hp/player.maxHp)*100);
hp.style.width = hpPct+'%';
hpVal.textContent = Math.ceil(player.hp)+'/'+player.maxHp;
const lv = player.level, maxLv=LEVEL_XP.length;
const curXp = player.xp - (LEVEL_XP[lv-1]||0);
const needXp = (LEVEL_XP[lv]||LEVEL_XP[maxLv-1]) - (LEVEL_XP[lv-1]||0);
const xpPct = lv>=maxLv ? 100 : Math.min(100,(curXp/needXp)*100);
xpBar.style.width = xpPct+'%';
xpVal.textContent = Math.floor(curXp)+'/'+needXp;
lvBadge.textContent = `LV ${lv} – ${LEVEL_NAMES[lv-1]||'Slimera'}`;
updateAbilityBar();
updateMinimap();
}
function updateAbilityBar(){
const bar = document.getElementById('abilityBar');
bar.innerHTML='';
const now = Date.now();
for(let i=0;i<Math.min(5,player.abilities.length);i++){
const id = player.abilities[i];
const abil = ABILITIES[id];
const cdEnd = player.abilityCooldowns[id]||0;
const isActive = player.activeEffects[id] && player.activeEffects[id]>now;
const cdLeft = Math.max(0,(cdEnd-now)/1000);
const slot = document.createElement('div');
slot.className = 'ability-slot'+(i===player.activeAbilityIdx?' active':'')+(cdLeft<=0?' ready':'');
slot.innerHTML = `${abil.emoji}${cdLeft>0?`<div class="cd-overlay">${cdLeft.toFixed(1)}</div>`:''}${isActive?`<div class="cd-overlay" style="background:rgba(0,200,80,.5)">⚡</div>`:''}
<span class="key-hint">${i+1}</span>`;
slot.addEventListener('click',()=>useAbility(i));
bar.appendChild(slot);
}
}
// ─────────────────────────────────────────────
// MINIMAP
// ─────────────────────────────────────────────
function initMinimap(){
minimapCanvas = document.createElement('canvas');
minimapCanvas.width = 100; minimapCanvas.height = 100;
minimapCanvas.id = 'minimapCanvas';
const div = document.createElement('div');
div.id = 'minimap'; div.appendChild(minimapCanvas);
document.body.appendChild(div);
minimapCtx = minimapCanvas.getContext('2d');
}
function updateMinimap(){
if(!minimapCtx) return;
const mc=minimapCtx, mw=100, mh=100;
mc.clearRect(0,0,mw,mh);
// bg
mc.fillStyle='rgba(0,0,0,.5)'; mc.fillRect(0,0,mw,mh);
// tiles
const stepX = WORLD_W/mw, stepY = WORLD_H/mh;
for(let y=0;y<mh;y+=2) for(let x=0;x<mw;x+=2){
const t = tileAt(x*stepX, y*stepY);
mc.fillStyle = t==='water'?'#2a6090':t==='forest'?'#1a4a10':t==='deep_grass'?'#2a6020':'#3a8030';
mc.fillRect(x,y,2,2);
}
// creatures as dots
const nightVision = player.activeEffects.night && player.activeEffects.night > Date.now();
for(const c of creatures){
if(!nightVision && Math.hypot(player.x-c.x,player.y-c.y)>400) continue;
const mx2 = c.x/WORLD_W*mw, my2 = c.y/WORLD_H*mh;
mc.fillStyle = c.def.rare ? '#ffd700' : c.def.col;
mc.fillRect(mx2-1,my2-1,2,2);
}
// player dot
const px=player.x/WORLD_W*mw, py=player.y/WORLD_H*mh;
mc.fillStyle=player.color;
mc.shadowColor=player.color; mc.shadowBlur=5;
mc.beginPath();mc.arc(px,py,3,0,Math.PI*2);mc.fill();
mc.shadowBlur=0;
// viewport box
mc.strokeStyle='rgba(255,255,255,.4)';mc.lineWidth=1;
mc.strokeRect(cam.x/WORLD_W*mw, cam.y/WORLD_H*mh, W/WORLD_W*mw, H/WORLD_H*mh);
}
// ─────────────────────────────────────────────
// MESSAGES
// ─────────────────────────────────────────────
let msgTimeout=null;
function showMsg(title, body, dur=2800){
const el = document.getElementById('msgPopup');
document.getElementById('msgTitle').textContent=title;
document.getElementById('msgBody').textContent=body;
el.classList.remove('hidden');
if(msgTimeout) clearTimeout(msgTimeout);
msgTimeout = setTimeout(()=>el.classList.add('hidden'), dur);
}
function showAbilityUnlock(id, fromName){
const abil = ABILITIES[id];
const el = document.getElementById('abilityUnlock');
document.getElementById('unlockName').textContent = abil.emoji+' '+abil.name+' UNLOCKED!';
document.getElementById('unlockDesc').textContent = `Absorbed from ${fromName}! ${abil.desc}`;
el.classList.remove('hidden');
setTimeout(()=>el.classList.add('hidden'), 3500);
}
// ─────────────────────────────────────────────
// GAME LOOP
// ─────────────────────────────────────────────
function gameLoop(ts){
if(!gameRunning) return;
const dt = Math.min(50, ts - lastTime);
lastTime = ts;
updatePlayer(dt);
for(const c of creatures) updateCreatureAI(c, dt);
updateParticles(dt);
// Draw
ctx.clearRect(0,0,W,H);
drawWorld();
drawDecorations();
for(const c of creatures) drawCreature(c);
drawPlayer();
drawParticlesAndTexts();
requestAnimationFrame(gameLoop);
}
// ─────────────────────────────────────────────
// START / END GAME
// ─────────────────────────────────────────────
function startGame(){
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
// Reset player
Object.assign(player,{
x:WORLD_W/2,y:WORLD_H/2,vx:0,vy:0,
hp:100,maxHp:100,xp:0,level:1,
size:14,baseSpeed:2.2,
color:'#7dff7a',color2:'#4acc45',
abilities:[],activeAbilityIdx:0,
abilityCooldowns:{},abilityEndTimes:{},
activeEffects:{},wobble:0,
facing:0,devourAnim:0,dead:false,invincible:0
});
genWorld();
spawnCreatures();
initMinimap();
gameRunning=true;
lastTime=performance.now();
requestAnimationFrame(gameLoop);
showMsg('🌿 Welcome to Slimera!','Find and devour creatures to grow & evolve!',3000);
}
function endGame(){
gameRunning=false;
player.dead=true;
const el = document.getElementById('gameOverScreen');
document.getElementById('gameOverText').textContent =
`You reached Level ${player.level} (${LEVEL_NAMES[player.level-1]}) with ${Math.floor(player.xp)} XP and ${player.abilities.length} abilities!`;
el.classList.remove('hidden');
}
// ─────────────────────────────────────────────
// INPUT
// ─────────────────────────────────────────────
document.addEventListener('keydown',e=>{
keys[e.key]=true;
if(e.key===' '||e.key==='e'||e.key==='E'){ e.preventDefault(); tryDevour(); }
if(e.key==='1') useAbility(0);
if(e.key==='2') useAbility(1);
if(e.key==='3') useAbility(2);
if(e.key==='4') useAbility(3);
if(e.key==='5') useAbility(4);
});
document.addEventListener('keyup',e=>{ keys[e.key]=false; });
// Mobile joystick
const joystickArea = document.getElementById('joystickArea');
const joystickKnob = document.getElementById('joystickKnob');
let joystickOrigin = null;
joystickArea.addEventListener('touchstart',e=>{
e.preventDefault();
const t=e.touches[0];
const r=joystickArea.getBoundingClientRect();
joystickOrigin={x:r.left+r.width/2, y:r.top+r.height/2};
joystick.active=true;
},{passive:false});
joystickArea.addEventListener('touchmove',e=>{
e.preventDefault();
if(!joystickOrigin) return;
const t=e.touches[0];
const dx=t.clientX-joystickOrigin.x, dy=t.clientY-joystickOrigin.y;
const dist=Math.hypot(dx,dy);
const maxR=40;
const fx=dist>maxR?dx/dist:dx/maxR;
const fy=dist>maxR?dy/dist:dy/maxR;
joystick.dx=fx; joystick.dy=fy;
const kx=Math.min(maxR,dist)*fx, ky=Math.min(maxR,dist)*fy;
joystickKnob.style.left=(55+kx-23)+'px';
joystickKnob.style.top=(55+ky-23)+'px';
},{passive:false});
['touchend','touchcancel'].forEach(ev=>joystickArea.addEventListener(ev,e=>{
joystick.active=false;joystick.dx=0;joystick.dy=0;
joystickKnob.style.left='32px';joystickKnob.style.top='32px';
}));
document.getElementById('btnDevour').addEventListener('touchstart',e=>{ e.preventDefault(); tryDevour(); },{passive:false});
document.getElementById('btnDevour').addEventListener('click',()=>tryDevour());
document.getElementById('btnAbility').addEventListener('touchstart',e=>{ e.preventDefault(); useAbility(player.activeAbilityIdx); },{passive:false});
document.getElementById('btnAbility').addEventListener('click',()=>useAbility(player.activeAbilityIdx));
// swipe ability switch
let abSwipe=null;
document.getElementById('btnAbility').addEventListener('touchstart',e=>{ abSwipe={x:e.touches[0].clientX}; });
document.getElementById('btnAbility').addEventListener('touchend',e=>{
if(!abSwipe) return;
const dx=e.changedTouches[0].clientX-abSwipe.x;
if(Math.abs(dx)>30){
if(dx>0) player.activeAbilityIdx=Math.min(player.abilities.length-1,player.activeAbilityIdx+1);
else player.activeAbilityIdx=Math.max(0,player.activeAbilityIdx-1);
updateAbilityBar();
}
abSwipe=null;
});
document.getElementById('startBtn').addEventListener('click',startGame);
document.getElementById('restartBtn').addEventListener('click',startGame);
</script>
</body>
</html>
4
3
142KB
149KB
208.0ms
400.0ms
294.0ms