558 lines
20 KiB
JavaScript
558 lines
20 KiB
JavaScript
/* ============================================================
|
||
sockets/1vKI_daily.socket.js
|
||
KI-Matches für die Tagesherausforderung (Himmelstor Daily)
|
||
– Haiku entscheidet den KI-Zug via Anthropic API
|
||
============================================================ */
|
||
|
||
'use strict';
|
||
|
||
const db = require('../database/database');
|
||
const { getMaxRarity } = require('../utils/rarity');
|
||
const { runCombatPhase } = require('./combat');
|
||
const pointsRoute = require('../routes/points.route');
|
||
|
||
/* ── Set aller aktiven KI-Match-IDs (wird von arena.socket.js importiert) */
|
||
const htAiMatchIds = new Set();
|
||
|
||
/* ── KI-Match Rooms ─────────────────────────────────────────
|
||
matchId → {
|
||
playerSocketId, playerSlot, playerAccountId,
|
||
aiSlot, aiHand, aiDeck,
|
||
boardState, boardCards,
|
||
leftSlot, hp, maxHp, station,
|
||
gameOver, aiTurnInProgress
|
||
}
|
||
*/
|
||
const htAiRooms = new Map();
|
||
|
||
/* ── HP-Formel (identisch zu arena.socket.js) ─────────────── */
|
||
function calcAvatarHp(level) {
|
||
return 20 + (Math.max(1, Math.min(50, level || 1)) - 1) * 2;
|
||
}
|
||
|
||
/* ── Wächter-Namen pro Station ────────────────────────────── */
|
||
const GUARD_NAMES = [
|
||
"", "Torwächter", "Waldläufer", "Steinbrecher",
|
||
"Schattenjäger", "Eisenherzog", "Sturmritter", "Himmelsherold",
|
||
];
|
||
|
||
/* ── KI-Deck laden (Schwierigkeit skaliert mit Station) ───────
|
||
Station 1 → 5 Karten, max. Rarity 1
|
||
Station 2 → 7 Karten, max. Rarity 1
|
||
Station 3 → 9 Karten, max. Rarity 2
|
||
Station 4 → 11 Karten, max. Rarity 2
|
||
Station 5 → 12 Karten, max. Rarity 3
|
||
Station 6 → 14 Karten, max. Rarity 3
|
||
Station 7 → 15 Karten, max. Rarity 4
|
||
────────────────────────────────────────────────────────────── */
|
||
async function loadAiDeck(station, playerLevel) {
|
||
// Rarity nach Station (Schwierigkeitsstufe)
|
||
const stationRarity = station <= 2 ? 1
|
||
: station <= 4 ? 2
|
||
: station <= 6 ? 3
|
||
: 4;
|
||
|
||
// KI darf maximal 1 Rarity über dem Spieler liegen
|
||
const playerMaxRarity = getMaxRarity(playerLevel || 1);
|
||
const maxRarity = Math.min(stationRarity, playerMaxRarity + 1);
|
||
|
||
// Kartenanzahl nach Station (3–15)
|
||
const deckSize = station === 1 ? 5
|
||
: station === 2 ? 7
|
||
: station === 3 ? 9
|
||
: station === 4 ? 11
|
||
: station === 5 ? 12
|
||
: station === 6 ? 14
|
||
: 15;
|
||
|
||
try {
|
||
// Zuerst passende Karten mit korrekter Rarity holen
|
||
const [cards] = await db.query(
|
||
`SELECT id, name, image, attack, defends, cooldown, \`range\`, \`race\`, rarity
|
||
FROM cards
|
||
WHERE rarity <= ?
|
||
ORDER BY RAND()
|
||
LIMIT ?`,
|
||
[maxRarity, deckSize]
|
||
);
|
||
|
||
// Wenn nicht genug Karten → Rarity schrittweise erhöhen
|
||
let result = [...cards];
|
||
|
||
if (result.length < deckSize) {
|
||
for (let r = maxRarity + 1; r <= 6 && result.length < deckSize; r++) {
|
||
const [more] = await db.query(
|
||
`SELECT id, name, image, attack, defends, cooldown, \`range\`, \`race\`, rarity
|
||
FROM cards WHERE rarity = ? ORDER BY RAND() LIMIT ?`,
|
||
[r, deckSize - result.length]
|
||
);
|
||
result = [...result, ...more];
|
||
}
|
||
}
|
||
|
||
// Letzter Fallback: alle Karten ohne Rarity-Filter
|
||
if (result.length === 0) {
|
||
const [all] = await db.query(
|
||
`SELECT id, name, image, attack, defends, cooldown, \`range\`, \`race\`, rarity
|
||
FROM cards ORDER BY RAND() LIMIT ?`,
|
||
[deckSize]
|
||
);
|
||
result = all;
|
||
}
|
||
|
||
// Noch immer zu wenig? → vorhandene Karten so oft wiederholen bis deckSize erreicht
|
||
if (result.length > 0 && result.length < deckSize) {
|
||
const base = [...result];
|
||
while (result.length < deckSize) {
|
||
result.push({ ...base[result.length % base.length] });
|
||
}
|
||
console.log(`[HT] KI-Deck aufgefüllt durch Wiederholung: ${base.length} unique → ${result.length} Karten`);
|
||
}
|
||
|
||
console.log(`[HT] KI-Deck Station ${station}: ${result.length} Karten, max. Rarity ${maxRarity}`);
|
||
return result.map(c => ({ ...c, currentCd: 0 }));
|
||
|
||
} catch (err) {
|
||
console.error('[HT] loadAiDeck Fehler:', err);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/* ── boardState → boardCards Array ───────────────────────── */
|
||
function boardStateToCards(boardState) {
|
||
return Object.entries(boardState).map(([boardSlot, entry]) => ({
|
||
boardSlot, card: entry.card, owner: entry.owner, slot: entry.owner,
|
||
}));
|
||
}
|
||
|
||
/* ── Haiku API: KI-Zug entscheiden ───────────────────────── */
|
||
async function askHaiku(boardState, aiHand, availableSlots, aiSlot, leftSlot) {
|
||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||
if (!apiKey) { console.warn('[HT] ANTHROPIC_API_KEY fehlt'); return null; }
|
||
|
||
const isLeft = aiSlot === leftSlot;
|
||
|
||
// Vereinfachter Board-Überblick
|
||
const boardSummary = Object.entries(boardState).map(([slot, e]) => ({
|
||
slot, card: e.card.name, owner: e.owner === aiSlot ? 'KI' : 'Spieler',
|
||
atk: e.card.attack, def: e.card.defends,
|
||
}));
|
||
|
||
const handSummary = aiHand.map((c, i) => ({
|
||
index: i, name: c.name, attack: c.attack ?? 0,
|
||
defends: c.defends ?? 0, race: c.race ?? 0, range: c.range ?? 1,
|
||
}));
|
||
|
||
const prompt = `Kartenspiel. Du bist die KI (${isLeft ? 'linke Seite, Slots 1-3, bewegst dich nach rechts' : 'rechte Seite, Slots 9-11, bewegst dich nach links'}).
|
||
Board: ${JSON.stringify(boardSummary)}
|
||
Deine Hand: ${JSON.stringify(handSummary)}
|
||
Freie Slots: ${JSON.stringify(availableSlots)}
|
||
|
||
Wähle eine Karte und einen freien Slot. Bevorzuge Karten mit hohem Angriff oder hoher Bewegung (race).
|
||
Antworte NUR mit validem JSON: {"card_index":0,"slot":"row1-slot-9"}
|
||
Wenn keine Karte oder kein Slot: {"skip":true}`;
|
||
|
||
try {
|
||
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'x-api-key': apiKey,
|
||
'anthropic-version': '2023-06-01',
|
||
},
|
||
body: JSON.stringify({
|
||
model: 'claude-haiku-4-5-20251001',
|
||
max_tokens: 80,
|
||
messages: [{ role: 'user', content: prompt }],
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
const text = data.content?.[0]?.text ?? '{}';
|
||
// JSON aus Antwort extrahieren
|
||
const match = text.match(/\{[^}]+\}/);
|
||
if (!match) return null;
|
||
return JSON.parse(match[0]);
|
||
} catch (err) {
|
||
console.error('[HT] Haiku API Fehler:', err);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/* ── KI-Zug ausführen ─────────────────────────────────────── */
|
||
async function playAiTurn(io, matchId) {
|
||
const aiRoom = htAiRooms.get(matchId);
|
||
if (!aiRoom || aiRoom.aiTurnInProgress || aiRoom.gameOver) return;
|
||
aiRoom.aiTurnInProgress = true;
|
||
|
||
try {
|
||
// Freie Slots auf KI-Seite
|
||
const isLeft = aiRoom.aiSlot === aiRoom.leftSlot;
|
||
const aiIdxs = isLeft ? [1, 2, 3] : [9, 10, 11];
|
||
const freeSlots = [];
|
||
for (const row of ['row1', 'row2']) {
|
||
for (const idx of aiIdxs) {
|
||
const slotId = `${row}-slot-${idx}`;
|
||
if (!aiRoom.boardState[slotId]) freeSlots.push(slotId);
|
||
}
|
||
}
|
||
|
||
const readyCards = aiRoom.aiHand.filter(c => (c.currentCd ?? 0) <= 0);
|
||
|
||
if (readyCards.length > 0 && freeSlots.length > 0) {
|
||
const decision = await askHaiku(
|
||
aiRoom.boardState, readyCards, freeSlots, aiRoom.aiSlot, aiRoom.leftSlot
|
||
);
|
||
|
||
if (decision && !decision.skip && decision.card_index != null && decision.slot) {
|
||
const card = readyCards[decision.card_index];
|
||
const slotId = decision.slot;
|
||
|
||
if (card && freeSlots.includes(slotId)) {
|
||
// Karte aufs Board legen
|
||
aiRoom.boardState[slotId] = { card, owner: aiRoom.aiSlot };
|
||
aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
|
||
|
||
// Karte aus Hand entfernen, neue Karte ziehen
|
||
const idx = aiRoom.aiHand.findIndex(c => c === card);
|
||
if (idx > -1) aiRoom.aiHand.splice(idx, 1);
|
||
if (aiRoom.aiDeck.length > 0) {
|
||
const drawn = aiRoom.aiDeck.shift();
|
||
drawn.currentCd = drawn.cooldown ?? 0;
|
||
aiRoom.aiHand.push(drawn);
|
||
}
|
||
|
||
// Spieler informieren
|
||
io.to(aiRoom.playerSocketId).emit('card_played', {
|
||
matchId, slot: aiRoom.aiSlot, boardSlot: slotId,
|
||
row: slotId.split('-slot-')[0],
|
||
slotIndex: parseInt(slotId.split('-slot-')[1]),
|
||
card,
|
||
});
|
||
|
||
console.log(`[HT] KI spielt: ${card.name} → ${slotId} | Match ${matchId}`);
|
||
await sleep(900); // Kurze Pause damit Spieler die Karte sieht
|
||
}
|
||
}
|
||
}
|
||
|
||
// KI-Hand Cooldowns ticken
|
||
aiRoom.aiHand.forEach(c => { if ((c.currentCd ?? 0) > 0) c.currentCd--; });
|
||
|
||
// Kampfphase – KI ist am Zug
|
||
const combatEvents = runCombatPhase(aiRoom.boardState, aiRoom.leftSlot, aiRoom.aiSlot);
|
||
aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
|
||
const finalBoard = aiRoom.boardCards;
|
||
|
||
io.to(aiRoom.playerSocketId).emit('combat_phase', { events: combatEvents, finalBoard });
|
||
|
||
// Avatar-Schaden verarbeiten
|
||
const matchEnded = await htProcessAvatarAttacks(io, matchId, aiRoom, combatEvents);
|
||
if (matchEnded) return;
|
||
|
||
// Zug zurück an Spieler
|
||
const delay = calcCombatDuration(combatEvents) + 300;
|
||
setTimeout(() => {
|
||
if (aiRoom.gameOver) return;
|
||
io.to(aiRoom.playerSocketId).emit('turn_change', {
|
||
activeSlot: aiRoom.playerSlot,
|
||
boardSync: aiRoom.boardCards,
|
||
hp: aiRoom.hp,
|
||
maxHp: aiRoom.maxHp,
|
||
});
|
||
console.log(`[HT] Zug zurück an Spieler | Match ${matchId}`);
|
||
}, delay);
|
||
|
||
} finally {
|
||
aiRoom.aiTurnInProgress = false;
|
||
}
|
||
}
|
||
|
||
/* ── Avatar-Schaden (KI-Match) ────────────────────────────── */
|
||
async function htProcessAvatarAttacks(io, matchId, aiRoom, 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;
|
||
if (aiRoom.hp[target] == null) continue;
|
||
|
||
aiRoom.hp[target] = Math.max(0, aiRoom.hp[target] - (ev.damage ?? 0));
|
||
|
||
io.to(aiRoom.playerSocketId).emit('avatar_damaged', {
|
||
slot: target, damage: ev.damage,
|
||
remainingHp: aiRoom.hp[target], maxHp: aiRoom.maxHp[target] ?? 20,
|
||
});
|
||
|
||
if (aiRoom.hp[target] <= 0) {
|
||
await htHandleMatchEnd(io, matchId, aiRoom, target);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/* ── Match beenden (KI-Match) ──────────────────────────────── */
|
||
async function htHandleMatchEnd(io, matchId, aiRoom, loserSlot) {
|
||
if (aiRoom.gameOver) return;
|
||
aiRoom.gameOver = true;
|
||
htAiMatchIds.delete(matchId);
|
||
|
||
const playerWon = loserSlot === aiRoom.aiSlot;
|
||
console.log(`[HT] Match Ende: ${playerWon ? 'Spieler gewinnt' : 'KI gewinnt'} | Station ${aiRoom.station} | ${matchId}`);
|
||
|
||
if (playerWon) {
|
||
// Station als abgeschlossen markieren
|
||
try {
|
||
await db.query(
|
||
`INSERT IGNORE INTO daily_completions (user_id, event_id) VALUES (?, ?)`,
|
||
[aiRoom.playerAccountId, aiRoom.station]
|
||
);
|
||
} catch (err) {
|
||
console.error('[HT] Completion speichern Fehler:', err);
|
||
}
|
||
|
||
// Punkte vergeben (Platzhalter: 10 Punkte pro Station)
|
||
let winResult = { awarded: 0 };
|
||
if (aiRoom.playerAccountId) {
|
||
try {
|
||
winResult = await pointsRoute.awardPoints(aiRoom.playerAccountId, 10);
|
||
} catch {}
|
||
}
|
||
|
||
io.to(aiRoom.playerSocketId).emit('ht_station_complete', {
|
||
station: aiRoom.station,
|
||
});
|
||
|
||
io.to(aiRoom.playerSocketId).emit('match_result', {
|
||
won: true,
|
||
awarded: winResult.awarded ?? 0,
|
||
level_up: winResult.level_up ?? false,
|
||
new_level:winResult.new_level ?? null,
|
||
});
|
||
} else {
|
||
io.to(aiRoom.playerSocketId).emit('match_result', {
|
||
won: false,
|
||
awarded: 0,
|
||
});
|
||
}
|
||
|
||
// Aufräumen nach kurzer Verzögerung
|
||
setTimeout(() => {
|
||
htAiRooms.delete(matchId);
|
||
}, 30000);
|
||
}
|
||
|
||
/* ── Animation-Dauer Berechnung ──────────────────────────── */
|
||
function calcCombatDuration(events) {
|
||
let total = 600 + 500 + 400;
|
||
for (const ev of events) {
|
||
if (ev.type === 'move') total += 350;
|
||
if (ev.type === 'attack') total += 450;
|
||
if (ev.type === 'die') total += 300;
|
||
if (ev.type === 'avatar_attack') total += 500;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
HAUPT-HANDLER
|
||
═══════════════════════════════════════════════════════════ */
|
||
function registerHimmelstorHandlers(io, socket) {
|
||
|
||
/* ── Spieler betritt Daily-Match ────────────────────────── */
|
||
socket.on('ht_join_daily', async (data) => {
|
||
const { station, deckId } = data;
|
||
if (!station || station < 1 || station > 7) return;
|
||
|
||
// Spieler-Account laden
|
||
let accountId = null, playerLevel = 1;
|
||
if (socket.request?.session?.user?.id) {
|
||
accountId = socket.request.session.user.id;
|
||
try {
|
||
const [[acc]] = await db.query(
|
||
"SELECT level FROM accounts WHERE id = ?", [accountId]
|
||
);
|
||
playerLevel = acc?.level ?? 1;
|
||
} catch {}
|
||
}
|
||
|
||
// Prüfen ob Station bereits abgeschlossen
|
||
if (accountId) {
|
||
const [done] = await db.query(
|
||
"SELECT id FROM daily_completions WHERE user_id = ? AND event_id = ?",
|
||
[accountId, station]
|
||
);
|
||
if (done.length > 0) {
|
||
socket.emit('ht_daily_error', { message: `Station ${station} wurde heute bereits abgeschlossen.` });
|
||
return;
|
||
}
|
||
}
|
||
|
||
// KI-Deck laden
|
||
const aiDeck = await loadAiDeck(station, playerLevel);
|
||
if (aiDeck.length === 0) {
|
||
socket.emit('ht_daily_error', { message: 'Keine Karten für diese Station gefunden.' });
|
||
return;
|
||
}
|
||
|
||
// Match erstellen
|
||
const matchId = `ht_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||
const playerSlot = 'player1'; // Spieler immer links
|
||
const aiSlot = 'player2';
|
||
const leftSlot = 'player1';
|
||
|
||
const maxHpPlayer = calcAvatarHp(playerLevel);
|
||
const maxHpAi = calcAvatarHp(Math.min(50, station * 7)); // KI wird mit Station stärker
|
||
|
||
// AI-Room anlegen
|
||
const aiRoom = {
|
||
playerSocketId : socket.id,
|
||
playerSlot,
|
||
playerAccountId : accountId,
|
||
aiSlot,
|
||
aiHand : aiDeck.slice(0, 3), // 3 Startkarten
|
||
aiDeck : aiDeck.slice(3),
|
||
boardState : {},
|
||
boardCards : [],
|
||
leftSlot,
|
||
hp : { player1: maxHpPlayer, player2: maxHpAi },
|
||
maxHp : { player1: maxHpPlayer, player2: maxHpAi },
|
||
station,
|
||
gameOver : false,
|
||
aiTurnInProgress: false,
|
||
};
|
||
htAiRooms.set(matchId, aiRoom);
|
||
htAiMatchIds.add(matchId);
|
||
|
||
// io._arenaRooms eintragen damit arena_join/battlefield funktioniert
|
||
if (!io._arenaRooms) io._arenaRooms = new Map();
|
||
io._arenaRooms.set(matchId, {
|
||
sockets : { player1: socket.id, player2: null }, // player2 = KI (kein socket)
|
||
names : { player1: 'Du', player2: GUARD_NAMES[station] || `Wächter ${station}` },
|
||
accountIds : { player1: accountId, player2: null },
|
||
boardCards : [],
|
||
boardState : aiRoom.boardState, // Referenz! Gleicher Zustand
|
||
leftSlot,
|
||
hp : aiRoom.hp,
|
||
maxHp : aiRoom.maxHp,
|
||
gameOver : false,
|
||
});
|
||
|
||
console.log(`[HT] Daily Match: Station ${station} | ${matchId} | Spieler ${socket.id}`);
|
||
|
||
// Match-Gefunden an Spieler senden
|
||
socket.emit('ht_daily_match_found', {
|
||
matchId,
|
||
mySlot: playerSlot,
|
||
});
|
||
});
|
||
|
||
/* ── arena_join für KI-Matches abfangen ─────────────────── */
|
||
socket.on('arena_join', (data) => {
|
||
const { matchId } = data;
|
||
if (!matchId || !htAiMatchIds.has(matchId)) return; // Nur KI-Matches hier
|
||
|
||
const aiRoom = htAiRooms.get(matchId);
|
||
if (!aiRoom) return;
|
||
|
||
// Socket + Namen + AccountId aktualisieren
|
||
aiRoom.playerSocketId = socket.id;
|
||
// accountId aus arena_join data setzen falls noch nicht gesetzt (Session-Fallback)
|
||
if (!aiRoom.playerAccountId && data.accountId) {
|
||
aiRoom.playerAccountId = data.accountId;
|
||
}
|
||
// Auch io._arenaRooms synchronisieren
|
||
const ioRoom2 = io._arenaRooms?.get(matchId);
|
||
if (ioRoom2 && !ioRoom2.accountIds?.player1 && data.accountId) {
|
||
if (!ioRoom2.accountIds) ioRoom2.accountIds = {};
|
||
ioRoom2.accountIds.player1 = data.accountId;
|
||
}
|
||
const myIngameName = data.playerName || 'Du';
|
||
const ioRoom = io._arenaRooms?.get(matchId);
|
||
if (ioRoom) ioRoom.sockets.player1 = socket.id;
|
||
|
||
socket.join('arena_' + matchId);
|
||
|
||
// Direkt arena_ready senden (kein warten auf Gegner)
|
||
socket.emit('arena_ready', {
|
||
player1 : 'Du',
|
||
player2 : GUARD_NAMES[aiRoom.station] || `Wächter ${aiRoom.station}`,
|
||
boardSync: [],
|
||
hp : aiRoom.hp,
|
||
maxHp : aiRoom.maxHp,
|
||
});
|
||
|
||
// HP initialisieren
|
||
socket.emit('hp_init', { hp: aiRoom.hp, maxHp: aiRoom.maxHp });
|
||
|
||
// ht_ai_setup: setzt links/rechts explizit ohne seed-basierten Flip
|
||
setTimeout(() => {
|
||
socket.emit('ht_ai_setup', {
|
||
playerSlot : aiRoom.playerSlot, // 'player1'
|
||
leftSlot : aiRoom.leftSlot, // 'player1' (Spieler immer links)
|
||
playerName : myIngameName || 'Du',
|
||
aiName : GUARD_NAMES[aiRoom.station] || `Wächter ${aiRoom.station}`,
|
||
hp : aiRoom.hp,
|
||
maxHp : aiRoom.maxHp,
|
||
});
|
||
}, 250);
|
||
|
||
// Zug starten (nach Setup-Animation)
|
||
setTimeout(() => {
|
||
socket.emit('turn_change', {
|
||
activeSlot: aiRoom.playerSlot,
|
||
boardSync : [],
|
||
hp : aiRoom.hp,
|
||
maxHp : aiRoom.maxHp,
|
||
});
|
||
}, 700);
|
||
|
||
console.log(`[HT] arena_join KI-Match: ${matchId}`);
|
||
});
|
||
|
||
/* ── end_turn für KI-Matches ────────────────────────────── */
|
||
socket.on('end_turn', async (data) => {
|
||
const { matchId, slot } = data;
|
||
if (!matchId || !htAiMatchIds.has(matchId)) return; // Nur KI-Matches hier
|
||
|
||
const aiRoom = htAiRooms.get(matchId);
|
||
if (!aiRoom || aiRoom.gameOver) return;
|
||
if (slot !== aiRoom.playerSlot) return; // Nur Spieler-Züge akzeptieren
|
||
|
||
// boardState aus io._arenaRooms synchronisieren (Spieler hat Karten gelegt)
|
||
const ioRoom = io._arenaRooms?.get(matchId);
|
||
if (ioRoom) {
|
||
aiRoom.boardState = ioRoom.boardState;
|
||
aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
|
||
}
|
||
|
||
// Kampfphase – Spieler ist am Zug
|
||
const combatEvents = runCombatPhase(aiRoom.boardState, aiRoom.leftSlot, aiRoom.playerSlot);
|
||
aiRoom.boardCards = boardStateToCards(aiRoom.boardState);
|
||
if (ioRoom) { ioRoom.boardState = aiRoom.boardState; ioRoom.boardCards = aiRoom.boardCards; }
|
||
|
||
socket.emit('combat_phase', { events: combatEvents, finalBoard: aiRoom.boardCards });
|
||
|
||
// Avatar-Schaden nach Spieler-Zug prüfen
|
||
const matchEnded = await htProcessAvatarAttacks(io, matchId, aiRoom, combatEvents);
|
||
if (matchEnded) return;
|
||
|
||
// Nach Animationsdauer → KI-Zug
|
||
const delay = calcCombatDuration(combatEvents) + 400;
|
||
setTimeout(async () => {
|
||
if (aiRoom.gameOver) return;
|
||
await playAiTurn(io, matchId);
|
||
}, delay);
|
||
});
|
||
|
||
/* ── station_complete bestätigen (nach iframe-close) ────── */
|
||
socket.on('ht_station_ack', (data) => {
|
||
// Kann verwendet werden um UI zu aktualisieren
|
||
console.log(`[HT] Station ACK: ${data.station}`);
|
||
});
|
||
}
|
||
|
||
module.exports = { registerHimmelstorHandlers, htAiMatchIds };
|