"use strict"; const ANT_NEST_START_COUNT = 3; const ANT_NEST_MAX_COUNT = 10; const ANT_NEST_MAX_OUTSIDE = 3; const ANT_WORKER_HP = 24; const ANT_QUEEN_HP = 135; const ANT_WORKER_RADIUS = 4.2; const ANT_QUEEN_RADIUS = 9.5; const ANT_WORKER_SEARCH_SPEED = 12.9375; const ANT_WORKER_RETURN_SPEED = 18.0; const ANT_WORKER_RETURN_WEAK_SPEED = 9.0; const ANT_WORKER_DRAG_SPEED = 6.1875; const ANT_QUEEN_SPEED = 13.5; function clampAntHp(v) { return clamp(Number(v) || 0, 0, ANT_WORKER_HP); } function buildAntWorkerPool(count = ANT_NEST_START_COUNT) { const n = clamp(Math.round(Number(count) || 0), 0, ANT_NEST_MAX_COUNT); return Array.from({ length: n }, () => ANT_WORKER_HP); } function sanitizeAntWorkerPool(list, fallbackCount = 0) { if (!Array.isArray(list)) return buildAntWorkerPool(fallbackCount); const out = list.map(v => clampAntHp(v)).filter(v => v > 0.01); return out.slice(0, ANT_NEST_MAX_COUNT); } function ensureAntNestWorkerPool(nest) { if (!nest || nest.type !== "ant_nest") return []; // Extension hook: future nest hunger/growth state should attach to the nest // item here without changing current worker count balance. if (!Array.isArray(nest.antWorkers)) { nest.antWorkers = buildAntWorkerPool(nest.antCount ?? ANT_NEST_START_COUNT); } else { nest.antWorkers = sanitizeAntWorkerPool(nest.antWorkers, nest.antCount ?? nest.antWorkers.length); } return nest.antWorkers; } function syncAntNestCount(worldRef, nest) { if (!nest || nest.type !== "ant_nest") return 0; ensureAntNestWorkerPool(nest); const outside = (worldRef?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.homeId === nest.id).length; nest.antCount = clamp((nest.antWorkers?.length || 0) + outside, 0, ANT_NEST_MAX_COUNT); if (nest.antCount <= 0) { nest.antWorkers = []; nest.amount = 0; if (worldRef) worldRef.drawListDirty = true; } return nest.antCount; } class AntActor { constructor(worldRef, opts = {}) { this.world = worldRef; this.id = opts.id || (crypto.randomUUID ? crypto.randomUUID() : `ant-${Date.now()}-${Math.random()}`); this.kind = opts.kind || "worker"; this.homeId = opts.homeId || ""; this.targetId = opts.targetId || ""; this.targetToken = opts.targetToken || 0; this.targetRef = opts.targetRef || null; this.x = Number.isFinite(opts.x) ? opts.x : 0; this.y = Number.isFinite(opts.y) ? opts.y : 0; this.vx = opts.vx || 0; this.vy = opts.vy || 0; this.r = opts.r || (this.kind === "queen" ? ANT_QUEEN_RADIUS : ANT_WORKER_RADIUS); this.maxHp = Number.isFinite(opts.maxHp) ? opts.maxHp : (this.kind === "queen" ? ANT_QUEEN_HP : ANT_WORKER_HP); this.hp = Number.isFinite(opts.hp) ? opts.hp : this.maxHp; this.hpBarTimer = Number.isFinite(opts.hpBarTimer) ? opts.hpBarTimer : 0; this.state = opts.state || (this.kind === "queen" ? "founding" : "search"); this.age = opts.age || 0; this.seed = opts.seed || Math.random() * 1000; this.wanderAngle = opts.wanderAngle || rand(0, Math.PI * 2); this.nextTurn = opts.nextTurn || rand(0.4, 1.6); this.foundingX = opts.foundingX; this.foundingY = opts.foundingY; this.foundingDelayUntil = Number.isFinite(opts.foundingDelayUntil) ? opts.foundingDelayUntil : -Infinity; this.carryLogAt = opts.carryLogAt || -999; this.foundingAttempts = opts.foundingAttempts || 0; this.dead = Boolean(opts.dead); this.burning = Boolean(opts.burning); this.burnTimer = Number.isFinite(opts.burnTimer) ? opts.burnTimer : 0; this.burnTurnTimer = 0; } igniteFire(source = null) { if (this.dead) return false; globalThis.TarinaiAchievements?.recordAntDamageSource?.(this, source, { world: this.world, reason: "fire" }); this.burning = true; this.burnTimer = Math.max(this.burnTimer || 0, 4.2 + rand(0, 2.4)); this.state = "panic"; this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.burnWanderAngle = rand(0, Math.PI * 2); this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.0); this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(10, this.r * 2.8), life: 0.18, color: "rgba(255,110,36,0.56)" })); return true; } extinguishFire(reason = "") { if (!this.burning && !(this.burnTimer > 0)) return false; this.burning = false; this.burnTimer = 0; this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(9, this.r * 2.2), life: 0.16, color: "rgba(108,192,236,0.58)" })); return true; } updateBurning(dt) { if (!this.burning && !(this.burnTimer > 0)) return false; this.burning = true; this.burnTimer = Math.max(0, Number(this.burnTimer || 0) - dt); this.hp = Math.max(0, Number(this.hp || 0) - dt * (this.kind === "queen" ? 11 : 7.5)); this.hpBarTimer = Math.max(this.hpBarTimer || 0, 0.55); const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.r || 5, 7)); if (water) { globalThis.TarinaiItemDynamicToolSystem?.consumeWaterForFire?.(this.world, water, 5); this.extinguishFire("water"); return true; } const mul = globalThis.TarinaiItemDynamicToolSystem?.fireTempMultiplier?.(this.world, this.x, this.y, this) || 1; if (deterministicChance(this.world, "ant-fire-extinguish", dt * 0.020 * mul, this)) { this.extinguishFire("natural"); return true; } globalThis.TarinaiBurnMotionUtil.updateBurnWanderAngle(this, dt, { angleKey: "ant-burn-angle", turnKey: "ant-burn-turn", jitterKey: "ant-burn-jitter", bucketScale: 6, turnMin: 0.08, turnMax: 0.28, jitterMin: -9.0, jitterMax: 9.0, }); const speed = this.kind === "queen" ? ANT_QUEEN_SPEED * 3.0 : ANT_WORKER_RETURN_SPEED * 3.3; this.vx = Math.cos(this.burnWanderAngle || 0) * speed; this.vy = Math.sin(this.burnWanderAngle || 0) * speed; this.x = clamp(this.x + this.vx * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding); this.y = clamp(this.y + this.vy * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding); for (const viewer of [...(this.world?.nearbyTarinai?.(this.x, this.y, 170, true) || [])]) { if (!viewer || viewer.dead) continue; globalThis.TarinaiItemDynamicToolSystem?.panicTarinaiFromFire?.(viewer, this, this.world); } const contactRadius = Math.max(10, (this.r || 5) * 2.1); for (const t of [...(this.world?.nearbyTarinai?.(this.x, this.y, contactRadius + 26, true) || [])]) { if (!t || t.dead) continue; if (distXY(this.x, this.y, t.x, t.y) <= contactRadius + Math.max(t.radius || 18, 12) * 0.7) t.igniteFire?.(this, false); } for (const it of [...(this.world?.nearbyItems?.(this.x, this.y, contactRadius + 28, true) || [])]) { if (!it || it.dead || it.type !== "grass") continue; if (distXY(this.x, this.y, it.x, it.y) <= contactRadius + Math.max(it.r || 16, 12)) globalThis.TarinaiItemDynamicToolSystem?.igniteGrassByFire?.(it, this, this.world, dt); } if (deterministicChance(this.world, "ant-fire-effect", dt * 2.0, this)) { this.world?.effects?.push(new Effect("flame", this.x, this.y - this.r, { vx: rand(-2, 2), vy: rand(-14, -6), size: Math.max(4, this.r * 1.0), life: 0.22, color: "rgba(236,46,24,0.72)" })); } if (this.burnTimer <= 0.01) this.extinguishFire("timeout"); return true; } homeNest() { const nest = this.world?.itemById?.(this.homeId) || null; return nest && nest.type === "ant_nest" ? nest : null; } adoptableNest() { let best = null; let bestScore = Infinity; for (const nest of this.world?.items || []) { if (!nest || nest.dead || nest.type !== "ant_nest") continue; if (typeof ensureAntNestWorkerPool === "function") ensureAntNestWorkerPool(nest); const outside = (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.homeId === nest.id).length; const inside = Array.isArray(nest.antWorkers) ? nest.antWorkers.length : Math.max(0, (nest.antCount || 0) - outside); const count = inside + outside; if (count >= (ANT_NEST_MAX_COUNT || 10)) continue; const d = distXY(this.x, this.y, nest.x, nest.y); const score = d + count * 26; if (score < bestScore) { best = nest; bestScore = score; } } return best; } updateWanderNoNest(dt) { const adopt = this.adoptableNest(); if (adopt) { this.homeId = adopt.id; this.state = "return"; this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.world.drawListDirty = true; return; } this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.state = "wander"; this.nextTurn -= dt; if (this.nextTurn <= 0) { this.wanderAngle += rand(-1.35, 1.35); this.nextTurn = rand(0.55, 1.45); } const speed = (ANT_WORKER_SEARCH_SPEED || 10) * clamp((this.hp || this.maxHp || ANT_WORKER_HP) / Math.max(1, this.maxHp || ANT_WORKER_HP), 0.45, 1.0) * 0.86; this.vx = Math.cos(this.wanderAngle) * speed; this.vy = Math.sin(this.wanderAngle) * speed * 0.78; this.x = clamp(this.x + this.vx * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding); this.y = clamp(this.y + this.vy * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding); } targetTarinai() { if (this.targetRef && !this.targetRef.dead && this.targetRef.id === this.targetId && this.targetRef.liveToken === this.targetToken) return this.targetRef; const live = this.world?.liveTarinaiById?.(this.targetId, this.targetToken) || null; this.targetRef = live; return live; } activeHaulersForTarget() { return (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId && a.targetId === this.targetId && a.targetToken === this.targetToken); } isLeadHauler() { const haulers = this.activeHaulersForTarget(); if (!haulers.length) return true; let lead = haulers[0]; for (const a of haulers) if (String(a.id) < String(lead.id)) lead = a; return lead === this; } chooseTarget(maxDist = 56) { // Extension hook: future carrying success and territorial behavior should // filter or rank candidates here while preserving sleep-disease hauling. let best = null; let bestScore = Infinity; const candidates = this.world?.nearbyTarinai?.(this.x, this.y, maxDist, true) || this.world?.tarinai || []; for (const t of candidates) { if (!t || t.dead || t.insideNestBoxId || t.isZunchiSlave) continue; const d = distXY(this.x, this.y, t.x, t.y); if (d > maxDist) continue; if ((t.antDraggedTimer || 0) > 0 && (t.antDraggedBy || "") && t.antDraggedBy !== this.id) { const haulers = (this.world?.ants || []).filter(a => a && !a.dead && a.kind === "worker" && a.state === "drag" && a.targetId === t.id && a.targetToken === t.liveToken).length; if (haulers >= ANT_NEST_MAX_OUTSIDE) continue; } const weak = 1 - (typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(t) : clamp((t.energy || 0) / Math.max(1, t.maxEnergy || 100), 0, 1)); const score = d - weak * 18 + stableUnit(`${this.id}:${t.id}`, "ant-prey") * 6; if (score < bestScore) { best = t; bestScore = score; } } return best; } update(dt) { if (this.dead) return; const hpBefore = this.hp; this.age += dt; if (Number(this.electricShockUntil || 0) > Number(this.world?.time || 0)) { this.state = "panic"; this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.nextTurn = Math.min(Number(this.nextTurn || 0), 0); this.wanderAngle = Number(this.wanderAngle || 0) + rand(-2.4, 2.4); const shockSpeed = (this.kind === "queen" ? ANT_QUEEN_SPEED : ANT_WORKER_RETURN_SPEED) * 1.75; this.vx = Math.cos(this.wanderAngle) * shockSpeed; this.vy = Math.sin(this.wanderAngle) * shockSpeed; this.x = clamp(this.x + this.vx * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding); this.y = clamp(this.y + this.vy * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding); if (this.hp <= 0) this.dieAsCorpse(this.homeNest?.() || null); } else if (this.burning || (this.burnTimer || 0) > 0.02) { this.updateBurning(dt); if (this.hp <= 0) this.dieAsCorpse(this.homeNest?.() || null); } else if (this.kind === "queen") this.updateQueen(dt); else this.updateWorker(dt); if (Number.isFinite(hpBefore) && this.hp < hpBefore - 0.01) this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.25); this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt); } updateWorker(dt) { const home = this.homeNest(); if (!home) { this.updateWanderNoNest(dt); return; } if (this.hp <= 0) { this.dieAsCorpse(home); return; } if (this.state === "drag") return this.updateDrag(dt, home); if (this.state === "return") return this.updateReturn(dt, home); this.updateSearch(dt, home); } updateSearch(dt, home) { const target = this.chooseTarget(52); if (target) { this.state = "drag"; this.targetId = target.id; this.targetToken = target.liveToken || 0; this.targetRef = target; this.carryLogAt = this.world?.time || 0; target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.45); target.antDraggedBy = this.id; target.fearTimer = Math.max(target.fearTimer || 0, 0.55); target.thought = "\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b"; target.recordChangeCause?.("\u30a2\u30ea\u306b\u904b\u3070\u308c\u305f", "\u72b6\u614b"); audio.antDrag?.(); this.world.drawListDirty = true; return; } this.nextTurn -= dt; if (this.nextTurn <= 0) { const toHome = Math.atan2(home.y - this.y, home.x - this.x); const far = distXY(this.x, this.y, home.x, home.y); const bias = far > 220 ? 0.58 : 0.18; this.wanderAngle = this.wanderAngle * (1 - bias) + toHome * bias + rand(-1.2, 1.2); this.nextTurn = rand(0.7, 1.9); } const hpFactor = clamp(this.hp / ANT_WORKER_HP, 0.45, 1.0); const speed = (ANT_WORKER_SEARCH_SPEED + Math.sin(this.age * 2.4 + this.seed) * 0.75) * hpFactor; this.vx = Math.cos(this.wanderAngle) * speed; this.vy = Math.sin(this.wanderAngle) * speed * 0.78; this.x = clamp(this.x + this.vx * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding); this.y = clamp(this.y + this.vy * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding); if (distXY(this.x, this.y, home.x, home.y) > 220) this.state = "return"; } returnToNest(home) { if (!home || home.dead || home.type !== "ant_nest") { const adopt = this.adoptableNest(); if (adopt) { this.homeId = adopt.id; this.state = "return"; return; } this.updateWanderNoNest(0.016); return; } ensureAntNestWorkerPool(home).push(clampAntHp(this.hp)); syncAntNestCount(this.world, home); this.dead = true; this.world.drawListDirty = true; } updateReturn(dt, home) { const d = distXY(this.x, this.y, home.x, home.y) || 1; if (d < Math.max(16, home.r * 0.66)) { this.returnToNest(home); return; } const speed = this.hp <= 8 ? ANT_WORKER_RETURN_WEAK_SPEED : ANT_WORKER_RETURN_SPEED; this.x += (home.x - this.x) / d * speed * dt; this.y += (home.y - this.y) / d * speed * dt; this.vx = (home.x - this.x) / d * speed; this.vy = (home.y - this.y) / d * speed; } updateDrag(dt, home) { const target = this.targetTarinai(); if (!target || target.dead || target.insideNestBoxId) { this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.state = "return"; return; } const haulers = Math.max(1, this.activeHaulersForTarget().length); const leadHauler = this.isLeadHauler(); const targetWasSleeping = Boolean(target.sleeping || target.state === "sleep"); target.antDraggedTimer = Math.max(target.antDraggedTimer || 0, 0.35); target.antDraggedBy = this.id; if (leadHauler && (this.world?.time || 0) - (this.lastCarryEventAt || -999) > 0.75) { this.lastCarryEventAt = this.world?.time || 0; } target.target = null; if (targetWasSleeping) { target.startSleeping?.(null, "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b"); } else { if (target.enterPanic) target.enterPanic({ threat: this, reason: "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b", fear: 0.36, wake: true, cause: "ant_drag" }); else { target.setActionState?.("panic", { target: target.panicDestination ? target.panicDestination(this) : null, reason: "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b", wake: true }); target.fearTimer = Math.max(target.fearTimer || 0, 0.36); } } const dHome = distXY(target.x, target.y, home.x, home.y) || 1; if (dHome < Math.max(18, home.r * 0.66)) { this.world.completeAntHaul?.(home, target); this.targetId = ""; this.targetToken = 0; this.targetRef = null; this.state = "return"; return; } const hpFactor = clamp(this.hp / ANT_WORKER_HP, 0.45, 1.0); const haulerBoost = 1 + Math.min(0.18, Math.max(0, haulers - 1) * 0.045); const speed = ANT_WORKER_DRAG_SPEED * hpFactor * haulerBoost; const ux = (home.x - target.x) / dHome; const uy = (home.y - target.y) / dHome; if (leadHauler) { target.x = clamp(target.x + ux * speed * dt, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding); target.y = clamp(target.y + uy * speed * dt, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding); target.energy = clamp((target.energy || 0) - dt * 0.028, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(target) : (Number(target.maxEnergy) || 100)); } const offsetAngle = this.seed + this.age * 2.0 + (haulers > 1 ? this.activeHaulersForTarget().findIndex(a => a === this) * 1.7 : 0); const ring = target.radius * (0.64 + 0.06 * Math.min(haulers, 3)); this.x = target.x - ux * ring + Math.cos(offsetAngle) * target.radius * 0.24; this.y = target.y - uy * ring * 0.72 + Math.sin(offsetAngle) * target.radius * 0.18; this.vx = ux * speed; this.vy = uy * speed; const aggression = Math.max(0, target.currentPersonality?.aggression || 0); const stillSleeping = Boolean(target.sleeping || targetWasSleeping || target.state === "sleep"); const aggressiveAttack = !stillSleeping && (target.shouldApplyPersonalityBehavior?.("aggression", 1) || aggression >= 0.5 || target.state === "ant_attack"); const struggle = stillSleeping ? 0 : (aggressiveAttack ? (1.25 + aggression * 1.6 + (typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(target) : clamp((target.energy || 0) / Math.max(1, target.maxEnergy || 100), 0, 1)) * 0.35) / Math.max(1, haulers * 0.9) : (0.42 + (typeof tarinaiEnergyRatio === "function" ? tarinaiEnergyRatio(target) : clamp((target.energy || 0) / Math.max(1, target.maxEnergy || 100), 0, 1)) * 0.18) / Math.max(1, haulers)); globalThis.TarinaiAchievements?.recordAntDamageSource?.(this, target, { world: this.world, reason: "tarinai_struggle" }); this.hp -= dt * struggle; if ((this.world.time || 0) - (this.carryLogAt || -999) > 8 && Math.random() < dt * 0.04) { this.carryLogAt = this.world.time || 0; this.world.log(`${target.name}\u304c\u30a2\u30ea\u306b\u5f15\u3063\u5f35\u3089\u308c\u3066\u3044\u308b\u3002`, "event", { participants: [target] }); } if (this.hp <= 0) this.dieAsCorpse(home); } dieAsCorpse(home = null) { if (this.dead) return; this.dead = true; globalThis.TarinaiAchievements?.recordAntKilled?.({ world: this.world, ant: this, home }); audio.antDie?.(); const corpse = new Item("ant_corpse", this.x, this.y); corpse.amount = 24; this.world.addItem?.(corpse, "ant-corpse"); this.world.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" })); if (home) syncAntNestCount(this.world, home); this.world.ensureItemBuckets?.("ant-corpse"); this.world.drawListDirty = true; } buildNestAtFoundingSpot(force = false) { if (!this.world) return null; // Extension hook: queen reproduction currently means founding a new nest. // Future growth rules should branch here without changing current behavior. const base = new Item("ant_nest", this.foundingX, this.foundingY); let spot = null; if (this.world.findPlacementSpot) { spot = this.world.findPlacementSpot(base, { allowOriginal: true, maxRadius: force ? 520 : 220, attempts: force ? 160 : 72 }); } if (!spot && force) { spot = this.world.placementClampPointFor ? this.world.placementClampPointFor(base, this.foundingX, this.foundingY) : { x: this.foundingX, y: this.foundingY }; } if (!spot) return null; base.x = spot.x; base.y = spot.y; base.antCount = ANT_NEST_START_COUNT; base.antWorkers = buildAntWorkerPool(ANT_NEST_START_COUNT); if (!force && this.world.placementBlocked?.(base)) return null; const addedNest = this.world.addItem?.(base, "ant-nest-built") || null; if (!addedNest) return null; audio.antNest?.(); this.world.ensureSpatial?.("ant-nest-built"); this.world.drawListDirty = true; this.world.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u30a2\u30ea\u306e\u5de3\u3092\u4f5c\u3063\u305f\u3002", "event"); return base; } updateQueen(dt) { if (!Number.isFinite(this.foundingX) || !Number.isFinite(this.foundingY)) { const p = this.world?.findAntNestFoundingSpot?.(this) || { x: this.x + rand(-140, 140), y: this.y + rand(-100, 100) }; this.foundingX = p.x; this.foundingY = p.y; } let d = distXY(this.x, this.y, this.foundingX, this.foundingY) || 1; if (d < 20) { if ((this.world?.time || 0) < (this.foundingDelayUntil || -Infinity)) { this.vx = 0; this.vy = 0; return; } this.foundingAttempts += 1; const placed = this.buildNestAtFoundingSpot(this.foundingAttempts >= 8); if (placed) { this.dead = true; return; } const retrySpot = this.world?.findAntNestFoundingSpot?.({ x: this.x, y: this.y }) || this.world?.findAntNestFoundingSpot?.(this.homeNest?.() || this); if (retrySpot) { this.foundingX = retrySpot.x; this.foundingY = retrySpot.y; } else { this.wanderAngle += rand(-1.4, 1.4); this.foundingX = clamp(this.x + Math.cos(this.wanderAngle) * 90, 56, this.world.w - 56); this.foundingY = clamp(this.y + Math.sin(this.wanderAngle) * 70, 56, this.world.h - 56); } d = distXY(this.x, this.y, this.foundingX, this.foundingY) || 1; } const speed = ANT_QUEEN_SPEED; this.x += (this.foundingX - this.x) / d * speed * dt; this.y += (this.foundingY - this.y) / d * speed * dt; this.vx = (this.foundingX - this.x) / d * speed; this.vy = (this.foundingY - this.y) / d * speed; } workerSpriteId() { return "ant_worker"; } spriteId() { return this.kind === "queen" ? "ant_queen" : this.workerSpriteId(); } draw(ctx, ...args) { if (this.dead) return; const id = this.spriteId(); const img = getRenderableImage(id, id); const lighting = args.find(value => value && typeof value === "object") || null; const lightState = lighting || getLightingState(this.world); ctx.save(); const shadow = projectedShadowParams(lightState, this.kind === "queen" ? 0.78 : 0.26); drawProjectedShadow(ctx, this.x, this.y + this.r * 0.62, this.r * 1.5, this.r * 0.30, { ...shadow, alpha: shadow.alpha * 0.72 }); ctx.translate(this.x, this.y); const angle = this.kind === "queen" ? Math.atan2((this.foundingY ?? this.y) - this.y, (this.foundingX ?? this.x) - this.x) : Math.atan2(this.vy || 0, this.vx || 1); ctx.rotate(angle); if (img) { const w = this.kind === "queen" ? this.r * 5.25 : this.r * 3.8; const metrics = getImageMetrics(id); const h = w * (metrics?.ratio || 0.52); ctx.globalAlpha = 0.96; ctx.drawImage(img, -w * 0.5, -h * 0.58, w, h); } else { ctx.fillStyle = "rgba(44,44,44,0.92)"; ctx.beginPath(); ctx.ellipse(0, 0, this.r * 1.3, this.r * 0.72, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(this.r * 1.15, -this.r * 0.05, this.r * 0.46, 0, Math.PI * 2); ctx.fill(); } if (this.burning || (this.burnTimer || 0) > 0.02) { const drawFire = typeof drawSimpleFireShape === "function" ? drawSimpleFireShape : null; if (drawFire) { const phase = this.seed + this.age * 8.0; drawFire(ctx, -this.r * 0.10, -this.r * 0.86, Math.max(5, this.r * 1.05), { phase, alpha: 0.86, shadow: false }); drawFire(ctx, this.r * 0.54, -this.r * 0.48, Math.max(4, this.r * 0.78), { phase: phase + 1.5, alpha: 0.72, shadow: false }); } else { ctx.fillStyle = "rgba(236,46,24,0.78)"; ctx.beginPath(); ctx.ellipse(0, -this.r * 0.8, this.r * 0.42, this.r * 0.86, 0, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); this.drawHealthBar(ctx); } drawHealthBar(ctx) { if ((this.hpBarTimer || 0) <= 0) return; const maxHp = Math.max(1, this.maxHp || (this.kind === "queen" ? ANT_QUEEN_HP : ANT_WORKER_HP)); const pct = clamp((this.hp || 0) / maxHp, 0, 1); const bw = this.kind === "queen" ? 28 : 18; const bh = this.kind === "queen" ? 4 : 3; const y = this.y - this.r * (this.kind === "queen" ? 2.0 : 2.6); ctx.save(); ctx.globalAlpha = clamp((this.hpBarTimer || 0) / 0.65, 0.35, 1); ctx.fillStyle = "rgba(255,252,242,0.86)"; roundedRect(ctx, this.x - bw / 2 - 1.5, y - 1.5, bw + 3, bh + 3, 4); ctx.fill(); ctx.fillStyle = "rgba(63,50,42,0.30)"; roundedRect(ctx, this.x - bw / 2, y, bw, bh, 3); ctx.fill(); ctx.fillStyle = pct > 0.45 ? "rgba(118,190,80,0.88)" : (pct > 0.20 ? "rgba(232,172,68,0.90)" : "rgba(218,82,70,0.92)"); roundedRect(ctx, this.x - bw / 2, y, bw * pct, bh, 3); ctx.fill(); ctx.restore(); } }