Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/scripts/symbol-rain.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// Spawn rates
spawnRate: 0.5,
burstSpawnRate: 0.15,
symbolsPerWave: 7,
symbolsPerWave: 14,
waveInterval: 80,
guaranteedSpawnInterval: 5000,
// Face reveal
Expand All @@ -37,7 +37,7 @@
// Layout
columnWidth: 50,
gridCellSize: 100,
poolSize: 30,
poolSize: 60,
Comment on lines 30 to +40

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doubling both symbolsPerWave (7→14) and poolSize (30→60) quadruples the total potential symbol pool while only doubling the spawning rate. This could lead to memory/DOM bloat if the animation cleanup doesn't keep pace, especially on lower-end devices. Consider monitoring DOM node count in the performance budget or implementing a more aggressive pool recycling strategy to prevent accumulation of inactive symbol elements.

Copilot uses AI. Check for mistakes.
// Desktop collision (pixels)
desktopSymbolHeight: 30,
desktopSymbolWidth: 30,
Expand Down
4 changes: 3 additions & 1 deletion src/scripts/worm-movement-navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ console.log("🐛 Worm movement navigation loading...");
return false;
}

const symbolsToSearch = this.getCachedAllSymbols();
const symbolsToSearch = worm.isPurple
? this.getCachedAllSymbols()
: this.getCachedRevealedSymbols();
Comment on lines +52 to +54

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purple worms can still search getCachedAllSymbols() during rush targeting (lines 52-54), but the actual steal logic in worm-system.behavior.js now restricts them to revealed-symbol only. This creates an inconsistency: purple worms will rush toward hidden symbols they detect via getCachedAllSymbols(), but then fail to steal them when they arrive because stealSymbol() filters for revealed-symbol only. This wastes movement and creates confusing behavior. Either purple worms should search only revealed symbols in both places, or they should be allowed to steal all symbols in both places.

Suggested change
const symbolsToSearch = worm.isPurple
? this.getCachedAllSymbols()
: this.getCachedRevealedSymbols();
// Purple worms should only rush toward symbols they can actually steal.
// Steal logic is restricted to revealed symbols, so we target only revealed.
const symbolsToSearch = this.getCachedRevealedSymbols();

Copilot uses AI. Check for mistakes.

const targetElement = this._resolveTargetElement(worm, symbolsToSearch);

Expand Down
43 changes: 11 additions & 32 deletions src/scripts/worm-system.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
return;
}

// FIX: Purple worms need access to ALL symbols (including hidden), not just revealed
const symbolsSource = this.getCachedAllSymbols();
const symbolsSource = this.getCachedRevealedSymbols();

// Get all available symbols (not stolen, not spaces, not completed)
const allAvailableSymbols = Array.from(symbolsSource).filter(
Expand All @@ -42,37 +41,22 @@
!el.classList.contains("completed-row-symbol"),
);

// PURPLE WORM LOGIC: Only steal blue symbols when NO red symbols available
// PURPLE WORM LOGIC: can only steal symbols currently visible to the user
let availableSymbols;
if (worm.canStealBlue && worm.isPurple) {
// First, try to get red (hidden) symbols only
const redSymbols = allAvailableSymbols.filter((el) =>
el.classList.contains("hidden-symbol"),
availableSymbols = allAvailableSymbols.filter((el) =>
el.classList.contains("revealed-symbol"),
);
console.log(
`🟣 PURPLE WORM - ${availableSymbols.length} revealed symbols available`,
);

if (redSymbols.length > 0) {
// Red symbols available - purple worm steals red symbols like normal
availableSymbols = redSymbols;
console.log(
`🟣 PURPLE WORM - ${redSymbols.length} red symbols available (preferring red)`,
);
} else {
// NO red symbols - now purple worm can steal blue symbols!
const blueSymbols = allAvailableSymbols.filter((el) =>
el.classList.contains("revealed-symbol"),
);
availableSymbols = blueSymbols;
console.log(
`🟣 PURPLE WORM - NO red symbols! Stealing blue symbols (${blueSymbols.length} available)`,
);
}
} else {
// Normal worm - only steal red (hidden) symbols
// All non-purple steal attempts are restricted to currently revealed symbols
availableSymbols = allAvailableSymbols.filter((el) =>
el.classList.contains("hidden-symbol"),
el.classList.contains("revealed-symbol"),
);
console.log(
`🐛 Normal worm - ${availableSymbols.length} red symbols available`,
`🐛 Normal worm - ${availableSymbols.length} revealed symbols available`,
);
Comment on lines +44 to 60

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment and code logic now contradict the PR description. The PR states "purple worm moves slower, steals visible symbols" but lines 47-52 only filter for revealed-symbol class, which is identical to the non-purple worm logic on lines 55-60. This means purple worms and normal worms now have the same symbol targeting behavior, eliminating any special purple worm steal mechanics. If purple worms are supposed to have unique targeting (like the old red-symbol preference), this logic needs to be different from the normal worm path.

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -178,18 +162,13 @@
));

