This commit is contained in:
cay 2026-04-13 18:40:09 +01:00
parent 0fd5ad2ca6
commit 4897265983
5 changed files with 351 additions and 94 deletions

View File

@ -289,6 +289,22 @@ document.getElementById('end-turn-btn')?.addEventListener('click', endMyTurn);
*/ */
const boardState = {}; const boardState = {};
/* ── Kampf + HP Animationen ────────────────────────────── */
(function() {
const s = document.createElement('style');
s.textContent = `
@keyframes dmg-float {
0% { opacity:1; transform:translateX(-50%) translateY(0); }
100% { opacity:0; transform:translateX(-50%) translateY(-44px); }
}
@keyframes combat-fade {
from { opacity:0; transform:translateX(-50%) scale(0.9); }
to { opacity:1; transform:translateX(-50%) scale(1); }
}
`;
document.head.appendChild(s);
})();
/* ── Spieler-Farbe auf Slot oder Avatar anwenden ────────────────── */ /* ── Spieler-Farbe auf Slot oder Avatar anwenden ────────────────── */
function applyOwnerStyle(el, owner) { function applyOwnerStyle(el, owner) {
if (!el || !owner) return; if (!el || !owner) return;
@ -418,6 +434,7 @@ socket.on('arena_ready', data => {
clearTimeout(readyFallbackTimer); clearTimeout(readyFallbackTimer);
document.getElementById('connecting-overlay')?.remove(); document.getElementById('connecting-overlay')?.remove();
if (data.boardSync) applyBoardSync(data.boardSync); if (data.boardSync) applyBoardSync(data.boardSync);
if (data.hp && data.maxHp) applyHpFromEvent(data);
const oppName = amIPlayer1 ? data.player2 : data.player1; const oppName = amIPlayer1 ? data.player2 : data.player1;
const oppEl = document.getElementById(amIPlayer1 ? 'nameRight' : 'nameLeft'); const oppEl = document.getElementById(amIPlayer1 ? 'nameRight' : 'nameLeft');
@ -445,6 +462,7 @@ socket.on('turn_change', data => {
const activeName = activeNameEl?.textContent || (nowMyTurn ? 'Du' : 'Gegner'); const activeName = activeNameEl?.textContent || (nowMyTurn ? 'Du' : 'Gegner');
console.log(`[1v1] turn_change: ${activeSlot} | meinZug: ${nowMyTurn}`); console.log(`[1v1] turn_change: ${activeSlot} | meinZug: ${nowMyTurn}`);
setTurnState(nowMyTurn, activeName); setTurnState(nowMyTurn, activeName);
if (data.hp && data.maxHp) applyHpFromEvent(data);
}); });
socket.on('turn_started', data => { socket.on('turn_started', data => {
@ -804,6 +822,79 @@ socket.on('combat_phase', data => {
}); });
/*
AVATAR HP Anzeige & Animationen
*/
function updateHpDisplay(slot, currentHp, maxHp) {
const isLeft = slot === (window._leftSlot || 'player1');
const orbEl = document.getElementById(isLeft ? 'orbLeft' : 'orbRight');
const hpEl = document.getElementById(isLeft ? 'hpLeft' : 'hpRight');
const avEl = document.getElementById(isLeft ? 'avLeft' : 'avRight');
if (orbEl) orbEl.textContent = currentHp;
if (hpEl) hpEl.textContent = currentHp;
/* Farbe je nach HP-Prozent */
const pct = maxHp > 0 ? currentHp / maxHp : 0;
const color = pct > 0.5 ? '#e74c3c' : pct > 0.25 ? '#e67e22' : '#8b0000';
if (orbEl) {
orbEl.style.background = `radial-gradient(circle at 40% 35%, ${color}, #3a0000)`;
orbEl.style.boxShadow = `0 0 12px ${color}cc`;
}
/* Avatar-Schüttelanimation + roter Flash */
if (avEl) {
avEl.style.transition = 'transform 0.07s ease';
avEl.style.transform = 'scale(1.07)';
setTimeout(() => { avEl.style.transform = 'scale(0.96)'; }, 70);
setTimeout(() => { avEl.style.transform = ''; }, 150);
const flash = document.createElement('div');
flash.style.cssText = 'position:absolute;inset:0;border-radius:inherit;background:rgba(220,50,50,0.4);z-index:20;pointer-events:none;';
avEl.style.position = 'relative';
avEl.appendChild(flash);
setTimeout(() => flash.remove(), 320);
}
}
function applyHpFromEvent(data) {
if (!data.hp || !data.maxHp) return;
['player1','player2'].forEach(slot => {
if (data.hp[slot] != null) updateHpDisplay(slot, data.hp[slot], data.maxHp[slot] ?? data.hp[slot]);
});
}
/* Initiale HP beim Spielstart */
socket.on('hp_init', data => {
applyHpFromEvent(data);
console.log('[HP] Init:', data.hp);
});
/* Avatar getroffen */
socket.on('avatar_damaged', data => {
const { slot, damage, remainingHp, maxHp } = data;
updateHpDisplay(slot, remainingHp, maxHp);
console.log(`[HP] ${slot} -${damage}${remainingHp}/${maxHp}`);
/* Schadens-Zahl einblenden */
const isLeft = slot === (window._leftSlot || 'player1');
const avEl = document.getElementById(isLeft ? 'avLeft' : 'avRight');
if (avEl) {
avEl.style.position = 'relative';
const dmg = document.createElement('div');
dmg.textContent = `-${damage}`;
dmg.style.cssText = `
position:absolute;top:15%;left:50%;transform:translateX(-50%);
font-family:'Cinzel',serif;font-size:calc(var(--s)*24);font-weight:700;
color:#e74c3c;text-shadow:0 2px 10px rgba(0,0,0,0.95);
pointer-events:none;z-index:30;
animation:dmg-float 1s ease forwards;`;
avEl.appendChild(dmg);
setTimeout(() => dmg.remove(), 1000);
}
});
/* /*
AUFGEBEN AUFGEBEN
*/ */
@ -878,35 +969,59 @@ function showResultOverlay(won, data) {
} }
function updateResultWithPoints(data) { function updateResultWithPoints(data) {
const overlay = document.getElementById('match-result-overlay'); /* Verhindert doppeltes Aufrufen */
const pointsEl = document.getElementById('result-points'); if (document.getElementById('match-end-overlay')) return;
const levelupEl = document.getElementById('result-levelup');
const progressEl = document.getElementById('result-progress-wrap');
const fillEl = document.getElementById('result-progress-fill');
const lvlLbl = document.getElementById('result-level-label');
const ptsLbl = document.getElementById('result-pts-label');
if (!overlay.classList.contains('show')) { const img = data.won ? '/images/victory.jpeg' : '/images/defeat.jpeg';
document.getElementById('result-title').textContent = data.won ? '\u2694\uFE0F SIEG!' : '\uD83D\uDC80 NIEDERLAGE'; const awarded = data.awarded ?? 0;
document.getElementById('result-title').className = 'result-title ' + (data.won ? 'win' : 'lose');
overlay.classList.add('show'); /* Fade-in Keyframe einmalig anlegen */
if (!document.getElementById('_matchEndStyle')) {
const st = document.createElement('style');
st.id = '_matchEndStyle';
st.textContent = `
@keyframes matchEndFadeIn { from{opacity:0} to{opacity:1} }
@keyframes matchPtsSlideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }`;
document.head.appendChild(st);
} }
pointsEl.textContent = data.awarded > 0 /* Punkte-Text aufbauen */
? '+' + data.awarded + ' Arena-Punkte' const ptsLine = awarded > 0
: 'Keine Punkte (Aufgabe zu fr\u00FCh oder Tageslimit)'; ? '+' + awarded + ' Arena-Punkte'
: 'Keine Punkte';
const lvlLine = data.level_up
? '⬆ LEVEL UP! → Level ' + data.new_level
: '';
if (data.level_up) { const overlay = document.createElement('div');
levelupEl.style.display = 'block'; overlay.id = 'match-end-overlay';
levelupEl.textContent = '\u2B06 LEVEL UP! \u2192 Level ' + data.new_level; overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#000;animation:matchEndFadeIn 0.5s ease forwards;';
} overlay.innerHTML = `
<img src="${img}" style="width:100%;height:100%;object-fit:cover;display:block;position:absolute;inset:0;">
<div style="
position:absolute;bottom:8%;left:50%;transform:translateX(-50%);
background:rgba(0,0,0,0.72);border:1px solid rgba(255,215,80,0.5);
border-radius:12px;padding:16px 36px;text-align:center;
font-family:'Cinzel',serif;
animation:matchPtsSlideUp 0.5s ease 0.3s both;">
<div style="font-size:calc(var(--s)*18);color:#f0d060;letter-spacing:3px;margin-bottom:6px;">${ptsLine}</div>
${lvlLine ? `<div style="font-size:calc(var(--s)*13);color:#7de87d;letter-spacing:2px;">${lvlLine}</div>` : ''}
</div>`;
document.body.appendChild(overlay);
/* Aktuellen Punktestand nachladen und anzeigen */
fetch('/api/points/me').then(r => r.json()).then(me => { fetch('/api/points/me').then(r => r.json()).then(me => {
progressEl.style.display = 'block'; const info = overlay.querySelector('div > div:first-child');
lvlLbl.textContent = 'Level ' + me.current_level + (me.next_level ? ' \u2192 ' + me.next_level : ' (MAX)'); if (info && awarded > 0) {
ptsLbl.textContent = me.points_this_level + ' / ' + me.points_for_next + ' Pts'; info.insertAdjacentHTML('afterend',
requestAnimationFrame(() => { fillEl.style.width = me.progress_percent + '%'; }); `<div style="font-size:calc(var(--s)*11);color:#a0a0a0;margin-top:4px;">Gesamt: ${me.arena_points} Pts • Level ${me.level}</div>`
);
}
}).catch(() => {}); }).catch(() => {});
/* Nach 3 Sekunden zur Arena weiterleiten */
setTimeout(() => closeToArena(), 3000);
} }
function closePopup() { closeToArena(); } function closePopup() { closeToArena(); }

View File

@ -17,21 +17,21 @@ function requireLogin(req, res, next) {
HELPER: Spieler-Stats laden HELPER: Spieler-Stats laden
======================== */ ======================== */
/* HP-Formel: 20 + (level-1)*2 | Level 1=20, Level 50=118 */
function calcAvatarHp(level) {
return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2;
}
async function getPlayerStats(userId) { async function getPlayerStats(userId) {
let hp = 20, mana = 3;
try { try {
const [[charStats]] = await db.query( const [[acc]] = await db.query(
"SELECT hp, mana FROM characters WHERE account_id = ?", "SELECT level FROM accounts WHERE id = ?",
[userId] [userId]
); );
if (charStats) { return { hp: calcAvatarHp(acc?.level ?? 1), mana: 3 };
hp = charStats.hp || 20;
mana = charStats.mana || 3;
}
} catch { } catch {
// Tabelle existiert evtl. noch nicht Defaults verwenden return { hp: 20, mana: 3 };
} }
return { hp, mana };
} }
/* ================================ /* ================================

View File

@ -13,7 +13,7 @@ const db = require("../database/database");
/* ── Punkte-Konfiguration ───────────────────────────────── */ /* ── Punkte-Konfiguration ───────────────────────────────── */
const POINTS = { const POINTS = {
"1v1": { win: 15, lose: 3 }, "1v1": { win: 15, lose: 5 },
"2v2": { win: 12, lose: 2 }, "2v2": { win: 12, lose: 2 },
"4v4": { win: 10, lose: 2 }, "4v4": { win: 10, lose: 2 },
}; };
@ -230,4 +230,5 @@ router.get("/me", requireLogin, async (req, res) => {
} }
}); });
router.awardPoints = awardPoints;
module.exports = router; module.exports = router;

