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.