hmsdzrt
This commit is contained in:
parent
0fd5ad2ca6
commit
4897265983
@ -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 = `
|
||||
<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 => {
|
||||
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',
|
||||
`<div style="font-size:calc(var(--s)*11);color:#a0a0a0;margin-top:4px;">Gesamt: ${me.arena_points} Pts • Level ${me.level}</div>`
|
||||
);
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
/* Nach 3 Sekunden zur Arena weiterleiten */
|
||||
setTimeout(() => closeToArena(), 3000);
|
||||
}
|
||||
|
||||
function closePopup() { closeToArena(); }
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
/* ================================
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user