function syncState() {
if(mode !== null || gameOver) return;
$.ajax({
url: 'api/action_game.php',
type: 'POST',
data: { action: 'get_state' },
dataType: 'json',
success: function(res) {
if (res.gameState && res.gameState.game_over === true) {
window.location.href = 'index.php';
return;
}
if(res.gameState) {
if (res.gameState.win === 'WIN') { showEndScreen("VICTOIRE", "L'ennemi a été éliminé.", true); return; }
if (res.gameState.win === 'LOSE') { showEndScreen("DÉFAITE", "Votre flotte a été anéantie.", false); return; }
ships = res.gameState.entities;
currentTurn = res.gameState.turn;
if (currentTurn !== previousTurn) {
visualTimer = 60;
previousTurn = currentTurn;
$('#status-text').text("Nouveau tour tactique.");
}
renderGame();
updateHud(res.msg);
}
}
});
}
setInterval (1s). Chaque seconde la variable locale perd 1. Si elle arrive à 0, elle appelle endTurn() pour forcer le passage de tour côté client.
function startVisualTimer() {
if (timerIntervalId) clearInterval(timerIntervalId);
timerIntervalId = setInterval(function() {
if (visualTimer > 0) visualTimer--;
else if (currentTurn === 'player' && !gameOver) {
visualTimer = 60;
$('#status-text').text("Temps écoulé ! Fin du tour auto.");
endTurn();
}
let sec = visualTimer < 10 ? "0" + visualTimer : visualTimer;
$('#timer-display').text("00:" + sec).toggleClass('timer-warning', visualTimer <= 10);
}, 1000);
}
function commitMove(x, y) {
$.ajax({
url: 'api/action_game.php',
type: 'POST',
data: { action: 'move', id: selectedShipId, x: x, y: y },
dataType: 'json',
success: function(res) {
resetMode(res.msg);
setTimeout(syncState, 200);
}
});
}
fireLaserEffect.
function commitAttack(target) {
let attacker = ships.find(s => parseInt(s.id) === parseInt(selectedShipId));
if (!attacker) { resetMode("Erreur: Attaquant introuvable."); return; }
$.ajax({
url: 'api/action_game.php',
type: 'POST',
data: { action: 'attack', attackerId: selectedShipId, targetId: target.id },
dataType: 'json',
success: function(res) {
if(res.success) fireLaserEffect(attacker, target);
resetMode(res.msg); syncState();
}
});
}
function commitDrone(x, y) {
$.ajax({
url: 'api/action_game.php',
type: 'POST',
data: { action: 'spawn_drone', parentId: selectedShipId, type: droneTypeToDeploy, x: x, y: y },
dataType: 'json',
success: function(res) { resetMode(res.msg); syncState(); }
});
}
fireHealEffect (laser vert).
function commitHeal(target) {
let healer = ships.find(s => parseInt(s.id) === parseInt(selectedShipId));
$.ajax({
url: 'api/action_game.php',
type: 'POST',
data: { action: 'heal', healerId: selectedShipId, targetId: target.id },
dataType: 'json',
success: function(res) {
if(res.success && healer) fireHealEffect(healer, target);
resetMode(res.msg); syncState();
}
});
}
function renderGame() {
const grid = $('#game-grid');
grid.empty();
let currentIds = ships.map(s => parseInt(s.id));
// Gestion Morts
for (let id in previousShipsState) {
if (!currentIds.includes(parseInt(id))) {
let d = previousShipsState[id];
playDeathAnimation(d.x, d.y);
}
}
// Boucle de rendu 10x10
for (let y = 0; y < 10; y++) {
for (let x = 0; x < 10; x++) {
let cell = $('<div class="cell"></div>').attr('data-x', x).attr('data-y', y);
let entity = ships.find(s => parseInt(s.x) === x && parseInt(s.y) === y);
// LOGIQUE CASE VIDE (Interactive)
if (!entity && currentTurn === 'player' && selectedShipId) {
let actor = ships.find(s => parseInt(s.id) === parseInt(selectedShipId));
if(actor) {
let dist = getDistance(actor.x, actor.y, x, y);
if (mode === 'move') {
let maxMove = (actor.subtype && actor.subtype.toLowerCase() === 'chasseur') ? 2 : 1;
if (dist <= maxMove) cell.addClass('valid-move').click(() => { playWarpOutEffect(actor.x, actor.y); commitMove(x, y); });
} else if (mode === 'drone' && dist === 1) {
cell.addClass('valid-drone').click(() => commitDrone(x, y));
}
}
}
// LOGIQUE ENTITÉ PRÉSENTE
if (entity) {
let visual = createEntityVisual(entity);
let shape = visual.find('.entity-shape');
let actor = selectedShipId ? ships.find(s => parseInt(s.id) === parseInt(selectedShipId)) : null;
let prevState = previousShipsState[entity.id];
let isNew = !prevState;
let hasMoved = prevState && (parseInt(prevState.x) !== parseInt(entity.x) || parseInt(prevState.y) !== parseInt(entity.y));
// Animations CSS
if (prevState && prevState.hp > entity.hp) shape.addClass('anim-hit');
if (entity.type === 'drone' && isNew) shape.addClass('anim-spawn');
else if (hasMoved || (isNew && Object.keys(previousShipsState).length > 0)) shape.addClass('anim-arrival');
else shape.addClass('anim-idle');
// Interactions (Attaque / Soin)
if (mode === 'attack' && actor && parseInt(entity.id) !== parseInt(selectedShipId) && currentTurn === 'player') {
let dist = getDistance(actor.x, actor.y, entity.x, entity.y);
if (dist <= 4 && entity.type === 'enemy' && !entity.hidden) {
shape.removeClass('anim-idle').addClass('valid-target');
visual.off('click').click((e) => { e.stopPropagation(); commitAttack(entity); });
}
}
if (mode === 'heal' && actor && currentTurn === 'player') {
let dist = getDistance(actor.x, actor.y, entity.x, entity.y);
if (dist <= 3 && entity.type !== 'enemy') {
shape.removeClass('anim-idle').addClass('valid-ally');
visual.off('click').click((e) => { e.stopPropagation(); commitHeal(entity); });
}
}
if (!mode) visual.click((e) => { e.stopPropagation(); if (currentTurn === 'player') openPanel(entity); });
cell.append(visual);
}
grid.append(cell);
}
}
previousShipsState = {};
ships.forEach(s => { previousShipsState[s.id] = { ...s }; });
}
function createEntityVisual(entity) {
let container = $('<div></div>').addClass('entity');
if (entity.hidden) container.addClass('is-hidden');
let shapeClass = 'entity-shape ' + (entity.type === 'ship' ? 'type-ship' : (entity.type === 'enemy' ? 'type-enemy' : 'type-drone'));
if (entity.subtype === 'soigneur' || (entity.name && entity.name.toLowerCase().includes('médicale'))) shapeClass = shapeClass.replace('type-ship', 'type-soigneur');
if (entity.type === 'drone') {
if(entity.subtype === 'kamikaze') shapeClass += ' drone-kamikaze';
if(entity.subtype === 'scout') shapeClass += ' drone-scout';
}
if (entity.id == selectedShipId) shapeClass += ' active';
let pct = (entity.hp / entity.maxHp) * 100;
let barColor = (entity.type === 'enemy') ? 'var(--neon-red)' : 'var(--neon-green)';
container.append(`<div class="${shapeClass}"></div><div class="hp-bar"><div class="hp-fill" style="width:${pct}%; background-color:${barColor}; box-shadow: 0 0 5px ${barColor};"></div></div>`);
return container;
}
Math.atan2 pour calculer l'angle de rotation et Math.sqrt pour la longueur. Crée une DIV transformée en CSS.
function createBeam(s, t, cls) {
let c1=$(`.cell[data-x='${s.x}'][data-y='${s.y}']`), c2=$(`.cell[data-x='${t.x}'][data-y='${t.y}']`);
if(!c1.length||!c2.length) return;
let x1=c1.offset().left+c1.width()/2, y1=c1.offset().top+c1.height()/2;
let x2=c2.offset().left+c2.width()/2, y2=c2.offset().top+c2.height()/2;
let l=Math.sqrt((x2-x1)**2+(y2-y1)**2);
let a=Math.atan2(y2-y1, x2-x1)*180/Math.PI;
let b=$('<div></div>').addClass(cls).css({top:y1,left:x1,width:l,transform:`rotate(${a}deg)`}).appendTo('body');
setTimeout(()=>b.css('opacity',0),100); setTimeout(()=>b.remove(),600);
}
function playWarpOutEffect(x, y) {
let e = $(`.cell[data-x='${x}'][data-y='${y}'] .entity`);
if(e.length) {
e.clone().removeClass('anim-idle active').addClass('fx-warp-out')
.css({position:'absolute',top:0,left:0,zIndex:100})
.appendTo(e.parent());
e.css('opacity',0);
}
}
function openPanel(s) {
selectedShipId = parseInt(s.id);
$('#panel-title').text(s.name);
$('#info-hp').text(s.hp + "/" + s.maxHp);
$('#info-energy').text(s.energy);
$('#info-owner').text(s.type === 'enemy' ? "HOSTILE" : "ALLIÉ").css('color', s.type === 'enemy' ? "var(--neon-red)" : "var(--neon-green)");
$('#info-type').text(s.subtype ? s.subtype.toUpperCase() : (s.type === 'drone' ? 'DRONE ' + (s.subtype || '') : s.type));
$('#controls-ship, #controls-drone-kami, #controls-healer').hide();
if (s.type !== 'enemy') {
$('#btn-orders').show();
if (s.subtype === 'soigneur' || (s.name && s.name.toLowerCase().includes('médicale'))) $('#controls-healer').show();
else if (s.type === 'ship') $('#controls-ship').show();
else if (s.type === 'drone' && s.subtype === 'kamikaze') $('#controls-drone-kami').show();
} else $('#btn-orders').hide();
showInfosView();
$('#overlay, #action-panel').fadeIn(200);
}
mode. Cela altère le comportement des clics dans la fonction renderGame() et affiche le bouton d'annulation.
function setMode(nm, txt) {
mode=nm;
$('#overlay, #action-panel').hide();
$('#btn-cancel-action').fadeIn(200);
$('#status-text').text(txt).css('color','#00f3ff');
renderGame();
}