Add unit previews and admin tower balancing

This commit is contained in:
Emporex committed 2026-10-11 21:33:31 +02:00
1 parent c342fcef30
commit 5cc948c72a
17 files changed
+10 -5

No files matched your search

+10 -5
View File
@@ -183,7 +183,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['balance_action'])) {
$rpcBase = preg_replace('~/private_stats$~', '/set_game_balance', (string) ($config['nakama_rpc_url'] ?? ''));
$towerForm = $_POST['towers'] ?? [];
$enemyForm = $_POST['enemies'] ?? [];
$balance = ['schema' => 2, 'towers' => [], 'enemies' => []];
$adminForm = is_array($_POST['admin_tower'] ?? null) ? $_POST['admin_tower'] : [];
$balance = ['schema' => 3, 'towers' => [], 'enemies' => [], 'admin_tower' => ['range' => $adminForm['range'] ?? '', 'cooldown' => $adminForm['cooldown'] ?? '']];
for ($i = 0; $i < 7; $i++) {
$row = is_array($towerForm[$i] ?? null) ? $towerForm[$i] : [];
$balance['towers'][] = ['cost' => $row['cost'] ?? '', 'damage' => $row['damage'] ?? '', 'range' => $row['range'] ?? '', 'cooldown' => $row['cooldown'] ?? '', 'targetClass' => $row['targetClass'] ?? '', 'bothScale' => $row['bothScale'] ?? ''];
@@ -326,7 +327,7 @@ if ($httpKey !== '') {
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow,noarchive"><meta http-equiv="refresh" content="300"><title>Void Bastion – Interne Statistik</title><style>
:root{color-scheme:dark;--bg:#06121e;--panel:#0a2030;--line:#26596a;--cyan:#5cecff;--muted:#92aebb;font-family:Inter,system-ui,sans-serif}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#12384b 0,transparent 35%),var(--bg);color:#edfaff;min-height:100vh}.shell{width:min(1120px,calc(100% - 32px));margin:auto;padding:48px 0}header{display:flex;justify-content:space-between;align-items:start;gap:24px;margin-bottom:34px}h1{margin:0;font-size:clamp(2rem,5vw,4rem);letter-spacing:-.04em}p{color:var(--muted)}a{color:var(--cyan)}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.card{padding:24px;border:1px solid var(--line);background:linear-gradient(145deg,#0d2c3d,#071722)}.card strong{display:block;color:var(--cyan);font-size:clamp(2rem,4vw,3.5rem);line-height:1}.card span{display:block;margin-top:10px;color:#b9ced7}.panel{margin-top:18px;padding:24px;border:1px solid var(--line);background:#081b28}.warning{color:#ffbd7b}.meta{font-size:.9rem}.form-grid{display:grid;grid-template-columns:minmax(200px,1fr) 180px minmax(220px,1fr) 200px;gap:14px;align-items:end}.field label{display:block;margin-bottom:7px;color:#b9ced7}.field select,.field input,.panel button{width:100%;min-height:46px;padding:9px 12px;border:1px solid #316f80;background:#071722;color:#edfaff;font:inherit}.panel button{background:#14576a;color:#fff;font-weight:700;cursor:pointer}.result{padding:14px;border:1px solid #247c66;background:#0a3028;color:#a5ffdc}.result code{display:block;margin:8px 0;font-size:1.3rem;letter-spacing:.12em;overflow-wrap:anywhere;color:#fff}.error{color:#ffac99}.meta{font-size:.9rem}.table-wrap{overflow-x:auto}.codes{width:100%;border-collapse:collapse;margin-top:16px;min-width:780px}.codes th,.codes td{text-align:left;padding:11px;border-bottom:1px solid #214553;vertical-align:top}.codes th{color:#9fd7e7;font-size:.85rem}.codes code{font-size:.95rem;overflow-wrap:anywhere}.status{display:inline-block;padding:4px 9px;border:1px solid #326;white-space:nowrap}.status-open{color:#a5ffdc;border-color:#247c66;background:#0a3028}.status-partial{color:#ffd88b;border-color:#84672f;background:#332911}.status-redeemed{color:#b6c7d1;border-color:#43515a;background:#1b2830}.note-cell{min-width:180px;white-space:pre-wrap;overflow-wrap:anywhere}@media(max-width:800px){.grid{grid-template-columns:1fr 1fr}header{display:block}.form-grid{grid-template-columns:1fr 1fr}}@media(max-width:480px){.grid,.form-grid{grid-template-columns:1fr}}
.delete-form{margin:0}.panel .delete-form button{width:auto;min-height:36px;padding:6px 10px;background:#602c38;border-color:#ad5963;font-size:.9rem;white-space:nowrap}
.balance-table{width:100%;border-collapse:collapse;min-width:900px;margin:14px 0}.balance-table th,.balance-table td{text-align:left;padding:8px;border-bottom:1px solid #214553;vertical-align:middle}.balance-table th{color:#9fd7e7;font-size:.85rem}.balance-table input,.balance-table select{width:100%;min-width:80px;min-height:38px;padding:6px;border:1px solid #316f80;background:#071722;color:#edfaff;font:inherit}.unit-mark{display:inline-grid;place-items:center;width:34px;height:34px;border:1px solid #316f80;background:#102b3a;color:#7de7ff;font-weight:800;margin-right:8px;vertical-align:middle}.balance-save{max-width:360px}
.balance-table{width:100%;border-collapse:collapse;min-width:900px;margin:14px 0}.balance-table th,.balance-table td{text-align:left;padding:8px;border-bottom:1px solid #214553;vertical-align:middle}.balance-table th{color:#9fd7e7;font-size:.85rem}.balance-table input,.balance-table select{width:100%;min-width:80px;min-height:38px;padding:6px;border:1px solid #316f80;background:#071722;color:#edfaff;font:inherit}.unit-mark{display:inline-grid;place-items:center;width:34px;height:34px;border:1px solid #316f80;background:#102b3a;color:#7de7ff;font-weight:800;margin-right:8px;vertical-align:middle}.unit-cell{min-width:180px;white-space:nowrap}.unit-preview{width:56px;height:42px;object-fit:contain;vertical-align:middle;margin-right:8px;border:1px solid #285364;background:linear-gradient(145deg,#102b3a,#071722);border-radius:4px}.unit-preview-flak{filter:hue-rotate(135deg) saturate(1.6)}.unit-preview-admin{display:inline-grid;place-items:center;color:#ffe49a;font-size:1.7rem}.unit-cell .unit-mark{display:none}.boss-info{margin:16px 0 8px;padding:14px 16px;border:1px solid #725d35;background:linear-gradient(110deg,#302713,#10212b);color:#ffdfa0}.boss-info strong{color:#ffe4a9}.balance-save{max-width:360px}
</style></head><body><main class="shell"><header><div><p>GERGAMES · NICHT ÖFFENTLICH</p><h1>Void Bastion Statistik</h1></div><a href="?logout=1">Abmelden</a></header>
<section class="grid"><article class="card"><strong><?= $downloadTotal ?></strong><span>Downloads insgesamt</span></article><article class="card"><strong><?= $latestDownloads ?></strong><span>Downloads aktuelles Release</span></article><article class="card"><strong><?= $players ?></strong><span>Registrierte Piloten</span></article><article class="card"><strong><?= $activeToday ?></strong><span>Heute aktive Piloten</span></article><article class="card"><strong><?= $platformDownloads['windows'] ?></strong><span>Windows-Downloads</span></article><article class="card"><strong><?= $platformDownloads['android'] ?></strong><span>Android-Downloads</span></article></section>
<section class="panel"><h2><?= e($latestRelease) ?></h2><p>Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.</p><?php if ($playerStats === null): ?><p class="warning">Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.</p><?php else: ?><p>Letzte Spieleraktualisierung: <?= e($updatedAt) ?></p><?php endif; ?><p class="meta">Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Spielerzahlen sind zusammengefasst; technische Rundenberichte sind nur hier einsehbar.</p></section>
@@ -337,14 +338,18 @@ if ($httpKey !== '') {
<?php if ($balanceFlash !== ''): ?><p class="result"><?= e($balanceFlash) ?></p><?php endif; ?>
<?php if ($balanceError !== ''): ?><p class="error"><?= e($balanceError) ?></p><?php elseif ($balanceLoadError !== ''): ?><p class="warning"><?= e($balanceLoadError) ?></p><?php endif; ?>
<?php if (is_array($balanceConfig)): ?>
<?php $towerNames = ['Kinetik','Laser','Rakete','Plasma','EMP-Puls','Railgun','Flak']; $enemyNames = ['Jäger','Fregatte','Kreuzer','Abfangjäger','Schildkreuzer','Belagerer','Späher-Rover','Sturmpanzer','Belagerungsläufer']; ?>
<?php $towerNames = ['Kinetik','Laser','Rakete','Plasma','EMP-Puls','Railgun','Flak']; $towerPreviews = ['kinetik.png','laser.png','rakete.png','plasma.png','emp-puls.png','railgun.png','flak.png']; $enemyNames = ['Jäger','Fregatte','Kreuzer','Abfangjäger','Schildkreuzer','Belagerer','Späher-Rover','Sturmpanzer','Belagerungsläufer']; $enemyPreviews = ['jaeger.png','fregatte.png','kreuzer.png','abfangjaeger.png','schildkreuzer.png','belagerer.png','spaeh-rover.png','sturmpanzer.png','belagerungslaeufer.png']; ?>
<form method="post" autocomplete="off"><input type="hidden" name="balance_action" value="save"><input type="hidden" name="csrf_token" value="<?= e((string) $_SESSION['csrf_token']) ?>">
<h3>Türme</h3><div class="table-wrap"><table class="balance-table"><thead><tr><th>Turm</th><th>Kosten</th><th>Schaden</th><th>Reichweite</th><th>Sek./Schuss</th><th>Zielklasse</th><th>Mehrzweck-Faktor</th></tr></thead><tbody>
<?php foreach ($towerNames as $i => $name): $row = $balanceConfig['towers'][$i] ?? []; ?><tr><td><span class="unit-mark" aria-hidden="true"><?= e(substr($name, 0, 1)) ?></span><?= e($name) ?></td>
<?php foreach ($towerNames as $i => $name): $row = $balanceConfig['towers'][$i] ?? []; ?><tr><td class="unit-cell"><img class="unit-preview<?= $i === 6 ? ' unit-preview-flak' : '' ?>" src="../../assets/balancing/<?= e($towerPreviews[$i]) ?>" alt="" aria-hidden="true" loading="lazy"><?= e($name) ?></td>
<?php foreach (['cost' => 'Kosten','damage' => 'Schaden','range' => 'Reichweite','cooldown' => 'Sek./Schuss'] as $field => $label): ?><td><input aria-label="<?= e($name . ' ' . $label) ?>" type="number" name="towers[<?= $i ?>][<?= $field ?>]" min="<?= $field === 'cost' ? '10' : ($field === 'cooldown' ? '0.04' : '0.1') ?>" max="<?= $field === 'cost' ? '50000' : '10000' ?>" step="<?= $field === 'cost' ? '1' : '0.01' ?>" value="<?= e((string) ($row[$field] ?? '')) ?>" required></td><?php endforeach; ?>
<td><select name="towers[<?= $i ?>][targetClass]"><?php foreach (['air' => 'Luft','ground' => 'Boden','both' => 'Beide'] as $value => $label): ?><option value="<?= e($value) ?>" <?= ($row['targetClass'] ?? '') === $value ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select></td><td><input aria-label="<?= e($name) ?> Mehrzweck-Faktor" type="number" name="towers[<?= $i ?>][bothScale]" min="0.05" max="2" step="0.01" value="<?= e((string) ($row['bothScale'] ?? 1)) ?>" required></td></tr><?php endforeach; ?></tbody></table></div>
<?php $adminTower = is_array($balanceConfig['admin_tower'] ?? null) ? $balanceConfig['admin_tower'] : []; ?>
<h3>Admin-Turm · Testmodus</h3><p>Dieser Turm kostet keine Energie und besiegt jedes Ziel mit einem Schuss. Im Koop trifft er nur Gegner auf der Seite, auf der er gebaut wurde. Reichweite und Schussabstand kannst du hier für neue Partien einstellen.</p>
<div class="table-wrap"><table class="balance-table"><thead><tr><th>Turm</th><th>Schaden</th><th>Reichweite</th><th>Sek./Schuss</th><th>Zielklasse</th></tr></thead><tbody><tr><td class="unit-cell"><span class="unit-preview unit-preview-admin" aria-hidden="true">✦</span>Admin-Turm</td><td>Sofort-KO</td><td><input aria-label="Admin-Turm Reichweite" type="number" name="admin_tower[range]" min="80" max="1200" step="1" value="<?= e((string) ($adminTower['range'] ?? 650)) ?>" required></td><td><input aria-label="Admin-Turm Sek./Schuss" type="number" name="admin_tower[cooldown]" min="0.04" max="20" step="0.01" value="<?= e((string) ($adminTower['cooldown'] ?? 0.08)) ?>" required></td><td>Luft + Boden</td></tr></tbody></table></div>
<h3>Gegner</h3><div class="table-wrap"><table class="balance-table"><thead><tr><th>Einheit</th><th>Hülle / HP</th><th>Geschwindigkeit</th><th>Belohnung</th><th>Reaktorschaden</th><th>Schild-Widerstand</th></tr></thead><tbody>
<?php foreach ($enemyNames as $i => $name): $row = $balanceConfig['enemies'][$i] ?? []; ?><tr><td><span class="unit-mark" aria-hidden="true"><?= e(substr($name, 0, 1)) ?></span><?= e($name) ?></td><?php foreach (['hp' => 'HP','speed' => 'Geschwindigkeit','reward' => 'Belohnung','breachDamage' => 'Reaktorschaden','shieldResistance' => 'Schild-Widerstand'] as $field => $label): ?><td><input aria-label="<?= e($name . ' ' . $label) ?>" type="number" name="enemies[<?= $i ?>][<?= $field ?>]" min="<?= $field === 'speed' || $field === 'hp' ? '1' : ($field === 'shieldResistance' ? '0.01' : '0') ?>" max="<?= $field === 'shieldResistance' ? '1' : '1000000' ?>" step="<?= $field === 'reward' || $field === 'breachDamage' ? '1' : '0.01' ?>" value="<?= e((string) ($row[$field] ?? '')) ?>" required></td><?php endforeach; ?></tr><?php endforeach; ?></tbody></table></div>
<?php foreach ($enemyNames as $i => $name): $row = $balanceConfig['enemies'][$i] ?? []; ?><tr><td class="unit-cell"><img class="unit-preview" src="../../assets/balancing/<?= e($enemyPreviews[$i]) ?>" alt="" aria-hidden="true" loading="lazy"><?= e($name) ?></td><?php foreach (['hp' => 'HP','speed' => 'Geschwindigkeit','reward' => 'Belohnung','breachDamage' => 'Reaktorschaden','shieldResistance' => 'Schild-Widerstand'] as $field => $label): ?><td><input aria-label="<?= e($name . ' ' . $label) ?>" type="number" name="enemies[<?= $i ?>][<?= $field ?>]" min="<?= $field === 'speed' || $field === 'hp' ? '1' : ($field === 'shieldResistance' ? '0.01' : '0') ?>" max="<?= $field === 'shieldResistance' ? '1' : '1000000' ?>" step="<?= $field === 'reward' || $field === 'breachDamage' ? '1' : '0.01' ?>" value="<?= e((string) ($row[$field] ?? '')) ?>" required></td><?php endforeach; ?></tr><?php endforeach; ?></tbody></table></div>
<aside class="boss-info"><strong>Was ist ein Boss?</strong> Ein besonders starker Kommandogegner, der am Ende jeder fünften Welle erscheint (Welle 5, 10, 15 …). Bosse sind größer, haben deutlich mehr Lebenspunkte und bringen eine höhere Belohnung. Erreicht ein Boss den Reaktor, verursacht er zusätzlich 4 Reaktorschaden. In Koop-Wellen können zwei Bosse erscheinen.</aside>
<button class="balance-save" type="submit">Balance speichern</button></form>
<?php else: ?><p class="warning">Das Balancing-Formular wird angezeigt, sobald der Runtime-Server die Balance-Funktionen bereitstellt.</p><?php endif; ?></section>
<section class="panel"><h2>Freischaltcodes erstellen</h2><p>Codes und Bemerkungen werden ausschließlich in der geschützten Serverablage verwaltet. Der Klartext ist nur in diesem angemeldeten Bereich sichtbar.</p>