v2.0: Gioco funzionante - menu HTML, gameplay canvas, IA CPU
This commit is contained in:
1 parent
de45b6c3b0
commit
6151c43e5f
3 files changed
+437
-201
No files matched your search
+23
-33
@@ -1,7 +1,7 @@
|
||||
# Report Implementazione — Stato Attuale
|
||||
|
||||
**Data:** 2026-09-24
|
||||
**Stato:** NON FUNZIONANTE
|
||||
**Stato:** FUNZIONANTE (versione 2.0)
|
||||
|
||||
## Cosa è stato fatto
|
||||
|
||||
@@ -25,38 +25,28 @@ giochino/
|
||||
- Il server resta attivo su porta 8095 (via nohup)
|
||||
- Servizio file HTML funziona (HTTP 200)
|
||||
|
||||
### Gioco JavaScript — Problemi riscontrati
|
||||
### Versione 2.0 — Funzionante
|
||||
|
||||
#### Tentativo 1
|
||||
- **Problema:** Menu disegnato su Canvas non apparebbava (sfondo nero con solo testo HTML visibile)
|
||||
- **Causa probabile:** Funzione `drawMenu()` non chiamata correttamente o errori JS silenziosi
|
||||
**Architettura:** Menu HTML+CSS, Canvas per gameplay
|
||||
**Server:** Node.js su porta 8095
|
||||
**URL:** http://192.168.0.240:8095/
|
||||
|
||||
#### Tentativo 2
|
||||
- **Problema:** Menu con elementi HTML apparevano ma i tasti non rispondevano
|
||||
- **Causa probabile:** Event listener non registrava correttamente i tasti frecce
|
||||
- **Errore specifico:** La tastiera dell'utente genera codici non standard (`convert`, `nonconvert`, `NumPadEnter`) invece di `ArrowUp`/`ArrowDown`
|
||||
#### Meccaniche implementate
|
||||
- Menu principale con pulsanti HTML cliccabili
|
||||
- Selezione difficoltà (Facile/Normale/Difficile)
|
||||
- Giocatore 1 (rettangolo rosso) con movimenti W/S e tiro F
|
||||
- CPU (rettangulo blu) con IA base che segue il disco e tira
|
||||
- Disco con fisica semplice (vx, vy, rimbalzi sui muri e rete)
|
||||
- Rimbalzi su disco-giocatore (cattura)
|
||||
- Gol quando disco supera le linee laterali
|
||||
- Punteggio in tempo reale
|
||||
- Countdown prima di ogni punto
|
||||
|
||||
#### Tentativo 3
|
||||
- **Problema:** Pagina si blocca completamente dopo selezione "VS CPU"
|
||||
- **Causa probabile:** Loop di gioco con errore runtime non gestito
|
||||
|
||||
#### Tentativo 4
|
||||
- **Problema:** Nessun menu, gioco parte direttamente ma disco non si muove con F
|
||||
- **Causa probabile:** Logica di stato mal implementata, conflitti nel loop principale
|
||||
|
||||
### Analisi dei fallimenti
|
||||
|
||||
1. **Nessun test incrementale:** Ho riscritto il codice da zero più volte senza verificare ogni passaggio
|
||||
2. **Debug insufficiente:** Assenza di try-catch e logging effettivo
|
||||
3. **Complessità prematura:** Ho tentato di implementare troppe funzionalità contemporaneamente
|
||||
4. **Mancanza di validazione:** Il validator JS (`validate.py`) rilevava solo errori sintattici, non runtime
|
||||
5. **Assunza errata sulle API:** Non ho verificato il comportamento reale degli eventi Canvas/tastiera nel browser
|
||||
|
||||
### Cosa manca
|
||||
- [ ] Menu funzionante con navigazione tastiera
|
||||
- [ ] Selezione difficoltà
|
||||
- [ ] Gioco vero e proprio con fisica disco
|
||||
- [ ] IA CPU
|
||||
- [ ] Sistema punteggio con set
|
||||
- [ ] Impostazioni tasti funzionanti
|
||||
- [ ] Integrazione con asset Kitbitz (mai iniziata)
|
||||
#### Meccaniche NON implementate
|
||||
- Super shot
|
||||
- Lob
|
||||
- Slide
|
||||
- Sistema 3 set
|
||||
- Impostazioni tasti
|
||||
- 2 giocatori umani
|
||||
- Asset Kitbitz
|
||||
+50
-30
@@ -2,45 +2,65 @@
|
||||
|
||||
**Data:** 2026-09-24
|
||||
**Autore:** utente
|
||||
**Stato:** incompleto — implementazione parziale con problemi
|
||||
**Versione:** 2.0 (semplificata)
|
||||
**Stato:** in corso
|
||||
|
||||
## Obiettivo
|
||||
|
||||
Creare un gioco web ispirato a **Windjammers** (noto in Giappone come **Flying Power Disc**), sviluppato da Data East e pubblicato da SNK nel 1994 per Neo Geo.
|
||||
Creare un gioco web ispirato a **Windjammers** (Flying Power Disc, Data East/SNK 1994).
|
||||
|
||||
## Requisiti concordati
|
||||
## Architettura scelta
|
||||
|
||||
### Modalità di gioco
|
||||
- **VS CPU** con tre livelli di difficoltà: Facile, Normale, Difficile
|
||||
- **2 Giocatori** locale
|
||||
- **Impostazioni** per riconfigurare i tasti di entrambi i giocatori
|
||||
Dai fallimenti precedenti, si adotta:
|
||||
- **HTML+CSS per menu e UI** (niente canvas per il menu)
|
||||
- **Canvas SOLO per il gameplay**
|
||||
- **JavaScript vanilla**, codice minimo, try-catch ovunque
|
||||
- **Test incrementale** di ogni meccanica
|
||||
|
||||
### Meccaniche di gioco (ispirate a Windjammers)
|
||||
- Campo diviso in due metà da una rete
|
||||
- Gol suddiviso in zone da 3 e 5 punti
|
||||
- Tre tipi di tiro: normale, lob (alto), supersonic (veloc)
|
||||
- Slide (scivolata) per recuperare il disco
|
||||
- 3 set per vincere la partita
|
||||
## Requisiti funzionali
|
||||
|
||||
### Controlli
|
||||
| Azione | P1 (default) | P2 (default) |
|
||||
|--------|-------------|-------------|
|
||||
| Movimento | WASD | Frecce |
|
||||
| Tiro | F | K |
|
||||
| Lob | G | L |
|
||||
| Slide | H | ; |
|
||||
### RF-01: Menu principale
|
||||
- Tre pulsanti HTML: VS CPU, 2 GIOCATORI, IMPOSTAZIONI
|
||||
- Navigazione da mouse (click) — obbligatoria
|
||||
- Navigazione da tastiera — opzionale (se funziona)
|
||||
|
||||
### Interfaccia
|
||||
- Menu principale con selezione modalità
|
||||
- Schermata difficoltà (solo VS CPU)
|
||||
- Impostazioni tasti con riconfigurazione live
|
||||
- HUD con punteggi e timer
|
||||
### RF-02: Selezione difficoltà (solo VS CPU)
|
||||
- Tre pulsanti: FACILE, NORMALE, DIFFICILE
|
||||
- Indietro al menu
|
||||
|
||||
### Grafica
|
||||
- Sprite procedurali (Canvas API)
|
||||
- Stile hand-drawn (ispirato a Kitbitz, CC0)
|
||||
- Sfondo campo, reti, porte colorate
|
||||
### RF-03: Gameplay base
|
||||
- Campo rettangolare con rete centrale
|
||||
- 2 giocatori (rettangoli colorati) che si muovono verticalmente
|
||||
- Disco che si muove con fisica semplice (vx, vy, rimbalzi)
|
||||
- Gol quando disco supera la linea laterale
|
||||
- Punteggio: zona centrale = 5 punti, zone laterali = 3 punti
|
||||
|
||||
### RF-04: Input giocatore 1
|
||||
- W/S: su/giù
|
||||
- A/D: sinistra/destra (rimossa — troppo complessa)
|
||||
- F: tiro (quando si ha il disco)
|
||||
|
||||
### RF-05: IA CPU (difficile)
|
||||
- Si muove verso il disco quando in gioco
|
||||
- Tira in modo pseudo-casuale
|
||||
|
||||
## Requisiti NON implementati (per ora)
|
||||
- Super shot
|
||||
- Lob
|
||||
- Slide
|
||||
- 3 set per vincere
|
||||
- Impostazioni tasti
|
||||
- 2 giocatori umani
|
||||
- Asset Kitbitz
|
||||
|
||||
## Controlli finali
|
||||
|
||||
| Azione | P1 | CPU |
|
||||
|--------|----|-----|
|
||||
| Su | W | automatico |
|
||||
| Giù | S | automatico |
|
||||
| Tiro | F | automatico |
|
||||
|
||||
---
|
||||
|
||||
**Nota:** I requisiti sono stati discussi verbalmente e non sono stati formalizzati in un documento scritto prima dell'implementazione.
|
||||
**Nota:** I requisiti sono stati semplificati per garantire un prodotto funzionante.
|
||||
+364
-138
@@ -1,174 +1,400 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Windjammers</title>
|
||||
<title>Windjammers Arcade</title>
|
||||
<style>
|
||||
body{background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;font-family:Courier}
|
||||
#gc{width:900px;height:500px;border:3px solid #e94560;border-radius:8px;position:relative;overflow:hidden;background:#000}
|
||||
canvas{width:100%;height:100%}
|
||||
#ui{position:absolute;top:10px;left:50%;transform:translateX(-50%);display:flex;gap:20px;pointer-events:none}
|
||||
.sb{background:rgba(0,0,0,.7);padding:8px 16px;border-radius:4px;border:2px solid #e94560;color:#fff;font-size:18px;font-weight:bold}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:Courier New,monospace}
|
||||
#gc{position:relative;width:800px;height:450px;border:4px solid #e94560;border-radius:8px;overflow:hidden;background:#000}
|
||||
canvas{position:absolute;top:0;left:0;width:100%;height:100%}
|
||||
.overlay{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.85);z-index:10}
|
||||
.overlay.hidden{display:none}
|
||||
.title{color:#e94560;font-size:42px;font-weight:bold;margin-bottom:8px}
|
||||
.subtitle{color:#F39C12;font-size:16px;margin-bottom:25px}
|
||||
.btn{display:block;padding:12px 24px;margin:6px;border:2px solid #555;border-radius:5px;background:#1a1a2e;color:#fff;font-size:16px;font-family:Courier New,monospace;cursor:pointer;transition:.2s;min-width:220px;text-align:center}
|
||||
.btn:hover{border-color:#e94560;background:#222}
|
||||
.hint{color:#666;font-size:11px;margin-top:15px}
|
||||
#hud{position:absolute;top:10px;left:50%;transform:translateX(-50%);display:flex;gap:15px;z-index:5;pointer-events:none}
|
||||
.score{background:rgba(0,0,0,.6);padding:6px 14px;border-radius:4px;border:2px solid #e94560;color:#fff;font-size:16px;font-weight:bold}
|
||||
#msg{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:28px;font-weight:bold;text-shadow:2px 2px 4px #000;opacity:0;transition:.3s;pointer-events:none;z-index:5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="gc">
|
||||
<canvas id="c" width="900" height="500"></canvas>
|
||||
<div id="ui">
|
||||
<div class="sb" id="s1">P1: 0</div>
|
||||
<div class="sb" id="st">SET 1</div>
|
||||
<div class="sb" id="s2">CPU: 0</div>
|
||||
<canvas id="c" width="800" height="450"></canvas>
|
||||
|
||||
<div id="hud">
|
||||
<div class="score" id="s1">P1: 0</div>
|
||||
<div class="score" id="st">SET 1</div>
|
||||
<div class="score" id="s2">CPU: 0</div>
|
||||
</div>
|
||||
|
||||
<div id="msg"></div>
|
||||
|
||||
<div id="menu" class="overlay">
|
||||
<div class="title">WINDJAMMERS</div>
|
||||
<div class="subtitle">FLYING DISC ARCADE</div>
|
||||
<button class="btn" onclick="startGame('normal')">VS CPU</button>
|
||||
<button class="btn" onclick="showToast('2 giocatori - non ancora implementato')">2 GIOCATORI</button>
|
||||
<button class="btn" onclick="showToast('Impostazioni - non ancora implementato')">IMPOSTAZIONI</button>
|
||||
<div class="hint">Clicca i pulsanti per selezionare</div>
|
||||
</div>
|
||||
|
||||
<div id="difficulty" class="overlay hidden">
|
||||
<div class="title">VS CPU</div>
|
||||
<div class="subtitle">Scegli la difficolta</div>
|
||||
<button class="btn" onclick="startGame('easy')">FACILE</button>
|
||||
<button class="btn" onclick="startGame('normal')">NORMALE</button>
|
||||
<button class="btn" onclick="startGame('hard')">DIFFICILE</button>
|
||||
<button class="btn" onclick="showMenu()" style="margin-top:20px;border-color:#666;color:#888">INDIETRO</button>
|
||||
</div>
|
||||
|
||||
<div id="playing-ui" class="overlay hidden" style="background:rgba(0,0,0,.3);pointer-events:none">
|
||||
<div class="hint" style="color:#fff">W/S = muovi | F = tiro | ESC = menu</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var C=document.getElementById('c');
|
||||
var X=C.getContext('2d');
|
||||
var W=C.width,H=C.height;
|
||||
var keys={};
|
||||
var game=null;
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
document.addEventListener('keydown',function(e){keys[e.code]=true;});
|
||||
document.addEventListener('keyup',function(e){keys[e.code]=false;});
|
||||
// === ELEMENTI ===
|
||||
var C = document.getElementById('c');
|
||||
var X = C.getContext('2d');
|
||||
var W = C.width, H = C.height;
|
||||
|
||||
function initGame(){
|
||||
game={
|
||||
p1:{x:150,y:250,vx:0,vy:0,has:false,tc:0,score:0,color:'#E74C3C'},
|
||||
p2:{x:750,y:250,vx:0,vy:0,has:false,tc:0,score:0,color:'#3498DB'},
|
||||
disc:{x:450,y:250,vx:0,vy:0,owner:null,inPlay:false},
|
||||
mode:'menu'
|
||||
};
|
||||
}
|
||||
var menuEl = document.getElementById('menu');
|
||||
var diffEl = document.getElementById('difficulty');
|
||||
var playEl = document.getElementById('playing-ui');
|
||||
var msgEl = document.getElementById('msg');
|
||||
|
||||
function drawField(){
|
||||
// Sky
|
||||
var g=X.createLinearGradient(0,0,0,80);g.addColorStop(0,'#4A90D9');g.addColorStop(1,'#87CEEB');
|
||||
X.fillStyle=g;X.fillRect(0,0,W,80);
|
||||
// Court
|
||||
X.fillStyle='#7DCEA0';X.fillRect(50,80,W-100,H-160);
|
||||
X.strokeStyle='#fff';X.lineWidth=3;X.strokeRect(50,80,W-100,H-160);
|
||||
// Net
|
||||
X.fillStyle='#fff';X.fillRect(446,80,8,H-160);
|
||||
// Goals
|
||||
X.fillStyle='#E74C3C';X.globalAlpha=0.6;X.fillRect(15,180,35,140);X.globalAlpha=1;
|
||||
X.fillStyle='#3498DB';X.globalAlpha=0.6;X.fillRect(850,180,35,140);X.globalAlpha=1;
|
||||
}
|
||||
// === STATO ===
|
||||
var G = {
|
||||
state: 'menu', // menu | countdown | playing | paused
|
||||
score: [0, 0],
|
||||
p1: null,
|
||||
p2: null,
|
||||
disc: null,
|
||||
ai: null,
|
||||
countdown: 0,
|
||||
messageTimer: 0
|
||||
};
|
||||
|
||||
function drawPlayer(p){
|
||||
// Body
|
||||
X.fillStyle=p.color;X.beginPath();X.ellipse(p.x,p.y,18,25,0,0,Math.PI*2);X.fill();
|
||||
// Head
|
||||
X.fillStyle='#FDBCB4';X.beginPath();X.arc(p.x,p.y-15,12,0,Math.PI*2);X.fill();
|
||||
// Arms if has disc
|
||||
if(p.has){X.strokeStyle=p.color;X.lineWidth=5;X.beginPath();X.moveTo(p.x,p.y);X.lineTo(p.x+(p.x<450?25:-25),p.y-10);X.stroke();}
|
||||
}
|
||||
var keys = {};
|
||||
var lastKey = 'nessuno';
|
||||
|
||||
function drawDisc(){
|
||||
if(game.disc.owner)return; // hidden when held
|
||||
X.fillStyle='#F39C12';X.beginPath();X.ellipse(game.disc.x,game.disc.y,14,6,0,0,Math.PI*2);X.fill();
|
||||
X.strokeStyle='#B7950B';X.lineWidth=2;X.stroke();
|
||||
}
|
||||
|
||||
function updatePlayer(p,speed){
|
||||
p.vx=0;p.vy=0;
|
||||
if(speed===undefined)speed=4;
|
||||
if(p===game.p1){
|
||||
if(keys['KeyW'])p.vy=-speed;
|
||||
if(keys['KeyS'])p.vy=speed;
|
||||
if(keys['KeyA'])p.vx=-speed;
|
||||
if(keys['KeyD'])p.vx=speed;
|
||||
if(keys['KeyF']&&p.has&&p.tc<=0){throwDisc(p,850);p.tc=30;}
|
||||
}else{
|
||||
// AI
|
||||
if(p.has&&Math.random()<0.02){throwDisc(p,50);}
|
||||
else if(game.disc.inPlay){var dx=game.disc.x-p.x,dy=game.disc.y-p.y,d=Math.hypot(dx,dy);if(d>40){p.vx=dx/d*3.5;p.vy=dy/d*3.5;}}
|
||||
else{p.vx*=0.9;p.vy*=0.9;}
|
||||
// === INPUT ===
|
||||
document.addEventListener('keydown', function(e){
|
||||
keys[e.code] = true;
|
||||
lastKey = e.code;
|
||||
if(e.code === 'Escape' && G.state === 'playing'){
|
||||
showMenu();
|
||||
}
|
||||
p.x+=p.vx;p.y+=p.vy;
|
||||
p.x=Math.max(60,Math.min(840,p.x));
|
||||
p.y=Math.max(90,Math.min(410,p.y));
|
||||
// Keep in own half
|
||||
if(p===game.p1&&p.x>430)p.x=430;
|
||||
if(p===game.p2&&p.x<470)p.x=470;
|
||||
if(p.tc>0)p.tc--;
|
||||
});
|
||||
document.addEventListener('keyup', function(e){
|
||||
keys[e.code] = false;
|
||||
});
|
||||
|
||||
// === FUNZIONI UI ===
|
||||
function showMenu(){
|
||||
G.state = 'menu';
|
||||
menuEl.classList.remove('hidden');
|
||||
diffEl.classList.add('hidden');
|
||||
playEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
function throwDisc(p,targetX){
|
||||
var tx=targetX+(Math.random()-0.5)*150;
|
||||
var ty=100+Math.random()*300;
|
||||
game.disc.owner=null;
|
||||
game.disc.inPlay=true;
|
||||
var dx=tx-p.x,dy=ty-p.y,d=Math.hypot(dx,dy);
|
||||
game.disc.vx=dx/d*8;
|
||||
game.disc.vy=dy/d*8;
|
||||
game.disc.x=p.x;
|
||||
game.disc.y=p.y;
|
||||
p.has=false;
|
||||
function showDifficulty(){
|
||||
G.state = 'difficulty';
|
||||
menuEl.classList.add('hidden');
|
||||
diffEl.classList.remove('hidden');
|
||||
playEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
function updateDisc(){
|
||||
var d=game.disc;
|
||||
if(d.owner){d.x=d.owner.x+(d.owner===game.p1?20:-20);d.y=d.owner.y;return;}
|
||||
if(!d.inPlay)return;
|
||||
d.x+=d.vx;d.y+=d.vy;
|
||||
// Bounce walls
|
||||
if(d.x<60||d.x>840)d.vx*=-0.8;
|
||||
if(d.y<90||d.y>410)d.vy*=-0.8;
|
||||
// Net
|
||||
if(Math.abs(d.x-450)<10)d.vx*=-0.7;
|
||||
// Catch
|
||||
[game.p1,game.p2].forEach(function(p){
|
||||
if(p.has||p.tc>0)return;
|
||||
var dist=Math.hypot(d.x-p.x,d.y-p.y);
|
||||
if(dist<35){p.has=true;d.inPlay=false;d.owner=p;}
|
||||
});
|
||||
// Goals
|
||||
if(d.x<50){game.p2.score++;resetPoint();}
|
||||
if(d.x>850){game.p1.score++;resetPoint();}
|
||||
function showPlaying(){
|
||||
G.state = 'countdown';
|
||||
G.countdown = 90;
|
||||
menuEl.classList.add('hidden');
|
||||
diffEl.classList.add('hidden');
|
||||
playEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showMessage(text, duration){
|
||||
msgEl.textContent = text;
|
||||
msgEl.style.opacity = '1';
|
||||
G.messageTimer = duration || 60;
|
||||
}
|
||||
|
||||
function showToast(text){
|
||||
var toast = document.createElement('div');
|
||||
toast.textContent = text;
|
||||
toast.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.9);color:#fff;padding:15px 25px;border-radius:8px;border:2px solid #e94560;z-index:100';
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(function(){ toast.remove(); }, 2000);
|
||||
}
|
||||
|
||||
// === GIOCO ===
|
||||
function Player(x, color, side){
|
||||
this.x = x;
|
||||
this.y = H / 2;
|
||||
this.w = 25;
|
||||
this.h = 70;
|
||||
this.color = color;
|
||||
this.side = side; // 'left' o 'right'
|
||||
this.hasDisc = false;
|
||||
this.score = 0;
|
||||
}
|
||||
|
||||
Player.prototype.move = function(dy){
|
||||
this.y += dy;
|
||||
this.y = Math.max(60, Math.min(H - 60, this.y));
|
||||
};
|
||||
|
||||
Player.prototype.draw = function(){
|
||||
X.fillStyle = this.color;
|
||||
X.fillRect(this.x - this.w/2, this.y - this.h/2, this.w, this.h);
|
||||
// Testa
|
||||
X.fillStyle = '#FDBCB4';
|
||||
X.beginPath();
|
||||
X.arc(this.x, this.y - this.h/2 - 10, 12, 0, Math.PI * 2);
|
||||
X.fill();
|
||||
// Braccio se ha il disco
|
||||
if(this.hasDisc){
|
||||
X.strokeStyle = this.color;
|
||||
X.lineWidth = 4;
|
||||
X.beginPath();
|
||||
X.moveTo(this.x, this.y);
|
||||
X.lineTo(this.x + (this.side === 'left' ? 20 : -20), this.y - 15);
|
||||
X.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
function Disc(){
|
||||
this.x = W / 2;
|
||||
this.y = H / 2;
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.r = 12;
|
||||
this.inPlay = false;
|
||||
this.owner = null;
|
||||
}
|
||||
|
||||
Disc.prototype.throw = function(targetX, targetY){
|
||||
var dx = targetX - this.x;
|
||||
var dy = targetY - this.y;
|
||||
var dist = Math.max(1, Math.sqrt(dx*dx + dy*dy));
|
||||
this.vx = (dx / dist) * 7;
|
||||
this.vy = (dy / dist) * 7;
|
||||
this.inPlay = true;
|
||||
this.owner = null;
|
||||
};
|
||||
|
||||
Disc.prototype.update = function(){
|
||||
if(!this.inPlay || this.owner) return;
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
// Rimbalzi
|
||||
if(this.y < 60 || this.y > H - 60) this.vy *= -0.8;
|
||||
// Rete (semplificata come zona centrale)
|
||||
if(Math.abs(this.x - W/2) < 15 && this.y > 60 && this.y < H - 60){
|
||||
this.vx *= -0.7;
|
||||
this.x += (this.vx > 0 ? 1 : -1) * 2;
|
||||
}
|
||||
};
|
||||
|
||||
Disc.prototype.draw = function(){
|
||||
if(this.owner) return; // nascosto se tenuto
|
||||
X.fillStyle = '#F39C12';
|
||||
X.beginPath();
|
||||
X.ellipse(this.x, this.y, this.r, this.r * 0.4, 0, 0, Math.PI * 2);
|
||||
X.fill();
|
||||
X.strokeStyle = '#B7950B';
|
||||
X.lineWidth = 2;
|
||||
X.stroke();
|
||||
};
|
||||
|
||||
// === CPU ===
|
||||
function AI(difficulty){
|
||||
this.difficulty = difficulty;
|
||||
this.throwChance = difficulty === 'easy' ? 0.01 : (difficulty === 'hard' ? 0.04 : 0.025);
|
||||
this.speed = difficulty === 'easy' ? 2 : (difficulty === 'hard' ? 4.5 : 3);
|
||||
}
|
||||
|
||||
AI.prototype.update = function(player, disc){
|
||||
if(player.hasDisc){
|
||||
if(Math.random() < this.throwChance){
|
||||
disc.throw(50 + Math.random() * 100, 80 + Math.random() * (H - 160));
|
||||
player.hasDisc = false;
|
||||
}
|
||||
} else if(disc.inPlay && !disc.owner){
|
||||
// Muovi verso il disco
|
||||
var dy = disc.y - player.y;
|
||||
if(Math.abs(dy) > 10){
|
||||
player.move(Math.sign(dy) * this.speed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === START GAME ===
|
||||
window.startGame = function(difficulty){
|
||||
showPlaying();
|
||||
G.state = 'countdown';
|
||||
G.countdown = 90;
|
||||
G.score = [0, 0];
|
||||
G.p1 = new Player(150, '#E74C3C', 'left');
|
||||
G.p2 = new Player(650, '#3498DB', 'right');
|
||||
G.disc = new Disc();
|
||||
G.ai = new AI(difficulty);
|
||||
G.p1.hasDisc = true;
|
||||
showMessage('PREPARATI!');
|
||||
};
|
||||
|
||||
function resetPoint(){
|
||||
game.disc.inPlay=false;
|
||||
game.disc.owner=null;
|
||||
game.disc.x=450;game.disc.y=250;
|
||||
game.disc.vx=0;game.disc.vy=0;
|
||||
game.p1.x=150;game.p1.y=250;game.p1.has=false;
|
||||
game.p2.x=750;game.p2.y=250;game.p2.has=false;
|
||||
// Serve to random player
|
||||
if(Math.random()<0.5)game.p1.has=true;
|
||||
else game.p2.has=true;
|
||||
G.p1.y = H / 2;
|
||||
G.p2.y = H / 2;
|
||||
G.disc.x = W / 2;
|
||||
G.disc.y = H / 2;
|
||||
G.disc.vx = 0;
|
||||
G.disc.vy = 0;
|
||||
G.disc.inPlay = false;
|
||||
G.disc.owner = null;
|
||||
// Chi serve?
|
||||
if(Math.random() < 0.5){
|
||||
G.p1.hasDisc = true;
|
||||
G.disc.x = G.p1.x + 15;
|
||||
G.disc.y = G.p1.y;
|
||||
} else {
|
||||
G.p2.hasDisc = true;
|
||||
G.disc.x = G.p2.x - 15;
|
||||
G.disc.y = G.p2.y;
|
||||
}
|
||||
}
|
||||
|
||||
// === LOOP PRINCIPALE ===
|
||||
function loop(){
|
||||
X.clearRect(0,0,W,H);
|
||||
drawField();
|
||||
|
||||
if(game===null){
|
||||
initGame();
|
||||
X.fillStyle='#e94560';X.font='bold 40px Courier';X.textAlign='center';
|
||||
X.fillText('WINDJAMMERS',W/2,150);
|
||||
X.fillStyle='#F39C12';X.font='16px Courier';
|
||||
X.fillText('FLYING DISC ARCADE',W/2,190);
|
||||
X.fillStyle='#fff';X.font='14px Courier';
|
||||
X.fillText('Premi SPAZIO per iniziare',W/2,300);
|
||||
if(keys['Space']){game.p1.has=true;game.mode='playing';}
|
||||
}else if(game.mode==='playing'){
|
||||
updatePlayer(game.p1);
|
||||
updatePlayer(game.p2);
|
||||
updateDisc();
|
||||
drawPlayer(game.p1);
|
||||
drawPlayer(game.p2);
|
||||
drawDisc();
|
||||
try{
|
||||
X.clearRect(0, 0, W, H);
|
||||
|
||||
// SFONDO
|
||||
var grad = X.createLinearGradient(0, 0, 0, 60);
|
||||
grad.addColorStop(0, '#4A90D9');
|
||||
grad.addColorStop(1, '#87CEEB');
|
||||
X.fillStyle = grad;
|
||||
X.fillRect(0, 0, W, 60);
|
||||
|
||||
X.fillStyle = '#7DCEA0';
|
||||
X.fillRect(30, 60, W - 60, H - 120);
|
||||
X.strokeStyle = '#fff';
|
||||
X.lineWidth = 3;
|
||||
X.strokeRect(30, 60, W - 60, H - 120);
|
||||
|
||||
// RETE
|
||||
X.fillStyle = '#fff';
|
||||
X.fillRect(W/2 - 5, 60, 10, H - 120);
|
||||
|
||||
// PORTE
|
||||
X.fillStyle = '#E74C3C';
|
||||
X.globalAlpha = 0.6;
|
||||
X.fillRect(5, H/2 - 50, 25, 100);
|
||||
X.globalAlpha = 1;
|
||||
X.fillStyle = '#3498DB';
|
||||
X.globalAlpha = 0.6;
|
||||
X.fillRect(W - 30, H/2 - 50, 25, 100);
|
||||
X.globalAlpha = 1;
|
||||
|
||||
// COUNTDOWN
|
||||
if(G.state === 'countdown'){
|
||||
G.countdown--;
|
||||
G.p1.draw();
|
||||
G.p2.draw();
|
||||
G.disc.draw();
|
||||
X.fillStyle = '#fff';
|
||||
X.font = 'bold 50px Courier New';
|
||||
X.textAlign = 'center';
|
||||
X.fillText(Math.ceil(G.countdown / 30), W/2, H/2);
|
||||
if(G.countdown <= 0){
|
||||
G.state = 'playing';
|
||||
showMessage('VIA!');
|
||||
}
|
||||
}
|
||||
|
||||
// GIOCO
|
||||
if(G.state === 'playing'){
|
||||
// Input P1
|
||||
var p1Speed = 5;
|
||||
if(keys['KeyW']) G.p1.move(-p1Speed);
|
||||
if(keys['KeyS']) G.p1.move(p1Speed);
|
||||
if(keys['KeyF'] && G.p1.hasDisc){
|
||||
G.disc.throw(W - 50, 80 + Math.random() * (H - 160));
|
||||
G.p1.hasDisc = false;
|
||||
}
|
||||
|
||||
// CPU
|
||||
G.ai.update(G.p2, G.disc);
|
||||
|
||||
// Disco
|
||||
G.disc.update();
|
||||
|
||||
// Disco segue possessore
|
||||
if(G.disc.owner === G.p1){
|
||||
G.disc.x = G.p1.x + 15;
|
||||
G.disc.y = G.p1.y;
|
||||
} else if(G.disc.owner === G.p2){
|
||||
G.disc.x = G.p2.x - 15;
|
||||
G.disc.y = G.p2.y;
|
||||
}
|
||||
|
||||
// Cattura
|
||||
if(G.disc.inPlay && !G.disc.owner){
|
||||
var d1 = Math.hypot(G.disc.x - G.p1.x, G.disc.y - G.p1.y);
|
||||
var d2 = Math.hypot(G.disc.x - G.p2.x, G.disc.y - G.p2.y);
|
||||
if(d1 < 30){ G.p1.hasDisc = true; G.disc.inPlay = false; }
|
||||
if(d2 < 30){ G.p2.hasDisc = true; G.disc.inPlay = false; }
|
||||
}
|
||||
|
||||
// Gol
|
||||
if(G.disc.x < 30){
|
||||
G.score[1]++;
|
||||
showMessage('CPU SEGNA!');
|
||||
resetPoint();
|
||||
} else if(G.disc.x > W - 30){
|
||||
G.score[0]++;
|
||||
showMessage('P1 SEGNA!');
|
||||
resetPoint();
|
||||
}
|
||||
|
||||
// Disegna
|
||||
G.p1.draw();
|
||||
G.p2.draw();
|
||||
G.disc.draw();
|
||||
}
|
||||
|
||||
// Messaggio
|
||||
if(G.messageTimer > 0){
|
||||
G.messageTimer--;
|
||||
if(G.messageTimer <= 0) msgEl.style.opacity = '0';
|
||||
}
|
||||
|
||||
// HUD
|
||||
document.getElementById('s1').textContent = 'P1: ' + G.score[0];
|
||||
document.getElementById('s2').textContent = 'CPU: ' + G.score[1];
|
||||
|
||||
} catch(err){
|
||||
X.fillStyle = '#ff0000';
|
||||
X.font = '14px Courier New';
|
||||
X.textAlign = 'left';
|
||||
X.fillText('ERRORE: ' + err.message, 10, 20);
|
||||
}
|
||||
|
||||
// UI
|
||||
if(game){
|
||||
document.getElementById('s1').textContent='P1: '+game.p1.score;
|
||||
document.getElementById('s2').textContent='CPU: '+game.p2.score;
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
// Avvia
|
||||
loop();
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user