Share this result

Previews are deleted daily. Get a permanent share link sent to your inbox:
Script
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Clash of Concubines</title> <style> body { margin:0; background:#0d0d12; display:flex; justify-content:center; align-items:center; min-height:100vh; font-family:'Segoe UI',system-ui,sans-serif; } canvas { background:#1a2a1a; box-shadow:0 0 40px #000, 0 0 80px #6a1b3a; border-radius:6px; cursor:crosshair; } </style> </head> <body> <canvas id="c" width="480" height="800"></canvas> <script> const cv = document.getElementById('c'), ctx = cv.getContext('2d'); const W = 480, H = 800; const RIVER_TOP = 380, RIVER_BOT = 420; const BRIDGES = [120, 360]; const CARDS = { hellhound: {name:'Hellhound', cost:2, hp:200, dmg:40, spd:150, range:25, cd:0.7, r:11, color:'#c0392b', desc:'Bitch in heat. Runs hot, dies fast, leaves a mess.'}, succubus: {name:'Succubus', cost:3, hp:350, dmg:55, spd:130, range:30, cd:0.9, r:13, color:'#8e44ad', lifesteal:0.5, desc:'Drains life with every touch. Sloppy, greedy, effective.'}, incubus: {name:'Incubus', cost:4, hp:900, dmg:80, spd:70, range:30, cd:1.2, r:17, color:'#6d4c41', desc:'Big, slow, hung like a siege engine. Soaks damage.'}, banshee: {name:'Banshee', cost:3, hp:250, dmg:70, spd:90, range:130, cd:1.1, r:12, color:'#5dade2', desc:'Screams from range. Glass cannon. Moans when she hits.'}, dominatrix:{name:'Dominatrix', cost:5, hp:600, dmg:130, spd:100, range:30, cd:1.0, r:14, color:'#e74c3c', desc:'Whip crack hits hard. Single target. No mercy.'}, broodmother:{name:'Broodmother', cost:5, hp:700, dmg:50, spd:80, range:30, cd:1.0, r:16, color:'#27ae60', desc:'Spits out spawn on death. Fertile to the end.', onDeath:'imp', onDeathCount:3}, concubine: {name:'Concubine', cost:4, hp:400, dmg:0, spd:90, range:120, cd:1.0, r:12, color:'#f39c12', heal:25, desc:'Heals allies with her presence. Keeps the line wet.'}, tentacle: {name:'Tentacle Horror', cost:6, hp:1200, dmg:60, spd:50, range:30, cd:1.0, r:19, color:'#16a085', spawner:'tentacle_sm', spawnCd:3, desc:'Tanky spawner. Wraps and fills. Spawns tentacles.'}, imp: {name:'Imp', cost:1, hp:100, dmg:20, spd:160, range:25, cd:0.6, r:8, color:'#d35400', desc:'Fodder. Fills a hole. That is all.'}, tentacle_sm:{name:'Tentacle', cost:1, hp:150, dmg:25, spd:100, range:25, cd:0.8, r:9, color:'#1abc9c', desc:'A smaller tentacle. Still gets the job done.'} }; const DECK = ['hellhound','succubus','incubus','banshee','dominatrix','broodmother','concubine','tentacle']; let S; function init() { S = { time: 0, over: false, winner: null, elixir: 5, aiElixir: 5, selected: -1, playerDeck: shuffle([...DECK]), aiDeck: shuffle([...DECK]), units: [], towers: [], uid: 1, aiTimer: 2, towers_destroyed: {player:0, ai:0} }; // towers: team 'player' bottom, 'ai' top S.towers.push(mkTower('ai', 120, 200, false), mkTower('ai', 360, 200, false), mkTower('ai', 240, 70, true)); S.towers.push(mkTower('player', 120, 600, false), mkTower('player', 360, 600, false), mkTower('player', 240, 730, true)); } function shuffle(a){ for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; } function mkTower(team, x, y, king) { return {team, x, y, king, hp: king?2400:1400, maxHp: king?2400:1400, dmg: king?70:50, cd: king?1.0:0.8, atkTimer:0, range: king?160:140, r: king?26:20, lane: x<240?'L':'R'}; } function spawnUnit(team, key, x, y) { const c = CARDS[key]; if (!c) return; S.units.push({ id:S.uid++, team, key, x, y, hp:c.hp, maxHp:c.hp, dmg:c.dmg, spd:c.spd, range:c.range, cd:c.cd, atkTimer:0, r:c.r, color:c.color, lifesteal:c.lifesteal||0, heal:c.heal||0, spawner:c.spawner||null, spawnCd:c.spawnCd||0, spawnTimer:0, onDeath:c.onDeath||null, onDeathCount:c.onDeathCount||0, lane: x<240?'L':'R', hitFlash:0 }); } function nearestBridge(x) { return Math.abs(x-BRIDGES[0]) < Math.abs(x-BRIDGES[1]) ? BRIDGES[0] : BRIDGES[1]; } function updateUnit(u, dt) { if (u.hp <= 0) return; // spawner if (u.spawner) { u.spawnTimer += dt; if (u.spawnTimer >= u.spawnCd) { u.spawnTimer = 0; spawnUnit(u.team, u.spawner, u.x + (Math.random()-0.5)*30, u.y + (Math.random()-0.5)*30); } } // heal aura if (u.heal) { for (const o of S.units) if (o !== u && o.team === u.team && o.hp > 0 && dist(o,u) < u.range) o.hp = Math.min(o.maxHp, o.hp + u.heal*dt); } // target let target = null, bestD = 1e9; for (const o of S.units) { if (o.team === u.team || o.hp <= 0) continue; const d = dist(u,o); if (d < 150 && d < bestD) { bestD = d; target = o; } } if (!target) { const enemy = u.team === 'player' ? 'ai' : 'player'; const princess = S.towers.find(t => t.team === enemy && !t.king && t.lane === u.lane && t.hp > 0); target = princess || S.towers.find(t => t.team === enemy && t.king && t.hp > 0); } if (!target) return; const d = dist(u, target); const reach = u.range + (target.r || 20); if (d <= reach) { u.atkTimer -= dt; if (u.atkTimer <= 0) { u.atkTimer = u.cd; target.hp -= u.dmg; target.hitFlash = 0.12; if (u.lifesteal) u.hp = Math.min(u.maxHp, u.hp + u.dmg * u.lifesteal); } } else { // move toward target, routing through bridge if crossing river let tx = target.x, ty = target.y; const crossUp = u.y > RIVER_BOT && ty < RIVER_TOP; const crossDown = u.y < RIVER_TOP && ty > RIVER_BOT; if ((crossUp || crossDown) && Math.abs(u.x - nearestBridge(u.x)) > 20) { tx = nearestBridge(u.x); ty = (RIVER_TOP+RIVER_BOT)/2; } const dx = tx - u.x, dy = ty - u.y, m = Math.hypot(dx,dy) || 1; u.x += (dx/m)*u.spd*dt; u.y += (dy/m)*u.spd*dt; } if (u.hitFlash > 0) u.hitFlash -= dt; } function updateTower(t, dt) { if (t.hp <= 0) return; t.atkTimer -= dt; if (t.atkTimer > 0) return; let target = null, bestD = 1e9; for (const u of S.units) { if (u.team === t.team || u.hp <= 0) continue; const d = dist(t,u); if (d < t.range && d < bestD) { bestD = d; target = u; } } if (target) { t.atkTimer = t.cd; target.hp -= t.dmg; target.hitFlash = 0.12; } } function separate(dt) { for (let i=0;i<S.units.length;i++) for (let j=i+1;j<S.units.length;j++) { const a = S.units[i], b = S.units[j]; if (a.hp <= 0 || b.hp <= 0) continue; const dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy) || 0.01; const min = a.r + b.r; if (d < min) { const push = (min-d)/2, nx = dx/d, ny = dy/d; a.x -= nx*push; a.y -= ny*push; b.x += nx*push; b.y += ny*push; } } } function dist(a,b){ return Math.hypot(a.x-b.x, a.y-b.y); } function update(dt) { if (S.over) return; S.time += dt; S.elixir = Math.min(10, S.elixir + dt/2.8); S.aiElixir = Math.min(10, S.aiElixir + dt/2.8); for (const u of S.units) updateUnit(u, dt); for (const t of S.towers) updateTower(t, dt); separate(dt); // deaths for (const u of S.units) { if (u.hp <= 0 && !u._dead) { u._dead = true; if (u.onDeath) for (let i=0;i<u.onDeathCount;i++) spawnUnit(u.team, u.onDeath, u.x+(Math.random()-0.5)*40, u.y+(Math.random()-0.5)*40); } } S.units = S.units.filter(u => u.hp > 0); // towers dead for (const t of S.towers) { if (t.hp <= 0 && !t._dead) { t._dead = true; if (t.king) { S.over = true; S.winner = t.team === 'player' ? 'ai' : 'player'; } } } // AI S.aiTimer -= dt; if (S.aiTimer <= 0) { const hand = S.aiDeck.slice(0,4); const affordable = hand.map((k,i)=>({k,i,c:CARDS[k]})).filter(o=>o.c.cost <= S.aiElixir); if (affordable.length) { const pick = affordable[Math.floor(Math.random()*affordable.length)]; const lane = Math.random() < 0.5 ? 120 : 360; const y = 260 + Math.random()*80; spawnUnit('ai', pick.k, lane + (Math.random()-0.5)*40, y); S.aiElixir -= pick.c.cost; const [c] = S.aiDeck.splice(pick.i,1); S.aiDeck.push(c); S.aiTimer = 1.8 + Math.random()*1.6; } else { S.aiTimer = 0.6; } } } function draw() { ctx.clearRect(0,0,W,H); // field ctx.fillStyle = '#243d24'; ctx.fillRect(0,0,W,H); // river const g = ctx.createLinearGradient(0,RIVER_TOP,0,RIVER_BOT); g.addColorStop(0,'#1a4a6a'); g.addColorStop(0.5,'#2a6a9a'); g.addColorStop(1,'#1a4a6a'); ctx.fillStyle = g; ctx.fillRect(0,RIVER_TOP,W,RIVER_BOT-RIVER_TOP); // bridges for (const bx of BRIDGES) { ctx.fillStyle = '#5d4037'; ctx.fillRect(bx-30, RIVER_TOP-6, 60, RIVER_BOT-RIVER_TOP+12); ctx.fillStyle = '#3e2723'; for (let i=0;i<6;i++) ctx.fillRect(bx-30, RIVER_TOP-6 + i*((RIVER_BOT-RIVER_TOP+12)/6), 60, 2); } // deploy zone hint if (S.selected >= 0) { ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.fillRect(0, RIVER_BOT, W, H-RIVER_BOT); } // towers for (const t of S.towers) { if (t.hp <= 0) { ctx.fillStyle='rgba(60,60,60,0.5)'; ctx.beginPath(); ctx.arc(t.x,t.y,t.r,0,7); ctx.fill(); continue; } ctx.fillStyle = t.team === 'player' ? '#2980b9' : '#c0392b'; ctx.beginPath(); ctx.arc(t.x, t.y, t.r, 0, 7); ctx.fill(); if (t.hitFlash > 0) { ctx.fillStyle = 'rgba(255,255,255,'+ (t.hitFlash*6) +')'; ctx.beginPath(); ctx.arc(t.x,t.y,t.r,0,7); ctx.fill(); t.hitFlash -= 0.016; } // hp bar ctx.fillStyle = '#000'; ctx.fillRect(t.x-30, t.y-t.r-9, 60, 5); ctx.fillStyle = t.team === 'player' ? '#3498db' : '#e74c3c'; ctx.fillRect(t.x-30, t.y-t.r-9, 60*(t.hp/t.maxHp), 5); } // units for (const u of S.units) { ctx.fillStyle = u.color; ctx.beginPath(); ctx.arc(u.x, u.y, u.r, 0, 7); ctx.fill(); if (u.hitFlash > 0) { ctx.fillStyle = 'rgba(255,255,255,'+(u.hitFlash*6)+')'; ctx.beginPath(); ctx.arc(u.x,u.y,u.r,0,7); ctx.fill(); } ctx.strokeStyle = u.team === 'player' ? '#5dade2' : '#e74c3c'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(u.x,u.y,u.r,0,7); ctx.stroke(); // hp bar const bw = u.r*2; ctx.fillStyle = '#000'; ctx.fillRect(u.x-bw/2, u.y-u.r-7, bw, 3); ctx.fillStyle = u.team === 'player' ? '#3498db' : '#e74c3c'; ctx.fillRect(u.x-bw/2, u.y-u.r-7, bw*(u.hp/u.maxHp), 3); } // elixir bar ctx.fillStyle = '#000'; ctx.fillRect(10, H-70, W-20, 18); const eg = ctx.createLinearGradient(10,0,W-20,0); eg.addColorStop(0,'#8e44ad'); eg.addColorStop(1,'#e91e63'); ctx.fillStyle = eg; ctx.fillRect(10, H-70, (W-20)*(S.elixir/10), 18); ctx.fillStyle = '#fff'; ctx.font = 'bold 12px monospace'; ctx.textAlign = 'center'; ctx.fillText(Math.floor(S.elixir)+' / 10', W/2, H-56); // hand const hand = S.playerDeck.slice(0,4); for (let i=0;i<4;i++) { const x = 10 + i*(115+4), y = H-46, w = 112, h = 40; const key = hand[i], c = CARDS[key]; const sel = S.selected === i; const aff = S.elixir >= c.cost; ctx.fillStyle = sel ? '#e91e63' : (aff ? '#2c3e50' : '#1a1a1a'); ctx.fillRect(x,y,w,h); ctx.strokeStyle = sel ? '#fff' : (aff ? '#555' : '#333'); ctx.lineWidth = 2; ctx.strokeRect(x,y,w,h); ctx.fillStyle = aff ? '#fff' : '#666'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(c.name, x+6, y+16); ctx.font = '10px sans-serif'; ctx.fillStyle = aff ? '#ffd54f' : '#555'; ctx.fillText('▮'+c.cost, x+6, y+32); ctx.fillStyle = c.color; ctx.beginPath(); ctx.arc(x+w-16, y+h/2, 8, 0, 7); ctx.fill(); } if (S.over) { ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(0,0,W,H); ctx.fillStyle = S.winner === 'player' ? '#e91e63' : '#e74c3c'; ctx.font = 'bold 40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(S.winner === 'player' ? 'VICTORY' : 'DEFEAT', W/2, H/2-20); ctx.fillStyle = '#fff'; ctx.font = '16px sans-serif'; ctx.fillText('click to play again', W/2, H/2+30); } } function pointIn(x,y,rx,ry,rw,rh){ return x>=rx && x<=rx+rw && y>=ry && y<=ry+rh; } cv.addEventListener('click', e => { const r = cv.getBoundingClientRect(); const mx = (e.clientX - r.left) * (W/r.width); const my = (e.clientY - r.top) * (H/r.height); if (S.over) { init(); return; } // card select for (let i=0;i<4;i++) { if (pointIn(mx,my, 10+i*119, H-46, 112, 40)) { const c = CARDS[S.playerDeck[i]]; if (S.elixir >= c.cost) S.selected = (S.selected === i ? -1 : i); return; } } // deploy if (S.selected >= 0 && my > RIVER_BOT && my < H-50) { const key = S.playerDeck[S.selected]; const c = CARDS[key]; if (S.elixir >= c.cost) { spawnUnit('player', key, mx, my); S.elixir -= c.cost; const [card] = S.playerDeck.splice(S.selected,1); S.playerDeck.push(card); S.selected = -1; } } }); let last = performance.now(); function loop(t) { const dt = Math.min(0.05, (t-last)/1000); last = t; update(dt); draw(); requestAnimationFrame(loop); } init(); requestAnimationFrame(loop); </script> </body> </html>
Landing Page
This ad does not have a landing page available
Network Timeline
Performance Summary

1

Requests

1

Domains

13KB

Transfer Size

13KB

Content Size

94.0ms

Dom Content Loaded

204.0ms

First Paint

94.0ms

Load Time
Domain Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...
Timings (ms)
Loading...
Total Time
Loading...
Content Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...