if (worm.isPurple && worm.canStealBlue) {
const redSymbols = allAvailableSymbols.filter((el) =>
el.classList.contains("hidden-symbol"),
);
if (redSymbols.length > 0) return redSymbols;

return allAvailableSymbols.filter((el) =>
el.classList.contains("revealed-symbol"),
);
}

return allAvailableSymbols.filter((el) =>
el.classList.contains("hidden-symbol"),
el.classList.contains("revealed-symbol"),
);
Comment on lines 164 to 172

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _getAvailableSymbolsForWorm method now returns identical symbol sets for both purple and non-purple worms (both filter for revealed-symbol class). This duplicates the filtering logic and makes the purple worm conditional branch pointless. Consider consolidating this into a single return statement since both paths are now identical.

Copilot uses AI. Check for mistakes.
};

Expand Down
2 changes: 2 additions & 0 deletions src/scripts/worm-system.effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,11 @@
proto.createSlimeSplat = function(x, y) {
const splat = document.createElement("div");
splat.className = "slime-splat";
splat.textContent = "🫟";

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS now uses ::before pseudo-element to add "🫟" glyph (line 118-122 in worm-effects.core.css), but the JS explicitly sets textContent to "🫟" as well. This creates a duplicate emoji - one from textContent and one from ::before. Either remove the ::before rule or remove the textContent assignment here to avoid displaying two splat emojis.

Suggested change
splat.textContent = "🫟";

Copilot uses AI. Check for mistakes.
splat.style.left = `${x}px`;
splat.style.top = `${y}px`;
splat.style.position = "fixed"; // Use fixed positioning to place at exact coordinates
splat.style.zIndex = "10002";

// Random rotation for variation
splat.style.transform = `translate(-50%, -50%) rotate(${Math.random() *
Expand Down
36 changes: 4 additions & 32 deletions src/scripts/worm-system.events.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,41 +108,13 @@

const normalizedWormSymbol = normalizeSymbol(worm.stolenSymbol);

if (normalizedWormSymbol === normalizedClicked) {
// PURPLE WORM: Turn green when matching symbol clicked (must click worm to destroy)
if (normalizedWormSymbol === normalizedClicked) {
// PURPLE WORM: matching rain symbol is the only valid kill path
if (worm.isPurple) {
console.log(
`🟣→🟢 User clicked rain symbol "${clickedSymbol}" - Purple worm ${worm.id} turns GREEN!`,
`💥 User clicked matching rain symbol "${clickedSymbol}" - EXPLODING purple worm ${worm.id}!`,
);

// Turn worm green (damaged state)
worm.element.style.filter = "hue-rotate(120deg) brightness(1.2)"; // Purple → Green
worm.element.classList.remove("purple-worm");
worm.element.classList.add("worm-damaged", "purple-turned-green");
worm.isPurple = false; // No longer purple
worm.canBeClicked = true; // Now clickable for destruction

// Flash effect
worm.element.style.animation = "worm-flash-green 0.5s ease-out";
setTimeout(() => {
worm.element.style.animation = "";
}, 500);

// Update click handler to explode instead of clone
worm.element.removeEventListener("click", worm.clickHandler);
worm.clickHandler = (e) => {
e.stopPropagation();
console.log(
`💥 Green (was purple) worm ${worm.id} clicked - EXPLODING!`,
);

// Drop power-up when purple worm (now green) is destroyed
this.dropPowerUp(worm.x, worm.y);

this.explodeWorm(worm, false);
};
worm.element.addEventListener("click", worm.clickHandler);

this.explodeWorm(worm, true);
return;
}

Expand Down
3 changes: 1 addition & 2 deletions src/scripts/worm-system.interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
worm.element.style.animation = "";
}, 500);

// FIX: Explode original purple worm AND clone it
this.explodeWorm(worm, false);
// Purple worms should not die on direct click; click is a clone penalty.
this.clonePurpleWorm(worm);
};

Expand Down
2 changes: 1 addition & 1 deletion src/scripts/worm-system.spawn.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
)}, ${startY.toFixed(0)}). Total worms: ${this.worms.length}`,
);
console.log(
`🟣 Purple worm moves slower, prioritizes RED symbols, and CLONES on click!`,
`🟣 Purple worm moves slower, steals visible symbols, and CLONES on click!`,
);

