tickcli-js 0.0.0-stage → 0.1.0
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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/client/game.js +982 -0
- package/client/game3d.js +109 -0
- package/client/relay.js +196 -0
- package/client/room.js +632 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/hub.d.ts +619 -0
- package/dist/hub.js +1777 -0
- package/dist/hub.js.map +1 -0
- package/dist/node.d.ts +106 -0
- package/dist/node.js +301 -0
- package/dist/node.js.map +1 -0
- package/dist/remote.d.ts +75 -0
- package/dist/remote.js +214 -0
- package/dist/remote.js.map +1 -0
- package/dist/serve.d.ts +42 -0
- package/dist/serve.js +231 -0
- package/dist/serve.js.map +1 -0
- package/examples/games/arena.html +108 -0
- package/examples/games/arena3d.html +110 -0
- package/examples/games/chess.html +132 -0
- package/examples/games/cup.html +52 -0
- package/examples/games/delve.html +137 -0
- package/examples/games/dungeon.html +110 -0
- package/examples/games/neon-paint.html +79 -0
- package/examples/games/own-engine.html +41 -0
- package/examples/games/race.html +83 -0
- package/examples/games/shooter.html +95 -0
- package/examples/games/sky-duel.html +136 -0
- package/examples/games/space-squad.html +3627 -0
- package/examples/games/tag.html +51 -0
- package/examples/games/tictactoe.html +53 -0
- package/package.json +101 -4
- package/src/cli.ts +59 -0
- package/src/hub.ts +1521 -0
- package/src/node.ts +245 -0
- package/src/remote.ts +236 -0
- package/src/serve.ts +224 -0
|
@@ -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>
|