tickcli-js 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3627 @@
1
+ <!doctype html><!-- made by scripts/bundle-game.ts from space-squad/: edit that folder, not this file --><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"><title>Star Squad: Alien Blitz</title>
2
+ <meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes"><meta name="format-detection" content="telephone=no"><meta name="theme-color" content="#0b0b1e">
3
+ <style>
4
+ /* Star Squad: page styles. The game shell (lib:game) handles layout, the canvas and the touch controls;
5
+ this file only stops phone browser gestures from getting in the way of play. */
6
+
7
+ /* No tap flash, no text selection, no long-press callout or magnifier (iOS loupe), no font boosting */
8
+ html, body, body * {
9
+ -webkit-tap-highlight-color: transparent;
10
+ -webkit-touch-callout: none;
11
+ -webkit-user-select: none;
12
+ user-select: none;
13
+ -webkit-text-size-adjust: 100%;
14
+ text-size-adjust: 100%;
15
+ }
16
+ /* manipulation = taps and scrolling still work, but double-tap zoom and its 300 ms delay are gone */
17
+ html, body, body * { touch-action: manipulation; }
18
+ /* the game canvas takes every touch itself: no pinch, pan or zoom while you hold Fire or the d-pad */
19
+ canvas { touch-action: none; }
20
+ /* no rubber-band bounce or pull-to-refresh while playing */
21
+ html, body { overscroll-behavior: none; }
22
+ /* pictures can't be dragged out or saved by a long press */
23
+ img { -webkit-user-drag: none; user-drag: none; }
24
+ /* text fields (room code, names) stay normal so typing and pasting work */
25
+ input, textarea, select, [contenteditable] {
26
+ -webkit-user-select: text;
27
+ user-select: text;
28
+ -webkit-touch-callout: default;
29
+ }
30
+
31
+ </style></head><body>
32
+ <script>
33
+ // Star Squad: phone browser guards (loads first). CSS in style.css does most of the work; this covers what CSS can't:
34
+ // Safari's pinch gestures, double-tap zoom on older iPhones, long-press menus and stray text selection.
35
+ "use strict";
36
+ (function () {
37
+ const typing = e => { const t = e.target; return !!(t && t.closest && t.closest("input, textarea, select, [contenteditable]")); };
38
+ const stop = e => { if (!typing(e) && e.cancelable) e.preventDefault(); };
39
+ // pinch-zoom (Safari's own gesture events, and two-finger touches everywhere else)
40
+ for (const ev of ["gesturestart", "gesturechange", "gestureend"]) document.addEventListener(ev, stop, { passive: false });
41
+ document.addEventListener("touchmove", e => { if (e.touches.length > 1) stop(e); }, { passive: false });
42
+ // double-tap zoom on older iOS: a second tap on the same spot within 300 ms is swallowed outside buttons and inputs
43
+ let lastTap = 0, lx = 0, ly = 0;
44
+ document.addEventListener("touchend", e => {
45
+ const t = e.changedTouches[0], now = e.timeStamp, el = e.target;
46
+ const btn = el && el.closest && el.closest("button, a, input, textarea, select, [role=button]");
47
+ if (!btn && t && now - lastTap < 300 && Math.abs(t.clientX - lx) < 40 && Math.abs(t.clientY - ly) < 40) stop(e);
48
+ lastTap = now; if (t) { lx = t.clientX; ly = t.clientY; }
49
+ }, { passive: false });
50
+ document.addEventListener("dblclick", stop, { passive: false });
51
+ // long press: no "Save image / Copy" menu, no text selection handles
52
+ document.addEventListener("contextmenu", stop);
53
+ document.addEventListener("selectstart", stop);
54
+ // if a triple tap still managed to select something, clear it
55
+ document.addEventListener("selectionchange", () => { const s = window.getSelection && window.getSelection(); if (s && s.rangeCount && !typing({ target: document.activeElement })) s.removeAllRanges(); });
56
+ })();
57
+
58
+ </script>
59
+ <script type="application/json" id="levels">{"planets":[{"n":"Verdis Prime","sky":["#08202e","#1f6f5c"],"far":"#123d3a","mid":"#1b5e4a","gr":"#2d4a3a","top":"#74c69d","a1":"#b5e48c","a2":"#38b000","moon":"#a7c957","ring":0,"pool":[0,0,1,2,3,6],"boss":"Queen Glorpa","fly":0,"deco":"fern","traps":["acid","spore"],"wx":[0,1,2,5]},{"n":"Pyros","sky":["#1e0606","#a33a12"],"far":"#3d1010","mid":"#5c1a0f","gr":"#3b1d14","top":"#ff7b00","a1":"#ffba08","a2":"#d00000","moon":"#ffba08","ring":0,"pool":[1,1,2,3,4,6],"boss":"Magma Titan","fly":0,"deco":"rock","traps":["lava","geyser"],"wx":[0,6,7,2]},{"n":"Cryonis","sky":["#0a1a3a","#5fa8d3"],"far":"#1b3a5c","mid":"#3a6ea5","gr":"#5a82a6","top":"#e0fbfc","a1":"#caf0f8","a2":"#0077b6","moon":"#caf0f8","ring":1,"pool":[0,2,2,3,4,6],"boss":"Frost Wyrm","fly":1,"deco":"ice","traps":["ice","icicle"],"wx":[0,3,4,5]},{"n":"Neon Moon","sky":["#10002b","#5a189a"],"far":"#240046","mid":"#3c096c","gr":"#240046","top":"#e0aaff","a1":"#f72585","a2":"#7209b7","moon":"#f72585","ring":1,"pool":[1,2,3,3,4,6],"boss":"Crystal Overmind","fly":0,"deco":"crystal","traps":["fence","pad"],"wx":[0,1,2,7]},{"n":"Void Hive","sky":["#050505","#3d0c11"],"far":"#1a0508","mid":"#2b0b0e","gr":"#1a1414","top":"#9d0208","a1":"#ff595e","a2":"#6a040f","moon":"#9d0208","ring":0,"pool":[0,1,2,3,4,4,6,6],"boss":"Hive Mother","fly":1,"deco":"spike","traps":["goo","spikes"],"wx":[0,5,8,2]}],"songs":[{"n":"Jungle Drop","bpm":118,"root":43,"sc":[0,2,4,5,7,9,10],"prog":[0,6,3,0,0,6,4,4],"k":"x.....x...x.....","s":"....x.......x...","h":"x.x.x.x.x.x.x.xo","b":"0..0..2.3..0.2..","bw":"triangle","bv":0.2,"bl":900,"a":"0.1.2.3.2.1.0.2.","aw":"triangle","av":0.05,"ao":24,"pw":"sine","pv":0.022,"lw":"square","lv":0.035,"ll":2600,"lo":24,"ld":0.62,"echo":0.5,"seed":11},{"n":"Magma Run","bpm":140,"root":40,"sc":[0,1,3,5,7,8,10],"prog":[0,0,1,0,5,5,6,1],"k":"x.x...x.x.x...x.","s":"....x.......x..x","h":"x.x.x.x.x.x.x.x.","b":"0000000000003.2.","bw":"sawtooth","bv":0.13,"bl":650,"a":"0...2...3...2...","aw":"sawtooth","av":0.04,"ao":12,"pw":null,"pv":0,"lw":"sawtooth","lv":0.03,"ll":2000,"lo":24,"ld":0.7,"echo":0.25,"stab":1,"seed":23},{"n":"Frozen Signal","bpm":92,"root":38,"sc":[0,2,3,5,7,9,10],"prog":[0,0,2,3,5,3,2,4],"k":"x.........x.....","s":"........x.......","h":"..x...x...x...x.","b":"0.......2.......","bw":"sine","bv":0.26,"bl":500,"a":"3.2.1.0.4.2.1.2.","aw":"sine","av":0.045,"ao":36,"bell":1,"pw":"triangle","pv":0.028,"lw":"sine","lv":0.05,"ll":5000,"lo":36,"ld":0.4,"echo":0.8,"seed":37},{"n":"Neon Moonlight","bpm":108,"root":45,"sc":[0,2,3,5,7,8,10],"prog":[0,5,2,6],"k":"x...x...x...x...","s":"....x.......x...","h":"..x...x...x...x.","b":"0303030303030303","bw":"sawtooth","bv":0.1,"bl":800,"a":"0123012301230123","aw":"square","av":0.03,"ao":24,"pw":"sawtooth","pv":0.016,"lw":"square","lv":0.035,"ll":3000,"lo":24,"ld":0.55,"echo":0.6,"seed":53},{"n":"Hive Pulse","bpm":84,"root":36,"sc":[0,1,4,5,7,8,11],"prog":[0,0,1,0,0,0,5,4],"k":"x.....x..x......","s":"........x.......","h":"x..x..x..x..x...","b":"0.0.0.0.0.0.1.0.","bw":"sawtooth","bv":0.12,"bl":420,"a":null,"aw":"","av":0,"ao":0,"pw":"sine","pv":0.03,"drone":1,"lw":"triangle","lv":0.05,"ll":1800,"lo":24,"ld":0.32,"echo":0.7,"metal":1,"seed":71}],"bossSongs":[{"n":"Slime Queen's Court","boss":1,"bpm":136,"root":41,"sc":[0,1,4,5,7,8,10],"prog":[0,0,1,0,5,5,1,0],"k":"x..x..x.x..x..x.","s":"....x.......x..x","h":"x.xxx.xxx.xxx.xo","b":"0.00.2.03.0.2.1.","bw":"square","bv":0.1,"bl":700,"a":"0.2.3.2.0.2.4.2.","aw":"triangle","av":0.04,"ao":24,"pw":null,"pv":0,"lw":"square","lv":0.04,"ll":2800,"lo":24,"ld":0.7,"echo":0.35,"seed":101},{"n":"Titan's Forge","boss":1,"bpm":104,"root":33,"sc":[0,1,3,5,7,8,10],"prog":[0,0,1,0,0,0,6,1],"k":"x.x.....x.x.....","s":"....x.......x...","h":"x.x.x.x.x.x.x.x.","b":"00..0.00..0.3.1.","bw":"sawtooth","bv":0.14,"bl":500,"a":null,"aw":"","av":0,"ao":0,"pw":"sawtooth","pv":0.015,"lw":"sawtooth","lv":0.035,"ll":1600,"lo":24,"ld":0.5,"echo":0.3,"stab":1,"metal":1,"seed":211},{"n":"Wyrm Dive","boss":1,"bpm":154,"root":38,"sc":[0,2,3,5,7,8,11],"prog":[0,5,3,4,0,5,6,4],"k":"x.....x.x.....x.","s":"....x.......x...","h":"xxxxxxxxxxxxxxxx","b":"0.3.0.3.0.3.2.4.","bw":"triangle","bv":0.2,"bl":900,"a":"4.3.2.0.4.3.2.1.","aw":"sine","av":0.045,"ao":36,"bell":1,"pw":"triangle","pv":0.02,"lw":"sine","lv":0.055,"ll":5000,"lo":36,"ld":0.75,"echo":0.6,"seed":307},{"n":"Prism Protocol","boss":1,"bpm":126,"root":40,"sc":[0,2,3,5,7,9,10],"prog":[0,0,3,3,5,5,4,4],"k":"x...x...x...x..x","s":"....x.......x...","h":"..x...x...x...x.","b":"0.0.3.0.0.0.3.2.","bw":"sawtooth","bv":0.11,"bl":1000,"a":"0123321001233210","aw":"sawtooth","av":0.03,"ao":24,"pw":"sawtooth","pv":0.018,"lw":"square","lv":0.04,"ll":3200,"lo":24,"ld":0.6,"echo":0.55,"seed":401},{"n":"Brood Queen","boss":1,"bpm":96,"root":34,"sc":[0,1,4,5,7,8,11],"prog":[0,0,1,0,5,4,1,0],"k":"x.....x..x..x...","s":"....x.......x.x.","h":"x..x..x..x..x.x.","b":"0.0.1.0.0.0.2.1.","bw":"sawtooth","bv":0.13,"bl":400,"a":"0..1..2..3..2..1","aw":"triangle","av":0.035,"ao":24,"pw":"sine","pv":0.03,"lw":"triangle","lv":0.05,"ll":2000,"lo":24,"ld":0.45,"echo":0.7,"drone":1,"metal":1,"seed":503}]}</script>
60
+ <script src="lib:room"></script>
61
+ <script src="lib:game"></script>
62
+ <script>
63
+ // Star Squad: Alien Blitz: data, helpers, level generation and local state (loads first; every file shares these globals)
64
+ "use strict";
65
+ const VERSION = "v2.7"; // shown small on the hero select screen and the start screen; bump it with each update
66
+ const W = 800, H = 450, L = 10400;
67
+ // boss arena at the far end of every map (everything below is measured from the map end, so L can change)
68
+ const GATE = L - 760, BX0 = L - 680, BX1 = L - 50, BOSS_X = L - 180;
69
+
70
+ // ---------- Data ----------
71
+ const CLS = [
72
+ { n: "Soldier", hp: 120, spd: 210, arm: 1, w: "rifle", sk: "Grenade Barrage", cd: 6, c: "#7fb069", d: "Burst rifle, tough" },
73
+ { n: "Wizard", hp: 85, spd: 200, arm: 1, w: "bolt", sk: "Meteor Storm", cd: 9, c: "#c77dff", d: "Chain bolts, floaty jump" },
74
+ { n: "Warrior", hp: 170, spd: 215, arm: .7, w: "wave", sk: "Berserk Spin", cd: 8, c: "#ff6b6b", d: "3-hit sword wave combo" },
75
+ { n: "Archer", hp: 95, spd: 245, arm: 1, w: "arrow", sk: "Arrow Rain", cd: 5, c: "#52b788", d: "Crits, double jump" },
76
+ { n: "Engineer", hp: 110, spd: 205, arm: .9, w: "nail", sk: "Auto Turret", cd: 11, c: "#f4a261", d: "Hot rivets + turrets" },
77
+ { n: "Robot", hp: 150, spd: 185, arm: .8, w: "plasma", sk: "Mega Laser", cd: 7, c: "#4cc9f0", d: "Blast plasma, armored" }
78
+ ];
79
+ // Weapons. r is also the shot's id when k is missing, so every r is unique.
80
+ // Behaviors: pel = shots per trigger, pierce, aoe = blast radius (lite = small pop), crit = chance of double damage,
81
+ // chain = lightning jumps to n more aliens, burn = sets aliens on fire, chill = slows (6 quick hits freeze),
82
+ // cut = destroys alien shots it touches, cl = rockets split into 2 bomblets
83
+ const WP = {
84
+ rifle: { n: "Burst Rifle", r: 3, v: 780, rate: .07, dmg: 10, life: .9, c: "#ffe066" },
85
+ bolt: { n: "Arcane Bolt", r: 6, v: 540, rate: .3, dmg: 22, life: 1.1, c: "#c77dff", chain: 1 },
86
+ wave: { n: "Sword Wave", r: 11, v: 620, rate: .24, dmg: 30, life: .24, c: "#e0fbfc", pierce: 1 },
87
+ arrow: { n: "Longbow", r: 4, v: 950, rate: .28, dmg: 20, life: 1, c: "#d8f3dc", crit: .15 },
88
+ nail: { n: "Rivet Gun", r: 5, v: 700, rate: .17, dmg: 12, life: .9, c: "#ffb703" },
89
+ plasma: { n: "Plasma Cannon", r: 8, v: 520, rate: .4, dmg: 28, life: 1, c: "#4cc9f0", aoe: 36, lite: 1 },
90
+ // class finishers fired by the patterns in fireW
91
+ wave2: { n: "Cross Slash", r: 14, v: 560, rate: .45, dmg: 55, life: .3, c: "#ffafcc", pierce: 1 },
92
+ arrow2: { n: "Power Shot", r: 5.2, v: 1250, rate: .28, dmg: 38, life: 1, c: "#95d5b2", pierce: 1, crit: .15 },
93
+ nail2: { n: "Hot Rivet", r: 7.8, v: 640, rate: .17, dmg: 18, life: .9, c: "#ff5400", aoe: 48, lite: 1 },
94
+ // pickups
95
+ H: { n: "Heavy MG", r: 3.5, v: 880, rate: .05, dmg: 9, life: .8, c: "#ffd166", ammo: 160 },
96
+ S: { n: "Spread Gun", r: 4.5, v: 640, rate: .26, dmg: 10, life: .7, c: "#ff70a6", ammo: 150, pel: 5 },
97
+ L: { n: "Laser", r: 6.5, v: 1300, rate: .09, dmg: 13, life: .6, c: "#ff4d6d", ammo: 120, pierce: 1, cut: 1 },
98
+ R: { n: "Rockets", r: 9, v: 480, rate: .55, dmg: 45, life: 1.4, c: "#ff9f1c", ammo: 20, aoe: 85, cl: 1 },
99
+ F: { n: "Flamer", r: 7, v: 380, rate: .05, dmg: 5, life: .4, c: "#ff6b35", ammo: 220, pierce: 1, burn: 1 },
100
+ Z: { n: "Tesla Coil", r: 5.6, v: 900, rate: .2, dmg: 12, life: .45, c: "#7df9ff", ammo: 90, chain: 3 },
101
+ C: { n: "Cryo Blaster", r: 7.4, v: 620, rate: .16, dmg: 8, life: .8, c: "#a2d2ff", ammo: 140, chill: 1 },
102
+ M: { n: "Micro Missiles", r: 6.2, v: 500, rate: .5, dmg: 18, life: 1.3, c: "#ffbe0b", ammo: 60, pel: 3, aoe: 42, lite: 1 }
103
+ };
104
+ // class weapons grow at Lv 5 and Lv 10 (tier 1 and 2); UPG says what changed
105
+ const tierOf = lv => (lv || 1) >= 10 ? 2 : (lv || 1) >= 5 ? 1 : 0, TIERN = ["", " II", " III"];
106
+ const UPG = [
107
+ ["4-round bursts", "twin bursts"],
108
+ ["chains to 2", "chains to 3"],
109
+ ["longer Cross Slash", "double Cross Slash"],
110
+ ["triple Power Shot", "double crits"],
111
+ ["hot rivet every 4th", "hot rivet every 3rd"],
112
+ ["bigger blasts", "twin plasma"]
113
+ ];
114
+ const SP = {
115
+ fz: { n: "Frost Nova", c: "#a2d2ff", i: "❄" },
116
+ th: { n: "Thunderstorm", c: "#ffd60a", i: "⚡" },
117
+ hl: { n: "Healing Light", c: "#80ffdb", i: "✚" },
118
+ sh: { n: "Star Shield", c: "#ffc8dd", i: "★" },
119
+ // helper toys: picked up into the spell slot, the Spell button calls the helper (allies.js)
120
+ dr: { n: "Attack Drone", c: "#4cc9f0", i: "🛸", ally: 1, s: "Drone" },
121
+ mh: { n: "Mini Mech", c: "#f4a261", i: "🤖", ally: 1, s: "Mech" },
122
+ md: { n: "Medic Drone", c: "#80ffdb", i: "💗", ally: 1, s: "Medic" },
123
+ dc: { n: "Holo Decoy", c: "#b8c0ff", i: "👥", ally: 1, s: "Decoy" }
124
+ };
125
+ // how long each helper stays (seconds)
126
+ const ALLY = { dr: 20, mh: 25, md: 18, dc: 10 };
127
+ const AL = [
128
+ { n: "Glorp", hp: 30, r: 18, v: 55, dmg: 12, xp: 10, sc: 100 },
129
+ { n: "Hopper", hp: 26, r: 15, v: 120, dmg: 10, xp: 12, sc: 120 },
130
+ { n: "Buzzwing", hp: 20, r: 14, v: 110, dmg: 10, xp: 12, sc: 120 },
131
+ { n: "Spitter", hp: 45, r: 20, v: 25, dmg: 10, xp: 18, sc: 150 },
132
+ { n: "Brute", hp: 170, r: 30, v: 45, dmg: 24, xp: 50, sc: 400 },
133
+ { n: "Boss", hp: 1500, r: 62, v: 70, dmg: 30, xp: 300, sc: 5000 },
134
+ { n: "Shambler", hp: 55, r: 22, v: 42, dmg: 14, xp: 22, sc: 220 },
135
+ { n: "Egg Pod", hp: 60, r: 16, v: 0, dmg: 6, xp: 8, sc: 80 },
136
+ { n: "Mini Boss", hp: 750, r: 40, v: 80, dmg: 15, xp: 150, sc: 2500 } // half the boss's health and bite
137
+ ];
138
+ // Bosses, one per planet (index = planet % 5): HP multiplier and the names of 3 attacks + the special used when enraged
139
+ const BOSS = [
140
+ { hp: 1, atk: ["Acid Spit", "Brood Call", "Body Slam", "Slime Rain"] },
141
+ { hp: 1.25, atk: ["Magma Fists", "Eruption", "Meteor Shower", "Lava Wave"] },
142
+ { hp: .9, atk: ["Ice Lances", "Frost Breath", "Dive Strike", "Blizzard"] },
143
+ { hp: 1, atk: ["Prism Beam", "Shard Storm", "Blink Nova", "Spiral Prism"] },
144
+ { hp: 1.1, atk: ["Spore Swarm", "Egg Clutch", "Stinger Rain", "Hive Shield"] }
145
+ ];
146
+ // Mini bosses ambush once per planet at the halfway mark (index = planet % 5). They get the planet boss's HP multiplier on
147
+ // top of their own half-size HP, and their shots and hazards hit at MINI_POW of a boss's. fly = hovers instead of walking.
148
+ // (Both then get BIG_HP / BIG_DMG below.)
149
+ const MINI = [
150
+ { n: "Thornback", fly: 0, atk: ["Thorn Volley", "Rolling Charge", "Acid Fountain"] },
151
+ { n: "Cinder Crab", fly: 0, atk: ["Ember Spray", "Claw Crush", "Lava Spit"] },
152
+ { n: "Frost Moth", fly: 1, atk: ["Ice Fan", "Frost Dust", "Hailstorm"] },
153
+ { n: "Prism Sentry", fly: 1, atk: ["Shard Ring", "Sweep Beam", "Zap Burst"] },
154
+ { n: "Hive Warden", fly: 1, atk: ["Stinger Dive", "Spore Puff", "Larva Drop"] }
155
+ ];
156
+ const MINI_X = Math.round(L * .5), MINI_POW = .5;
157
+ // bosses and mini bosses: extra health and damage (body hits, shots and hazards; not the small aliens they summon)
158
+ const BIG_HP = 3, BIG_DMG = 3;
159
+ // pits: fall in and you lose this share of your max HP, then pop back out at the nearer edge
160
+ const PIT_DMG = .5;
161
+ // Enemy shot kinds: hit box half-size (hx, hy), damage, gravity, colour (none = planet colour);
162
+ // gw = rolls along the ground at this height (jump it), home = steers toward the nearest hero, chill = slows the hero
163
+ const EBK = [
164
+ { hx: 14, hy: 24, d: 11, g: 0 }, // 0 spit orb
165
+ { hx: 15, hy: 25, d: 12, g: 620, c: "#9ef01a" }, // 1 acid glob
166
+ { hx: 18, hy: 28, d: 16, g: 520, c: "#ff7b00" }, // 2 magma fireball
167
+ { hx: 16, hy: 26, d: 14, g: 380, c: "#ff9f1c" }, // 3 meteor
168
+ { hx: 12, hy: 22, d: 10, g: 0, c: "#caf0f8" }, // 4 ice lance
169
+ { hx: 14, hy: 24, d: 4, g: 0, c: "#e0fbfc", chill: 1 }, // 5 frost breath
170
+ { hx: 13, hy: 23, d: 11, g: 0, c: "#f72585" }, // 6 crystal shard
171
+ { hx: 13, hy: 23, d: 9, g: 0, c: "#c77dff", home: 1 }, // 7 homing spore
172
+ { hx: 11, hy: 22, d: 10, g: 0, c: "#ff595e" }, // 8 stinger
173
+ { hx: 16, hy: 20, d: 15, g: 0, c: "#9ef01a", gw: 14 }, // 9 slime shockwave
174
+ { hx: 26, hy: 24, d: 20, g: 0, c: "#ff7b00", gw: 18 } // 10 lava wave
175
+ ];
176
+ // planets and their soundtracks live in levels.json (inlined by index.html as <script type="application/json" id="levels">)
177
+ const LEVELS = JSON.parse(document.getElementById("levels").textContent);
178
+ const PL = LEVELS.planets;
179
+ const LOOTW = ["H", "S", "L", "R", "F", "Z", "C", "M"], LOOTS = ["fz", "th", "hl", "sh"], LOOTA = ["dr", "mh", "md", "dc"];
180
+
181
+ // ---------- Helpers ----------
182
+ const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
183
+ // difficulty (picked by the host on the hero screen): alien HP, damage to heroes, starting lives, extra aliens per group, score bonus.
184
+ // MEDIUM is the original game, every multiplier is 1.
185
+ const DIFF = [
186
+ { n: "EASY", c: "#52b788", d: "8 lives · softer aliens", hp: .7, dm: .6, lives: 8, ex: -1, sc: .75 },
187
+ { n: "MEDIUM", c: "#ffd60a", d: "5 lives · the classic", hp: 1, dm: 1, lives: 5, ex: 0, sc: 1 },
188
+ { n: "HARD", c: "#ff4d6d", d: "3 lives · tougher · 1.5× score", hp: 1.35, dm: 1.4, lives: 3, ex: 1, sc: 1.5 }
189
+ ];
190
+ let diffPref = 1; // the host's own last pick (saved in the profile)
191
+ const rnd = (a, b) => a + Math.random() * (b - a);
192
+ const pick = a => a[Math.floor(Math.random() * a.length)];
193
+ const need = lv => 40 + lv * 30;
194
+ const lvMul = lv => 1 + .08 * ((lv || 1) - 1);
195
+ function rng(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
196
+ function hsh(s) { let h = 7; s = String(s); for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h); }
197
+ function rr(c, x, y, w, h, r) { c.beginPath(); c.moveTo(x + r, y); c.arcTo(x + w, y, x + w, y + h, r); c.arcTo(x + w, y + h, x, y + h, r); c.arcTo(x, y + h, x, y, r); c.arcTo(x, y, x + w, y, r); c.closePath(); }
198
+ function circ(c, x, y, r, col) { c.fillStyle = col; c.beginPath(); c.arc(x, y, Math.max(.1, r), 0, 6.283); c.fill(); }
199
+
200
+ // ---------- Level generation (same on every screen) ----------
201
+ const LV = {};
202
+ function gy0(pts, x) { const i = clamp(Math.floor(x / 200), 0, pts.length - 2), f = clamp((x - i * 200) / 200, 0, 1), k = (1 - Math.cos(f * Math.PI)) / 2; return pts[i] + (pts[i + 1] - pts[i]) * k; }
203
+ const gnd = (lv, x) => gy0(lv.pts, x);
204
+ function level(p) {
205
+ p = p || 0; if (LV[p]) return LV[p];
206
+ const P = PL[p % PL.length], R = rng(p * 7919 + 13), pts = [];
207
+ for (let x = 0; x <= L + 400; x += 200) pts.push(x < 600 || x > L - 800 ? 380 : Math.round(372 + (R() - .5) * 76));
208
+ const lv = { pts, P, plats: [], deco: [], sp: [], stars: [], traps: [] };
209
+ for (let x = 650; x < L - 950; x += 240 + R() * 220) {
210
+ if (R() < .62) {
211
+ const w = Math.round(110 + R() * 90), y = Math.round(gy0(pts, x + w / 2) - 85 - R() * 40);
212
+ lv.plats.push({ x: Math.round(x), y, w });
213
+ if (R() < .4) lv.plats.push({ x: Math.round(x + 25 + R() * 30), y: Math.round(y - 92 - R() * 15), w: Math.round(w * .75) });
214
+ }
215
+ }
216
+ lv.plats.push({ x: L - 600, y: 292, w: 130 }, { x: L - 240, y: 292, w: 130 }, { x: L - 410, y: 205, w: 110 });
217
+ for (let x = 140; x < L; x += 35 + R() * 95) lv.deco.push({ x: Math.round(x), s: .6 + R() * .9, k: Math.floor(R() * 3) });
218
+ for (let x = 760; x < L - 900; x += 230 + R() * 170) { const t = P.pool[Math.floor(R() * P.pool.length)]; lv.sp.push({ x: Math.round(x), t, n: t === 4 ? 1 : 2 + Math.floor(R() * 2) }); }
219
+ for (let i = 0; i < 90; i++) lv.stars.push({ x: R() * 1600, y: R() * 260, s: R() * 1.6 + .4, p: R() * 6 });
220
+ // traps: each planet has its own two kinds (levels.json "traps"), spaced out between the landing site and the boss gate
221
+ const TK = P.traps || [];
222
+ for (let x = 1100; TK.length && x < L - 1300; x += 480 + R() * 420) {
223
+ const k = TK[Math.floor(R() * TK.length)], w = { acid: 100, lava: 100, goo: 120, ice: 180, spikes: 70 }[k] || 0;
224
+ lv.traps.push({ i: lv.traps.length, k, x: Math.round(x), w: w ? Math.round(w + R() * w * .5) : 0, ph: Math.round(R() * 50) / 10 });
225
+ }
226
+ // pits: jumpable holes in the gaps between traps, at least 900 px apart (own random stream, so the rest of the map is unchanged)
227
+ const R2 = rng(p * 104729 + 71), gaps = [[0, 1300]].concat(lv.traps.map(t => [t.x - 140, t.x + Math.max(t.w, 30) + 140]), [[L - 1400, L]]);
228
+ lv.holes = [];
229
+ for (let i = 0, last = -1e9; i + 1 < gaps.length; i++) {
230
+ const a = gaps[i][1] + 20, b = gaps[i + 1][0] - 20, w = Math.round(80 + R2() * 45);
231
+ const x = Math.max(Math.round(a + R2() * Math.max(0, b - a - w)), last + 900);
232
+ if (x + w > b) continue;
233
+ lv.holes.push({ x, w }); last = x + w;
234
+ }
235
+ lv.deco = lv.deco.filter(d => !lv.holes.some(h => d.x > h.x - 25 && d.x < h.x + h.w + 25));
236
+ return LV[p] = lv;
237
+ }
238
+ // the pit under x (at least m px inside its edges), or null
239
+ function holeAt(lv, x, m) { m = m || 0; for (const h of lv.holes) if (x > h.x + m && x < h.x + h.w - m) return h; return null; }
240
+ // x moved just outside any pit (pad px past the nearer edge)
241
+ function outHole(lv, x, pad) { pad = pad || 0; for (const h of lv.holes) if (x > h.x - pad && x < h.x + h.w + pad) return x < h.x + h.w / 2 ? h.x - pad : h.x + h.w + pad; return x; }
242
+
243
+ // ---------- Local state ----------
244
+ let T = 0, ME = "", chosen = false, sel = 0, myCls = 0, px = 100, py = 380, vx = 0, vy = 0, onG = false, jumps = 0, face = 1;
245
+ let fireCd = 0, shotN = 0, spin = 0, jPrev = false, jBuf = 0, firing = 0, camX = 0, shake = 0, joinT = -9, lastSn, lastRv, curPlanet = -1, mateWait = 0, spawnX = 0, act = 0;
246
+ let lastBoss = "", lastPhase = "", saveT = 0, savedLv = 1, savedXp = 0, volleyQ = [], flashScr = null;
247
+ // game feel: coyote time, jump cut, squash/stretch, hit-stop, slow-mo, camera lookahead, hurt feedback, combo
248
+ let lastWu = 0, coyT = 0, jCut = false, sq = 0, hsT = 0, slowT = 0, look = 0, hurtT = -9, lastHp = -1, ghostHp = 0, lastCb = 0, nearRevive = null;
249
+ const COYOTE = .1, JBUF = .12, DELAY = .11;
250
+ let parts = [], banners = [], beams = [], bolts = [], flash = {}, disp = {}, ebL = {}, shotPrev = {}, pickT = {}, seenFx = {}, cleanT = 0;
251
+ // host-only working copy of the shared world (reloaded from g.shared when the host changes)
252
+ let HC = null, hcT = -9, freshMatch = false, pushT = 0, seq = 0, seen = {}, hitSet = {}, dead = {}, hitsBuf = [], cutBuf = [], ambT = 3, pruneT = 0;
253
+
254
+ function srcOf(g) { return g.isHost && HC ? HC : g.shared; }
255
+ function myId(g) { if (g.me && g.me.id) return g.me.id; for (const p of g.players) if (p.me) return p.id; return ""; }
256
+ function findP(g, id) { for (const p of g.players) if (p.id === id) return p; return null; }
257
+ function nid(p) { return p + (++seq).toString(36) + Math.random().toString(36).slice(2, 4); }
258
+ function profLv(g, c) { return (g.profile && g.profile["L" + c]) || 1; }
259
+
260
+ </script>
261
+ <script>
262
+ // Star Squad: sound effects, mixing bus and the procedural per-planet soundtrack
263
+ "use strict";
264
+ // ---------- Sound ----------
265
+ let AC = null, NB = null; const sT = {};
266
+ let vary = true;
267
+ function tone(f, d, ty, v, sl, dl) { if (vary) { const k = rnd(.93, 1.07); f *= k; if (sl) sl *= k; } const t0 = AC.currentTime + (dl || 0), o = AC.createOscillator(), gn = AC.createGain(); o.type = ty; o.frequency.setValueAtTime(f, t0); if (sl) o.frequency.exponentialRampToValueAtTime(sl, t0 + d); gn.gain.setValueAtTime(v, t0); gn.gain.exponentialRampToValueAtTime(.0001, t0 + d); o.connect(gn); gn.connect(BUS.sfx); o.start(t0); o.stop(t0 + d + .02); }
268
+ function nbuf() { if (!NB) { NB = AC.createBuffer(1, AC.sampleRate * .5, AC.sampleRate); const a = NB.getChannelData(0); for (let i = 0; i < a.length; i++) a[i] = Math.random() * 2 - 1; } return NB; }
269
+ function noise(d, v) { const s = AC.createBufferSource(), gn = AC.createGain(); s.buffer = nbuf(); gn.gain.setValueAtTime(v, AC.currentTime); gn.gain.exponentialRampToValueAtTime(.0001, AC.currentTime + d); s.connect(gn); gn.connect(BUS.sfx); s.start(); s.stop(AC.currentTime + d); }
270
+ // one AudioContext, made on the first tap/key (browser autoplay rules), feeding a mixing bus
271
+ function ac() {
272
+ if (!AC) { const A = window.AudioContext || window.webkitAudioContext; if (!A) return null; AC = new A(); mkBus(); MU.timer = setInterval(musTick, 25); }
273
+ if (AC.state === "suspended" && !document.hidden) AC.resume();
274
+ return AC;
275
+ }
276
+ // bus: sfx + music -> master -> compressor (glues the mix, stops clipping when everything fires at once) -> speakers
277
+ // music also feeds a generated-impulse reverb and a tempo-synced echo, so the synths sit in a space instead of sounding dry
278
+ let BUS = null;
279
+ function mkBus() {
280
+ const A = AC, B = BUS = {};
281
+ B.comp = A.createDynamicsCompressor(); B.comp.threshold.value = -16; B.comp.knee.value = 10; B.comp.ratio.value = 4; B.comp.attack.value = .004; B.comp.release.value = .22;
282
+ B.master = A.createGain(); B.master.gain.value = .9; B.master.connect(B.comp); B.comp.connect(A.destination);
283
+ B.sfx = A.createGain(); B.sfx.gain.value = 1; B.sfx.connect(B.master);
284
+ B.mus = A.createGain(); B.mus.gain.value = 0; B.mus.connect(B.master);
285
+ B.in = A.createGain(); B.in.connect(B.mus);
286
+ const n = A.sampleRate * 1.8 | 0, ir = A.createBuffer(2, n, A.sampleRate);
287
+ for (let ch = 0; ch < 2; ch++) { const a = ir.getChannelData(ch); for (let i = 0; i < n; i++) a[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / n, 3.2); }
288
+ B.rv = A.createConvolver(); B.rv.buffer = ir; B.rvG = A.createGain(); B.rvG.gain.value = .3; B.in.connect(B.rv); B.rv.connect(B.rvG); B.rvG.connect(B.mus);
289
+ B.echo = A.createGain(); B.dl = A.createDelay(2); B.fb = A.createGain(); B.fb.gain.value = .32; B.dlG = A.createGain(); B.dlG.gain.value = .35;
290
+ const lp = A.createBiquadFilter(); lp.type = "lowpass"; lp.frequency.value = 3200; // darker repeats, like a tape echo
291
+ B.echo.connect(B.dl); B.dl.connect(lp); lp.connect(B.fb); B.fb.connect(B.dl); lp.connect(B.dlG); B.dlG.connect(B.mus);
292
+ }
293
+ // ---------- Music: one procedural soundtrack per planet ----------
294
+ // "Two clocks" scheduling (web.dev/articles/audio-scheduling): a 25 ms timer queues notes 120 ms ahead on the audio clock,
295
+ // so the beat stays tight even when the game hitches. Changes of planet, boss and "cleared" land on the next bar line.
296
+ // Each song (levels.json "songs"): tempo, key (bass root as MIDI note), scale, chord per bar (scale degree), 16-step drum/bass/arp patterns.
297
+ // Pattern digits are chord tones: 0 root, 1 third, 2 fifth, 3 octave, 4 ninth; "." rests.
298
+ // Song ids: 0-4 planet themes, 5-9 boss themes (levels.json "bossSongs"); +100 = the remix used on the second lap
299
+ // of planets (a minor third higher, a little faster, new melodies), so every map in a 10-planet run sounds different.
300
+ const SONGS = LEVELS.songs.concat(LEVELS.bossSongs || []), NS = LEVELS.songs.length, REMIX = {};
301
+ function songOf(id) {
302
+ if (id < 100) return SONGS[id];
303
+ const S = SONGS[id - 100]; return REMIX[id] || (REMIX[id] = Object.assign({}, S, { root: S.root + 3, bpm: Math.round(S.bpm * 1.06), seed: S.seed + 40 }));
304
+ }
305
+ const MU ={ on: true, song: -1, want: 0, mode: 0, wantMode: 0, step: 0, bar: 0, next: 0, timer: 0, fan: false, mot: {} };
306
+ const MUSV = .42, mf = n => 440 * Math.pow(2, (n - 69) / 12);
307
+ function mrng(seed) { const R = rng(seed); return { f: R, p: a => a[Math.floor(R() * a.length)] }; }
308
+ // a 2-bar melody as scale steps above the chord root, walking by small steps and landing on chord tones on the beat
309
+ function motif(S, seed) {
310
+ const R = mrng(seed), m = new Array(32).fill(null); let d = 4;
311
+ for (let s = 0; s < 32; s += 2) {
312
+ if (s && R.f() > S.ld) continue;
313
+ d = clamp(d + R.p([-2, -1, -1, 1, 1, 2, 0]), 0, 9);
314
+ if (s % 8 === 0 && d % 2) d += d > 4 ? -1 : 1;
315
+ m[s] = [d, R.p([2, 2, 4, 4, 6])];
316
+ }
317
+ return m;
318
+ }
319
+ function motifs(i) { const S = songOf(i); return MU.mot[i] || (MU.mot[i] = [motif(S, S.seed), motif(S, S.seed + 1), motif(S, S.seed + 2)]); }
320
+ function deg(S, cd, d) { const i = cd + d, o = Math.floor(i / 7); return S.root + S.sc[((i % 7) + 7) % 7] + 12 * o; }
321
+ const TONE = [0, 2, 4, 7, 8];
322
+ // a synth voice: oscillator (+ optional detuned twin) -> lowpass with a little "pluck" sweep -> envelope -> bus (+ echo send)
323
+ function mv(t, n, d, w, v, o) {
324
+ o = o || {};
325
+ const g = AC.createGain(), f = AC.createBiquadFilter(), a = o.a || .006, r = o.r || .08, oscs = [];
326
+ f.type = "lowpass"; f.Q.value = o.q || .8; f.frequency.setValueAtTime((o.lp || 4000) * (o.sw || 2.2), t); f.frequency.exponentialRampToValueAtTime(o.lp || 4000, t + Math.min(d, .25));
327
+ for (const dt of o.det ? [-o.det, o.det] : [0]) { const os = AC.createOscillator(); os.type = w; os.frequency.setValueAtTime(mf(n), t); if (dt) os.detune.setValueAtTime(dt, t); os.connect(f); oscs.push(os); }
328
+ g.gain.setValueAtTime(.0001, t); g.gain.exponentialRampToValueAtTime(v, t + a); g.gain.exponentialRampToValueAtTime(Math.max(.0002, v * (o.sus == null ? .55 : o.sus)), t + Math.max(a + .01, d)); g.gain.exponentialRampToValueAtTime(.0001, t + d + r);
329
+ f.connect(g); g.connect(BUS.in); if (o.e) { const s = AC.createGain(); s.gain.value = o.e; g.connect(s); s.connect(BUS.echo); }
330
+ for (const os of oscs) { os.start(t); os.stop(t + d + r + .03); }
331
+ }
332
+ function drum(t, k, v) {
333
+ if (k === "k") { const o = AC.createOscillator(), g = AC.createGain(); o.frequency.setValueAtTime(150, t); o.frequency.exponentialRampToValueAtTime(42, t + .12); g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(.0001, t + .34); o.connect(g); g.connect(BUS.mus); o.start(t); o.stop(t + .36); return; }
334
+ const s = AC.createBufferSource(), f = AC.createBiquadFilter(), g = AC.createGain(), len = k === "s" ? .17 : k === "o" ? .16 : k === "c" ? .9 : k === "m" ? .09 : .035;
335
+ s.buffer = nbuf(); s.loop = true; s.playbackRate.value = k === "m" ? .6 : 1;
336
+ f.type = k === "s" ? "bandpass" : "highpass"; f.frequency.value = k === "s" ? 1900 : k === "c" ? 5200 : k === "m" ? 3000 : 7500; f.Q.value = k === "m" ? 8 : .7;
337
+ g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(.0001, t + len);
338
+ s.connect(f); f.connect(g); g.connect(k === "s" || k === "c" || k === "m" ? BUS.in : BUS.mus); s.start(t, Math.random() * .3); s.stop(t + len + .02);
339
+ if (k === "s") { const o = AC.createOscillator(), og = AC.createGain(); o.type = "triangle"; o.frequency.setValueAtTime(200, t); o.frequency.exponentialRampToValueAtTime(120, t + .07); og.gain.setValueAtTime(v * .7, t); og.gain.exponentialRampToValueAtTime(.0001, t + .09); o.connect(og); og.connect(BUS.mus); o.start(t); o.stop(t + .1); }
340
+ }
341
+ function musVol(t, v, tc) { const g = BUS.mus.gain; g.cancelScheduledValues(t); g.setTargetAtTime(v, t, tc || .3); }
342
+ function musDuck() { if (!BUS || !MU.on) return; const t = AC.currentTime; musVol(t, MUSV * .45, .015); BUS.mus.gain.setTargetAtTime(MUSV, t + .25, .35); }
343
+ // game state -> music: mode 0 = calm (hero select, planet cleared), 1 = level, 2 = boss fight, 3 = boss enraged (faster, harder)
344
+ function setMusic(pi, mode) {
345
+ const k = ((pi % NS) + NS) % NS, lap = Math.floor(Math.max(0, pi) / NS) % 2;
346
+ MU.want = (mode >= 2 && NS + k < SONGS.length ? NS + k : k) + lap * 100; MU.wantMode = mode;
347
+ }
348
+ function musTick() {
349
+ if (!AC || AC.state !== "running") return;
350
+ const now = AC.currentTime;
351
+ if (MU.next < now - .2) MU.next = now + .06; // after a stall or a hidden tab: resync instead of firing a burst of late notes
352
+ while (MU.next < now + .12) {
353
+ const t = MU.next, i = MU.step % 16;
354
+ if (i === 0) { // bar line: apply song / mode changes here so they land on the beat
355
+ if (MU.want !== MU.song) {
356
+ MU.song = MU.want; MU.bar = 0; MU.mode = MU.wantMode;
357
+ if (MU.on) { BUS.mus.gain.cancelScheduledValues(t); BUS.mus.gain.setValueAtTime(MUSV * .15, t); BUS.mus.gain.linearRampToValueAtTime(MUSV, t + (MU.mode >= 2 ? .8 : 3)); } // boss music bursts in
358
+ if (MU.mode) drum(t, "c", MU.mode >= 2 ? .16 : .1);
359
+ } else if (MU.wantMode !== MU.mode) { if (MU.wantMode >= 2 || (MU.wantMode === 1 && MU.mode === 0)) drum(t, "c", .14); MU.mode = MU.wantMode; MU.bar = MU.mode === 1 ? 4 : 0; }
360
+ if (BUS.dl) BUS.dl.delayTime.setTargetAtTime(Math.min(1.9, 45 / songOf(MU.song).bpm), t, .05); // dotted-eighth echo
361
+ }
362
+ playStep(t, i);
363
+ MU.next += 15 / (songOf(MU.song).bpm * (MU.mode === 3 ? 1.1 : 1)); // one 16th note; an enraged boss speeds the music up
364
+ MU.step++; if (MU.step % 16 === 0) MU.bar++;
365
+ }
366
+ }
367
+ function playStep(t, i) {
368
+ const S = songOf(MU.song), mode = MU.mode, bar = MU.bar, cd = S.prog[bar % S.prog.length], sd = 15 / S.bpm, rage = mode === 3;
369
+ // 16-bar form while playing a level: 4 groove (no lead), 8 theme, 4 breakdown (no kick) so long fights don't get tiring
370
+ const ph = bar % 16, sec = mode >= 2 ? "boss" : mode === 0 ? "calm" : ph < 4 ? "groove" : ph < 12 ? "theme" : "break";
371
+ if (rage && i % 4 === 2) drum(t, "k", .32); // enraged: double kick
372
+ if (rage && i === 0 && bar % 2 === 0) mv(t, deg(S, cd, 0) - 12, sd * 8, "sawtooth", .05, { lp: 300, sw: 4, sus: .8, r: .2, det: 12 }); // growling sub
373
+ const tn = (ch, base) => { const c = +ch; return deg(S, cd, TONE[c]) + base; };
374
+ // drums
375
+ if (sec !== "calm") {
376
+ if ((S.k[i] === "x" && sec !== "break") || (sec === "boss" && i % 4 === 0)) drum(t, "k", sec === "boss" ? .5 : .42);
377
+ if (S.s[i] === "x" || (bar % 4 === 3 && i >= 12 && sec !== "break" && (i % 2 === 0 || sec === "boss"))) drum(t, "s", .2);
378
+ if (S.h[i] !== "." || sec === "boss") drum(t, S.h[i] === "o" ? "o" : "h", sec === "boss" ? (i % 2 ? .035 : .06) : .05);
379
+ if (S.metal && i === 10 && bar % 2) drum(t, "m", .12);
380
+ }
381
+ // bass (held until the next note)
382
+ const bc = S.b[i];
383
+ if (bc !== "." && (sec !== "calm" || i === 0)) {
384
+ let L = 1; while (L < 4 && S.b[(i + L) % 16] === ".") L++;
385
+ if (sec === "calm") L = 16;
386
+ mv(t, tn(bc, 0), sd * L * .92, S.bw, S.bv * (sec === "calm" ? .6 : 1), { lp: S.bl, sw: sec === "boss" ? 3.5 : 2, sus: .7, r: .06 });
387
+ }
388
+ if (S.drone && i === 0) mv(t, S.root - 12 + 6 * (bar % 4 === 3), sd * 16, "sine", .06, { a: .4, sus: .9, r: .6, lp: 400 });
389
+ // power-chord stabs (Pyros)
390
+ if (S.stab && sec !== "calm" && (i === 0 || i === 6 || (i === 12 && bar % 2))) for (const ch of [0, 2, 3]) mv(t, tn(ch, 12), sd * 1.6, "sawtooth", .03, { lp: 1800, sw: 1.5, det: 8 });
391
+ // arpeggio
392
+ if (S.a && S.a[i] !== ".") mv(t, tn(S.a[i], S.ao), sd * (S.bell ? 3 : .9), S.aw, S.av * (sec === "calm" ? .7 : 1), { lp: S.bell ? 6000 : 2400, sw: 2.5, sus: S.bell ? .25 : .4, r: S.bell ? .5 : .05, e: S.echo * .6 });
393
+ if (S.bell && S.a && S.a[i] !== ".") mv(t, tn(S.a[i], S.ao + 19), sd * 2, "sine", S.av * .25, { r: .3, sus: .1 }); // inharmonic partial = bell
394
+ // pad chord, slow attack, once per bar
395
+ if (S.pw && i === 0) for (const ch of [0, 1, 2]) mv(t, tn(ch, 24), sd * 16, S.pw, S.pv, { a: sd * 4, sus: .8, r: .8, lp: 1600, sw: 1, det: S.pw === "sawtooth" ? 9 : 0 });
396
+ // lead melody: A A' A C over 8 bars; one octave higher in boss fights
397
+ if (sec === "theme" || sec === "boss") {
398
+ const M = motifs(MU.song), which = [0, 0, 1, 1, 0, 0, 2, 2][bar % 8], ev = M[which][(bar % 2) * 16 + i];
399
+ if (ev) mv(t, deg(S, cd, ev[0]) + S.lo + (sec === "boss" && (!S.boss || rage) ? 12 : 0), sd * ev[1] * .9, S.lw, S.lv, { lp: S.ll, sw: 1.6, sus: .6, r: .12, det: 5, e: S.echo });
400
+ }
401
+ // victory fanfare when a planet is cleared
402
+ if (MU.fan && i === 0) { MU.fan = false; [0, 2, 4, 7, 9, 14].forEach((d, j) => mv(t + j * sd, deg(S, 0, d) + 24, sd * (j === 5 ? 8 : 1.5), "square", .045, { lp: 3500, e: .4 })); }
403
+ }
404
+ function toggleMusic(g) {
405
+ MU.on = !MU.on; if (BUS) musVol(AC.currentTime, MU.on ? MUSV : 0, .15);
406
+ try { g && g.profile && g.profile.set && g.profile.set({ mus: MU.on ? 1 : 0 }); } catch (e) { }
407
+ }
408
+ function musIcon(c, x, y) { // tap or press M to toggle music (drawn by hud.js iconBtn, crossed out when off)
409
+ iconBtn(c, x, y, "♪", MU.on ? "#ffd60a" : "#6c757d", MU.on);
410
+ if (!MU.on) { c.strokeStyle = "#ff4d6d"; c.lineWidth = 2; c.beginPath(); c.moveTo(x - 7, y + 7); c.lineTo(x + 7, y - 7); c.stroke(); }
411
+ }
412
+ let GREF = null;
413
+ for (const ev of ["pointerdown", "touchend", "click"]) window.addEventListener(ev, () => ac(), { passive: true });
414
+ window.addEventListener("keydown", e => { ac(); if ((e.key === "m" || e.key === "M") && !e.repeat) toggleMusic(GREF); });
415
+ document.addEventListener("visibilitychange", () => { if (!AC) return; if (document.hidden) AC.suspend(); else AC.resume(); });
416
+ function sfx(n) {
417
+ if (sT[n] && T - sT[n] < (n === "hit" ? .07 : .05)) return; sT[n] = T;
418
+ try {
419
+ if (!ac()) return;
420
+ vary = n !== "pick" && n !== "lvl" && n !== "revive";
421
+ switch (n) {
422
+ case "hurt": tone(160, .18, "square", .05, 70); noise(.08, .04); break;
423
+ case "revive": [392, 523, 659].forEach((f, i) => tone(f, .18, "sine", .07, 0, i * .08)); break;
424
+ case "warn": tone(880, .08, "square", .025); break;
425
+ case "shot": tone(520, .06, "square", .02, 180); break;
426
+ case "heavy": tone(300, .1, "sawtooth", .03, 60); break;
427
+ case "laser": tone(1400, .08, "sawtooth", .015, 300); break;
428
+ case "magic": tone(880, .12, "sine", .04, 1600); break;
429
+ case "arrow": tone(1600, .06, "triangle", .02, 500); break;
430
+ case "flame": noise(.06, .02); break;
431
+ case "boom": noise(.4, .12); tone(90, .35, "sine", .15, 30); musDuck(); break;
432
+ case "hit": tone(240, .04, "square", .012, 120); break;
433
+ case "splat": noise(.15, .05); tone(200, .12, "sine", .05, 60); break;
434
+ case "pick": tone(660, .08, "triangle", .06); tone(990, .12, "triangle", .06, 0, .08); break;
435
+ case "lvl": [523, 659, 784, 1047].forEach((f, i) => tone(f, .15, "triangle", .07, 0, i * .09)); break;
436
+ case "jump": tone(300, .1, "square", .02, 600); break;
437
+ case "zap": tone(1800, .03, "square", .01, 900); break;
438
+ case "beam": tone(200, .5, "sawtooth", .06, 1200); break;
439
+ case "spell": tone(400, .4, "sine", .07, 1200); tone(600, .4, "triangle", .04, 1800, .05); break;
440
+ case "skill": tone(500, .15, "square", .03, 900); break;
441
+ case "down": tone(400, .5, "sawtooth", .05, 60); break;
442
+ case "boss": tone(110, .8, "sawtooth", .07, 55); tone(116, .8, "sawtooth", .05, 58); break;
443
+ case "pop": noise(.12, .04); tone(160, .12, "sine", .05, 50); break;
444
+ case "thunder": noise(.9, .1); tone(70, .9, "sine", .12, 35); tone(140, .25, "sawtooth", .03, 60); musDuck(); break;
445
+ case "rumble": noise(.5, .025); tone(55, .5, "sine", .05, 40); break;
446
+ case "heal": tone(880, .12, "sine", .025, 1320); break;
447
+ case "boing": tone(220, .22, "sine", .07, 900); tone(330, .15, "triangle", .03, 1200, .03); break;
448
+ case "hiss": noise(.35, .03); break;
449
+ case "crack": noise(.12, .05); tone(1800, .08, "triangle", .03, 400); break;
450
+ case "crit": tone(1900, .07, "triangle", .04, 2600); tone(2500, .06, "sine", .03, 0, .04); break;
451
+ case "slash": noise(.1, .04); tone(700, .14, "sawtooth", .025, 160); break;
452
+ case "tesla": tone(2200, .05, "square", .012, 700); tone(90, .06, "sawtooth", .02, 60); break;
453
+ case "cryo": tone(1700, .09, "sine", .03, 900); tone(2600, .05, "triangle", .012, 0, .02); break;
454
+ case "missile": noise(.12, .03); tone(250, .16, "square", .015, 900); break;
455
+ }
456
+ } catch (e) { }
457
+ }
458
+
459
+ </script>
460
+ <script>
461
+ // Star Squad: particles, shared effects, the host's world simulation, the local player's shooting and netcode smoothing
462
+ "use strict";
463
+ // ---------- Particles & banners ----------
464
+ function addP(x, y, vx, vy, l, c, s, k, txt, gr, x2, y2) { if (parts.length >= 420 && k !== 1) return; parts.push({ x, y, vx, vy, l, ml: l, c, s, k: k || 0, txt, g: gr || 0, x2, y2, add: ADDC.has(c) && k !== 1 && k !== 4 }); }
465
+ function updParts(dt) { for (const p of parts) { p.l -= dt; p.vy += p.g * dt; p.x += p.vx * dt; p.y += p.vy * dt; } parts = parts.filter(p => p.l > 0); }
466
+ // trauma model: events add trauma (0..1), it decays, shake grows with trauma squared
467
+ function addShake(v) { shake = Math.min(1, shake + v); }
468
+ function banner(t, c, d) { banners.push({ t, c, s: T, e: T + d }); if (banners.length > 3) banners.shift(); }
469
+
470
+ // ---------- Effects (sent by the host, seen by everyone) ----------
471
+ function fx(g, name, d) { d = d || {}; d.u = Math.floor(Math.random() * 1e9); handleFx(g, name, d); try { g.fx(name, d); } catch (e) { } }
472
+ function playerPos(g, id) { if (id === ME && chosen) return { x: px, y: py }; const d = disp["p" + id]; if (d) return d; const p = findP(g, id); return p && p.state ? { x: p.state.x, y: p.state.y } : null; }
473
+ function itemName(k) { return WP[k] ? WP[k].n : SP[k] ? SP[k].n : k === "hp" ? "Medkit" : "XP Star"; }
474
+ function itemColor(k) { return WP[k] ? WP[k].c : SP[k] ? SP[k].c : k === "hp" ? "#ff4d6d" : "#ffd60a"; }
475
+ function handleFx(g, n, d) {
476
+ if (!d) return;
477
+ if (d.u != null) { if (seenFx[d.u]) return; seenFx[d.u] = T; }
478
+ const pal = level(g.shared.planet || 0).P;
479
+ if (n === "hits") {
480
+ let cr = false;
481
+ for (const h of d.h || []) {
482
+ flash[h[0]] = T;
483
+ for (let i = 0; i < 3; i++) addP(h[1], h[2], rnd(-150, 150), rnd(-150, 50), .25, "#fff", 2, 0);
484
+ if (h[4]) { cr = true; addP(h[1] + rnd(-8, 8), h[2] - 14, 0, -80, .8, "#ff9f1c", 17, 1, h[3] + "!"); } // critical hit
485
+ else addP(h[1] + rnd(-8, 8), h[2] - 10, 0, -55, .6, "#ffe066", 12, 1, String(h[3]));
486
+ }
487
+ for (const p of d.c || []) for (let i = 0; i < 6; i++) addP(p[0], p[1], rnd(-140, 140), rnd(-140, 140), .25, "#ff4d6d", 2.5, 0); // laser cut an alien shot
488
+ if (cr) sfx("crit"); else if ((d.h || []).length) sfx("hit");
489
+ } else if (n === "die") {
490
+ const big = d.t === 4 || d.t === 5 || d.t === 8;
491
+ for (let i = 0; i < (big ? 40 : 16); i++) addP(d.x, d.y, rnd(-260, 260) * (big ? 1.4 : 1), rnd(-320, 80), rnd(.4, .9), i % 2 ? pal.a1 : pal.a2, rnd(2, big ? 7 : 4), 0, null, 900);
492
+ addP(d.x, d.y, 0, 0, .3, pal.a1, big ? 90 : 34, 2);
493
+ if (FL.length < 24) FL.push({ x: d.x, y: d.y, r: big ? 280 : 120, c: pal.a1, t: T, d: big ? .6 : .3 });
494
+ sfx("splat");
495
+ if (d.t === 4) { addShake(.45); hsT = Math.max(hsT, .06); }
496
+ if (d.t === 5) { addShake(1); slowT = T + .9; hsT = .12; }
497
+ if (d.t === 8) { addShake(.7); slowT = T + .5; hsT = .1; }
498
+ } else if (n === "minin") { // the halfway mini boss arrives
499
+ banner("⚠ MINI BOSS: " + MINI[(g.shared.planet || 0) % 5].n.toUpperCase(), "#ff9f1c", 2.5); sfx("boss"); addShake(.3);
500
+ } else if (n === "boom") {
501
+ for (let i = 0; i < 26; i++) addP(d.x, d.y, rnd(-220, 220), rnd(-260, 60), rnd(.3, .7), pick(["#ffba08", "#ff7b00", "#fff3b0", "#d00000"]), rnd(3, 7), 0, null, 200);
502
+ for (let i = 0; i < 6; i++) addP(d.x, d.y, rnd(-40, 40), rnd(-80, -20), rnd(.6, 1), "rgba(90,90,90,1)", rnd(8, 14), 4);
503
+ addP(d.x, d.y, 0, 0, .3, "#fff3b0", d.s || 80, 2);
504
+ if (FL.length < 24) FL.push({ x: d.x, y: d.y, r: (d.s || 80) * 2.6, c: "#ff9f1c", t: T, d: .5 });
505
+ SC.push({ x: d.x, y: gnd(level(g.shared.planet || 0), d.x), s: (d.s || 80) * .55, t: T }); if (SC.length > 16) SC.shift();
506
+ addShake(Math.abs(d.x - px) < 500 ? .3 : .1); sfx("boom");
507
+ } else if (n === "pop") { // small blast: plasma, hot rivets, micro missiles, rocket bomblets
508
+ const col = d.c || "#ff9f1c";
509
+ for (let i = 0; i < 12; i++) addP(d.x, d.y, rnd(-160, 160), rnd(-180, 60), rnd(.2, .45), i % 3 ? col : "#fff3b0", rnd(2, 5), 0, null, 300);
510
+ addP(d.x, d.y, 0, 0, .22, col, d.s || 40, 2);
511
+ if (FL.length < 24) FL.push({ x: d.x, y: d.y, r: (d.s || 40) * 2.4, c: col, t: T, d: .3 });
512
+ if (Math.abs(d.x - px) < 400) addShake(.05); sfx("pop");
513
+ } else if (n === "arc") { // chain lightning: a jagged line through every alien it jumped to
514
+ const p = d.p || [], col = d.c || "#7df9ff";
515
+ for (let i = 0; i + 3 < p.length; i += 2) {
516
+ let x = p[i], y = p[i + 1];
517
+ for (let j = 1; j <= 3; j++) { const k = j / 3, nx = p[i] + (p[i + 2] - p[i]) * k + (j < 3 ? rnd(-12, 12) : 0), ny = p[i + 1] + (p[i + 3] - p[i + 1]) * k + (j < 3 ? rnd(-12, 12) : 0); addP(x, y, 0, 0, .14, col, 3, 3, null, 0, nx, ny); addP(x, y, 0, 0, .14, "#ffffff", 1.2, 3, null, 0, nx, ny); x = nx; y = ny; }
518
+ for (let j = 0; j < 4; j++) addP(p[i + 2], p[i + 3], rnd(-90, 90), rnd(-90, 90), .2, col, 2, 0);
519
+ }
520
+ sfx("tesla");
521
+ } else if (n === "strike") { // weather lightning: a bolt, a white flash, sparks and a scorch mark
522
+ bolts.push({ x: d.x, t: T, s: Math.random() * 1000 }); flashScr = { c: "#e0e7ff", t: T };
523
+ for (let i = 0; i < 16; i++) addP(d.x, d.y - 4, rnd(-200, 200), rnd(-260, -40), rnd(.3, .6), i % 2 ? "#ffd60a" : "#fff", rnd(2, 4), 0, null, 700);
524
+ addP(d.x, d.y, 0, 0, .3, "#ffd60a", 60, 2);
525
+ SC.push({ x: d.x, y: d.y, s: 34, t: T }); if (SC.length > 16) SC.shift();
526
+ addShake(Math.abs(d.x - px) < 450 ? .35 : .1); sfx("thunder");
527
+ } else if (n === "heal") { // Medic Drone pulse
528
+ for (let i = 0; i < 6; i++) addP(d.x + rnd(-40, 40), d.y + rnd(0, 60), 0, rnd(-50, -30), .7, "#80ffdb", 11, 1, "+");
529
+ addP(d.x, d.y + 30, 0, 0, .35, "#80ffdb", 70, 2); sfx("heal");
530
+ } else if (n === "poof") { // a helper leaves
531
+ for (let i = 0; i < 10; i++) addP(d.x, d.y, rnd(-80, 80), rnd(-100, 20), rnd(.3, .6), "rgba(200,200,220,1)", rnd(5, 9), 4);
532
+ } else if (n === "zap") {
533
+ addP(d.x1, d.y1, 0, 0, .09, d.c || "#ffd60a", 2, 3, null, 0, d.x2, d.y2); sfx("zap");
534
+ } else if (n === "beam") {
535
+ beams.push({ x: d.x, y: d.y, f: d.f, t: T }); addShake(.4); sfx("beam");
536
+ } else if (n === "lvl") {
537
+ if (d.id === ME) {
538
+ banner("LEVEL UP! Lv " + d.lv, "#ffd60a", 2); sfx("lvl");
539
+ const tr = tierOf(d.lv); if (chosen && tr && tierOf(d.lv - 1) < tr) banner(WP[CLS[myCls].w].n + TIERN[tr] + ": " + UPG[myCls][tr - 1], CLS[myCls].c, 3); // weapon upgrade
540
+ }
541
+ const pp = playerPos(g, d.id);
542
+ if (pp) for (let i = 0; i < 24; i++) { const a = i / 24 * 6.283; addP(pp.x, pp.y - 24, Math.cos(a) * 160, Math.sin(a) * 160, .7, "#ffd60a", 3, 0); }
543
+ } else if (n === "pick") {
544
+ addP(d.x, d.y - 24, 0, -40, 1.1, itemColor(d.k), 12, 1, itemName(d.k));
545
+ for (let i = 0; i < 10; i++) addP(d.x, d.y, rnd(-90, 90), rnd(-160, -40), .5, itemColor(d.k), 3, 0, null, 300);
546
+ if (d.id === ME) sfx("pick");
547
+ } else if (n === "spell") {
548
+ const s = SP[d.k]; if (!s) return;
549
+ flashScr = { c: s.c, t: T };
550
+ if (d.k === "th") for (const x of d.xs || []) bolts.push({ x, t: T, s: Math.random() * 1000 });
551
+ banner(s.n + "!", s.c, 1.4); sfx("spell");
552
+ if (d.k === "fz") addShake(.2);
553
+ } else if (n === "revive") {
554
+ for (let i = 0; i < 20; i++) { const a = i / 20 * 6.283; addP(d.x, d.y - 24, Math.cos(a) * 120, Math.sin(a) * 120, .6, "#80ffdb", 3, 0); }
555
+ addP(d.x, d.y - 60, 0, -30, 1.2, "#80ffdb", 14, 1, "REVIVED!"); sfx("revive");
556
+ } else if (n === "slam") { // Queen Glorpa lands: dust, slime splats and a big shake
557
+ for (let i = 0; i < 18; i++) addP(d.x + rnd(-70, 70), d.y, rnd(-220, 220), rnd(-260, -60), rnd(.4, .8), i % 2 ? "#9ef01a" : "rgba(200,200,200,1)", rnd(3, 7), i % 2 ? 0 : 4, null, 600);
558
+ addShake(.7); sfx("boom");
559
+ } else if (n === "blink") { // Crystal Overmind teleports
560
+ for (let i = 0; i < 18; i++) { const a = i / 18 * 6.283; addP(d.x, d.y, Math.cos(a) * 220, Math.sin(a) * 220, .45, "#ff4d6d", 3, 0); }
561
+ addP(d.x, d.y, 0, 0, .35, "#ffc8dd", 70, 2);
562
+ } else if (n === "batk") { // a boss attack goes off
563
+ sfx(d.s || "warn"); if (d.b) banner(d.b, "#ff70a6", 2.2);
564
+ } else if (n === "down") {
565
+ for (let i = 0; i < 12; i++) addP(d.x, d.y - 20, rnd(-60, 60), rnd(-90, -10), rnd(.6, 1.2), "rgba(120,120,120,1)", rnd(6, 12), 4);
566
+ sfx("down");
567
+ }
568
+ }
569
+
570
+ // ---------- Host: world simulation ----------
571
+ function freshHC(df) { return { planet: 0, lives: DIFF[df].lives, df, score: 0, phase: "play", nextAt: 0, spawned: 0, boss: "", bossDone: false, mini: "", miniDone: false, front: 100, wx: 0, wd: 1, wxT: 0, pl: {}, mobs: {}, eb: {}, loot: {}, obj: {}, dPl: 1, dLoot: 1 }; }
572
+ const dfOf = () => DIFF[HC.df] || DIFF[1];
573
+ function loadHC(g) {
574
+ const s = g.shared, c = o => JSON.parse(JSON.stringify(o || {}));
575
+ // shared mobs are packed (packMob): put back what the host keeps only for itself, or a flier's sway reads Math.sin(undefined) and turns its x into NaN
576
+ const mobs = c(s.mobs); for (const id in mobs) { const m = mobs[id]; if (m.sd == null) m.sd = Math.random() * 6; if (m.vx == null) m.vx = 0; if (m.vy == null) m.vy = 0; if (m.cd == null) m.cd = g.now + 1; if (m.hc == null) m.hc = 0; }
577
+ return { planet: s.planet || 0, lives: s.lives == null ? 5 : s.lives, df: s.df == null ? 1 : s.df, score: s.score || 0, phase: s.phase || "play", nextAt: s.nextAt || 0, spawned: s.spawned || 0, boss: s.boss || "", bossDone: !!s.bossDone, mini: s.mini || "", miniDone: !!s.miniDone, front: s.front || 100,
578
+ wx: s.wx || 0, wd: s.wd || 1, wxT: s.wxT || 0, pl: c(s.pl), mobs, eb: c(s.eb), loot: c(s.loot), obj: c(s.obj), dPl: 1, dLoot: 1 };
579
+ }
580
+ const r2 = v => Math.round(v * 100) / 100;
581
+ function packMob(m) { const o = { x: Math.round(m.x), y: Math.round(m.y), t: m.t, hp: Math.max(0, Math.ceil(m.hp)), mhp: Math.round(m.mhp), f: m.f }; if (m.fz) o.fz = r2(m.fz); if (m.at) o.at = r2(m.at); if (m.dn) o.dn = r2(m.dn); if (m.ch) o.ch = r2(m.ch); o.wu = r2(m.wu || 0);
582
+ if (m.bn) o.bn = 1; if (m.cl) o.cl = 1; // burning, chilled
583
+ if (m.t === 5 || m.t === 8) { o.pn = m.pn || 0; if (m.sh) o.sh = 1; if (m.lk) o.lk = r2(m.lk); } // boss: current attack, shield, locked in place
584
+ if (m.t === 8) { if (m.rl) o.rl = r2(m.rl); if (m.dv) o.dv = r2(m.dv); } // mini boss rolling / lunging, diving
585
+ return o; }
586
+ function packEb(e) { const o = { x: Math.round(e.x), y: Math.round(e.y), vx: Math.round(e.vx), vy: Math.round(e.vy) }; if (e.k) o.k = e.k; return o; }
587
+ function packObj(o) {
588
+ const p = { t: o.t, x: Math.round(o.x), y: Math.round(o.y), o: o.o || "", f: o.f || 1 };
589
+ if (o.t === "pil" || o.t === "laz" || o.t === "ltn") { p.w = r2(o.w); p.e = r2(o.e); } // boss hazards and lightning: warning ends at w, gone at e
590
+ if (o.t === "laz") { p.a = r2(o.a); p.b = r2(o.b); } // beam sweeps from angle a to b
591
+ if (ALLY[o.t]) p.e = Math.round(o.e); // helpers blink before they leave
592
+ return p;
593
+ }
594
+ function sect(g, k, f) { const cur = HC[k], old = g.shared[k] || {}, o = {}; for (const id in cur) o[id] = f ? f(cur[id]) : cur[id]; for (const id in old) if (!(id in cur)) o[id] = null; return o; }
595
+ function pushAll(g, all) {
596
+ const o = { planet: HC.planet, lives: HC.lives, df: HC.df, score: Math.round(HC.score), phase: HC.phase, nextAt: r2(HC.nextAt), spawned: HC.spawned, boss: HC.boss, bossDone: HC.bossDone, mini: HC.mini || "", miniDone: !!HC.miniDone, front: Math.round(HC.front), wx: HC.wx, wd: HC.wd, wxT: r2(HC.wxT) };
597
+ o.mobs = sect(g, "mobs", packMob); o.eb = sect(g, "eb", packEb); o.obj = sect(g, "obj", packObj);
598
+ if (all || HC.dPl) { o.pl = sect(g, "pl"); HC.dPl = 0; }
599
+ if (all || HC.dLoot) { o.loot = sect(g, "loot"); HC.dLoot = 0; }
600
+ g.shared.set(o);
601
+ if (hitsBuf.length || cutBuf.length) { fx(g, "hits", { h: hitsBuf.slice(0, 14), c: cutBuf.slice(0, 8) }); hitsBuf = []; cutBuf = []; }
602
+ }
603
+ function lvlOf(id) { return HC.pl[id] ? HC.pl[id].lv : 1; }
604
+ // one shot from a hero's current weapon (a crate weapon while it has ammo, else their class weapon), at their level
605
+ function shotDmg(id) { const q = HC.pl[id], k = q && q.w && WP[q.w] && q.ammo > 0 ? q.w : q ? CLS[q.c].w : "rifle"; return WP[k].dmg * lvMul(lvlOf(id)); }
606
+ const turretDmg = id => shotDmg(id) / 3; // Engineer turret zap: 1/3 of a shot
607
+ const laserDmg = id => shotDmg(id) * 4; // Robot Mega Laser: 4 shots' worth to everything in the beam
608
+ function spawnMob(g, t, x, hpM) {
609
+ const A = AL[t], lv = level(HC.planet), id = nid("m"); x = clamp(x, 30, L - 30);
610
+ const fly = t === 2 || (t === 5 && lv.P.fly) || (t === 8 && MINI[HC.planet % 5].fly);
611
+ HC.mobs[id] = { x, y: fly ? (t === 5 ? 170 : t === 8 ? gnd(lv, x) - 190 : 110 + Math.random() * 120) : gnd(lv, x) - A.r, t, hp: A.hp * hpM, mhp: A.hp * hpM, f: -1, vx: 0, vy: 0, cd: g.now + 1 + Math.random() * 1.5, hc: 0, sd: Math.random() * 6 };
612
+ if (t === 7) HC.mobs[id].ht = g.now + 8; // egg pods hatch after 8 s
613
+ return id;
614
+ }
615
+ let ebDm = 1; // damage scale stamped on new alien shots (mini boss attacks set it to MINI_POW)
616
+ function ebAdd(g, x, y, vx, vy, k, life) { if (Object.keys(HC.eb).length >= 50) return; const e = HC.eb[nid("e")] = { x, y, vx, vy, k: k || 0, e: g.now + (life || 4) }; if (ebDm !== 1) e.dm = ebDm; }
617
+ function shootEb(g, x, y, a, sp, k, life) { ebAdd(g, x, y, Math.cos(a) * sp, Math.sin(a) * sp, k, life); }
618
+ // lob a shot of a gravity kind so it lands on (tx, ty) after t seconds
619
+ function lob(g, x, y, tx, ty, t, k) { const G = EBK[k].g; ebAdd(g, x, y, (tx - x) / t, (ty - y - .5 * G * t * t) / t, k, t + 1.5); }
620
+ function drop(g, x, k, life) {
621
+ if (!k) { const r = Math.random(); k = r < .44 ? pick(LOOTW) : r < .62 ? pick(LOOTS) : r < .77 ? pick(LOOTA) : r < .9 ? "hp" : "xp"; }
622
+ const ids = Object.keys(HC.loot);
623
+ if (ids.length >= 20) { let o = ids[0]; for (const i of ids) if (HC.loot[i].e < HC.loot[o].e) o = i; delete HC.loot[o]; }
624
+ x = clamp(outHole(level(HC.planet), x, 20), 30, L - 30);
625
+ HC.loot[nid("l")] = { x: Math.round(x), y: Math.round(gnd(level(HC.planet), x) - 16), k, e: Math.round(g.now + (life || 30)) };
626
+ HC.dLoot = 1;
627
+ }
628
+ function give(g, id, xp) {
629
+ const q = HC.pl[id]; if (!q || xp <= 0) return;
630
+ q.xp += xp; HC.dPl = 1;
631
+ while (q.lv < 50 && q.xp >= need(q.lv)) { q.xp -= need(q.lv); q.lv++; q.mhp = Math.round(CLS[q.c].hp * (1 + .07 * (q.lv - 1))); q.hp = q.mhp; fx(g, "lvl", { id, lv: q.lv }); }
632
+ if (q.lv >= 50) q.xp = Math.min(q.xp, need(50) - 1);
633
+ }
634
+ function hurt(g, id, dmg) {
635
+ const q = HC.pl[id], now = g.now; if (!q || q.dead || q.inv > now || q.ifr > now) return;
636
+ q.hp = Math.max(0, Math.round(q.hp - dmg * CLS[q.c].arm * dfOf().dm)); HC.dPl = 1; // every hit (aliens, shots, traps, weather) is scaled by difficulty here
637
+ q.ifr = r2(now + .7); // invincibility frames: every hit is readable, no instant multi-hit deaths
638
+ if (q.hp <= 0) { const p = findP(g, id); knockOut(g, q, p && p.state ? p.state.x : 0, p && p.state ? p.state.y : 0); }
639
+ }
640
+ function knockOut(g, q, x, y) {
641
+ if (HC.lives > 0) { HC.lives--; q.dead = r2(g.now + 5); } else q.dead = -1;
642
+ q.w = ""; q.ammo = 0; q.rp = 0; q.cb = 0;
643
+ const lv = level(HC.planet), sx = outHole(lv, x, 40); // never leave a revive beacon at the bottom of a pit
644
+ q.dx = Math.round(sx); q.dy = Math.round(Math.min(y, gnd(lv, sx)));
645
+ fx(g, "down", { x: q.dx, y: q.dy });
646
+ }
647
+ // a hero fell into a pit: lose PIT_DMG of max HP (no armour, no shields), so a second fall from half health is a knock-out
648
+ function pitFall(g, q, x) {
649
+ const now = g.now; if (now < (q.fallT || 0)) return; q.fallT = now + 1;
650
+ q.hp = Math.max(0, q.hp - Math.ceil(q.mhp * PIT_DMG)); q.ifr = r2(now + .7); HC.dPl = 1;
651
+ if (q.hp <= 0) knockOut(g, q, x, gnd(level(HC.planet), x));
652
+ }
653
+ function kill(g, mid, from) {
654
+ const m = HC.mobs[mid], A = AL[m.t], pi = HC.planet;
655
+ if (m.t === 6 && !m.rz) { // Shamblers collapse, then get back up once at half health
656
+ m.rz = 1; m.hp = m.mhp * .5; m.dn = r2(g.now + 1.8); m.wu = 0; m.ch = 0;
657
+ fx(g, "die", { x: Math.round(m.x), y: Math.round(m.y), t: m.t });
658
+ return;
659
+ }
660
+ delete HC.mobs[mid];
661
+ const kq = HC.pl[from];
662
+ let cm = 1;
663
+ if (kq) { kq.cb = g.now < (kq.ct || 0) ? (kq.cb || 0) + 1 : 1; kq.ct = r2(g.now + 2.5); cm = 1 + Math.min(kq.cb - 1, 20) * .1; HC.dPl = 1; }
664
+ HC.score += Math.round(A.sc * (1 + pi * .25) * cm * dfOf().sc);
665
+ const xp = Math.round(A.xp * (1 + pi * .25));
666
+ give(g, from, xp);
667
+ for (const id in HC.pl) if (id !== from) give(g, id, Math.round(xp * .3));
668
+ fx(g, "die", { x: Math.round(m.x), y: Math.round(m.y), t: m.t });
669
+ if (m.t === 5) {
670
+ // the arena calms down: boss hazards, eggs and shots vanish with it
671
+ for (const oid in HC.obj) if (HC.obj[oid].t === "pil" || HC.obj[oid].t === "laz") delete HC.obj[oid];
672
+ for (const id in HC.mobs) if (HC.mobs[id].t === 7) delete HC.mobs[id];
673
+ HC.eb = {};
674
+ for (let i = 0; i < 6; i++) drop(g, m.x - 150 + i * 60);
675
+ HC.boss = ""; HC.bossDone = true; HC.phase = "clear"; HC.nextAt = g.now + 7;
676
+ for (const id in HC.pl) { give(g, id, 150); const q = HC.pl[id]; if (!q.dead) q.hp = q.mhp; }
677
+ HC.dPl = 1;
678
+ } else if (m.t === 8) { // mini boss down: a loot shower and bonus XP for the whole squad
679
+ HC.mini = ""; HC.miniDone = true;
680
+ for (let i = 0; i < 3; i++) drop(g, m.x - 60 + i * 60);
681
+ for (const id in HC.pl) give(g, id, 60);
682
+ fx(g, "batk", { s: "revive", b: MINI[pi % 5].n.toUpperCase() + " DEFEATED!" });
683
+ } else if (Math.random() < [.13, .13, .15, .25, .7, 0, .25, .2][m.t]) drop(g, m.x);
684
+ }
685
+ function damage(g, mid, dmg, from, kx, crit, quiet) {
686
+ const m = HC.mobs[mid]; if (!m || (m.dn && m.dn > g.now)) return;
687
+ if (m.sh) return; // Hive Mother's shield: only breaks when her eggs are destroyed
688
+ m.hp -= dmg;
689
+ if (kx != null && m.t !== 5 && m.t !== 8) { const kb = Math.min(14, dmg * .3) * (m.t === 4 ? .3 : 1); m.x += (m.x >= kx ? 1 : -1) * kb; } // knockback
690
+ if (!quiet && hitsBuf.length < 30) hitsBuf.push([mid, Math.round(m.x), Math.round(m.y - AL[m.t].r * .5), Math.round(dmg), crit ? 1 : 0]);
691
+ if (m.hp <= 0) kill(g, mid, from);
692
+ }
693
+ // lite = a small "pop" (no screen-wide shake, no scorch mark) for everyday splash weapons
694
+ function boom(g, x, y, rad, dmg, from, lite, col) {
695
+ for (const mid in HC.mobs) { const m = HC.mobs[mid]; if (Math.hypot(m.x - x, m.y - y) < rad + AL[m.t].r) damage(g, mid, dmg, from); }
696
+ if (lite) fx(g, "pop", { x: Math.round(x), y: Math.round(y), s: Math.round(rad), c: col }); else fx(g, "boom", { x: Math.round(x), y: Math.round(y), s: rad });
697
+ }
698
+ // lightning jumps from the hit alien to the nearest n others within reach
699
+ function chainHit(g, mid0, x, y, n, dmg, from, col) {
700
+ const hit = { [mid0]: 1 }, pts = [Math.round(x), Math.round(y)];
701
+ for (let j = 0; j < n; j++) {
702
+ let best = null, bd = 170 * 170;
703
+ for (const id in HC.mobs) { if (hit[id]) continue; const m = HC.mobs[id]; if (m.dn && m.dn > g.now) continue; const d2 = (m.x - x) ** 2 + (m.y - y) ** 2; if (d2 < bd) { bd = d2; best = id; } }
704
+ if (!best) break;
705
+ const m = HC.mobs[best]; hit[best] = 1; x = m.x; y = m.y; pts.push(Math.round(x), Math.round(y));
706
+ damage(g, best, dmg, from);
707
+ }
708
+ if (pts.length > 2) fx(g, "arc", { p: pts, c: col });
709
+ }
710
+ // after a hit lands: fire (damage over time) and frost (slow, then freeze after 6 quick hits)
711
+ function afflict(g, mid, w, from) {
712
+ const m = HC.mobs[mid]; if (!m) return;
713
+ if (w.burn) { m.bn = r2(g.now + 2); m.bo = from; }
714
+ if (w.chill) { m.cl = r2(g.now + 1.5); m.cs = (m.cs || 0) + 1; if (m.cs >= 6) { m.cs = 0; m.fz = g.now + (m.t === 5 || m.t === 8 ? .7 : 1.8); } }
715
+ }
716
+ function wkOf(s) { if (s.k && WP[s.k]) return s.k; let b = "rifle", bd = 99; for (const k in WP) { const d = Math.abs(WP[k].r - (s.r || 3)); if (d < bd) { bd = d; b = k; } } return b; }
717
+ function startPlanet(g) { // supplies along the (long) way: weapons, spells and a medkit before the boss
718
+ for (const [x, k] of [[520, pick(LOOTW)], [1500, pick(LOOTA)], [2300, pick(LOOTS)], [3800, pick(LOOTW)], [5600, pick(LOOTS)], [6400, pick(LOOTA)], [7200, pick(LOOTW)], [L - 1150, "hp"]]) drop(g, x, k, 999);
719
+ }
720
+ function nextPlanet(g) {
721
+ HC.planet++; HC.spawned = 0; HC.boss = ""; HC.bossDone = false; HC.mini = ""; HC.miniDone = false; HC.phase = "play"; HC.front = 100;
722
+ HC.warpT = g.now + 3; // players' last positions are still in the old boss arena until their screens catch up
723
+ HC.wxT = 0; // new planet, new weather
724
+ HC.mobs = {}; HC.eb = {}; HC.loot = {}; HC.obj = {}; HC.lives += 2;
725
+ for (const id in HC.pl) { const q = HC.pl[id]; q.dead = 0; q.hp = q.mhp; q.inv = r2(g.now + 3); }
726
+ HC.dPl = 1; HC.dLoot = 1; startPlanet(g);
727
+ }
728
+ function doSkill(g, id, q, st, f) {
729
+ const lm = lvMul(q.lv), x = st.x, y = st.y, now = g.now;
730
+ if (q.c === 0) {
731
+ for (let i = 0; i < 3; i++) HC.obj[nid("o")] = { t: "gr", x, y: y - 34, vx: f * (220 + i * 130), vy: -420 - i * 60, e: now + 1.1 + i * .12, o: id };
732
+ } else if (q.c === 1) {
733
+ const ts = Object.values(HC.mobs).filter(m => Math.abs(m.x - x) < 650).sort((a, b) => Math.abs(a.x - x) - Math.abs(b.x - x));
734
+ for (let i = 0; i < 6; i++) { const tx = ts.length ? ts[i % ts.length].x + rnd(-40, 40) : x + f * (120 + i * 90); HC.obj[nid("o")] = { t: "met", x: tx - 60, vx: 110, y: -60 - i * 120, o: id }; }
735
+ } else if (q.c === 2) {
736
+ q.inv = r2(now + 3); HC.obj[nid("o")] = { t: "spin", x, y, o: id, e: now + 3, nt: now };
737
+ } else if (q.c === 4) {
738
+ const mine = Object.keys(HC.obj).filter(k => HC.obj[k].t === "tur" && HC.obj[k].o === id);
739
+ if (mine.length >= 2) delete HC.obj[mine[0]];
740
+ HC.obj[nid("o")] = { t: "tur", x: x + f * 24, y, o: id, f, e: now + 16, nt: now + .4 };
741
+ } else if (q.c === 5) {
742
+ const by = y - 30;
743
+ for (const mid in HC.mobs) { const m = HC.mobs[mid], dx = (m.x - x) * f; if (dx > -10 && dx < 780 && Math.abs(m.y - by) < 36 + AL[m.t].r) damage(g, mid, laserDmg(id), id); }
744
+ for (const eid in HC.eb) { const e = HC.eb[eid], dx = (e.x - x) * f; if (dx > -10 && dx < 780 && Math.abs(e.y - by) < 40) delete HC.eb[eid]; }
745
+ fx(g, "beam", { x: Math.round(x), y: Math.round(by), f });
746
+ }
747
+ // Archer (3): the archer's own screen fires the volley when it sees the skill counter change
748
+ }
749
+ function doSpell(g, id, q, st, k) {
750
+ const now = g.now, lm = lvMul(q.lv), xs = [];
751
+ if (ALLY[k]) callAlly(g, id, st, k); // helper toy (allies.js)
752
+ if (k === "fz") for (const mid in HC.mobs) { const m = HC.mobs[mid]; if (Math.abs(m.x - st.x) < 650) m.fz = now + (m.t === 5 || m.t === 8 ? 2 : 4.5); }
753
+ if (k === "th") for (const mid in HC.mobs) { const m = HC.mobs[mid]; if (Math.abs(m.x - st.x) < 650) { if (xs.length < 10) xs.push(Math.round(m.x)); damage(g, mid, 75 * lm, id); } }
754
+ if (k === "hl") for (const pid in HC.pl) { const o = HC.pl[pid]; if (!o.dead) o.hp = Math.min(o.mhp, Math.round(o.hp + o.mhp * .6)); }
755
+ if (k === "sh") for (const pid in HC.pl) HC.pl[pid].inv = r2(now + 6);
756
+ HC.dPl = 1;
757
+ fx(g, "spell", { k, x: Math.round(st.x), y: Math.round(st.y), xs });
758
+ }
759
+
760
+ // ---------- Bosses: one per planet, each with its own movement, 3 attacks and a special it uses when enraged ----------
761
+ // Every attack is telegraphed: m.pn is picked at wind-up (clients show its name over the "!"), it fires when m.wu runs out.
762
+ // Multi-shot attacks queue a burst (m.bu shots left, m.bt next shot, m.bk which attack); m.lk holds the boss still.
763
+ const inArena = x => clamp(x, BX0, BX1); // BX0 / BX1: arena edges, set in data.js from the map length
764
+ function bossTick(g, m, tg, al, lv, pi, now, dt, sp, hpM) {
765
+ const k = pi % 5, enr = m.hp < m.mhp * .4, gy = gnd(lv, m.x) - AL[5].r, held = m.lk && m.lk > now;
766
+ if (m.bu > 0 && now > m.bt) { m.bu--; m.bt = now + bossBurst(g, m, tg, k); }
767
+ if (k === 4 && m.sh && !Object.values(HC.mobs).some(o => o.t === 7)) { m.sh = 0; fx(g, "batk", { s: "revive", b: "HIVE SHIELD DOWN!" }); }
768
+ // movement
769
+ if (k === 0) { // Queen Glorpa hops around the arena; a body slam lands with shockwaves
770
+ m.vy = (m.vy || 0) + 1500 * dt; m.x += (m.vx || 0) * dt; m.y += m.vy * dt;
771
+ if (m.y >= gy) {
772
+ m.y = gy;
773
+ if (m.sl && m.vy > 0) { m.sl = 0; for (const d of [-1, 1]) ebAdd(g, m.x + d * 50, gy, d * 300, 0, 9, 2.4); fx(g, "slam", { x: Math.round(m.x), y: Math.round(gy + AL[5].r) }); }
774
+ m.vy = 0; m.vx = 0;
775
+ if (!m.wu && now > (m.hop || 0)) { const want = inArena(tg.x + (m.x > tg.x ? 230 : -230)); m.vx = clamp(want - m.x, -260, 260) * (enr ? 1.4 : 1); m.vy = -520; m.hop = now + (enr ? .9 : 1.4); }
776
+ }
777
+ } else if (k === 1) { // Magma Titan stomps slowly toward you
778
+ const want = inArena(tg.x + (m.x > tg.x ? 170 : -170));
779
+ if (!m.wu && !held && Math.abs(want - m.x) > 8) m.x += Math.sign(want - m.x) * sp * (enr ? .9 : .6) * dt;
780
+ m.y = gy;
781
+ } else if (k === 2) { // Frost Wyrm swoops in figure-eights, or dives straight at you
782
+ if (m.dv > now) { m.x += m.dvx * dt; m.y = Math.min(gy, m.y + m.dvy * dt); }
783
+ else if (!held) { const tx = (BX0 + BX1) / 2 + 290 * Math.sin(now * .55 + m.sd), ty = 150 + 55 * Math.sin(now * 1.1 + m.sd); m.x += (tx - m.x) * Math.min(1, dt * 1.8); m.y += (ty - m.y) * Math.min(1, dt * 1.8); }
784
+ } else if (k === 3) { // Crystal Overmind floats above the ground and keeps its distance
785
+ if (!held) { const tx = inArena(tg.x + (m.x > tg.x ? 170 : -170)), ty = 225 + 25 * Math.sin(now * 1.5); m.x += (tx - m.x) * Math.min(1, dt * 1.2); m.y += (ty - m.y) * Math.min(1, dt * 2); }
786
+ } else { // Hive Mother hovers; before Stinger Rain she moves right above you
787
+ const over = m.wu && m.pn === 2, tx = inArena(over ? tg.x : tg.x + (m.x > tg.x ? 200 : -200)), ty = over ? 130 : 165 + 30 * Math.sin(now * 1.3);
788
+ m.x += (tx - m.x) * Math.min(1, dt * (over ? 2.5 : 1.1)); m.y += (ty - m.y) * Math.min(1, dt * 1.5);
789
+ }
790
+ if (held || m.sl || m.dv > now) return;
791
+ // attacks: three in turn; when enraged, every third one is the special
792
+ if (!m.wu && now > m.cd) {
793
+ m.pc = (m.pc || 0) + 1;
794
+ if (enr && m.pc % 3 === 0 && !(k === 4 && m.sh)) m.pn = 3; else { m.px = ((m.px == null ? -1 : m.px) + 1) % 3; m.pn = m.px; }
795
+ m.wu = now + (m.pn === 3 ? 1 : enr ? .5 : .7);
796
+ } else if (m.wu && now > m.wu) {
797
+ m.wu = 0; m.at = now; m.cd = now + (enr ? 1.4 : 2.1) + (m.pn === 3 ? .8 : 0);
798
+ bossAttack(g, m, tg, al, lv, k, m.pn, now, hpM);
799
+ }
800
+ }
801
+ function bossAttack(g, m, tg, al, lv, k, pn, now, hpM) {
802
+ const bx = m.x + m.f * 40, by = m.y - 10, ang = Math.atan2(tg.y - by, tg.x - bx), room = Object.keys(HC.mobs).length < 30;
803
+ const gnY = x => gnd(lv, x) - 20, burst = (n, at) => { m.bu = n; m.bk = pn; m.bt = now + (at || 0); };
804
+ let s = "warn";
805
+ if (k === 0) {
806
+ if (pn === 0) { for (const o of [-130, -65, 0, 65, 130]) lob(g, bx, by - 20, tg.x + o, gnY(tg.x + o), .9 + Math.random() * .3, 1); s = "splat"; }
807
+ else if (pn === 1) { if (room) for (let i = 0; i < 3; i++) spawnMob(g, 0, m.x + (i - 1) * 90, hpM * .45); s = "splat"; }
808
+ else if (pn === 2) { m.sl = 1; m.vy = -950; m.vx = (inArena(tg.x) - m.x) / 1.25; m.y -= 2; s = "jump"; }
809
+ else { for (let i = 0; i < 12; i++) ebAdd(g, BX0 - 60 + Math.random() * (BX1 - BX0 + 120), -30 - i * 45, 0, 40, 1, 5); s = "splat"; }
810
+ } else if (k === 1) {
811
+ if (pn === 0) { [-90, 0, 90].forEach((o, i) => lob(g, bx, by - 30, tg.x + o, gnY(tg.x + o), .9 + i * .2, 2)); s = "heavy"; }
812
+ else if (pn === 1) {
813
+ const xs = al.slice(0, 4).map(a => a.x); xs.push(BX0 + Math.random() * (BX1 - BX0), BX0 + Math.random() * (BX1 - BX0));
814
+ for (const x of xs) HC.obj[nid("o")] = { t: "pil", x: Math.round(x), y: Math.round(gnd(lv, x)), o: "", w: now + .9, e: now + 1.6, dm: ebDm };
815
+ s = "boss";
816
+ }
817
+ else if (pn === 2) { for (let i = 0; i < 8; i++) ebAdd(g, tg.x + (Math.random() - .5) * 520, -40 - i * 70, (Math.random() - .5) * 80, 60, 3, 6); s = "boom"; }
818
+ else { for (const d of [-1, 1]) ebAdd(g, m.x + d * 60, gnY(m.x), d * 330, 0, 10, 3.2); s = "boom"; }
819
+ } else if (k === 2) {
820
+ if (pn === 0) { burst(6); s = "laser"; }
821
+ else if (pn === 1) { m.lk = now + 1.1; burst(10); s = "flame"; }
822
+ else if (pn === 2) { const d = Math.hypot(tg.x - m.x, tg.y - m.y) || 1; m.dv = now + 1; m.dvx = (tg.x - m.x) / d * 720; m.dvy = (tg.y - m.y) / d * 720; s = "beam"; }
823
+ else { for (let i = 0; i < 14; i++) ebAdd(g, BX0 - 60 + Math.random() * (BX1 - BX0 + 120), -20 - i * 35, 0, 300, 4, 4); s = "laser"; }
824
+ } else if (k === 3) {
825
+ if (pn === 0) { const sd = Math.random() < .5 ? 1 : -1; HC.obj[nid("o")] = { t: "laz", x: Math.round(m.x), y: Math.round(m.y), o: "", a: ang - .6 * sd, b: ang + .6 * sd, w: now + .75, e: now + 2.3, dm: ebDm }; m.lk = now + 2.3; s = "beam"; }
826
+ else if (pn === 1) { burst(3); s = "zap"; }
827
+ else if (pn === 2) {
828
+ fx(g, "blink", { x: Math.round(m.x), y: Math.round(m.y) });
829
+ m.x = inArena(tg.x + (Math.random() - .5) * 240); m.y = 200;
830
+ fx(g, "blink", { x: Math.round(m.x), y: Math.round(m.y) });
831
+ for (let i = 0; i < 14; i++) shootEb(g, m.x, m.y, i / 14 * 6.283, 230, 6, 3); s = "zap";
832
+ }
833
+ else { m.lk = now + 2.4; m.sa = ang; burst(20); s = "magic"; }
834
+ } else {
835
+ if (pn === 0) { for (let i = 0; i < 6; i++) shootEb(g, bx, by + 10, ang + (i - 2.5) * .35, 150, 7, 4.5); s = "splat"; }
836
+ else if (pn === 1) { if (room) for (let i = 0; i < 3; i++) spawnMob(g, 7, inArena(tg.x + (i - 1) * 130), hpM * .6); s = "spell"; }
837
+ else if (pn === 2) { burst(3); s = "arrow"; }
838
+ else { for (let i = 0; i < 3; i++) spawnMob(g, 7, BX0 + 60 + i * (BX1 - BX0 - 120) / 2, hpM * .6); m.sh = 1; fx(g, "batk", { s: "spell", b: "HIVE SHIELD! Smash the eggs!" }); return; }
839
+ }
840
+ fx(g, "batk", { s });
841
+ }
842
+ // one shot of a queued burst; returns the wait until the next one
843
+ function bossBurst(g, m, tg, k) {
844
+ const bx = m.x + m.f * 50, by = m.y + 4, ang = Math.atan2(tg.y - by, tg.x - bx);
845
+ if (k === 2 && m.bk === 0) { shootEb(g, bx, by, ang + (Math.random() - .5) * .1, 560, 4, 2.5); return .11; }
846
+ if (k === 2 && m.bk === 1) { for (let i = 0; i < 2; i++) shootEb(g, bx, by, ang + (Math.random() - .5) * .7, 200 + Math.random() * 80, 5, 1.5); return .1; }
847
+ if (k === 3 && m.bk === 1) { for (let i = -2; i <= 2; i++) shootEb(g, m.x, m.y, ang + i * .2, 380, 6, 3); return .3; }
848
+ if (k === 3 && m.bk === 3) { m.sa = (m.sa || 0) + .45; for (const d of [0, Math.PI]) shootEb(g, m.x, m.y, m.sa + d, 220, 6, 2.2); return .12; }
849
+ if (k === 4 && m.bk === 2) { for (let i = -3; i <= 3; i++) shootEb(g, m.x, m.y + 30, Math.PI / 2 + i * .17, 400, 8, 2.5); return .25; }
850
+ return .2;
851
+ }
852
+
853
+ // ---------- Mini bosses: a halfway ambush on every planet, 3 telegraphed attacks in turn (MINI in data.js) ----------
854
+ // m.rl = rolling / lunging along the ground (speed m.rv), m.dv = diving at a hero; both deal contact damage.
855
+ // Every shot and hazard they make is scaled by MINI_POW × BIG_DMG (hostTick sets ebDm around miniTick; hazards copy it to o.dm).
856
+ function miniTick(g, m, tg, lv, pi, now, dt, sp, hpM) {
857
+ const k = pi % 5, M = MINI[k], enr = m.hp < m.mhp * .4, gy = gnd(lv, m.x) - AL[8].r, held = m.lk && m.lk > now;
858
+ if (m.bu > 0 && now > m.bt) { m.bu--; m.bt = now + miniBurst(g, m, tg, k); }
859
+ if (m.ck && !(m.rl > now)) { // Claw Crush lands: a fire column under each claw
860
+ m.ck = 0;
861
+ for (const d of [-1, 1]) { const x = m.x + d * 55; HC.obj[nid("o")] = { t: "pil", x: Math.round(x), y: Math.round(gnd(lv, x)), o: "", w: now + .3, e: now + .9, dm: ebDm }; }
862
+ fx(g, "slam", { x: Math.round(m.x), y: Math.round(gy + AL[8].r) });
863
+ }
864
+ if (m.rl > now) { m.f = m.rv > 0 ? 1 : -1; m.x += m.rv * dt; m.y = gy; }
865
+ else if (m.dv > now) { m.x += m.dvx * dt; m.y = Math.min(gy, m.y + m.dvy * dt); }
866
+ else if (!M.fly) { // walkers keep a fighting distance and follow the squad
867
+ const want = tg.x + (m.x > tg.x ? 210 : -210);
868
+ if (!m.wu && !held && Math.abs(want - m.x) > 10) m.x += Math.sign(want - m.x) * sp * (enr ? 2 : 1.6) * dt;
869
+ m.y = gy;
870
+ } else if (!held) { // flyers hover off to one side, bobbing
871
+ const tx = tg.x + (m.x > tg.x ? 230 : -230) + Math.sin(now * .9 + m.sd) * 60, ty = gnd(lv, m.x) - (k === 3 ? 160 : 190) + 30 * Math.sin(now * 1.6 + m.sd);
872
+ m.x += (tx - m.x) * Math.min(1, dt * 1.4); m.y += (ty - m.y) * Math.min(1, dt * 1.6);
873
+ }
874
+ if (held || m.rl > now || m.dv > now || Math.abs(tg.x - m.x) > 700) return;
875
+ if (!m.wu && now > m.cd) { m.px = ((m.px == null ? -1 : m.px) + 1) % 3; m.pn = m.px; m.wu = now + (enr ? .55 : .75); }
876
+ else if (m.wu && now > m.wu) {
877
+ m.wu = 0; m.at = now; m.cd = now + (enr ? 1.5 : 2.3);
878
+ miniAttack(g, m, tg, lv, k, m.pn, now, hpM);
879
+ }
880
+ }
881
+ function miniAttack(g, m, tg, lv, k, pn, now, hpM) {
882
+ const bx = m.x + m.f * 34, by = m.y - 6, ang = Math.atan2(tg.y - by, tg.x - bx), room = Object.keys(HC.mobs).length < 30;
883
+ const gnY = x => gnd(lv, x) - 20, burst = n => { m.bu = n; m.bk = pn; m.bt = now; };
884
+ let s = "warn";
885
+ if (k === 0) { // Thornback
886
+ if (pn === 0) { burst(3); s = "arrow"; } // Thorn Volley: three fans of thorns
887
+ else if (pn === 1) { m.rl = now + 1.2; m.rv = m.f * 560; s = "heavy"; } // Rolling Charge: curls up and bowls through
888
+ else { for (let i = 0; i < 6; i++) { const tx = tg.x + (i - 2.5) * 70; lob(g, m.x, m.y - 30, tx, gnY(tx), .8 + i * .08, 1); } s = "splat"; } // Acid Fountain
889
+ } else if (k === 1) { // Cinder Crab
890
+ if (pn === 0) { burst(5); s = "flame"; } // Ember Spray: lobbed fireballs around you
891
+ else if (pn === 1) { m.rl = now + .45; m.rv = m.f * 620; m.ck = 1; s = "heavy"; } // Claw Crush: a lunge, then fire columns
892
+ else { ebAdd(g, m.x + m.f * 44, gnY(m.x), m.f * 300, 0, 10, 3); s = "boom"; } // Lava Spit: a lava wave to jump
893
+ } else if (k === 2) { // Frost Moth
894
+ if (pn === 0) { for (let i = -2; i <= 2; i++) shootEb(g, bx, by, ang + i * .16, 420, 4, 2.5); s = "laser"; } // Ice Fan
895
+ else if (pn === 1) { m.lk = now + 1; burst(9); s = "cryo"; } // Frost Dust: a chilling cone
896
+ else { for (let i = 0; i < 8; i++) ebAdd(g, tg.x + (i - 3.5) * 55 + rnd(-15, 15), -20 - i * 40, 0, 320, 4, 4); s = "laser"; } // Hailstorm
897
+ } else if (k === 3) { // Prism Sentry
898
+ if (pn === 0) { for (let i = 0; i < 10; i++) shootEb(g, m.x, m.y, i / 10 * 6.283 + now, 210, 6, 3); s = "zap"; } // Shard Ring
899
+ else if (pn === 1) { const sd = Math.random() < .5 ? 1 : -1; HC.obj[nid("o")] = { t: "laz", x: Math.round(m.x), y: Math.round(m.y), o: "", a: ang - .45 * sd, b: ang + .45 * sd, w: now + .7, e: now + 1.9, dm: ebDm }; m.lk = now + 1.9; s = "beam"; } // Sweep Beam
900
+ else { burst(4); s = "zap"; } // Zap Burst
901
+ } else { // Hive Warden
902
+ if (pn === 0) { const d = Math.hypot(tg.x - m.x, tg.y - m.y) || 1; m.dv = now + .9; m.dvx = (tg.x - m.x) / d * 640; m.dvy = (tg.y - m.y) / d * 640; s = "beam"; } // Stinger Dive
903
+ else if (pn === 1) { for (let i = 0; i < 4; i++) shootEb(g, bx, by + 10, ang + (i - 1.5) * .4, 140, 7, 4.5); s = "splat"; } // Spore Puff: homing spores
904
+ else { if (room) for (const o of [-90, 90]) spawnMob(g, 7, clamp(tg.x + o, 40, GATE - 40), hpM * .35); s = "spell"; } // Larva Drop: two egg pods
905
+ }
906
+ fx(g, "batk", { s });
907
+ }
908
+ // one shot of a mini boss burst; returns the wait until the next one
909
+ function miniBurst(g, m, tg, k) {
910
+ const bx = m.x + m.f * 34, by = m.y - 4, ang = Math.atan2(tg.y - by, tg.x - bx);
911
+ if (k === 0) { for (let i = -1; i <= 1; i++) shootEb(g, bx, by - 10, ang + i * .22, 380, 0, 2); return .32; }
912
+ if (k === 1) { const tx = tg.x + rnd(-130, 130); lob(g, bx, by - 20, tx, gnd(level(HC.planet), tx) - 20, .75, 2); return .18; }
913
+ if (k === 2) { shootEb(g, bx, by + 8, ang + rnd(-.35, .35), 220 + rnd(0, 60), 5, 1.6); return .1; }
914
+ if (k === 3) { for (let i = -1; i <= 1; i++) shootEb(g, m.x, m.y, ang + i * .12, 400, 6, 2.5); return .22; }
915
+ return .2;
916
+ }
917
+
918
+ function hostTick(g, dt) {
919
+ const now = g.now, pi = HC.planet, lv = level(pi), P = lv.P;
920
+ const al = []; let front = 0, back = 1e9, pc = 0;
921
+ for (const p of g.players) {
922
+ const q = HC.pl[p.id], st = p.state;
923
+ if (!q || !st || st.cls < 0 || p.online === false) continue;
924
+ if (!Number.isFinite(st.x) || !Number.isFinite(st.y)) continue; // a hero whose position has not arrived yet: aliens chasing it would end up at NaN
925
+ pc++; if (q.dead) continue;
926
+ // right after a warp, ignore stale far-right positions: they would push "front" to the boss arena and spawn every group there at once
927
+ if (now < (HC.warpT || 0) && st.x > 1200) continue;
928
+ al.push({ id: p.id, x: st.x, y: st.y - 22 }); front = Math.max(front, st.x); back = Math.min(back, st.x);
929
+ }
930
+ if (!pc) return;
931
+ if (al.length) HC.front = front; else { front = HC.front; back = HC.front; }
932
+ const hpM = (1 + .35 * pi) * (1 + .3 * (pc - 1)) * dfOf().hp, dmM = 1 + .15 * pi;
933
+
934
+ // respawns and game over
935
+ for (const id in HC.pl) { const q = HC.pl[id]; if (q.dead > 0 && now > q.dead) { q.dead = 0; q.hp = q.mhp; q.inv = r2(now + 2.5); q.rx = Math.round(outHole(lv, Math.max(60, HC.front - 80), 40)); q.rv = (q.rv || 0) + 1; HC.dPl = 1; } }
936
+ // co-op revive: stand on a downed teammate's beacon for 2 s
937
+ for (const id in HC.pl) {
938
+ const q = HC.pl[id]; if (!q.dead) continue;
939
+ let helper = false; for (const a of al) if (Math.abs(a.x - (q.dx || 0)) < 50 && Math.abs(a.y + 22 - (q.dy || 0)) < 70) helper = true;
940
+ const before = q.rp || 0;
941
+ q.rp = r2(helper ? before + dt : Math.max(0, before - dt * 2));
942
+ if (q.rp !== before) HC.dPl = 1;
943
+ if (q.rp >= 2) { q.dead = 0; q.rp = 0; q.hp = Math.round(q.mhp * .5); q.inv = r2(now + 2); q.rx = q.dx || 100; q.rv = (q.rv || 0) + 1; HC.dPl = 1; fx(g, "revive", { x: q.dx, y: q.dy }); }
944
+ }
945
+ let allOut = true;
946
+ for (const p of g.players) { const q = HC.pl[p.id]; if (q && p.online !== false && p.state && p.state.cls >= 0 && q.dead !== -1) allOut = false; }
947
+ if (allOut && !HC.over) { HC.over = 1; pushAll(g, true); g.gameOver(); return; }
948
+
949
+ let mc = Object.keys(HC.mobs).length;
950
+ if (HC.phase === "clear") { if (now > HC.nextAt) nextPlanet(g); }
951
+ else {
952
+ while (HC.spawned < lv.sp.length && lv.sp[HC.spawned].x < front + 720 && mc < 34) {
953
+ const sg = lv.sp[HC.spawned++], ex = sg.t === 4 ? 0 : dfOf().ex; // Brutes stay as placed
954
+ let n = sg.n + (sg.t === 4 ? 0 : Math.floor((pc - 1) * .7) + (pi >= 3 ? 1 : 0));
955
+ if (ex) n = Math.max(1, n + ex);
956
+ for (let i = 0; i < n; i++) { spawnMob(g, sg.t, Math.max(sg.x, front + 520) + i * 45, hpM); mc++; }
957
+ }
958
+ ambT -= dt;
959
+ if (ambT <= 0) { ambT = 7 - Math.min(4, pi); if (mc < 24 && front > 600 && front < L - 900) { spawnMob(g, 2, front + 560, hpM); mc++; } }
960
+ // halfway ambush: the planet's mini boss drops in just ahead of the squad; the boss waits until it is beaten
961
+ if (HC.mini && !HC.mobs[HC.mini]) { HC.mini = ""; HC.miniDone = true; }
962
+ if (!HC.mini && !HC.miniDone && front > MINI_X) { HC.mini = spawnMob(g, 8, Math.min(front + 480, GATE - 200), hpM * BOSS[pi % 5].hp * BIG_HP); mc++; fx(g, "minin", {}); }
963
+ if (!HC.boss && !HC.bossDone && !HC.mini && front > L - 820) { HC.boss = spawnMob(g, 5, BOSS_X, hpM * BOSS[pi % 5].hp * BIG_HP); mc++; fx(g, "bossin", {}); }
964
+ }
965
+
966
+ // aliens (they chase the nearest hero, but a Holo Decoy looks twice as tempting)
967
+ const tgs = al.slice();
968
+ for (const oid in HC.obj) { const o = HC.obj[oid]; if (o.t === "dc") tgs.push({ id: "", x: o.x, y: o.y - 22, dc: 1 }); }
969
+ for (const id in HC.mobs) {
970
+ const m = HC.mobs[id], A = AL[m.t];
971
+ if (m.t !== 5 && m.t !== 8 && m.x < back - 1100) { delete HC.mobs[id]; continue; }
972
+ if (m.bn) { // on fire: a quiet tick every 0.3 s (the flames show it, no number spam)
973
+ if (now > m.bn) delete m.bn;
974
+ else if (now > (m.bnt || 0)) { m.bnt = now + .3; damage(g, id, 4 * lvMul(lvlOf(m.bo)), m.bo, null, false, true); if (!HC.mobs[id]) continue; }
975
+ }
976
+ if (m.cl && now > m.cl) { delete m.cl; m.cs = 0; }
977
+ if (m.fz && m.fz > now) continue;
978
+ const mdt = m.cl ? dt * .5 : dt; // chilled aliens move at half speed
979
+ if (m.dn) { if (m.dn > now) continue; m.dn = 0; } // downed Shambler: harmless until it stands up
980
+ let tg = null, bd = 1e9;
981
+ for (const a of tgs) { const d = (Math.abs(a.x - m.x) + Math.abs(a.y - m.y) * .5) * (a.dc ? .5 : 1); if (d < bd) { bd = d; tg = a; } }
982
+ if (!tg) { if (m.t !== 2 && m.t !== 5 && m.t !== 8) m.y = gnd(lv, m.x) - A.r; continue; }
983
+ const dx = tg.x - m.x, ad = Math.abs(dx), sp = A.v * (1 + .06 * pi);
984
+ m.f = dx > 0 ? 1 : -1;
985
+ if (m.t === 0) { if (ad > 8) m.x += m.f * sp * mdt; m.y = gnd(lv, m.x) - A.r; }
986
+ else if (m.t === 1) {
987
+ m.vy += 1500 * mdt; m.x += m.vx * mdt; m.y += m.vy * mdt;
988
+ const g2 = gnd(lv, m.x) - A.r;
989
+ if (m.y >= g2) { m.y = g2; m.vy = 0; m.vx *= .7; if (now > m.cd && ad < 700) { m.vy = -480 - Math.random() * 220; m.vx = m.f * (sp * 1.5 + Math.random() * 80); m.cd = now + .7 + Math.random() * .9; } }
990
+ }
991
+ else if (m.t === 2) {
992
+ const dive = m.dv && m.dv > now, s2 = dive ? sp * 2 : sp;
993
+ const tx = dive ? tg.x : tg.x + Math.sin(now * 1.3 + m.sd) * 140, ty = dive ? tg.y : tg.y - 110 + Math.sin(now * 2.5 + m.sd) * 45;
994
+ m.x += clamp((tx - m.x) * 2, -s2, s2) * mdt; m.y += clamp((ty - m.y) * 2, -s2, s2) * mdt;
995
+ m.y = clamp(m.y, 40, gnd(lv, m.x) - 16);
996
+ if (!dive && ad < 170 && now > m.cd) { m.dv = now + .7; m.cd = now + 2.5 + Math.random() * 2; }
997
+ }
998
+ else if (m.t === 3) {
999
+ if (ad > 380) m.x += m.f * sp * mdt; else if (ad < 220) m.x -= m.f * sp * mdt;
1000
+ m.y = gnd(lv, m.x) - A.r;
1001
+ // wind-up (0.5 s glow) before spitting, so players can react
1002
+ if (!m.wu && now > m.cd && ad < 640) m.wu = now + .5;
1003
+ else if (m.wu && now > m.wu) { m.wu = 0; m.cd = now + 2 + Math.random() * 1.2; m.at = now; const bx = m.x + m.f * 14, by = m.y - 4; shootEb(g, bx, by, Math.atan2(tg.y - by, tg.x - bx), 250); }
1004
+ }
1005
+ else if (m.t === 4) {
1006
+ const ch = m.ch && m.ch > now;
1007
+ if (m.wu) { if (now > m.wu) { m.wu = 0; m.ch = now + .8; m.cd = now + 3.5; m.cf = m.f; } } // rear back, then charge
1008
+ else if (!ch && ad < 320 && now > m.cd) m.wu = now + .45;
1009
+ if (ch) m.f = m.cf || m.f;
1010
+ if (!m.wu) m.x += m.f * sp * (ch ? 3.6 : 1) * mdt;
1011
+ m.y = gnd(lv, m.x) - A.r;
1012
+ }
1013
+ else if (m.t === 6) { // Shambler: lurches forward, groans (telegraph), then lunges
1014
+ const ch = m.ch && m.ch > now;
1015
+ if (m.wu) { if (now > m.wu) { m.wu = 0; m.ch = now + .6; m.cd = now + 3 + Math.random() * 1.5; m.cf = m.f; } }
1016
+ else if (!ch && ad < 260 && now > m.cd) m.wu = now + .5;
1017
+ if (ch) m.f = m.cf || m.f;
1018
+ if (!m.wu) m.x += m.f * sp * (ch ? 4.2 : .5 + .5 * Math.abs(Math.sin(now * 2.5 + m.sd))) * mdt;
1019
+ m.y = gnd(lv, m.x) - A.r;
1020
+ }
1021
+ else if (m.t === 7) { // egg pod: sits still, then hatches into a Buzzwing or a Shambler
1022
+ m.y = gnd(lv, m.x) - A.r;
1023
+ if (now > m.ht) { delete HC.mobs[id]; fx(g, "die", { x: Math.round(m.x), y: Math.round(m.y), t: 7 }); spawnMob(g, Math.random() < .6 ? 2 : 6, m.x, hpM * .8); continue; }
1024
+ }
1025
+ else if (m.t === 8) { ebDm = MINI_POW * BIG_DMG; miniTick(g, m, tg, lv, pi, now, mdt, sp, hpM); ebDm = 1; }
1026
+ else { ebDm = BIG_DMG; bossTick(g, m, tg, al, lv, pi, now, mdt, sp, hpM); ebDm = 1; }
1027
+ // walkers stop at the edge of a pit (rolls and charges end there too)
1028
+ if (m.t !== 2 && m.t !== 5 && !(m.t === 8 && MINI[pi % 5].fly) && m.y >= gnd(lv, m.x) - A.r - 4) {
1029
+ const nx = outHole(lv, m.x, A.r * .6); if (nx !== m.x) { m.x = nx; m.y = gnd(lv, nx) - A.r; m.ch = 0; m.rl = 0; }
1030
+ }
1031
+ m.x = clamp(m.x, 20, L - 20);
1032
+ for (const a of al) if (Math.abs(a.x - m.x) < A.r + 12 && Math.abs(a.y - m.y) < A.r + 22 && now > m.hc) { hurt(g, a.id, A.dmg * dmM * (m.t === 5 || m.t === 8 ? BIG_DMG : 1)); m.hc = now + .7; }
1033
+ }
1034
+
1035
+ // alien shots
1036
+ for (const id in HC.eb) {
1037
+ const e = HC.eb[id], K = EBK[e.k || 0];
1038
+ if (K.g) e.vy += K.g * dt;
1039
+ if (K.home && al.length) { // spores turn toward the nearest hero
1040
+ let tx = al[0], bd = 1e9; for (const a of al) { const d = Math.abs(a.x - e.x) + Math.abs(a.y - e.y); if (d < bd) { bd = d; tx = a; } }
1041
+ const sp0 = Math.hypot(e.vx, e.vy) || 150, cur = Math.atan2(e.vy, e.vx), want = Math.atan2(tx.y - e.y, tx.x - e.x);
1042
+ let da = want - cur; while (da > Math.PI) da -= 6.283; while (da < -Math.PI) da += 6.283;
1043
+ const na = cur + clamp(da, -1.9 * dt, 1.9 * dt); e.vx = Math.cos(na) * sp0; e.vy = Math.sin(na) * sp0;
1044
+ }
1045
+ e.x += e.vx * dt; e.y += e.vy * dt;
1046
+ if (K.gw) { e.y = gnd(lv, e.x) - K.gw; if (holeAt(lv, e.x, 4)) { delete HC.eb[id]; continue; } } // ground waves pour into pits
1047
+ if (now > e.e || e.y > gnd(lv, e.x) + 5 || e.y < -700 || e.x < 0 || e.x > L) { delete HC.eb[id]; continue; }
1048
+ for (const a of al) if (Math.abs(a.x - e.x) < K.hx && Math.abs(a.y - e.y) < K.hy) {
1049
+ const q = HC.pl[a.id]; if (K.chill && q && !(q.inv > now)) { q.sl = r2(now + 2.5); HC.dPl = 1; }
1050
+ hurt(g, a.id, K.d * dmM * (e.dm || 1));
1051
+ if (!K.gw) { delete HC.eb[id]; break; } // ground waves roll on through everyone
1052
+ }
1053
+ }
1054
+
1055
+ // planet traps and weather (traps.js, weather.js)
1056
+ hostTraps(g, al, lv, now, dmM);
1057
+ hostWeather(g, al, lv, now);
1058
+
1059
+ // hero shots
1060
+ const ids = {};
1061
+ for (const s of g.shots || []) {
1062
+ ids[s.id] = 1; if (dead[s.id]) continue;
1063
+ const k = wkOf(s), w = WP[k];
1064
+ if (!seen[s.id]) { seen[s.id] = 1; const q = HC.pl[s.from]; if (q && w.ammo && q.w === k && q.ammo > 0) { q.ammo--; if (q.ammo <= 0) { q.w = ""; q.ammo = 0; } HC.dPl = 1; } }
1065
+ if (w.cut) for (const eid in HC.eb) { // the laser slices through alien shots (not ground waves)
1066
+ const e = HC.eb[eid]; if (EBK[e.k || 0].gw || Math.abs(e.x - s.x) > 26 || Math.abs(e.y - s.y) > 26) continue;
1067
+ delete HC.eb[eid]; if (cutBuf.length < 8) cutBuf.push([Math.round(e.x), Math.round(e.y)]);
1068
+ }
1069
+ for (const mid in HC.mobs) {
1070
+ const m = HC.mobs[mid], rr2 = AL[m.t].r + s.r + 2, ddx = s.x - m.x, ddy = s.y - m.y;
1071
+ if (ddx * ddx + ddy * ddy > rr2 * rr2 || (m.dn && m.dn > now)) continue; // shots fly over a downed Shambler
1072
+ if (w.pierce) { const hs = hitSet[s.id] || (hitSet[s.id] = {}); if (hs[mid]) continue; hs[mid] = 1; }
1073
+ const slv = lvlOf(s.from), tr = tierOf(slv);
1074
+ let dmg = w.dmg * lvMul(slv), crit = false;
1075
+ if (w.crit && Math.random() < w.crit * (tr > 1 ? 2 : 1)) { dmg *= 2; crit = true; }
1076
+ const sp0 = findP(g, s.from), kx = sp0 && sp0.state ? sp0.state.x : s.x - 1;
1077
+ if (w.aoe) boom(g, s.x, s.y, w.aoe * (k === "plasma" && tr ? 1.45 : 1), dmg, s.from, w.lite, w.c); else damage(g, mid, dmg, s.from, kx, crit);
1078
+ if (w.cl) for (const d of [-1, 1]) boom(g, s.x + d * 55, s.y - 8, 42, dmg * .4, s.from, 1, "#ffba08"); // rocket bomblets
1079
+ if (w.chain) chainHit(g, mid, s.x, s.y, w.chain + (k === "bolt" ? tr : 0), dmg * (k === "bolt" ? .6 : .7), s.from, w.c);
1080
+ afflict(g, mid, w, s.from);
1081
+ if (!w.pierce) { dead[s.id] = 1; g.hit(s); break; }
1082
+ }
1083
+ }
1084
+ if ((pruneT -= dt) < 0) { pruneT = 2; for (const k in seen) if (!ids[k]) delete seen[k]; for (const k in hitSet) if (!ids[k]) delete hitSet[k]; for (const k in dead) if (!ids[k]) delete dead[k]; }
1085
+
1086
+ // skill objects
1087
+ for (const oid in HC.obj) {
1088
+ const o = HC.obj[oid];
1089
+ if (o.t === "gr") {
1090
+ o.vy += 1300 * dt; o.x += o.vx * dt; o.y += o.vy * dt;
1091
+ const gy = gnd(lv, o.x) - 5; if (o.y > gy) { o.y = gy; o.vy *= -.35; o.vx *= .6; }
1092
+ if (now > o.e) { delete HC.obj[oid]; boom(g, o.x, o.y, 90, 45 * lvMul(lvlOf(o.o)), o.o); }
1093
+ } else if (o.t === "met") {
1094
+ o.y += 780 * dt; o.x += o.vx * dt;
1095
+ if (o.y >= gnd(lv, o.x) - 6) { delete HC.obj[oid]; boom(g, o.x, o.y, 95, 60 * lvMul(lvlOf(o.o)), o.o); }
1096
+ } else if (o.t === "spin") {
1097
+ const p = findP(g, o.o), q = HC.pl[o.o];
1098
+ if (!p || !p.state || !q || q.dead || now > o.e) { delete HC.obj[oid]; continue; }
1099
+ o.x = p.state.x; o.y = p.state.y;
1100
+ if (now > o.nt) { o.nt = now + .2; for (const mid in HC.mobs) { const m = HC.mobs[mid]; if (Math.hypot(m.x - o.x, m.y - (o.y - 22)) < 100 + AL[m.t].r * .5) damage(g, mid, 16 * lvMul(q.lv), o.o); } }
1101
+ } else if (ALLY[o.t]) { allyTick(g, oid, o, lv, now, dt);
1102
+ } else if (o.t === "ltn") { // weather: a lightning strike lands when its warning runs out
1103
+ if (now > o.w) { delete HC.obj[oid]; strike(g, o.x, o.y, al, dmM); }
1104
+ } else if (o.t === "pil") { // Magma Titan eruption: glowing warning, then a fire column
1105
+ if (now > o.e) { delete HC.obj[oid]; continue; }
1106
+ if (now > o.w) for (const a of al) if (Math.abs(a.x - o.x) < 30) hurt(g, a.id, 18 * dmM * (o.dm || 1));
1107
+ } else if (o.t === "laz") { // Crystal Overmind prism beam: aim line, then a sweeping beam
1108
+ if (now > o.e) { delete HC.obj[oid]; continue; }
1109
+ if (now > o.w) {
1110
+ const an = o.a + (o.b - o.a) * clamp((now - o.w) / (o.e - o.w), 0, 1), cx = Math.cos(an), cy = Math.sin(an);
1111
+ for (const a of al) { const dx = a.x - o.x, dy = a.y - o.y, along = dx * cx + dy * cy; if (along > 0 && along < 900 && Math.abs(dx * cy - dy * cx) < 20) hurt(g, a.id, 14 * dmM * (o.dm || 1)); }
1112
+ }
1113
+ } else if (o.t === "tur") {
1114
+ if (now > o.e) { delete HC.obj[oid]; continue; }
1115
+ if (now > o.nt) {
1116
+ let best = null, bd2 = 480 * 480;
1117
+ for (const mid in HC.mobs) { const m = HC.mobs[mid], d2 = (m.x - o.x) ** 2 + (m.y - o.y + 26) ** 2; if (d2 < bd2) { bd2 = d2; best = mid; } }
1118
+ if (best) { const m = HC.mobs[best]; o.nt = now + .35; o.f = m.x > o.x ? 1 : -1; fx(g, "zap", { x1: Math.round(o.x + o.f * 16), y1: Math.round(o.y - 26), x2: Math.round(m.x), y2: Math.round(m.y) }); damage(g, best, turretDmg(o.o), o.o); }
1119
+ else o.nt = now + .15;
1120
+ }
1121
+ }
1122
+ }
1123
+ for (const lid in HC.loot) if (now > HC.loot[lid].e) { delete HC.loot[lid]; HC.dLoot = 1; }
1124
+ }
1125
+
1126
+ // ---------- Local player ----------
1127
+ function aim(src) {
1128
+ const base = face > 0 ? 0 : Math.PI, ms = src.mobs || {}, ox = px + face * 20, oy = py - 27;
1129
+ let best = null, bd = 1e9;
1130
+ for (const id in ms) {
1131
+ const d = disp[id] || ms[id], dx = d.x - ox, dy = d.y - oy;
1132
+ if (dx * face <= 0 || Math.abs(dx) > 640) continue;
1133
+ const a = Math.atan2(dy, dx); let da = Math.abs(a - base); if (da > Math.PI) da = 2 * Math.PI - da;
1134
+ if (da > .6) continue;
1135
+ const dist = dx * dx + dy * dy; if (dist < bd) { bd = dist; best = a; }
1136
+ }
1137
+ return best == null ? base : best;
1138
+ }
1139
+ // Shooting patterns: every class weapon (and pickup) fires in its own rhythm. shotN counts triggers (reset after a
1140
+ // pause), so combos and bursts restart cleanly. Each pattern lists shots as [angle offset, weapon key, life];
1141
+ // returns the wait until the next trigger.
1142
+ function fireW(g, k, ang, lv) {
1143
+ const tr = tierOf(lv), n = shotN++, out = [];
1144
+ let cd = WP[k].rate;
1145
+ const S = (a, kk, life) => out.push([a, kk || k, life]);
1146
+ if (k === "rifle") { const b = tr ? 4 : 3; cd = n % b === b - 1 ? .3 : .07; if (tr > 1) { S(-.035); S(.035); } else S(rnd(-.02, .02)); } // bursts
1147
+ else if (k === "bolt") S(n % 2 ? .07 : -.07); // bolts weave up and down
1148
+ else if (k === "wave") { // slash, slash, Cross Slash
1149
+ if (n % 3 === 2) { cd = .45; if (tr > 1) { S(-.16, "wave2"); S(.16, "wave2"); } else S(0, "wave2", tr ? .42 : 0); }
1150
+ else S(n % 3 ? .05 : -.05);
1151
+ }
1152
+ else if (k === "arrow") { if (n % 4 === 3) { if (tr) { S(-.08, "arrow2"); S(0, "arrow2"); S(.08, "arrow2"); } else S(0, "arrow2"); } else S(0); } // every 4th: Power Shot
1153
+ else if (k === "nail") S(rnd(-.04, .04), n % (5 - tr) === 4 - tr ? "nail2" : k); // hot rivet every 5th / 4th / 3rd
1154
+ else if (k === "plasma") { if (tr > 1 && n % 2) { S(-.09); S(.09); } else S(0); }
1155
+ else if (k === "H") { spin = Math.min(1, spin + .12); cd = .13 - .08 * spin; S(rnd(-.06, .06) * (.4 + spin)); } // spins up, shakes more
1156
+ else if (k === "S") { if (n % 2) for (const a of [-.05, -.017, .017, .05]) S(a); else for (let i = -2; i <= 2; i++) S(i * .13); } // wide, tight, wide...
1157
+ else if (k === "R") S(n % 2 ? .04 : -.04); // two launch tubes
1158
+ else if (k === "F") S(rnd(-.14, .14));
1159
+ else if (k === "M") for (const a of [-.16, 0, .16]) S(a + rnd(-.03, .03)); // a fanned salvo of 3
1160
+ else if (k === "Z") S(rnd(-.03, .03));
1161
+ else for (let i = 0, p = WP[k].pel || 1; i < p; i++) S(p > 1 ? (i - (p - 1) / 2) * .12 : 0);
1162
+ const ox = px + face * 22, oy = py - (myCls === 5 ? 28 : 27);
1163
+ for (const [da, kk, life] of out) {
1164
+ const w = WP[kk], a = ang + da * face, v = w.v * (kk === "M" ? rnd(.85, 1.1) : 1);
1165
+ g.shoot({ x: ox, y: oy, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: life || w.life, r: w.r, k: kk });
1166
+ }
1167
+ const top = out[0][1], w = WP[top];
1168
+ firing = T + .12;
1169
+ const recoil = { R: 170, plasma: 90, S: 60, wave: 40, wave2: 120, M: 70, arrow2: 50 }[top] || 0; // heavy guns kick you back
1170
+ if (recoil) { vx -= face * recoil; if (top === "R" || top === "wave2") addShake(.12); }
1171
+ addP(ox + face * 6, oy, face * 40, 0, .06, w.c, top === "wave2" || top === "nail2" || top === "arrow2" ? 14 : 8, 0);
1172
+ if (k === "Z") for (let i = 0; i < 3; i++) addP(ox, oy, face * rnd(40, 160), rnd(-90, 90), .1, "#7df9ff", 2, 0);
1173
+ if (k === "rifle" || k === "H" || k === "nail" || k === "S") addP(px + face * 4, oy + 2, -face * rnd(40, 90), rnd(-160, -90), .5, "#e9c46a", 2, 0, null, 900); // shell casing
1174
+ sfx({ bolt: "magic", arrow: "arrow", arrow2: "arrow", L: "laser", plasma: "laser", F: "flame", R: "heavy", wave: "heavy", wave2: "slash", nail2: "heavy", Z: "tesla", C: "cryo", M: "missile" }[top] || "shot");
1175
+ return cd;
1176
+ }
1177
+ function volley(g) {
1178
+ const base = face > 0 ? 0 : Math.PI;
1179
+ for (let i = 0; i < 9; i++) { const a = base + face * (-.5 + i * .1); g.shoot({ x: px + face * 14, y: py - 30, vx: Math.cos(a) * 900, vy: Math.sin(a) * 900, life: .9, r: 4, k: "arrow" }); }
1180
+ sfx("arrow");
1181
+ }
1182
+ // fell to the bottom of a pit: pop back out just past the nearer edge and ask the host for the damage
1183
+ let fellT = -9;
1184
+ function fallPit(g, lv, h) {
1185
+ px = clamp(px < h.x + h.w / 2 ? h.x - 36 : h.x + h.w + 36, 15, L - 15); py = gnd(lv, px) - 70; vx = 0; vy = 0; onG = false; fellT = T;
1186
+ g.act("fall", { x: Math.round(px) });
1187
+ banner("FELL IN A PIT! −" + Math.round(PIT_DMG * 100) + "% HP", "#ff4d6d", 1.6); addShake(.35); sfx("down"); padRumble(.6, .8, 280);
1188
+ for (let i = 0; i < 14; i++) addP(px + rnd(-12, 12), py - 20, rnd(-90, 90), rnd(-90, 40), .5, i % 2 ? "#ffd60a" : "#fff", 3, 0);
1189
+ }
1190
+ function choose(g) {
1191
+ chosen = true; myCls = sel; curPlanet = -1; joinT = -9; lastSn = undefined; lastRv = undefined; lastHp = -1; lastCb = 0; look = 0;
1192
+ savedLv = profLv(g, myCls); savedXp = (g.profile && g.profile["X" + myCls]) || 0;
1193
+ sfx("pick");
1194
+ }
1195
+ // Snapshot interpolation: keep timestamped samples per entity and draw ~110 ms in the past,
1196
+ // blending between the two samples around that time (host draws its own world directly).
1197
+ function track(id, x, y, direct) {
1198
+ let b = disp[id];
1199
+ if (!b || direct) { if (!b) b = disp[id] = { s: [] }; b.s = [{ t: T, x, y }]; b.x = x; b.y = y; b.t = T; return; }
1200
+ b.t = T;
1201
+ const l = b.s[b.s.length - 1];
1202
+ if (l.x !== x || l.y !== y) {
1203
+ if (Math.abs(x - l.x) + Math.abs(y - l.y) > 300) b.s = [{ t: T, x, y }]; // teleport: snap
1204
+ else {
1205
+ if (T - l.t > .25) b.s.push({ t: T - .067, x: l.x, y: l.y }); // was idle: start moving from here
1206
+ b.s.push({ t: T, x, y }); if (b.s.length > 10) b.s.splice(0, b.s.length - 10);
1207
+ }
1208
+ }
1209
+ const rt = T - DELAY, s = b.s;
1210
+ if (rt <= s[0].t) { b.x = s[0].x; b.y = s[0].y; return; }
1211
+ for (let i = s.length - 1; i > 0; i--) {
1212
+ if (s[i - 1].t <= rt) {
1213
+ const a = s[i - 1], c = s[i];
1214
+ if (rt >= c.t) { b.x = c.x; b.y = c.y; } else { const k = (rt - a.t) / (c.t - a.t); b.x = a.x + (c.x - a.x) * k; b.y = a.y + (c.y - a.y) * k; }
1215
+ return;
1216
+ }
1217
+ }
1218
+ b.x = s[s.length - 1].x; b.y = s[s.length - 1].y;
1219
+ }
1220
+ function syncDisp(g, dt, src) {
1221
+ const direct = g.isHost && !!HC;
1222
+ const sm = (id, x, y) => track(id, x, y, direct);
1223
+ for (const id in src.mobs || {}) { const m = src.mobs[id]; if (m) sm(id, m.x, m.y); }
1224
+ for (const id in src.obj || {}) { const o = src.obj[id]; if (o) sm("o" + id, o.x, o.y); }
1225
+ for (const p of g.players) if (!p.me && p.state) track("p" + p.id, p.state.x, p.state.y, false);
1226
+ const eb = src.eb || {};
1227
+ for (const id in eb) {
1228
+ const e = eb[id]; if (!e) continue;
1229
+ let l = ebL[id]; if (!l) l = ebL[id] = { x: e.x, y: e.y, sx: e.x, sy: e.y, vx: e.vx, vy: e.vy };
1230
+ if (e.x !== l.sx || e.y !== l.sy) { l.sx = e.x; l.sy = e.y; l.x = e.x; l.y = e.y; l.vx = e.vx; l.vy = e.vy; }
1231
+ l.k = e.k || 0; const K = EBK[l.k];
1232
+ if (K.g) l.vy += K.g * dt; // lobbed shots fall between updates too
1233
+ l.x += l.vx * dt; l.y += l.vy * dt; l.t = T;
1234
+ }
1235
+ for (const id in ebL) if (!eb[id]) delete ebL[id];
1236
+ if ((cleanT -= dt) < 0) { cleanT = 2; for (const id in disp) if (T - disp[id].t > 1) delete disp[id]; for (const id in seenFx) if (T - seenFx[id] > 5) delete seenFx[id]; for (const id in flash) if (T - flash[id] > 1) delete flash[id]; }
1237
+ }
1238
+
1239
+ </script>
1240
+ <script>
1241
+ // Star Squad: render pipeline (cached HD layers, lighting, glow, bloom), planet backgrounds and world drawing
1242
+ "use strict";
1243
+ // ---------- Render pipeline: cached HD layers, dynamic lighting, additive glow, bloom ----------
1244
+ const RES = 2, TW = 1600, AMB = [.3, .36, .2, .48, .56], BLOOMA = [.3, .34, .16, .4, .36];
1245
+ const ADDC = new Set(["#ffba08", "#ff7b00", "#fff3b0", "#fff", "#ffd60a", "#ffe066", "#80ffdb", "#4cc9f0", "#c77dff", "#ff4d6d", "#ffc8dd", "#a2d2ff", "#ff9f1c"]);
1246
+ let BG = { pi: -1 }, LT = [], FL = [], SC = [], Q = 2, ftAvg = 16, lastDraw = 0, slowFor = 0, lightC = null, lightX = null, bloomC = null, bloomX = null, HOLE = null;
1247
+ const GT = {}, SHC = {};
1248
+ function hex2rgb(h) { h = String(h).replace("#", ""); if (h.length === 3) h = h.split("").map(v => v + v).join(""); const n = parseInt(h, 16) || 0; return [n >> 16 & 255, n >> 8 & 255, n & 255]; }
1249
+ function mix(a, b, t) { const A = hex2rgb(a), B = hex2rgb(b); return "#" + A.map((v, i) => Math.round(v + (B[i] - v) * t).toString(16).padStart(2, "0")).join(""); }
1250
+ function rgba(h, a) { const A = hex2rgb(h); return "rgba(" + A[0] + "," + A[1] + "," + A[2] + "," + a + ")"; }
1251
+ function mkC(w, h, res) { const r = res || RES, cv = document.createElement("canvas"); cv.width = Math.ceil(w * r); cv.height = Math.ceil(h * r); const x = cv.getContext("2d"); x.scale(r, r); return [cv, x]; }
1252
+ function glowTex(col) {
1253
+ if (GT[col]) return GT[col];
1254
+ const [cv, x] = mkC(64, 64, 1), g = x.createRadialGradient(32, 32, 0, 32, 32, 32);
1255
+ g.addColorStop(0, "rgba(255,255,255,1)"); g.addColorStop(.15, rgba(col, .95)); g.addColorStop(.45, rgba(col, .32)); g.addColorStop(1, rgba(col, 0));
1256
+ x.fillStyle = g; x.fillRect(0, 0, 64, 64); return GT[col] = cv;
1257
+ }
1258
+ function holeTex() {
1259
+ if (HOLE) return HOLE;
1260
+ const [cv, x] = mkC(64, 64, 1), g = x.createRadialGradient(32, 32, 0, 32, 32, 32);
1261
+ g.addColorStop(0, "rgba(0,0,0,1)"); g.addColorStop(.4, "rgba(0,0,0,.8)"); g.addColorStop(1, "rgba(0,0,0,0)");
1262
+ x.fillStyle = g; x.fillRect(0, 0, 64, 64); return HOLE = cv;
1263
+ }
1264
+ // a light: r = radius it clears the darkness, a = strength, ca = strength of its colored additive glow
1265
+ function pushL(x, y, r, c, a, ca) { if (LT.length < 220) LT.push({ x, y, r, c, a, ca }); }
1266
+ // ctx.filter is missing in some browsers and fails silently, so test that blur really works
1267
+ const FILTER_OK = (() => { try {
1268
+ const a = document.createElement("canvas"), b = document.createElement("canvas"); a.width = a.height = b.width = b.height = 9;
1269
+ const ag = a.getContext("2d"), bg = b.getContext("2d"); ag.fillStyle = "#fff"; ag.fillRect(4, 4, 1, 1);
1270
+ bg.filter = "blur(2px)"; bg.drawImage(a, 0, 0); return bg.getImageData(2, 4, 1, 1).data[3] > 0;
1271
+ } catch (e) { return false; } })();
1272
+ function ridgeF(R) { return [[2, 26], [5, 15], [11, 7], [23, 3]].map(v => [v[0], v[1] * (.7 + R() * .6), R() * 6.283]); }
1273
+ function ridgeY(x, rf) { let y = 0; for (const f of rf) y += Math.sin(6.2832 * f[0] * x / TW + f[2]) * f[1]; return y; }
1274
+ function farStruct(x, k, bx, by, s, P, R, col) {
1275
+ x.save(); x.translate(bx, by); x.scale(s, s);
1276
+ if (k === "fern") { // giant glowing alien mushroom trees
1277
+ const h = 90 + R() * 80; x.fillStyle = col;
1278
+ x.beginPath(); x.moveTo(-6, 0); x.quadraticCurveTo(-16, -h * .5, -3, -h); x.lineTo(3, -h); x.quadraticCurveTo(-6, -h * .5, 7, 0); x.fill();
1279
+ x.beginPath(); x.ellipse(0, -h, 46 + R() * 22, 18, 0, Math.PI, 0); x.fill();
1280
+ x.globalCompositeOperation = "lighter";
1281
+ for (let i = 0; i < 6; i++) { const gx = -36 + i * 14, gy = -h - 5 - R() * 9, g = x.createRadialGradient(gx, gy, 0, gx, gy, 11); g.addColorStop(0, rgba(P.a1, .75)); g.addColorStop(1, rgba(P.a1, 0)); x.fillStyle = g; x.fillRect(gx - 11, gy - 11, 22, 22); }
1282
+ } else if (k === "rock") { // volcano with lava streams and smoke
1283
+ const w = 120 + R() * 80, h = 90 + R() * 60; x.fillStyle = col;
1284
+ x.beginPath(); x.moveTo(-w, 0); x.lineTo(-18, -h); x.lineTo(18, -h); x.lineTo(w, 0); x.fill();
1285
+ x.fillStyle = "rgba(40,18,18,.35)"; for (let i = 0; i < 6; i++) { x.beginPath(); x.arc(-8 + i * 7 + R() * 10, -h - 22 - i * 20, 14 + i * 6, 0, 6.283); x.fill(); }
1286
+ x.globalCompositeOperation = "lighter";
1287
+ x.strokeStyle = rgba("#ff7b00", .85); x.lineWidth = 3; x.beginPath(); x.moveTo(-6, -h); x.quadraticCurveTo(-22, -h * .5, -36, 0); x.stroke();
1288
+ x.beginPath(); x.moveTo(8, -h); x.quadraticCurveTo(26, -h * .4, 30, 0); x.stroke();
1289
+ const g = x.createRadialGradient(0, -h, 0, 0, -h, 70); g.addColorStop(0, rgba("#ff7b00", .65)); g.addColorStop(1, rgba("#ff7b00", 0)); x.fillStyle = g; x.fillRect(-70, -h - 70, 140, 140);
1290
+ } else if (k === "ice") { // glacier spires
1291
+ for (let i = 0; i < 3; i++) {
1292
+ const ox = -30 + i * 30 + R() * 10, h = 70 + R() * 110, w = 14 + R() * 10;
1293
+ x.fillStyle = col; x.beginPath(); x.moveTo(ox - w, 0); x.lineTo(ox, -h); x.lineTo(ox + w, 0); x.fill();
1294
+ x.fillStyle = "rgba(255,255,255,.22)"; x.beginPath(); x.moveTo(ox - 1, -h + 6); x.lineTo(ox - w * .6, 0); x.lineTo(ox - 1, 0); x.fill();
1295
+ }
1296
+ } else if (k === "crystal") { // giant crystal towers with glowing edges
1297
+ for (let i = 0; i < 3; i++) {
1298
+ const ox = -30 + i * 28 + R() * 10, h = 80 + R() * 120, w = 12 + R() * 10;
1299
+ x.save(); x.translate(ox, 0); x.rotate((R() - .5) * .3); x.fillStyle = col;
1300
+ x.beginPath(); x.moveTo(-w, 0); x.lineTo(-w * .7, -h * .8); x.lineTo(0, -h); x.lineTo(w * .7, -h * .8); x.lineTo(w, 0); x.closePath(); x.fill();
1301
+ x.globalCompositeOperation = "lighter"; x.strokeStyle = rgba(P.a1, .55); x.lineWidth = 1.5; x.stroke(); x.restore();
1302
+ }
1303
+ } else { // organic hive towers with lit windows
1304
+ const h = 110 + R() * 90; x.fillStyle = col;
1305
+ x.beginPath(); x.moveTo(-26, 0); x.bezierCurveTo(-36, -h * .4, -8, -h * .6, -12, -h); x.quadraticCurveTo(0, -h - 22, 12, -h); x.bezierCurveTo(8, -h * .6, 36, -h * .4, 26, 0); x.fill();
1306
+ x.globalCompositeOperation = "lighter"; for (let i = 0; i < 8; i++) { x.fillStyle = rgba(P.a1, .45 + R() * .45); x.fillRect(-10 + R() * 18, -8 - h * R() * .9, 3, 4); }
1307
+ }
1308
+ x.restore();
1309
+ }
1310
+ function fgStruct(x, k, bx, by, R, col, P) {
1311
+ x.save(); x.translate(bx, by); x.fillStyle = col;
1312
+ if (k === "fern") { // big dark leaves
1313
+ const n = 3 + Math.floor(R() * 3);
1314
+ for (let i = 0; i < n; i++) {
1315
+ const a = -Math.PI / 2 + (i - (n - 1) / 2) * .45 + (R() - .5) * .3, len = 50 + R() * 55, tx = Math.cos(a) * len, ty = Math.sin(a) * len, nx = -Math.sin(a) * 12, ny = Math.cos(a) * 12;
1316
+ x.beginPath(); x.moveTo(0, 0); x.quadraticCurveTo(tx * .5 + nx, ty * .5 + ny, tx, ty); x.quadraticCurveTo(tx * .5 - nx, ty * .5 - ny, 0, 0); x.fill();
1317
+ }
1318
+ } else if (k === "crystal" || k === "ice") { // shards
1319
+ for (let i = 0; i < 3; i++) {
1320
+ const ox = (i - 1) * 16, h = 30 + R() * 60, w = 8 + R() * 8; x.beginPath(); x.moveTo(ox - w, 0); x.lineTo(ox, -h); x.lineTo(ox + w, 0); x.fill();
1321
+ x.strokeStyle = rgba(k === "ice" ? "#e0fbfc" : P.a1, .35); x.lineWidth = 1.2; x.stroke();
1322
+ }
1323
+ } else { // jagged rocks / thorns
1324
+ x.beginPath(); x.moveTo(-50, 0);
1325
+ for (let i = 0; i <= 8; i++) x.lineTo(-50 + i * 12.5, -(i % 2 ? 18 + R() * 40 : 6 + R() * 12));
1326
+ x.lineTo(50, 0); x.fill();
1327
+ if (k === "rock") { x.globalCompositeOperation = "lighter"; x.strokeStyle = rgba("#ff7b00", .5); x.lineWidth = 1.5; x.beginPath(); x.moveTo(-20, -4); x.lineTo(-8, -18); x.lineTo(4, -10); x.stroke(); }
1328
+ }
1329
+ x.restore();
1330
+ }
1331
+ function buildBG(pi) {
1332
+ const lv = level(pi), P = lv.P, k = P.deco, R = rng(pi * 131 + 7);
1333
+ const pickR = a => a[Math.floor(R() * a.length)];
1334
+ // sky: gradient, nebulae, stars, horizon glow
1335
+ const [sky, s] = mkC(W, H, 1.5);
1336
+ let gr = s.createLinearGradient(0, 0, 0, H); gr.addColorStop(0, P.sky[0]); gr.addColorStop(.65, P.sky[1]); gr.addColorStop(1, mix(P.sky[1], P.top, .25));
1337
+ s.fillStyle = gr; s.fillRect(0, 0, W, H);
1338
+ s.globalCompositeOperation = "lighter";
1339
+ for (let i = 0; i < 16; i++) { const x = R() * W, y = R() * H * .6, r = 80 + R() * 220, col = pickR([P.a1, P.a2, P.top, P.moon]), g2 = s.createRadialGradient(x, y, 0, x, y, r); g2.addColorStop(0, rgba(col, .08 + R() * .08)); g2.addColorStop(1, rgba(col, 0)); s.fillStyle = g2; s.fillRect(x - r, y - r, 2 * r, 2 * r); }
1340
+ for (let i = 0; i < 280; i++) { const sz = R() < .95 ? R() * 1.2 + .3 : R() * 2 + 1.2; s.fillStyle = "rgba(255,255,255," + (R() * .6 + .1) + ")"; s.fillRect(R() * W, R() * H * .7, sz, sz); }
1341
+ gr = s.createLinearGradient(0, H * .45, 0, H * .85); gr.addColorStop(0, rgba(P.top, 0)); gr.addColorStop(1, rgba(P.top, .2)); s.fillStyle = gr; s.fillRect(0, H * .45, W, H * .55);
1342
+ s.globalCompositeOperation = "source-over";
1343
+ // moon: lit sphere with atmosphere, craters, terminator and rings
1344
+ const [moon, m] = mkC(320, 320); const mc = 160, mr = 70;
1345
+ let g3 = m.createRadialGradient(mc, mc, mr * .8, mc, mc, mr * 1.7); g3.addColorStop(0, rgba(P.moon, .35)); g3.addColorStop(1, rgba(P.moon, 0)); m.fillStyle = g3; m.fillRect(0, 0, 320, 320);
1346
+ if (P.ring) { m.strokeStyle = rgba(mix(P.moon, "#ffffff", .4), .5); m.lineWidth = 6; m.beginPath(); m.ellipse(mc, mc, 130, 22, -.25, Math.PI, 2 * Math.PI); m.stroke(); }
1347
+ g3 = m.createRadialGradient(mc - 25, mc - 28, 5, mc, mc, mr); g3.addColorStop(0, mix(P.moon, "#ffffff", .45)); g3.addColorStop(.55, P.moon); g3.addColorStop(1, mix(P.moon, "#000000", .55));
1348
+ m.fillStyle = g3; m.beginPath(); m.arc(mc, mc, mr, 0, 6.283); m.fill();
1349
+ m.save(); m.beginPath(); m.arc(mc, mc, mr, 0, 6.283); m.clip();
1350
+ for (let i = 0; i < 9; i++) { const cx = mc + (R() - .5) * 110, cy = mc + (R() - .5) * 110, cr = 4 + R() * 13; m.fillStyle = "rgba(0,0,0,.16)"; m.beginPath(); m.arc(cx, cy, cr, 0, 6.283); m.fill(); m.strokeStyle = "rgba(255,255,255,.12)"; m.lineWidth = 1.5; m.beginPath(); m.arc(cx + 1, cy + 1, cr, 3.6, 5.6); m.stroke(); }
1351
+ g3 = m.createRadialGradient(mc + 60, mc + 50, 10, mc + 40, mc + 30, mr * 1.4); g3.addColorStop(0, "rgba(0,0,0,.65)"); g3.addColorStop(1, "rgba(0,0,0,0)"); m.fillStyle = g3; m.fillRect(0, 0, 320, 320);
1352
+ m.restore();
1353
+ if (P.ring) { m.strokeStyle = rgba(mix(P.moon, "#ffffff", .4), .65); m.lineWidth = 6; m.beginPath(); m.ellipse(mc, mc, 130, 22, -.25, 0, Math.PI); m.stroke(); }
1354
+ // far layer: ridge + landmark structures + atmospheric haze (tiles seamlessly every 1600 px)
1355
+ const FH = 300, [far, f] = mkC(TW, FH), rf = ridgeF(R), fcol = mix(P.far, P.sky[1], .25);
1356
+ gr = f.createLinearGradient(0, 60, 0, FH); gr.addColorStop(0, mix(fcol, P.top, .12)); gr.addColorStop(1, P.far);
1357
+ f.fillStyle = gr; f.beginPath(); f.moveTo(0, FH); for (let x = 0; x <= TW; x += 8) f.lineTo(x, 120 + ridgeY(x, rf)); f.lineTo(TW, FH); f.closePath(); f.fill();
1358
+ for (let i = 0; i < 8; i++) { const bx = R() * TW, sc = .8 + R() * .5, seed = R() * 1e6; for (const o of [-TW, 0, TW]) farStruct(f, k, bx + o, 126 + ridgeY(((bx % TW) + TW) % TW, rf), sc, P, rng(seed), mix(fcol, "#000000", .1)); }
1359
+ f.globalCompositeOperation = "source-atop"; gr = f.createLinearGradient(0, 0, 0, FH); gr.addColorStop(0, rgba(P.sky[1], .15)); gr.addColorStop(1, rgba(P.sky[1], .6)); f.fillStyle = gr; f.fillRect(0, 0, TW, FH); f.globalCompositeOperation = "source-over";
1360
+ // mid layer: closer hills with rim light
1361
+ const MH = 200, [mid, md] = mkC(TW, MH), rm = ridgeF(R), mcol = P.mid;
1362
+ for (let i = 0; i < 6; i++) { const bx = R() * TW, seed = R() * 1e6; for (const o of [-TW, 0, TW]) farStruct(md, k, bx + o, 80 + ridgeY(((bx % TW) + TW) % TW, rm), .55 + R() * .2, P, rng(seed), mix(mcol, "#000000", .15)); }
1363
+ gr = md.createLinearGradient(0, 40, 0, MH); gr.addColorStop(0, mix(mcol, P.top, .12)); gr.addColorStop(1, mix(mcol, "#000000", .35));
1364
+ md.fillStyle = gr; md.beginPath(); md.moveTo(0, MH); for (let x = 0; x <= TW; x += 8) md.lineTo(x, 76 + ridgeY(x, rm)); md.lineTo(TW, MH); md.closePath(); md.fill();
1365
+ md.strokeStyle = rgba(P.top, .35); md.lineWidth = 2; md.beginPath(); for (let x = 0; x <= TW; x += 8) { const y = 76 + ridgeY(x, rm); x ? md.lineTo(x, y) : md.moveTo(x, y); } md.stroke();
1366
+ md.globalCompositeOperation = "source-atop"; gr = md.createLinearGradient(0, 0, 0, MH); gr.addColorStop(0, rgba(P.sky[1], .1)); gr.addColorStop(1, rgba(P.sky[1], .3)); md.fillStyle = gr; md.fillRect(0, 0, TW, MH); md.globalCompositeOperation = "source-over";
1367
+ // ground texture: speckles, pebbles, strata (seamless 256 px tile)
1368
+ const [tex, t] = mkC(256, 256);
1369
+ t.fillStyle = P.gr; t.fillRect(0, 0, 256, 256);
1370
+ for (let i = 0; i < 3; i++) { const yy = 40 + i * 80 + R() * 20, ph = R() * 6; t.strokeStyle = "rgba(0,0,0,.14)"; t.lineWidth = 3 + R() * 4; t.beginPath(); for (let x = 0; x <= 256; x += 8) { const y = yy + Math.sin(6.2832 * 2 * x / 256 + ph) * 6; x ? t.lineTo(x, y) : t.moveTo(x, y); } t.stroke(); }
1371
+ for (let i = 0; i < 1100; i++) { t.fillStyle = R() < .5 ? "rgba(0,0,0," + (R() * .18) + ")" : "rgba(255,255,255," + (R() * .07) + ")"; t.fillRect(R() * 256, R() * 256, 1 + R() * 2, 1 + R() * 2); }
1372
+ for (let i = 0; i < 16; i++) { const x = 12 + R() * 232, y = 12 + R() * 232, rx = 3 + R() * 7, ry = rx * (.5 + R() * .4); t.fillStyle = mix(P.gr, "#000000", .35); t.beginPath(); t.ellipse(x, y, rx, ry, R(), 0, 6.283); t.fill(); t.fillStyle = rgba(mix(P.gr, "#ffffff", .35), .5); t.beginPath(); t.ellipse(x - rx * .25, y - ry * .35, rx * .5, ry * .35, 0, 0, 6.283); t.fill(); }
1373
+ // foreground silhouettes (parallax faster than the world for depth)
1374
+ const FG = 130, [fg, fx] = mkC(TW, FG), fcol2 = mix(P.gr, "#000000", .7);
1375
+ for (let i = 0; i < 7; i++) { const bx = R() * TW, seed = R() * 1e6; for (const o of [-TW, 0, TW]) fgStruct(fx, k, bx + o, FG + 6, rng(seed), fcol2, P); }
1376
+ // vignette
1377
+ const [vig, v] = mkC(W, H, 1), gv = v.createRadialGradient(W / 2, H / 2, H * .42, W / 2, H / 2, W * .72);
1378
+ gv.addColorStop(0, "rgba(0,0,0,0)"); gv.addColorStop(1, "rgba(0,0,0,.6)"); v.fillStyle = gv; v.fillRect(0, 0, W, H);
1379
+ // ambient motes: spores, embers, snow, sparkles, ash
1380
+ const motes = []; for (let i = 0; i < 46; i++) motes.push({ x: R() * W, y: R() * H, z: .3 + R() * .9, s: .6 + R() * 1.6, ph: R() * 6.283 });
1381
+ BG = { pi, sky, moon, far, mid, tex, fg, vig, motes, pat: null };
1382
+ }
1383
+ function ensureBG(pi, c) {
1384
+ if (BG.pi !== pi) buildBG(pi);
1385
+ if (!BG.pat) { try { BG.pat = c.createPattern(BG.tex, "repeat"); if (BG.pat && BG.pat.setTransform && typeof DOMMatrix !== "undefined") BG.pat.setTransform(new DOMMatrix().scale(1 / RES)); } catch (e) { BG.pat = null; } }
1386
+ }
1387
+ function tileLayer(c, img, par, y, h, cx) { const o = -(((cx * par) % TW) + TW) % TW; c.drawImage(img, o, y, TW, h); c.drawImage(img, o + TW, y, TW, h); }
1388
+ const MOTE = [
1389
+ { c: "#b5e48c", vx: 6, vy: -10, add: 1 }, { c: "#ff9f1c", vx: 10, vy: -34, add: 1 }, { c: "#ffffff", vx: -14, vy: 28, add: 1 },
1390
+ { c: "#f72585", vx: 4, vy: -6, add: 1 }, { c: "#ff595e", vx: -8, vy: 18, add: 1 }
1391
+ ];
1392
+ function drawMotes(c, pi, cx) {
1393
+ const M = MOTE[pi % 5]; c.save(); c.globalCompositeOperation = "lighter"; const tex = glowTex(M.c);
1394
+ for (const m of BG.motes) {
1395
+ const x = ((m.x - cx * m.z * .5 + (M.vx + Math.sin(T * .7 + m.ph) * 8) * T * m.z) % W + W) % W, y = ((m.y + M.vy * T * m.z) % H + H) % H, r = m.s * 4 * m.z;
1396
+ c.globalAlpha = (.35 + .35 * Math.sin(T * 2 + m.ph)) * m.z; c.drawImage(tex, x - r, y - r, r * 2, r * 2);
1397
+ }
1398
+ c.restore();
1399
+ }
1400
+ function lightPass(c, pi, sx, sy) {
1401
+ const LS = .5, lw = W * LS, lh = H * LS;
1402
+ if (!lightC) { lightC = document.createElement("canvas"); lightC.width = lw; lightC.height = lh; lightX = lightC.getContext("2d"); }
1403
+ const x = lightX, amb = AMB[pi % 5], base = mix(PL[pi % 5].sky[0], "#000000", .55), ht = holeTex();
1404
+ x.globalCompositeOperation = "source-over"; x.globalAlpha = 1; x.clearRect(0, 0, lw, lh);
1405
+ const gr = x.createLinearGradient(0, 0, 0, lh); gr.addColorStop(0, rgba(base, amb * .35)); gr.addColorStop(.6, rgba(base, amb)); gr.addColorStop(1, rgba(base, Math.min(.85, amb * 1.15)));
1406
+ x.fillStyle = gr; x.fillRect(0, 0, lw, lh);
1407
+ x.globalCompositeOperation = "destination-out";
1408
+ for (const L of LT) { const X = (L.x - camX + sx) * LS, Y = (L.y + sy) * LS, r = L.r * LS; if (X < -r || X > lw + r || Y < -r || Y > lh + r) continue; x.globalAlpha = clamp(L.a, 0, 1); x.drawImage(ht, X - r, Y - r, r * 2, r * 2); }
1409
+ x.globalAlpha = 1; x.globalCompositeOperation = "source-over";
1410
+ c.drawImage(lightC, 0, 0, W, H);
1411
+ }
1412
+ function glowPass(c, sx, sy) {
1413
+ c.save(); c.globalCompositeOperation = "lighter";
1414
+ for (const L of LT) { if (!L.ca) continue; const X = L.x - camX + sx, Y = L.y + sy, r = L.r * .6; if (X < -r || X > W + r || Y < -r || Y > H + r) continue; c.globalAlpha = clamp(L.ca, 0, 1); c.drawImage(glowTex(L.c), X - r, Y - r, r * 2, r * 2); }
1415
+ c.restore();
1416
+ }
1417
+ function bloom(c, a) {
1418
+ const cv = c.canvas; if (!cv || !cv.width) return;
1419
+ const bw = Math.max(1, cv.width >> 2), bh = Math.max(1, cv.height >> 2);
1420
+ if (!bloomC) { bloomC = document.createElement("canvas"); bloomX = bloomC.getContext("2d"); }
1421
+ if (bloomC.width !== bw || bloomC.height !== bh) { bloomC.width = bw; bloomC.height = bh; }
1422
+ bloomX.globalCompositeOperation = "copy"; bloomX.filter = "brightness(0.7) contrast(2.8) blur(3px)"; bloomX.drawImage(cv, 0, 0, bw, bh); bloomX.filter = "none";
1423
+ c.save(); c.setTransform(1, 0, 0, 1, 0, 0); c.globalCompositeOperation = "lighter"; c.globalAlpha = a; c.drawImage(bloomC, 0, 0, cv.width, cv.height); c.restore();
1424
+ }
1425
+ // quality governor: if frames get slow for 2 s, drop bloom, then dynamic lighting
1426
+ function govern() {
1427
+ const tn = performance.now();
1428
+ if (lastDraw) { const ft = Math.min(60, tn - lastDraw); ftAvg += (ft - ftAvg) * .05; if (ftAvg > 26) { slowFor += ft; if (slowFor > 2000 && Q > 0) { Q--; slowFor = 0; ftAvg = 16; } } else slowFor = 0; }
1429
+ lastDraw = tn;
1430
+ }
1431
+ function shades(pal) { const k = pal.a1 + pal.a2; return SHC[k] || (SHC[k] = { a1L: mix(pal.a1, "#ffffff", .45), a1D: mix(pal.a1, "#000000", .4), a2L: mix(pal.a2, "#ffffff", .4), a2D: mix(pal.a2, "#000000", .55) }); }
1432
+ function bodyGrad(c, r, base, L, D) { const g = c.createRadialGradient(-r * .35, -r * .45, r * .1, 0, 0, r * 1.15); g.addColorStop(0, L); g.addColorStop(.5, base); g.addColorStop(1, D); return g; }
1433
+
1434
+ // ---------- Drawing: world ----------
1435
+ function drawBg(c, lv, cx, tm, pi) {
1436
+ const P = lv.P;
1437
+ c.drawImage(BG.sky, 0, 0, W, H);
1438
+ c.fillStyle = "#fff"; // a few live twinkling stars on top of the baked starfield
1439
+ for (const s of lv.stars) { const x = ((s.x - cx * .03) % 1600 + 1600) % 1600; if (x > W) continue; c.globalAlpha = .2 + .7 * Math.abs(Math.sin(tm * 1.5 + s.p)); c.fillRect(x, s.y, s.s, s.s); }
1440
+ c.globalAlpha = 1;
1441
+ const mx = 620 - cx * .02;
1442
+ c.drawImage(BG.moon, mx - 160, 95 - 160, 320, 320);
1443
+ c.globalAlpha = .6; circ(c, mx - 380 - cx * .01, 60, 14, mix(P.moon, "#ffffff", .5)); c.globalAlpha = 1;
1444
+ if (pi % 5 === 0 || pi % 5 === 2 || pi % 5 === 3) { // god rays
1445
+ c.save(); c.globalCompositeOperation = "lighter";
1446
+ for (let i = 0; i < 4; i++) {
1447
+ const bx = ((i * 270 - cx * .08) % 1100 + 1100) % 1100 - 160, sw = Math.sin(tm * .3 + i * 1.7) * 24;
1448
+ const g = c.createLinearGradient(0, 0, 0, H * .85); g.addColorStop(0, rgba(P.top, .085)); g.addColorStop(1, rgba(P.top, 0));
1449
+ c.fillStyle = g; c.beginPath(); c.moveTo(bx, 0); c.lineTo(bx + 55, 0); c.lineTo(bx + 230 + sw, H * .85); c.lineTo(bx + 120 + sw, H * .85); c.closePath(); c.fill();
1450
+ }
1451
+ c.restore();
1452
+ }
1453
+ tileLayer(c, BG.far, .18, H - 300, 300, cx);
1454
+ tileLayer(c, BG.mid, .42, H - 200, 200, cx);
1455
+ }
1456
+ // a pit: dark shaft with lit rock walls, crumbling lips, a faint planet-coloured glow far below and pebbles trickling in
1457
+ function drawPit(c, lv, h, tm) {
1458
+ const P = lv.P, xa = h.x, xb = h.x + h.w, ya = gnd(lv, xa), yb = gnd(lv, xb), top = Math.min(ya, yb);
1459
+ const pg = c.createLinearGradient(0, top, 0, H); pg.addColorStop(0, mix(P.gr, "#000000", .55)); pg.addColorStop(.35, mix(P.gr, "#000000", .85)); pg.addColorStop(1, "#000");
1460
+ c.beginPath(); c.moveTo(xa, ya); c.lineTo(xb, yb); c.lineTo(xb, H + 10); c.lineTo(xa, H + 10); c.closePath(); c.fillStyle = pg; c.fill();
1461
+ c.save(); c.globalCompositeOperation = "lighter";
1462
+ const gg = c.createLinearGradient(0, H - 70, 0, H); gg.addColorStop(0, rgba(P.a1, 0)); gg.addColorStop(1, rgba(P.a1, .22 + .06 * Math.sin(tm * 2 + xa)));
1463
+ c.fillStyle = gg; c.fillRect(xa, H - 70, h.w, 70); c.restore();
1464
+ // side walls in perspective, with rock strata
1465
+ for (const [x, y, d] of [[xa, ya, 1], [xb, yb, -1]]) {
1466
+ c.beginPath(); c.moveTo(x, y); c.lineTo(x + d * 14, y + 12); c.lineTo(x + d * 14, H + 10); c.lineTo(x, H + 10); c.closePath();
1467
+ const wg = c.createLinearGradient(0, y, 0, H); wg.addColorStop(0, mix(P.gr, "#000000", .25)); wg.addColorStop(1, mix(P.gr, "#000000", .8)); c.fillStyle = wg; c.fill();
1468
+ c.strokeStyle = "rgba(0,0,0,.35)"; c.lineWidth = 1.5; c.beginPath();
1469
+ for (let i = 1; i < 4; i++) { const sy = y + 12 + i * 15 + (hsh(x + i) % 6); c.moveTo(x, sy); c.lineTo(x + d * 14, sy + 3); }
1470
+ c.stroke();
1471
+ c.strokeStyle = INK; c.lineWidth = 4; c.beginPath(); c.moveTo(x, y); c.lineTo(x, H + 10); c.stroke();
1472
+ }
1473
+ // crumbling lips and falling pebbles
1474
+ for (const [x, y, d] of [[xa, ya, 1], [xb, yb, -1]]) { c.beginPath(); c.moveTo(x - d * 4, y - 1); c.lineTo(x + d * 6, y + 2); c.lineTo(x + d * 2, y + 8); c.closePath(); inked(c, mix(P.gr, "#ffffff", .1), 2); }
1475
+ if (Math.random() < .03) addP(xa + 6 + Math.random() * (h.w - 12), top + 6, rnd(-10, 10), 40, .9, mix(P.gr, "#ffffff", .2), 2.5, 0, null, 400);
1476
+ }
1477
+ function drawGround(c, lv, cx, tm) {
1478
+ const P = lv.P, x0 = Math.floor(cx / 20) * 20 - 20, x1 = cx + W + 40, k = P.deco;
1479
+ // solid stretches of ground on screen: pits cut gaps in it (lv.holes is sorted by x)
1480
+ const segs = []; let sa = x0;
1481
+ for (const h of lv.holes) { if (h.x + h.w < x0 || h.x > x1) continue; if (h.x > sa) segs.push([sa, h.x]); sa = Math.max(sa, h.x + h.w); }
1482
+ if (sa < x1) segs.push([sa, x1]);
1483
+ const walk = (s, off, fn) => { for (let x = s[0]; ; x = Math.min(s[1], Math.floor(x / 20) * 20 + 20)) { fn(x, gnd(lv, x) + off); if (x >= s[1]) break; } };
1484
+ const path = () => { c.beginPath(); for (const s of segs) { c.moveTo(s[0], H + 10); walk(s, 0, (x, y) => c.lineTo(x, y)); c.lineTo(s[1], H + 10); c.closePath(); } };
1485
+ path(); c.fillStyle = BG.pat || P.gr; c.fill();
1486
+ const dg = c.createLinearGradient(0, 345, 0, H); dg.addColorStop(0, "rgba(0,0,0,0)"); dg.addColorStop(1, "rgba(0,0,0,.6)");
1487
+ path(); c.fillStyle = dg; c.fill();
1488
+ for (const h of lv.holes) if (h.x + h.w > x0 && h.x < x1) drawPit(c, lv, h, tm);
1489
+ const line = (off) => { c.beginPath(); for (const s of segs) { let first = true; walk(s, off, (x, y) => { first ? c.moveTo(x, y) : c.lineTo(x, y); first = false; }); } };
1490
+ line(5); c.strokeStyle = mix(P.top, "#000000", .55); c.lineWidth = 10; c.stroke();
1491
+ line(1); c.strokeStyle = P.top; c.lineWidth = 5; c.stroke();
1492
+ line(-1); c.strokeStyle = "rgba(255,255,255,.35)"; c.lineWidth = 1.5; c.stroke();
1493
+ // surface detail: grass, embers, frost, crystal grit, thorns (one batched path)
1494
+ c.beginPath();
1495
+ for (let x = Math.floor(cx / 6) * 6 - 12; x <= x1; x += 6) {
1496
+ if (holeAt(lv, x, -3)) continue;
1497
+ const h = hsh(x) % 100, y = gnd(lv, x) - 1;
1498
+ if (k === "fern") { const sw = Math.sin(tm * 2 + x * .05) * 2.5; c.moveTo(x, y); c.lineTo(x + sw + (h % 5) - 2, y - 4 - (h % 9)); }
1499
+ else if (k === "spike" && h < 30) { c.moveTo(x - 2, y); c.lineTo(x, y - 4 - h % 6); c.lineTo(x + 2, y); }
1500
+ else if ((k === "crystal" || k === "ice") && h < 22) { c.moveTo(x - 2, y); c.lineTo(x, y - 3 - h % 7); c.lineTo(x + 2, y); }
1501
+ }
1502
+ if (k === "fern") { c.strokeStyle = mix(P.top, "#ffffff", .15); c.lineWidth = 1.5; c.stroke(); }
1503
+ else { c.fillStyle = k === "spike" ? "#2b0b0e" : k === "ice" ? "#e0fbfc" : "#f72585"; c.fill(); }
1504
+ if (k === "rock" || k === "ice") {
1505
+ for (let x = Math.floor(cx / 50) * 50; x <= x1; x += 50) {
1506
+ if (holeAt(lv, x, -16)) continue;
1507
+ const h = hsh(x * 7) % 100, y = gnd(lv, x) + 6 + h % 20;
1508
+ if (k === "rock" && h < 45) { c.strokeStyle = rgba("#ff7b00", .45 + .35 * Math.sin(tm * 2.5 + x)); c.lineWidth = 1.5; c.beginPath(); c.moveTo(x, y); c.lineTo(x + 8, y + 5); c.lineTo(x + 14, y + 2); c.stroke(); if (h < 20) pushL(x + 7, y, 40, "#ff7b00", .45, .25); }
1509
+ if (k === "ice" && h < 40) { c.globalAlpha = .4 + .6 * Math.abs(Math.sin(tm * 3 + x)); c.fillStyle = "#fff"; c.fillRect(x, gnd(lv, x) - 2, 2, 2); c.globalAlpha = 1; }
1510
+ }
1511
+ }
1512
+ }
1513
+ function drawDeco(c, lv, cx, tm) {
1514
+ const P = lv.P, k = P.deco;
1515
+ for (const d of lv.deco) {
1516
+ if (d.x < cx - 60 || d.x > cx + W + 60) continue;
1517
+ const dy = gnd(lv, d.x) + 3;
1518
+ if (k === "rock") pushL(d.x, dy - 12 * d.s, 55 * d.s, "#ff7b00", .5, .3);
1519
+ else if (k === "crystal") pushL(d.x, dy - 20 * d.s, 80 * d.s, "#f72585", .55, .35);
1520
+ else if (k === "fern" && d.k === 2) pushL(d.x, dy - 44 * d.s, 50, P.a1, .5, .4);
1521
+ else if (k === "spike" && d.k === 1) pushL(d.x + 18 * d.s, dy - 10 * d.s, 45, "#ff595e", .45, .35);
1522
+ c.save(); c.translate(d.x, dy); c.scale(d.s, d.s);
1523
+ if (k === "fern") {
1524
+ c.strokeStyle = d.k ? P.top : "#40916c"; c.lineWidth = 3;
1525
+ for (let i = -2; i <= 2; i++) { c.beginPath(); c.moveTo(0, 0); c.quadraticCurveTo(i * 8, -30, i * 16 + Math.sin(tm * 1.5 + d.x) * 3, -38 + Math.abs(i) * 8); c.stroke(); }
1526
+ if (d.k === 2) { c.globalAlpha = .5 + .3 * Math.sin(tm * 3 + d.x); circ(c, 0, -44, 5, P.a1); c.globalAlpha = 1; }
1527
+ } else if (k === "rock") {
1528
+ c.fillStyle = "#2b1b17"; c.beginPath(); c.moveTo(-18, 0); c.lineTo(-10, -22); c.lineTo(4, -28); c.lineTo(16, -12); c.lineTo(20, 0); c.fill();
1529
+ c.strokeStyle = "#ff7b00"; c.globalAlpha = .5 + .5 * Math.sin(tm * 3 + d.x); c.lineWidth = 2; c.beginPath(); c.moveTo(-6, -4); c.lineTo(0, -16); c.lineTo(6, -10); c.stroke(); c.globalAlpha = 1;
1530
+ } else if (k === "ice" || k === "crystal") {
1531
+ const col = k === "ice" ? "rgba(202,240,248,.85)" : (d.k ? "#f72585" : "#b5179e");
1532
+ if (k === "crystal") { c.globalAlpha = .18 + .12 * Math.sin(tm * 2 + d.x); circ(c, 0, -20, 28, "#f72585"); c.globalAlpha = 1; }
1533
+ c.fillStyle = col;
1534
+ for (const s of [[-8, 30, 7], [0, 46, 9], [9, 24, 6]]) { c.beginPath(); c.moveTo(s[0] - s[2], 0); c.lineTo(s[0], -s[1]); c.lineTo(s[0] + s[2], 0); c.fill(); }
1535
+ c.strokeStyle = "rgba(255,255,255,.6)"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(-1, -40); c.lineTo(-3, -6); c.stroke();
1536
+ } else {
1537
+ c.fillStyle = "#2b0b0e";
1538
+ for (const s of [[-10, 26], [0, 40], [10, 22]]) { c.beginPath(); c.moveTo(s[0] - 6, 0); c.lineTo(s[0], -s[1]); c.lineTo(s[0] + 6, 0); c.fill(); }
1539
+ if (d.k === 1) { c.fillStyle = "#3d0c11"; c.beginPath(); c.ellipse(18, -10, 10, 12, 0, 0, 6.283); c.fill(); c.globalAlpha = .4 + .4 * Math.sin(tm * 2 + d.x); circ(c, 18, -10, 4, "#ff595e"); c.globalAlpha = 1; }
1540
+ }
1541
+ c.restore();
1542
+ }
1543
+ }
1544
+ // ---------- Crashed dropship: the squad's ride, nose-first in the dirt at the start of every planet ----------
1545
+ // Side-view ship drawn in the same flat-colour + ink-outline style as the Kenney heroes, baked once into a 2x canvas.
1546
+ // Ship space: nose to the right, origin mid-hull; the sprite tilts it nose-down by WK_A.
1547
+ let WRECK = null;
1548
+ const WK_A = .14, WK_C = Math.cos(WK_A), WK_S = Math.sin(WK_A);
1549
+ function bakeWreck() {
1550
+ const [cv, x] = mkC(300, 170);
1551
+ const P2 = s => new Path2D(s);
1552
+ const part = (p, col, w) => { x.strokeStyle = INK; x.lineWidth = w || 5; x.lineJoin = "round"; x.stroke(p); x.fillStyle = col; x.fill(p); };
1553
+ x.translate(150, 118); x.rotate(WK_A);
1554
+ // landing leg, buckled under the tail
1555
+ x.lineCap = "round"; x.strokeStyle = INK; x.lineWidth = 9; x.beginPath(); x.moveTo(-74, 18); x.lineTo(-84, 32); x.lineTo(-90, 44); x.stroke();
1556
+ x.strokeStyle = "#6c757d"; x.lineWidth = 4.5; x.stroke();
1557
+ part(P2("M-102 42 L-78 44 L-80 48 L-104 47 Z"), "#495057", 3);
1558
+ // tail fin with a red tip, then the engine pod
1559
+ part(P2("M-58 -28 L-94 -70 L-76 -73 L-28 -30 Z"), "#dee2e6");
1560
+ part(P2("M-94 -70 L-76 -73 L-68 -63 L-86 -58 Z"), "#e63946", 3);
1561
+ part(P2("M-126 -16 Q-130 -2 -126 14 L-100 18 L-100 -20 Z"), "#495057");
1562
+ part(P2("M-130 -14 Q-136 -2 -130 12 L-124 12 Q-128 -2 -124 -14 Z"), "#212529", 3);
1563
+ // hull, then shading clipped to it: lit top edge, shaded belly, red squad stripe, hazard chevrons, soot
1564
+ const hull = P2("M-104 -14 C-100 -26 -70 -30 -30 -31 L40 -31 C78 -30 104 -14 110 4 C112 16 96 26 70 27 L-92 24 C-104 22 -110 6 -104 -14 Z");
1565
+ part(hull, "#e9ecef", 6);
1566
+ x.save(); x.clip(hull);
1567
+ x.fillStyle = "#ffffff"; x.fillRect(-110, -34, 230, 6);
1568
+ x.fillStyle = "#adb5bd"; x.fillRect(-110, 10, 230, 30);
1569
+ x.fillStyle = "#ced4da"; x.fillRect(-110, 6, 230, 4);
1570
+ x.fillStyle = "#e63946"; x.fillRect(-110, -9, 150, 8); x.fillStyle = "#b5232f"; x.fillRect(-110, -2, 150, 2);
1571
+ x.fillStyle = "#ffd60a"; x.fillRect(-110, -26, 20, 50);
1572
+ x.fillStyle = "#14121c"; for (let i = 0; i < 6; i++) { x.beginPath(); x.moveTo(-110 + i * 9 - 6, 24); x.lineTo(-110 + i * 9, 24); x.lineTo(-110 + i * 9 + 14, -26); x.lineTo(-110 + i * 9 + 8, -26); x.fill(); }
1573
+ x.strokeStyle = "rgba(14,12,24,.28)"; x.lineWidth = 1.5;
1574
+ for (const px of [-64, 8, 44]) { x.beginPath(); x.moveTo(px, -34); x.lineTo(px + 2, 30); x.stroke(); }
1575
+ x.fillStyle = "rgba(14,12,24,.35)"; for (let i = -84; i < 70; i += 12) { x.beginPath(); x.arc(i, -25, 1.2, 0, 6.283); x.fill(); }
1576
+ for (const s of [[96, 18, 46], [56, 10, 22], [-20, 26, 30]]) { const g = x.createRadialGradient(s[0], s[1], 0, s[0], s[1], s[2]); g.addColorStop(0, "rgba(20,14,12,.6)"); g.addColorStop(1, "rgba(20,14,12,0)"); x.fillStyle = g; x.fillRect(s[0] - s[2], s[1] - s[2], s[2] * 2, s[2] * 2); }
1577
+ x.restore();
1578
+ // torn hull breach (sparks and flames come out of it live)
1579
+ part(P2("M46 2 L52 -6 L56 2 L63 -4 L64 6 L72 8 L64 14 L66 21 L57 16 L50 22 L50 13 L42 10 Z"), "#1b1b2f", 3);
1580
+ x.strokeStyle = "#ff9f1c"; x.lineWidth = 1.5; x.beginPath(); x.moveTo(50, 13); x.lineTo(57, 16); x.lineTo(64, 14); x.stroke();
1581
+ // bubble canopy: glass gradient, cracks, a bright reflection streak
1582
+ const can = P2("M28 -30 C34 -54 80 -56 104 -14 C88 -22 60 -29 28 -30 Z");
1583
+ const gg = x.createLinearGradient(0, -52, 0, -16); gg.addColorStop(0, "#caf0f8"); gg.addColorStop(.45, "#4cc9f0"); gg.addColorStop(1, "#1d3557");
1584
+ part(can, gg, 5);
1585
+ x.save(); x.clip(can);
1586
+ x.strokeStyle = "rgba(255,255,255,.75)"; x.lineWidth = 1.2; x.beginPath(); x.moveTo(74, -40); x.lineTo(66, -32); x.lineTo(70, -26); x.moveTo(66, -32); x.lineTo(58, -34); x.moveTo(74, -40); x.lineTo(84, -36); x.stroke();
1587
+ x.fillStyle = "rgba(255,255,255,.7)"; x.beginPath(); x.moveTo(40, -36); x.quadraticCurveTo(52, -50, 70, -50); x.lineTo(68, -46); x.quadraticCurveTo(54, -45, 45, -34); x.fill();
1588
+ x.restore();
1589
+ // open side hatch with the boarding ramp down to the ground, warm light inside
1590
+ x.fillStyle = "#1b1b2f"; x.strokeStyle = INK; x.lineWidth = 4; rr(x, -34, -20, 32, 30, 5); x.stroke(); x.fill();
1591
+ x.fillStyle = "#ffb703"; x.fillRect(-30, 4, 24, 3); x.fillStyle = "rgba(255,183,3,.25)"; x.fillRect(-30, -16, 24, 20);
1592
+ part(P2("M-34 10 L-2 10 L-6 36 L-42 36 Z"), "#ced4da", 4);
1593
+ x.strokeStyle = "#868e96"; x.lineWidth = 2; for (let i = 1; i < 4; i++) { const t = i / 4; x.beginPath(); x.moveTo(-34 - 8 * t, 10 + 26 * t); x.lineTo(-2 - 4 * t, 10 + 26 * t); x.stroke(); }
1594
+ // near-side wing, snapped off half way (jagged edge)
1595
+ part(P2("M-58 6 L-92 28 L-80 34 L-72 27 L-68 33 L-62 26 L-56 31 L-38 12 Z"), "#ced4da", 4);
1596
+ x.fillStyle = "#e63946"; x.beginPath(); x.moveTo(-58, 6); x.lineTo(-92, 28); x.lineTo(-87, 30); x.lineTo(-54, 9); x.fill();
1597
+ // squad emblem and hull number
1598
+ x.beginPath(); x.arc(18, 4, 9, 0, 6.283); x.strokeStyle = INK; x.lineWidth = 3; x.stroke(); x.fillStyle = "#1d3557"; x.fill();
1599
+ x.fillStyle = "#ffd60a"; x.beginPath(); for (let i = 0; i < 10; i++) { const a = i / 10 * 6.283 - Math.PI / 2, r = i % 2 ? 2.6 : 6; x.lineTo(18 + Math.cos(a) * r, 4 + Math.sin(a) * r); } x.fill();
1600
+ x.fillStyle = "#1d3557"; x.font = "bold 10px sans-serif"; x.textAlign = "center"; x.fillText("SQ-1", -70, 16);
1601
+ // bent antenna
1602
+ x.strokeStyle = INK; x.lineWidth = 4; x.beginPath(); x.moveTo(-6, -31); x.lineTo(-2, -44); x.lineTo(10, -50); x.stroke();
1603
+ x.strokeStyle = "#adb5bd"; x.lineWidth = 2; x.stroke();
1604
+ return cv;
1605
+ }
1606
+ // ship space -> world (the sprite's ship origin sits 32 px above the ground at gx)
1607
+ function wkP(gx, gy, sx, sy) { return [gx + sx * WK_C - sy * WK_S, gy - 32 + sx * WK_S + sy * WK_C]; }
1608
+ function drawWreck(c, lv, tm) {
1609
+ if (!WRECK) WRECK = bakeWreck();
1610
+ const P = lv.P, gx = 135, gy = gnd(lv, gx);
1611
+ c.fillStyle = "rgba(0,0,0,.35)"; c.beginPath(); c.ellipse(gx - 10, gy + 2, 120, 7, 0, 0, 6.283); c.fill();
1612
+ c.drawImage(WRECK, gx - 150, gy - 150, 300, 170);
1613
+ // dirt ploughed up over the nose, with a few inked rocks
1614
+ c.beginPath(); c.moveTo(gx + 64, gy + 4); c.quadraticCurveTo(gx + 92, gy - 24, gx + 118, gy - 16); c.quadraticCurveTo(gx + 140, gy - 10, gx + 156, gy + 4); c.closePath();
1615
+ inked(c, BG.pat || P.gr, 3);
1616
+ c.strokeStyle = P.top; c.lineWidth = 3; c.beginPath(); c.moveTo(gx + 74, gy - 6); c.quadraticCurveTo(gx + 94, gy - 22, gx + 118, gy - 15); c.stroke();
1617
+ for (const r of [[gx + 150, gy - 1, 6], [gx + 168, gy, 4], [gx + 58, gy - 1, 5]]) { c.beginPath(); c.moveTo(r[0] - r[2], r[1] + 2); c.lineTo(r[0] - r[2] * .6, r[1] - r[2]); c.lineTo(r[0] + r[2] * .7, r[1] - r[2] * .8); c.lineTo(r[0] + r[2], r[1] + 2); c.closePath(); inked(c, mix(P.mid, "#000000", .2), 2); }
1618
+ // live bits: engine smoke, breach fire, wing sparks, tail beacon, hatch light
1619
+ const [ex, ey] = wkP(gx, gy, -132, -2), [bx, by] = wkP(gx, gy, 57, 8), [wx, wy] = wkP(gx, gy, -70, 30), [fx, fy] = wkP(gx, gy, -86, -70), [hx, hy] = wkP(gx, gy, -18, -4);
1620
+ if (Math.random() < .3) addP(ex + rnd(-3, 3), ey, rnd(-22, -6), rnd(-46, -28), 2.4, "#5c5c66", rnd(6, 10), 4);
1621
+ if (Math.random() < .22) addP(bx, by - 4, rnd(-8, 8), rnd(-52, -34), 1.8, "#3a3440", rnd(5, 8), 4);
1622
+ if (Math.random() < .035) for (let i = 0; i < 6; i++) addP(wx, wy, rnd(-90, 30), rnd(-140, -40), rnd(.3, .6), pick(["#ffd60a", "#fff3b0"]), rnd(2, 3.5), 0, null, 650);
1623
+ const fl = .75 + .25 * Math.sin(tm * 23) * Math.sin(tm * 7.3);
1624
+ c.save(); c.globalCompositeOperation = "lighter";
1625
+ for (let i = 0; i < 3; i++) { const s = (9 - i * 2.5) * fl; c.drawImage(glowTex(i ? "#ffba08" : "#ff7b00"), bx - s + i * 2 - 2, by - 6 - s - i * 4, s * 2, s * 2); }
1626
+ const on = (tm % 1.4) < .18;
1627
+ if (on) c.drawImage(glowTex("#ff4d6d"), fx - 12, fy - 12, 24, 24);
1628
+ c.restore();
1629
+ ic(c, fx, fy, 3, on ? "#fff" : "#9d0208", 2);
1630
+ pushL(bx, by, 70 * fl, "#ff7b00", .6, .45); pushL(hx, hy, 60, "#ffb703", .45, .3);
1631
+ if (on) pushL(fx, fy, 60, "#ff4d6d", .5, .6);
1632
+ }
1633
+ function drawProps(c, lv, cx, tm) {
1634
+ const P = lv.P;
1635
+ if (cx < 420) drawWreck(c, lv, tm);
1636
+ if (cx > GATE - 800) { // boss gate: two inked pylons with an arc of energy between them
1637
+ const ga = gnd(lv, GATE), gb = gnd(lv, GATE + 40), pulse = .55 + .45 * Math.sin(tm * 4);
1638
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .25 + .2 * pulse; c.strokeStyle = "#ff4d6d"; c.lineWidth = 3;
1639
+ c.beginPath(); for (let i = 0; i <= 8; i++) { const t = i / 8, yy = ga - 128 + (gb - ga) * t + (i % 8 ? (hsh(Math.floor(tm * 12) + i) % 9 - 4) : 0); i ? c.lineTo(GATE + 40 * t, yy) : c.moveTo(GATE, yy); } c.stroke(); c.restore();
1640
+ for (const gx of [GATE, GATE + 40]) {
1641
+ const gy = gnd(lv, gx);
1642
+ c.beginPath(); c.moveTo(gx - 13, gy); c.lineTo(gx - 8, gy - 116); c.lineTo(gx + 8, gy - 116); c.lineTo(gx + 13, gy); c.closePath(); inked(c, "#343a40", 3.5);
1643
+ c.fillStyle = "#495057"; c.fillRect(gx - 7, gy - 112, 5, 108);
1644
+ for (let i = 0; i < 5; i++) { rr(c, gx - 9, gy - 104 + i * 21, 18, 7, 2); inked(c, i % 2 ? P.top : mix(P.top, "#000000", .3), 2); }
1645
+ rr(c, gx - 11, gy - 124, 22, 10, 3); inked(c, "#212529", 3);
1646
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = pulse; c.drawImage(glowTex("#ff4d6d"), gx - 18, gy - 146, 36, 36); c.restore();
1647
+ ic(c, gx, gy - 128, 6, "#ff8fa3", 2.5); circ(c, gx - 1.5, gy - 129.5, 2, "#fff");
1648
+ pushL(gx, gy - 128, 70, "#ff4d6d", .5, .5 * pulse);
1649
+ }
1650
+ }
1651
+ }
1652
+ function drawLoot(c, l, tm, now) {
1653
+ if (l.e - now < 5 && Math.floor(tm * 8) % 2) return;
1654
+ const x = l.x, y = l.y + Math.sin(tm * 3 + l.x) * 3, k = l.k, col = itemColor(k);
1655
+ c.globalAlpha = .22; circ(c, x, y, 21 + Math.sin(tm * 4) * 2, col); c.globalAlpha = 1;
1656
+ c.textAlign = "center"; c.textBaseline = "middle";
1657
+ if (WP[k]) { // supply crate: steel box, lit lid, weapon-coloured band, white letter badge
1658
+ rr(c, x - 15, y - 12, 30, 24, 4); inked(c, "#5c677d", 3);
1659
+ c.fillStyle = "#7d8597"; c.fillRect(x - 12.5, y - 9.5, 25, 5);
1660
+ c.fillStyle = col; c.fillRect(x - 13.5, y - 1, 27, 5);
1661
+ c.fillStyle = "#33415c"; for (const sx of [-10.5, 10.5]) for (const sy of [-7.5, 8]) { c.beginPath(); c.arc(x + sx, y + sy, 1.3, 0, 6.283); c.fill(); }
1662
+ ic(c, x, y + 1.5, 7.5, "#f8f9fa", 2.5);
1663
+ c.fillStyle = "#14121c"; c.font = "bold 10px sans-serif"; c.fillText(k, x, y + 2);
1664
+ } else if (SP[k] && SP[k].ally) { // helper toy: a wobbling capsule, coloured dome on a white base, the helper's icon on the seam
1665
+ c.save(); c.translate(x, y); c.rotate(Math.sin(tm * 2.5 + x) * .18);
1666
+ c.beginPath(); c.arc(0, 0, 12, Math.PI, 0); c.closePath(); inked(c, col, 3);
1667
+ c.beginPath(); c.arc(0, 0, 12, 0, Math.PI); c.closePath(); inked(c, "#f8f9fa", 3);
1668
+ c.fillStyle = "rgba(255,255,255,.6)"; c.beginPath(); c.ellipse(-4, -6, 4, 2.2, -.5, 0, 6.283); c.fill();
1669
+ c.font = "12px sans-serif"; c.fillText(SP[k].i, 0, 1);
1670
+ c.restore();
1671
+ const a = tm * 3; circ(c, x + Math.cos(a) * 17, y + Math.sin(a) * 7, 2, col);
1672
+ } else if (SP[k]) { // glass spell orb with a sparkle orbiting it
1673
+ const g = c.createRadialGradient(x - 4, y - 4, 1, x, y, 12); g.addColorStop(0, "#ffffff"); g.addColorStop(.35, col); g.addColorStop(1, mix(col, "#000000", .45));
1674
+ ic(c, x, y, 11, g, 3);
1675
+ c.fillStyle = "#14121c"; c.font = "bold 11px sans-serif"; c.fillText(SP[k].i, x, y + 1);
1676
+ const a = tm * 4; circ(c, x + Math.cos(a) * 16, y + Math.sin(a) * 6, 2, "#fff");
1677
+ } else if (k === "hp") { // medkit with a handle
1678
+ c.beginPath(); c.moveTo(x - 5, y - 10); c.lineTo(x - 5, y - 14); c.lineTo(x + 5, y - 14); c.lineTo(x + 5, y - 10); ink(c, 3); c.stroke();
1679
+ rr(c, x - 13, y - 10, 26, 20, 4); inked(c, "#f8f9fa", 3);
1680
+ c.fillStyle = "#dee2e6"; c.fillRect(x - 11.5, y + 4, 23, 4.5);
1681
+ c.beginPath(); c.rect(x - 3, y - 7, 6, 14); c.rect(x - 7, y - 3, 14, 6); inked(c, "#e63946", 2);
1682
+ } else { // XP star spins like a coin
1683
+ c.save(); c.translate(x, y); c.scale(Math.max(.25, Math.abs(Math.cos(tm * 2.5))), 1);
1684
+ const star = (ro, ri, ox, oy) => { c.beginPath(); for (let i = 0; i < 10; i++) { const a = i / 10 * 6.283 - Math.PI / 2, r = i % 2 ? ri : ro; c.lineTo(ox + Math.cos(a) * r, oy + Math.sin(a) * r); } c.closePath(); };
1685
+ star(13, 6, 0, 0); inked(c, "#ffd60a", 3);
1686
+ star(6.5, 3, -1, -1.5); c.fillStyle = "#fff3b0"; c.fill();
1687
+ c.restore();
1688
+ }
1689
+ c.textBaseline = "alphabetic";
1690
+ }
1691
+ // enemy shots: each boss weapon has its own look (kinds in EBK), all in the inked house style
1692
+ function drawEb(c, e, P, tm) {
1693
+ const k = e.k || 0, K = EBK[k], col = K.c || P.a1, x = e.x, y = e.y, an = Math.atan2(e.vy || 0, e.vx || 1);
1694
+ c.globalAlpha = .3; circ(c, x, y, k >= 9 ? 0 : k === 2 ? 18 : 10, col); c.globalAlpha = 1;
1695
+ if (k === 0) { ic(c, x, y, 5.5, col, 2.5); circ(c, x - 1, y - 1, 2, "#fff"); }
1696
+ else if (k === 1) { // acid glob: teardrop pointing back along its path
1697
+ c.save(); c.translate(x, y); c.rotate(an); c.beginPath(); c.moveTo(-14, 0); c.quadraticCurveTo(-4, -8, 6, -6); c.arc(4, 0, 6, -1.2, 1.2); c.quadraticCurveTo(-4, 8, -14, 0); inked(c, "#80b918", 2.5);
1698
+ circ(c, 5, -2, 2, "#d9ed92"); c.restore();
1699
+ } else if (k === 2) { // magma fireball
1700
+ c.save(); c.globalCompositeOperation = "lighter"; c.drawImage(glowTex("#ff7b00"), x - 22, y - 22, 44, 44); c.restore();
1701
+ ic(c, x, y, 9, "#ff9f1c", 3); circ(c, x - 2, y - 2, 4.5, "#fff3b0");
1702
+ if (Math.random() < .5) addP(x, y, rnd(-20, 20), rnd(-40, 0), .35, "#ff7b00", rnd(3, 5), 0);
1703
+ } else if (k === 3) { // meteor: inked rock with a fiery tail
1704
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .7; c.drawImage(glowTex("#ff9f1c"), x - 16 - e.vx * .05, y - 30, 32, 40); c.restore();
1705
+ c.save(); c.translate(x, y); c.rotate(tm * 3); c.beginPath(); c.moveTo(-10, -4); c.lineTo(-4, -11); c.lineTo(7, -9); c.lineTo(11, 2); c.lineTo(4, 10); c.lineTo(-8, 8); c.closePath(); inked(c, "#6c584c", 2.5);
1706
+ c.fillStyle = "#a98467"; c.fillRect(-4, -6, 5, 3); c.restore();
1707
+ } else if (k === 4) { // ice lance / icicle
1708
+ c.save(); c.translate(x, y); c.rotate(an); c.beginPath(); c.moveTo(14, 0); c.lineTo(-2, -5); c.lineTo(-14, 0); c.lineTo(-2, 5); c.closePath(); inked(c, "#caf0f8", 2.5);
1709
+ c.strokeStyle = "#fff"; c.lineWidth = 1.2; c.beginPath(); c.moveTo(10, 0); c.lineTo(-8, 0); c.stroke(); c.restore();
1710
+ } else if (k === 5) { // frost breath puff
1711
+ c.globalAlpha = .55; circ(c, x, y, 9 + Math.sin(tm * 9 + x) * 2, "#e0fbfc"); c.globalAlpha = .8; circ(c, x + 2, y - 2, 4, "#fff"); c.globalAlpha = 1;
1712
+ } else if (k === 6) { // spinning crystal shard
1713
+ c.save(); c.translate(x, y); c.rotate(tm * 8 + x * .01); c.beginPath(); c.moveTo(0, -10); c.lineTo(6, 0); c.lineTo(0, 10); c.lineTo(-6, 0); c.closePath(); inked(c, "#f72585", 2.5);
1714
+ c.fillStyle = "#ffc8dd"; c.beginPath(); c.moveTo(0, -8); c.lineTo(3, 0); c.lineTo(0, 0); c.closePath(); c.fill(); c.restore();
1715
+ } else if (k === 7) { // homing spore with little spikes
1716
+ const p = 1 + .15 * Math.sin(tm * 12 + x); c.save(); c.translate(x, y); c.scale(p, p);
1717
+ c.beginPath(); for (let i = 0; i < 8; i++) { const a = i / 8 * 6.283 + tm; c.moveTo(Math.cos(a) * 6, Math.sin(a) * 6); c.lineTo(Math.cos(a) * 11, Math.sin(a) * 11); } ink(c, 3); c.stroke(); c.strokeStyle = "#c77dff"; c.lineWidth = 1.5; c.stroke();
1718
+ ic(c, 0, 0, 7, "#7209b7", 2.5); circ(c, -2, -2, 2.5, "#e0aaff"); c.restore();
1719
+ } else if (k === 8) { // stinger
1720
+ c.save(); c.translate(x, y); c.rotate(an); c.beginPath(); c.moveTo(13, 0); c.lineTo(-8, -3.5); c.lineTo(-12, 0); c.lineTo(-8, 3.5); c.closePath(); inked(c, "#9d0208", 2);
1721
+ c.fillStyle = "#ff595e"; c.fillRect(-6, -1, 10, 2); c.restore();
1722
+ } else if (k === 9) { // slime shockwave rolling along the ground
1723
+ const gy = y + K.gw, s = Math.sign(e.vx || 1);
1724
+ c.beginPath(); c.moveTo(x - 28 * s, gy); c.quadraticCurveTo(x - 10 * s, gy - 30, x + 14 * s, gy - 22); c.quadraticCurveTo(x + 22 * s, gy - 10, x + 24 * s, gy); c.closePath(); inked(c, "rgba(158,240,26,.85)", 3);
1725
+ if (Math.random() < .4) addP(x, gy - 10, -s * rnd(20, 60), rnd(-90, -30), .35, "#9ef01a", 3, 0, null, 400);
1726
+ } else if (k === 10) { // lava wave: a tall rolling crest of fire
1727
+ const gy = y + K.gw, s = Math.sign(e.vx || 1), wob = Math.sin(tm * 20) * 4;
1728
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .8; c.drawImage(glowTex("#ff7b00"), x - 50, gy - 70, 100, 80); c.restore();
1729
+ c.beginPath(); c.moveTo(x - 40 * s, gy); c.quadraticCurveTo(x - 12 * s, gy - 46 + wob, x + 16 * s, gy - 40); c.quadraticCurveTo(x + 30 * s, gy - 18, x + 34 * s, gy); c.closePath(); inked(c, "#ff7b00", 3);
1730
+ c.beginPath(); c.moveTo(x - 24 * s, gy); c.quadraticCurveTo(x - 4 * s, gy - 28, x + 14 * s, gy - 24); c.quadraticCurveTo(x + 22 * s, gy - 10, x + 24 * s, gy); c.closePath(); c.fillStyle = "#ffd60a"; c.fill();
1731
+ if (Math.random() < .6) addP(x, gy - 30, -s * rnd(30, 90), rnd(-140, -60), .4, "#ffba08", rnd(3, 5), 0, null, 300);
1732
+ }
1733
+ pushL(x, y, k >= 9 ? 120 : k === 2 ? 90 : 55, col, .6, .6);
1734
+ }
1735
+ function drawShot(c, s, k, dx, dy) {
1736
+ const w = WP[k], x = s.x, y = s.y, r = s.r || w.r;
1737
+ if (k === "rifle" || k === "H" || k === "nail" || k === "L") {
1738
+ const len = k === "L" ? 42 : k === "nail" ? 10 : 18;
1739
+ c.strokeStyle = w.c; c.lineWidth = k === "L" ? 4 : r; c.lineCap = "round";
1740
+ c.beginPath(); c.moveTo(x - dx * len, y - dy * len); c.lineTo(x, y); c.stroke();
1741
+ if (k === "L") { c.strokeStyle = "#fff"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(x - dx * len, y - dy * len); c.lineTo(x, y); c.stroke(); }
1742
+ c.lineCap = "butt";
1743
+ } else if (k === "wave") {
1744
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1745
+ c.strokeStyle = "rgba(224,251,252,.9)"; c.lineWidth = 5; c.beginPath(); c.arc(-6, 0, 16, -1.1, 1.1); c.stroke();
1746
+ c.strokeStyle = "rgba(160,220,255,.5)"; c.lineWidth = 9; c.beginPath(); c.arc(-10, 0, 16, -1, 1); c.stroke();
1747
+ c.restore();
1748
+ } else if (k === "arrow") {
1749
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1750
+ c.strokeStyle = "#9c6644"; c.lineWidth = 2; c.beginPath(); c.moveTo(-18, 0); c.lineTo(2, 0); c.stroke();
1751
+ c.fillStyle = "#e9ecef"; c.beginPath(); c.moveTo(2, -3); c.lineTo(8, 0); c.lineTo(2, 3); c.fill();
1752
+ c.fillStyle = "#e63946"; c.fillRect(-19, -3, 5, 2); c.fillRect(-19, 1, 5, 2);
1753
+ c.restore();
1754
+ } else if (k === "R") {
1755
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1756
+ c.fillStyle = "#ff7b00"; c.beginPath(); c.moveTo(-10, -3); c.lineTo(-18 - Math.random() * 8, 0); c.lineTo(-10, 3); c.fill();
1757
+ c.fillStyle = "#adb5bd"; rr(c, -11, -3.5, 15, 7, 3); c.fill(); c.fillStyle = "#e63946"; c.beginPath(); c.moveTo(4, -3.5); c.lineTo(10, 0); c.lineTo(4, 3.5); c.fill();
1758
+ c.restore();
1759
+ if (Math.random() < .5) addP(x - dx * 14, y - dy * 14, rnd(-10, 10), rnd(-20, 0), .5, "rgba(150,150,150,1)", 5, 4);
1760
+ } else if (k === "F") {
1761
+ c.globalAlpha = .7; circ(c, x, y, r * (1 + Math.random() * .5), pick(["#ff6b35", "#ffba08", "#d00000"])); circ(c, x, y, r * .5, "#fff3b0"); c.globalAlpha = 1;
1762
+ } else if (k === "wave2") { // Cross Slash: two crossed pink crescents with a white edge
1763
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1764
+ c.globalCompositeOperation = "lighter";
1765
+ for (const s of [-1, 1]) {
1766
+ c.save(); c.rotate(s * .55);
1767
+ c.strokeStyle = "rgba(255,175,204,.55)"; c.lineWidth = 12; c.beginPath(); c.arc(-12, 0, 22, -1, 1); c.stroke();
1768
+ c.strokeStyle = "#fff"; c.lineWidth = 4; c.beginPath(); c.arc(-6, 0, 22, -1.1, 1.1); c.stroke();
1769
+ c.restore();
1770
+ }
1771
+ c.restore();
1772
+ if (Math.random() < .5) addP(x - dx * 10, y + rnd(-14, 14), -dx * 60, rnd(-30, 30), .25, "#ffafcc", 3, 0);
1773
+ } else if (k === "arrow2") { // Power Shot: a glowing arrow with a light trail
1774
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1775
+ c.globalCompositeOperation = "lighter"; c.globalAlpha = .6; c.drawImage(glowTex("#95d5b2"), -40, -8, 50, 16); c.globalAlpha = 1; c.globalCompositeOperation = "source-over";
1776
+ c.strokeStyle = "#e9f5db"; c.lineWidth = 2.5; c.beginPath(); c.moveTo(-20, 0); c.lineTo(3, 0); c.stroke();
1777
+ c.fillStyle = "#fff"; c.beginPath(); c.moveTo(2, -4.5); c.lineTo(11, 0); c.lineTo(2, 4.5); c.fill();
1778
+ c.fillStyle = "#52b788"; c.fillRect(-21, -4, 6, 3); c.fillRect(-21, 1, 6, 3);
1779
+ c.restore();
1780
+ } else if (k === "nail2") { // hot rivet: white-hot core, sparks fly off
1781
+ c.save(); c.globalCompositeOperation = "lighter"; c.drawImage(glowTex("#ff5400"), x - 14, y - 14, 28, 28); c.restore();
1782
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx)); rr(c, -8, -3, 13, 6, 2); inked(c, "#ff5400", 2); c.fillStyle = "#fff3b0"; c.fillRect(-5, -1, 8, 2); c.restore();
1783
+ if (Math.random() < .4) addP(x, y, rnd(-60, 60), rnd(-90, 10), .25, "#ffba08", 2, 0, null, 400);
1784
+ } else if (k === "Z") { // tesla orb with crackling arcs
1785
+ c.save(); c.globalCompositeOperation = "lighter"; c.drawImage(glowTex("#7df9ff"), x - 16, y - 16, 32, 32);
1786
+ c.strokeStyle = "#e0ffff"; c.lineWidth = 1.5; c.beginPath();
1787
+ for (let i = 0; i < 3; i++) { const a = Math.random() * 6.283; c.moveTo(x, y); c.lineTo(x + Math.cos(a) * 7 + rnd(-3, 3), y + Math.sin(a) * 7 + rnd(-3, 3)); c.lineTo(x + Math.cos(a) * 13, y + Math.sin(a) * 13); }
1788
+ c.stroke(); c.restore();
1789
+ circ(c, x, y, 4.5, "#7df9ff"); circ(c, x, y, 2, "#fff");
1790
+ } else if (k === "C") { // cryo shard: a spinning six-point snowflake leaving frost
1791
+ c.save(); c.translate(x, y); c.rotate(T * 9 + x * .02);
1792
+ c.strokeStyle = "#1b263b"; c.lineWidth = 5; c.lineCap = "round"; c.beginPath(); for (let i = 0; i < 3; i++) { const a = i * 1.047; c.moveTo(Math.cos(a) * 8, Math.sin(a) * 8); c.lineTo(-Math.cos(a) * 8, -Math.sin(a) * 8); } c.stroke();
1793
+ c.strokeStyle = "#caf0f8"; c.lineWidth = 2.5; c.stroke(); c.lineCap = "butt";
1794
+ circ(c, 0, 0, 2.5, "#fff"); c.restore();
1795
+ if (Math.random() < .35) addP(x, y, rnd(-20, 20), rnd(-10, 30), .4, "#e0fbfc", 2, 0);
1796
+ } else if (k === "M") { // micro missile: small yellow body, flickering flame, smoke puffs
1797
+ c.save(); c.translate(x, y); c.rotate(Math.atan2(dy, dx));
1798
+ c.fillStyle = "#ff7b00"; c.beginPath(); c.moveTo(-6, -2); c.lineTo(-11 - Math.random() * 6, 0); c.lineTo(-6, 2); c.fill();
1799
+ rr(c, -7, -2.5, 11, 5, 2.5); inked(c, "#ffbe0b", 1.5); c.fillStyle = "#e63946"; c.beginPath(); c.moveTo(4, -2.5); c.lineTo(8, 0); c.lineTo(4, 2.5); c.fill();
1800
+ c.restore();
1801
+ if (Math.random() < .5) addP(x - dx * 10, y - dy * 10, rnd(-8, 8), rnd(-15, 0), .4, "rgba(170,170,170,1)", 3.5, 4);
1802
+ } else if (k === "plasma") { // plasma orb with a pulsing ring
1803
+ const p = 1 + .2 * Math.sin(T * 30 + x * .05);
1804
+ c.globalAlpha = .3; circ(c, x, y, r * 2.1 * p, w.c); c.globalAlpha = 1;
1805
+ c.strokeStyle = "#e0fbfc"; c.lineWidth = 1.5; c.beginPath(); c.arc(x, y, r * 1.4 * p, 0, 6.283); c.stroke();
1806
+ circ(c, x, y, r, w.c); circ(c, x, y, r * .45, "#fff");
1807
+ } else {
1808
+ c.globalAlpha = .3; circ(c, x, y, r * 2, w.c); c.globalAlpha = 1; circ(c, x, y, r, w.c); circ(c, x, y, r * .45, "#fff");
1809
+ if (k === "bolt" && Math.random() < .5) addP(x - dx * 8, y - dy * 8, rnd(-30, 30), rnd(-30, 30), .3, "#e0aaff", 2.5, 0); // arcane sparkles
1810
+ }
1811
+ }
1812
+ function drawObj(c, o, x, y, tm, src, now) {
1813
+ if (o.t === "ltn") { drawLtn(c, o, now, tm); return; } // weather.js
1814
+ if (ALLY[o.t]) { drawAlly(c, o, x, y, tm, now); return; } // allies.js
1815
+ if (o.t === "pil") { // eruption: a glowing crack while it warns, then a column of fire
1816
+ const live = now > o.w, gy = o.y;
1817
+ c.save(); c.globalCompositeOperation = "lighter";
1818
+ if (!live) {
1819
+ const p = clamp(1 - (o.w - now) / .9, 0, 1);
1820
+ c.globalAlpha = .4 + .5 * Math.abs(Math.sin(tm * 18)); c.drawImage(glowTex("#ff7b00"), o.x - 30 - p * 10, gy - 16, 60 + p * 20, 26);
1821
+ if (Math.random() < .5) addP(o.x + rnd(-14, 14), gy - 4, rnd(-10, 10), rnd(-120, -60), .4, "#ffba08", 3, 0);
1822
+ } else {
1823
+ const h = 260, wob = Math.sin(tm * 30) * 4;
1824
+ const gr = c.createLinearGradient(0, gy - h, 0, gy); gr.addColorStop(0, "rgba(255,123,0,0)"); gr.addColorStop(.35, "rgba(255,123,0,.8)"); gr.addColorStop(1, "rgba(255,243,176,.95)");
1825
+ c.fillStyle = gr; c.beginPath(); c.moveTo(o.x - 26, gy); c.quadraticCurveTo(o.x - 34 + wob, gy - h * .5, o.x - 6, gy - h); c.lineTo(o.x + 6, gy - h); c.quadraticCurveTo(o.x + 34 - wob, gy - h * .5, o.x + 26, gy); c.closePath(); c.fill();
1826
+ c.globalAlpha = .8; c.fillStyle = "#fff3b0"; c.fillRect(o.x - 6, gy - h * .7, 12, h * .7);
1827
+ if (Math.random() < .6) addP(o.x + rnd(-20, 20), gy - rnd(0, 200), rnd(-30, 30), rnd(-200, -80), .5, "#ff7b00", rnd(3, 6), 0);
1828
+ }
1829
+ c.restore();
1830
+ pushL(o.x, gy - (live ? 120 : 10), live ? 200 : 70, "#ff7b00", live ? 1 : .5, live ? .8 : .4);
1831
+ return;
1832
+ }
1833
+ if (o.t === "laz") { // prism beam: thin aim line while it warns, then a sweeping beam with a white core
1834
+ const live = now > o.w, an = live ? o.a + (o.b - o.a) * clamp((now - o.w) / (o.e - o.w), 0, 1) : o.a, len = 900, ex = o.x + Math.cos(an) * len, ey = o.y + Math.sin(an) * len;
1835
+ c.save(); c.globalCompositeOperation = "lighter"; c.lineCap = "round";
1836
+ if (!live) { c.globalAlpha = .45 + .4 * Math.abs(Math.sin(tm * 20)); c.strokeStyle = "#ff4d6d"; c.lineWidth = 2; c.setLineDash([12, 10]); c.beginPath(); c.moveTo(o.x, o.y); c.lineTo(ex, ey); c.stroke(); c.setLineDash([]); }
1837
+ else {
1838
+ for (const [w, col, a] of [[34, "#f72585", .35], [18, "#ff70a6", .7], [6, "#ffffff", 1]]) { c.globalAlpha = a; c.strokeStyle = col; c.lineWidth = w + Math.sin(tm * 40) * 2; c.beginPath(); c.moveTo(o.x, o.y); c.lineTo(ex, ey); c.stroke(); }
1839
+ for (let i = 1; i <= 5; i++) pushL(o.x + Math.cos(an) * i * 160, o.y + Math.sin(an) * i * 160, 120, "#f72585", .7, .6);
1840
+ }
1841
+ c.restore();
1842
+ return;
1843
+ }
1844
+ if (o.t === "gr") { ic(c, x, y, 5.5, "#386641", 2.5); c.fillStyle = "#6a994e"; c.fillRect(x - 3, y - 3, 3, 2); ic(c, x + 1, y - 5, 2, Math.floor(tm * 10) % 2 ? "#ff0000" : "#550000", 1.5); }
1845
+ else if (o.t === "met") {
1846
+ c.globalAlpha = .35; circ(c, x, y, 18, "#ff7b00"); c.globalAlpha = 1; circ(c, x, y, 10, "#ffba08"); circ(c, x - 2, y - 2, 4, "#fff3b0");
1847
+ if (Math.random() < .7) addP(x, y, rnd(-30, 30), rnd(-60, -20), .4, pick(["#ff7b00", "#ffba08"]), rnd(3, 6), 0);
1848
+ } else if (o.t === "tur") {
1849
+ let a = o.f > 0 ? 0 : Math.PI, bd = 480 * 480;
1850
+ for (const id in src.mobs || {}) { const m = disp[id] || src.mobs[id], d2 = (m.x - x) ** 2 + (m.y - y + 26) ** 2; if (d2 < bd) { bd = d2; a = Math.atan2(m.y - (y - 26), m.x - x); } }
1851
+ c.beginPath(); c.moveTo(x, y - 18); c.lineTo(x - 12, y); c.moveTo(x, y - 18); c.lineTo(x + 12, y); c.moveTo(x, y - 18); c.lineTo(x, y);
1852
+ c.lineCap = "round"; ink(c, 6); c.stroke(); c.strokeStyle = "#6c757d"; c.lineWidth = 3; c.stroke(); c.lineCap = "butt";
1853
+ c.save(); c.translate(x, y - 26); c.rotate(a); c.beginPath(); c.rect(2, -3.5, 20, 7); inked(c, "#343a40", 2.5); c.fillStyle = "#6c757d"; c.fillRect(18, -3.5, 4, 7); c.restore();
1854
+ rr(c, x - 11, y - 33, 22, 15, 4); inked(c, "#f4a261", 3); c.fillStyle = "#e76f51"; c.fillRect(x - 9, y - 23, 18, 3);
1855
+ ic(c, x, y - 27, 3.5, Math.floor(tm * 4) % 2 ? "#4cc9f0" : "#fff", 2);
1856
+ }
1857
+ }
1858
+
1859
+ </script>
1860
+ <script>
1861
+ // Star Squad: hero art data. Kenney "Toon Characters 1" vector parts (CC0, www.kenney.nl), rigged in sprites.js
1862
+ "use strict";
1863
+ // ---------- Hero art: Kenney "Toon Characters 1" vector parts (CC0, www.kenney.nl), rigged in code ----------
1864
+ // Each part is a list of [fill, SVG path] layers in Kenney's part space (96x128 cell, feet at y=0 of the rig).
1865
+ const KN = {
1866
+ MP: { // Male person
1867
+ arm: [["#BF7958", "M 13.15 10.6 Q 11.9 7.15 9.95 3.9 4.35 5.25 -0.3 10.1 0.55 11.45 1.2 12.85 2.75 16.05 3.15 19.75 3.4 22.15 5.3 23.7 7.25 25.2 9.7 25 12.1 24.7 13.65 22.75 15.2 20.9 14.95 18.4 14.6 15.5 13.85 12.85 13.55 11.7 13.15 10.6 Z"],
1868
+ ["#E7F3FC", "M -0.2 -5 Q -2.65 -4.95 -4.4 -3.2 -6.1 -1.4 -6 1 -6 3.45 -4.3 5.15 -1.9 7.45 -0.3 10.1 4.35 5.25 9.95 3.9 7.5 0.05 4 -3.35 2.25 -4.95 -0.2 -5 Z"],
1869
+ ["#BFD4E5", "M 12.4 4.8 L 10.35 1.2 -2.55 8.7 -0.55 12.25 12.4 4.8 Z"]],
1870
+ leg: [["#4C4EAA", "M 5.3 14 L 5.45 9 -6.45 9 -6.6 14 5.25 14 5.3 14 Z"],
1871
+ ["#FFFFFF", "M 11 20 L 5.25 14 -6.6 14 -6.85 20 11 20 Z"],
1872
+ ["#EEA160", "M 10.9 23 L 11 20 -6.85 20 -7 23 10.9 23 Z"],
1873
+ ["#676AC6", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 L -6.45 9 5.45 9 5.9 -0.2 Z"]],
1874
+ hand: [["#A8603F", "M -9.2 7.6 L -9.25 6.65 Q -8.75 0.45 -5.45 -2.95 -14.15 3.5 -9.25 7.65 L -9.2 7.6 Z"],
1875
+ ["#BF7958", "M 7 -1.65 Q 6.95 -3 5.5 -3 L -5.45 -2.95 Q -8.75 0.45 -9.25 6.65 L -9.2 7.6 -9.25 7.65 Q -9.3 9.3 -9.25 11.15 L -9.25 11.55 Q -9.25 12.95 -7.1 13.35 -3.95 14.05 -0.95 14 7.55 13.95 7 -0.75 L 7 -1.65 Z"]],
1876
+ body: [["#BFD4E5", "M 14.6 2.8 L 14.6 -2.15 -15.45 -2.15 -15.45 2.8 14.6 2.8 Z"],
1877
+ ["#676AC6", "M 13.1 8 Q 14.5 6.1 14.6 2.8 L -15.45 2.8 Q -15.35 6.1 -13.95 8 L 13.1 8 Z"],
1878
+ ["#E7F3FC", "M 17.8 -15.8 Q 18.85 -21.2 15.2 -21 L -16.1 -21 Q -19.75 -21.2 -18.7 -15.8 L -15.45 -2.15 14.6 -2.15 17.8 -15.8 Z"],
1879
+ ["#A8603F", "M 7.6 -28.45 Q 4.45 -31 0.05 -31 -4.35 -31 -7.5 -28.45 -10.6 -25.75 -10.6 -22 -10.6 -21.75 -10.55 -21.5 -10.4 -18.1 -7.5 -15.7 -4.35 -13 0.05 -13 4.45 -13 7.6 -15.7 10.5 -18.1 10.65 -21.5 10.7 -21.75 10.7 -22 10.7 -25.75 7.6 -28.45 Z"],
1880
+ ["#D78B49", "M -3.15 -1.95 L -17.25 -0.7 Q -16.85 1.75 -16.4 4.5 L -3.95 3 Q -3.45 0.7 -3.15 -1.95 Z"],
1881
+ ["#EEA160", "M -3.15 -1.95 Q -2.8 -5.45 -2.9 -9.5 -9.65 -13.75 -13.6 -22.4 L -18.15 -23.25 Q -20.2 -19.55 -17.25 -0.7 L -3.15 -1.95 Z"],
1882
+ ["#D78B49", "M 16.85 -0.7 L 2.75 -1.95 Q 3.05 0.7 3.55 3 L 16 4.5 Q 16.45 1.75 16.85 -0.7 Z"],
1883
+ ["#EEA160", "M 2.5 -9.5 Q 2.4 -5.45 2.75 -1.95 L 16.85 -0.7 Q 19.8 -19.55 17.75 -23.25 L 13.2 -22.4 Q 9.25 -13.75 2.5 -9.5 Z"],
1884
+ ["#BFD4E5", "M -13.05 -22 L -15.1 -18.4 -2 -10.8 0.05 -14.45 -13.05 -22 M 13.15 -22 L 0.05 -14.45 2.1 -10.8 15.2 -18.4 13.15 -22 Z"],
1885
+ ["#E7F3FC", "M 2.1 -10.8 L 0.05 -14.45 -2 -10.8 2.1 -10.8 Z"]],
1886
+ head: [["#454A51", "M 12.65 -35.05 L 9.75 -39.6 Q -4.85 -40.75 -17.6 -38.7 -24.75 -37.7 -24.1 -31.9 L -23.9 -30.2 Q -22.95 -33.75 -17.55 -34.5 -4.8 -36.5 9.8 -35.35 L 12.65 -35.05 M 23.15 -30.65 L 23.3 -31.9 Q 24 -37.55 17.85 -38.7 L 13.3 -34.95 17.9 -34.5 Q 22.2 -33.7 23.15 -30.65 Z"],
1887
+ ["#3C3E41", "M 17.9 -34.5 L 13.3 -34.95 12.9 -34.65 12.65 -35.05 9.8 -35.35 Q -4.8 -36.5 -17.55 -34.5 -22.95 -33.75 -23.9 -30.2 L -21.65 -10.8 Q -21.1 -6.45 -15.95 -6.45 L 15.15 -6.45 Q 20.35 -6.45 20.85 -10.8 L 23.15 -30.65 Q 22.2 -33.7 17.9 -34.5 Z"],
1888
+ ["#BF7958", "M -24.65 -11.25 Q -27 -8.9 -27 -5.55 -27 -2.2 -24.65 0.15 -22.25 2.6 -19 2.6 -15.7 2.6 -13.3 0.15 -11 -2.2 -11 -5.55 -11 -8.9 -13.3 -11.25 -15.7 -13.7 -19 -13.7 -22.25 -13.7 -24.65 -11.25 M 12.35 -11.25 Q 10 -8.9 10 -5.55 10 -2.2 12.35 0.15 14.75 2.6 18 2.6 21.3 2.6 23.7 0.15 26 -2.2 26 -5.55 26 -8.9 23.7 -11.25 21.3 -13.7 18 -13.7 14.75 -13.7 12.35 -11.25 Z"],
1889
+ ["#A8603F", "M -23.75 -5.55 Q -23.75 -3.5 -22.35 -2.1 -20.95 -0.65 -19 -0.65 -16.95 -0.65 -15.55 -2.1 -14.15 -3.5 -14.15 -5.55 -14.15 -7.5 -15.55 -9 -16.95 -10.4 -19 -10.4 -20.95 -10.4 -22.35 -9 -23.75 -7.5 -23.75 -5.55 M 18 -10.4 Q 16.05 -10.4 14.65 -9 13.2 -7.5 13.2 -5.55 13.2 -3.5 14.65 -2.1 16.05 -0.65 18 -0.65 20 -0.65 21.45 -2.1 22.85 -3.5 22.85 -5.55 22.85 -7.5 21.45 -9 20 -10.4 18 -10.4 Z"],
1890
+ ["#BF7958", "M 11.7 8 Q 17.1 7.3 17.8 2.2 L 20.45 -23.8 -21.4 -23.8 -18.2 8 11.7 8 Z"],
1891
+ ["#FFFFFF", "M -12.95 -8.15 Q -12.95 -6.4 -11.85 -5.15 -10.65 -3.8 -8.85 -3.8 -7.15 -3.8 -6 -5.15 -4.85 -6.4 -4.85 -8.15 -4.85 -9.9 -6 -11.15 -7.15 -12.45 -8.85 -12.45 -10.65 -12.45 -11.85 -11.15 -12.95 -9.9 -12.95 -8.15 M 5.1 -5.15 Q 6.35 -3.8 8.1 -3.8 9.85 -3.8 10.9 -5.15 12.15 -6.4 12.15 -8.15 12.15 -9.9 10.9 -11.15 9.85 -12.45 8.1 -12.45 6.35 -12.45 5.1 -11.15 4 -9.9 4 -8.15 4 -6.4 5.1 -5.15 Z"],
1892
+ ["#3C3E41", "M -10.45 -8.25 L -10.45 -8 Q -10.4 -7 -9.75 -6.25 -9.05 -5.45 -8.05 -5.45 -7 -5.45 -6.3 -6.25 -5.65 -7 -5.6 -8 L -5.6 -8.25 Q -5.65 -9.3 -6.3 -10.05 -7 -10.85 -8.05 -10.85 -9.05 -10.85 -9.75 -10.05 -10.4 -9.3 -10.45 -8.25 M 9.4 -8.25 Q 9.35 -9.3 8.65 -10.05 8 -10.85 7 -10.85 6 -10.85 5.2 -10.05 4.6 -9.3 4.55 -8.35 L 4.55 -8 Q 4.6 -7 5.2 -6.25 6 -5.45 7 -5.45 8 -5.45 8.65 -6.25 9.35 -7 9.4 -8 L 9.4 -8.25 Z"],
1893
+ ["#3C3E41", "M -3.4 -11.3 L -3.2 -14.15 -14 -14.65 -14.15 -11.8 -3.4 -11.3 M 2.9 -14.15 L 3.1 -11.3 13.85 -11.8 13.7 -14.65 2.9 -14.15 Z"],
1894
+ ["#3C3E41", "M -0.7 -3.3 Q -4.25 -3.3 -6.7 -1.85 -9 -0.55 -9.3 1.25 -9.4 1.5 -9.3 1.8 -9.2 2.05 -9 2.25 -8.8 2.45 -8.5 2.5 -4.2 3.15 -0.4 1.5 3.35 3.15 7.7 2.5 8 2.45 8.2 2.25 8.4 2.05 8.5 1.8 8.6 1.5 8.55 1.25 8.2 -0.55 5.9 -1.85 3.4 -3.3 -0.1 -3.3 L -0.7 -3.3 Z"],
1895
+ ["#955435", "M 2.1 -3.4 Q 3.1 -4.1 3.1 -5.05 3.1 -5.95 2.1 -6.7 1.1 -7.35 -0.35 -7.35 -1.75 -7.35 -2.75 -6.7 -3.75 -5.95 -3.75 -5.05 -3.75 -4.1 -2.75 -3.4 -1.75 -2.65 -0.35 -2.65 1.1 -2.65 2.1 -3.4 Z"],
1896
+ ["#3C3E41", "M -20.6 -23.3 L -20.65 -23.3 Q -21.65 -23.15 -22.3 -22.3 -22.95 -21.55 -22.8 -20.55 L -20.9 -4.55 Q -20.8 -3.6 -20 -2.9 -19.2 -2.3 -18.2 -2.45 L -18.15 -2.45 Q -17.15 -2.55 -16.55 -3.35 -15.95 -4.15 -16.05 -5.2 L -17.95 -21.1 Q -18.1 -22.15 -18.9 -22.75 -19.65 -23.35 -20.6 -23.3 M 17.9 -22.75 Q 17.15 -22.15 17 -21.1 L 15.1 -5.2 Q 15 -4.15 15.6 -3.35 16.2 -2.55 17.2 -2.45 L 17.25 -2.45 Q 18.2 -2.3 19 -2.9 19.85 -3.6 19.95 -4.55 L 21.85 -20.55 Q 22 -21.55 21.35 -22.3 20.7 -23.15 19.7 -23.3 L 19.65 -23.3 Q 18.65 -23.35 17.9 -22.75 Z"],
1897
+ ["#3C3E41", "M 14.65 -19.9 Q 21.05 -21.95 21.05 -24.65 21.05 -27.3 14.65 -29 8.35 -30.7 -0.7 -30.45 -9.65 -30.15 -16.05 -28.1 -22.4 -26 -22.4 -23.35 -22.4 -20.65 -16.05 -19 -9.65 -17.25 -0.7 -17.55 8.35 -17.8 14.65 -19.9 Z"]]
1898
+ },
1899
+ MA: { // Male adventurer
1900
+ arm: [["#3DBF7B", "M 9.95 3.9 Q 7.5 0.05 4 -3.35 2.25 -4.95 -0.2 -5 -2.65 -4.95 -4.4 -3.2 -6.1 -1.4 -6 1 -6 3.45 -4.3 5.15 -1.9 7.45 -0.3 10.1 4.35 5.25 9.95 3.9 Z"],
1901
+ ["#FFFFFF", "M 13.85 12.85 Q 13.55 11.7 13.15 10.6 11.9 7.15 9.95 3.9 4.35 5.25 -0.3 10.1 0.55 11.45 1.2 12.85 L 13.85 12.85 Z"],
1902
+ ["#FFD7B1", "M 1.2 12.85 Q 2.75 16.05 3.15 19.75 3.4 22.15 5.3 23.7 7.25 25.2 9.7 25 12.1 24.7 13.65 22.75 15.2 20.9 14.95 18.4 14.6 15.5 13.85 12.85 L 1.2 12.85 Z"],
1903
+ ["#25A863", "M 12.4 4.8 L 10.35 1.2 -2.55 8.7 -0.55 12.25 12.4 4.8 Z"]],
1904
+ leg: [["#D78B49", "M 5.25 14 L 5.3 14 5.45 9 -6.45 9 -6.6 14 5.25 14 Z"],
1905
+ ["#BF7958", "M 11 20 L 5.25 14 -6.6 14 -7 23 10.9 23 11 20 Z"],
1906
+ ["#EEA160", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 L -6.45 9 5.45 9 5.9 -0.2 Z"]],
1907
+ hand: [["#F0C49A", "M -9.2 7.6 L -9.25 6.65 Q -8.75 0.45 -5.45 -2.95 -14.15 3.5 -9.25 7.65 L -9.2 7.6 Z"],
1908
+ ["#FFD7B1", "M 7 -1.65 Q 6.95 -3 5.5 -3 L -5.45 -2.95 Q -8.75 0.45 -9.25 6.65 L -9.2 7.6 -9.25 7.65 Q -9.3 9.3 -9.25 11.15 L -9.25 11.55 Q -9.25 12.95 -7.1 13.35 -3.95 14.05 -0.95 14 7.55 13.95 7 -0.75 L 7 -1.65 Z"]],
1909
+ body: [["#BF7958", "M 14.55 -2.15 L -15.5 -2.15 -15.5 2.8 14.55 2.8 14.55 -2.15 Z"],
1910
+ ["#EEA160", "M -15.5 2.8 Q -15.4 6.1 -14 8 L 13.05 8 Q 14.45 6.1 14.55 2.8 L -15.5 2.8 Z"],
1911
+ ["#3DBF7B", "M 14.55 -2.15 L 17.75 -15.8 Q 18.8 -21.2 15.15 -21 L -16.15 -21 Q -19.8 -21.2 -18.75 -15.8 L -15.5 -2.15 14.55 -2.15 Z"],
1912
+ ["#BF7958", "M -17.55 -5.05 L -12.65 -3.7 -8.75 -18.25 -14 -18.25 -17.55 -5.05 M 12.65 -3.7 L 17.55 -5.05 14 -18.25 8.75 -18.25 12.65 -3.7 Z"],
1913
+ ["#F0C49A", "M 10 -22 Q 10 -25.75 6.95 -28.45 3.85 -31 -0.5 -31 -4.85 -31 -7.95 -28.45 -11 -25.75 -11 -22 -11 -21.75 -10.95 -21.5 -10.8 -18.1 -7.95 -15.7 -4.85 -13 -0.5 -13 3.85 -13 6.95 -15.7 9.8 -18.1 9.95 -21.5 10 -21.75 10 -22 Z"],
1914
+ ["#25A863", "M -13.1 -22 L -15.15 -18.4 -2.05 -10.8 0 -14.45 -13.1 -22 M 13.1 -22 L 0 -14.45 2.05 -10.8 15.15 -18.4 13.1 -22 Z"],
1915
+ ["#3DBF7B", "M 2.05 -10.8 L 0 -14.45 -2.05 -10.8 2.05 -10.8 Z"]],
1916
+ head: [["#FFD7B1", "M -24.15 -11.25 Q -26.5 -8.9 -26.5 -5.55 -26.5 -2.2 -24.15 0.15 -21.75 2.55 -18.5 2.55 -15.2 2.55 -12.8 0.15 -10.5 -2.2 -10.5 -5.55 -10.5 -8.9 -12.8 -11.25 -15.2 -13.7 -18.5 -13.7 -21.75 -13.7 -24.15 -11.25 M 18.55 -13.7 Q 15.3 -13.7 12.9 -11.25 10.55 -8.9 10.55 -5.55 10.55 -2.2 12.9 0.15 15.3 2.55 18.55 2.55 21.85 2.55 24.25 0.15 26.55 -2.2 26.55 -5.55 26.55 -8.9 24.25 -11.25 21.85 -13.7 18.55 -13.7 Z"],
1917
+ ["#E7B687", "M -23.25 -5.55 Q -23.25 -3.5 -21.85 -2.1 -20.5 -0.65 -18.5 -0.65 -16.45 -0.65 -15.05 -2.1 -13.65 -3.5 -13.65 -5.55 -13.65 -7.5 -15.05 -9 -16.45 -10.4 -18.5 -10.4 -20.5 -10.4 -21.85 -9 -23.25 -7.5 -23.25 -5.55 M 15.2 -2.1 Q 16.55 -0.65 18.55 -0.65 20.55 -0.65 22 -2.1 23.4 -3.5 23.4 -5.55 23.4 -7.5 22 -9 20.55 -10.4 18.55 -10.4 16.55 -10.4 15.2 -9 13.75 -7.5 13.75 -5.55 13.75 -3.5 15.2 -2.1 Z"],
1918
+ ["#FFD7B1", "M 12.25 8.05 Q 17.65 7.3 18.4 2.2 L 21.05 -24 -21 -24 -17.75 8.05 12.25 8.05 Z"],
1919
+ ["#BF7958", "M 20.4 -44.4 L 7.85 -32.25 Q -9.25 -38.65 -26.55 -27.15 L -18.6 -15.6 -18.6 -20.75 Q -17.8 -20.9 -17 -21.1 -16.85 -21.05 -16.7 -21 -11.45 -18.65 -3.95 -18.65 3.5 -18.65 8.75 -21 10.2 -21.65 11.25 -22.3 15.9 -21.8 20.55 -20.75 L 20.55 -15 25.5 -24.6 Q 27.45 -29.1 27.9 -33.25 27 -39.4 20.4 -44.4 Z"],
1920
+ ["#DB855C", "M 7.85 -32.25 L 20.4 -44.4 Q 27 -39.4 27.9 -33.25 29.05 -43.8 20.4 -52 L 7.85 -36.75 Q -9.95 -45.05 -28 -29.15 L -26.55 -27.15 Q -9.25 -38.65 7.85 -32.25 Z"],
1921
+ ["#BF7958", "M -3.4 -14.5 L -4.35 -17.95 -13.45 -15.55 -12.45 -12.1 -3.4 -14.5 Z"],
1922
+ ["#FFFFFF", "M -12.45 -7.15 Q -12.45 -5.4 -11.35 -4.1 -10.15 -2.8 -8.35 -2.8 -6.65 -2.8 -5.5 -4.1 -4.3 -5.4 -4.3 -7.15 -4.3 -8.9 -5.5 -10.15 -6.65 -11.4 -8.35 -11.4 -10.15 -11.4 -11.35 -10.15 -12.45 -8.9 -12.45 -7.15 M 8.6 -11.4 Q 6.9 -11.4 5.65 -10.15 4.55 -8.9 4.55 -7.15 4.55 -5.4 5.65 -4.1 6.9 -2.8 8.6 -2.8 10.35 -2.8 11.45 -4.1 12.7 -5.4 12.7 -7.15 12.7 -8.9 11.45 -10.15 10.35 -11.4 8.6 -11.4 Z"],
1923
+ ["#BF7958", "M -5.1 -7.15 Q -5.1 -8.6 -5.8 -9.65 -6.5 -10.7 -7.55 -10.7 -8.55 -10.7 -9.25 -9.65 -9.95 -8.6 -9.95 -7.15 -9.95 -5.65 -9.25 -4.65 -8.55 -3.55 -7.55 -3.55 -6.5 -3.55 -5.8 -4.65 -5.1 -5.65 -5.1 -7.15 M 5.75 -9.65 Q 5.1 -8.6 5.1 -7.15 5.1 -5.65 5.75 -4.65 6.55 -3.55 7.5 -3.55 8.5 -3.55 9.15 -4.65 9.9 -5.65 9.9 -7.15 9.9 -8.6 9.15 -9.65 8.5 -10.7 7.5 -10.7 6.55 -10.7 5.75 -9.65 Z"],
1924
+ ["#BF7958", "M 4.95 -15.15 L 4.45 -11.6 13.7 -10.25 14.3 -13.8 4.95 -15.15 Z"],
1925
+ ["#E7B687", "M 2.6 -2.4 Q 3.65 -3.1 3.65 -4 3.65 -4.95 2.6 -5.7 1.6 -6.35 0.15 -6.35 -1.25 -6.35 -2.25 -5.7 -3.2 -4.95 -3.2 -4 -3.2 -3.1 -2.25 -2.4 -1.25 -1.65 0.15 -1.65 1.6 -1.65 2.6 -2.4 Z"],
1926
+ ["#BF7958", "M -22.3 -20.5 L -20.45 -4.6 Q -20.35 -3.6 -19.5 -2.9 -18.7 -2.3 -17.7 -2.45 L -17.65 -2.45 Q -16.65 -2.55 -16.05 -3.35 -15.45 -4.2 -15.55 -5.2 L -17.45 -21.1 Q -17.6 -22.15 -18.4 -22.75 -19.15 -23.35 -20.15 -23.3 L -20.2 -23.3 Q -21.15 -23.15 -21.8 -22.3 -22.45 -21.55 -22.3 -20.5 M 18.45 -22.75 Q 17.65 -22.15 17.5 -21.1 L 15.65 -5.2 Q 15.55 -4.2 16.15 -3.35 16.7 -2.55 17.7 -2.45 L 17.75 -2.45 Q 18.75 -2.3 19.55 -2.9 20.4 -3.6 20.5 -4.6 L 22.4 -20.5 Q 22.55 -21.55 21.9 -22.3 21.25 -23.15 20.25 -23.3 L 20.2 -23.3 Q 19.2 -23.35 18.45 -22.75 Z"],
1927
+ ["#FFFFFF", "M 2.75 3.2 L 2.75 0 -4.9 0 Q -5.25 0 -5.55 0.25 -5.9 0.4 -5.95 0.75 -6.05 1 -5.95 1.4 -5.9 1.9 -5.6 2.45 -5.35 2.8 -5.05 3.2 L 2.75 3.2 Z"],
1928
+ ["#BF7958", "M 6.55 0.25 Q 6.25 0 5.9 0 L 2.75 0 2.75 3.2 -5.05 3.2 Q -4.75 3.75 -4.25 4.1 -2.3 6 0.5 6 3.2 6 5.2 4.15 L 5.3 4.1 Q 6.05 3.4 6.55 2.45 6.85 1.9 7 1.4 7.05 1 6.95 0.75 6.85 0.4 6.55 0.25 Z"]]
1929
+ },
1930
+ FP: { // Female person
1931
+ back: [["#454A51", "M -16.4 -34.65 L -20.1 -34.85 Q -20.7 -34.9 -21.2 -34.45 -21.7 -34 -21.75 -33.35 -22.1 -26.3 -27.35 -20.95 -27.8 -20.6 -27.8 -19.95 -27.85 -19.25 -27.45 -18.8 L -27.45 -18.75 Q -27.4 -18.8 -27.35 -18.85 -19.6 -25.6 -16.95 -33.7 -16.8 -34.2 -16.4 -34.65 Z"],
1932
+ ["#3C3E41", "M -5.95 -34.05 L -16.4 -34.65 Q -16.8 -34.2 -16.95 -33.7 -19.6 -25.6 -27.35 -18.85 -27.4 -18.8 -27.45 -18.75 -27 -18.3 -26.4 -18.2 -9.9 -16.45 -4.5 -31.85 -4.35 -32.4 -4.45 -32.85 -4.6 -33.35 -5 -33.65 -5.45 -34.05 -5.95 -34.05 Z"],
1933
+ ["#3C3E41", "M 17 -33.7 Q 16.85 -34.2 16.45 -34.65 L 6 -34.05 Q 5.5 -34.05 5.05 -33.65 4.65 -33.35 4.5 -32.85 4.4 -32.4 4.55 -31.85 9.95 -16.45 26.45 -18.2 27.05 -18.3 27.5 -18.75 27.45 -18.8 27.4 -18.85 19.65 -25.6 17 -33.7 Z"],
1934
+ ["#454A51", "M 21.8 -33.35 Q 21.75 -34 21.25 -34.45 20.75 -34.9 20.15 -34.85 L 16.45 -34.65 Q 16.85 -34.2 17 -33.7 19.65 -25.6 27.4 -18.85 27.45 -18.8 27.5 -18.75 L 27.5 -18.8 Q 27.9 -19.25 27.85 -19.95 27.85 -20.6 27.4 -20.95 22.15 -26.3 21.8 -33.35 Z"]],
1935
+ arm: [["#BF7958", "M 4.85 17.35 Q 5.5 20.9 5.75 22.85 6 24.8 8.15 25 10.2 25.15 11.8 23.8 13.4 22.45 13.6 20.45 14 16 12.95 11.9 L 4.2 15.15 Q 4.65 16.3 4.85 17.35 Z"],
1936
+ ["#3DBF7B", "M 0.3 8.75 Q 1.55 10.4 2.5 11.9 L 11.6 8.05 Q 10.7 6 9.4 4 6.8 0 2.6 -3.7 1 -5.1 -1.1 -5 -3.25 -4.9 -4.6 -3.35 -6.1 -1.9 -6 0.2 -5.9 2.25 -4.3 3.6 -1.5 6.1 0.3 8.75 Z"],
1937
+ ["#25A863", "M 12.95 11.9 L 14 11.45 12.9 7.5 11.6 8.05 2.5 11.9 1.6 12.25 2.9 15.65 4.2 15.15 12.95 11.9 Z"]],
1938
+ leg: [["#FFFFFF", "M 10.9 23 L 11 20 -3.95 20 -3.95 23 10.9 23 Z"],
1939
+ ["#BFD4E5", "M 11 20 Q 6.15 18.65 5.25 14 L -3.95 14 -3.95 20 11 20 Z"],
1940
+ ["#BF7958", "M 5.25 14 L 5.3 14 5.45 9 -3.95 9 -3.95 14 5.25 14 Z"],
1941
+ ["#676AC6", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 -4.05 2.2 -3.95 9 L 5.45 9 5.9 -0.2 Z"]],
1942
+ hand: [["#A8603F", "M -8.35 7.6 L -8.4 6.65 Q -7.9 0.45 -3.8 -3 -13.2 3.55 -8.4 7.65 L -8.35 7.6 Z"],
1943
+ ["#BF7958", "M 4.2 -3 L -3.8 -3 Q -7.9 0.45 -8.4 6.65 L -8.35 7.6 -8.4 7.65 Q -8.45 9.3 -8.4 11.2 L -8.4 11.55 Q -8.4 13 -6.35 13.4 -3.25 14.05 -0.35 14 7.05 13.95 4.2 -3 Z"]],
1944
+ body: [["#676AC6", "M -13.75 3.45 Q -13.8 5.6 -13.15 8 L 12.15 8 Q 12.8 5.6 12.75 3.45 L -13.75 3.45 Z"],
1945
+ ["#25A863", "M -13.9 -1.5 L -10.95 -1.5 Q -10.2 -5.75 -15.5 -9.5 L -15.3 -8.7 Q -14.25 -5.35 -13.9 -1.5 M 14.3 -8.7 L 14.5 -9.5 Q 9.2 -5.75 10 -1.5 L 12.95 -1.5 Q 13.3 -5.35 14.3 -8.7 M 9.45 -9.1 Q 9.45 -9.95 6.6 -10.6 3.65 -11.1 -0.5 -11.1 -4.65 -11.1 -7.6 -10.6 -10.45 -9.95 -10.45 -9.1 -10.45 -8.25 -7.6 -7.75 -4.65 -7.1 -0.5 -7.1 3.65 -7.1 6.6 -7.75 9.45 -8.25 9.45 -9.1 Z"],
1946
+ ["#3C3E41", "M 14.1 -1.5 L 12.95 -1.5 10 -1.5 -10.95 -1.5 -13.9 -1.5 -15.05 -1.5 -14.3 3.45 -13.75 3.45 12.75 3.45 13.35 3.45 14.1 -1.5 Z"],
1947
+ ["#3DBF7B", "M 17.75 -15.85 Q 18.75 -20.9 15.65 -21.1 15.4 -21.1 15.15 -21.1 L -16.15 -21.1 Q -19.8 -21.3 -18.75 -15.85 -16.55 -12.7 -15.3 -8.7 L -15.5 -9.5 Q -10.2 -5.75 -10.95 -1.5 L 10 -1.5 Q 9.2 -5.75 14.5 -9.5 L 14.3 -8.7 Q 15.55 -12.7 17.75 -15.85 M 6.6 -10.6 Q 9.45 -9.95 9.45 -9.1 9.45 -8.25 6.6 -7.75 3.65 -7.1 -0.5 -7.1 -4.65 -7.1 -7.6 -7.75 -10.45 -8.25 -10.45 -9.1 -10.45 -9.95 -7.6 -10.6 -4.65 -11.1 -0.5 -11.1 3.65 -11.1 6.6 -10.6 Z"],
1948
+ ["#FFFFFF", "M 10.05 -21.25 Q 9.75 -18.05 7.05 -15.75 3.9 -13.1 -0.5 -13.1 -4.9 -13.1 -8.05 -15.75 -10.75 -18 -11 -21.15 -11.55 -19.9 -11.55 -18.55 -11.55 -18.3 -11.5 -18.05 -11.35 -14.95 -8.35 -12.8 -5.1 -10.35 -0.5 -10.35 4.1 -10.35 7.4 -12.8 10.4 -14.95 10.55 -18.05 10.6 -18.3 10.6 -18.55 10.6 -19.95 10.05 -21.25 Z"],
1949
+ ["#BF7958", "M 10.1 -22.05 Q 10.1 -25.75 7.05 -28.45 3.9 -31 -0.5 -31 -4.9 -31 -8.05 -28.45 -11.1 -25.75 -11.1 -22.05 -11.1 -21.8 -11.05 -21.55 -11.05 -21.35 -11 -21.15 -10.75 -18 -8.05 -15.75 -4.9 -13.1 -0.5 -13.1 3.9 -13.1 7.05 -15.75 9.75 -18.05 10.05 -21.25 10.05 -21.4 10.05 -21.55 10.1 -21.8 10.1 -22.05 Z"]],
1950
+ head: [["#3C3E41", "M 23.75 -18.55 Q 23.2 -25.75 16.55 -31 9.3 -36.75 -0.95 -36.75 -11.2 -36.75 -18.45 -31 -25.05 -25.75 -25.6 -18.55 -25.05 -10.4 -18.45 -4.5 -11.2 2 -0.95 2 9.3 2 16.55 -4.5 23.2 -10.4 23.75 -18.55 Z"],
1951
+ ["#454A51", "M 23.8 -20 Q 23.8 -29.1 16.55 -35.6 9.3 -42 -0.95 -42 -11.2 -42 -18.45 -35.6 -25.65 -29.1 -25.65 -20 -25.65 -19.25 -25.6 -18.55 -25.05 -25.75 -18.45 -31 -11.2 -36.75 -0.95 -36.75 9.3 -36.75 16.55 -31 23.2 -25.75 23.75 -18.55 23.8 -19.25 23.8 -20 Z"],
1952
+ ["#BF7958", "M -24.65 -16.95 Q -27 -14.6 -27 -11.2 -27 -7.9 -24.65 -5.55 -22.3 -3.1 -19 -3.1 -15.75 -3.1 -13.35 -5.55 -11 -7.9 -11 -11.2 -11 -14.6 -13.35 -16.95 -15.75 -19.35 -19 -19.35 -22.3 -19.35 -24.65 -16.95 M 12.35 -5.55 Q 14.7 -3.1 18 -3.1 21.25 -3.1 23.65 -5.55 26 -7.9 26 -11.2 26 -14.6 23.65 -16.95 21.25 -19.35 18 -19.35 14.7 -19.35 12.35 -16.95 10 -14.6 10 -11.2 10 -7.9 12.35 -5.55 Z"],
1953
+ ["#A8603F", "M -23.8 -11.2 Q -23.8 -9.2 -22.4 -7.8 -21 -6.35 -19 -6.35 -16.95 -6.35 -15.6 -7.8 -14.2 -9.2 -14.2 -11.2 -14.2 -13.15 -15.6 -14.7 -16.95 -16.1 -19 -16.1 -21 -16.1 -22.4 -14.7 -23.8 -13.15 -23.8 -11.2 M 18 -16.1 Q 16 -16.1 14.6 -14.7 13.2 -13.15 13.2 -11.2 13.2 -9.2 14.6 -7.8 16 -6.35 18 -6.35 20 -6.35 21.4 -7.8 22.8 -9.2 22.8 -11.2 22.8 -13.15 21.4 -14.7 20 -16.1 18 -16.1 Z"],
1954
+ ["#BF7958", "M 18.1 -3.65 L 20.4 -26.2 -21.45 -26.2 -19.15 -3.65 Q -17.1 3.75 -8.75 6.65 -4.55 8.1 -0.55 8 3.5 8.1 7.65 6.65 16.05 3.75 18.1 -3.65 Z"],
1955
+ ["#955435", "M 1.85 -4.85 Q 2.9 -5.55 2.9 -6.45 2.9 -7.35 1.85 -8.1 0.85 -8.75 -0.6 -8.75 -1.95 -8.75 -2.95 -8.1 -3.95 -7.35 -3.95 -6.45 -3.95 -5.55 -2.95 -4.85 -1.95 -4.05 -0.6 -4.05 0.85 -4.05 1.85 -4.85 Z"],
1956
+ ["#FFFFFF", "M -13.1 -9.55 Q -13.1 -11.3 -11.95 -12.55 -10.75 -13.85 -9 -13.85 -7.3 -13.85 -6.15 -12.55 -4.95 -11.3 -4.95 -9.55 -4.95 -7.8 -6.15 -6.55 -7.3 -5.25 -9 -5.25 -10.75 -5.25 -11.95 -6.55 -13.1 -7.8 -13.1 -9.55 M 3.9 -9.55 Q 3.9 -11.3 5.1 -12.55 6.2 -13.85 7.9 -13.85 9.7 -13.85 10.9 -12.55 12.05 -11.3 12.05 -9.55 12.05 -7.8 10.9 -6.55 9.7 -5.25 7.9 -5.25 6.2 -5.25 5.1 -6.55 3.9 -7.8 3.9 -9.55 Z"],
1957
+ ["#3C3E41", "M -13.1 -9.2 Q -13.1 -9.4 -13.1 -9.55 -13.1 -11.3 -11.95 -12.55 -11 -13.55 -9.65 -13.8 L -15.4 -13.8 Q -13.3 -12.15 -13.1 -9.2 M -6.45 -7.05 Q -5.75 -8.05 -5.75 -9.55 -5.75 -11 -6.45 -12.05 -7.15 -13.1 -8.2 -13.1 -9.2 -13.1 -9.85 -12.05 -10.55 -11 -10.55 -9.55 -10.55 -8.05 -9.85 -7.05 -9.2 -6 -8.2 -6 -7.15 -6 -6.45 -7.05 M 12.05 -9.55 Q 12.05 -9.4 12.05 -9.2 12.25 -12.15 14.3 -13.8 L 8.6 -13.8 Q 9.95 -13.55 10.9 -12.55 12.05 -11.3 12.05 -9.55 M 4.7 -9.55 Q 4.7 -8.05 5.4 -7.05 6.05 -6 7.1 -6 8.1 -6 8.8 -7.05 9.5 -8.05 9.5 -9.55 9.5 -11 8.8 -12.05 8.1 -13.1 7.1 -13.1 6.05 -13.1 5.4 -12.05 4.7 -11 4.7 -9.55 Z"],
1958
+ ["#FFFFFF", "M 1.75 1.2 L 1.75 -2 -5.9 -2 Q -6.25 -2 -6.55 -1.75 -6.9 -1.6 -6.95 -1.25 -7.05 -1 -6.95 -0.6 -6.9 -0.1 -6.6 0.45 -6.35 0.8 -6.05 1.2 L 1.75 1.2 Z"],
1959
+ ["#A8603F", "M 4.9 -2 L 1.75 -2 1.75 1.2 -6.05 1.2 Q -5.75 1.75 -5.25 2.1 -3.3 4 -0.5 4 2.2 4 4.2 2.15 L 4.3 2.1 Q 5.05 1.4 5.55 0.45 5.85 -0.1 6 -0.6 6.05 -1 5.95 -1.25 5.85 -1.6 5.55 -1.75 5.25 -2 4.9 -2 Z"],
1960
+ ["#3C3E41", "M 20.05 -17.65 L 21.05 -26.05 20.5 -26.05 20.5 -26.9 -22.2 -26.9 -21.55 -19.25 Q -10.8 -19.45 -5.65 -23.35 -0.45 -16.9 5.4 -16.75 L 6.85 -19.85 Q 13.6 -17.35 20.05 -17.65 Z"],
1961
+ ["#71A2BE", "M 17.1 -26.95 Q 16.3 -28.05 15.1 -29.1 9.55 -33.8 -0.5 -33.8 -10.55 -33.8 -16.05 -29.15 -16.45 -28.9 -16.7 -28.55 -17.55 -27.85 -18.1 -26.95 -18.7 -26.15 -18.25 -25 -11.8 -28.7 -0.5 -28.7 10.7 -28.7 17.2 -25.05 17.65 -26.15 17.1 -26.95 Z"],
1962
+ ["#3C3E41", "M -16.6 -12.55 Q -16 -13.35 -16.1 -14.4 L -16.75 -20.05 -21.7 -20.05 -20.95 -13.8 Q -20.85 -12.8 -20 -12.1 -19.2 -11.5 -18.2 -11.65 L -18.15 -11.65 Q -17.15 -11.75 -16.6 -12.55 Z M 14.85 -14.4 Q 14.75 -13.35 15.35 -12.55 15.95 -11.75 16.95 -11.65 L 17 -11.65 Q 18 -11.5 18.8 -12.1 19.65 -12.8 19.75 -13.8 L 20.5 -20.05 15.5 -20.05 14.85 -14.4 Z"],
1963
+ ["#3C3E41", "M -14.35 -18.35 L -15 -15.95 -5.55 -15 -5 -17 -14.35 -18.35 Z M 14 -15.8 L 13.6 -18.2 4.25 -17.75 4.6 -15.7 14 -15.8 Z"],
1964
+ ["#EEA160", "M 19.85 -7.8 Q 19.45 -7.1 19.65 -6.4 L 20.65 -2.55 Q 20.85 -1.85 21.5 -1.45 22.15 -1.1 22.9 -1.3 L 22.95 -1.3 Q 23.65 -1.5 24.05 -2.15 24.45 -2.85 24.25 -3.6 L 23.25 -7.4 Q 23.05 -8.1 22.35 -8.5 21.65 -8.85 21 -8.65 L 20.95 -8.65 Q 20.2 -8.45 19.85 -7.8 Z M -20.8 -7.8 Q -21.15 -8.45 -21.9 -8.65 L -21.95 -8.65 Q -22.65 -8.85 -23.35 -8.5 -24.05 -8.1 -24.25 -7.4 L -25.2 -3.6 Q -25.4 -2.85 -25 -2.15 -24.6 -1.5 -23.95 -1.3 L -23.9 -1.3 Q -23.15 -1.1 -22.5 -1.45 -21.8 -1.85 -21.6 -2.55 L -20.6 -6.4 Q -20.4 -7.1 -20.8 -7.8 Z"]]
1965
+ },
1966
+ FA: { // Female adventurer
1967
+ back: [["#BF7958", "M 18.85 -55.05 Q 11.05 -61.15 0 -61.15 -11 -61.15 -18.9 -55.05 -21.95 -52.65 -23.8 -49.85 -25.05 -46.2 -25.05 -42.05 L -25.05 -41.45 Q -25 -38.8 -24.7 -36.3 L -23.9 -24.15 Q -23.9 -23.55 -23.85 -22.95 -23.55 -19.3 -22.2 -16.1 -20.5 -11.9 -17 -8.4 -10.05 -1.4 -0.15 -1.4 9.7 -1.4 16.65 -8.4 20.4 -12.1 22.15 -16.25 22.8 -17.8 23.15 -19.4 25.1 -27.75 24.9 -35.8 24.85 -37.3 24.95 -38.7 24.95 -38.95 25 -39.25 25.05 -40.55 25.05 -41.9 L 25.05 -42.05 Q 25.05 -46.2 23.8 -49.85 21.9 -52.65 18.85 -55.05 Z"],
1968
+ ["#DB855C", "M 0 -61.15 Q 11.05 -61.15 18.85 -55.05 21.9 -52.65 23.8 -49.85 22 -55.15 17.7 -59.4 10.35 -66.6 0 -66.6 -10.35 -66.6 -17.75 -59.4 -22.05 -55.1 -23.8 -49.85 -21.95 -52.65 -18.9 -55.05 -11 -61.15 0 -61.15 Z"]],
1969
+ arm: [["#FFD7B1", "M 3.6 13.9 Q 4.55 15.7 4.85 17.3 5.5 20.85 5.75 22.85 5.95 24.8 8.1 25 10.2 25.15 11.8 23.8 13.35 22.45 13.55 20.4 14.1 15.1 12.4 10.15 L 3.6 13.9 Z"],
1970
+ ["#79ADCB", "M 12.4 10.15 Q 11.35 7 9.4 4 6.8 0 2.6 -3.75 1 -5.1 -1.05 -5 -3.2 -4.9 -4.6 -3.4 -6.05 -1.9 -5.95 0.15 -5.85 2.2 -4.3 3.6 -1.5 6.05 0.3 8.75 2.45 11.5 3.6 13.9 L 12.4 10.15 Z"],
1971
+ ["#5A93B4", "M 1.6 12.25 L 2.9 15.65 14 11.45 12.85 7.5 1.6 12.25 Z"]],
1972
+ leg: [["#FFD7B1", "M 5.3 14 L 5.45 9 -3.95 9 -3.95 14 5.25 14 5.3 14 Z"],
1973
+ ["#BF7958", "M 10.9 23 L 11 20 Q 6.15 18.65 5.25 14 L -3.95 14 -3.95 23 10.9 23 Z"],
1974
+ ["#EEA160", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 -4.05 2.2 -3.95 9 L 5.45 9 5.9 -0.2 Z"]],
1975
+ hand: [["#F0C49A", "M -8.35 7.6 L -8.4 6.65 Q -7.9 0.45 -3.8 -3 -13.2 3.55 -8.4 7.65 L -8.35 7.6 Z"],
1976
+ ["#FFD7B1", "M 4.2 -3 L -3.8 -3 Q -7.9 0.45 -8.4 6.65 L -8.35 7.6 -8.4 7.65 Q -8.45 9.3 -8.4 11.2 L -8.4 11.55 Q -8.4 13 -6.35 13.4 -3.25 14.05 -0.35 14 7.05 13.95 4.2 -3 Z"]],
1977
+ body: [["#5A93B4", "M -15.5 -9.45 L -15.3 -8.65 Q -13.65 -3.3 -13.75 3.4 -7.25 -3.65 -15.5 -9.45 M 14.35 -8.65 L 14.55 -9.45 Q 6.3 -3.65 12.8 3.4 12.7 -3.3 14.35 -8.65 Z"],
1978
+ ["#EEA160", "M 12.2 8 Q 12.85 5.6 12.8 3.4 L -13.75 3.4 Q -13.8 5.6 -13.15 8 L 12.2 8 Z"],
1979
+ ["#79ADCB", "M 14.55 -9.45 L 14.35 -8.65 Q 15.6 -12.65 17.75 -15.8 18.75 -20.8 15.7 -21 15.45 -21 15.2 -21 L -16.15 -21 Q -19.8 -21.2 -18.75 -15.8 -16.55 -12.65 -15.3 -8.65 L -15.5 -9.45 Q -7.25 -3.65 -13.75 3.4 L 12.8 3.4 Q 6.3 -3.65 14.55 -9.45 M 9.5 -9.05 Q 9.5 -8.25 6.6 -7.75 3.65 -7.1 -0.5 -7.1 -4.6 -7.1 -7.55 -7.75 -10.45 -8.25 -10.45 -9.05 -10.45 -9.9 -7.55 -10.55 -4.6 -11.05 -0.5 -11.05 3.65 -11.05 6.6 -10.55 9.5 -9.9 9.5 -9.05 Z"],
1980
+ ["#69A1C2", "M 6.6 -7.75 Q 9.5 -8.25 9.5 -9.05 9.5 -9.9 6.6 -10.55 3.65 -11.05 -0.5 -11.05 -4.6 -11.05 -7.55 -10.55 -10.45 -9.9 -10.45 -9.05 -10.45 -8.25 -7.55 -7.75 -4.6 -7.1 -0.5 -7.1 3.65 -7.1 6.6 -7.75 Z"],
1981
+ ["#BF7958", "M 14 5 L 14 0 -14 0 -14 5 14 5 Z"],
1982
+ ["#BF7958", "M 12.8 12.75 L 12.85 12.75 Q 13.85 12.75 14.5 12.05 15.15 11.3 14.95 10.3 L 14.05 4.75 9 4.75 9.9 10.3 Q 10.1 11.3 10.9 12.05 11.75 12.75 12.8 12.75 Z"],
1983
+ ["#F0C49A", "M 10 -22 Q 10 -25.75 6.95 -28.45 3.85 -31 -0.5 -31 -4.85 -31 -7.95 -28.45 -11 -25.75 -11 -22 -11 -21.75 -10.95 -21.5 -10.8 -18.1 -7.95 -15.7 -4.85 -13 -0.5 -13 3.85 -13 6.95 -15.7 9.8 -18.1 9.95 -21.5 10 -21.75 10 -22 Z"],
1984
+ ["#BF7958", "M -11.9 -25 L -16 -21.25 Q -6.55 -6 7.95 4 L 12 0.3 Q -1.25 -9.2 -11.9 -25 Z"]],
1985
+ head: [["#FFD7B1", "M 18 -19.5 Q 14.7 -19.5 12.35 -17.1 10 -14.7 10 -11.35 10 -8 12.35 -5.6 14.7 -3.2 18 -3.2 21.3 -3.2 23.65 -5.6 26 -8 26 -11.35 26 -14.7 23.65 -17.1 21.3 -19.5 18 -19.5 Z M -24.65 -17.1 Q -27 -14.7 -27 -11.35 -27 -8 -24.65 -5.6 -22.3 -3.2 -19 -3.2 -15.75 -3.2 -13.35 -5.6 -11 -8 -11 -11.35 -11 -14.7 -13.35 -17.1 -15.75 -19.5 -19 -19.5 -22.3 -19.5 -24.65 -17.1 Z"],
1986
+ ["#E7B687", "M 14.6 -7.9 Q 16 -6.45 18 -6.45 20 -6.45 21.45 -7.9 22.8 -9.3 22.8 -11.35 22.8 -13.3 21.45 -14.8 20 -16.25 18 -16.25 16 -16.25 14.6 -14.8 13.2 -13.3 13.2 -11.35 13.2 -9.3 14.6 -7.9 Z M -23.75 -11.35 Q -23.75 -9.3 -22.4 -7.9 -21 -6.45 -19 -6.45 -16.95 -6.45 -15.6 -7.9 -14.2 -9.3 -14.2 -11.35 -14.2 -13.3 -15.6 -14.8 -16.95 -16.25 -19 -16.25 -21 -16.25 -22.4 -14.8 -23.75 -13.3 -23.75 -11.35 Z"],
1987
+ ["#FFD7B1", "M 18.1 -3.7 L 20.4 -26.4 -21.45 -26.4 -19.15 -3.7 Q -17.1 3.75 -8.7 6.6 -4.55 8.1 -0.55 8 3.5 8.1 7.65 6.6 16.05 3.75 18.1 -3.7 Z"],
1988
+ ["#E7B687", "M 1.85 -4.9 Q 2.9 -5.6 2.9 -6.55 2.9 -7.45 1.85 -8.2 0.85 -8.85 -0.6 -8.85 -1.95 -8.85 -2.95 -8.2 -3.95 -7.45 -3.95 -6.55 -3.95 -5.6 -2.95 -4.9 -1.95 -4.15 -0.6 -4.15 0.85 -4.15 1.85 -4.9 Z"],
1989
+ ["#FFFFFF", "M -13.1 -9.65 Q -13.1 -11.45 -11.95 -12.7 -10.75 -13.95 -8.95 -13.95 -7.3 -13.95 -6.15 -12.7 -4.95 -11.45 -4.95 -9.65 -4.95 -7.9 -6.15 -6.65 -7.3 -5.3 -8.95 -5.3 -10.75 -5.3 -11.95 -6.65 -13.1 -7.8 -13.1 -9.65 M 3.9 -9.65 Q 3.9 -11.45 5.1 -12.7 6.25 -13.95 7.9 -13.95 9.7 -13.95 10.9 -12.7 12.05 -11.45 12.05 -9.65 12.05 -7.8 10.9 -6.65 9.7 -5.3 7.9 -5.3 6.25 -5.3 5.1 -6.65 3.9 -7.9 3.9 -9.65 Z"],
1990
+ ["#BF7958", "M -13.1 -9.3 Q -13.1 -9.5 -13.1 -9.65 -13.1 -11.45 -11.95 -12.7 -11 -13.7 -9.65 -13.9 L -15.4 -13.9 Q -13.3 -12.3 -13.1 -9.3 M -6.45 -7.15 Q -5.75 -8.15 -5.75 -9.65 -5.75 -11.15 -6.45 -12.2 -7.15 -13.25 -8.2 -13.25 -9.15 -13.25 -9.85 -12.2 -10.55 -11.15 -10.55 -9.65 -10.55 -8.15 -9.85 -7.15 -9.15 -6.05 -8.2 -6.05 -7.15 -6.05 -6.45 -7.15 M 12.05 -9.65 Q 12.05 -9.5 12.05 -9.3 12.25 -12.3 14.3 -13.9 L 8.6 -13.9 Q 9.95 -13.7 10.9 -12.7 12.05 -11.45 12.05 -9.65 M 4.7 -9.65 Q 4.7 -8.15 5.4 -7.15 6.1 -6.05 7.1 -6.05 8.1 -6.05 8.8 -7.15 9.5 -8.15 9.5 -9.65 9.5 -11.15 8.8 -12.2 8.1 -13.25 7.1 -13.25 6.1 -13.25 5.4 -12.2 4.7 -11.15 4.7 -9.65 Z"],
1991
+ ["#BF7958", "M 14.05 -16.65 L 14.05 -19.1 4.7 -20.2 4.7 -18.15 14.05 -16.65 Z M -15.05 -17.05 L -15.05 -14.6 -5.7 -16.1 -5.7 -18.2 -15.05 -17.05 Z"],
1992
+ ["#FFFFFF", "M -6.95 -1.25 Q -7.05 -1 -6.95 -0.6 -6.9 -0.1 -6.6 0.45 -6.35 0.8 -6.05 1.2 L 1.75 1.2 1.75 -2 -5.9 -2 Q -6.25 -2 -6.55 -1.75 -6.9 -1.6 -6.95 -1.25 Z"],
1993
+ ["#BF7958", "M 4.9 -2 L 1.75 -2 1.75 1.2 -6.05 1.2 Q -5.75 1.75 -5.25 2.1 -3.3 4 -0.5 4 2.2 4 4.2 2.15 L 4.3 2.1 Q 5.05 1.4 5.55 0.45 5.85 -0.1 6 -0.6 6.05 -1 5.95 -1.25 5.85 -1.6 5.55 -1.75 5.25 -2 4.9 -2 Z"],
1994
+ ["#DB855C", "M 18.9 -8.45 L 20.05 -17.8 Q 8.45 -17.25 -4.3 -26.1 0.45 -11.75 18.9 -8.45 Z"],
1995
+ ["#BF7958", "M 20.05 -17.8 L 21.2 -27.1 -22.2 -27.1 -21.55 -19.4 Q -10.8 -19.6 -4.3 -26.1 8.45 -17.25 20.05 -17.8 Z"],
1996
+ ["#71A2BE", "M 17.1 -27.15 Q 16.3 -28.25 15.1 -29.3 9.55 -34 -0.5 -34 -10.55 -34 -16.05 -29.35 -16.4 -29.1 -16.65 -28.75 -17.55 -28.05 -18.1 -27.15 -18.7 -26.35 -18.25 -25.2 -11.8 -28.9 -0.5 -28.9 10.7 -28.9 17.2 -25.25 17.65 -26.35 17.1 -27.15 Z"]]
1997
+ },
1998
+ RB: { // Robot
1999
+ arm: [["#477CA6", "M 6.4 14.7 Q 5.09 14.7 3.95 14.2 4.69 17.01 4.8 20.2 4.85 21.8 6.15 22.95 7.35 24.05 9.05 24 10.75 23.95 11.95 22.75 13.05 21.5 13 19.9 12.83 15.35 11.7 11.4 11.3 12.32 10.5 13.1 8.8 14.7 6.4 14.7 M 6.4 3.45 Q 7.43 3.45 8.3 3.75 5.39 -0.85 0.6 -4.2 -0.8 -5.2 -2.5 -4.95 -4.2 -4.7 -5.15 -3.4 -6.2 -2.1 -5.95 -0.45 -5.65 1.15 -4.3 2.15 -1.2 4.37 0.85 7.3 1.25 6.1 2.25 5.1 4 3.45 6.4 3.45 Z"],
2000
+ ["#334E63", "M 11.7 11.4 Q 12.25 10.35 12.25 9.1 12.25 6.8 10.5 5.1 9.53 4.16 8.3 3.75 7.43 3.45 6.4 3.45 4 3.45 2.25 5.1 1.25 6.1 0.85 7.3 0.55 8.15 0.55 9.1 0.55 11.4 2.25 13.1 3.05 13.83 3.95 14.2 5.09 14.7 6.4 14.7 8.8 14.7 10.5 13.1 11.3 12.32 11.7 11.4 Z"],
2001
+ ["#477CA6", "M 7.1 6.3 Q 8.3 6.3 9.1 7.1 10 7.9 10 9.05 10 10.2 9.1 11 8.3 11.8 7.1 11.8 5.9 11.8 5.05 11 4.25 10.2 4.25 9.05 4.25 7.9 5.05 7.1 5.9 6.3 7.1 6.3 Z"]],
2002
+ leg: [["#334E63", "M 11 20 L 5.25 14 -0.55 14 -0.55 9.65 Q -0.55 8.4 -1.45 7.5 -2.35 6.6 -3.6 6.6 L -3.65 6.6 Q -4.9 6.6 -5.8 7.5 -6.25 7.95 -6.5 8.5 -6.7 9.05 -6.7 9.65 L -7 23 10.9 23 11 20 Z"],
2003
+ ["#477CA6", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 L -6.5 8.5 Q -6.25 7.95 -5.8 7.5 -4.9 6.6 -3.65 6.6 L -3.6 6.6 Q -2.35 6.6 -1.45 7.5 -0.55 8.4 -0.55 9.65 L -0.55 14 5.25 14 5.3 14 5.9 -0.2 Z M -3.6 8.35 Q -3 8.35 -2.55 8.8 -2.1 9.25 -2.1 9.85 -2.1 10.45 -2.55 10.9 -3 11.35 -3.6 11.35 -4.2 11.35 -4.65 10.9 -5.1 10.45 -5.1 9.85 -5.1 9.25 -4.65 8.8 -4.2 8.35 -3.6 8.35 Z"]],
2004
+ hand: [["#BFD4E5", "M -1.15 10.65 Q -1.25 10.55 -1.3 10.7 -1.4 10.85 -1.45 11.05 L -1.45 14.55 Q -1.45 14.7 -1.3 14.95 -1.2 15 -1.05 15 1.75 14.95 3.75 12.95 L 3.8 12.95 Q 5.9 10.85 6 7.85 6.05 4.8 4.05 2.7 L 3.9 2.55 Q 3.87 3.11 3.7 3.65 3.19 5.55 1.5 6.6 1.75 7.15 1.75 7.75 1.65 8.9 0.95 9.75 0.9 9.75 0.8 9.8 0.05 10.6 -1.15 10.65 M -5.7 -0.45 Q -6.11 -0.36 -6.55 -0.25 -9.35 0.4 -10.95 3 -12.45 5.6 -11.8 8.5 -11.8 8.55 -11.8 8.6 -11.05 11.35 -8.7 12.85 -8.55 12.95 -8.4 12.95 -8.3 12.9 -8.25 12.75 L -6.3 9.7 Q -6.2 9.6 -6.2 9.5 -6.3 9.3 -6.4 9.2 -7.3 8.6 -7.6 7.5 -7.6 7.45 -7.6 7.4 -7.85 6.3 -7.25 5.3 -6.76 4.51 -6.05 4.15 -6.62 2.51 -6.2 0.8 -6.01 0.14 -5.7 -0.45 Z"],
2005
+ ["#5393C5", "M 3.25 -0.3 Q 2.25 -2.15 0.2 -2.8 -1.8 -3.4 -3.75 -2.35 -5.01 -1.6 -5.7 -0.45 -6.01 0.14 -6.2 0.8 -6.62 2.51 -6.05 4.15 -5.91 4.48 -5.75 4.8 -4.7 6.65 -2.65 7.25 -0.6 7.8 1.25 6.8 1.39 6.72 1.5 6.6 3.19 5.55 3.7 3.65 3.87 3.11 3.9 2.55 4.06 1.1 3.25 -0.3 Z"]],
2006
+ body: [["#334E63", "M -12.95 -14.9 Q -11 -16.8 -11 -19.5 -11 -22.2 -12.95 -24.1 -14.85 -26 -17.55 -26 -20.25 -26 -22.1 -24.1 -24 -22.2 -24 -19.5 -24 -16.8 -22.1 -14.9 -20.25 -13 -17.55 -13 -14.85 -13 -12.95 -14.9 M 21.1 -14.9 Q 23 -16.8 23 -19.5 23 -22.2 21.1 -24.1 19.2 -26 16.5 -26 13.8 -26 11.95 -24.1 10 -22.2 10 -19.5 10 -16.8 11.95 -14.9 13.8 -13 16.5 -13 19.2 -13 21.1 -14.9 Z"],
2007
+ ["#5393C5", "M 12.9 8 Q 14.3 6.05 14.4 2.75 L -15.5 2.75 Q -15.4 6.05 -14 8 L 12.9 8 Z"],
2008
+ ["#334E63", "M 7.75 -2.25 Q 9.1 -4.45 9.1 -7.25 9.1 -11.25 6.3 -14.1 3.45 -16.95 -0.55 -16.95 -4.55 -16.95 -7.4 -14.1 -10.25 -11.25 -10.25 -7.25 -10.25 -4.45 -8.85 -2.25 L -15.5 -2.25 -15.5 2.75 14.4 2.75 14.4 -2.25 7.75 -2.25 Z"],
2009
+ ["#FF6699", "M 2.85 -3.55 L 4.5 -8.8 Q 4.55 -9.05 4.5 -9.3 4.4 -9.5 4.25 -9.7 L 0 -12.85 Q -0.25 -13 -0.45 -13 -0.7 -13 -0.85 -12.85 L -5.15 -9.7 Q -5.35 -9.5 -5.45 -9.3 -5.5 -9.05 -5.45 -8.8 L -3.8 -3.55 Q -3.75 -3.35 -3.5 -3.2 -3.3 -3.05 -3.1 -3.05 L 2.15 -3.05 Q 2.4 -3.05 2.6 -3.2 2.8 -3.35 2.85 -3.55 Z"],
2010
+ ["#477CA6", "M 17.65 -16 Q 18.9 -21 15.4 -21.25 L 10.05 -21.25 Q 10.05 -21.35 10.05 -21.5 10.1 -21.75 10.1 -22 10.1 -25.75 7.05 -28.45 3.9 -31 -0.5 -31 -4.9 -31 -8.05 -28.45 -11.1 -25.75 -11.1 -22 -11.1 -21.75 -11.05 -21.5 -11.05 -21.35 -11 -21.25 L -16.15 -21.25 Q -19.8 -21.45 -18.75 -16 L -15.5 -2.25 -8.85 -2.25 Q -10.25 -4.45 -10.25 -7.25 -10.25 -11.25 -7.4 -14.1 -4.55 -16.95 -0.55 -16.95 3.45 -16.95 6.3 -14.1 9.1 -11.25 9.1 -7.25 9.1 -4.45 7.75 -2.25 L 14.4 -2.25 17.65 -16 Z"]],
2011
+ head: [["#BFD4E5", "M 11.85 -7.1 Q 17 -12.2 17 -19.45 17 -26.7 11.85 -31.85 6.7 -37 -0.55 -37 -7.8 -37 -12.95 -31.85 -18.1 -26.7 -18.1 -19.45 -18.1 -12.2 -12.95 -7.1 -7.8 -1.9 -0.55 -1.9 6.7 -1.9 11.85 -7.1 Z"],
2012
+ ["#FFFFFF", "M 6.5 -12.45 Q 9.4 -15.35 9.4 -19.45 9.4 -23.55 6.5 -26.45 3.55 -29.4 -0.55 -29.4 -4.65 -29.4 -7.55 -26.45 -10.5 -23.55 -10.5 -19.45 -10.5 -15.35 -7.55 -12.45 -4.65 -9.5 -0.55 -9.5 3.55 -9.5 6.5 -12.45 Z"],
2013
+ ["#477CA6", "M -16 -30.45 Q -16.95 -29.9 -17.25 -28.8 L -19.45 -20.6 Q -19.75 -19.5 -19.2 -18.45 -18.55 -17.5 -17.45 -17.2 L -17.4 -17.2 Q -16.3 -16.9 -15.3 -17.4 -14.3 -18.05 -14 -19.15 L -11.85 -27.35 Q -11.55 -28.45 -12.15 -29.45 -12.7 -30.45 -13.75 -30.75 L -13.8 -30.75 Q -14.9 -31.05 -16 -30.45 Z M 16.3 -28.8 Q 16 -29.9 15.05 -30.45 13.95 -31.05 12.85 -30.75 L 12.8 -30.75 Q 11.7 -30.45 11.15 -29.45 10.55 -28.45 10.85 -27.35 L 13.05 -19.15 Q 13.35 -18.05 14.35 -17.4 15.35 -16.9 16.45 -17.2 L 16.5 -17.2 Q 17.55 -17.5 18.2 -18.45 18.75 -19.5 18.45 -20.6 L 16.3 -28.8 Z"],
2014
+ ["#5393C5", "M -24.65 -11 Q -27 -8.65 -27 -5.35 -27 -2.05 -24.65 0.25 -22.25 2.65 -19 2.65 -15.7 2.65 -13.3 0.25 -11 -2.05 -11 -5.35 -11 -8.65 -13.3 -11 -15.7 -13.4 -19 -13.4 -22.25 -13.4 -24.65 -11 Z M 12.35 -11 Q 10 -8.65 10 -5.35 10 -2.05 12.35 0.25 14.75 2.65 18 2.65 21.3 2.65 23.7 0.25 26 -2.05 26 -5.35 26 -8.65 23.7 -11 21.3 -13.4 18 -13.4 14.75 -13.4 12.35 -11 Z"],
2015
+ ["#477CA6", "M -23.75 -5.35 Q -23.75 -3.35 -22.35 -1.95 -20.95 -0.5 -19 -0.5 -16.95 -0.5 -15.55 -1.95 -14.15 -3.35 -14.15 -5.35 -14.15 -7.3 -15.55 -8.75 -16.95 -10.15 -19 -10.15 -20.95 -10.15 -22.35 -8.75 -23.75 -7.3 -23.75 -5.35 Z M 18 -10.15 Q 16.05 -10.15 14.65 -8.75 13.2 -7.3 13.2 -5.35 13.2 -3.35 14.65 -1.95 16.05 -0.5 18 -0.5 20 -0.5 21.45 -1.95 22.85 -3.35 22.85 -5.35 22.85 -7.3 21.45 -8.75 20 -10.15 18 -10.15 Z"],
2016
+ ["#334E63", "M -18.2 8 L -10.2 8 Q -10.25 -1.8 -19.5 -4.7 L -18.2 8 Z M 11.7 8 Q 17.05 7.3 17.8 2.3 L 18.7 -6.45 Q 9.65 -1.8 9.3 8 L 11.7 8 Z"],
2017
+ ["#5393C5", "M 18.7 -6.45 L 20.45 -23.35 -21.4 -23.35 -19.5 -4.7 Q -10.25 -1.8 -10.2 8 L 9.3 8 Q 9.65 -1.8 18.7 -6.45 Z"],
2018
+ ["#477CA6", "M 5.45 -4.6 Q 7.8 -6.95 7.8 -10.25 7.8 -13.6 5.45 -15.9 3.1 -18.25 -0.2 -18.25 -3.55 -18.25 -5.85 -15.9 -8.2 -13.6 -8.2 -10.25 -8.2 -6.95 -5.85 -4.6 -3.55 -2.25 -0.2 -2.25 3.1 -2.25 5.45 -4.6 Z"],
2019
+ ["#FFCC00", "M 3.55 -6.5 Q 5.15 -8.05 5.15 -10.25 5.15 -12.5 3.55 -14.1 2 -15.6 -0.2 -15.6 -2.45 -15.6 -4.05 -14.1 -5.6 -12.5 -5.6 -10.25 -5.6 -8.05 -4.05 -6.5 -2.45 -4.9 -0.2 -4.9 2 -4.9 3.55 -6.5 Z"],
2020
+ ["#FFFFFF", "M 2.25 -8.75 Q 2.9 -9.35 2.9 -10.25 2.9 -11.15 2.25 -11.8 1.7 -12.4 0.8 -12.4 -0.1 -12.4 -0.75 -11.8 -1.35 -11.15 -1.35 -10.25 -1.35 -9.35 -0.75 -8.75 -0.1 -8.1 0.8 -8.1 1.7 -8.1 2.25 -8.75 Z"],
2021
+ ["#334E63", "M 7.1 3.45 Q 7.1 2.25 6.2 1.35 5.6 0.75 4.9 0.6 4.95 0.85 4.95 1.15 L 4.95 1.2 Q 4.95 2.15 4.25 2.8 3.6 3.5 2.65 3.5 L -3.8 3.5 Q -4.75 3.5 -5.45 2.8 -6.05 2.15 -6.05 1.2 L -6.05 1.15 Q -6.05 0.85 -6 0.6 -6.75 0.75 -7.35 1.35 -8.2 2.25 -8.2 3.45 -8.2 4.65 -7.35 5.5 -6.45 6.4 -5.3 6.4 L 4.15 6.4 Q 5.35 6.4 6.2 5.5 7.1 4.65 7.1 3.45 Z"],
2022
+ ["#BFD4E5", "M 4.95 1.15 Q 4.95 0.85 4.9 0.6 4.55 0.5 4.15 0.5 L -5.3 0.5 Q -5.65 0.5 -6 0.6 -6.05 0.85 -6.05 1.15 L -6.05 1.2 Q -6.05 2.15 -5.45 2.8 -4.75 3.5 -3.8 3.5 L 2.65 3.5 Q 3.6 3.5 4.25 2.8 4.95 2.15 4.95 1.2 L 4.95 1.15 Z"],
2023
+ ["#477CA6", "M 15.15 -7.9 Q 16.05 -8.75 16.05 -9.95 16.05 -11.15 15.15 -12.05 14.3 -12.9 13.1 -12.9 11.9 -12.9 11 -12.05 10.15 -11.15 10.15 -9.95 10.15 -8.75 11 -7.9 11.9 -7.05 13.1 -7.05 14.3 -7.05 15.15 -7.9 Z M -11.55 -7.9 Q -10.65 -8.75 -10.65 -9.95 -10.65 -11.15 -11.55 -12.05 -12.4 -12.9 -13.55 -12.9 -14.75 -12.9 -15.65 -12.05 -16.5 -11.15 -16.5 -9.95 -16.5 -8.75 -15.65 -7.9 -14.75 -7.05 -13.55 -7.05 -12.4 -7.05 -11.55 -7.9 Z"]]
2024
+ },
2025
+ ZB: { // Zombie (used by the Shambler alien)
2026
+ arm: [["#4C4EAA", "M 11.75 7.25 L 13.95 7.25 Q 11.3 6.05 9.95 3.9 7.5 0.05 4 -3.35 2.25 -4.95 -0.2 -5 -2.65 -4.95 -4.4 -3.2 -6.1 -1.4 -6 1 -6 3.45 -4.3 5.15 -1.9 7.45 -0.3 10.1 1.4 12.4 1.3 15.2 L 1.75 14.05 3 10.7 Q 5.5 12.15 7.45 12.2 8.45 10.1 8.25 7.25 L 11.75 7.25 Z"],
2027
+ ["#3DBF9E", "M 13.15 10.6 Q 13 10.2 12.85 9.8 L 2.55 16.5 Q 2.95 18.05 3.15 19.75 3.4 22.15 5.3 23.7 7.25 25.2 9.7 25 12.1 24.7 13.65 22.75 15.2 20.9 14.95 18.4 14.6 15.5 13.85 12.85 13.55 11.7 13.15 10.6 Z"],
2028
+ ["#29A686", "M 12.85 9.8 Q 12.35 8.5 11.75 7.25 L 8.25 7.25 Q 8.45 10.1 7.45 12.2 5.5 12.15 3 10.7 L 1.75 14.05 Q 2.2 15.25 2.55 16.5 L 12.85 9.8 Z"]],
2029
+ leg: [["#3DBF9E", "M 5.3 14 L 5.45 9 -0.5 9 Q -2.2 12.35 -6.55 12.5 L -6.6 14 5.25 14 5.3 14 Z"],
2030
+ ["#3C3E41", "M 10.9 23 L 11 20 5.25 14 -6.6 14 -7 23 10.9 23 Z"],
2031
+ ["#EEA160", "M 5.9 -0.2 Q 5.9 -2.6 4 -4.3 2.15 -6 -0.55 -6 -3.2 -6 -5.1 -4.3 -7 -2.6 -7 -0.2 L -6.45 9 -6.55 12.5 Q -2.2 12.35 -0.5 9 L 5.45 9 5.9 -0.2 Z"]],
2032
+ hand: [["#29A686", "M -9.2 7.6 L -9.25 6.65 Q -8.75 0.45 -5.45 -2.95 -14.15 3.5 -9.25 7.65 L -9.2 7.6 Z"],
2033
+ ["#3DBF9E", "M 7 -1.65 Q 6.95 -3 5.5 -3 L -5.45 -2.95 Q -8.75 0.45 -9.25 6.65 L -9.2 7.6 -9.25 7.65 Q -9.3 9.3 -9.25 11.15 L -9.25 11.55 Q -9.25 12.95 -7.1 13.35 -3.95 14.05 -0.95 14 7.55 13.95 7 -0.75 L 7 -1.65 Z"]],
2034
+ body: [["#EEA160", "M 14.55 2.8 L 11.8 2.8 7.65 -1.65 3.15 2.8 -15.5 2.8 Q -15.4 6.1 -14 8 L 13.05 8 Q 14.45 6.1 14.55 2.8 Z"],
2035
+ ["#676AC6", "M 17.75 -15.8 Q 18.8 -21.2 15.15 -21 L -16.15 -21 Q -19.8 -21.2 -18.75 -15.8 L -15.5 -2.15 -15.5 2.8 3.15 2.8 7.65 -1.65 11.8 2.8 14.55 2.8 14.55 -2.15 17.75 -15.8 Z"],
2036
+ ["#29A686", "M 7.55 -28.45 Q 4.4 -31 0 -31 -4.4 -31 -7.55 -28.45 -10.65 -25.75 -10.65 -22 -10.65 -21.75 -10.6 -21.5 -10.45 -18.1 -7.55 -15.7 -4.4 -13 0 -13 4.4 -13 7.55 -15.7 10.45 -18.1 10.6 -21.5 10.65 -21.75 10.65 -22 10.65 -25.75 7.55 -28.45 Z"],
2037
+ ["#4C4EAA", "M -13.1 -22 L -15.15 -18.4 -2.05 -10.8 0 -14.45 -13.1 -22 M 13.1 -22 L 0 -14.45 2.05 -10.8 15.15 -18.4 13.1 -22 Z"],
2038
+ ["#676AC6", "M 2.05 -10.8 L 0 -14.45 -2.05 -10.8 2.05 -10.8 Z"],
2039
+ ["#3DBF9E", "M 0.95 -8.35 Q 0.8 -8.45 0.7 -8.35 -0.65 -7.95 -3.15 -5.55 L -3.2 -5.55 Q -5.55 -3 -6 -1.75 -6.05 -1.6 -6 -1.5 -5.95 -1.35 -5.8 -1.25 -5.65 -1.2 -5.55 -1.25 -4.2 -1.65 -1.65 -3.95 L -1.65 -4 Q 0.7 -6.55 1.1 -7.9 1.15 -8.05 1.1 -8.15 1.05 -8.3 0.95 -8.35 Z"],
2040
+ ["#3DBF9E", "M -13.1 -13.75 Q -13.15 -13.6 -13.15 -13.5 -13.1 -12.1 -11.4 -9.05 L -11.4 -9 Q -9.55 -6.05 -8.5 -5.3 -8.35 -5.25 -8.25 -5.25 -8.05 -5.25 -7.95 -5.35 -7.85 -5.5 -7.85 -5.6 -7.9 -7 -9.5 -10.1 -11.4 -13 -12.6 -13.75 -12.75 -13.85 -12.85 -13.85 -13 -13.8 -13.1 -13.75 Z"]],
2041
+ head: [["#3DBF9E", "M -24.65 -11.25 Q -27 -8.9 -27 -5.5 -27 -2.2 -24.65 0.2 -22.25 2.6 -18.95 2.6 -15.65 2.6 -13.25 0.2 -10.9 -2.2 -10.9 -5.5 -10.9 -8.9 -13.25 -11.25 -15.65 -13.65 -18.95 -13.65 -22.25 -13.65 -24.65 -11.25 Z M 12.6 -11.25 Q 10.25 -8.9 10.25 -5.5 10.25 -2.2 12.6 0.2 15 2.6 18.3 2.6 21.6 2.6 24 0.2 26.35 -2.2 26.35 -5.5 26.35 -8.9 24 -11.25 21.6 -13.65 18.3 -13.65 15 -13.65 12.6 -11.25 Z"],
2042
+ ["#29A686", "M -23.75 -5.5 Q -23.75 -3.5 -22.35 -2.1 -20.95 -0.6 -18.95 -0.6 -16.9 -0.6 -15.5 -2.1 -14.1 -3.5 -14.1 -5.5 -14.1 -7.45 -15.5 -9 -16.9 -10.4 -18.95 -10.4 -20.95 -10.4 -22.35 -9 -23.75 -7.45 -23.75 -5.5 M 18.3 -10.4 Q 16.3 -10.4 14.9 -9 13.45 -7.45 13.45 -5.5 13.45 -3.5 14.9 -2.1 16.3 -0.6 18.3 -0.6 20.3 -0.6 21.75 -2.1 23.15 -3.5 23.15 -5.5 23.15 -7.45 21.75 -9 20.3 -10.4 18.3 -10.4 Z"],
2043
+ ["#3DBF9E", "M 11.75 8 Q 17.1 7.3 17.85 2.15 L 20.5 -24.2 -21.5 -24.2 -18.25 8 11.75 8 Z"],
2044
+ ["#3C3E41", "M 26.6 -36.25 Q 26.5 -36.45 26.4 -36.65 L 20.6 -36.1 Q 19.8 -28.75 12.65 -25.4 14.2 -34.3 11.55 -36.05 7.8 -37.4 3.6 -42.05 1.65 -44.5 -0.1 -42.2 -5.05 -35 -11.3 -30.35 -15.85 -32 -20.5 -35.35 -22.35 -31.2 -22.45 -30.8 -23.55 -27.3 -23.3 -23.8 -23.2 -22.55 -22.95 -21.2 L -19.05 -15.6 -19.05 -20.75 Q -18.25 -20.9 -17.45 -21.05 -17.3 -21 -17.15 -20.95 -11.85 -18.65 -4.35 -18.65 3.15 -18.65 8.45 -20.95 9.9 -21.6 10.95 -22.3 15.6 -21.75 20.3 -20.75 L 20.3 -14.95 21.5 -17.4 Q 25.75 -25.7 26.15 -26.8 26.65 -28.4 26.85 -30 L 26.85 -30.1 Q 27.2 -33.2 26.6 -36.25 Z"],
2045
+ ["#454A51", "M 26.6 -36.25 Q 25.95 -39.35 24.25 -42.4 L 13.65 -40.9 Q 8.9 -40.4 2.75 -46.85 0.75 -49.15 -0.7 -46.85 -4.6 -39.5 -11.4 -35.35 -15.1 -33.7 -20.5 -35.35 -15.85 -32 -11.3 -30.35 -5.05 -35 -0.1 -42.2 1.65 -44.5 3.6 -42.05 7.8 -37.4 11.55 -36.05 14.2 -34.3 12.65 -25.4 19.8 -28.75 20.6 -36.1 L 26.4 -36.65 Q 26.5 -36.45 26.6 -36.25 Z"],
2046
+ ["#29A686", "M 2.25 -2.4 Q 3.3 -3.1 3.3 -4 3.3 -4.9 2.25 -5.65 1.25 -6.3 -0.2 -6.3 -1.6 -6.3 -2.6 -5.65 -3.6 -4.9 -3.6 -4 -3.6 -3.1 -2.6 -2.4 -1.6 -1.65 -0.2 -1.65 1.25 -1.65 2.25 -2.4 Z"],
2047
+ ["#29A686", "M 16.65 -4.35 Q 16.65 -6.85 15 -8.7 13.25 -10.5 10.7 -10.5 8.25 -10.5 6.6 -8.7 4.85 -6.85 4.85 -4.35 4.85 -1.85 6.6 0 8.25 1.85 10.7 1.85 13.25 1.85 15 0 16.65 -1.85 16.65 -4.35 Z"],
2048
+ ["#FFCC00", "M 7.15 -5.7 Q 7.15 -4.25 8.15 -3.25 9.05 -2.2 10.45 -2.2 11.95 -2.2 12.9 -3.25 13.85 -4.25 13.85 -5.7 13.85 -7.1 12.9 -8.2 11.95 -9.2 10.45 -9.2 9.05 -9.2 8.15 -8.2 7.15 -7.1 7.15 -5.7 Z"],
2049
+ ["#29A686", "M -5.5 -3.85 Q -3.75 -5.65 -3.75 -8.2 -3.75 -10.7 -5.5 -12.5 -7.15 -14.3 -9.6 -14.3 -12.15 -14.3 -13.9 -12.5 -15.55 -10.7 -15.55 -8.2 -15.55 -5.65 -13.9 -3.85 -12.15 -2 -9.6 -2 -7.15 -2 -5.5 -3.85 Z"],
2050
+ ["#454A51", "M -16.05 -17.3 L -15.15 -13.8 -6.05 -16.3 -6.95 -19.75 -16.05 -17.3 Z M 15.55 -10.7 L 15.95 -14.25 6.55 -15.4 6.2 -11.8 15.55 -10.7 Z"],
2051
+ ["#FFCC00", "M -7.05 -7.05 Q -6.05 -8.05 -6.05 -9.55 -6.05 -10.95 -7.05 -12 -7.95 -13 -9.35 -13 -10.85 -13 -11.8 -12 -12.75 -10.95 -12.75 -9.55 -12.75 -8.05 -11.8 -7.05 -10.85 -6 -9.35 -6 -7.95 -6 -7.05 -7.05 Z"],
2052
+ ["#3C3E41", "M -20.6 -23.3 L -20.65 -23.3 Q -21.65 -23.15 -22.3 -22.3 -22.95 -21.5 -22.8 -20.5 L -21.5 -9.95 Q -21.4 -9 -20.55 -8.3 -19.75 -7.65 -18.75 -7.8 L -18.7 -7.8 Q -17.7 -7.9 -17.1 -8.75 -16.5 -9.55 -16.6 -10.55 L -17.9 -21.05 Q -18.05 -22.1 -18.85 -22.75 -19.6 -23.35 -20.6 -23.3 M 18.2 -22.75 Q 17.4 -22.1 17.25 -21.05 L 15.8 -9.15 Q 15.7 -8.15 16.3 -7.3 16.9 -6.5 17.9 -6.4 L 17.95 -6.4 Q 18.95 -6.25 19.75 -6.85 20.6 -7.55 20.7 -8.55 L 22.15 -20.5 Q 22.3 -21.5 21.65 -22.3 21 -23.15 20 -23.3 L 19.95 -23.3 Q 18.95 -23.35 18.2 -22.75 Z"],
2053
+ ["#197E64", "M 3.4 1.3 Q 2 0.6 0.1 0.6 -1.8 0.6 -3.2 1.3 -4.85 2.15 -4.8 3.4 -4.8 4.15 -4.15 4.8 -3.9 5.05 -3.5 5.1 -3.1 5.1 -2.8 4.95 -2.45 4.75 -2.4 4.45 -2.25 4.15 -1.75 3.95 -0.9 3.5 0.3 3.5 1.5 3.5 2.35 3.95 2.75 4.1 2.85 4.35 3.05 4.6 3.4 4.75 3.8 4.85 4.15 4.75 4.55 4.6 4.7 4.35 5 3.9 5 3.4 5.05 2.15 3.4 1.3 Z"]]
2054
+ }
2055
+ };
2056
+
2057
+ </script>
2058
+ <script>
2059
+ // Star Squad: the ink house style, hero rigs (Kenney parts from kenney.js), weapons, headgear and aliens
2060
+ "use strict";
2061
+ // Per-hero base character + recolors (m: limbs and body; per-part maps override; head maps only apply to head/back hair)
2062
+ const HART = [
2063
+ { b: "MP", m: { "#EEA160": "#6b8f47", "#D78B49": "#4f6b34", "#E7F3FC": "#d8c99b", "#BFD4E5": "#4a3f2f", "#676AC6": "#5a6b3c", "#4C4EAA": "#434f2c" }, arm: { "#E7F3FC": "#6b8f47", "#BFD4E5": "#4f6b34" }, leg: { "#FFFFFF": "#3b3631", "#EEA160": "#26221f" } },
2064
+ { b: "MP", m: { "#EEA160": "#7b2cbf", "#D78B49": "#5a189a", "#E7F3FC": "#e0aaff", "#BFD4E5": "#ffd60a", "#676AC6": "#3c096c", "#4C4EAA": "#240046" }, arm: { "#E7F3FC": "#7b2cbf" }, leg: { "#FFFFFF": "#5a189a", "#EEA160": "#240046" }, hl: { 0: "#ced4da", 1: "#e9ecef", 7: "#f8f9fa", 8: "#f8f9fa", 10: "#e9ecef", 11: "#e9ecef" } },
2065
+ { b: "MA", m: { "#3DBF7B": "#adb5bd", "#25A863": "#6c757d", "#EEA160": "#495057", "#D78B49": "#343a40", "#FFFFFF": "#ced4da" } },
2066
+ { b: "FA", m: { "#79ADCB": "#52b788", "#5A93B4": "#2d6a4f", "#69A1C2": "#40916c", "#EEA160": "#6f4e37" }, head: { "#71A2BE": "#2d6a4f" } },
2067
+ { b: "FP", m: { "#3DBF7B": "#f4a261", "#25A863": "#e76f51", "#676AC6": "#264653", "#3C3E41": "#6f4518" }, leg: { "#BFD4E5": "#495057", "#FFFFFF": "#343a40" } },
2068
+ { b: "RB", m: { "#FF6699": "#4cc9f0" } }
2069
+ ];
2070
+ let ART = null, ZART = null;
2071
+ function bakeSet(B, h) {
2072
+ const o = {};
2073
+ for (const p in B) {
2074
+ const mp = p === "head" || p === "back" ? (h.head || {}) : Object.assign({}, h.m, h[p] || {});
2075
+ o[p] = B[p].map(([f, d], i) => { const col = (p === "head" && h.hl && h.hl[i]) || mp[f] || f; return [col, new Path2D(d), mix(col, "#000000", .28)]; });
2076
+ }
2077
+ return o;
2078
+ }
2079
+ function bakeArt() { ART = HART.map(h => bakeSet(KN[h.b], h)); ZART = bakeSet(KN.ZB, {}); }
2080
+ // each part is stroked first, then filled, so only its silhouette keeps a dark outline; back limbs use the darker shade, hit flashes a solid colour
2081
+ function kp(c, L, dark, solid) {
2082
+ c.lineJoin = "round"; c.lineWidth = 5; c.strokeStyle = "rgba(14,12,24,.9)";
2083
+ for (const l of L) c.stroke(l[1]);
2084
+ for (const l of L) { c.fillStyle = solid || (dark ? l[2] : l[0]); c.fill(l[1]); }
2085
+ }
2086
+ function limb(c, L, x, y, r, dark, solid) { c.save(); c.translate(x, y); c.rotate(r); kp(c, L, dark, solid); c.restore(); }
2087
+ // Shambler: Kenney zombie on the same rig, drawn with its feet at (0, 0); lies down while "dead", then climbs back up
2088
+ function zombie(c, m, f, tm, now, fl, sd) {
2089
+ if (!ZART) bakeArt();
2090
+ const Z = ZART, wf = fl ? "#fff" : null, down = m.dn && m.dn > now, wu = m.wu && m.wu > now, ch = m.ch && m.ch > now;
2091
+ c.save(); c.scale(f * .58, .58);
2092
+ if (down) c.rotate(-1.45 * clamp((m.dn - now) / .5, 0, 1));
2093
+ const s = down ? 0 : Math.sin(tm * (ch ? 16 : 5) + sd), lf = s * (ch ? .6 : .35), by = 23 * (1 - Math.cos(lf));
2094
+ const aF = wu ? -.75 + Math.sin(tm * 22) * .12 : -.08 + Math.sin(tm * 3 + sd) * .12, aB = aF + .18;
2095
+ const ht = wu ? -.3 : .18 + Math.sin(tm * 2.3 + sd) * .08;
2096
+ const sbx = -7, sfx = 11, sy = -42.2 + by;
2097
+ const hbx = sbx + Math.cos(aB) * ARML, hby = sy + Math.sin(aB) * ARML, hfx = sfx + Math.cos(aF) * ARML, hfy = sy + Math.sin(aF) * ARML;
2098
+ limb(c, Z.leg, -4, -23.05 + by, -lf, true, wf); limb(c, Z.leg, 4, -23.05 + by, lf, false, wf);
2099
+ c.translate(0, -23 + by); c.rotate(ch ? .3 : .12); c.translate(0, 23 - by); // hunched forward from the hips
2100
+ limb(c, Z.arm, sbx, sy, aB - ARM0, true, wf); limb(c, Z.hand, hbx, hby, aB - ARM0, true, wf);
2101
+ c.save(); c.translate(0, -25.95 + by); kp(c, Z.body, false, wf); c.restore();
2102
+ limb(c, Z.arm, sfx, sy, aF - ARM0, false, wf);
2103
+ c.save(); c.translate(3.3, -53.8 + by); c.rotate(ht); kp(c, Z.head, false, wf);
2104
+ if (!fl && !down) { c.globalCompositeOperation = "lighter"; c.globalAlpha = .45 + .25 * Math.sin(tm * 6 + sd); circ(c, 10.5, -5.7, 8, "#ffcc00"); circ(c, -9.4, -9.5, 7, "#ffcc00"); }
2105
+ c.restore();
2106
+ limb(c, Z.hand, hfx, hfy, aF - ARM0, false, wf);
2107
+ c.restore();
2108
+ }
2109
+ const ARM0 = Math.atan2(22.1, 8.6), ARML = Math.hypot(22.1, 8.6); // Kenney arm hangs at this angle; the hand sits at its tip
2110
+ function muzzle(c, x, y, col) { c.fillStyle = col; c.beginPath(); for (let i = 0; i < 10; i++) { const a = i * Math.PI / 5, r = i % 2 ? 6 : 19; c.lineTo(x + Math.cos(a) * r, y + Math.sin(a) * r); } c.closePath(); c.fill(); circ(c, x, y, 5, "#fff"); }
2111
+ function inkFill(c, col) { c.fillStyle = col; c.fill(); c.stroke(); }
2112
+ // house style shared by props, loot and aliens so they match the Kenney heroes: flat colour inside a dark ink outline
2113
+ const INK = "rgba(14,12,24,.9)";
2114
+ function ink(c, w) { c.strokeStyle = INK; c.lineWidth = w || 3; c.lineJoin = "round"; }
2115
+ function inked(c, col, w) { ink(c, w); c.stroke(); c.fillStyle = col; c.fill(); } // stroke first: the fill covers the inner half, leaving a clean outline
2116
+ function ic(c, x, y, r, col, w) { c.beginPath(); c.arc(x, y, Math.max(.1, r), 0, 6.283); inked(c, col, w || 2.5); }
2117
+ // weapon in the hand frame: +x is the aim direction, origin at the grip
2118
+ function weapon(c, k, t, fire, mf, aim) {
2119
+ c.strokeStyle = "#14121c"; c.lineWidth = 2.2; c.lineJoin = "round";
2120
+ if (k === 0) { // assault rifle
2121
+ c.beginPath(); c.moveTo(-17, -4); c.lineTo(-2, -4); c.lineTo(-2, 5); c.lineTo(-15, 8); c.closePath(); inkFill(c, "#5c4033");
2122
+ c.beginPath(); c.moveTo(6, 4); c.lineTo(13, 4); c.lineTo(16, 15); c.lineTo(9, 16); c.closePath(); inkFill(c, "#1f2133");
2123
+ rr(c, -4, -6, 30, 10, 2); inkFill(c, "#2b2d42");
2124
+ rr(c, 2, -10, 13, 4, 1); inkFill(c, "#3d405b");
2125
+ c.beginPath(); c.rect(26, -3.5, 16, 4.5); inkFill(c, "#8d99ae");
2126
+ c.beginPath(); c.rect(40, -5, 5, 7.5); inkFill(c, "#495057");
2127
+ c.fillStyle = "rgba(255,255,255,.22)"; c.fillRect(-2, -5, 26, 1.6);
2128
+ if (mf) muzzle(c, 50, -1.2, "#ffe066");
2129
+ } else if (k === 4) { // rivet gun with pressure tank
2130
+ rr(c, -8, -15, 18, 8, 4); inkFill(c, "#e76f51");
2131
+ rr(c, -5, -8, 28, 14, 5); inkFill(c, "#495057");
2132
+ c.beginPath(); c.rect(5, -8, 5, 14); inkFill(c, "#f77f00");
2133
+ rr(c, 22, -4, 11, 7, 1.5); inkFill(c, "#343a40");
2134
+ c.fillStyle = "#ffd166"; c.fillRect(31, -2, 2.5, 3);
2135
+ if (mf) muzzle(c, 38, -.5, "#ffb703");
2136
+ } else if (k === 5) { // plasma arm cannon
2137
+ rr(c, -12, -11, 46, 22, 8); inkFill(c, "#5c6773");
2138
+ rr(c, -8, -7, 38, 14, 6); c.fillStyle = "#8d99ae"; c.fill();
2139
+ c.save(); c.globalCompositeOperation = "lighter"; c.fillStyle = "#4cc9f0";
2140
+ for (let i = 0; i < 3; i++) { c.globalAlpha = .55 + .45 * Math.sin(t * 8 - i); c.fillRect(4 + i * 8, -11, 3, 22); }
2141
+ c.restore();
2142
+ circ(c, 34, 0, 9, "#2b2d42"); circ(c, 34, 0, 5.5 + (mf ? 2 : 0), mf ? "#fff" : "#4cc9f0");
2143
+ if (mf) muzzle(c, 42, 0, "#4cc9f0");
2144
+ } else if (k === 3) { // longbow: string pulled back while firing
2145
+ const px = fire ? (mf ? -24 : -18) : -12;
2146
+ c.strokeStyle = "#eee"; c.lineWidth = 1.2; c.beginPath(); c.moveTo(-12, -22); c.lineTo(px, 0); c.lineTo(-12, 22); c.stroke();
2147
+ c.lineCap = "round"; c.strokeStyle = "#14121c"; c.lineWidth = 7; c.beginPath(); c.arc(-22, 0, 24.5, -1.15, 1.15); c.stroke();
2148
+ c.strokeStyle = "#9c6644"; c.lineWidth = 4.5; c.stroke();
2149
+ c.strokeStyle = "#ddb892"; c.lineWidth = 1.2; c.beginPath(); c.arc(-22, 0, 25.5, -1, 1); c.stroke(); c.lineCap = "butt";
2150
+ if (fire && !mf) { c.strokeStyle = "#d8f3dc"; c.lineWidth = 2; c.beginPath(); c.moveTo(px, 0); c.lineTo(16, 0); c.stroke(); c.fillStyle = "#adb5bd"; c.beginPath(); c.moveTo(15, -4); c.lineTo(23, 0); c.lineTo(15, 4); c.fill(); c.fillStyle = "#e63946"; c.fillRect(px - 1, -3, 6, 2); c.fillRect(px - 1, 1, 6, 2); }
2151
+ } else if (k === 2) { // broadsword
2152
+ c.beginPath(); c.moveTo(6, -4.5); c.lineTo(50, -4.5); c.lineTo(60, 0); c.lineTo(50, 4.5); c.lineTo(6, 4.5); c.closePath(); inkFill(c, "#e9ecef");
2153
+ c.fillStyle = "#adb5bd"; c.fillRect(9, -1, 38, 2);
2154
+ c.fillStyle = "rgba(255,255,255,.8)"; c.fillRect(9, -3.6, 40, 1.2);
2155
+ rr(c, 1, -10, 5, 20, 2); inkFill(c, "#ffd60a");
2156
+ c.beginPath(); c.rect(-9, -2.6, 11, 5.2); inkFill(c, "#6f4518");
2157
+ c.beginPath(); c.arc(-10, 0, 3.4, 0, 6.283); inkFill(c, "#ffd60a");
2158
+ } else if (k === 1) { // staff stays near-vertical, tilting toward the target while casting
2159
+ c.rotate(-Math.PI / 2 + (fire ? .55 : .12) - aim);
2160
+ c.lineCap = "round"; c.strokeStyle = "#14121c"; c.lineWidth = 7.5; c.beginPath(); c.moveTo(-34, 0); c.lineTo(52, 0); c.stroke();
2161
+ c.strokeStyle = "#7f5539"; c.lineWidth = 4.5; c.stroke(); c.lineCap = "butt";
2162
+ c.strokeStyle = "#ffd60a"; c.lineWidth = 2; c.beginPath(); c.arc(58, 0, 8, .6, 5.7); c.stroke();
2163
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .4; circ(c, 60, 0, (mf ? 22 : 14) + Math.sin(t * 6) * 2, "#c77dff"); c.restore();
2164
+ circ(c, 60, 0, 7, mf ? "#fff" : "#e0aaff");
2165
+ }
2166
+ }
2167
+ // helmets, hats and beards in the head frame
2168
+ function headGear(c, k, t) {
2169
+ c.strokeStyle = "#14121c"; c.lineWidth = 2.5; c.lineJoin = "round";
2170
+ if (k === 0) { // combat helmet with goggles
2171
+ c.beginPath(); c.ellipse(-.5, -27, 26, 20, 0, Math.PI, 0); c.closePath(); inkFill(c, "#556b2f");
2172
+ c.fillStyle = "rgba(255,255,255,.13)"; c.beginPath(); c.ellipse(-8, -38, 11, 5, -.3, 0, 6.283); c.fill();
2173
+ rr(c, -29, -30, 57, 7, 3.5); inkFill(c, "#435725");
2174
+ c.fillStyle = "#2b2b2b"; c.fillRect(-24, -40, 47, 5);
2175
+ for (const gx of [-8, 8]) { c.beginPath(); c.arc(gx, -38, 6, 0, 6.283); inkFill(c, "#4cc9f0"); c.fillStyle = "rgba(255,255,255,.6)"; c.fillRect(gx - 3, -41, 2.5, 2.5); }
2176
+ } else if (k === 1) { // wizard hat + beard
2177
+ c.beginPath(); c.moveTo(-17, -6); c.quadraticCurveTo(-18, 14, -1, 27); c.quadraticCurveTo(15, 14, 17, -6); c.quadraticCurveTo(9, 1, 0, -.5); c.quadraticCurveTo(-9, 1, -17, -6); c.closePath(); inkFill(c, "#f8f9fa");
2178
+ c.beginPath(); c.ellipse(-.5, 0, 9, 3.2, 0, 0, 6.283); c.fillStyle = "#e9ecef"; c.fill();
2179
+ c.beginPath(); c.ellipse(0, -25, 32, 7, 0, 0, 6.283); inkFill(c, "#5a189a");
2180
+ const tip = Math.sin(t * 3) * 4;
2181
+ c.beginPath(); c.moveTo(-21, -27); c.quadraticCurveTo(-12, -52, -16 + tip, -84); c.quadraticCurveTo(2, -56, 21, -27); c.closePath(); inkFill(c, "#7b2cbf");
2182
+ c.fillStyle = "#ffd60a"; c.beginPath(); c.moveTo(-20, -30); c.quadraticCurveTo(0, -35, 20, -30); c.lineTo(19, -35); c.quadraticCurveTo(0, -40, -18.5, -35); c.closePath(); c.fill();
2183
+ for (const [sx, sy, sr] of [[-4, -52, 3.4], [6, -44, 2.4], [-11, -62, 2]]) { c.beginPath(); for (let i = 0; i < 8; i++) { const a = i * Math.PI / 4, r = i % 2 ? sr * .4 : sr; c.lineTo(sx + Math.cos(a) * r, sy + Math.sin(a) * r); } c.fill(); }
2184
+ } else if (k === 2) { // horned helmet with nose guard
2185
+ for (const s of [-1, 1]) { c.beginPath(); c.moveTo(s * 19, -36); c.quadraticCurveTo(s * 40, -40, s * 36, -64); c.quadraticCurveTo(s * 30, -46, s * 13, -44); c.closePath(); inkFill(c, "#fefae0"); }
2186
+ c.beginPath(); c.ellipse(0, -28, 25, 21, 0, Math.PI, 0); c.closePath(); inkFill(c, "#ced4da");
2187
+ c.fillStyle = "rgba(255,255,255,.35)"; c.beginPath(); c.ellipse(-8, -40, 10, 4.5, -.3, 0, 6.283); c.fill();
2188
+ rr(c, -27, -31, 54, 7, 3); inkFill(c, "#8d99ae");
2189
+ rr(c, -2.5, -27, 5, 13, 2); inkFill(c, "#8d99ae");
2190
+ c.fillStyle = "#6c757d"; for (let i = -20; i <= 20; i += 10) c.fillRect(i - 1, -29, 2, 2);
2191
+ } else if (k === 4) { // hard hat with headlamp
2192
+ c.beginPath(); c.ellipse(-.5, -30, 26, 18, 0, Math.PI, 0); c.closePath(); inkFill(c, "#ffd60a");
2193
+ rr(c, -3.5, -47, 7, 17, 3); c.fillStyle = "#ffe566"; c.fill();
2194
+ rr(c, -31, -32, 61, 6, 3); inkFill(c, "#f4c20d");
2195
+ c.beginPath(); c.arc(-.5, -38, 4.5, 0, 6.283); inkFill(c, "#fff3b0");
2196
+ } else if (k === 5) { // antenna
2197
+ c.strokeStyle = "#334E63"; c.lineWidth = 3; c.beginPath(); c.moveTo(-.5, -36); c.lineTo(-.5, -48); c.stroke();
2198
+ circ(c, -.5, -50, 4, Math.floor(t * 2) % 2 ? "#ff006e" : "#4cc9f0");
2199
+ }
2200
+ }
2201
+
2202
+ // ---------- Drawing: heroes ----------
2203
+ function hero(c, k, x, y, f, t, mv, air, fire, sc) {
2204
+ if (!ART) bakeArt();
2205
+ const A = ART[k], S = .6;
2206
+ c.save(); c.translate(x, y); c.scale(f * sc * S, sc * S);
2207
+ const mf = fire && Math.floor(t * 30) % 2 === 0, gun = k === 0 || k === 4 || k === 5;
2208
+ const s = mv && !air ? Math.sin(t * 11) : 0;
2209
+ const lf = air ? -.55 : s * .6, lb = air ? .45 : -s * .6;
2210
+ const by = air ? -2 : mv ? 23 * (1 - Math.cos(s * .6)) : Math.sin(t * 3) * .7; // hip drop keeps the planted foot on the ground
2211
+ const ht = air ? -.06 : mv ? s * .04 : Math.sin(t * 2) * .02;
2212
+ // arm directions (0 = straight ahead, +down)
2213
+ let aF = ARM0 - s * .5, aB = ARM0 + (air ? -1.1 : s * .5);
2214
+ if (gun) { aF = mf ? -.05 : 0; aB = .25; }
2215
+ else if (k === 3) { aF = -.04; aB = fire ? .02 : .7; }
2216
+ else if (k === 2) { aF = fire ? -.35 + Math.sin(t * 36) * 1.15 : -.95 + s * .1; aB = .55 + s * .1; }
2217
+ else if (k === 1) { aF = fire ? .05 : .8 + s * .15; }
2218
+ const sbx = -7, sby = -42.2 + by, sfx = 11 - (gun && mf ? 2.5 : 0), sfy = -42.2 + by;
2219
+ const hbx = sbx + Math.cos(aB) * ARML, hby = sby + Math.sin(aB) * ARML, hfx = sfx + Math.cos(aF) * ARML, hfy = sfy + Math.sin(aF) * ARML;
2220
+ const hx = 3.3, hy = -53.8 + by;
2221
+ // gear that hangs behind the body
2222
+ if (k === 2) { const w = Math.sin(t * 9) * 4 + (mv ? 8 : 0) + (air ? 6 : 0); c.beginPath(); c.moveTo(-12, -64 + by); c.quadraticCurveTo(-30 - w * .5, -40 + by, -32 - w, -8); c.lineTo(-4, -14 + by); c.closePath(); c.fillStyle = "#9d0208"; c.strokeStyle = "#14121c"; c.lineWidth = 2.5; c.fill(); c.stroke(); }
2223
+ if (A.back) { c.save(); c.translate(hx, hy); c.rotate(ht); c.translate(-.55, 25); kp(c, A.back); c.restore(); }
2224
+ if (k === 3) { c.save(); c.translate(-20, -40 + by); c.rotate(-.5); c.strokeStyle = "#14121c"; c.lineWidth = 2.2; rr(c, -5.5, -6, 11, 32, 3); inkFill(c, "#7f5539"); c.fillStyle = "#e63946"; for (let i = -1; i <= 1; i++) c.fillRect(i * 3.2 - 1.3, -15, 2.6, 9); c.restore(); }
2225
+ limb(c, A.arm, sbx, sby, aB - ARM0, true); limb(c, A.hand, hbx, hby, aB - ARM0, true);
2226
+ limb(c, A.leg, -4, -23.05 + by, lb, true); limb(c, A.leg, 4, -23.05 + by, lf, false);
2227
+ if (k === 1) { const sw = s * 4; c.beginPath(); c.moveTo(-16, -34 + by); c.lineTo(15, -34 + by); c.lineTo(22 + sw, -6); c.quadraticCurveTo(0, -1, -22 + sw, -6); c.closePath(); c.fillStyle = "#5a189a"; c.strokeStyle = "#14121c"; c.lineWidth = 2.5; c.fill(); c.stroke(); c.strokeStyle = "#ffd60a"; c.lineWidth = 2.5; c.beginPath(); c.moveTo(21 + sw, -9); c.quadraticCurveTo(0, -4, -21 + sw, -9); c.stroke(); }
2228
+ c.save(); c.translate(0, -25.95 + by); kp(c, A.body); c.restore();
2229
+ if (k === 5) { c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .45 + .25 * Math.sin(t * 5); circ(c, -.45, -34 + by, 11, "#4cc9f0"); c.restore(); }
2230
+ if (k === 2) { c.save(); c.translate(hbx + 4, hby + 4); c.strokeStyle = "#14121c"; c.lineWidth = 2.5; c.beginPath(); c.arc(0, 0, 16, 0, 6.283); inkFill(c, "#7f5539"); c.strokeStyle = "#ced4da"; c.lineWidth = 3; c.beginPath(); c.arc(0, 0, 13.5, 0, 6.283); c.stroke(); circ(c, 0, 0, 4.5, "#ced4da"); c.restore(); }
2231
+ const g = c.createRadialGradient(0, 0, 0, 0, 0, 13.45); g.addColorStop(0, "rgba(0,0,0,.3)"); g.addColorStop(1, "rgba(0,0,0,0)");
2232
+ c.save(); c.translate(1.9, -45.9 + by); c.scale(1.5, 1); c.fillStyle = g; c.beginPath(); c.arc(0, 0, 13.45, 0, 6.283); c.fill(); c.restore(); // head shadow on the chest
2233
+ limb(c, A.arm, sfx, sfy, aF - ARM0, false);
2234
+ c.save(); c.translate(hx, hy); c.rotate(ht); kp(c, A.head); headGear(c, k, t); c.restore();
2235
+ const r = aF - ARM0, gx = hfx - Math.cos(r) - 6 * Math.sin(r), gy = hfy - Math.sin(r) + 6 * Math.cos(r); // grip point inside the fist
2236
+ if (k !== 5) { c.save(); c.translate(gx, gy); c.rotate(aF); weapon(c, k, t, fire, mf, aF); c.restore(); }
2237
+ limb(c, A.hand, hfx, hfy, r, false);
2238
+ if (k === 5) { c.save(); c.translate(gx, gy); c.rotate(aF); weapon(c, k, t, fire, mf, aF); c.restore(); }
2239
+ c.restore();
2240
+ }
2241
+
2242
+ // ---------- Drawing: aliens ----------
2243
+ function alien(c, m, id, x, y, pal, tm, now, fl, pi) {
2244
+ const A = AL[m.t], r = A.r, f = m.f || -1, ph = tm * 4 + (hsh(id) % 7);
2245
+ const C1 = fl ? "#fff" : pal.a1, C2 = fl ? "#fff" : pal.a2, EY = "#fff", PU = "#111";
2246
+ if (m.t === 5 || m.t === 8) { const gy = gnd(level(pi), x), k = clamp(1 - (gy - y - r) / 300, .3, 1); c.fillStyle = "rgba(0,0,0," + (.3 * k) + ")"; c.beginPath(); c.ellipse(x, gy, r * 1.1 * k, 7 * k, 0, 0, 6.283); c.fill(); }
2247
+ else if (m.t !== 2) { c.fillStyle = "rgba(0,0,0,.25)"; c.beginPath(); c.ellipse(x, y + r - 1, r * .9, 4, 0, 0, 6.283); c.fill(); }
2248
+ c.save(); c.translate(x, y);
2249
+ const S = shades(pal), G1 = fl ? "#fff" : bodyGrad(c, r, pal.a1, S.a1L, S.a1D), G2 = fl ? "#fff" : bodyGrad(c, r, pal.a2, S.a2L, S.a2D);
2250
+ if (m.t === 0) {
2251
+ const sq = Math.sin(ph * 1.6) * .1;
2252
+ c.save(); c.scale(1 + sq, 1 - sq);
2253
+ c.beginPath(); c.ellipse(0, 2, r, r * .82, 0, 0, 6.283); inked(c, G2, 4);
2254
+ c.globalAlpha = .55; c.fillStyle = C1; c.beginPath(); c.ellipse(-r * .25, -r * .2, r * .45, r * .3, -.4, 0, 6.283); c.fill(); c.globalAlpha = 1;
2255
+ c.restore();
2256
+ for (const s of [-1, 1]) {
2257
+ const ex = s * 6 + f * 3, ey = -r - 6 + Math.sin(ph + s) * 2;
2258
+ c.beginPath(); c.moveTo(s * 5, -r * .6); c.lineTo(ex, ey); ink(c, 6.5); c.stroke(); c.strokeStyle = C2; c.lineWidth = 3; c.stroke();
2259
+ ic(c, ex, ey, 4.5, EY, 2.5); circ(c, ex + f * 1.5, ey, 2, PU);
2260
+ }
2261
+ c.fillStyle = "#2b0a0a"; c.beginPath(); c.ellipse(f * 6, 4, 6, 3 + Math.abs(Math.sin(ph)) * 2, 0, 0, 6.283); c.fill();
2262
+ } else if (m.t === 1) {
2263
+ c.lineCap = "round";
2264
+ c.beginPath(); c.moveTo(-f * 6, 4); c.lineTo(-f * 17, -6); c.lineTo(-f * 13, r); c.moveTo(f * 6, 6); c.lineTo(f * 11, r);
2265
+ ink(c, 6.5); c.stroke(); c.strokeStyle = C2; c.lineWidth = 3; c.stroke();
2266
+ c.beginPath(); c.ellipse(0, 0, r, r * .75, f * .2, 0, 6.283); inked(c, G1, 4);
2267
+ c.fillStyle = C2; c.fillRect(-5, -r * .6, 3, r * 1.1); c.fillRect(1, -r * .7, 3, r * 1.2);
2268
+ ic(c, f * 7, -4, 6.5, EY, 2.5); circ(c, f * 9, -4, 3, "#d00000");
2269
+ c.strokeStyle = C2;
2270
+ c.lineWidth = 1.5; c.beginPath(); c.moveTo(f * 3, -r * .7); c.quadraticCurveTo(f * 6, -r - 8, f * 12, -r - 6 + Math.sin(ph) * 2); c.stroke();
2271
+ c.lineCap = "butt";
2272
+ } else if (m.t === 2) {
2273
+ const fw = Math.abs(Math.sin(tm * 40));
2274
+ c.fillStyle = "rgba(255,255,255,.45)";
2275
+ c.beginPath(); c.ellipse(-f * 4, -r * .7, r * .55, r * .95 * fw + 2, -f * .4, 0, 6.283); c.fill();
2276
+ c.beginPath(); c.ellipse(f * 3, -r * .6, r * .45, r * .8 * fw + 2, f * .4, 0, 6.283); c.fill();
2277
+ c.beginPath(); c.moveTo(-f * (r - 2), -3); c.lineTo(-f * (r + 10), 2); c.lineTo(-f * (r - 2), 4); inked(c, C2, 2.5);
2278
+ c.beginPath(); c.ellipse(0, 0, r * 1.1, r * .6, 0, 0, 6.283); inked(c, G2, 3.5);
2279
+ c.fillStyle = C1; c.fillRect(-4, -r * .55, 3, r * 1.1); c.fillRect(-10, -r * .45, 3, r * .9);
2280
+ ic(c, f * r * .75, -2, 4, "#ff0054", 2); circ(c, f * r * .85, -3, 1.5, "#fff");
2281
+ } else if (m.t === 3) {
2282
+ const open = m.at && now - m.at < .35;
2283
+ c.beginPath(); c.ellipse(0, r, 13, 4, 0, 0, 6.283); inked(c, C2, 3);
2284
+ c.beginPath(); c.rect(-4, r * .4, 8, r * .6); inked(c, C2, 3);
2285
+ ic(c, 0, -2, r * .85 + Math.sin(ph) * 1.5, G1, 4);
2286
+ c.fillStyle = C2; circ(c, -6, -8, 3, C2); circ(c, 4, 4, 2.5, C2); circ(c, -8, 5, 2, C2);
2287
+ c.fillStyle = "#1a0000"; c.beginPath(); c.ellipse(f * r * .45, 0, 5, open ? 8 : 3, 0, 0, 6.283); c.fill();
2288
+ for (const e of [[-5, -r * .7], [1, -r * .85], [7, -r * .65]]) { ic(c, e[0], e[1], 3, EY, 2); circ(c, e[0] + f, e[1], 1.4, PU); }
2289
+ if (open) { c.globalAlpha = .6; circ(c, f * (r * .45 + 6), 0, 6, pal.a1); c.globalAlpha = 1; }
2290
+ } else if (m.t === 4) {
2291
+ const sw = Math.sin(ph) * 3;
2292
+ rr(c, -r - 6, -8 + sw, 12, 26, 5); inked(c, C2, 3.5);
2293
+ c.beginPath(); c.rect(-r * .6, r - 8, 10, 10); c.rect(r * .3, r - 8, 10, 10); inked(c, C2, 3.5);
2294
+ c.beginPath(); c.moveTo(-r * .6, -r + 4); c.lineTo(-r * .95, -r - 14); c.lineTo(-r * .3, -r + 4); c.moveTo(r * .6, -r + 4); c.lineTo(r * .95, -r - 14); c.lineTo(r * .3, -r + 4); inked(c, fl ? "#fff" : "#fefae0", 3);
2295
+ rr(c, -r, -r, r * 2, r * 2 - 6, 12); inked(c, G2, 4.5);
2296
+ c.fillStyle = C1; c.beginPath(); c.ellipse(0, r * .35, r * .65, r * .45, 0, 0, 6.283); c.fill();
2297
+ ic(c, f * 6, -r * .45, 4, "#ff4d6d", 2); ic(c, f * 17, -r * .45, 4, "#ff4d6d", 2);
2298
+ c.fillStyle = "#1a0000"; rr(c, f > 0 ? 0 : -24, -6, 24, 9, 3); c.fill();
2299
+ c.fillStyle = "#fff"; for (let i = 0; i < 4; i++) { const tx = (f > 0 ? 2 : -22) + i * 6; c.beginPath(); c.moveTo(tx, -6); c.lineTo(tx + 3, -1); c.lineTo(tx + 6, -6); c.fill(); }
2300
+ rr(c, r - 6, -8 - sw, 12, 26, 5); inked(c, C2, 3.5);
2301
+ } else if (m.t === 6) {
2302
+ c.translate(0, r); zombie(c, m, f, tm, now, fl, hsh(id) % 7);
2303
+ } else if (m.t === 7) { // egg pod: leathery egg with veins and a glowing embryo
2304
+ const p = 1 + .06 * Math.sin(tm * 5 + hsh(id));
2305
+ c.save(); c.translate(0, r); c.scale(p, 1 / p); c.translate(0, -r);
2306
+ c.beginPath(); c.ellipse(0, -2, r * .85, r * 1.15, 0, 0, 6.283); inked(c, fl ? "#fff" : "#c9ada7", 3);
2307
+ if (!fl) {
2308
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .45 + .3 * Math.sin(tm * 6 + hsh(id)); c.drawImage(glowTex("#ff595e"), -12, -14, 24, 24); c.restore();
2309
+ c.strokeStyle = "#9a8c98"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(-8, -14); c.quadraticCurveTo(-2, -4, -9, 6); c.moveTo(6, -16); c.quadraticCurveTo(10, -6, 4, 8); c.stroke();
2310
+ c.fillStyle = "rgba(255,255,255,.45)"; c.beginPath(); c.ellipse(-5, -12, 3, 6, -.3, 0, 6.283); c.fill();
2311
+ }
2312
+ c.restore();
2313
+ } else if (m.t === 8) miniArt(c, m, pi % 5, f, tm, now, fl, pi);
2314
+ else bossArt(c, m, pi % 5, f, tm, now, fl, pi);
2315
+ // rim light along the top edge
2316
+ if (!fl && m.t !== 6 && m.t !== 5 && m.t !== 7 && m.t !== 8) { c.strokeStyle = "rgba(255,255,255,.22)"; c.lineWidth = m.t >= 4 ? 3 : 2; c.beginPath(); c.arc(0, m.t === 0 ? 2 : 0, r * .9, Math.PI * 1.08, Math.PI * 1.62); c.stroke(); }
2317
+ c.restore();
2318
+ if (m.wu && m.wu > now && !(m.fz && m.fz > now)) { // wind-up warning
2319
+ if (m.t === 3) { c.globalAlpha = .35 + .3 * Math.sin(tm * 30); circ(c, x, y - 2, r + 6, pal.a1); c.globalAlpha = 1; }
2320
+ else if (m.t === 5 || m.t === 8) { // bosses and mini bosses call out the attack that is coming; specials in red
2321
+ const sp3 = m.t === 5 && m.pn === 3, s = 1 + .1 * Math.sin(tm * 25), t = "! " + (m.t === 5 ? BOSS : MINI)[pi % 5].atk[m.pn || 0].toUpperCase() + " !";
2322
+ c.save(); c.translate(x, Math.max(26, y - r - (m.t === 5 ? 46 : 34))); c.scale(s, s); c.textAlign = "center"; c.font = "bold " + (sp3 ? 22 : 18) + "px sans-serif";
2323
+ c.lineWidth = 5; c.strokeStyle = "#000"; c.strokeText(t, 0, 0); c.fillStyle = sp3 ? "#ff4d6d" : "#ffd60a"; c.fillText(t, 0, 0); c.restore();
2324
+ }
2325
+ else { const s = 1 + .15 * Math.sin(tm * 25); c.save(); c.translate(x, y - r - (m.t === 5 ? 34 : m.t === 6 ? 44 : 22)); c.scale(s, s); c.textAlign = "center"; c.font = "bold " + (m.t === 5 ? 34 : 24) + "px sans-serif"; c.lineWidth = 4; c.strokeStyle = "#000"; c.strokeText("!", 0, 0); c.fillStyle = "#ffd60a"; c.fillText("!", 0, 0); c.restore(); }
2326
+ }
2327
+ if (m.fz && m.fz > now) { c.globalAlpha = .45; circ(c, x, y, r + 5, "#a2d2ff"); c.globalAlpha = 1; c.strokeStyle = "#fff"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(x - r * .6, y - r * .3); c.lineTo(x - r * .2, y + r * .2); c.moveTo(x + r * .4, y - r * .5); c.lineTo(x + r * .1, y); c.stroke(); }
2328
+ else if (m.cl) { c.globalAlpha = .2 + .08 * Math.sin(tm * 8); circ(c, x, y, r + 3, "#a2d2ff"); c.globalAlpha = 1; if (Math.random() < .15) addP(x + rnd(-r, r), y - r * .5, 0, rnd(10, 30), .5, "#e0fbfc", 2, 0); } // chilled
2329
+ if (m.bn && Math.random() < .45) addP(x + rnd(-r * .6, r * .6), y + rnd(-r * .4, r * .4), rnd(-15, 15), rnd(-110, -50), rnd(.25, .45), pick(["#ff7b00", "#ffba08", "#ff6b35"]), rnd(3, 6), 0); // on fire
2330
+ if (m.t !== 5 && m.t !== 8 && m.hp < m.mhp && !(m.dn && m.dn > now)) { const w = Math.max(24, r * 1.6), by = y - r - (m.t === 6 ? 30 : 14); c.fillStyle = "rgba(0,0,0,.6)"; c.fillRect(x - w / 2, by, w, 4); c.fillStyle = "#ff4d6d"; c.fillRect(x - w / 2, by, w * clamp(m.hp / m.mhp, 0, 1), 4); }
2331
+ }
2332
+
2333
+ // ---------- Drawing: the five planet bosses (drawn facing right, mirrored by f; origin = hitbox centre) ----------
2334
+ function bossArt(c, m, k, f, tm, now, fl, pi) {
2335
+ const r = AL[5].r, wu = m.wu && m.wu > now, hit = m.at && now - m.at < .45, enr = m.hp < m.mhp * .4;
2336
+ const C = col => fl ? "#fff" : col, grad = (x0, y0, rad, a, b, d) => { if (fl) return "#fff"; const g = c.createRadialGradient(x0, y0, rad * .1, 0, 0, rad); g.addColorStop(0, a); g.addColorStop(.5, b); g.addColorStop(1, d); return g; };
2337
+ const glow = (col, x, y, s, a) => { c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = a; c.drawImage(glowTex(col), x - s, y - s, s * 2, s * 2); c.restore(); };
2338
+ c.save(); c.scale(f, 1);
2339
+ if (k === 0) { // Queen Glorpa: a giant crowned slime that squashes, hops and slams
2340
+ const air = m.y < gnd(level(pi), m.x) - r - 6, s = air ? -.14 : wu ? .12 + .05 * Math.sin(tm * 30) : Math.sin(tm * 3) * .05;
2341
+ c.translate(0, r); c.scale(1 + s, 1 - s); c.translate(0, -r);
2342
+ for (let i = -2; i <= 2; i++) { const d = 6 * Math.abs(Math.sin(tm * 2 + i)); c.beginPath(); c.ellipse(i * 28, r - 2 + d * .5, 7, 4 + d * .5, 0, 0, 6.283); inked(c, C("#55a630"), 2.5); }
2343
+ c.beginPath(); c.moveTo(-r * 1.3, r); c.bezierCurveTo(-r * 1.42, -r * .3, -r * .75, -r * 1.05, 0, -r * 1.05); c.bezierCurveTo(r * .75, -r * 1.05, r * 1.42, -r * .3, r * 1.3, r); c.closePath();
2344
+ inked(c, grad(-r * .4, -r * .5, r * 1.4, "#d9ed92", "#70e000", "#2b9348"), 5);
2345
+ if (!fl) { // see-through jelly: nucleus, rising bubbles, gloss
2346
+ c.save(); c.clip();
2347
+ c.globalAlpha = .3; circ(c, -18, r * .3, r * .42, "#007f5f");
2348
+ c.globalAlpha = .55; for (let i = 0; i < 7; i++) circ(c, Math.sin(i * 2.3) * r * .9, r * .8 - ((tm * 18 + i * 31) % (r * 1.7)), 3 + (i % 3) * 2, "#e9f5db");
2349
+ c.globalAlpha = .6; c.fillStyle = "#fff"; c.beginPath(); c.ellipse(-r * .5, -r * .55, r * .3, r * .12, -.6, 0, 6.283); c.fill();
2350
+ c.restore(); c.globalAlpha = 1;
2351
+ }
2352
+ for (const ex of [-6, 30]) { ic(c, ex, -r * .3, 13, "#fff", 3); circ(c, ex + 4, -r * .3 + 1, 6, enr || wu ? "#d00000" : "#111"); circ(c, ex + 2, -r * .3 - 2, 2, "#fff"); }
2353
+ if (wu || enr) { ink(c, 4); c.beginPath(); c.moveTo(-20, -r * .62); c.lineTo(4, -r * .5); c.moveTo(42, -r * .62); c.lineTo(20, -r * .5); c.stroke(); }
2354
+ const mo = hit || wu ? 18 : 8; c.beginPath(); c.moveTo(-14, r * .2); c.quadraticCurveTo(14, r * .2 + mo * 2, 44, r * .2); c.closePath(); inked(c, "#3a0a0a", 3);
2355
+ c.fillStyle = "#fff"; for (let i = 0; i < 5; i++) { const tx = -8 + i * 10; c.beginPath(); c.moveTo(tx, r * .2); c.lineTo(tx + 4, r * .2 + 6); c.lineTo(tx + 8, r * .2); c.fill(); }
2356
+ const cy = -r * 1.02; c.beginPath(); c.moveTo(-26, cy + 4); c.lineTo(-24, cy - 22); c.lineTo(-12, cy - 8); c.lineTo(0, cy - 30); c.lineTo(12, cy - 8); c.lineTo(24, cy - 22); c.lineTo(26, cy + 4); c.closePath(); inked(c, C("#ffd60a"), 3);
2357
+ for (const gx of [-14, 0, 14]) ic(c, gx, cy - 4, 3.5, gx ? "#4cc9f0" : "#f72585", 1.5);
2358
+ } else if (k === 1) { // Magma Titan: a lava-cracked rock golem with huge fists
2359
+ const st = Math.sin(tm * 4), rock = C("#3d2b25"), glowA = .6 + .4 * Math.sin(tm * 5);
2360
+ const arm = (sx, a) => { // shoulder (sx, -40); a = angle from straight down (raised ~2.5)
2361
+ const ex = sx + Math.sin(a) * 44, ey = -40 + Math.cos(a) * 44;
2362
+ c.lineCap = "round"; c.beginPath(); c.moveTo(sx, -40); c.lineTo(ex, ey); ink(c, 26); c.stroke(); c.strokeStyle = C("#4a3429"); c.lineWidth = 19; c.stroke(); c.lineCap = "butt";
2363
+ c.beginPath(); for (let i = 0; i < 7; i++) { const t = i / 7 * 6.283, rr2 = 20 + (i % 2) * 4; c.lineTo(ex + Math.cos(t) * rr2, ey + Math.sin(t) * rr2); } c.closePath(); inked(c, C("#5e4337"), 3.5);
2364
+ if (!fl) { c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = glowA; c.strokeStyle = "#ff7b00"; c.lineWidth = 2.5; c.beginPath(); c.moveTo(ex - 8, ey - 6); c.lineTo(ex, ey + 2); c.lineTo(ex + 9, ey - 3); c.stroke(); c.restore(); }
2365
+ };
2366
+ arm(-44, wu ? 2.7 : -.25 - st * .3);
2367
+ for (const [lx, o] of [[-34, st], [8, -st]]) { rr(c, lx, 24 + o * 3, 26, 38 - o * 3, 6); inked(c, rock, 4); }
2368
+ c.beginPath(); for (const p of [[-58, -28], [-38, -64], [14, -72], [54, -50], [62, 0], [42, 40], [-44, 40], [-64, 6]]) c.lineTo(p[0], p[1]); c.closePath();
2369
+ inked(c, grad(-20, -40, 90, "#6f4e37", "#3d2b25", "#1f1410"), 5);
2370
+ if (!fl) { // lava cracks and the molten core
2371
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = glowA; c.strokeStyle = "#ff7b00"; c.lineWidth = 3;
2372
+ c.beginPath(); c.moveTo(-40, -40); c.lineTo(-22, -24); c.lineTo(-30, 0); c.lineTo(-14, 22); c.moveTo(40, -36); c.lineTo(28, -20); c.lineTo(44, 8); c.moveTo(-6, 18); c.lineTo(10, 36); c.stroke(); c.restore();
2373
+ glow("#ff7b00", 10, -12, 46, glowA);
2374
+ }
2375
+ ic(c, 10, -12, 15, fl ? "#fff" : grad(6, -16, 18, "#fff3b0", "#ffba08", "#ff4800"), 3.5);
2376
+ c.beginPath(); for (const p of [[4, -66], [12, -92], [40, -94], [48, -72], [36, -60]]) c.lineTo(p[0], p[1]); c.closePath(); inked(c, C("#4a3429"), 4);
2377
+ for (const hx of [14, 38]) { c.beginPath(); c.moveTo(hx - 4, -90); c.quadraticCurveTo(hx - 14, -110, hx - 2, -118); c.lineTo(hx + 4, -92); c.closePath(); inked(c, C("#2b1b17"), 2.5); }
2378
+ c.fillStyle = wu || enr ? "#ffffff" : "#ffd60a"; c.fillRect(22, -82, 9, 3.5); c.fillRect(35, -82, 7, 3.5); if (!fl) glow("#ffba08", 32, -80, 18, .8);
2379
+ arm(46, wu ? 2.5 : .25 + st * .3);
2380
+ } else if (k === 2) { // Frost Wyrm: a flying ice serpent with a long segmented body
2381
+ const flap = Math.sin(tm * 6), open = hit || (m.lk && m.lk > now) ? .4 : wu ? .2 : .05, pts = [];
2382
+ for (let i = 1; i <= 9; i++) pts.push([-i * 22 - 8, Math.sin(tm * 3 - i * .65) * 16 + i * 2, 30 - i * 2.2]);
2383
+ const [lx, ly] = pts[8]; c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx - 34, ly - 22); c.lineTo(lx - 24, ly); c.lineTo(lx - 34, ly + 20); c.closePath(); inked(c, C("#90e0ef"), 3);
2384
+ c.beginPath(); c.moveTo(-30, -18); c.quadraticCurveTo(-70, -80 - flap * 26, -120, -64 - flap * 34); c.lineTo(-96, -40); c.lineTo(-84, -30); c.lineTo(-62, -24); c.closePath(); inked(c, fl ? "#fff" : "rgba(202,240,248,.7)", 3);
2385
+ for (let i = 8; i >= 0; i--) {
2386
+ const [px, py, pr] = pts[i];
2387
+ c.beginPath(); c.moveTo(px - pr * .5, py - pr * .8); c.lineTo(px, py - pr * 1.45); c.lineTo(px + pr * .5, py - pr * .8); c.closePath(); inked(c, C("#e0fbfc"), 2.5);
2388
+ ic(c, px, py, pr, fl ? "#fff" : (i % 2 ? "#48cae4" : "#00b4d8"), 3.5);
2389
+ if (!fl) { c.fillStyle = "#caf0f8"; c.beginPath(); c.ellipse(px, py + pr * .45, pr * .7, pr * .35, 0, 0, 6.283); c.fill(); }
2390
+ }
2391
+ for (const [hx, hy] of [[-10, -22], [4, -24]]) { c.beginPath(); c.moveTo(hx, hy); c.quadraticCurveTo(hx - 24, hy - 18, hx - 46, hy - 24); c.lineTo(hx - 8, hy + 6); c.closePath(); inked(c, C("#e9ecef"), 2.5); }
2392
+ c.save(); c.translate(-8, 6); c.rotate(open); c.beginPath(); c.moveTo(0, 0); c.lineTo(48, 2); c.lineTo(38, 12); c.lineTo(-6, 14); c.closePath(); inked(c, C("#48cae4"), 3.5);
2393
+ c.fillStyle = "#fff"; for (let i = 0; i < 4; i++) { c.beginPath(); c.moveTo(14 + i * 8, 2); c.lineTo(18 + i * 8, -5); c.lineTo(22 + i * 8, 2); c.fill(); } c.restore();
2394
+ c.beginPath(); c.moveTo(-28, -18); c.lineTo(30, -14); c.lineTo(56, 0); c.lineTo(22, 6); c.lineTo(-26, 12); c.closePath(); inked(c, grad(0, -10, 50, "#caf0f8", "#48cae4", "#0077b6"), 4);
2395
+ ic(c, 12, -6, 5.5, wu || enr ? "#ffffff" : "#4cc9f0", 2); if (!fl) glow("#4cc9f0", 12, -6, 16, .9);
2396
+ if (!fl && (open > .2 || wu)) glow("#e0fbfc", 58, 4, 30, .5 + .3 * Math.sin(tm * 20));
2397
+ } else if (k === 3) { // Crystal Overmind: a faceted gem with one eye and orbiting shards
2398
+ const spin = tm * 1.2, shardsOut = m.pn === 1 && now - m.at < 1.2, blink = wu && m.pn === 2;
2399
+ if (blink) c.globalAlpha = .35 + .65 * Math.abs(Math.sin(tm * 25));
2400
+ const shard = (i, front) => {
2401
+ const a = spin + i * 1.047, sx = Math.cos(a) * 98, sy = Math.sin(a) * 34 - 6;
2402
+ if ((Math.sin(a) > 0) !== front || shardsOut) return;
2403
+ c.save(); c.translate(sx, sy); c.rotate(a * .5); c.beginPath(); c.moveTo(0, -18); c.lineTo(8, 0); c.lineTo(0, 18); c.lineTo(-8, 0); c.closePath(); inked(c, C(front ? "#f72585" : "#b5179e"), 3);
2404
+ c.fillStyle = "rgba(255,255,255,.5)"; c.beginPath(); c.moveTo(0, -14); c.lineTo(4, 0); c.lineTo(0, 0); c.fill(); c.restore();
2405
+ };
2406
+ for (let i = 0; i < 6; i++) shard(i, false);
2407
+ if (!fl) glow("#f72585", 0, 0, 110, .35 + .15 * Math.sin(tm * 3));
2408
+ const rot = tm * .3, vs = []; for (let i = 0; i < 8; i++) { const a = rot + i / 8 * 6.283, rr2 = i % 2 ? 52 : 64; vs.push([Math.cos(a) * rr2, Math.sin(a) * rr2]); }
2409
+ c.beginPath(); for (const v of vs) c.lineTo(v[0], v[1]); c.closePath(); ink(c, 6); c.stroke();
2410
+ const shadesC = ["#f72585", "#b5179e", "#7209b7", "#ff70a6"];
2411
+ for (let i = 0; i < 8; i++) { const a = vs[i], b = vs[(i + 1) % 8]; c.beginPath(); c.moveTo(0, 0); c.lineTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.closePath(); c.fillStyle = C(shadesC[i % 4]); c.fill(); }
2412
+ c.strokeStyle = "rgba(255,255,255,.35)"; c.lineWidth = 1.5; c.beginPath(); for (const v of vs) { c.moveTo(0, 0); c.lineTo(v[0], v[1]); } c.stroke();
2413
+ const beam = m.lk && m.lk > now; c.beginPath(); c.ellipse(0, 0, 24, 19, 0, 0, 6.283); inked(c, "#fff", 3.5);
2414
+ ic(c, 7, 0, 11, beam || enr ? "#ff0054" : "#e0aaff", 2); c.fillStyle = "#10002b"; c.beginPath(); c.ellipse(8, 0, 3, 8, 0, 0, 6.283); c.fill(); circ(c, 4, -4, 2.5, "#fff");
2415
+ for (let i = 0; i < 6; i++) shard(i, true);
2416
+ } else { // Hive Mother: an insect queen with an egg-filled abdomen, many eyes and snapping mandibles
2417
+ const flap = Math.abs(Math.sin(tm * 24)), open = hit || wu ? .5 : .15 + .1 * Math.sin(tm * 4);
2418
+ for (const [wx, s] of [[-14, 1], [6, .8]]) { c.beginPath(); c.moveTo(wx, -26); c.quadraticCurveTo(wx - 50, -60 - flap * 50 * s, wx - 100, -40 - flap * 40 * s); c.quadraticCurveTo(wx - 60, -20, wx, -18); c.closePath(); inked(c, fl ? "#fff" : "rgba(255,200,200,.35)", 2.5); }
2419
+ c.beginPath(); c.ellipse(-60, 16, 62, 40, -.15, 0, 6.283); inked(c, grad(-70, 0, 70, "#9d0208", "#6a040f", "#370617"), 5);
2420
+ if (!fl) {
2421
+ c.strokeStyle = "rgba(255,89,94,.45)"; c.lineWidth = 3; for (const sx of [-88, -64, -40]) { c.beginPath(); c.ellipse(sx, 16, 6, 36, -.15, -1.3, 1.3); c.stroke(); }
2422
+ for (const [ex, ey] of [[-84, 22], [-60, 30], [-72, 2]]) glow("#ff595e", ex, ey, 14 + 4 * Math.sin(tm * 4 + ex), .7);
2423
+ }
2424
+ c.lineCap = "round"; c.beginPath(); for (const lx of [-16, 0, 16]) { c.moveTo(lx, 18); c.lineTo(lx - 10, 44 + Math.sin(tm * 5 + lx) * 4); c.lineTo(lx - 4, 64); } ink(c, 7); c.stroke(); c.strokeStyle = C("#370617"); c.lineWidth = 3.5; c.stroke(); c.lineCap = "butt";
2425
+ c.beginPath(); c.ellipse(0, -4, 38, 30, 0, 0, 6.283); inked(c, C("#9d0208"), 4.5);
2426
+ if (!fl) { c.fillStyle = "rgba(255,255,255,.18)"; c.beginPath(); c.ellipse(-8, -18, 18, 7, -.3, 0, 6.283); c.fill(); }
2427
+ c.strokeStyle = INK; c.lineWidth = 4; c.beginPath(); c.moveTo(40, -40); c.quadraticCurveTo(46, -70, 30, -84); c.moveTo(52, -38); c.quadraticCurveTo(66, -64, 62, -82); c.stroke();
2428
+ c.strokeStyle = C("#6a040f"); c.lineWidth = 2; c.stroke();
2429
+ for (const s of [-1, 1]) { c.save(); c.translate(64, -8); c.rotate(s * open); c.beginPath(); c.moveTo(0, 0); c.quadraticCurveTo(22, s * 6, 26, s * 18); c.lineTo(18, s * 10); c.quadraticCurveTo(10, s * 6, 0, s * 6); c.closePath(); inked(c, C("#fefae0"), 2.5); c.restore(); }
2430
+ ic(c, 42, -18, 26, C("#6a040f"), 4);
2431
+ for (const [ex, ey, er] of [[52, -26, 7], [38, -30, 6], [58, -12, 4], [44, -10, 4]]) { ic(c, ex, ey, er, wu || enr ? "#ffffff" : "#ff595e", 1.8); if (!fl) glow("#ff0054", ex, ey, er * 2.4, .7); }
2432
+ }
2433
+ c.restore();
2434
+ if (k === 4 && m.sh) { // hive shield: a glowing honeycomb bubble
2435
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .55 + .2 * Math.sin(tm * 6);
2436
+ c.strokeStyle = "#ff70a6"; c.lineWidth = 4; c.beginPath(); c.arc(0, 0, 112, 0, 6.283); c.stroke();
2437
+ c.globalAlpha = .25; c.lineWidth = 1.5; c.beginPath();
2438
+ for (let i = 0; i < 6; i++) { const a = i / 6 * 6.283 + tm * .3; for (let j = 0; j < 6; j++) { const b = a + j / 6 * 6.283; const hx = Math.cos(a) * 70 + Math.cos(b) * 22, hy = Math.sin(a) * 70 + Math.sin(b) * 22; j ? c.lineTo(hx, hy) : c.moveTo(hx, hy); } c.closePath(); }
2439
+ c.stroke(); c.restore();
2440
+ }
2441
+ }
2442
+
2443
+ </script>
2444
+ <script>
2445
+ // Star Squad: the five halfway mini bosses (one per planet, index = planet % 5), in the same ink style as the planet bosses.
2446
+ // Drawn facing right and mirrored by f; origin = hitbox centre (r = AL[8].r). m.rl = rolling / lunging, m.dv = diving.
2447
+ "use strict";
2448
+ function miniArt(c, m, k, f, tm, now, fl, pi) {
2449
+ const r = AL[8].r, wu = m.wu && m.wu > now, hit = m.at && now - m.at < .45, enr = m.hp < m.mhp * .4, mad = wu || enr;
2450
+ const C = col => fl ? "#fff" : col, grad = (x0, y0, rad, a, b, d) => { if (fl) return "#fff"; const g = c.createRadialGradient(x0, y0, rad * .1, 0, 0, rad); g.addColorStop(0, a); g.addColorStop(.5, b); g.addColorStop(1, d); return g; };
2451
+ const glow = (col, x, y, s, a) => { if (fl) return; c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = a; c.drawImage(glowTex(col), x - s, y - s, s * 2, s * 2); c.restore(); };
2452
+ c.save(); c.scale(f, 1);
2453
+ if (k === 0) { // Thornback: an armoured thorn beetle that curls into a spiked ball to roll
2454
+ if (m.rl && m.rl > now) {
2455
+ c.rotate(tm * 13);
2456
+ for (let i = 0; i < 10; i++) { c.save(); c.rotate(i * .628); c.beginPath(); c.moveTo(r * .7, -7); c.lineTo(r * 1.12, 0); c.lineTo(r * .7, 7); c.closePath(); inked(c, C("#fefae0"), 2.5); c.restore(); }
2457
+ ic(c, 0, 0, r * .85, grad(-12, -14, r, "#b5e48c", "#52b788", "#1b4332"), 4.5);
2458
+ c.strokeStyle = fl ? "#fff" : "rgba(10,40,25,.5)"; c.lineWidth = 3; c.beginPath();
2459
+ for (let i = -1; i <= 1; i++) { c.moveTo(-r * .78, i * 18); c.quadraticCurveTo(0, i * 18 - 10, r * .78, i * 18); }
2460
+ c.stroke();
2461
+ } else {
2462
+ const walk = Math.sin(tm * 9), sq = wu ? .06 + .03 * Math.sin(tm * 30) : 0;
2463
+ for (const [lx, o] of [[-26, walk], [-6, -walk], [12, walk], [28, -walk]]) { rr(c, lx - 6, r * .3 - Math.max(0, o) * 4, 12, 22, 5); inked(c, C("#1b4332"), 3); }
2464
+ c.save(); c.translate(0, r); c.scale(1 + sq, 1 - sq); c.translate(0, -r);
2465
+ // thorns along the shell ridge (drawn first so the shell hides their roots); they bristle before an attack
2466
+ for (let i = 0; i < 6; i++) {
2467
+ const a = Math.PI * (1.12 + i * .155), l = 16 + (i % 2) * 6 + (wu ? 6 : 0);
2468
+ c.save(); c.translate(-8 + Math.cos(a) * r, 6 + Math.sin(a) * r * .8); c.rotate(a + Math.PI / 2);
2469
+ c.beginPath(); c.moveTo(-6, 4); c.lineTo(0, -l); c.lineTo(6, 4); c.closePath(); inked(c, C(wu ? "#ffd60a" : "#fefae0"), 2.5); c.restore();
2470
+ }
2471
+ c.beginPath(); c.ellipse(-8, 6, r * 1.05, r * .85, 0, Math.PI, 0); c.lineTo(r * .97, r * .5); c.quadraticCurveTo(-8, r * .78, -r * 1.13, r * .5); c.closePath();
2472
+ inked(c, grad(-14, -16, r * 1.2, "#b5e48c", "#52b788", "#1b4332"), 4.5);
2473
+ c.strokeStyle = fl ? "#fff" : "rgba(10,40,25,.45)"; c.lineWidth = 2.5;
2474
+ for (const s of [.75, .45]) { c.beginPath(); c.ellipse(-8, 6, r * s, r * s * .8, 0, Math.PI * 1.1, Math.PI * 1.9); c.stroke(); }
2475
+ if (!fl) { c.fillStyle = "rgba(255,255,255,.25)"; c.beginPath(); c.ellipse(-22, -r * .45, 14, 5, -.3, 0, 6.283); c.fill(); }
2476
+ c.restore();
2477
+ // head with a curved horn
2478
+ c.beginPath(); c.moveTo(42, -6); c.quadraticCurveTo(58, -26, 70, -22); c.lineTo(54, 0); c.closePath(); inked(c, C("#fefae0"), 2.5);
2479
+ c.beginPath(); c.ellipse(38, 6, 18, 15, 0, 0, 6.283); inked(c, C("#2d6a4f"), 3.5);
2480
+ ic(c, 44, 1, 5, mad ? "#ff0054" : "#ffd60a", 2); circ(c, 46, 1, 2, "#111");
2481
+ c.fillStyle = "#1a0000"; c.beginPath(); c.ellipse(50, 13, 6, hit || wu ? 6 : 2, 0, 0, 6.283); c.fill();
2482
+ if (wu) glow("#9ef01a", -8, -r * .4, 56, .4);
2483
+ }
2484
+ } else if (k === 1) { // Cinder Crab: a lava crab with one huge crushing claw
2485
+ const lunge = m.rl && m.rl > now, sc = Math.sin(tm * (lunge ? 30 : 12)), open = wu ? .55 + .1 * Math.sin(tm * 30) : lunge ? .05 : .22 + .08 * Math.sin(tm * 3);
2486
+ const pincer = (x, y, s, op) => {
2487
+ c.save(); c.translate(x, y); c.scale(s, s);
2488
+ c.save(); c.rotate(-op); c.beginPath(); c.moveTo(-6, -4); c.quadraticCurveTo(14, -26, 36, -8); c.lineTo(16, -2); c.closePath(); inked(c, C("#e85d04"), 3); c.restore();
2489
+ c.save(); c.rotate(op); c.beginPath(); c.moveTo(-6, 4); c.quadraticCurveTo(12, 18, 30, 6); c.lineTo(14, 2); c.closePath(); inked(c, C("#dc2f02"), 3); c.restore();
2490
+ c.beginPath(); c.ellipse(-6, 0, 16, 12, 0, 0, 6.283); inked(c, C("#9d0208"), 3);
2491
+ c.restore();
2492
+ };
2493
+ glow("#ff7b00", 0, r * .5, 70, .35);
2494
+ c.lineCap = "round"; // three scuttling legs each side
2495
+ for (let i = 0; i < 3; i++) for (const s of [-1, 1]) {
2496
+ const o = sc * (i % 2 ? 1 : -1) * 5;
2497
+ c.beginPath(); c.moveTo(s * (8 + i * 12), 8); c.lineTo(s * (30 + i * 10), -6 + o); c.lineTo(s * (36 + i * 10), r * .95);
2498
+ ink(c, 8); c.stroke(); c.strokeStyle = C("#6a040f"); c.lineWidth = 4; c.stroke();
2499
+ }
2500
+ c.lineCap = "butt";
2501
+ pincer(r * .75, 16, .62, open * .6); // small claw, behind the shell
2502
+ c.beginPath(); c.moveTo(-r * 1.15, 6); c.quadraticCurveTo(-r * 1.1, -r * .75, 0, -r * .78); c.quadraticCurveTo(r * 1.1, -r * .75, r * 1.15, 6); c.quadraticCurveTo(0, r * .55, -r * 1.15, 6); c.closePath();
2503
+ inked(c, grad(-10, -18, r * 1.3, "#f48c06", "#9d0208", "#370617"), 4.5);
2504
+ if (!fl) { // glowing magma spots and a gloss
2505
+ for (const [sx, sy, sr] of [[-20, -14, 5], [6, -20, 4], [22, -8, 3.5], [-34, -2, 3]]) { c.globalAlpha = .6 + .3 * Math.sin(tm * 4 + sx); circ(c, sx, sy, sr, "#ffba08"); }
2506
+ c.globalAlpha = 1; c.fillStyle = "rgba(255,255,255,.18)"; c.beginPath(); c.ellipse(-12, -r * .5, 18, 5, -.2, 0, 6.283); c.fill();
2507
+ }
2508
+ for (const ex of [10, 24]) { // eye stalks
2509
+ c.beginPath(); c.moveTo(ex - 2, -r * .6); c.lineTo(ex, -r * .95); ink(c, 6); c.stroke(); c.strokeStyle = C("#9d0208"); c.lineWidth = 3; c.stroke();
2510
+ ic(c, ex, -r * .98, 6, "#fff", 2); circ(c, ex + 2, -r * .98, 2.6, mad ? "#d00000" : "#111");
2511
+ }
2512
+ c.fillStyle = "#370617"; c.beginPath(); c.ellipse(r * .85, 2, 5, hit || wu ? 5 : 2, 0, 0, 6.283); c.fill();
2513
+ c.beginPath(); c.moveTo(r * .8, -4); c.lineTo(r * 1.1, -16); ink(c, 14); c.stroke(); c.strokeStyle = C("#9d0208"); c.lineWidth = 9; c.stroke(); // big claw arm
2514
+ pincer(r * 1.3, -20, 1, open);
2515
+ if (wu) glow("#ffba08", r * 1.6, -22, 30, .6);
2516
+ } else if (k === 2) { // Frost Moth: frosted wings with eye spots, a furry body and feathery antennae
2517
+ const fs = .35 + .65 * Math.abs(Math.sin(tm * (wu ? 14 : 8))), lk = m.lk && m.lk > now;
2518
+ const wing = (dx, far) => {
2519
+ c.save(); c.translate(dx, -8); c.scale(1, far ? fs * .9 : fs);
2520
+ c.beginPath(); c.moveTo(0, 0); c.bezierCurveTo(-20, -70, -95, -88, -88, -30); c.quadraticCurveTo(-60, -4, 0, 0); c.closePath(); inked(c, fl ? "#fff" : far ? "rgba(144,224,239,.55)" : "rgba(202,240,248,.8)", 2.5);
2521
+ c.beginPath(); c.moveTo(0, 4); c.bezierCurveTo(-14, 44, -62, 54, -54, 18); c.quadraticCurveTo(-30, 4, 0, 4); c.closePath(); inked(c, fl ? "#fff" : far ? "rgba(144,224,239,.5)" : "rgba(173,232,244,.75)", 2.5);
2522
+ if (!fl && !far) { ic(c, -58, -46, 11, "#4cc9f0", 2); circ(c, -58, -46, 5, mad ? "#ff0054" : "#023e8a"); circ(c, -60, -48, 1.8, "#fff"); ic(c, -32, 26, 6, "#90e0ef", 1.5); }
2523
+ c.restore();
2524
+ };
2525
+ wing(10, true);
2526
+ c.beginPath(); c.ellipse(-22, 8, 26, 13, .35, 0, 6.283); inked(c, grad(-26, 2, 30, "#ffffff", "#caf0f8", "#48cae4"), 3.5); // abdomen
2527
+ if (!fl) { c.strokeStyle = "rgba(0,119,182,.45)"; c.lineWidth = 2.5; c.beginPath(); for (const sx of [-36, -26, -16]) { const sy = 8 + (sx + 22) * .36; c.moveTo(sx - 3, sy - 10); c.lineTo(sx + 3, sy + 10); } c.stroke(); }
2528
+ c.beginPath(); c.ellipse(8, -4, 18, 15, 0, 0, 6.283); inked(c, grad(4, -10, 20, "#ffffff", "#e0fbfc", "#90e0ef"), 3.5); // fuzzy thorax
2529
+ for (const [x0, y0, x1, y1] of [[30, -16, 26, -54], [36, -14, 52, -48]]) { // feathery antennae
2530
+ c.beginPath(); c.moveTo(x0, y0); c.lineTo(x1, y1); ink(c, 3); c.stroke();
2531
+ c.strokeStyle = C("#e0fbfc"); c.lineWidth = 1.6; c.beginPath();
2532
+ for (let i = 1; i <= 5; i++) { const t2 = i / 6, ax = x0 + (x1 - x0) * t2, ay = y0 + (y1 - y0) * t2; c.moveTo(ax - 5, ay - 2); c.lineTo(ax + 5, ay - 2); }
2533
+ c.stroke();
2534
+ }
2535
+ ic(c, 28, -8, 12, C("#e0fbfc"), 3);
2536
+ c.beginPath(); c.ellipse(33, -10, 6, 8, .2, 0, 6.283); inked(c, mad ? "#ff0054" : "#023e8a", 2); if (!fl) circ(c, 31, -13, 2, "#fff");
2537
+ wing(-2, false);
2538
+ if (wu || lk) glow("#e0fbfc", 34, 4, 40, .5 + .3 * Math.sin(tm * 20));
2539
+ } else if (k === 3) { // Prism Sentry: a faceted crystal core inside a spinning metal ring, one lens eye
2540
+ const lk = m.lk && m.lk > now, ra = r * 1.25, sp = tm * (wu ? 5 : 2);
2541
+ const ring = (a0, a1) => { c.beginPath(); c.ellipse(0, 0, ra, ra * .32, 0, a0, a1); ink(c, 10); c.stroke(); c.strokeStyle = C("#8d99ae"); c.lineWidth = 5; c.stroke(); };
2542
+ const nodes = front => { for (let i = 0; i < 4; i++) { const a = sp + i * 1.571, s = Math.sin(a); if ((s > 0) !== front) continue; ic(c, Math.cos(a) * ra, s * ra * .32, 5, mad ? "#ff0054" : "#7df9ff", 2); } };
2543
+ glow("#f72585", 0, r * .95, 26, .5 + .3 * Math.sin(tm * 9));
2544
+ c.beginPath(); c.moveTo(-8, r * .7); c.lineTo(0, r * 1.05 + 4 * Math.sin(tm * 20)); c.lineTo(8, r * .7); c.closePath(); c.fillStyle = C("#ff70a6"); c.fill(); // thruster flame
2545
+ ring(Math.PI, 2 * Math.PI); nodes(false);
2546
+ const P4 = [[0, -r * 1.05], [r * .62, 0], [0, r * .85], [-r * .62, 0]], FC = ["#ff70a6", "#f72585", "#7209b7", "#b5179e"];
2547
+ c.beginPath(); for (const p of P4) c.lineTo(p[0], p[1]); c.closePath(); ink(c, 6); c.stroke();
2548
+ for (let i = 0; i < 4; i++) { const a = P4[i], b = P4[(i + 1) % 4]; c.beginPath(); c.moveTo(0, 0); c.lineTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.closePath(); c.fillStyle = C(FC[i]); c.fill(); }
2549
+ if (!fl) { c.fillStyle = "rgba(255,255,255,.45)"; c.beginPath(); c.moveTo(-4, -r * .9); c.lineTo(-r * .4, -4); c.lineTo(-r * .22, -4); c.closePath(); c.fill(); }
2550
+ glow("#f72585", 0, 0, 70, .3 + .15 * Math.sin(tm * 3));
2551
+ ic(c, 6, -2, 11, "#fff", 3); ic(c, 8, -2, 6.5, lk || mad ? "#ff0054" : "#7df9ff", 1.5); circ(c, 9, -2, 2.6, "#10002b"); circ(c, 5, -5, 1.8, "#fff");
2552
+ if (lk) glow("#ff0054", 8, -2, 26, .8);
2553
+ ring(0, Math.PI); nodes(true);
2554
+ } else { // Hive Warden: an armoured wasp knight with a glowing stinger; tips nose-down when it dives
2555
+ const fw = Math.abs(Math.sin(tm * 40));
2556
+ if (m.dv && m.dv > now) c.rotate(.55);
2557
+ if (!fl) { // buzzing wings
2558
+ c.fillStyle = "rgba(255,255,255,.4)"; c.strokeStyle = "rgba(14,12,24,.5)"; c.lineWidth = 1.5;
2559
+ for (const [wx, s] of [[-6, 1], [6, .8]]) { c.beginPath(); c.ellipse(wx - 18, -26 - fw * 8 * s, 30 * s, 10 + fw * 8, -.5, 0, 6.283); c.fill(); c.stroke(); }
2560
+ }
2561
+ c.save(); c.translate(-26, 10); c.rotate(-.45); // striped abdomen hanging back, stinger at the tip
2562
+ c.beginPath(); c.moveTo(-28, 0); c.lineTo(-50, 4); c.lineTo(-28, 9); c.closePath(); inked(c, C("#fefae0"), 2.5);
2563
+ c.beginPath(); c.ellipse(0, 4, 30, 17, 0, 0, 6.283); inked(c, C("#3c096c"), 4);
2564
+ if (!fl) { c.save(); c.clip(); c.fillStyle = "#ff9f1c"; for (const sx of [-18, -4, 10]) c.fillRect(sx, -16, 6, 40); c.restore(); ink(c, 4); c.stroke(); }
2565
+ glow("#ff0054", -46, 4, 14, .8 + .2 * Math.sin(tm * 8));
2566
+ c.restore();
2567
+ c.lineCap = "round"; c.beginPath(); for (const lx of [-6, 6, 16]) { c.moveTo(lx, 8); c.lineTo(lx - 6, 26 + Math.sin(tm * 6 + lx) * 3); c.lineTo(lx - 2, 38); }
2568
+ ink(c, 5); c.stroke(); c.strokeStyle = C("#240046"); c.lineWidth = 2.5; c.stroke(); c.lineCap = "butt";
2569
+ c.beginPath(); c.ellipse(4, -4, 20, 17, 0, 0, 6.283); inked(c, grad(0, -12, 22, "#9d4edd", "#5a189a", "#240046"), 4); // armoured thorax
2570
+ if (!fl) { c.strokeStyle = "#ff9f1c"; c.lineWidth = 2.5; c.beginPath(); c.arc(4, -4, 13, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); c.fillStyle = "rgba(255,255,255,.2)"; c.beginPath(); c.ellipse(-2, -14, 8, 4, -.3, 0, 6.283); c.fill(); }
2571
+ c.strokeStyle = INK; c.lineWidth = 3; c.beginPath(); c.moveTo(30, -22); c.quadraticCurveTo(34, -44, 26, -52); c.moveTo(38, -20); c.quadraticCurveTo(48, -40, 46, -50); c.stroke();
2572
+ c.strokeStyle = C("#7b2cbf"); c.lineWidth = 1.5; c.stroke();
2573
+ const op = hit || wu ? .5 : .15;
2574
+ for (const s of [-1, 1]) { c.save(); c.translate(42, -4); c.rotate(s * op); c.beginPath(); c.moveTo(0, 0); c.quadraticCurveTo(12, s * 4, 14, s * 12); c.lineTo(8, s * 6); c.closePath(); inked(c, C("#fefae0"), 2); c.restore(); }
2575
+ ic(c, 30, -12, 14, C("#7b2cbf"), 3.5); // helmeted head with a big compound eye
2576
+ c.beginPath(); c.ellipse(36, -15, 6.5, 8.5, .25, 0, 6.283); inked(c, mad ? "#ffffff" : "#ff595e", 2); glow("#ff0054", 36, -15, 16, .7);
2577
+ if (!fl) { c.fillStyle = "rgba(255,255,255,.7)"; c.fillRect(33, -20, 2.5, 2.5); }
2578
+ }
2579
+ c.restore();
2580
+ }
2581
+
2582
+ </script>
2583
+ <script>
2584
+ // Star Squad: planet traps. Each planet has its own two kinds (levels.json "traps"), placed by level() in data.js.
2585
+ // Every trap runs on the shared clock (g.now + its own phase), so all screens see the same thing with no network
2586
+ // traffic; only the host decides damage. Traps hurt aliens too, so luring them in pays off.
2587
+ "use strict";
2588
+ // cy = cycle length in seconds, d = damage to heroes, md = damage to aliens (0 = harmless to them)
2589
+ const TRAP = {
2590
+ acid: { n: "Acid pool", d: 8 },
2591
+ lava: { n: "Lava pit", d: 12 },
2592
+ goo: { n: "Hive goo", d: 4 }, // also slows you and weakens your jump
2593
+ spore: { n: "Spore pod", d: 10, md: 20, cy: 3.5 }, // swells 1.6-2.3 s, bursts 2.3-3.1 s
2594
+ geyser: { n: "Lava geyser", d: 16, md: 40, cy: 4.5 }, // rumbles 0-1 s, erupts 1-2.1 s
2595
+ ice: { n: "Ice slick" }, // slippery: slow to start, slow to stop
2596
+ icicle: { n: "Icicle", d: 16, md: 40, cy: 5 }, // shakes 2.6-3.4 s, falls 3.4-3.85 s, regrows 4.2-5 s
2597
+ fence: { n: "Laser fence", d: 14, md: 30, cy: 4 }, // on 0-1.8 s, flickers 3.4-4 s before it comes back
2598
+ pad: { n: "Bounce pad" }, // launches you up to the high ledges
2599
+ spikes: { n: "Spike trap", d: 14, md: 30, cy: 3 } // rumbles 0-0.6 s, up 0.6-1.5 s
2600
+ };
2601
+ const POOL = { acid: ["#ccff33", "#70e000", "#2b6b1a"], lava: ["#fff3b0", "#ff7b00", "#9d0208"], goo: ["#e0aaff", "#9d4edd", "#3c096c"] };
2602
+ function trapT(t, now) { const cy = TRAP[t.k].cy; return cy ? ((now + t.ph) % cy + cy) % cy : 0; }
2603
+ function icicleY(lv, t, ph) { if (ph < 3.4 || ph > 3.85) return null; const k = (ph - 3.4) / .45; return 66 + (gnd(lv, t.x) - 66) * k * k; } // tip height while it falls
2604
+ // damage a trap does right now to something whose feet are at (x, fy); 0 = safe
2605
+ function trapHit(lv, t, x, fy, now) {
2606
+ const K = TRAP[t.k]; if (!K.d) return 0;
2607
+ const ph = trapT(t, now), gy = gnd(lv, t.x);
2608
+ if (POOL[t.k]) return x > t.x + 6 && x < t.x + t.w - 6 && fy > gnd(lv, x) - 6 ? K.d : 0;
2609
+ if (t.k === "spore") return ph > 2.3 && ph < 3.1 && Math.hypot(x - t.x, fy - 20 - (gy - 30)) < 80 ? K.d : 0;
2610
+ if (t.k === "geyser") return ph > 1 && ph < 2.1 && Math.abs(x - t.x) < 26 && fy > gy - 230 ? K.d : 0;
2611
+ if (t.k === "icicle") { const iy = icicleY(lv, t, ph); return iy != null && Math.abs(x - t.x) < 22 && iy > fy - 52 && iy < fy + 6 ? K.d : 0; }
2612
+ if (t.k === "fence") return ph < 1.8 && Math.abs(x - t.x) < 12 && fy > gy - 118 ? K.d : 0;
2613
+ if (t.k === "spikes") return ph > .6 && ph < 1.5 && x > t.x && x < t.x + t.w && fy > gnd(lv, x) - 26 ? K.d : 0;
2614
+ return 0;
2615
+ }
2616
+
2617
+ // ---------- Host: who gets hurt (heroes keep their usual 0.7 s invincibility after a hit) ----------
2618
+ function hostTraps(g, al, lv, now, dmM) {
2619
+ for (const t of lv.traps) {
2620
+ const x0 = t.x - 120, x1 = t.x + t.w + 120;
2621
+ for (const a of al) if (a.x > x0 && a.x < x1) { const d = trapHit(lv, t, a.x, a.y + 22, now); if (d) hurt(g, a.id, d * dmM); }
2622
+ if (!TRAP[t.k].md) continue;
2623
+ for (const mid in HC.mobs) {
2624
+ const m = HC.mobs[mid]; if (m.t === 5 || m.t === 7 || m.t === 8 || m.x < x0 || m.x > x1 || now < (m.tc || 0)) continue;
2625
+ if (trapHit(lv, t, m.x, m.y + AL[m.t].r, now)) { m.tc = now + .5; damage(g, mid, TRAP[t.k].md * (1 + .3 * HC.planet), "", null); }
2626
+ }
2627
+ }
2628
+ }
2629
+
2630
+ // ---------- Local hero: what the ground under your feet does (1 = ice, 2 = goo, 4 = bounce pad) ----------
2631
+ function trapFeel(lv, x) {
2632
+ let r = 0;
2633
+ for (const t of lv.traps) {
2634
+ if (t.k === "ice" && x > t.x && x < t.x + t.w) r |= 1;
2635
+ else if (t.k === "goo" && x > t.x && x < t.x + t.w) r |= 2;
2636
+ else if (t.k === "pad" && Math.abs(x - t.x) < 24) r |= 4;
2637
+ }
2638
+ return r;
2639
+ }
2640
+
2641
+ // ---------- Drawing (inked house style; sounds and sparks fire when a trap changes state on screen) ----------
2642
+ function warnMark(c, x, y, tm) {
2643
+ const s = 1 + .15 * Math.sin(tm * 25); c.save(); c.translate(x, y); c.scale(s, s); c.textAlign = "center";
2644
+ c.font = "bold 22px sans-serif"; c.lineWidth = 4; c.strokeStyle = "#000"; c.strokeText("!", 0, 0); c.fillStyle = "#ffd60a"; c.fillText("!", 0, 0); c.restore();
2645
+ }
2646
+ function drawTraps(c, lv, cx, now, tm) {
2647
+ for (const t of lv.traps) {
2648
+ if (t.x + t.w + 90 < cx || t.x - 90 > cx + W) { t._p = null; continue; }
2649
+ const ph = trapT(t, now), gy = gnd(lv, t.x), pv = t._p, near = Math.abs(t.x - px) < 650;
2650
+ const crossed = v => pv != null && pv < v && ph >= v, wrapped = pv != null && pv > ph + .5; t._p = ph;
2651
+ if (POOL[t.k]) { // sunken pool that follows the ground, with a shiny surface and bubbles
2652
+ const [hi, mid, lo] = POOL[t.k], x0 = t.x, x1 = t.x + t.w, xm = (x0 + x1) / 2, ym = gnd(lv, xm), surf = x => gnd(lv, x) + 2 + Math.sin(tm * 3 + x * .08) * 1.2;
2653
+ c.beginPath(); c.moveTo(x0, gnd(lv, x0) + 1);
2654
+ for (let x = x0 + 10; x < x1; x += 10) c.lineTo(x, surf(x));
2655
+ c.lineTo(x1, gnd(lv, x1) + 1); c.quadraticCurveTo(xm, ym + 28, x0, gnd(lv, x0) + 1); c.closePath();
2656
+ const gr = c.createLinearGradient(0, ym - 2, 0, ym + 20); gr.addColorStop(0, mid); gr.addColorStop(1, lo); inked(c, gr, 3);
2657
+ c.strokeStyle = hi; c.lineWidth = 2; c.beginPath(); for (let x = x0 + 10; x < x1 - 8; x += 8) { const y = surf(x) + 1; x === x0 + 10 ? c.moveTo(x, y) : c.lineTo(x, y); } c.stroke();
2658
+ for (let i = 0; i < 4; i++) { const f = (tm * .6 + i * .27 + t.i * .13) % 1, bx = x0 + 14 + ((i * 47 + t.i * 31) % Math.max(1, t.w - 28)); c.globalAlpha = 1 - f; c.lineWidth = 1.5; c.beginPath(); c.arc(bx, surf(bx) + 8 - f * 10, 2 + f * 3, 0, 6.283); c.stroke(); }
2659
+ c.globalAlpha = 1;
2660
+ if (t.k === "lava") { pushL(xm, ym, t.w * .9, "#ff7b00", .7, .55); if (Math.random() < .15) addP(x0 + Math.random() * t.w, ym, rnd(-10, 10), rnd(-80, -40), .5, "#ffba08", rnd(2, 4), 0); }
2661
+ else if (t.k === "acid") { pushL(xm, ym, t.w * .7, "#9ef01a", .4, .35); if (Math.random() < .06) addP(x0 + Math.random() * t.w, ym - 2, rnd(-6, 6), rnd(-40, -20), 1, "rgba(180,255,120,1)", rnd(5, 8), 4); }
2662
+ else pushL(xm, ym, t.w * .6, "#9d4edd", .35, .3);
2663
+ } else if (t.k === "ice") { // glassy slick on the ground, with a moving glint
2664
+ const x0 = t.x, x1 = t.x + t.w;
2665
+ c.beginPath(); c.moveTo(x0, gnd(lv, x0) + 2);
2666
+ for (let x = x0; x <= x1; x += 12) c.lineTo(x, gnd(lv, x) - 2);
2667
+ c.lineTo(x1, gnd(lv, x1) + 2); for (let x = x1; x >= x0; x -= 12) c.lineTo(x, gnd(lv, x) + 6); c.closePath();
2668
+ c.globalAlpha = .85; inked(c, "#caf0f8", 2); c.globalAlpha = 1;
2669
+ const gl = x0 + ((tm * 90 + t.i * 50) % (t.w + 60)) - 30;
2670
+ if (gl > x0 && gl < x1 - 14) { c.strokeStyle = "#fff"; c.lineWidth = 2; c.beginPath(); c.moveTo(gl, gnd(lv, gl) + 3); c.lineTo(gl + 12, gnd(lv, gl + 12) - 1); c.stroke(); }
2671
+ pushL(t.x + t.w / 2, gy, t.w * .6, "#caf0f8", .3, .15);
2672
+ } else if (t.k === "spore") { // bulb on a stalk: swells and blushes red, then bursts into a toxic cloud
2673
+ const sw = ph > 1.6 && ph < 2.3 ? (ph - 1.6) / .7 : 0, burst = ph >= 2.3 && ph < 3.1, s = 1 + sw * .35 + (sw ? Math.sin(tm * 30) * .04 : 0);
2674
+ if (crossed(2.3) && near) { for (let i = 0; i < 18; i++) { const a = Math.random() * 6.283; addP(t.x, gy - 28, Math.cos(a) * rnd(60, 160), Math.sin(a) * rnd(60, 160), rnd(.4, .8), "#b5e48c", rnd(3, 6), 0); } sfx("hiss"); }
2675
+ c.beginPath(); c.moveTo(t.x - 5, gy + 1); c.quadraticCurveTo(t.x - 8, gy - 10, t.x - 3, gy - 16); c.lineTo(t.x + 3, gy - 16); c.quadraticCurveTo(t.x + 8, gy - 10, t.x + 5, gy + 1); c.closePath(); inked(c, "#386641", 2.5);
2676
+ c.save(); c.translate(t.x, gy - 27); c.scale(s, s);
2677
+ if (!burst) {
2678
+ c.beginPath(); c.ellipse(0, 0, 15, 13, 0, 0, 6.283); inked(c, sw ? mix("#a7c957", "#ff4d6d", sw) : "#a7c957", 3);
2679
+ circ(c, -5, -3, 3, "#f2e8cf"); circ(c, 5, 3, 2.2, "#f2e8cf"); circ(c, 4, -6, 1.6, "#f2e8cf");
2680
+ } else for (const a of [-2.3, -1.57, -.84]) { c.save(); c.rotate(a + Math.PI / 2); c.beginPath(); c.ellipse(0, -9, 5, 10, 0, 0, 6.283); inked(c, "#6a994e", 2); c.restore(); }
2681
+ c.restore();
2682
+ if (burst) { const k = (ph - 2.3) / .8; c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = (1 - k) * .55; for (let i = 0; i < 6; i++) { const a = i * 1.047 + t.i, r = 30 + k * 40; c.drawImage(glowTex("#b5e48c"), t.x + Math.cos(a) * r * .6 - r, gy - 30 + Math.sin(a) * r * .4 - r, r * 2, r * 2); } c.restore(); }
2683
+ if (sw) warnMark(c, t.x, gy - 52, tm);
2684
+ pushL(t.x, gy - 26, 50, "#b5e48c", .4, .25 + sw * .5);
2685
+ } else if (t.k === "geyser") { // volcanic vent: rumbles and glows, then shoots a column of fire
2686
+ const warn = ph < 1, live = ph >= 1 && ph < 2.1;
2687
+ if (wrapped && near) sfx("rumble");
2688
+ if (crossed(1) && near) { sfx("hiss"); addShake(.12); }
2689
+ c.beginPath(); c.moveTo(t.x - 28, gy + 2); c.lineTo(t.x - 12, gy - 15); c.lineTo(t.x + 12, gy - 15); c.lineTo(t.x + 28, gy + 2); c.closePath(); inked(c, "#3b1d14", 3);
2690
+ c.fillStyle = warn || live ? "#ffba08" : "#ff7b00"; c.fillRect(t.x - 8, gy - 17, 16, 4);
2691
+ if (warn) {
2692
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .3 + .6 * ph * Math.abs(Math.sin(tm * 18)); c.drawImage(glowTex("#ff7b00"), t.x - 30, gy - 40, 60, 46); c.restore();
2693
+ if (Math.random() < .3) addP(t.x + rnd(-8, 8), gy - 16, rnd(-15, 15), rnd(-120, -60), .4, "#ffba08", 3, 0);
2694
+ if (ph > .35) warnMark(c, t.x, gy - 46, tm);
2695
+ } else if (live) {
2696
+ const h = 230 * Math.min(1, (ph - 1) / .15) * (ph > 1.9 ? (2.1 - ph) / .2 : 1), wob = Math.sin(tm * 30) * 4;
2697
+ c.save(); c.globalCompositeOperation = "lighter";
2698
+ const gr = c.createLinearGradient(0, gy - h, 0, gy); gr.addColorStop(0, "rgba(255,123,0,0)"); gr.addColorStop(.3, "rgba(255,123,0,.85)"); gr.addColorStop(1, "rgba(255,243,176,.95)");
2699
+ c.fillStyle = gr; c.beginPath(); c.moveTo(t.x - 22, gy - 14); c.quadraticCurveTo(t.x - 30 + wob, gy - h * .5, t.x - 6, gy - h); c.lineTo(t.x + 6, gy - h); c.quadraticCurveTo(t.x + 30 - wob, gy - h * .5, t.x + 22, gy - 14); c.closePath(); c.fill();
2700
+ c.globalAlpha = .8; c.fillStyle = "#fff3b0"; c.fillRect(t.x - 5, gy - h * .7, 10, h * .7 - 14);
2701
+ c.restore();
2702
+ if (Math.random() < .6) addP(t.x + rnd(-16, 16), gy - rnd(20, h), rnd(-40, 40), rnd(-160, -60), .5, "#ff7b00", rnd(3, 6), 0);
2703
+ pushL(t.x, gy - h / 2, 200, "#ff7b00", 1, .8);
2704
+ } else pushL(t.x, gy - 14, 40, "#ff7b00", .4, .25);
2705
+ } else if (t.k === "icicle") { // ice ledge at the top of the screen; the big icicle shakes, drops, shatters and grows back
2706
+ c.beginPath(); c.moveTo(t.x - 58, -10); c.lineTo(t.x + 58, -10); c.lineTo(t.x + 52, 20);
2707
+ for (let i = 5; i >= -5; i--) c.lineTo(t.x + i * 10, 24 + (i % 2 ? 0 : 6) + ((i + 5 + t.i) % 3) * 3);
2708
+ c.closePath(); inked(c, "#a9d6e5", 3);
2709
+ c.beginPath(); for (const o of [-40, -24, 26, 42]) { const len = 10 + ((o + t.i * 7) % 3 + 3) % 3 * 5; c.moveTo(o + t.x - 4, 26); c.lineTo(o + t.x + 4, 26); c.lineTo(o + t.x, 26 + len); c.closePath(); } inked(c, "#caf0f8", 2);
2710
+ const spike = (x, base, tip, a) => { c.globalAlpha = a; c.beginPath(); c.moveTo(x - 9, base); c.lineTo(x + 9, base); c.lineTo(x, tip); c.closePath(); inked(c, "#e0fbfc", 2.5); c.strokeStyle = "#fff"; c.lineWidth = 1.5; c.beginPath(); c.moveTo(x - 3, base + 4); c.lineTo(x - 1, tip - 8); c.stroke(); c.globalAlpha = 1; };
2711
+ const iy = icicleY(lv, t, ph);
2712
+ if (ph < 3.4) { const sh = ph > 2.6 ? Math.sin(tm * 60) * 2 : 0; spike(t.x + sh, 24, 66, 1); if (ph > 2.6) warnMark(c, t.x, 92, tm); }
2713
+ else if (iy != null) spike(t.x, iy - 42, iy, 1);
2714
+ else if (ph > 4.2) { const k = (ph - 4.2) / .8; spike(t.x, 24, 24 + 42 * k, k); }
2715
+ if (ph > 2.6 && ph < 3.85) { const k = (ph - 2.6) / 1.25; c.fillStyle = "rgba(0,0,0," + (.15 + .3 * k) + ")"; c.beginPath(); c.ellipse(t.x, gy + 1, 8 + 14 * k, 3 + 2 * k, 0, 0, 6.283); c.fill(); } // shadow: where it will land
2716
+ if (crossed(2.6) && near) sfx("crack");
2717
+ if (crossed(3.85) && near) { for (let i = 0; i < 16; i++) addP(t.x, gy - 4, rnd(-170, 170), rnd(-220, -60), rnd(.4, .7), i % 2 ? "#e0fbfc" : "#a9d6e5", rnd(2, 5), 0, null, 900); sfx("crack"); addShake(.1); }
2718
+ } else if (t.k === "fence") { // emitter on the ground, hover drone above; the beam between them switches on and off
2719
+ const on = ph < 1.8, warn = ph > 3.4, top = gy - 122, bob = Math.sin(tm * 3 + t.i) * 2;
2720
+ if (wrapped && near) sfx("zap");
2721
+ if (on || warn) {
2722
+ c.save(); c.globalCompositeOperation = "lighter"; c.lineCap = "round";
2723
+ if (on) { for (const [w, col, a] of [[16, "#f72585", .35], [7, "#ff70a6", .8], [2.5, "#ffffff", 1]]) { c.globalAlpha = a; c.strokeStyle = col; c.lineWidth = w + Math.sin(tm * 40) * 1.5; c.beginPath(); c.moveTo(t.x, gy - 12); c.lineTo(t.x, top + bob + 2); c.stroke(); } pushL(t.x, gy - 60, 110, "#f72585", .8, .6); }
2724
+ else if (Math.floor(tm * 16) % 2) { c.globalAlpha = .6; c.strokeStyle = "#ffd60a"; c.lineWidth = 2; c.setLineDash([6, 6]); c.beginPath(); c.moveTo(t.x, gy - 12); c.lineTo(t.x, top + bob + 2); c.stroke(); c.setLineDash([]); }
2725
+ c.restore();
2726
+ } else { c.strokeStyle = "rgba(255,77,109,.18)"; c.lineWidth = 2; c.setLineDash([2, 8]); c.beginPath(); c.moveTo(t.x, gy - 12); c.lineTo(t.x, top + bob); c.stroke(); c.setLineDash([]); }
2727
+ rr(c, t.x - 10, gy - 15, 20, 16, 3); inked(c, "#343a40", 3);
2728
+ rr(c, t.x - 13, top - 10 + bob, 26, 12, 4); inked(c, "#343a40", 3);
2729
+ const lc = on ? "#ff4d6d" : warn ? "#ffd60a" : "#6c757d";
2730
+ ic(c, t.x, gy - 8, 3, lc, 1.5); ic(c, t.x, top - 4 + bob, 3, lc, 1.5);
2731
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .5 + .3 * Math.sin(tm * 20); c.drawImage(glowTex("#4cc9f0"), t.x - 8, top + bob - 2, 16, 14); c.restore(); // drone thruster
2732
+ } else if (t.k === "pad") { // springy launch pad with glowing chevrons
2733
+ const b = Math.abs(Math.sin(tm * 4 + t.i)) * 2;
2734
+ rr(c, t.x - 22, gy - 8, 44, 10, 4); inked(c, "#3c096c", 3);
2735
+ c.strokeStyle = "#adb5bd"; c.lineWidth = 2; c.beginPath(); for (const sx of [-10, 10]) { c.moveTo(t.x + sx - 4, gy - 8); c.lineTo(t.x + sx + 4, gy - 11 + b * .5); c.lineTo(t.x + sx - 4, gy - 13 + b); } c.stroke();
2736
+ rr(c, t.x - 19, gy - 18 + b, 38, 6, 3); inked(c, "#f72585", 2.5);
2737
+ c.save(); c.globalCompositeOperation = "lighter"; c.strokeStyle = "#e0aaff"; c.lineWidth = 2.5; c.lineCap = "round";
2738
+ for (let i = 0; i < 3; i++) { const y = gy - 28 - i * 10; c.globalAlpha = .25 + .75 * ((tm * 2 - i * .33) % 1 + 1) % 1; c.beginPath(); c.moveTo(t.x - 7, y + 5); c.lineTo(t.x, y); c.lineTo(t.x + 7, y + 5); c.stroke(); }
2739
+ c.restore();
2740
+ pushL(t.x, gy - 16, 60, "#f72585", .5, .4);
2741
+ } else if (t.k === "spikes") { // bone spikes in a grate: a red rumble, then they stab up
2742
+ const warn = ph < .6, up = ph >= .6 && ph < 1.5, hgt = up ? 26 * Math.min(1, (ph - .6) / .08) : warn ? 4 + Math.sin(tm * 40) * 1.5 : 1;
2743
+ if (crossed(.6) && near) sfx("crack");
2744
+ c.fillStyle = "#2b0b0e"; c.beginPath(); c.moveTo(t.x, gnd(lv, t.x) + 1); for (let x = t.x; x <= t.x + t.w; x += 10) c.lineTo(x, gnd(lv, x) + 1); c.lineTo(t.x + t.w, gnd(lv, t.x + t.w) + 5); c.lineTo(t.x, gnd(lv, t.x) + 5); c.closePath(); c.fill();
2745
+ c.beginPath(); for (let x = t.x + 6; x < t.x + t.w - 2; x += 10) { const y = gnd(lv, x); c.moveTo(x - 4, y); c.lineTo(x, y - hgt); c.lineTo(x + 4, y); c.closePath(); }
2746
+ inked(c, up ? "#e9d8a6" : "#9a8c98", 2);
2747
+ if (warn) { c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .5 + .4 * Math.sin(tm * 30); c.drawImage(glowTex("#ff595e"), t.x - 10, gy - 20, t.w + 20, 30); c.restore(); if (ph > .2) warnMark(c, t.x + t.w / 2, gy - 22, tm); }
2748
+ if (up) pushL(t.x + t.w / 2, gy - 10, 60, "#ff595e", .4, .3);
2749
+ }
2750
+ }
2751
+ }
2752
+
2753
+ </script>
2754
+ <script>
2755
+ // Star Squad: weather. The host picks a weather that fits the planet (levels.json "wx") on arrival and again about once
2756
+ // a minute, shared as wx (kind) and wd (wind direction). Wind pushes heroes, thunderstorms throw telegraphed lightning,
2757
+ // meteor showers drop meteors, fog and blizzards cut how far you can see. The rest is screen-space particles.
2758
+ "use strict";
2759
+ const WXT = [
2760
+ { n: "Clear skies", i: "☀" },
2761
+ { n: "Rain", i: "🌧", rain: 1, dark: .08 },
2762
+ { n: "Thunderstorm", i: "⛈", rain: 1.7, dark: .24, bolt: 1, wind: 30 },
2763
+ { n: "Snow", i: "❄", snow: 1 },
2764
+ { n: "Blizzard", i: "🌨", snow: 2.4, wind: 80, fog: .3, dark: .08 },
2765
+ { n: "Fog", i: "🌫", fog: .62 },
2766
+ { n: "Ash Storm", i: "🌋", ash: 1, wind: 55, dark: .16, fog: .14 },
2767
+ { n: "Meteor Shower", i: "☄", met: 1, dark: .06 },
2768
+ { n: "Spore Storm", i: "🍄", spore: 1, wind: 40, fog: .2 }
2769
+ ];
2770
+ let WXP = [], wxLast = -1, wxCam = 0, wxT0 = 0, ltT = 0, mtT = 0;
2771
+ const FOGT = {};
2772
+ const wxNow = s => WXT[(s && s.wx) || 0] || WXT[0];
2773
+ function wxWind(s) { return (wxNow(s).wind || 0) * ((s && s.wd) || 1); } // px per second pushing heroes sideways
2774
+
2775
+ // ---------- Host ----------
2776
+ function hostWeather(g, al, lv, now) {
2777
+ const list = lv.P.wx || [0];
2778
+ if (!HC.wxT || now > HC.wxT) {
2779
+ let n = pick(list); for (let i = 0; i < 6 && list.length > 1 && n === HC.wx; i++) n = pick(list);
2780
+ HC.wx = n; HC.wd = Math.random() < .5 ? -1 : 1; HC.wxT = now + rnd(45, 75);
2781
+ }
2782
+ const X = WXT[HC.wx] || WXT[0];
2783
+ if (HC.phase === "clear" || !al.length) return;
2784
+ if (X.bolt && now > ltT) { // lightning: a glowing target on the ground for 1.1 s, then the strike
2785
+ ltT = now + rnd(2.2, 4); const a = pick(al), x = clamp(a.x + rnd(-170, 170), 40, L - 40);
2786
+ HC.obj[nid("o")] = { t: "ltn", x: Math.round(x), y: Math.round(gnd(lv, x)), o: "", w: now + 1.1, e: now + 1.3 };
2787
+ }
2788
+ if (X.met && now > mtT) { mtT = now + rnd(1.5, 2.5); const a = pick(al); ebAdd(g, a.x + rnd(-280, 140), -40, rnd(40, 120), 140, 3, 6); } // falling meteor (alien shot kind 3)
2789
+ }
2790
+ function strike(g, x, y, al, dmM) { // lightning hurts heroes and aliens alike
2791
+ for (const a of al) if (Math.abs(a.x - x) < 42) hurt(g, a.id, 20 * dmM);
2792
+ for (const mid in HC.mobs) { const m = HC.mobs[mid]; if (Math.abs(m.x - x) < 46 + AL[m.t].r * .5) damage(g, mid, 60 * (1 + .3 * HC.planet), "", null); }
2793
+ fx(g, "strike", { x: Math.round(x), y: Math.round(y) });
2794
+ }
2795
+
2796
+ // ---------- Everyone ----------
2797
+ function wxClient(g) { // a banner when the sky changes
2798
+ const w = g.shared.wx || 0;
2799
+ if (w !== wxLast) { if (wxLast >= 0 && chosen) { const X = WXT[w] || WXT[0]; banner(X.i + " " + X.n.toUpperCase(), "#bde0fe", 2.2); } wxLast = w; }
2800
+ }
2801
+ function fogTex(col) {
2802
+ if (FOGT[col]) return FOGT[col];
2803
+ const [cv, x] = mkC(128, 128, 1), gr = x.createRadialGradient(64, 64, 0, 64, 64, 64);
2804
+ gr.addColorStop(0, rgba(col, .8)); gr.addColorStop(1, rgba(col, 0)); x.fillStyle = gr; x.fillRect(0, 0, 128, 128); return FOGT[col] = cv;
2805
+ }
2806
+ // lightning warning: a crackling target ring where the bolt will land
2807
+ function drawLtn(c, o, now, tm) {
2808
+ const k = clamp(1 - (o.w - now) / 1.1, 0, 1), gy = o.y;
2809
+ c.save(); c.globalCompositeOperation = "lighter";
2810
+ c.globalAlpha = .35 + .5 * k * Math.abs(Math.sin(tm * 20)); c.drawImage(glowTex("#ffd60a"), o.x - 40, gy - 22, 80, 34);
2811
+ c.strokeStyle = "#ffd60a"; c.lineWidth = 2; c.globalAlpha = .8; c.beginPath(); c.ellipse(o.x, gy, 44 - 20 * k, 8 - 3 * k, 0, 0, 6.283); c.stroke();
2812
+ c.globalAlpha = .25 + .4 * k; c.setLineDash([10, 14]); c.beginPath(); c.moveTo(o.x, 0); c.lineTo(o.x, gy); c.stroke(); c.setLineDash([]);
2813
+ c.restore();
2814
+ if (Math.random() < .3) addP(o.x + rnd(-20, 20), gy - 4, rnd(-40, 40), rnd(-90, -30), .2, "#ffd60a", 2, 0);
2815
+ pushL(o.x, gy - 10, 70, "#ffd60a", .5, .4 * k);
2816
+ }
2817
+ function drawWeather(c, g, lv) {
2818
+ const s = g.shared, X = wxNow(s), dt = Math.min(.05, Math.max(0, T - wxT0)), dc = camX - wxCam, wind = wxWind(s);
2819
+ wxT0 = T; wxCam = camX; // particles live in screen space but slide with the camera, so they feel part of the world
2820
+ const amt = X.rain || X.snow || (X.ash || X.spore ? 1 : 0), want = amt ? Math.round((Q >= 2 ? 120 : Q === 1 ? 80 : 50) * Math.min(1.7, amt)) : 0;
2821
+ while (WXP.length < want) WXP.push({ x: Math.random() * W, y: Math.random() * H, z: .5 + Math.random() * .8, p: Math.random() * 6.283 });
2822
+ if (WXP.length > want) WXP.length = want;
2823
+ const wrapX = d => { if (d.x < -20) d.x += W + 40; else if (d.x > W + 20) d.x -= W + 40; };
2824
+ if (X.dark) { c.fillStyle = "rgba(4,6,18," + X.dark + ")"; c.fillRect(0, 0, W, H); }
2825
+ if (X.bolt && Math.random() < dt * .08) { flashScr = { c: "#c8d0ff", t: T }; sfx("rumble"); } // far-off sheet lightning
2826
+ if (X.met) { // streaks high in the sky (the dangerous ones are the rocks that fall)
2827
+ c.save(); c.globalCompositeOperation = "lighter"; c.lineCap = "round"; c.strokeStyle = "#ffd6a5"; c.lineWidth = 2;
2828
+ for (let i = 0; i < 4; i++) { const ph = T * .35 + i * .29, f = ph % 1, n = Math.floor(ph), x = ((i * 233 + n * 377) % (W + 200)) - 100 + f * 260, y = 20 + (i * 47) % 140 + f * 120; c.globalAlpha = Math.sin(f * Math.PI) * .8; c.beginPath(); c.moveTo(x, y); c.lineTo(x - 60, y - 28); c.stroke(); }
2829
+ c.restore();
2830
+ }
2831
+ if (X.rain) { // slanted streaks, one batched path; splashes where they hit the ground
2832
+ const vx = wind * 2.2 - 40, vy = 760;
2833
+ c.strokeStyle = "rgba(190,215,255,.42)"; c.lineWidth = 1.3; c.beginPath();
2834
+ for (const d of WXP) {
2835
+ d.x += vx * d.z * dt - dc * d.z; d.y += vy * d.z * dt; wrapX(d);
2836
+ const gy = gnd(lv, d.x + camX);
2837
+ if (d.y > gy) { if (Math.random() < .25) addP(d.x + camX, gy - 1, rnd(-40, 40), rnd(-90, -40), .25, "#cfe2ff", 1.6, 0, null, 600); d.y = -rnd(0, 60); d.x = Math.random() * W; }
2838
+ c.moveTo(d.x, d.y); c.lineTo(d.x - vx * .02 * d.z, d.y - 16 * d.z);
2839
+ }
2840
+ c.stroke();
2841
+ }
2842
+ if (X.snow) {
2843
+ c.fillStyle = "rgba(255,255,255,.85)";
2844
+ for (const d of WXP) {
2845
+ d.x += (wind * 1.4 + Math.sin(T * 1.5 + d.p) * 22) * d.z * dt - dc * d.z; d.y += (55 + 40 * d.z) * X.snow * .6 * dt; wrapX(d);
2846
+ if (d.y > H + 4) { d.y = -4; d.x = Math.random() * W; }
2847
+ const r = 1 + d.z * 1.6; c.fillRect(d.x - r / 2, d.y - r / 2, r, r);
2848
+ }
2849
+ }
2850
+ if (X.ash || X.spore) { // glowing embers rising, or purple spores drifting down
2851
+ const tex = glowTex(X.ash ? "#ff7b00" : "#c77dff");
2852
+ c.save(); c.globalCompositeOperation = "lighter";
2853
+ for (const d of WXP) {
2854
+ d.x += (wind * 1.6 + Math.sin(T + d.p) * 18) * d.z * dt - dc * d.z; d.y += (X.ash ? -22 + Math.sin(T * 2 + d.p) * 20 : 14 + Math.sin(T * 1.3 + d.p) * 16) * d.z * dt; wrapX(d);
2855
+ if (d.y < -10) d.y += H + 20; else if (d.y > H + 10) d.y -= H + 20;
2856
+ const r = (X.ash ? 2 : 3) * d.z * (1 + .3 * Math.sin(T * 4 + d.p)); c.globalAlpha = .55 * d.z; c.drawImage(tex, d.x - r * 2, d.y - r * 2, r * 4, r * 4);
2857
+ }
2858
+ c.restore();
2859
+ }
2860
+ if (X.fog) { // thick away from you, thinner close by, with drifting banks
2861
+ const col = mix(lv.P.sky[1], "#ffffff", .35), hx = chosen ? px - camX : W / 2, hy = chosen ? py - 30 : H / 2;
2862
+ const gr = c.createRadialGradient(hx, hy, 90, hx, hy, 430); gr.addColorStop(0, rgba(col, X.fog * .15)); gr.addColorStop(1, rgba(col, Math.min(.9, X.fog * 1.25)));
2863
+ c.fillStyle = gr; c.fillRect(0, 0, W, H);
2864
+ const ft = fogTex(col), span = W + 400; c.globalAlpha = X.fog * .7;
2865
+ for (let i = 0; i < 5; i++) { const x = ((i * 260 - camX * .6 + T * (12 + wind * .5)) % span + span) % span - 200, y = 250 + (i % 3) * 50; c.drawImage(ft, x - 200, y - 60, 400, 120); }
2866
+ c.globalAlpha = 1;
2867
+ }
2868
+ }
2869
+
2870
+ </script>
2871
+ <script>
2872
+ // Star Squad: helper toys. Capsules drop like spells; the Spell button calls the helper for a while (ALLY in data.js).
2873
+ // Helpers are host objects in HC.obj (t = dr / mh / md / dc, o = owner), so every screen sees them move smoothly.
2874
+ // Attack Drone: hovers by its owner and zaps the nearest alien. Mini Mech: walks into the fight and blasts aliens.
2875
+ // Medic Drone: heals every hero close to it once a second. Holo Decoy: a hologram the aliens chase instead of you.
2876
+ "use strict";
2877
+ // ---------- Host ----------
2878
+ function callAlly(g, id, st, t) {
2879
+ const now = g.now, f = st.f > 0 ? 1 : -1, lv = level(HC.planet);
2880
+ for (const oid in HC.obj) { const o = HC.obj[oid]; if (o.t === t && o.o === id) { o.e = now + ALLY[t]; return; } } // calling the same helper again: it stays longer
2881
+ const fly = t === "dr" || t === "md", x0 = clamp(st.x + (t === "dc" ? f * 70 : -f * 34), 30, L - 30), x = fly ? x0 : outHole(lv, x0, 20); // ground helpers never stand in a pit
2882
+ HC.obj[nid("o")] = { t, o: id, x, y: fly ? st.y - 90 : gnd(lv, x), f, e: now + ALLY[t], nt: now + .6, sd: Math.random() * 6 };
2883
+ }
2884
+ function nearMob(g, x, y, r) {
2885
+ let best = null, bd = r * r;
2886
+ for (const mid in HC.mobs) { const m = HC.mobs[mid]; if ((m.dn && m.dn > g.now) || m.sh) continue; const d2 = (m.x - x) ** 2 + (m.y - y) ** 2; if (d2 < bd) { bd = d2; best = mid; } }
2887
+ return best;
2888
+ }
2889
+ function allyTick(g, oid, o, lv, now, dt) {
2890
+ const p = findP(g, o.o), q = HC.pl[o.o];
2891
+ if (now > o.e || !p || !p.state || !q) { delete HC.obj[oid]; fx(g, "poof", { x: Math.round(o.x), y: Math.round(o.y - 20) }); return; }
2892
+ const ox = p.state.x, oy = p.state.y, f = p.state.f || 1, lm = lvMul(q.lv);
2893
+ if (o.t === "dr" || o.t === "md") { // hover beside the owner, a little behind, bobbing
2894
+ const tx = ox - f * 34 + Math.sin(now * 2 + o.sd) * 10, ty = oy - 92 + Math.sin(now * 3 + o.sd) * 6;
2895
+ o.x += (tx - o.x) * Math.min(1, dt * 4); o.y += (ty - o.y) * Math.min(1, dt * 4); o.f = f;
2896
+ }
2897
+ if (o.t === "dr" && now > o.nt) {
2898
+ const mid = nearMob(g, o.x, o.y, 400);
2899
+ if (mid) { const m = HC.mobs[mid]; o.nt = now + .35; o.f = m.x > o.x ? 1 : -1; fx(g, "zap", { x1: Math.round(o.x + o.f * 10), y1: Math.round(o.y + 4), x2: Math.round(m.x), y2: Math.round(m.y), c: "#4cc9f0" }); damage(g, mid, 9 * lm, o.o); }
2900
+ else o.nt = now + .15;
2901
+ } else if (o.t === "md" && now > o.nt) {
2902
+ o.nt = now + 1; let healed = false;
2903
+ for (const pid in HC.pl) {
2904
+ const pq = HC.pl[pid], pp = findP(g, pid); if (pq.dead || !pp || !pp.state || pq.hp >= pq.mhp) continue;
2905
+ if (Math.abs(pp.state.x - o.x) < 170) { pq.hp = Math.min(pq.mhp, pq.hp + Math.ceil(pq.mhp * .04)); healed = true; }
2906
+ }
2907
+ if (healed) { HC.dPl = 1; fx(g, "heal", { x: Math.round(o.x), y: Math.round(o.y) }); }
2908
+ } else if (o.t === "mh") { // walk to blaster range of the nearest alien near the owner, otherwise trot at heel
2909
+ const mid = nearMob(g, ox, oy - 20, 420), m = mid && HC.mobs[mid];
2910
+ const tx = m ? m.x - Math.sign(m.x - o.x || 1) * 150 : ox - f * 46, dx = tx - o.x;
2911
+ if (Math.abs(dx) > 6) { o.x = clamp(o.x + Math.sign(dx) * Math.min(Math.abs(dx), 190 * dt), 20, L - 20); o.f = Math.sign(dx); }
2912
+ if (m) o.f = m.x > o.x ? 1 : -1;
2913
+ o.x = outHole(lv, o.x, 16); o.y = gnd(lv, o.x);
2914
+ if (m && now > o.nt && Math.abs(m.x - o.x) < 300) { o.nt = now + .45; fx(g, "zap", { x1: Math.round(o.x + o.f * 18), y1: Math.round(o.y - 22), x2: Math.round(m.x), y2: Math.round(m.y), c: "#ff9f1c" }); damage(g, mid, 11 * lm, o.o, o.x); }
2915
+ }
2916
+ // Holo Decoy stands still; hostTick makes the aliens chase it
2917
+ }
2918
+
2919
+ // ---------- Drawing ----------
2920
+ function allyColor(o) { const s = GREF && srcOf(GREF), q = s && s.pl && s.pl[o.o]; return q ? CLS[q.c].c : "#4cc9f0"; }
2921
+ function drawAlly(c, o, x, y, tm, now) {
2922
+ const f = o.f || 1, col = allyColor(o), fading = o.e && o.e - now < 3 && Math.floor(tm * 8) % 2;
2923
+ c.save(); if (fading) c.globalAlpha = .4;
2924
+ if (o.t === "dr" || o.t === "md") { // little quadcopter: spinning rotors, inked body, lens or red cross
2925
+ const by = y + Math.sin(tm * 4 + hsh(o.o) % 6) * 2;
2926
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = (fading ? .2 : .5) + .2 * Math.sin(tm * 20); c.drawImage(glowTex(o.t === "md" ? "#80ffdb" : "#4cc9f0"), x - 10, by + 4, 20, 18); c.restore();
2927
+ c.beginPath(); c.moveTo(x - 17, by - 7); c.lineTo(x + 17, by - 7); ink(c, 3); c.stroke();
2928
+ for (const sx of [-17, 17]) { c.fillStyle = "rgba(206,212,218,.7)"; c.beginPath(); c.ellipse(x + sx, by - 10, 1 + 9 * Math.abs(Math.cos(tm * 40 + sx)), 2, 0, 0, 6.283); c.fill(); }
2929
+ rr(c, x - 12, by - 8, 24, 14, 6); inked(c, o.t === "md" ? "#f8f9fa" : col, 3);
2930
+ if (o.t === "md") { c.fillStyle = "#e63946"; c.fillRect(x - 2, by - 5, 4, 9); c.fillRect(x - 5, by - 2, 10, 4); }
2931
+ else { ic(c, x + f * 6, by - 1, 3.5, "#212529", 2); circ(c, x + f * 6, by - 1, 1.6, "#ff4d6d"); }
2932
+ pushL(x, by, 60, o.t === "md" ? "#80ffdb" : "#4cc9f0", .5, .35);
2933
+ if (o.t === "md") { c.globalAlpha = (fading ? .1 : .2) + .1 * Math.sin(tm * 4); c.strokeStyle = "#80ffdb"; c.lineWidth = 2; c.beginPath(); c.arc(x, by + 70, 160, Math.PI * 1.15, Math.PI * 1.85); c.stroke(); } // heal range
2934
+ } else if (o.t === "mh") { // mini mech: stubby legs, boxy body in the owner's colour, arm blaster, antenna light
2935
+ c.fillStyle = "rgba(0,0,0,.3)"; c.beginPath(); c.ellipse(x, y + 1, 14, 3.5, 0, 0, 6.283); c.fill();
2936
+ c.translate(x, y); c.scale(f, 1);
2937
+ const w = Math.sin(x * .12) * 4;
2938
+ c.lineCap = "round"; c.beginPath(); c.moveTo(-5, -12); c.lineTo(-5 + w, 0); c.moveTo(5, -12); c.lineTo(5 - w, 0); ink(c, 6); c.stroke(); c.strokeStyle = "#6c757d"; c.lineWidth = 3; c.stroke(); c.lineCap = "butt";
2939
+ rr(c, -12, -32, 24, 21, 5); inked(c, "#adb5bd", 3); c.fillStyle = col; c.fillRect(-10.5, -23, 21, 4);
2940
+ rr(c, 6, -26, 16, 7, 2); inked(c, "#495057", 2.5);
2941
+ rr(c, -8, -41, 16, 10, 3); inked(c, "#343a40", 2.5); circ(c, 3, -36, 2.4, "#80ffdb");
2942
+ c.strokeStyle = "#212529"; c.lineWidth = 2; c.beginPath(); c.moveTo(-3, -41); c.lineTo(-3, -48); c.stroke(); circ(c, -3, -49, 2.2, Math.floor(tm * 3) % 2 ? "#ff4d6d" : "#9d0208");
2943
+ pushL(x, y - 26, 55, col, .4, .2);
2944
+ } else if (o.t === "dc") { // hologram of its owner on a little projector
2945
+ const s = GREF && srcOf(GREF), q = s && s.pl && s.pl[o.o];
2946
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .35;
2947
+ const gr = c.createLinearGradient(0, y - 70, 0, y); gr.addColorStop(0, "rgba(184,192,255,0)"); gr.addColorStop(1, "rgba(184,192,255,.8)");
2948
+ c.fillStyle = gr; c.beginPath(); c.moveTo(x - 6, y - 4); c.lineTo(x - 26, y - 70); c.lineTo(x + 26, y - 70); c.lineTo(x + 6, y - 4); c.closePath(); c.fill(); c.restore();
2949
+ if (q) { c.save(); c.globalAlpha = (fading ? .2 : .5) + .15 * Math.sin(tm * 30); hero(c, q.c, x, y - 4, f, tm, false, false, false, 1); c.restore(); }
2950
+ c.strokeStyle = "rgba(184,192,255,.5)"; c.lineWidth = 1; for (let yy = y - 64; yy < y - 4; yy += 5) { c.beginPath(); c.moveTo(x - 16, yy + (tm * 40 % 5)); c.lineTo(x + 16, yy + (tm * 40 % 5)); c.stroke(); }
2951
+ rr(c, x - 12, y - 6, 24, 7, 3); inked(c, "#343a40", 2.5); circ(c, x, y - 3, 2, "#b8c0ff");
2952
+ pushL(x, y - 30, 80, "#b8c0ff", .5, .35);
2953
+ }
2954
+ c.restore();
2955
+ }
2956
+
2957
+ </script>
2958
+ <script>
2959
+ // Star Squad: in-game HUD and the hero select screen
2960
+ "use strict";
2961
+ // ---------- HUD ----------
2962
+ function bar(c, x, y, w, h, f, col) { c.fillStyle = "rgba(0,0,0,.55)"; rr(c, x, y, w, h, h / 2); c.fill(); if (f > 0) { c.fillStyle = col; rr(c, x, y, Math.max(h, w * clamp(f, 0, 1)), h, h / 2); c.fill(); } }
2963
+ function hud(c, g, src) {
2964
+ const now = g.now, pi = g.shared.planet || 0, lv = level(pi), P = lv.P, q = src.pl && src.pl[ME];
2965
+ c.textAlign = "left";
2966
+ if (chosen && q) {
2967
+ const C = CLS[q.c];
2968
+ c.fillStyle = "rgba(0,0,0,.45)"; rr(c, 8, 8, 238, 66, 10); c.fill();
2969
+ c.save(); c.beginPath(); c.rect(8, 8, 42, 66); c.clip(); hero(c, q.c, 26, 70, 1, T, false, false, false, .7); c.restore();
2970
+ c.fillStyle = "#fff"; c.font = "bold 13px sans-serif"; c.fillText(C.n + " · Lv " + q.lv, 52, 24);
2971
+ const hf = q.hp / q.mhp;
2972
+ bar(c, 52, 30, 186, 11, ghostHp / q.mhp, "rgba(255,255,255,.55)"); // damage drains slowly so you see how much you lost
2973
+ if (hf > 0) { c.fillStyle = hf > .5 ? "#52b788" : hf > .25 ? "#ffb703" : "#e63946"; rr(c, 52, 30, Math.max(11, 186 * clamp(hf, 0, 1)), 11, 5.5); c.fill(); }
2974
+ c.font = "bold 9px sans-serif"; c.fillStyle = "#fff"; c.fillText(q.hp + " / " + q.mhp, 58, 39);
2975
+ bar(c, 52, 45, 186, 5, q.xp / need(q.lv), "#ffd60a");
2976
+ c.font = "11px sans-serif"; c.fillStyle = "#ddd";
2977
+ const wk = q.w && q.ammo > 0 ? q.w : C.w, pk = wk !== C.w;
2978
+ c.fillStyle = WP[wk].c; c.fillText(WP[wk].n + (pk ? " · " + q.ammo : TIERN[tierOf(q.lv)] + " · ∞"), 52, 65);
2979
+ if (wk === "H" && spin > .05) bar(c, 52, 68, 110, 3, spin, spin > .9 ? "#ff7b00" : "#ffd166"); // Heavy MG spin-up
2980
+ if (q.w && q.ammo > 0 && WP[q.w].ammo) { const af = q.ammo / WP[q.w].ammo; bar(c, 170, 59, 68, 5, af, af < .2 && Math.floor(T * 6) % 2 ? "#e63946" : WP[q.w].c); }
2981
+ // combo meter
2982
+ if (q.cb >= 2 && q.ct > now) {
2983
+ const cf = clamp((q.ct - now) / 2.5, 0, 1), pop = 1 + Math.max(0, .25 - (T - (hud.cbT || 0))) * 2;
2984
+ if (q.cb !== hud.cbN) { hud.cbN = q.cb; hud.cbT = T; }
2985
+ c.save(); c.translate(127, 96); c.scale(pop, pop); c.textAlign = "center";
2986
+ c.font = "bold 20px sans-serif"; c.lineWidth = 4; c.strokeStyle = "rgba(0,0,0,.6)"; c.strokeText("x" + q.cb + " COMBO", 0, 0); c.fillStyle = "#ff70a6"; c.fillText("x" + q.cb + " COMBO", 0, 0);
2987
+ c.restore(); bar(c, 77, 102, 100, 4, cf, "#ff70a6"); c.textAlign = "left";
2988
+ }
2989
+ // skill + spell slots
2990
+ const sy = H - 60;
2991
+ c.fillStyle = "rgba(0,0,0,.5)"; rr(c, 10, sy, 50, 50, 8); c.fill(); rr(c, 66, sy, 50, 50, 8); c.fill();
2992
+ c.strokeStyle = C.c; c.lineWidth = 2; rr(c, 10, sy, 50, 50, 8); c.stroke();
2993
+ c.textAlign = "center"; c.fillStyle = C.c; c.font = "bold 9px sans-serif";
2994
+ const words = C.sk.split(" "); c.fillText(words[0], 35, sy + 22); c.fillText(words[1] || "", 35, sy + 33);
2995
+ const cdl = (q.sk || 0) - now;
2996
+ if (cdl > 0) { c.fillStyle = "rgba(0,0,0,.6)"; const fh = 50 * clamp(cdl / C.cd, 0, 1); c.fillRect(10, sy + 50 - fh, 50, fh); c.fillStyle = "#fff"; c.font = "bold 16px sans-serif"; c.fillText(Math.ceil(cdl), 35, sy + 31); }
2997
+ if (q.sp && SP[q.sp]) { const s = SP[q.sp]; c.globalAlpha = .3 + .2 * Math.sin(T * 5); circ(c, 91, sy + 22, 16, s.c); c.globalAlpha = 1; circ(c, 91, sy + 22, 11, s.c); c.fillStyle = "#1a1a2e"; c.font = "bold 13px sans-serif"; c.fillText(s.i, 91, sy + 27); c.fillStyle = s.c; c.font = "bold 8px sans-serif"; c.fillText(s.s || s.n.split(" ")[0], 91, sy + 45); }
2998
+ else { c.fillStyle = "#666"; c.font = "bold 9px sans-serif"; c.fillText("no spell", 91, sy + 29); }
2999
+ if (T < 25) { c.fillStyle = "rgba(255,255,255,.5)"; c.font = "9px sans-serif"; const pn = PAD.on && padNames(); c.fillText(pn ? pn.skill : "I", 35, sy - 3); c.fillText(pn ? pn.spell : "L", 91, sy - 3); c.textAlign = "left"; c.fillText(pn ? pn.fire + " fire · " + pn.jump + " jump · ↓ drop" : "J fire · K jump · ↓ drop", 124, H - 14); }
3000
+ c.textAlign = "left";
3001
+ }
3002
+ // planet + progress
3003
+ c.textAlign = "center"; c.fillStyle = "rgba(0,0,0,.4)"; rr(c, W / 2 - 120, 8, 240, 34, 10); c.fill();
3004
+ c.fillStyle = P.top; c.font = "bold 12px sans-serif"; c.fillText("PLANET " + (pi + 1) + " · " + P.n.toUpperCase(), W / 2, 22);
3005
+ bar(c, W / 2 - 100, 28, 200, 6, 1, "rgba(255,255,255,.12)");
3006
+ for (const p of g.players) { if (!p.state || p.state.cls < 0) continue; const x = p.me ? px : p.state.x; circ(c, W / 2 - 100 + 200 * clamp(x / L, 0, 1), 31, p.me ? 4 : 3, p.me ? "#fff" : (CLS[Math.round(p.state.cls)] || CLS[0]).c); }
3007
+ circ(c, W / 2 + 104, 31, 5, "#ff4d6d");
3008
+ // weather pill under the planet bar
3009
+ const X = wxNow(g.shared), wd = wxWind(g.shared), wt = X.i + " " + X.n + (wd ? (wd > 0 ? " · wind →" : " · wind ←") : "");
3010
+ c.font = "bold 10px sans-serif"; const ww = c.measureText(wt).width + 18;
3011
+ c.fillStyle = "rgba(0,0,0,.4)"; rr(c, W / 2 - ww / 2, 45, ww, 16, 8); c.fill(); c.fillStyle = "#bde0fe"; c.fillText(wt, W / 2, 57);
3012
+ // lives + squad
3013
+ c.textAlign = "right"; c.fillStyle = "#ff4d6d"; c.font = "bold 14px sans-serif"; c.fillText("♥ " + (g.shared.lives == null ? 5 : g.shared.lives), W - 12, 24);
3014
+ let yy = 56; // below the corner buttons
3015
+ for (const p of g.players) {
3016
+ if (p.me || !p.state || p.state.cls < 0) continue; const o = src.pl && src.pl[p.id]; if (!o) continue;
3017
+ c.fillStyle = CLS[o.c].c; c.font = "bold 10px sans-serif"; c.fillText((p.name || "Ally").slice(0, 10) + " Lv" + o.lv, W - 12, yy);
3018
+ bar(c, W - 92, yy + 3, 80, 4, o.dead ? 0 : o.hp / o.mhp, "#52b788"); yy += 22;
3019
+ }
3020
+ // boss bar
3021
+ const bm = src.boss && src.mobs && src.mobs[src.boss];
3022
+ if (bm) {
3023
+ c.textAlign = "center"; c.fillStyle = "#ff4d6d"; c.font = "bold 12px sans-serif"; c.fillText(P.boss.toUpperCase() + (bm.sh ? " · SHIELDED: DESTROY THE EGGS" : ""), W / 2, H - 30);
3024
+ bar(c, W / 2 - 180, H - 24, 360, 10, bm.hp / bm.mhp, bm.sh ? "#ff70a6" : "#e63946");
3025
+ }
3026
+ // mini boss bar (a little shorter, in orange)
3027
+ const mb = !bm && src.mini && src.mobs && src.mobs[src.mini];
3028
+ if (mb) {
3029
+ c.textAlign = "center"; c.fillStyle = "#ff9f1c"; c.font = "bold 12px sans-serif"; c.fillText("MINI BOSS · " + MINI[pi % 5].n.toUpperCase(), W / 2, H - 30);
3030
+ bar(c, W / 2 - 130, H - 24, 260, 9, mb.hp / mb.mhp, "#ff9f1c");
3031
+ }
3032
+ // GO arrow
3033
+ if (chosen && g.shared.phase !== "clear" && !bm && !mb) {
3034
+ let near = false; for (const id in src.mobs || {}) { const m = src.mobs[id]; if (m && Math.abs(m.x - px) < 750) { near = true; break; } }
3035
+ if (!near && Math.floor(T * 2.5) % 2) { c.textAlign = "right"; c.fillStyle = "#ffd60a"; c.font = "bold 28px sans-serif"; c.fillText("GO ➜", W - 20, H / 2); }
3036
+ }
3037
+ if (g.shared.phase === "clear") { c.textAlign = "center"; c.fillStyle = "#80ffdb"; c.font = "bold 16px sans-serif"; c.fillText("Warping to next planet in " + Math.max(0, Math.ceil((g.shared.nextAt || 0) - now)) + "…", W / 2, H / 2 + 40); }
3038
+ // banners
3039
+ c.textAlign = "center";
3040
+ banners.forEach((b, i) => {
3041
+ const a = clamp(Math.min((T - b.s) * 5, (b.e - T) * 3), 0, 1), sc = 1 + Math.max(0, .3 - (T - b.s)) ;
3042
+ c.save(); c.globalAlpha = a; c.translate(W / 2, 130 + i * 44); c.scale(sc, sc);
3043
+ c.font = "bold 32px sans-serif"; c.lineWidth = 6; c.strokeStyle = "rgba(0,0,0,.7)"; c.strokeText(b.t, 0, 0); c.fillStyle = b.c; c.fillText(b.t, 0, 0);
3044
+ c.restore();
3045
+ });
3046
+ // hurt vignette
3047
+ if (T - hurtT < .3) { const a = 1 - (T - hurtT) / .3, gr = c.createRadialGradient(W / 2, H / 2, H * .35, W / 2, H / 2, W * .6); gr.addColorStop(0, "rgba(230,57,70,0)"); gr.addColorStop(1, "rgba(230,57,70," + (.55 * a) + ")"); c.fillStyle = gr; c.fillRect(0, 0, W, H); }
3048
+ // revive prompt
3049
+ if (nearRevive && !(q && q.dead)) {
3050
+ c.textAlign = "center"; c.fillStyle = "#80ffdb"; c.font = "bold 14px sans-serif";
3051
+ c.fillText(nearRevive.rp > 0 ? "Reviving " + nearRevive.n + "…" : "Stand on " + nearRevive.n + "'s beacon to revive them", W / 2, H - 70);
3052
+ }
3053
+ // downed
3054
+ if (chosen && q && q.dead) {
3055
+ c.fillStyle = "rgba(60,0,0,.35)"; c.fillRect(0, 0, W, H);
3056
+ c.fillStyle = "#fff"; c.font = "bold 28px sans-serif"; c.fillText("YOU WERE DOWNED", W / 2, H / 2 - 10);
3057
+ c.font = "15px sans-serif";
3058
+ c.fillText(q.dead > 0 ? "A teammate can revive you, or you drop back in " + Math.max(0, Math.ceil(q.dead - now)) + "…" : "No lives left. A teammate can still revive you at your beacon!", W / 2, H / 2 + 18);
3059
+ if (q.rp > 0) { c.fillStyle = "#80ffdb"; c.fillText("Being revived… " + Math.round(clamp(q.rp / 2, 0, 1) * 100) + "%", W / 2, H / 2 + 42); }
3060
+ }
3061
+ if (chosen && !q) { c.fillStyle = "#fff"; c.font = "14px sans-serif"; c.fillText("Joining the squad…", W / 2, H / 2); }
3062
+ }
3063
+ function drawSelect(c, g) {
3064
+ c.fillStyle = "rgba(5,5,20,.72)"; c.fillRect(0, 0, W, H);
3065
+ c.textAlign = "center"; c.fillStyle = "#ffd60a"; c.font = "bold 30px sans-serif"; c.fillText("STAR SQUAD", W / 2, 40);
3066
+ c.fillStyle = "#ccc"; c.font = "13px sans-serif"; c.fillText("Choose your hero · levels are saved per hero" + ((g.profile && g.profile.best) ? " · best: planet " + g.profile.best : ""), W / 2, 60);
3067
+ for (let i = 0; i < 6; i++) {
3068
+ const C = CLS[i], x = 35 + (i % 3) * 248, y = 74 + Math.floor(i / 3) * 172, w = 234, h = 160, on = i === sel;
3069
+ c.fillStyle = on ? "rgba(255,255,255,.14)" : "rgba(255,255,255,.06)"; rr(c, x, y, w, h, 12); c.fill();
3070
+ if (on) { c.strokeStyle = C.c; c.lineWidth = 3 + Math.sin(T * 6); rr(c, x, y, w, h, 12); c.stroke(); }
3071
+ c.globalAlpha = .25; circ(c, x + 46, y + 100, 40, C.c); c.globalAlpha = 1;
3072
+ c.save(); c.beginPath(); c.rect(x + 2, y + 2, 88, h - 4); c.clip(); // keep long weapons out of the stats column
3073
+ hero(c, i, x + 40, y + 146, 1, T, on, false, on && Math.floor(T * 2) % 3 === 0, 1.15);
3074
+ c.restore();
3075
+ c.textAlign = "left"; c.fillStyle = C.c; c.font = "bold 18px sans-serif"; c.fillText(C.n, x + 94, y + 30);
3076
+ c.fillStyle = "#ffd60a"; c.font = "bold 12px sans-serif"; c.fillText("Lv " + profLv(g, i), x + 94, y + 48);
3077
+ c.fillStyle = "#aaa"; c.font = "10px sans-serif"; c.fillText("HP", x + 94, y + 68); c.fillText("SPD", x + 94, y + 84);
3078
+ bar(c, x + 120, y + 61, 100, 7, C.hp / 170, "#52b788"); bar(c, x + 120, y + 77, 100, 7, (C.spd - 150) / 100, "#4cc9f0");
3079
+ c.fillStyle = "#fff"; c.font = "bold 11px sans-serif"; c.fillText("★ " + C.sk, x + 94, y + 108);
3080
+ c.fillStyle = "#ccc"; c.font = "11px sans-serif"; c.fillText(C.d, x + 94, y + 126);
3081
+ c.fillStyle = "#888"; c.fillText(WP[C.w].n, x + 94, y + 144);
3082
+ }
3083
+ c.textAlign = "center"; c.fillStyle = "#fff"; c.font = "13px sans-serif";
3084
+ const pn = PAD.on && padNames();
3085
+ c.fillText(pn ? "🎮 Stick / d-pad to choose · " + pn.jump + " to deploy · " + pn.sh + " difficulty" : "Arrows to choose · Space / Fire to deploy · or tap a hero", W / 2, H - 10);
3086
+ c.textAlign = "left"; c.fillStyle = "rgba(255,255,255,.35)"; c.font = "10px sans-serif"; c.fillText(VERSION, 12, H - 10);
3087
+ drawDiff(c, g);
3088
+ }
3089
+
3090
+ // ---------- Difficulty picker (hero screen, top-left) ----------
3091
+ // Only the host can change it, and only before anyone deploys; everyone else sees the host's pick. Keys 1 / 2 / 3 work too.
3092
+ function diffAt(i) { return { x: 14 + i * 74, y: 12, w: 70, h: 30 }; }
3093
+ function diffHit(x, y) { for (let i = 0; i < 3; i++) { const b = diffAt(i); if (x >= b.x - 2 && x <= b.x + b.w + 2 && y >= b.y - 6 && y <= b.y + b.h + 6) return i; } return -1; }
3094
+ function diffLocked(g) { return !g.isHost || !HC || Object.keys(HC.pl).length > 0; }
3095
+ function curDiff(g) { const d = g.isHost && HC ? HC.df : g.shared.df; return d == null ? 1 : d; }
3096
+ function setDiff(g, d) {
3097
+ if (diffLocked(g) || d === HC.df) return;
3098
+ diffPref = d; HC.df = d; HC.lives = DIFF[d].lives; sfx("pick");
3099
+ try { g.profile && g.profile.set && g.profile.set({ df: d }); } catch (e) { }
3100
+ }
3101
+ function drawDiff(c, g) {
3102
+ const cur = curDiff(g), lock = diffLocked(g);
3103
+ for (let i = 0; i < 3; i++) {
3104
+ const D = DIFF[i], b = diffAt(i), on = i === cur;
3105
+ c.globalAlpha = lock && !on ? .3 : 1;
3106
+ c.fillStyle = on ? D.c : "rgba(255,255,255,.07)"; rr(c, b.x, b.y, b.w, b.h, 9); c.fill();
3107
+ c.strokeStyle = D.c; c.lineWidth = on ? 2 : 1.5; c.stroke();
3108
+ c.textAlign = "center"; c.fillStyle = on ? "#14121c" : D.c; c.font = "bold 11px sans-serif"; c.fillText(D.n, b.x + b.w / 2, b.y + 19);
3109
+ }
3110
+ c.globalAlpha = 1; c.textAlign = "left"; c.fillStyle = "#aaa"; c.font = "10px sans-serif";
3111
+ c.fillText(lock ? "Difficulty set by the host" : DIFF[cur].d, 16, 60); // same baseline as the subtitle
3112
+ }
3113
+ window.addEventListener("keydown", e => { if (!chosen && !helpOpen && GREF && e.key >= "1" && e.key <= "3") setDiff(GREF, +e.key - 1); });
3114
+
3115
+ // ---------- Corner buttons (music, how to play) and the How to Play panel ----------
3116
+ // On the hero screen they sit top-right; in a match they sit beside the player panel. Same look, same spacing.
3117
+ let helpOpen = false, helpSeen = false;
3118
+ const ICONS = ["mus", "help"];
3119
+ function iconAt(i) { return chosen ? { x: W - 106 + i * 34, y: 24 } : { x: W - 62 + i * 34, y: 28 }; } // in a match: left of the ♥ lives
3120
+ function iconHit(x, y) { for (let i = 0; i < ICONS.length; i++) { const p = iconAt(i); if (Math.hypot(x - p.x, y - p.y) < 17) return ICONS[i]; } return null; }
3121
+ function iconBtn(c, x, y, glyph, col, on) {
3122
+ c.fillStyle = "rgba(0,0,0,.35)"; c.beginPath(); c.arc(x, y + 2, 14, 0, 6.283); c.fill(); // drop shadow
3123
+ c.fillStyle = on ? "#2b2d4a" : "#1a1b2e"; c.beginPath(); c.arc(x, y, 14, 0, 6.283); c.fill();
3124
+ c.strokeStyle = col; c.lineWidth = 2; c.stroke();
3125
+ c.textAlign = "center"; c.textBaseline = "middle"; c.fillStyle = col; c.font = "bold 15px sans-serif"; c.fillText(glyph, x, y + 1); c.textBaseline = "alphabetic";
3126
+ }
3127
+ function drawIcons(c) {
3128
+ const m = iconAt(0), h = iconAt(1);
3129
+ musIcon(c, m.x, m.y);
3130
+ if (!helpSeen && !chosen) { const a = (T * 1.5) % 1; c.globalAlpha = 1 - a; c.strokeStyle = "#80ffdb"; c.lineWidth = 2; c.beginPath(); c.arc(h.x, h.y, 14 + a * 12, 0, 6.283); c.stroke(); c.globalAlpha = 1; } // gentle "look here" pulse
3131
+ iconBtn(c, h.x, h.y, "?", "#80ffdb", helpOpen);
3132
+ }
3133
+ function openHelp(v) { helpOpen = v; if (v) { helpSeen = true; sfx("pick"); } }
3134
+ window.addEventListener("keydown", e => { if (e.repeat) return; if (e.key === "h" || e.key === "H" || e.key === "?") openHelp(!helpOpen); else if (e.key === "Escape" && helpOpen) openHelp(false); });
3135
+
3136
+ const HELP_KEYS = [["← → A D", "Move"], ["↑ W Space K", "Jump (hold = higher)"], ["J", "Fire (auto-aims)"], ["I", "Class skill"], ["L", "Spell / call helper"], ["↓", "Drop through a ledge"], ["M", "Music on / off"], ["H", "Open / close this help"]];
3137
+ const HELP_TIPS = ["A yellow ! means an alien is about to attack", "Stand on a downed friend's beacon 2 s to revive", "Chain kills quickly for a combo bonus", "Your hero's weapon upgrades at Lv 5 and Lv 10", "Clear a planet: +2 lives and everyone is back", "Traps and lightning hurt aliens too: lure them!"];
3138
+ const HELP_CRATES = { H: "Heavy MG", S: "Spread", L: "Laser", R: "Rockets", F: "Flamer", Z: "Tesla", C: "Cryo", M: "Missiles" };
3139
+ function helpPanel(c) {
3140
+ c.fillStyle = "rgba(5,5,20,.78)"; c.fillRect(0, 0, W, H);
3141
+ const px0 = 50, py0 = 26, pw = 700, ph = 398;
3142
+ c.fillStyle = "#14162b"; rr(c, px0, py0, pw, ph, 16); c.fill();
3143
+ c.strokeStyle = "rgba(128,255,219,.6)"; c.lineWidth = 2; rr(c, px0, py0, pw, ph, 16); c.stroke();
3144
+ c.textAlign = "center"; c.fillStyle = "#ffd60a"; c.font = "bold 22px sans-serif"; c.fillText("HOW TO PLAY", W / 2, py0 + 34);
3145
+ iconBtn(c, px0 + pw - 26, py0 + 26, "✕", "#ff8fa3", false);
3146
+ c.strokeStyle = "rgba(255,255,255,.1)"; c.lineWidth = 1; c.beginPath(); c.moveTo(W / 2, py0 + 58); c.lineTo(W / 2, py0 + ph - 40); c.stroke();
3147
+ // left column: controls as key chips, labels lined up in one column
3148
+ const lx = 80, rx = 424, head = (t, x, y) => { c.textAlign = "left"; c.fillStyle = "#80ffdb"; c.font = "bold 13px sans-serif"; c.fillText(t, x, y); };
3149
+ head("CONTROLS", lx, py0 + 70);
3150
+ c.textBaseline = "middle";
3151
+ HELP_KEYS.forEach(([k, d], i) => {
3152
+ const y = py0 + 94 + i * 25; c.font = "bold 11px sans-serif"; const kw = c.measureText(k).width + 14;
3153
+ c.fillStyle = "#2b2d4a"; rr(c, lx, y - 10, kw, 20, 5); c.fill(); c.fillStyle = "rgba(255,255,255,.15)"; c.fillRect(lx + 3, y + 8, kw - 6, 2);
3154
+ c.textAlign = "center"; c.fillStyle = "#fff"; c.fillText(k, lx + kw / 2, y);
3155
+ c.textAlign = "left"; c.fillStyle = "#ddd"; c.font = "12px sans-serif"; c.fillText(d, lx + 112, y);
3156
+ });
3157
+ c.fillStyle = "#999"; c.font = "11px sans-serif"; c.fillText("Phones: d-pad + Fire, Jump, Skill and Spell buttons", lx, py0 + 302);
3158
+ c.fillText("Each hero shoots differently: try them all!", lx, py0 + 322);
3159
+ const pn = padNames(); // controller layout (Xbox names until a PlayStation or Switch pad is connected)
3160
+ c.fillStyle = PAD.on ? "#80ffdb" : "#999"; c.fillText("🎮 Pad: stick / d-pad move · " + pn.jump + " jump · " + pn.fire + " fire", lx, py0 + 342);
3161
+ c.fillText(pn.skill + " skill · " + pn.spell + " spell · " + pn.start + " help · " + pn.back + " music", lx, py0 + 358);
3162
+ // right column: tips, then the crates and spell orbs you can find
3163
+ head("TIPS", rx, py0 + 70);
3164
+ c.font = "12px sans-serif";
3165
+ HELP_TIPS.forEach((t, i) => { const y = py0 + 92 + i * 19; circ(c, rx + 4, y, 2.5, "#ffd60a"); c.fillStyle = "#ddd"; c.fillText(t, rx + 14, y); });
3166
+ head("CRATES (limited ammo)", rx, py0 + 214);
3167
+ Object.keys(HELP_CRATES).forEach((k, i) => {
3168
+ const x = rx + (i % 4) * 76, y = py0 + 234 + Math.floor(i / 4) * 24;
3169
+ c.fillStyle = WP[k].c; rr(c, x, y - 9, 18, 18, 4); c.fill();
3170
+ c.textAlign = "center"; c.fillStyle = "#14121c"; c.font = "bold 11px sans-serif"; c.fillText(k, x + 9, y + 1);
3171
+ c.textAlign = "left"; c.fillStyle = "#ccc"; c.font = "11px sans-serif"; c.fillText(HELP_CRATES[k], x + 23, y);
3172
+ });
3173
+ // spell orbs, then helper toys (both go in the spell slot; the Spell button uses them)
3174
+ const row = (title, keys, y0) => {
3175
+ head(title, rx, y0);
3176
+ keys.forEach((k, i) => {
3177
+ const s = SP[k], x = rx + i * 76, y = y0 + 20;
3178
+ circ(c, x + 9, y, 9, s.c); c.textAlign = "center"; c.fillStyle = "#14121c"; c.font = "bold 10px sans-serif"; c.fillText(s.i, x + 9, y + 1);
3179
+ c.textAlign = "left"; c.fillStyle = "#ccc"; c.font = "11px sans-serif"; c.fillText(s.s || { fz: "Freeze", th: "Storm", hl: "Heal", sh: "Shield" }[k], x + 23, y);
3180
+ });
3181
+ };
3182
+ row("SPELL ORBS", LOOTS, py0 + 288);
3183
+ row("HELPER TOYS (Spell button calls them)", LOOTA, py0 + 336);
3184
+ c.textBaseline = "alphabetic"; c.textAlign = "center"; c.fillStyle = "#888"; c.font = "11px sans-serif";
3185
+ c.fillText("Tap anywhere, press H or any controller button to close", W / 2, py0 + ph - 12);
3186
+ }
3187
+
3188
+ </script>
3189
+ <script>
3190
+ // Star Squad: game controllers (Xbox, PlayStation, Switch Pro, 8BitDo and most USB / Bluetooth pads) through the browser Gamepad API.
3191
+ // padPoll runs at the start of every update; main.js merges the stick and buttons with keyboard / touch (padAxis, padHeld)
3192
+ // and button presses go through the same gameInput as keys, so controllers behave exactly like the keyboard.
3193
+ "use strict";
3194
+ // "standard" layout: 0 bottom, 1 right, 2 left, 3 top face button, 4/5 bumpers, 6/7 triggers, 8 back/share, 9 start/options, 12-15 d-pad
3195
+ const PADB = { jump: [0], fire: [2, 7], skill: [1, 4], spell: [3, 5] };
3196
+ // what is printed on the buttons, per controller family (same positions, different names)
3197
+ const PADN = {
3198
+ xbox: { jump: "A", fire: "X / RT", skill: "B / LB", spell: "Y / RB", start: "Menu", back: "View", sh: "LB / RB" },
3199
+ ps: { jump: "✕", fire: "□ / R2", skill: "○ / L1", spell: "△ / R1", start: "Options", back: "Share", sh: "L1 / R1" },
3200
+ sw: { jump: "B", fire: "Y / ZR", skill: "A / L", spell: "X / R", start: "+", back: "−", sh: "L / R" }
3201
+ };
3202
+ const PAD = { on: false, kind: "xbox", ax: { x: 0, y: 0 }, held: {}, prev: {}, nav: "", navT: 0 };
3203
+ function padKind(id) { id = (id || "").toLowerCase(); return /054c|playstation|dualsense|dualshock|wireless controller/.test(id) ? "ps" : /057e|nintendo|pro controller|joy-con/.test(id) ? "sw" : "xbox"; }
3204
+ function padGet() { try { for (const p of navigator.getGamepads ? navigator.getGamepads() : []) if (p && p.connected) return p; } catch (e) { } return null; }
3205
+ function padNames() { return PADN[PAD.kind]; }
3206
+ function padPoll(g) {
3207
+ const gp = padGet();
3208
+ if (!gp) { if (PAD.on) { PAD.on = false; PAD.held = {}; PAD.prev = {}; PAD.ax = { x: 0, y: 0 }; banner("🎮 Controller disconnected", "#bde0fe", 1.6); } return; }
3209
+ if (!PAD.on) { PAD.on = true; PAD.kind = padKind(gp.id); banner("🎮 Controller connected", "#80ffdb", 1.6); }
3210
+ const b = i => !!(gp.buttons[i] && (gp.buttons[i].pressed || gp.buttons[i].value > .5)), dz = v => Math.abs(v) < .3 ? 0 : v;
3211
+ let x = dz(gp.axes[0] || 0), y = dz(gp.axes[1] || 0);
3212
+ if (b(14)) x = -1; if (b(15)) x = 1; if (b(12)) y = -1; if (b(13)) y = 1;
3213
+ PAD.ax = { x, y };
3214
+ const h = { start: b(9), back: b(8), lb: b(4), rb: b(5) };
3215
+ for (const k in PADB) h[k] = PADB[k].some(b);
3216
+ const pr = k => h[k] && !PAD.prev[k];
3217
+ PAD.held = h; PAD.prev = h;
3218
+ if (pr("start")) { openHelp(!helpOpen); return; }
3219
+ if (pr("back")) toggleMusic(g);
3220
+ if (!chosen && !helpOpen) { // hero screen: stick / d-pad picks a hero (repeats while held), bumpers change the difficulty
3221
+ const d = Math.abs(x) > .5 ? (x > 0 ? "right" : "left") : Math.abs(y) > .5 ? (y > 0 ? "down" : "up") : "";
3222
+ if (d && (d !== PAD.nav || T > PAD.navT)) { gameInput(g, d); PAD.navT = T + (d !== PAD.nav ? .4 : .18); }
3223
+ PAD.nav = d;
3224
+ if (pr("lb")) setDiff(g, Math.max(0, curDiff(g) - 1));
3225
+ if (pr("rb")) setDiff(g, Math.min(2, curDiff(g) + 1));
3226
+ }
3227
+ for (const k of ["jump", "fire", "skill", "spell"]) if (pr(k)) gameInput(g, k);
3228
+ }
3229
+ // movement: keyboard / touch plus the controller; in a match the stick never jumps (A does) but down still drops through ledges
3230
+ function padAxis(a) {
3231
+ a = a || { x: 0, y: 0 }; if (!PAD.on) return a;
3232
+ return { x: clamp(a.x + PAD.ax.x, -1, 1), y: clamp(a.y + Math.max(0, PAD.ax.y), -1, 1) };
3233
+ }
3234
+ function padHeld(h) { h = h || {}; if (!PAD.on) return h; return { fire: h.fire || PAD.held.fire, jump: h.jump || PAD.held.jump, skill: h.skill || PAD.held.skill, spell: h.spell || PAD.held.spell }; }
3235
+ // a short buzz on controllers that can (Chrome / Edge; others just ignore it)
3236
+ function padRumble(weak, strong, ms) {
3237
+ if (!PAD.on) return;
3238
+ try { const v = padGet().vibrationActuator; if (v && v.playEffect) { const p = v.playEffect("dual-rumble", { duration: ms, weakMagnitude: weak, strongMagnitude: strong }); if (p && p.catch) p.catch(() => { }); } } catch (e) { }
3239
+ }
3240
+
3241
+ </script>
3242
+ <script>
3243
+ // Star Squad: the app's start screen (drawn by the game shell) shows the version on one line and the room code on another.
3244
+ // This joins them into one line, "v2.6 – Room code: 66GX", to save space. It only rearranges text it can find:
3245
+ // when there is no room code (Play alone) or the screen looks different, it leaves everything as it is.
3246
+ "use strict";
3247
+ (function () {
3248
+ let queued = false, last = 0;
3249
+ function owners(re) { // elements whose own text matches re (not inside the canvas or our own tag)
3250
+ const out = [], w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); let n;
3251
+ while ((n = w.nextNode())) { const el = n.parentElement; if (el && re.test(n.nodeValue) && !el.closest("canvas, script, style, .ss-ver")) out.push(el); }
3252
+ return out;
3253
+ }
3254
+ function tidy() {
3255
+ queued = false; last = Date.now();
3256
+ if (!document.body || typeof VERSION === "undefined") return;
3257
+ const room = owners(/room\s*code/i)[0], ver = owners(new RegExp("^\\s*" + VERSION.replace(/\./g, "\\.") + "\\s*$"))[0];
3258
+ const verOnly = ver && ver.textContent.trim() === VERSION && (!room || !ver.contains(room));
3259
+ if (!room) { if (verOnly) ver.style.display = ""; return; } // no room code on screen: keep the version line
3260
+ if (!room.querySelector(".ss-ver")) { const s = document.createElement("span"); s.className = "ss-ver"; s.textContent = VERSION + " – "; s.style.opacity = ".75"; room.insertBefore(s, room.firstChild); }
3261
+ if (verOnly) ver.style.display = "none";
3262
+ }
3263
+ function soon() { if (queued) return; queued = true; setTimeout(tidy, Math.max(0, 250 - (Date.now() - last))); } // at most 4 checks a second
3264
+ new MutationObserver(soon).observe(document.documentElement, { childList: true, subtree: true, characterData: true });
3265
+ soon();
3266
+ })();
3267
+
3268
+ </script>
3269
+ <script>
3270
+ // Star Squad: the Game({...}) shell call: setup, host loop, local player update, input, actions and the frame draw (loads last)
3271
+ "use strict";
3272
+ // ---------- Input: one press from the keyboard, the touch buttons or a game controller (pad.js) ----------
3273
+ function gameInput(g, a) {
3274
+ if (helpOpen) { openHelp(false); if (!chosen) return; } // any key closes the help; on the hero screen it does nothing else
3275
+ if (!chosen) {
3276
+ if (a === "left") sel = (sel + 5) % 6; else if (a === "right") sel = (sel + 1) % 6;
3277
+ else if (a === "up" || a === "down") sel = (sel + 3) % 6;
3278
+ else if (a === "action" || a === "fire" || a === "jump") choose(g);
3279
+ return;
3280
+ }
3281
+ if (a === "up" || a === "action" || a === "jump") jBuf = JBUF;
3282
+ const q = srcOf(g).pl && srcOf(g).pl[ME];
3283
+ if (!q || q.dead) return;
3284
+ if (a === "skill" && g.now >= (q.sk || 0)) g.act("skill", { f: face });
3285
+ if (a === "spell" && q.sp) g.act("spell", {});
3286
+ }
3287
+ // ---------- Game ----------
3288
+ Game({
3289
+ title: "Star Squad: Alien Blitz",
3290
+ help: VERSION, // start screen stays short: just the version (the full guide lives in the ? panel, hud.js helpPanel)
3291
+ aspect: 16 / 9,
3292
+ size: [W, H],
3293
+ multiplayer: { max: 4, min: 1, player: { x: [0, L], y: [-300, 600], cls: [-1, 5], f: [-1, 1], m: [0, 1], j: [0, 1], s: [0, 1], a: [0, 9] } },
3294
+ buttons: [
3295
+ { id: "fire", label: "Fire", key: "j" },
3296
+ { id: "jump", label: "Jump", key: "k" },
3297
+ { id: "skill", label: "Skill", key: "i" },
3298
+ { id: "spell", label: "Spell", key: "l" }
3299
+ ],
3300
+
3301
+ setup(g) {
3302
+ T = 0; chosen = false; camX = 0; parts = []; banners = []; beams = []; bolts = []; flash = {}; disp = {}; ebL = {}; shotPrev = {}; pickT = {}; volleyQ = []; shotN = 0; spin = 0; wxLast = -1; WXP = [];
3303
+ joinT = -9; lastSn = undefined; lastRv = undefined; curPlanet = -1; mateWait = 0; act = 0; lastBoss = ""; lastPhase = ""; vx = 0; vy = 0; flashScr = null;
3304
+ HC = null; seen = {}; hitSet = {}; dead = {}; hitsBuf = []; cutBuf = []; ambT = 3;
3305
+ diffPref = g.profile && g.profile.df != null ? clamp(g.profile.df | 0, 0, 2) : 1;
3306
+ if (g.isHost) { freshMatch = true; g.shared.set({ planet: 0, lives: DIFF[diffPref].lives, df: diffPref, score: 0, phase: "play", nextAt: 0, spawned: 0, boss: "", bossDone: false, mini: "", miniDone: false, front: 100 }); }
3307
+ g.me.set({ x: 0, y: 0, cls: -1, f: 1, m: 0, j: 0, s: 0, a: 0 });
3308
+ GREF = g; MU.on = !(g.profile && g.profile.mus === 0);
3309
+ },
3310
+
3311
+ host(g, dt) {
3312
+ if (!HC || g.now - hcT > .5) {
3313
+ HC = freshMatch ? freshHC(diffPref) : loadHC(g);
3314
+ if (freshMatch) startPlanet(g);
3315
+ freshMatch = false; pushAll(g, true);
3316
+ }
3317
+ hcT = g.now;
3318
+ hostTick(g, dt);
3319
+ pushT -= dt; if (pushT <= 0) { pushT = 1 / 15; pushAll(g, false); }
3320
+ },
3321
+
3322
+ update(g, dt) {
3323
+ T += dt; ME = myId(g);
3324
+ const s = g.shared, src = srcOf(g), pi = s.planet || 0, lv = level(pi);
3325
+ g.score = s.score || 0;
3326
+ updParts(T < slowT ? dt * .3 : dt); shake = Math.max(0, shake - dt * 1.6);
3327
+ banners = banners.filter(b => T < b.e);
3328
+ syncDisp(g, dt, src);
3329
+ if (s.boss && s.boss !== lastBoss) { banner("⚠ " + lv.P.boss + " approaches!", "#ff4d6d", 2.5); sfx("boss"); }
3330
+ lastBoss = s.boss || "";
3331
+ const bwm = s.boss && src.mobs && src.mobs[s.boss];
3332
+ if (bwm && bwm.wu > g.now && bwm.wu !== lastWu) { lastWu = bwm.wu; sfx("warn"); }
3333
+ if (s.phase === "clear" && lastPhase !== "clear") { banner("PLANET CLEARED!", "#80ffdb", 3); MU.fan = true; }
3334
+ lastPhase = s.phase;
3335
+ wxClient(g);
3336
+ GREF = g; setMusic(pi, !chosen || s.phase === "clear" ? 0 : s.boss ? (bwm && bwm.hp < bwm.mhp * .4 ? 3 : 2) : 1); // each boss has its own theme; it speeds up when the boss is enraged
3337
+ padPoll(g); // game controller (pad.js)
3338
+ if (!chosen) { g.me.set({ x: 0, y: 0, cls: -1, f: 1, m: 0, j: 0, s: 0, a: 0 }); camX = (Math.sin(T * .05) * .5 + .5) * (L - W); return; }
3339
+
3340
+ if (pi !== curPlanet) {
3341
+ const first = curPlanet < 0; curPlanet = pi; px = spawnX = 80 + Math.random() * 80; py = -30; vx = 0; vy = 0;
3342
+ // joining a planet already under way (a late joiner, or picked a hero after the others): drop in next to a teammate, not back at the start.
3343
+ // Their positions can arrive a few frames after the first one, so keep looking for 1.2 s (see below), not just once.
3344
+ mateWait = first ? T + 1.2 : 0;
3345
+ banner("PLANET " + (pi + 1) + " · " + lv.P.n.toUpperCase(), lv.P.top, 2.5);
3346
+ if (g.profile && g.profile.set && pi + 1 > (g.profile.best || 0)) { try { g.profile.set({ best: pi + 1 }); } catch (e) { } }
3347
+ }
3348
+ if (mateWait) {
3349
+ const mates = g.players.filter(p => !p.me && p.state && p.state.cls >= 0 && typeof p.state.x === "number" && p.state.x > 200);
3350
+ // next to the most active teammate (state.a, 0-9); a tie goes to the one furthest along
3351
+ const busier = (a, b) => ((b.state.a || 0) - (a.state.a || 0)) || (b.state.x - a.state.x);
3352
+ if (mates.length && Math.abs(px - spawnX) < 40) { const m = mates.reduce((a, b) => busier(a, b) > 0 ? b : a).state; px = clamp(m.x + (Math.random() < .5 ? -1 : 1) * (50 + Math.random() * 60), 40, L - 40); py = -30; vx = 0; vy = 0; mateWait = 0; }
3353
+ else if (T > mateWait || Math.abs(px - spawnX) >= 40) mateWait = 0; // nobody ahead, or the player already ran off: stay where they are
3354
+ }
3355
+ const q = src.pl && src.pl[ME];
3356
+ if ((!q || q.c !== myCls) && T - joinT > 1) { joinT = T; g.act("join", { c: myCls, lv: savedLv, xp: savedXp }); }
3357
+ if (q) {
3358
+ if (lastRv === undefined) lastRv = q.rv; else if (q.rv !== lastRv) { lastRv = q.rv; px = q.rx; py = -30; vy = 0; }
3359
+ if (lastSn === undefined) lastSn = q.sn; else if (q.sn !== lastSn) { lastSn = q.sn; sfx("skill"); if (myCls === 3) { volley(g); volleyQ.push(T + .22); } }
3360
+ saveT -= dt;
3361
+ if (saveT <= 0) { saveT = 2; if (q.lv !== savedLv || q.xp !== savedXp) { savedLv = q.lv; savedXp = q.xp; try { g.profile && g.profile.set && g.profile.set({ ["L" + myCls]: q.lv, ["X" + myCls]: q.xp }); } catch (e) { } } }
3362
+ }
3363
+ const down = !!(q && q.dead), held = padHeld(g.held);
3364
+ // hurt feedback: red flash, knockback away from the nearest alien, short hit-stop
3365
+ if (q && !q.dead && lastHp >= 0 && q.hp < lastHp) {
3366
+ hurtT = T; hsT = Math.max(hsT, .05); addShake(.25); sfx("hurt"); padRumble(.3, .5, 180);
3367
+ let kd = -face, bd = 1e9;
3368
+ for (const id in src.mobs || {}) { const d = disp[id]; if (d && Math.abs(d.x - px) < bd) { bd = Math.abs(d.x - px); kd = px >= d.x ? 1 : -1; } }
3369
+ if (T - fellT > 1.5) { vx = kd * 260; if (onG) { vy = -260; onG = false; } } // no knockback right after a pit fall: it could push you straight back in
3370
+ }
3371
+ if (q) { if (lastHp < 0 || q.hp > lastHp) ghostHp = q.hp; lastHp = q.hp; ghostHp = Math.max(q.hp, ghostHp - q.mhp * .6 * dt); }
3372
+ if (q && q.cb !== lastCb) { if (q.cb >= 10 && q.cb % 10 === 0 && q.cb > lastCb) banner("COMBO x" + q.cb + "!", "#ff70a6", 1.2); lastCb = q.cb; }
3373
+ sq += (0 - sq) * Math.min(1, dt * 12);
3374
+ if (hsT > 0) hsT -= dt; // hit-stop: freeze our own hero for a few frames
3375
+ else if (!down) {
3376
+ const C = CLS[myCls], a = padAxis(g.axis());
3377
+ // snappy acceleration, stronger braking, less control in the air
3378
+ const tf = onG && py >= gnd(lv, px) - 3 ? trapFeel(lv, px) : 0; // the ground under you: 1 ice, 2 goo, 4 bounce pad
3379
+ const tgt = a.x * C.spd * (q && q.sl > g.now ? .55 : 1) * (tf & 2 ? .5 : 1), moving = Math.abs(a.x) > .2; // Frost Breath chills you to 55% speed, goo to 50%
3380
+ const rate = (moving ? (onG ? 2600 : 1700) : (onG ? 3400 : 900)) * dt * (tf & 1 ? .13 : 1); // ice: slow to start, slow to stop
3381
+ vx = vx < tgt ? Math.min(tgt, vx + rate) : Math.max(tgt, vx - rate);
3382
+ // holding Fire locks your facing, so you can back away while shooting
3383
+ if (moving && !held.fire) face = a.x > 0 ? 1 : -1;
3384
+ const jh = !!held.jump || a.y < -.5, edge = jh && !jPrev;
3385
+ coyT = onG ? COYOTE : coyT - dt;
3386
+ if ((edge || jBuf > 0) && coyT > 0) { vy = tf & 2 ? -560 : -700; onG = false; coyT = 0; jumps = 1; jBuf = 0; jCut = jh; sq = .22; sfx("jump"); }
3387
+ else if (!onG && edge && myCls === 3 && jumps < 2) { vy = -600; jumps = 2; jCut = true; sq = .2; for (let i = 0; i < 8; i++) addP(px, py, rnd(-80, 80), rnd(20, 80), .35, "#d8f3dc", 3, 0); sfx("jump"); }
3388
+ if (jCut && !jh && vy < -200) { vy *= .45; jCut = false; } // tap = short hop, hold = full jump
3389
+ if (vy >= 0) jCut = false;
3390
+ jPrev = jh; jBuf = Math.max(0, jBuf - dt);
3391
+ const grav = myCls === 1 && jh && vy > 0 ? 380 : vy > 0 ? 2350 : 1750; // fall faster than you rise
3392
+ vy = Math.min(950, vy + grav * dt);
3393
+ const wasG = onG, fallV = vy, ny = py + vy * dt;
3394
+ const pw = holeAt(lv, px, 10); // already below the rim of a pit: its walls hold you in
3395
+ px = clamp(px + (vx + wxWind(s) * (onG ? .7 : 1)) * dt, 15, L - 15); // weather wind pushes you a little
3396
+ if (pw && py > Math.min(gnd(lv, pw.x), gnd(lv, pw.x + pw.w)) + 6) px = clamp(px, pw.x + 11, pw.x + pw.w - 11);
3397
+ const gy = gnd(lv, px), pit = holeAt(lv, px, 10); onG = false;
3398
+ if (vy >= 0 && !(a.y > .5)) for (const p of lv.plats) { if (px > p.x - 8 && px < p.x + p.w + 8 && py <= p.y + 1 && ny >= p.y) { py = p.y; vy = 0; onG = true; break; } }
3399
+ if (!onG) { py = ny; if (!pit && (py >= gy || (wasG && vy >= 0 && gy - py < 10))) { py = gy; vy = 0; onG = true; } }
3400
+ if (pit && !onG && py > gy + 130) fallPit(g, lv, pit); // out of sight at the bottom
3401
+ if (onG) jumps = 0;
3402
+ if (onG && py >= gnd(lv, px) - 3 && (trapFeel(lv, px) & 4)) { // bounce pad: up to the high ledges
3403
+ vy = -1080; onG = false; jumps = 1; jCut = false; sq = .3; sfx("boing");
3404
+ for (let i = 0; i < 10; i++) addP(px + rnd(-14, 14), py - 4, rnd(-60, 60), rnd(-200, -80), .4, "#e0aaff", 3, 0);
3405
+ }
3406
+ if (onG && !wasG && fallV > 320) { sq = -clamp(fallV / 2600, .1, .3); for (let i = 0; i < 6; i++) addP(px + rnd(-10, 10), py, rnd(-90, 90), rnd(-50, -10), .35, "rgba(200,200,200,1)", rnd(3, 6), 4); }
3407
+ // fire
3408
+ fireCd -= dt;
3409
+ if (q && held.fire && fireCd <= 0) { const k = q.w && q.ammo > 0 ? q.w : C.w; fireCd = fireW(g, k, aim(src), q.lv); }
3410
+ if (!held.fire) { spin = Math.max(0, spin - dt * 2); if (fireCd < -.45) shotN = 0; } // a pause resets bursts, combos and the MG spin-up
3411
+ // pickups
3412
+ for (const id in src.loot || {}) { const l = src.loot[id]; if (!l) continue; if (Math.abs(l.x - px) < 26 && Math.abs(l.y - (py - 20)) < 38 && !(pickT[id] > T)) { pickT[id] = T + .5; g.act("pick", { id }); } }
3413
+ }
3414
+ if (volleyQ.length && T > volleyQ[0]) { volleyQ.shift(); if (!down) volley(g); }
3415
+ // how active this hero has been lately (moving, shooting; fades over ~6 s), shared as 0-9 so a late joiner can drop in where the action is
3416
+ act = act * Math.exp(-dt / 6) + dt * ((Math.abs(vx) > 20 ? 1 : 0) + (T < firing ? 1.5 : 0));
3417
+ g.me.set({ x: Math.round(px), y: Math.round(py), cls: myCls, f: face, m: Math.abs(vx) > 20 ? 1 : 0, j: onG ? 0 : 1, s: T < firing ? 1 : 0, a: Math.min(9, Math.round(act * .6)) });
3418
+ // camera lookahead: show more of the screen in the direction you face and run
3419
+ look += (face * 110 + vx * .12 - look) * (1 - Math.exp(-3 * dt));
3420
+ const tx = clamp(px - W / 2 + look, 0, L - W);
3421
+ camX += (tx - camX) * (1 - Math.exp(-8 * dt));
3422
+ },
3423
+
3424
+ onInput(g, a) { gameInput(g, a); },
3425
+
3426
+ onPointer(g, x, y) {
3427
+ ac();
3428
+ if (helpOpen) { openHelp(false); return; }
3429
+ const icon = iconHit(x, y);
3430
+ if (icon === "mus") { toggleMusic(g); return; }
3431
+ if (icon === "help") { openHelp(true); return; }
3432
+ if (chosen) return;
3433
+ const dh = diffHit(x, y); if (dh >= 0) { setDiff(g, dh); return; }
3434
+ for (let i = 0; i < 6; i++) { const cx = 35 + (i % 3) * 248, cy = 74 + Math.floor(i / 3) * 172; if (x >= cx && x <= cx + 234 && y >= cy && y <= cy + 160) { sel = i; choose(g); return; } }
3435
+ },
3436
+
3437
+ onAction(g, from, name, d) {
3438
+ if (!HC) return false;
3439
+ const id = (from && from.id) || from, now = g.now; d = d || {};
3440
+ if (name === "join") {
3441
+ const c = clamp(d.c | 0, 0, 5), lv = clamp(d.lv | 0, 1, 50), ex = HC.pl[id];
3442
+ if (ex && ex.c === c) return;
3443
+ const mhp = Math.round(CLS[c].hp * (1 + .07 * (lv - 1)));
3444
+ HC.pl[id] = { c, lv, xp: clamp(d.xp | 0, 0, need(lv) - 1), mhp, hp: mhp, w: "", ammo: 0, sp: "", sk: 0, sn: 0, dead: 0, inv: r2(now + 2), ifr: 0, rv: 0, rx: 100, rp: 0, dx: 0, dy: 0, cb: 0, ct: 0 };
3445
+ HC.dPl = 1; return;
3446
+ }
3447
+ const q = HC.pl[id], p = findP(g, id);
3448
+ if (!q || !p || !p.state || q.dead) return false;
3449
+ const st = p.state;
3450
+ if (name === "fall") { // only near a real pit
3451
+ const x = +d.x || st.x, lv = level(HC.planet);
3452
+ if (!lv.holes.some(h => x > h.x - 120 && x < h.x + h.w + 120)) return false;
3453
+ pitFall(g, q, x); return;
3454
+ }
3455
+ if (name === "pick") {
3456
+ const l = HC.loot[d.id];
3457
+ if (!l || Math.abs(l.x - st.x) > 70 || Math.abs(l.y - st.y) > 90) return false;
3458
+ delete HC.loot[d.id]; HC.dLoot = 1; HC.dPl = 1;
3459
+ if (WP[l.k]) { q.w = l.k; q.ammo = WP[l.k].ammo; }
3460
+ else if (SP[l.k]) q.sp = l.k;
3461
+ else if (l.k === "hp") q.hp = Math.min(q.mhp, Math.round(q.hp + q.mhp * .5));
3462
+ else give(g, id, 60);
3463
+ fx(g, "pick", { x: l.x, y: l.y, k: l.k, id });
3464
+ return;
3465
+ }
3466
+ if (name === "skill") {
3467
+ if (now < q.sk) return false;
3468
+ q.sk = r2(now + CLS[q.c].cd); q.sn = (q.sn || 0) + 1; HC.dPl = 1;
3469
+ doSkill(g, id, q, st, d.f > 0 ? 1 : -1); return;
3470
+ }
3471
+ if (name === "spell") {
3472
+ if (!q.sp) return false;
3473
+ const k = q.sp; q.sp = ""; HC.dPl = 1;
3474
+ doSpell(g, id, q, st, k); return;
3475
+ }
3476
+ return false;
3477
+ },
3478
+
3479
+ onFx(g, from, name, d) { if (name === "bossin") return; handleFx(g, name, d); },
3480
+
3481
+ draw(g, c) {
3482
+ const s = g.shared, src = srcOf(g), pi = s.planet || 0, lv = level(pi), P = lv.P, now = g.now;
3483
+ govern(); ensureBG(pi, c); LT.length = 0;
3484
+ drawBg(c, lv, camX, T, pi);
3485
+ const amp = shake * shake * 18, sx = amp ? (Math.random() - .5) * amp : 0, sy = amp ? (Math.random() - .5) * amp : 0;
3486
+ c.save(); c.translate(Math.round(-camX + sx), Math.round(sy));
3487
+ drawGround(c, lv, camX, T);
3488
+ SC = SC.filter(d => T - d.t < 8); // scorch marks left by explosions
3489
+ for (const d of SC) { c.globalAlpha = .5 * (1 - (T - d.t) / 8); c.fillStyle = "#0a0505"; c.beginPath(); c.ellipse(d.x, d.y + 3, d.s, d.s * .2, 0, 0, 6.283); c.fill(); }
3490
+ c.globalAlpha = 1;
3491
+ drawDeco(c, lv, camX, T);
3492
+ drawProps(c, lv, camX, T);
3493
+ drawTraps(c, lv, camX, now, T);
3494
+ for (const p of lv.plats) { // hover platforms: metal plating, rivets, glowing under-thrusters
3495
+ if (p.x + p.w < camX - 20 || p.x > camX + W + 20) continue;
3496
+ c.fillStyle = "rgba(0,0,0,.3)"; rr(c, p.x + 4, p.y + 6, p.w, 14, 6); c.fill();
3497
+ const pg = c.createLinearGradient(0, p.y, 0, p.y + 16); pg.addColorStop(0, mix(P.mid, "#ffffff", .18)); pg.addColorStop(1, mix(P.mid, "#000000", .45));
3498
+ rr(c, p.x, p.y, p.w, 16, 6); inked(c, pg, 3.5);
3499
+ c.fillStyle = P.top; rr(c, p.x, p.y, p.w, 4, 2); c.fill();
3500
+ c.fillStyle = "rgba(255,255,255,.35)"; c.fillRect(p.x + 4, p.y, p.w - 8, 1);
3501
+ c.fillStyle = mix(P.mid, "#000000", .6); for (const rx of [p.x + 7, p.x + p.w - 7]) { c.beginPath(); c.arc(rx, p.y + 10, 2, 0, 6.283); c.fill(); }
3502
+ const flick = .7 + .3 * Math.sin(T * 9 + p.x);
3503
+ c.fillStyle = rgba(P.a1, .9 * flick); c.fillRect(p.x + p.w / 2 - 14, p.y + 15, 28, 2);
3504
+ pushL(p.x + p.w / 2, p.y + 18, Math.max(70, p.w * .7), P.a1, .5, .35 * flick);
3505
+ }
3506
+ for (const id in src.loot || {}) { const l = src.loot[id]; if (l && l.x > camX - 40 && l.x < camX + W + 40) { drawLoot(c, l, T, now); pushL(l.x, l.y, 70, itemColor(l.k), .7, .4); } }
3507
+ for (const id in src.obj || {}) {
3508
+ const o = src.obj[id]; if (!o || o.t === "spin") continue; const d = disp["o" + id] || o; drawObj(c, o, d.x, d.y, T, src, now);
3509
+ if (o.t === "met") pushL(d.x, d.y, 150, "#ff9f1c", 1, .8); else if (o.t === "tur") pushL(d.x, d.y - 26, 70, "#4cc9f0", .6, .3); else if (o.t === "gr") pushL(d.x, d.y, 30, "#ff0000", .5, .4);
3510
+ }
3511
+ const apal = { a1: P.a1, a2: P.a2, fly: P.fly };
3512
+ for (const id in src.mobs || {}) {
3513
+ const m = src.mobs[id]; if (!m) continue; const d = disp[id] || m;
3514
+ if (d.x < camX - 120 || d.x > camX + W + 120) continue;
3515
+ const big = m.t === 4; // bosses draw their own ground shadow (a blur shadow on that many shapes is too slow on phones)
3516
+ if (big) { c.save(); c.shadowColor = "rgba(0,0,0,.55)"; c.shadowBlur = 8; c.shadowOffsetY = 3; }
3517
+ alien(c, m, id, d.x, d.y, apal, T, now, flash[id] && T - flash[id] < .08, pi);
3518
+ if (big) c.restore();
3519
+ const r = AL[m.t].r; // bioluminescent glow + glowing eyes
3520
+ pushL(d.x, d.y, r * (m.t === 5 ? 4 : m.t === 8 ? 3.2 : 2.6), P.a1, .4, .1);
3521
+ if (m.t === 6) pushL(d.x + (m.f || -1) * 6, d.y - 22, 34, "#ffcc00", .3, m.wu > now ? .9 : .4); // Shambler eyes
3522
+ else pushL(d.x + (m.f || -1) * r * .5, d.y - r * .3, m.t === 5 ? 60 : m.t === 8 ? 40 : 26, m.t >= 4 || m.t === 2 ? "#ff4d6d" : P.a1, .3, m.wu > now ? .9 : .35);
3523
+ }
3524
+ // revive beacons for downed heroes
3525
+ nearRevive = null;
3526
+ for (const p of g.players) {
3527
+ const q = src.pl && src.pl[p.id]; if (!q || !q.dead || !p.state || p.state.cls < 0) continue;
3528
+ const bx = q.dx || 0, by = q.dy || 0, col = CLS[q.c].c;
3529
+ pushL(bx, by - 40, 120, col, .7, .45);
3530
+ c.globalAlpha = .18 + .1 * Math.sin(T * 4); c.fillStyle = col; c.fillRect(bx - 14, by - 160, 28, 160); c.globalAlpha = 1;
3531
+ c.save(); c.globalAlpha = .55; hero(c, q.c, bx, by, 1, 0, false, false, false, .8); c.restore();
3532
+ c.strokeStyle = "rgba(255,255,255,.25)"; c.lineWidth = 4; c.beginPath(); c.arc(bx, by - 26, 24, 0, 6.283); c.stroke();
3533
+ if (q.rp > 0) { c.strokeStyle = "#80ffdb"; c.beginPath(); c.arc(bx, by - 26, 24, -Math.PI / 2, -Math.PI / 2 + 6.283 * clamp(q.rp / 2, 0, 1)); c.stroke(); }
3534
+ c.textAlign = "center"; c.font = "bold 10px sans-serif"; c.fillStyle = "#fff"; c.fillText(p.me ? "YOU (downed)" : (p.name || "Ally").slice(0, 10), bx, by - 58);
3535
+ if (!p.me && chosen && Math.abs(px - bx) < 120) nearRevive = { n: p.name || "ally", rp: q.rp || 0 };
3536
+ }
3537
+ // heroes
3538
+ for (const p of g.players) {
3539
+ const st = p.state; if (!st || st.cls < 0) continue;
3540
+ const q = src.pl && src.pl[p.id]; if (q && q.dead) continue;
3541
+ let x, y, mv, air, fi, f;
3542
+ if (p.me) { if (!chosen) continue; x = px; y = py; mv = Math.abs(vx) > 20; air = !onG; fi = T < firing; f = face; }
3543
+ else { const d = disp["p" + p.id]; x = d ? d.x : st.x; y = d ? d.y : st.y; mv = !!st.m; air = !!st.j; fi = !!st.s; f = st.f || 1; }
3544
+ const cls = p.me ? myCls : Math.round(st.cls); if (!CLS[cls]) continue; // cls is eased like a position: mid-change it can read 2.4
3545
+ if (!air) { c.fillStyle = "rgba(0,0,0,.35)"; c.beginPath(); c.ellipse(x, y + 1, 15, 4, 0, 0, 6.283); c.fill(); }
3546
+ // hero light, team-colored ground ring, muzzle flash light
3547
+ pushL(x, y - 26, p.me ? 210 : 170, CLS[cls].c, 1, .12);
3548
+ c.save(); c.globalCompositeOperation = "lighter"; c.globalAlpha = .35; c.strokeStyle = CLS[cls].c; c.lineWidth = 2; c.beginPath(); c.ellipse(x, y + 1, 18, 5, 0, 0, 6.283); c.stroke(); c.restore();
3549
+ if (fi) pushL(x + f * 32, y - 28, 110, cls === 5 || cls === 1 ? WP[CLS[cls].w].c : "#ffe066", 1, .7);
3550
+ for (const oid in src.obj || {}) { const o = src.obj[oid]; if (o && o.t === "spin" && o.o === p.id) { c.save(); c.translate(x, y - 24); c.globalAlpha = .25; circ(c, 0, 0, 60, "#ff6b6b"); c.globalAlpha = 1; c.rotate(T * 16); c.fillStyle = "#e9ecef"; for (let i = 0; i < 3; i++) { c.rotate(2.094); c.fillRect(10, -3, 48, 6); } c.restore(); } }
3551
+ if (q && q.inv > now) { c.globalAlpha = .3 + .15 * Math.sin(T * 10); circ(c, x, y - 24, 32, "#ffc8dd"); c.globalAlpha = 1; c.strokeStyle = "rgba(255,255,255,.7)"; c.lineWidth = 1.5; c.beginPath(); c.arc(x, y - 24, 32, 0, 6.283); c.stroke(); }
3552
+ if (q && q.sl > now) { // chilled by Frost Breath: icy ring and drifting snow
3553
+ c.globalAlpha = .28 + .1 * Math.sin(T * 6); circ(c, x, y - 26, 26, "#a2d2ff"); c.globalAlpha = 1;
3554
+ if (Math.random() < .3) addP(x + rnd(-14, 14), y - rnd(10, 50), rnd(-10, 10), rnd(10, 30), .6, "#e0fbfc", 2.5, 0);
3555
+ }
3556
+ const blink = q && q.ifr > now && Math.floor(T * 16) % 2 === 0;
3557
+ c.save(); if (blink) c.globalAlpha = .35;
3558
+ c.translate(x, y); const s2 = p.me ? sq : 0; c.scale(1 - s2 * .6, 1 + s2);
3559
+ hero(c, cls, 0, 0, f, T + (hsh(p.id) % 5), mv, air, fi, 1);
3560
+ c.restore();
3561
+ c.textAlign = "center"; c.font = "bold 10px sans-serif"; c.fillStyle = CLS[cls].c;
3562
+ c.fillText((p.me ? "YOU" : (p.name || "Ally").slice(0, 10)) + (q ? " · Lv" + q.lv : ""), x, y - 86);
3563
+ if (!p.me && q) bar(c, x - 18, y - 82, 36, 4, q.hp / q.mhp, "#52b788");
3564
+ }
3565
+ // shots
3566
+ const np = {};
3567
+ for (const sh of g.shots || []) {
3568
+ const pv = shotPrev[sh.id]; let dx = 1, dy = 0;
3569
+ if (pv) { const ddx = sh.x - pv.x, ddy = sh.y - pv.y, l = Math.hypot(ddx, ddy); if (l > .5) { dx = ddx / l; dy = ddy / l; } else { dx = pv.dx; dy = pv.dy; } }
3570
+ np[sh.id] = { x: sh.x, y: sh.y, dx, dy };
3571
+ const wk = wkOf(sh); drawShot(c, sh, wk, dx, dy);
3572
+ pushL(sh.x, sh.y, WP[wk].r * 7 + 24, WP[wk].c, .7, .5);
3573
+ }
3574
+ shotPrev = np;
3575
+ for (const id in ebL) drawEb(c, ebL[id], P, T);
3576
+ // beams and lightning
3577
+ beams = beams.filter(b => T - b.t < .45);
3578
+ for (const b of beams) {
3579
+ const a = 1 - (T - b.t) / .45, x2 = b.x + b.f * 780;
3580
+ c.fillStyle = "rgba(76,201,240," + (a * .5) + ")"; c.fillRect(Math.min(b.x, x2), b.y - 20 * a, 780, 40 * a);
3581
+ c.fillStyle = "rgba(255,255,255," + a + ")"; c.fillRect(Math.min(b.x, x2), b.y - 5 * a, 780, 10 * a);
3582
+ for (let i = 0; i < 6; i++) pushL(b.x + b.f * (40 + i * 140), b.y, 140, "#4cc9f0", a, a * .6);
3583
+ }
3584
+ bolts = bolts.filter(b => T - b.t < .5);
3585
+ for (const b of bolts) {
3586
+ const a = 1 - (T - b.t) / .5, gy = gnd(lv, b.x); c.strokeStyle = "rgba(255,214,10," + a + ")"; c.lineWidth = 4; c.beginPath(); c.moveTo(b.x, 0);
3587
+ for (let y = 30; y < gy; y += 30) c.lineTo(b.x + ((hsh(b.s + y) % 30) - 15), y); c.lineTo(b.x, gy); c.stroke();
3588
+ c.strokeStyle = "rgba(255,255,255," + a + ")"; c.lineWidth = 1.5; c.stroke();
3589
+ pushL(b.x, gy - 40, 220, "#ffd60a", a, a * .7);
3590
+ }
3591
+ FL = FL.filter(f => T - f.t < f.d); // explosion / death flashes
3592
+ for (const f of FL) { const a = 1 - (T - f.t) / f.d; pushL(f.x, f.y, f.r, f.c, a, a * .9); }
3593
+ // particles: normal first, then additive (fire, sparks, energy) so they glow
3594
+ for (let pass = 0; pass < 2; pass++) {
3595
+ if (pass) c.globalCompositeOperation = "lighter";
3596
+ for (const p of parts) {
3597
+ if (!!p.add !== !!pass) continue;
3598
+ const a = clamp(p.l / p.ml, 0, 1);
3599
+ if (p.k === 1) { c.globalAlpha = a; c.textAlign = "center"; c.font = "bold " + p.s + "px sans-serif"; c.lineWidth = 3; c.strokeStyle = "rgba(0,0,0,.6)"; c.strokeText(p.txt, p.x, p.y); c.fillStyle = p.c; c.fillText(p.txt, p.x, p.y); }
3600
+ else if (p.k === 2) { c.globalAlpha = a; c.strokeStyle = p.c; c.lineWidth = 3; c.beginPath(); c.arc(p.x, p.y, 4 + p.s * (1 - a), 0, 6.283); c.stroke(); }
3601
+ else if (p.k === 3) { c.globalAlpha = a; c.strokeStyle = p.c; c.lineWidth = p.s; c.beginPath(); c.moveTo(p.x, p.y); c.lineTo(p.x2, p.y2); c.stroke(); }
3602
+ else if (p.k === 4) { c.globalAlpha = a * .45; circ(c, p.x, p.y, p.s * (1.6 - a), p.c); }
3603
+ else if (p.add) { c.globalAlpha = a; const r = p.s * 1.6; c.drawImage(glowTex(p.c), p.x - r, p.y - r, r * 2, r * 2); }
3604
+ else { c.globalAlpha = a; c.fillStyle = p.c; c.fillRect(p.x - p.s / 2, p.y - p.s / 2, p.s, p.s); }
3605
+ }
3606
+ }
3607
+ c.globalCompositeOperation = "source-over";
3608
+ c.globalAlpha = 1;
3609
+ c.restore();
3610
+ // post-processing: dynamic light map, colored glow, atmosphere, foreground, vignette, bloom
3611
+ if (Q >= 1) lightPass(c, pi, sx, sy);
3612
+ glowPass(c, sx, sy);
3613
+ drawMotes(c, pi, camX);
3614
+ drawWeather(c, g, lv);
3615
+ c.globalAlpha = .92; tileLayer(c, BG.fg, 1.35, H - 124 + sy, 130, camX - sx); c.globalAlpha = 1;
3616
+ c.drawImage(BG.vig, 0, 0, W, H);
3617
+ if (Q >= 2 && FILTER_OK) bloom(c, BLOOMA[pi % 5]);
3618
+ if (flashScr && T - flashScr.t < .4) { c.globalAlpha = .35 * (1 - (T - flashScr.t) / .4); c.fillStyle = flashScr.c; c.fillRect(0, 0, W, H); c.globalAlpha = 1; }
3619
+ hud(c, g, src);
3620
+ if (!chosen) drawSelect(c, g);
3621
+ drawIcons(c);
3622
+ if (helpOpen) helpPanel(c);
3623
+ }
3624
+ });
3625
+
3626
+ </script>
3627
+ </body></html>