PageSourceSearch

https://the-torchbearer.netlify.app/src/ui/kit.js?v=27Jul-TB8

js the-torchbearer.netlify.app collected 2026-10-03 11:30:05 UTC 8,120 bytes, 221 lines download raw bytes

1/* The Torchbearer — the UI kit.
2 *
3 * Every screen used to draw its own boxes, its own tabs and its own idea of a
4 * button, so nothing lined up with anything and the game read as five separate
5 * student projects stapled together. These are the shared parts.
6 *
7 * Rules the whole UI follows:
8 *   - one panel style: dark fill, hairline border, optional accent head band
9 *   - one button style: framed, lifts and brightens on hover, keycap on the right
10 *   - 8px spacing grid, so things line up without anyone measuring
11 *   - accent colour carries meaning (class, rarity, danger), never decoration
12 */
13window.SK = window.SK || {};
14
15SK.ui = {
16  GRID: 8,
17  PANEL_FILL: 0x1d1811,
18  PANEL_FILL_HOT: 0x2b2318,
19  PANEL_LINE: 0x4a4036,
20
21  /* A framed panel. Returns the graphics so callers can repaint on hover. */
22  panel: function (scene, x, y, w, h, opts) {
23    opts = opts || {};
24    var g = scene.add.graphics();
25    var accent = opts.accent;
26    var draw = function (hot) {
27      g.clear();
28      g.fillStyle(hot ? SK.ui.PANEL_FILL_HOT : SK.ui.PANEL_FILL,
29        opts.alpha === undefined ? 0.94 : opts.alpha);
30      g.fillRoundedRect(x, y, w, h, opts.radius || 8);
31      g.lineStyle(hot ? 2 : 1, hot ? (accent || SK.COLORS.gold) : SK.ui.PANEL_LINE, 1);
32      g.strokeRoundedRect(x, y, w, h, opts.radius || 8);
33      if (accent && opts.band !== false) {
34        var bh = opts.bandH || 28;
35        g.fillStyle(accent, 0.13);
36        g.fillRoundedRect(x + 1, y + 1, w - 2, bh,
37          { tl: (opts.radius || 8) - 1, tr: (opts.radius || 8) - 1, bl: 0, br: 0 });
38        g.fillStyle(accent, 1);
39        g.fillRect(x + 1, y + bh, w - 2, 2);
40      }
41    };
42    draw(false);
43    g.repaint = draw;
44    return g;
45  },
46
47  /* Screen header: title, optional subtitle, and a rule beneath. */
48  header: function (scene, cx, y, title, subtitle, opts) {
49    opts = opts || {};
50    var out = {};
51    out.title = scene.add.text(cx, y, title, {
52      fontFamily: SK.FONT, fontSize: (opts.size || 30) + 'px',
53      color: opts.color || SK.CSS.gold,
54      stroke: '#12100c', strokeThickness: 4
55    }).setOrigin(0.5, 0);
56    if (subtitle) {
57      out.sub = scene.add.text(cx, y + (opts.size || 30) + 8, subtitle, {
58        fontFamily: SK.FONT, fontSize: '13px', color: SK.CSS.labelDim
59      }).setOrigin(0.5, 0);
60    }
61    if (opts.rule !== false) {
62      var rw = opts.ruleW || 420;
63      var ry = y + (opts.size || 30) + (subtitle ? 44 : 12);
64      var g = scene.add.graphics();
65      g.fillStyle(SK.ui.PANEL_LINE, 1);
66      g.fillRect(cx - rw / 2, ry, rw, 1);
67      // A short gold pip centred on the rule, so the eye lands on the title.
68      g.fillStyle(SK.COLORS.gold, 1);
69      g.fillRect(cx - 18, ry, 36, 1);
70      out.ruleY = ry;
71    }
72    return out;
73  },
74
75  divider: function (scene, x, y, w) {
76    var g = scene.add.graphics();
77    g.fillStyle(SK.ui.PANEL_LINE, 1);
78    g.fillRect(x, y, w, 1);
79    return g;
80  },
81
82  /* A labelled key, drawn like a physical cap. Used everywhere a control is
83   * mentioned so the player learns one visual language for input. */
84  keycap: function (scene, x, y, key, opts) {
85    opts = opts || {};
86    var pad = key.length > 1 ? 9 : 11;
87    var t = scene.add.text(0, 0, key, {
88      fontFamily: SK.FONT, fontSize: (opts.size || 13) + 'px',
89      color: opts.color || SK.CSS.bone
90    }).setOrigin(0.5);
91    var w = t.width + pad * 2, h = (opts.size || 13) + 12;
92    var g = scene.add.graphics();
93    g.fillStyle(0x2e2a22, 1);
94    g.fillRoundedRect(x - w / 2, y - h / 2, w, h, 4);
95    g.lineStyle(1, opts.line || 0x6a6052, 1);
96    g.strokeRoundedRect(x - w / 2, y - h / 2, w, h, 4);
97    t.setPosition(x, y);
98    return { g: g, text: t, w: w, h: h };
99  },
100
101  /* Framed, hoverable button with an optional keycap on the right. */
102  button: function (scene, x, y, w, h, label, opts) {
103    opts = opts || {};
104    var accent = opts.accent || SK.COLORS.gold;
105    var c = scene.add.container(x, y);
106    var g = scene.add.graphics();
107    var paint = function (hot) {
108      g.clear();
109      g.fillStyle(hot ? SK.ui.PANEL_FILL_HOT : SK.ui.PANEL_FILL, 0.96);
110      g.fillRoundedRect(-w / 2, -h / 2, w, h, 6);
111      g.lineStyle(hot ? 2 : 1, hot ? accent : SK.ui.PANEL_LINE, 1);
112      g.strokeRoundedRect(-w / 2, -h / 2, w, h, 6);
113      if (hot) {
114        g.fillStyle(accent, 1);
115        g.fillRect(-w / 2 + 1, -h / 2 + 1, 3, h - 2);
116      }
117    };
118    paint(false);
119
120    var parts = [g];
121    var tx = opts.sub ? -h / 2 + 8 : 0;
122    var title = scene.add.text(-w / 2 + 18, opts.sub ? -h / 2 + 9 : 0, label, {
123      fontFamily: SK.FONT, fontSize: (opts.size || 19) + 'px',
124      color: opts.color || SK.CSS.gold
125    }).setOrigin(0, opts.sub ? 0 : 0.5);
126    parts.push(title);
127    if (opts.sub) {
128      parts.push(scene.add.text(-w / 2 + 18, h / 2 - 22, opts.sub, {
129        fontFamily: SK.FONT, fontSize: '12px', color: SK.CSS.labelDim
130      }).setOrigin(0, 0));
131    }
132    if (opts.key) {
133      var cap = SK.ui.keycap(scene, w / 2 - 34, 0, opts.key);
134      parts.push(cap.g, cap.text);
135    }
136    c.add(parts);
137    c.setSize(w, h);
138    // The WHOLE frame is the target, and it says so with the cursor. A button
139    // you have to hit the label of is a button that feels broken.
140    c.setInteractive(new Phaser.Geom.Rectangle(-w / 2, -h / 2, w, h),
141      Phaser.Geom.Rectangle.Contains);
142    c.input.cursor = 'pointer';
143    c.on('pointerover', function () {
144      paint(true);
145      scene.tweens.add({ targets: c, x: x + 4, duration: 110 });
146      if (SK.synth) SK.synth.blip();
147    });
148    c.on('pointerout', function () {
149      paint(false);
150      scene.tweens.add({ targets: c, x: x, duration: 110 });
151    });
152    if (opts.onClick) c.on('pointerdown', opts.onClick);
153    c.paint = paint;
154    return c;
155  },
156
157  /* Horizontal meter with a label. value is 0..1. */
158  statBar: function (scene, x, y, w, label, value, color, valueText) {
159    var g = scene.add.graphics();
160    var lab = scene.add.text(x, y - 3, label, {
161      fontFamily: SK.FONT, fontSize: '11px', color: SK.CSS.labelDim
162    });
163    var barX = x + 84, barW = w - 84;
164    g.fillStyle(0x000000, 0.45);
165    g.fillRect(barX - 1, y - 1, barW + 2, 9);
166    g.fillStyle(0x3a332a, 1);
167    g.fillRect(barX, y, barW, 7);
168    g.fillStyle(color || SK.COLORS.gold, 1);
169    g.fillRect(barX, y, Math.round(barW * Phaser.Math.Clamp(value, 0, 1)), 7);
170    var val = null;
171    if (valueText) {
172      val = scene.add.text(barX + barW + 8, y - 3, valueText, {
173        fontFamily: SK.FONT, fontSize: '11px', color: SK.CSS.bone
174      });
175    }
176    return { g: g, label: lab, value: val };
177  },
178
179  /* Tab strip. Returns a paint(activeId) function. */
180  tabs: function (scene, cx, y, items, onPick) {
181    var made = [];
182    var totalW = items.length * 132;
183    var startX = cx - totalW / 2 + 66;
184    items.forEach(function (it, i) {
185      var x = startX + i * 132;
186      var g = scene.add.graphics();
187      var t = scene.add.text(x, y, it[1], {
188        fontFamily: SK.FONT, fontSize: '15px', color: SK.CSS.labelDim
189      }).setOrigin(0.5);
190
191      // A transparent plate over the whole pill. Making only the glyphs
192      // clickable meant half the visible tab did nothing.
193      var hit = scene.add.rectangle(x, y, 124, 34, 0xffffff, 0)
194        .setInteractive({ useHandCursor: true });
195      hit.on('pointerdown', function () { onPick(it[0]); });
196      hit.on('pointerover', function () {
197        if (SK.synth) SK.synth.blip();
198        if (t.__on !== true) t.setColor(SK.CSS.bone);
199      });
200      hit.on('pointerout', function () {
201        if (t.__on !== true) t.setColor(SK.CSS.labelDim);
202      });
203      made.push({ id: it[0], g: g, t: t, x: x, hit: hit });
204    });
205    var paint = function (active) {
206      made.forEach(function (m) {
207        var on = m.id === active;
208        m.t.__on = on;
209        m.t.setColor(on ? SK.CSS.gold : SK.CSS.labelDim);
210        m.g.clear();
211        if (on) {
212          m.g.fillStyle(SK.COLORS.gold, 0.10);
213          m.g.fillRoundedRect(m.x - 60, y - 15, 120, 30, 5);
214          m.g.fillStyle(SK.COLORS.gold, 1);
215          m.g.fillRect(m.x - 26, y + 15, 52, 2);
216        }
217      });
218    };
219    return { paint: paint, items: made };
220  }
221};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.