hmsdzrt
This commit is contained in:
parent
0fd5ad2ca6
commit
4897265983
@ -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(); }
|
||||||
|
|||||||
@ -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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}`,
|
||||||
|
|||||||
@ -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 (11→1)
|
|
||||||
* Rechter Spieler (dir -1): niedrigste Slot-Zahl zuerst (1→11)
|
|
||||||
* → 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user