@bongos/core 1.21.112 → 1.21.114
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 +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
|
|
49
49
|
(function (root) {
|
|
50
50
|
'use strict';
|
|
51
|
+
// the living model's one count rule (gov-living.js), so the city never counts for itself
|
|
52
|
+
const Living = root.GovLiving || (typeof require === 'function' ? require('./gov-living.js') : null);
|
|
51
53
|
|
|
52
54
|
// ---------- the iso sub-palette (Street.dc.html theme(mode).iso) ----------
|
|
53
55
|
function isoTheme(mode) {
|
|
@@ -230,7 +232,12 @@
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
// ---------- the city's fixed layout (Street.dc.html homeView) ----------
|
|
233
|
-
|
|
235
|
+
// The back corner (5,5), the centre (7,7) and the front corner (9,9) stand in
|
|
236
|
+
// one screen column, so whatever is at the back sits behind the centre. The
|
|
237
|
+
// Charter, the tallest, takes the back corner and rises over the quarter;
|
|
238
|
+
// Planning takes the centre, where nothing in front is tall enough to hide its
|
|
239
|
+
// lit floor (task 1004718: behind the Charter, Planning's floor never showed).
|
|
240
|
+
const CIVIC_SLOTS = Object.freeze([['charter', 5, 5], ['rules', 7, 5], ['planning', 9, 5], ['people', 5, 7], ['treasury', 7, 7], ['release', 9, 7], ['disputes', 5, 9], ['automation', 7, 9], ['review', 9, 9]]);
|
|
234
241
|
const ROOF = Object.freeze({ charter: 158, rules: 36, treasury: 66, planning: 51, people: 31, release: 72, disputes: 60, review: 53, automation: 86 });
|
|
235
242
|
|
|
236
243
|
// The four prototype districts (route = the parcel road it feeds), then the
|
|
@@ -364,21 +371,536 @@
|
|
|
364
371
|
// The home line, when the docket could not be read.
|
|
365
372
|
const FALLBACK_LINE = 'the docket could not be read just now';
|
|
366
373
|
|
|
374
|
+
// ======================================================================
|
|
375
|
+
// THE LIVING CITY (task 1004685, the living-city prototype)
|
|
376
|
+
// ======================================================================
|
|
377
|
+
// When cityModel() is handed `living` (GovLiving.livingModel()'s output,
|
|
378
|
+
// gov-living.js) the city keeps its look and gains three levels, each with
|
|
379
|
+
// one meaning:
|
|
380
|
+
// ground the goal districts, hazed toward the void by depth band (a
|
|
381
|
+
// district with an agent at work stands one band clearer), and
|
|
382
|
+
// the spend conduit in the reserve blue, from the Treasury round
|
|
383
|
+
// the ring to them: the trunk's width is the real spend rate.
|
|
384
|
+
// civic the nine buildings, each now a core: the Lead's floor, then a
|
|
385
|
+
// capsule floor per committee room and built floor plug into a shaft on the
|
|
386
|
+
// roof, so structure is height. Ink windows say how busy it is;
|
|
387
|
+
// SALMON windows, on the floor whose seat the ask waits on, are
|
|
388
|
+
// the only salmon: questions waiting on the viewer. A build-out
|
|
389
|
+
// the form of government allows is a live site; a proposal waiting
|
|
390
|
+
// on a vote is a parked crane, a dashed-glass ghost annex and the
|
|
391
|
+
// council's tally arc; an open sitting is a light shaft.
|
|
392
|
+
// sky one chrome pod per active claim on a lane from the building its
|
|
393
|
+
// work answers to to its goal's district, at fixed spacing so the
|
|
394
|
+
// count reads; a dry pot's reserve-borrow beacon.
|
|
395
|
+
// Every mark is drawn from a real read (task 1004717: the prototype overlay
|
|
396
|
+
// is gone). Motion is the page's CSS: this file only names
|
|
397
|
+
// the parts that move (`cls`) and where their still frame stands (`style`).
|
|
398
|
+
const FLOOR_H = 13;
|
|
399
|
+
const BUILD_FLOORS = 3;
|
|
400
|
+
// [where the shaft starts, where the first capsule sits] on each recipe's roof
|
|
401
|
+
const CORE = Object.freeze({ charter: [144, 158], rules: [36, 36], treasury: [66, 66], planning: [38, 51], people: [0, 31], release: [56, 72], disputes: [48, 60], automation: [36, 44], review: [53, 53] });
|
|
402
|
+
// the main mass whose two seen faces carry the windows: x0, y0, x1, y1 on the slot, z0, z1
|
|
403
|
+
const BODY = Object.freeze({
|
|
404
|
+
charter: [0.3, 0.3, 1.7, 1.7, 6, 52], rules: [0.22, 0.62, 1.78, 1.38, 4, 32], treasury: [0.4, 0.4, 1.6, 1.6, 22, 52],
|
|
405
|
+
planning: [0.3, 0.3, 1.7, 1.7, 14, 38], people: [0.35, 1.1, 1.0, 1.7, 7, 14], release: [1.25, 0.75, 1.7, 1.25, 0, 66],
|
|
406
|
+
disputes: [0.75, 0.55, 1.25, 1.45, 14, 48], automation: [0.35, 0.4, 1.65, 1.5, 20, 36], review: [0.38, 0.38, 1.62, 1.62, 14, 50],
|
|
407
|
+
});
|
|
408
|
+
// where a crane's mast stands on the slot, and the z it stands on
|
|
409
|
+
const MAST = Object.freeze({ release: [1.5, 1.0, 66], treasury: [1.55, 1.0, 57] });
|
|
410
|
+
const LANE_PERIOD = 26; // seconds, the same on every lane: speed carries no data
|
|
411
|
+
const POD_GAP = 0.07; // the fixed spacing between pods on one lane, as a share of it
|
|
412
|
+
const MAX_LANE_PODS = 8; // a lane's convoy fits its spacing; past it the lane says "+N"
|
|
413
|
+
const MAX_PODS = 64; // the city's whole sky stays inside the moving-node budget (~100)
|
|
414
|
+
const AGE_FULL_DAYS = 14; // an age ring is full when the oldest ask on you is two weeks old
|
|
415
|
+
// The far, middle and near depth bands. Even the near band is hazed a little, so the civic
|
|
416
|
+
// quarter (never hazed) is the crispest thing in the city: the districts are its skyline.
|
|
417
|
+
const HAZE = Object.freeze([0.62, 0.48, 0.34]);
|
|
418
|
+
const ACTIVE_HAZE = 0.4; // a district with an agent at work keeps this share of its band's haze
|
|
419
|
+
const QUIET_HAZE = 0.3; // a civic building with no agent and no ask sits darker
|
|
420
|
+
// the haze the far bands mix toward: the void in dark, and on paper a cool sea-tinted
|
|
421
|
+
// paper, so distance reads as atmosphere (fainter, cooler, lower contrast) in both modes
|
|
422
|
+
const VOID = Object.freeze({ dark: '#000000', light: '#cbd3e4' }); // light: a cooler, deeper sea so the haze reads on paper (task 1004718)
|
|
423
|
+
|
|
424
|
+
const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
|
|
425
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
426
|
+
|
|
427
|
+
// ---------- haze: a tone mixed toward the void (drawing data, like isoTheme) ----------
|
|
428
|
+
function parseColor(c) {
|
|
429
|
+
let m = /^#([0-9a-f]{6})$/i.exec(String(c));
|
|
430
|
+
if (m) { const n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]; }
|
|
431
|
+
m = /^rgba?\(([^)]+)\)$/i.exec(String(c));
|
|
432
|
+
if (m) { const p = m[1].split(',').map((x) => Number(x.trim())); return [p[0], p[1], p[2], p.length > 3 ? p[3] : 1]; }
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const hex2 = (n) => ('0' + Math.max(0, Math.min(255, Math.round(n))).toString(16)).slice(-2);
|
|
436
|
+
// A solid face mixes toward the ground; a line or a glaze (alpha) thins out.
|
|
437
|
+
function mixToward(c, target, t) {
|
|
438
|
+
const a = parseColor(c), b = parseColor(target);
|
|
439
|
+
if (!a || !b || !(t > 0)) return c;
|
|
440
|
+
if (a[3] < 1) return 'rgba(' + a[0] + ',' + a[1] + ',' + a[2] + ',' + Math.round(a[3] * (1 - t) * 1000) / 1000 + ')';
|
|
441
|
+
return '#' + hex2(a[0] + (b[0] - a[0]) * t) + hex2(a[1] + (b[1] - a[1]) * t) + hex2(a[2] + (b[2] - a[2]) * t);
|
|
442
|
+
}
|
|
443
|
+
function hazeTheme(I, t, mode) {
|
|
444
|
+
if (!(t > 0)) return I;
|
|
445
|
+
const to = VOID[mode] || VOID.dark;
|
|
446
|
+
const out = {};
|
|
447
|
+
Object.keys(I).forEach((key) => {
|
|
448
|
+
const v = I[key];
|
|
449
|
+
if (v && typeof v === 'object') {
|
|
450
|
+
const o = {};
|
|
451
|
+
Object.keys(v).forEach((f) => { o[f] = f === 'dash' ? v[f] : mixToward(v[f], to, t); });
|
|
452
|
+
out[key] = o;
|
|
453
|
+
} else out[key] = typeof v === 'string' ? mixToward(v, to, t) : v;
|
|
454
|
+
});
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
// A district's band from its plot's centre: far (back corner), middle, near.
|
|
458
|
+
function hazeBand(lot) {
|
|
459
|
+
const d = (lot.x0 + lot.x1 + 1) / 2 + (lot.y0 + lot.y1 + 1) / 2;
|
|
460
|
+
return d < 8 ? 0 : d < 18 ? 1 : 2;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------- who and what stands in a building ----------
|
|
464
|
+
// A building's floors, bottom up: the Lead's (GET /government/leads, task 1004709),
|
|
465
|
+
// then one per committee room and one per floor, annex or district that stands
|
|
466
|
+
// built (its `structure`, task 1004711).
|
|
467
|
+
function structureFloors(b) {
|
|
468
|
+
const out = [];
|
|
469
|
+
if (!b) return out;
|
|
470
|
+
if (b.lead) out.push({ kind: 'lead', seat: 'lead:' + b.key, name: String(b.lead.name || 'Lead') });
|
|
471
|
+
((b.structure && b.structure.committees) || []).forEach((c) => out.push({ kind: 'committee', seat: 'committee:' + b.key + '.' + c.key, name: String(c.name || c.key) }));
|
|
472
|
+
((b.structure && b.structure.built) || []).forEach((x) => out.push({ kind: String(x.capsule || 'floor'), seat: null, name: String(x.name || 'Floor') }));
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
// A building's asks, less the ones the viewer has answered on this page. On you
|
|
476
|
+
// and on others are gov-living.js's own predicates (countsOf counts with them).
|
|
477
|
+
const onYouRule = (a) => (Living ? Living.isOnYou(a) : a.state === 'needs' && !!a.needs_you);
|
|
478
|
+
const onOthersRule = (a) => (Living ? Living.isOnOthers(a) : a.state === 'needs' && !a.needs_you);
|
|
479
|
+
function asksAt(b, answered) {
|
|
480
|
+
const gone = answered || {};
|
|
481
|
+
const all = (b && Array.isArray(b.asks) ? b.asks : []).filter((a) => a && !gone[a.id]);
|
|
482
|
+
const open = all.filter((a) => a.state !== 'settled');
|
|
483
|
+
return { all: all, open: open, onYou: all.filter(onYouRule), others: all.filter(onOthersRule) };
|
|
484
|
+
}
|
|
485
|
+
function isQuiet(b, v) { return !!b && !(b.agents > 0) && v.open.length === 0; }
|
|
486
|
+
|
|
487
|
+
// ---------- windows ----------
|
|
488
|
+
// Window cells on the two faces of a box the viewer sees (y = y1, then x = x1), rows bottom up.
|
|
489
|
+
function windowCells(x0, y0, x1, y1, z0, z1) {
|
|
490
|
+
const cells = [];
|
|
491
|
+
const rows = Math.max(1, Math.floor((z1 - z0 - 3) / 9));
|
|
492
|
+
const nx = Math.max(2, Math.round((x1 - x0) / 0.28)), ny = Math.max(2, Math.round((y1 - y0) / 0.28));
|
|
493
|
+
for (let j = 0; j < rows; j++) {
|
|
494
|
+
const z = z0 + 3 + j * 9;
|
|
495
|
+
for (let i = 0; i < nx; i++) cells.push({ face: 'y', u: x0 + (x1 - x0) * (i + 0.5) / nx, v: y1, z: z });
|
|
496
|
+
for (let i = 0; i < ny; i++) cells.push({ face: 'x', u: y0 + (y1 - y0) * (i + 0.5) / ny, v: x1, z: z });
|
|
497
|
+
}
|
|
498
|
+
return cells;
|
|
499
|
+
}
|
|
500
|
+
function windowD(k, c) {
|
|
501
|
+
const P = k.P, hw = 0.045, h = 3.5;
|
|
502
|
+
return c.face === 'y'
|
|
503
|
+
? k.poly([P(c.u - hw, c.v, c.z), P(c.u + hw, c.v, c.z), P(c.u + hw, c.v, c.z + h), P(c.u - hw, c.v, c.z + h)])
|
|
504
|
+
: k.poly([P(c.v, c.u - hw, c.z), P(c.v, c.u + hw, c.z), P(c.v, c.u + hw, c.z + h), P(c.v, c.u - hw, c.z + h)]);
|
|
505
|
+
}
|
|
506
|
+
// A capsule's round window: an octagon laid on the face (y = v, or x = v).
|
|
507
|
+
function porthole(k, face, u, v, z) {
|
|
508
|
+
const P = k.P, pts = [];
|
|
509
|
+
for (let i = 0; i < 8; i++) {
|
|
510
|
+
const a = Math.PI * 2 * i / 8, du = Math.cos(a) * 0.06, dz = Math.sin(a) * 2.6;
|
|
511
|
+
pts.push(face === 'y' ? P(u + du, v, z + dz) : P(v, u + du, z + dz));
|
|
512
|
+
}
|
|
513
|
+
return k.poly(pts);
|
|
514
|
+
}
|
|
515
|
+
function cls(out, d, c) { if (d) out.push({ d: d, f: 'none', s: 'none', dash: 'none', w: 1, cls: c }); }
|
|
516
|
+
|
|
517
|
+
// ---------- a crane ----------
|
|
518
|
+
// The mast, then the jib (with its ties, counterweight, hook and load) as its own list so a
|
|
519
|
+
// live site can swing it about the mast head. `parked` folds the jib along the other axis.
|
|
520
|
+
function crane(k, mx, my, z0, zTop, parked) {
|
|
521
|
+
const P = k.P, I = k.I, mast = [], jib = [];
|
|
522
|
+
let d = k.seg(P(mx, my, z0), P(mx, my, zTop + 6)) + k.seg(P(mx + 0.05, my + 0.05, z0), P(mx + 0.05, my + 0.05, zTop + 6));
|
|
523
|
+
for (let z = z0 + 6; z < zTop; z += 10) d += k.seg(P(mx, my, z), P(mx + 0.05, my + 0.05, z + 5));
|
|
524
|
+
push(mast, d, 'none', I.vacantLine);
|
|
525
|
+
const end = parked ? (dx) => P(mx, my - dx, zTop) : (dx) => P(mx - dx, my, zTop);
|
|
526
|
+
const at = parked ? (dx, z) => P(mx, my - dx, z) : (dx, z) => P(mx - dx, my, z);
|
|
527
|
+
const peak = P(mx, my, zTop + 12);
|
|
528
|
+
push(jib, k.seg(end(-0.3), end(1.0)) + k.seg(peak, end(1.0)) + k.seg(peak, end(-0.3)), 'none', I.vacantLine);
|
|
529
|
+
const hookAt = parked ? 0.35 : 0.7, drop = parked ? 6 : 18;
|
|
530
|
+
push(jib, k.seg(at(hookAt, zTop), at(hookAt, zTop - drop)), 'none', I.vacantLine);
|
|
531
|
+
const hx = parked ? mx : mx - hookAt, hy = parked ? my - hookAt : my;
|
|
532
|
+
isoBox(jib, k, hx - 0.06, hy - 0.06, hx + 0.06, hy + 0.06, zTop - drop - 6, 6, I.crate);
|
|
533
|
+
const cw = parked ? [mx - 0.06, my + 0.18, mx + 0.06, my + 0.3] : [mx + 0.18, my - 0.06, mx + 0.3, my + 0.06];
|
|
534
|
+
isoBox(jib, k, cw[0], cw[1], cw[2], cw[3], zTop - 5, 5, I.plinth);
|
|
535
|
+
const pivot = P(mx, my, zTop);
|
|
536
|
+
return { mast: mast, jib: jib, pivot: [r1(pivot[0]), r1(pivot[1])] };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---------- one civic building, alive ----------
|
|
540
|
+
// Draws the core, the capsule floors, the windows and the construction of one building into
|
|
541
|
+
// `polys` (static) and `parts` (ordered; a part with `cls` moves), and says where its top is.
|
|
542
|
+
function civicLiving(polys, k, slot, b, v, living) {
|
|
543
|
+
const key = slot[0], ox = slot[1], oy = slot[2], cx = ox + 1, cy = oy + 1, I = k.I, P = k.P;
|
|
544
|
+
const parts = [];
|
|
545
|
+
const floors = structureFloors(b);
|
|
546
|
+
const held = floors.map((f) => (f.seat ? v.onYou.filter((a) => a.waits_on && a.waits_on.seat === f.seat).length : 0));
|
|
547
|
+
const bodyYou = v.onYou.length - held.reduce((n, x) => n + x, 0);
|
|
548
|
+
|
|
549
|
+
// the windows of the main mass: salmon for the asks it holds, then ink for how busy it is
|
|
550
|
+
const bx = BODY[key];
|
|
551
|
+
const cells = windowCells(ox + bx[0], oy + bx[1], ox + bx[2], oy + bx[3], bx[4], bx[5])
|
|
552
|
+
.map((c, i) => ({ c: c, r: rnd(i * 7 + ox * 31 + oy * 17 + 3) }))
|
|
553
|
+
.sort((a, z) => a.r - z.r)
|
|
554
|
+
.map((x) => x.c);
|
|
555
|
+
const nYou = Math.min(cells.length, bodyYou * 2);
|
|
556
|
+
const nLit = Math.min(cells.length - nYou, Math.round(clamp01(b && b.lit) * cells.length * 0.6));
|
|
557
|
+
// what needs you lights the TOP rows first (still scattered within a row), so a
|
|
558
|
+
// neighbour in front never hides it (task 1004718); ink windows take the rest
|
|
559
|
+
const you = cells.slice().sort((a, z) => z.z - a.z).slice(0, nYou);
|
|
560
|
+
const rest = cells.filter((c) => you.indexOf(c) < 0);
|
|
561
|
+
cls(polys, you.map((c) => windowD(k, c)).join(''), 'city-win city-win--you');
|
|
562
|
+
cls(polys, rest.slice(0, nLit).map((c) => windowD(k, c)).join(''), 'city-win city-win--lit');
|
|
563
|
+
|
|
564
|
+
// the core and its capsule floors (the Lead's first), each plugging in on the world's arrive curve
|
|
565
|
+
const start = CORE[key][1];
|
|
566
|
+
let top = ROOF[key];
|
|
567
|
+
if (floors.length) {
|
|
568
|
+
const coreTop = start + floors.length * FLOOR_H + 3;
|
|
569
|
+
isoBox(polys, k, cx - 0.13, cy - 0.13, cx + 0.13, cy + 0.13, CORE[key][0], coreTop - CORE[key][0], I.roof, { fins: true, n: 2 });
|
|
570
|
+
floors.forEach((f, i) => {
|
|
571
|
+
const z0 = start + i * FLOOR_H + 1, h = FLOOR_H - 2, zc = z0 + h / 2;
|
|
572
|
+
const ySide = i % 2 === 0;
|
|
573
|
+
const box = ySide ? [cx - 0.25, cy + 0.13, cx + 0.25, cy + 0.47] : [cx + 0.13, cy - 0.25, cx + 0.47, cy + 0.25];
|
|
574
|
+
const shell = [];
|
|
575
|
+
isoBox(shell, k, box[0], box[1], box[2], box[3], z0, h, I.civic);
|
|
576
|
+
parts.push({ cls: 'city-plug', style: { '--i': String(i) }, polys: shell, floor: f.kind });
|
|
577
|
+
const win = ySide
|
|
578
|
+
? porthole(k, 'y', cx - 0.12, box[3], zc) + porthole(k, 'y', cx + 0.12, box[3], zc) + porthole(k, 'x', cy + 0.3, box[2], zc)
|
|
579
|
+
: porthole(k, 'x', cy - 0.12, box[2], zc) + porthole(k, 'x', cy + 0.12, box[2], zc) + porthole(k, 'y', cx + 0.3, box[3], zc);
|
|
580
|
+
const lit = held[i] > 0 ? 'city-win city-win--you' : (b && b.lit > 0.3 ? 'city-win city-win--lit' : 'city-win');
|
|
581
|
+
const pane = [{ d: win, f: I.door, s: 'none', dash: 'none', w: 1, cls: lit }];
|
|
582
|
+
parts.push({ cls: 'city-plug city-plug--detail', style: { '--i': String(i) }, polys: pane, floor: f.kind });
|
|
583
|
+
});
|
|
584
|
+
const cap = [];
|
|
585
|
+
isoBox(cap, k, cx - 0.17, cy - 0.17, cx + 0.17, cy + 0.17, coreTop, 2, I.roof);
|
|
586
|
+
parts.push({ cls: 'city-plug', style: { '--i': String(floors.length) }, polys: cap });
|
|
587
|
+
top = coreTop + 2;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// construction: an allowed build-out is a live site; a proposal waits as a ghost (real from task 1004711)
|
|
591
|
+
const list = (b && b.construction) || [];
|
|
592
|
+
const live = list.find((c) => c.authorised);
|
|
593
|
+
const pending = live ? null : list.find((c) => !c.authorised);
|
|
594
|
+
const total = BUILD_FLOORS * FLOOR_H;
|
|
595
|
+
const fp = [cx - 0.3, cy - 0.3, cx + 0.3, cy + 0.3];
|
|
596
|
+
const m = MAST[key] || [1.55, 1.0, start];
|
|
597
|
+
let site = null, shaft = null, tally = null;
|
|
598
|
+
if (live) {
|
|
599
|
+
const progress = clamp01(live.progress);
|
|
600
|
+
const built = Math.min(BUILD_FLOORS, Math.floor(progress * BUILD_FLOORS + 1e-9));
|
|
601
|
+
const frac = Math.round((progress * BUILD_FLOORS - built) * 1000) / 1000;
|
|
602
|
+
const solid = [];
|
|
603
|
+
for (let i = 0; i < built; i++) isoBox(solid, k, fp[0], fp[1], fp[2], fp[3], top + i * FLOOR_H, FLOOR_H - 1, I.civic, { rows: true });
|
|
604
|
+
if (solid.length) parts.push({ cls: null, polys: solid });
|
|
605
|
+
if (built < BUILD_FLOORS) {
|
|
606
|
+
const rising = [];
|
|
607
|
+
isoBox(rising, k, fp[0], fp[1], fp[2], fp[3], top + built * FLOOR_H, FLOOR_H - 1, I.civic);
|
|
608
|
+
parts.push({ cls: 'city-rise', style: { '--rise': String(frac) }, polys: rising });
|
|
609
|
+
}
|
|
610
|
+
const frame = [];
|
|
611
|
+
isoBox(frame, k, fp[0] - 0.04, fp[1] - 0.04, fp[2] + 0.04, fp[3] + 0.04, top, total, I.scaffold, { fins: true, n: 3 });
|
|
612
|
+
parts.push({ cls: 'city-scaffold', polys: frame });
|
|
613
|
+
const cage = [];
|
|
614
|
+
isoBox(cage, k, fp[2] - 0.02, fp[3] - 0.16, fp[2] + 0.1, fp[3] - 0.02, top, 6, I.crate);
|
|
615
|
+
const lift = r1((total - 6) * k.S);
|
|
616
|
+
parts.push({ cls: 'city-climb', style: { '--climb': -lift + 'px', '--climb-at': -r1(progress * lift) + 'px' }, polys: cage });
|
|
617
|
+
const c = crane(k, ox + m[0], oy + m[1], m[2], top + total + 10, false);
|
|
618
|
+
parts.push({ cls: 'city-crane', polys: c.mast });
|
|
619
|
+
parts.push({ cls: 'city-jib', style: { '--pivot': c.pivot[0] + 'px ' + c.pivot[1] + 'px' }, polys: c.jib });
|
|
620
|
+
site = { kind: 'live', id: live.id, label: live.label, progress: progress, built: built, floors: BUILD_FLOORS };
|
|
621
|
+
top = top + total + 22;
|
|
622
|
+
} else if (pending) {
|
|
623
|
+
const ghost = [];
|
|
624
|
+
isoBox(ghost, k, fp[0], fp[1], fp[2], fp[3], top, total, Object.assign({}, I.glass, { dash: '4 3' }), { fins: true, n: 3 });
|
|
625
|
+
parts.push({ cls: 'city-ghost', polys: ghost });
|
|
626
|
+
const c = crane(k, ox + m[0], oy + m[1], m[2], top + total + 6, true);
|
|
627
|
+
parts.push({ cls: 'city-crane city-crane--parked', polys: c.mast.concat(c.jib) });
|
|
628
|
+
// the council's tally: one dot per seat on an arc over the ghost, filled as they vote
|
|
629
|
+
// (before its sitting's tally is read, one open dot per holder: nobody has voted)
|
|
630
|
+
const ask = v.all.find((a) => a.id === pending.id || a.id === pending.ask_id) || null;
|
|
631
|
+
const t = ask && ask.tally ? ask.tally : null;
|
|
632
|
+
const holders = (living.byKey.charter && living.byKey.charter.seats || []).filter((s) => s.role === 'holder').length;
|
|
633
|
+
const seats = t ? Math.max(1, (Number(t.for) || 0) + (Number(t.against) || 0) + (Number(t.open) || 0)) : Math.max(3, holders);
|
|
634
|
+
const yes = t ? Number(t.for) || 0 : 0, no = t ? Number(t.against) || 0 : 0;
|
|
635
|
+
const centre = P(cx, cy, top + total + 4);
|
|
636
|
+
const arc = { yes: '', no: '', open: '' };
|
|
637
|
+
for (let i = 0; i < seats; i++) {
|
|
638
|
+
const a = Math.PI * (1.12 + 0.76 * (seats === 1 ? 0.5 : i / (seats - 1)));
|
|
639
|
+
const d = ellipse(r1(centre[0] + Math.cos(a) * 22), r1(centre[1] + Math.sin(a) * 14), 2.4, 2.4);
|
|
640
|
+
arc[i < yes ? 'yes' : i < yes + no ? 'no' : 'open'] += d;
|
|
641
|
+
}
|
|
642
|
+
const tl = [];
|
|
643
|
+
cls(tl, arc.yes, 'city-tally city-tally--yes');
|
|
644
|
+
cls(tl, arc.no, 'city-tally city-tally--no');
|
|
645
|
+
cls(tl, arc.open, 'city-tally city-tally--open');
|
|
646
|
+
parts.push({ cls: null, polys: tl });
|
|
647
|
+
tally = { seats: seats, yes: yes, no: no };
|
|
648
|
+
site = { kind: 'pending', id: pending.id, label: pending.label, needs: pending.needs || 'a vote', ask_id: ask ? ask.id : null };
|
|
649
|
+
// an open sitting: a light shaft from the ghost's roof
|
|
650
|
+
if (!ask || ask.state !== 'settled') {
|
|
651
|
+
const s0 = P(cx, cy, top + total);
|
|
652
|
+
shaft = { key: 'shaft:' + key, building: key, x: r1(s0[0]), y: r1(s0[1]), h: 150 };
|
|
653
|
+
}
|
|
654
|
+
top = top + total + 26;
|
|
655
|
+
}
|
|
656
|
+
return { parts: parts, top: top, floors: floors, held: held, bodyYou: bodyYou, windows: { cells: cells.length, you: nYou, lit: nLit }, site: site, tally: tally, shaft: shaft };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ---------- the sky: one pod per agent ----------
|
|
660
|
+
function skyLanes(k, traffic, districts, civicTop) {
|
|
661
|
+
const P = k.P, lanes = [], byKey = {}, lotOf = {};
|
|
662
|
+
districts.forEach((D) => { lotOf[D.goal_id] = D.lot; });
|
|
663
|
+
const slot = {};
|
|
664
|
+
CIVIC_SLOTS.forEach((s) => { slot[s[0]] = s; });
|
|
665
|
+
((traffic && traffic.claims) || []).forEach((c) => {
|
|
666
|
+
if (!c || !slot[c.building]) return;
|
|
667
|
+
const goal = c.goal_id != null && lotOf[String(c.goal_id)] ? String(c.goal_id) : null;
|
|
668
|
+
const key = c.building + '>' + (goal || 'here');
|
|
669
|
+
if (!byKey[key]) { byKey[key] = { key: key, building: c.building, goal_id: goal, claims: [] }; lanes.push(byKey[key]); }
|
|
670
|
+
byKey[key].claims.push(c);
|
|
671
|
+
});
|
|
672
|
+
lanes.forEach((L) => {
|
|
673
|
+
const s = slot[L.building];
|
|
674
|
+
const a = P(s[1] + 1, s[2] + 1, civicTop[L.building] + 18);
|
|
675
|
+
const lot = L.goal_id ? lotOf[L.goal_id] : null;
|
|
676
|
+
if (lot) {
|
|
677
|
+
const b = P((lot.x0 + lot.x1 + 1) / 2, (lot.y0 + lot.y1 + 1) / 2, 96);
|
|
678
|
+
const c = [(a[0] + b[0]) / 2, Math.min(a[1], b[1]) - 70];
|
|
679
|
+
L.d = 'M' + r1(a[0]) + ' ' + r1(a[1]) + 'Q' + r1(c[0]) + ' ' + r1(c[1]) + ' ' + r1(b[0]) + ' ' + r1(b[1]);
|
|
680
|
+
L.orbit = false;
|
|
681
|
+
} else {
|
|
682
|
+
// work with no district circles over its own building
|
|
683
|
+
L.d = 'M' + r1(a[0] - 26) + ' ' + r1(a[1]) + 'a26 9 0 1 0 52 0a26 9 0 1 0 -52 0';
|
|
684
|
+
L.orbit = true;
|
|
685
|
+
}
|
|
686
|
+
L.pods = L.claims.slice(0, MAX_LANE_PODS).map((c, i) => ({
|
|
687
|
+
key: 'pod:' + String(c.claim_id || c.task_id || i), task_id: c.task_id != null ? String(c.task_id) : null,
|
|
688
|
+
at: r1(28 + i * POD_GAP * 100), delay: -r1(LANE_PERIOD * (0.18 + i * POD_GAP)), dur: LANE_PERIOD,
|
|
689
|
+
}));
|
|
690
|
+
L.extra = L.claims.length - L.pods.length;
|
|
691
|
+
});
|
|
692
|
+
// past the moving-node budget the lanes keep their first pods and say how many more
|
|
693
|
+
let budget = MAX_PODS;
|
|
694
|
+
lanes.forEach((L) => {
|
|
695
|
+
const keep = Math.max(0, Math.min(L.pods.length, budget));
|
|
696
|
+
budget -= keep;
|
|
697
|
+
L.extra += L.pods.length - keep;
|
|
698
|
+
L.pods = L.pods.slice(0, keep);
|
|
699
|
+
const end = L.orbit ? null : /([-\d.]+) ([-\d.]+)$/.exec(L.d);
|
|
700
|
+
L.more = L.extra > 0 ? { n: L.extra, x: end ? Number(end[1]) : null, y: end ? Number(end[2]) : null } : null;
|
|
701
|
+
});
|
|
702
|
+
return lanes;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ---------- the ground: the spend conduit ----------
|
|
706
|
+
// The trunk runs from the Treasury onto the ring, round it; its width is the real spend rate
|
|
707
|
+
// (this month's spend over the days so far). A spur feeds each district its pot's share
|
|
708
|
+
// (GET /government/treasury, task 1004712); a dry pot's spur is
|
|
709
|
+
// starved and its district raises a beacon over its borrow from the reserve.
|
|
710
|
+
const RING = Object.freeze([4.2, 11.8]);
|
|
711
|
+
function spendRate(spend, now) {
|
|
712
|
+
if (!spend) return null;
|
|
713
|
+
const day = Number(now) > 0 ? new Date(Number(now)).getUTCDate() : null;
|
|
714
|
+
if (spend.month_usd != null && day) return Math.round(Number(spend.month_usd) / day * 100) / 100;
|
|
715
|
+
return spend.today_usd != null ? Number(spend.today_usd) : null;
|
|
716
|
+
}
|
|
717
|
+
function trunkWidth(rate) { return rate > 0 ? r1(Math.max(1.5, Math.min(8, Math.sqrt(rate) / 5))) : 0; }
|
|
718
|
+
function conduits(k, living, districts, now) {
|
|
719
|
+
const P = k.P, flat = [], beacons = [];
|
|
720
|
+
const spend = living.city && living.city.spend;
|
|
721
|
+
const rate = spendRate(spend, now);
|
|
722
|
+
const w = trunkWidth(rate);
|
|
723
|
+
const route = (pts) => pts.map((p, i) => { const q = P(p[0], p[1]); return (i ? 'L' : 'M') + r1(q[0]) + ' ' + r1(q[1]); }).join('');
|
|
724
|
+
if (w > 0) {
|
|
725
|
+
flat.push({ d: route([[RING[0], RING[0]], [RING[1], RING[0]], [RING[1], RING[1]], [RING[0], RING[1]]]) + 'Z', f: 'none', s: 'none', dash: 'none', w: w, cls: 'city-conduit', key: 'trunk' });
|
|
726
|
+
flat.push({ d: route([[10.85, 6], [RING[1], 6]]), f: 'none', s: 'none', dash: 'none', w: w, cls: 'city-conduit', key: 'treasury' });
|
|
727
|
+
}
|
|
728
|
+
const byGoal = {};
|
|
729
|
+
(living.districts || []).forEach((d) => { byGoal[d.goal_id] = d; });
|
|
730
|
+
const spent = districts.reduce((n, D) => { const p = byGoal[D.goal_id] && byGoal[D.goal_id].pot; return n + (p && p.spent_usd > 0 ? p.spent_usd : 0); }, 0);
|
|
731
|
+
const spurs = [];
|
|
732
|
+
districts.forEach((D) => {
|
|
733
|
+
const ld = byGoal[D.goal_id];
|
|
734
|
+
const pot = ld && ld.pot;
|
|
735
|
+
if (!pot || !(w > 0)) return;
|
|
736
|
+
const L = D.lot, cx = (L.x0 + L.x1 + 1) / 2, cy = (L.y0 + L.y1 + 1) / 2;
|
|
737
|
+
const ex = Math.max(RING[0], Math.min(RING[1], cx)), ey = Math.max(RING[0], Math.min(RING[1], cy));
|
|
738
|
+
const pts = ex !== cx && ey !== cy ? [[ex, ey], [ex, cy], [cx, cy]] : [[ex, ey], [cx, cy]];
|
|
739
|
+
const share = spent > 0 ? Math.max(0, pot.spent_usd) / spent : 0;
|
|
740
|
+
const sw = pot.dry ? 1.2 : r1(Math.max(0.75, w * Math.sqrt(share)));
|
|
741
|
+
flat.push({ d: route(pts), f: 'none', s: 'none', dash: pot.dry ? '2 3' : 'none', w: sw, cls: pot.dry ? 'city-conduit city-conduit--dry' : 'city-conduit', key: 'spur:' + D.goal_id });
|
|
742
|
+
spurs.push({ goal_id: D.goal_id, width: sw, share: Math.round(share * 1000) / 1000, dry: !!pot.dry });
|
|
743
|
+
if (pot.dry) {
|
|
744
|
+
// the reserve-borrow beacon, in the sky over the district
|
|
745
|
+
const foot = P(cx, cy, 70), head = P(cx, cy, 128);
|
|
746
|
+
const d = k.seg(foot, head);
|
|
747
|
+
const dia = 'M' + r1(head[0]) + ' ' + r1(head[1] - 5) + 'L' + r1(head[0] + 4) + ' ' + r1(head[1]) + 'L' + r1(head[0]) + ' ' + r1(head[1] + 5) + 'L' + r1(head[0] - 4) + ' ' + r1(head[1]) + 'Z';
|
|
748
|
+
beacons.push({ d: d, f: 'none', s: 'none', dash: '1 3', w: 1, cls: 'city-beacon city-beacon--stem' });
|
|
749
|
+
beacons.push({ d: dia, f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-beacon' });
|
|
750
|
+
beacons.push({ d: ellipse(r1(head[0]), r1(head[1]), 9, 4.5), f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-beacon city-beacon--ring' });
|
|
751
|
+
D.beacon = { goal_id: D.goal_id, ask_id: pot.borrow_ask_id || null, x: r1(head[0]), y: r1(head[1]) };
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
return { flat: flat, beacons: beacons, rate: rate, width: w, spurs: spurs };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ---------- the pins, read from the living asks ----------
|
|
758
|
+
// Salmon (urgent, needs) only for asks that wait on the viewer's own seat; others deciding is
|
|
759
|
+
// ink (moving); a building whose only matter is settled is a grey dot. The age ring is the
|
|
760
|
+
// oldest ask on you, full at two weeks. Every pin is a real matter.
|
|
761
|
+
function livingPins(k, living, answered, civicTop) {
|
|
762
|
+
const P = k.P, pins = [];
|
|
763
|
+
let n = 0;
|
|
764
|
+
CIVIC_SLOTS.forEach((s) => {
|
|
765
|
+
const b = living.byKey[s[0]];
|
|
766
|
+
const v = asksAt(b, answered);
|
|
767
|
+
if (!v.all.length) return;
|
|
768
|
+
let tier, list;
|
|
769
|
+
if (v.onYou.length) { tier = v.onYou.some((a) => a.urgent) ? 'urgent' : 'needs'; list = v.onYou; } else if (v.open.length) { tier = 'moving'; list = v.open; } else { tier = 'settled'; list = v.all; }
|
|
770
|
+
const m = list[0];
|
|
771
|
+
const top = P(s[1] + 1, s[2] + 1, civicTop[s[0]] + 8);
|
|
772
|
+
const g = glyph(m.rule, 7, 7, 13);
|
|
773
|
+
const ages = v.onYou.map((a) => a.age_days).filter((x) => x != null);
|
|
774
|
+
const oldest = ages.length ? Math.max.apply(null, ages) : null;
|
|
775
|
+
const title = String((m.shown && m.shown.question) || (m.raw && m.raw.title) || '');
|
|
776
|
+
const name = (b && b.name) || s[0];
|
|
777
|
+
const aria = tier === 'settled' ? name + ': settled. ' + title
|
|
778
|
+
: v.onYou.length ? name + ': ' + v.onYou.length + ' ' + words('on_you', v.onYou.length) + (oldest != null ? ', the oldest ' + oldest + (oldest === 1 ? ' day' : ' days') : '') + '. ' + title
|
|
779
|
+
: v.others.length ? name + ': ' + v.others.length + ' ' + words('on_others', v.others.length) + '. ' + title
|
|
780
|
+
: name + ': ' + v.open.length + ' moving. ' + title;
|
|
781
|
+
pins.push({
|
|
782
|
+
key: String(m.id), building: s[0], state: tier, count: tier === 'settled' ? 0 : list.length, matters: list.map((a) => String(a.id)),
|
|
783
|
+
title: title, aria: aria, x: r1(top[0] - 15), y: r1(top[1] - 52), h: 52, gf: g.fill, gl: g.line, delay: -(n * 0.9),
|
|
784
|
+
age: tier === 'urgent' || tier === 'needs' ? (oldest != null ? Math.round(clamp01(oldest / AGE_FULL_DAYS) * 1000) / 1000 : 0) : null,
|
|
785
|
+
oldest_days: oldest,
|
|
786
|
+
});
|
|
787
|
+
n += 1;
|
|
788
|
+
});
|
|
789
|
+
return pins;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ---------- the Lead plates, placed so none meets another or a pin ----------
|
|
793
|
+
// Most important first (asks on you, then a Lead, then the owner's): beside the top of the
|
|
794
|
+
// building, right then left, then a step lower; a plate with nowhere clear waits for the
|
|
795
|
+
// card, which always names who holds the building.
|
|
796
|
+
const PLATE_H = 18;
|
|
797
|
+
const plateWidth = (text) => Math.round(String(text).length * 6.1 + 16);
|
|
798
|
+
function placePlates(civic, pins, k) {
|
|
799
|
+
const boxes = pins.map((p) => ({ x0: p.x - 2, y0: p.y - 4, x1: p.x + 32, y1: p.y + 26, own: null }));
|
|
800
|
+
// another building's build site or ghost annex is never covered: it is the news on that roof
|
|
801
|
+
civic.forEach((c) => {
|
|
802
|
+
if (!c.site) return;
|
|
803
|
+
const h = (BUILD_FLOORS * FLOOR_H + 26) * k.S;
|
|
804
|
+
boxes.push({ x0: c.top[0] - 40, y0: c.top[1] + 6, x1: c.top[0] + 44, y1: c.top[1] + h, own: c.key });
|
|
805
|
+
});
|
|
806
|
+
const rank = (c) => (c.on_you > 0 ? 0 : c.plate.lead ? 1 : 2);
|
|
807
|
+
civic.map((c, i) => ({ c: c, i: i })).sort((a, b) => rank(a.c) - rank(b.c) || a.i - b.i).forEach((e) => {
|
|
808
|
+
const c = e.c, w = plateWidth(c.plate.text), tx = c.top[0], ty = c.top[1];
|
|
809
|
+
const hit = (b) => boxes.some((o) => o.own !== c.key && b.x0 < o.x1 && b.x1 > o.x0 && b.y0 < o.y1 && b.y1 > o.y0);
|
|
810
|
+
const tries = [[tx + 12, ty - 2], [tx - 12 - w, ty - 2], [tx + 12, ty - 20], [tx - 12 - w, ty - 20], [tx + 12, ty + 16], [tx - 12 - w, ty + 16]];
|
|
811
|
+
const at = tries.map((t) => ({ x0: t[0], y0: t[1] - PLATE_H / 2, x1: t[0] + w, y1: t[1] + PLATE_H / 2, own: null })).find((b) => !hit(b));
|
|
812
|
+
c.plate.w = w;
|
|
813
|
+
if (!at) { c.plate.hidden = true; return; }
|
|
814
|
+
boxes.push(at);
|
|
815
|
+
c.plate.x = r1(at.x0);
|
|
816
|
+
c.plate.y = r1((at.y0 + at.y1) / 2);
|
|
817
|
+
c.plate.hidden = false;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ---------- the card a building opens in place ----------
|
|
822
|
+
const money =(n) => '$' + String(Math.round(Number(n) || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
823
|
+
function factFor(b, living) {
|
|
824
|
+
const r = ((b && b.readings) || []).find((x) => x && x.status === 'live' && x.figure);
|
|
825
|
+
if (r) return { text: String(r.figure), line: String(r.line || '') };
|
|
826
|
+
const fence = living.city && living.city.fence;
|
|
827
|
+
if (b && b.key === 'automation' && fence) return { text: fence.enabled ? 'running unattended' : 'unattended off', line: '' };
|
|
828
|
+
if (b && b.agents > 0) return { text: b.agents + ' ' + words('agents_at_work', b.agents), line: '' };
|
|
829
|
+
if (b && b.decision && b.decision.rule_words) return { text: String(b.decision.rule_words), line: '' };
|
|
830
|
+
return { text: 'quiet', line: '' };
|
|
831
|
+
}
|
|
832
|
+
// The card is a row and two doors: every ask is answered on the deck, whose taps
|
|
833
|
+
// take the matter's real route (task 1004717: the card's in-place answer, which
|
|
834
|
+
// sent nothing, is gone). `seated`: who leads each building is known (GET
|
|
835
|
+
// /government/leads, task 1004709); without that read the card names nobody as
|
|
836
|
+
// holding the building, with it the Lead, or the owner.
|
|
837
|
+
function cardFor(b, v, living, seated) {
|
|
838
|
+
const key = b.key;
|
|
839
|
+
return {
|
|
840
|
+
key: key, name: String(b.name || key), plate: !seated ? null : b.lead ? String(b.lead.name || 'Lead') : 'owner', lead: !!seated && !!b.lead,
|
|
841
|
+
fact: factFor(b, living), on_you: v.onYou.length, on_others: v.others.length, open: v.open.length, agents: Number(b.agents) || 0,
|
|
842
|
+
deck_href: STREET_HREF + '#deck=' + key, enter_href: STREET_HREF + '#building=' + key,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ---------- the gate plaque, the door and the line ----------
|
|
847
|
+
// The words are gov-living.js's COUNT_WORDS, the labels every surface prints.
|
|
848
|
+
const words = (key, n) => (Living ? Living.countWords(key, n) : { on_you: 'to do', on_others: n === 1 ? 'waits on others' : 'wait on others', agents_at_work: n === 1 ? 'agent working' : 'agents working' }[key]);
|
|
849
|
+
function plaqueFor(living, constitution, onYou) {
|
|
850
|
+
const ra = constitution && constitution.charter && constitution.charter.reads_as;
|
|
851
|
+
const laws = (living.city && living.city.laws) || [];
|
|
852
|
+
const month = living.city && living.city.spend ? living.city.spend.month_usd : null;
|
|
853
|
+
return [
|
|
854
|
+
ra && ra.name ? { key: 'form', figure: String(ra.name), words: 'government' } : null,
|
|
855
|
+
laws.length ? { key: 'laws', figure: String(laws.length), words: laws.length === 1 ? 'law' : 'laws' } : null,
|
|
856
|
+
onYou > 0 ? { key: 'you', figure: String(onYou), words: words('on_you', onYou) } : null,
|
|
857
|
+
month != null ? { key: 'spend', figure: money(month), words: 'this month' } : null,
|
|
858
|
+
].filter(Boolean);
|
|
859
|
+
}
|
|
860
|
+
function ctaFor(onYou) {
|
|
861
|
+
return onYou > 0
|
|
862
|
+
? { label: 'decide ' + onYou + ' in a row', href: STREET_HREF + '#deck', aria: 'Decide the ' + onYou + ' questions on you, one card at a time' }
|
|
863
|
+
: { label: 'start governing', href: STREET_HREF, aria: 'Start governing: enter the street view' };
|
|
864
|
+
}
|
|
865
|
+
function livingLine(onYou, others, agents) {
|
|
866
|
+
const parts = [onYou ? onYou + ' ' + words('on_you', onYou) : 'nothing to do'];
|
|
867
|
+
if (others) parts.push(others + ' ' + words('on_others', others));
|
|
868
|
+
if (agents) parts.push(agents + ' ' + words('agents_at_work', agents));
|
|
869
|
+
return parts.join(' · ');
|
|
870
|
+
}
|
|
871
|
+
|
|
367
872
|
// The whole home view as data.
|
|
368
873
|
// docket the GET /government/docket body, or null when it failed
|
|
369
874
|
// goals GET /goals goals[] (any status; only 'open' ones are drawn)
|
|
370
875
|
// rollups GET /goals/rollups rollups[] (goal_id → task counts)
|
|
371
876
|
// mode 'dark' | 'light'
|
|
372
877
|
// now ms since the epoch (a deadline inside a day is urgent)
|
|
878
|
+
// and, for the living city (task 1004685):
|
|
879
|
+
// living GovLiving.livingModel() over the same reads (gov-living.js)
|
|
880
|
+
// constitution GET /government/constitution (the plaque's form of government)
|
|
881
|
+
// answered ask ids the viewer answered on this page (they leave the counts)
|
|
373
882
|
function cityModel(input) {
|
|
374
883
|
const opts = input || {};
|
|
375
884
|
const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
|
|
376
885
|
const mode = opts.mode === 'light' ? 'light' : 'dark';
|
|
377
886
|
const I = isoTheme(mode);
|
|
378
887
|
const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
|
|
379
|
-
const
|
|
888
|
+
const living = opts.living && Array.isArray(opts.living.buildings) ? Object.assign({}, opts.living, { byKey: {} }) : null;
|
|
889
|
+
if (living) living.buildings.forEach((b) => { if (b && b.key) living.byKey[b.key] = b; });
|
|
890
|
+
const answered = {};
|
|
891
|
+
(Array.isArray(opts.answered) ? opts.answered : []).forEach((id) => { answered[String(id)] = true; });
|
|
892
|
+
const views = {};
|
|
893
|
+
if (living) CIVIC_SLOTS.forEach((s) => { views[s[0]] = asksAt(living.byKey[s[0]], answered); });
|
|
894
|
+
// the page's numbers are gov-living.js's countsOf (opts.counts when the page already
|
|
895
|
+
// holds them), the same numbers the street, the deck and the shell's tally print
|
|
896
|
+
const tally = living ? (opts.counts || (Living ? Living.countsOf(living, { answered: Object.keys(answered) }) : null)) : null;
|
|
897
|
+
const onYou = tally && tally.on_you != null ? tally.on_you : living ? CIVIC_SLOTS.reduce((n2, s) => n2 + views[s[0]].onYou.length, 0) : 0;
|
|
898
|
+
// the windows light for the evening when nothing waits on you (in the living city: on your seats)
|
|
899
|
+
const evening = !!docket && (living ? onYou === 0 : !(Number(counts.needs) > 0));
|
|
380
900
|
const k = isoKit(60, 30, 720, 176, 30 / 28, I, evening);
|
|
381
|
-
const P = k.P
|
|
901
|
+
const P = k.P;
|
|
902
|
+
const kits = {};
|
|
903
|
+
const kitFor = (t) => (t > 0 ? (kits[t] = kits[t] || isoKit(60, 30, 720, 176, 30 / 28, hazeTheme(I, t, mode), evening)) : k);
|
|
382
904
|
|
|
383
905
|
// ground, the ring road and its lanes, the plaza
|
|
384
906
|
const flat = [];
|
|
@@ -406,15 +928,33 @@
|
|
|
406
928
|
|
|
407
929
|
// the civic quarter, then the districts' work, painted back to front
|
|
408
930
|
const blds = [];
|
|
931
|
+
const civicTop = {}, civicLive = {};
|
|
409
932
|
CIVIC_SLOTS.forEach((s) => {
|
|
410
933
|
const polys = [];
|
|
411
|
-
|
|
412
|
-
|
|
934
|
+
const b = living ? living.byKey[s[0]] : null;
|
|
935
|
+
const quiet = !!living && isQuiet(b, views[s[0]]);
|
|
936
|
+
const kk = quiet ? kitFor(QUIET_HAZE) : k;
|
|
937
|
+
isoInstitution(polys, kk, s[0], s[1], s[2], { fixes: rulesAtWork(docket) });
|
|
938
|
+
const item = { key: s[0], depth: s[1] + s[2] + 2, polys: polys };
|
|
939
|
+
civicTop[s[0]] = ROOF[s[0]];
|
|
940
|
+
if (living && b) {
|
|
941
|
+
const civ = civicLiving(polys, kk, s, b, views[s[0]], living);
|
|
942
|
+
item.parts = civ.parts;
|
|
943
|
+
item.quiet = quiet;
|
|
944
|
+
civicTop[s[0]] = civ.top;
|
|
945
|
+
civicLive[s[0]] = civ;
|
|
946
|
+
}
|
|
947
|
+
blds.push(item);
|
|
413
948
|
});
|
|
949
|
+
// the living city hazes the districts by depth band; one with an agent at work stands a band clearer
|
|
950
|
+
const agentsIn = {};
|
|
951
|
+
if (living) (living.districts || []).forEach((d) => { agentsIn[d.goal_id] = Number(d.agents) || 0; });
|
|
414
952
|
districts.forEach((D) => {
|
|
953
|
+
D.haze = living ? Math.round(HAZE[hazeBand(D.lot)] * (agentsIn[D.goal_id] > 0 ? ACTIVE_HAZE : 1) * 100) / 100 : 0;
|
|
954
|
+
const kk = kitFor(D.haze);
|
|
415
955
|
D.work.forEach((w) => {
|
|
416
956
|
const polys = [];
|
|
417
|
-
isoWork(polys,
|
|
957
|
+
isoWork(polys, kk, w.gx, w.gy, w.kind, w.h);
|
|
418
958
|
blds.push({ key: w.gx + ',' + w.gy, depth: w.gx + w.gy + 1, polys: polys });
|
|
419
959
|
});
|
|
420
960
|
});
|
|
@@ -429,9 +969,12 @@
|
|
|
429
969
|
if (!m || !slotOf[m.building]) return;
|
|
430
970
|
(atBuilding[m.building] = atBuilding[m.building] || []).push(m);
|
|
431
971
|
});
|
|
432
|
-
|
|
972
|
+
let pins = [];
|
|
433
973
|
let n = 0;
|
|
434
|
-
|
|
974
|
+
// who leads each building is known: GET /government/leads was read (task 1004709)
|
|
975
|
+
const seated = !!(living && living.seats_real);
|
|
976
|
+
if (living) pins = livingPins(k, living, answered, civicTop);
|
|
977
|
+
else CIVIC_SLOTS.forEach((s) => {
|
|
435
978
|
const bp = buildingPin(atBuilding[s[0]], opts.now);
|
|
436
979
|
if (!bp) return;
|
|
437
980
|
const top = P(s[1] + 1, s[2] + 1, ROOF[s[0]] + 8);
|
|
@@ -458,14 +1001,58 @@
|
|
|
458
1001
|
toReview.forEach((d, i) => { if (fed[i]) [0, 1].forEach((j) => parcels.push({ key: 'p' + i + j, d: d, dur: 12 + i, delay: -(i * 2.7 + j * 6.2), out: false })); });
|
|
459
1002
|
if (districts.length) [0, 1].forEach((j) => parcels.push({ key: 'out' + j, d: route([[10.6, 9.6], [11.5, 9.6], [11.5, 8], [11.5, 0]]), dur: 10, delay: -(j * 5), out: true }));
|
|
460
1003
|
|
|
461
|
-
|
|
1004
|
+
let line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
|
|
462
1005
|
const pinCount = { needs: 0, moving: 0, settled: 0 };
|
|
463
1006
|
pins.forEach((p) => { pinCount[p.state === 'urgent' ? 'needs' : p.state] += 1; });
|
|
464
|
-
|
|
1007
|
+
let label = 'The governor city: the nine civic buildings round the plaza and '
|
|
465
1008
|
+ (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
|
|
466
1009
|
+ (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
|
|
467
1010
|
|
|
468
|
-
|
|
1011
|
+
const out = { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
|
|
1012
|
+
if (!living) return out;
|
|
1013
|
+
|
|
1014
|
+
// ---- the living city's three levels (task 1004685) ----
|
|
1015
|
+
const spend = conduits(k, living, districts, opts.now);
|
|
1016
|
+
// the conduit runs on the ground, over the plots and under every building
|
|
1017
|
+
out.flat = flat.concat(spend.flat);
|
|
1018
|
+
const lanes = skyLanes(k, living.city && living.city.traffic, districts, civicTop);
|
|
1019
|
+
const sky = [];
|
|
1020
|
+
lanes.forEach((L) => sky.push({ d: L.d, f: 'none', s: 'none', dash: 'none', w: 2, cls: 'city-lane', key: 'lane:' + L.key }));
|
|
1021
|
+
spend.beacons.forEach((b) => sky.push(b));
|
|
1022
|
+
const civic = CIVIC_SLOTS.map((s) => {
|
|
1023
|
+
const b = living.byKey[s[0]];
|
|
1024
|
+
if (!b) return null;
|
|
1025
|
+
const v = views[s[0]], civ = civicLive[s[0]];
|
|
1026
|
+
const top = P(s[1] + 1, s[2] + 1, civicTop[s[0]]);
|
|
1027
|
+
const foot = P(s[1] + 2, s[2] + 2, 0);
|
|
1028
|
+
return {
|
|
1029
|
+
key: s[0], name: String(b.name || s[0]), top: [r1(top[0]), r1(top[1])],
|
|
1030
|
+
hit: { x: r1(top[0] - 28), y: r1(top[1] - 6), w: 56, h: r1(foot[1] - top[1] + 6) },
|
|
1031
|
+
plate: { text: b.lead ? String(b.lead.name || 'Lead') : 'owner', lead: !!b.lead, x: r1(top[0] + 10), y: r1(top[1] - 4) },
|
|
1032
|
+
floors: civ.floors.map((f, i) => ({ kind: f.kind, seat: f.seat, name: f.name, on_you: civ.held[i] })),
|
|
1033
|
+
body_on_you: civ.bodyYou, windows: civ.windows, site: civ.site, tally: civ.tally, shaft: civ.shaft, quiet: !!civ && blds.some((x) => x.key === s[0] && x.quiet),
|
|
1034
|
+
on_you: v.onYou.length, on_others: v.others.length, open: v.open.length, card: cardFor(b, v, living, seated),
|
|
1035
|
+
};
|
|
1036
|
+
}).filter(Boolean);
|
|
1037
|
+
placePlates(civic, pins, k);
|
|
1038
|
+
// a plate names a Lead or the owner's seat: none where the real read does not say who
|
|
1039
|
+
if (!seated) civic.forEach((c) => { c.plate.hidden = true; });
|
|
1040
|
+
const others = tally && tally.on_others != null ? tally.on_others : CIVIC_SLOTS.reduce((n2, s) => n2 + views[s[0]].others.length, 0);
|
|
1041
|
+
const agents = tally && tally.agents_at_work != null ? tally.agents_at_work : living.city && living.city.traffic ? Number(living.city.traffic.agents) || 0 : 0;
|
|
1042
|
+
line = docket ? livingLine(onYou, others, agents) : FALLBACK_LINE;
|
|
1043
|
+
label = 'The governor city: the nine civic buildings round the plaza, ' + (districts.length === 1 ? 'one district' : districts.length + ' districts')
|
|
1044
|
+
+ ' for the open goals, and ' + agents + (agents === 1 ? ' agent' : ' agents') + ' in the sky lanes. ' + line + '.';
|
|
1045
|
+
const pods = lanes.reduce((n2, L) => n2 + L.pods.length, 0);
|
|
1046
|
+
const shafts = civic.filter((c) => c.shaft).map((c) => c.shaft);
|
|
1047
|
+
return Object.assign(out, {
|
|
1048
|
+
line: line, label: label, living: true,
|
|
1049
|
+
sky: sky, lanes: lanes, shafts: shafts, civic: civic,
|
|
1050
|
+
spend: { rate_usd_per_day: spend.rate, width: spend.width, spurs: spend.spurs },
|
|
1051
|
+
beacons: districts.filter((D) => D.beacon).map((D) => D.beacon),
|
|
1052
|
+
plaque: plaqueFor(living, opts.constitution, onYou), cta: ctaFor(onYou),
|
|
1053
|
+
counts: { on_you: onYou, on_others: others, open: onYou + others, agents_at_work: agents, moving: tally ? tally.moving : null },
|
|
1054
|
+
motion: { pods: pods, sites: civic.filter((c) => c.site && c.site.kind === 'live').length * 3, shafts: shafts.length, parcels: parcels.length, pins: pins.length + pins.filter((p) => p.state === 'urgent').length },
|
|
1055
|
+
});
|
|
469
1056
|
}
|
|
470
1057
|
|
|
471
1058
|
// Where the start governing button goes: the street (task 1004525 builds it),
|
|
@@ -479,6 +1066,12 @@
|
|
|
479
1066
|
matterTier: matterTier, buildingPin: buildingPin, pinAria: pinAria, TIERS: TIERS, URGENT_MS: URGENT_MS,
|
|
480
1067
|
CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS,
|
|
481
1068
|
FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
|
|
1069
|
+
// the living city (task 1004685)
|
|
1070
|
+
hazeTheme: hazeTheme, mixToward: mixToward, hazeBand: hazeBand, structureFloors: structureFloors, asksAt: asksAt,
|
|
1071
|
+
spendRate: spendRate, trunkWidth: trunkWidth, plaqueFor: plaqueFor, ctaFor: ctaFor, livingLine: livingLine,
|
|
1072
|
+
FLOOR_H: FLOOR_H, BUILD_FLOORS: BUILD_FLOORS, CORE: CORE, BODY: BODY, LANE_PERIOD: LANE_PERIOD, POD_GAP: POD_GAP,
|
|
1073
|
+
MAX_LANE_PODS: MAX_LANE_PODS, MAX_PODS: MAX_PODS, ACTIVE_HAZE: ACTIVE_HAZE,
|
|
1074
|
+
AGE_FULL_DAYS: AGE_FULL_DAYS, HAZE: HAZE, QUIET_HAZE: QUIET_HAZE,
|
|
482
1075
|
};
|
|
483
1076
|
root.GovCity = api;
|
|
484
1077
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|