View File

@ -4,7 +4,14 @@
inkl. Kampfphase nach end_turn inkl. Kampfphase nach end_turn
============================================================ */ ============================================================ */
const { runCombatPhase } = require('./combat'); // combat.js liegt im selben /sockets/ Ordner const { runCombatPhase } = require('./combat');
const db = require('../database/database');
const pointsRoute = require('../routes/points.route');
/* ── HP-Formel (muss mit arena.route.js übereinstimmen) ── */
function calcAvatarHp(level) {
return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2;
}
const waitingPool = new Map(); const waitingPool = new Map();
const LEVEL_RANGE = 5; const LEVEL_RANGE = 5;
@ -223,19 +230,20 @@ function stopReadyTimer(io, matchId) {
damit turn_change erst NACH den Animationen gesendet wird. damit turn_change erst NACH den Animationen gesendet wird.
*/ */
function calcCombatDuration(events) { function calcCombatDuration(events) {
// Muss mit den COMBAT_DELAY_* Werten in 1v1.js übereinstimmen const DELAY_BANNER = 600;
const DELAY_BANNER = 600; // initiale Pause vor erstem Event const DELAY_MOVE = 350;
const DELAY_MOVE = 350; const DELAY_ATTACK = 450;
const DELAY_ATTACK = 450; const DELAY_DIE = 300;
const DELAY_DIE = 300; const DELAY_AVATAR_ATTACK= 500;
const DELAY_FINAL = 500; // finalBoard sync const DELAY_FINAL = 500;
const BUFFER = 400; // Sicherheitspuffer const BUFFER = 400;
let total = DELAY_BANNER + DELAY_FINAL + BUFFER; let total = DELAY_BANNER + DELAY_FINAL + BUFFER;
for (const ev of events) { for (const ev of events) {
if (ev.type === 'move') total += DELAY_MOVE; if (ev.type === 'move') total += DELAY_MOVE;
if (ev.type === 'attack') total += DELAY_ATTACK; if (ev.type === 'attack') total += DELAY_ATTACK;
if (ev.type === 'die') total += DELAY_DIE; if (ev.type === 'die') total += DELAY_DIE;
if (ev.type === 'avatar_attack')total += DELAY_AVATAR_ATTACK;
} }
return total; return total;
} }
@ -418,6 +426,142 @@ function emitToOpponent(io, matchId, senderSlot, event, data) {
} }
} }
/*
AVATAR-HP HELPERS
*/
/** HP für beide Spieler beim Spielstart initialisieren */
async function initMatchHP(io, matchId, room) {
try {
for (const slot of ['player1', 'player2']) {
const accountId = room.accountIds[slot];
if (!accountId) continue;
const [[acc]] = await db.query(
"SELECT level FROM accounts WHERE id = ?",
[accountId]
);
const level = acc?.level ?? 1;
const maxHp = calcAvatarHp(level);
room.hp[slot] = maxHp;
room.maxHp[slot] = maxHp;
await db.query(
`INSERT INTO arena_match_hp (match_id, slot, account_id, current_hp, max_hp)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE current_hp = ?, max_hp = ?`,
[matchId, slot, accountId, maxHp, maxHp, maxHp, maxHp]
);
}
// Initiale HP an beide Clients senden
emitToMatch(io, matchId, 'hp_init', { hp: room.hp, maxHp: room.maxHp });
console.log(`[HP] Init Match ${matchId}: P1=${room.hp.player1}/${room.maxHp.player1}, P2=${room.hp.player2}/${room.maxHp.player2}`);
} catch (err) {
console.error('[HP] initMatchHP Fehler:', err);
}
}
/** Avatar-Treffer aus Combat-Events verarbeiten, Match bei Tod beenden */
async function processAvatarAttacks(io, matchId, room, events) {
const avatarEvents = events.filter(e => e.type === 'avatar_attack');
if (avatarEvents.length === 0) return false;
for (const ev of avatarEvents) {
const target = ev.target; // 'player1' | 'player2'
if (room.hp[target] == null) continue;
room.hp[target] = Math.max(0, (room.hp[target] ?? 0) - ev.damage);
// In DB persistieren
try {
await db.query(
`UPDATE arena_match_hp SET current_hp = ?
WHERE match_id = ? AND slot = ?`,
[room.hp[target], matchId, target]
);
} catch (err) {
console.error('[HP] DB Update Fehler:', err);
}
// Treffer an beide Clients senden
emitToMatch(io, matchId, 'avatar_damaged', {
slot : target,
damage : ev.damage,
remainingHp: room.hp[target],
maxHp : room.maxHp[target] ?? 20,
});
console.log(`[HP] ${target} trifft ${ev.damage} Schaden → verbleibend: ${room.hp[target]}`);
// Match-Ende prüfen
if (room.hp[target] <= 0) {
await handleMatchEnd(io, matchId, room, target);
return true; // Match beendet
}
}
return false;
}
/** Match beenden: Ergebnis emitieren + 15 Punkte an Gewinner vergeben */
async function handleMatchEnd(io, matchId, room, loserSlot) {
if (room.gameOver) return;
room.gameOver = true;
const winnerSlot = loserSlot === 'player1' ? 'player2' : 'player1';
const winnerAccId = room.accountIds[winnerSlot];
const loserAccId = room.accountIds[loserSlot];
console.log(`[Match] Ende: Gewinner=${winnerSlot} (${winnerAccId}), Verlierer=${loserSlot} (${loserAccId}) | ${matchId}`);
// Gewinner: 15 Punkte | Verlierer: 5 Punkte
let winnerResult = { awarded: 0 };
let loserResult = { awarded: 0 };
if (winnerAccId) {
try {
winnerResult = await pointsRoute.awardPoints(winnerAccId, 15);
} catch (err) {
console.error('[Match] Gewinner-Punkte Fehler:', err);
}
}
if (loserAccId) {
try {
loserResult = await pointsRoute.awardPoints(loserAccId, 5);
} catch (err) {
console.error('[Match] Verlierer-Punkte Fehler:', err);
}
}
// Ergebnis an beide Clients senden
const room_ = getRoom(io, matchId);
if (room_.sockets[winnerSlot]) {
io.to(room_.sockets[winnerSlot]).emit('match_result', {
won : true,
awarded : winnerResult.awarded ?? 0,
level_up : winnerResult.level_up ?? false,
new_level: winnerResult.new_level ?? null,
});
}
if (room_.sockets[loserSlot]) {
io.to(room_.sockets[loserSlot]).emit('match_result', {
won : false,
awarded : loserResult.awarded ?? 0,
level_up : loserResult.level_up ?? false,
new_level: loserResult.new_level ?? null,
});
}
// HP-Eintrag bereinigen
try {
await db.query("DELETE FROM arena_match_hp WHERE match_id = ?", [matchId]);
} catch {}
}
/* /*
HAUPT-HANDLER HAUPT-HANDLER
*/ */
@ -459,11 +603,15 @@ function registerArenaHandlers(io, socket) {
if (!io._arenaRooms) io._arenaRooms = new Map(); if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) { if (!io._arenaRooms.has(matchId)) {
io._arenaRooms.set(matchId, { io._arenaRooms.set(matchId, {
sockets : {}, sockets : {},
names : {}, names : {},
boardCards: [], // Array-Format für boardSync/reconnect accountIds : {}, // { player1: accountId, player2: accountId }
boardState: {}, // NEU: { [slotId]: { card, owner } } für Kampfphase boardCards : [],
leftSlot : null, // NEU: welcher Slot ist der linke Spieler boardState : {},
leftSlot : null,
hp : {}, // { player1: current, player2: current } in-memory
maxHp : {}, // { player1: max, player2: max }
gameOver : false,
}); });
} }
@ -476,6 +624,9 @@ function registerArenaHandlers(io, socket) {
room.names[slot] = playerName || "Spieler"; room.names[slot] = playerName || "Spieler";
} }
// Account-ID speichern (wird für HP-Init und Punkte-Vergabe benötigt)
if (data.accountId) room.accountIds[slot] = data.accountId;
console.log( console.log(
`[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`, `[1v1] Name gesetzt: slot=${slot}, name=${room.names[slot]}, playerName=${playerName}`,
); );
@ -495,6 +646,8 @@ function registerArenaHandlers(io, socket) {
player1 : room.names["player1"] || "Spieler 1", player1 : room.names["player1"] || "Spieler 1",
player2 : room.names["player2"] || "Spieler 2", player2 : room.names["player2"] || "Spieler 2",
boardSync: room.boardCards || [], boardSync: room.boardCards || [],
hp : room.hp || {},
maxHp : room.maxHp || {},
}); });
startReadyTimer(io, matchId); startReadyTimer(io, matchId);
} else { } else {
@ -561,7 +714,7 @@ function registerArenaHandlers(io, socket) {
}); });
/* ── Startspieler festlegen ── */ /* ── Startspieler festlegen ── */
socket.on("start_turn_request", (data) => { socket.on("start_turn_request", async (data) => {
const { matchId, starterSlot } = data; const { matchId, starterSlot } = data;
if (!matchId || !starterSlot) return; if (!matchId || !starterSlot) return;
if (!io._turnInit) io._turnInit = new Set(); if (!io._turnInit) io._turnInit = new Set();
@ -573,10 +726,15 @@ function registerArenaHandlers(io, socket) {
const room = io._arenaRooms?.get(matchId); const room = io._arenaRooms?.get(matchId);
if (room) room.leftSlot = starterSlot; if (room) room.leftSlot = starterSlot;
// Avatar-HP initialisieren
await initMatchHP(io, matchId, room);
const boardCards = room?.boardCards || []; const boardCards = room?.boardCards || [];
emitToMatch(io, matchId, "turn_change", { emitToMatch(io, matchId, "turn_change", {
activeSlot: starterSlot, activeSlot: starterSlot,
boardSync : boardCards, boardSync : boardCards,
hp : room.hp,
maxHp : room.maxHp,
}); });
console.log( console.log(
`[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`, `[1v1] Spiel startet → ${starterSlot} (linker Spieler) beginnt | Match ${matchId}`,
@ -614,12 +772,19 @@ function registerArenaHandlers(io, socket) {
`[1v1] Kampfphase: ${combatEvents.length} Events | Zug: ${slot}${nextSlot} | Match ${matchId}`, `[1v1] Kampfphase: ${combatEvents.length} Events | Zug: ${slot}${nextSlot} | Match ${matchId}`,
); );
// Avatar-Schaden aus Combat-Events verarbeiten
const matchEnded = await processAvatarAttacks(io, matchId, room, combatEvents);
if (matchEnded) return; // Match vorbei kein turn_change mehr
// Zugwechsel NACH den Client-Animationen senden // Zugwechsel NACH den Client-Animationen senden
const animDuration = calcCombatDuration(combatEvents); const animDuration = calcCombatDuration(combatEvents);
setTimeout(() => { setTimeout(() => {
if (room.gameOver) return;
emitToMatch(io, matchId, "turn_change", { emitToMatch(io, matchId, "turn_change", {
activeSlot: nextSlot, activeSlot: nextSlot,
boardSync : room.boardCards, // aktualisierter Stand nach Kampf boardSync : room.boardCards,
hp : room.hp,
maxHp : room.maxHp,
}); });
console.log( console.log(
`[1v1] turn_change gesendet nach ${animDuration}ms | ${slot}${nextSlot} | Match ${matchId}`, `[1v1] turn_change gesendet nach ${animDuration}ms | ${slot}${nextSlot} | Match ${matchId}`,

View File

@ -2,104 +2,80 @@
* sockets/combat.js Server-seitige Kampfphasen-Logik für 1v1 * sockets/combat.js Server-seitige Kampfphasen-Logik für 1v1
* *
* PRO KARTE: erst bewegen, dann sofort angreifen dann nächste Karte. * PRO KARTE: erst bewegen, dann sofort angreifen dann nächste Karte.
* * Avatar-Angriff wenn Range über das Spielfeld hinausreicht.
* Reihenfolge (nur Karten des aktiven Spielers):
* Linker Spieler (dir +1): höchste Slot-Zahl zuerst (111)
* Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst (111)
* Vorderste Karte verarbeitet zuerst, macht Platz für die dahinter.
*/ */
'use strict'; 'use strict';
/**
* @param {Object} boardState wird in-place verändert
* @param {string} leftSlot wer links steht ('player1'|'player2')
* @param {string} activeSlot wer gerade am Zug ist ('player1'|'player2')
* @returns {Array} geordnete Event-Liste für den Client
*/
function runCombatPhase(boardState, leftSlot, activeSlot) { function runCombatPhase(boardState, leftSlot, activeSlot) {
const events = []; const events = [];
const isActiveLeft = activeSlot === leftSlot; const isActiveLeft = activeSlot === leftSlot;
const dir = isActiveLeft ? 1 : -1; const dir = isActiveLeft ? 1 : -1;
const opponentSlot = activeSlot === 'player1' ? 'player2' : 'player1';
/* ── Karten des aktiven Spielers sammeln ────────────────── */
const myCards = []; const myCards = [];
for (let slotIndex = 1; slotIndex <= 11; slotIndex++) { for (let slotIndex = 1; slotIndex <= 11; slotIndex++) {
for (const row of ['row1', 'row2']) { for (const row of ['row1', 'row2']) {
const slotId = `${row}-slot-${slotIndex}`; const slotId = `${row}-slot-${slotIndex}`;
if (boardState[slotId]?.owner === activeSlot) { if (boardState[slotId]?.owner === activeSlot) myCards.push(slotId);
myCards.push(slotId);
}
} }
} }
/* ── Vorderste Karte zuerst ─────────────────────────────── */
myCards.sort((a, b) => { myCards.sort((a, b) => {
const ia = parseInt(a.split('-slot-')[1], 10); const ia = parseInt(a.split('-slot-')[1], 10);
const ib = parseInt(b.split('-slot-')[1], 10); const ib = parseInt(b.split('-slot-')[1], 10);
return isActiveLeft ? ib - ia : ia - ib; return isActiveLeft ? ib - ia : ia - ib;
}); });
/* ── Jede Karte: erst bewegen, dann sofort angreifen ────── */
for (const startSlotId of myCards) { for (const startSlotId of myCards) {
const entry = boardState[startSlotId]; const entry = boardState[startSlotId];
if (!entry) continue; // wurde durch vorherigen Angriff bereits entfernt (sollte nicht vorkommen, aber sicher ist sicher) if (!entry) continue;
const { card } = entry; const { card } = entry;
const row = startSlotId.split('-slot-')[0]; const row = startSlotId.split('-slot-')[0];
const race = card.race ?? 0; const race = card.race ?? 0;
const atk = card.attack ?? 0;
const range = card.range ?? 0;
let currentPos = parseInt(startSlotId.split('-slot-')[1], 10); let currentPos = parseInt(startSlotId.split('-slot-')[1], 10);
let currentSlotId = startSlotId; let currentSlotId = startSlotId;
/* ── 1. BEWEGEN ─────────────────────────────────────── */ /* ── BEWEGEN ── */
for (let step = 0; step < race; step++) { for (let step = 0; step < race; step++) {
const nextPos = currentPos + dir; const nextPos = currentPos + dir;
if (nextPos < 1 || nextPos > 11) break; if (nextPos < 1 || nextPos > 11) break;
const nextSlotId = `${row}-slot-${nextPos}`; const nextSlotId = `${row}-slot-${nextPos}`;
if (boardState[nextSlotId]) break; // eigene oder feindliche Karte blockiert if (boardState[nextSlotId]) break;
delete boardState[currentSlotId]; delete boardState[currentSlotId];
boardState[nextSlotId] = entry; boardState[nextSlotId] = entry;
events.push({ type: 'move', from: currentSlotId, to: nextSlotId, owner: activeSlot }); events.push({ type: 'move', from: currentSlotId, to: nextSlotId, owner: activeSlot });
currentSlotId = nextSlotId; currentSlotId = nextSlotId;
currentPos = nextPos; currentPos = nextPos;
} }
/* ── 2. ANGREIFEN (sofort nach der Bewegung) ────────── */ /* ── ANGREIFEN ── */
const range = card.range ?? 0;
for (let r = 1; r <= range; r++) { for (let r = 1; r <= range; r++) {
const targetPos = currentPos + dir * r; const targetPos = currentPos + dir * r;
if (targetPos < 1 || targetPos > 11) break;
/* Avatar-Angriff: Range geht über das Spielfeld hinaus */
if (targetPos < 1 || targetPos > 11) {
events.push({ type: 'avatar_attack', from: currentSlotId, target: opponentSlot, damage: atk });
break;
}
const targetSlotId = `${row}-slot-${targetPos}`; const targetSlotId = `${row}-slot-${targetPos}`;
const target = boardState[targetSlotId]; const target = boardState[targetSlotId];
if (!target) continue; // leeres Feld → weiter scannen if (!target) continue;
if (target.owner === activeSlot) continue; // eigene Karte → Range geht hindurch if (target.owner === activeSlot) continue;
// Feindliche Karte gefunden → Angriff
const atk = card.attack ?? 0;
target.card = { ...target.card, defends: (target.card.defends ?? 0) - atk }; target.card = { ...target.card, defends: (target.card.defends ?? 0) - atk };
events.push({ type: 'attack', from: currentSlotId, to: targetSlotId, damage: atk, remainingDef: target.card.defends });
events.push({
type : 'attack',
from : currentSlotId,
to : targetSlotId,
damage : atk,
remainingDef: target.card.defends,
});
if (target.card.defends <= 0) { if (target.card.defends <= 0) {
delete boardState[targetSlotId]; delete boardState[targetSlotId];
events.push({ type: 'die', slotId: targetSlotId }); events.push({ type: 'die', slotId: targetSlotId });
} }
break;
break; // nur erste feindliche Karte angreifen
} }
} }