310 lines
9.1 KiB
JavaScript
310 lines
9.1 KiB
JavaScript
/**
|
||
* ResearchIcons — procedural tech glyphs for the tree nodes and the detail
|
||
* readout (128×128 canvas textures, neon stroke + soft glow, transparent
|
||
* background). Names come from each node's `icon` key; unknown names fall
|
||
* back to `diamond`. Tintable at use (Phaser tint multiplies).
|
||
*/
|
||
import { toColor } from '../utils/Color.js';
|
||
|
||
export const ICON_NAMES = ['tether', 'tether2', 'tether3', 'mining', 'mining2', 'storage', 'anchor', 'signal', 'diamond', 'map', 'gate'];
|
||
|
||
export function iconKey(name, color = 0x00e5ff) {
|
||
const n = ICON_NAMES.includes(name) ? name : 'diamond';
|
||
const c = toColor(color);
|
||
return `__research_icon_${n}_${c.toString(16).padStart(6, '0')}`;
|
||
}
|
||
|
||
export function ensureIcon(scene, name, color = 0x00e5ff) {
|
||
const key = iconKey(name, color);
|
||
if (scene.textures.exists(key)) return key;
|
||
|
||
const S = 128;
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = S;
|
||
canvas.height = S;
|
||
const ctx = canvas.getContext('2d');
|
||
const hex = `#${toColor(color).toString(16).padStart(6, '0')}`;
|
||
const cx = S / 2;
|
||
const cy = S / 2;
|
||
|
||
ctx.clearRect(0, 0, S, S);
|
||
ctx.strokeStyle = hex;
|
||
ctx.fillStyle = hex;
|
||
ctx.shadowColor = hex;
|
||
ctx.shadowBlur = 9;
|
||
ctx.lineWidth = 5;
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
|
||
const diamond = (x, y, r) => {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, y - r);
|
||
ctx.lineTo(x + r, y);
|
||
ctx.lineTo(x, y + r);
|
||
ctx.lineTo(x - r, y);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
};
|
||
|
||
const n = ICON_NAMES.includes(name) ? name : 'diamond';
|
||
if (n === 'tether') {
|
||
// Concentric tether rings + core + tick marks + a satellite on the mid ring.
|
||
const rings = [
|
||
[52, 0.5],
|
||
[38, 0.75],
|
||
[24, 1.0],
|
||
];
|
||
for (const [r, a] of rings) {
|
||
ctx.globalAlpha = a;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 7, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx + Math.cos(a) * 55, cy + Math.sin(a) * 55);
|
||
ctx.lineTo(cx + Math.cos(a) * 61, cy + Math.sin(a) * 61);
|
||
ctx.stroke();
|
||
}
|
||
diamond(cx + 38, cy, 8);
|
||
} else if (n === 'tether2') {
|
||
// Level-2 tether: two bright concentric level rings + core + tick
|
||
// marks + TWO satellites riding the outer ring (L1 shows one).
|
||
const rings = [
|
||
[52, 1.0],
|
||
[34, 0.85],
|
||
];
|
||
for (const [r, a] of rings) {
|
||
ctx.globalAlpha = a;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 8, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx + Math.cos(a) * 55, cy + Math.sin(a) * 55);
|
||
ctx.lineTo(cx + Math.cos(a) * 61, cy + Math.sin(a) * 61);
|
||
ctx.stroke();
|
||
}
|
||
diamond(cx + 52 * Math.cos(-Math.PI / 3), cy + 52 * Math.sin(-Math.PI / 3), 8);
|
||
diamond(cx + 52 * Math.cos(Math.PI / 3), cy + 52 * Math.sin(Math.PI / 3), 8);
|
||
} else if (n === 'tether3') {
|
||
// Level-3 tether: two bright concentric level rings + core + tick
|
||
// marks + THREE satellites riding the outer ring (L1 one, L2 two).
|
||
const rings = [
|
||
[52, 1.0],
|
||
[34, 0.85],
|
||
];
|
||
for (const [r, a] of rings) {
|
||
ctx.globalAlpha = a;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 8, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx + Math.cos(a) * 55, cy + Math.sin(a) * 55);
|
||
ctx.lineTo(cx + Math.cos(a) * 61, cy + Math.sin(a) * 61);
|
||
ctx.stroke();
|
||
}
|
||
for (const a of [-Math.PI / 2, Math.PI / 6, (5 * Math.PI) / 6]) {
|
||
diamond(cx + 52 * Math.cos(a), cy + 52 * Math.sin(a), 8);
|
||
}
|
||
} else if (n === 'mining') {
|
||
// Improved Mining Arm: a drill bit (shaft + diamond tip) striking an
|
||
// asteroid, chips flying.
|
||
ctx.globalAlpha = 0.9;
|
||
ctx.beginPath();
|
||
ctx.arc(74, 74, 26, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.arc(82, 66, 5, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.arc(68, 84, 4, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(24, 24);
|
||
ctx.lineTo(52, 52);
|
||
ctx.stroke();
|
||
diamond(58, 58, 9);
|
||
diamond(88, 42, 5);
|
||
diamond(44, 90, 4);
|
||
} else if (n === 'mining2') {
|
||
// Advanced Mining Arm: the same drill, plus speed lines behind the
|
||
// shaft (twice as fast) and one more chip flying.
|
||
ctx.globalAlpha = 0.55;
|
||
ctx.beginPath();
|
||
ctx.moveTo(32, 14);
|
||
ctx.lineTo(50, 32);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(14, 32);
|
||
ctx.lineTo(32, 50);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 0.9;
|
||
ctx.beginPath();
|
||
ctx.arc(74, 74, 26, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.arc(82, 66, 5, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.arc(68, 84, 4, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(24, 24);
|
||
ctx.lineTo(50, 50);
|
||
ctx.stroke();
|
||
diamond(57, 57, 10);
|
||
diamond(88, 42, 5);
|
||
diamond(44, 90, 4);
|
||
diamond(96, 62, 4);
|
||
} else if (n === 'storage') {
|
||
// Improved Mining Storage: a hex container with a stack of mineral
|
||
// diamonds inside + a growth arrow breaking out the top (more room).
|
||
ctx.globalAlpha = 0.95;
|
||
ctx.beginPath();
|
||
for (let i = 0; i < 6; i++) {
|
||
const a = -Math.PI / 2 + (i * Math.PI) / 3;
|
||
const x = 64 + 38 * Math.cos(a);
|
||
const y = 70 + 38 * Math.sin(a);
|
||
if (i) ctx.lineTo(x, y);
|
||
else ctx.moveTo(x, y);
|
||
}
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.moveTo(64, 44);
|
||
ctx.lineTo(64, 20);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(56, 28);
|
||
ctx.lineTo(64, 20);
|
||
ctx.lineTo(72, 28);
|
||
ctx.stroke();
|
||
diamond(64, 74, 11);
|
||
diamond(48, 88, 6);
|
||
diamond(80, 88, 6);
|
||
} else if (n === 'anchor') {
|
||
// Anchor ring + stock + crossbar + flukes (the relay bolted to a body).
|
||
ctx.globalAlpha = 0.9;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 46, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy - 24);
|
||
ctx.lineTo(cx, cy + 20);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy - 24, 7, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx - 15, cy - 8);
|
||
ctx.lineTo(cx + 15, cy - 8);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy + 4, 20, Math.PI * 0.12, Math.PI * 0.88);
|
||
ctx.stroke();
|
||
diamond(cx - 19, cy + 15, 6);
|
||
diamond(cx + 19, cy + 15, 6);
|
||
} else if (n === 'signal') {
|
||
// Long-range array: source dot + rising arcs.
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy + 22, 7, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
const arcs = [
|
||
[19, 0.95],
|
||
[33, 0.7],
|
||
[47, 0.45],
|
||
];
|
||
for (const [r, a] of arcs) {
|
||
ctx.globalAlpha = a;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy + 22, r, -Math.PI * 0.82, -Math.PI * 0.18);
|
||
ctx.stroke();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
} else if (n === 'map') {
|
||
// The SYSTEM category's chart glyph — a folded map sheet with a plotted
|
||
// route between two NAV points (the per-system '{System} Map' tech).
|
||
const w = 62,
|
||
h = 50,
|
||
x = cx - w / 2,
|
||
y = cy - h / 2;
|
||
ctx.beginPath();
|
||
ctx.roundRect(x, y, w, h, 7);
|
||
ctx.stroke();
|
||
// fold line
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, y);
|
||
ctx.lineTo(cx, y + h);
|
||
ctx.stroke();
|
||
// route: two plotted points + the line between them
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + w * 0.22, y + h * 0.68);
|
||
ctx.lineTo(x + w * 0.78, y + h * 0.32);
|
||
ctx.stroke();
|
||
diamond(x + w * 0.22, y + h * 0.68, 5);
|
||
diamond(x + w * 0.78, y + h * 0.32, 5);
|
||
} else if (n === 'gate') {
|
||
// The SYSTEM category's jumpgate glyph — the ring + its inner aperture
|
||
// (+ the four aperture seams), like the world's gate entity minus field.
|
||
ctx.globalAlpha = 0.95;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 44, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
ctx.lineWidth = 3.5;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, 19, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.lineWidth = 5;
|
||
ctx.globalAlpha = 0.6;
|
||
for (const a of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2]) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx + Math.cos(a) * 28, cy + Math.sin(a) * 28);
|
||
ctx.lineTo(cx + Math.cos(a) * 37, cy + Math.sin(a) * 37);
|
||
ctx.stroke();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
} else {
|
||
// diamond — the default glyph
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy - 42);
|
||
ctx.lineTo(cx + 42, cy);
|
||
ctx.lineTo(cx, cy + 42);
|
||
ctx.lineTo(cx - 42, cy);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 0.55;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy - 22);
|
||
ctx.lineTo(cx + 22, cy);
|
||
ctx.lineTo(cx, cy + 22);
|
||
ctx.lineTo(cx - 22, cy);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
|
||
scene.textures.addCanvas(key, canvas);
|
||
return key;
|
||
}
|