@bongos/core 1.21.7 → 1.21.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +98 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +182 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +20 -5
- package/docs/copy-registry.json +163 -16
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +33 -4
- package/docs/page-readings.json +44 -2
- package/modules/discord/board-broadcast.js +2 -0
- package/modules/government/board.js +82 -23
- package/modules/government/db.js +81 -6
- package/modules/government/decision-board.js +300 -0
- package/modules/government/decision-rules.js +312 -0
- package/modules/government/docket.js +49 -9
- package/modules/government/migrations/government_023_decision_rules.sql +190 -0
- package/modules/government/routes/government.js +115 -0
- package/modules/hall-ui/public/city-draw.js +11 -2
- package/modules/hall-ui/public/street-draw.js +585 -0
- package/modules/hall-ui/public/street.css +188 -0
- package/modules/hall-ui/public/street.html +118 -0
- package/modules/hall-ui/public/street.js +364 -0
- package/modules/hall-ui/public/street.states.json +43 -0
- package/modules/hall-ui/records/governor-street.md +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/serve-internal.js +12 -0
- package/src/module-api.js +1 -1
- package/tests/government_abuse_matrix.mjs +3 -0
- package/tests/government_decision_rules.mjs +701 -0
- package/tests/government_routes.mjs +4 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_audit.mjs +9 -1
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_street.mjs +521 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
// modules/hall-ui/public/street-draw.js — THE STREET's drawing, as data (task
|
|
2
|
+
// 1004525, goal 1000125; R03 of the Governor City).
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. The street view of the reference design, docs/design/mocks/
|
|
5
|
+
// governor-city/Street.dc.html: its front-on drawing kit (box, gable, sawtooth,
|
|
6
|
+
// tree, light), the nine civic recipes (civic), the district work (work), the
|
|
7
|
+
// prototype's `ready` slab (workItem), the street's theme (theme() minus its iso
|
|
8
|
+
// half, which city-draw.js already carries) and the street branch of
|
|
9
|
+
// renderVals(): the segs order, the road, the crates, the names and state lines,
|
|
10
|
+
// the group brackets, the pins, the doors and the six readings (signsOf). Every
|
|
11
|
+
// kit function is the prototype's, ported verbatim; tests/hall_street.mjs holds
|
|
12
|
+
// them equal by running the prototype's own Component class beside this file.
|
|
13
|
+
// The decision glyphs and the seeded random are city-draw.js's (GovCity), which
|
|
14
|
+
// are the prototype's too: one copy, never a fork.
|
|
15
|
+
//
|
|
16
|
+
// ONE CHANGE OF INPUT. renderVals() drew the prototype's sample docket and its
|
|
17
|
+
// four hand-placed districts. streetModel() reads the real governor docket (GET
|
|
18
|
+
// /api/bongos/government/docket, task 1004523) for the buildings, pins, state
|
|
19
|
+
// lines and readings, the open goals and their task rollups (GET /goals, GET
|
|
20
|
+
// /goals/rollups) for the districts, and the viewer's earnings for "your share
|
|
21
|
+
// this month". Where the real data had to bend the design, it is said once here
|
|
22
|
+
// and in the ship notes:
|
|
23
|
+
// * Districts. One per OPEN goal, in the order the goals come, between
|
|
24
|
+
// Automation and Review. Up to four keep the prototype's 90 px plots; past
|
|
25
|
+
// four each plot narrows (never under 56 px) and past that the street grows
|
|
26
|
+
// longer than the 1440 px artboard, so the page scrolls it sideways rather
|
|
27
|
+
// than drop a goal.
|
|
28
|
+
// * A district's work. The prototype hand-placed four buildings per district.
|
|
29
|
+
// Here the plot's slots (one per ~21 px) are shared among the goal's tasks in
|
|
30
|
+
// proportion: shipped work stands built (tower, setback or shed, in the
|
|
31
|
+
// city's shares), work being built is a braced scaffold, work not started is
|
|
32
|
+
// the prototype's `ready` slab (workItem). A goal with no tasks keeps the
|
|
33
|
+
// prototype's trees.
|
|
34
|
+
// * Review. The prototype opens with Review a vacant lot. Here Review is held
|
|
35
|
+
// (drawn glass) while any source that stands in it can be read; it draws as
|
|
36
|
+
// the empty seat only when none can (every Review source off or still to
|
|
37
|
+
// come). The matters standing at Review (criteria awaiting a tester's UAT)
|
|
38
|
+
// are the work queued at its door: up to three crates outside it and the
|
|
39
|
+
// prototype's dashed count pin, "waiting for a tester", instead of one pin
|
|
40
|
+
// each.
|
|
41
|
+
// * Pins. A building can hold more matters than the prototype's one; up to
|
|
42
|
+
// three stand over it, stacked one label above the other, needs first (the
|
|
43
|
+
// docket's own order). The building's state line carries the full count.
|
|
44
|
+
// * State lines. The reading that reads a building gives its line when it is
|
|
45
|
+
// live (people, money, fairness, rules at work); Planning says how many goals
|
|
46
|
+
// are open; otherwise the line counts the building's matters.
|
|
47
|
+
// * Your share. The sum of the viewer's earnings this calendar month (UTC), in
|
|
48
|
+
// dollars at the platform's peg of one credit to one dollar (ADR 0054). The
|
|
49
|
+
// prototype's "+$24" flash on a release waits for the card that releases
|
|
50
|
+
// (task 1004528).
|
|
51
|
+
//
|
|
52
|
+
// PURE. No DOM, no fetch, no clock (the month for "your share" is passed in):
|
|
53
|
+
// the same inputs give the same street, so the tests pin the model without a
|
|
54
|
+
// browser and street.js only turns the lists into nodes.
|
|
55
|
+
|
|
56
|
+
(function (root) {
|
|
57
|
+
'use strict';
|
|
58
|
+
const City = root.GovCity || (typeof require === 'function' ? require('./city-draw.js') : null);
|
|
59
|
+
|
|
60
|
+
// ---------- the street's palette (Street.dc.html theme(mode), less .iso) ----------
|
|
61
|
+
function theme(mode) {
|
|
62
|
+
if (mode === 'light') {
|
|
63
|
+
return {
|
|
64
|
+
bg: '#edecee', card: '#fbfafc', ink: '#17181a', soft: '#55565a', faint: '#65656a', rule: '#dcdbdd', ruleStrong: '#c6c6c8',
|
|
65
|
+
accentText: '#a3481a', wash: 'rgba(23,24,26,0.045)', line: 'rgba(23,24,26,0.30)', road: 'rgba(23,24,26,0.045)', lane: 'rgba(23,24,26,0.22)',
|
|
66
|
+
shadow: 'rgba(23,24,26,0.10)',
|
|
67
|
+
civic: { f: '#fbfafc', r: '#dedde2', t: '#f1f0f3', s: 'rgba(23,24,26,0.34)' },
|
|
68
|
+
work: { f: '#f4f3f6', r: '#d9d8dd', t: '#e9e8ec', s: 'rgba(23,24,26,0.24)' },
|
|
69
|
+
roof: { f: '#e6e5e9', r: '#cfced4', t: '#dcdbe0', s: 'rgba(23,24,26,0.34)' },
|
|
70
|
+
glass: { f: 'rgba(91,111,208,0.13)', r: 'rgba(91,111,208,0.22)', t: 'rgba(91,111,208,0.10)', s: 'rgba(60,80,180,0.50)' },
|
|
71
|
+
ghost: { f: 'rgba(23,24,26,0.02)', r: 'none', t: 'none', s: 'rgba(23,24,26,0.40)', dash: '4 4' },
|
|
72
|
+
scaffold: { f: 'rgba(23,24,26,0.02)', r: 'rgba(23,24,26,0.02)', t: 'rgba(23,24,26,0.02)', s: 'rgba(23,24,26,0.45)', dash: '3 3' },
|
|
73
|
+
win: 'rgba(23,24,26,0.13)', winLit: 'rgba(214,140,80,0.60)', tree: 'rgba(91,111,208,0.22)', treeLine: 'rgba(60,80,180,0.45)', beacon: '#fb9569',
|
|
74
|
+
pinMoving: { bg: '#17181a', gc: '#ffffff', stem: 'rgba(23,24,26,0.45)' }, pinSettled: { bg: '#e2e1e4', border: '1px solid rgba(23,24,26,0.22)', gc: '#65656a', stem: 'rgba(23,24,26,0.25)' },
|
|
75
|
+
seatFor: { bg: '#17181a', fg: '#ffffff' }, seatOpen: 'rgba(23,24,26,0.40)', crate: 'rgba(23,24,26,0.55)', crateFill: 'rgba(23,24,26,0.04)',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
bg: '#000000', card: '#101012', ink: '#ffffff', soft: '#c0c0c0', faint: '#a8a8a8', rule: 'rgba(255,255,255,0.12)', ruleStrong: 'rgba(255,255,255,0.18)',
|
|
80
|
+
accentText: '#fcb596', wash: 'rgba(255,255,255,0.045)', line: 'rgba(255,255,255,0.26)', road: 'rgba(255,255,255,0.03)', lane: 'rgba(255,255,255,0.18)',
|
|
81
|
+
shadow: 'none',
|
|
82
|
+
civic: { f: '#25252a', r: '#16161a', t: '#3b3b41', s: 'rgba(255,255,255,0.15)' },
|
|
83
|
+
work: { f: '#1b1b1f', r: '#0f0f12', t: '#2d2d32', s: 'rgba(255,255,255,0.12)' },
|
|
84
|
+
roof: { f: '#2f2f35', r: '#1d1d22', t: '#38383e', s: 'rgba(255,255,255,0.15)' },
|
|
85
|
+
glass: { f: 'rgba(120,140,230,0.16)', r: 'rgba(100,120,210,0.12)', t: 'rgba(150,170,255,0.22)', s: 'rgba(170,190,255,0.42)' },
|
|
86
|
+
ghost: { f: 'rgba(255,255,255,0.02)', r: 'none', t: 'none', s: 'rgba(255,255,255,0.40)', dash: '4 4' },
|
|
87
|
+
scaffold: { f: 'rgba(255,255,255,0.02)', r: 'rgba(255,255,255,0.015)', t: 'rgba(255,255,255,0.03)', s: 'rgba(255,255,255,0.45)', dash: '3 3' },
|
|
88
|
+
win: 'rgba(255,255,255,0.08)', winLit: 'rgba(255,214,170,0.80)', tree: 'rgba(91,111,208,0.32)', treeLine: 'rgba(140,155,230,0.45)', beacon: '#fff4e6',
|
|
89
|
+
pinMoving: { bg: '#ffffff', gc: '#17181a', stem: 'rgba(255,255,255,0.45)' }, pinSettled: { bg: '#1e1e22', border: '1px solid rgba(255,255,255,0.22)', gc: '#a8a8a8', stem: 'rgba(255,255,255,0.22)' },
|
|
90
|
+
seatFor: { bg: '#ffffff', fg: '#17181a' }, seatOpen: 'rgba(255,255,255,0.40)', crate: 'rgba(255,255,255,0.60)', crateFill: 'rgba(255,255,255,0.04)',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------- the front-on kit (verbatim) ----------
|
|
95
|
+
function push(out, d, f, s, dash, w) { out.push({ d: d, f: f, s: s, dash: dash || 'none', w: w || 1 }); }
|
|
96
|
+
function rect(x, y, w, h) { return 'M' + x + ' ' + y + 'h' + w + 'v' + h + 'h' + (-w) + 'Z'; }
|
|
97
|
+
function ellipse(cx, cy, rx, ry) { return 'M' + (cx - rx) + ' ' + cy + 'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z'; }
|
|
98
|
+
|
|
99
|
+
function box(out, T, x, G, w, h, tone, o) {
|
|
100
|
+
o = o || {};
|
|
101
|
+
const d = o.d === undefined ? 7 : o.d, z = o.z || 0, base = G - z, y = base - h, dash = tone.dash || 'none';
|
|
102
|
+
push(out, 'M' + (x + w) + ' ' + y + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (base - d) + 'L' + (x + w) + ' ' + base + 'Z', tone.r, tone.s, dash);
|
|
103
|
+
push(out, 'M' + x + ' ' + y + 'L' + (x + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w) + ' ' + y + 'Z', tone.t, tone.s, dash);
|
|
104
|
+
push(out, rect(x, y, w, h), tone.f, tone.s, dash);
|
|
105
|
+
if (o.win) {
|
|
106
|
+
let p = '';
|
|
107
|
+
for (let wy = y + 7; wy < base - 9; wy += 9) for (let wx = x + 5; wx < x + w - 6; wx += 8) p += rect(wx, wy, 4, 4);
|
|
108
|
+
if (p) push(out, p, o.lit ? T.winLit : T.win, 'none');
|
|
109
|
+
}
|
|
110
|
+
if (o.fins) {
|
|
111
|
+
let p = '';
|
|
112
|
+
for (let i = 1; i < o.fins; i++) { const fx = Math.round(x + w * i / o.fins) + 0.5; p += 'M' + fx + ' ' + (y + 3) + 'L' + fx + ' ' + (base - 3); }
|
|
113
|
+
push(out, p, 'none', tone.s);
|
|
114
|
+
}
|
|
115
|
+
if (o.grid) {
|
|
116
|
+
let p = '';
|
|
117
|
+
for (let gy = y + 10; gy < base - 2; gy += 10) p += 'M' + x + ' ' + gy + 'h' + w;
|
|
118
|
+
for (let i = 1; i < o.grid; i++) p += 'M' + Math.round(x + w * i / o.grid) + ' ' + y + 'V' + base;
|
|
119
|
+
push(out, p, 'none', tone.s);
|
|
120
|
+
}
|
|
121
|
+
if (o.door) {
|
|
122
|
+
const dw = Math.max(6, Math.round(w * 0.16)), dh = Math.min(15, Math.round(h * 0.42));
|
|
123
|
+
push(out, rect(Math.round(x + w / 2 - dw / 2), base - dh, dw, dh), T.bg === '#000000' ? '#050506' : '#cfced3', tone.s);
|
|
124
|
+
}
|
|
125
|
+
if (o.brace) push(out, 'M' + x + ' ' + y + 'L' + (x + w) + ' ' + base + 'M' + (x + w) + ' ' + y + 'L' + x + ' ' + base, 'none', tone.s, '3 3');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function gable(out, x, yTop, w, hr, tone) {
|
|
129
|
+
const d = 7;
|
|
130
|
+
push(out, 'M' + (x + w / 2) + ' ' + (yTop - hr) + 'L' + (x + w / 2 + d) + ' ' + (yTop - hr - d) + 'L' + (x + w + d) + ' ' + (yTop - d) + 'L' + (x + w) + ' ' + yTop + 'Z', tone.r, tone.s);
|
|
131
|
+
push(out, 'M' + x + ' ' + yTop + 'L' + (x + w / 2) + ' ' + (yTop - hr) + 'L' + (x + w) + ' ' + yTop + 'Z', tone.f, tone.s);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function sawtooth(out, x, yTop, w, n, hr, tone, glass) {
|
|
135
|
+
for (let i = 0; i < n; i++) {
|
|
136
|
+
const x0 = x + w * i / n, x1 = x0 + w / n;
|
|
137
|
+
push(out, 'M' + x0 + ' ' + yTop + 'L' + x1 + ' ' + (yTop - hr) + 'L' + x1 + ' ' + yTop + 'Z', tone.f, tone.s);
|
|
138
|
+
push(out, rect(x1 - 3, yTop - hr + 3, 3, hr - 3), glass.f, glass.s);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function tree(out, T, cx, G, size) {
|
|
143
|
+
push(out, 'M' + cx + ' ' + G + 'V' + (G - size), 'none', T.treeLine);
|
|
144
|
+
push(out, ellipse(cx, G - size - size * 0.35, size * 0.5, size * 0.5), T.tree, T.treeLine);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function light(out, cx, cy, r, color) {
|
|
148
|
+
push(out, ellipse(cx, cy, r * 2.6, r * 2.6), 'rgba(251,149,105,0.10)', 'none');
|
|
149
|
+
push(out, ellipse(cx, cy, r, r), color, 'none');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Each civic building, drawn front-on inside its own width w with its base on
|
|
153
|
+
// the street G. Returns its roof height.
|
|
154
|
+
function civic(out, T, kind, x, G, w, st) {
|
|
155
|
+
const C = T.civic, R = T.roof, Gl = T.glass, lit = st.evening;
|
|
156
|
+
if (kind === 'charter') {
|
|
157
|
+
box(out, T, x, G, w, 8, C);
|
|
158
|
+
box(out, T, x + 6, G, w - 12, 70, C, { z: 8, win: true, lit: lit });
|
|
159
|
+
box(out, T, x + 14, G, w - 28, 60, C, { z: 78, win: true, lit: lit });
|
|
160
|
+
box(out, T, x + 22, G, w - 44, 50, C, { z: 138, fins: 4 });
|
|
161
|
+
box(out, T, x + 30, G, w - 60, 24, Gl, { z: 188, grid: 2 });
|
|
162
|
+
light(out, x + w / 2 + 3, G - 222, 3.2, T.beacon);
|
|
163
|
+
return 226;
|
|
164
|
+
}
|
|
165
|
+
if (kind === 'rules') {
|
|
166
|
+
box(out, T, x, G, w, 6, C);
|
|
167
|
+
box(out, T, x + 6, G, w - 12, 46, C, { z: 6, fins: 10, win: false });
|
|
168
|
+
box(out, T, x + 2, G, w - 4, 6, R, { z: 52 });
|
|
169
|
+
if (st.fixes) push(out, rect(x + 10, G - 46, w - 20, 3), T.winLit, 'none');
|
|
170
|
+
return 64;
|
|
171
|
+
}
|
|
172
|
+
if (kind === 'disputes') {
|
|
173
|
+
box(out, T, x, G, 24, 40, C, { win: true, lit: lit });
|
|
174
|
+
box(out, T, x + 24, G, 32, 84, C, { fins: 3, door: true });
|
|
175
|
+
gable(out, x + 24, G - 84, 32, 16, R);
|
|
176
|
+
box(out, T, x + 56, G, 24, 40, C, { win: true, lit: lit });
|
|
177
|
+
return 102;
|
|
178
|
+
}
|
|
179
|
+
if (kind === 'people') {
|
|
180
|
+
box(out, T, x, G, 34, 28, C, { door: true });
|
|
181
|
+
gable(out, x, G - 28, 34, 15, R);
|
|
182
|
+
box(out, T, x + 40, G, 40, 36, C, { door: true, win: true, lit: lit });
|
|
183
|
+
gable(out, x + 40, G - 36, 40, 18, R);
|
|
184
|
+
return 58;
|
|
185
|
+
}
|
|
186
|
+
if (kind === 'planning') {
|
|
187
|
+
box(out, T, x, G, w, 72, C, { win: true, lit: lit, door: true });
|
|
188
|
+
sawtooth(out, x, G - 72, w, 3, 18, R, Gl);
|
|
189
|
+
return 92;
|
|
190
|
+
}
|
|
191
|
+
if (kind === 'review') {
|
|
192
|
+
if (!st.reviewHeld) {
|
|
193
|
+
box(out, T, x + 3, G, w - 6, 82, T.ghost, { d: 0 });
|
|
194
|
+
push(out, 'M' + (x + 14) + ' ' + G + 'V' + (G - 34), 'none', T.ghost.s);
|
|
195
|
+
push(out, rect(x + 6, G - 46, 30, 12), T.bg, T.ghost.s);
|
|
196
|
+
return 86;
|
|
197
|
+
}
|
|
198
|
+
box(out, T, x, G, w, 22, C, { door: true });
|
|
199
|
+
box(out, T, x + 5, G, w - 10, 60, Gl, { z: 22, grid: 5 });
|
|
200
|
+
box(out, T, x + 2, G, w - 4, 4, R, { z: 82 });
|
|
201
|
+
return 90;
|
|
202
|
+
}
|
|
203
|
+
if (kind === 'release') {
|
|
204
|
+
box(out, T, x, G, 26, 150, C, { win: true, lit: lit });
|
|
205
|
+
light(out, x + 16, G - 160, 2.6, T.beacon);
|
|
206
|
+
box(out, T, x + 26, G, 40, 18, C, { z: 104 });
|
|
207
|
+
box(out, T, x + 66, G, 26, 150, C, { win: true, lit: lit });
|
|
208
|
+
light(out, x + 82, G - 160, 2.6, T.beacon);
|
|
209
|
+
return 166;
|
|
210
|
+
}
|
|
211
|
+
if (kind === 'treasury') {
|
|
212
|
+
box(out, T, x, G, w, 10, C);
|
|
213
|
+
box(out, T, x + 8, G, w - 16, 82, C, { z: 10, fins: 5, door: true });
|
|
214
|
+
box(out, T, x + 4, G, w - 8, 8, R, { z: 92 });
|
|
215
|
+
box(out, T, x + 20, G, w - 40, 14, C, { z: 100 });
|
|
216
|
+
return 120;
|
|
217
|
+
}
|
|
218
|
+
if (kind === 'automation') {
|
|
219
|
+
box(out, T, x, G, w, 54, C, { win: true, lit: lit, door: true });
|
|
220
|
+
box(out, T, x + 10, G, 14, 46, C, { z: 54 });
|
|
221
|
+
box(out, T, x + w - 30, G, 24, 18, R, { z: 54, fins: 4 });
|
|
222
|
+
light(out, x + 20, G - 108, 2.8, T.beacon);
|
|
223
|
+
return 112;
|
|
224
|
+
}
|
|
225
|
+
return 60;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The prototype's district work, plus `slab`: its workItem 'ready' stage (a
|
|
229
|
+
// task not started yet), drawn at the street's base.
|
|
230
|
+
function work(out, T, kind, x, G, w, h, lit) {
|
|
231
|
+
if (kind === 'tower') box(out, T, x, G, w, h, T.work, { win: true, lit: lit, d: 5 });
|
|
232
|
+
else if (kind === 'setback') {
|
|
233
|
+
box(out, T, x, G, w, Math.round(h * 0.6), T.work, { win: true, lit: lit, d: 5 });
|
|
234
|
+
box(out, T, x + 4, G, w - 8, Math.round(h * 0.4), T.work, { z: Math.round(h * 0.6), win: true, lit: lit, d: 5 });
|
|
235
|
+
} else if (kind === 'shed') {
|
|
236
|
+
box(out, T, x, G, w, 16, T.work, { d: 5, door: true });
|
|
237
|
+
gable(out, x, G - 16, w, 10, T.roof);
|
|
238
|
+
} else if (kind === 'scaffold') box(out, T, x, G, w, h, T.scaffold, { d: 5, brace: true });
|
|
239
|
+
else if (kind === 'trees') { tree(out, T, x + w * 0.35, G, 12); tree(out, T, x + w * 0.75, G, 9); }
|
|
240
|
+
else if (kind === 'slab') box(out, T, x, G, w, 6, T.work, { d: 6 });
|
|
241
|
+
else if (kind === 'stakes') {
|
|
242
|
+
// workItem's 'backlog' stage (a staked plot), set on the street line
|
|
243
|
+
push(out, 'M' + x + ' ' + G + 'L' + (x + 7) + ' ' + (G - 7) + 'L' + (x + w + 7) + ' ' + (G - 7) + 'L' + (x + w) + ' ' + G + 'Z', 'none', T.ghost.s, '3 3');
|
|
244
|
+
push(out, 'M' + (x + 3) + ' ' + G + 'V' + (G - 16) + 'M' + (x + w - 3) + ' ' + G + 'V' + (G - 16), 'none', T.ghost.s);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------- the street's fixed layout (renderVals) ----------
|
|
249
|
+
const G = 556;
|
|
250
|
+
const SEGS = Object.freeze([
|
|
251
|
+
Object.freeze({ k: 'charter', w: 84, g: 0 }), Object.freeze({ k: 'rules', w: 96, g: 0 }), Object.freeze({ k: 'disputes', w: 80, g: 0 }),
|
|
252
|
+
Object.freeze({ k: 'people', w: 80, g: 1 }), Object.freeze({ k: 'planning', w: 80, g: 1 }),
|
|
253
|
+
Object.freeze({ k: 'automation', w: 80, g: 2 }),
|
|
254
|
+
Object.freeze({ districts: true, g: 2 }),
|
|
255
|
+
Object.freeze({ k: 'review', w: 86, g: 3 }), Object.freeze({ k: 'release', w: 92, g: 3 }),
|
|
256
|
+
Object.freeze({ k: 'treasury', w: 84, g: 4 }),
|
|
257
|
+
]);
|
|
258
|
+
const GROUP_NAMES = Object.freeze(['how we decide', 'who and what', 'the work', 'checks and going live', 'money']);
|
|
259
|
+
const CIVIC_KEYS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
|
|
260
|
+
const DISTRICT_W = 90;
|
|
261
|
+
const DISTRICT_MIN_W = 56;
|
|
262
|
+
const DISTRICT_SPAN = 4 * DISTRICT_W + 3 * 10; // the prototype's four plots and their gaps
|
|
263
|
+
const MAX_DISTRICTS = 8;
|
|
264
|
+
const MORE_KEY = 'more';
|
|
265
|
+
const GOALS_HREF = '/builders/goals';
|
|
266
|
+
const ARTBOARD_W = 1440;
|
|
267
|
+
const MAX_PINS_PER_BUILDING = 3;
|
|
268
|
+
const PIN_STACK = 40;
|
|
269
|
+
const MAX_CRATES = 3;
|
|
270
|
+
const HINT = 'work moves left to right · orange waits on you · white is moving without you';
|
|
271
|
+
const READING_KEYS = Object.freeze(['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
|
|
272
|
+
const READING_LABELS = Object.freeze({ flow: 'flow', promises: 'promises', money: 'money', people: 'people', fairness: 'fairness', rules: 'rules at work' });
|
|
273
|
+
const READING_BUILDING = Object.freeze({ flow: 'review', promises: 'planning', money: 'treasury', people: 'people', fairness: 'disputes', rules: 'rules' });
|
|
274
|
+
// Review's own sources (docket.js EXPECTED_SOURCES): while none can be read,
|
|
275
|
+
// no one holds the seat.
|
|
276
|
+
const REVIEW_SOURCES_HELD = Object.freeze(['live', 'not_yours', 'unavailable']);
|
|
277
|
+
|
|
278
|
+
// Where a door opens. The interiors are task 1004526's; until they land the
|
|
279
|
+
// street marks the building the address names (README "Where the screens live").
|
|
280
|
+
function buildingHref(key) { return '/builders/street#building=' + encodeURIComponent(key); }
|
|
281
|
+
function districtHref(goalId) { return '/builders/street#district=' + encodeURIComponent(goalId); }
|
|
282
|
+
// `#building=charter` / `#district=1000125` → { kind, key }, else null.
|
|
283
|
+
function parseHash(hash) {
|
|
284
|
+
const m = /^#(building|district)=([^&]+)$/.exec(String(hash || ''));
|
|
285
|
+
if (!m) return null;
|
|
286
|
+
let key;
|
|
287
|
+
try { key = decodeURIComponent(m[2]); } catch (_) { return null; }
|
|
288
|
+
if (m[1] === 'building' && CIVIC_KEYS.indexOf(key) < 0) return null;
|
|
289
|
+
return { kind: m[1], key: key };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function districtWidth(n) {
|
|
293
|
+
if (n <= 4) return DISTRICT_W;
|
|
294
|
+
return Math.max(DISTRICT_MIN_W, Math.floor((DISTRICT_SPAN - (n - 1) * 10) / n));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The segs with x filled in: renderVals()' spacing (30 between groups, 10
|
|
298
|
+
// between districts, 18 otherwise), starting at 43. Past MAX_DISTRICTS open
|
|
299
|
+
// goals the first seven stand as districts and the eighth plot is the rest,
|
|
300
|
+
// staked out, a door to the goals page.
|
|
301
|
+
function layout(goals) {
|
|
302
|
+
const shown = goals.length > MAX_DISTRICTS ? goals.slice(0, MAX_DISTRICTS - 1) : goals;
|
|
303
|
+
const more = goals.length - shown.length;
|
|
304
|
+
const dw = districtWidth(shown.length + (more ? 1 : 0));
|
|
305
|
+
const segs = [];
|
|
306
|
+
SEGS.forEach((s) => {
|
|
307
|
+
if (s.districts) {
|
|
308
|
+
shown.forEach((g) => segs.push({ d: String(g.id), title: String(g.title || ''), w: dw, g: 2 }));
|
|
309
|
+
if (more) segs.push({ d: MORE_KEY, more: more, title: more + ' more goals', w: dw, g: 2 });
|
|
310
|
+
} else segs.push({ k: s.k, w: s.w, g: s.g });
|
|
311
|
+
});
|
|
312
|
+
let x = 43;
|
|
313
|
+
segs.forEach((s, i) => {
|
|
314
|
+
if (i > 0) x += segs[i - 1].g !== s.g ? 30 : (s.d ? 10 : 18);
|
|
315
|
+
s.x = x; x += s.w;
|
|
316
|
+
});
|
|
317
|
+
return { segs: segs, width: Math.max(ARTBOARD_W, x + 43), districtW: dw };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function count(rollup, k) { return Math.max(0, Math.floor(Number(rollup && rollup[k]) || 0)); }
|
|
321
|
+
|
|
322
|
+
// A goal's tasks as the buildings on its plot (see the header).
|
|
323
|
+
function districtItems(goalId, w, rollup) {
|
|
324
|
+
const done = count(rollup, 'done'), active = count(rollup, 'active');
|
|
325
|
+
const waiting = count(rollup, 'ready') + count(rollup, 'blocked') + count(rollup, 'backlog');
|
|
326
|
+
const total = done + active + waiting;
|
|
327
|
+
const seed = String(goalId).split('').reduce((a, c) => a + c.charCodeAt(0), 0);
|
|
328
|
+
if (!total) return [{ kind: 'trees', w: Math.round(20 * 0.78), h: 0 }];
|
|
329
|
+
const slots = Math.max(1, Math.floor((w - 2) / 21));
|
|
330
|
+
// Every class that has work gets a slot when there is room, then the rest by share.
|
|
331
|
+
const classes = [['done', done], ['active', active], ['waiting', waiting]].filter((c) => c[1] > 0);
|
|
332
|
+
const n = {};
|
|
333
|
+
classes.forEach((c) => { n[c[0]] = 0; });
|
|
334
|
+
let left = slots;
|
|
335
|
+
classes.forEach((c) => { if (left > 0) { n[c[0]] += 1; left -= 1; } });
|
|
336
|
+
while (left > 0) {
|
|
337
|
+
let best = null, gap = -Infinity;
|
|
338
|
+
classes.forEach((c) => { const g2 = c[1] / total - n[c[0]] / slots; if (g2 > gap) { gap = g2; best = c[0]; } });
|
|
339
|
+
n[best] += 1; left -= 1;
|
|
340
|
+
}
|
|
341
|
+
const items = [];
|
|
342
|
+
let i = 0;
|
|
343
|
+
const raw = (j) => 20 + Math.round(City.rnd(seed * 7 + j * 13 + 1) * 6);
|
|
344
|
+
for (let j = 0; j < (n.done || 0); j++, i++) {
|
|
345
|
+
const r = City.rnd(seed * 17 + i * 11 + 3), r2 = City.rnd(seed * 13 + i * 29 + 5);
|
|
346
|
+
const kind = r < 0.58 ? 'tower' : r < 0.8 ? 'setback' : 'shed';
|
|
347
|
+
items.push({ kind: kind, w: Math.round(raw(i) * 0.78), h: kind === 'shed' ? 0 : Math.round(46 + r2 * 74) });
|
|
348
|
+
}
|
|
349
|
+
for (let j = 0; j < (n.active || 0); j++, i++) items.push({ kind: 'scaffold', w: Math.round(raw(i) * 0.78), h: Math.round(56 + City.rnd(seed * 5 + i) * 18) });
|
|
350
|
+
for (let j = 0; j < (n.waiting || 0); j++, i++) items.push({ kind: 'slab', w: Math.round(raw(i) * 0.78), h: 6 });
|
|
351
|
+
return items;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function districtLine(rollup) {
|
|
355
|
+
const done = count(rollup, 'done'), active = count(rollup, 'active');
|
|
356
|
+
const waiting = count(rollup, 'ready') + count(rollup, 'blocked') + count(rollup, 'backlog');
|
|
357
|
+
if (done) return done + ' built';
|
|
358
|
+
if (active) return active + ' being built';
|
|
359
|
+
if (waiting) return waiting + ' waiting';
|
|
360
|
+
return 'no work yet';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function countsLine(c) {
|
|
364
|
+
const n = (k) => Math.max(0, Number(c && c[k]) || 0);
|
|
365
|
+
if (n('needs')) return n('needs') + (n('needs') === 1 ? ' needs you' : ' need you');
|
|
366
|
+
if (n('moving')) return n('moving') + ' moving';
|
|
367
|
+
if (n('settled')) return n('settled') + ' settled';
|
|
368
|
+
return 'nothing open';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function pinState(state) {
|
|
372
|
+
return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
376
|
+
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
377
|
+
// A deadline in the prototype's words: 'today', a weekday inside the week, else
|
|
378
|
+
// 'oct 9'. Read in UTC against `now` (passed in, so the model stays pure).
|
|
379
|
+
function whenWord(deadline, now) {
|
|
380
|
+
if (!deadline) return '';
|
|
381
|
+
const d = new Date(deadline);
|
|
382
|
+
if (isNaN(d.getTime())) return '';
|
|
383
|
+
const day = (t) => Math.floor(t / 86400000);
|
|
384
|
+
const diff = day(d.getTime()) - day(now);
|
|
385
|
+
if (diff === 0) return 'today';
|
|
386
|
+
if (diff > 0 && diff < 7) return DAYS[d.getUTCDay()];
|
|
387
|
+
return MONTHS[d.getUTCMonth()] + ' ' + d.getUTCDate();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// The viewer's share this month: the sum of GET /builders/:id/earnings days
|
|
391
|
+
// inside the calendar month of `now` (UTC). null when the read failed.
|
|
392
|
+
function monthShare(earnings, now) {
|
|
393
|
+
if (!earnings || !Array.isArray(earnings.days)) return null;
|
|
394
|
+
const t = new Date(now);
|
|
395
|
+
const ym = t.getUTCFullYear() + '-' + String(t.getUTCMonth() + 1).padStart(2, '0');
|
|
396
|
+
return earnings.days.reduce((s, d) => s + (d && String(d.day || '').slice(0, 7) === ym ? Math.round(Number(d.credits) || 0) : 0), 0);
|
|
397
|
+
}
|
|
398
|
+
function money(n) { return n == null ? '—' : '$' + Math.round(n).toLocaleString('en-US'); }
|
|
399
|
+
|
|
400
|
+
const FALLBACK_LINE = 'the docket could not be read just now';
|
|
401
|
+
|
|
402
|
+
// The whole street view as data.
|
|
403
|
+
// docket GET /government/docket body, or null when it failed
|
|
404
|
+
// goals GET /goals goals[] (only 'open' ones are drawn)
|
|
405
|
+
// rollups GET /goals/rollups rollups[]
|
|
406
|
+
// earnings GET /builders/:viewer/earnings body, or null
|
|
407
|
+
// mode 'dark' | 'light'
|
|
408
|
+
// now ms since the epoch (for deadlines and the month)
|
|
409
|
+
// here parseHash() of the address, or null
|
|
410
|
+
function streetModel(input) {
|
|
411
|
+
const opts = input || {};
|
|
412
|
+
const docket = opts.docket && typeof opts.docket === 'object' && Array.isArray(opts.docket.matters) ? opts.docket : null;
|
|
413
|
+
const mode = opts.mode === 'light' ? 'light' : 'dark';
|
|
414
|
+
const T = theme(mode);
|
|
415
|
+
const now = Number(opts.now) || 0;
|
|
416
|
+
const here = opts.here || null;
|
|
417
|
+
const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
|
|
418
|
+
const needs = Math.max(0, Number(counts.needs) || 0);
|
|
419
|
+
const evening = !!docket && needs === 0;
|
|
420
|
+
|
|
421
|
+
const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
|
|
422
|
+
const byGoal = {};
|
|
423
|
+
(Array.isArray(opts.rollups) ? opts.rollups : []).forEach((r) => { if (r && r.goal_id != null) byGoal[String(r.goal_id)] = r; });
|
|
424
|
+
const L = layout(goals);
|
|
425
|
+
const segs = L.segs, W = L.width;
|
|
426
|
+
const at = {};
|
|
427
|
+
segs.forEach((s) => { at[s.k || ('d:' + s.d)] = s; });
|
|
428
|
+
|
|
429
|
+
const matters = docket ? docket.matters.filter((m) => m && CIVIC_KEYS.indexOf(m.building) >= 0) : [];
|
|
430
|
+
const bInfo = {};
|
|
431
|
+
(docket && Array.isArray(docket.buildings) ? docket.buildings : []).forEach((b) => { if (b && b.key) bInfo[b.key] = b; });
|
|
432
|
+
const reviewSources = (docket && Array.isArray(docket.sources) ? docket.sources : []).filter((s) => s && s.building === 'review');
|
|
433
|
+
// An unread docket keeps Review held: the empty seat is a claim about the
|
|
434
|
+
// project, and nothing has been read to make it.
|
|
435
|
+
const reviewHeld = !docket || !reviewSources.length || reviewSources.some((s) => REVIEW_SOURCES_HELD.indexOf(s.status) >= 0);
|
|
436
|
+
const queue = matters.filter((m) => m.building === 'review' && pinState(m.state) !== 'settled');
|
|
437
|
+
|
|
438
|
+
// the road
|
|
439
|
+
const flat = [];
|
|
440
|
+
push(flat, rect(32, G, W - 64, 30), T.road, 'none');
|
|
441
|
+
push(flat, 'M32 ' + G + 'H' + (W - 32), 'none', T.line);
|
|
442
|
+
push(flat, 'M32 ' + (G + 15) + 'H' + (W - 32), 'none', T.lane, '6 10');
|
|
443
|
+
|
|
444
|
+
// the buildings, and every district's work
|
|
445
|
+
const blds = [];
|
|
446
|
+
const roofs = {};
|
|
447
|
+
segs.forEach((s) => {
|
|
448
|
+
if (T.shadow !== 'none') push(flat, ellipse(s.x + s.w / 2 + 4, G + 2, s.w * 0.56, 5), T.shadow, 'none');
|
|
449
|
+
if (s.k) {
|
|
450
|
+
const polys = [];
|
|
451
|
+
roofs[s.k] = civic(polys, T, s.k, s.x, G, s.w, { evening: evening, reviewHeld: reviewHeld, fixes: City.rulesAtWork(docket) });
|
|
452
|
+
blds.push({ key: s.k, polys: polys });
|
|
453
|
+
} else {
|
|
454
|
+
let wx = s.x + 2;
|
|
455
|
+
const items = s.more ? [{ kind: 'stakes', w: s.w - 10, h: 0 }] : districtItems(s.d, s.w, byGoal[s.d]);
|
|
456
|
+
let top = 0;
|
|
457
|
+
items.forEach((it, j) => {
|
|
458
|
+
if (wx + it.w > s.x + s.w) return;
|
|
459
|
+
const polys = [];
|
|
460
|
+
work(polys, T, it.kind, wx, G, it.w, it.h, evening);
|
|
461
|
+
blds.push({ key: 'd:' + s.d + ':' + j, polys: polys });
|
|
462
|
+
top = Math.max(top, it.kind === 'shed' ? 26 : it.kind === 'trees' ? 22 : it.kind === 'stakes' ? 16 : it.h);
|
|
463
|
+
wx += it.w + 3;
|
|
464
|
+
});
|
|
465
|
+
roofs['d:' + s.d] = top;
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// work queued at Review: crates outside its door
|
|
470
|
+
const rv = at.review;
|
|
471
|
+
const crates = queue.slice(0, MAX_CRATES).map((m, i) => ({ key: 'c' + i, x: rv.x - 20 - i * 19, y: G + 9, matter: String(m.id) }));
|
|
472
|
+
|
|
473
|
+
// names and one line of state under every building
|
|
474
|
+
const reading = {};
|
|
475
|
+
(docket && Array.isArray(docket.readings) ? docket.readings : []).forEach((r) => { if (r && r.key) reading[r.key] = r; });
|
|
476
|
+
const readingFor = {};
|
|
477
|
+
READING_KEYS.forEach((k) => { const r = reading[k]; if (r && r.status === 'live' && r.figure) readingFor[r.building || READING_BUILDING[k]] = r; });
|
|
478
|
+
function civicState(k) {
|
|
479
|
+
if (!docket) return '';
|
|
480
|
+
if (k === 'review') return !reviewHeld ? 'empty seat' : queue.length ? queue.length + ' waiting' : 'nothing waiting';
|
|
481
|
+
if (k === 'planning') return goals.length + (goals.length === 1 ? ' goal open' : ' goals open');
|
|
482
|
+
if (readingFor[k]) return readingFor[k].figure;
|
|
483
|
+
return countsLine(bInfo[k] ? bInfo[k].counts : null);
|
|
484
|
+
}
|
|
485
|
+
const cols = segs.map((s) => s.k
|
|
486
|
+
? { key: s.k, x: s.x - 10, w: s.w + 20, name: s.k, title: s.k, state: civicState(s.k), strong: s.k === 'review' && !reviewHeld }
|
|
487
|
+
// districts stand 10 apart, so their names keep to their own plot (and ellipsis there)
|
|
488
|
+
: { key: 'd:' + s.d, x: s.x - 4, w: s.w + 8, name: s.title, title: s.title, state: s.more ? 'on the goals page' : districtLine(byGoal[s.d]), strong: false });
|
|
489
|
+
const groups = GROUP_NAMES.map((n, gi) => {
|
|
490
|
+
const mem = segs.filter((s) => s.g === gi);
|
|
491
|
+
const x0 = mem[0].x, x1 = mem[mem.length - 1].x + mem[mem.length - 1].w;
|
|
492
|
+
return { key: n, name: n, x: x0, w: x1 - x0 };
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// pins, each with its label always showing (up to three stacked per building)
|
|
496
|
+
const pins = [];
|
|
497
|
+
const byB = {};
|
|
498
|
+
matters.forEach((m) => { if (m.building !== 'review') (byB[m.building] = byB[m.building] || []).push(m); });
|
|
499
|
+
CIVIC_KEYS.forEach((k) => {
|
|
500
|
+
const s = at[k];
|
|
501
|
+
const here2 = (byB[k] || []).slice(0, MAX_PINS_PER_BUILDING);
|
|
502
|
+
const top = G - roofs[k] - 10;
|
|
503
|
+
here2.forEach((m, i) => {
|
|
504
|
+
const g = City.glyph(m.rule, 9, 9, 16);
|
|
505
|
+
const when = whenWord(m.deadline, now);
|
|
506
|
+
const short = String(m.short || m.title || '');
|
|
507
|
+
pins.push({
|
|
508
|
+
key: String(m.id), building: k, state: pinState(m.state), isCount: false, count: '',
|
|
509
|
+
x: Math.round(s.x + s.w / 2 - 55 + (k === 'release' ? 4 : 0)), y: top - 78 - i * PIN_STACK, h: 78 + i * PIN_STACK,
|
|
510
|
+
label: when ? short + ' · ' + when : short, aria: String(m.title || short), gf: g.fill, gl: g.line,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
if (queue.length) {
|
|
515
|
+
pins.push({ key: 'queue', building: 'review', state: 'queue', isCount: true, count: String(queue.length), x: Math.round(rv.x - 58), y: G - 70, h: 64, label: 'waiting for a tester', aria: (queue.length === 1 ? 'One piece of work waits' : queue.length + ' pieces of work wait') + ' for a tester at Review', gf: '', gl: '' });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// every building and district on the street is a door
|
|
519
|
+
const doors = segs.map((s) => s.k
|
|
520
|
+
? { key: s.k, x: s.x - 4, y: G - roofs[s.k] - 6, w: s.w + 14, h: roofs[s.k] + 10, href: buildingHref(s.k), aria: 'Go inside ' + s.k, here: !!here && here.kind === 'building' && here.key === s.k }
|
|
521
|
+
: s.more
|
|
522
|
+
? { key: 'd:' + MORE_KEY, x: s.x - 2, y: G - 130, w: s.w + 4, h: 132, href: GOALS_HREF, aria: 'See the other ' + s.more + ' open goals', here: false }
|
|
523
|
+
: { key: 'd:' + s.d, x: s.x - 2, y: G - 130, w: s.w + 4, h: 132, href: districtHref(s.d), aria: 'Go into the ' + (s.title || 'goal') + ' district', here: !!here && here.kind === 'district' && here.key === s.d });
|
|
524
|
+
|
|
525
|
+
// the six readings
|
|
526
|
+
const signs = READING_KEYS.map((k) => {
|
|
527
|
+
const r = reading[k] || { key: k, status: 'unavailable' };
|
|
528
|
+
const b = r.building || READING_BUILDING[k];
|
|
529
|
+
const live = r.status === 'live';
|
|
530
|
+
const bNeeds = bInfo[b] && bInfo[b].counts ? Number(bInfo[b].counts.needs) > 0 : false;
|
|
531
|
+
const figure = live && r.figure ? String(r.figure) : '—';
|
|
532
|
+
const line = live ? String(r.line || '') : r.status === 'not_yours' ? 'not yours to read here' : String(r.reason || (docket ? 'It could not be read just now.' : FALLBACK_LINE));
|
|
533
|
+
const label = String(r.label || READING_LABELS[k]);
|
|
534
|
+
return { key: k, label: label, figure: figure, line: line, building: b, href: buildingHref(b), hot: !!docket && (r.hot === true || bNeeds), here: !!here && here.kind === 'building' && here.key === b, aria: label + ': ' + figure + '. Go to ' + b };
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const moving = Math.max(0, Number(counts.moving) || 0), settled = Math.max(0, Number(counts.settled) || 0);
|
|
538
|
+
const share = monthShare(opts.earnings, now);
|
|
539
|
+
const pinCount = { needs: 0, moving: 0, settled: 0 };
|
|
540
|
+
pins.forEach((p) => { if (pinCount[p.state] != null) pinCount[p.state] += 1; });
|
|
541
|
+
return {
|
|
542
|
+
mode: mode, width: W, evening: evening, reviewHeld: reviewHeld,
|
|
543
|
+
flat: flat, blds: blds, crates: crates, cols: cols, groups: groups, pins: pins, doors: doors, signs: signs,
|
|
544
|
+
hud: { needs: docket ? String(needs) : '—', moving: docket ? String(moving) : '—', share: money(share), hot: needs > 0 },
|
|
545
|
+
line: docket ? String(docket.line || '') : FALLBACK_LINE,
|
|
546
|
+
hint: docket && !evening ? HINT : '',
|
|
547
|
+
eveningLine: 'this month · ' + moving + ' moving · ' + settled + ' settled · ' + money(share) + ' to your share',
|
|
548
|
+
label: 'The street: the nine civic buildings in the order work moves, and '
|
|
549
|
+
+ (goals.length === 1 ? 'one district' : goals.length + ' districts') + ', one for each open goal'
|
|
550
|
+
+ (docket ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.'),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// The short card a pin opens (task 1004528 makes it act). null for an unknown key.
|
|
555
|
+
const STATE_WORDS = Object.freeze({ needs: 'This waits on you.', moving: 'This is moving without you.', settled: 'Settled, or not yours to act on.' });
|
|
556
|
+
function cardFor(docket, key, now) {
|
|
557
|
+
const matters = docket && Array.isArray(docket.matters) ? docket.matters : [];
|
|
558
|
+
if (key === 'queue') {
|
|
559
|
+
const q = matters.filter((m) => m && m.building === 'review' && pinState(m.state) !== 'settled');
|
|
560
|
+
if (!q.length) return null;
|
|
561
|
+
return { key: 'queue', building: 'review', where: 'review', when: q.length + ' waiting', title: 'Waiting for a tester', lines: q.slice(0, 5).map((m) => String(m.title || m.short || '')), result: 'Each waits for someone who did not build it to sign it off.', href: q[0].href || null, proposer: '', tally: '' };
|
|
562
|
+
}
|
|
563
|
+
const m = matters.find((x) => x && String(x.id) === String(key));
|
|
564
|
+
if (!m) return null;
|
|
565
|
+
const p = m.proposer && (m.proposer.name || m.proposer.login) ? 'raised by ' + (m.proposer.name || m.proposer.login) : '';
|
|
566
|
+
const t = m.tally && typeof m.tally === 'object' ? m.tally : null;
|
|
567
|
+
// The card's tally line, in the prototype's order: '4 for · 1 against · needs everyone'.
|
|
568
|
+
const tally = t ? [t.for != null ? t.for + ' for' : null, t.against != null ? t.against + ' against' : null, t.needs ? String(t.needs) : null].filter(Boolean).join(' · ') : '';
|
|
569
|
+
return {
|
|
570
|
+
key: String(m.id), building: m.building, where: String(m.where || m.building || '').toLowerCase(), when: whenWord(m.deadline, now),
|
|
571
|
+
title: String(m.title || m.short || ''), lines: [], result: STATE_WORDS[pinState(m.state)], href: typeof m.href === 'string' && m.href.charAt(0) === '/' ? m.href : null, proposer: p, tally: tally,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const api = {
|
|
576
|
+
theme: theme, box: box, gable: gable, sawtooth: sawtooth, tree: tree, light: light, civic: civic, work: work,
|
|
577
|
+
layout: layout, districtWidth: districtWidth, districtItems: districtItems, districtLine: districtLine, countsLine: countsLine,
|
|
578
|
+
whenWord: whenWord, monthShare: monthShare, money: money, parseHash: parseHash, buildingHref: buildingHref, districtHref: districtHref,
|
|
579
|
+
streetModel: streetModel, cardFor: cardFor,
|
|
580
|
+
G: G, SEGS: SEGS, GROUP_NAMES: GROUP_NAMES, CIVIC_KEYS: CIVIC_KEYS, READING_KEYS: READING_KEYS, ARTBOARD_W: ARTBOARD_W,
|
|
581
|
+
MAX_PINS_PER_BUILDING: MAX_PINS_PER_BUILDING, MAX_CRATES: MAX_CRATES, MAX_DISTRICTS: MAX_DISTRICTS, GOALS_HREF: GOALS_HREF, HINT: HINT, FALLBACK_LINE: FALLBACK_LINE, HOME_HREF: '/builders/city',
|
|
582
|
+
};
|
|
583
|
+
root.GovStreet = api;
|
|
584
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
585
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|