terminal-mmo 0.1.1 → 0.2.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.
Files changed (2) hide show
  1. package/dist/cli.js +897 -449
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ var SHOOTER = {
37
37
  var PROJECTILE = { w: 1, h: 1 };
38
38
  var PROGRESSION = { levelCap: 30 };
39
39
  var SPAWN = { x: 10, y: GROUND_TOP - BOX.h };
40
- var TOWN = { w: 80 };
40
+ var ZONE_MAX = { w: 2000, h: 200 };
41
41
  var TOWN_SPAWN = { x: 12, y: GROUND_TOP - BOX.h };
42
42
  var RESPAWN = { delaySec: 5 };
43
43
  var XP_PER_KILL = 12;
@@ -59,6 +59,154 @@ function meleeHitbox(p) {
59
59
  function aabbOverlap(a, b) {
60
60
  return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
61
61
  }
62
+ // ../shared/src/sprites/sprite.ts
63
+ var SENTINEL = "\xB7";
64
+ var MIRROR = {
65
+ "(": ")",
66
+ ")": "(",
67
+ "[": "]",
68
+ "]": "[",
69
+ "{": "}",
70
+ "}": "{",
71
+ "<": ">",
72
+ ">": "<",
73
+ "/": "\\",
74
+ "\\": "/",
75
+ "`": "'",
76
+ "'": "`",
77
+ "\u258C": "\u2590",
78
+ "\u2590": "\u258C",
79
+ "\u2598": "\u259D",
80
+ "\u259D": "\u2598",
81
+ "\u2596": "\u2597",
82
+ "\u2597": "\u2596",
83
+ "\u259B": "\u259C",
84
+ "\u259C": "\u259B",
85
+ "\u2599": "\u259F",
86
+ "\u259F": "\u2599",
87
+ "\u259A": "\u259E",
88
+ "\u259E": "\u259A"
89
+ };
90
+ function splitTrimPad(art) {
91
+ const lines = art.split(`
92
+ `);
93
+ while (lines.length > 0 && lines[0].trim() === "")
94
+ lines.shift();
95
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "")
96
+ lines.pop();
97
+ const width = lines.reduce((w, l) => Math.max(w, l.length), 0);
98
+ return lines.map((l) => l.padEnd(width, " "));
99
+ }
100
+ function mirrorGlyphs(rows) {
101
+ return rows.map((row) => {
102
+ let out = "";
103
+ for (let i = row.length - 1;i >= 0; i--)
104
+ out += MIRROR[row[i]] ?? row[i];
105
+ return out;
106
+ });
107
+ }
108
+ function reverseRows(rows) {
109
+ return rows.map((row) => {
110
+ let out = "";
111
+ for (let i = row.length - 1;i >= 0; i--)
112
+ out += row[i];
113
+ return out;
114
+ });
115
+ }
116
+
117
+ class Sprite {
118
+ w;
119
+ h;
120
+ glyphRight;
121
+ glyphLeft;
122
+ colorRight;
123
+ colorLeft;
124
+ constructor(glyph, opts) {
125
+ const { defaultKey } = opts;
126
+ if (defaultKey.length !== 1)
127
+ throw new Error(`Sprite defaultKey must be a single char, got "${defaultKey}"`);
128
+ const glyphRows = splitTrimPad(glyph).map((r) => r.replaceAll(SENTINEL, " "));
129
+ this.h = glyphRows.length;
130
+ this.w = glyphRows.length > 0 ? glyphRows[0].length : 0;
131
+ let colorRows;
132
+ if (opts.colors === undefined) {
133
+ colorRows = glyphRows.map((r) => defaultKey.repeat(r.length));
134
+ } else {
135
+ const parsed = splitTrimPad(opts.colors);
136
+ const cw = parsed.length > 0 ? parsed[0].length : 0;
137
+ if (parsed.length !== this.h || cw !== this.w)
138
+ throw new Error(`Sprite colour grid (${cw}x${parsed.length}) must match glyph grid (${this.w}x${this.h})`);
139
+ colorRows = parsed.map((r) => Array.from(r, (c) => c === SENTINEL || c === " " ? defaultKey : c).join(""));
140
+ }
141
+ this.glyphRight = glyphRows;
142
+ this.glyphLeft = mirrorGlyphs(glyphRows);
143
+ this.colorRight = colorRows;
144
+ this.colorLeft = reverseRows(colorRows);
145
+ }
146
+ rows(facing = 1) {
147
+ return facing === 1 ? this.glyphRight : this.glyphLeft;
148
+ }
149
+ colorKeys(facing = 1) {
150
+ return facing === 1 ? this.colorRight : this.colorLeft;
151
+ }
152
+ }
153
+
154
+ // ../shared/src/emote.ts
155
+ var FACE = `
156
+ \xB7\u259F\u2580\u2580\u2580\u2599\xB7
157
+ \u2590\u2588\u2588\u2588\u2588\u2588\u258C
158
+ \u2590\u2588\u2588\u2588\u2588\u2588\u258C
159
+ \xB7\u259C\u2584\u2584\u2584\u259B\xB7`;
160
+ var EMOTES = [
161
+ {
162
+ id: "love",
163
+ sprite: new Sprite(`
164
+ \u2584\u2588\u2588\u2584\u2588\u2588\u2584
165
+ \u2580\u2588\u2588\u2588\u2588\u2588\u2580
166
+ \xB7\xB7\u2580\u2588\u2580\xB7\xB7`, { defaultKey: "m" })
167
+ },
168
+ {
169
+ id: "laugh",
170
+ sprite: new Sprite(FACE, {
171
+ defaultKey: "y",
172
+ colors: `
173
+ \xB7yyyyy\xB7
174
+ yykykyy
175
+ ykkkkky
176
+ \xB7yyyyy\xB7`
177
+ })
178
+ },
179
+ {
180
+ id: "cry",
181
+ sprite: new Sprite(`
182
+ \xB7\u259F\u2580\u2580\u2580\u2599\xB7
183
+ \u2590\u2588\u2588\u2588\u2588\u2588\u258C
184
+ \u2590\u2588\u2588\u2584\u2588\u2588\u258C
185
+ \xB7\u259C\u2584\u2584\u2584\u259B\xB7`, {
186
+ defaultKey: "y",
187
+ colors: `
188
+ \xB7yyyyy\xB7
189
+ yykykyy
190
+ yyckcyy
191
+ \xB7yyyyy\xB7`
192
+ })
193
+ },
194
+ {
195
+ id: "angry",
196
+ sprite: new Sprite(FACE, {
197
+ defaultKey: "m",
198
+ colors: `
199
+ \xB7mmmmm\xB7
200
+ mmkmkmm
201
+ mmkkkmm
202
+ \xB7mmmmm\xB7`
203
+ })
204
+ }
205
+ ];
206
+ var EMOTE_TTL = 2.5;
207
+ function emoteById(id) {
208
+ return EMOTES.find((e) => e.id === id);
209
+ }
62
210
  // ../shared/src/rng.ts
63
211
  function rngNext(state) {
64
212
  const s = state + 1831565813 | 0;
@@ -133,47 +281,6 @@ function isSolid(t, cx, cy) {
133
281
  return true;
134
282
  return t.cells[cy * t.w + cx] === 1;
135
283
  }
136
- function makeStarterField(seed = 1337) {
137
- const { w, h } = WORLD;
138
- const cells = new Uint8Array(w * h);
139
- for (let y = GROUND_TOP;y < h; y++)
140
- for (let x = 0;x < w; x++)
141
- cells[y * w + x] = 1;
142
- let s = seed;
143
- const next = () => {
144
- const r = rngNext(s);
145
- s = r.state;
146
- return r.value;
147
- };
148
- for (let i = 0;i < 70; i++) {
149
- const px = Math.floor(next() * (w - 16)) + 2;
150
- const py = GROUND_TOP - 4 - Math.floor(next() * 18);
151
- const len = 6 + Math.floor(next() * 12);
152
- for (let x = px;x < Math.min(px + len, w); x++)
153
- cells[py * w + x] = 1;
154
- }
155
- return { w, h, cells };
156
- }
157
- function makeTownTerrain() {
158
- const w = TOWN.w;
159
- const h = WORLD.h;
160
- const cells = new Uint8Array(w * h);
161
- const solid = (x, y) => {
162
- if (x >= 0 && x < w && y >= 0 && y < h)
163
- cells[y * w + x] = 1;
164
- };
165
- for (let y = GROUND_TOP;y < h; y++)
166
- for (let x = 0;x < w; x++)
167
- solid(x, y);
168
- for (let y = GROUND_TOP - 12;y < GROUND_TOP; y++) {
169
- solid(0, y);
170
- solid(w - 1, y);
171
- }
172
- const daisY = GROUND_TOP - 3;
173
- for (let x = Math.floor(w / 2) - 6;x <= Math.floor(w / 2) + 6; x++)
174
- solid(x, daisY);
175
- return { w, h, cells };
176
- }
177
284
 
178
285
  // ../shared/src/physics.ts
179
286
  function stepEntity(t, src, ctl, dt) {
@@ -316,7 +423,7 @@ function stepProjectile(t, p, dt) {
316
423
  return { ...p, x, y, life };
317
424
  }
318
425
  // ../shared/src/protocol.ts
319
- var PROTOCOL_VERSION = 1;
426
+ var PROTOCOL_VERSION = 3;
320
427
  var textEncoder = new TextEncoder;
321
428
  var textDecoder = new TextDecoder;
322
429
 
@@ -431,7 +538,13 @@ class Reader {
431
538
  return this.buf.byteLength - this.pos;
432
539
  }
433
540
  }
434
- var CLIENT_TAG = { hello: 1, input: 2, chat: 3 };
541
+ var CLIENT_TAG = {
542
+ hello: 1,
543
+ input: 2,
544
+ chat: 3,
545
+ whisper: 4,
546
+ emote: 5
547
+ };
435
548
  function encodeClientMessage(msg) {
436
549
  const w = new Writer;
437
550
  switch (msg.t) {
@@ -456,10 +569,27 @@ function encodeClientMessage(msg) {
456
569
  w.u8(CLIENT_TAG.chat);
457
570
  w.str(msg.text);
458
571
  break;
572
+ case "whisper":
573
+ w.u8(CLIENT_TAG.whisper);
574
+ w.str(msg.to);
575
+ w.str(msg.text);
576
+ break;
577
+ case "emote":
578
+ w.u8(CLIENT_TAG.emote);
579
+ w.str(msg.emote);
580
+ break;
459
581
  }
460
582
  return w.finish();
461
583
  }
462
- var SERVER_TAG = { welcome: 1, snapshot: 2, chat: 3, reject: 4 };
584
+ var SERVER_TAG = {
585
+ welcome: 1,
586
+ snapshot: 2,
587
+ chat: 3,
588
+ reject: 4,
589
+ whisper: 5,
590
+ notice: 6,
591
+ emote: 7
592
+ };
463
593
  var ENTITY_TYPES = ["player", "chaser", "shooter"];
464
594
  var SLOTS = ["weapon", "armor", "accessory"];
465
595
  var RARITIES2 = [
@@ -570,12 +700,225 @@ function decodeServerMessage(buf) {
570
700
  }
571
701
  case SERVER_TAG.chat:
572
702
  return { t: "chat", sessionId: r.u32(), handle: r.str(), text: r.str() };
703
+ case SERVER_TAG.whisper:
704
+ return {
705
+ t: "whisper",
706
+ fromSessionId: r.u32(),
707
+ from: r.str(),
708
+ to: r.str(),
709
+ text: r.str()
710
+ };
711
+ case SERVER_TAG.notice:
712
+ return { t: "notice", text: r.str() };
713
+ case SERVER_TAG.emote:
714
+ return { t: "emote", sessionId: r.u32(), emote: r.str() };
573
715
  case SERVER_TAG.reject:
574
716
  return { t: "reject", reason: r.str() };
575
717
  default:
576
718
  throw new Error(`unknown server message tag ${tag}`);
577
719
  }
578
720
  }
721
+ // ../shared/src/sprites/chaser.ts
722
+ var GLYPH = `
723
+ \u259A\xB7\u259F\u2599\xB7\u259E\xB7
724
+ \u259F\u2588\u2588\u2588\u2588\u2599\xB7
725
+ \u259E\u259B\u259B\u259B\u259B\u258C\xB7
726
+ \u2590\u259F\u259F\u259F\u259F\u2596\xB7
727
+ \u259E\xB7\xB7\xB7\xB7\u259A\xB7`;
728
+ var COLORS = `
729
+ \xB7\xB7\xB7\xB7\xB7\xB7\xB7
730
+ \xB7g\xB7\xB7g\xB7\xB7
731
+ \xB7\xB7\xB7\xB7\xB7\xB7\xB7
732
+ \xB7\xB7\xB7\xB7\xB7\xB7\xB7
733
+ \xB7\xB7\xB7\xB7\xB7\xB7\xB7`;
734
+ var chaser = new Sprite(GLYPH, { defaultKey: "m", colors: COLORS });
735
+
736
+ // ../shared/src/sprites/merchant.ts
737
+ var GLYPH2 = `
738
+ \xB7\xB7\u259F\u2599\xB7\xB7
739
+ \xB7\u259F\u2588\u2588\u2599\xB7
740
+ \u259F\u259B\u2588\u2588\u259C\u2599
741
+ \u2588\u2588\u2588\u2588\u2588\u2588
742
+ \u259D\u2588\u2588\u2588\u2588\u2598`;
743
+ var COLORS2 = `
744
+ \xB7\xB7oo\xB7\xB7
745
+ \xB7oooo\xB7
746
+ oooooo
747
+ cccccc
748
+ \xB7oooo\xB7`;
749
+ var merchant = new Sprite(GLYPH2, { defaultKey: "o", colors: COLORS2 });
750
+
751
+ // ../shared/src/sprites/player.ts
752
+ var GLYPH3 = `
753
+ \xB7\u2590\u259B\u2588\u2588\u2588\u259C\u258C\xB7
754
+ \u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598
755
+ \xB7\xB7\u2598\u2598\xB7\u259D\u259D\xB7\xB7`;
756
+ var COLORS3 = `
757
+ \xB7ppppppp\xB7
758
+ ppppppppp
759
+ \xB7\xB7pp\xB7pp\xB7\xB7`;
760
+ var player = new Sprite(GLYPH3, { defaultKey: "p", colors: COLORS3 });
761
+
762
+ // ../shared/src/sprites/shooter.ts
763
+ var GLYPH4 = `
764
+ \xB7\u2597\u2584\u2584\u2584\u2596\xB7
765
+ \u259F\u2588\u2588\u2588\u2588\u2588\u2599
766
+ \u2588\u2588\u259F\u2588\u2599\u2588\u2588
767
+ \u259C\u2588\u2588\u2588\u2588\u2588\u259B
768
+ \xB7\u259D\u2580\u2580\u2580\u2598\xB7`;
769
+ var COLORS4 = `
770
+ \xB7ooooo\xB7
771
+ oogggoo
772
+ oggkggo
773
+ oogggoo
774
+ \xB7ooooo\xB7`;
775
+ var shooter = new Sprite(GLYPH4, { defaultKey: "o", colors: COLORS4 });
776
+
777
+ // ../shared/src/sprites/index.ts
778
+ var REGISTRY = {
779
+ player,
780
+ chaser,
781
+ shooter
782
+ };
783
+ function spriteFor(type) {
784
+ return REGISTRY[type];
785
+ }
786
+ var NPC_REGISTRY = {
787
+ vendor: merchant
788
+ };
789
+ function spriteForNpc(kind) {
790
+ return NPC_REGISTRY[kind];
791
+ }
792
+
793
+ // ../shared/src/render.ts
794
+ function blitSprite(buf, sprite, sx, sy, facing, hurt, style) {
795
+ const sw = buf.width;
796
+ const sh = buf.height;
797
+ const glyphs = sprite.rows(facing);
798
+ const keys = sprite.colorKeys(facing);
799
+ for (let ry = 0;ry < sprite.h; ry++) {
800
+ const py = sy + ry;
801
+ if (py < 0 || py >= sh)
802
+ continue;
803
+ const row = glyphs[ry];
804
+ const krow = keys[ry];
805
+ for (let rx = 0;rx < sprite.w; rx++) {
806
+ const ch = row[rx];
807
+ if (ch === " ")
808
+ continue;
809
+ const px = sx + rx;
810
+ if (px < 0 || px >= sw)
811
+ continue;
812
+ const fg = hurt ? style.hurt : style.palette[krow[rx]] ?? style.paletteDefault;
813
+ buf.setCellWithAlphaBlending(px, py, ch, fg, style.transparent);
814
+ }
815
+ }
816
+ }
817
+ function drawText(buf, x, y, text, fg, transparent) {
818
+ if (y < 0 || y >= buf.height)
819
+ return;
820
+ for (let i = 0;i < text.length; i++) {
821
+ const px = x + i;
822
+ if (px < 0 || px >= buf.width)
823
+ continue;
824
+ buf.setCellWithAlphaBlending(px, y, text[i], fg, transparent);
825
+ }
826
+ }
827
+ function drawEntitySprite(buf, e, cam, style) {
828
+ const sprite = spriteFor(e.type);
829
+ const sx = Math.round(e.x - Math.floor((sprite.w - BOX.w) / 2) - cam.x);
830
+ const sy = Math.round(e.y + BOX.h - sprite.h - cam.y);
831
+ blitSprite(buf, sprite, sx, sy, e.facing, e.hurtT > 0.3, style);
832
+ }
833
+ function drawNameplate(buf, e, cam, style) {
834
+ if (!e.name)
835
+ return;
836
+ const sprite = spriteFor(e.type);
837
+ const top = Math.round(e.y + BOX.h - sprite.h - cam.y);
838
+ const cx = e.x + BOX.w / 2 - cam.x;
839
+ const x = Math.round(cx - e.name.length / 2);
840
+ drawText(buf, x, top - 1, e.name, style.nameplate, style.transparent);
841
+ }
842
+ function renderZoneScene(buf, scene, cam, style) {
843
+ const sw = buf.width;
844
+ const sh = buf.height;
845
+ const { terrain } = scene;
846
+ const ww = terrain.w;
847
+ const wh = terrain.h;
848
+ const camX = Math.round(cam.x);
849
+ const camY = Math.round(cam.y);
850
+ buf.clear(style.bg);
851
+ for (let sy = 0;sy < sh; sy++) {
852
+ const wy = sy + camY;
853
+ for (let sx = 0;sx < sw; sx++) {
854
+ const wx = sx + camX;
855
+ if (isSolid(terrain, wx, wy) && wx >= 0 && wx < ww && wy >= 0 && wy < wh)
856
+ buf.setCell(sx, sy, "\u2588", style.terrainFg, style.terrainBg);
857
+ }
858
+ }
859
+ for (const pr of scene.portals) {
860
+ for (let yy = 0;yy < pr.h; yy++) {
861
+ for (let xx = 0;xx < pr.w; xx++) {
862
+ const px = pr.x + xx - camX;
863
+ const py = pr.y + yy - camY;
864
+ if (px >= 0 && px < sw && py >= 0 && py < sh)
865
+ buf.setCellWithAlphaBlending(px, py, "\u2592", style.portal, style.transparent);
866
+ }
867
+ }
868
+ }
869
+ for (const n of scene.npcs) {
870
+ const sprite = spriteForNpc(n.kind);
871
+ const sx = Math.round(n.x + Math.floor((n.w - sprite.w) / 2)) - camX;
872
+ const sy = Math.round(n.y + n.h - sprite.h) - camY;
873
+ blitSprite(buf, sprite, sx, sy, 1, false, style);
874
+ }
875
+ const sprites = [...scene.entities].sort((a, b) => a.y - b.y);
876
+ for (const e of sprites) {
877
+ drawEntitySprite(buf, e, cam, style);
878
+ drawNameplate(buf, e, cam, style);
879
+ }
880
+ }
881
+ // ../shared/src/sceneStyle.ts
882
+ var SCENE_COLORS = {
883
+ bg: [16, 18, 26, 255],
884
+ terrainFg: [70, 82, 104, 255],
885
+ terrainBg: [34, 40, 54, 255],
886
+ portal: [180, 130, 255, 255],
887
+ transparent: [0, 0, 0, 0],
888
+ hurt: [255, 240, 120, 255],
889
+ nameplate: [150, 156, 168, 255],
890
+ paletteDefault: [232, 232, 238, 255]
891
+ };
892
+ var SCENE_PALETTE = {
893
+ p: [255, 150, 40, 255],
894
+ m: [220, 90, 90, 255],
895
+ g: [170, 240, 95, 255],
896
+ s: [186, 196, 210, 255],
897
+ w: [150, 96, 52, 255],
898
+ y: [242, 210, 92, 255],
899
+ e: [236, 190, 150, 255],
900
+ f: [110, 200, 110, 255],
901
+ c: [132, 222, 230, 255],
902
+ o: [232, 230, 216, 255],
903
+ k: [64, 66, 82, 255]
904
+ };
905
+ function buildSceneStyle(toColor) {
906
+ const c = (q) => toColor(q[0], q[1], q[2], q[3]);
907
+ const palette = {};
908
+ for (const [k, q] of Object.entries(SCENE_PALETTE))
909
+ palette[k] = c(q);
910
+ return {
911
+ bg: c(SCENE_COLORS.bg),
912
+ terrainFg: c(SCENE_COLORS.terrainFg),
913
+ terrainBg: c(SCENE_COLORS.terrainBg),
914
+ portal: c(SCENE_COLORS.portal),
915
+ transparent: c(SCENE_COLORS.transparent),
916
+ hurt: c(SCENE_COLORS.hurt),
917
+ nameplate: c(SCENE_COLORS.nameplate),
918
+ palette,
919
+ paletteDefault: c(SCENE_COLORS.paletteDefault)
920
+ };
921
+ }
579
922
  // ../shared/src/skills.ts
580
923
  var POWER_STRIKE = {
581
924
  id: "power-strike",
@@ -628,72 +971,6 @@ function spawnMonster(type, id, x, y, spawnIndex) {
628
971
  spawnIndex
629
972
  };
630
973
  }
631
- function makeFieldZone(id, seed = 1337) {
632
- const terrain = makeStarterField(seed);
633
- const spawns = [];
634
- for (let i = 0;i < 8; i++) {
635
- const x = 40 + i * 22;
636
- const type = i % 3 === 2 ? "shooter" : "chaser";
637
- spawns.push({ type, x, y: GROUND_TOP - BOX.h });
638
- }
639
- let mid = 2;
640
- const monsters = spawns.map((s, i) => spawnMonster(s.type, mid++, s.x, s.y, i));
641
- return {
642
- id,
643
- type: "field",
644
- terrain,
645
- monsters,
646
- projectiles: [],
647
- nextProjectileId: 1,
648
- spawns,
649
- respawns: [],
650
- nextMonsterId: mid,
651
- portals: [
652
- {
653
- x: 24,
654
- y: GROUND_TOP - 7,
655
- w: 4,
656
- h: 7,
657
- target: "town-01",
658
- arrival: { x: 12, y: GROUND_TOP - BOX.h }
659
- }
660
- ]
661
- };
662
- }
663
- function makeTownZone(id) {
664
- return {
665
- id,
666
- type: "town",
667
- terrain: makeTownTerrain(),
668
- monsters: [],
669
- projectiles: [],
670
- nextProjectileId: 1,
671
- spawns: [],
672
- respawns: [],
673
- nextMonsterId: 1,
674
- portals: [
675
- {
676
- x: TOWN.w - 16,
677
- y: GROUND_TOP - 7,
678
- w: 4,
679
- h: 7,
680
- target: "field-01",
681
- arrival: { x: SPAWN.x, y: SPAWN.y }
682
- }
683
- ],
684
- npcs: [
685
- {
686
- id: 1,
687
- kind: "vendor",
688
- name: "Merchant",
689
- x: 32,
690
- y: GROUND_TOP - BOX.h,
691
- w: 4,
692
- h: BOX.h
693
- }
694
- ]
695
- };
696
- }
697
974
 
698
975
  // ../shared/src/zone.ts
699
976
  function clientStepAvatar(t, avatar, ctl, dtMs) {
@@ -807,12 +1084,12 @@ function stepZone(state, intents, dtMs) {
807
1084
  m = { ...m, attackT: SHOOTER.fireCooldown };
808
1085
  }
809
1086
  }
810
- let killer = -1;
811
1087
  for (let i = 0;i < avatars.length; i++) {
812
1088
  const hb = hitboxes[i];
813
1089
  if (hb && m.hurtT <= 0 && aabbOverlap(hb, entityBox(m))) {
814
- m = { ...m, hp: m.hp - damages[i], hurtT: 0.6 };
815
- killer = i;
1090
+ const sid = avatars[i].sessionId;
1091
+ const contributors = m.contributors?.includes(sid) ? m.contributors : [...m.contributors ?? [], sid];
1092
+ m = { ...m, hp: m.hp - damages[i], hurtT: 0.6, contributors };
816
1093
  break;
817
1094
  }
818
1095
  }
@@ -834,8 +1111,11 @@ function stepZone(state, intents, dtMs) {
834
1111
  if (m.hp > 0) {
835
1112
  monsters.push(m);
836
1113
  } else {
837
- const credited = killer >= 0 ? killer : 0;
838
- avatars[credited] = grantKill(avatars[credited]);
1114
+ for (const sid of m.contributors ?? []) {
1115
+ const idx = avatars.findIndex((a) => a.sessionId === sid);
1116
+ if (idx >= 0)
1117
+ avatars[idx] = grantKill(avatars[idx]);
1118
+ }
839
1119
  if (m.spawnIndex !== undefined)
840
1120
  respawns.push({
841
1121
  spawnIndex: m.spawnIndex,
@@ -925,15 +1205,306 @@ function grantKill(sa) {
925
1205
  log
926
1206
  };
927
1207
  }
1208
+ // ../../zones/catalogs.json
1209
+ var catalogs_default = {
1210
+ monsters: [
1211
+ {
1212
+ id: "chaser",
1213
+ behavior: "chaser",
1214
+ name: "Slime"
1215
+ },
1216
+ {
1217
+ id: "shooter",
1218
+ behavior: "shooter",
1219
+ name: "Sporeling"
1220
+ }
1221
+ ],
1222
+ npcs: [
1223
+ {
1224
+ id: "merchant",
1225
+ kind: "vendor",
1226
+ name: "Merchant"
1227
+ }
1228
+ ]
1229
+ };
1230
+
1231
+ // ../../zones/field-01.zone
1232
+ var field_01_default = `{
1233
+ "id": "field-01",
1234
+ "type": "field",
1235
+ "spawns": {
1236
+ "c": "chaser",
1237
+ "s": "shooter"
1238
+ },
1239
+ "portals": {
1240
+ "P": {
1241
+ "target": "town-01",
1242
+ "arrival": [
1243
+ 12,
1244
+ 32
1245
+ ]
1246
+ }
1247
+ }
1248
+ }
1249
+ ---
1250
+ ................................................................................................................................................................
1251
+ ................................................................................................................................................................
1252
+ ................................................................................................................................................................
1253
+ ................................................................................................................................................................
1254
+ ................................................................................................................................................................
1255
+ ................................................................................................................................................................
1256
+ ................................................................................................................................................................
1257
+ ................................................................................................................................................................
1258
+ ................................................................................................................................................................
1259
+ ................................................................................................................................................................
1260
+ ................................................................................................................................................................
1261
+ ................................................................................................................................................................
1262
+ ................................................................................................................................................................
1263
+ ................................................................................................................................................................
1264
+ ................................................................................................................................................................
1265
+ ................................................................................................................................................................
1266
+ ................................................................................................................................................................
1267
+ ................................................................................................................................................................
1268
+ ................................................................................................................................................................
1269
+ ................................................................................................................................................................
1270
+ ................................................................................................................................................................
1271
+ ................................................................................................................................................................
1272
+ ........................................................................................................................................s.......................
1273
+ ................................................................................................................................................................
1274
+ ............................................................................s...................................................................................
1275
+ ................................................................................................................................................................
1276
+ ................................................................................................................................................................
1277
+ ..................................................................................................................................################..............
1278
+ .......................................................c........................................................................................................
1279
+ .....................................................................#################..........................................................................
1280
+ ....P.............................................................................................................###########...................................
1281
+ ................................................................................................................................................................
1282
+ ..........................c.............c.............................................................................c.........c.....................c.........
1283
+ ..................................................##############................................................................................................
1284
+ ..................................................................................................###########...................................................
1285
+ ................................................................................................................................................................
1286
+ ................................................................................................................................................................
1287
+ ################################################################################################################################################################
1288
+ ################################################################################################################################################################
1289
+ ################################################################################################################################################################
1290
+ `;
1291
+
1292
+ // ../../zones/town-01.zone
1293
+ var town_01_default = `{
1294
+ "id": "town-01",
1295
+ "type": "town",
1296
+ "npcs": {
1297
+ "M": "merchant"
1298
+ },
1299
+ "portals": {
1300
+ "P": {
1301
+ "target": "field-01",
1302
+ "arrival": [
1303
+ 10,
1304
+ 32
1305
+ ]
1306
+ }
1307
+ }
1308
+ }
1309
+ ---
1310
+ ....................................................................................................................................
1311
+ ....................................................................................................................................
1312
+ ....................................................................................................................................
1313
+ ....................................................................................................................................
1314
+ ....................................................................................................................................
1315
+ ....................................................................................................................................
1316
+ ....................................................................................................................................
1317
+ ....................................................................................................................................
1318
+ ....................................................................................................................................
1319
+ ....................................................................................................................................
1320
+ ....................................................................................................................................
1321
+ ....................................................................................................................................
1322
+ ....................................................................................................................................
1323
+ ....................................................................................................................................
1324
+ ....................................................................................................................................
1325
+ ....................................................................................................................................
1326
+ ....................................................................................................................................
1327
+ ....................................................................................................................................
1328
+ ....................................................................................................................................
1329
+ ....................................................................................................................................
1330
+ ....................................................................................................................................
1331
+ ....................................................................................................................................
1332
+ .............................................................................................####...................................
1333
+ ....................................................................................................................................
1334
+ ....................................................................................................................................
1335
+ ....................................................................................................................................
1336
+ ............................................................................................#######.................................
1337
+ ....................................................................................................................................
1338
+ ................................###########.........................................................................................
1339
+ ....................................................................................................................................
1340
+ ..........................................................................................###########........................P......
1341
+ ..............................###############.......................................................................................
1342
+ ....................M...............................................................................................................
1343
+ ..................................................###############........................................................#........#.
1344
+ ............................###################.........................................#############.........############........#.
1345
+ ........................................................................########.........................................#........#.
1346
+ .........................................................................................................................#........#.
1347
+ ####################################################################################################################################
1348
+ ####################################################################################################################################
1349
+ ####################################################################################################################################
1350
+ `;
1351
+
1352
+ // ../shared/src/zoneFormat.ts
1353
+ var PORTAL_BOX = { w: 4, h: 7 };
1354
+ var NPC_W = 4;
1355
+
1356
+ class ZoneParseError extends Error {
1357
+ code;
1358
+ constructor(code, message) {
1359
+ super(message);
1360
+ this.code = code;
1361
+ this.name = "ZoneParseError";
1362
+ }
1363
+ }
1364
+ function resolveMonster(catalog, id) {
1365
+ const e = catalog.find((m) => m.id === id);
1366
+ if (!e)
1367
+ throw new ZoneParseError("unknown-monster", `monster id '${id}' not in catalog`);
1368
+ return e;
1369
+ }
1370
+ function resolveNpc(catalog, id) {
1371
+ const e = catalog.find((n) => n.id === id);
1372
+ if (!e)
1373
+ throw new ZoneParseError("unknown-npc", `npc id '${id}' not in catalog`);
1374
+ return e;
1375
+ }
1376
+ function parseZone(text, catalogs) {
1377
+ const lines = text.split(`
1378
+ `);
1379
+ const di = lines.findIndex((l) => l.trim() === "---");
1380
+ if (di === -1)
1381
+ throw new ZoneParseError("no-delimiter", "missing '---' delimiter between header and grid");
1382
+ const header = parseHeader(lines.slice(0, di).join(`
1383
+ `));
1384
+ const glyphs = buildGlyphMap(header);
1385
+ const body = lines.slice(di + 1);
1386
+ while (body.length > 0 && body[body.length - 1] === "")
1387
+ body.pop();
1388
+ const h = body.length;
1389
+ const w = body.reduce((m, l) => Math.max(m, l.length), 0);
1390
+ if (h === 0 || w === 0)
1391
+ throw new ZoneParseError("empty-grid", "grid has no cells");
1392
+ if (w > ZONE_MAX.w || h > ZONE_MAX.h)
1393
+ throw new ZoneParseError("too-large", `grid ${w}\xD7${h} is too large (cap ${ZONE_MAX.w}\xD7${ZONE_MAX.h})`);
1394
+ const cells = new Uint8Array(w * h);
1395
+ const spawns = [];
1396
+ const monsters = [];
1397
+ const npcs = [];
1398
+ const portals = [];
1399
+ let nextMonsterId = 2;
1400
+ let nextNpcId = 1;
1401
+ for (let y = 0;y < h; y++) {
1402
+ const line = body[y];
1403
+ for (let x = 0;x < line.length; x++) {
1404
+ const ch = line[x];
1405
+ if (ch === "." || ch === " ")
1406
+ continue;
1407
+ if (ch === "#") {
1408
+ cells[y * w + x] = 1;
1409
+ continue;
1410
+ }
1411
+ const g = glyphs.get(ch);
1412
+ if (!g)
1413
+ throw new ZoneParseError("unknown-glyph", `glyph '${ch}' at (${x},${y}) is not declared in the header`);
1414
+ if (g.kind === "spawn") {
1415
+ const type = resolveMonster(catalogs.monsters, g.ref).behavior;
1416
+ monsters.push(spawnMonster(type, nextMonsterId++, x, y, spawns.length));
1417
+ spawns.push({ type, x, y });
1418
+ } else if (g.kind === "npc") {
1419
+ const entry = resolveNpc(catalogs.npcs, g.ref);
1420
+ npcs.push({
1421
+ id: nextNpcId++,
1422
+ kind: entry.kind,
1423
+ name: entry.name,
1424
+ x,
1425
+ y,
1426
+ w: NPC_W,
1427
+ h: BOX.h
1428
+ });
1429
+ } else {
1430
+ portals.push({
1431
+ x,
1432
+ y,
1433
+ w: PORTAL_BOX.w,
1434
+ h: PORTAL_BOX.h,
1435
+ target: g.ref.target,
1436
+ arrival: { x: g.ref.arrival[0], y: g.ref.arrival[1] }
1437
+ });
1438
+ }
1439
+ }
1440
+ }
1441
+ const zone = {
1442
+ id: header.id,
1443
+ type: header.type,
1444
+ terrain: { w, h, cells },
1445
+ monsters,
1446
+ projectiles: [],
1447
+ nextProjectileId: 1,
1448
+ spawns,
1449
+ respawns: [],
1450
+ nextMonsterId,
1451
+ portals
1452
+ };
1453
+ if (npcs.length > 0)
1454
+ zone.npcs = npcs;
1455
+ return zone;
1456
+ }
1457
+ function parseHeader(text) {
1458
+ let header;
1459
+ try {
1460
+ header = JSON.parse(text);
1461
+ } catch (e) {
1462
+ throw new ZoneParseError("bad-json", `header is not valid JSON: ${e.message}`);
1463
+ }
1464
+ if (typeof header.id !== "string" || header.id.length === 0)
1465
+ throw new ZoneParseError("bad-header", "header.id must be a non-empty string");
1466
+ if (header.type !== "field" && header.type !== "town")
1467
+ throw new ZoneParseError("bad-header", `header.type must be 'field' or 'town', got '${header.type}'`);
1468
+ return header;
1469
+ }
1470
+ function buildGlyphMap(header) {
1471
+ const map = new Map;
1472
+ const add = (ch, g) => {
1473
+ if (ch.length !== 1)
1474
+ throw new ZoneParseError("bad-header", `glyph key '${ch}' must be one character`);
1475
+ if (ch === "#" || ch === "." || ch === " ")
1476
+ throw new ZoneParseError("bad-header", `'${ch}' is reserved and cannot be a glyph key`);
1477
+ if (map.has(ch))
1478
+ throw new ZoneParseError("bad-header", `glyph '${ch}' is declared more than once`);
1479
+ map.set(ch, g);
1480
+ };
1481
+ for (const [ch, ref] of Object.entries(header.spawns ?? {}))
1482
+ add(ch, { kind: "spawn", ref });
1483
+ for (const [ch, ref] of Object.entries(header.npcs ?? {}))
1484
+ add(ch, { kind: "npc", ref });
1485
+ for (const [ch, ref] of Object.entries(header.portals ?? {}))
1486
+ add(ch, { kind: "portal", ref });
1487
+ return map;
1488
+ }
1489
+
1490
+ // ../shared/src/zoneContent.ts
1491
+ var CATALOGS = catalogs_default;
1492
+ function loadZones() {
1493
+ return [parseZone(town_01_default, CATALOGS), parseZone(field_01_default, CATALOGS)];
1494
+ }
1495
+
928
1496
  // ../shared/src/sim.ts
1497
+ function createGameFromZones(zones, startId, seed = 1) {
1498
+ const rec = {};
1499
+ for (const z of zones)
1500
+ rec[z.id] = z;
1501
+ const start = rec[startId] ?? zones[0];
1502
+ const player2 = spawnPlayerState(start.id, SPAWN.x, SPAWN.y, seed);
1503
+ return { player: player2, world: { zones: rec, tick: 0 } };
1504
+ }
929
1505
  function createGame(seed = 1) {
930
- const field = makeFieldZone("field-01");
931
- const town = makeTownZone("town-01");
932
- const player = spawnPlayerState(field.id, SPAWN.x, SPAWN.y, seed);
933
- return {
934
- player,
935
- world: { zones: { [field.id]: field, [town.id]: town }, tick: 0 }
936
- };
1506
+ const loaded = loadZones();
1507
+ return createGameFromZones(loaded, loaded[0].id, seed);
937
1508
  }
938
1509
  function step(game, input, dtMs) {
939
1510
  const dt = Math.min(dtMs / 1000, PHYS.maxDt);
@@ -953,13 +1524,13 @@ function step(game, input, dtMs) {
953
1524
  };
954
1525
  const dest = game.world.zones[portal.target];
955
1526
  const log = [...game.player.log.slice(-5), `Entered the ${dest.type}.`];
956
- const player2 = {
1527
+ const player3 = {
957
1528
  ...game.player,
958
1529
  avatar,
959
1530
  zoneId: portal.target,
960
1531
  log
961
1532
  };
962
- return { player: player2, world: { ...game.world, tick: game.world.tick + 1 } };
1533
+ return { player: player3, world: { ...game.world, tick: game.world.tick + 1 } };
963
1534
  }
964
1535
  }
965
1536
  const predicted = stepEntity(t, game.player.avatar, { moveX: input.moveX, jump: input.jump }, dt).e;
@@ -988,7 +1559,7 @@ function step(game, input, dtMs) {
988
1559
  };
989
1560
  const next = stepZone({ zone, avatars: [sa], tick: game.world.tick }, [intent], dtMs);
990
1561
  const out = next.avatars[0];
991
- const player = {
1562
+ const player2 = {
992
1563
  avatar: out.avatar,
993
1564
  progress: out.progress,
994
1565
  inventory: out.inventory,
@@ -1003,7 +1574,7 @@ function step(game, input, dtMs) {
1003
1574
  zones: { ...game.world.zones, [zone.id]: next.zone },
1004
1575
  tick: next.tick
1005
1576
  };
1006
- return { player, world };
1577
+ return { player: player2, world };
1007
1578
  }
1008
1579
  // ../shared/src/vendor.ts
1009
1580
  var RARITY_VALUE = {
@@ -1032,6 +1603,30 @@ import { createCliRenderer } from "@opentui/core";
1032
1603
 
1033
1604
  // ../client/src/chat.ts
1034
1605
  var MAX_LEN = CHAT_MAX_LEN;
1606
+ var WHISPER_USAGE = "Usage: /w <handle> <message>";
1607
+ var EMOTE_USAGE = `Usage: /em <${EMOTES.map((e) => e.id).join("|")}>`;
1608
+ function parseChatCommand(line) {
1609
+ const trimmed = line.trim();
1610
+ const em = /^\/(?:em|emote)\b\s*(.*)$/s.exec(trimmed);
1611
+ if (em) {
1612
+ const name = em[1].trim().split(/\s+/)[0] ?? "";
1613
+ if (!name || !emoteById(name))
1614
+ return { kind: "error", message: EMOTE_USAGE };
1615
+ return { kind: "emote", emote: name };
1616
+ }
1617
+ const m = /^\/(?:w|whisper)\b\s*(.*)$/s.exec(trimmed);
1618
+ if (!m)
1619
+ return { kind: "say", text: trimmed };
1620
+ const rest = m[1].trimStart();
1621
+ const sp = rest.search(/\s/);
1622
+ if (sp < 0)
1623
+ return { kind: "error", message: WHISPER_USAGE };
1624
+ const to = rest.slice(0, sp);
1625
+ const text = rest.slice(sp + 1).trim();
1626
+ if (!to || !text)
1627
+ return { kind: "error", message: WHISPER_USAGE };
1628
+ return { kind: "whisper", to, text };
1629
+ }
1035
1630
 
1036
1631
  class ChatInput {
1037
1632
  open = false;
@@ -1077,7 +1672,7 @@ import {
1077
1672
 
1078
1673
  // ../client/src/theme.ts
1079
1674
  import { RGBA } from "@opentui/core";
1080
- var COLORS = {
1675
+ var COLORS5 = {
1081
1676
  bg: RGBA.fromInts(16, 18, 26, 255),
1082
1677
  terrainFg: RGBA.fromInts(70, 82, 104, 255),
1083
1678
  terrainBg: RGBA.fromInts(34, 40, 54, 255),
@@ -1094,24 +1689,25 @@ var COLORS = {
1094
1689
  chat: RGBA.fromInts(120, 200, 235, 255),
1095
1690
  bubbleFg: RGBA.fromInts(236, 236, 242, 255),
1096
1691
  bubbleBorder: RGBA.fromInts(120, 200, 235, 255),
1097
- bubbleBg: RGBA.fromInts(20, 24, 34, 255)
1692
+ bubbleBg: RGBA.fromInts(20, 24, 34, 255),
1693
+ emote: RGBA.fromInts(255, 220, 110, 255)
1098
1694
  };
1099
1695
 
1100
1696
  // ../client/src/hud.ts
1101
- var HINT = "move \u2190/\u2192 a/d jump \u2423/\u2191 attack j/x skill k interact e chat \u23CE quit q";
1697
+ var HINT = "move \u2190/\u2192 a/d jump \u2423/\u2191 attack j/x skill k interact e chat \u23CE (/w whisper, /em emote) quit q";
1102
1698
  var Z = 10;
1103
1699
  var CHAT_LINES = 4;
1104
- function skillReadout(player2) {
1700
+ function skillReadout(player3) {
1105
1701
  const segs = [];
1106
1702
  for (let slot = 1;; slot++) {
1107
- const skill = skillForSlot(player2.class ?? "warrior", slot);
1703
+ const skill = skillForSlot(player3.class ?? "warrior", slot);
1108
1704
  if (!skill)
1109
1705
  break;
1110
1706
  let state;
1111
- if (!skillUnlocked(skill, player2.progress.level))
1707
+ if (!skillUnlocked(skill, player3.progress.level))
1112
1708
  state = `L${skill.unlockLevel}`;
1113
1709
  else {
1114
- const cd = player2.skillCooldowns?.[skill.id] ?? 0;
1710
+ const cd = player3.skillCooldowns?.[skill.id] ?? 0;
1115
1711
  state = cd > 0 ? `${cd.toFixed(1)}s` : "ready";
1116
1712
  }
1117
1713
  segs.push(`k ${skill.name}: ${state}`);
@@ -1138,24 +1734,24 @@ class Hud {
1138
1734
  height: 1,
1139
1735
  flexDirection: "row",
1140
1736
  justifyContent: "space-between",
1141
- backgroundColor: COLORS.hudBg,
1737
+ backgroundColor: COLORS5.hudBg,
1142
1738
  shouldFill: true,
1143
1739
  zIndex: Z
1144
1740
  });
1145
1741
  this.stats = new TextRenderable(ctx, {
1146
1742
  content: "",
1147
- fg: COLORS.hud,
1148
- bg: COLORS.hudBg
1743
+ fg: COLORS5.hud,
1744
+ bg: COLORS5.hudBg
1149
1745
  });
1150
1746
  this.alpha = new TextRenderable(ctx, {
1151
1747
  content: "",
1152
- fg: COLORS.vendor,
1153
- bg: COLORS.hudBg
1748
+ fg: COLORS5.vendor,
1749
+ bg: COLORS5.hudBg
1154
1750
  });
1155
1751
  this.meta = new TextRenderable(ctx, {
1156
1752
  content: "",
1157
- fg: COLORS.dim,
1158
- bg: COLORS.hudBg
1753
+ fg: COLORS5.dim,
1754
+ bg: COLORS5.hudBg
1159
1755
  });
1160
1756
  this.topBar.add(this.stats);
1161
1757
  this.topBar.add(this.alpha);
@@ -1167,29 +1763,29 @@ class Hud {
1167
1763
  flexDirection: "column",
1168
1764
  zIndex: Z
1169
1765
  });
1170
- this.bottom.add(new TextRenderable(ctx, { content: HINT, fg: COLORS.dim, bg: COLORS.bg }));
1766
+ this.bottom.add(new TextRenderable(ctx, { content: HINT, fg: COLORS5.dim, bg: COLORS5.bg }));
1171
1767
  this.skills = new TextRenderable(ctx, {
1172
1768
  content: "",
1173
- fg: COLORS.melee,
1174
- bg: COLORS.bg
1769
+ fg: COLORS5.melee,
1770
+ bg: COLORS5.bg
1175
1771
  });
1176
1772
  this.bottom.add(this.skills);
1177
1773
  this.log = new TextRenderable(ctx, {
1178
1774
  content: "",
1179
- fg: COLORS.dim,
1180
- bg: COLORS.bg
1775
+ fg: COLORS5.dim,
1776
+ bg: COLORS5.bg
1181
1777
  });
1182
1778
  this.bottom.add(this.log);
1183
1779
  this.chat = new TextRenderable(ctx, {
1184
1780
  content: "",
1185
- fg: COLORS.chat,
1186
- bg: COLORS.bg
1781
+ fg: COLORS5.chat,
1782
+ bg: COLORS5.bg
1187
1783
  });
1188
1784
  this.bottom.add(this.chat);
1189
1785
  this.chatInput = new TextRenderable(ctx, {
1190
1786
  content: "",
1191
- fg: COLORS.melee,
1192
- bg: COLORS.bg
1787
+ fg: COLORS5.melee,
1788
+ bg: COLORS5.bg
1193
1789
  });
1194
1790
  this.bottom.add(this.chatInput);
1195
1791
  }
@@ -1201,14 +1797,14 @@ class Hud {
1201
1797
  this.alpha.content = " \u26A0 ALPHA \xB7 progress resets when the server restarts ";
1202
1798
  }
1203
1799
  update(game, fps) {
1204
- const { player: player2 } = game;
1205
- const p = player2.avatar;
1206
- const zone2 = activeZone(game.world, player2.zoneId);
1800
+ const { player: player3 } = game;
1801
+ const p = player3.avatar;
1802
+ const zone2 = activeZone(game.world, player3.zoneId);
1207
1803
  const hpPct = Math.max(0, Math.round(p.hp / p.maxHp * 100));
1208
- this.stats.content = ` L${player2.progress.level} HP ${Math.max(0, Math.round(p.hp))}/${p.maxHp} (${hpPct}%) XP ${player2.progress.xp} Gold ${player2.progress.gold} Items ${player2.inventory.length} `;
1804
+ this.stats.content = ` L${player3.progress.level} HP ${Math.max(0, Math.round(p.hp))}/${p.maxHp} (${hpPct}%) XP ${player3.progress.xp} Gold ${player3.progress.gold} Items ${player3.inventory.length} `;
1209
1805
  this.meta.content = `FPS ${fps} monsters ${zone2.monsters.length} `;
1210
- this.skills.content = skillReadout(player2);
1211
- this.log.content = player2.log.slice(-3).join(`
1806
+ this.skills.content = skillReadout(player3);
1807
+ this.log.content = player3.log.slice(-3).join(`
1212
1808
  `);
1213
1809
  }
1214
1810
  updateChat(lines, open, draft) {
@@ -1386,6 +1982,7 @@ class NetClient {
1386
1982
  latest = null;
1387
1983
  chatLog = [];
1388
1984
  bubbles = new Map;
1985
+ emotes = new Map;
1389
1986
  rejected = null;
1390
1987
  constructor(url, handle, onReject = () => {}) {
1391
1988
  this.onReject = onReject;
@@ -1414,15 +2011,27 @@ class NetClient {
1414
2011
  return;
1415
2012
  }
1416
2013
  if (msg.t === "chat") {
1417
- this.chatLog.push(`${msg.handle}: ${msg.text}`);
1418
- if (this.chatLog.length > MAX_CHAT_LOG)
1419
- this.chatLog.splice(0, this.chatLog.length - MAX_CHAT_LOG);
2014
+ this.pushChat(`${msg.handle}: ${msg.text}`);
1420
2015
  this.bubbles.set(msg.sessionId, {
1421
2016
  text: msg.text,
1422
2017
  ttl: bubbleTtl(msg.text.length)
1423
2018
  });
1424
2019
  return;
1425
2020
  }
2021
+ if (msg.t === "whisper") {
2022
+ const line = msg.fromSessionId === this.sessionId ? `[you \u2192 ${msg.to}] ${msg.text}` : `[${msg.from} \u2192 you] ${msg.text}`;
2023
+ this.pushChat(line);
2024
+ return;
2025
+ }
2026
+ if (msg.t === "notice") {
2027
+ this.notice(msg.text);
2028
+ return;
2029
+ }
2030
+ if (msg.t === "emote") {
2031
+ if (emoteById(msg.emote))
2032
+ this.emotes.set(msg.sessionId, { id: msg.emote, ttl: EMOTE_TTL });
2033
+ return;
2034
+ }
1426
2035
  if (msg.zoneId !== this.zoneId) {
1427
2036
  this.zoneId = msg.zoneId;
1428
2037
  this.buffer = new SnapshotBuffer;
@@ -1433,6 +2042,14 @@ class NetClient {
1433
2042
  sample(nowMs) {
1434
2043
  return this.buffer.sample(nowMs - INTERP_DELAY_MS);
1435
2044
  }
2045
+ pushChat(line) {
2046
+ this.chatLog.push(line);
2047
+ if (this.chatLog.length > MAX_CHAT_LOG)
2048
+ this.chatLog.splice(0, this.chatLog.length - MAX_CHAT_LOG);
2049
+ }
2050
+ notice(text) {
2051
+ this.pushChat(`* ${text}`);
2052
+ }
1436
2053
  decayBubbles(dtSec) {
1437
2054
  for (const [id, b] of this.bubbles) {
1438
2055
  b.ttl -= dtSec;
@@ -1440,6 +2057,13 @@ class NetClient {
1440
2057
  this.bubbles.delete(id);
1441
2058
  }
1442
2059
  }
2060
+ decayEmotes(dtSec) {
2061
+ for (const [id, e] of this.emotes) {
2062
+ e.ttl -= dtSec;
2063
+ if (e.ttl <= 0)
2064
+ this.emotes.delete(id);
2065
+ }
2066
+ }
1443
2067
  send(msg) {
1444
2068
  if (this.ready && this.ws.readyState === WebSocket.OPEN)
1445
2069
  this.ws.send(encodeClientMessage(msg));
@@ -1488,7 +2112,7 @@ function monsterEntity(m) {
1488
2112
  attackT: 0
1489
2113
  };
1490
2114
  }
1491
- function snapshotToGame(field, predicted, ownSessionId, snapshot, localSkillCooldowns, bubbles = new Map) {
2115
+ function snapshotToGame(field, predicted, ownSessionId, snapshot, localSkillCooldowns, bubbles = new Map, emotes = new Map) {
1492
2116
  const monsters = snapshot ? snapshot.monsters.map(monsterEntity) : [];
1493
2117
  const projectiles = snapshot ? snapshot.projectiles : [];
1494
2118
  const others = snapshot ? snapshot.avatars.filter((a) => a.sessionId !== ownSessionId).map((a) => {
@@ -1496,15 +2120,23 @@ function snapshotToGame(field, predicted, ownSessionId, snapshot, localSkillCool
1496
2120
  const bubble = bubbles.get(a.sessionId)?.text;
1497
2121
  if (bubble)
1498
2122
  e.bubble = bubble;
2123
+ const emote2 = emotes.get(a.sessionId)?.id;
2124
+ if (emote2)
2125
+ e.emote = emote2;
1499
2126
  return e;
1500
2127
  }) : [];
1501
2128
  const ownBubble = bubbles.get(ownSessionId)?.text;
1502
- const avatar = ownBubble ? { ...predicted, bubble: ownBubble } : predicted;
2129
+ const ownEmote = emotes.get(ownSessionId)?.id;
2130
+ let avatar = predicted;
2131
+ if (ownBubble)
2132
+ avatar = { ...avatar, bubble: ownBubble };
2133
+ if (ownEmote)
2134
+ avatar = { ...avatar, emote: ownEmote };
1503
2135
  const progress = snapshot?.progress ?? { level: 1, xp: 0, gold: 0 };
1504
2136
  const inventory = snapshot?.inventory ?? [];
1505
2137
  const log = snapshot?.log ?? ["Connecting\u2026"];
1506
2138
  const zone2 = { ...field, monsters, projectiles };
1507
- const player2 = {
2139
+ const player3 = {
1508
2140
  avatar,
1509
2141
  progress,
1510
2142
  inventory,
@@ -1516,7 +2148,7 @@ function snapshotToGame(field, predicted, ownSessionId, snapshot, localSkillCool
1516
2148
  skillCooldowns: localSkillCooldowns
1517
2149
  };
1518
2150
  return {
1519
- player: player2,
2151
+ player: player3,
1520
2152
  world: { zones: { [field.id]: zone2 }, tick: snapshot?.tick ?? 0 },
1521
2153
  others
1522
2154
  };
@@ -1524,7 +2156,8 @@ function snapshotToGame(field, predicted, ownSessionId, snapshot, localSkillCool
1524
2156
 
1525
2157
  // ../client/src/playfield.ts
1526
2158
  import {
1527
- Renderable
2159
+ Renderable,
2160
+ RGBA as RGBA2
1528
2161
  } from "@opentui/core";
1529
2162
 
1530
2163
  // ../client/src/camera.ts
@@ -1568,232 +2201,37 @@ function stepCamera(state, zoneId, avatarX, avatarY, view) {
1568
2201
  return { cam, center: { x: cx, y: cy }, zoneId };
1569
2202
  }
1570
2203
 
1571
- // ../client/src/sprites/sprite.ts
1572
- var SENTINEL = "\xB7";
1573
- var MIRROR = {
1574
- "(": ")",
1575
- ")": "(",
1576
- "[": "]",
1577
- "]": "[",
1578
- "{": "}",
1579
- "}": "{",
1580
- "<": ">",
1581
- ">": "<",
1582
- "/": "\\",
1583
- "\\": "/",
1584
- "`": "'",
1585
- "'": "`",
1586
- "\u258C": "\u2590",
1587
- "\u2590": "\u258C",
1588
- "\u2598": "\u259D",
1589
- "\u259D": "\u2598",
1590
- "\u2596": "\u2597",
1591
- "\u2597": "\u2596",
1592
- "\u259B": "\u259C",
1593
- "\u259C": "\u259B",
1594
- "\u2599": "\u259F",
1595
- "\u259F": "\u2599",
1596
- "\u259A": "\u259E",
1597
- "\u259E": "\u259A"
1598
- };
1599
- function splitTrimPad(art) {
1600
- const lines = art.split(`
1601
- `);
1602
- while (lines.length > 0 && lines[0].trim() === "")
1603
- lines.shift();
1604
- while (lines.length > 0 && lines[lines.length - 1].trim() === "")
1605
- lines.pop();
1606
- const width = lines.reduce((w, l) => Math.max(w, l.length), 0);
1607
- return lines.map((l) => l.padEnd(width, " "));
1608
- }
1609
- function mirrorGlyphs(rows) {
1610
- return rows.map((row) => {
1611
- let out = "";
1612
- for (let i = row.length - 1;i >= 0; i--)
1613
- out += MIRROR[row[i]] ?? row[i];
1614
- return out;
1615
- });
1616
- }
1617
- function reverseRows(rows) {
1618
- return rows.map((row) => {
1619
- let out = "";
1620
- for (let i = row.length - 1;i >= 0; i--)
1621
- out += row[i];
1622
- return out;
1623
- });
1624
- }
1625
-
1626
- class Sprite {
1627
- w;
1628
- h;
1629
- glyphRight;
1630
- glyphLeft;
1631
- colorRight;
1632
- colorLeft;
1633
- constructor(glyph, opts) {
1634
- const { defaultKey } = opts;
1635
- if (defaultKey.length !== 1)
1636
- throw new Error(`Sprite defaultKey must be a single char, got "${defaultKey}"`);
1637
- const glyphRows = splitTrimPad(glyph).map((r) => r.replaceAll(SENTINEL, " "));
1638
- this.h = glyphRows.length;
1639
- this.w = glyphRows.length > 0 ? glyphRows[0].length : 0;
1640
- let colorRows;
1641
- if (opts.colors === undefined) {
1642
- colorRows = glyphRows.map((r) => defaultKey.repeat(r.length));
1643
- } else {
1644
- const parsed = splitTrimPad(opts.colors);
1645
- const cw = parsed.length > 0 ? parsed[0].length : 0;
1646
- if (parsed.length !== this.h || cw !== this.w)
1647
- throw new Error(`Sprite colour grid (${cw}x${parsed.length}) must match glyph grid (${this.w}x${this.h})`);
1648
- colorRows = parsed.map((r) => Array.from(r, (c) => c === SENTINEL || c === " " ? defaultKey : c).join(""));
1649
- }
1650
- this.glyphRight = glyphRows;
1651
- this.glyphLeft = mirrorGlyphs(glyphRows);
1652
- this.colorRight = colorRows;
1653
- this.colorLeft = reverseRows(colorRows);
1654
- }
1655
- rows(facing = 1) {
1656
- return facing === 1 ? this.glyphRight : this.glyphLeft;
1657
- }
1658
- colorKeys(facing = 1) {
1659
- return facing === 1 ? this.colorRight : this.colorLeft;
1660
- }
1661
- }
1662
-
1663
- // ../client/src/sprites/chaser.ts
1664
- var GLYPH = `
1665
- \u259A\xB7\u259F\u2599\xB7\u259E\xB7
1666
- \u259F\u2588\u2588\u2588\u2588\u2599\xB7
1667
- \u259E\u259B\u259B\u259B\u259B\u258C\xB7
1668
- \u2590\u259F\u259F\u259F\u259F\u2596\xB7
1669
- \u259E\xB7\xB7\xB7\xB7\u259A\xB7`;
1670
- var COLORS2 = `
1671
- \xB7\xB7\xB7\xB7\xB7\xB7\xB7
1672
- \xB7g\xB7\xB7g\xB7\xB7
1673
- \xB7\xB7\xB7\xB7\xB7\xB7\xB7
1674
- \xB7\xB7\xB7\xB7\xB7\xB7\xB7
1675
- \xB7\xB7\xB7\xB7\xB7\xB7\xB7`;
1676
- var chaser = new Sprite(GLYPH, { defaultKey: "m", colors: COLORS2 });
1677
-
1678
- // ../client/src/sprites/merchant.ts
1679
- var GLYPH2 = `
1680
- \xB7\xB7\u259F\u2599\xB7\xB7
1681
- \xB7\u259F\u2588\u2588\u2599\xB7
1682
- \u259F\u259B\u2588\u2588\u259C\u2599
1683
- \u2588\u2588\u2588\u2588\u2588\u2588
1684
- \u259D\u2588\u2588\u2588\u2588\u2598`;
1685
- var COLORS3 = `
1686
- \xB7\xB7oo\xB7\xB7
1687
- \xB7oooo\xB7
1688
- oooooo
1689
- cccccc
1690
- \xB7oooo\xB7`;
1691
- var merchant = new Sprite(GLYPH2, { defaultKey: "o", colors: COLORS3 });
1692
-
1693
- // ../client/src/sprites/player.ts
1694
- var GLYPH3 = `
1695
- \xB7\u2590\u259B\u2588\u2588\u2588\u259C\u258C\xB7
1696
- \u259D\u259C\u2588\u2588\u2588\u2588\u2588\u259B\u2598
1697
- \xB7\xB7\u2598\u2598\xB7\u259D\u259D\xB7\xB7`;
1698
- var COLORS4 = `
1699
- \xB7ppppppp\xB7
1700
- ppppppppp
1701
- \xB7\xB7pp\xB7pp\xB7\xB7`;
1702
- var player2 = new Sprite(GLYPH3, { defaultKey: "p", colors: COLORS4 });
1703
-
1704
- // ../client/src/sprites/shooter.ts
1705
- var GLYPH4 = `
1706
- \xB7\u2597\u2584\u2584\u2584\u2596\xB7
1707
- \u259F\u2588\u2588\u2588\u2588\u2588\u2599
1708
- \u2588\u2588\u259F\u2588\u2599\u2588\u2588
1709
- \u259C\u2588\u2588\u2588\u2588\u2588\u259B
1710
- \xB7\u259D\u2580\u2580\u2580\u2598\xB7`;
1711
- var COLORS5 = `
1712
- \xB7ooooo\xB7
1713
- oogggoo
1714
- oggkggo
1715
- oogggoo
1716
- \xB7ooooo\xB7`;
1717
- var shooter = new Sprite(GLYPH4, { defaultKey: "o", colors: COLORS5 });
1718
-
1719
- // ../client/src/sprites/palette.ts
1720
- import { RGBA as RGBA2 } from "@opentui/core";
1721
- var PALETTE = {
1722
- p: RGBA2.fromInts(255, 150, 40, 255),
1723
- m: RGBA2.fromInts(220, 90, 90, 255),
1724
- g: RGBA2.fromInts(170, 240, 95, 255),
1725
- s: RGBA2.fromInts(186, 196, 210, 255),
1726
- w: RGBA2.fromInts(150, 96, 52, 255),
1727
- y: RGBA2.fromInts(242, 210, 92, 255),
1728
- e: RGBA2.fromInts(236, 190, 150, 255),
1729
- f: RGBA2.fromInts(110, 200, 110, 255),
1730
- c: RGBA2.fromInts(132, 222, 230, 255),
1731
- o: RGBA2.fromInts(232, 230, 216, 255),
1732
- k: RGBA2.fromInts(64, 66, 82, 255)
1733
- };
1734
-
1735
- // ../client/src/sprites/index.ts
1736
- var REGISTRY = {
1737
- player: player2,
1738
- chaser,
1739
- shooter
1740
- };
1741
- function spriteFor(type) {
1742
- return REGISTRY[type];
1743
- }
1744
- var NPC_REGISTRY = {
1745
- vendor: merchant
1746
- };
1747
- function spriteForNpc(kind) {
1748
- return NPC_REGISTRY[kind];
1749
- }
1750
-
1751
2204
  // ../client/src/playfield.ts
1752
- function blitSprite(buf, sprite, sx, sy, facing, sw, sh, hurt) {
1753
- const glyphs = sprite.rows(facing);
1754
- const keys = sprite.colorKeys(facing);
1755
- for (let ry = 0;ry < sprite.h; ry++) {
1756
- const py = sy + ry;
1757
- if (py < 0 || py >= sh)
1758
- continue;
1759
- const row = glyphs[ry];
1760
- const krow = keys[ry];
1761
- for (let rx = 0;rx < sprite.w; rx++) {
1762
- const ch = row[rx];
1763
- if (ch === " ")
1764
- continue;
1765
- const px = sx + rx;
1766
- if (px < 0 || px >= sw)
1767
- continue;
1768
- const fg = hurt ? COLORS.hurt : PALETTE[krow[rx]] ?? COLORS.hud;
1769
- buf.setCellWithAlphaBlending(px, py, ch, fg, COLORS.transparent);
2205
+ var STYLE = buildSceneStyle((r, g, b, a) => RGBA2.fromInts(r, g, b, a));
2206
+ function textContent(lines, fg) {
2207
+ return {
2208
+ w: Math.max(1, ...lines.map((l) => l.length)),
2209
+ h: lines.length,
2210
+ cell(x, y) {
2211
+ const ch = lines[y]?.[x];
2212
+ return ch && ch !== " " ? { ch, fg } : null;
1770
2213
  }
1771
- }
1772
- }
1773
- function drawSprite(buf, e, cam, sw, sh) {
1774
- const sprite = spriteFor(e.type);
1775
- const sx = Math.round(e.x - Math.floor((sprite.w - BOX.w) / 2) - cam.x);
1776
- const sy = Math.round(e.y + BOX.h - sprite.h - cam.y);
1777
- blitSprite(buf, sprite, sx, sy, e.facing, sw, sh, e.hurtT > 0.3);
2214
+ };
1778
2215
  }
1779
- function drawNameplate(buf, e, cam, sw, sh) {
1780
- if (!e.name)
1781
- return;
1782
- const sprite = spriteFor(e.type);
1783
- const top = Math.round(e.y + BOX.h - sprite.h - cam.y);
1784
- const cx = e.x + BOX.w / 2 - cam.x;
1785
- const x = Math.round(cx - e.name.length / 2);
1786
- drawText(buf, x, top - 1, e.name, COLORS.dim, sw, sh);
2216
+ function spriteContent(sprite, palette, paletteDefault) {
2217
+ const rows = sprite.rows(1);
2218
+ const keys = sprite.colorKeys(1);
2219
+ return {
2220
+ w: sprite.w,
2221
+ h: sprite.h,
2222
+ cell(x, y) {
2223
+ const ch = rows[y]?.[x];
2224
+ if (!ch || ch === " ")
2225
+ return null;
2226
+ return { ch, fg: palette[keys[y]?.[x]] ?? paletteDefault };
2227
+ }
2228
+ };
1787
2229
  }
1788
- function drawSpeechBubble(buf, e, cam, sw, sh) {
1789
- if (!e.bubble)
1790
- return;
2230
+ function drawOverheadBox(buf, e, cam, sw, sh, content, border) {
1791
2231
  const sprite = spriteFor(e.type);
1792
2232
  const top = Math.round(e.y + BOX.h - sprite.h - cam.y);
1793
- const lines = layoutBubble(e.bubble);
1794
- const innerW = Math.max(...lines.map((l) => l.length));
1795
- const boxW = innerW + 2;
1796
- const boxH = lines.length + 2;
2233
+ const boxW = content.w + 2;
2234
+ const boxH = content.h + 2;
1797
2235
  const cx = e.x + BOX.w / 2 - cam.x;
1798
2236
  const tailY = top - 2;
1799
2237
  const tailX = Math.round(cx);
@@ -1811,9 +2249,8 @@ function drawSpeechBubble(buf, e, cam, sw, sh) {
1811
2249
  continue;
1812
2250
  const lastCol = rx === boxW - 1;
1813
2251
  let ch = " ";
1814
- let fg = COLORS.bubbleFg;
2252
+ let fg = border;
1815
2253
  if (ry === 0 || lastRow || rx === 0 || lastCol) {
1816
- fg = COLORS.bubbleBorder;
1817
2254
  if (ry === 0)
1818
2255
  ch = rx === 0 ? "\u256D" : lastCol ? "\u256E" : "\u2500";
1819
2256
  else if (lastRow)
@@ -1821,22 +2258,41 @@ function drawSpeechBubble(buf, e, cam, sw, sh) {
1821
2258
  else
1822
2259
  ch = "\u2502";
1823
2260
  } else {
1824
- ch = lines[ry - 1]?.[rx - 1] ?? " ";
2261
+ const c = content.cell(rx - 1, ry - 1);
2262
+ if (c) {
2263
+ ch = c.ch;
2264
+ fg = c.fg;
2265
+ }
1825
2266
  }
1826
- buf.setCell(px, py, ch, fg, COLORS.bubbleBg);
2267
+ buf.setCell(px, py, ch, fg, COLORS5.bubbleBg);
1827
2268
  }
1828
2269
  }
1829
2270
  if (tailY >= 0 && tailY < sh && tailX >= 0 && tailX < sw)
1830
- buf.setCell(tailX, tailY, "\u25BC", COLORS.bubbleBorder, COLORS.bubbleBg);
2271
+ buf.setCell(tailX, tailY, "\u25BC", border, COLORS5.bubbleBg);
2272
+ }
2273
+ function drawSpeechBubble(buf, e, cam, sw, sh) {
2274
+ if (!e.bubble)
2275
+ return;
2276
+ const content = textContent(layoutBubble(e.bubble), COLORS5.bubbleFg);
2277
+ drawOverheadBox(buf, e, cam, sw, sh, content, COLORS5.bubbleBorder);
2278
+ }
2279
+ function drawEmote(buf, e, cam, sw, sh) {
2280
+ if (!e.emote)
2281
+ return;
2282
+ const def = emoteById(e.emote);
2283
+ if (!def)
2284
+ return;
2285
+ const content = spriteContent(def.sprite, STYLE.palette, STYLE.paletteDefault);
2286
+ drawOverheadBox(buf, e, cam, sw, sh, content, COLORS5.emote);
1831
2287
  }
1832
- function drawText(buf, x, y, text, fg, sw, sh) {
2288
+ function drawText2(buf, x, y, text, fg, sw, sh) {
1833
2289
  if (y < 0 || y >= sh)
1834
2290
  return;
1835
2291
  for (let i = 0;i < text.length; i++) {
1836
2292
  const px = x + i;
1837
2293
  if (px < 0 || px >= sw)
1838
2294
  continue;
1839
- buf.setCellWithAlphaBlending(px, y, text[i], fg, COLORS.transparent);
2295
+ buf.setCellWithAlphaBlending(px, y, text[i], fg, COLORS5.transparent);
1840
2296
  }
1841
2297
  }
1842
2298
  function drawPlayfield(buf, game, cam) {
@@ -1845,50 +2301,28 @@ function drawPlayfield(buf, game, cam) {
1845
2301
  const sw = buf.width;
1846
2302
  const sh = buf.height;
1847
2303
  const p = player3.avatar;
1848
- const ww = zone2.terrain.w;
1849
- const wh = zone2.terrain.h;
1850
2304
  const camX = Math.round(cam.x);
1851
2305
  const camY = Math.round(cam.y);
1852
- buf.clear(COLORS.bg);
1853
- for (let sy = 0;sy < sh; sy++) {
1854
- const wy = sy + camY;
1855
- for (let sx = 0;sx < sw; sx++) {
1856
- const wx = sx + camX;
1857
- if (isSolid(zone2.terrain, wx, wy) && wx >= 0 && wx < ww && wy >= 0 && wy < wh)
1858
- buf.setCell(sx, sy, "\u2588", COLORS.terrainFg, COLORS.terrainBg);
1859
- }
1860
- }
2306
+ const others = game.others ?? [];
2307
+ const npcs = zone2.npcs ?? [];
2308
+ renderZoneScene(buf, {
2309
+ terrain: zone2.terrain,
2310
+ portals: zone2.portals,
2311
+ npcs,
2312
+ entities: [...zone2.monsters, ...others]
2313
+ }, cam, STYLE);
1861
2314
  const onPortal = zone2.portals.find((pr) => aabbOverlap(entityBox(p), pr));
1862
- for (const pr of zone2.portals) {
1863
- for (let yy = 0;yy < pr.h; yy++) {
1864
- for (let xx = 0;xx < pr.w; xx++) {
1865
- const px = pr.x + xx - camX;
1866
- const py = pr.y + yy - camY;
1867
- if (px >= 0 && px < sw && py >= 0 && py < sh)
1868
- buf.setCellWithAlphaBlending(px, py, "\u2592", COLORS.portal, COLORS.transparent);
1869
- }
1870
- }
1871
- }
1872
2315
  if (onPortal) {
1873
2316
  const dest = game.world.zones[onPortal.target]?.type ?? "zone";
1874
2317
  const label = `\u21B5 e enter the ${dest.charAt(0).toUpperCase()}${dest.slice(1)}`;
1875
- drawText(buf, Math.round(onPortal.x) - camX, Math.round(onPortal.y) - camY - 1, label, COLORS.portal, sw, sh);
2318
+ drawText2(buf, Math.round(onPortal.x) - camX, Math.round(onPortal.y) - camY - 1, label, COLORS5.portal, sw, sh);
1876
2319
  }
1877
- const npcs = zone2.npcs ?? [];
1878
2320
  const onNpc = npcs.find((n) => aabbOverlap(entityBox(p), n));
1879
- for (const n of npcs) {
1880
- const sprite = spriteForNpc(n.kind);
1881
- const sx = Math.round(n.x + Math.floor((n.w - sprite.w) / 2)) - camX;
1882
- const sy = Math.round(n.y + n.h - sprite.h) - camY;
1883
- blitSprite(buf, sprite, sx, sy, 1, sw, sh, false);
1884
- if (n === onNpc)
1885
- drawText(buf, sx, sy - 1, `\u21B5 e talk to ${n.name}`, COLORS.vendor, sw, sh);
1886
- }
1887
- const others = game.others ?? [];
1888
- const sprites = [...zone2.monsters, ...others].sort((a, b) => a.y - b.y);
1889
- for (const e of sprites) {
1890
- drawSprite(buf, e, cam, sw, sh);
1891
- drawNameplate(buf, e, cam, sw, sh);
2321
+ if (onNpc) {
2322
+ const sprite = spriteForNpc(onNpc.kind);
2323
+ const sx = Math.round(onNpc.x + Math.floor((onNpc.w - sprite.w) / 2)) - camX;
2324
+ const sy = Math.round(onNpc.y + onNpc.h - sprite.h) - camY;
2325
+ drawText2(buf, sx, sy - 1, `\u21B5 e talk to ${onNpc.name}`, COLORS5.vendor, sw, sh);
1892
2326
  }
1893
2327
  if (p.attackT > COMBAT.attackCooldown - 0.12) {
1894
2328
  const hb = meleeHitbox(p);
@@ -1897,7 +2331,7 @@ function drawPlayfield(buf, game, cam) {
1897
2331
  const px = Math.round(hb.x + xx - cam.x);
1898
2332
  const py = Math.round(hb.y + yy - cam.y);
1899
2333
  if (px >= 0 && px < sw && py >= 0 && py < sh)
1900
- buf.setCellWithAlphaBlending(px, py, p.facing === 1 ? "/" : "\\", COLORS.melee, COLORS.transparent);
2334
+ buf.setCellWithAlphaBlending(px, py, p.facing === 1 ? "/" : "\\", COLORS5.melee, COLORS5.transparent);
1901
2335
  }
1902
2336
  }
1903
2337
  }
@@ -1914,21 +2348,24 @@ function drawPlayfield(buf, game, cam) {
1914
2348
  const px = Math.round(hb.x + xx - cam.x);
1915
2349
  const py = Math.round(hb.y + yy - cam.y);
1916
2350
  if (px >= 0 && px < sw && py >= 0 && py < sh)
1917
- buf.setCellWithAlphaBlending(px, py, "\u2726", COLORS.melee, COLORS.transparent);
2351
+ buf.setCellWithAlphaBlending(px, py, "\u2726", COLORS5.melee, COLORS5.transparent);
1918
2352
  }
1919
2353
  }
1920
2354
  }
1921
- drawSprite(buf, p, cam, sw, sh);
2355
+ drawEntitySprite(buf, p, cam, STYLE);
1922
2356
  for (const e of others)
1923
2357
  drawSpeechBubble(buf, e, cam, sw, sh);
1924
2358
  drawSpeechBubble(buf, p, cam, sw, sh);
2359
+ for (const e of others)
2360
+ drawEmote(buf, e, cam, sw, sh);
2361
+ drawEmote(buf, p, cam, sw, sh);
1925
2362
  for (const pr of zone2.projectiles) {
1926
2363
  const px = Math.round(pr.x - cam.x);
1927
2364
  const py = Math.round(pr.y - cam.y);
1928
2365
  if (px < 0 || px >= sw || py < 0 || py >= sh)
1929
2366
  continue;
1930
2367
  const ch = pr.vx < 0 ? "\u25C4" : pr.vx > 0 ? "\u25BA" : "\u25CF";
1931
- buf.setCellWithAlphaBlending(px, py, ch, COLORS.projectile, COLORS.transparent);
2368
+ buf.setCellWithAlphaBlending(px, py, ch, COLORS5.projectile, COLORS5.transparent);
1932
2369
  }
1933
2370
  }
1934
2371
 
@@ -1984,25 +2421,25 @@ class Shop {
1984
2421
  padding: 1,
1985
2422
  border: true,
1986
2423
  borderStyle: "single",
1987
- borderColor: COLORS.vendor,
2424
+ borderColor: COLORS5.vendor,
1988
2425
  title: " Merchant \u2014 sell loot ",
1989
- titleColor: COLORS.vendor,
1990
- backgroundColor: COLORS.hudBg
2426
+ titleColor: COLORS5.vendor,
2427
+ backgroundColor: COLORS5.hudBg
1991
2428
  });
1992
2429
  this.gold = new TextRenderable2(ctx, {
1993
2430
  content: "",
1994
- fg: COLORS.vendor,
1995
- bg: COLORS.hudBg
2431
+ fg: COLORS5.vendor,
2432
+ bg: COLORS5.hudBg
1996
2433
  });
1997
2434
  this.list = new TextRenderable2(ctx, {
1998
2435
  content: "",
1999
- fg: COLORS.hud,
2000
- bg: COLORS.hudBg
2436
+ fg: COLORS5.hud,
2437
+ bg: COLORS5.hudBg
2001
2438
  });
2002
2439
  const footer = new TextRenderable2(ctx, {
2003
2440
  content: "\u2191/\u2193 select \u21B5 sell e/esc close",
2004
- fg: COLORS.dim,
2005
- bg: COLORS.hudBg
2441
+ fg: COLORS5.dim,
2442
+ bg: COLORS5.hudBg
2006
2443
  });
2007
2444
  panel.add(this.gold);
2008
2445
  panel.add(this.list);
@@ -2097,11 +2534,6 @@ function fpsMeter() {
2097
2534
  return fps;
2098
2535
  };
2099
2536
  }
2100
- if (OFFLINE)
2101
- runOffline();
2102
- else
2103
- runNetworked(SERVER);
2104
- renderer.start();
2105
2537
  function runOffline() {
2106
2538
  let game = createGame();
2107
2539
  const shop = new Shop(renderer);
@@ -2169,8 +2601,9 @@ function runOffline() {
2169
2601
  shop.update(game.player);
2170
2602
  });
2171
2603
  }
2604
+ var LOCAL_ZONES = new Map(loadZones().map((z) => [z.id, z]));
2172
2605
  function localZone(id) {
2173
- return id === "town-01" ? makeTownZone(id) : makeFieldZone(id || "field-01");
2606
+ return LOCAL_ZONES.get(id) ?? LOCAL_ZONES.get("field-01") ?? loadZones()[0];
2174
2607
  }
2175
2608
  function runNetworked(url) {
2176
2609
  const handle = process.env.USER || "wanderer";
@@ -2188,8 +2621,17 @@ function runNetworked(url) {
2188
2621
  renderer.keyInput.on("keypress", (k) => {
2189
2622
  if (chat.open) {
2190
2623
  const r = chat.key(k);
2191
- if (r.action === "send")
2192
- net.send({ t: "chat", text: r.text });
2624
+ if (r.action === "send") {
2625
+ const cmd = parseChatCommand(r.text);
2626
+ if (cmd.kind === "say")
2627
+ net.send({ t: "chat", text: cmd.text });
2628
+ else if (cmd.kind === "whisper")
2629
+ net.send({ t: "whisper", to: cmd.to, text: cmd.text });
2630
+ else if (cmd.kind === "emote")
2631
+ net.send({ t: "emote", emote: cmd.emote });
2632
+ else
2633
+ net.notice(cmd.message);
2634
+ }
2193
2635
  return;
2194
2636
  }
2195
2637
  if (k.name === "q")
@@ -2259,9 +2701,15 @@ function runNetworked(url) {
2259
2701
  const fps = meter(dt);
2260
2702
  const view = net.sample(performance.now());
2261
2703
  net.decayBubbles(dt / 1000);
2262
- const game = snapshotToGame(zone2, predicted, net.sessionId, view, localCd, net.bubbles);
2704
+ net.decayEmotes(dt / 1000);
2705
+ const game = snapshotToGame(zone2, predicted, net.sessionId, view, localCd, net.bubbles, net.emotes);
2263
2706
  playfield.game = game;
2264
2707
  hud.update(game, fps);
2265
2708
  hud.updateChat(net.chatLog, chat.open, chat.text);
2266
2709
  });
2267
2710
  }
2711
+ if (OFFLINE)
2712
+ runOffline();
2713
+ else
2714
+ runNetworked(SERVER);
2715
+ renderer.start();