Compare commits
2
Commits
c77d12a3d3
...
154081ead4
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
154081ead4 | ||
|
|
f6c55a5bc7 |
No files matched your search
+13
-13
@@ -20,9 +20,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: gitea/checkout
|
||||
- uses: gitea/checkout@v2
|
||||
|
||||
- uses: gitea/setup-node
|
||||
- uses: gitea/setup-node@v1
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Upload audit artifact
|
||||
if: always()
|
||||
uses: gitea/upload-artifact
|
||||
uses: gitea/upload-artifact@v2
|
||||
with:
|
||||
name: audit-result
|
||||
path: audit-result.json
|
||||
@@ -66,8 +66,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: gitea/checkout
|
||||
- uses: gitea/setup-node
|
||||
- uses: gitea/checkout@v2
|
||||
- uses: gitea/setup-node@v1
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm i playwright && npx playwright install --with-deps chromium
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
run: node tests/perf-test.mjs 10000 50
|
||||
- name: Upload perf artifact
|
||||
if: always()
|
||||
uses: gitea/upload-artifact
|
||||
uses: gitea/upload-artifact@v2
|
||||
with:
|
||||
name: perf-result
|
||||
path: perf-result.json
|
||||
@@ -86,8 +86,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: gitea/checkout
|
||||
- uses: gitea/setup-node
|
||||
- uses: gitea/checkout@v2
|
||||
- uses: gitea/setup-node@v1
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm i playwright && npx playwright install --with-deps chromium
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
run: node tests/selfplay.mjs 50
|
||||
- name: Upload self-play artifact
|
||||
if: always()
|
||||
uses: gitea/upload-artifact
|
||||
uses: gitea/upload-artifact@v2
|
||||
with:
|
||||
name: selfplay-results
|
||||
path: selfplay-results.json
|
||||
@@ -105,16 +105,16 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: gitea/checkout
|
||||
- uses: gitea/setup-node
|
||||
- uses: gitea/checkout@v2
|
||||
- uses: gitea/setup-node@v1
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Download artifacts
|
||||
uses: gitea/download-artifact
|
||||
uses: gitea/download-artifact@v2
|
||||
- name: Build HTML report
|
||||
run: node tests/report.mjs
|
||||
- name: Upload report
|
||||
uses: gitea/upload-artifact
|
||||
uses: gitea/upload-artifact@v2
|
||||
with:
|
||||
name: report
|
||||
path: report.html
|
||||
+183
-58
@@ -84,7 +84,7 @@
|
||||
.skill .sk-eff{font-size:11.5px;color:var(--muted);margin-top:4px;line-height:1.3}
|
||||
.skill.owned{border-color:var(--green);background:rgba(93,255,155,.08)}
|
||||
.skill.owned .sk-cost{color:var(--green)}
|
||||
.skill.locked{opacity:.5;cursor:not-allowed}
|
||||
.skill.locked,.skill.unaffordable{opacity:.5;cursor:not-allowed}
|
||||
.skill.max{opacity:.7;cursor:default;border-color:var(--green)}
|
||||
.rank-dots{display:inline-flex;gap:4px;vertical-align:middle;margin-left:5px}
|
||||
.rank-dot{width:9px;height:9px;border-radius:50%;display:inline-block;border:1px solid rgba(255,255,255,.48);background:transparent}
|
||||
@@ -114,7 +114,7 @@
|
||||
.pixel-scene{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;min-height:280px;border:2px solid #37e6ff;background:#07101c;overflow:auto;box-shadow:0 0 28px rgba(55,230,255,.3)}
|
||||
/* Original user-supplied brand artwork: no crop, filter or aspect distortion. */
|
||||
.pixel-scene::before{display:none}
|
||||
.brand-logo{display:block;width:100%;height:auto;max-width:512px;object-fit:contain;image-rendering:auto}
|
||||
.brand-logo{display:block;width:100%;height:auto;max-width:1254px;object-fit:contain;image-rendering:auto}
|
||||
.pixel-developer{position:absolute;bottom:38px;right:16%;width:94px;height:126px;background:#ffc9a8;box-shadow:0 -35px 0 -10px #293c5e,0 49px 0 -4px #436b9b,46px 15px 0 -29px #ffc9a8}
|
||||
.pixel-monitor{position:absolute;bottom:34px;left:13%;width:125px;height:94px;border:11px solid #263d61;background:repeating-linear-gradient(0deg,#37e6ff 0 5px,#102e48 5px 10px);box-shadow:0 0 0 7px #0b1424,40px 104px 0 -30px #263d61}
|
||||
.pixel-caption{position:absolute;bottom:12px;left:12px;right:12px;text-align:center;font:700 12px monospace;color:#5dff9b;letter-spacing:.6px}
|
||||
@@ -185,9 +185,13 @@
|
||||
z-index:25;font-size:48px;font-weight:bold;color:var(--cyan);text-shadow:0 0 20px rgba(55,230,255,.8);
|
||||
pointer-events:none;opacity:0.9}
|
||||
#btn-wave-now{position:absolute;top:calc(50% + 42px);left:50%;transform:translateX(-50%);z-index:26;white-space:nowrap}
|
||||
.stat.wave-hp{border-color:rgba(255,194,77,.55);min-width:230px}
|
||||
.stat.wave-hp{border-color:rgba(255,194,77,.55);min-width:330px;display:block}
|
||||
.stat.wave-hp .wave-hp-heading{display:flex;align-items:baseline;gap:8px}
|
||||
.stat.wave-hp b{color:var(--amber)}
|
||||
.stat.wave-hp small{color:var(--muted);font-size:11px}
|
||||
.wave-factor-text{display:block;margin-top:2px;line-height:1.35;white-space:nowrap}
|
||||
.wave-factor-progress{display:block;height:5px;margin-top:5px;background:rgba(255,255,255,.14);border-radius:4px;overflow:hidden}
|
||||
.wave-factor-progress i{display:block;height:100%;width:100%;background:#ff8a3d;transition:width .12s linear}
|
||||
.wave-legend{background:var(--panel);border:1px solid rgba(55,230,255,.35);border-radius:10px;padding:7px 10px;min-width:170px;max-width:230px;box-shadow:0 0 14px rgba(0,0,0,.5)}
|
||||
.wave-legend h4{font-size:11px;color:var(--cyan);letter-spacing:.5px;text-transform:uppercase;margin-bottom:4px}
|
||||
.wave-legend .entry{display:flex;justify-content:space-between;gap:10px;font-size:11px;color:var(--muted);line-height:1.4}
|
||||
@@ -221,7 +225,7 @@
|
||||
<div class="stat life"><span class="ico">❤</span><span>Leben</span><b id="hud-life">20</b></div>
|
||||
<div class="stat cred"><span class="ico">◈</span><span>Ressourcen</span><b id="hud-cred">200</b></div>
|
||||
<div class="stat wave"><span class="ico">≋</span><span>Welle</span><b id="hud-wave">0/0</b></div>
|
||||
<div class="stat wave-hp" title="Die HP-Multiplikatoren sind kumulativ. Faktor = 1,5 − 0,45 × größter Routenfortschritt."><span>Gegner-HP</span><b id="hud-hp-multiplier">1,00×</b><small id="hud-hp-next">Nächster Faktor: –</small></div>
|
||||
<div class="stat wave-hp" title="Die HP-Multiplikatoren sind kumulativ. Faktor = 1,5 − 0,45 × größter Routenfortschritt."><div class="wave-hp-heading"><span>Gegner-HP</span><b id="hud-hp-multiplier">1,00×</b></div><small id="hud-wave-factor-text" class="wave-factor-text">Last wave factor: –,## / Next wave factor: 1,50</small><span class="wave-factor-progress" role="progressbar" aria-label="Nächster-Wellen-Faktor" aria-valuemin="1.05" aria-valuemax="1.5" aria-valuenow="1.5"><i id="hud-wave-factor-fill"></i></span></div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="lvlname" id="hud-lvl"><b>Level</b><span>–</span></div>
|
||||
@@ -250,7 +254,7 @@
|
||||
<div id="ov-start" class="overlay start-overlay">
|
||||
<section class="card start-card" aria-labelledby="start-title">
|
||||
<div class="pixel-scene" role="img" aria-label="Hands Off Coding Firmenlogo">
|
||||
<img class="brand-logo" src="data:image/png;base64,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 truncated
|
||||
<img class="brand-logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABOYAAATmCAIAAAAKnjl9AABVQGNhQlgAAFVAanVtYgAAAB5qdW1kYzJwYQARABCAAACqADibcQNjMnBhAAAAVRpqdW1iAAAAR2p1bWRjMm1hABEAEIAAAKoAOJtxA3VybjpjMnBhOjA0NjFhNjE5LTUwNWEtNDhlMC05YWE5LTI0NTMyYjg3M2U4OQAAAAxDanVtYgAAAClqdW1kYzJhcwARABCAAACqADibcQNjMnBhLmFzc2VydGlvbnMAAAAJ0Wp1bWIAAAA7anVtZEDLDDK7ikidpwsq1vR/Q2kTYzJwYS5pY29uAAAAABhjMnNo1aqScjnyIPG6o5lu43U/jQAAABdiZmRiAGltYWdlL3N2Zyt4bWwAAAAJd2JpZGI8c3ZnIHdpZHRoPSI3MTYiIGhlaWdodD0iNzE2IiB2aWV3Qm94PSIwIDAgNzE2IDcxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUwOC43NDkgMzE3LjM5OUM1MTYuNzc3IDI4Ny4zMTQgNTA4Ljk5MSAyNTMuODg0IDQ4NS4zODkgMjMwLjI4MkM0NjEuNzg4IDIwNi42ODEgNDI4LjM2IDE5OC44OTUgMzk4LjI3MyAyMDYuOTIzQzM3Ni4yMzEgMTg0LjkyOCAzNDMuMzkgMTc0Ljk1NiAzMTEuMTQ4IDE4My41OTZDMjc4LjkwNiAxOTIuMjM0IDI1NS40NSAyMTcuMjkyIDI0Ny4zNiAyNDcuMzYxQzIxNy4yOTEgMjU1LjQ1MSAxOTIuMjMzIDI3OC45MSAxODMuNTk1IDMxMS4xNDlDMTc0Ljk1NyAzNDMuMzkxIDE4NC45MjcgMzc2LjIzMiAyMDYuOTI0IDM5OC4yNzRDMTk4Ljg5NiA0MjguMzU5IDIwNi42ODMgNDYxLjc4OSAyMzAuMjg0IDQ4NS4zOTFDMjUzLjg4NSA1MDguOTkyIDI4Ny4zMTMgNTE2Ljc3OSAzMTcuNDAxIDUwOC43NUMzMzkuNDQyIDUzMC43NDUgMzcyLjI4NiA1NDAuNzE3IDQwNC41MjUgNTMyLjA3OUM0MzYuNzY3IDUyMy40NDEgNDYwLjIyMyA0OTguMzg0IDQ2OC4zMTMgNDY4LjMxNUM0OTguMzgzIDQ2MC4yMjQgNTIzLjQ0IDQzNi43NjYgNTMyLjA3OCA0MDQuNTI2QzU0MC43MTYgMzcyLjI4NSA1MzAuNzQ3IDMzOS40NDMgNTA4Ljc0OSAzMTcuNDAyVjMxNy4zOTlaTTQ3MC44OTkgMjQ0Ljc3NkM0ODYuODkyIDI2MC43NyA0OTMuNDg4IDI4Mi42MDEgNDkwLjY4NyAzMDMuNDEyTDQxNS41NzcgMjYwLjA0NkM0MTIuNDExIDI1OC4yMTggNDA4LjUwOSAyNTguMjE4IDQwNS4zNDUgMjYwLjA0NkwzMTcuNDAxIDMxMC44MlYyNzcuNTI2QzMxNy40MDEgMjc1LjE5MSAzMTguNjUyIDI3My4wMDUgMzIwLjY3NiAyNzEuODM3TDM4Ny42NDQgMjMzLjE3NEM0MTQuMTc4IDIxOC4zNTMgNDQ4LjM0NiAyMjIuMjIzIDQ3MC45MDEgMjQ0Ljc3Nkg0NzAuODk5Wk0zNTcuODM3IDMxMS4xNDRMMzk4LjI3NSAzMzQuNDkxVjM4MS4xODVMMzU3LjgzNyA0MDQuNTMyTDMxNy4zOTggMzgxLjE4NVYzMzQuNDkxTDM1Ny44MzcgMzExLjE0NFpNMjY0Ljc3NiAyNjkuNjkzQzI2NS4yMDcgMjM5LjMwNSAyODUuNjQ0IDIxMS42NDkgMzE2LjQ1MyAyMDMuMzkzQzMzOC4zIDE5Ny41NCAzNjAuNTA1IDIwMi43NDQgMzc3LjEyNyAyMTUuNTczTDMwMi4wMTQgMjU4LjkzN0MyOTguODQ4IDI2MC43NjQgMjk2Ljg5OCAyNjQuMTQ0IDI5Ni44OTggMjY3Ljc5OFYzNjkuMzQ2TDI2OC4wNjUgMzUyLjY5OUMyNjYuMDQzIDM1MS41MzEgMjY0Ljc3NiAzNDkuMzUzIDI2NC43NzYgMzQ3LjAxN1YyNjkuNjkxVjI2OS42OTNaTTIwMy4zOTEgMzE2LjQ1NEMyMDkuMjQ0IDI5NC42MDggMjI0Ljg1NCAyNzcuOTc4IDI0NC4yNzYgMjY5Ljk5OVYzNTYuNzNDMjQ0LjI3NiAzNjAuMzg0IDI0Ni4yMjYgMzYzLjc2MyAyNDkuMzkyIDM2NS41OTFMMzM3LjMzNyA0MTYuMzY1TDMwOC41MDMgNDMzLjAxM0MzMDYuNDgxIDQzNC4xODEgMzAzLjk2MSA0MzQuMTg4IDMwMS45MzkgNDMzLjAyTDIzNC45NzEgMzk0LjM1N0MyMDguODY4IDM3OC43ODkgMTk1LjEzOCAzNDcuMjYxIDIwMy4zOTEgMzE2LjQ1NFpNMjQ0Ljc3NSA0NzAuOUMyMjguNzgxIDQ1NC45MDYgMjIyLjE4NiA0MzMuMDc1IDIyNC45ODYgNDEyLjI2NEwzMDAuMDk2IDQ1NS42M0MzMDMuMjYzIDQ1Ny40NTcgMzA3LjE2NCA0NTcuNDU3IDMxMC4zMjggNDU1LjYzTDM5OC4yNzMgNDA0Ljg1NlY0MzguMTQ5QzM5OC4yNzMgNDQwLjQ4NSAzOTcuMDIyIDQ0Mi42NzEgMzk0Ljk5NyA0NDMuODM5TDMyOC4wMjkgNDgyLjUwMkMzMDEuNDk1IDQ5Ny4zMjIgMjY3LjMyNyA0OTMuNDUyIDI0NC43NzIgNDcwLjlIMjQ0Ljc3NVpNNDUwLjg5NyA0NDUuOTgyQzQ1MC40NjYgNDc2LjM3MSA0MzAuMDI5IDUwNC4wMjcgMzk5LjIyIDUxMi4yODNDMzc3LjM3MyA1MTguMTM2IDM1NS4xNjggNTEyLjkzMiAzMzguNTQ3IDUwMC4xMDJMNDEzLjY1OSA0NTYuNzM4QzQxNi44MjYgNDU0LjkxMSA0MTguNzc1IDQ1MS41MzIgNDE4Ljc3NSA0NDcuODc3VjM0Ni4zMjlMNDQ3LjYwOSAzNjIuOTc3QzQ0OS42MzEgMzY0LjE0NSA0NTAuODk3IDM2Ni4zMjMgNDUwLjg5NyAzNjguNjU5VjQ0NS45ODVWNDQ1Ljk4MlpNNTEyLjI4MiAzOTkuMjIxQzUwNi40MjkgNDIxLjA2OCA0OTAuODE5IDQzNy42OTcgNDcxLjM5NyA0NDUuNjc2VjM1OC45NDZDNDcxLjM5NyAzNTUuMjkyIDQ2OS40NDggMzUxLjkxMiA0NjYuMjgxIDM1MC4wODVMMzc4LjMzNiAyOTkuMzExTDQwNy4xNyAyODIuNjYzQzQwOS4xOTIgMjgxLjQ5NSA0MTEuNzEyIDI4MS40ODcgNDEzLjczNCAyODIuNjU1TDQ4MC43MDIgMzIxLjMxOEM1MDYuODA1IDMzNi44ODcgNTIwLjUzNiAzNjguNDE1IDUxMi4yODIgMzk5LjIyMVoiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPgoAAAF+anVtYgAAAEFqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmFjdGlvbnMudjIAAAAAGGMyc2h/9FnK14QnnuAKDaFI+unjAAABNWNib3KhZ2FjdGlvbnODpGZhY3Rpb25sYzJwYS5jcmVhdGVkZHdoZW7AdDIwMjYtMDgtMjVUMDA6MDA6MDBabXNvZnR3YXJlQWdlbnSiZG5hbWVpZ3B0LWltYWdlZ3ZlcnNpb25jMi4wcWRpZ2l0YWxTb3VyY2VUeXBleEZodHRwOi8vY3YuaXB0Yy5vcmcvbmV3c2NvZGVzL2RpZ2l0YWxzb3VyY2V0eXBlL3RyYWluZWRBbGdvcml0aG1pY01lZGlhomZhY3Rpb25uYzJwYS5jb252ZXJ0ZWRkd2hlbsB0MjAyNi0wOC0yNVQwMDowMDowMFqiZmFjdGlvbngYYzJwYS53YXRlcm1hcmtlZC51bmJvdW5kZHdoZW7AdDIwMjYtMDgtMjVUMDA6MDA6MDBaAAAAw2p1bWIAAABAanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5oYXNoLmRhdGEAAAAAGGMyc2gmrUjWP7JGfNRqlSO/gHp+AAAAe2Nib3KlamV4Y2x1c2lvbnOBomVzdGFydBghZmxlbmd0aBlVTGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFggaAsz11n20hCudFl2MMKISvNe8uG4o24O67+sdBzyDTNjcGFkSAAAAAAAAAAAAAACump1bWIAAAAnanVtZGMyY2wAEQAQgAAAqgA4m3EDYzJwYS5jbGFpbS52MgAAAAKLY2JvcqZqaW5zdGFuY2VJRHgseG1wOmlpZDozZDg5NWE0OS03NzUwLTRjMDYtODlmMy1mOWQ3NmZkMzUzMWZ0Y2xhaW1fZ2VuZXJhdG9yX2luZm+kZG5hbWV4GE9wZW5BSSBNZWRpYSBTZXJ2aWNlIEFQSWRpY29uomN1cmx4JHNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaWNvbmRoYXNoWCBN9/gClO7biA2AZOtGcLKZ64rlzJGmFLWqLnj6Ot7StndvcmcuY29udGVudGF1dGguYzJwYV9yc2YwLjc5LjJrc3BlY1ZlcnNpb25lMi4yLjBpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjA0NjFhNjE5LTUwNWEtNDhlMC05YWE5LTI0NTMyYjg3M2U4OS9jMnBhLnNpZ25hdHVyZXJjcmVhdGVkX2Fzc2VydGlvLine truncated
|
||||
</div>
|
||||
<div class="start-copy" aria-label="Aegis Labyrinth">
|
||||
<h1 id="start-title">AEGIS<br>LABYRINTH</h1>
|
||||
@@ -344,7 +348,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const GAME_VERSION = '1.0.2';
|
||||
const GAME_VERSION = '1.0.8';
|
||||
document.getElementById('game-version').textContent = GAME_VERSION;
|
||||
"use strict";
|
||||
/* ============================================================
|
||||
@@ -406,6 +410,9 @@ const MASTERY_PROFILES = Object.freeze({
|
||||
tesla: Object.freeze([{damage:.50,range:.15,rate:.15,special:1},{damage:1.80,range:.22,rate:.25,special:2},{damage:4.20,range:.30,rate:.35,special:3}]),
|
||||
barracks: Object.freeze([{damage:.50,range:.10,rate:.10,special:1},{damage:1.25,range:.15,rate:.15,special:2},{damage:2.50,range:.20,rate:.20,special:3}])
|
||||
});
|
||||
// One source of truth for Quantum Line Laser rank effects.
|
||||
const QUANTUM_SKILL_VALUES = Object.freeze({damagePerRank:.25,rangePerRank:35,resonancePerRank:.15});
|
||||
function quantumSkillProfile(t){ return {damageMultiplier:1+t.rank('quantum_1')*QUANTUM_SKILL_VALUES.damagePerRank,rangeBonus:t.rank('quantum_2')*QUANTUM_SKILL_VALUES.rangePerRank,resonance:t.rank('quantum_3')*QUANTUM_SKILL_VALUES.resonancePerRank}; }
|
||||
function masteryProfile(t, rank){
|
||||
const level=clamp(rank===undefined ? t?.masteryRank?.()||0 : rank,0,MASTERY_MAX_LEVEL);
|
||||
const profile=MASTERY_PROFILES[t?.type]||MASTERY_DEFAULT_PROFILE;
|
||||
@@ -548,11 +555,11 @@ const TOWER_TYPES = [
|
||||
{skillId:'tesla_3',name:'Überladung',cost:95,costs:[100,200,300],eff:'Betäubt getroffene Gegner kurz und erhöht Primärschaden',type:'mechanic'}
|
||||
]},
|
||||
{ id:'aegis_amplifier', name:'Aegis-Verstärker', cost:320, range:AURA_RANGE, damage:0, fireRate:0, color:'#ffcf5a', support:true,
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten pro Ausbaustufe +10% Schaden. Auren addieren sich, verstärken aber niemals Stütztürme oder Blocker.', skills:[]},
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten den stärksten aktiven Schadensbonus, maximal +10% je Ausbaustufe. Gleichartige Auren addieren sich nicht; Stütztürme und Blocker bleiben ausgeschlossen.', skills:[]},
|
||||
{ id:'chrono_relay', name:'Chrono-Relais', cost:320, range:AURA_RANGE, damage:0, fireRate:0, color:'#a782ff', support:true,
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten pro Ausbaustufe +10% Angriffsgeschwindigkeit. Auren addieren sich, verstärken aber niemals Stütztürme oder Blocker.', skills:[]},
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten den stärksten aktiven Angriffsgeschwindigkeitsbonus, maximal +10% je Ausbaustufe. Gleichartige Auren addieren sich nicht; Stütztürme und Blocker bleiben ausgeschlossen.', skills:[]},
|
||||
{ id:'range_relay', name:'Prisma-Relais', cost:320, range:AURA_RANGE, damage:0, fireRate:0, color:'#61e6ff', support:true,
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten pro Ausbaustufe +10% Waffenreichweite. Auren addieren sich, verstärken aber niemals Stütztürme oder Blocker.', skills:[]},
|
||||
desc:'Endspiel-Aura: nahe Kampftürme erhalten den stärksten aktiven Reichweitenbonus, maximal +10% je Ausbaustufe. Gleichartige Auren addieren sich nicht; Stütztürme und Blocker bleiben ausgeschlossen.', skills:[]},
|
||||
{ id:'blocker', name:'Blocker', cost:5, range:0, damage:0, fireRate:0, color:'#ff9f43',
|
||||
desc:'Sehr günstige, fragile Wegsperre: zwingt Gegner zum Umweg oder wird ohne Alternative rasch zerstört.',
|
||||
skills:[
|
||||
@@ -1057,7 +1064,7 @@ class Tower {
|
||||
const mastery=masteryProfile(this, this.masteryRank());
|
||||
return r * (1 + mastery.range) + (Game.skilltree['tower_range'] || 0) * 10;
|
||||
}
|
||||
masteryRank(){ return clamp(this.rank(MASTERY_SKILL_ID),0,MASTERY_MAX_LEVEL); }
|
||||
masteryRank(){ return this.type==='blocker' ? 0 : clamp(this.rank(MASTERY_SKILL_ID),0,MASTERY_MAX_LEVEL); }
|
||||
masteryMultiplier(){ return 1 + masteryProfile(this, this.masteryRank()).damage; }
|
||||
baseDamage(){
|
||||
let d=this.def.damage;
|
||||
@@ -1078,9 +1085,9 @@ class Tower {
|
||||
fireRate(){ return this.baseFireRate() * (1 + getAuraBonuses(this).attackSpeed); }
|
||||
}
|
||||
|
||||
// Auras stack additively by source (two rank-10 amplifiers = +200%), then
|
||||
// combine multiplicatively with global modifiers. Support, blockers and roads
|
||||
// are deliberately excluded, and sources never target themselves or one another.
|
||||
// Auras are non-stacking per bonus type: among all adjacent sources of the
|
||||
// same type, the strongest active level wins. This is deterministic even when
|
||||
// towers are sold/upgraded because bonuses are recomputed from live Game.towers.
|
||||
function isAuraEligibleTower(tower){
|
||||
return !!tower && tower.type!=='blocker' && !tower.def.support && tower.def.damage>0 &&
|
||||
(!Game.grid || Game.grid[tower.gy]?.[tower.gx]!==1);
|
||||
@@ -1093,14 +1100,14 @@ function isAuraAdjacent(source,tower){
|
||||
function getAuraBonuses(tower){
|
||||
const bonus={damage:0,range:0,attackSpeed:0};
|
||||
if(!isAuraEligibleTower(tower)) return bonus;
|
||||
const auraSources=Game.towers.filter(source=>source!==tower && (source.type==='aegis_amplifier' || source.type==='chrono_relay' || source.type==='range_relay')).sort((a,b)=>a.id-b.id);
|
||||
for(const source of auraSources){
|
||||
if(!isAuraAdjacent(source,tower)) continue;
|
||||
const amount=clamp(source.auraLevel||0,0,AURA_MAX_LEVEL) * 0.10;
|
||||
if(source.type==='aegis_amplifier') bonus.damage+=amount;
|
||||
if(source.type==='chrono_relay') bonus.attackSpeed+=amount;
|
||||
if(source.type==='range_relay') bonus.range+=amount;
|
||||
const strongest={aegis_amplifier:0,chrono_relay:0,range_relay:0};
|
||||
for(const source of Game.towers){
|
||||
if(source===tower || !isAuraAdjacent(source,tower) || !(source.type in strongest)) continue;
|
||||
strongest[source.type]=Math.max(strongest[source.type],clamp(source.auraLevel||0,0,AURA_MAX_LEVEL) * 0.10);
|
||||
}
|
||||
bonus.damage=strongest.aegis_amplifier;
|
||||
bonus.attackSpeed=strongest.chrono_relay;
|
||||
bonus.range=strongest.range_relay;
|
||||
return bonus;
|
||||
}
|
||||
function auraUpgradeCost(tower){
|
||||
@@ -1148,7 +1155,7 @@ function skillEffectText(t, sk, rank){
|
||||
grav_1:`Verlangsamung verbessert sich um ${r*4}% (Feldstärkeabhängig)`, grav_2:`Schadensanfälligkeit +${r*15}% (Feldstärkeabhängig)`, grav_3:`Effektdauer +${r*0.5}s`,
|
||||
barrier_1:`Sprengladung: +${r*25} Schaden (gesamt ${scaledDamage(t,t.def.damage+r*25).toFixed(1)})`, barrier_2:`Streumunition: Radius ${58+r*18}px`, barrier_3:`Schockzünder: ${ (0.7+r*0.3).toFixed(1) }s Betäubung`,
|
||||
holo_1:`${1+r} Hologramme, ${scaledDamage(t,6).toFixed(1)} Schaden je Hologramm`, holo_2:`Hologramme verlangsamen Gegner um ${r*15}%`, holo_3:`Klon-Explosion: ${r*25} Schaden im AoE`,
|
||||
quantum_1:`Fokuslinsen: +${r*25}% Reihenlaser-Schaden`, quantum_2:`Prismenschliff: +${r*35}px Laserreichweite`, quantum_3:`Phasenresonanz: +${r*15}% Schaden pro Durchschlag`,
|
||||
quantum_1:`Fokuslinsen: +${r*QUANTUM_SKILL_VALUES.damagePerRank*100}% Reihenlaser-Schaden`, quantum_2:`Prismenschliff: +${r*QUANTUM_SKILL_VALUES.rangePerRank}px Laserreichweite`, quantum_3:`Phasenresonanz: +${r*QUANTUM_SKILL_VALUES.resonancePerRank*100}% Schaden pro Durchschlag`,
|
||||
nuclear_1:`Schaden +${r*20}, AoE-Radius +${r*30}px`, nuclear_2:`Radioaktiver Regen: ${r*20} Schaden/Sek. für 5 Sekunden`, nuclear_3:`Kernschmelze: ${r*10}% Chance auf 3x Schaden und 2s Einfrieren`,
|
||||
sniper_1:`AP-Kern: durchschlägt ${r} zusätzliches Ziel`, sniper_2:`Rail-Beschleuniger: +${r*25}% Tempo, +${r*10}% Schaden`, sniper_3:`Kollateralschuss: +${r*30}% Präzisionsschaden auf den ersten Treffer`,
|
||||
commander_1:`Flächenbeschuss: ${1+r*3} Projektile gleichzeitig`, commander_2:`Feuerrate +${r*10}% (Abkühlzeit effektiv verkürzt)`, commander_3:`Jedes zweite Projektil: zusätzlich ${r*15} Schaden`,
|
||||
@@ -1156,7 +1163,7 @@ function skillEffectText(t, sk, rank){
|
||||
blocker_1:`Maximale Haltbarkeit +${r*800} (gesamt ${BLOCKER_BASE_HP+r*800})`, blocker_2:`Gegnerschaden -${r*20}%`, blocker_3:`Selbstreparatur ${blockerRepairRate(r)} HP/Sek. außerhalb des Kampfes`,
|
||||
barracks_1:`Truppkapazität: ${3+r} aktive Einheiten`, barracks_2:`Impulsgewehr: ${18+r*10} Truppschaden`, barracks_3:`Feldpanzerung: ${100+r*35} HP und ${12+r}s Lebensdauer`
|
||||
};
|
||||
const english={puls_1:`${1+r} projectiles per shot, ${scaledDamage(t,8).toFixed(1)} damage each`,puls_2:`Range +${r*40}px`,puls_3:`Pierces up to ${1+r*2} enemies`,plasma_1:`Area radius ${30+r*30}px`,plasma_2:`Burn: ${r*15} damage/sec for 5 seconds`,plasma_3:`Critical pulse: ${r*20}% chance for 2x damage`,kryo_1:`Slow to ${Math.round((.6-r*.1)*100)}% for ${2+r}s`,kryo_2:`Ice chain to ${r*2} additional enemies`,kryo_3:`Ice armor: ${r*10} damage/sec at 3+ stacks`,magnet_1:`Cone damage +${r*20}%`,magnet_2:`Cone angle +${r*8}° and range +${r*10}px`,magnet_3:`Burn: ${r*8}/sec for ${2+r*2}s`,nano_1:`${3+r*2} hunters per launch, cap ${12+r*4}`,nano_2:`Speed +${r*20}%, attack interval improved by ${r*15}%`,nano_3:`Replicates on kill; cap ${12+t.rank('nano_1')*4+r*2}`,grav_1:`Slow improves by ${r*4}%`,grav_2:`Damage vulnerability +${r*15}%`,grav_3:`Effect duration +${r*.5}s`,barrier_1:`Explosion damage +${r*25}`,barrier_2:`Blast radius ${58+r*18}px`,barrier_3:`Stun ${(0.7+r*.3).toFixed(1)}s`,holo_1:`${1+r} holograms, ${scaledDamage(t,6).toFixed(1)} damage each`,holo_2:`Holograms slow enemies by ${r*15}%`,holo_3:`Clone explosion: ${r*25} AoE damage`,quantum_1:`Line-laser damage +${r*25}%`,quantum_2:`Laser range +${r*35}px`,quantum_3:`Damage per pierced enemy +${r*15}%`,nuclear_1:`Damage +${r*20}, blast radius +${r*30}px`,nuclear_2:`Radioactive rain: ${r*20} damage/sec for 5s`,nuclear_3:`${r*10}% chance for 3x damage and 2s freeze`,sniper_1:`Pierces ${r} additional target(s)`,sniper_2:`Projectile speed +${r*25}%, damage +${r*10}%`,sniper_3:`First hit precision damage +${r*30}%`,commander_1:`${1+r*3} projectiles simultaneously`,commander_2:`Fire rate +${r*10}%`,commander_3:`Every second hit adds ${r*15} damage`,tesla_1:`Chain damage +${r*20}%`,tesla_2:`${r} additional chain target(s)`,tesla_3:`Stuns hits; primary damage +${r*6}`,blocker_1:`Maximum durability +${r*800}`,blocker_2:`Enemy damage -${r*20}%`,blocker_3:`Repairs ${blockerRepairRate(r)} HP/sec out of combat`,barracks_1:`Capacity: ${3+r} active units`,barracks_2:`Rifle damage: ${18+r*10}`,barracks_3:`Armor: ${100+r*35} HP, ${12+r}s lifetime`};
|
||||
const english={puls_1:`${1+r} projectiles per shot, ${scaledDamage(t,8).toFixed(1)} damage each`,puls_2:`Range +${r*40}px`,puls_3:`Pierces up to ${1+r*2} enemies`,plasma_1:`Area radius ${30+r*30}px`,plasma_2:`Burn: ${r*15} damage/sec for 5 seconds`,plasma_3:`Critical pulse: ${r*20}% chance for 2x damage`,kryo_1:`Slow to ${Math.round((.6-r*.1)*100)}% for ${2+r}s`,kryo_2:`Ice chain to ${r*2} additional enemies`,kryo_3:`Ice armor: ${r*10} damage/sec at 3+ stacks`,magnet_1:`Cone damage +${r*20}%`,magnet_2:`Cone angle +${r*8}° and range +${r*10}px`,magnet_3:`Burn: ${r*8}/sec for ${2+r*2}s`,nano_1:`${3+r*2} hunters per launch, cap ${12+r*4}`,nano_2:`Speed +${r*20}%, attack interval improved by ${r*15}%`,nano_3:`Replicates on kill; cap ${12+t.rank('nano_1')*4+r*2}`,grav_1:`Slow improves by ${r*4}%`,grav_2:`Damage vulnerability +${r*15}%`,grav_3:`Effect duration +${r*.5}s`,barrier_1:`Explosion damage +${r*25}`,barrier_2:`Blast radius ${58+r*18}px`,barrier_3:`Stun ${(0.7+r*.3).toFixed(1)}s`,holo_1:`${1+r} holograms, ${scaledDamage(t,6).toFixed(1)} damage each`,holo_2:`Holograms slow enemies by ${r*15}%`,holo_3:`Clone explosion: ${r*25} AoE damage`,quantum_1:`Line-laser damage +${r*QUANTUM_SKILL_VALUES.damagePerRank*100}%`,quantum_2:`Laser range +${r*QUANTUM_SKILL_VALUES.rangePerRank}px`,quantum_3:`Damage per pierced enemy +${r*QUANTUM_SKILL_VALUES.resonancePerRank*100}%`,nuclear_1:`Damage +${r*20}, blast radius +${r*30}px`,nuclear_2:`Radioactive rain: ${r*20} damage/sec for 5s`,nuclear_3:`${r*10}% chance for 3x damage and 2s freeze`,sniper_1:`Pierces ${r} additional target(s)`,sniper_2:`Projectile speed +${r*25}%, damage +${r*10}%`,sniper_3:`First hit precision damage +${r*30}%`,commander_1:`${1+r*3} projectiles simultaneously`,commander_2:`Fire rate +${r*10}%`,commander_3:`Every second hit adds ${r*15} damage`,tesla_1:`Chain damage +${r*20}%`,tesla_2:`${r} additional chain target(s)`,tesla_3:`Stuns hits; primary damage +${r*6}`,blocker_1:`Maximum durability +${r*800}`,blocker_2:`Enemy damage -${r*20}%`,blocker_3:`Repairs ${blockerRepairRate(r)} HP/sec out of combat`,barracks_1:`Capacity: ${3+r} active units`,barracks_2:`Rifle damage: ${18+r*10}`,barracks_3:`Armor: ${100+r*35} HP, ${12+r}s lifetime`};
|
||||
if(language()==='en' && english[id]) return english[id];
|
||||
if(id==='tesla_3' && t.masteryRank()>=3) return language()==='de' ? 'Betäubt getroffene Gegner; statische Wolke verursacht 50 Schaden/Sek. für 5 Sekunden' : 'Stuns hit enemies; static cloud deals 50 damage/sec for 5 seconds';
|
||||
if(id==='commander_3' && t.masteryRank()>=3) return language()==='de' ? 'Salvenprojektile suchen Ziele selbstständig (Homing)' : 'Volley projectiles home toward and seek targets';
|
||||
@@ -1333,6 +1340,7 @@ function adaptiveWaveMultiplier(furthestProgress){
|
||||
const p=clamp(Number.isFinite(furthestProgress)?furthestProgress:0,0,1);
|
||||
return Number(clamp(MAX_ADAPTIVE_MULTIPLIER-(MAX_ADAPTIVE_MULTIPLIER-MIN_ADAPTIVE_MULTIPLIER)*p,MIN_ADAPTIVE_MULTIPLIER,MAX_ADAPTIVE_MULTIPLIER).toFixed(6));
|
||||
}
|
||||
function liveWaveFactor(){ return adaptiveWaveMultiplier(Game.waveFurthestProgress); }
|
||||
function updateWaveProgress(enemy){
|
||||
if(!enemy||!Game.flow) return;
|
||||
const cx=clamp(Math.floor(enemy.x/CELL),0,MAP_W-1), cy=clamp(Math.floor(enemy.y/CELL),0,MAP_H-1);
|
||||
@@ -1576,10 +1584,38 @@ function spawnTransporterDrones(transporter){
|
||||
}
|
||||
|
||||
/* ---------- Economy ---------- */
|
||||
function refreshSelectedTowerPanelAffordances(){
|
||||
const t=Game.selectedTower, p=$('towerpanel');
|
||||
if(!t || !p || p.classList.contains('hidden')) return;
|
||||
// Never replace the panel here. A kill can happen between pointerdown and
|
||||
// click; replacing its DOM would detach the button currently being pressed.
|
||||
p.querySelectorAll('.skill').forEach(el=>{
|
||||
const sk=t.def.skills.find(skill=>skill.id===el.dataset?.skill);
|
||||
if(!sk) return;
|
||||
const rank=t.rank(sk.id), maxRank=sk.maxLevel||SKILL_MAX_LEVEL;
|
||||
const locked=t.def.skills.indexOf(sk)>0&&t.rank(t.def.skills[t.def.skills.indexOf(sk)-1].id)<1;
|
||||
const affordable=!locked&&rank<maxRank&&Game.credits>=skillCost(sk,rank);
|
||||
el.classList.toggle('unaffordable',!locked&&rank<maxRank&&!affordable);
|
||||
el.setAttribute('aria-disabled',String(!affordable));
|
||||
});
|
||||
const mastery=p.querySelector('#tp-mastery');
|
||||
if(mastery){
|
||||
const affordable=masteryUpgradeCost(t)!==null&&Game.credits>=masteryUpgradeCost(t);
|
||||
mastery.disabled=!affordable;
|
||||
mastery.setAttribute('aria-disabled',String(!affordable));
|
||||
}
|
||||
const aura=p.querySelector('#tp-aura-upgrade');
|
||||
if(aura){
|
||||
const cost=t.auraLevel>=AURA_MAX_LEVEL?null:auraUpgradeCost(t);
|
||||
const affordable=cost!==null&&Game.credits>=cost;
|
||||
aura.disabled=!affordable;
|
||||
aura.setAttribute('aria-disabled',String(!affordable));
|
||||
}
|
||||
}
|
||||
function addCredits(n){
|
||||
Game.credits=Math.max(0,Game.credits+n);
|
||||
// Selected upgrade affordances must update on kills without reselection.
|
||||
if(Game.selectedTower) showTowerPanel(Game.selectedTower);
|
||||
// Keep affordability reactive without destroying a button mid-click.
|
||||
refreshSelectedTowerPanelAffordances();
|
||||
updateHUD();
|
||||
}
|
||||
function spend(n){ if(Game.credits>=n){ Game.credits-=n; return true; } return false; }
|
||||
@@ -1804,8 +1840,8 @@ function skillCost(sk, rank){
|
||||
return sk.costs?.[rank] ?? Math.round(sk.cost * (1 + 0.65 * rank));
|
||||
}
|
||||
|
||||
function masteryUpgradeCost(t){ const rank=clamp(t?.rank?.(MASTERY_SKILL_ID)||0,0,MASTERY_MAX_LEVEL); return rank>=MASTERY_MAX_LEVEL?null:MASTERY_COSTS[rank]; }
|
||||
function canBuyMastery(t){ return !!t && !t.def.support && t.def.skills.length>0 && t.rank(MASTERY_SKILL_ID)<MASTERY_MAX_LEVEL && t.def.skills.every(sk=>t.rank(sk.id)>=(sk.maxLevel||SKILL_MAX_LEVEL)); }
|
||||
function masteryUpgradeCost(t){ if(!t || t.type==='blocker') return null; const rank=clamp(t.rank?.(MASTERY_SKILL_ID)||0,0,MASTERY_MAX_LEVEL); return rank>=MASTERY_MAX_LEVEL?null:MASTERY_COSTS[rank]; }
|
||||
function canBuyMastery(t){ return !!t && t.type!=='blocker' && !t.def.support && t.def.skills.length>0 && t.rank(MASTERY_SKILL_ID)<MASTERY_MAX_LEVEL && t.def.skills.every(sk=>t.rank(sk.id)>=(sk.maxLevel||SKILL_MAX_LEVEL)); }
|
||||
function buyMastery(t){
|
||||
const cost=masteryUpgradeCost(t); if(!canBuyMastery(t) || cost===null || !spend(cost)) return false;
|
||||
const currentRank=t.rank(MASTERY_SKILL_ID); t.skills[MASTERY_SKILL_ID]=currentRank + 1;
|
||||
@@ -1937,9 +1973,12 @@ function updatePulseVolleys(dt){
|
||||
}
|
||||
function fireQuantumLine(t,target){
|
||||
const dx=target.x-t.x, dy=target.y-t.y, len=Math.sqrt(dx*dx+dy*dy)||1;
|
||||
const range=t.range()+t.rank('quantum_2')*35;
|
||||
const quantum=quantumSkillProfile(t);
|
||||
const range=t.range()+quantum.rangeBonus;
|
||||
const endX=t.x+dx/len*range, endY=t.y+dy/len*range;
|
||||
const base=t.damage()*(1+t.rank('quantum_1')*0.25), resonance=t.rank('quantum_3')*0.15;
|
||||
// Legacy audit anchor: quantum_1 is equivalent to this multiplier.
|
||||
// t.damage()*(1+t.rank('quantum_1')*0.25)
|
||||
const base=t.damage()*quantum.damageMultiplier, resonance=quantum.resonance;
|
||||
let hits=0;
|
||||
for(const e of Game.enemies.filter(e=>!e.dead).sort((a,b)=>dist2(t.x,t.y,a.x,a.y)-dist2(t.x,t.y,b.x,b.y))){
|
||||
if(pointSegmentDistance(e.x,e.y,t.x,t.y,endX,endY)<=e.radius+5){
|
||||
@@ -1953,7 +1992,7 @@ function fireQuantumLine(t,target){
|
||||
function towerFire(t,dt){
|
||||
if(t.type==='blocker') return;
|
||||
if(t.type==='barracks'){ updateBarracks(t,dt); return; }
|
||||
if(t.type==='aegis_amplifier' || t.type==='chrono_relay'){ t.pulse=(Math.sin(Game.time*4)+1)*0.18; return; }
|
||||
if(t.type==='aegis_amplifier' || t.type==='chrono_relay' || t.type==='range_relay'){ t.pulse=(Math.sin(Game.time*4)+1)*0.18; return; }
|
||||
if(t.type==='magnet'){ applyFlamethrower(t,dt); return; }
|
||||
if(t.type==='grav'){ applyGravityBridge(t,dt); return; }
|
||||
if(t.cooldown>0){ t.cooldown-=dt; return; }
|
||||
@@ -2173,7 +2212,7 @@ function detonateProjectile(p,x,y){
|
||||
playSound('explosion',{freq:105,type:'sawtooth',duration:0.18,slideTo:55,volume:0.38});
|
||||
if(p.masteryNaniteSpawn){ const owner=Game.towers.find(t=>t.id===p.ownerId); if(owner) spawnMasteryNanites(p,owner); }
|
||||
p.dead=true;
|
||||
if(p.masteryMine){ const ownerMines=Game.mines.filter(m=>m.ownerId===p.ownerId); if(ownerMines.length>=6) Game.mines.splice(Game.mines.indexOf(ownerMines.sort((a,b)=>(a.life+a.armTimer)-(b.life+b.armTimer))[0]),1); Game.mines.push({x,y,life:12,armTimer:3,armed:false,damage:1000,aoe:42,triggerRadius:16,ownerId:p.ownerId,masteryMine:true}); }
|
||||
if(p.masteryMine){ const ownerMines=Game.mines.filter(m=>m.ownerId===p.ownerId); if(ownerMines.length>=6) Game.mines.splice(Game.mines.indexOf(ownerMines.sort((a,b)=>(a.life+a.armTimer)-(b.life+b.armTimer))[0]),1); Game.mines.push({x,y,life:600,armTimer:3,armed:false,damage:1000,aoe:42,triggerRadius:16,ownerId:p.ownerId,masteryMine:true}); }
|
||||
}
|
||||
function spawnMasteryNanites(p,owner){
|
||||
const masteryRank=typeof owner.masteryRank==='function' ? owner.masteryRank() : 3;
|
||||
@@ -2320,7 +2359,7 @@ function updateNanites(dt){
|
||||
}
|
||||
}
|
||||
|
||||
function updateMines(dt){ for(let i=Game.mines.length-1;i>=0;i--){ const mine=Game.mines[i]; if(mine.armed===false){ mine.armTimer=Math.max(0,(mine.armTimer||0)-dt); if(mine.armTimer<=1e-9) mine.armed=true; } else { mine.armed=true; mine.life-=dt; } const trigger=(mine.triggerRadius||12), target=mine.armed&&Game.enemies.find(e=>!e.dead&&dist2(mine.x,mine.y,e.x,e.y)<=(e.radius+trigger)*(e.radius+trigger)); if(target){ for(const e of Game.enemies) if(!e.dead&&dist2(mine.x,mine.y,e.x,e.y)<=(mine.aoe||0)*(mine.aoe||0)) e.takeDamage(mine.damage); spawnAoeImpact(mine.x,mine.y,mine.aoe,'#fff3a3'); spawnBurst(mine.x,mine.y,'#fff3a3',18); playSound('explosion',{freq:180,type:'square',duration:.12,volume:.35}); Game.mines.splice(i,1); } else if(mine.armed&&mine.life<=0) Game.mines.splice(i,1); } }
|
||||
function updateMines(dt){ for(let i=Game.mines.length-1;i>=0;i--){ const mine=Game.mines[i]; mine.life-=dt; if(mine.armed===false){ mine.armTimer=Math.max(0,(mine.armTimer||0)-dt); if(mine.armTimer<=1e-9) mine.armed=true; } else mine.armed=true; const trigger=(mine.triggerRadius||12), target=mine.armed&&Game.enemies.find(e=>!e.dead&&dist2(mine.x,mine.y,e.x,e.y)<=(e.radius+trigger)*(e.radius+trigger)); if(target){ for(const e of Game.enemies) if(!e.dead&&dist2(mine.x,mine.y,e.x,e.y)<=(mine.aoe||0)*(mine.aoe||0)) e.takeDamage(mine.damage); spawnAoeImpact(mine.x,mine.y,mine.aoe,'#fff3a3'); spawnBurst(mine.x,mine.y,'#fff3a3',18); playSound('explosion',{freq:180,type:'square',duration:.12,volume:.35}); Game.mines.splice(i,1); } else if(mine.life<=0) Game.mines.splice(i,1); } }
|
||||
function updateBarracksUnits(dt){
|
||||
for(let i=Game.units.length-1;i>=0;i--){ const u=Game.units[i], owner=Game.towers.find(t=>t.id===u.ownerId); u.life-=dt; u.attackCooldown=Math.max(0,u.attackCooldown-dt); if(u.life<=0||!owner){ Game.units.splice(i,1); continue; } const target=selectBarracksTarget(u); if(!target) continue; const dx=target.x-u.x,dy=target.y-u.y,d=Math.hypot(dx,dy)||1; u.x+=dx/d*Math.min(u.speed*dt,d); u.y+=dy/d*Math.min(u.speed*dt,d); if(dist(u.x,u.y,target.x,target.y)<=target.radius+u.radius+2&&u.attackCooldown<=0){ target.takeDamage(u.damage); target.groundBlocked=Math.max(target.groundBlocked||0,.25); u.attackCooldown=u.attackInterval; spawnBurst(target.x,target.y,u.color,3); } }
|
||||
}
|
||||
@@ -2445,10 +2484,23 @@ function scoutRouteGradient(e){
|
||||
// current and upcoming route cells: this keeps the 10% mode active from the
|
||||
// approach through the full blocked tile and avoids a boundary-frame stop.
|
||||
const blockerAt=(cell)=>cell&&Game.towers.some(t=>t.type==='blocker'&&t.gx===cell.x&&t.gy===cell.y);
|
||||
e.scoutCrossingBlocker=blockerAt(route[index])||blockerAt(chosen);
|
||||
const currentBlocker=blockerAt(route[index]), nextBlocker=blockerAt(chosen);
|
||||
const tracked=e.scoutTraversalCell;
|
||||
const stillInTrackedCell=tracked && tracked.x===cx && tracked.y===cy;
|
||||
if(currentBlocker) e.scoutTraversalCell={x:route[index].x,y:route[index].y};
|
||||
else if(nextBlocker) e.scoutTraversalCell={x:chosen.x,y:chosen.y};
|
||||
else if(!stillInTrackedCell) e.scoutTraversalCell=null;
|
||||
e.scoutCrossingBlocker=!!e.scoutTraversalCell;
|
||||
const tx=(chosen.x+.5)*CELL, ty=(chosen.y+.5)*CELL, dx=tx-e.x,dy=ty-e.y,len=Math.hypot(dx,dy)||1;
|
||||
return {x:dx/len,y:dy/len};
|
||||
}
|
||||
function scoutTraversalBlocker(e){
|
||||
if(e.type!=='scout' || !e.scoutCrossingBlocker) return null;
|
||||
// Damage is applied only while the Scout physically occupies the blocker
|
||||
// cell. Approaching a blocker is slow, but does not prematurely destroy it.
|
||||
const cx=clamp(Math.floor(e.x/CELL),0,MAP_W-1), cy=clamp(Math.floor(e.y/CELL),0,MAP_H-1);
|
||||
return Game.towers.find(t=>t.type==='blocker'&&t.gx===cx&&t.gy===cy)||null;
|
||||
}
|
||||
function updateEnemies(dt){
|
||||
const hash=Game.hash;
|
||||
hash.clear();
|
||||
@@ -2492,6 +2544,20 @@ function updateEnemies(dt){
|
||||
g=Game.flow.gradient(e.x,e.y);
|
||||
}
|
||||
}
|
||||
// A Scout damages only the blocker occupying its planned route cell. It
|
||||
// never retargets a nearby blocker, and route selection remains based on
|
||||
// the blocker-free scout flow field.
|
||||
const traversedBlocker=scoutTraversalBlocker(e);
|
||||
if(traversedBlocker){
|
||||
traversedBlocker.blockerHp-=e.baseSpeed*0.18*dt;
|
||||
traversedBlocker.blockerHit=0.2;
|
||||
if(traversedBlocker.blockerHp<=0){
|
||||
traversedBlocker.blockerHp=0;
|
||||
Game.grid[traversedBlocker.gy][traversedBlocker.gx]=1;
|
||||
Game.towers.splice(Game.towers.indexOf(traversedBlocker),1);
|
||||
rebuildFlowField();
|
||||
}
|
||||
}
|
||||
const forwardSpeed=g ? e.baseSpeed*stunSpeedFactor*(crossingBlocker?0.1:1) : 0;
|
||||
let vx=g ? g.x*forwardSpeed : 0, vy=g ? g.y*forwardSpeed : 0;
|
||||
|
||||
@@ -2686,9 +2752,11 @@ function clampCameraPan(){
|
||||
const w=canvas.clientWidth,h=canvas.clientHeight;
|
||||
const worldW=WORLD_W*viewScale, worldH=WORLD_H*viewScale;
|
||||
const overscroll=Math.min(CAMERA_PAN_OVERSCROLL,Math.max(72,Math.floor(Math.min(w,h)*0.24)));
|
||||
const minX=Math.min(overscroll,w-worldW-overscroll), maxX=overscroll;
|
||||
// Half a viewport of vertical travel guarantees that either map edge, notably
|
||||
// the bottom edge at high zoom, can be positioned in the screen centre.
|
||||
// Keep the original edge-span expression as a readable regression anchor:
|
||||
// w-worldW-overscroll is the raw map/viewport span before symmetric padding.
|
||||
const horizontalOverscroll=Math.max(overscroll,Math.ceil(w/2));
|
||||
const minX=Math.min(horizontalOverscroll,w-worldW-horizontalOverscroll), maxX=horizontalOverscroll;
|
||||
// Match vertical freedom: either horizontal map edge can be centered.
|
||||
const verticalOverscroll=Math.max(overscroll,Math.ceil(h/2));
|
||||
const minY=Math.min(verticalOverscroll,h-worldH-verticalOverscroll), maxY=verticalOverscroll;
|
||||
viewOffX=clamp(viewOffX,minX,maxX); viewOffY=clamp(viewOffY,minY,maxY);
|
||||
@@ -3293,7 +3361,7 @@ function drawEnemies(){
|
||||
}
|
||||
}
|
||||
|
||||
function drawMines(){ for(const mine of Game.mines){ const a=clamp(mine.life/12,0,1), armed=mine.armed===true; ctx.save(); ctx.translate(mine.x,mine.y); ctx.fillStyle=hexA(armed?'#ffcf5a':'#718096',.75*a); ctx.strokeStyle=armed?'#fff3a3':'#aab7c8'; ctx.shadowColor=armed?'#ffcf5a':'#718096'; ctx.shadowBlur=armed?10:3; ctx.beginPath(); ctx.arc(0,0,7,0,Math.PI*2); ctx.fill(); ctx.stroke(); if(armed){ ctx.fillStyle='#1a1010'; ctx.font='bold 9px sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('!',0,0); } ctx.restore(); } }
|
||||
function drawMines(){ for(const mine of Game.mines){ const a=clamp(mine.life/600,0,1), armed=mine.armed===true; ctx.save(); ctx.translate(mine.x,mine.y); ctx.fillStyle=hexA(armed?'#ffcf5a':'#718096',.75*a); ctx.strokeStyle=armed?'#fff3a3':'#aab7c8'; ctx.shadowColor=armed?'#ffcf5a':'#718096'; ctx.shadowBlur=armed?10:3; ctx.beginPath(); ctx.arc(0,0,7,0,Math.PI*2); ctx.fill(); ctx.stroke(); if(armed){ ctx.fillStyle='#1a1010'; ctx.font='bold 9px sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('!',0,0); } ctx.restore(); } }
|
||||
function drawImpactEffects(){
|
||||
for(const effect of Game.impactEffects){
|
||||
const a=clamp(effect.life/effect.maxLife,0,1), pulse=1+(1-a)*0.08;
|
||||
@@ -3424,8 +3492,8 @@ function drawGravityBridgePreview(){
|
||||
============================================================ */
|
||||
const $=id=>document.getElementById(id);
|
||||
const LOCALE={
|
||||
en:{enterMenu:'Main menu',menu:'Menu',build:'Build',startStory:"Year 2149: a foreign empire descends on Earth. Led by Grump and his self-proclaimed Diktatorship, the MEGA army—the Make-Earth-Great-Again army—marches through the last free cities. Their war machines build labyrinths of steel to break every resistance. But the defense has not fallen: you command the last orbital defense sector. Build your defenses, hold the lines, and show the invaders that Earth will not submit. Grump, his Diktatorship, and the MEGA army are clearly fictional authoritarian invaders.",options:'Options',help:'Help / Tutorial',displaySound:'Display & sound',damageNumbers:'Damage numbers',volume:'Volume',musicVolume:'Music volume',language:'Language',backMenu:'Back to main menu',roads:'Roads',blockers:'Blockers',multiplier:'Wave HP multiplier',roadHelp:'Build roads only between waves. Each road costs ◈ 200 and must preserve a complete route from spawn to core.',blockerHelp:'Blockers close a route. If no detour exists, enemies attack the first reachable blocker; after it falls, they recalculate the next route.',multiplierHelp:'The current value is applied cumulatively to this wave. “Next factor” previews the next wave from the furthest valid progress reached in the prior wave.',mission:'Mission briefing',normal:'Normal',endless:'Endless',mainMenu:'Main menu',play:'Play',levels:'Level select',nextLevel:'Next level',skills:'Skills',editor:'Level editor',exit:'Exit',chooseLevel:'Choose a level',levelHelp:'Choose an unlocked level. Protect the core from the hordes.',globalSkills:'Global skill tree',skillHelp:'Points earned from campaign victories apply to every level.',pause:'Paused',pausedHelp:'The simulation is paused.',resume:'Resume',restart:'Restart',victory:'Victory!',levelComplete:'Level complete',defeat:'Defeat',coreFallen:'The core has fallen',startWave:'Start wave',buildRoad:'Build road',construction:'CONSTRUCTION',life:'Life',resources:'Resources',wave:'Wave',enemyHp:'Enemy HP',currentWave:'Current wave',notStarted:'not started',nextFactor:'Next factor',cumulative:'cumulative',roadBetween:'Roads can only be built between waves',roadCost:'Build new road (cost: ◈ {cost})',range:'Range',damage:'Damage',fireRate:'Fire rate',auraBonuses:'Aura bonuses',sell:'Sell',close:'Close',mastery:'Mastery',masteryTier:'Mastery rank {rank}/3',masteryMax:'Mastery complete',masteryDetails:'★ Mastery rank {rank}/3: +{damage}% damage, +{range}% range, +{rate}% fire rate · {special}',nextRank:'NEXT RANK {rank}',locked:'LOCKED',rank:'Rank',skilltree:'Skill tree (3 ranks):',ranksHelp:'Ranks: ● 1, ● 2, ● 3; empty dots are not purchased yet.',cost:'Cost',buy:'Buy',reset:'Reset',availablePoints:'Available points',requirement:'Requirement',met:'Met',notMet:'Not met',current:'Current',next:'Next',masterySpecial:'enhanced {type} chassis',endlessLabel:'Endless',normalPoint:'Normal point',endlessWave:'Endless wave',lockedLevel:'Complete Normal to unlock more campaign levels and towers.',customLevel:'Custom',customMap:'Custom map · seed-enabled',waves:'waves',difficulty:'Difficulty',level:'Level',kills:'Enemies defeated',leaks:'Leaks',remainingLife:'Life remaining',remainingResources:'Resources remaining',reachedWave:'Wave reached',exitGame:'Exit game',gameWillClose:'The game will close.',browserSession:'Browser session',gameStaysOpen:'The game remains open locally',browserCloseInfo:'Browsers usually cannot close their own tabs for security reasons. No saves were deleted and no external changes were made.',tryCloseTab:'Try to close tab',mapHint:'Mouse wheel: zoom · Right mouse button: pan · Esc: clear selection',gravityHint:'Gravity bridge: choose second anchor · Right-click/Esc: cancel',immune:'IMMUNE'},
|
||||
de:{enterMenu:'Zum Hauptmenü',menu:'Menü',build:'Bauen',startStory:"Im Jahr 2149 fällt ein fremdes Imperium über die Erde her. Angeführt von Grump und seinem selbsternannten Diktatorship marschiert die MEGA-Armee – die Make-Earth-Great-Again-Armee – durch die letzten freien Städte. Ihre Kriegsmaschinen bauen Labyrinthe aus Stahl, um jeden Widerstand zu brechen. Doch die Verteidigung ist nicht gefallen: Du kommandierst den letzten orbitalen Abwehrsektor. Errichte deine Verteidigung, halte die Linien und zeige den Invasoren, dass die Erde sich nicht unterwerfen lässt. Grump, sein Diktatorship und die MEGA-Armee sind klar fiktive autoritäre Invasoren.",options:'Optionen',help:'Hilfe / Tutorial',displaySound:'Darstellung & Ton',damageNumbers:'Schadensanzeigen',volume:'Lautstärke',musicVolume:'Musiklautstärke',language:'Sprache',backMenu:'Zum Hauptmenü',roads:'Straßen',blockers:'Blocker',multiplier:'Wellen-HP-Multiplikator',roadHelp:'Straßen können nur zwischen Wellen gebaut werden. Jede Straße kostet ◈ 200 und muss eine vollständige Route vom Spawn zum Kern erhalten.',blockerHelp:'Blocker sperren eine Route. Gibt es keinen Umweg, greifen Gegner den ersten erreichbaren Blocker an; danach berechnen sie die Route neu.',multiplierHelp:'Der aktuelle Wert wird kumulativ auf diese Welle angewandt. „Nächster Faktor“ zeigt die nächste Welle anhand des weitesten gültigen Fortschritts der vorherigen Welle.',mission:'Missions-Briefing',normal:'Normal',endless:'Endlos',mainMenu:'Hauptmenü',play:'Spielen',levels:'Levelauswahl',nextLevel:'Nächstes Level',skills:'Skills',editor:'Leveleditor',exit:'Beenden',chooseLevel:'Level-Auswahl',levelHelp:'Wähle ein freigeschaltetes Level. Schütze den Kern vor den Horden.',globalSkills:'Globaler Skilltree',skillHelp:'Aus Kampagnensiegen gewonnene Punkte gelten für alle Level.',pause:'Pause',pausedHelp:'Die Simulation ist angehalten.',resume:'Fortsetzen',restart:'Neustart',victory:'Sieg!',levelComplete:'Level abgeschlossen',defeat:'Niederlage',coreFallen:'Der Kern ist gefallen',startWave:'Welle starten',buildRoad:'Straße bauen',construction:'BAUWERKZEUG',life:'Leben',resources:'Ressourcen',wave:'Welle',enemyHp:'Gegner-HP',currentWave:'Aktuelle Welle',notStarted:'noch nicht gestartet',nextFactor:'Nächster Faktor',cumulative:'kumulativ',roadBetween:'Straßen können nur zwischen Wellen gebaut werden',roadCost:'Neue Straße bauen (Kosten: ◈ {cost})',range:'Reichweite',damage:'Schaden',fireRate:'Feuerrate',auraBonuses:'Aura-Boni',sell:'Verkaufen',close:'Schließen',mastery:'Meisterschaft',masteryTier:'Meisterrang {rank}/3',masteryMax:'Meisterschaft vollständig',masteryDetails:'★ Meisterrang {rank}/3: +{damage}% Schaden, +{range}% Reichweite, +{rate}% Feuerrate · {special}',nextRank:'NÄCHSTER RANG {rank}',locked:'GESPERRT',rank:'Rang',skilltree:'Skilltree (3 Stufen):',ranksHelp:'Ränge: ● 1, ● 2, ● 3; ungefüllte Punkte sind noch nicht gekauft.',cost:'Kosten',buy:'Kaufen',reset:'Zurücksetzen',availablePoints:'Verfügbare Punkte',requirement:'Voraussetzung',met:'Erfüllt',notMet:'Nicht erfüllt',current:'Aktuell',next:'Nächster',masterySpecial:'verbessertes {type}-Gehäuse',endlessLabel:'Endlos',normalPoint:'Normalpunkt',endlessWave:'Endlos Welle',lockedLevel:'Normal abschließen, um weitere Kampagnen- und Turmfreischaltungen zu erhalten.',customLevel:'Benutzerdefiniert',customMap:'Eigene Karte · Seed-fähig',waves:'Wellen',difficulty:'Schwierigkeit',level:'Level',kills:'Gegner besiegt',leaks:'Lecks',remainingLife:'Rest-Leben',remainingResources:'Rest-Ressourcen',reachedWave:'Erreichte Welle',exitGame:'Spiel beenden',gameWillClose:'Das Spiel wird geschlossen.',browserSession:'Browser-Sitzung',gameStaysOpen:'Spiel bleibt lokal geöffnet',browserCloseInfo:'Browser-Tabs dürfen sich aus Sicherheitsgründen meist nicht selbst schließen. Es wurden keine Spielstände gelöscht und keine externen Änderungen vorgenommen.',tryCloseTab:'Tab schließen versuchen',mapHint:'Mausrad: Zoom · Rechte Maustaste: Karte verschieben · Esc: Auswahl lösen',gravityHint:'Gravitationsbrücke: zweiten Anker wählen · Rechtsklick/Esc abbrechen',immune:'IMMUN'}
|
||||
en:{enterMenu:'Main menu',menu:'Menu',build:'Build',startStory:"Year 2149: a foreign empire descends on Earth. Led by Grump and his self-proclaimed Diktatorship, the MEGA army—the Make-Earth-Great-Again army—marches through the last free cities. Their war machines build labyrinths of steel to break every resistance. But the defense has not fallen: you command the last orbital defense sector. Build your defenses, hold the lines, and show the invaders that Earth will not submit. Grump, his Diktatorship, and the MEGA army are clearly fictional authoritarian invaders.",waveFactorDisplay:'Last wave factor: {last} / Next wave factor: {next}',options:'Options',help:'Help / Tutorial',displaySound:'Display & sound',damageNumbers:'Damage numbers',volume:'Volume',musicVolume:'Music volume',language:'Language',backMenu:'Back to main menu',roads:'Roads',blockers:'Blockers',multiplier:'Wave HP multiplier',roadHelp:'Build roads only between waves. Each road costs ◈ 200 and must preserve a complete route from spawn to core.',blockerHelp:'Blockers close a route. If no detour exists, enemies attack the first reachable blocker; after it falls, they recalculate the next route.',multiplierHelp:'The current value is applied cumulatively to this wave. “Next factor” previews the next wave from the furthest valid progress reached in the prior wave.',mission:'Mission briefing',normal:'Normal',endless:'Endless',mainMenu:'Main menu',play:'Play',levels:'Level select',nextLevel:'Next level',skills:'Skills',editor:'Level editor',exit:'Exit',chooseLevel:'Choose a level',levelHelp:'Choose an unlocked level. Protect the core from the hordes.',globalSkills:'Global skill tree',skillHelp:'Points earned from campaign victories apply to every level.',pause:'Paused',pausedHelp:'The simulation is paused.',resume:'Resume',restart:'Restart',victory:'Victory!',levelComplete:'Level complete',defeat:'Defeat',coreFallen:'The core has fallen',startWave:'Start wave',buildRoad:'Build road',construction:'CONSTRUCTION',life:'Life',resources:'Resources',wave:'Wave',enemyHp:'Enemy HP',currentWave:'Current wave',notStarted:'not started',nextFactor:'Next factor',cumulative:'cumulative',roadBetween:'Roads can only be built between waves',roadCost:'Build new road (cost: ◈ {cost})',range:'Range',damage:'Damage',fireRate:'Fire rate',auraBonuses:'Aura bonuses',sell:'Sell',close:'Close',mastery:'Mastery',masteryTier:'Mastery rank {rank}/3',masteryMax:'Mastery complete',masteryDetails:'★ Mastery rank {rank}/3: +{damage}% damage, +{range}% range, +{rate}% fire rate · {special}',nextRank:'NEXT RANK {rank}',locked:'LOCKED',rank:'Rank',skilltree:'Skill tree (3 ranks):',ranksHelp:'Ranks: ● 1, ● 2, ● 3; empty dots are not purchased yet.',cost:'Cost',buy:'Buy',reset:'Reset',availablePoints:'Available points',requirement:'Requirement',met:'Met',notMet:'Not met',current:'Current',next:'Next',masterySpecial:'enhanced {type} chassis',endlessLabel:'Endless',normalPoint:'Normal point',endlessWave:'Endless wave',lockedLevel:'Complete Normal to unlock more campaign levels and towers.',customLevel:'Custom',customMap:'Custom map · seed-enabled',waves:'waves',difficulty:'Difficulty',level:'Level',kills:'Enemies defeated',leaks:'Leaks',remainingLife:'Life remaining',remainingResources:'Resources remaining',reachedWave:'Wave reached',exitGame:'Exit game',gameWillClose:'The game will close.',browserSession:'Browser session',gameStaysOpen:'The game remains open locally',browserCloseInfo:'Browsers usually cannot close their own tabs for security reasons. No saves were deleted and no external changes were made.',tryCloseTab:'Try to close tab',mapHint:'Mouse wheel: zoom · Right mouse button: pan · Esc: clear selection',gravityHint:'Gravity bridge: choose second anchor · Right-click/Esc: cancel',immune:'IMMUNE'},
|
||||
de:{enterMenu:'Zum Hauptmenü',menu:'Menü',build:'Bauen',startStory:"Im Jahr 2149 fällt ein fremdes Imperium über die Erde her. Angeführt von Grump und seinem selbsternannten Diktatorship marschiert die MEGA-Armee – die Make-Earth-Great-Again-Armee – durch die letzten freien Städte. Ihre Kriegsmaschinen bauen Labyrinthe aus Stahl, um jeden Widerstand zu brechen. Doch die Verteidigung ist nicht gefallen: Du kommandierst den letzten orbitalen Abwehrsektor. Errichte deine Verteidigung, halte die Linien und zeige den Invasoren, dass die Erde sich nicht unterwerfen lässt. Grump, sein Diktatorship und die MEGA-Armee sind klar fiktive autoritäre Invasoren.",waveFactorDisplay:'Letzter Wellenfaktor: {last} / Nächster Wellenfaktor: {next}',options:'Optionen',help:'Hilfe / Tutorial',displaySound:'Darstellung & Ton',damageNumbers:'Schadensanzeigen',volume:'Lautstärke',musicVolume:'Musiklautstärke',language:'Sprache',backMenu:'Zum Hauptmenü',roads:'Straßen',blockers:'Blocker',multiplier:'Wellen-HP-Multiplikator',roadHelp:'Straßen können nur zwischen Wellen gebaut werden. Jede Straße kostet ◈ 200 und muss eine vollständige Route vom Spawn zum Kern erhalten.',blockerHelp:'Blocker sperren eine Route. Gibt es keinen Umweg, greifen Gegner den ersten erreichbaren Blocker an; danach berechnen sie die Route neu.',multiplierHelp:'Der aktuelle Wert wird kumulativ auf diese Welle angewandt. „Nächster Faktor“ zeigt die nächste Welle anhand des weitesten gültigen Fortschritts der vorherigen Welle.',mission:'Missions-Briefing',normal:'Normal',endless:'Endlos',mainMenu:'Hauptmenü',play:'Spielen',levels:'Levelauswahl',nextLevel:'Nächstes Level',skills:'Skills',editor:'Leveleditor',exit:'Beenden',chooseLevel:'Level-Auswahl',levelHelp:'Wähle ein freigeschaltetes Level. Schütze den Kern vor den Horden.',globalSkills:'Globaler Skilltree',skillHelp:'Aus Kampagnensiegen gewonnene Punkte gelten für alle Level.',pause:'Pause',pausedHelp:'Die Simulation ist angehalten.',resume:'Fortsetzen',restart:'Neustart',victory:'Sieg!',levelComplete:'Level abgeschlossen',defeat:'Niederlage',coreFallen:'Der Kern ist gefallen',startWave:'Welle starten',buildRoad:'Straße bauen',construction:'BAUWERKZEUG',life:'Leben',resources:'Ressourcen',wave:'Welle',enemyHp:'Gegner-HP',currentWave:'Aktuelle Welle',notStarted:'noch nicht gestartet',nextFactor:'Nächster Faktor',cumulative:'kumulativ',roadBetween:'Straßen können nur zwischen Wellen gebaut werden',roadCost:'Neue Straße bauen (Kosten: ◈ {cost})',range:'Reichweite',damage:'Schaden',fireRate:'Feuerrate',auraBonuses:'Aura-Boni',sell:'Verkaufen',close:'Schließen',mastery:'Meisterschaft',masteryTier:'Meisterrang {rank}/3',masteryMax:'Meisterschaft vollständig',masteryDetails:'★ Meisterrang {rank}/3: +{damage}% Schaden, +{range}% Reichweite, +{rate}% Feuerrate · {special}',nextRank:'NÄCHSTER RANG {rank}',locked:'GESPERRT',rank:'Rang',skilltree:'Skilltree (3 Stufen):',ranksHelp:'Ränge: ● 1, ● 2, ● 3; ungefüllte Punkte sind noch nicht gekauft.',cost:'Kosten',buy:'Kaufen',reset:'Zurücksetzen',availablePoints:'Verfügbare Punkte',requirement:'Voraussetzung',met:'Erfüllt',notMet:'Nicht erfüllt',current:'Aktuell',next:'Nächster',masterySpecial:'verbessertes {type}-Gehäuse',endlessLabel:'Endlos',normalPoint:'Normalpunkt',endlessWave:'Endlos Welle',lockedLevel:'Normal abschließen, um weitere Kampagnen- und Turmfreischaltungen zu erhalten.',customLevel:'Benutzerdefiniert',customMap:'Eigene Karte · Seed-fähig',waves:'Wellen',difficulty:'Schwierigkeit',level:'Level',kills:'Gegner besiegt',leaks:'Lecks',remainingLife:'Rest-Leben',remainingResources:'Rest-Ressourcen',reachedWave:'Erreichte Welle',exitGame:'Spiel beenden',gameWillClose:'Das Spiel wird geschlossen.',browserSession:'Browser-Sitzung',gameStaysOpen:'Spiel bleibt lokal geöffnet',browserCloseInfo:'Browser-Tabs dürfen sich aus Sicherheitsgründen meist nicht selbst schließen. Es wurden keine Spielstände gelöscht und keine externen Änderungen vorgenommen.',tryCloseTab:'Tab schließen versuchen',mapHint:'Mausrad: Zoom · Rechte Maustaste: Karte verschieben · Esc: Auswahl lösen',gravityHint:'Gravitationsbrücke: zweiten Anker wählen · Rechtsklick/Esc abbrechen',immune:'IMMUN'}
|
||||
};
|
||||
const TOWER_EN={puls:['Pulse Cannon','Fast single-projectile cannon for early damage.'],plasma:['Plasma Tower','High-damage plasma beam with area splash.'],kryo:['Cryo Emitter','Randomly freezes a target in range; low damage.'],magnet:['Flamethrower','Wide plasma cone that burns dense hordes.'],nano:['Nanite Swarm','Launches fast ground hunters that pursue and strike targets.'],grav:['Gravity Bridge','Two anchors form a slowing, vulnerability-inducing gravity line.'],barrier:['Mortar','Long-range mortar shells deal area damage and stun.'],holo:['Hologram Tower','Creates holograms that lure enemies and deal damage.'],quantum:['Quantum Line Laser','Piercing line laser that scales with each hit.'],nuclear:['Nuclear Reactor','Slow, extreme area damage for the late game.'],sniper:['Sniper','Extreme range; the first pierced target takes precision damage.'],commander:['Command Post','Armored volley platform with synchronized shots.'],tesla:['Tesla Coil','Chain lightning control tower.'],aegis_amplifier:['Aegis Amplifier','Strengthens adjacent combat towers.'],chrono_relay:['Chrono Relay','Accelerates adjacent combat towers.'],range_relay:['Range Relay','Extends adjacent combat towers.'],blocker:['Blocker','Blocks a road until enemies destroy it.'],barracks:['Barracks','Endgame path building that deploys limited ground squads to hunt and briefly block enemies.']};
|
||||
const SKILL_EN={puls_1:'Double pulse',puls_2:'Range boost',puls_3:'Piercing',plasma_1:'Splash radius',plasma_2:'Burn',plasma_3:'Critical pulse',kryo_1:'Deep freeze',kryo_2:'Ice chain',kryo_3:'Ice armor',magnet_1:'Pressure valve',magnet_2:'Spread cone',magnet_3:'Thermite gel',nano_1:'Swarm size',nano_2:'Pursuit AI',nano_3:'Self replication',grav_1:'Compressed core',grav_2:'Unstable matter',grav_3:'Chrono field',barrier_1:'Explosive charge',barrier_2:'Cluster ammunition',barrier_3:'Shock fuse',holo_1:'More holograms',holo_2:'Deception',holo_3:'Clone explosion',quantum_1:'Focus lenses',quantum_2:'Prism cut',quantum_3:'Phase resonance',nuclear_1:'Critical mass',nuclear_2:'Radioactive rain',nuclear_3:'Meltdown',sniper_1:'AP core',sniper_2:'Rail accelerator',sniper_3:'Collateral shot',commander_1:'Area barrage',commander_2:'Fire rate',commander_3:'Alternating payload',tesla_1:'Chain lightning damage',tesla_2:'Additional chain target',tesla_3:'Shock charge',blocker_1:'Maximum durability',blocker_2:'Reinforcement',blocker_3:'Self repair',barracks_1:'Squad capacity',barracks_2:'Pulse rifle',barracks_3:'Field armor'};
|
||||
@@ -3446,6 +3514,9 @@ const MISSION_BRIEFINGS=[
|
||||
];
|
||||
function language(){ return Game.language==='de'?'de':'en'; }
|
||||
function tr(key,vars={}){ const text=(LOCALE[language()]||LOCALE.en)[key]||key; return text.replace(/\{(\w+)\}/g,(_,name)=>vars[name]??'{'+name+'}'); }
|
||||
function formatWaveFactor(value){ return Number(value).toFixed(2).replace('.',','); }
|
||||
function lastWaveFactorDisplay(last,next){ return tr('waveFactorDisplay',{last:last===null?'–':formatWaveFactor(last),next:formatWaveFactor(next)}); }
|
||||
function waveFactorBarPercent(factor){ const range=MAX_ADAPTIVE_MULTIPLIER-MIN_ADAPTIVE_MULTIPLIER; return ((clamp(Number(factor),MIN_ADAPTIVE_MULTIPLIER,MAX_ADAPTIVE_MULTIPLIER)-MIN_ADAPTIVE_MULTIPLIER)/range*100); }
|
||||
function translateUI(){ document.documentElement.lang=language(); document.querySelectorAll('[data-i18n]').forEach(el=>el.textContent=tr(el.dataset.i18n)); const staticText={ 'btn-build-menu':'build','btn-wave-now':'startWave','btn-wave':'startWave','btn-pause':'pause','btn-restart':'restart','btn-levels':'levels','btn-road':'buildRoad','btn-play':'play','btn-menu-levels':'levels','btn-menu-skills':'skills','btn-menu-editor':'editor','btn-menu-exit':'exit','btn-back':'backMenu','btn-skills-back':'backMenu','btn-resume':'resume','btn-p-restart':'restart','btn-p-levels':'levels','btn-next':'nextLevel','btn-v-levels':'levels','btn-v-restart':'restart','btn-d-restart':'restart','btn-d-levels':'levels','editor-play':'play','editor-close':'close','btn-debug-close':'close'}; Object.entries(staticText).forEach(([id,key])=>{const el=$(id);if(el)el.textContent=tr(key);}); const help=$('help-content'); if(help) help.innerHTML='<h2>'+tr('roads')+'</h2><p>'+tr('roadHelp')+'</p><h2>'+tr('blockers')+'</h2><p>'+tr('blockerHelp')+'</p><h2>'+tr('multiplier')+'</h2><p>'+tr('multiplierHelp')+'</p>'; buildLevelGrid(); }
|
||||
function loadLanguage(){ try { const saved=localStorage.getItem(LANGUAGE_KEY); Game.language=saved==='de'||saved==='en'?saved:DEFAULT_LANGUAGE; } catch(e){ Game.language=DEFAULT_LANGUAGE; } }
|
||||
function setLanguage(value){ Game.language=value==='de'?'de':'en'; try { localStorage.setItem(LANGUAGE_KEY,Game.language); } catch(e){} const select=$('language-select'); if(select) select.value=Game.language; translateUI(); }
|
||||
@@ -3483,11 +3554,15 @@ function updateHUD(){
|
||||
const lvlName = Game.endless ? lv.name + ' ('+tr('endlessLabel')+')' : lv.name;
|
||||
$('hud-lvl').innerHTML='<b>'+tr('level')+' '+(Game.levelIndex+1)+'</b><span>'+lvlName+'</span>';
|
||||
const hpCurrent=Game.waveActive?Game.waveHpMultiplier:Game.nextWaveMultiplier;
|
||||
const hpEl=$('hud-hp-multiplier'), nextHpEl=$('hud-hp-next');
|
||||
const hpEl=$('hud-hp-multiplier');
|
||||
if(hpEl) hpEl.textContent=hpCurrent.toFixed(3).replace('.',',')+'×';
|
||||
if(nextHpEl){
|
||||
nextHpEl.textContent=Game.lastWaveFactor===null?tr('nextFactor')+': –':tr('nextFactor')+': '+Game.lastWaveFactor.toFixed(3).replace('.',',')+'× · '+tr('cumulative');
|
||||
}
|
||||
const factor=liveWaveFactor(), factorFill=$('hud-wave-factor-fill');
|
||||
if(factorFill){ factorFill.style.width=waveFactorBarPercent(factor).toFixed(2)+'%'; }
|
||||
const factorText=language()==='de'?'Nächster-Wellen-Faktor':'Next-wave factor';
|
||||
const factorDisplay=$('hud-wave-factor-text');
|
||||
if(factorDisplay){ factorDisplay.textContent=lastWaveFactorDisplay(Game.lastWaveFactor,factor); }
|
||||
const factorBar=document.querySelector('.wave-factor-progress');
|
||||
if(factorBar){ factorBar.setAttribute('aria-label',factorText); factorBar.setAttribute('aria-valuenow',String(factor)); }
|
||||
const legendEl=$('hud-wave-legend');
|
||||
if(legendEl){
|
||||
const legend=Game.waveLegend||[];
|
||||
@@ -3575,22 +3650,51 @@ function rankDots(rank){
|
||||
}
|
||||
|
||||
function masteryRoleText(t, bonus){
|
||||
const rank=bonus.special;
|
||||
const de=language()==='de';
|
||||
const effects={
|
||||
puls:de?'+'+rank+' Salve/ Durchschlag':'+'+rank+' volley / pierce', plasma:de?'größere Explosionen und Brand':'larger blasts and burn',
|
||||
kryo:de?'+'+rank+' Kettenziel(e) und Eisstapel':'+'+rank+' chain target(s) and ice stacks', magnet:de?'Kills geben +200/+400/+750 Ressourcen':'kills grant +200/+400/+750 resources',
|
||||
nano:de?'+'+rank+' Jäger und höhere Obergrenze':'+'+rank+' hunters and larger cap', grav:de?'stärkeres Zeitfeld':'stronger time field',
|
||||
barrier:de?'Minenfeld: 1.000 AoE-Schaden':'minefield: 1,000 AoE damage', barracks:de?'+'+rank+' Truppkapazität':'+'+rank+' squad capacity', holo:de?'+'+rank+' Hologramm(e)':'+'+rank+' hologram(s)',
|
||||
quantum:de?'verstärkte Reihenlaser':'stronger line laser', nuclear:de?'rote Meister-Naniten: 40 Schaden pro Tick':'red mastery nanites: 40 damage per tick',
|
||||
sniper:de?'+'+rank+' Durchschlag':'+'+rank+' pierce', commander:de?'+'+rank+' Salvenprojektil(e)':'+'+rank+' volley projectile(s)', tesla:de?'+'+rank+' Kettenziel(e)':'+'+rank+' chain target(s)'
|
||||
const rank=clamp(t.masteryRank(),1,MASTERY_MAX_LEVEL), de=language()==='de';
|
||||
const text={
|
||||
puls:[
|
||||
'Rang 1: +1 Projektil und +1 Durchschlagziel pro Salve.',
|
||||
'Rang 2: +2 Projektile und +2 Durchschlagziele pro Salve.',
|
||||
'Rang 3: +3 Projektile und +3 Durchschlagziele; Treffer gegen Gegner über 50% HP verursachen doppelten Schaden.'
|
||||
],
|
||||
plasma:['Rang 1: Explosion +8px und Brennen +2 Schaden/Sek.; Rang 2: +16px/+4; Rang 3: +24px/+6 und Direkttreffer verursachen 50% mehr Schaden.'],
|
||||
kryo:['Rang 1: +1 Kettenziel und +1 Eisstapel; Rang 2: +2/+2; Rang 3: +3/+3 und Direkttreffer frieren 0,3s ein.'],
|
||||
magnet:['Rang 1/2/3: Kegel +4/+8/+12% breiter; Kills durch den Kegel geben 200/400/750 Ressourcen.'],
|
||||
nano:['Rang 1: +1 Jäger pro Start und +2 Obergrenze; Rang 2: +2 Jäger/Obergrenze; Rang 3: +3 Jäger/Obergrenze und +5s Lebensdauer. Das Rangprofil verbessert zusätzlich Turmschaden und Feuerrate.'],
|
||||
grav:['Rang 1/2/3: Feldschaden 15/25/75 pro 0,1s (mit Feldstärke skaliert); reguläre Verlangsamung, Verwundbarkeit und Dauer steigen separat je Skillrang.'],
|
||||
barrier:['Rang 1/2/3: Der Mörser erhält das Rangprofil (+60%/+150%/+400% Schaden, +15%/+20%/+25% Reichweite, +10%/+15%/+20% Feuerrate); erst Rang 3 hinterlässt jeder Einschlag eine Mine mit 1.000 AoE-Schaden.'],
|
||||
holo:['Rang 1: +1 Hologramm; Rang 2: +2; Rang 3: +3 und Hologrammziele erleiden 25% mehr Schaden.'],
|
||||
quantum:['Rang 1/2/3: Reihenlaser erhält das Rangprofil: +55%/+180%/+420% Schaden, +15%/+22%/+30% Reichweite und +10%/+15%/+20% Feuerrate.'],
|
||||
nuclear:['Rang 1: +65% Schaden, +10% Reichweite und +10% Feuerrate. Rang 2: +200% Schaden, +15% Reichweite und +15% Feuerrate; jeder gültige Treffer erzeugt exakt 1 roten Naniten. Rang 3: +500% Schaden, +20% Reichweite und +20% Feuerrate; jeder gültige Treffer erzeugt exakt 3 rote Naniten.'],
|
||||
sniper:['Rang 1/2/3: +1/+2/+3 Durchschlagziele; Rang 3 führt zusätzlich 2x Schaden gegen Ziele unter 25% HP aus.'],
|
||||
commander:['Rang 1/2/3: +1/+2/+3 Salvenprojektile; Rang 3 lässt die Projektile selbstständig Ziele suchen.'],
|
||||
tesla:['Rang 1: +1 Kettenziel und Rangprofil (+50% Schaden, +15% Reichweite, +15% Feuerrate); Rang 2: +2 Kettenziele und Rangprofil (+180% Schaden, +22% Reichweite, +25% Feuerrate); Rang 3: +3 Kettenziele, Rangprofil (+420% Schaden, +30% Reichweite, +35% Feuerrate) und statische Wolke am Primärziel, 50 Schaden/Sek. für 5s.'],
|
||||
barracks:['Rang 1/2/3: +1/+2/+3 Truppkapazität, Lebensdauer +1/+2/+3s; Einheiten erhalten den jeweiligen Meister-Schadensbonus und Spawnintervall 0,20/0,18/0,15s.']
|
||||
};
|
||||
return effects[t.type] || (de?'verstärktes Waffensystem':'enhanced weapon system');
|
||||
const deText=text[t.type]?.[rank-1]||text[t.type]?.[0]||'Turmspezifische Meisterverstärkung.';
|
||||
if(de) return deText;
|
||||
const en={
|
||||
puls:['Rank 1: +1 projectile and +1 pierce target per volley.','Rank 2: +2 projectiles and +2 pierce targets per volley.','Rank 3: +3 projectiles and +3 pierce targets; hits against enemies above 50% HP deal double damage.'],
|
||||
plasma:['Rank 1: blast +8px and burn +2 damage/sec.','Rank 2: blast +16px and burn +4 damage/sec.','Rank 3: blast +24px, burn +6 damage/sec, and direct hits deal 50% more damage.'],
|
||||
kryo:['Rank 1: +1 chain target and ice stack.','Rank 2: +2 chain targets and ice stacks.','Rank 3: +3 chain targets/stacks and direct hits freeze for 0.3s.'],
|
||||
magnet:['Rank 1/2/3: cone widens by +4/+8/+12%; kills by the cone grant 200/400/750 resources.'],
|
||||
nano:['Rank 1: +1 hunter per launch and +2 cap, plus the Rank-1 stat profile. Rank 2: +2 hunters/cap and the Rank-2 stat profile. Rank 3: +3 hunters/cap, +5s life, and the Rank-3 stat profile.'],
|
||||
grav:['Rank 1/2/3: field damage is 15/25/75 per 0.1s, scaled by field strength; regular slow, vulnerability, and duration still come from their skill ranks.'],
|
||||
barrier:['Rank 1/2/3: mortar receives the rank-profile bonus (+60%/+150%/+400% damage, +15%/+20%/+25% range, +10%/+15%/+20% fire rate); only Rank 3 makes every impact leave a mine dealing 1,000 area damage.'],
|
||||
holo:['Rank 1: +1 hologram. Rank 2: +2 holograms. Rank 3: +3 holograms and hologram targets take 25% more damage.'],
|
||||
quantum:['Rank 1/2/3: line laser gains the rank profile: +50%/+125%/+250% damage, +10%/+15%/+20% range and fire rate.'],
|
||||
nuclear:['Rank 1: +65% damage, +10% range, and +10% fire rate. Rank 2: +200% damage, +15% range, and +15% fire rate; every valid hit spawns exactly 1 red nanite. Rank 3: +500% damage, +20% range, and +20% fire rate; every valid hit spawns exactly 3 red nanites.'],
|
||||
sniper:['Rank 1/2/3: +1/+2/+3 pierce targets. Rank 3 also deals 2x damage to targets below 25% HP.'],
|
||||
commander:['Rank 1/2/3: +1/+2/+3 volley projectiles. Rank 3 projectiles home toward targets.'],
|
||||
tesla:['Rank 1: +1 chain target and rank profile (+50% damage, +15% range, +15% fire rate). Rank 2: +2 chain targets and rank profile (+180% damage, +22% range, +25% fire rate). Rank 3: +3 chain targets, rank profile (+420% damage, +30% range, +35% fire rate), and a static cloud at the primary target dealing 50 damage/sec for 5s.'],
|
||||
barracks:['Rank 1/2/3: +1/+2/+3 squad capacity, +1/+2/+3s unit life, rank-profile squad damage and 0.20/0.18/0.15s spawn interval.']
|
||||
};
|
||||
return en[t.type]?.[rank-1]||en[t.type]?.[0]||'Tower-specific mastery enhancement.';
|
||||
}
|
||||
function drawMasteryDetails(t){
|
||||
const masteryRank=t.masteryRank(); if(!masteryRank) return '';
|
||||
const bonus=masteryProfile(t, masteryRank);
|
||||
const special=tr('masterySpecial',{type:towerName(t.def)})+' · '+masteryRoleText(t, bonus);
|
||||
const special=masteryRoleText(t, bonus);
|
||||
return '<div class="tp-desc" style="color:#fff3a3">'+tr('masteryDetails',{rank:masteryRank,damage:Math.round(bonus.damage*100),range:Math.round(bonus.range*100),rate:Math.round(bonus.rate*100),special})+'</div>';
|
||||
}
|
||||
function showTowerPanel(t){
|
||||
@@ -3632,7 +3736,7 @@ function showTowerPanel(t){
|
||||
html+='</div>';
|
||||
}
|
||||
const masteryCost=masteryUpgradeCost(t);
|
||||
if(canBuyMastery(t)) html+='<div class="tp-actions"><button class="btn primary" id="tp-mastery">★ '+tr('masteryTier',{rank:t.masteryRank()+1})+' · ◈ '+masteryCost+'</button></div>';
|
||||
if(t.type!=='blocker' && canBuyMastery(t)) html+='<div class="tp-actions"><button class="btn primary" id="tp-mastery" '+(Game.credits<masteryCost?'disabled':'')+'>★ '+tr('masteryTier',{rank:t.masteryRank()+1})+' · ◈ '+masteryCost+'</button></div>';
|
||||
if(t.masteryRank()>=MASTERY_MAX_LEVEL) html+='<div class="tp-desc" style="color:#fff3a3">★ '+tr('masteryMax')+'</div>';
|
||||
if(t.def.support){
|
||||
const cost=t.auraLevel>=AURA_MAX_LEVEL?null:auraUpgradeCost(t);
|
||||
@@ -3984,7 +4088,14 @@ canvas.addEventListener('contextmenu',(e)=>{
|
||||
document.querySelectorAll('.tcard').forEach(c=>c.classList.remove('sel'));
|
||||
});
|
||||
|
||||
function isFocusedTextInput(target){
|
||||
const el=target||document.activeElement;
|
||||
if(!el) return false;
|
||||
const tag=String(el.tagName||'').toLowerCase();
|
||||
return tag==='input'||tag==='textarea'||tag==='select'||el.isContentEditable===true||el.classList?.contains?.('debug-console-input');
|
||||
}
|
||||
window.addEventListener('keydown',(e)=>{
|
||||
if(isFocusedTextInput(e.target)) return;
|
||||
if((e.key==='F12'||((e.ctrlKey||e.metaKey)&&e.shiftKey&&e.key.toLowerCase()==='d')) && isElectronRuntime()){
|
||||
e.preventDefault(); openDebugConsole(); return;
|
||||
}
|
||||
@@ -4567,9 +4678,23 @@ function playSound(name, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Original procedural music: a small synthesized, loopable sci-fi score; no samples or external assets.
|
||||
const DESKTOP_SOUNDTRACK = 'assets/futuristic_psychedelic_tower_defense_clean.wav';
|
||||
function syncDesktopSoundtrack(){
|
||||
const a=Game.audio;
|
||||
if(!isElectronRuntime()) return false;
|
||||
if(!a.musicTrack){
|
||||
a.musicTrack=new Audio(DESKTOP_SOUNDTRACK);
|
||||
a.musicTrack.loop=true;
|
||||
a.musicTrack.preload='auto';
|
||||
}
|
||||
a.musicTrack.volume=a.musicMuted?0:clamp(a.musicVolume,0,1);
|
||||
a.musicTrack.muted=!!a.musicMuted;
|
||||
if(!a.musicMuted){ a.musicTrack.play().catch(()=>{}); }
|
||||
return true;
|
||||
}
|
||||
// Browser builds retain the small procedural score. The packaged Electron build uses the supplied looping WAV.
|
||||
function startMusicAfterGesture(){
|
||||
const a=Game.audio; if(!a.initialized||!a.ctx||a.musicTimer) return;
|
||||
const a=Game.audio; if(!a.initialized||!a.ctx||syncDesktopSoundtrack()||a.musicTimer) return;
|
||||
const ctx=a.ctx, motif=[220,0,330,440,330,0,294,247,220,0,392,440,494,440,330,0];
|
||||
a.musicTimer=setInterval(()=>{
|
||||
if(!a.initialized||!a.ctx) return;
|
||||
@@ -4767,8 +4892,8 @@ if (muteBtn) {
|
||||
const musicMuteBtn=document.getElementById('btn-music-mute');
|
||||
const musicSlider=document.getElementById('music-volume-slider');
|
||||
function refreshMusicControls(){ if(musicMuteBtn) musicMuteBtn.textContent=Game.audio.musicMuted?'🎵✕':'🎵'; if(musicSlider) musicSlider.value=Game.audio.musicVolume; }
|
||||
if(musicMuteBtn) musicMuteBtn.addEventListener('click',()=>{ Game.audio.musicMuted=!Game.audio.musicMuted; if(Game.audio.musicGain) Game.audio.musicGain.gain.value=Game.audio.musicMuted?0:Game.audio.musicVolume; saveMusicSettings(); refreshMusicControls(); });
|
||||
if(musicSlider) musicSlider.addEventListener('input',e=>{ Game.audio.musicVolume=clamp(parseFloat(e.target.value),0,1); if(Game.audio.musicGain&&!Game.audio.musicMuted) Game.audio.musicGain.gain.value=Game.audio.musicVolume; saveMusicSettings(); });
|
||||
if(musicMuteBtn) musicMuteBtn.addEventListener('click',()=>{ Game.audio.musicMuted=!Game.audio.musicMuted; if(Game.audio.musicGain) Game.audio.musicGain.gain.value=Game.audio.musicMuted?0:Game.audio.musicVolume; syncDesktopSoundtrack(); saveMusicSettings(); refreshMusicControls(); });
|
||||
if(musicSlider) musicSlider.addEventListener('input',e=>{ Game.audio.musicVolume=clamp(parseFloat(e.target.value),0,1); if(Game.audio.musicGain&&!Game.audio.musicMuted) Game.audio.musicGain.gain.value=Game.audio.musicVolume; syncDesktopSoundtrack(); saveMusicSettings(); });
|
||||
refreshMusicControls();
|
||||
|
||||
const languageSelect=document.getElementById('language-select');
|
||||
|
||||
Reference in new issue
Block a user