// Start animation loop if not already running
Expand Down
6 changes: 3 additions & 3 deletions src/styles/css/score-timer.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@
}

.hud-label {
font-size: 12px;
font-size: 8px;
letter-spacing: 2px;
opacity: 0.9;
}

.hud-value {
font-size: 28px;
font-size: 20px;
Comment on lines +60 to +66

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reducing HUD font size by ~30% (12px→8px for labels, 28px→20px for values) may create accessibility issues for users with visual impairments or on smaller screens. Consider testing against WCAG 2.1 minimum font size guidelines (typically 14px for body text). If these are decorative/secondary HUD elements, ensure critical game state is still visible at a comfortable size.

Copilot uses AI. Check for mistakes.
font-weight: 900;
line-height: 1.05;
}
Expand Down Expand Up @@ -158,7 +158,7 @@
}

.hud-value {
font-size: 22px;
font-size: 15px;
}
}

Expand Down
28 changes: 28 additions & 0 deletions tests/worm-behavior.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,32 @@ test.describe("Worm behavior: aggression, targeting, and click rules", () => {

expect(afterSecondClick).toBeFalsy();
});

test("purple worm click clones instead of dying", async ({ page }) => {
await page.evaluate(() => {
document.dispatchEvent(new CustomEvent("purpleWormTriggered"));
});

await page.waitForFunction(
() => window.wormSystem?.worms.some((w) => w.active && w.isPurple),
);

const beforeClickCount = await page.evaluate(
() => window.wormSystem.worms.filter((w) => w.active && w.isPurple).length,
);

const purpleWorm = page.locator(".worm-container.purple-worm").first();
await purpleWorm.click({ force: true });

await page.waitForTimeout(400);

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses page.waitForTimeout(400) for synchronization, which is a brittle timing-based wait. If worm cloning animation or processing takes longer than 400ms (for example, on slower CI machines), the test could flake. Consider using waitForFunction with a condition that checks the actual purple worm count has increased, or adding a deterministic event that signals when cloning is complete.

Suggested change
await page.waitForTimeout(400);
await page.waitForFunction(
(previousCount) =>
window.wormSystem?.worms.filter((w) => w.active && w.isPurple).length >
previousCount,
beforeClickCount,
);

Copilot uses AI. Check for mistakes.

const afterClickState = await page.evaluate(() => ({
purpleCount: window.wormSystem.worms.filter((w) => w.active && w.isPurple)
.length,
totalActive: window.wormSystem.worms.filter((w) => w.active).length,
}));

expect(afterClickState.purpleCount).toBeGreaterThan(beforeClickCount);
expect(afterClickState.totalActive).toBeGreaterThanOrEqual(2);
});
});