Spaces:
Running
Running
Upload 3 files
Browse files- game.js +633 -0
- index.html +64 -17
- style.css +16 -28
game.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
// Worms-style demo - full features (terrain mask, weapons, rope, jetpack, simple AI)
|
| 3 |
+
// Author: generated by ChatGPT for user demo
|
| 4 |
+
(() => {
|
| 5 |
+
// Canvas & contexts
|
| 6 |
+
const canvas = document.getElementById('gameCanvas');
|
| 7 |
+
const ctx = canvas.getContext('2d');
|
| 8 |
+
|
| 9 |
+
// Offscreen mask for destructible terrain
|
| 10 |
+
const maskCanvas = document.createElement('canvas');
|
| 11 |
+
maskCanvas.width = canvas.width;
|
| 12 |
+
maskCanvas.height = canvas.height;
|
| 13 |
+
const maskCtx = maskCanvas.getContext('2d');
|
| 14 |
+
|
| 15 |
+
// Constants & state
|
| 16 |
+
const GRAVITY = 0.45;
|
| 17 |
+
const FPS = 60;
|
| 18 |
+
let cameraX = 0;
|
| 19 |
+
let wind = { speed: 0, dir: 1 };
|
| 20 |
+
|
| 21 |
+
// Players / worms
|
| 22 |
+
let worms = [];
|
| 23 |
+
let currentIndex = 0;
|
| 24 |
+
let isPaused = false;
|
| 25 |
+
let autoFireAI = false;
|
| 26 |
+
|
| 27 |
+
const WEAPONS = [
|
| 28 |
+
{ id:'BAZOOKA', name:'Bazooka', explosion:40, fuse:0, projectileType:'SHELL' },
|
| 29 |
+
{ id:'GRENADE', name:'Granada 3s', explosion:35, fuse:180, projectileType:'GRENADE' },
|
| 30 |
+
{ id:'ROCKET', name:'Cohete', explosion:28, fuse:0, projectileType:'ROCKET' },
|
| 31 |
+
{ id:'AIRSTRIKE', name:'Airstrike', explosion:30, fuse:0, projectileType:'BOMB' }
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
// Projectiles and particles
|
| 35 |
+
let projectiles = [];
|
| 36 |
+
let particles = [];
|
| 37 |
+
|
| 38 |
+
// Controls & sensitivity
|
| 39 |
+
let moveSpeed = 4;
|
| 40 |
+
let jumpStrength = 10;
|
| 41 |
+
let powerMultiplier = 0.9;
|
| 42 |
+
let aimSensitivityDen = 24;
|
| 43 |
+
let aimAngle = Math.PI/4;
|
| 44 |
+
let currentPower = 0;
|
| 45 |
+
let isCharging = false;
|
| 46 |
+
const maxPower = 120;
|
| 47 |
+
|
| 48 |
+
// Rope & jetpack flags
|
| 49 |
+
let isRoping = false;
|
| 50 |
+
let rope = { pivot:{x:0,y:0}, length:120 };
|
| 51 |
+
let isJetpacking = false;
|
| 52 |
+
let jetFuel = 3.5; // seconds
|
| 53 |
+
|
| 54 |
+
// Terrain mask setup (draw initial blob terrain)
|
| 55 |
+
function resetTerrain() {
|
| 56 |
+
maskCtx.clearRect(0,0,maskCanvas.width,maskCanvas.height);
|
| 57 |
+
// Fill full then carve sky
|
| 58 |
+
maskCtx.fillStyle = 'black';
|
| 59 |
+
maskCtx.fillRect(0,0,maskCanvas.width,maskCanvas.height);
|
| 60 |
+
maskCtx.globalCompositeOperation = 'destination-out';
|
| 61 |
+
// Draw mountains/ground as opaque holes -> use arcs to create terrain
|
| 62 |
+
const base = maskCanvas.height * 0.7;
|
| 63 |
+
for(let x=0;x<maskCanvas.width;x+=1){
|
| 64 |
+
const h = base + Math.sin(x*0.01)*50 + (Math.random()-0.5)*20;
|
| 65 |
+
maskCtx.beginPath();
|
| 66 |
+
maskCtx.rect(x, h, 1, maskCanvas.height-h);
|
| 67 |
+
maskCtx.fill();
|
| 68 |
+
}
|
| 69 |
+
maskCtx.globalCompositeOperation = 'source-over';
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Initialize worms, place on terrain by sampling mask
|
| 73 |
+
function sampleTerrainY(x) {
|
| 74 |
+
x = Math.floor(Math.max(0, Math.min(maskCanvas.width-1, x)));
|
| 75 |
+
// scan from top until pixel is transparent (sky), then next opaque is ground: invert logic
|
| 76 |
+
const d = maskCtx.getImageData(x,0,1,maskCanvas.height).data;
|
| 77 |
+
// we filled ground as transparent (destination-out), so find first alpha < 255?
|
| 78 |
+
for(let y=0;y<maskCanvas.height;y++){
|
| 79 |
+
const a = d[y*4+3];
|
| 80 |
+
if(a === 0) { // transparent -> sky
|
| 81 |
+
// continue
|
| 82 |
+
} else {
|
| 83 |
+
// opaque => ground (we draw ground as full alpha when not erased)
|
| 84 |
+
return y;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
return maskCanvas.height - 10;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function placeWorms() {
|
| 91 |
+
worms = [
|
| 92 |
+
{ id:1, x:100, y:0, size:18, color:'#ff6bcb', vx:0, vy:0, life:100, weaponIndex:0, isAI:false, fuel:jetFuel, animFrame:0, facing:1 },
|
| 93 |
+
{ id:2, x: canvas.width-140, y:0, size:18, color:'#6bf2ff', vx:0, vy:0, life:100, weaponIndex:1, isAI:true, fuel:jetFuel, animFrame:0, facing:-1 }
|
| 94 |
+
];
|
| 95 |
+
worms.forEach(w=>{
|
| 96 |
+
const fx = Math.floor(w.x);
|
| 97 |
+
const ty = findGroundY(fx);
|
| 98 |
+
w.y = ty - w.size;
|
| 99 |
+
w.fallStartY = w.y;
|
| 100 |
+
});
|
| 101 |
+
currentIndex = 0;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function findGroundY(x) {
|
| 105 |
+
// returns y coordinate of ground surface at x (topmost opaque)
|
| 106 |
+
x = Math.floor(Math.max(0,Math.min(maskCanvas.width-1,x)));
|
| 107 |
+
const img = maskCtx.getImageData(x,0,1,maskCanvas.height).data;
|
| 108 |
+
for(let y=0;y<maskCanvas.height;y++){
|
| 109 |
+
const a = img[y*4+3];
|
| 110 |
+
if(a !== 0) return y;
|
| 111 |
+
}
|
| 112 |
+
return maskCanvas.height-10;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Terrain destruction: erase circle from mask
|
| 116 |
+
function blastTerrain(cx, cy, r) {
|
| 117 |
+
maskCtx.save();
|
| 118 |
+
maskCtx.globalCompositeOperation = 'destination-out';
|
| 119 |
+
maskCtx.beginPath();
|
| 120 |
+
maskCtx.arc(cx, cy, r, 0, Math.PI*2);
|
| 121 |
+
maskCtx.fill();
|
| 122 |
+
maskCtx.restore();
|
| 123 |
+
// spawn particles
|
| 124 |
+
for(let i=0;i<Math.min(120, r*3); i++){
|
| 125 |
+
const a = Math.random()*Math.PI*2;
|
| 126 |
+
const s = Math.random()*4 + 1;
|
| 127 |
+
particles.push({x:cx, y:cy, vx:Math.cos(a)*s, vy:Math.sin(a)*s -2, life:40 + Math.random()*40, size:1+Math.random()*2, color:'#ffb86b'});
|
| 128 |
+
}
|
| 129 |
+
// apply damage to worms
|
| 130 |
+
applyDamage(cx,cy,r);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function applyDamage(cx,cy,r) {
|
| 134 |
+
worms.forEach(w=>{
|
| 135 |
+
const dx = w.x - cx;
|
| 136 |
+
const dy = w.y - cy;
|
| 137 |
+
const dist = Math.sqrt(dx*dx + dy*dy);
|
| 138 |
+
if(dist < r*1.5) {
|
| 139 |
+
const damage = Math.floor(60 * (1 - dist/(r*1.5)));
|
| 140 |
+
if(damage>0) {
|
| 141 |
+
w.life = Math.max(0, w.life - damage);
|
| 142 |
+
// knockback
|
| 143 |
+
const k = damage*0.15;
|
| 144 |
+
if(dist>0.1) {
|
| 145 |
+
w.vx += (dx/dist)*k;
|
| 146 |
+
w.vy += (dy/dist)*k*0.6;
|
| 147 |
+
} else {
|
| 148 |
+
w.vx += (Math.random()-0.5)*k;
|
| 149 |
+
w.vy -= k;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
});
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// PROJECTILES update (with substeps for stability)
|
| 157 |
+
function updateProjectiles() {
|
| 158 |
+
const substeps = 3;
|
| 159 |
+
for(let s=0;s<substeps;s++){
|
| 160 |
+
for(let i=projectiles.length-1;i>=0;i--){
|
| 161 |
+
const p = projectiles[i];
|
| 162 |
+
// wind effect for non bombs
|
| 163 |
+
if(p.kind !== 'BOMB') {
|
| 164 |
+
p.vx += (wind.dir * wind.speed) * (p.kind === 'ROCKET' ? 0.18 : 0.08) / substeps;
|
| 165 |
+
}
|
| 166 |
+
// gravity
|
| 167 |
+
p.vy += GRAVITY / substeps;
|
| 168 |
+
p.x += p.vx / substeps;
|
| 169 |
+
p.y += p.vy / substeps;
|
| 170 |
+
|
| 171 |
+
// collision with terrain: check mask alpha at integer x,y
|
| 172 |
+
const ix = Math.floor(p.x), iy = Math.floor(p.y);
|
| 173 |
+
if(ix>=0 && ix<maskCanvas.width && iy>=0 && iy<maskCanvas.height) {
|
| 174 |
+
const a = maskCtx.getImageData(ix,iy,1,1).data[3];
|
| 175 |
+
if(a !== 0) {
|
| 176 |
+
// collision
|
| 177 |
+
if(p.type === 'GRENADE') {
|
| 178 |
+
// bounce then set fuse
|
| 179 |
+
p.vy *= -0.28;
|
| 180 |
+
p.vx *= 0.5;
|
| 181 |
+
p.detonating = true;
|
| 182 |
+
p.y = iy - p.size;
|
| 183 |
+
} else {
|
| 184 |
+
// explode
|
| 185 |
+
blastTerrain(p.x, p.y, p.explosion);
|
| 186 |
+
projectiles.splice(i,1);
|
| 187 |
+
continue;
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
// fuse ticks
|
| 192 |
+
if(p.type === 'GRENADE' && p.fuse>0) {
|
| 193 |
+
p.fuse--;
|
| 194 |
+
if(p.fuse<=0) {
|
| 195 |
+
blastTerrain(p.x, p.y, p.explosion);
|
| 196 |
+
projectiles.splice(i,1);
|
| 197 |
+
continue;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// offscreen cleanup
|
| 202 |
+
if(p.x < -200 || p.x > canvas.width+200 || p.y > canvas.height+300) {
|
| 203 |
+
projectiles.splice(i,1);
|
| 204 |
+
continue;
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// PARTICLES update
|
| 211 |
+
function updateParticles() {
|
| 212 |
+
for(let i=particles.length-1;i>=0;i--){
|
| 213 |
+
const pt = particles[i];
|
| 214 |
+
pt.vy += 0.12;
|
| 215 |
+
pt.x += pt.vx;
|
| 216 |
+
pt.y += pt.vy;
|
| 217 |
+
pt.life--;
|
| 218 |
+
pt.size *= 0.995;
|
| 219 |
+
if(pt.life<=0 || pt.size<0.2) particles.splice(i,1);
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Update worms physics
|
| 224 |
+
function updateWorms() {
|
| 225 |
+
worms.forEach(w=>{
|
| 226 |
+
// gravity if not rope controlling
|
| 227 |
+
if(!(isRoping && worms[currentIndex]===w)) w.vy += GRAVITY;
|
| 228 |
+
if(isJetpacking && worms[currentIndex]===w && w.fuel>0) {
|
| 229 |
+
w.vy -= 0.9;
|
| 230 |
+
w.fuel = Math.max(0, w.fuel - (1/FPS));
|
| 231 |
+
// small lateral control by player input (handled elsewhere)
|
| 232 |
+
}
|
| 233 |
+
w.x += w.vx;
|
| 234 |
+
w.y += w.vy;
|
| 235 |
+
// sample collision with mask
|
| 236 |
+
const ix = Math.floor(w.x);
|
| 237 |
+
const iy = Math.floor(w.y + w.size);
|
| 238 |
+
if(ix>=0 && ix<maskCanvas.width && iy>=0 && iy<maskCanvas.height) {
|
| 239 |
+
const a = maskCtx.getImageData(ix,iy,1,1).data[3];
|
| 240 |
+
if(a !== 0) {
|
| 241 |
+
// on ground - push up
|
| 242 |
+
const groundY = findGroundY(ix);
|
| 243 |
+
if(w.y + w.size > groundY) {
|
| 244 |
+
// falling damage
|
| 245 |
+
const fallDist = (w.fallStartY || w.y) - groundY;
|
| 246 |
+
if(fallDist > 120) {
|
| 247 |
+
const dmg = Math.floor((fallDist-120)*0.18);
|
| 248 |
+
w.life = Math.max(0, w.life - dmg);
|
| 249 |
+
}
|
| 250 |
+
w.y = groundY - w.size;
|
| 251 |
+
w.vy = 0;
|
| 252 |
+
w.vx *= 0.7;
|
| 253 |
+
w.fallStartY = w.y;
|
| 254 |
+
}
|
| 255 |
+
} else {
|
| 256 |
+
// in air: if previously grounded set fall start
|
| 257 |
+
if(!w.fallStartY) w.fallStartY = w.y;
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
// boundaries
|
| 261 |
+
w.x = Math.max(w.size, Math.min(canvas.width - w.size, w.x));
|
| 262 |
+
// anim frame
|
| 263 |
+
w.animFrame = (w.animFrame + 0.2) % 4;
|
| 264 |
+
});
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
// Rope physics (simple pendulum)
|
| 268 |
+
function updateRope() {
|
| 269 |
+
if(!isRoping) return;
|
| 270 |
+
const w = worms[currentIndex];
|
| 271 |
+
const dx = w.x - rope.pivot.x;
|
| 272 |
+
const dy = w.y - rope.pivot.y;
|
| 273 |
+
let ang = Math.atan2(dy,dx);
|
| 274 |
+
// simple swing influence
|
| 275 |
+
ang += 0.02;
|
| 276 |
+
w.x = rope.pivot.x + Math.cos(ang)*rope.length;
|
| 277 |
+
w.y = rope.pivot.y + Math.sin(ang)*rope.length;
|
| 278 |
+
w.vx = 0; w.vy = 0;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// Camera follow
|
| 282 |
+
function updateCamera() {
|
| 283 |
+
const target = projectiles.length>0 ? projectiles[0] : worms[currentIndex];
|
| 284 |
+
const targetX = Math.max(0, Math.min(canvas.width, (target.x || 0) - canvas.width/2 + 0));
|
| 285 |
+
cameraX += (targetX - cameraX) * 0.08;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Drawing routines
|
| 289 |
+
function draw() {
|
| 290 |
+
// clear
|
| 291 |
+
ctx.clearRect(0,0,canvas.width,canvas.height);
|
| 292 |
+
ctx.save();
|
| 293 |
+
ctx.translate(-cameraX,0);
|
| 294 |
+
|
| 295 |
+
// draw background sky gradient
|
| 296 |
+
const g = ctx.createLinearGradient(0,0,0,canvas.height);
|
| 297 |
+
g.addColorStop(0,'#7ec0ff'); g.addColorStop(1,'#2d6b2d');
|
| 298 |
+
ctx.fillStyle = g;
|
| 299 |
+
ctx.fillRect(cameraX,0,canvas.width,canvas.height);
|
| 300 |
+
|
| 301 |
+
// draw mask (terrain) by compositing maskCanvas over green ground
|
| 302 |
+
ctx.drawImage(maskCanvas, -cameraX, 0);
|
| 303 |
+
|
| 304 |
+
// draw particles
|
| 305 |
+
particles.forEach(p=>{
|
| 306 |
+
ctx.globalAlpha = Math.max(0, p.life/80);
|
| 307 |
+
ctx.fillStyle = p.color;
|
| 308 |
+
ctx.beginPath(); ctx.arc(p.x, p.y, p.size,0,Math.PI*2); ctx.fill();
|
| 309 |
+
});
|
| 310 |
+
ctx.globalAlpha = 1;
|
| 311 |
+
|
| 312 |
+
// draw projectiles
|
| 313 |
+
projectiles.forEach(p=>{
|
| 314 |
+
ctx.beginPath();
|
| 315 |
+
if(p.type==='GRENADE') ctx.fillStyle='lime';
|
| 316 |
+
else if(p.type==='ROCKET') ctx.fillStyle='orange';
|
| 317 |
+
else if(p.type==='SHELL') ctx.fillStyle='red';
|
| 318 |
+
else ctx.fillStyle='purple';
|
| 319 |
+
ctx.arc(p.x, p.y, p.size,0,Math.PI*2); ctx.fill();
|
| 320 |
+
});
|
| 321 |
+
|
| 322 |
+
// draw rope line if active
|
| 323 |
+
if(isRoping) {
|
| 324 |
+
const w = worms[currentIndex];
|
| 325 |
+
ctx.strokeStyle = '#e4c07a'; ctx.lineWidth = 3;
|
| 326 |
+
ctx.beginPath(); ctx.moveTo(rope.pivot.x, rope.pivot.y); ctx.lineTo(w.x, w.y); ctx.stroke();
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// draw worms
|
| 330 |
+
worms.forEach(w=>{
|
| 331 |
+
// shadow
|
| 332 |
+
ctx.fillStyle='rgba(0,0,0,0.3)';
|
| 333 |
+
ctx.beginPath(); ctx.ellipse(w.x, w.y + w.size*0.9, w.size*1.1, w.size*0.45, 0,0,Math.PI*2); ctx.fill();
|
| 334 |
+
// body (simulate sprite by changing ellipse width)
|
| 335 |
+
const sway = Math.sin(w.animFrame*1.6)*2;
|
| 336 |
+
ctx.fillStyle = w.color;
|
| 337 |
+
ctx.beginPath(); ctx.ellipse(w.x, w.y + sway, w.size, w.size*0.8, 0,0,Math.PI*2); ctx.fill();
|
| 338 |
+
// life bar
|
| 339 |
+
ctx.fillStyle='black'; ctx.fillRect(w.x-30, w.y - w.size - 18, 60,8);
|
| 340 |
+
ctx.fillStyle='#7fff7f'; ctx.fillRect(w.x-30, w.y - w.size - 18, 60*(w.life/100),8);
|
| 341 |
+
// fuel / small text
|
| 342 |
+
ctx.fillStyle='white'; ctx.font='11px Arial';
|
| 343 |
+
ctx.fillText(`ID:${w.id}`, w.x - 12, w.y + w.size + 14);
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
ctx.restore();
|
| 347 |
+
|
| 348 |
+
// HUD overlay
|
| 349 |
+
document.getElementById('windInfo').textContent = `${wind.dir===1? '→':'←'} ${wind.speed.toFixed(1)}`;
|
| 350 |
+
document.getElementById('turnWorm').textContent = worms[currentIndex] ? worms[currentIndex].id : '-';
|
| 351 |
+
// power bar
|
| 352 |
+
const pct = Math.min(1, currentPower / maxPower) * 100;
|
| 353 |
+
document.getElementById('powerFill').style.width = pct + '%';
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
// Simple AI: try angles and power to hit enemy by simulating trajectory
|
| 357 |
+
function aiTakeTurn(worm) {
|
| 358 |
+
const enemy = worms.find(x=>x.id!==worm.id && x.life>0);
|
| 359 |
+
if(!enemy) { endTurn(); return; }
|
| 360 |
+
// try samples
|
| 361 |
+
let best = null;
|
| 362 |
+
for(let a=10;a<=80;a+=6){
|
| 363 |
+
const ang = a * Math.PI/180;
|
| 364 |
+
for(let p=20;p<=100;p+=8){
|
| 365 |
+
// simulate simple ballistic with wind
|
| 366 |
+
const pos = simulateShot(worm.x + Math.cos(ang)*(worm.size+6), worm.y - Math.sin(ang)*(worm.size+6), Math.cos(ang)*p, -Math.sin(ang)*p, ang);
|
| 367 |
+
const dx = pos.x - enemy.x;
|
| 368 |
+
const dy = pos.y - enemy.y;
|
| 369 |
+
const dist = Math.sqrt(dx*dx+dy*dy);
|
| 370 |
+
if(!best || dist < best.dist) best = {ang,p,dist,pos};
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
if(best && best.dist < 80) {
|
| 374 |
+
// set aim and fire after short timeout
|
| 375 |
+
aimAngle = best.ang;
|
| 376 |
+
currentPower = best.p;
|
| 377 |
+
setTimeout(()=> fireWeapon(true), 600);
|
| 378 |
+
} else {
|
| 379 |
+
// move a bit with chance
|
| 380 |
+
if(Math.random()<0.5) {
|
| 381 |
+
worm.vx += (enemy.x < worm.x ? -1:1) * 2;
|
| 382 |
+
setTimeout(()=> endTurn(), 700);
|
| 383 |
+
} else {
|
| 384 |
+
// try airstrike if far
|
| 385 |
+
if(Math.random()<0.4) {
|
| 386 |
+
// set weapon to airstrike
|
| 387 |
+
worm.weaponIndex = WEAPONS.findIndex(w=>w.id==='AIRSTRIKE');
|
| 388 |
+
fireAirstrike(worm);
|
| 389 |
+
} else {
|
| 390 |
+
endTurn();
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// simulate shot forward for AI heuristics (no explosion)
|
| 397 |
+
function simulateShot(x0,y0,vx,vy,ang) {
|
| 398 |
+
let x=x0,y=y0;
|
| 399 |
+
for(let t=0;t<300;t++){
|
| 400 |
+
vx += (wind.dir*wind.speed)*0.08;
|
| 401 |
+
vy += GRAVITY;
|
| 402 |
+
x += vx*0.1; y += vy*0.1;
|
| 403 |
+
if(x<0||x>canvas.width||y>canvas.height) break;
|
| 404 |
+
}
|
| 405 |
+
return {x,y};
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// firing functions
|
| 409 |
+
function startCharging() {
|
| 410 |
+
if(isCharging || worms[currentIndex].life<=0) return;
|
| 411 |
+
isCharging = true; currentPower = 6;
|
| 412 |
+
chargeLoop();
|
| 413 |
+
}
|
| 414 |
+
function chargeLoop() {
|
| 415 |
+
if(!isCharging) return;
|
| 416 |
+
currentPower += powerMultiplier * 2.4;
|
| 417 |
+
if(currentPower > maxPower) currentPower = maxPower;
|
| 418 |
+
setTimeout(chargeLoop, 50);
|
| 419 |
+
}
|
| 420 |
+
function releaseCharge() {
|
| 421 |
+
if(!isCharging) return;
|
| 422 |
+
isCharging = false;
|
| 423 |
+
fireWeapon(false);
|
| 424 |
+
}
|
| 425 |
+
function fireWeapon(fromAI=false) {
|
| 426 |
+
const w = worms[currentIndex];
|
| 427 |
+
const weapon = WEAPONS[w.weaponIndex];
|
| 428 |
+
if(!weapon) return;
|
| 429 |
+
if(weapon.id==='BAZOOKA') {
|
| 430 |
+
projectiles.push({x:w.x + Math.cos(aimAngle)*(w.size+6), y:w.y - Math.sin(aimAngle)*(w.size+6), vx: currentPower*Math.cos(aimAngle), vy:-currentPower*Math.sin(aimAngle), size:6, type:'SHELL', explosion:weapon.explosion, kind:'SHELL'});
|
| 431 |
+
} else if(weapon.id==='GRENADE') {
|
| 432 |
+
projectiles.push({x:w.x + Math.cos(aimAngle)*(w.size+6), y:w.y - Math.sin(aimAngle)*(w.size+6), vx: currentPower*Math.cos(aimAngle), vy:-currentPower*Math.sin(aimAngle), size:6, type:'GRENADE', explosion:weapon.explosion, fuse:weapon.fuse, kind:'GRENADE'});
|
| 433 |
+
} else if(weapon.id==='ROCKET') {
|
| 434 |
+
projectiles.push({x:w.x + Math.cos(aimAngle)*(w.size+6), y:w.y - Math.sin(aimAngle)*(w.size+6), vx: (currentPower+6)*Math.cos(aimAngle), vy:-(currentPower+6)*Math.sin(aimAngle), size:5, type:'ROCKET', explosion:weapon.explosion, kind:'ROCKET'});
|
| 435 |
+
} else if(weapon.id==='AIRSTRIKE') {
|
| 436 |
+
fireAirstrike(w);
|
| 437 |
+
}
|
| 438 |
+
currentPower = 0;
|
| 439 |
+
// end turn shortly after
|
| 440 |
+
setTimeout(()=> {
|
| 441 |
+
endTurn();
|
| 442 |
+
if(!fromAI && worms[currentIndex].isAI) {
|
| 443 |
+
// if next is AI, let it decide
|
| 444 |
+
setTimeout(()=> aiTurnIfNeeded(), 600);
|
| 445 |
+
}
|
| 446 |
+
}, 700);
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function fireAirstrike(worm) {
|
| 450 |
+
const dist = 300;
|
| 451 |
+
const targetX = worm.x + Math.cos(aimAngle)*dist;
|
| 452 |
+
const cols = 5;
|
| 453 |
+
const spread = 120;
|
| 454 |
+
for(let i=0;i<cols;i++){
|
| 455 |
+
const offsetX = (i - (cols-1)/2)*(spread/(cols-1));
|
| 456 |
+
projectiles.push({x:targetX + offsetX, y:-80 - Math.random()*60, vx:0, vy:1 + Math.random()*0.6, size:6, type:'BOMB', explosion:30, kind:'BOMB'});
|
| 457 |
+
}
|
| 458 |
+
setTimeout(()=> endTurn(), 800);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function endTurn() {
|
| 462 |
+
// check game over
|
| 463 |
+
const living = worms.filter(w=>w.life>0);
|
| 464 |
+
if(living.length<=1) {
|
| 465 |
+
isPaused = true;
|
| 466 |
+
alert('Juego terminado. Refresca para reiniciar.');
|
| 467 |
+
return;
|
| 468 |
+
}
|
| 469 |
+
// advance to next alive
|
| 470 |
+
let next = (currentIndex+1)%worms.length;
|
| 471 |
+
for(let i=0;i<worms.length;i++){
|
| 472 |
+
if(worms[next].life>0) break;
|
| 473 |
+
next = (next+1)%worms.length;
|
| 474 |
+
}
|
| 475 |
+
currentIndex = next;
|
| 476 |
+
// reset some states
|
| 477 |
+
isRoping = false; isJetpacking=false; jetpackTimerReset();
|
| 478 |
+
generateWind();
|
| 479 |
+
// if AI turn, let it act
|
| 480 |
+
setTimeout(()=> aiTurnIfNeeded(), 450);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
function aiTurnIfNeeded() {
|
| 484 |
+
const w = worms[currentIndex];
|
| 485 |
+
if(w && w.life>0 && w.isAI) {
|
| 486 |
+
// small thinking delay
|
| 487 |
+
setTimeout(()=> aiTakeTurn(w), 350 + Math.random()*500);
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
function generateWind(){
|
| 492 |
+
wind.speed = 0.5 + Math.random()*2.0;
|
| 493 |
+
wind.dir = Math.random()<0.5? -1:1;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// input controls basic
|
| 497 |
+
document.getElementById('leftBtn').addEventListener('click', ()=> {
|
| 498 |
+
const w = worms[currentIndex]; if(!w || w.life<=0) return; w.vx -= moveSpeed*0.6;
|
| 499 |
+
});
|
| 500 |
+
document.getElementById('rightBtn').addEventListener('click', ()=> {
|
| 501 |
+
const w = worms[currentIndex]; if(!w || w.life<=0) return; w.vx += moveSpeed*0.6;
|
| 502 |
+
});
|
| 503 |
+
document.getElementById('jumpBtn').addEventListener('click', ()=> {
|
| 504 |
+
const w = worms[currentIndex]; if(!w) return;
|
| 505 |
+
const fx = Math.floor(w.x), gy = findGroundY(fx);
|
| 506 |
+
if(w.y + w.size >= gy - 1) {
|
| 507 |
+
w.vx += Math.cos(aimAngle)* (jumpStrength*0.45);
|
| 508 |
+
w.vy += -Math.abs(Math.sin(aimAngle))* jumpStrength;
|
| 509 |
+
w.fallStartY = w.y;
|
| 510 |
+
}
|
| 511 |
+
});
|
| 512 |
+
document.getElementById('aimUpBtn').addEventListener('click', ()=> { aimAngle += Math.PI/aimSensitivityDen; });
|
| 513 |
+
document.getElementById('aimDownBtn').addEventListener('click', ()=> { aimAngle -= Math.PI/aimSensitivityDen; });
|
| 514 |
+
|
| 515 |
+
// charge / fire UI
|
| 516 |
+
document.getElementById('fireBtn').addEventListener('mousedown', ()=> startCharging());
|
| 517 |
+
document.getElementById('fireBtn').addEventListener('touchstart', (e)=>{ e.preventDefault(); startCharging(); });
|
| 518 |
+
document.getElementById('fireBtn').addEventListener('mouseup', ()=> releaseCharge());
|
| 519 |
+
document.getElementById('fireBtn').addEventListener('touchend', ()=> releaseCharge());
|
| 520 |
+
|
| 521 |
+
document.getElementById('weaponBtn').addEventListener('click', ()=> {
|
| 522 |
+
const w = worms[currentIndex];
|
| 523 |
+
if(!w) return;
|
| 524 |
+
w.weaponIndex = (w.weaponIndex+1) % WEAPONS.length;
|
| 525 |
+
document.getElementById('weaponBtn').textContent = '🔫 ' + WEAPONS[w.weaponIndex].name;
|
| 526 |
+
});
|
| 527 |
+
|
| 528 |
+
document.getElementById('ropeBtn').addEventListener('click', ()=> {
|
| 529 |
+
if(isRoping) { isRoping=false; return; }
|
| 530 |
+
// find anchor along aim
|
| 531 |
+
const w = worms[currentIndex]; if(!w) return;
|
| 532 |
+
const maxLen=320; let found=false;
|
| 533 |
+
for(let d=40; d<=maxLen; d+=8){
|
| 534 |
+
const px = w.x + Math.cos(aimAngle)*d;
|
| 535 |
+
const py = w.y - Math.sin(aimAngle)*d;
|
| 536 |
+
const gx = Math.floor(px);
|
| 537 |
+
if(gx>=0 && gx<maskCanvas.width){
|
| 538 |
+
const a = maskCtx.getImageData(gx, Math.floor(py),1,1).data[3];
|
| 539 |
+
if(a!==0){
|
| 540 |
+
rope.pivot.x=px; rope.pivot.y=py; rope.length=d; isRoping=true; found=true; break;
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
}
|
| 544 |
+
if(!found) { for(let i=0;i<20;i++) particles.push({x:w.x, y:w.y, vx:(Math.random()-0.5)*2, vy:-Math.random()*2, life:12+Math.random()*20, size:2, color:'#fff'}); }
|
| 545 |
+
});
|
| 546 |
+
|
| 547 |
+
document.getElementById('jetPackBtn').addEventListener('click', ()=> {
|
| 548 |
+
if(isJetpacking){ isJetpacking=false; setTimeout(()=> endTurn(), 400); return; }
|
| 549 |
+
const w = worms[currentIndex]; if(!w) return;
|
| 550 |
+
// only allow if on ground
|
| 551 |
+
const gy = findGroundY(Math.floor(w.x));
|
| 552 |
+
if(w.y + w.size >= gy - 1) { isJetpacking=true; w.fuel = jetFuel; }
|
| 553 |
+
});
|
| 554 |
+
|
| 555 |
+
// panels and sliders
|
| 556 |
+
document.getElementById('catalogBtn').addEventListener('click', ()=> togglePanel('catalogPanel',true));
|
| 557 |
+
document.getElementById('sensitivityBtn').addEventListener('click', ()=> togglePanel('sensitivityPanel',true));
|
| 558 |
+
document.querySelectorAll('.close').forEach(b=> b.addEventListener('click', ()=> togglePanel(b.getAttribute('data-target'), false)));
|
| 559 |
+
|
| 560 |
+
document.getElementById('moveSpeedSlider').addEventListener('input', (e)=> { moveSpeed = +e.target.value; document.getElementById('moveSpeedValue').textContent = moveSpeed; });
|
| 561 |
+
document.getElementById('jumpStrengthSlider').addEventListener('input', (e)=> { jumpStrength = +e.target.value; document.getElementById('jumpStrengthValue').textContent = jumpStrength; });
|
| 562 |
+
document.getElementById('powerMultiplierSlider').addEventListener('input', (e)=> { powerMultiplier = +e.target.value; document.getElementById('powerMultiplierValue').textContent = powerMultiplier; });
|
| 563 |
+
document.getElementById('aimSensitivitySlider').addEventListener('input', (e)=> { aimSensitivityDen = +e.target.value; document.getElementById('aimSensitivityValue').textContent = aimSensitivityDen; });
|
| 564 |
+
|
| 565 |
+
function togglePanel(id, show) {
|
| 566 |
+
const el = document.getElementById(id);
|
| 567 |
+
el.style.display = show ? 'flex' : 'none';
|
| 568 |
+
if(show && id==='catalogPanel') populateCatalog();
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
// catalog population
|
| 572 |
+
function populateCatalog(){
|
| 573 |
+
const ul = document.getElementById('catalogList'); ul.innerHTML='';
|
| 574 |
+
WEAPONS.forEach((w,idx)=>{
|
| 575 |
+
const li=document.createElement('li'); li.className='catalog-item';
|
| 576 |
+
li.innerHTML = `<div><strong>${w.name}</strong><div>Expl:${w.explosion}</div></div><button data-idx="${idx}">Usar</button>`;
|
| 577 |
+
ul.appendChild(li);
|
| 578 |
+
});
|
| 579 |
+
ul.querySelectorAll('button').forEach(b=> b.addEventListener('click', (ev)=> {
|
| 580 |
+
const idx = +ev.target.dataset.idx;
|
| 581 |
+
const w = worms[currentIndex]; if(!w) return;
|
| 582 |
+
w.weaponIndex = idx;
|
| 583 |
+
document.getElementById('weaponBtn').textContent = '🔫 ' + WEAPONS[w.weaponIndex].name;
|
| 584 |
+
togglePanel('catalogPanel', false);
|
| 585 |
+
}));
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
// pause
|
| 589 |
+
document.getElementById('pauseBtn').addEventListener('click', ()=> { isPaused = !isPaused; document.getElementById('pauseBtn').textContent = isPaused ? 'Reanudar' : 'Pausa'; });
|
| 590 |
+
|
| 591 |
+
// auto AI toggle
|
| 592 |
+
document.getElementById('autoFireBtn').addEventListener('click', ()=> { autoFireAI = !autoFireAI; document.getElementById('autoFireBtn').textContent = autoFireAI ? '🤖 Auto ON' : '🤖 Auto'; });
|
| 593 |
+
|
| 594 |
+
// keyboard
|
| 595 |
+
window.addEventListener('keydown', (e)=>{
|
| 596 |
+
if(e.key==='a') worms[currentIndex].vx -= moveSpeed*0.6;
|
| 597 |
+
if(e.key==='d') worms[currentIndex].vx += moveSpeed*0.6;
|
| 598 |
+
if(e.key===' ') startCharging();
|
| 599 |
+
if(e.key==='p') { isPaused = !isPaused; }
|
| 600 |
+
});
|
| 601 |
+
window.addEventListener('keyup', (e)=> { if(e.key===' ') releaseCharge(); });
|
| 602 |
+
|
| 603 |
+
// jetpack timer reset
|
| 604 |
+
function jetpackTimerReset(){ worms.forEach(w=> w.fuel = jetFuel); isJetpacking=false; }
|
| 605 |
+
|
| 606 |
+
// main loop
|
| 607 |
+
function loop(ts) {
|
| 608 |
+
if(!isPaused) {
|
| 609 |
+
updateProjectiles();
|
| 610 |
+
updateParticles();
|
| 611 |
+
updateWorms();
|
| 612 |
+
updateRope();
|
| 613 |
+
updateCamera();
|
| 614 |
+
// AI auto-turn
|
| 615 |
+
if(autoFireAI) {
|
| 616 |
+
const w = worms[currentIndex]; if(w && w.isAI) aiTakeTurn(w);
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
draw();
|
| 620 |
+
requestAnimationFrame(loop);
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
// init
|
| 624 |
+
resetTerrain();
|
| 625 |
+
placeWorms();
|
| 626 |
+
generateWind();
|
| 627 |
+
document.getElementById('weaponBtn').textContent = '🔫 ' + WEAPONS[worms[currentIndex].weaponIndex].name;
|
| 628 |
+
// start loop
|
| 629 |
+
requestAnimationFrame(loop);
|
| 630 |
+
|
| 631 |
+
// expose for debugging
|
| 632 |
+
window._wormsGame = { worms, projectiles, blastTerrain, maskCanvas };
|
| 633 |
+
})();
|
index.html
CHANGED
|
@@ -1,19 +1,66 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
| 6 |
+
<title>Worms - Versión Completa (Demo)</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="uiTop">
|
| 11 |
+
<div id="info">Viento: <span id="windInfo">0</span></div>
|
| 12 |
+
<div id="turnInfo">Turno: <span id="turnWorm">-</span></div>
|
| 13 |
+
<button id="pauseBtn">Pausa</button>
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<canvas id="gameCanvas" width="1024" height="600"></canvas>
|
| 17 |
+
|
| 18 |
+
<div id="controls">
|
| 19 |
+
<div class="row">
|
| 20 |
+
<button id="leftBtn">◀️</button>
|
| 21 |
+
<button id="rightBtn">▶️</button>
|
| 22 |
+
<button id="jumpBtn">⬆️</button>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="row">
|
| 25 |
+
<button id="aimDownBtn">➖ Ángulo</button>
|
| 26 |
+
<button id="aimUpBtn">➕ Ángulo</button>
|
| 27 |
+
<button id="fireBtn">🔴 Cargar</button>
|
| 28 |
+
<button id="autoFireBtn">🤖 Auto</button>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="row">
|
| 31 |
+
<button id="ropeBtn">➰ Soga</button>
|
| 32 |
+
<button id="jetPackBtn">✈️ Jetpack</button>
|
| 33 |
+
<button id="weaponBtn">🔫 Bazooka</button>
|
| 34 |
+
<button id="catalogBtn">📖 Catálogo</button>
|
| 35 |
+
<button id="sensitivityBtn">⚙️ Sensibilidad</button>
|
| 36 |
+
</div>
|
| 37 |
+
<div id="powerBar"><div id="powerFill"></div></div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<!-- Panels -->
|
| 41 |
+
<div id="catalogPanel" class="panel">
|
| 42 |
+
<div class="panel-inner">
|
| 43 |
+
<button class="close" data-target="catalogPanel">✖</button>
|
| 44 |
+
<h3>Catálogo de armas</h3>
|
| 45 |
+
<ul id="catalogList"></ul>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div id="sensitivityPanel" class="panel">
|
| 50 |
+
<div class="panel-inner">
|
| 51 |
+
<button class="close" data-target="sensitivityPanel">✖</button>
|
| 52 |
+
<h3>Ajustes</h3>
|
| 53 |
+
<label>Velocidad: <span id="moveSpeedValue">4</span></label>
|
| 54 |
+
<input id="moveSpeedSlider" type="range" min="1" max="10" value="4" />
|
| 55 |
+
<label>Salto: <span id="jumpStrengthValue">10</span></label>
|
| 56 |
+
<input id="jumpStrengthSlider" type="range" min="6" max="22" value="10" />
|
| 57 |
+
<label>Potencia: <span id="powerMultiplierValue">0.9</span></label>
|
| 58 |
+
<input id="powerMultiplierSlider" type="range" min="0.2" max="2" step="0.1" value="0.9" />
|
| 59 |
+
<label>Ángulo sens: <span id="aimSensitivityValue">24</span></label>
|
| 60 |
+
<input id="aimSensitivitySlider" type="range" min="8" max="64" value="24" />
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<script src="game.js"></script>
|
| 65 |
+
</body>
|
| 66 |
</html>
|
style.css
CHANGED
|
@@ -1,28 +1,16 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
| 1 |
+
*{box-sizing:border-box}
|
| 2 |
+
body{margin:0;font-family:Inter,Arial;background:#0f1720;color:#e6eef8;display:flex;flex-direction:column;align-items:center}
|
| 3 |
+
#uiTop{width:100%;display:flex;justify-content:space-between;padding:8px 16px;align-items:center}
|
| 4 |
+
#gameCanvas{background:linear-gradient(#74c476,#2d6b2d);border:6px solid #05131a;box-shadow:0 8px 30px rgba(0,0,0,0.6)}
|
| 5 |
+
#controls{width:1024px;display:flex;flex-direction:column;gap:8px;padding:8px}
|
| 6 |
+
#controls .row{display:flex;gap:8px;justify-content:center}
|
| 7 |
+
button{padding:8px 12px;border-radius:8px;border:none;background:#0ea5a4;color:#001;cursor:pointer;font-weight:700}
|
| 8 |
+
button:active{transform:translateY(1px);opacity:0.9}
|
| 9 |
+
#powerBar{height:10px;background:#123;border-radius:6px;margin-top:6px;overflow:hidden}
|
| 10 |
+
#powerFill{height:100%;width:0%;background:linear-gradient(90deg,#ff5f6d,#ffc371)}
|
| 11 |
+
.panel{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(2,6,23,0.6)}
|
| 12 |
+
.panel .panel-inner{background:#082028;padding:16px;border-radius:10px;width:90%;max-width:420px;box-shadow:0 10px 30px rgba(0,0,0,0.6)}
|
| 13 |
+
.panel h3{margin-top:0}
|
| 14 |
+
.catalog-item{background:#072b2b;padding:8px;margin:6px 0;border-radius:6px;display:flex;justify-content:space-between;align-items:center}
|
| 15 |
+
#info{font-weight:700}
|
| 16 |
+
#turnInfo{font-weight:700}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|