Worms
inkling · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 42,340 bytes · SHA-256 47b014db58ec
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worms: Artillery Arena — Pixel Warfare</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Orbitron:wght@400;700;900&family=VT323&display=swap');
:root{
--bg-deep:#0b0c15;
--bg-card:rgba(255,255,255,0.05);
--accent:#ffcc00;
--accent-2:#00e5ff;
--accent-3:#ff4444;
--text:#f0f0f0;
--text-dim:#aaa;
--font-display:'Bangers',system-ui;
--font-mono:'VT323',monospace;
--font-tech:'Orbitron',sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box;user-select:none;}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);font-family:var(--font-display);color:var(--text);}
/* Background ambient gradient */
body::before{
content:'';position:fixed;inset:0;pointer-events:none;
background:radial-gradient(ellipse at 50% 40%, rgba(255,204,0,0.08) 0%, transparent 70%),
radial-gradient(ellipse at 30% 80%, rgba(0,229,255,0.06) 0%, transparent 60%);
z-index:0;
}
#gameCanvas{display:block;position:fixed;inset:0;width:100vw;height:100vh;z-index:1;}
/* HUD Layer */
.hud{position:fixed;z-index:10;pointer-events:none;}
/* Top Bar */
.top-bar{
top:0;left:0;right:0;height:72px;
display:flex;align-items:center;justify-content:space-between;
padding:0 24px;
background:linear-gradient(180deg,rgba(11,12,21,0.85) 0%,rgba(11,12,21,0.4) 100%);
border-bottom:2px solid rgba(255,204,0,0.3);
backdrop-filter:blur(12px) saturate(1.3);
box-shadow:0 4px 30px rgba(255,204,0,0.15);
}
.team-strip{display:flex;gap:16px;align-items:center;}
.team-card{
background:linear-gradient(135deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02));
border:1px solid rgba(255,255,255,0.12);
border-radius:14px;padding:6px 14px 6px 8px;
display:flex;align-items:center;gap:8px;
transition:transform .2s, box-shadow .2s;
box-shadow:0 2px 12px rgba(0,0,0,0.3);
}
.team-card.active{
border-color:var(--accent);box-shadow:0 0 20px rgba(255,204,0,0.35);
transform:translateY(-2px);
}
.team-icon{width:28px;height:28px;border-radius:50%;border:2px solid rgba(255,255,255,0.8);box-shadow:0 0 8px currentColor;flex-shrink:0;}
.team-info{line-height:1.2;}
.team-name{font-family:var(--font-tech);font-size:13px;letter-spacing:1px;text-transform:uppercase;}
.team-health{font-family:var(--font-mono);font-size:15px;color:var(--accent);}
.team-units{font-family:var(--font-mono);font-size:11px;color:var(--text-dim);}
.turn-display{
text-align:center;pointer-events:auto;
}
.turn-label{font-family:var(--font-tech);font-size:11px;letter-spacing:3px;color:var(--accent-2);text-transform:uppercase;}
.turn-value{
font-size:34px;font-weight:900;color:var(--accent);text-shadow:0 0 25px rgba(255,204,0,0.6);
transition:text-shadow .3s;
}
.turn-value.pulse{text-shadow:0 0 40px rgba(255,204,0,0.9), 0 0 60px rgba(255,44,68,0.6);animation:timerPulse .8s infinite alternate;}
@keyframes timerPulse{
from{transform:scale(1);text-shadow:0 0 25px rgba(255,204,0,0.6);}
to{transform:scale(1.08);text-shadow:0 0 45px rgba(255,204,0,1), 0 0 60px rgba(255,44,68,0.7);}
}
/* Wind indicator */
.wind-display{
display:flex;align-items:center;gap:8px;
background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);
border-radius:20px;padding:6px 14px;
}
.wind-arrow{font-size:22px;color:var(--accent-2);transition:transform .3s;}
.wind-label{font-family:var(--font-tech);font-size:10px;color:var(--text-dim);letter-spacing:2px;}
.wind-val{font-family:var(--font-mono);font-size:15px;color:var(--text);}
/* Bottom Bar */
.bottom-bar{
bottom:0;left:0;right:0;height:96px;
display:flex;align-items:center;justify-content:space-between;
padding:0 24px;
background:linear-gradient(0deg,rgba(11,12,21,0.9) 0%,rgba(11,12,21,0.5) 100%);
border-top:2px solid rgba(0,229,255,0.25);
backdrop-filter:blur(12px);
}
.weapon-panel{display:flex;gap:8px;align-items:center;}
.weapon-btn{
pointer-events:auto;position:relative;
background:linear-gradient(135deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03));
border:1px solid rgba(255,255,255,0.15);
border-radius:12px;padding:10px 14px;
color:var(--text);cursor:pointer;transition:all .2s;
font-family:var(--font-tech);font-size:12px;letter-spacing:1px;
min-width:80px;text-align:center;overflow:hidden;
box-shadow:0 4px 15px rgba(0,0,0,0.3);
}
.weapon-btn:hover:not(.disabled){transform:translateY(-3px);box-shadow:0 8px 25px rgba(255,204,0,0.3);border-color:rgba(255,204,0,0.5);}
.weapon-btn.active{background:linear-gradient(135deg,rgba(255,204,0,0.2),rgba(255,204,0,0.05));border-color:var(--accent);box-shadow:0 0 20px rgba(255,204,0,0.3);}
.weapon-btn.disabled{opacity:0.35;cursor:default;}
.weapon-btn .w-name{display:block;font-weight:700;font-size:11px;}
.weapon-btn .w-qty{display:block;font-family:var(--font-mono);font-size:18px;color:var(--accent);margin-top:2px;}
.unit-stats{display:flex;gap:16px;align-items:center;pointer-events:auto;}
.stat-box{background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:10px;padding:6px 12px;text-align:center;min-width:70px;}
.stat-label{font-family:var(--font-tech);font-size:9px;color:var(--text-dim);letter-spacing:2px;text-transform:uppercase;}
.stat-val{font-family:var(--font-mono);font-size:20px;color:var(--text);}
/* Floating combat text */
.floating-text{
position:fixed;pointer-events:none;z-index:50;
font-family:var(--font-display);font-size:28px;font-weight:900;
text-shadow:0 2px 4px rgba(0,0,0,0.8),0 0 20px currentColor;
animation:floatingUp 1.2s ease-out forwards;
}
@keyframes floatingUp{
0%{opacity:1;transform:translateY(0) scale(1);}
60%{opacity:1;transform:translateY(-30px) scale(1.15);}
100%{opacity:0;transform:translateY(-60px) scale(0.9);}
}
/* Menu / Intro overlay */
.overlay{
position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
background:rgba(11,12,21,0.92);backdrop-filter:blur(20px);
flex-direction:column;gap:24px;
transition:opacity .5s;
}
.overlay.hidden{opacity:0;pointer-events:none;}
.game-title{
font-family:var(--font-display);font-size:72px;color:var(--accent);
text-shadow:0 4px 30px rgba(255,204,0,0.5),0 0 80px rgba(255,204,0,0.3);
letter-spacing:6px;line-height:1.1;text-align:center;
}
.game-sub{
font-family:var(--font-tech);font-size:18px;color:var(--accent-2);letter-spacing:8px;text-transform:uppercase;
text-shadow:0 0 20px rgba(0,229,255,0.4);
}
.setup-panel{
background:rgba(255,255,255,0.04);border:2px solid rgba(255,255,255,0.1);
border-radius:24px;padding:32px 48px;max-width:700px;width:90%;
backdrop-filter:blur(16px);box-shadow:0 20px 60px rgba(0,0,0,0.5);
}
.setup-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;}
.setup-row{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid rgba(255,255,255,0.08);}
.setup-row h3{font-family:var(--font-tech);font-size:13px;letter-spacing:2px;color:var(--text-dim);}
.setup-row .value{font-family:var(--font-mono);font-size:18px;color:var(--accent);}
.setup-btns{display:flex;gap:12px;margin-top:20px;justify-content:center;}
.setup-btn{
pointer-events:auto;background:linear-gradient(135deg,var(--accent),#ffaa00);
color:#1a1200;border:none;border-radius:10px;padding:14px 36px;font-family:var(--font-display);font-size:22px;cursor:pointer;
box-shadow:0 6px 25px rgba(255,204,0,0.35);transition:all .2s;letter-spacing:2px;
}
.setup-btn:hover{transform:translateY(-2px);box-shadow:0 10px 35px rgba(255,204,0,0.5);}
.setup-btn.secondary{background:linear-gradient(135deg,rgba(255,255,255,0.1),rgba(255,255,255,0.05));color:var(--text);font-size:16px;padding:10px 24px;box-shadow:0 4px 15px rgba(0,0,0,0.3);}
.terrain-preview{
display:flex;gap:8px;justify-content:center;margin-top:8px;
}
.terrain-card{
background:rgba(255,255,255,0.05);border:2px solid rgba(255,255,255,0.1);border-radius:10px;padding:8px 16px;cursor:pointer;transition:all .2s;font-family:var(--font-tech);font-size:12px;color:var(--text-dim);}
.terrain-card.active{border-color:var(--accent);color:var(--accent);box-shadow:0 0 15px rgba(255,204,0,0.15);}
/* Aim preview line */
.aim-guide{pointer-events:none;position:fixed;z-index:5;}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<!-- HUD -->
<div class="hud top-bar">
<div class="team-strip" id="teamsHUD"></div>
<div class="turn-display">
<div class="turn-label">TURN — <span id="turnTeamName">WARRIORS</span></div>
<div class="turn-value" id="turnTimer">30</div>
</div>
<div class="wind-display">
<div class="wind-arrow" id="windArrow">→</div>
<div>
<div class="wind-label">WIND</div>
<div class="wind-val" id="windVal">0.3</div>
</div>
</div>
</div>
<div class="hud bottom-bar">
<div class="weapon-panel" id="weaponPanel"></div>
<div class="unit-stats">
<div class="stat-box">
<div class="stat-label">HEALTH</div>
<div class="stat-val" id="unitHealth">100</div>
</div>
<div class="stat-box">
<div class="stat-label">POS</div>
<div class="stat-val" id="unitPos">0,0</div>
</div>
</div>
</div>
<!-- Intro / Menu -->
<div class="overlay" id="mainOverlay">
<h1 class="game-title">WORM ARTILLERY</h1>
<h2 class="game-sub">PIXEL WARFARE</h2>
<div class="setup-panel">
<div class="setup-grid">
<div class="setup-row"><h3>TERRAIN SHAPE</h3>
<div class="terrain-preview">
<button class="terrain-card active" onclick="setTerrain('cave')">CAVE</button>
<button class="terrain-card" onclick="setTerrain('island')">ISLAND</button>
<button class="terrain-card" onclick="setTerrain('twin')">TWIN ISLANDS</button>
</div>
</div>
<div class="setup-row"><h3>TEAMS</h3><div class="value" id="teamCount">2</div></div>
<div class="setup-row"><h3>WIND VARIABLE</h3><div class="value">ON (Dynamic)</div></div>
<div class="setup-row"><h3>WATER PHYSICS</h3><div class="value">ON (Drown / Float)</div></div>
<div class="setup-row"><h3>AI OPPONENTS</h3><div class="value">ENABLED</div></div>
</div>
<div class="setup-btns">
<button class="setup-btn" onclick="startGame()">LAUNCH BATTLE</button>
<button class="setup-btn secondary" onclick="cycleTeams()">TEAMS: 2 → 4</button>
</div>
</div>
</div>
<script>
// ============================================
// GAME ENGINE — Pixel Artillery Arena
// ============================================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H, CX, CY;
function resize(){ W=canvas.width=window.innerWidth; H=canvas.height=window.innerHeight; CX=W/2; CY=H/2; }
window.addEventListener('resize', resize); resize();
// ============================================
// STATE
// ============================================
let gameState = 'menu'; // 'menu','setup','playing','gameover'
let terrainType = 'cave'; // 'cave','island','twin'
let teamCount = 2;
let activeTeam = 0;
let activeUnitIndex = 0;
let turnTimer = 30; // seconds
let turnStartTime = 0;
let wind = 0; // -1 to 1
let windTarget = 0;
let terrain = []; // 2D array of terrain pixels [y][x] 0=air, 1=solid, 2=debris, 3=water
let units = [];
let particles = [];
let projectiles = [];
let floatingTexts = [];
let screenShake = 0;
let impactFreeze = 0;
let mouseX = CX, mouseY = CY;
let aimAngle = Math.PI/4;
let aimPower = 20;
let cameraX = 0, cameraY = 0;
let cameraScale = 1;
let targetScale = 1;
// Weapons
const WEAPONS = [
{ name:'BAZOOKA', qty:3, type:'indirect', color:'#ff4444', radius:60, damage:35, icon:'💥' },
{ name:'GRENADE', qty:5, type:'indirect', color:'#ffaa00', radius:35, damage:25, icon:'💣' },
{ name:'LASER BEAM', qty:2, type:'direct', color:'#00e5ff', radius:0, damage:45, icon:'⚡' },
{ name:'AIR STRIKE', qty:1, type:'highaoe', color:'#ffcc00', radius:120, damage:60, icon:'☄️' },
{ name:'DRILL', qty:4, type:'indirect', color:'#aa44aa', radius:30, damage:20, icon:'🌀' },
{ name:'TELEPORT', qty:2, type:'utility', color:'#44ff88', radius:0, damage:0, icon:'🌀' },
{ name:'SHIELD', qty:2, type:'utility', color:'#aaaaff', radius:0, damage:0, icon:'🛡️' },
];
// Colors
const TEAM_COLORS = ['#ffcc00','#ff4444','#00e5ff','#ff66aa'];
// ============================================
// SOUND ENGINE (Web Audio API)
// ============================================
const AudioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playTone(freq, type='triangle', duration=0.15, gain=0.05, detune=0){
try{
const o=AudioCtx.createOscillator();
const g=AudioCtx.createGain();
o.type=type; o.frequency.setValueAtTime(freq, AudioCtx.currentTime);
o.detune.setValueAtTime(detune, AudioCtx.currentTime);
g.gain.setValueAtTime(gain, AudioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, AudioCtx.currentTime+duration);
o.connect(g); g.connect(AudioCtx.destination);
o.start(); o.stop(AudioCtx.currentTime+duration);
}catch(e){}
}
function playSound(name){
if(gameState==='menu') return; // suppress in menu for now, but we'll use it
if(name==='jump') playTone(300, 'sine', 0.12, 0.03);
if(name==='land') playTone(120, 'sine', 0.2, 0.03);
if(name==='shoot') playTone(500, 'triangle', 0.15, 0.02);
if(name==='explosion'){
playTone(150, 'sine', 0.3, 0.06);
setTimeout(()=>playTone(60, 'sine', 0.4, 0.05), 30);
setTimeout(()=>playTone(30, 'sine', 0.5, 0.05), 80);
}
if(name==='damage') playTone(220, 'square', 0.25, 0.04);
if(name==='drown'){
playTone(60, 'sine', 0.3, 0.05);
playTone(30, 'sine', 0.5, 0.05);
}
if(name==='click'){ playTone(800, 'triangle', 0.05, 0.02); }
if(name==='turnStart'){
playTone(400, 'triangle', 0.1, 0.03);
setTimeout(()=>playTone(600, 'triangle', 0.1, 0.03), 100);
}
if(name==='timerTick'){ playTone(200+turnTimer*5, 'triangle', 0.05, 0.015); }
if(name==='victory'){
playTone(523, 'triangle', 0.3, 0.03);
setTimeout(()=>playTone(659, 'triangle', 0.3, 0.03), 150);
setTimeout(()=>playTone(784, 'triangle', 0.5, 0.03), 300);
}
}
// ============================================
// PARALLAX STARS BACKGROUND (behind terrain)
// ============================================
let stars = [];
for(let i=0;i<200;i++){
stars.push({x:Math.random()*W, y:Math.random()*H, r:0.5+Math.random()*1.5, speed:0.2+Math.random()*0.8});
}
// ============================================
// TERRAIN GENERATION
// ============================================
function generateTerrain(type){
const cols = 200; // lower resolution for pixel feel but faster
const rows = 120;
terrain = [];
for(let y=0;y<rows;y++) terrain[y]=new Uint8Array(cols);
// Generate based on shape
if(type==='cave'){
// Cave shape: high walls, low ceiling, floor is flat with some bumps
for(let x=0;x<cols;x++){
for(let y=0;y<rows;y++){
// Ceiling curve
const ceilingY = Math.floor(20 + Math.sin(x/20)*10 + Math.cos(x/10)*5);
// Floor with bumps
const floorY = Math.floor(90 + Math.sin(x/15)*8);
// Walls on sides
if(x<10 || x>cols-10) terrain[y][x]=1;
if(y<ceilingY || y>floorY) terrain[y][x]=1;
// Random debris inside
if(y>floorY-10 && y<floorY && Math.random()>0.7) terrain[y][x]=1;
}
}
// Water at bottom in some spots
for(let x=20;x<cols-20;x++){
if(Math.random()>0.85) for(let y=95;y<110;y++) if(y<rows) terrain[y][x]=3; // water
}
} else if(type==='island'){
for(let x=0;x<cols;x++){
for(let y=0;y<rows;y++){
// Island mound
const mound = Math.sin(x/cols*Math.PI)*30 + Math.sin(x/cols*Math.PI*3)*10;
const islandTop = 60 - mound;
if(y>islandTop && y>30) terrain[y][x]=1;
}
}
// Water below island
for(let x=0;x<cols;x++){
for(let y=70;y<110;y++){
if(y<rows && Math.random()>0.3) terrain[y][x]=3;
}
}
} else if(type==='twin'){
// Two islands
for(let x=0;x<cols;x++){
for(let y=0;y<rows;y++){
const left = Math.sin((x-20)/30*Math.PI)*25 + Math.sin((x-20)/15)*8;
const right = Math.sin((x-120)/30*Math.PI)*25 + Math.sin((x-120)/15)*8;
if(x<70 && y>55-left) terrain[y][x]=1;
if(x>100 && y>55-right) terrain[y][x]=1;
}
}
// Water everywhere between and below
for(let x=0;x<cols;x++){
for(let y=65;y<110;y++){
if(y<rows && (x<90 || x>80)) terrain[y][x]=3;
}
}
}
// Convert to world coordinates mapping
// We'll treat cols = width in pixels at a certain scale
// For simplicity: world width = 160 units, height = 80 units
}
function getTerrainAt(x, y){
// x,y are world coordinates
const cols = 200; const rows = 120;
const cx = Math.floor((x / 160) * cols);
const cy = Math.floor((y / 80) * rows);
if(cx<0||cx>=cols||cy<0||cy>=rows) return 0;
return terrain[cy][cx];
}
function setTerrain(x,y,val){
const cols=200, rows=120;
const cx=Math.floor((x/160)*cols); const cy=Math.floor((y/80)*rows);
if(cx>=0&&cx<cols&&cy>=0&&cy<rows) terrain[cy][cx]=val;
}
// ============================================
// UNIT SETUP
// ============================================
function spawnUnits(){
units=[];
const cols=200; const rows=120;
// Spawn points depending on terrain
let spawns=[];
if(terrainType==='cave'){
spawns=[
{x:20,y:75},{x:60,y:75},{x:100,y:75},{x:140,y:75}
];
} else if(terrainType==='island'){
spawns=[
{x:35,y:45},{x:80,y:45},{x:125,y:45},{x:160,y:45}
];
} else if(terrainType==='twin'){
spawns=[
{x:25,y:45},{x:50,y:45},{x:120,y:45},{x:150,y:45}
];
}
for(let t=0;t<teamCount;t++){
for(let u=0;u<4;u++){
const spawn = spawns[(t*4+u)%spawns.length];
units.push({
team:t,
x: spawn.x + Math.random()*10 - 5,
y: spawn.y,
vx:0, vy:0,
w:10, h:10,
hp:100,
maxHp:100,
alive:true,
shielded: false,
name:'W'+(t+1)+(u+1),
faceDir:1,
jumpCooldown:0,
moveTimer:0,
onGround:false,
inWater:false,
});
}
}
activeTeam=0; activeUnitIndex=0; turnTimer=30;
}
// ============================================
// PROJECTILES & PARTICLES
// ============================================
function spawnProjectile(type, x, y, vx, vy, team){
const w = WEAPONS[type];
projectiles.push({
x, y, vx, vy,
team, weapon: type,
radius: w.radius,
damage: w.damage,
life: 120,
active: true,
trail: []
});
}
function spawnExplosion(x,y,radius,damage,team){
// Particles
const count = Math.floor(radius*3);
for(let i=0;i<count;i++){
const angle = Math.random()*Math.PI*2;
const speed = 1+Math.random()*4;
particles.push({
x, y,
vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed,
life: 20+Math.random()*40,
color: Math.random()>0.5 ? '#ffaa00' : '#ff4444',
size: 1+Math.random()*3,
type:'fire'
});
}
// Debris chunks
for(let i=0;i<15;i++){
const angle = Math.random()*Math.PI*2;
particles.push({
x, y,
vx: Math.cos(angle)*(speed*0.5), vy: Math.sin(angle)*(speed*0.5)-2,
life: 60+Math.random()*80,
color: '#8a6d3b', size: 3+Math.random()*5,
type:'debris', angle: Math.random()*Math.PI*2, rotSpeed: (Math.random()-0.5)*0.2
});
}
// Sound
playSound('explosion');
screenShake = Math.min(radius/4, 15);
if(radius>80) impactFreeze = 10;
// Destroy terrain
for(let dx=-radius;dx<=radius;dx+=3){
for(let dy=-radius;dy<=radius;dy+=3){
const d=Math.sqrt(dx*dx+dy*dy);
if(d<=radius && Math.random()>0.2){
const wx=x+dx, wy=y+dy;
// Convert to terrain grid
const cols=200, rows=120;
const cx=Math.floor((wx/160)*cols);
const cy=Math.floor((wy/80)*rows);
if(cx>=0&&cx<cols&&cy>=0&&cy<rows){
if(terrain[cy][cx]===1){
terrain[cy][cx]=0; // clear solid
// Add falling debris particle
particles.push({
x:wx, y:wy,
vx:(Math.random()-0.5)*2, vy:-1-Math.random()*3,
life:40+Math.random()*60,
color:'#8a6d3b', size:2+Math.random()*4,
type:'debris', angle:Math.random()*Math.PI*2, rotSpeed:(Math.random()-0.5)*0.1
});
} else if(terrain[cy][cx]===3){
terrain[cy][cx]=0; // clear water too? Actually just keep for now
}
}
}
}
}
// Damage units
for(let u of units){
if(!u.alive) continue;
const dx=u.x-x, dy=u.y-y;
const d=Math.sqrt(dx*dx+dy*dy);
if(d<radius+3){
const dmg = Math.floor(w.damage*(1-d/radius));
u.hp-=dmg; u.vx+=dx/d*2; u.vy-=dy/d*2;
spawnFloatingText(u.x, u.y-15, '-'+dmg, '#ff4444');
playSound('damage');
}
}
}
function spawnFloatingText(x,y,text,color='#ffcc00'){
floatingTexts.push({x,y,text,color,life:60,vy:-1});
}
// ============================================
// CAMERA
// ============================================
function updateCamera(){
const activeUnit = units.find(u=>u.team===activeTeam && u.alive);
if(activeUnit){
const targetX = activeUnit.x;
const targetY = activeUnit.y - 15;
cameraX += (targetX - cameraX) * 0.08;
cameraY += (targetY - cameraY) * 0.08;
}
}
function getScreenX(x){ return x*cameraScale; }
function getScreenY(y){ return y*cameraScale; }
// ============================================
// DRAWING
// ============================================
function draw(){
// Clear with deep color
ctx.fillStyle = '#0b0c15';
ctx.fillRect(0,0,W,H);
// Stars (parallax)
ctx.save();
ctx.translate(-cameraX*0.1 + W/2, -cameraY*0.1 + H/2);
for(let s of stars){
ctx.fillStyle = `rgba(255,255,255,${0.3+s.r/5})`;
ctx.fillRect(s.x, s.y, s.r, s.r);
}
ctx.restore();
// Apply camera transform
ctx.save();
// Center camera on target
const activeU = units.find(u=>u.team===activeTeam && u.alive);
let cx=0, cy=0, cs=1;
if(activeU){ cx=activeU.x; cy=activeU.y; cs=1; }
// Dynamic zoom based on action
if(projectiles.length>0 || screenShake>0){ cs=0.85; }
else if(activeU && activeU.hp<30){ cs=0.9; }
targetScale = cs;
cameraScale += (targetScale - cameraScale) * 0.1;
ctx.translate(W/2 - cx*cameraScale, H/2 - cy*cameraScale*0.7); // slightly offset up for HUD
// Apply screen shake
if(screenShake>0){
const sx = (Math.random()-0.5)*screenShake*2;
const sy = (Math.random()-0.5)*screenShake*2;
ctx.translate(sx,sy);
screenShake *= 0.9;
}
// Terrain
const cols=200, rows=120;
const pw=160/cols, ph=80/rows; // world per cell
for(let y=0;y<rows;y++){
for(let x=0;x<cols;x++){
const val = terrain[y][x];
if(val===0) continue;
const wx = x*pw + pw/2; // world x center
const wy = y*ph + ph/2; // world y center
if(val===1){
const grad = ctx.createLinearGradient(wx-pw/2, wy-ph/2, wx+pw/2, wy+ph/2);
grad.addColorStop(0,'#5a4a3a');
grad.addColorStop(1,'#3a2a1a');
ctx.fillStyle = grad;
ctx.fillRect(wx-pw/2, wy-ph/2, pw, ph);
// Border
ctx.strokeStyle = '#2a1a0a';
ctx.strokeRect(wx-pw/2, wy-ph/2, pw, ph);
} else if(val===3){
// Water
const grad = ctx.createLinearGradient(wx-pw/2, wy-ph/2, wx+pw/2, wy+ph/2);
grad.addColorStop(0,'#0066aa');
grad.addColorStop(0.5,'#0099ff');
grad.addColorStop(1,'#004488');
ctx.fillStyle = grad;
ctx.fillRect(wx-pw/2, wy-ph/2, pw, ph);
// Ripples (animated using time - we'll approximate)
}
}
}
// Draw units
for(let u of units){
if(!u.alive) continue;
// Body
ctx.save();
ctx.translate(u.x, u.y);
if(u.faceDir<0) ctx.scale(-1,1);
// Head/body
ctx.fillStyle = TEAM_COLORS[u.team];
ctx.shadowColor = TEAM_COLORS[u.team];
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.ellipse(0, -2, 5, 7, 0, 0, Math.PI*2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(1, -4, 1.2, 0, Math.PI*2); ctx.fill();
// Mouth
ctx.strokeStyle = '#fff'; ctx.lineWidth=0.5;
ctx.beginPath(); ctx.arc(0, -1, 2.5, 0.2, Math.PI-0.3); ctx.stroke();
// Shield if active
if(u.shielded){
ctx.strokeStyle = 'rgba(170,170,255,0.6)';
ctx.lineWidth=2;
ctx.beginPath(); ctx.arc(0,0,10,0,Math.PI*2); ctx.stroke();
}
ctx.restore();
// Health bar
const hw = 14, hh = 3;
ctx.fillStyle = '#333';
ctx.fillRect(u.x-7, u.y-10, hw, hh);
const healthRatio = u.hp/u.maxHp;
ctx.fillStyle = healthRatio>0.3 ? '#44ff44' : '#ff4444';
ctx.fillRect(u.x-7, u.y-10, hw*healthRatio, hh);
}
// Projectiles
for(let p of projectiles){
if(!p.active) continue;
ctx.save();
ctx.translate(p.x, p.y);
const w = WEAPONS[p.weapon];
ctx.fillStyle = w.color;
ctx.shadowColor = w.color;
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI*2);
ctx.fill();
// Trail
if(p.trail.length>0){
ctx.beginPath();
ctx.moveTo(p.trail[0].x-p.x, p.trail[0].y-p.y);
for(let i=1;i<p.trail.length;i++){
ctx.lineTo(p.trail[i].x-p.x, p.trail[i].y-p.y);
}
ctx.strokeStyle = w.color;
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.3;
ctx.stroke();
ctx.globalAlpha = 1;
}
ctx.restore();
}
// Particles
for(let part of particles){
ctx.save();
ctx.translate(part.x, part.y);
if(part.type==='debris'){
ctx.rotate(part.angle);
ctx.fillStyle = part.color;
ctx.fillRect(-part.size/2, -part.size/2, part.size, part.size);
part.angle += part.rotSpeed;
} else {
ctx.fillStyle = part.color;
ctx.beginPath();
ctx.arc(0,0,part.size,0,Math.PI*2);
ctx.fill();
}
ctx.restore();
}
// Water surface effects
for(let y=0;y<rows;y++){
for(let x=0;x<cols;x++){
if((terrain[y][x]||0)===3){
const wx = x*pw + pw/2, wy = y*ph + ph/2;
const rip = Math.sin((wx+Date.now()/300)*3)*0.5;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.fillRect(wx-pw/2, wy-ph/2+rip, pw, 1);
}
}
}
ctx.restore(); // End camera
// Floating combat text
for(let ft of floatingTexts){
ft.y += ft.vy; ft.life--;
if(ft.life<=0) continue;
ctx.save();
ctx.font = 'bold 22px Bangers';
ctx.fillStyle = ft.color;
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.strokeText(ft.text, W/2 + (ft.x-cameraX*cameraScale) - W/2 + (ft.x-cameraX*cameraScale - W/2), H/2 - (ft.y-cameraY*cameraScale*0.7) + H/2 - (ft.y-cameraY*cameraScale*0.7 - H/2));
// Simplify: screen-space projection
const sx = (ft.x - cameraX)*cameraScale + W/2;
const sy = (ft.y - cameraY*0.7)*cameraScale + H/2;
ctx.globalAlpha = ft.life/60;
ctx.fillText(ft.text, sx, sy);
ctx.restore();
}
// Predictive aim line (drawn in world space, converted to screen)
if(gameState==='playing'){
drawAimLine();
}
// Impact freeze effect (flash)
if(impactFreeze>0){
ctx.fillStyle = `rgba(255,200,0,${impactFreeze/20})`;
ctx.fillRect(0,0,W,H);
impactFreeze--;
}
}
function drawAimLine(){
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(!activeU) return;
const w = WEAPONS[getSelectedWeaponIndex()];
if(w.type==='utility') return;
// Predict trajectory
let px = activeU.x, py = activeU.y - 5;
let vx = Math.cos(aimAngle)*aimPower;
let vy = Math.sin(aimAngle)*aimPower;
ctx.save();
ctx.translate(W/2 - cameraX*cameraScale, H/2 - cameraY*cameraScale*0.7);
if(screenShake>0){
const sx = (Math.random()-0.5)*screenShake*2;
const sy = (Math.random()-0.5)*screenShake*2;
ctx.translate(sx,sy);
}
ctx.beginPath();
ctx.moveTo(px, py);
for(let step=0;step<30;step++){
vx += (wind/100)*0.3; // wind affects trajectory
vy += 0.8; // gravity
px += vx; py += vy;
// Check collision with terrain
if(getTerrainAt(px, py)===1) break;
// Draw dot
ctx.lineTo(px, py);
}
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.setLineDash([4,3]);
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
}
// ============================================
// GAME LOOP
// ============================================
function getSelectedWeaponIndex(){
return 0; // We'll manage selection via UI
}
// Update physics
function updatePhysics(dt){
// Update wind gradually
windTarget = Math.sin(Date.now()/5000)*0.6 + Math.random()*0.3 - 0.15;
wind += (windTarget - wind) * 0.02;
// Update wind UI
document.getElementById('windVal').textContent = wind.toFixed(2);
const rot = wind*20;
document.getElementById('windArrow').style.transform = `rotate(${rot}deg)`;
// Update particles
for(let p=particles.length-1;p>=0;p--){
const part = particles[p];
part.x += part.vx; part.y += part.vy;
part.vy += 0.1; // gravity on debris
part.life--;
if(part.life<=0 || part.y>90) particles.splice(p,1);
}
// Update floating texts
for(let ft=floatingTexts.length-1;ft>=0;ft--){
const text = floatingTexts[ft];
text.life--;
if(text.life<=0) floatingTexts.splice(ft,1);
}
// Update units
for(let u of units){
if(!u.alive) continue;
// Apply wind to falling units
u.vx += wind*0.05;
// Gravity
u.vy += 1.2;
// Friction
u.vx *= 0.98;
// Movement (turn-based manual)
// But for physics we apply velocity
u.x += u.vx;
u.y += u.vy;
// Terrain collision
// Sample multiple points
const w=5, h=5;
let colliding = false;
for(let dy=0;dy<=h;dy+=h){
for(let dx=0;dx<=w;dx+=w){
const tx = u.x + dx - w/2;
const ty = u.y + dy - h/2;
const val = getTerrainAt(tx, ty);
if(val===1){
colliding = true;
// Resolve
u.vy = 0; u.vx = 0;
// Push out upward
u.y = Math.floor(ty/ (80/120))*(80/120) - h/2 - 0.5; // approximate
// Actually let's use simple approach
}
if(val===3){
// Water
u.inWater = true;
u.vy = Math.min(u.vy, 0.5); // float/slower fall
if(Math.random()>0.98){
// Splash particle
particles.push({x:tx,y:ty,vx:0,vy:-1,life:15,color:'#0099ff',size:2,type:'fire'});
}
} else {
u.inWater = false;
}
}
}
// If in water too long, drown
if(u.inWater && Math.random()>0.99){
// Actually let's apply damage over time
if(Math.random()>0.995){
u.hp -= 2;
spawnFloatingText(u.x, u.y-8, '-2', '#0099ff');
}
}
// Fall damage
if(colliding && u.vy>4){
const dmg = Math.floor(u.vy*2);
u.hp -= dmg;
spawnFloatingText(u.x, u.y-8, '-'+dmg, '#ffaa00');
playSound('land');
u.vy = -2; // bounce slightly
}
// Boundary
if(u.x<2) u.x=2; if(u.x>158) u.x=158;
if(u.y>85) u.y=85; // bottom boundary
u.onGround = colliding;
if(u.hp<=0){ u.alive=false; playSound('damage'); }
if(u.hp>u.maxHp) u.hp=u.maxHp;
}
// Update projectiles
for(let p=projectiles.length-1;p>=0;p--){
const proj = projectiles[p];
proj.life--;
proj.vx += (wind/100)*0.3;
proj.vy += 0.8;
proj.x += proj.vx;
proj.y += proj.vy;
proj.trail.push({x:proj.x, y:proj.y});
if(proj.trail.length>15) proj.trail.shift();
// Collision
if(getTerrainAt(proj.x, proj.y)===1 || proj.life<=0){
spawnExplosion(proj.x, proj.y, WEAPONS[proj.weapon].radius, WEAPONS[proj.weapon].damage, proj.team);
projectiles.splice(p,1);
continue;
}
// Water interaction
if(getTerrainAt(proj.x, proj.y)===3){
// Small splash
particles.push({x:proj.x, y:proj.y, vx:(Math.random()-0.5), vy:-2, life:10, color:'#0099ff', size:2, type:'fire'});
}
}
// Turn timer
if(gameState==='playing'){
const now = Date.now();
const elapsed = (now - turnStartTime)/1000;
turnTimer = Math.max(0, 30 - elapsed);
document.getElementById('turnTimer').textContent = Math.ceil(turnTimer);
if(turnTimer<=5) document.getElementById('turnTimer').classList.add('pulse'); else document.getElementById('turnTimer').classList.remove('pulse');
if(turnTimer<=0){
turnTimer = 30;
turnStartTime = now;
nextTurn();
}
}
}
// ============================================
// TURN MANAGEMENT
// ============================================
function nextTurn(){
// Find next alive unit in next team
playSound('turnStart');
do{
activeUnitIndex = (activeUnitIndex+1)%units.length;
activeTeam = units[activeUnitIndex].team;
const teamUnits = units.filter(u=>u.team===activeTeam);
if(teamUnits.every(u=>!u.alive)){
// Skip dead team? Actually just continue
}
} while(!units[activeUnitIndex].alive);
turnStartTime = Date.now();
turnTimer = 30;
// Update HUD
document.getElementById('turnTeamName').textContent = 'TEAM '+(activeTeam+1);
document.getElementById('turnTimer').classList.remove('pulse');
document.getElementById('turnTimer').textContent = '30';
// CPU turn if it's a bot team
// For simplicity, we'll make team 2+ AI when teamCount>1 or all teams have AI except human
if(isAITeam(activeTeam)){
// Small delay then run AI
setTimeout(runAITurn, 800);
}
}
function isAITeam(team){
// Even teams are AI if teamCount>1
if(teamCount===2) return team===1;
if(teamCount===4) return team>=1; // Team 0 human, 1-3 AI? Let's say all except first
return false;
}
function runAITurn(){
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(!activeU) return;
// Simple AI: jump sometimes, aim at closest enemy, fire
// Find closest enemy
let closest = null; let minD = Infinity;
for(let u of units){
if(u.team===activeTeam || !u.alive) continue;
const d=Math.hypot(u.x-activeU.x, u.y-activeU.y);
if(d<minD){ minD=d; closest=u; }
}
// Aim at closest
if(closest){
const dx = closest.x - activeU.x;
const dy = closest.y - activeU.y;
aimAngle = Math.atan2(dy, dx);
// Random power
aimPower = 10+Math.random()*25;
// Fire a weapon with qty>0
const weaponIndex = Math.floor(Math.random()*WEAPONS.length);
const w = WEAPONS[weaponIndex];
// Check if we have that weapon
// We'll just always use first available
const avail = WEAPONS.findIndex(w=>w.qty>0);
if(avail>=0){
fireWeapon(avail);
}
// Also move randomly sometimes
if(Math.random()>0.5){
if(Math.random()>0.5){
// Jump
if(activeU.onGround) activeU.vy = -6 - Math.random()*3;
} else {
activeU.vx = (Math.random()>0.5?1:-1)*3;
}
}
}
// AI takes shorter time? We'll just let timer run
// Actually AI should act quickly
setTimeout(()=>{
turnTimer = 2; // Force quick end
}, 400);
}
// ============================================
// INPUT
// ============================================
window.addEventListener('mousemove', e=>{
mouseX = e.clientX; mouseY = e.clientY;
// Calculate aim relative to active unit
if(gameState==='playing'){
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(activeU){
const screenU = {x:(activeU.x-cameraX)*cameraScale+W/2, y:(activeU.y-cameraY*0.7)*cameraScale+H/2};
aimAngle = Math.atan2(mouseY-screenU.y, mouseX-screenU.x);
aimPower = 5 + Math.hypot(mouseX-screenU.x, mouseY-screenU.y)*0.3;
aimPower = Math.min(aimPower, 40);
}
}
});
window.addEventListener('click', e=>{
// Only handle clicks in menu or weapon buttons
});
window.addEventListener('keydown', e=>{
if(gameState!=='playing') return;
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(!activeU) return;
if(e.key==='ArrowLeft' || e.key==='a'){
activeU.vx = -3; activeU.faceDir = -1;
playSound('jump');
}
if(e.key==='ArrowRight' || e.key==='d'){
activeU.vx = 3; activeU.faceDir = 1;
playSound('jump');
}
if((e.key==='ArrowUp' || e.key==='w') && activeU.onGround){
activeU.vy = -7 - Math.random()*2;
activeU.onGround = false;
playSound('jump');
}
if(e.key===' '){
// Fire selected weapon (default index based on selection)
fireWeapon(getSelectedWeaponIndex());
}
if(e.key==='1') selectWeapon(0);
if(e.key==='2') selectWeapon(1);
if(e.key==='3') selectWeapon(2);
if(e.key==='4') selectWeapon(3);
if(e.key==='5') selectWeapon(4);
if(e.key==='6') selectWeapon(5);
if(e.key==='7') selectWeapon(6);
});
let selectedWeapon = 0;
function getSelectedWeaponIndex(){ return selectedWeapon; }
function selectWeapon(i){
selectedWeapon = i;
document.querySelectorAll('.weapon-btn').forEach(b=>b.classList.remove('active'));
document.getElementById('wbtn'+i).classList.add('active');
playSound('click');
}
function fireWeapon(wi){
const w = WEAPONS[wi];
if(w.qty<=0 || gameState!=='playing') return;
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(!activeU) return;
w.qty--;
playSound('shoot');
if(w.type==='indirect' || w.type==='highaoe'){
spawnProjectile(wi, activeU.x, activeU.y-5, Math.cos(aimAngle)*aimPower*0.8, Math.sin(aimAngle)*aimPower*0.6, activeTeam);
} else if(w.type==='direct'){
// Instant ray
const steps = 50;
for(let s=0;s<steps;s++){
const tx = activeU.x + Math.cos(aimAngle)*(10+s*8);
const ty = activeU.y -5 + Math.sin(aimAngle)*(10+s*8);
if(getTerrainAt(tx,ty)===1){
spawnExplosion(tx,ty,20,30,activeTeam);
break;
}
// Damage units along path
for(let u of units){
if(!u.alive || u.team===activeTeam) continue;
const d=Math.hypot(u.x-tx, u.y-ty);
if(d<6){
u.hp-=w.damage;
spawnFloatingText(u.x,u.y-8,'-'+w.damage,'#ff4444');
}
}
}
} else if(w.type==='utility'){
if(w.name.includes('SHIELD')){
activeU.shielded = true;
setTimeout(()=>{ activeU.shielded=false; }, 3000);
} else if(w.name.includes('TELEPORT')){
activeU.x += Math.cos(aimAngle)*15;
activeU.y += Math.sin(aimAngle)*15 - 10;
spawnExplosion(activeU.x, activeU.y, 10, 0, activeTeam);
}
}
// End turn faster if fired
turnTimer = 5;
}
function updateWeaponUI(){
const panel = document.getElementById('weaponPanel');
panel.innerHTML = '';
WEAPONS.forEach((w,i)=>{
const btn = document.createElement('button');
btn.className = 'weapon-btn' + (i===selectedWeapon?' active':'') + (w.qty<=0?' disabled':'');
btn.id = 'wbtn'+i;
btn.innerHTML = `<span class="w-name">${w.name}</span><span class="w-qty">${w.qty}</span>`;
btn.onclick = () => { if(w.qty>0) selectWeapon(i); };
panel.appendChild(btn);
});
}
function cycleTeams(){
teamCount = teamCount===2?4:(teamCount===4?2:2);
document.getElementById('teamCount').textContent = teamCount;
playSound('click');
}
function setTerrain(t){
terrainType = t;
document.querySelectorAll('.terrain-card').forEach(c=>c.classList.remove('active'));
event.target.closest('.terrain-card').classList.add('active');
playSound('click');
}
function startGame(){
document.getElementById('mainOverlay').classList.add('hidden');
gameState = 'playing';
generateTerrain(terrainType);
spawnUnits();
updateWeaponUI();
turnStartTime = Date.now();
updateHUD();
loop();
}
function updateHUD(){
const hud = document.getElementById('teamsHUD');
hud.innerHTML = '';
for(let t=0;t<teamCount;t++){
const alive = units.filter(u=>u.team===t && u.alive);
const card = document.createElement('div');
card.className = 'team-card' + (t===activeTeam?' active':'');
card.innerHTML = `
<div class="team-icon" style="color:${TEAM_COLORS[t]};background:${TEAM_COLORS[t]};box-shadow:0 0 10px ${TEAM_COLORS[t]};"></div>
<div class="team-info">
<div class="team-name">TEAM ${t+1}</div>
<div class="team-health">${alive.length} ALIVE</div>
<div class="team-units">HP: ${alive.map(u=>u.hp).reduce((a,b)=>a+b,0)}/400</div>
</div>
`;
hud.appendChild(card);
}
// Unit stats
const activeU = units.find(u=>u.team===activeTeam && u.alive);
if(activeU){
document.getElementById('unitHealth').textContent = Math.floor(activeU.hp);
document.getElementById('unitPos').textContent = Math.round(activeU.x)+','+Math.round(activeU.y);
}
}
// ============================================
// MAIN LOOP
// ============================================
let lastTime = 0;
function loop(timestamp){
if(gameState!=='playing' && !document.getElementById('mainOverlay').classList.contains('hidden')){
// Menu scene: just draw stars and simple background
ctx.fillStyle='#0b0c15'; ctx.fillRect(0,0,W,H);
ctx.save();
ctx.translate(W/2, H/2);
for(let s of stars){
ctx.fillStyle=`rgba(255,255,255,${0.1+s.r/10})`;
ctx.fillRect(s.x - cameraX*0.05 + W/2, s.y - cameraY*0.05 + H/2, s.r, s.r);
}
ctx.restore();
requestAnimationFrame(loop);
return;
}
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
updatePhysics(dt);
updateCamera();
draw();
updateHUD();
updateWeaponUI();
if(gameState==='playing'){
// Check game over
const aliveTeams = new Set(units.filter(u=>u.alive).map(u=>u.team));
if(aliveTeams.size<=1 && units.some(u=>u.alive)){
gameState = 'gameover';
playSound('victory');
setTimeout(()=>{
alert('TEAM '+(Array.from(aliveTeams)[0]+1)+' WINS!');
document.getElementById('mainOverlay').classList.remove('hidden');
gameState='menu';
}, 2000);
}
}
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"2a7e24e4-63e9-4979-a7d6-589b07790998","tokensIn":643619,"tokensOut":17379,"tokensTotal":660998,"turns":16,"toolCalls":15,"failedToolCalls":1,"timestamp":"2026-07-18T22:00:39.513Z"} -->