/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Carnivore class
var Carnivore = Container.expand(function () {
var self = Container.call(this);
// Uncommon: 15% chance to be resistant to parasites
self.parasiteResistant = Math.random() < 0.15;
var carnAsset = self.attachAsset('carnivore', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.25,
scaleY: 1.25
});
self.target = null;
// Assign base speed, will adjust for personality below
self.speed = 1.5 + Math.random() * 1.2;
self.eaten = false;
self._chaseTime = 0; // How long we've been chasing a herbivore
self._tired = false; // Are we tired?
self._tiredTimer = 0; // How long left to rest
// Track tired attempts per herbivore (herbivore.id -> count)
self._tiredAttempts = {};
// --- Personality system ---
// Personalities: "normal", "bloodthirsty", "weak"
var carnPersonalities = ["normal", "bloodthirsty", "weak"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% bloodthirsty, 35% weak
if (rand < 0.3) {
self.personality = "normal";
} else if (rand < 0.65) {
self.personality = "bloodthirsty";
} else {
self.personality = "weak";
}
// Adjust speed for bloodthirsty carnivores
if (self.personality === "bloodthirsty") {
self.speed += 0.7 + Math.random() * 0.5; // noticeably faster
}
// For weak carnivores, no speed change, but will tire faster in update logic
// For weak: timer for running away from other carnivores
self._weakRunTimer = 0;
self._weakRunAngle = 0;
// --- State management for wandering/hungry ---
self.state = "hungry"; // "wandering" or "hungry"
self._eatCooldown = 0; // frames left in wandering after eating
self.update = function () {
if (self.eaten) return;
// --- CARNIVORES CANNOT STARVE: No starvation logic for carnivores ---
// --- WINTER BEHAVIOR: Wanderlust, ignore herbivores, and cannot starve ---
var isWinter = !!window.winterState;
if (isWinter) {
// Always wander, never chase or eat, never get tired, never starve
self.state = "wandering";
self.target = null;
self._tired = false;
self._tiredTimer = 0;
self._chaseTime = 0;
// Wander constantly
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
return;
}
// --- Personality: weak carnivore runs from other carnivores, can be infected by fungi ---
if (self.personality === "weak") {
// If running away, keep running for a short time
if (self._weakRunTimer > 0) {
self.x += Math.cos(self._weakRunAngle) * self.speed * 1.5;
self.y += Math.sin(self._weakRunAngle) * self.speed * 1.5;
self._weakRunTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// While running, don't do anything else
return;
}
// Check for nearby other carnivores (not self)
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var c = carnivores[ci];
if (c === self || c.eaten) continue;
var dx = c.x - self.x;
var dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minCarnDist) {
minCarnDist = dist;
nearestCarn = c;
}
}
if (nearestCarn && Math.sqrt(minCarnDist) < 220) {
// Run away from the other carnivore for 60-120 frames
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var angle = Math.atan2(dy, dx);
self._weakRunAngle = angle;
self._weakRunTimer = 60 + Math.floor(Math.random() * 60);
// Move immediately this frame
self.x += Math.cos(self._weakRunAngle) * self.speed * 1.5;
self.y += Math.sin(self._weakRunAngle) * self.speed * 1.5;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// Weak carnivores can be infected by fungi (handled in Parasite/Fungi logic)
}
// Handle tired state
// Bloodthirsty carnivores never get tired
if (self.personality === "bloodthirsty") {
self._tired = false;
self._tiredTimer = 0;
} else {
if (self._tired) {
self._tiredTimer--;
if (self._tiredTimer <= 0) {
self._tired = false;
self._chaseTime = 0;
}
// While tired, don't chase or wander, just stand still
return;
}
}
// Handle eat cooldown (wandering after eating)
if (self._eatCooldown > 0) {
self._eatCooldown--;
if (self._eatCooldown === 0) {
self.state = "hungry";
}
}
// State: wandering (ignore prey)
if (self.state === "wandering") {
self.target = null; // Don't chase
// --- Random wandering when not chasing herbivores ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._chaseTime = 0; // Not chasing, reset chase timer
return;
}
// State: hungry (look for prey)
if (self.state === "hungry") {
// --- CARNIVORE POPULATION CONTROL: If at max, hunt less frequently ---
if (typeof carnivores !== "undefined" && carnivores.length >= 4) {
// Only hunt every 120 frames (2 seconds)
if (typeof self._huntDelay === "undefined") self._huntDelay = Math.floor(Math.random() * 120);
self._huntDelay++;
if (self._huntDelay < 120) {
// Wander instead of hunting
self.state = "wandering";
self._huntDelay = self._huntDelay % 120;
return;
}
self._huntDelay = 0;
}
// Find nearest prey if not already targeting
if (!self.target || self.target.eaten) {
var minDist = 999999,
closest = null;
// --- Bloodthirsty: can hunt herbivores and other carnivores (except self) ---
if (self.personality === "bloodthirsty") {
// Hunt herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
// Hunt other carnivores (not self, not eaten)
for (var i = 0; i < carnivores.length; i++) {
var c = carnivores[i];
if (c === self || c.eaten) continue;
var dx = c.x - self.x,
dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = c;
}
}
} else if (self.personality === "weak") {
// Weak carnivores do not hunt other carnivores, only herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Ignore this herbivore if we've gotten tired chasing it 2 or more times this cycle
if (h._uniqueId === undefined) {
// Assign a unique id to each herbivore if not present
h._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
}
if (self._tiredAttempts[h._uniqueId] >= 2) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
} else {
// Normal: only hunt herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Ignore this herbivore if we've gotten tired chasing it 2 or more times this cycle
if (h._uniqueId === undefined) {
// Assign a unique id to each herbivore if not present
h._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
}
if (self._tiredAttempts[h._uniqueId] >= 2) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
}
// (Carnivores ignore pollinators)
self.target = closest;
}
// --- Random wandering when not chasing herbivores ---
if ((!self.target || self.target.eaten) && (!self._wanderTimer || self._wanderTimer <= 0)) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (!self.target || self.target.eaten) {
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._chaseTime = 0; // Not chasing, reset chase timer
}
// Move toward target herbivore
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Track how long we've been chasing
self._chaseTime++;
// If we've been chasing for more than X seconds, get tired
var chaseLimit = 360; // default 6 seconds
var tiredDuration = 600; // default 10 seconds
if (self.personality === "weak") {
chaseLimit = 180; // 3 seconds for weak carnivores
tiredDuration = 900; // weak carnivores rest longer (15 seconds)
}
if (self._chaseTime > chaseLimit) {
self._tired = true;
self._tiredTimer = tiredDuration;
self._chaseTime = 0;
// Track tired attempts for this herbivore
if (self.target && self.target._uniqueId !== undefined) {
if (!self._tiredAttempts[self.target._uniqueId]) self._tiredAttempts[self.target._uniqueId] = 0;
self._tiredAttempts[self.target._uniqueId]++;
// If tired 2 or more times, ignore this herbivore for the rest of the cycle
if (self._tiredAttempts[self.target._uniqueId] >= 2) {
self.target = null;
self.state = "wandering";
}
}
return;
}
// Eat target if close enough
// Prevent eating in autumn
if (dist < 60 && !self.target.eaten) {
if (window.autumnState) {
// In autumn, do not eat, just wander
self.target = null;
self.state = "wandering";
self._eatCooldown = 3600;
self._chaseTime = 0;
return;
}
// --- Bloodthirsty: can eat other carnivores or herbivores ---
// --- Normal/Weak: only eat herbivores ---
var ateCarnivore = false;
if (self.personality === "bloodthirsty" && carnivores.indexOf(self.target) !== -1 && self.target !== self) {
// Eat other carnivore
if (typeof self.target.die === "function") self.target.die();
ateCarnivore = true;
} else if (herbivores.indexOf(self.target) !== -1) {
// Eat herbivore
if (typeof self.target.die === "function") self.target.die();
} else {
// Not a valid target, do nothing
self.target = null;
return;
}
// Move away from the eaten target
if (self.target) {
var awayDX = self.x - self.target.x;
var awayDY = self.y - self.target.y;
var awayDist = Math.sqrt(awayDX * awayDX + awayDY * awayDY);
if (awayDist > 0) {
// Move 100px away in the opposite direction
self.x += awayDX / awayDist * 100;
self.y += awayDY / awayDist * 100;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
}
// Track herbivores/carnivores eaten for egg laying
if (ateCarnivore) {
self._herbivoresEaten = (self._herbivoresEaten || 0) + 1;
} else {
self._herbivoresEaten = (self._herbivoresEaten || 0) + 1;
}
// Lay an egg if 3 prey have been eaten, then reset counter
// --- CARNIVORE POPULATION CONTROL: Only lay egg if < 4 carnivores ---
if (self._herbivoresEaten >= 3) {
if (typeof carnivores !== "undefined" && carnivores.length < 4) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isCarnivoreEgg = true;
egg._carnivoreHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
self._herbivoresEaten = 0;
}
self.target = null;
self._chaseTime = 0; // Reset chase timer after eating
// Animate carnivore "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
// After eating, enter wandering state for 1 minute (3600 frames)
self.state = "wandering";
self._eatCooldown = 3600;
}
}
} // end hungry state
};
// Carnivores are not eaten in this MVP
return self;
});
// Egg class
var Egg = Container.expand(function () {
var self = Container.call(this);
// Always use carnivore_egg asset if this is a carnivore egg, pollinator_egg if pollinator, else normal egg
var eggAsset;
if (self._isCarnivoreEgg === true || self._carnivoreHatch === true) {
eggAsset = self.attachAsset('carnivore_egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
} else if (self._isPollinatorEgg === true || self._pollinatorHatch === true) {
eggAsset = self.attachAsset('pollinator_egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
} else {
eggAsset = self.attachAsset('egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
}
self.hatched = false;
self._hatchTimer = 420 + Math.floor(Math.random() * 180); // 7-10 seconds
self.update = function () {
if (self.hatched) return;
// Pause hatching if it's summer or winter
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
if (isSummer || isWinter) {
// Do not decrement hatch timer, eggs are dormant
return;
}
// --- POLLINATOR POPULATION CONTROL: Prevent pollinator eggs from hatching if there are already 10 pollinators ---
if ((self._isPollinatorEgg || self._pollinatorHatch) && typeof pollinators !== "undefined" && pollinators.length >= 10) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
// --- CARNIVORE POPULATION CONTROL: Prevent carnivore eggs from hatching if there are already 4 carnivores ---
if ((self._isCarnivoreEgg || self._carnivoreHatch) && typeof carnivores !== "undefined" && carnivores.length >= 4) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
self._hatchTimer--;
// --- PARASITE POPULATION CONTROL: Prevent parasite eggs from hatching if there are already 4 parasites ---
if ((self._isParasiteEgg || self._parasiteHatch) && typeof parasites !== "undefined" && parasites.length >= 4) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
// --- HERBIVORE POPULATION CONTROL: Prevent herbivore eggs from hatching if there are already 6 herbivores ---
if (!self._isCarnivoreEgg && !self._carnivoreHatch && !self._isPollinatorEgg && !self._pollinatorHatch && !self._isParasiteEgg && !self._parasiteHatch && typeof herbivores !== "undefined" && herbivores.length >= 6) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
if (self._hatchTimer <= 0) {
// In autumn, eggs have a reduced chance to hatch (e.g. 60% hatch, 40% fail)
if (window.autumnState && Math.random() < 0.4) {
// Egg fails to hatch, animate and destroy
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
self.hatched = true;
// Animate egg hatching
tween(self, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Spawn a new carnivore if this is a carnivore egg, pollinator if pollinator egg, parasite if parasite egg, otherwise herbivore
var popSnd = LK.getSound('Hatch');
if (self._isCarnivoreEgg && self._carnivoreHatch) {
var carn = new Carnivore();
carn.x = self.x;
carn.y = self.y;
carn.scaleX = 1;
carn.scaleY = 1;
carn.alpha = 1;
if (self.parent) self.parent.addChild(carn);
if (typeof carnivores !== "undefined") carnivores.push(carn);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else if (self._isPollinatorEgg && self._pollinatorHatch) {
var poll = new Pollinator();
poll.x = self.x;
poll.y = self.y;
poll.scaleX = 1;
poll.scaleY = 1;
poll.alpha = 1;
// Reset pollinator starve timer on hatch
poll._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// Set pollinator to hungry state after hatching
poll.state = "hungry";
poll.target = null;
if (self.parent) self.parent.addChild(poll);
if (typeof pollinators !== "undefined") pollinators.push(poll);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else if (self._isParasiteEgg && self._parasiteHatch) {
// If this egg was laid by a parasite attached to a resistant host, always inherit resistance
var inheritResistance = false;
if (typeof self._parentHost !== "undefined" && self._parentHost && self._parentHost.parasiteResistant === true) {
inheritResistance = true;
}
var parasite = new Parasite();
parasite.x = self.x;
parasite.y = self.y;
parasite.scaleX = 1;
parasite.scaleY = 1;
parasite.alpha = 1;
if (inheritResistance) {
parasite.parasiteResistant = true;
}
if (self.parent) self.parent.addChild(parasite);
if (typeof parasites !== "undefined") parasites.push(parasite);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else {
// Spawn a new Nummer at this position
var herb = new Nummer();
herb._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herb.x = self.x;
herb.y = self.y;
herb.scaleX = 1;
herb.scaleY = 1;
herb.alpha = 1;
// If this egg was laid by a parasite attached to a resistant host, always inherit resistance
if (typeof self._parentHost !== "undefined" && self._parentHost && self._parentHost.parasiteResistant === true) {
herb.parasiteResistant = true;
}
if (self.parent) self.parent.addChild(herb);
herbivores.push(herb);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
}
self.destroy();
}
});
}
};
return self;
});
// Fungi class
var Fungi = Container.expand(function () {
var self = Container.call(this);
// Track the cycle the fungi was created in
self._bornCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
// Use fungi asset
var fungiAsset = self.attachAsset('fungi', {
anchorX: 0.5,
anchorY: 0.5
});
// Fungi AI: shoot spores at nearby herbivores
self._sporeCooldown = 0;
self.update = function () {
// Fungi lifespan removed: fungi no longer die after 1 cycle
// Only shoot if cooldown is 0
if (self._sporeCooldown > 0) {
self._sporeCooldown--;
return;
}
// --- FUNGI POPULATION CONTROL: Prevent infecting with spores if 20 or more fungi ---
if (typeof fungis !== "undefined" && fungis.length >= 20) {
return;
}
// Find nearest herbivore within 400px
var minDist = 999999,
closest = null;
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten || h.spored) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist && Math.sqrt(dist) < 400) {
minDist = dist;
closest = h;
}
}
if (closest) {
// Shoot a spore at the herbivore
var spore = new FungiSpore();
spore.x = self.x;
spore.y = self.y;
spore.target = closest;
game.addChild(spore);
if (!fungiSpores) fungiSpores = [];
fungiSpores.push(spore);
self._sporeCooldown = 120 + Math.floor(Math.random() * 60); // 2-3 seconds cooldown
}
};
// Track if this fungi is currently 'blocked' from respawning after being eaten
self._blockedRespawn = false;
// Add die method so fungi disappear when eaten
self.die = function () {
if (self.eaten) return;
self.eaten = true;
// Animate shrink and fade out, then destroy
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Nummer class (formerly Herbivore)
var Nummer = Container.expand(function () {
var self = Container.call(this);
// Uncommon: 15% chance to be resistant to parasites
self.parasiteResistant = Math.random() < 0.15;
// Track if this Nummer was hit by a spore
self.spored = false;
var herbAsset = self.attachAsset('herbivore', {
anchorX: 0.5,
anchorY: 0.5
});
self.target = null;
self.speed = 1.2 + Math.random() * 1.0;
self.eaten = false;
// Track number of plants eaten for egg laying
self._plantsEaten = 0;
// --- State management for hungry/sleepy ---
self.state = "hungry"; // "hungry" or "sleepy"
self._sleepTimer = 0; // frames left in sleepy state
// --- Starvation timer: die if not eating for 2 cycles (80 seconds = 4800 frames) ---
self._starveTimer = 4800; // 80 seconds at 60fps
// --- Personality system ---
// Personalities: "normal", "curious", "grumpy"
var personalities = ["normal", "curious", "grumpy"];
// Assign a random personality at creation
var rand = Math.random();
// Make random traits more common: 30% normal, 35% curious, 35% grumpy
if (rand < 0.3) {
self.personality = "normal";
} else if (rand < 0.65) {
self.personality = "curious";
} else {
self.personality = "grumpy";
}
// For curious: timer for following pollinators
self._curiousFollowTimer = 0;
self._curiousPollinatorTarget = null;
// For grumpy: scare cooldown
self._grumpyScareCooldown = 0;
self.update = function () {
if (self.eaten) return;
// --- HERBIVORE HUNGER QUEUE LOGIC ---
// Only 2 herbivores can be in "hungry" state at a time. Others must wait in "waiting" state.
if (!self._hungerQueueState) self._hungerQueueState = "unset"; // "unset", "waiting", "hungry"
if (self.state === "hungry" && self._hungerQueueState !== "hungry") {
// Try to enter the hunger queue
if (herbivoreHungerQueue.indexOf(self) === -1) {
herbivoreHungerQueue.push(self);
}
// If not in the first 2, set to waiting
if (herbivoreHungerQueue[0] !== self && herbivoreHungerQueue[1] !== self) {
self._hungerQueueState = "waiting";
self.state = "waiting";
// Don't do hungry logic this frame
} else {
self._hungerQueueState = "hungry";
// Continue as hungry
}
} else if (self.state === "waiting" && self._hungerQueueState !== "waiting") {
// Enter waiting queue if not already
if (herbivoreHungerQueue.indexOf(self) === -1) {
herbivoreHungerQueue.push(self);
}
// If now in first 2, become hungry
if (herbivoreHungerQueue[0] === self || herbivoreHungerQueue[1] === self) {
self.state = "hungry";
self._hungerQueueState = "hungry";
} else {
self._hungerQueueState = "waiting";
// Don't do hungry logic this frame
}
} else if (self.state === "hungry" && (herbivoreHungerQueue[0] === self || herbivoreHungerQueue[1] === self)) {
self._hungerQueueState = "hungry";
} else if (self.state === "hungry" && herbivoreHungerQueue.indexOf(self) > 1) {
// If somehow in hungry state but not allowed, set to waiting
self.state = "waiting";
self._hungerQueueState = "waiting";
}
// If in waiting state, just wander
if (self.state === "waiting") {
// --- Random wandering ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
// Don't do hungry logic
return;
}
// --- HERBIVORE RANDOM SOUND LOGIC ---
// Only make sound if not eaten and not in winter (optional: you can allow in winter if desired)
if (typeof self._nextHerbSoundTimer === "undefined") {
// Set initial timer to random 6-10 seconds (360-600 frames)
self._nextHerbSoundTimer = 360 + Math.floor(Math.random() * 241);
}
if (self._nextHerbSoundTimer > 0) {
self._nextHerbSoundTimer--;
} else {
// Play the 'Herbivore' sound
var snd = LK.getSound('Herbivore');
if (snd && typeof snd.play === "function") snd.play();
// Reset timer to next random interval (6-10 seconds)
self._nextHerbSoundTimer = 360 + Math.floor(Math.random() * 241);
}
// --- WINTER BEHAVIOR: Move slower, avoid eating, and halt starvation meter ---
var isWinter = !!window.winterState;
var originalSpeed = self.speed;
if (isWinter) {
// Move slower in winter
self.speed = originalSpeed * 0.4;
} else {
self.speed = originalSpeed;
}
// Starvation logic: decrement timer if alive and hungry/wandering, but not in winter
if ((self.state === "hungry" || self.state === "wandering") && !isWinter) {
self._starveTimer--;
if (self._starveTimer <= 0 && !self.eaten) {
// Starve and die
self.die();
return;
}
}
// Handle infected state
if (self.infected) {
self.infectedTimer = (self.infectedTimer || 0) + 1;
// Future: add infected behavior here (e.g. slow, animation, etc)
}
// If infection was pending from summer, and now it's not summer, apply infection
if (self._pendingFungiInfection && !window.summerState) {
self._pendingFungiInfection = false;
self.infected = true;
self.infectedTimer = 0;
// Tint permanently purple
tween(self, {
tint: 0xbb88ff
}, {
duration: 80
});
}
// Handle sleepy state
// If just left hungry state (became wandering or sleepy), remove from hunger queue and allow next in queue to become hungry
if ((self.state === "wandering" || self.state === "sleepy") && self._hungerQueueState === "hungry") {
var idx = herbivoreHungerQueue.indexOf(self);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
}
self._hungerQueueState = "unset";
// Next in queue (if any) becomes hungry
for (var i = 0; i < herbivoreHungerQueue.length; i++) {
var h = herbivoreHungerQueue[i];
if (h && h.state === "waiting") {
h.state = "hungry";
h._hungerQueueState = "hungry";
break;
}
}
}
if (self.state === "sleepy") {
// Check for nearby carnivore and possibly wake up
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
}
// If carnivore is within 300px, 50% chance to wake up and run away
if (nearestCarn && Math.sqrt(minCarnDist) < 300) {
// Only try to wake up once per frame if still sleepy
if (!self._triedWakeThisFrame) {
self._triedWakeThisFrame = true;
if (Math.random() < 0.5) {
// Wake up and run away from carnivore
self.state = "hungry";
// Move away from carnivore immediately
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
// Move a burst away (3x speed for one frame)
self.x += dx / dist * (self.speed * 3);
self.y += dy / dist * (self.speed * 3);
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// Don't continue sleeping
return;
}
}
} else {
self._triedWakeThisFrame = false;
}
self._sleepTimer--;
if (self._sleepTimer <= 0) {
self.state = "wandering";
self._wanderAfterSleepTimer = 600; // 10 seconds at 60fps
}
// While sleepy, don't move or seek food
return;
}
// Wandering state after sleep: ignore plants, just wander for 10s, then become hungry
if (self.state === "wandering") {
if (typeof self._wanderAfterSleepTimer === "undefined") self._wanderAfterSleepTimer = 600;
self._wanderAfterSleepTimer--;
// --- Random wandering ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
if (self._wanderAfterSleepTimer <= 0) {
self.state = "hungry";
}
return;
}
// Only seek food if hungry
if (self.state === "hungry") {
var isWinter = !!window.winterState;
// --- PERSONALITY: CURIOUS ---
// If curious, sometimes follow pollinators for a few seconds
if (self.personality === "curious") {
// If not already following, 1% chance per frame to start following a pollinator
if (self._curiousFollowTimer <= 0 && Math.random() < 0.01 && pollinators.length > 0) {
// Pick a random pollinator to follow
var idx = Math.floor(Math.random() * pollinators.length);
var poll = pollinators[idx];
if (poll && !poll.eaten) {
self._curiousPollinatorTarget = poll;
self._curiousFollowTimer = 120 + Math.floor(Math.random() * 120); // 2-4 seconds
}
}
// If following a pollinator
if (self._curiousFollowTimer > 0 && self._curiousPollinatorTarget && !self._curiousPollinatorTarget.eaten) {
var dx = self._curiousPollinatorTarget.x - self.x;
var dy = self._curiousPollinatorTarget.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
self._curiousFollowTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// If timer runs out or pollinator is gone, stop following
if (self._curiousFollowTimer <= 0 || self._curiousPollinatorTarget.eaten) {
self._curiousFollowTimer = 0;
self._curiousPollinatorTarget = null;
}
// While following, do not seek plants
return;
}
// If not following, proceed as normal below
}
// --- PERSONALITY: GRUMPY ---
// If grumpy, check for carnivore within 120px and scare it away if it is targeting this herbivore
if (self.personality === "grumpy" && self._grumpyScareCooldown <= 0) {
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var dx = carn.x - self.x;
var dy = carn.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// If carnivore is close and targeting this herbivore
// Bloodthirsty carnivores are not scared of grumpy herbivores
if (dist < 120 && carn.target === self && !carn._tired && carn.personality !== "bloodthirsty") {
// Scare the carnivore away: set it to wandering, clear its target, and make it tired
carn.state = "wandering";
carn.target = null;
carn._tired = true;
carn._tiredTimer = 300 + Math.floor(Math.random() * 200); // 5-8 seconds
carn._chaseTime = 0;
// Grumpy herbivore enters a short cooldown before it can scare again
self._grumpyScareCooldown = 300; // 5 seconds
// Optional: animate the herbivore (e.g. scale up briefly)
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 80,
easing: tween.easeIn
});
}
});
break;
}
}
}
if (self._grumpyScareCooldown > 0) self._grumpyScareCooldown--;
// --- END PERSONALITY LOGIC ---
// Normal hungry logic
// In winter, do not seek or eat plants, but wander randomly
if (isWinter) {
// Wander only, do not target plants
self.target = null;
// --- Random wandering in winter ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
} else {
// Always target the nearest plant when hungry
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
self.target = closest;
}
// Nummer can walk over fungi as if it is not there: do not treat fungi as obstacles, so do not check for collision or avoid them at all in movement logic.
// Check for nearest carnivore
var nearestCarn = null;
var minCarnDist = 999999;
var tiredCarnivore = null;
var tiredCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
// Track tired carnivore chasing this herbivore
if (carn._tired && carn.target === self && cdist < tiredCarnDist) {
tiredCarnivore = carn;
tiredCarnDist = cdist;
}
}
var carnivoreTooClose = false;
var carnDist = 0;
var runAwayDX = 0,
runAwayDY = 0;
if (nearestCarn) {
carnDist = Math.sqrt((nearestCarn.x - self.x) * (nearestCarn.x - self.x) + (nearestCarn.y - self.y) * (nearestCarn.y - self.y));
if (nearestCarn._tired && nearestCarn.target === self) {
// If a tired carnivore was chasing this herbivore, teleport a tiny distance in front of it (away from carnivore)
// Only teleport once per tired state
if (!self._teleportedFromTiredCarnivore) {
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
// Only teleport if not already far away
// Teleport 80px away in the direction away from carnivore
var teleportDist = 80;
var tx = nearestCarn.x + dx / (dist || 1) * teleportDist;
var ty = nearestCarn.y + dy / (dist || 1) * teleportDist;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (tx < minX) tx = minX;
if (tx > maxX) tx = maxX;
if (ty < minY) ty = minY;
if (ty > maxY) ty = maxY;
self.x = tx;
self.y = ty;
}
self._teleportedFromTiredCarnivore = true;
// After teleport, set a run away timer so it keeps running for a while
self._runAwayFromTiredCarnivoreTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
// Pick a run direction directly away from carnivore
if (dist > 0) {
self._runAwayAngle = Math.atan2(dy, dx);
} else {
self._runAwayAngle = Math.random() * Math.PI * 2;
}
}
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
} else if (carnDist < 300) {
// If carnivore is within 300px, run away
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
// Reset teleport flag if not being chased by tired carnivore
self._teleportedFromTiredCarnivore = false;
}
}
// --- New: If a tired carnivore was chasing this herbivore, charge away until far enough ---
// (Removed barrier logic: herbivores can always run away from tired carnivores)
if (carnivoreTooClose && carnDist > 1) {
// If running from a tired carnivore and we have a run timer, keep running in the set direction
if (typeof self._runAwayFromTiredCarnivoreTimer !== "undefined" && self._runAwayFromTiredCarnivoreTimer > 0) {
self.x += Math.cos(self._runAwayAngle) * self.speed * 2.2;
self.y += Math.sin(self._runAwayAngle) * self.speed * 2.2;
self._runAwayFromTiredCarnivoreTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// When timer runs out, clear teleport/run state so it can look for food again
if (self._runAwayFromTiredCarnivoreTimer <= 0) {
self._runAwayFromTiredCarnivoreTimer = 0;
self._teleportedFromTiredCarnivore = false;
}
// While running, do not seek food
return;
} else {
// Run away from carnivore (normal, not tired chase)
self.x += runAwayDX / carnDist * (self.speed * 1.5);
self.y += runAwayDY / carnDist * (self.speed * 1.5);
// Reset teleport/run state if not running from tired carnivore
self._teleportedFromTiredCarnivore = false;
self._runAwayFromTiredCarnivoreTimer = 0;
}
} else {
// --- Random wandering when not chasing plant ---
if ((!self.target || self.target.eaten) && (!self._wanderTimer || self._wanderTimer <= 0)) {
// Pick a new random direction every 60-120 frames
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (!self.target || self.target.eaten) {
// Wander in the chosen direction
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
}
// Move toward target plant
if (self.target && !self.target.eaten) {
var isWinter = !!window.winterState;
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat plant if close enough, but not in winter
if (!isWinter && dist < 60 && !self.target.eaten) {
var eatenPlant = self.target;
// Play chomp sound when a herbivore eats a plant
var chompSnd = LK.getSound('Chomp');
if (chompSnd && typeof chompSnd.play === "function") chompSnd.play();
self.target.die();
self.target = null;
// Animate Nummer "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
// After eating, enter sleepy state for 10 seconds (600 frames) with a chance to skip sleep
if (Math.random() < 0.7) {
// 70% chance to sleep, 30% chance to skip
self.state = "sleepy";
self._sleepTimer = 600;
} else {
// Skip sleep, go straight to wandering
self.state = "wandering";
self._wanderAfterSleepTimer = 600; // 10 seconds wandering
}
// Reset starvation timer after eating only if plant is not brown
if (!(eatenPlant && eatenPlant._isBrown)) {
self._starveTimer = 4800; // 80 seconds at 60fps
}
// Increment plants eaten counter
self._plantsEaten = (self._plantsEaten || 0) + 1;
// Lay an egg if 4 plants have been eaten, then reset counter
// --- HERBIVORE POPULATION CONTROL: Only lay egg if < 6 herbivores ---
if (self._plantsEaten >= 4) {
if (typeof herbivores !== "undefined" && herbivores.length < 6) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
self._plantsEaten = 0;
}
}
}
}
}
};
// Called when eaten by carnivore
self.die = function (_onFinish) {
if (self.eaten) return;
self.eaten = true;
// Remove from hunger queue if present
var idx = herbivoreHungerQueue.indexOf(self);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
}
// If infected, spawn a fungi at this position
// --- FUNGI POPULATION CONTROL: Prevent dropping fungi at death if 20 or more fungi ---
if (self.infected && (typeof fungis === "undefined" || fungis.length < 20)) {
var newFungi = new Fungi();
// If fungi is spawned by Nummer death, set its _bornCycle to the next cycle so it lives a full cycle
if (typeof cycleCount !== "undefined") {
newFungi._bornCycle = cycleCount + 1;
}
newFungi.x = self.x;
newFungi.y = self.y;
newFungi.scaleX = 1;
newFungi.scaleY = 1;
newFungi.alpha = 1;
if (self.parent) self.parent.addChild(newFungi);
if (typeof fungis !== "undefined") fungis.push(newFungi);
}
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
if (_onFinish) _onFinish();
}
});
};
// Called when hit by a spore
self.sporeHit = function () {
if (self.spored) return; // Only allow once
self.spored = true;
// If it's summer, delay infection until next winter or spring
if (window.summerState) {
// Mark as pending infection, but do not infect yet
self._pendingFungiInfection = true;
// Optionally, you could tint a different color to show pending state, or do nothing
return;
}
// Enter infected state immediately if not summer
self.infected = true;
self.infectedTimer = 0;
// Tint permanently purple
tween(self, {
tint: 0xbb88ff
}, {
duration: 80
});
};
return self;
});
// Parasite class (seeks and attaches to herbivores or carnivores)
var Parasite = Container.expand(function () {
var self = Container.call(this);
var parasiteAsset = self.attachAsset('Parasite', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
// State: "seek", "attached"
self.state = "seek";
self.target = null;
self.attachedTo = null;
self.offsetX = 0;
self.offsetY = 0;
self.speed = 1.5 + Math.random() * 0.7;
// Track last intersecting for attach logic
self._lastIntersecting = false;
self.update = function () {
// If attached, follow the host
if (self.state === "attached" && self.attachedTo) {
// AUTUMN: Detach immediately in autumn
if (window.autumnState) {
// Detach from host
if (self.attachedTo._parasiteAttached === self) {
self.attachedTo._parasiteAttached = null;
}
self.state = "seek";
self.attachedTo = null;
self.target = null;
self._wanderTimer = 0;
// Allow laying an egg only once per parasite in autumn
if (!self._laidAutumnEgg) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
self._laidAutumnEgg = true;
}
return;
}
// If host is eaten, parasite dies with host
if (self.attachedTo.eaten) {
// Animate parasite dying and remove from array
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
// Remove from parasites array if present
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(self);
if (idx !== -1) parasites.splice(idx, 1);
}
}
});
return;
}
// If host is a herbivore and is infected by shrooms, let go immediately
if (typeof self.attachedTo.spored !== "undefined" && self.attachedTo.spored === true) {
// Detach from host
if (self.attachedTo._parasiteAttached === self) {
self.attachedTo._parasiteAttached = null;
}
self.state = "seek";
self.attachedTo = null;
self.target = null;
// Wander in a new random direction
self._wanderTimer = 0;
return;
}
// Stay at the same offset from host
self.x = self.attachedTo.x + self.offsetX;
self.y = self.attachedTo.y + self.offsetY;
// --- Parasite lays an egg every cycle if attached to a host, but only if < 4 parasites ---
if (typeof self._lastCycle === "undefined") self._lastCycle = -1;
if (typeof cycleCount !== "undefined" && self._lastCycle !== cycleCount) {
self._lastCycle = cycleCount;
if (typeof parasites !== "undefined" && parasites.length < 4) {
// Lay a parasite egg at current position
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
// Pass parent host resistance to egg for inheritance
if (self.attachedTo && self.attachedTo.parasiteResistant === true) {
egg._parentHost = self.attachedTo;
}
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
}
return;
}
// If seeking, look for nearest herbivore or carnivore
if (self.state === "seek") {
// In autumn, cannot infect, so just wander
if (window.autumnState) {
// Wander randomly if no target
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersecting = false;
return;
}
// If no target or target is eaten or already has a parasite attached (not self), find new target
if (!self.target || self.target.eaten || self.target._parasiteAttached && self.target._parasiteAttached !== self) {
var minDist = 999999,
closest = null;
// Search herbivores first
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Only attach if not already parasitized
if (h._parasiteAttached) continue;
// Avoid targeting spore-infected herbivores
if (h.spored === true) continue;
// Avoid parasite-resistant herbivores
if (h.parasiteResistant === true) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
// If no herbivore, search carnivores
if (!closest) {
for (var i = 0; i < carnivores.length; i++) {
var c = carnivores[i];
if (c.eaten) continue;
if (c._parasiteAttached) continue;
// Only infect weak carnivores (personality === "weak")
if (c.personality !== "weak") continue;
// Avoid parasite-resistant carnivores
if (c.parasiteResistant === true) continue;
var dx = c.x - self.x,
dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = c;
}
}
}
self.target = closest;
}
// If we have a target, move toward it
if (self.target && !self.target.eaten && !self.target._parasiteAttached) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Attach if close enough and not already attached
var isIntersecting = self.intersects(self.target);
if (!self._lastIntersecting && isIntersecting && !self.target._parasiteAttached) {
// Attach to host
self.state = "attached";
self.attachedTo = self.target;
// Store offset so parasite stays visually attached
self.offsetX = self.x - self.attachedTo.x;
self.offsetY = self.y - self.attachedTo.y;
// Mark host as parasitized
self.attachedTo._parasiteAttached = self;
}
self._lastIntersecting = isIntersecting;
} else {
// Wander randomly if no target
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersecting = false;
}
}
};
return self;
});
// Plant class
var Plant = Container.expand(function () {
var self = Container.call(this);
var plantAsset = self.attachAsset('plant', {
anchorX: 0.5,
anchorY: 0.5
});
self.plantAsset = plantAsset; // Expose for tinting
self._canDie = true; // Track if plant can die
// Plants are static, but we can animate them when eaten
self.eaten = false;
// Track the cycle the plant was created in
self._bornCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
self.die = function (_onFinish2) {
if (self.eaten) return;
if (self._canDie === false) return; // Prevent dying if not allowed
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
if (_onFinish2) _onFinish2();
}
});
};
// Plants die after 1 cycle
self.update = function () {
if (self.eaten) return;
// --- Summer: chance to turn brown and become unpollinatable ---
if (self._invulnerable) {
// While invulnerable, never turn brown or die
self._isBrown = false;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0xffffff;
}
} else {
if ((window.summerState || window.springState) && !self._isBrown && !self.eaten) {
// Only roll once per plant per summer or spring
if (typeof self._brownChecked === "undefined" || self._brownChecked !== cycleCount) {
self._brownChecked = cycleCount;
if (window.summerState && Math.random() < 0.12) {
// ~12% chance per cycle in summer
self._isBrown = true;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0x996633; // brown
}
} else if (window.springState && Math.random() < 0.002) {
// ~0.2% chance per cycle in spring (VERY low)
self._isBrown = true;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0x996633; // brown
}
}
}
}
// If not summer, reset brown state
if (!window.summerState && self._isBrown) {
self._isBrown = false;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0xffffff;
}
}
// Only die if at least one full cycle has passed since birth
if (typeof cycleCount !== "undefined" && typeof self._bornCycle !== "undefined") {
// Brown plants in summer: die after 1/2 cycle (shorter lifespan)
if (self._isBrown && window.summerState && self._canDie !== false) {
if (cycleCount > self._bornCycle + 0.5) {
self.die();
}
} else if (cycleCount > self._bornCycle + 1 && self._canDie !== false) {
self.die();
}
}
}
};
return self;
});
// Pollinator class
var Pollinator = Container.expand(function () {
var self = Container.call(this);
var pollAsset = self.attachAsset('pollinator', {
anchorX: 0.5,
anchorY: 0.5
});
// --- Pollinator Starvation Logic ---
self.state = "seek"; // "seek", "scatter", "planting", "hungry"
self.target = null;
self.speed = 2 + Math.random();
self._scatterTimer = 0;
self._scatterAngle = 0;
self._plantSeedTimer = 0;
self._hasSeed = true;
self.eaten = false;
self.lastWasTouchingPlant = false;
// --- Pollinator Starve Timer ---
self._starveTimer = 3000 + Math.floor(Math.random() * 1200); // 1 cycle = 50s = 3000 frames, randomize a bit
self._lastCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
// --- Pollination counter for egg laying ---
self._pollinations = 0;
self._layingEgg = false;
self._eggObj = null;
// --- After eating shroom, next pollination is a shroom ---
self._nextPollinationIsShroom = false;
self._didShroomPollination = false;
self.update = function () {
if (self.eaten) return;
// --- POLLINATOR STARVATION LOGIC (handled in constructor patch above) ---
// --- POLLINATOR HUNGRY STATE: Seek fungi and eat if hungry ---
if (self.state === "hungry") {
// Prevent pollinators from eating fungi during autumn, winter, and summer
var isAutumn = !!window.autumnState;
var isWinter = !!window.winterState;
var isSummer = !!window.summerState;
if (isAutumn || isWinter || isSummer) {
// Just wander randomly, do not seek or eat fungi, and do not decrement starve timer
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// Randomly (1 in 8 chance per frame) pick a new fungi to seek, or keep current
if (!self.target || self.target.eaten || Math.random() < 0.125) {
// Find nearest fungi
var minDist = 999999,
closest = null;
for (var i = 0; i < fungis.length; i++) {
var f = fungis[i];
if (f.eaten) continue;
var dx = f.x - self.x,
dy = f.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = f;
}
}
self.target = closest;
}
// Move toward fungi if any
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x,
dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat fungi if close enough
if (dist < 60 && !self.target.eaten) {
// Play chomp sound when pollinator eats fungi
var chompSnd = LK.getSound('Chomp');
if (chompSnd && typeof chompSnd.play === "function") chompSnd.play();
// Track how many times this fungi has been eaten by pollinators
if (typeof self.target._pollinatorEatCount === "undefined") {
self.target._pollinatorEatCount = 0;
}
self.target._pollinatorEatCount++;
// If eaten 3 times, do nothing (fungi does not disappear anymore)
if (self.target._pollinatorEatCount >= 3) {
// Fungi no longer disappears after being eaten 3 times
// Optionally, you could add a visual effect or reset the counter, but do nothing here
}
// Reset starve timer
self._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// After eating, return to normal pollinator state
self.state = "seek";
self.target = null;
// Set flag so next pollination is a shroom
self._nextPollinationIsShroom = true;
self._didShroomPollination = false;
// Animate pollinator "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
}
} else {
// If no fungi, wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// --- POLLINATOR STARVATION: decrement timer unless hibernating or in autumn, winter, or summer ---
var isAutumn = !!window.autumnState;
var isWinter = !!window.winterState;
var isSummer = !!window.summerState;
if (!(typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating) && !isAutumn && !isWinter && !isSummer) {
// Only decrement if not hibernating and not in autumn, winter, or summer
self._starveTimer--;
if (self._starveTimer <= 0 && !self.eaten) {
// Die of starvation!
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
}
// --- POLLINATOR HUNGRY STATE: If timer is low, become hungry and seek fungi ---
if (self.state !== "hungry" && self._starveTimer < 900) {
self.state = "hungry";
self.target = null;
}
// --- If pollinator eats fungi, reset timer ---
self._tryEatFungi = function () {
// Find nearest fungi
var minDist = 999999,
closest = null;
for (var i = 0; i < fungis.length; i++) {
var f = fungis[i];
if (f.eaten) continue;
var dx = f.x - self.x,
dy = f.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = f;
}
}
if (closest) {
self.target = closest;
var dx = closest.x - self.x,
dy = closest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat fungi if close enough
if (dist < 60 && !closest.eaten) {
if (typeof closest.die === "function") closest.die();
// Reset starve timer
self._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// After eating, return to normal pollinator state
self.state = "seek";
self.target = null;
// Animate pollinator "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return true;
}
}
return false;
};
// --- POLLINATOR ACTIVE/NONACTIVE BEHAVIOR ---
// If more than 5 pollinators, only 3 are 'active' and make sound, others are 'nonactive'
if (typeof pollinators !== "undefined" && pollinators.length > 5) {
// Assign active/nonactive roles once per frame for all pollinators
if (typeof window._activePollinatorIds === "undefined") window._activePollinatorIds = [];
// Only recalculate once per frame
if (typeof window._activePollinatorFrame !== "number" || window._activePollinatorFrame !== LK.ticks) {
// Pick 3 unique random pollinators to be active
var ids = [];
var pool = [];
for (var i = 0; i < pollinators.length; i++) {
if (!pollinators[i].eaten) pool.push(i);
}
// Shuffle pool
for (var i = pool.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = pool[i];
pool[i] = pool[j];
pool[j] = tmp;
}
ids = pool.slice(0, 3);
window._activePollinatorIds = ids;
window._activePollinatorFrame = LK.ticks;
}
// Set self.activeBehavior based on selection
var myIdx = -1;
for (var i = 0; i < pollinators.length; i++) {
if (pollinators[i] === self) {
myIdx = i;
break;
}
}
self.behavior = window._activePollinatorIds.indexOf(myIdx) !== -1 ? "active" : "nonactive";
} else {
// If 5 or fewer, all are active
self.behavior = "active";
}
// --- POLLINATOR NOISE: Play sound at random intervals, only if active ---
// Nonactive pollinators are incapable of making sound unless population is reduced to 3 or fewer
if (typeof self._nextNoiseTimer === "undefined") {
// Set initial timer to random 1-4 seconds (60-240 frames)
self._nextNoiseTimer = 60 + Math.floor(Math.random() * 180);
}
if (self._nextNoiseTimer > 0) {
self._nextNoiseTimer--;
} else {
var canMakeSound = false;
if (typeof pollinators !== "undefined" && pollinators.length <= 3) {
// All pollinators can make sound if population is 3 or fewer
canMakeSound = true;
} else if (self.behavior === "active") {
// Only active pollinators can make sound if more than 3
canMakeSound = true;
}
if (canMakeSound && !(typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating)) {
// Play the 'Bug' sound
var snd = LK.getSound('Bug');
if (snd && typeof snd.play === "function") snd.play();
}
// Reset timer to next random interval (1-4 seconds)
self._nextNoiseTimer = 60 + Math.floor(Math.random() * 180);
}
// --- HIBERNATION: If background is white, pollinators do nothing, don't move, don't die, don't act ---
if (typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating) {
// Do not decrement lifetime, do not move, do not die, do not lay eggs, do not plant seeds
return;
}
// If currently laying an egg, wait for egg to hatch before continuing
if (self._layingEgg && self._eggObj) {
// If it's summer or winter, eggs will not hatch, so pollinator should move away and continue pollinating
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
if (isSummer || isWinter) {
// Move away from the egg and resume pollinating
if (self._eggObj) {
// Move pollinator a bit away from the egg (random direction)
var angle = Math.random() * Math.PI * 2;
self.x += Math.cos(angle) * 80;
self.y += Math.sin(angle) * 80;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._eggObj = null;
self._layingEgg = false;
self._pollinations = 0; // Reset counter
// Resume pollinating
self.state = "seek";
self.target = null;
// Continue with rest of update
} else {
// Wait for egg to hatch, then continue pollinating
if (self._eggObj.hatched) {
// Spawn a new pollinator at egg position
var newPoll = new Pollinator();
newPoll.x = self._eggObj.x;
newPoll.y = self._eggObj.y;
newPoll.scaleX = 1;
newPoll.scaleY = 1;
newPoll.alpha = 1;
if (self.parent) self.parent.addChild(newPoll);
if (typeof pollinators !== "undefined") pollinators.push(newPoll);
// Remove egg from global eggs array
var idx = eggs.indexOf(self._eggObj);
if (idx !== -1) eggs.splice(idx, 1);
self._eggObj.destroy();
self._eggObj = null;
self._layingEgg = false;
self._pollinations = 0; // Reset counter
} else {
// Wait for egg to hatch, do nothing else
return;
}
}
}
// SEEK: Find nearest plant and move to it, but run from carnivores if close
if (self.state === "seek") {
// Check for nearest carnivore
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
}
var carnivoreTooClose = false;
var carnDist = 0;
var runAwayDX = 0,
runAwayDY = 0;
if (nearestCarn) {
carnDist = Math.sqrt((nearestCarn.x - self.x) * (nearestCarn.x - self.x) + (nearestCarn.y - self.y) * (nearestCarn.y - self.y));
if (carnDist < 220) {
// Run if carnivore is within 220px
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
}
}
if (carnivoreTooClose && carnDist > 1) {
// Run away from carnivore (burst speed)
self.x += runAwayDX / carnDist * (self.speed * 2.2);
self.y += runAwayDY / carnDist * (self.speed * 2.2);
} else {
// Find nearest plant
if (!self.target || self.target.eaten) {
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
// Ignore brown plants in summer
if (window.summerState && p._isBrown) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
self.target = closest;
}
// Move toward target plant
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Check for touching plant (within 60px)
var isTouching = dist < 60;
if (!self.lastWasTouchingPlant && isTouching) {
// If plant is brown, skip and immediately seek a new healthy plant
if (self.target && self.target._isBrown) {
// Find next healthy plant (not brown, not eaten)
var minDist2 = 999999,
closest2 = null;
for (var j = 0; j < plants.length; j++) {
var p2 = plants[j];
if (p2.eaten) continue;
if (window.summerState && p2._isBrown) continue;
var dx2 = p2.x - self.x,
dy2 = p2.y - self.y;
var dist2 = dx2 * dx2 + dy2 * dy2;
if (dist2 < minDist2) {
minDist2 = dist2;
closest2 = p2;
}
}
self.target = closest2;
// Do not pollinate, just move on
self.lastWasTouchingPlant = isTouching;
return;
}
// Just touched plant
// --- Pollinate a shroom instead of a plant if flag is set ---
// (Removed: pollinators no longer spawn fungi or delete shrooms)
if (self._nextPollinationIsShroom && !self._didShroomPollination) {
self._didShroomPollination = true;
self._nextPollinationIsShroom = false;
// After pollinating a shroom, next pollination is normal
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
self._pollinations = (self._pollinations || 0) + 1;
// If pollinated 3 times, lay an egg and continue pollinating (do not wait for egg to hatch)
if (self._pollinations >= 3 && !self._layingEgg) {
// In autumn, pollinators do NOT lay eggs, just pollinate and continue
if (window.autumnState) {
self._pollinations = 0; // Reset pollination counter in autumn
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
// --- POLLINATOR POPULATION CONTROL: Only lay egg if < 10 pollinators ---
// --- FERN POPULATION CONTROL: Only lay egg if < 20 ferns (plants) ---
if (typeof pollinators !== "undefined" && pollinators.length >= 10 || typeof plants !== "undefined" && plants.length >= 20) {
// Do not lay egg, just reset pollination counter and scatter
self._pollinations = 0;
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60);
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
// Lay a pollinator egg at current position
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isPollinatorEgg = true;
egg._pollinatorHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
// Reset pollination counter and continue pollinating
self._pollinations = 0;
// Set pollinator to hungry state after laying an egg
self.state = "hungry";
self.target = null;
self.lastWasTouchingPlant = isTouching;
return;
}
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
}
self.lastWasTouchingPlant = isTouching;
} else {
// No plant to go to, wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
}
}
}
// SCATTER: Move in a random direction for a short time
else if (self.state === "scatter") {
self.x += Math.cos(self._scatterAngle) * self.speed * 2;
self.y += Math.sin(self._scatterAngle) * self.speed * 2;
self._scatterTimer--;
if (self._scatterTimer <= 0) {
self.state = "planting";
self._plantSeedTimer = 30 + Math.floor(Math.random() * 30); // short pause before planting
}
}
// PLANTING: Pause, then plant a seed
else if (self.state === "planting") {
self._plantSeedTimer--;
if (self._plantSeedTimer <= 0 && self._hasSeed) {
// Place a seed at current position
var seed = new Seed();
seed.x = self.x;
seed.y = self.y;
seed.scaleX = 0.4;
seed.scaleY = 0.4;
seed.alpha = 1;
seeds.push(seed);
if (self.parent) self.parent.addChild(seed);
// Play plop sound when pollinator places a seed
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
self._hasSeed = false;
// After planting, look for another plant
self.state = "seek";
self.target = null;
self._hasSeed = true;
}
}
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
};
self.die = function () {
if (self.eaten) return;
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Seed class
var Seed = Container.expand(function () {
var self = Container.call(this);
// Use dedicated brown seed asset
var seedAsset = self.attachAsset('seed', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
self.growing = false;
self.eaten = false;
self._growTimer = 600; // 10 seconds at 60fps
self.update = function () {
if (self.eaten) return;
if (!self.growing) {
self._growTimer--;
// --- FERN POPULATION CONTROL: If 20 or more ferns, seed always fails to germinate ---
if (typeof plants !== "undefined" && plants.length >= 20) {
// 100% fail to germinate
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
if (self._growTimer <= 0) {
// --- 30% chance for seed to not germinate and disappear ---
if (Math.random() < 0.3) {
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
// --- Summer or Autumn state: chance for seed to fail germination and disappear ---
if (window.summerState && Math.random() < 0.4) {
// 40% chance to fail in summer, just disappear
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
} else if (window.autumnState && Math.random() < 0.55) {
// 55% chance to fail in autumn, just disappear
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
self.growing = true;
// Animate seed growing into plant
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Play grow sound when seed germinates
var growSnd = LK.getSound('Grow');
if (growSnd && typeof growSnd.play === "function") growSnd.play();
// Replace seed with a new plant at same position
var newPlant = new Plant();
newPlant.x = self.x;
newPlant.y = self.y;
newPlant.scaleX = 1;
newPlant.scaleY = 1;
newPlant.alpha = 1;
// Set the plant's _bornCycle to the current cycleCount so its cycle restarts
if (typeof cycleCount !== "undefined") {
newPlant._bornCycle = cycleCount;
}
plants.push(newPlant);
if (self.parent) self.parent.addChild(newPlant);
self.eaten = true;
self.destroy();
}
});
}
}
};
// If something eats the seed (future-proof)
self.die = function () {
if (self.eaten) return;
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Spore class (projectile shot by fungi)
var Spore = Container.expand(function () {
var self = Container.call(this);
// Use a simple green circle for the spore
var sporeAsset = self.attachAsset('plant', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
self.target = null;
self.speed = 2.5;
self._alive = true;
self.update = function () {
if (!self.target || self.target.eaten || !self._alive) {
self.destroy();
self._alive = false;
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Check collision with target herbivore
if (!self._lastIntersecting && self.intersects(self.target)) {
// "Hit" the herbivore: destroy spore, animate herbivore, but do not kill
if (self.target && typeof self.target.sporeHit === "function") {
self.target.sporeHit();
}
self.destroy();
self._alive = false;
}
self._lastIntersecting = self.intersects(self.target);
};
return self;
});
// FungiSpore class for future extensibility (could add poison, etc)
// Modified: FungiSpore now takes a straight path (direction set at spawn)
var FungiSpore = Spore.expand(function () {
var self = Spore.call(this);
// Store initial direction at spawn
self._vx = 0;
self._vy = 0;
// Set direction only once at spawn, toward target
self.setDirection = function () {
if (self.target) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self._vx = dx / dist * self.speed;
self._vy = dy / dist * self.speed;
} else {
self._vx = 0;
self._vy = 0;
}
} else {
self._vx = 0;
self._vy = 0;
}
};
// Call setDirection at spawn
self.setDirection();
// Override update to move in a straight line
self.update = function () {
if (!self.target || self.target.eaten || !self._alive) {
self.destroy();
self._alive = false;
return;
}
self.x += self._vx;
self.y += self._vy;
// Check collision with target herbivore
if (!self._lastIntersecting && self.intersects(self.target)) {
if (self.target && typeof self.target.sporeHit === "function") {
self.target.sporeHit();
}
self.destroy();
self._alive = false;
}
self._lastIntersecting = self.intersects(self.target);
};
return self;
});
// Worm class (moves only in summer and winter, buried in autumn and spring)
var Worm = Container.expand(function () {
var self = Container.call(this);
// Determine if worm is buried at spawn
self.buried = false;
var isAutumn = !!window.autumnState;
var isSpring = !!window.springState;
if (isAutumn || isSpring) {
self.buried = true;
}
// Use the worm image asset for the worm, or buried asset if buried
self._wormAsset = null;
if (self.buried) {
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self._wormAsset = self.attachAsset('Worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Movement speed for worms
self.speed = 1 + Math.random() * 0.5;
// Track wander direction and timer
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
// Track brown plants eaten in summer
self._brownPlantsEaten = 0;
// Track last brown plant intersected (for collision detection)
self._lastIntersectingBrownPlant = null;
self.update = function () {
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
var isAutumn = !!window.autumnState;
var isSpring = !!window.springState;
// If currently buried but season changed to summer/winter, unbury and swap asset
if (self.buried && (isSummer || isWinter)) {
self.buried = false;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// If currently not buried but season changed to autumn/spring, bury and swap asset
if (!self.buried && (isAutumn || isSpring)) {
self.buried = true;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// --- Worm AI: In summer, seek and eat brown plants, then bury after 3 eaten ---
if (isSummer && !self.buried) {
// Find nearest brown plant
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
if (!p._isBrown) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
if (closest) {
// Move toward brown plant
var dx = closest.x - self.x;
var dy = closest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat brown plant if close enough and not already eaten
var isIntersecting = self.intersects(closest);
if ((!self._lastIntersectingBrownPlant || self._lastIntersectingBrownPlant !== closest) && isIntersecting && !closest.eaten) {
if (typeof closest.die === "function") {
closest.die();
}
self._brownPlantsEaten = (self._brownPlantsEaten || 0) + 1;
// After eating 3 brown plants, bury and swap asset
if (self._brownPlantsEaten >= 3) {
self.buried = true;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
self._brownPlantsEaten = 0;
// Immediately stop moving after burying
self._wanderTimer = 0;
self._wanderAngle = 0;
}
}
self._lastIntersectingBrownPlant = isIntersecting ? closest : null;
} else {
// No brown plant: wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersectingBrownPlant = null;
}
} else if (isWinter && !self.buried) {
// Wander randomly in winter
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
// Reset brown plant eating state in winter
self._brownPlantsEaten = 0;
self._lastIntersectingBrownPlant = null;
}
// In autumn and spring, do nothing (no movement, buried)
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2ecc40 //{7C} // Set to a green background
});
/****
* Game Code
****/
// Pollinator sound effects (buzz, chirp, etc)
// --- Pollinator Hibernation State ---
var pollinatorsHibernating = false;
// --- Year Counter ---
// Add a year counter above the cycle counter that increments after winter ends
var yearCount = 1;
var yearTxt = new Text2("Year: 1", {
size: 80,
fill: "#000"
});
yearTxt.anchor.set(0.5, 0);
// Place above the cycle counter
LK.gui.top.addChild(yearTxt);
yearTxt.x = 500;
yearTxt.y = 40;
// --- Cycle Counter ---
// Add a cycle counter at the bottom of the screen that increments every 50 seconds (3000 frames at 60fps)
var cycleCount = 0;
var cycleTxt = new Text2("Cycle: 0", {
size: 80,
fill: "#000"
});
cycleTxt.anchor.set(0.5, 0);
// Place at top left, but not in the top 100px reserved area
LK.gui.top.addChild(cycleTxt);
cycleTxt.x = 500;
cycleTxt.y = 120;
// --- Season Text at Bottom ---
// Helper to get current season as string
function getCurrentSeason() {
if (window.summerState) return "Summer";
if (window.winterState) return "Winter";
if (window.springState) return "Spring";
if (window.autumnState) return "Autumn";
// Fallback: guess from background color
if (game.backgroundColor === 0xffff00) return "Summer";
if (game.backgroundColor === 0xffffff) return "Winter";
if (game.backgroundColor === 0x8B5A2B) return "Autumn";
return "Spring";
}
var seasonTxt = new Text2("Season: " + getCurrentSeason(), {
size: 90,
fill: "#000"
});
seasonTxt.anchor.set(0, 1);
LK.gui.bottom.addChild(seasonTxt);
seasonTxt.x = 40;
seasonTxt.y = 0;
// Helper to update season text
function updateSeasonText() {
if (seasonTxt && typeof seasonTxt.setText === "function") {
seasonTxt.setText("Season: " + getCurrentSeason());
if (typeof seasonTxt.setStyle === "function") seasonTxt.setStyle({
fill: "#000"
});
}
}
// --- Cycle Countdown Timer Text ---
var cycleDurationMs = 50000; // 50,000 ms = 50 seconds
var cycleTimeLeftMs = cycleDurationMs;
var cycleTimerTxt = new Text2("Next cycle: 50s", {
size: 60,
fill: "#000"
});
cycleTimerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(cycleTimerTxt);
cycleTimerTxt.x = 500;
cycleTimerTxt.y = cycleTxt.y + 80; // Place under the cycle counter
// Timer to increment cycle every 50 seconds
var cycleInterval = null; // Will be started after first organism is placed
var cycleCountdownInterval = null; // For updating the countdown text
// Helper to set all on-screen text color to black
function updateTextColors(bgColor) {
var textColor = "#000";
if (cycleTxt && typeof cycleTxt.setStyle === "function") cycleTxt.setStyle({
fill: textColor
});
if (cycleTimerTxt && typeof cycleTimerTxt.setStyle === "function") cycleTimerTxt.setStyle({
fill: textColor
});
if (deleteBtnTxt && typeof deleteBtnTxt.setStyle === "function") deleteBtnTxt.setStyle({
fill: textColor
});
if (skipCycleBtnTxt && typeof skipCycleBtnTxt.setStyle === "function") skipCycleBtnTxt.setStyle({
fill: textColor
});
// Palette item labels
for (var i = 0; i < paletteItems.length; i++) {
for (var j = 0; j < paletteItems[i].children.length; j++) {
var child = paletteItems[i].children[j];
if (child instanceof Text2 && typeof child.setStyle === "function") {
child.setStyle({
fill: textColor
});
}
}
}
// Palette scroll arrows
if (window.paletteLeftBtn) {
for (var i = 0; i < window.paletteLeftBtn.children.length; i++) {
var child = window.paletteLeftBtn.children[i];
if (child instanceof Text2 && typeof child.setStyle === "function") child.setStyle({
fill: textColor
});
}
}
if (window.paletteRightBtn) {
for (var i = 0; i < window.paletteRightBtn.children.length; i++) {
var child = window.paletteRightBtn.children[i];
if (child instanceof Text2 && typeof child.setStyle === "function") child.setStyle({
fill: textColor
});
}
}
}
function startCycleInterval() {
if (cycleInterval !== null) return;
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
cycleInterval = LK.setInterval(function () {
cycleCount++;
cycleTxt.setText("Cycle: " + cycleCount);
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
// --- Streak Counter Logic ---
// If skip was used this cycle, reset streak and clear flag
if (streakSkipThisCycle) {
resetStreak();
streakSkipThisCycle = false;
} else {
// If at least one organism is alive, increment streak, else reset
var anyAlive = plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0;
if (anyAlive) {
incrementStreak();
} else {
resetStreak();
}
}
// Reset tired attempts for all carnivores at the start of each cycle
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i] && carnivores[i]._tiredAttempts) {
carnivores[i]._tiredAttempts = {};
}
}
// --- Extinction: If no plants, kill all herbivores, pollinators, and carnivores ---
if (plants.length === 0) {
// Herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (typeof h.die === "function") h.die();else if (typeof h.destroy === "function") h.destroy();
herbivores.splice(i, 1);
}
// Pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (typeof p.die === "function") p.die();else if (typeof p.destroy === "function") p.destroy();
pollinators.splice(i, 1);
}
// Carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (typeof c.die === "function") c.die();else if (typeof c.destroy === "function") c.destroy();
carnivores.splice(i, 1);
}
// --- Make randomize button available on total extinction ---
if (typeof randomizeBtn !== "undefined") {
// Check if all organisms are dead (plants, herbivores, carnivores, pollinators, fungis, parasites, worms)
var allGone = plants.length === 0 && herbivores.length === 0 && carnivores.length === 0 && pollinators.length === 0 && fungis.length === 0 && parasites.length === 0 && worms.length === 0;
if (allGone) {
randomizeBtn._disabled = false;
randomizeBtnBox.color = 0x228B22;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomize");
} else {
// If any organisms are present, keep randomize button disabled
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
}
}
}
// Set background color every 3 cycles: green, yellow, brown, white, repeat (spring, summer, autumn, winter)
var mod = cycleCount % 12;
if (mod === 0) {
// SPRING
game.setBackgroundColor(0x2ecc40); // green
updateTextColors(0x2ecc40);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = true;
window.winterState = false;
window.autumnState = false;
// Play mellow music
LK.playMusic('mellow');
// --- YEAR COUNTER: Increment year after winter ends (when entering spring) ---
if (cycleCount > 0) {
yearCount++;
if (yearTxt && typeof yearTxt.setText === "function") {
yearTxt.setText("Year: " + yearCount);
}
}
// Restore plant color to normal (green) and allow dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
// --- SPRING: Hatch all parasite eggs (eggs with _isParasiteEgg and _parasiteHatch) ---
for (var i = eggs.length - 1; i >= 0; i--) {
var e = eggs[i];
if (e && e._isParasiteEgg && e._parasiteHatch && !e.hatched) {
// Instantly hatch the egg into a parasite
e.hatched = true;
// Animate egg hatching
tween(e, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function (egg) {
return function () {
var parasite = new Parasite();
parasite.x = egg.x;
parasite.y = egg.y;
parasite.scaleX = 1;
parasite.scaleY = 1;
parasite.alpha = 1;
if (egg.parent) egg.parent.addChild(parasite);
if (typeof parasites !== "undefined") parasites.push(parasite);
egg.destroy();
};
}(e)
});
eggs.splice(i, 1);
}
}
// --- Resume herbivore movement after winter ends ---
for (var i = 0; i < herbivores.length; i++) {
if (herbivores[i] && typeof herbivores[i].speed === "number") {
herbivores[i].speed = 1.2 + Math.random() * 1.0;
}
}
// --- Resume carnivore hunting after winter ends --- (Ava)
// Reset carnivore state to "hungry" so they resume hunting in spring
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i]) {
carnivores[i].state = "hungry";
carnivores[i]._tired = false;
carnivores[i]._tiredTimer = 0;
carnivores[i]._chaseTime = 0;
carnivores[i].target = null;
}
}
updateSeasonText();
} else if (mod === 3) {
// SUMMER
game.setBackgroundColor(0xffff00); // yellow
updateTextColors(0xffff00);
pollinatorsHibernating = false;
window.summerState = true;
window.winterState = false;
window.springState = false;
window.autumnState = false;
// Plants can die, but keep their color normal
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
updateSeasonText();
} else if (mod === 6) {
// AUTUMN
game.setBackgroundColor(0x8B5A2B); // brown
updateTextColors(0x8B5A2B);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = false;
window.winterState = false;
window.autumnState = true;
// Make all plants brown and allow dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x996633; // brown
}
plants[i]._canDie = true;
plants[i]._isBrown = true;
}
updateSeasonText();
} else if (mod === 9) {
// WINTER
game.setBackgroundColor(0xffffff); // white
updateTextColors(0xffffff);
pollinatorsHibernating = true;
window.summerState = false;
window.springState = false;
window.winterState = true;
window.autumnState = false;
// --- WINTER: Parasites die and lay eggs before death ---
for (var i = parasites.length - 1; i >= 0; i--) {
var parasite = parasites[i];
if (parasite && !parasite.eaten) {
// Lay a parasite egg at current position before dying
var egg = new Egg();
egg.x = parasite.x;
egg.y = parasite.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
if (parasite.parent) parasite.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
// Animate parasite dying and remove from array
tween(parasite, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function (p) {
return function () {
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(p);
if (idx !== -1) parasites.splice(idx, 1);
}
p.destroy();
};
}(parasite)
});
}
}
// Kill all brown plants when winter starts
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
// Only kill brown plants; healthy plants do not die in winter
if (p && p._isBrown && !p.eaten && typeof p.die === "function") {
p.die();
}
}
// Make all plants blue and prevent dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x3399ff;
}
plants[i]._canDie = false;
}
// Cull a random portion of eggs during winter to reduce lag
if (eggs && eggs.length > 0) {
// Remove up to 40% of eggs at random, but always leave at least 3 eggs if possible
var eggsToCull = Math.floor(eggs.length * 0.4);
var minEggsLeft = 3;
if (eggs.length - eggsToCull < minEggsLeft) {
eggsToCull = Math.max(0, eggs.length - minEggsLeft);
}
for (var i = 0; i < eggsToCull; i++) {
// Pick a random egg index
var idx = Math.floor(Math.random() * eggs.length);
var e = eggs[idx];
if (e && typeof e.destroy === "function") {
e.destroy();
}
eggs.splice(idx, 1);
}
}
updateSeasonText();
}
}, cycleDurationMs);
// Start countdown updater (every 100ms for smoothness)
if (cycleCountdownInterval === null) {
cycleCountdownInterval = LK.setInterval(function () {
if (cycleTimeLeftMs > 0) {
cycleTimeLeftMs -= 100;
if (cycleTimeLeftMs < 0) cycleTimeLeftMs = 0;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
}
}, 100);
}
}
// --- End of Game Code ---;;
// --- Asset Initialization ---
// new carnivore egg texture
var plants = [];
var herbivores = [];
// --- Herbivore Hunger Queue: Only 2 can be hungry at a time ---
var herbivoreHungerQueue = []; // Array of herbivores waiting to be hungry
var carnivores = [];
var seeds = [];
var eggs = []; // Track all eggs globally
var pollinators = [];
var fungis = []; // Track all fungi globally
var parasites = []; // Track all parasites globally
var fungiSpores = []; // Track all spores shot by fungi
// --- Worms spawned at game start ---
// If you want worms to be present at game start, add them here and set them buried until the next season
var worms = [];
// Worms are not spawned at the top of the screen at game start
// --- Palette UI ---
var paletteY = 180; // y position for palette
var paletteSpacing = 260;
var paletteItems = [];
// --- Delete Mode Button ---
var deleteMode = false;
var deleteBtn = new Container();
var deleteBtnBox = deleteBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0xcc3333,
shape: 'box'
});
var deleteBtnTxt = new Text2("Delete: OFF", {
size: 54,
fill: "#000"
});
deleteBtnTxt.anchor.set(0.5, 0.5);
deleteBtn.addChild(deleteBtnTxt);
// Place delete button under the cycle counter and timer
deleteBtn.x = 500;
deleteBtn.y = cycleTimerTxt.y + 140; // Move further down (was 90px, now 140px below the timer text)
deleteBtn.interactive = true;
deleteBtn.visible = true;
deleteBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
deleteMode = !deleteMode;
deleteBtnBox.color = deleteMode ? 0xff4444 : 0xcc3333;
deleteBtnTxt.setText(deleteMode ? "Delete: ON" : "Delete: OFF");
};
LK.gui.top.addChild(deleteBtn);
// --- Skip Cycle Button ---
var skipCycleBtn = new Container();
var skipCycleBtnBox = skipCycleBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x3388cc,
shape: 'box'
});
var skipCycleBtnTxt = new Text2("Skip Cycle", {
size: 54,
fill: "#000"
});
skipCycleBtnTxt.anchor.set(0.5, 0.5);
skipCycleBtn.addChild(skipCycleBtnTxt);
// Place skip button under the delete button
skipCycleBtn.x = 500;
skipCycleBtn.y = deleteBtn.y + 120;
skipCycleBtn.interactive = true;
skipCycleBtn.visible = true;
// --- Skip Cycle Cooldown State ---
var skipCycleCooldownActive = false;
var skipCycleCooldownTimeout = null;
// --- Skip Cycle Cooldown Counter Text ---
var skipCycleCountdownTxt = new Text2("", {
size: 54,
fill: "#fff"
});
skipCycleCountdownTxt.anchor.set(0, 0.5);
// Position to the right of the skip button (visually next to it)
skipCycleCountdownTxt.x = 130;
skipCycleCountdownTxt.y = 0;
skipCycleCountdownTxt.visible = false;
skipCycleBtn.addChild(skipCycleCountdownTxt);
var skipCycleCountdownInterval = null;
var skipCycleCountdownValue = 0;
// --- Extinction Button ---
var extinctionBtn = new Container();
var extinctionBtnBox = extinctionBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x555555,
shape: 'box'
});
var extinctionBtnTxt = new Text2("Extinction", {
size: 54,
fill: "#000"
});
extinctionBtnTxt.anchor.set(0.5, 0.5);
extinctionBtn.addChild(extinctionBtnTxt);
// Place extinction button under the skip cycle button
extinctionBtn.x = 500;
extinctionBtn.y = skipCycleBtn.y + 120;
extinctionBtn.interactive = true;
extinctionBtn.visible = true;
LK.gui.top.addChild(extinctionBtn);
// --- Achievements Button and Menu removed ---
// --- Randomize Button ---
var randomizeBtn = new Container();
var randomizeBtnBox = randomizeBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x228B22,
shape: 'box'
});
var randomizeBtnTxt = new Text2("Randomize", {
size: 54,
fill: "#000"
});
randomizeBtnTxt.anchor.set(0.5, 0.5);
randomizeBtn.addChild(randomizeBtnTxt);
// Place randomize button under the extinction button
randomizeBtn.x = 500;
randomizeBtn.y = extinctionBtn.y + 120;
randomizeBtn.interactive = true;
randomizeBtn.visible = true;
LK.gui.top.addChild(randomizeBtn);
// --- Personality Reveal Button ---
var personalityBtn = new Container();
var personalityBtnBox = personalityBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x8e44ad,
shape: 'box'
});
var personalityBtnTxt = new Text2("Personality", {
size: 54,
fill: "#000"
});
personalityBtnTxt.anchor.set(0.5, 0.5);
personalityBtn.addChild(personalityBtnTxt);
// Place personality button under the randomize button
personalityBtn.x = 500;
personalityBtn.y = randomizeBtn.y + 120;
personalityBtn.interactive = true;
personalityBtn.visible = true;
LK.gui.top.addChild(personalityBtn);
// --- Personality Reveal Text Above Each Organism ---
var personalityPopup = null;
var personalityTextNodes = []; // Track all personality text objects
// --- Streak Counter UI and Variables ---
// Streak: counts how many cycles at least one organism is alive, resets if all die or if you skip a cycle
var streakCount = 0;
var streakTxt = new Text2("Streak: 0", {
size: 60,
fill: "#000"
});
streakTxt.anchor.set(0.5, 0.5);
// Place streak counter under the personality button (y + 120)
streakTxt.x = personalityBtn.x;
streakTxt.y = personalityBtn.y + 120;
LK.gui.top.addChild(streakTxt);
// --- Score Counter UI and Variables ---
var scoreCount = 0;
var scoreTxt = new Text2("Score: 0", {
size: 60,
fill: "#000"
});
scoreTxt.anchor.set(0.5, 0.5);
// Place score counter under the streak counter
scoreTxt.x = streakTxt.x;
scoreTxt.y = streakTxt.y + 80;
LK.gui.top.addChild(scoreTxt);
// Helper to update score text
function updateScoreText() {
if (scoreTxt && typeof scoreTxt.setText === "function") {
scoreTxt.setText("Score: " + scoreCount);
if (typeof scoreTxt.setStyle === "function") scoreTxt.setStyle({
fill: "#000"
});
}
}
// --- Retract/Expand Streak and Score Counter with Top Buttons ---
// Patch updateTopButtonsRetracted to also move/hide streakTxt and scoreTxt
var _originalUpdateTopButtonsRetracted = updateTopButtonsRetracted;
updateTopButtonsRetracted = function updateTopButtonsRetracted() {
if (topButtonsRetracted) {
// Move streak counter offscreen and hide
var offX = 2048 + 300;
tween(streakTxt, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
streakTxt.visible = false;
}
});
// Move score counter offscreen and hide
tween(scoreTxt, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
scoreTxt.visible = false;
}
});
} else {
// Move streak counter back and show
streakTxt.visible = true;
tween(streakTxt, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
// Move score counter back and show
scoreTxt.visible = true;
tween(scoreTxt, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Call original for other buttons
if (typeof _originalUpdateTopButtonsRetracted === "function") {
_originalUpdateTopButtonsRetracted();
}
};
// Track if a skip happened this cycle (reset streak if so)
var streakSkipThisCycle = false;
// Helper to update streak text
function updateStreakText() {
if (streakTxt && typeof streakTxt.setText === "function") {
streakTxt.setText("Streak: " + streakCount);
if (typeof streakTxt.setStyle === "function") streakTxt.setStyle({
fill: "#000"
});
}
}
function resetStreak() {
streakCount = 0;
updateStreakText();
// Reset score to 0 when streak is reset
scoreCount = 0;
updateScoreText();
// Play the 'Lost' sound every time the streak is reset
var lostSnd = LK.getSound('Lost');
if (lostSnd && typeof lostSnd.play === "function") lostSnd.play();
// Show "streak lost!" text in the middle of the screen in red for 5 seconds
if (typeof window._streakLostTxt !== "undefined" && window._streakLostTxt && window._streakLostTxt.parent) {
window._streakLostTxt.parent.removeChild(window._streakLostTxt);
if (typeof window._streakLostTxt.destroy === "function") window._streakLostTxt.destroy();
window._streakLostTxt = null;
}
var streakLostTxt = new Text2("streak lost!", {
size: 160,
fill: "#c00",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
streakLostTxt.anchor.set(0.5, 0.5);
streakLostTxt.x = 2048 / 2;
streakLostTxt.y = 2732 / 2;
game.addChild(streakLostTxt);
window._streakLostTxt = streakLostTxt;
if (typeof window._streakLostTimeout !== "undefined" && window._streakLostTimeout) {
LK.clearTimeout(window._streakLostTimeout);
}
window._streakLostTimeout = LK.setTimeout(function () {
if (window._streakLostTxt && window._streakLostTxt.parent) {
window._streakLostTxt.parent.removeChild(window._streakLostTxt);
if (typeof window._streakLostTxt.destroy === "function") window._streakLostTxt.destroy();
}
window._streakLostTxt = null;
window._streakLostTimeout = null;
}, 5000);
}
function incrementStreak() {
streakCount++;
updateStreakText();
// Increment score by 50 every time the streak increases
scoreCount += 50;
updateScoreText();
// Play the 'Streak' sound every time the streak counter increases
var streakSnd = LK.getSound('Streak');
if (streakSnd && typeof streakSnd.play === "function") streakSnd.play();
}
function showPersonalityPopup() {
// Remove any previous personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
// For each organism, show a floating text above it with its personality
function addPersonalityTextFor(arr, getType, getPers) {
for (var i = 0; i < arr.length; i++) {
var org = arr[i];
if (!org || typeof org.x !== "number" || typeof org.y !== "number") continue;
var type = getType(org);
var pers = getPers(org);
var txt = new Text2(pers, {
size: 44,
fill: pers === "N/A" ? "#888" : "#2c3e50"
});
txt.anchor.set(0.5, 1);
txt.x = org.x;
txt.y = org.y - 70;
txt._personalityTarget = org;
txt._personalityType = type;
game.addChild(txt);
personalityTextNodes.push(txt);
}
}
// Ferns (plants)
addPersonalityTextFor(plants, function () {
return "Fern";
}, function () {
return "N/A";
});
// Nummer (herbivores)
addPersonalityTextFor(herbivores, function () {
return "Nummer";
}, function (org) {
// Only show personality if actually placed (on map, not destroyed)
if (typeof org.eaten !== "undefined" && org.eaten) return "N/A";
return org.personality || "N/A";
});
// Hunter (carnivores)
addPersonalityTextFor(carnivores, function () {
return "Hunter";
}, function (org) {
// Only show personality if actually placed (on map, not destroyed)
if (typeof org.eaten !== "undefined" && org.eaten) return "N/A";
return org.personality || "N/A";
});
// Pollinator
addPersonalityTextFor(pollinators, function () {
return "Pollinator";
}, function (org) {
return org.personality || "N/A";
});
// Shroom (fungi)
addPersonalityTextFor(fungis, function () {
return "Shroom";
}, function () {
return "N/A";
});
// Parasite
addPersonalityTextFor(parasites, function () {
return "Parasite";
}, function () {
return "N/A";
});
// Worm
addPersonalityTextFor(worms, function () {
return "Worm";
}, function () {
return "N/A";
});
// Patch game.update to keep personality text above each organism while visible
if (!game._personalityTextPatched) {
var _originalGameUpdate = game.update;
game.update = function () {
// Move each personality text to follow its organism
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = personalityTextNodes.length - 1; i >= 0; i--) {
var txt = personalityTextNodes[i];
var org = txt && txt._personalityTarget;
if (!org || typeof org.x !== "number" || typeof org.y !== "number" || org.eaten) {
// Remove text if organism is gone
if (txt.parent) txt.parent.removeChild(txt);
if (typeof txt.destroy === "function") txt.destroy();
personalityTextNodes.splice(i, 1);
continue;
}
txt.x = org.x;
txt.y = org.y - 70;
}
}
if (_originalGameUpdate) _originalGameUpdate.apply(this, arguments);
};
game._personalityTextPatched = true;
}
// Add a close button in the top right to remove all personality text
if (!window._personalityTextCloseBtn) {
var closeBtn = new Container();
var closeBtnBox = closeBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 80,
color: 0x8e44ad,
shape: 'box'
});
var closeBtnTxt = new Text2("Hide", {
size: 44,
fill: "#fff"
});
closeBtnTxt.anchor.set(0.5, 0.5);
closeBtn.addChild(closeBtnTxt);
closeBtn.x = 2048 - 200;
closeBtn.y = 180;
closeBtn.interactive = true;
closeBtn.visible = true;
closeBtn.down = function () {
// Remove all personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
if (closeBtn && closeBtn.parent) closeBtn.parent.removeChild(closeBtn);
window._personalityTextCloseBtn = null;
};
LK.gui.top.addChild(closeBtn);
window._personalityTextCloseBtn = closeBtn;
}
}
// Button handler
// Track if personality text is currently shown
var personalityTextVisible = false;
personalityBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (personalityTextVisible) {
// Hide all personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
// Remove close button if present
if (window._personalityTextCloseBtn) {
if (window._personalityTextCloseBtn.parent) window._personalityTextCloseBtn.parent.removeChild(window._personalityTextCloseBtn);
window._personalityTextCloseBtn = null;
}
personalityTextVisible = false;
} else {
showPersonalityPopup();
personalityTextVisible = true;
}
};
// --- Randomize Button Handler ---
randomizeBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
// Only randomize if not retracted or already disabled
if (topButtonsRetracted || randomizeBtn._disabled) return;
// Helper to get a random position in the map area
function getRandomPos() {
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
return {
x: minX + Math.random() * (maxX - minX),
y: minY + Math.random() * (maxY - minY)
};
}
// Add 3-5 ferns
var fernCount = 3 + Math.floor(Math.random() * 3); // 3,4,5
for (var i = 0; i < fernCount; i++) {
var pos = getRandomPos();
var fern = new Plant();
fern.x = pos.x;
fern.y = pos.y;
fern.scaleX = 1;
fern.scaleY = 1;
fern.alpha = 1;
plants.push(fern);
game.addChild(fern);
}
// Add 3-5 herbivores
var herbCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < herbCount; i++) {
var pos = getRandomPos();
var herb = new Nummer();
herb._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herb.x = pos.x;
herb.y = pos.y;
herb.scaleX = 1;
herb.scaleY = 1;
herb.alpha = 1;
// Start in wandering state when spawned by randomize button
herb.state = "wandering";
herb._wanderAfterSleepTimer = 600; // 10 seconds wandering
// --- Randomize herbivore personality ---
var personalities = ["normal", "curious", "grumpy"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% curious, 35% grumpy
if (rand < 0.3) {
herb.personality = "normal";
} else if (rand < 0.65) {
herb.personality = "curious";
} else {
herb.personality = "grumpy";
}
herbivores.push(herb);
game.addChild(herb);
}
// Add 3-5 pollinators
var pollCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < pollCount; i++) {
var pos = getRandomPos();
var poll = new Pollinator();
poll.x = pos.x;
poll.y = pos.y;
poll.scaleX = 1;
poll.scaleY = 1;
poll.alpha = 1;
pollinators.push(poll);
game.addChild(poll);
}
// Add 0-1 carnivores
var carnCount = Math.floor(Math.random() * 2); // 0 or 1
for (var i = 0; i < carnCount; i++) {
var pos = getRandomPos();
var carn = new Carnivore();
// --- Randomize carnivore personality ---
var carnPersonalities = ["normal", "bloodthirsty", "weak"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% bloodthirsty, 35% weak
if (rand < 0.3) {
carn.personality = "normal";
} else if (rand < 0.65) {
carn.personality = "bloodthirsty";
} else {
carn.personality = "weak";
}
// Adjust speed for bloodthirsty carnivores
if (carn.personality === "bloodthirsty") {
carn.speed = 1.5 + Math.random() * 1.2 + 0.7 + Math.random() * 0.5;
}
carn.x = pos.x;
carn.y = pos.y;
carn.scaleX = 1.25;
carn.scaleY = 1.25;
carn.alpha = 1;
carnivores.push(carn);
game.addChild(carn);
}
// Play plop sound for feedback
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
// --- Unlock skip cycle button when randomize is pressed ---
skipCycleBtn._skipLocked = false;
firstOrganismPlaced = true;
// --- Restart the cycle timer when randomize is pressed ---
if (cycleInterval !== null) {
LK.clearInterval(cycleInterval);
cycleInterval = null;
}
if (cycleCountdownInterval !== null) {
LK.clearInterval(cycleCountdownInterval);
cycleCountdownInterval = null;
}
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
startCycleInterval();
// --- Disable and color the randomize button red ---
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
// --- Disable randomize button if any organisms are present ---
if (plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0) {
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
}
};
// Asteroid extinction event handler
// --- Extinction Event Rate Limiting ---
// Track timestamps (in ms) of recent extinction events
if (typeof window._extinctionEventTimes === "undefined") window._extinctionEventTimes = [];
extinctionBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
LK.getSound('explosion').play();
// Asteroid visual: big circle in center of map
var asteroid = LK.getAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 400,
color: 0x888888,
shape: 'ellipse'
});
asteroid.x = 124 + 1800 / 2;
asteroid.y = paletteY + 320 + 2000 / 2;
asteroid.alpha = 0.0;
game.addChild(asteroid);
// Animate asteroid fade in, then out
tween(asteroid, {
alpha: 1
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(asteroid, {
alpha: 0
}, {
duration: 700,
easing: tween.easeOut,
onFinish: function onFinish() {
asteroid.destroy();
}
});
}
});
// Flash screen for dramatic effect
LK.effects.flashScreen(0xff6600, 800);
// --- Extinction Event Rate Limiting Logic ---
// Add current timestamp to the event list
var now = Date.now();
window._extinctionEventTimes.push(now);
// Only keep events in the last 10 seconds (10000 ms)
var cutoff = now - 10000;
for (var i = window._extinctionEventTimes.length - 1; i >= 0; i--) {
if (window._extinctionEventTimes[i] < cutoff) {
window._extinctionEventTimes.splice(i, 1);
}
}
// If more than 3 extinction events in last 10 seconds, kill all creatures
if (window._extinctionEventTimes.length > 3) {
// Gather all organisms (plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites)
var allGroups = [plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites];
for (var g = 0; g < allGroups.length; g++) {
var arr = allGroups[g];
for (var i = arr.length - 1; i >= 0; i--) {
var org = arr[i];
arr.splice(i, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
}
// Optionally, flash the screen again for drama
LK.effects.flashScreen(0x000000, 1200);
// Clear the event times to prevent repeated wipes
window._extinctionEventTimes = [];
return;
}
// Gather all organisms (plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites)
var allGroups = [plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites];
for (var g = 0; g < allGroups.length; g++) {
var arr = allGroups[g];
if (arr.length === 0) continue;
// If the group has 3 or fewer, kill 1 or 2 at random (never all)
if (arr.length <= 3) {
var toKill = Math.floor(Math.random() * arr.length); // 0, 1, or 2
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
} else if (arr.length % 2 !== 0) {
// If uneven, kill a random number between 1 and half the group (rounded down)
var maxKill = Math.floor(arr.length / 2);
var toKill = 1 + Math.floor(Math.random() * maxKill);
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
} else {
// Even and more than 3: kill half (rounded down), random selection
var survivors = Math.ceil(arr.length / 2);
var toKill = arr.length - survivors;
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
}
}
};
// --- Prevent skipping cycle until first organism is placed ---
var firstOrganismPlaced = false;
skipCycleBtn._skipLocked = true; // Lock skipping at start
skipCycleBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
// --- Streak: Reset streak and play lost sound immediately when skip is pressed ---
streakCount = 0;
updateStreakText();
var lostSnd = LK.getSound('Lost');
if (lostSnd && typeof lostSnd.play === "function") lostSnd.play();
// --- Prevent skipping if all organisms are gone (plants, herbivores, carnivores, pollinators, fungis, parasites, worms) ---
var allGone = plants.length === 0 && herbivores.length === 0 && carnivores.length === 0 && pollinators.length === 0 && fungis.length === 0 && parasites.length === 0 && worms.length === 0;
if (allGone) {
// --- Streak: Reset streak if all organisms die on skip ---
resetStreak();
// Flash skip button red for 2 seconds
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 2000);
return;
}
if (skipCycleBtn._skipLocked) {
// Flash skip button red for 2 seconds
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
// Restore color after 2 seconds
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 2000);
return;
}
// --- 3 Second Cooldown Logic ---
if (skipCycleCooldownActive) {
// --- Streak: Reset streak if skip is attempted during cooldown (optional, but for safety) ---
resetStreak();
// Flash skip button red for 1 second if on cooldown
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 1000);
return;
}
skipCycleCooldownActive = true;
skipCycleBtnBox.color = 0xaaaaaa;
skipCycleBtnTxt.setText("Cooldown");
// Show and start the white countdown
skipCycleCountdownValue = 3;
skipCycleCountdownTxt.setText("" + skipCycleCountdownValue);
skipCycleCountdownTxt.visible = true;
// Clear any previous interval
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
// Start interval to update countdown every 1s
skipCycleCountdownInterval = LK.setInterval(function () {
skipCycleCountdownValue--;
if (skipCycleCountdownValue > 0) {
skipCycleCountdownTxt.setText("" + skipCycleCountdownValue);
} else {
skipCycleCountdownTxt.visible = false;
skipCycleCountdownTxt.setText("");
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
}
}, 1000);
// Restore after 3 seconds
if (skipCycleCooldownTimeout) {
LK.clearTimeout(skipCycleCooldownTimeout);
}
skipCycleCooldownTimeout = LK.setTimeout(function () {
skipCycleCooldownActive = false;
skipCycleBtnBox.color = 0x3388cc;
skipCycleBtnTxt.setText("Skip Cycle");
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
// Hide the countdown if still visible
skipCycleCountdownTxt.visible = false;
skipCycleCountdownTxt.setText("");
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
}, 3000);
// --- Streak: Mark that skip was used this cycle (will reset streak on next cycle) ---
streakSkipThisCycle = true;
// Increment cycle and update UI
cycleCount++;
cycleTxt.setText("Cycle: " + cycleCount);
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
// --- Extinction: If no plants, kill all herbivores, pollinators, and carnivores ---
if (plants.length === 0) {
// Herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (typeof h.die === "function") h.die();else if (typeof h.destroy === "function") h.destroy();
herbivores.splice(i, 1);
}
// Pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (typeof p.die === "function") p.die();else if (typeof p.destroy === "function") p.destroy();
pollinators.splice(i, 1);
}
// Carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (typeof c.die === "function") c.die();else if (typeof c.destroy === "function") c.destroy();
carnivores.splice(i, 1);
}
}
// Set background color every 3 cycles: green, yellow, brown, white, repeat (spring, summer, autumn, winter)
var mod = cycleCount % 12;
if (mod === 0) {
// SPRING
game.setBackgroundColor(0x2ecc40); // green
updateTextColors(0x2ecc40);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = true;
window.winterState = false;
window.autumnState = false;
// Play mellow music
LK.playMusic('mellow');
// --- YEAR COUNTER: Increment year after winter ends (when entering spring) ---
if (cycleCount > 0) {
yearCount++;
if (yearTxt && typeof yearTxt.setText === "function") {
yearTxt.setText("Year: " + yearCount);
}
}
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
for (var i = 0; i < herbivores.length; i++) {
if (herbivores[i] && typeof herbivores[i].speed === "number") {
herbivores[i].speed = 1.2 + Math.random() * 1.0;
}
}
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i]) {
carnivores[i].state = "hungry";
carnivores[i]._tired = false;
carnivores[i]._tiredTimer = 0;
carnivores[i]._chaseTime = 0;
carnivores[i].target = null;
}
}
updateSeasonText();
} else if (mod === 3) {
// SUMMER
game.setBackgroundColor(0xffff00); // yellow
updateTextColors(0xffff00);
pollinatorsHibernating = false;
window.summerState = true;
window.winterState = false;
window.springState = false;
window.autumnState = false;
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
updateSeasonText();
} else if (mod === 6) {
// AUTUMN
game.setBackgroundColor(0x8B5A2B); // brown
updateTextColors(0x8B5A2B);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = false;
window.winterState = false;
window.autumnState = true;
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x996633; // brown
}
plants[i]._canDie = true;
plants[i]._isBrown = true;
}
updateSeasonText();
} else if (mod === 9) {
// WINTER
game.setBackgroundColor(0xffffff); // white
updateTextColors(0xffffff);
pollinatorsHibernating = true;
window.summerState = false;
window.springState = false;
window.winterState = true;
window.autumnState = false;
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
if (p && p._isBrown && !p.eaten && typeof p.die === "function") {
p.die();
}
}
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x3399ff;
}
plants[i]._canDie = false;
}
updateSeasonText();
}
// Randomize all organism positions except fungi, plants, and lichen
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
// Helper to randomize array of organisms
function randomizeOrganisms(arr) {
for (var i = 0; i < arr.length; i++) {
var org = arr[i];
// If this is a host with a parasite attached, skip randomizing its position
if (org && typeof org.x === "number" && typeof org.y === "number" && !org.eaten && !org._parasiteAttached // Only randomize if not hosting a parasite
) {
org.x = minX + Math.random() * (maxX - minX);
org.y = minY + Math.random() * (maxY - minY);
}
}
}
// When randomizing parasites, only randomize those not attached to a host
function randomizeFreeParasites(arr) {
for (var i = 0; i < arr.length; i++) {
var parasite = arr[i];
if (parasite && typeof parasite.x === "number" && typeof parasite.y === "number" && !parasite.eaten && parasite.state !== "attached") {
parasite.x = minX + Math.random() * (maxX - minX);
parasite.y = minY + Math.random() * (maxY - minY);
}
}
}
randomizeOrganisms(herbivores);
randomizeOrganisms(carnivores);
randomizeOrganisms(pollinators);
randomizeOrganisms(seeds);
randomizeOrganisms(eggs);
randomizeOrganisms(worms);
randomizeFreeParasites(parasites);
// Fungi, plants, and lichen are NOT moved
// (fungis, plants, window.lichens)
};
// --- Patch createOrganism to unlock skip after first organism placed ---
var _originalCreateOrganism = createOrganism;
createOrganism = function createOrganism(type, x, y) {
var obj = _originalCreateOrganism(type, x, y);
if (!firstOrganismPlaced) {
firstOrganismPlaced = true;
skipCycleBtn._skipLocked = false;
}
return obj;
};
// --- Retract/Expand Widget for Top Buttons ---
// State for retraction
var topButtonsRetracted = false;
// Widget button (a small round button at the right of the top buttons)
var retractWidget = new Container();
var retractWidgetBox = retractWidget.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 90,
height: 90,
color: 0x888888,
shape: 'ellipse'
});
var retractWidgetTxt = new Text2("≡", {
size: 60,
fill: "#fff"
});
retractWidgetTxt.anchor.set(0.5, 0.5);
retractWidget.addChild(retractWidgetTxt);
// Place widget to the right of the top buttons
retractWidget.x = 300;
retractWidget.y = deleteBtn.y + 50;
retractWidget.interactive = true;
retractWidget.visible = true;
// Helper to update button visibility/position
function updateTopButtonsRetracted() {
if (topButtonsRetracted) {
// Move buttons offscreen right, hide text, and set visible to false
var offX = 2048 + 300;
tween(skipCycleBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
skipCycleBtn.visible = false;
}
});
tween(deleteBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
deleteBtn.visible = false;
}
});
tween(extinctionBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
extinctionBtn.visible = false;
}
});
// Hide randomize button as well
if (typeof randomizeBtn !== "undefined") {
tween(randomizeBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
randomizeBtn.visible = false;
}
});
}
// Hide personality button as well
if (typeof personalityBtn !== "undefined") {
tween(personalityBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
personalityBtn.visible = false;
}
});
}
retractWidgetTxt.setText("▶");
// Also move retractWidget to the right when retracted
tween(retractWidget, {
x: 700 //{Fe} // Move even further right
}, {
duration: 200,
easing: tween.easeIn
});
} else {
// Move buttons back to original positions and set visible to true
tween(retractWidget, {
x: 300
}, {
duration: 200,
easing: tween.easeOut
});
skipCycleBtn.visible = true;
deleteBtn.visible = true;
extinctionBtn.visible = true;
if (typeof randomizeBtn !== "undefined") {
randomizeBtn.visible = true;
tween(randomizeBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Show and move personality button back
if (typeof personalityBtn !== "undefined") {
personalityBtn.visible = true;
tween(personalityBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
tween(skipCycleBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
tween(deleteBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
tween(extinctionBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
retractWidgetTxt.setText("≡");
}
}
// Widget click toggles retraction
retractWidget.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
topButtonsRetracted = !topButtonsRetracted;
// Animate widget itself to the right when retracting, back to original when expanding
if (topButtonsRetracted) {
tween(retractWidget, {
x: 500
}, {
duration: 200,
easing: tween.easeIn
});
} else {
tween(retractWidget, {
x: 300
}, {
duration: 200,
easing: tween.easeOut
});
}
updateTopButtonsRetracted();
};
// Add widget to GUI
LK.gui.top.addChild(skipCycleBtn);
LK.gui.top.addChild(extinctionBtn);
LK.gui.top.addChild(randomizeBtn);
LK.gui.top.addChild(retractWidget);
// On game start, ensure buttons are expanded
updateTopButtonsRetracted();
// Shift the palette even further left so it is not offscreen
var paletteTotalWidth = paletteSpacing * 2;
var paletteXStart = 2048 / 2 - paletteTotalWidth / 2 - 1300;
// Create palette items for drag-and-drop
function createPaletteItem(type, x, y) {
var assetId, color, label;
if (type === 'plant') {
assetId = 'plant';
color = 0x6adf60;
label = 'Fern';
} else if (type === 'herbivore') {
assetId = 'herbivore';
color = 0xf7e26b;
label = 'Nummer';
} else if (type === 'pollinator') {
assetId = 'pollinator';
color = 0xffc300;
label = 'Pollinator';
} else if (type === 'fungi') {
assetId = 'fungi';
color = 0xbb88ff;
label = 'Shroom';
} else if (type === 'parasite') {
assetId = 'Parasite';
color = 0x8888ff;
label = 'Parasite';
} else if (type === 'worm') {
assetId = 'Worm';
color = 0x888888;
label = 'Worm';
} else {
assetId = 'carnivore';
color = 0xe74c3c;
label = 'Hunter';
}
;
// Update all eggs
for (var i = eggs.length - 1; i >= 0; i--) {
var e = eggs[i];
if (!e.parent || e.hatched) {
eggs.splice(i, 1);
continue;
}
if (e.update) e.update();
}
;
// Update all pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (p.eaten) {
pollinators.splice(i, 1);
continue;
}
if (p.update) p.update();
}
var node = new Container();
var icon;
if (type === 'parasite') {
icon = node.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
} else {
icon = node.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
}
node.x = x;
node.y = y;
node.type = type;
// --- Population Counter Above Icon ---
var popCount = 0;
if (type === 'plant') popCount = plants.length;else if (type === 'herbivore') popCount = herbivores.length;else if (type === 'carnivore') popCount = carnivores.length;else if (type === 'pollinator') popCount = pollinators.length;else if (type === 'fungi') popCount = fungis.length;else if (type === 'parasite') popCount = parasites.length;
var popTxt = new Text2("" + popCount, {
size: 54,
fill: "#000"
});
popTxt.anchor.set(0.5, 1);
popTxt.y = -70;
node.addChild(popTxt);
node._popTxt = popTxt; // Store for update
// Add label
var txt = new Text2(label, {
size: 60,
fill: "#000"
});
txt.anchor.set(0.5, 0);
txt.y = 60;
node.addChild(txt);
// Add to GUI overlay (so it stays on top)
LK.gui.top.addChild(node);
paletteItems.push(node);
return node;
}
// --- Palette Scroll Bar ---
// All available organism types in palette
var paletteTypes = ['plant', 'herbivore', 'carnivore', 'pollinator', 'fungi', 'parasite', 'worm'];
var paletteScrollOffset = 0; // How much the palette is scrolled (in px)
var paletteScrollMin = 0;
var paletteScrollMax = Math.max(0, paletteTypes.length * paletteSpacing - paletteSpacing * 3); // Show 3 at a time
// Remove old palette items if any
for (var i = 0; i < paletteItems.length; i++) {
if (paletteItems[i].parent) paletteItems[i].parent.removeChild(paletteItems[i]);
}
paletteItems = [];
// Helper to render palette items based on scroll offset
function renderPaletteItems() {
// Remove all current palette items from GUI
for (var i = 0; i < paletteItems.length; i++) {
if (paletteItems[i].parent) paletteItems[i].parent.removeChild(paletteItems[i]);
}
paletteItems = [];
// Only show up to 3 at a time, centered
var visibleCount = 3;
var startIdx = Math.floor(paletteScrollOffset / paletteSpacing);
var offsetPx = paletteScrollOffset % paletteSpacing;
for (var i = 0; i < visibleCount; i++) {
var idx = startIdx + i;
if (idx >= 0 && idx < paletteTypes.length) {
var px = paletteXStart + i * paletteSpacing - offsetPx;
var node = createPaletteItem(paletteTypes[idx], px, paletteY);
paletteItems.push(node);
}
}
// Draw left/right scroll buttons
if (!window.paletteLeftBtn) {
window.paletteLeftBtn = new Container();
var leftIcon = window.paletteLeftBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 120,
color: 0x444444,
shape: 'box'
});
var leftArrow = new Text2("<", {
size: 80,
fill: "#000"
});
leftArrow.anchor.set(0.5, 0.5);
leftArrow.x = 0;
leftArrow.y = 0;
window.paletteLeftBtn.addChild(leftArrow);
window.paletteLeftBtn.x = paletteXStart - 120;
window.paletteLeftBtn.y = paletteY;
window.paletteLeftBtn.interactive = true;
LK.gui.top.addChild(window.paletteLeftBtn);
window.paletteLeftBtn.visible = false;
}
if (!window.paletteRightBtn) {
window.paletteRightBtn = new Container();
var rightIcon = window.paletteRightBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 120,
color: 0x444444,
shape: 'box'
});
var rightArrow = new Text2(">", {
size: 80,
fill: "#000"
});
rightArrow.anchor.set(0.5, 0.5);
rightArrow.x = 0;
rightArrow.y = 0;
window.paletteRightBtn.addChild(rightArrow);
window.paletteRightBtn.x = paletteXStart + paletteSpacing * visibleCount + 40;
window.paletteRightBtn.y = paletteY;
window.paletteRightBtn.interactive = true;
LK.gui.top.addChild(window.paletteRightBtn);
window.paletteRightBtn.visible = false;
}
// Show/hide buttons
window.paletteLeftBtn.visible = paletteScrollOffset > paletteScrollMin;
window.paletteRightBtn.visible = paletteScrollOffset < paletteScrollMax;
}
renderPaletteItems();
// --- Palette Scroll Button Handlers ---
window.paletteLeftBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (paletteScrollOffset > paletteScrollMin) {
paletteScrollOffset -= paletteSpacing;
if (paletteScrollOffset < paletteScrollMin) paletteScrollOffset = paletteScrollMin;
renderPaletteItems();
}
};
window.paletteRightBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (paletteScrollOffset < paletteScrollMax) {
paletteScrollOffset += paletteSpacing;
if (paletteScrollOffset > paletteScrollMax) paletteScrollOffset = paletteScrollMax;
renderPaletteItems();
}
};
// --- End Palette Scroll Bar ---
// --- Drag and Drop Logic ---
var dragging = null; // {type, node, paletteRef}
var dragOffsetX = 0,
dragOffsetY = 0;
// Helper: create organism at x,y
function createOrganism(type, x, y) {
var obj;
if (type === 'plant') {
obj = new Plant();
plants.push(obj);
// If background is white, make plant blue and undying
if (game.backgroundColor === 0xffffff) {
if (obj.plantAsset && typeof obj.plantAsset.tint !== "undefined") {
obj.plantAsset.tint = 0x3399ff;
}
obj._canDie = false;
}
// If placed in summer, make plant invulnerable for 10 seconds
if (game.backgroundColor === 0xffff00 || window.summerState) {
obj._invulnerable = true;
obj._canDie = false;
// Remove invulnerability after 10 seconds (600 frames)
LK.setTimeout(function () {
if (plants.indexOf(obj) !== -1) {
obj._invulnerable = false;
obj._canDie = true;
}
}, 10000);
}
} else if (type === 'herbivore') {
obj = new Nummer();
obj._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herbivores.push(obj);
} else if (type === 'pollinator') {
obj = new Pollinator();
pollinators.push(obj);
} else if (type === 'fungi') {
obj = new Fungi();
// If fungi is spawned after the cycle has started, set its _bornCycle to the next cycle so it lives a full cycle
if (typeof cycleCount !== "undefined") {
obj._bornCycle = cycleCount + 1;
}
fungis.push(obj);
} else if (type === 'parasite') {
obj = new Parasite();
parasites.push(obj);
// If spawned in winter, kill parasite immediately
if (window.winterState) {
// Animate parasite dying and remove from array
tween(obj, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(obj);
if (idx !== -1) parasites.splice(idx, 1);
}
obj.destroy();
}
});
}
} else if (type === 'worm') {
obj = new Worm();
worms.push(obj);
// If spawned in autumn or spring, set buried state (handled in Worm class, but ensure correct state at spawn)
if ((window.autumnState || window.springState) && obj) {
obj.buried = true;
if (obj._wormAsset) {
obj.removeChild(obj._wormAsset);
}
obj._wormAsset = obj.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// If spawned in the first 2 cycles, set buried state
if (typeof cycleCount !== "undefined" && cycleCount < 2 && obj) {
obj.buried = true;
if (obj._wormAsset) {
obj.removeChild(obj._wormAsset);
}
obj._wormAsset = obj.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
} else {
obj = new Carnivore();
carnivores.push(obj);
}
obj.x = x;
obj.y = y;
obj.scaleX = 1;
obj.scaleY = 1;
obj.alpha = 1;
game.addChild(obj);
// Play plop sound when placing an organism from the palette
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
// Start the cycle interval if not already started
if (typeof startCycleInterval === "function") startCycleInterval();
// If personality button is enabled (personality text nodes are present), show personality text for this organism
if (personalityTextNodes && personalityTextNodes.length > 0) {
// Helper to add personality text for a single organism
var addPersonalityTextForSingle = function addPersonalityTextForSingle(org, type, pers) {
var txt = new Text2(pers, {
size: 44,
fill: pers === "N/A" ? "#888" : "#2c3e50"
});
txt.anchor.set(0.5, 1);
txt.x = org.x;
txt.y = org.y - 70;
txt._personalityTarget = org;
txt._personalityType = type;
game.addChild(txt);
personalityTextNodes.push(txt);
}; // Determine type and personality string
var type = null,
pers = "N/A";
if (obj instanceof Plant) {
type = "Fern";
pers = "N/A";
} else if (obj instanceof Nummer) {
type = "Nummer";
pers = obj.personality || "N/A";
} else if (obj instanceof Carnivore) {
type = "Hunter";
pers = obj.personality || "N/A";
} else if (obj instanceof Pollinator) {
type = "Pollinator";
pers = obj.personality || "N/A";
} else if (obj instanceof Fungi) {
type = "Shroom";
pers = "N/A";
} else if (obj instanceof Parasite) {
type = "Parasite";
pers = "N/A";
} else if (obj instanceof Worm) {
type = "Worm";
pers = "N/A";
}
addPersonalityTextForSingle(obj, type, pers);
}
return obj;
}
// Helper: check if point is inside a palette item
function isInPalette(x, y, node) {
var bounds = {
x: node.x - 60,
y: node.y - 60,
w: 120,
h: 120
};
return x >= bounds.x && x <= bounds.x + bounds.w && y >= bounds.y && y <= bounds.y + bounds.h;
}
// --- Event Handlers ---
// Global button click sound handler: play click sound for any button on screen
if (typeof window._globalButtonClickHandlerAttached === "undefined") {
window._globalButtonClickHandlerAttached = true;
// Patch LK.gui.top's down event to play click sound for any button
var _originalGuiTopDown = LK.gui.top.down;
LK.gui.top.down = function (x, y, obj) {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (typeof _originalGuiTopDown === "function") _originalGuiTopDown.call(this, x, y, obj);
};
// Patch LK.gui.bottom's down event to play click sound for any button
var _originalGuiBottomDown = LK.gui.bottom.down;
LK.gui.bottom.down = function (x, y, obj) {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (typeof _originalGuiBottomDown === "function") _originalGuiBottomDown.call(this, x, y, obj);
};
}
// Down: start drag if on palette
game.down = function (x, y, obj) {
// If delete mode is ON, check if click is on an organism and delete it
if (deleteMode) {
// Include lichens in allOrganisms if present
var allOrganisms = plants.concat(herbivores, carnivores, pollinators, fungis, parasites, worms);
for (var i = allOrganisms.length - 1; i >= 0; i--) {
var org = allOrganisms[i];
var dx = x - org.x;
var dy = y - org.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var isFungi = fungis.indexOf(org) !== -1;
var canDelete = isFungi || !org.eaten;
if (canDelete && dist < 60) {
// Remove from correct array
if (plants.indexOf(org) !== -1) {
var idx = plants.indexOf(org);
if (idx !== -1) plants.splice(idx, 1);
} else if (herbivores.indexOf(org) !== -1) {
var idx = herbivores.indexOf(org);
if (idx !== -1) {
// Remove from hunger queue if present
var hqidx = herbivoreHungerQueue.indexOf(org);
if (hqidx !== -1) {
herbivoreHungerQueue.splice(hqidx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(idx, 1);
}
} else if (carnivores.indexOf(org) !== -1) {
var idx = carnivores.indexOf(org);
if (idx !== -1) carnivores.splice(idx, 1);
} else if (pollinators.indexOf(org) !== -1) {
var idx = pollinators.indexOf(org);
if (idx !== -1) pollinators.splice(idx, 1);
} else if (fungis.indexOf(org) !== -1) {
var idx = fungis.indexOf(org);
if (idx !== -1) fungis.splice(idx, 1);
}
if (typeof org.die === "function") {
org.die();
} else if (typeof org.destroy === "function") {
org.destroy();
}
return;
}
}
// If not on an organism, do nothing in delete mode
return;
}
// Check if touch/click is on a palette item
for (var i = 0; i < paletteItems.length; i++) {
var p = paletteItems[i];
// Convert GUI coordinates to game coordinates
var guiPos = LK.gui.top.toLocal({
x: x,
y: y
}, game);
if (isInPalette(guiPos.x, guiPos.y, p)) {
// Start dragging a new organism
dragging = {
type: p.type,
node: createOrganism(p.type, x, y),
paletteRef: p
};
dragOffsetX = 0;
dragOffsetY = 0;
// Make it semi-transparent while dragging
dragging.node.alpha = 0.7;
return;
}
}
// If not on palette, check if on an organism to move it
// Check if touch/click is on an organism to move it
var found = false;
var allOrganisms = plants.concat(herbivores, carnivores, pollinators, fungis, parasites, worms);
for (var i = allOrganisms.length - 1; i >= 0; i--) {
var org = allOrganisms[i];
// Only allow moving if not eaten and touch is within 60px radius of center
var dx = x - org.x;
var dy = y - org.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var isFungi = fungis.indexOf(org) !== -1;
var isParasite = parasites.indexOf(org) !== -1;
var canDrag = isFungi || isParasite || !org.eaten;
if (canDrag && dist < 60) {
var type;
if (plants.indexOf(org) !== -1) type = 'plant';else if (herbivores.indexOf(org) !== -1) type = 'herbivore';else if (carnivores.indexOf(org) !== -1) type = 'carnivore';else if (pollinators.indexOf(org) !== -1) type = 'pollinator';else if (fungis.indexOf(org) !== -1) type = 'fungi';else if (parasites.indexOf(org) !== -1) type = 'parasite';
// (UI label is handled in createPaletteItem, so no change needed here for type)
dragging = {
type: type,
node: org,
paletteRef: null
};
dragOffsetX = org.x - x;
dragOffsetY = org.y - y;
org.alpha = 0.7;
found = true;
break;
}
}
if (!found) {
dragging = null;
}
};
// Move: update drag position
game.move = function (x, y, obj) {
if (dragging && dragging.node) {
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
var newX = x + dragOffsetX;
var newY = y + dragOffsetY;
// Clamp to map area
if (newX < minX) newX = minX;
if (newX > maxX) newX = maxX;
if (newY < minY) newY = minY;
if (newY > maxY) newY = maxY;
dragging.node.x = newX;
dragging.node.y = newY;
}
};
// Up: drop organism if dragging
game.up = function (x, y, obj) {
if (dragging && dragging.node) {
// If dropped inside palette area, destroy (cancel)
var guiPos = LK.gui.top.toLocal({
x: x,
y: y
}, game);
var cancel = false;
for (var i = 0; i < paletteItems.length; i++) {
if (isInPalette(guiPos.x, guiPos.y, paletteItems[i])) {
cancel = true;
break;
}
}
if (cancel) {
// Remove organism
dragging.node.destroy();
if (dragging.type === 'plant') {
var idx = plants.indexOf(dragging.node);
if (idx !== -1) plants.splice(idx, 1);
} else if (dragging.type === 'herbivore') {
var idx = herbivores.indexOf(dragging.node);
if (idx !== -1) {
// Remove from hunger queue if present
var hqidx = herbivoreHungerQueue.indexOf(dragging.node);
if (hqidx !== -1) {
herbivoreHungerQueue.splice(hqidx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(idx, 1);
}
} else if (dragging.type === 'carnivore') {
var idx = carnivores.indexOf(dragging.node);
if (idx !== -1) carnivores.splice(idx, 1);
} else if (dragging.type === 'pollinator') {
var idx = pollinators.indexOf(dragging.node);
if (idx !== -1) pollinators.splice(idx, 1);
} else if (dragging.type === 'fungi') {
var idx = fungis.indexOf(dragging.node);
if (idx !== -1) fungis.splice(idx, 1);
} else if (dragging.type === 'parasite') {
var idx = parasites.indexOf(dragging.node);
if (idx !== -1) parasites.splice(idx, 1);
} else if (dragging.type === 'worm') {
var idx = worms.indexOf(dragging.node);
if (idx !== -1) worms.splice(idx, 1);
}
} else {
// Place organism: snap alpha to 1
dragging.node.alpha = 1;
// If dropping a parasite, set state to 'seek' and detach from host if needed
if (dragging.type === 'parasite') {
// If attached, detach
if (dragging.node.state === "attached") {
if (dragging.node.attachedTo && dragging.node.attachedTo._parasiteAttached === dragging.node) {
dragging.node.attachedTo._parasiteAttached = null;
}
dragging.node.attachedTo = null;
}
dragging.node.state = "seek";
dragging.node.target = null;
}
}
dragging = null;
}
};
// --- Main Update Loop ---
game.update = function () {
// Update all herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (h.eaten) {
// Remove from hunger queue if present
var idx = herbivoreHungerQueue.indexOf(h);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(i, 1);
continue;
}
if (h.update) h.update();
}
// Update all carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (c.eaten) {
carnivores.splice(i, 1);
continue;
}
if (c.update) c.update();
}
// Update all worms
for (var i = worms.length - 1; i >= 0; i--) {
var w = worms[i];
if (w.update) w.update();
}
// In summer, randomly turn some healthy plants brown
if (window.summerState) {
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
// Only affect healthy, non-brown, non-eaten plants
if (!p.eaten && !p._isBrown) {
// Smaller chance per frame (e.g. 0.0005 = ~0.05% per frame)
if (Math.random() < 0.0005) {
p._isBrown = true;
if (p.plantAsset && typeof p.plantAsset.tint !== "undefined") {
p.plantAsset.tint = 0x996633; // brown
}
}
}
}
}
// Clean up dead plants
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
if (p.eaten) {
plants.splice(i, 1);
}
}
// Update all seeds
for (var i = seeds.length - 1; i >= 0; i--) {
var s = seeds[i];
if (s.eaten) {
seeds.splice(i, 1);
continue;
}
if (s.update) s.update();
}
;
// Update all fungi
for (var i = fungis.length - 1; i >= 0; i--) {
var f = fungis[i];
if (f.update) f.update();
}
// Update all fungi spores
for (var i = fungiSpores.length - 1; i >= 0; i--) {
var s = fungiSpores[i];
if (!s.parent || !s._alive) {
fungiSpores.splice(i, 1);
continue;
}
if (s.update) s.update();
}
// Update all parasites
for (var i = parasites.length - 1; i >= 0; i--) {
var b = parasites[i];
if (b.update) b.update();
}
// --- Update palette population counters ---
for (var i = 0; i < paletteItems.length; i++) {
var node = paletteItems[i];
if (!node._popTxt) continue;
var type = node.type;
var popCount = 0;
var maxCount = null;
if (type === 'plant') {
popCount = plants.length;
maxCount = 20;
} else if (type === 'herbivore') {
popCount = herbivores.length;
maxCount = 6;
} else if (type === 'carnivore') {
popCount = carnivores.length;
maxCount = 4;
} else if (type === 'pollinator') {
popCount = pollinators.length;
maxCount = 10;
} else if (type === 'fungi') {
popCount = fungis.length;
} else if (type === 'parasite') {
popCount = parasites.length;
maxCount = 4;
} else if (type === 'worm') {
popCount = worms.length;
}
node._popTxt.setText("" + popCount);
// Set counter color to red if at population control max, else black
if (type === 'herbivore' && popCount >= 6 || type === 'pollinator' && popCount >= 10 || type === 'plant' && popCount >= 20 || type === 'carnivore' && popCount >= 4 || type === 'parasite' && popCount >= 4) {
if (typeof node._popTxt.setStyle === "function") node._popTxt.setStyle({
fill: "#c00"
});
} else {
if (typeof node._popTxt.setStyle === "function") node._popTxt.setStyle({
fill: "#000"
});
}
}
// --- Continuously keep randomize button disabled if organisms are present ---
if (plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0) {
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
} else {
randomizeBtn._disabled = false;
randomizeBtnBox.color = 0x228B22;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomize");
}
};
// --- Instructions Text ---
// --- Random seed spawning around the map if there are any plants ---
var randomSeedSpawnTimer = null;
function startRandomSeedSpawning() {
if (randomSeedSpawnTimer !== null) return;
randomSeedSpawnTimer = LK.setInterval(function () {
// Only spawn if there are any plants
if (plants.length > 0) {
// 40% chance per interval to spawn a seed
if (Math.random() < 0.4) {
// Pick a random position inside the map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
var sx = minX + Math.random() * (maxX - minX);
var sy = minY + Math.random() * (maxY - minY);
var seed = new Seed();
seed.x = sx;
seed.y = sy;
seed.scaleX = 0.4;
seed.scaleY = 0.4;
seed.alpha = 1;
seeds.push(seed);
game.addChild(seed);
}
}
// If no plants, stop spawning
if (plants.length === 0 && randomSeedSpawnTimer !== null) {
LK.clearInterval(randomSeedSpawnTimer);
randomSeedSpawnTimer = null;
}
}, 120); // Try every 2 seconds
}
// Start random seed spawning at game start
startRandomSeedSpawning();
LK.playMusic('mellow', {
loop: true
});
// --- Map Area Border (for visual feedback) ---
// Use a simple colored rectangle instead of a plant asset for the map border background
var mapBorder = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800,
height: 2000,
color: 0x2e8b57,
shape: 'box'
});
mapBorder.alpha = 0.12;
game.addChild(mapBorder);
mapBorder.x = 124;
mapBorder.y = paletteY + 320;
// --- Add invisible border walls to prevent creatures from exiting the map area ---
var borderThickness = 40;
var borderAlpha = 0; // fully invisible
// Left border
var borderLeft = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: borderThickness,
height: 2000,
color: 0x000000,
shape: 'box'
});
borderLeft.alpha = borderAlpha;
game.addChild(borderLeft);
borderLeft.x = 124 - borderThickness;
borderLeft.y = paletteY + 320;
// Right border
var borderRight = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: borderThickness,
height: 2000,
color: 0x000000,
shape: 'box'
});
borderRight.alpha = borderAlpha;
game.addChild(borderRight);
borderRight.x = 124 + 1800;
borderRight.y = paletteY + 320;
// Top border
var borderTop = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800 + borderThickness * 2,
height: borderThickness,
color: 0x000000,
shape: 'box'
});
borderTop.alpha = borderAlpha;
game.addChild(borderTop);
borderTop.x = 124 - borderThickness;
borderTop.y = paletteY + 320 - borderThickness;
// Bottom border
var borderBottom = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800 + borderThickness * 2,
height: borderThickness,
color: 0x000000,
shape: 'box'
});
borderBottom.alpha = borderAlpha;
game.addChild(borderBottom);
borderBottom.x = 124 - borderThickness;
borderBottom.y = paletteY + 320 + 2000;
// --- End of Game Code ---; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Carnivore class
var Carnivore = Container.expand(function () {
var self = Container.call(this);
// Uncommon: 15% chance to be resistant to parasites
self.parasiteResistant = Math.random() < 0.15;
var carnAsset = self.attachAsset('carnivore', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.25,
scaleY: 1.25
});
self.target = null;
// Assign base speed, will adjust for personality below
self.speed = 1.5 + Math.random() * 1.2;
self.eaten = false;
self._chaseTime = 0; // How long we've been chasing a herbivore
self._tired = false; // Are we tired?
self._tiredTimer = 0; // How long left to rest
// Track tired attempts per herbivore (herbivore.id -> count)
self._tiredAttempts = {};
// --- Personality system ---
// Personalities: "normal", "bloodthirsty", "weak"
var carnPersonalities = ["normal", "bloodthirsty", "weak"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% bloodthirsty, 35% weak
if (rand < 0.3) {
self.personality = "normal";
} else if (rand < 0.65) {
self.personality = "bloodthirsty";
} else {
self.personality = "weak";
}
// Adjust speed for bloodthirsty carnivores
if (self.personality === "bloodthirsty") {
self.speed += 0.7 + Math.random() * 0.5; // noticeably faster
}
// For weak carnivores, no speed change, but will tire faster in update logic
// For weak: timer for running away from other carnivores
self._weakRunTimer = 0;
self._weakRunAngle = 0;
// --- State management for wandering/hungry ---
self.state = "hungry"; // "wandering" or "hungry"
self._eatCooldown = 0; // frames left in wandering after eating
self.update = function () {
if (self.eaten) return;
// --- CARNIVORES CANNOT STARVE: No starvation logic for carnivores ---
// --- WINTER BEHAVIOR: Wanderlust, ignore herbivores, and cannot starve ---
var isWinter = !!window.winterState;
if (isWinter) {
// Always wander, never chase or eat, never get tired, never starve
self.state = "wandering";
self.target = null;
self._tired = false;
self._tiredTimer = 0;
self._chaseTime = 0;
// Wander constantly
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
return;
}
// --- Personality: weak carnivore runs from other carnivores, can be infected by fungi ---
if (self.personality === "weak") {
// If running away, keep running for a short time
if (self._weakRunTimer > 0) {
self.x += Math.cos(self._weakRunAngle) * self.speed * 1.5;
self.y += Math.sin(self._weakRunAngle) * self.speed * 1.5;
self._weakRunTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// While running, don't do anything else
return;
}
// Check for nearby other carnivores (not self)
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var c = carnivores[ci];
if (c === self || c.eaten) continue;
var dx = c.x - self.x;
var dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minCarnDist) {
minCarnDist = dist;
nearestCarn = c;
}
}
if (nearestCarn && Math.sqrt(minCarnDist) < 220) {
// Run away from the other carnivore for 60-120 frames
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var angle = Math.atan2(dy, dx);
self._weakRunAngle = angle;
self._weakRunTimer = 60 + Math.floor(Math.random() * 60);
// Move immediately this frame
self.x += Math.cos(self._weakRunAngle) * self.speed * 1.5;
self.y += Math.sin(self._weakRunAngle) * self.speed * 1.5;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// Weak carnivores can be infected by fungi (handled in Parasite/Fungi logic)
}
// Handle tired state
// Bloodthirsty carnivores never get tired
if (self.personality === "bloodthirsty") {
self._tired = false;
self._tiredTimer = 0;
} else {
if (self._tired) {
self._tiredTimer--;
if (self._tiredTimer <= 0) {
self._tired = false;
self._chaseTime = 0;
}
// While tired, don't chase or wander, just stand still
return;
}
}
// Handle eat cooldown (wandering after eating)
if (self._eatCooldown > 0) {
self._eatCooldown--;
if (self._eatCooldown === 0) {
self.state = "hungry";
}
}
// State: wandering (ignore prey)
if (self.state === "wandering") {
self.target = null; // Don't chase
// --- Random wandering when not chasing herbivores ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._chaseTime = 0; // Not chasing, reset chase timer
return;
}
// State: hungry (look for prey)
if (self.state === "hungry") {
// --- CARNIVORE POPULATION CONTROL: If at max, hunt less frequently ---
if (typeof carnivores !== "undefined" && carnivores.length >= 4) {
// Only hunt every 120 frames (2 seconds)
if (typeof self._huntDelay === "undefined") self._huntDelay = Math.floor(Math.random() * 120);
self._huntDelay++;
if (self._huntDelay < 120) {
// Wander instead of hunting
self.state = "wandering";
self._huntDelay = self._huntDelay % 120;
return;
}
self._huntDelay = 0;
}
// Find nearest prey if not already targeting
if (!self.target || self.target.eaten) {
var minDist = 999999,
closest = null;
// --- Bloodthirsty: can hunt herbivores and other carnivores (except self) ---
if (self.personality === "bloodthirsty") {
// Hunt herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
// Hunt other carnivores (not self, not eaten)
for (var i = 0; i < carnivores.length; i++) {
var c = carnivores[i];
if (c === self || c.eaten) continue;
var dx = c.x - self.x,
dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = c;
}
}
} else if (self.personality === "weak") {
// Weak carnivores do not hunt other carnivores, only herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Ignore this herbivore if we've gotten tired chasing it 2 or more times this cycle
if (h._uniqueId === undefined) {
// Assign a unique id to each herbivore if not present
h._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
}
if (self._tiredAttempts[h._uniqueId] >= 2) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
} else {
// Normal: only hunt herbivores
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Ignore this herbivore if we've gotten tired chasing it 2 or more times this cycle
if (h._uniqueId === undefined) {
// Assign a unique id to each herbivore if not present
h._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
}
if (self._tiredAttempts[h._uniqueId] >= 2) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
}
// (Carnivores ignore pollinators)
self.target = closest;
}
// --- Random wandering when not chasing herbivores ---
if ((!self.target || self.target.eaten) && (!self._wanderTimer || self._wanderTimer <= 0)) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (!self.target || self.target.eaten) {
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._chaseTime = 0; // Not chasing, reset chase timer
}
// Move toward target herbivore
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Track how long we've been chasing
self._chaseTime++;
// If we've been chasing for more than X seconds, get tired
var chaseLimit = 360; // default 6 seconds
var tiredDuration = 600; // default 10 seconds
if (self.personality === "weak") {
chaseLimit = 180; // 3 seconds for weak carnivores
tiredDuration = 900; // weak carnivores rest longer (15 seconds)
}
if (self._chaseTime > chaseLimit) {
self._tired = true;
self._tiredTimer = tiredDuration;
self._chaseTime = 0;
// Track tired attempts for this herbivore
if (self.target && self.target._uniqueId !== undefined) {
if (!self._tiredAttempts[self.target._uniqueId]) self._tiredAttempts[self.target._uniqueId] = 0;
self._tiredAttempts[self.target._uniqueId]++;
// If tired 2 or more times, ignore this herbivore for the rest of the cycle
if (self._tiredAttempts[self.target._uniqueId] >= 2) {
self.target = null;
self.state = "wandering";
}
}
return;
}
// Eat target if close enough
// Prevent eating in autumn
if (dist < 60 && !self.target.eaten) {
if (window.autumnState) {
// In autumn, do not eat, just wander
self.target = null;
self.state = "wandering";
self._eatCooldown = 3600;
self._chaseTime = 0;
return;
}
// --- Bloodthirsty: can eat other carnivores or herbivores ---
// --- Normal/Weak: only eat herbivores ---
var ateCarnivore = false;
if (self.personality === "bloodthirsty" && carnivores.indexOf(self.target) !== -1 && self.target !== self) {
// Eat other carnivore
if (typeof self.target.die === "function") self.target.die();
ateCarnivore = true;
} else if (herbivores.indexOf(self.target) !== -1) {
// Eat herbivore
if (typeof self.target.die === "function") self.target.die();
} else {
// Not a valid target, do nothing
self.target = null;
return;
}
// Move away from the eaten target
if (self.target) {
var awayDX = self.x - self.target.x;
var awayDY = self.y - self.target.y;
var awayDist = Math.sqrt(awayDX * awayDX + awayDY * awayDY);
if (awayDist > 0) {
// Move 100px away in the opposite direction
self.x += awayDX / awayDist * 100;
self.y += awayDY / awayDist * 100;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
}
// Track herbivores/carnivores eaten for egg laying
if (ateCarnivore) {
self._herbivoresEaten = (self._herbivoresEaten || 0) + 1;
} else {
self._herbivoresEaten = (self._herbivoresEaten || 0) + 1;
}
// Lay an egg if 3 prey have been eaten, then reset counter
// --- CARNIVORE POPULATION CONTROL: Only lay egg if < 4 carnivores ---
if (self._herbivoresEaten >= 3) {
if (typeof carnivores !== "undefined" && carnivores.length < 4) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isCarnivoreEgg = true;
egg._carnivoreHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
self._herbivoresEaten = 0;
}
self.target = null;
self._chaseTime = 0; // Reset chase timer after eating
// Animate carnivore "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
// After eating, enter wandering state for 1 minute (3600 frames)
self.state = "wandering";
self._eatCooldown = 3600;
}
}
} // end hungry state
};
// Carnivores are not eaten in this MVP
return self;
});
// Egg class
var Egg = Container.expand(function () {
var self = Container.call(this);
// Always use carnivore_egg asset if this is a carnivore egg, pollinator_egg if pollinator, else normal egg
var eggAsset;
if (self._isCarnivoreEgg === true || self._carnivoreHatch === true) {
eggAsset = self.attachAsset('carnivore_egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
} else if (self._isPollinatorEgg === true || self._pollinatorHatch === true) {
eggAsset = self.attachAsset('pollinator_egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
} else {
eggAsset = self.attachAsset('egg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1
});
}
self.hatched = false;
self._hatchTimer = 420 + Math.floor(Math.random() * 180); // 7-10 seconds
self.update = function () {
if (self.hatched) return;
// Pause hatching if it's summer or winter
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
if (isSummer || isWinter) {
// Do not decrement hatch timer, eggs are dormant
return;
}
// --- POLLINATOR POPULATION CONTROL: Prevent pollinator eggs from hatching if there are already 10 pollinators ---
if ((self._isPollinatorEgg || self._pollinatorHatch) && typeof pollinators !== "undefined" && pollinators.length >= 10) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
// --- CARNIVORE POPULATION CONTROL: Prevent carnivore eggs from hatching if there are already 4 carnivores ---
if ((self._isCarnivoreEgg || self._carnivoreHatch) && typeof carnivores !== "undefined" && carnivores.length >= 4) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
self._hatchTimer--;
// --- PARASITE POPULATION CONTROL: Prevent parasite eggs from hatching if there are already 4 parasites ---
if ((self._isParasiteEgg || self._parasiteHatch) && typeof parasites !== "undefined" && parasites.length >= 4) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
// --- HERBIVORE POPULATION CONTROL: Prevent herbivore eggs from hatching if there are already 6 herbivores ---
if (!self._isCarnivoreEgg && !self._carnivoreHatch && !self._isPollinatorEgg && !self._pollinatorHatch && !self._isParasiteEgg && !self._parasiteHatch && typeof herbivores !== "undefined" && herbivores.length >= 6) {
// Do not decrement hatch timer, do not hatch, just wait
return;
}
if (self._hatchTimer <= 0) {
// In autumn, eggs have a reduced chance to hatch (e.g. 60% hatch, 40% fail)
if (window.autumnState && Math.random() < 0.4) {
// Egg fails to hatch, animate and destroy
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
self.hatched = true;
// Animate egg hatching
tween(self, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Spawn a new carnivore if this is a carnivore egg, pollinator if pollinator egg, parasite if parasite egg, otherwise herbivore
var popSnd = LK.getSound('Hatch');
if (self._isCarnivoreEgg && self._carnivoreHatch) {
var carn = new Carnivore();
carn.x = self.x;
carn.y = self.y;
carn.scaleX = 1;
carn.scaleY = 1;
carn.alpha = 1;
if (self.parent) self.parent.addChild(carn);
if (typeof carnivores !== "undefined") carnivores.push(carn);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else if (self._isPollinatorEgg && self._pollinatorHatch) {
var poll = new Pollinator();
poll.x = self.x;
poll.y = self.y;
poll.scaleX = 1;
poll.scaleY = 1;
poll.alpha = 1;
// Reset pollinator starve timer on hatch
poll._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// Set pollinator to hungry state after hatching
poll.state = "hungry";
poll.target = null;
if (self.parent) self.parent.addChild(poll);
if (typeof pollinators !== "undefined") pollinators.push(poll);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else if (self._isParasiteEgg && self._parasiteHatch) {
// If this egg was laid by a parasite attached to a resistant host, always inherit resistance
var inheritResistance = false;
if (typeof self._parentHost !== "undefined" && self._parentHost && self._parentHost.parasiteResistant === true) {
inheritResistance = true;
}
var parasite = new Parasite();
parasite.x = self.x;
parasite.y = self.y;
parasite.scaleX = 1;
parasite.scaleY = 1;
parasite.alpha = 1;
if (inheritResistance) {
parasite.parasiteResistant = true;
}
if (self.parent) self.parent.addChild(parasite);
if (typeof parasites !== "undefined") parasites.push(parasite);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
} else {
// Spawn a new Nummer at this position
var herb = new Nummer();
herb._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herb.x = self.x;
herb.y = self.y;
herb.scaleX = 1;
herb.scaleY = 1;
herb.alpha = 1;
// If this egg was laid by a parasite attached to a resistant host, always inherit resistance
if (typeof self._parentHost !== "undefined" && self._parentHost && self._parentHost.parasiteResistant === true) {
herb.parasiteResistant = true;
}
if (self.parent) self.parent.addChild(herb);
herbivores.push(herb);
if (popSnd && typeof popSnd.play === "function") popSnd.play();
}
self.destroy();
}
});
}
};
return self;
});
// Fungi class
var Fungi = Container.expand(function () {
var self = Container.call(this);
// Track the cycle the fungi was created in
self._bornCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
// Use fungi asset
var fungiAsset = self.attachAsset('fungi', {
anchorX: 0.5,
anchorY: 0.5
});
// Fungi AI: shoot spores at nearby herbivores
self._sporeCooldown = 0;
self.update = function () {
// Fungi lifespan removed: fungi no longer die after 1 cycle
// Only shoot if cooldown is 0
if (self._sporeCooldown > 0) {
self._sporeCooldown--;
return;
}
// --- FUNGI POPULATION CONTROL: Prevent infecting with spores if 20 or more fungi ---
if (typeof fungis !== "undefined" && fungis.length >= 20) {
return;
}
// Find nearest herbivore within 400px
var minDist = 999999,
closest = null;
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten || h.spored) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist && Math.sqrt(dist) < 400) {
minDist = dist;
closest = h;
}
}
if (closest) {
// Shoot a spore at the herbivore
var spore = new FungiSpore();
spore.x = self.x;
spore.y = self.y;
spore.target = closest;
game.addChild(spore);
if (!fungiSpores) fungiSpores = [];
fungiSpores.push(spore);
self._sporeCooldown = 120 + Math.floor(Math.random() * 60); // 2-3 seconds cooldown
}
};
// Track if this fungi is currently 'blocked' from respawning after being eaten
self._blockedRespawn = false;
// Add die method so fungi disappear when eaten
self.die = function () {
if (self.eaten) return;
self.eaten = true;
// Animate shrink and fade out, then destroy
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Nummer class (formerly Herbivore)
var Nummer = Container.expand(function () {
var self = Container.call(this);
// Uncommon: 15% chance to be resistant to parasites
self.parasiteResistant = Math.random() < 0.15;
// Track if this Nummer was hit by a spore
self.spored = false;
var herbAsset = self.attachAsset('herbivore', {
anchorX: 0.5,
anchorY: 0.5
});
self.target = null;
self.speed = 1.2 + Math.random() * 1.0;
self.eaten = false;
// Track number of plants eaten for egg laying
self._plantsEaten = 0;
// --- State management for hungry/sleepy ---
self.state = "hungry"; // "hungry" or "sleepy"
self._sleepTimer = 0; // frames left in sleepy state
// --- Starvation timer: die if not eating for 2 cycles (80 seconds = 4800 frames) ---
self._starveTimer = 4800; // 80 seconds at 60fps
// --- Personality system ---
// Personalities: "normal", "curious", "grumpy"
var personalities = ["normal", "curious", "grumpy"];
// Assign a random personality at creation
var rand = Math.random();
// Make random traits more common: 30% normal, 35% curious, 35% grumpy
if (rand < 0.3) {
self.personality = "normal";
} else if (rand < 0.65) {
self.personality = "curious";
} else {
self.personality = "grumpy";
}
// For curious: timer for following pollinators
self._curiousFollowTimer = 0;
self._curiousPollinatorTarget = null;
// For grumpy: scare cooldown
self._grumpyScareCooldown = 0;
self.update = function () {
if (self.eaten) return;
// --- HERBIVORE HUNGER QUEUE LOGIC ---
// Only 2 herbivores can be in "hungry" state at a time. Others must wait in "waiting" state.
if (!self._hungerQueueState) self._hungerQueueState = "unset"; // "unset", "waiting", "hungry"
if (self.state === "hungry" && self._hungerQueueState !== "hungry") {
// Try to enter the hunger queue
if (herbivoreHungerQueue.indexOf(self) === -1) {
herbivoreHungerQueue.push(self);
}
// If not in the first 2, set to waiting
if (herbivoreHungerQueue[0] !== self && herbivoreHungerQueue[1] !== self) {
self._hungerQueueState = "waiting";
self.state = "waiting";
// Don't do hungry logic this frame
} else {
self._hungerQueueState = "hungry";
// Continue as hungry
}
} else if (self.state === "waiting" && self._hungerQueueState !== "waiting") {
// Enter waiting queue if not already
if (herbivoreHungerQueue.indexOf(self) === -1) {
herbivoreHungerQueue.push(self);
}
// If now in first 2, become hungry
if (herbivoreHungerQueue[0] === self || herbivoreHungerQueue[1] === self) {
self.state = "hungry";
self._hungerQueueState = "hungry";
} else {
self._hungerQueueState = "waiting";
// Don't do hungry logic this frame
}
} else if (self.state === "hungry" && (herbivoreHungerQueue[0] === self || herbivoreHungerQueue[1] === self)) {
self._hungerQueueState = "hungry";
} else if (self.state === "hungry" && herbivoreHungerQueue.indexOf(self) > 1) {
// If somehow in hungry state but not allowed, set to waiting
self.state = "waiting";
self._hungerQueueState = "waiting";
}
// If in waiting state, just wander
if (self.state === "waiting") {
// --- Random wandering ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
// Don't do hungry logic
return;
}
// --- HERBIVORE RANDOM SOUND LOGIC ---
// Only make sound if not eaten and not in winter (optional: you can allow in winter if desired)
if (typeof self._nextHerbSoundTimer === "undefined") {
// Set initial timer to random 6-10 seconds (360-600 frames)
self._nextHerbSoundTimer = 360 + Math.floor(Math.random() * 241);
}
if (self._nextHerbSoundTimer > 0) {
self._nextHerbSoundTimer--;
} else {
// Play the 'Herbivore' sound
var snd = LK.getSound('Herbivore');
if (snd && typeof snd.play === "function") snd.play();
// Reset timer to next random interval (6-10 seconds)
self._nextHerbSoundTimer = 360 + Math.floor(Math.random() * 241);
}
// --- WINTER BEHAVIOR: Move slower, avoid eating, and halt starvation meter ---
var isWinter = !!window.winterState;
var originalSpeed = self.speed;
if (isWinter) {
// Move slower in winter
self.speed = originalSpeed * 0.4;
} else {
self.speed = originalSpeed;
}
// Starvation logic: decrement timer if alive and hungry/wandering, but not in winter
if ((self.state === "hungry" || self.state === "wandering") && !isWinter) {
self._starveTimer--;
if (self._starveTimer <= 0 && !self.eaten) {
// Starve and die
self.die();
return;
}
}
// Handle infected state
if (self.infected) {
self.infectedTimer = (self.infectedTimer || 0) + 1;
// Future: add infected behavior here (e.g. slow, animation, etc)
}
// If infection was pending from summer, and now it's not summer, apply infection
if (self._pendingFungiInfection && !window.summerState) {
self._pendingFungiInfection = false;
self.infected = true;
self.infectedTimer = 0;
// Tint permanently purple
tween(self, {
tint: 0xbb88ff
}, {
duration: 80
});
}
// Handle sleepy state
// If just left hungry state (became wandering or sleepy), remove from hunger queue and allow next in queue to become hungry
if ((self.state === "wandering" || self.state === "sleepy") && self._hungerQueueState === "hungry") {
var idx = herbivoreHungerQueue.indexOf(self);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
}
self._hungerQueueState = "unset";
// Next in queue (if any) becomes hungry
for (var i = 0; i < herbivoreHungerQueue.length; i++) {
var h = herbivoreHungerQueue[i];
if (h && h.state === "waiting") {
h.state = "hungry";
h._hungerQueueState = "hungry";
break;
}
}
}
if (self.state === "sleepy") {
// Check for nearby carnivore and possibly wake up
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
}
// If carnivore is within 300px, 50% chance to wake up and run away
if (nearestCarn && Math.sqrt(minCarnDist) < 300) {
// Only try to wake up once per frame if still sleepy
if (!self._triedWakeThisFrame) {
self._triedWakeThisFrame = true;
if (Math.random() < 0.5) {
// Wake up and run away from carnivore
self.state = "hungry";
// Move away from carnivore immediately
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
// Move a burst away (3x speed for one frame)
self.x += dx / dist * (self.speed * 3);
self.y += dy / dist * (self.speed * 3);
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// Don't continue sleeping
return;
}
}
} else {
self._triedWakeThisFrame = false;
}
self._sleepTimer--;
if (self._sleepTimer <= 0) {
self.state = "wandering";
self._wanderAfterSleepTimer = 600; // 10 seconds at 60fps
}
// While sleepy, don't move or seek food
return;
}
// Wandering state after sleep: ignore plants, just wander for 10s, then become hungry
if (self.state === "wandering") {
if (typeof self._wanderAfterSleepTimer === "undefined") self._wanderAfterSleepTimer = 600;
self._wanderAfterSleepTimer--;
// --- Random wandering ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
if (self._wanderAfterSleepTimer <= 0) {
self.state = "hungry";
}
return;
}
// Only seek food if hungry
if (self.state === "hungry") {
var isWinter = !!window.winterState;
// --- PERSONALITY: CURIOUS ---
// If curious, sometimes follow pollinators for a few seconds
if (self.personality === "curious") {
// If not already following, 1% chance per frame to start following a pollinator
if (self._curiousFollowTimer <= 0 && Math.random() < 0.01 && pollinators.length > 0) {
// Pick a random pollinator to follow
var idx = Math.floor(Math.random() * pollinators.length);
var poll = pollinators[idx];
if (poll && !poll.eaten) {
self._curiousPollinatorTarget = poll;
self._curiousFollowTimer = 120 + Math.floor(Math.random() * 120); // 2-4 seconds
}
}
// If following a pollinator
if (self._curiousFollowTimer > 0 && self._curiousPollinatorTarget && !self._curiousPollinatorTarget.eaten) {
var dx = self._curiousPollinatorTarget.x - self.x;
var dy = self._curiousPollinatorTarget.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
self._curiousFollowTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// If timer runs out or pollinator is gone, stop following
if (self._curiousFollowTimer <= 0 || self._curiousPollinatorTarget.eaten) {
self._curiousFollowTimer = 0;
self._curiousPollinatorTarget = null;
}
// While following, do not seek plants
return;
}
// If not following, proceed as normal below
}
// --- PERSONALITY: GRUMPY ---
// If grumpy, check for carnivore within 120px and scare it away if it is targeting this herbivore
if (self.personality === "grumpy" && self._grumpyScareCooldown <= 0) {
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var dx = carn.x - self.x;
var dy = carn.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// If carnivore is close and targeting this herbivore
// Bloodthirsty carnivores are not scared of grumpy herbivores
if (dist < 120 && carn.target === self && !carn._tired && carn.personality !== "bloodthirsty") {
// Scare the carnivore away: set it to wandering, clear its target, and make it tired
carn.state = "wandering";
carn.target = null;
carn._tired = true;
carn._tiredTimer = 300 + Math.floor(Math.random() * 200); // 5-8 seconds
carn._chaseTime = 0;
// Grumpy herbivore enters a short cooldown before it can scare again
self._grumpyScareCooldown = 300; // 5 seconds
// Optional: animate the herbivore (e.g. scale up briefly)
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 80,
easing: tween.easeIn
});
}
});
break;
}
}
}
if (self._grumpyScareCooldown > 0) self._grumpyScareCooldown--;
// --- END PERSONALITY LOGIC ---
// Normal hungry logic
// In winter, do not seek or eat plants, but wander randomly
if (isWinter) {
// Wander only, do not target plants
self.target = null;
// --- Random wandering in winter ---
if (!self._wanderTimer || self._wanderTimer <= 0) {
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
} else {
// Always target the nearest plant when hungry
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
self.target = closest;
}
// Nummer can walk over fungi as if it is not there: do not treat fungi as obstacles, so do not check for collision or avoid them at all in movement logic.
// Check for nearest carnivore
var nearestCarn = null;
var minCarnDist = 999999;
var tiredCarnivore = null;
var tiredCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
// Track tired carnivore chasing this herbivore
if (carn._tired && carn.target === self && cdist < tiredCarnDist) {
tiredCarnivore = carn;
tiredCarnDist = cdist;
}
}
var carnivoreTooClose = false;
var carnDist = 0;
var runAwayDX = 0,
runAwayDY = 0;
if (nearestCarn) {
carnDist = Math.sqrt((nearestCarn.x - self.x) * (nearestCarn.x - self.x) + (nearestCarn.y - self.y) * (nearestCarn.y - self.y));
if (nearestCarn._tired && nearestCarn.target === self) {
// If a tired carnivore was chasing this herbivore, teleport a tiny distance in front of it (away from carnivore)
// Only teleport once per tired state
if (!self._teleportedFromTiredCarnivore) {
var dx = self.x - nearestCarn.x;
var dy = self.y - nearestCarn.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
// Only teleport if not already far away
// Teleport 80px away in the direction away from carnivore
var teleportDist = 80;
var tx = nearestCarn.x + dx / (dist || 1) * teleportDist;
var ty = nearestCarn.y + dy / (dist || 1) * teleportDist;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (tx < minX) tx = minX;
if (tx > maxX) tx = maxX;
if (ty < minY) ty = minY;
if (ty > maxY) ty = maxY;
self.x = tx;
self.y = ty;
}
self._teleportedFromTiredCarnivore = true;
// After teleport, set a run away timer so it keeps running for a while
self._runAwayFromTiredCarnivoreTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
// Pick a run direction directly away from carnivore
if (dist > 0) {
self._runAwayAngle = Math.atan2(dy, dx);
} else {
self._runAwayAngle = Math.random() * Math.PI * 2;
}
}
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
} else if (carnDist < 300) {
// If carnivore is within 300px, run away
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
// Reset teleport flag if not being chased by tired carnivore
self._teleportedFromTiredCarnivore = false;
}
}
// --- New: If a tired carnivore was chasing this herbivore, charge away until far enough ---
// (Removed barrier logic: herbivores can always run away from tired carnivores)
if (carnivoreTooClose && carnDist > 1) {
// If running from a tired carnivore and we have a run timer, keep running in the set direction
if (typeof self._runAwayFromTiredCarnivoreTimer !== "undefined" && self._runAwayFromTiredCarnivoreTimer > 0) {
self.x += Math.cos(self._runAwayAngle) * self.speed * 2.2;
self.y += Math.sin(self._runAwayAngle) * self.speed * 2.2;
self._runAwayFromTiredCarnivoreTimer--;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
// When timer runs out, clear teleport/run state so it can look for food again
if (self._runAwayFromTiredCarnivoreTimer <= 0) {
self._runAwayFromTiredCarnivoreTimer = 0;
self._teleportedFromTiredCarnivore = false;
}
// While running, do not seek food
return;
} else {
// Run away from carnivore (normal, not tired chase)
self.x += runAwayDX / carnDist * (self.speed * 1.5);
self.y += runAwayDY / carnDist * (self.speed * 1.5);
// Reset teleport/run state if not running from tired carnivore
self._teleportedFromTiredCarnivore = false;
self._runAwayFromTiredCarnivoreTimer = 0;
}
} else {
// --- Random wandering when not chasing plant ---
if ((!self.target || self.target.eaten) && (!self._wanderTimer || self._wanderTimer <= 0)) {
// Pick a new random direction every 60-120 frames
var angle = Math.random() * Math.PI * 2;
self._wanderAngle = angle;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (!self.target || self.target.eaten) {
// Wander in the chosen direction
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
}
// Move toward target plant
if (self.target && !self.target.eaten) {
var isWinter = !!window.winterState;
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat plant if close enough, but not in winter
if (!isWinter && dist < 60 && !self.target.eaten) {
var eatenPlant = self.target;
// Play chomp sound when a herbivore eats a plant
var chompSnd = LK.getSound('Chomp');
if (chompSnd && typeof chompSnd.play === "function") chompSnd.play();
self.target.die();
self.target = null;
// Animate Nummer "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
// After eating, enter sleepy state for 10 seconds (600 frames) with a chance to skip sleep
if (Math.random() < 0.7) {
// 70% chance to sleep, 30% chance to skip
self.state = "sleepy";
self._sleepTimer = 600;
} else {
// Skip sleep, go straight to wandering
self.state = "wandering";
self._wanderAfterSleepTimer = 600; // 10 seconds wandering
}
// Reset starvation timer after eating only if plant is not brown
if (!(eatenPlant && eatenPlant._isBrown)) {
self._starveTimer = 4800; // 80 seconds at 60fps
}
// Increment plants eaten counter
self._plantsEaten = (self._plantsEaten || 0) + 1;
// Lay an egg if 4 plants have been eaten, then reset counter
// --- HERBIVORE POPULATION CONTROL: Only lay egg if < 6 herbivores ---
if (self._plantsEaten >= 4) {
if (typeof herbivores !== "undefined" && herbivores.length < 6) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
self._plantsEaten = 0;
}
}
}
}
}
};
// Called when eaten by carnivore
self.die = function (_onFinish) {
if (self.eaten) return;
self.eaten = true;
// Remove from hunger queue if present
var idx = herbivoreHungerQueue.indexOf(self);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
}
// If infected, spawn a fungi at this position
// --- FUNGI POPULATION CONTROL: Prevent dropping fungi at death if 20 or more fungi ---
if (self.infected && (typeof fungis === "undefined" || fungis.length < 20)) {
var newFungi = new Fungi();
// If fungi is spawned by Nummer death, set its _bornCycle to the next cycle so it lives a full cycle
if (typeof cycleCount !== "undefined") {
newFungi._bornCycle = cycleCount + 1;
}
newFungi.x = self.x;
newFungi.y = self.y;
newFungi.scaleX = 1;
newFungi.scaleY = 1;
newFungi.alpha = 1;
if (self.parent) self.parent.addChild(newFungi);
if (typeof fungis !== "undefined") fungis.push(newFungi);
}
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
if (_onFinish) _onFinish();
}
});
};
// Called when hit by a spore
self.sporeHit = function () {
if (self.spored) return; // Only allow once
self.spored = true;
// If it's summer, delay infection until next winter or spring
if (window.summerState) {
// Mark as pending infection, but do not infect yet
self._pendingFungiInfection = true;
// Optionally, you could tint a different color to show pending state, or do nothing
return;
}
// Enter infected state immediately if not summer
self.infected = true;
self.infectedTimer = 0;
// Tint permanently purple
tween(self, {
tint: 0xbb88ff
}, {
duration: 80
});
};
return self;
});
// Parasite class (seeks and attaches to herbivores or carnivores)
var Parasite = Container.expand(function () {
var self = Container.call(this);
var parasiteAsset = self.attachAsset('Parasite', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
// State: "seek", "attached"
self.state = "seek";
self.target = null;
self.attachedTo = null;
self.offsetX = 0;
self.offsetY = 0;
self.speed = 1.5 + Math.random() * 0.7;
// Track last intersecting for attach logic
self._lastIntersecting = false;
self.update = function () {
// If attached, follow the host
if (self.state === "attached" && self.attachedTo) {
// AUTUMN: Detach immediately in autumn
if (window.autumnState) {
// Detach from host
if (self.attachedTo._parasiteAttached === self) {
self.attachedTo._parasiteAttached = null;
}
self.state = "seek";
self.attachedTo = null;
self.target = null;
self._wanderTimer = 0;
// Allow laying an egg only once per parasite in autumn
if (!self._laidAutumnEgg) {
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
self._laidAutumnEgg = true;
}
return;
}
// If host is eaten, parasite dies with host
if (self.attachedTo.eaten) {
// Animate parasite dying and remove from array
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
// Remove from parasites array if present
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(self);
if (idx !== -1) parasites.splice(idx, 1);
}
}
});
return;
}
// If host is a herbivore and is infected by shrooms, let go immediately
if (typeof self.attachedTo.spored !== "undefined" && self.attachedTo.spored === true) {
// Detach from host
if (self.attachedTo._parasiteAttached === self) {
self.attachedTo._parasiteAttached = null;
}
self.state = "seek";
self.attachedTo = null;
self.target = null;
// Wander in a new random direction
self._wanderTimer = 0;
return;
}
// Stay at the same offset from host
self.x = self.attachedTo.x + self.offsetX;
self.y = self.attachedTo.y + self.offsetY;
// --- Parasite lays an egg every cycle if attached to a host, but only if < 4 parasites ---
if (typeof self._lastCycle === "undefined") self._lastCycle = -1;
if (typeof cycleCount !== "undefined" && self._lastCycle !== cycleCount) {
self._lastCycle = cycleCount;
if (typeof parasites !== "undefined" && parasites.length < 4) {
// Lay a parasite egg at current position
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
// Pass parent host resistance to egg for inheritance
if (self.attachedTo && self.attachedTo.parasiteResistant === true) {
egg._parentHost = self.attachedTo;
}
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
}
}
return;
}
// If seeking, look for nearest herbivore or carnivore
if (self.state === "seek") {
// In autumn, cannot infect, so just wander
if (window.autumnState) {
// Wander randomly if no target
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersecting = false;
return;
}
// If no target or target is eaten or already has a parasite attached (not self), find new target
if (!self.target || self.target.eaten || self.target._parasiteAttached && self.target._parasiteAttached !== self) {
var minDist = 999999,
closest = null;
// Search herbivores first
for (var i = 0; i < herbivores.length; i++) {
var h = herbivores[i];
if (h.eaten) continue;
// Only attach if not already parasitized
if (h._parasiteAttached) continue;
// Avoid targeting spore-infected herbivores
if (h.spored === true) continue;
// Avoid parasite-resistant herbivores
if (h.parasiteResistant === true) continue;
var dx = h.x - self.x,
dy = h.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = h;
}
}
// If no herbivore, search carnivores
if (!closest) {
for (var i = 0; i < carnivores.length; i++) {
var c = carnivores[i];
if (c.eaten) continue;
if (c._parasiteAttached) continue;
// Only infect weak carnivores (personality === "weak")
if (c.personality !== "weak") continue;
// Avoid parasite-resistant carnivores
if (c.parasiteResistant === true) continue;
var dx = c.x - self.x,
dy = c.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = c;
}
}
}
self.target = closest;
}
// If we have a target, move toward it
if (self.target && !self.target.eaten && !self.target._parasiteAttached) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Attach if close enough and not already attached
var isIntersecting = self.intersects(self.target);
if (!self._lastIntersecting && isIntersecting && !self.target._parasiteAttached) {
// Attach to host
self.state = "attached";
self.attachedTo = self.target;
// Store offset so parasite stays visually attached
self.offsetX = self.x - self.attachedTo.x;
self.offsetY = self.y - self.attachedTo.y;
// Mark host as parasitized
self.attachedTo._parasiteAttached = self;
}
self._lastIntersecting = isIntersecting;
} else {
// Wander randomly if no target
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersecting = false;
}
}
};
return self;
});
// Plant class
var Plant = Container.expand(function () {
var self = Container.call(this);
var plantAsset = self.attachAsset('plant', {
anchorX: 0.5,
anchorY: 0.5
});
self.plantAsset = plantAsset; // Expose for tinting
self._canDie = true; // Track if plant can die
// Plants are static, but we can animate them when eaten
self.eaten = false;
// Track the cycle the plant was created in
self._bornCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
self.die = function (_onFinish2) {
if (self.eaten) return;
if (self._canDie === false) return; // Prevent dying if not allowed
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
if (_onFinish2) _onFinish2();
}
});
};
// Plants die after 1 cycle
self.update = function () {
if (self.eaten) return;
// --- Summer: chance to turn brown and become unpollinatable ---
if (self._invulnerable) {
// While invulnerable, never turn brown or die
self._isBrown = false;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0xffffff;
}
} else {
if ((window.summerState || window.springState) && !self._isBrown && !self.eaten) {
// Only roll once per plant per summer or spring
if (typeof self._brownChecked === "undefined" || self._brownChecked !== cycleCount) {
self._brownChecked = cycleCount;
if (window.summerState && Math.random() < 0.12) {
// ~12% chance per cycle in summer
self._isBrown = true;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0x996633; // brown
}
} else if (window.springState && Math.random() < 0.002) {
// ~0.2% chance per cycle in spring (VERY low)
self._isBrown = true;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0x996633; // brown
}
}
}
}
// If not summer, reset brown state
if (!window.summerState && self._isBrown) {
self._isBrown = false;
if (self.plantAsset && typeof self.plantAsset.tint !== "undefined") {
self.plantAsset.tint = 0xffffff;
}
}
// Only die if at least one full cycle has passed since birth
if (typeof cycleCount !== "undefined" && typeof self._bornCycle !== "undefined") {
// Brown plants in summer: die after 1/2 cycle (shorter lifespan)
if (self._isBrown && window.summerState && self._canDie !== false) {
if (cycleCount > self._bornCycle + 0.5) {
self.die();
}
} else if (cycleCount > self._bornCycle + 1 && self._canDie !== false) {
self.die();
}
}
}
};
return self;
});
// Pollinator class
var Pollinator = Container.expand(function () {
var self = Container.call(this);
var pollAsset = self.attachAsset('pollinator', {
anchorX: 0.5,
anchorY: 0.5
});
// --- Pollinator Starvation Logic ---
self.state = "seek"; // "seek", "scatter", "planting", "hungry"
self.target = null;
self.speed = 2 + Math.random();
self._scatterTimer = 0;
self._scatterAngle = 0;
self._plantSeedTimer = 0;
self._hasSeed = true;
self.eaten = false;
self.lastWasTouchingPlant = false;
// --- Pollinator Starve Timer ---
self._starveTimer = 3000 + Math.floor(Math.random() * 1200); // 1 cycle = 50s = 3000 frames, randomize a bit
self._lastCycle = typeof cycleCount !== "undefined" ? cycleCount : 0;
// --- Pollination counter for egg laying ---
self._pollinations = 0;
self._layingEgg = false;
self._eggObj = null;
// --- After eating shroom, next pollination is a shroom ---
self._nextPollinationIsShroom = false;
self._didShroomPollination = false;
self.update = function () {
if (self.eaten) return;
// --- POLLINATOR STARVATION LOGIC (handled in constructor patch above) ---
// --- POLLINATOR HUNGRY STATE: Seek fungi and eat if hungry ---
if (self.state === "hungry") {
// Prevent pollinators from eating fungi during autumn, winter, and summer
var isAutumn = !!window.autumnState;
var isWinter = !!window.winterState;
var isSummer = !!window.summerState;
if (isAutumn || isWinter || isSummer) {
// Just wander randomly, do not seek or eat fungi, and do not decrement starve timer
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// Randomly (1 in 8 chance per frame) pick a new fungi to seek, or keep current
if (!self.target || self.target.eaten || Math.random() < 0.125) {
// Find nearest fungi
var minDist = 999999,
closest = null;
for (var i = 0; i < fungis.length; i++) {
var f = fungis[i];
if (f.eaten) continue;
var dx = f.x - self.x,
dy = f.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = f;
}
}
self.target = closest;
}
// Move toward fungi if any
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x,
dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat fungi if close enough
if (dist < 60 && !self.target.eaten) {
// Play chomp sound when pollinator eats fungi
var chompSnd = LK.getSound('Chomp');
if (chompSnd && typeof chompSnd.play === "function") chompSnd.play();
// Track how many times this fungi has been eaten by pollinators
if (typeof self.target._pollinatorEatCount === "undefined") {
self.target._pollinatorEatCount = 0;
}
self.target._pollinatorEatCount++;
// If eaten 3 times, do nothing (fungi does not disappear anymore)
if (self.target._pollinatorEatCount >= 3) {
// Fungi no longer disappears after being eaten 3 times
// Optionally, you could add a visual effect or reset the counter, but do nothing here
}
// Reset starve timer
self._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// After eating, return to normal pollinator state
self.state = "seek";
self.target = null;
// Set flag so next pollination is a shroom
self._nextPollinationIsShroom = true;
self._didShroomPollination = false;
// Animate pollinator "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return;
}
} else {
// If no fungi, wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
}
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
return;
}
// --- POLLINATOR STARVATION: decrement timer unless hibernating or in autumn, winter, or summer ---
var isAutumn = !!window.autumnState;
var isWinter = !!window.winterState;
var isSummer = !!window.summerState;
if (!(typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating) && !isAutumn && !isWinter && !isSummer) {
// Only decrement if not hibernating and not in autumn, winter, or summer
self._starveTimer--;
if (self._starveTimer <= 0 && !self.eaten) {
// Die of starvation!
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
}
// --- POLLINATOR HUNGRY STATE: If timer is low, become hungry and seek fungi ---
if (self.state !== "hungry" && self._starveTimer < 900) {
self.state = "hungry";
self.target = null;
}
// --- If pollinator eats fungi, reset timer ---
self._tryEatFungi = function () {
// Find nearest fungi
var minDist = 999999,
closest = null;
for (var i = 0; i < fungis.length; i++) {
var f = fungis[i];
if (f.eaten) continue;
var dx = f.x - self.x,
dy = f.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = f;
}
}
if (closest) {
self.target = closest;
var dx = closest.x - self.x,
dy = closest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat fungi if close enough
if (dist < 60 && !closest.eaten) {
if (typeof closest.die === "function") closest.die();
// Reset starve timer
self._starveTimer = 3000 + Math.floor(Math.random() * 1200);
// After eating, return to normal pollinator state
self.state = "seek";
self.target = null;
// Animate pollinator "eating"
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
return true;
}
}
return false;
};
// --- POLLINATOR ACTIVE/NONACTIVE BEHAVIOR ---
// If more than 5 pollinators, only 3 are 'active' and make sound, others are 'nonactive'
if (typeof pollinators !== "undefined" && pollinators.length > 5) {
// Assign active/nonactive roles once per frame for all pollinators
if (typeof window._activePollinatorIds === "undefined") window._activePollinatorIds = [];
// Only recalculate once per frame
if (typeof window._activePollinatorFrame !== "number" || window._activePollinatorFrame !== LK.ticks) {
// Pick 3 unique random pollinators to be active
var ids = [];
var pool = [];
for (var i = 0; i < pollinators.length; i++) {
if (!pollinators[i].eaten) pool.push(i);
}
// Shuffle pool
for (var i = pool.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = pool[i];
pool[i] = pool[j];
pool[j] = tmp;
}
ids = pool.slice(0, 3);
window._activePollinatorIds = ids;
window._activePollinatorFrame = LK.ticks;
}
// Set self.activeBehavior based on selection
var myIdx = -1;
for (var i = 0; i < pollinators.length; i++) {
if (pollinators[i] === self) {
myIdx = i;
break;
}
}
self.behavior = window._activePollinatorIds.indexOf(myIdx) !== -1 ? "active" : "nonactive";
} else {
// If 5 or fewer, all are active
self.behavior = "active";
}
// --- POLLINATOR NOISE: Play sound at random intervals, only if active ---
// Nonactive pollinators are incapable of making sound unless population is reduced to 3 or fewer
if (typeof self._nextNoiseTimer === "undefined") {
// Set initial timer to random 1-4 seconds (60-240 frames)
self._nextNoiseTimer = 60 + Math.floor(Math.random() * 180);
}
if (self._nextNoiseTimer > 0) {
self._nextNoiseTimer--;
} else {
var canMakeSound = false;
if (typeof pollinators !== "undefined" && pollinators.length <= 3) {
// All pollinators can make sound if population is 3 or fewer
canMakeSound = true;
} else if (self.behavior === "active") {
// Only active pollinators can make sound if more than 3
canMakeSound = true;
}
if (canMakeSound && !(typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating)) {
// Play the 'Bug' sound
var snd = LK.getSound('Bug');
if (snd && typeof snd.play === "function") snd.play();
}
// Reset timer to next random interval (1-4 seconds)
self._nextNoiseTimer = 60 + Math.floor(Math.random() * 180);
}
// --- HIBERNATION: If background is white, pollinators do nothing, don't move, don't die, don't act ---
if (typeof pollinatorsHibernating !== "undefined" && pollinatorsHibernating) {
// Do not decrement lifetime, do not move, do not die, do not lay eggs, do not plant seeds
return;
}
// If currently laying an egg, wait for egg to hatch before continuing
if (self._layingEgg && self._eggObj) {
// If it's summer or winter, eggs will not hatch, so pollinator should move away and continue pollinating
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
if (isSummer || isWinter) {
// Move away from the egg and resume pollinating
if (self._eggObj) {
// Move pollinator a bit away from the egg (random direction)
var angle = Math.random() * Math.PI * 2;
self.x += Math.cos(angle) * 80;
self.y += Math.sin(angle) * 80;
// Clamp to map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._eggObj = null;
self._layingEgg = false;
self._pollinations = 0; // Reset counter
// Resume pollinating
self.state = "seek";
self.target = null;
// Continue with rest of update
} else {
// Wait for egg to hatch, then continue pollinating
if (self._eggObj.hatched) {
// Spawn a new pollinator at egg position
var newPoll = new Pollinator();
newPoll.x = self._eggObj.x;
newPoll.y = self._eggObj.y;
newPoll.scaleX = 1;
newPoll.scaleY = 1;
newPoll.alpha = 1;
if (self.parent) self.parent.addChild(newPoll);
if (typeof pollinators !== "undefined") pollinators.push(newPoll);
// Remove egg from global eggs array
var idx = eggs.indexOf(self._eggObj);
if (idx !== -1) eggs.splice(idx, 1);
self._eggObj.destroy();
self._eggObj = null;
self._layingEgg = false;
self._pollinations = 0; // Reset counter
} else {
// Wait for egg to hatch, do nothing else
return;
}
}
}
// SEEK: Find nearest plant and move to it, but run from carnivores if close
if (self.state === "seek") {
// Check for nearest carnivore
var nearestCarn = null;
var minCarnDist = 999999;
for (var ci = 0; ci < carnivores.length; ci++) {
var carn = carnivores[ci];
if (carn.eaten) continue;
var cdx = carn.x - self.x;
var cdy = carn.y - self.y;
var cdist = cdx * cdx + cdy * cdy;
if (cdist < minCarnDist) {
minCarnDist = cdist;
nearestCarn = carn;
}
}
var carnivoreTooClose = false;
var carnDist = 0;
var runAwayDX = 0,
runAwayDY = 0;
if (nearestCarn) {
carnDist = Math.sqrt((nearestCarn.x - self.x) * (nearestCarn.x - self.x) + (nearestCarn.y - self.y) * (nearestCarn.y - self.y));
if (carnDist < 220) {
// Run if carnivore is within 220px
carnivoreTooClose = true;
runAwayDX = self.x - nearestCarn.x;
runAwayDY = self.y - nearestCarn.y;
}
}
if (carnivoreTooClose && carnDist > 1) {
// Run away from carnivore (burst speed)
self.x += runAwayDX / carnDist * (self.speed * 2.2);
self.y += runAwayDY / carnDist * (self.speed * 2.2);
} else {
// Find nearest plant
if (!self.target || self.target.eaten) {
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
// Ignore brown plants in summer
if (window.summerState && p._isBrown) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
self.target = closest;
}
// Move toward target plant
if (self.target && !self.target.eaten) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Check for touching plant (within 60px)
var isTouching = dist < 60;
if (!self.lastWasTouchingPlant && isTouching) {
// If plant is brown, skip and immediately seek a new healthy plant
if (self.target && self.target._isBrown) {
// Find next healthy plant (not brown, not eaten)
var minDist2 = 999999,
closest2 = null;
for (var j = 0; j < plants.length; j++) {
var p2 = plants[j];
if (p2.eaten) continue;
if (window.summerState && p2._isBrown) continue;
var dx2 = p2.x - self.x,
dy2 = p2.y - self.y;
var dist2 = dx2 * dx2 + dy2 * dy2;
if (dist2 < minDist2) {
minDist2 = dist2;
closest2 = p2;
}
}
self.target = closest2;
// Do not pollinate, just move on
self.lastWasTouchingPlant = isTouching;
return;
}
// Just touched plant
// --- Pollinate a shroom instead of a plant if flag is set ---
// (Removed: pollinators no longer spawn fungi or delete shrooms)
if (self._nextPollinationIsShroom && !self._didShroomPollination) {
self._didShroomPollination = true;
self._nextPollinationIsShroom = false;
// After pollinating a shroom, next pollination is normal
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
self._pollinations = (self._pollinations || 0) + 1;
// If pollinated 3 times, lay an egg and continue pollinating (do not wait for egg to hatch)
if (self._pollinations >= 3 && !self._layingEgg) {
// In autumn, pollinators do NOT lay eggs, just pollinate and continue
if (window.autumnState) {
self._pollinations = 0; // Reset pollination counter in autumn
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
// --- POLLINATOR POPULATION CONTROL: Only lay egg if < 10 pollinators ---
// --- FERN POPULATION CONTROL: Only lay egg if < 20 ferns (plants) ---
if (typeof pollinators !== "undefined" && pollinators.length >= 10 || typeof plants !== "undefined" && plants.length >= 20) {
// Do not lay egg, just reset pollination counter and scatter
self._pollinations = 0;
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60);
self._scatterAngle = Math.random() * Math.PI * 2;
self.lastWasTouchingPlant = isTouching;
return;
}
// Lay a pollinator egg at current position
var egg = new Egg();
egg.x = self.x;
egg.y = self.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isPollinatorEgg = true;
egg._pollinatorHatch = true;
if (self.parent) self.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
// Reset pollination counter and continue pollinating
self._pollinations = 0;
// Set pollinator to hungry state after laying an egg
self.state = "hungry";
self.target = null;
self.lastWasTouchingPlant = isTouching;
return;
}
self.state = "scatter";
self._scatterTimer = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
self._scatterAngle = Math.random() * Math.PI * 2;
}
self.lastWasTouchingPlant = isTouching;
} else {
// No plant to go to, wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
}
}
}
}
// SCATTER: Move in a random direction for a short time
else if (self.state === "scatter") {
self.x += Math.cos(self._scatterAngle) * self.speed * 2;
self.y += Math.sin(self._scatterAngle) * self.speed * 2;
self._scatterTimer--;
if (self._scatterTimer <= 0) {
self.state = "planting";
self._plantSeedTimer = 30 + Math.floor(Math.random() * 30); // short pause before planting
}
}
// PLANTING: Pause, then plant a seed
else if (self.state === "planting") {
self._plantSeedTimer--;
if (self._plantSeedTimer <= 0 && self._hasSeed) {
// Place a seed at current position
var seed = new Seed();
seed.x = self.x;
seed.y = self.y;
seed.scaleX = 0.4;
seed.scaleY = 0.4;
seed.alpha = 1;
seeds.push(seed);
if (self.parent) self.parent.addChild(seed);
// Play plop sound when pollinator places a seed
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
self._hasSeed = false;
// After planting, look for another plant
self.state = "seek";
self.target = null;
self._hasSeed = true;
}
}
// Keep inside map bounds
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
};
self.die = function () {
if (self.eaten) return;
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Seed class
var Seed = Container.expand(function () {
var self = Container.call(this);
// Use dedicated brown seed asset
var seedAsset = self.attachAsset('seed', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4
});
self.growing = false;
self.eaten = false;
self._growTimer = 600; // 10 seconds at 60fps
self.update = function () {
if (self.eaten) return;
if (!self.growing) {
self._growTimer--;
// --- FERN POPULATION CONTROL: If 20 or more ferns, seed always fails to germinate ---
if (typeof plants !== "undefined" && plants.length >= 20) {
// 100% fail to germinate
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
if (self._growTimer <= 0) {
// --- 30% chance for seed to not germinate and disappear ---
if (Math.random() < 0.3) {
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
// --- Summer or Autumn state: chance for seed to fail germination and disappear ---
if (window.summerState && Math.random() < 0.4) {
// 40% chance to fail in summer, just disappear
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
} else if (window.autumnState && Math.random() < 0.55) {
// 55% chance to fail in autumn, just disappear
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
return;
}
self.growing = true;
// Animate seed growing into plant
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Play grow sound when seed germinates
var growSnd = LK.getSound('Grow');
if (growSnd && typeof growSnd.play === "function") growSnd.play();
// Replace seed with a new plant at same position
var newPlant = new Plant();
newPlant.x = self.x;
newPlant.y = self.y;
newPlant.scaleX = 1;
newPlant.scaleY = 1;
newPlant.alpha = 1;
// Set the plant's _bornCycle to the current cycleCount so its cycle restarts
if (typeof cycleCount !== "undefined") {
newPlant._bornCycle = cycleCount;
}
plants.push(newPlant);
if (self.parent) self.parent.addChild(newPlant);
self.eaten = true;
self.destroy();
}
});
}
}
};
// If something eats the seed (future-proof)
self.die = function () {
if (self.eaten) return;
self.eaten = true;
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Spore class (projectile shot by fungi)
var Spore = Container.expand(function () {
var self = Container.call(this);
// Use a simple green circle for the spore
var sporeAsset = self.attachAsset('plant', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.25,
scaleY: 0.25
});
self.target = null;
self.speed = 2.5;
self._alive = true;
self.update = function () {
if (!self.target || self.target.eaten || !self._alive) {
self.destroy();
self._alive = false;
return;
}
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Check collision with target herbivore
if (!self._lastIntersecting && self.intersects(self.target)) {
// "Hit" the herbivore: destroy spore, animate herbivore, but do not kill
if (self.target && typeof self.target.sporeHit === "function") {
self.target.sporeHit();
}
self.destroy();
self._alive = false;
}
self._lastIntersecting = self.intersects(self.target);
};
return self;
});
// FungiSpore class for future extensibility (could add poison, etc)
// Modified: FungiSpore now takes a straight path (direction set at spawn)
var FungiSpore = Spore.expand(function () {
var self = Spore.call(this);
// Store initial direction at spawn
self._vx = 0;
self._vy = 0;
// Set direction only once at spawn, toward target
self.setDirection = function () {
if (self.target) {
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self._vx = dx / dist * self.speed;
self._vy = dy / dist * self.speed;
} else {
self._vx = 0;
self._vy = 0;
}
} else {
self._vx = 0;
self._vy = 0;
}
};
// Call setDirection at spawn
self.setDirection();
// Override update to move in a straight line
self.update = function () {
if (!self.target || self.target.eaten || !self._alive) {
self.destroy();
self._alive = false;
return;
}
self.x += self._vx;
self.y += self._vy;
// Check collision with target herbivore
if (!self._lastIntersecting && self.intersects(self.target)) {
if (self.target && typeof self.target.sporeHit === "function") {
self.target.sporeHit();
}
self.destroy();
self._alive = false;
}
self._lastIntersecting = self.intersects(self.target);
};
return self;
});
// Worm class (moves only in summer and winter, buried in autumn and spring)
var Worm = Container.expand(function () {
var self = Container.call(this);
// Determine if worm is buried at spawn
self.buried = false;
var isAutumn = !!window.autumnState;
var isSpring = !!window.springState;
if (isAutumn || isSpring) {
self.buried = true;
}
// Use the worm image asset for the worm, or buried asset if buried
self._wormAsset = null;
if (self.buried) {
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
self._wormAsset = self.attachAsset('Worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Movement speed for worms
self.speed = 1 + Math.random() * 0.5;
// Track wander direction and timer
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
// Track brown plants eaten in summer
self._brownPlantsEaten = 0;
// Track last brown plant intersected (for collision detection)
self._lastIntersectingBrownPlant = null;
self.update = function () {
var isSummer = !!window.summerState;
var isWinter = !!window.winterState;
var isAutumn = !!window.autumnState;
var isSpring = !!window.springState;
// If currently buried but season changed to summer/winter, unbury and swap asset
if (self.buried && (isSummer || isWinter)) {
self.buried = false;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// If currently not buried but season changed to autumn/spring, bury and swap asset
if (!self.buried && (isAutumn || isSpring)) {
self.buried = true;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// --- Worm AI: In summer, seek and eat brown plants, then bury after 3 eaten ---
if (isSummer && !self.buried) {
// Find nearest brown plant
var minDist = 999999,
closest = null;
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
if (p.eaten) continue;
if (!p._isBrown) continue;
var dx = p.x - self.x,
dy = p.y - self.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
closest = p;
}
}
if (closest) {
// Move toward brown plant
var dx = closest.x - self.x;
var dy = closest.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 1) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
// Eat brown plant if close enough and not already eaten
var isIntersecting = self.intersects(closest);
if ((!self._lastIntersectingBrownPlant || self._lastIntersectingBrownPlant !== closest) && isIntersecting && !closest.eaten) {
if (typeof closest.die === "function") {
closest.die();
}
self._brownPlantsEaten = (self._brownPlantsEaten || 0) + 1;
// After eating 3 brown plants, bury and swap asset
if (self._brownPlantsEaten >= 3) {
self.buried = true;
if (self._wormAsset) {
self.removeChild(self._wormAsset);
}
self._wormAsset = self.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
self._brownPlantsEaten = 0;
// Immediately stop moving after burying
self._wanderTimer = 0;
self._wanderAngle = 0;
}
}
self._lastIntersectingBrownPlant = isIntersecting ? closest : null;
} else {
// No brown plant: wander randomly
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
self._lastIntersectingBrownPlant = null;
}
} else if (isWinter && !self.buried) {
// Wander randomly in winter
if (!self._wanderTimer || self._wanderTimer <= 0) {
self._wanderAngle = Math.random() * Math.PI * 2;
self._wanderTimer = 60 + Math.floor(Math.random() * 60);
}
if (typeof self._wanderAngle === "number") {
self.x += Math.cos(self._wanderAngle) * self.speed * 0.5;
self.y += Math.sin(self._wanderAngle) * self.speed * 0.5;
self._wanderTimer--;
// Keep inside map bounds (strictly)
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
if (self.x < minX) self.x = minX;
if (self.x > maxX) self.x = maxX;
if (self.y < minY) self.y = minY;
if (self.y > maxY) self.y = maxY;
}
// Reset brown plant eating state in winter
self._brownPlantsEaten = 0;
self._lastIntersectingBrownPlant = null;
}
// In autumn and spring, do nothing (no movement, buried)
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2ecc40 //{7C} // Set to a green background
});
/****
* Game Code
****/
// Pollinator sound effects (buzz, chirp, etc)
// --- Pollinator Hibernation State ---
var pollinatorsHibernating = false;
// --- Year Counter ---
// Add a year counter above the cycle counter that increments after winter ends
var yearCount = 1;
var yearTxt = new Text2("Year: 1", {
size: 80,
fill: "#000"
});
yearTxt.anchor.set(0.5, 0);
// Place above the cycle counter
LK.gui.top.addChild(yearTxt);
yearTxt.x = 500;
yearTxt.y = 40;
// --- Cycle Counter ---
// Add a cycle counter at the bottom of the screen that increments every 50 seconds (3000 frames at 60fps)
var cycleCount = 0;
var cycleTxt = new Text2("Cycle: 0", {
size: 80,
fill: "#000"
});
cycleTxt.anchor.set(0.5, 0);
// Place at top left, but not in the top 100px reserved area
LK.gui.top.addChild(cycleTxt);
cycleTxt.x = 500;
cycleTxt.y = 120;
// --- Season Text at Bottom ---
// Helper to get current season as string
function getCurrentSeason() {
if (window.summerState) return "Summer";
if (window.winterState) return "Winter";
if (window.springState) return "Spring";
if (window.autumnState) return "Autumn";
// Fallback: guess from background color
if (game.backgroundColor === 0xffff00) return "Summer";
if (game.backgroundColor === 0xffffff) return "Winter";
if (game.backgroundColor === 0x8B5A2B) return "Autumn";
return "Spring";
}
var seasonTxt = new Text2("Season: " + getCurrentSeason(), {
size: 90,
fill: "#000"
});
seasonTxt.anchor.set(0, 1);
LK.gui.bottom.addChild(seasonTxt);
seasonTxt.x = 40;
seasonTxt.y = 0;
// Helper to update season text
function updateSeasonText() {
if (seasonTxt && typeof seasonTxt.setText === "function") {
seasonTxt.setText("Season: " + getCurrentSeason());
if (typeof seasonTxt.setStyle === "function") seasonTxt.setStyle({
fill: "#000"
});
}
}
// --- Cycle Countdown Timer Text ---
var cycleDurationMs = 50000; // 50,000 ms = 50 seconds
var cycleTimeLeftMs = cycleDurationMs;
var cycleTimerTxt = new Text2("Next cycle: 50s", {
size: 60,
fill: "#000"
});
cycleTimerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(cycleTimerTxt);
cycleTimerTxt.x = 500;
cycleTimerTxt.y = cycleTxt.y + 80; // Place under the cycle counter
// Timer to increment cycle every 50 seconds
var cycleInterval = null; // Will be started after first organism is placed
var cycleCountdownInterval = null; // For updating the countdown text
// Helper to set all on-screen text color to black
function updateTextColors(bgColor) {
var textColor = "#000";
if (cycleTxt && typeof cycleTxt.setStyle === "function") cycleTxt.setStyle({
fill: textColor
});
if (cycleTimerTxt && typeof cycleTimerTxt.setStyle === "function") cycleTimerTxt.setStyle({
fill: textColor
});
if (deleteBtnTxt && typeof deleteBtnTxt.setStyle === "function") deleteBtnTxt.setStyle({
fill: textColor
});
if (skipCycleBtnTxt && typeof skipCycleBtnTxt.setStyle === "function") skipCycleBtnTxt.setStyle({
fill: textColor
});
// Palette item labels
for (var i = 0; i < paletteItems.length; i++) {
for (var j = 0; j < paletteItems[i].children.length; j++) {
var child = paletteItems[i].children[j];
if (child instanceof Text2 && typeof child.setStyle === "function") {
child.setStyle({
fill: textColor
});
}
}
}
// Palette scroll arrows
if (window.paletteLeftBtn) {
for (var i = 0; i < window.paletteLeftBtn.children.length; i++) {
var child = window.paletteLeftBtn.children[i];
if (child instanceof Text2 && typeof child.setStyle === "function") child.setStyle({
fill: textColor
});
}
}
if (window.paletteRightBtn) {
for (var i = 0; i < window.paletteRightBtn.children.length; i++) {
var child = window.paletteRightBtn.children[i];
if (child instanceof Text2 && typeof child.setStyle === "function") child.setStyle({
fill: textColor
});
}
}
}
function startCycleInterval() {
if (cycleInterval !== null) return;
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
cycleInterval = LK.setInterval(function () {
cycleCount++;
cycleTxt.setText("Cycle: " + cycleCount);
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
// --- Streak Counter Logic ---
// If skip was used this cycle, reset streak and clear flag
if (streakSkipThisCycle) {
resetStreak();
streakSkipThisCycle = false;
} else {
// If at least one organism is alive, increment streak, else reset
var anyAlive = plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0;
if (anyAlive) {
incrementStreak();
} else {
resetStreak();
}
}
// Reset tired attempts for all carnivores at the start of each cycle
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i] && carnivores[i]._tiredAttempts) {
carnivores[i]._tiredAttempts = {};
}
}
// --- Extinction: If no plants, kill all herbivores, pollinators, and carnivores ---
if (plants.length === 0) {
// Herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (typeof h.die === "function") h.die();else if (typeof h.destroy === "function") h.destroy();
herbivores.splice(i, 1);
}
// Pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (typeof p.die === "function") p.die();else if (typeof p.destroy === "function") p.destroy();
pollinators.splice(i, 1);
}
// Carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (typeof c.die === "function") c.die();else if (typeof c.destroy === "function") c.destroy();
carnivores.splice(i, 1);
}
// --- Make randomize button available on total extinction ---
if (typeof randomizeBtn !== "undefined") {
// Check if all organisms are dead (plants, herbivores, carnivores, pollinators, fungis, parasites, worms)
var allGone = plants.length === 0 && herbivores.length === 0 && carnivores.length === 0 && pollinators.length === 0 && fungis.length === 0 && parasites.length === 0 && worms.length === 0;
if (allGone) {
randomizeBtn._disabled = false;
randomizeBtnBox.color = 0x228B22;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomize");
} else {
// If any organisms are present, keep randomize button disabled
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
}
}
}
// Set background color every 3 cycles: green, yellow, brown, white, repeat (spring, summer, autumn, winter)
var mod = cycleCount % 12;
if (mod === 0) {
// SPRING
game.setBackgroundColor(0x2ecc40); // green
updateTextColors(0x2ecc40);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = true;
window.winterState = false;
window.autumnState = false;
// Play mellow music
LK.playMusic('mellow');
// --- YEAR COUNTER: Increment year after winter ends (when entering spring) ---
if (cycleCount > 0) {
yearCount++;
if (yearTxt && typeof yearTxt.setText === "function") {
yearTxt.setText("Year: " + yearCount);
}
}
// Restore plant color to normal (green) and allow dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
// --- SPRING: Hatch all parasite eggs (eggs with _isParasiteEgg and _parasiteHatch) ---
for (var i = eggs.length - 1; i >= 0; i--) {
var e = eggs[i];
if (e && e._isParasiteEgg && e._parasiteHatch && !e.hatched) {
// Instantly hatch the egg into a parasite
e.hatched = true;
// Animate egg hatching
tween(e, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function (egg) {
return function () {
var parasite = new Parasite();
parasite.x = egg.x;
parasite.y = egg.y;
parasite.scaleX = 1;
parasite.scaleY = 1;
parasite.alpha = 1;
if (egg.parent) egg.parent.addChild(parasite);
if (typeof parasites !== "undefined") parasites.push(parasite);
egg.destroy();
};
}(e)
});
eggs.splice(i, 1);
}
}
// --- Resume herbivore movement after winter ends ---
for (var i = 0; i < herbivores.length; i++) {
if (herbivores[i] && typeof herbivores[i].speed === "number") {
herbivores[i].speed = 1.2 + Math.random() * 1.0;
}
}
// --- Resume carnivore hunting after winter ends --- (Ava)
// Reset carnivore state to "hungry" so they resume hunting in spring
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i]) {
carnivores[i].state = "hungry";
carnivores[i]._tired = false;
carnivores[i]._tiredTimer = 0;
carnivores[i]._chaseTime = 0;
carnivores[i].target = null;
}
}
updateSeasonText();
} else if (mod === 3) {
// SUMMER
game.setBackgroundColor(0xffff00); // yellow
updateTextColors(0xffff00);
pollinatorsHibernating = false;
window.summerState = true;
window.winterState = false;
window.springState = false;
window.autumnState = false;
// Plants can die, but keep their color normal
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
updateSeasonText();
} else if (mod === 6) {
// AUTUMN
game.setBackgroundColor(0x8B5A2B); // brown
updateTextColors(0x8B5A2B);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = false;
window.winterState = false;
window.autumnState = true;
// Make all plants brown and allow dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x996633; // brown
}
plants[i]._canDie = true;
plants[i]._isBrown = true;
}
updateSeasonText();
} else if (mod === 9) {
// WINTER
game.setBackgroundColor(0xffffff); // white
updateTextColors(0xffffff);
pollinatorsHibernating = true;
window.summerState = false;
window.springState = false;
window.winterState = true;
window.autumnState = false;
// --- WINTER: Parasites die and lay eggs before death ---
for (var i = parasites.length - 1; i >= 0; i--) {
var parasite = parasites[i];
if (parasite && !parasite.eaten) {
// Lay a parasite egg at current position before dying
var egg = new Egg();
egg.x = parasite.x;
egg.y = parasite.y;
egg.scaleX = 0.5;
egg.scaleY = 0.5;
egg.alpha = 1;
egg._isParasiteEgg = true;
egg._parasiteHatch = true;
if (parasite.parent) parasite.parent.addChild(egg);
if (typeof eggs !== "undefined") eggs.push(egg);
// Animate parasite dying and remove from array
tween(parasite, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function (p) {
return function () {
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(p);
if (idx !== -1) parasites.splice(idx, 1);
}
p.destroy();
};
}(parasite)
});
}
}
// Kill all brown plants when winter starts
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
// Only kill brown plants; healthy plants do not die in winter
if (p && p._isBrown && !p.eaten && typeof p.die === "function") {
p.die();
}
}
// Make all plants blue and prevent dying
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x3399ff;
}
plants[i]._canDie = false;
}
// Cull a random portion of eggs during winter to reduce lag
if (eggs && eggs.length > 0) {
// Remove up to 40% of eggs at random, but always leave at least 3 eggs if possible
var eggsToCull = Math.floor(eggs.length * 0.4);
var minEggsLeft = 3;
if (eggs.length - eggsToCull < minEggsLeft) {
eggsToCull = Math.max(0, eggs.length - minEggsLeft);
}
for (var i = 0; i < eggsToCull; i++) {
// Pick a random egg index
var idx = Math.floor(Math.random() * eggs.length);
var e = eggs[idx];
if (e && typeof e.destroy === "function") {
e.destroy();
}
eggs.splice(idx, 1);
}
}
updateSeasonText();
}
}, cycleDurationMs);
// Start countdown updater (every 100ms for smoothness)
if (cycleCountdownInterval === null) {
cycleCountdownInterval = LK.setInterval(function () {
if (cycleTimeLeftMs > 0) {
cycleTimeLeftMs -= 100;
if (cycleTimeLeftMs < 0) cycleTimeLeftMs = 0;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
}
}, 100);
}
}
// --- End of Game Code ---;;
// --- Asset Initialization ---
// new carnivore egg texture
var plants = [];
var herbivores = [];
// --- Herbivore Hunger Queue: Only 2 can be hungry at a time ---
var herbivoreHungerQueue = []; // Array of herbivores waiting to be hungry
var carnivores = [];
var seeds = [];
var eggs = []; // Track all eggs globally
var pollinators = [];
var fungis = []; // Track all fungi globally
var parasites = []; // Track all parasites globally
var fungiSpores = []; // Track all spores shot by fungi
// --- Worms spawned at game start ---
// If you want worms to be present at game start, add them here and set them buried until the next season
var worms = [];
// Worms are not spawned at the top of the screen at game start
// --- Palette UI ---
var paletteY = 180; // y position for palette
var paletteSpacing = 260;
var paletteItems = [];
// --- Delete Mode Button ---
var deleteMode = false;
var deleteBtn = new Container();
var deleteBtnBox = deleteBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0xcc3333,
shape: 'box'
});
var deleteBtnTxt = new Text2("Delete: OFF", {
size: 54,
fill: "#000"
});
deleteBtnTxt.anchor.set(0.5, 0.5);
deleteBtn.addChild(deleteBtnTxt);
// Place delete button under the cycle counter and timer
deleteBtn.x = 500;
deleteBtn.y = cycleTimerTxt.y + 140; // Move further down (was 90px, now 140px below the timer text)
deleteBtn.interactive = true;
deleteBtn.visible = true;
deleteBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
deleteMode = !deleteMode;
deleteBtnBox.color = deleteMode ? 0xff4444 : 0xcc3333;
deleteBtnTxt.setText(deleteMode ? "Delete: ON" : "Delete: OFF");
};
LK.gui.top.addChild(deleteBtn);
// --- Skip Cycle Button ---
var skipCycleBtn = new Container();
var skipCycleBtnBox = skipCycleBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x3388cc,
shape: 'box'
});
var skipCycleBtnTxt = new Text2("Skip Cycle", {
size: 54,
fill: "#000"
});
skipCycleBtnTxt.anchor.set(0.5, 0.5);
skipCycleBtn.addChild(skipCycleBtnTxt);
// Place skip button under the delete button
skipCycleBtn.x = 500;
skipCycleBtn.y = deleteBtn.y + 120;
skipCycleBtn.interactive = true;
skipCycleBtn.visible = true;
// --- Skip Cycle Cooldown State ---
var skipCycleCooldownActive = false;
var skipCycleCooldownTimeout = null;
// --- Skip Cycle Cooldown Counter Text ---
var skipCycleCountdownTxt = new Text2("", {
size: 54,
fill: "#fff"
});
skipCycleCountdownTxt.anchor.set(0, 0.5);
// Position to the right of the skip button (visually next to it)
skipCycleCountdownTxt.x = 130;
skipCycleCountdownTxt.y = 0;
skipCycleCountdownTxt.visible = false;
skipCycleBtn.addChild(skipCycleCountdownTxt);
var skipCycleCountdownInterval = null;
var skipCycleCountdownValue = 0;
// --- Extinction Button ---
var extinctionBtn = new Container();
var extinctionBtnBox = extinctionBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x555555,
shape: 'box'
});
var extinctionBtnTxt = new Text2("Extinction", {
size: 54,
fill: "#000"
});
extinctionBtnTxt.anchor.set(0.5, 0.5);
extinctionBtn.addChild(extinctionBtnTxt);
// Place extinction button under the skip cycle button
extinctionBtn.x = 500;
extinctionBtn.y = skipCycleBtn.y + 120;
extinctionBtn.interactive = true;
extinctionBtn.visible = true;
LK.gui.top.addChild(extinctionBtn);
// --- Achievements Button and Menu removed ---
// --- Randomize Button ---
var randomizeBtn = new Container();
var randomizeBtnBox = randomizeBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x228B22,
shape: 'box'
});
var randomizeBtnTxt = new Text2("Randomize", {
size: 54,
fill: "#000"
});
randomizeBtnTxt.anchor.set(0.5, 0.5);
randomizeBtn.addChild(randomizeBtnTxt);
// Place randomize button under the extinction button
randomizeBtn.x = 500;
randomizeBtn.y = extinctionBtn.y + 120;
randomizeBtn.interactive = true;
randomizeBtn.visible = true;
LK.gui.top.addChild(randomizeBtn);
// --- Personality Reveal Button ---
var personalityBtn = new Container();
var personalityBtnBox = personalityBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 100,
color: 0x8e44ad,
shape: 'box'
});
var personalityBtnTxt = new Text2("Personality", {
size: 54,
fill: "#000"
});
personalityBtnTxt.anchor.set(0.5, 0.5);
personalityBtn.addChild(personalityBtnTxt);
// Place personality button under the randomize button
personalityBtn.x = 500;
personalityBtn.y = randomizeBtn.y + 120;
personalityBtn.interactive = true;
personalityBtn.visible = true;
LK.gui.top.addChild(personalityBtn);
// --- Personality Reveal Text Above Each Organism ---
var personalityPopup = null;
var personalityTextNodes = []; // Track all personality text objects
// --- Streak Counter UI and Variables ---
// Streak: counts how many cycles at least one organism is alive, resets if all die or if you skip a cycle
var streakCount = 0;
var streakTxt = new Text2("Streak: 0", {
size: 60,
fill: "#000"
});
streakTxt.anchor.set(0.5, 0.5);
// Place streak counter under the personality button (y + 120)
streakTxt.x = personalityBtn.x;
streakTxt.y = personalityBtn.y + 120;
LK.gui.top.addChild(streakTxt);
// --- Score Counter UI and Variables ---
var scoreCount = 0;
var scoreTxt = new Text2("Score: 0", {
size: 60,
fill: "#000"
});
scoreTxt.anchor.set(0.5, 0.5);
// Place score counter under the streak counter
scoreTxt.x = streakTxt.x;
scoreTxt.y = streakTxt.y + 80;
LK.gui.top.addChild(scoreTxt);
// Helper to update score text
function updateScoreText() {
if (scoreTxt && typeof scoreTxt.setText === "function") {
scoreTxt.setText("Score: " + scoreCount);
if (typeof scoreTxt.setStyle === "function") scoreTxt.setStyle({
fill: "#000"
});
}
}
// --- Retract/Expand Streak and Score Counter with Top Buttons ---
// Patch updateTopButtonsRetracted to also move/hide streakTxt and scoreTxt
var _originalUpdateTopButtonsRetracted = updateTopButtonsRetracted;
updateTopButtonsRetracted = function updateTopButtonsRetracted() {
if (topButtonsRetracted) {
// Move streak counter offscreen and hide
var offX = 2048 + 300;
tween(streakTxt, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
streakTxt.visible = false;
}
});
// Move score counter offscreen and hide
tween(scoreTxt, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
scoreTxt.visible = false;
}
});
} else {
// Move streak counter back and show
streakTxt.visible = true;
tween(streakTxt, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
// Move score counter back and show
scoreTxt.visible = true;
tween(scoreTxt, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Call original for other buttons
if (typeof _originalUpdateTopButtonsRetracted === "function") {
_originalUpdateTopButtonsRetracted();
}
};
// Track if a skip happened this cycle (reset streak if so)
var streakSkipThisCycle = false;
// Helper to update streak text
function updateStreakText() {
if (streakTxt && typeof streakTxt.setText === "function") {
streakTxt.setText("Streak: " + streakCount);
if (typeof streakTxt.setStyle === "function") streakTxt.setStyle({
fill: "#000"
});
}
}
function resetStreak() {
streakCount = 0;
updateStreakText();
// Reset score to 0 when streak is reset
scoreCount = 0;
updateScoreText();
// Play the 'Lost' sound every time the streak is reset
var lostSnd = LK.getSound('Lost');
if (lostSnd && typeof lostSnd.play === "function") lostSnd.play();
// Show "streak lost!" text in the middle of the screen in red for 5 seconds
if (typeof window._streakLostTxt !== "undefined" && window._streakLostTxt && window._streakLostTxt.parent) {
window._streakLostTxt.parent.removeChild(window._streakLostTxt);
if (typeof window._streakLostTxt.destroy === "function") window._streakLostTxt.destroy();
window._streakLostTxt = null;
}
var streakLostTxt = new Text2("streak lost!", {
size: 160,
fill: "#c00",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
streakLostTxt.anchor.set(0.5, 0.5);
streakLostTxt.x = 2048 / 2;
streakLostTxt.y = 2732 / 2;
game.addChild(streakLostTxt);
window._streakLostTxt = streakLostTxt;
if (typeof window._streakLostTimeout !== "undefined" && window._streakLostTimeout) {
LK.clearTimeout(window._streakLostTimeout);
}
window._streakLostTimeout = LK.setTimeout(function () {
if (window._streakLostTxt && window._streakLostTxt.parent) {
window._streakLostTxt.parent.removeChild(window._streakLostTxt);
if (typeof window._streakLostTxt.destroy === "function") window._streakLostTxt.destroy();
}
window._streakLostTxt = null;
window._streakLostTimeout = null;
}, 5000);
}
function incrementStreak() {
streakCount++;
updateStreakText();
// Increment score by 50 every time the streak increases
scoreCount += 50;
updateScoreText();
// Play the 'Streak' sound every time the streak counter increases
var streakSnd = LK.getSound('Streak');
if (streakSnd && typeof streakSnd.play === "function") streakSnd.play();
}
function showPersonalityPopup() {
// Remove any previous personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
// For each organism, show a floating text above it with its personality
function addPersonalityTextFor(arr, getType, getPers) {
for (var i = 0; i < arr.length; i++) {
var org = arr[i];
if (!org || typeof org.x !== "number" || typeof org.y !== "number") continue;
var type = getType(org);
var pers = getPers(org);
var txt = new Text2(pers, {
size: 44,
fill: pers === "N/A" ? "#888" : "#2c3e50"
});
txt.anchor.set(0.5, 1);
txt.x = org.x;
txt.y = org.y - 70;
txt._personalityTarget = org;
txt._personalityType = type;
game.addChild(txt);
personalityTextNodes.push(txt);
}
}
// Ferns (plants)
addPersonalityTextFor(plants, function () {
return "Fern";
}, function () {
return "N/A";
});
// Nummer (herbivores)
addPersonalityTextFor(herbivores, function () {
return "Nummer";
}, function (org) {
// Only show personality if actually placed (on map, not destroyed)
if (typeof org.eaten !== "undefined" && org.eaten) return "N/A";
return org.personality || "N/A";
});
// Hunter (carnivores)
addPersonalityTextFor(carnivores, function () {
return "Hunter";
}, function (org) {
// Only show personality if actually placed (on map, not destroyed)
if (typeof org.eaten !== "undefined" && org.eaten) return "N/A";
return org.personality || "N/A";
});
// Pollinator
addPersonalityTextFor(pollinators, function () {
return "Pollinator";
}, function (org) {
return org.personality || "N/A";
});
// Shroom (fungi)
addPersonalityTextFor(fungis, function () {
return "Shroom";
}, function () {
return "N/A";
});
// Parasite
addPersonalityTextFor(parasites, function () {
return "Parasite";
}, function () {
return "N/A";
});
// Worm
addPersonalityTextFor(worms, function () {
return "Worm";
}, function () {
return "N/A";
});
// Patch game.update to keep personality text above each organism while visible
if (!game._personalityTextPatched) {
var _originalGameUpdate = game.update;
game.update = function () {
// Move each personality text to follow its organism
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = personalityTextNodes.length - 1; i >= 0; i--) {
var txt = personalityTextNodes[i];
var org = txt && txt._personalityTarget;
if (!org || typeof org.x !== "number" || typeof org.y !== "number" || org.eaten) {
// Remove text if organism is gone
if (txt.parent) txt.parent.removeChild(txt);
if (typeof txt.destroy === "function") txt.destroy();
personalityTextNodes.splice(i, 1);
continue;
}
txt.x = org.x;
txt.y = org.y - 70;
}
}
if (_originalGameUpdate) _originalGameUpdate.apply(this, arguments);
};
game._personalityTextPatched = true;
}
// Add a close button in the top right to remove all personality text
if (!window._personalityTextCloseBtn) {
var closeBtn = new Container();
var closeBtnBox = closeBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 80,
color: 0x8e44ad,
shape: 'box'
});
var closeBtnTxt = new Text2("Hide", {
size: 44,
fill: "#fff"
});
closeBtnTxt.anchor.set(0.5, 0.5);
closeBtn.addChild(closeBtnTxt);
closeBtn.x = 2048 - 200;
closeBtn.y = 180;
closeBtn.interactive = true;
closeBtn.visible = true;
closeBtn.down = function () {
// Remove all personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
if (closeBtn && closeBtn.parent) closeBtn.parent.removeChild(closeBtn);
window._personalityTextCloseBtn = null;
};
LK.gui.top.addChild(closeBtn);
window._personalityTextCloseBtn = closeBtn;
}
}
// Button handler
// Track if personality text is currently shown
var personalityTextVisible = false;
personalityBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (personalityTextVisible) {
// Hide all personality text nodes
if (personalityTextNodes && personalityTextNodes.length) {
for (var i = 0; i < personalityTextNodes.length; i++) {
var node = personalityTextNodes[i];
if (node && node.parent) node.parent.removeChild(node);
if (node && typeof node.destroy === "function") node.destroy();
}
personalityTextNodes = [];
}
// Remove close button if present
if (window._personalityTextCloseBtn) {
if (window._personalityTextCloseBtn.parent) window._personalityTextCloseBtn.parent.removeChild(window._personalityTextCloseBtn);
window._personalityTextCloseBtn = null;
}
personalityTextVisible = false;
} else {
showPersonalityPopup();
personalityTextVisible = true;
}
};
// --- Randomize Button Handler ---
randomizeBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
// Only randomize if not retracted or already disabled
if (topButtonsRetracted || randomizeBtn._disabled) return;
// Helper to get a random position in the map area
function getRandomPos() {
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
return {
x: minX + Math.random() * (maxX - minX),
y: minY + Math.random() * (maxY - minY)
};
}
// Add 3-5 ferns
var fernCount = 3 + Math.floor(Math.random() * 3); // 3,4,5
for (var i = 0; i < fernCount; i++) {
var pos = getRandomPos();
var fern = new Plant();
fern.x = pos.x;
fern.y = pos.y;
fern.scaleX = 1;
fern.scaleY = 1;
fern.alpha = 1;
plants.push(fern);
game.addChild(fern);
}
// Add 3-5 herbivores
var herbCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < herbCount; i++) {
var pos = getRandomPos();
var herb = new Nummer();
herb._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herb.x = pos.x;
herb.y = pos.y;
herb.scaleX = 1;
herb.scaleY = 1;
herb.alpha = 1;
// Start in wandering state when spawned by randomize button
herb.state = "wandering";
herb._wanderAfterSleepTimer = 600; // 10 seconds wandering
// --- Randomize herbivore personality ---
var personalities = ["normal", "curious", "grumpy"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% curious, 35% grumpy
if (rand < 0.3) {
herb.personality = "normal";
} else if (rand < 0.65) {
herb.personality = "curious";
} else {
herb.personality = "grumpy";
}
herbivores.push(herb);
game.addChild(herb);
}
// Add 3-5 pollinators
var pollCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < pollCount; i++) {
var pos = getRandomPos();
var poll = new Pollinator();
poll.x = pos.x;
poll.y = pos.y;
poll.scaleX = 1;
poll.scaleY = 1;
poll.alpha = 1;
pollinators.push(poll);
game.addChild(poll);
}
// Add 0-1 carnivores
var carnCount = Math.floor(Math.random() * 2); // 0 or 1
for (var i = 0; i < carnCount; i++) {
var pos = getRandomPos();
var carn = new Carnivore();
// --- Randomize carnivore personality ---
var carnPersonalities = ["normal", "bloodthirsty", "weak"];
var rand = Math.random();
// Make random traits more common: 30% normal, 35% bloodthirsty, 35% weak
if (rand < 0.3) {
carn.personality = "normal";
} else if (rand < 0.65) {
carn.personality = "bloodthirsty";
} else {
carn.personality = "weak";
}
// Adjust speed for bloodthirsty carnivores
if (carn.personality === "bloodthirsty") {
carn.speed = 1.5 + Math.random() * 1.2 + 0.7 + Math.random() * 0.5;
}
carn.x = pos.x;
carn.y = pos.y;
carn.scaleX = 1.25;
carn.scaleY = 1.25;
carn.alpha = 1;
carnivores.push(carn);
game.addChild(carn);
}
// Play plop sound for feedback
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
// --- Unlock skip cycle button when randomize is pressed ---
skipCycleBtn._skipLocked = false;
firstOrganismPlaced = true;
// --- Restart the cycle timer when randomize is pressed ---
if (cycleInterval !== null) {
LK.clearInterval(cycleInterval);
cycleInterval = null;
}
if (cycleCountdownInterval !== null) {
LK.clearInterval(cycleCountdownInterval);
cycleCountdownInterval = null;
}
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
startCycleInterval();
// --- Disable and color the randomize button red ---
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
// --- Disable randomize button if any organisms are present ---
if (plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0) {
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
}
};
// Asteroid extinction event handler
// --- Extinction Event Rate Limiting ---
// Track timestamps (in ms) of recent extinction events
if (typeof window._extinctionEventTimes === "undefined") window._extinctionEventTimes = [];
extinctionBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
LK.getSound('explosion').play();
// Asteroid visual: big circle in center of map
var asteroid = LK.getAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 400,
height: 400,
color: 0x888888,
shape: 'ellipse'
});
asteroid.x = 124 + 1800 / 2;
asteroid.y = paletteY + 320 + 2000 / 2;
asteroid.alpha = 0.0;
game.addChild(asteroid);
// Animate asteroid fade in, then out
tween(asteroid, {
alpha: 1
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
tween(asteroid, {
alpha: 0
}, {
duration: 700,
easing: tween.easeOut,
onFinish: function onFinish() {
asteroid.destroy();
}
});
}
});
// Flash screen for dramatic effect
LK.effects.flashScreen(0xff6600, 800);
// --- Extinction Event Rate Limiting Logic ---
// Add current timestamp to the event list
var now = Date.now();
window._extinctionEventTimes.push(now);
// Only keep events in the last 10 seconds (10000 ms)
var cutoff = now - 10000;
for (var i = window._extinctionEventTimes.length - 1; i >= 0; i--) {
if (window._extinctionEventTimes[i] < cutoff) {
window._extinctionEventTimes.splice(i, 1);
}
}
// If more than 3 extinction events in last 10 seconds, kill all creatures
if (window._extinctionEventTimes.length > 3) {
// Gather all organisms (plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites)
var allGroups = [plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites];
for (var g = 0; g < allGroups.length; g++) {
var arr = allGroups[g];
for (var i = arr.length - 1; i >= 0; i--) {
var org = arr[i];
arr.splice(i, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
}
// Optionally, flash the screen again for drama
LK.effects.flashScreen(0x000000, 1200);
// Clear the event times to prevent repeated wipes
window._extinctionEventTimes = [];
return;
}
// Gather all organisms (plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites)
var allGroups = [plants, herbivores, carnivores, pollinators, fungis, seeds, eggs, worms, parasites];
for (var g = 0; g < allGroups.length; g++) {
var arr = allGroups[g];
if (arr.length === 0) continue;
// If the group has 3 or fewer, kill 1 or 2 at random (never all)
if (arr.length <= 3) {
var toKill = Math.floor(Math.random() * arr.length); // 0, 1, or 2
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
} else if (arr.length % 2 !== 0) {
// If uneven, kill a random number between 1 and half the group (rounded down)
var maxKill = Math.floor(arr.length / 2);
var toKill = 1 + Math.floor(Math.random() * maxKill);
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
} else {
// Even and more than 3: kill half (rounded down), random selection
var survivors = Math.ceil(arr.length / 2);
var toKill = arr.length - survivors;
for (var i = 0; i < toKill; i++) {
if (arr.length === 0) break;
var idx = Math.floor(Math.random() * arr.length);
var org = arr[idx];
arr.splice(idx, 1);
if (org && typeof org.die === "function") {
org.die();
} else if (org && typeof org.destroy === "function") {
org.destroy();
}
}
}
}
};
// --- Prevent skipping cycle until first organism is placed ---
var firstOrganismPlaced = false;
skipCycleBtn._skipLocked = true; // Lock skipping at start
skipCycleBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
// --- Streak: Reset streak and play lost sound immediately when skip is pressed ---
streakCount = 0;
updateStreakText();
var lostSnd = LK.getSound('Lost');
if (lostSnd && typeof lostSnd.play === "function") lostSnd.play();
// --- Prevent skipping if all organisms are gone (plants, herbivores, carnivores, pollinators, fungis, parasites, worms) ---
var allGone = plants.length === 0 && herbivores.length === 0 && carnivores.length === 0 && pollinators.length === 0 && fungis.length === 0 && parasites.length === 0 && worms.length === 0;
if (allGone) {
// --- Streak: Reset streak if all organisms die on skip ---
resetStreak();
// Flash skip button red for 2 seconds
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 2000);
return;
}
if (skipCycleBtn._skipLocked) {
// Flash skip button red for 2 seconds
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
// Restore color after 2 seconds
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 2000);
return;
}
// --- 3 Second Cooldown Logic ---
if (skipCycleCooldownActive) {
// --- Streak: Reset streak if skip is attempted during cooldown (optional, but for safety) ---
resetStreak();
// Flash skip button red for 1 second if on cooldown
var originalColor = skipCycleBtnBox.color;
skipCycleBtnBox.color = 0xff4444;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#fff"
});
}
LK.setTimeout(function () {
skipCycleBtnBox.color = originalColor;
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
}, 1000);
return;
}
skipCycleCooldownActive = true;
skipCycleBtnBox.color = 0xaaaaaa;
skipCycleBtnTxt.setText("Cooldown");
// Show and start the white countdown
skipCycleCountdownValue = 3;
skipCycleCountdownTxt.setText("" + skipCycleCountdownValue);
skipCycleCountdownTxt.visible = true;
// Clear any previous interval
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
// Start interval to update countdown every 1s
skipCycleCountdownInterval = LK.setInterval(function () {
skipCycleCountdownValue--;
if (skipCycleCountdownValue > 0) {
skipCycleCountdownTxt.setText("" + skipCycleCountdownValue);
} else {
skipCycleCountdownTxt.visible = false;
skipCycleCountdownTxt.setText("");
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
}
}, 1000);
// Restore after 3 seconds
if (skipCycleCooldownTimeout) {
LK.clearTimeout(skipCycleCooldownTimeout);
}
skipCycleCooldownTimeout = LK.setTimeout(function () {
skipCycleCooldownActive = false;
skipCycleBtnBox.color = 0x3388cc;
skipCycleBtnTxt.setText("Skip Cycle");
if (typeof skipCycleBtnBox.setStyle === "function") {
skipCycleBtnBox.setStyle({
fill: "#000"
});
}
// Hide the countdown if still visible
skipCycleCountdownTxt.visible = false;
skipCycleCountdownTxt.setText("");
if (skipCycleCountdownInterval) {
LK.clearInterval(skipCycleCountdownInterval);
skipCycleCountdownInterval = null;
}
}, 3000);
// --- Streak: Mark that skip was used this cycle (will reset streak on next cycle) ---
streakSkipThisCycle = true;
// Increment cycle and update UI
cycleCount++;
cycleTxt.setText("Cycle: " + cycleCount);
cycleTimeLeftMs = cycleDurationMs;
cycleTimerTxt.setText("Next cycle: " + Math.ceil(cycleTimeLeftMs / 1000) + "s");
// --- Extinction: If no plants, kill all herbivores, pollinators, and carnivores ---
if (plants.length === 0) {
// Herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (typeof h.die === "function") h.die();else if (typeof h.destroy === "function") h.destroy();
herbivores.splice(i, 1);
}
// Pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (typeof p.die === "function") p.die();else if (typeof p.destroy === "function") p.destroy();
pollinators.splice(i, 1);
}
// Carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (typeof c.die === "function") c.die();else if (typeof c.destroy === "function") c.destroy();
carnivores.splice(i, 1);
}
}
// Set background color every 3 cycles: green, yellow, brown, white, repeat (spring, summer, autumn, winter)
var mod = cycleCount % 12;
if (mod === 0) {
// SPRING
game.setBackgroundColor(0x2ecc40); // green
updateTextColors(0x2ecc40);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = true;
window.winterState = false;
window.autumnState = false;
// Play mellow music
LK.playMusic('mellow');
// --- YEAR COUNTER: Increment year after winter ends (when entering spring) ---
if (cycleCount > 0) {
yearCount++;
if (yearTxt && typeof yearTxt.setText === "function") {
yearTxt.setText("Year: " + yearCount);
}
}
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
for (var i = 0; i < herbivores.length; i++) {
if (herbivores[i] && typeof herbivores[i].speed === "number") {
herbivores[i].speed = 1.2 + Math.random() * 1.0;
}
}
for (var i = 0; i < carnivores.length; i++) {
if (carnivores[i]) {
carnivores[i].state = "hungry";
carnivores[i]._tired = false;
carnivores[i]._tiredTimer = 0;
carnivores[i]._chaseTime = 0;
carnivores[i].target = null;
}
}
updateSeasonText();
} else if (mod === 3) {
// SUMMER
game.setBackgroundColor(0xffff00); // yellow
updateTextColors(0xffff00);
pollinatorsHibernating = false;
window.summerState = true;
window.winterState = false;
window.springState = false;
window.autumnState = false;
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0xffffff;
}
plants[i]._canDie = true;
}
updateSeasonText();
} else if (mod === 6) {
// AUTUMN
game.setBackgroundColor(0x8B5A2B); // brown
updateTextColors(0x8B5A2B);
pollinatorsHibernating = false;
window.summerState = false;
window.springState = false;
window.winterState = false;
window.autumnState = true;
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x996633; // brown
}
plants[i]._canDie = true;
plants[i]._isBrown = true;
}
updateSeasonText();
} else if (mod === 9) {
// WINTER
game.setBackgroundColor(0xffffff); // white
updateTextColors(0xffffff);
pollinatorsHibernating = true;
window.summerState = false;
window.springState = false;
window.winterState = true;
window.autumnState = false;
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
if (p && p._isBrown && !p.eaten && typeof p.die === "function") {
p.die();
}
}
for (var i = 0; i < plants.length; i++) {
if (plants[i] && plants[i].plantAsset && typeof plants[i].plantAsset.tint !== "undefined") {
plants[i].plantAsset.tint = 0x3399ff;
}
plants[i]._canDie = false;
}
updateSeasonText();
}
// Randomize all organism positions except fungi, plants, and lichen
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
// Helper to randomize array of organisms
function randomizeOrganisms(arr) {
for (var i = 0; i < arr.length; i++) {
var org = arr[i];
// If this is a host with a parasite attached, skip randomizing its position
if (org && typeof org.x === "number" && typeof org.y === "number" && !org.eaten && !org._parasiteAttached // Only randomize if not hosting a parasite
) {
org.x = minX + Math.random() * (maxX - minX);
org.y = minY + Math.random() * (maxY - minY);
}
}
}
// When randomizing parasites, only randomize those not attached to a host
function randomizeFreeParasites(arr) {
for (var i = 0; i < arr.length; i++) {
var parasite = arr[i];
if (parasite && typeof parasite.x === "number" && typeof parasite.y === "number" && !parasite.eaten && parasite.state !== "attached") {
parasite.x = minX + Math.random() * (maxX - minX);
parasite.y = minY + Math.random() * (maxY - minY);
}
}
}
randomizeOrganisms(herbivores);
randomizeOrganisms(carnivores);
randomizeOrganisms(pollinators);
randomizeOrganisms(seeds);
randomizeOrganisms(eggs);
randomizeOrganisms(worms);
randomizeFreeParasites(parasites);
// Fungi, plants, and lichen are NOT moved
// (fungis, plants, window.lichens)
};
// --- Patch createOrganism to unlock skip after first organism placed ---
var _originalCreateOrganism = createOrganism;
createOrganism = function createOrganism(type, x, y) {
var obj = _originalCreateOrganism(type, x, y);
if (!firstOrganismPlaced) {
firstOrganismPlaced = true;
skipCycleBtn._skipLocked = false;
}
return obj;
};
// --- Retract/Expand Widget for Top Buttons ---
// State for retraction
var topButtonsRetracted = false;
// Widget button (a small round button at the right of the top buttons)
var retractWidget = new Container();
var retractWidgetBox = retractWidget.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 90,
height: 90,
color: 0x888888,
shape: 'ellipse'
});
var retractWidgetTxt = new Text2("≡", {
size: 60,
fill: "#fff"
});
retractWidgetTxt.anchor.set(0.5, 0.5);
retractWidget.addChild(retractWidgetTxt);
// Place widget to the right of the top buttons
retractWidget.x = 300;
retractWidget.y = deleteBtn.y + 50;
retractWidget.interactive = true;
retractWidget.visible = true;
// Helper to update button visibility/position
function updateTopButtonsRetracted() {
if (topButtonsRetracted) {
// Move buttons offscreen right, hide text, and set visible to false
var offX = 2048 + 300;
tween(skipCycleBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
skipCycleBtn.visible = false;
}
});
tween(deleteBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
deleteBtn.visible = false;
}
});
tween(extinctionBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
extinctionBtn.visible = false;
}
});
// Hide randomize button as well
if (typeof randomizeBtn !== "undefined") {
tween(randomizeBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
randomizeBtn.visible = false;
}
});
}
// Hide personality button as well
if (typeof personalityBtn !== "undefined") {
tween(personalityBtn, {
x: offX
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
personalityBtn.visible = false;
}
});
}
retractWidgetTxt.setText("▶");
// Also move retractWidget to the right when retracted
tween(retractWidget, {
x: 700 //{Fe} // Move even further right
}, {
duration: 200,
easing: tween.easeIn
});
} else {
// Move buttons back to original positions and set visible to true
tween(retractWidget, {
x: 300
}, {
duration: 200,
easing: tween.easeOut
});
skipCycleBtn.visible = true;
deleteBtn.visible = true;
extinctionBtn.visible = true;
if (typeof randomizeBtn !== "undefined") {
randomizeBtn.visible = true;
tween(randomizeBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Show and move personality button back
if (typeof personalityBtn !== "undefined") {
personalityBtn.visible = true;
tween(personalityBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
}
tween(skipCycleBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
tween(deleteBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
tween(extinctionBtn, {
x: 500
}, {
duration: 200,
easing: tween.easeOut
});
retractWidgetTxt.setText("≡");
}
}
// Widget click toggles retraction
retractWidget.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
topButtonsRetracted = !topButtonsRetracted;
// Animate widget itself to the right when retracting, back to original when expanding
if (topButtonsRetracted) {
tween(retractWidget, {
x: 500
}, {
duration: 200,
easing: tween.easeIn
});
} else {
tween(retractWidget, {
x: 300
}, {
duration: 200,
easing: tween.easeOut
});
}
updateTopButtonsRetracted();
};
// Add widget to GUI
LK.gui.top.addChild(skipCycleBtn);
LK.gui.top.addChild(extinctionBtn);
LK.gui.top.addChild(randomizeBtn);
LK.gui.top.addChild(retractWidget);
// On game start, ensure buttons are expanded
updateTopButtonsRetracted();
// Shift the palette even further left so it is not offscreen
var paletteTotalWidth = paletteSpacing * 2;
var paletteXStart = 2048 / 2 - paletteTotalWidth / 2 - 1300;
// Create palette items for drag-and-drop
function createPaletteItem(type, x, y) {
var assetId, color, label;
if (type === 'plant') {
assetId = 'plant';
color = 0x6adf60;
label = 'Fern';
} else if (type === 'herbivore') {
assetId = 'herbivore';
color = 0xf7e26b;
label = 'Nummer';
} else if (type === 'pollinator') {
assetId = 'pollinator';
color = 0xffc300;
label = 'Pollinator';
} else if (type === 'fungi') {
assetId = 'fungi';
color = 0xbb88ff;
label = 'Shroom';
} else if (type === 'parasite') {
assetId = 'Parasite';
color = 0x8888ff;
label = 'Parasite';
} else if (type === 'worm') {
assetId = 'Worm';
color = 0x888888;
label = 'Worm';
} else {
assetId = 'carnivore';
color = 0xe74c3c;
label = 'Hunter';
}
;
// Update all eggs
for (var i = eggs.length - 1; i >= 0; i--) {
var e = eggs[i];
if (!e.parent || e.hatched) {
eggs.splice(i, 1);
continue;
}
if (e.update) e.update();
}
;
// Update all pollinators
for (var i = pollinators.length - 1; i >= 0; i--) {
var p = pollinators[i];
if (p.eaten) {
pollinators.splice(i, 1);
continue;
}
if (p.update) p.update();
}
var node = new Container();
var icon;
if (type === 'parasite') {
icon = node.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
} else {
icon = node.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
}
node.x = x;
node.y = y;
node.type = type;
// --- Population Counter Above Icon ---
var popCount = 0;
if (type === 'plant') popCount = plants.length;else if (type === 'herbivore') popCount = herbivores.length;else if (type === 'carnivore') popCount = carnivores.length;else if (type === 'pollinator') popCount = pollinators.length;else if (type === 'fungi') popCount = fungis.length;else if (type === 'parasite') popCount = parasites.length;
var popTxt = new Text2("" + popCount, {
size: 54,
fill: "#000"
});
popTxt.anchor.set(0.5, 1);
popTxt.y = -70;
node.addChild(popTxt);
node._popTxt = popTxt; // Store for update
// Add label
var txt = new Text2(label, {
size: 60,
fill: "#000"
});
txt.anchor.set(0.5, 0);
txt.y = 60;
node.addChild(txt);
// Add to GUI overlay (so it stays on top)
LK.gui.top.addChild(node);
paletteItems.push(node);
return node;
}
// --- Palette Scroll Bar ---
// All available organism types in palette
var paletteTypes = ['plant', 'herbivore', 'carnivore', 'pollinator', 'fungi', 'parasite', 'worm'];
var paletteScrollOffset = 0; // How much the palette is scrolled (in px)
var paletteScrollMin = 0;
var paletteScrollMax = Math.max(0, paletteTypes.length * paletteSpacing - paletteSpacing * 3); // Show 3 at a time
// Remove old palette items if any
for (var i = 0; i < paletteItems.length; i++) {
if (paletteItems[i].parent) paletteItems[i].parent.removeChild(paletteItems[i]);
}
paletteItems = [];
// Helper to render palette items based on scroll offset
function renderPaletteItems() {
// Remove all current palette items from GUI
for (var i = 0; i < paletteItems.length; i++) {
if (paletteItems[i].parent) paletteItems[i].parent.removeChild(paletteItems[i]);
}
paletteItems = [];
// Only show up to 3 at a time, centered
var visibleCount = 3;
var startIdx = Math.floor(paletteScrollOffset / paletteSpacing);
var offsetPx = paletteScrollOffset % paletteSpacing;
for (var i = 0; i < visibleCount; i++) {
var idx = startIdx + i;
if (idx >= 0 && idx < paletteTypes.length) {
var px = paletteXStart + i * paletteSpacing - offsetPx;
var node = createPaletteItem(paletteTypes[idx], px, paletteY);
paletteItems.push(node);
}
}
// Draw left/right scroll buttons
if (!window.paletteLeftBtn) {
window.paletteLeftBtn = new Container();
var leftIcon = window.paletteLeftBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 120,
color: 0x444444,
shape: 'box'
});
var leftArrow = new Text2("<", {
size: 80,
fill: "#000"
});
leftArrow.anchor.set(0.5, 0.5);
leftArrow.x = 0;
leftArrow.y = 0;
window.paletteLeftBtn.addChild(leftArrow);
window.paletteLeftBtn.x = paletteXStart - 120;
window.paletteLeftBtn.y = paletteY;
window.paletteLeftBtn.interactive = true;
LK.gui.top.addChild(window.paletteLeftBtn);
window.paletteLeftBtn.visible = false;
}
if (!window.paletteRightBtn) {
window.paletteRightBtn = new Container();
var rightIcon = window.paletteRightBtn.attachAsset('box', {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 120,
color: 0x444444,
shape: 'box'
});
var rightArrow = new Text2(">", {
size: 80,
fill: "#000"
});
rightArrow.anchor.set(0.5, 0.5);
rightArrow.x = 0;
rightArrow.y = 0;
window.paletteRightBtn.addChild(rightArrow);
window.paletteRightBtn.x = paletteXStart + paletteSpacing * visibleCount + 40;
window.paletteRightBtn.y = paletteY;
window.paletteRightBtn.interactive = true;
LK.gui.top.addChild(window.paletteRightBtn);
window.paletteRightBtn.visible = false;
}
// Show/hide buttons
window.paletteLeftBtn.visible = paletteScrollOffset > paletteScrollMin;
window.paletteRightBtn.visible = paletteScrollOffset < paletteScrollMax;
}
renderPaletteItems();
// --- Palette Scroll Button Handlers ---
window.paletteLeftBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (paletteScrollOffset > paletteScrollMin) {
paletteScrollOffset -= paletteSpacing;
if (paletteScrollOffset < paletteScrollMin) paletteScrollOffset = paletteScrollMin;
renderPaletteItems();
}
};
window.paletteRightBtn.down = function () {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (paletteScrollOffset < paletteScrollMax) {
paletteScrollOffset += paletteSpacing;
if (paletteScrollOffset > paletteScrollMax) paletteScrollOffset = paletteScrollMax;
renderPaletteItems();
}
};
// --- End Palette Scroll Bar ---
// --- Drag and Drop Logic ---
var dragging = null; // {type, node, paletteRef}
var dragOffsetX = 0,
dragOffsetY = 0;
// Helper: create organism at x,y
function createOrganism(type, x, y) {
var obj;
if (type === 'plant') {
obj = new Plant();
plants.push(obj);
// If background is white, make plant blue and undying
if (game.backgroundColor === 0xffffff) {
if (obj.plantAsset && typeof obj.plantAsset.tint !== "undefined") {
obj.plantAsset.tint = 0x3399ff;
}
obj._canDie = false;
}
// If placed in summer, make plant invulnerable for 10 seconds
if (game.backgroundColor === 0xffff00 || window.summerState) {
obj._invulnerable = true;
obj._canDie = false;
// Remove invulnerability after 10 seconds (600 frames)
LK.setTimeout(function () {
if (plants.indexOf(obj) !== -1) {
obj._invulnerable = false;
obj._canDie = true;
}
}, 10000);
}
} else if (type === 'herbivore') {
obj = new Nummer();
obj._uniqueId = "herb_" + Math.floor(Math.random() * 1e9) + "_" + Math.floor(Math.random() * 1e9);
herbivores.push(obj);
} else if (type === 'pollinator') {
obj = new Pollinator();
pollinators.push(obj);
} else if (type === 'fungi') {
obj = new Fungi();
// If fungi is spawned after the cycle has started, set its _bornCycle to the next cycle so it lives a full cycle
if (typeof cycleCount !== "undefined") {
obj._bornCycle = cycleCount + 1;
}
fungis.push(obj);
} else if (type === 'parasite') {
obj = new Parasite();
parasites.push(obj);
// If spawned in winter, kill parasite immediately
if (window.winterState) {
// Animate parasite dying and remove from array
tween(obj, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (typeof parasites !== "undefined") {
var idx = parasites.indexOf(obj);
if (idx !== -1) parasites.splice(idx, 1);
}
obj.destroy();
}
});
}
} else if (type === 'worm') {
obj = new Worm();
worms.push(obj);
// If spawned in autumn or spring, set buried state (handled in Worm class, but ensure correct state at spawn)
if ((window.autumnState || window.springState) && obj) {
obj.buried = true;
if (obj._wormAsset) {
obj.removeChild(obj._wormAsset);
}
obj._wormAsset = obj.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
// If spawned in the first 2 cycles, set buried state
if (typeof cycleCount !== "undefined" && cycleCount < 2 && obj) {
obj.buried = true;
if (obj._wormAsset) {
obj.removeChild(obj._wormAsset);
}
obj._wormAsset = obj.attachAsset('Buried_worm', {
anchorX: 0.5,
anchorY: 0.5
});
}
} else {
obj = new Carnivore();
carnivores.push(obj);
}
obj.x = x;
obj.y = y;
obj.scaleX = 1;
obj.scaleY = 1;
obj.alpha = 1;
game.addChild(obj);
// Play plop sound when placing an organism from the palette
var plopSnd = LK.getSound('Plop');
if (plopSnd && typeof plopSnd.play === "function") plopSnd.play();
// Start the cycle interval if not already started
if (typeof startCycleInterval === "function") startCycleInterval();
// If personality button is enabled (personality text nodes are present), show personality text for this organism
if (personalityTextNodes && personalityTextNodes.length > 0) {
// Helper to add personality text for a single organism
var addPersonalityTextForSingle = function addPersonalityTextForSingle(org, type, pers) {
var txt = new Text2(pers, {
size: 44,
fill: pers === "N/A" ? "#888" : "#2c3e50"
});
txt.anchor.set(0.5, 1);
txt.x = org.x;
txt.y = org.y - 70;
txt._personalityTarget = org;
txt._personalityType = type;
game.addChild(txt);
personalityTextNodes.push(txt);
}; // Determine type and personality string
var type = null,
pers = "N/A";
if (obj instanceof Plant) {
type = "Fern";
pers = "N/A";
} else if (obj instanceof Nummer) {
type = "Nummer";
pers = obj.personality || "N/A";
} else if (obj instanceof Carnivore) {
type = "Hunter";
pers = obj.personality || "N/A";
} else if (obj instanceof Pollinator) {
type = "Pollinator";
pers = obj.personality || "N/A";
} else if (obj instanceof Fungi) {
type = "Shroom";
pers = "N/A";
} else if (obj instanceof Parasite) {
type = "Parasite";
pers = "N/A";
} else if (obj instanceof Worm) {
type = "Worm";
pers = "N/A";
}
addPersonalityTextForSingle(obj, type, pers);
}
return obj;
}
// Helper: check if point is inside a palette item
function isInPalette(x, y, node) {
var bounds = {
x: node.x - 60,
y: node.y - 60,
w: 120,
h: 120
};
return x >= bounds.x && x <= bounds.x + bounds.w && y >= bounds.y && y <= bounds.y + bounds.h;
}
// --- Event Handlers ---
// Global button click sound handler: play click sound for any button on screen
if (typeof window._globalButtonClickHandlerAttached === "undefined") {
window._globalButtonClickHandlerAttached = true;
// Patch LK.gui.top's down event to play click sound for any button
var _originalGuiTopDown = LK.gui.top.down;
LK.gui.top.down = function (x, y, obj) {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (typeof _originalGuiTopDown === "function") _originalGuiTopDown.call(this, x, y, obj);
};
// Patch LK.gui.bottom's down event to play click sound for any button
var _originalGuiBottomDown = LK.gui.bottom.down;
LK.gui.bottom.down = function (x, y, obj) {
var clickSnd = LK.getSound('Click');
if (clickSnd && typeof clickSnd.play === "function") clickSnd.play();
if (typeof _originalGuiBottomDown === "function") _originalGuiBottomDown.call(this, x, y, obj);
};
}
// Down: start drag if on palette
game.down = function (x, y, obj) {
// If delete mode is ON, check if click is on an organism and delete it
if (deleteMode) {
// Include lichens in allOrganisms if present
var allOrganisms = plants.concat(herbivores, carnivores, pollinators, fungis, parasites, worms);
for (var i = allOrganisms.length - 1; i >= 0; i--) {
var org = allOrganisms[i];
var dx = x - org.x;
var dy = y - org.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var isFungi = fungis.indexOf(org) !== -1;
var canDelete = isFungi || !org.eaten;
if (canDelete && dist < 60) {
// Remove from correct array
if (plants.indexOf(org) !== -1) {
var idx = plants.indexOf(org);
if (idx !== -1) plants.splice(idx, 1);
} else if (herbivores.indexOf(org) !== -1) {
var idx = herbivores.indexOf(org);
if (idx !== -1) {
// Remove from hunger queue if present
var hqidx = herbivoreHungerQueue.indexOf(org);
if (hqidx !== -1) {
herbivoreHungerQueue.splice(hqidx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(idx, 1);
}
} else if (carnivores.indexOf(org) !== -1) {
var idx = carnivores.indexOf(org);
if (idx !== -1) carnivores.splice(idx, 1);
} else if (pollinators.indexOf(org) !== -1) {
var idx = pollinators.indexOf(org);
if (idx !== -1) pollinators.splice(idx, 1);
} else if (fungis.indexOf(org) !== -1) {
var idx = fungis.indexOf(org);
if (idx !== -1) fungis.splice(idx, 1);
}
if (typeof org.die === "function") {
org.die();
} else if (typeof org.destroy === "function") {
org.destroy();
}
return;
}
}
// If not on an organism, do nothing in delete mode
return;
}
// Check if touch/click is on a palette item
for (var i = 0; i < paletteItems.length; i++) {
var p = paletteItems[i];
// Convert GUI coordinates to game coordinates
var guiPos = LK.gui.top.toLocal({
x: x,
y: y
}, game);
if (isInPalette(guiPos.x, guiPos.y, p)) {
// Start dragging a new organism
dragging = {
type: p.type,
node: createOrganism(p.type, x, y),
paletteRef: p
};
dragOffsetX = 0;
dragOffsetY = 0;
// Make it semi-transparent while dragging
dragging.node.alpha = 0.7;
return;
}
}
// If not on palette, check if on an organism to move it
// Check if touch/click is on an organism to move it
var found = false;
var allOrganisms = plants.concat(herbivores, carnivores, pollinators, fungis, parasites, worms);
for (var i = allOrganisms.length - 1; i >= 0; i--) {
var org = allOrganisms[i];
// Only allow moving if not eaten and touch is within 60px radius of center
var dx = x - org.x;
var dy = y - org.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var isFungi = fungis.indexOf(org) !== -1;
var isParasite = parasites.indexOf(org) !== -1;
var canDrag = isFungi || isParasite || !org.eaten;
if (canDrag && dist < 60) {
var type;
if (plants.indexOf(org) !== -1) type = 'plant';else if (herbivores.indexOf(org) !== -1) type = 'herbivore';else if (carnivores.indexOf(org) !== -1) type = 'carnivore';else if (pollinators.indexOf(org) !== -1) type = 'pollinator';else if (fungis.indexOf(org) !== -1) type = 'fungi';else if (parasites.indexOf(org) !== -1) type = 'parasite';
// (UI label is handled in createPaletteItem, so no change needed here for type)
dragging = {
type: type,
node: org,
paletteRef: null
};
dragOffsetX = org.x - x;
dragOffsetY = org.y - y;
org.alpha = 0.7;
found = true;
break;
}
}
if (!found) {
dragging = null;
}
};
// Move: update drag position
game.move = function (x, y, obj) {
if (dragging && dragging.node) {
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
var newX = x + dragOffsetX;
var newY = y + dragOffsetY;
// Clamp to map area
if (newX < minX) newX = minX;
if (newX > maxX) newX = maxX;
if (newY < minY) newY = minY;
if (newY > maxY) newY = maxY;
dragging.node.x = newX;
dragging.node.y = newY;
}
};
// Up: drop organism if dragging
game.up = function (x, y, obj) {
if (dragging && dragging.node) {
// If dropped inside palette area, destroy (cancel)
var guiPos = LK.gui.top.toLocal({
x: x,
y: y
}, game);
var cancel = false;
for (var i = 0; i < paletteItems.length; i++) {
if (isInPalette(guiPos.x, guiPos.y, paletteItems[i])) {
cancel = true;
break;
}
}
if (cancel) {
// Remove organism
dragging.node.destroy();
if (dragging.type === 'plant') {
var idx = plants.indexOf(dragging.node);
if (idx !== -1) plants.splice(idx, 1);
} else if (dragging.type === 'herbivore') {
var idx = herbivores.indexOf(dragging.node);
if (idx !== -1) {
// Remove from hunger queue if present
var hqidx = herbivoreHungerQueue.indexOf(dragging.node);
if (hqidx !== -1) {
herbivoreHungerQueue.splice(hqidx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(idx, 1);
}
} else if (dragging.type === 'carnivore') {
var idx = carnivores.indexOf(dragging.node);
if (idx !== -1) carnivores.splice(idx, 1);
} else if (dragging.type === 'pollinator') {
var idx = pollinators.indexOf(dragging.node);
if (idx !== -1) pollinators.splice(idx, 1);
} else if (dragging.type === 'fungi') {
var idx = fungis.indexOf(dragging.node);
if (idx !== -1) fungis.splice(idx, 1);
} else if (dragging.type === 'parasite') {
var idx = parasites.indexOf(dragging.node);
if (idx !== -1) parasites.splice(idx, 1);
} else if (dragging.type === 'worm') {
var idx = worms.indexOf(dragging.node);
if (idx !== -1) worms.splice(idx, 1);
}
} else {
// Place organism: snap alpha to 1
dragging.node.alpha = 1;
// If dropping a parasite, set state to 'seek' and detach from host if needed
if (dragging.type === 'parasite') {
// If attached, detach
if (dragging.node.state === "attached") {
if (dragging.node.attachedTo && dragging.node.attachedTo._parasiteAttached === dragging.node) {
dragging.node.attachedTo._parasiteAttached = null;
}
dragging.node.attachedTo = null;
}
dragging.node.state = "seek";
dragging.node.target = null;
}
}
dragging = null;
}
};
// --- Main Update Loop ---
game.update = function () {
// Update all herbivores
for (var i = herbivores.length - 1; i >= 0; i--) {
var h = herbivores[i];
if (h.eaten) {
// Remove from hunger queue if present
var idx = herbivoreHungerQueue.indexOf(h);
if (idx !== -1) {
herbivoreHungerQueue.splice(idx, 1);
// Next in queue (if any) becomes hungry
for (var j = 0; j < herbivoreHungerQueue.length; j++) {
var nextH = herbivoreHungerQueue[j];
if (nextH && nextH.state === "waiting") {
nextH.state = "hungry";
nextH._hungerQueueState = "hungry";
break;
}
}
}
herbivores.splice(i, 1);
continue;
}
if (h.update) h.update();
}
// Update all carnivores
for (var i = carnivores.length - 1; i >= 0; i--) {
var c = carnivores[i];
if (c.eaten) {
carnivores.splice(i, 1);
continue;
}
if (c.update) c.update();
}
// Update all worms
for (var i = worms.length - 1; i >= 0; i--) {
var w = worms[i];
if (w.update) w.update();
}
// In summer, randomly turn some healthy plants brown
if (window.summerState) {
for (var i = 0; i < plants.length; i++) {
var p = plants[i];
// Only affect healthy, non-brown, non-eaten plants
if (!p.eaten && !p._isBrown) {
// Smaller chance per frame (e.g. 0.0005 = ~0.05% per frame)
if (Math.random() < 0.0005) {
p._isBrown = true;
if (p.plantAsset && typeof p.plantAsset.tint !== "undefined") {
p.plantAsset.tint = 0x996633; // brown
}
}
}
}
}
// Clean up dead plants
for (var i = plants.length - 1; i >= 0; i--) {
var p = plants[i];
if (p.eaten) {
plants.splice(i, 1);
}
}
// Update all seeds
for (var i = seeds.length - 1; i >= 0; i--) {
var s = seeds[i];
if (s.eaten) {
seeds.splice(i, 1);
continue;
}
if (s.update) s.update();
}
;
// Update all fungi
for (var i = fungis.length - 1; i >= 0; i--) {
var f = fungis[i];
if (f.update) f.update();
}
// Update all fungi spores
for (var i = fungiSpores.length - 1; i >= 0; i--) {
var s = fungiSpores[i];
if (!s.parent || !s._alive) {
fungiSpores.splice(i, 1);
continue;
}
if (s.update) s.update();
}
// Update all parasites
for (var i = parasites.length - 1; i >= 0; i--) {
var b = parasites[i];
if (b.update) b.update();
}
// --- Update palette population counters ---
for (var i = 0; i < paletteItems.length; i++) {
var node = paletteItems[i];
if (!node._popTxt) continue;
var type = node.type;
var popCount = 0;
var maxCount = null;
if (type === 'plant') {
popCount = plants.length;
maxCount = 20;
} else if (type === 'herbivore') {
popCount = herbivores.length;
maxCount = 6;
} else if (type === 'carnivore') {
popCount = carnivores.length;
maxCount = 4;
} else if (type === 'pollinator') {
popCount = pollinators.length;
maxCount = 10;
} else if (type === 'fungi') {
popCount = fungis.length;
} else if (type === 'parasite') {
popCount = parasites.length;
maxCount = 4;
} else if (type === 'worm') {
popCount = worms.length;
}
node._popTxt.setText("" + popCount);
// Set counter color to red if at population control max, else black
if (type === 'herbivore' && popCount >= 6 || type === 'pollinator' && popCount >= 10 || type === 'plant' && popCount >= 20 || type === 'carnivore' && popCount >= 4 || type === 'parasite' && popCount >= 4) {
if (typeof node._popTxt.setStyle === "function") node._popTxt.setStyle({
fill: "#c00"
});
} else {
if (typeof node._popTxt.setStyle === "function") node._popTxt.setStyle({
fill: "#000"
});
}
}
// --- Continuously keep randomize button disabled if organisms are present ---
if (plants.length > 0 || herbivores.length > 0 || carnivores.length > 0 || pollinators.length > 0 || fungis.length > 0 || parasites.length > 0 || worms.length > 0) {
randomizeBtn._disabled = true;
randomizeBtnBox.color = 0xcc2222;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomized");
} else {
randomizeBtn._disabled = false;
randomizeBtnBox.color = 0x228B22;
if (typeof randomizeBtnBox.setStyle === "function") {
randomizeBtnBox.setStyle({
fill: "#fff"
});
}
randomizeBtnTxt.setText("Randomize");
}
};
// --- Instructions Text ---
// --- Random seed spawning around the map if there are any plants ---
var randomSeedSpawnTimer = null;
function startRandomSeedSpawning() {
if (randomSeedSpawnTimer !== null) return;
randomSeedSpawnTimer = LK.setInterval(function () {
// Only spawn if there are any plants
if (plants.length > 0) {
// 40% chance per interval to spawn a seed
if (Math.random() < 0.4) {
// Pick a random position inside the map area
var minX = 124 + 60;
var maxX = 124 + 1800 - 60;
var minY = paletteY + 320 + 60;
var maxY = paletteY + 320 + 2000 - 60;
var sx = minX + Math.random() * (maxX - minX);
var sy = minY + Math.random() * (maxY - minY);
var seed = new Seed();
seed.x = sx;
seed.y = sy;
seed.scaleX = 0.4;
seed.scaleY = 0.4;
seed.alpha = 1;
seeds.push(seed);
game.addChild(seed);
}
}
// If no plants, stop spawning
if (plants.length === 0 && randomSeedSpawnTimer !== null) {
LK.clearInterval(randomSeedSpawnTimer);
randomSeedSpawnTimer = null;
}
}, 120); // Try every 2 seconds
}
// Start random seed spawning at game start
startRandomSeedSpawning();
LK.playMusic('mellow', {
loop: true
});
// --- Map Area Border (for visual feedback) ---
// Use a simple colored rectangle instead of a plant asset for the map border background
var mapBorder = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800,
height: 2000,
color: 0x2e8b57,
shape: 'box'
});
mapBorder.alpha = 0.12;
game.addChild(mapBorder);
mapBorder.x = 124;
mapBorder.y = paletteY + 320;
// --- Add invisible border walls to prevent creatures from exiting the map area ---
var borderThickness = 40;
var borderAlpha = 0; // fully invisible
// Left border
var borderLeft = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: borderThickness,
height: 2000,
color: 0x000000,
shape: 'box'
});
borderLeft.alpha = borderAlpha;
game.addChild(borderLeft);
borderLeft.x = 124 - borderThickness;
borderLeft.y = paletteY + 320;
// Right border
var borderRight = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: borderThickness,
height: 2000,
color: 0x000000,
shape: 'box'
});
borderRight.alpha = borderAlpha;
game.addChild(borderRight);
borderRight.x = 124 + 1800;
borderRight.y = paletteY + 320;
// Top border
var borderTop = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800 + borderThickness * 2,
height: borderThickness,
color: 0x000000,
shape: 'box'
});
borderTop.alpha = borderAlpha;
game.addChild(borderTop);
borderTop.x = 124 - borderThickness;
borderTop.y = paletteY + 320 - borderThickness;
// Bottom border
var borderBottom = LK.getAsset('box', {
anchorX: 0,
anchorY: 0,
width: 1800 + borderThickness * 2,
height: borderThickness,
color: 0x000000,
shape: 'box'
});
borderBottom.alpha = borderAlpha;
game.addChild(borderBottom);
borderBottom.x = 124 - borderThickness;
borderBottom.y = paletteY + 320 + 2000;
// --- End of Game Code ---;
tentacled red circle with a mouth and no eyes. In-Game asset. 2d. High contrast. No shadows. Very simple
A plant. In-Game asset. 2d. High contrast. No shadows. Very simple
Brown Seed. In-Game asset. 2d. High contrast. No shadows. Very simple
A small orange circle with spider legs 1 cute eye and no mouth. In-Game asset. 2d. High contrast. No shadows. Very very simple
Make a red egg with dark red spots. In-Game asset. 2d. High contrast. No shadows
Purple mushroom. In-Game asset. 2d. High contrast. No shadows. Very simple
A green shiny orb with a black circle. In-Game asset. 2d. High contrast. No shadows. Very simple
make a cyan lichen. In-Game asset. 2d. High contrast. No shadows. very simple
Just the color blue filling the entire space. In-Game asset. 2d. High contrast. No shadows
A brown circle with bug like antennas and bug legs with bug jaws and no eyes. In-Game asset. 2d. High contrast. No shadows. Very simple
A venus flytrap with one mouth and is looking up side view. In-Game asset. 2d. High contrast. No shadows VERY SIMPLE
Short brown worm. In-Game asset. 2d. High contrast. No shadows. Very simple
Pile of dirt. In-Game asset. 2d. High contrast. No shadows. Very simple
A yellow circle with 3 eyes no mouth and the eyes are in a triangle orientation. In-Game asset. 2d. High contrast. No shadows
Shrub. In-Game asset. 2d. High contrast. No shadows. Very simple