diff --git a/public/js/buildings/1v1.js b/public/js/buildings/1v1.js
index 671edc4..f847388 100644
--- a/public/js/buildings/1v1.js
+++ b/public/js/buildings/1v1.js
@@ -289,6 +289,22 @@ document.getElementById('end-turn-btn')?.addEventListener('click', endMyTurn);
*/
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 ────────────────── */
function applyOwnerStyle(el, owner) {
if (!el || !owner) return;
@@ -418,6 +434,7 @@ socket.on('arena_ready', data => {
clearTimeout(readyFallbackTimer);
document.getElementById('connecting-overlay')?.remove();
if (data.boardSync) applyBoardSync(data.boardSync);
+ if (data.hp && data.maxHp) applyHpFromEvent(data);
const oppName = amIPlayer1 ? data.player2 : data.player1;
const oppEl = document.getElementById(amIPlayer1 ? 'nameRight' : 'nameLeft');
@@ -445,6 +462,7 @@ socket.on('turn_change', data => {
const activeName = activeNameEl?.textContent || (nowMyTurn ? 'Du' : 'Gegner');
console.log(`[1v1] turn_change: ${activeSlot} | meinZug: ${nowMyTurn}`);
setTurnState(nowMyTurn, activeName);
+ if (data.hp && data.maxHp) applyHpFromEvent(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
═══════════════════════════════════════════════════════════ */
@@ -878,35 +969,59 @@ function showResultOverlay(won, data) {
}
function updateResultWithPoints(data) {
- const overlay = document.getElementById('match-result-overlay');
- const pointsEl = document.getElementById('result-points');
- 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');
+ /* Verhindert doppeltes Aufrufen */
+ if (document.getElementById('match-end-overlay')) return;
- if (!overlay.classList.contains('show')) {
- document.getElementById('result-title').textContent = data.won ? '\u2694\uFE0F SIEG!' : '\uD83D\uDC80 NIEDERLAGE';
- document.getElementById('result-title').className = 'result-title ' + (data.won ? 'win' : 'lose');
- overlay.classList.add('show');
+ const img = data.won ? '/images/victory.jpeg' : '/images/defeat.jpeg';
+ const awarded = data.awarded ?? 0;
+
+ /* 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
- ? '+' + data.awarded + ' Arena-Punkte'
- : 'Keine Punkte (Aufgabe zu fr\u00FCh oder Tageslimit)';
+ /* Punkte-Text aufbauen */
+ const ptsLine = awarded > 0
+ ? '+' + awarded + ' Arena-Punkte'
+ : 'Keine Punkte';
+ const lvlLine = data.level_up
+ ? '⬆ LEVEL UP! → Level ' + data.new_level
+ : '';
- if (data.level_up) {
- levelupEl.style.display = 'block';
- levelupEl.textContent = '\u2B06 LEVEL UP! \u2192 Level ' + data.new_level;
- }
+ const overlay = document.createElement('div');
+ overlay.id = 'match-end-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#000;animation:matchEndFadeIn 0.5s ease forwards;';
+ overlay.innerHTML = `
+
+
+
${ptsLine}
+ ${lvlLine ? `
${lvlLine}
` : ''}
+
`;
+ document.body.appendChild(overlay);
+
+ /* Aktuellen Punktestand nachladen und anzeigen */
fetch('/api/points/me').then(r => r.json()).then(me => {
- progressEl.style.display = 'block';
- lvlLbl.textContent = 'Level ' + me.current_level + (me.next_level ? ' \u2192 ' + me.next_level : ' (MAX)');
- ptsLbl.textContent = me.points_this_level + ' / ' + me.points_for_next + ' Pts';
- requestAnimationFrame(() => { fillEl.style.width = me.progress_percent + '%'; });
+ const info = overlay.querySelector('div > div:first-child');
+ if (info && awarded > 0) {
+ info.insertAdjacentHTML('afterend',
+ `Gesamt: ${me.arena_points} Pts • Level ${me.level}
`
+ );
+ }
}).catch(() => {});
+
+ /* Nach 3 Sekunden zur Arena weiterleiten */
+ setTimeout(() => closeToArena(), 3000);
}
function closePopup() { closeToArena(); }
diff --git a/routes/arena.route.js b/routes/arena.route.js
index be64c53..bb091aa 100644
--- a/routes/arena.route.js
+++ b/routes/arena.route.js
@@ -17,21 +17,21 @@ function requireLogin(req, res, next) {
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) {
- let hp = 20, mana = 3;
try {
- const [[charStats]] = await db.query(
- "SELECT hp, mana FROM characters WHERE account_id = ?",
+ const [[acc]] = await db.query(
+ "SELECT level FROM accounts WHERE id = ?",
[userId]
);
- if (charStats) {
- hp = charStats.hp || 20;
- mana = charStats.mana || 3;
- }
+ return { hp: calcAvatarHp(acc?.level ?? 1), mana: 3 };
} catch {
- // Tabelle existiert evtl. noch nicht – Defaults verwenden
+ return { hp: 20, mana: 3 };
}
- return { hp, mana };
}
/* ================================
diff --git a/routes/points.route.js b/routes/points.route.js
index b9d3243..70f57b0 100644
--- a/routes/points.route.js
+++ b/routes/points.route.js
@@ -13,7 +13,7 @@ const db = require("../database/database");
/* ── Punkte-Konfiguration ───────────────────────────────── */
const POINTS = {
- "1v1": { win: 15, lose: 3 },
+ "1v1": { win: 15, lose: 5 },
"2v2": { win: 12, lose: 2 },
"4v4": { win: 10, lose: 2 },
};
@@ -230,4 +230,5 @@ router.get("/me", requireLogin, async (req, res) => {
}
});
+router.awardPoints = awardPoints;
module.exports = router;
diff --git a/sockets/arena.socket.js b/sockets/arena.socket.js
index f1f1c73..5f7bb42 100644
--- a/sockets/arena.socket.js
+++ b/sockets/arena.socket.js
@@ -4,7 +4,14 @@
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 LEVEL_RANGE = 5;
@@ -223,19 +230,20 @@ function stopReadyTimer(io, matchId) {
damit turn_change erst NACH den Animationen gesendet wird.
═══════════════════════════════════════════════════════════ */
function calcCombatDuration(events) {
- // Muss mit den COMBAT_DELAY_* Werten in 1v1.js übereinstimmen
- const DELAY_BANNER = 600; // initiale Pause vor erstem Event
- const DELAY_MOVE = 350;
- const DELAY_ATTACK = 450;
- const DELAY_DIE = 300;
- const DELAY_FINAL = 500; // finalBoard sync
- const BUFFER = 400; // Sicherheitspuffer
+ const DELAY_BANNER = 600;
+ const DELAY_MOVE = 350;
+ const DELAY_ATTACK = 450;
+ const DELAY_DIE = 300;
+ const DELAY_AVATAR_ATTACK= 500;
+ const DELAY_FINAL = 500;
+ const BUFFER = 400;
let total = DELAY_BANNER + DELAY_FINAL + BUFFER;
for (const ev of events) {
- if (ev.type === 'move') total += DELAY_MOVE;
- if (ev.type === 'attack') total += DELAY_ATTACK;
- if (ev.type === 'die') total += DELAY_DIE;
+ if (ev.type === 'move') total += DELAY_MOVE;
+ if (ev.type === 'attack') total += DELAY_ATTACK;
+ if (ev.type === 'die') total += DELAY_DIE;
+ if (ev.type === 'avatar_attack')total += DELAY_AVATAR_ATTACK;
}
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
═══════════════════════════════════════════════════════════ */
@@ -459,11 +603,15 @@ function registerArenaHandlers(io, socket) {
if (!io._arenaRooms) io._arenaRooms = new Map();
if (!io._arenaRooms.has(matchId)) {
io._arenaRooms.set(matchId, {
- sockets : {},
- names : {},
- boardCards: [], // Array-Format für boardSync/reconnect
- boardState: {}, // NEU: { [slotId]: { card, owner } } für Kampfphase
- leftSlot : null, // NEU: welcher Slot ist der linke Spieler
+ sockets : {},
+ names : {},
+ accountIds : {}, // { player1: accountId, player2: accountId }
+ boardCards : [],
+ 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";
}
+ // Account-ID speichern (wird für HP-Init und Punkte-Vergabe benötigt)
+ if (data.accountId) room.accountIds[slot] = data.accountId;
+
console.log(
`[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",
player2 : room.names["player2"] || "Spieler 2",
boardSync: room.boardCards || [],
+ hp : room.hp || {},
+ maxHp : room.maxHp || {},
});
startReadyTimer(io, matchId);
} else {
@@ -561,7 +714,7 @@ function registerArenaHandlers(io, socket) {
});
/* ── Startspieler festlegen ── */
- socket.on("start_turn_request", (data) => {
+ socket.on("start_turn_request", async (data) => {
const { matchId, starterSlot } = data;
if (!matchId || !starterSlot) return;
if (!io._turnInit) io._turnInit = new Set();
@@ -573,10 +726,15 @@ function registerArenaHandlers(io, socket) {
const room = io._arenaRooms?.get(matchId);
if (room) room.leftSlot = starterSlot;
+ // Avatar-HP initialisieren
+ await initMatchHP(io, matchId, room);
+
const boardCards = room?.boardCards || [];
emitToMatch(io, matchId, "turn_change", {
activeSlot: starterSlot,
boardSync : boardCards,
+ hp : room.hp,
+ maxHp : room.maxHp,
});
console.log(
`[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}`,
);
+ // 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
const animDuration = calcCombatDuration(combatEvents);
setTimeout(() => {
+ if (room.gameOver) return;
emitToMatch(io, matchId, "turn_change", {
activeSlot: nextSlot,
- boardSync : room.boardCards, // aktualisierter Stand nach Kampf
+ boardSync : room.boardCards,
+ hp : room.hp,
+ maxHp : room.maxHp,
});
console.log(
`[1v1] turn_change gesendet nach ${animDuration}ms | ${slot} → ${nextSlot} | Match ${matchId}`,
diff --git a/sockets/combat.js b/sockets/combat.js
index bdb8e17..bff0dcd 100644
--- a/sockets/combat.js
+++ b/sockets/combat.js
@@ -2,104 +2,80 @@
* sockets/combat.js – Server-seitige Kampfphasen-Logik für 1v1
*
* PRO KARTE: erst bewegen, dann sofort angreifen → dann nächste Karte.
- *
- * Reihenfolge (nur Karten des aktiven Spielers):
- * Linker Spieler (dir +1): höchste Slot-Zahl zuerst (11→1)
- * Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst (1→11)
- * → Vorderste Karte verarbeitet zuerst, macht Platz für die dahinter.
+ * Avatar-Angriff wenn Range über das Spielfeld hinausreicht.
*/
-
'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) {
- const events = [];
+ const events = [];
const isActiveLeft = activeSlot === leftSlot;
const dir = isActiveLeft ? 1 : -1;
+ const opponentSlot = activeSlot === 'player1' ? 'player2' : 'player1';
- /* ── Karten des aktiven Spielers sammeln ────────────────── */
const myCards = [];
for (let slotIndex = 1; slotIndex <= 11; slotIndex++) {
for (const row of ['row1', 'row2']) {
const slotId = `${row}-slot-${slotIndex}`;
- if (boardState[slotId]?.owner === activeSlot) {
- myCards.push(slotId);
- }
+ if (boardState[slotId]?.owner === activeSlot) myCards.push(slotId);
}
}
- /* ── Vorderste Karte zuerst ─────────────────────────────── */
myCards.sort((a, b) => {
const ia = parseInt(a.split('-slot-')[1], 10);
const ib = parseInt(b.split('-slot-')[1], 10);
return isActiveLeft ? ib - ia : ia - ib;
});
- /* ── Jede Karte: erst bewegen, dann sofort angreifen ────── */
for (const startSlotId of myCards) {
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 row = startSlotId.split('-slot-')[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 currentSlotId = startSlotId;
- /* ── 1. BEWEGEN ─────────────────────────────────────── */
+ /* ── BEWEGEN ── */
for (let step = 0; step < race; step++) {
const nextPos = currentPos + dir;
if (nextPos < 1 || nextPos > 11) break;
-
const nextSlotId = `${row}-slot-${nextPos}`;
- if (boardState[nextSlotId]) break; // eigene oder feindliche Karte blockiert
-
+ if (boardState[nextSlotId]) break;
delete boardState[currentSlotId];
boardState[nextSlotId] = entry;
-
events.push({ type: 'move', from: currentSlotId, to: nextSlotId, owner: activeSlot });
-
currentSlotId = nextSlotId;
currentPos = nextPos;
}
- /* ── 2. ANGREIFEN (sofort nach der Bewegung) ────────── */
- const range = card.range ?? 0;
-
+ /* ── ANGREIFEN ── */
for (let r = 1; r <= range; 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 target = boardState[targetSlotId];
- if (!target) continue; // leeres Feld → weiter scannen
- if (target.owner === activeSlot) continue; // eigene Karte → Range geht hindurch
+ if (!target) continue;
+ if (target.owner === activeSlot) continue;
- // Feindliche Karte gefunden → Angriff
- const atk = card.attack ?? 0;
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) {
delete boardState[targetSlotId];
events.push({ type: 'die', slotId: targetSlotId });
}
-
- break; // nur erste feindliche Karte angreifen
+ break;
}
}