Skip to content
Closed
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
239 changes: 122 additions & 117 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>CIWS Command</title>
<title>Cervix Command</title>
<link rel="stylesheet" href="style.css" />
<!-- Resolve bare "three" + addon imports to the locally vendored copy. -->
<script type="importmap">
Expand Down
53 changes: 28 additions & 25 deletions js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,16 +395,19 @@ export const CONFIG = {
},

// --- Visual / palette (CSS color strings; THREE.Color accepts these) ----
// Themed as the warm, fleshy interior of the reproductive tract: deep red
// walls, golden ova, pale-cream sperm, acid-green and immune defenses. The
// exotic sperm variants keep distinct hues so they stay readable at a glance.
colors: {
sky: ['#0a1428', '#05070d'],
ground: '#0d1320',
groundLine: '#2c5a8f',
city: '#37e0d8',
cityDead: '#2a3340',
turret: '#8893a4',
turretActive: '#ffb347',
barrel: '#c2ccda',
missile: '#ff5a4d',
sky: ['#3a0d20', '#160206'], // soft tissue depth — lit pink fading to dark
ground: '#2a0a16', // the vaginal wall floor
groundLine: '#d05a86', // pink membrane horizon
city: '#ffd86b', // a healthy golden ovum
cityDead: '#4a3a2a', // a spent / ruptured ovum
turret: '#e08fb0', // cervix tissue
turretActive: '#ff5fa0',
barrel: '#f4c6d8',
missile: '#eaf0f4', // a plain sperm — pale translucent cream
missileEvasive: '#c264ff',
missileHypersonic: '#ff7a1f',
missileMirv: '#9bff42',
Expand All @@ -414,28 +417,28 @@ export const CONFIG = {
missileGlidebomb: '#cbb878',
missileDrone: '#c9d4df',
missileNuke: '#ff2438',
flare: '#ffce6e',
missileTrail: '#ff5a4d',
laser: '#ff4df0',
bullet: '#ffe98a',
flare: '#ffce6e', // ejected decoy proteins
missileTrail: '#dfe7ee', // the lashing sperm tail
laser: '#9bff6a', // corrosive acid beam
bullet: '#d8f0e0', // a glob of cervical mucus
hitSpark: '#ffffff',
smoke: '#3a4252',
rocketSmoke: '#e6ebf2',
smoke: '#3a2230', // fleshy haze
rocketSmoke: '#f0e6ec',
fireball: '#ffd9a0',
interceptor: '#7cc6ff',
interceptorTrail: '#7cc6ff',
interceptorBlast: '#9be7ff',
interceptor: '#cfe8a0', // antibody / leukocyte
interceptorTrail: '#cfe8a0',
interceptorBlast: '#d8ffb0',
lock: '#86f7ff',
shield: '#6cf0ff',
explosion: '#ff5a4d', // standard-RV warhead pop — reads red, like the missile
shield: '#bfffd8', // translucent mucus plug
explosion: '#ff5a4d', // a sperm bursting — reads red against the cream body
groundExplosion: '#ff5a4d',
crosshair: '#ffe98a',
crosshairEmpty: '#ff5a4d',
credits: '#ffd86b',
shopPanel: 'rgba(14,22,38,0.92)',
shopRow: 'rgba(40,58,86,0.55)',
shopRowHover: 'rgba(80,120,170,0.7)',
hud: '#bcd2e6',
hudDim: '#5a6b7d',
shopPanel: 'rgba(30,8,18,0.92)',
shopRow: 'rgba(80,28,48,0.55)',
shopRowHover: 'rgba(150,60,100,0.7)',
hud: '#f2cdda',
hudDim: '#7d5a68',
},
};
86 changes: 57 additions & 29 deletions js/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -767,8 +767,8 @@ export class Game {
}

