Andro0s commited on
Commit
6a8dd9f
·
verified ·
1 Parent(s): 092e876

Upload 3 files

Browse files
Files changed (3) hide show
  1. game.js +633 -0
  2. index.html +64 -17
  3. 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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
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}