/**
* Nuke: the launch is DETECTED a few seconds before the warhead appears —
* klaxon plus a synthetic "Nuclear launch detected" voice — then it spawns.
* Mutant: the threat is DETECTED a few seconds before it appears —
* klaxon plus a synthetic "Mutant swimmer detected" voice — then it spawns.
*/
spawnNuke() {
this.nukesSpawned++;
Expand Down Expand Up @@ -1624,20 +1624,19 @@ export class Game {
const fade = 1 - (d2 / hoverR2) ** 2; // soft edge instead of popping
const p = this.renderer.worldToScreen(m.x, m.y);
const h = half[m.type] ?? 11;
const arm = Math.max(3, h * 0.45); // corner bracket arm length
const hot = m.type === 'nuke';
const col = hot ? C.crosshairEmpty : C.lock;
ctx.globalAlpha = (hot ? 0.9 : 0.55) * fade;
const tagAlpha = (hot ? 0.9 : 0.55) * fade;
ctx.strokeStyle = col;
for (const [sx, sy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {
const cx = p.x + sx * h;
const cy = p.y + sy * h;
ctx.beginPath();
ctx.moveTo(cx, cy + arm * -sy);
ctx.lineTo(cx, cy);
ctx.lineTo(cx + arm * -sx, cy);
ctx.stroke();
}
ctx.lineWidth = hot ? 1.6 : 1.2;
// Cell-membrane reticle: a softly wobbling amoeboid ring around the
// track with a fainter inner iris — a microscope picking out a cell.
const ph = this.time * 0.9 + (m.id || 0) * 0.7;
ctx.globalAlpha = tagAlpha;
this._membraneRing(ctx, p.x, p.y, h * 1.15, h * 0.14, 7, ph);
ctx.globalAlpha = tagAlpha * 0.45;
this._membraneRing(ctx, p.x, p.y, h * 0.6, h * 0.1, 6, -ph * 1.4);
ctx.globalAlpha = tagAlpha;
// De-clutter: a tag that would overprint one already on screen (e.g. a
// glide bomb just off its bomber's rack) keeps its brackets but stays
// silent — the display only labels what it can write legibly.
Expand Down Expand Up @@ -1754,6 +1753,22 @@ export class Game {
ctx.restore();
}

/** A softly wobbling closed ring — an amoeboid cell-membrane outline. */
_membraneRing(ctx, cx, cy, r, amp, lobes, phase) {
const N = 40;
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const a = (i / N) * TAU;
const rr = r + amp * Math.sin(lobes * a + phase);
const px = cx + Math.cos(a) * rr;
const py = cy + Math.sin(a) * rr;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();
}

drawCrosshair(ctx) {
const t = this.activeTurret;
// Red only when the gun mount is gone (nothing left to aim) during play;
Expand All @@ -1773,21 +1788,34 @@ export class Game {
}
ctx.save();
ctx.strokeStyle = col;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(x, y, 11, 0, TAU);
ctx.stroke();
ctx.beginPath();
for (const [dx, dy] of [
[-16, 0],
[16, 0],
[0, -16],
[0, 16],
]) {
ctx.moveTo(x + Math.sign(dx) * 6, y + Math.sign(dy) * 6);
ctx.lineTo(x + dx, y + dy);
ctx.lineWidth = 1.6;
// The aim marker is a living cell: a wobbling membrane ring, a few flagella
// flicking outward, and a bright nucleus pinned to the exact aim point.
const ph = this.time * 1.4;
this._membraneRing(ctx, x, y, 10, 1.6, 6, ph);
for (let k = 0; k < 4; k++) {
const a = (k / 4) * TAU + 0.15 * Math.sin(ph + k);
const ux = Math.cos(a);
const uy = Math.sin(a);
const perpx = -uy;
const perpy = ux;
const r0 = 11;
const r1 = 19;
const bow = 3 * Math.sin(ph * 1.6 + k * 1.7); // the hair lashes
ctx.beginPath();
ctx.moveTo(x + ux * r0, y + uy * r0);
ctx.quadraticCurveTo(
x + ux * ((r0 + r1) / 2) + perpx * bow,
y + uy * ((r0 + r1) / 2) + perpy * bow,
x + ux * r1,
y + uy * r1
);
ctx.stroke();
}
ctx.stroke();
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, 1.8, 0, TAU);
ctx.fill();
ctx.restore();
}

Expand All @@ -1801,7 +1829,7 @@ export class Game {
weight = 'normal',
glow = false,
} = opts;
ctx.font = `${weight} ${size}px "Courier New", monospace`;
ctx.font = `${weight} ${size}px "Trebuchet MS", "Avenir Next", "Segoe UI", system-ui, sans-serif`;
ctx.fillStyle = color;
ctx.textAlign = align;
ctx.textBaseline = baseline;
Expand Down Expand Up @@ -2384,7 +2412,7 @@ export class Game {

/** Greedy word-wrap using real text metrics at the given font size. */
_wrapText(ctx, str, maxW, size) {
ctx.font = `normal ${size}px "Courier New", monospace`;
ctx.font = `normal ${size}px "Trebuchet MS", "Avenir Next", "Segoe UI", system-ui, sans-serif`;
const lines = [];
let line = '';
for (const word of str.split(/\s+/)) {
Expand Down
Loading
Loading