@gtanczyk/diorama 0.0.0-stage → 0.2.0
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/LICENSE +202 -0
- package/README.md +157 -2
- package/app/bin/diorama.mjs +13 -0
- package/app/src/actions/budget.ts +160 -0
- package/app/src/actions/build.ts +158 -0
- package/app/src/actions/cast.ts +267 -0
- package/app/src/actions/depth.ts +95 -0
- package/app/src/actions/extend.ts +36 -0
- package/app/src/actions/faces.ts +160 -0
- package/app/src/actions/feedback.ts +87 -0
- package/app/src/actions/fix.ts +382 -0
- package/app/src/actions/history.ts +93 -0
- package/app/src/actions/idea-preview.ts +65 -0
- package/app/src/actions/idea-review.ts +102 -0
- package/app/src/actions/idea-sketch.ts +172 -0
- package/app/src/actions/index.ts +32 -0
- package/app/src/actions/items.ts +94 -0
- package/app/src/actions/kids.ts +215 -0
- package/app/src/actions/language.ts +52 -0
- package/app/src/actions/models.ts +64 -0
- package/app/src/actions/motion.ts +119 -0
- package/app/src/actions/pictures.ts +49 -0
- package/app/src/actions/reactions.ts +36 -0
- package/app/src/actions/registry.ts +101 -0
- package/app/src/actions/share.ts +32 -0
- package/app/src/actions/sound.ts +73 -0
- package/app/src/actions/start.ts +154 -0
- package/app/src/actions/step-work.ts +150 -0
- package/app/src/actions/story.ts +101 -0
- package/app/src/actions/studio.ts +133 -0
- package/app/src/actions/style.ts +72 -0
- package/app/src/actions/video.ts +46 -0
- package/app/src/actions/view.ts +109 -0
- package/app/src/actions/weather.ts +38 -0
- package/app/src/actions/world.ts +290 -0
- package/app/src/commands/action.ts +39 -0
- package/app/src/commands/backdrop.ts +188 -0
- package/app/src/commands/browsers.ts +21 -0
- package/app/src/commands/build.ts +503 -0
- package/app/src/commands/cast.ts +161 -0
- package/app/src/commands/clip.ts +431 -0
- package/app/src/commands/derive.ts +167 -0
- package/app/src/commands/doctor.ts +74 -0
- package/app/src/commands/explore.ts +123 -0
- package/app/src/commands/export.ts +158 -0
- package/app/src/commands/extend.ts +75 -0
- package/app/src/commands/faces.ts +80 -0
- package/app/src/commands/icons.ts +69 -0
- package/app/src/commands/index.ts +37 -0
- package/app/src/commands/language.ts +87 -0
- package/app/src/commands/layout.ts +31 -0
- package/app/src/commands/masks.ts +117 -0
- package/app/src/commands/mcp.ts +52 -0
- package/app/src/commands/model.ts +284 -0
- package/app/src/commands/paint.ts +182 -0
- package/app/src/commands/plates.ts +97 -0
- package/app/src/commands/play.ts +195 -0
- package/app/src/commands/playthrough.ts +111 -0
- package/app/src/commands/publish.ts +78 -0
- package/app/src/commands/reactions.ts +40 -0
- package/app/src/commands/reel.ts +46 -0
- package/app/src/commands/setup.ts +85 -0
- package/app/src/commands/sound.ts +100 -0
- package/app/src/commands/sprites.ts +232 -0
- package/app/src/commands/story.ts +37 -0
- package/app/src/commands/studio.ts +65 -0
- package/app/src/commands/style.ts +87 -0
- package/app/src/commands/verify.ts +45 -0
- package/app/src/commands/voice-retry.ts +14 -0
- package/app/src/commands/voices.ts +145 -0
- package/app/src/commands/weather.ts +52 -0
- package/app/src/commands/world.ts +104 -0
- package/app/src/film/clip.ts +386 -0
- package/app/src/film/reel.ts +126 -0
- package/app/src/film/screencast.ts +123 -0
- package/app/src/image/mask.ts +29 -0
- package/app/src/image/overlay.ts +38 -0
- package/app/src/image/raster.ts +224 -0
- package/app/src/main.ts +28 -0
- package/app/src/meta/choice.ts +27 -0
- package/app/src/meta/http.ts +99 -0
- package/app/src/meta/memo.ts +105 -0
- package/app/src/meta/muse.ts +70 -0
- package/app/src/meta/openrouter.ts +93 -0
- package/app/src/meta/prices.ts +25 -0
- package/app/src/meta/sam.ts +83 -0
- package/app/src/meta/schema.ts +63 -0
- package/app/src/meta/spark.ts +220 -0
- package/app/src/meta/step.ts +14 -0
- package/app/src/meta/voice-file.ts +78 -0
- package/app/src/meta/voice.ts +103 -0
- package/app/src/model/animation.ts +38 -0
- package/app/src/model/blender/relax.py +284 -0
- package/app/src/model/blender.ts +115 -0
- package/app/src/model/build.ts +50 -0
- package/app/src/model/face.ts +21 -0
- package/app/src/model/fit.ts +181 -0
- package/app/src/model/glb.ts +241 -0
- package/app/src/model/plan.ts +151 -0
- package/app/src/model/quality.ts +48 -0
- package/app/src/model/space.ts +168 -0
- package/app/src/model/sprites.ts +293 -0
- package/app/src/model/views.ts +163 -0
- package/app/src/play/creature.ts +43 -0
- package/app/src/play/depth-occlusion.ts +150 -0
- package/app/src/play/exit-outline.ts +48 -0
- package/app/src/play/face3d.ts +34 -0
- package/app/src/play/focus.ts +78 -0
- package/app/src/play/geometry.ts +272 -0
- package/app/src/play/gestures.ts +23 -0
- package/app/src/play/glow.ts +187 -0
- package/app/src/play/lipsync.ts +69 -0
- package/app/src/play/living.ts +83 -0
- package/app/src/play/outline-path.ts +130 -0
- package/app/src/play/parallax.ts +200 -0
- package/app/src/play/path.ts +262 -0
- package/app/src/play/preview.ts +71 -0
- package/app/src/play/reactions.ts +92 -0
- package/app/src/play/rig.ts +172 -0
- package/app/src/play/shadow.ts +253 -0
- package/app/src/play/sound.ts +51 -0
- package/app/src/play/weather.ts +58 -0
- package/app/src/qa/explore.ts +136 -0
- package/app/src/qa/probe.ts +139 -0
- package/app/src/qa/route.ts +118 -0
- package/app/src/registry.ts +23 -0
- package/app/src/scene/animation-source.ts +36 -0
- package/app/src/scene/assets.ts +37 -0
- package/app/src/scene/autofix.ts +133 -0
- package/app/src/scene/backdrop.ts +326 -0
- package/app/src/scene/blockin.ts +189 -0
- package/app/src/scene/brief.ts +74 -0
- package/app/src/scene/carry.ts +177 -0
- package/app/src/scene/cast-fixes.ts +33 -0
- package/app/src/scene/cast.ts +199 -0
- package/app/src/scene/corrections.ts +164 -0
- package/app/src/scene/critic.ts +169 -0
- package/app/src/scene/depthmap.ts +149 -0
- package/app/src/scene/derive.ts +475 -0
- package/app/src/scene/diagnose.ts +132 -0
- package/app/src/scene/face-calibration.ts +65 -0
- package/app/src/scene/face-registration.ts +21 -0
- package/app/src/scene/face-work.ts +71 -0
- package/app/src/scene/faces-data.ts +48 -0
- package/app/src/scene/faces-settings.ts +20 -0
- package/app/src/scene/faces.ts +435 -0
- package/app/src/scene/glows.ts +153 -0
- package/app/src/scene/held.ts +109 -0
- package/app/src/scene/icons.ts +185 -0
- package/app/src/scene/layers.ts +822 -0
- package/app/src/scene/layout.ts +216 -0
- package/app/src/scene/locate.ts +118 -0
- package/app/src/scene/maskcheck.ts +43 -0
- package/app/src/scene/masks.ts +392 -0
- package/app/src/scene/occluders.ts +123 -0
- package/app/src/scene/occlusion-check.ts +138 -0
- package/app/src/scene/paint-layout.ts +135 -0
- package/app/src/scene/paint.ts +81 -0
- package/app/src/scene/photo.ts +123 -0
- package/app/src/scene/plates.ts +256 -0
- package/app/src/scene/playbook.ts +121 -0
- package/app/src/scene/preview.ts +23 -0
- package/app/src/scene/reactions.ts +118 -0
- package/app/src/scene/restyle.ts +53 -0
- package/app/src/scene/rig-check.ts +68 -0
- package/app/src/scene/rig.ts +379 -0
- package/app/src/scene/scale.ts +148 -0
- package/app/src/scene/shadows.ts +215 -0
- package/app/src/scene/stairs.ts +183 -0
- package/app/src/scene/status.ts +32 -0
- package/app/src/scene/verify.ts +156 -0
- package/app/src/scene/walk.ts +326 -0
- package/app/src/scene/warp.ts +419 -0
- package/app/src/scene/weather.ts +118 -0
- package/app/src/scene/words.ts +65 -0
- package/app/src/secrets.ts +219 -0
- package/app/src/setup/checks.ts +110 -0
- package/app/src/sound/cost.ts +74 -0
- package/app/src/sound/data.ts +89 -0
- package/app/src/sound/fixes.ts +23 -0
- package/app/src/sound/issues.ts +21 -0
- package/app/src/sound/language.ts +11 -0
- package/app/src/sound/lipsync-make.ts +98 -0
- package/app/src/sound/lipsync.ts +24 -0
- package/app/src/sound/make.ts +229 -0
- package/app/src/sound/musicgen.py +20 -0
- package/app/src/sound/pcm.ts +280 -0
- package/app/src/sound/plan.ts +154 -0
- package/app/src/sound/retry.ts +43 -0
- package/app/src/sound/settings.ts +28 -0
- package/app/src/sound/sources.ts +81 -0
- package/app/src/sound/spoken.ts +70 -0
- package/app/src/sound/voices.ts +579 -0
- package/app/src/speech/transcribe.ts +38 -0
- package/app/src/speech/types.ts +18 -0
- package/app/src/story/check.ts +230 -0
- package/app/src/story/design.ts +108 -0
- package/app/src/story/engine.ts +233 -0
- package/app/src/story/fixes.ts +54 -0
- package/app/src/story/npc-guidance.ts +7 -0
- package/app/src/story/phrases.ts +4 -0
- package/app/src/story/schema.ts +92 -0
- package/app/src/studio/agent.ts +555 -0
- package/app/src/studio/discovery.ts +28 -0
- package/app/src/studio/events.ts +48 -0
- package/app/src/studio/history.ts +124 -0
- package/app/src/studio/jobs.ts +291 -0
- package/app/src/studio/locks.ts +33 -0
- package/app/src/studio/mcp.ts +86 -0
- package/app/src/studio/operation.ts +18 -0
- package/app/src/studio/repair.ts +31 -0
- package/app/src/studio/server.ts +423 -0
- package/app/src/studio/trouble.ts +48 -0
- package/app/src/world/design.ts +72 -0
- package/app/src/world/drawings.ts +198 -0
- package/app/src/world/extend-stage.ts +109 -0
- package/app/src/world/extend.ts +123 -0
- package/app/src/world/extension-progress.ts +10 -0
- package/app/src/world/files.ts +62 -0
- package/app/src/world/idea-review.ts +32 -0
- package/app/src/world/language.ts +116 -0
- package/app/src/world/photo-limits.ts +2 -0
- package/app/src/world/pool.ts +41 -0
- package/app/src/world/schedule.ts +64 -0
- package/app/src/world/schema.ts +74 -0
- package/app/src/world/sketch.ts +81 -0
- package/app/src/world/style-plan.ts +90 -0
- package/app/src/world/style-reference.ts +16 -0
- package/app/src/world/styled.ts +51 -0
- package/app/src/world/told.ts +105 -0
- package/app/src/world/video-limits.ts +3 -0
- package/app/src/world/video.ts +98 -0
- package/app/studio/activity.tsx +124 -0
- package/app/studio/agent.tsx +384 -0
- package/app/studio/api.ts +111 -0
- package/app/studio/atelier.tsx +234 -0
- package/app/studio/audio-missing.tsx +25 -0
- package/app/studio/audio-setup.tsx +33 -0
- package/app/studio/blender-setup.tsx +30 -0
- package/app/studio/build.tsx +679 -0
- package/app/studio/camera.tsx +59 -0
- package/app/studio/cast-work.tsx +207 -0
- package/app/studio/cast.tsx +178 -0
- package/app/studio/concept-edit.tsx +47 -0
- package/app/studio/creator-idea.tsx +46 -0
- package/app/studio/custom-look.tsx +36 -0
- package/app/studio/faces-setup.tsx +28 -0
- package/app/studio/favicon.svg +11 -0
- package/app/studio/figure.tsx +314 -0
- package/app/studio/fonts/OFL.txt +93 -0
- package/app/studio/fonts/fonts.css +7 -0
- package/app/studio/fonts/fraunces-italic-latin-ext.woff2 +0 -0
- package/app/studio/fonts/fraunces-italic-latin.woff2 +0 -0
- package/app/studio/fonts/fraunces-normal-latin-ext.woff2 +0 -0
- package/app/studio/fonts/fraunces-normal-latin.woff2 +0 -0
- package/app/studio/fonts/instrument-sans-normal-latin-ext.woff2 +0 -0
- package/app/studio/fonts/instrument-sans-normal-latin.woff2 +0 -0
- package/app/studio/icons.tsx +42 -0
- package/app/studio/idea-card.tsx +42 -0
- package/app/studio/idea-review.tsx +181 -0
- package/app/studio/idea-storyboard.tsx +134 -0
- package/app/studio/index.html +15 -0
- package/app/studio/job-words.ts +33 -0
- package/app/studio/lightbox.tsx +49 -0
- package/app/studio/main.tsx +1329 -0
- package/app/studio/nav.ts +14 -0
- package/app/studio/place.tsx +264 -0
- package/app/studio/review-sketch.tsx +47 -0
- package/app/studio/say-it.tsx +49 -0
- package/app/studio/step-work.tsx +134 -0
- package/app/studio/story.tsx +394 -0
- package/app/studio/style-menu.tsx +81 -0
- package/app/studio/style.css +1390 -0
- package/app/studio/sure.tsx +54 -0
- package/app/studio/tip.tsx +74 -0
- package/app/studio/tsconfig.json +5 -0
- package/app/studio/video.tsx +73 -0
- package/app/studio/world.tsx +188 -0
- package/app/tsconfig.json +13 -0
- package/app/web/audio.ts +315 -0
- package/app/web/face.ts +131 -0
- package/app/web/face3d.ts +35 -0
- package/app/web/glows.ts +387 -0
- package/app/web/hero3d.ts +728 -0
- package/app/web/index.html +118 -0
- package/app/web/living.ts +404 -0
- package/app/web/main.ts +3769 -0
- package/app/web/pad.ts +174 -0
- package/app/web/reactions.ts +79 -0
- package/app/web/sheet.ts +36 -0
- package/app/web/synth.ts +140 -0
- package/app/web/tsconfig.json +5 -0
- package/app/web/ui.ts +263 -0
- package/app/web/weather.ts +306 -0
- package/app/web/words.ts +12 -0
- package/docs/characters.md +58 -0
- package/docs/npc-motion.md +69 -0
- package/docs/scenes.md +75 -0
- package/docs/setup.md +60 -0
- package/docs/sound.md +23 -0
- package/docs/studio.md +62 -0
- package/package.json +70 -4
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
// diorama build <slug> ["<idea>"]: runs the steps, each as soon as what it needs is made (src/world/schedule.ts):
|
|
2
|
+
// every place on its own, the cast, voices and sound alongside. Keeps a build.json of what happened.
|
|
3
|
+
import { facesState } from '../scene/faces-settings.ts';
|
|
4
|
+
import { inStep } from '../meta/step.ts';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { defineCommand, emit, registry, takeOption } from '../registry.ts';
|
|
7
|
+
import { Geometry } from '../play/geometry.ts';
|
|
8
|
+
import { maskFromPng, type Mask } from '../image/mask.ts';
|
|
9
|
+
import { autoFixes } from '../scene/autofix.ts';
|
|
10
|
+
import { Corrections, readCorrections, writeCorrections } from '../scene/corrections.ts';
|
|
11
|
+
import { Layout } from '../scene/layout.ts';
|
|
12
|
+
import { withPlay, type Play } from '../scene/playbook.ts';
|
|
13
|
+
import { gridOf, type Finding, type Verdict } from '../scene/verify.ts';
|
|
14
|
+
import { WorldFiles } from '../world/files.ts';
|
|
15
|
+
import { drawingFiles } from '../world/drawings.ts';
|
|
16
|
+
import { toldFiles } from '../world/told.ts';
|
|
17
|
+
import { NEEDS, needs, PLACE_STEPS, STEPS, type Step } from '../world/schedule.ts';
|
|
18
|
+
import { itemSources } from '../scene/icons.ts';
|
|
19
|
+
import { Story } from '../story/schema.ts';
|
|
20
|
+
import { assetReport } from '../scene/assets.ts';
|
|
21
|
+
import { FacePreparationError } from '../scene/face-work.ts';
|
|
22
|
+
|
|
23
|
+
/** How the build's own repaints of a missing thing start, so a new take can drop them. */
|
|
24
|
+
const BUILD_REPAINT = 'Paint the';
|
|
25
|
+
|
|
26
|
+
export { PLACE_STEPS, STEPS } from '../world/schedule.ts';
|
|
27
|
+
|
|
28
|
+
/** What each place step leaves that later steps read; a place missing one never got past that step. */
|
|
29
|
+
const MADE: Partial<Record<Step, string>> = { layout: 'blockin.png', paint: 'painting.png', masks: 'masks.json', derive: 'geometry.json' };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The earliest step before `step` whose file the place lacks, with that file; null when it has them all. Steps this
|
|
33
|
+
* build ran itself (`ran`) are not looked at: they made the file or set the place aside already.
|
|
34
|
+
*/
|
|
35
|
+
export function placeGap(placeDir: string, step: Step, ran: readonly Step[] = []): { step: Step; file: string } | null {
|
|
36
|
+
for (const s of PLACE_STEPS.slice(0, PLACE_STEPS.indexOf(step)).filter((x) => !ran.includes(x))) {
|
|
37
|
+
const file = MADE[s];
|
|
38
|
+
if (file && !fs.existsSync(`${placeDir}/${file}`)) return { step: s, file };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const gapError = (place: string, gap: { step: string; file: string }) =>
|
|
44
|
+
`${place} never got past ${gap.step} (no ${gap.file}): fix why ${gap.step} failed there (place_status, the job log) and rebuild the place from ${gap.step}`;
|
|
45
|
+
|
|
46
|
+
type Report = { place: string; ok: boolean; errors: string[]; flagged: string[]; issues: Array<Finding & { play?: Play }>; from?: Step };
|
|
47
|
+
type Repair = { place: string; by: 'auto'; fixes: string[]; ok: boolean };
|
|
48
|
+
const line = (f: Finding) => `${f.code}${f.key ? ` ${f.key}` : ''}: ${f.detail}`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Model calls `step` sent (not answered from the memo) since `since`, and what they cost, read from the memo's call
|
|
52
|
+
* log. Only calls tagged with this step count: steps run side by side, and counting every call since the step
|
|
53
|
+
* began gave each step the others' calls too (the 3D step showed 137 Spark calls where it made about 25).
|
|
54
|
+
*/
|
|
55
|
+
export function stepCalls(files: WorldFiles, step: string, since: string): { sent: Record<string, number>; usd: number } {
|
|
56
|
+
const log = files.path('memo', 'calls.jsonl');
|
|
57
|
+
const sent: Record<string, number> = { spark: 0, muse: 0, sam: 0, sound: 0, music: 0, voice: 0, space: 0 };
|
|
58
|
+
let usd = 0;
|
|
59
|
+
if (!fs.existsSync(log)) return { sent, usd };
|
|
60
|
+
for (const line of fs.readFileSync(log, 'utf8').split('\n')) {
|
|
61
|
+
if (!line) continue;
|
|
62
|
+
const call = JSON.parse(line) as { at: string; model: string; hit: boolean; step?: string; usd?: number };
|
|
63
|
+
if (call.at < since || call.hit || call.step !== step) continue;
|
|
64
|
+
sent[call.model] = (sent[call.model] ?? 0) + 1;
|
|
65
|
+
usd += call.usd ?? 0;
|
|
66
|
+
}
|
|
67
|
+
return { sent, usd: Math.round(usd * 1000) / 1000 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
defineCommand({
|
|
71
|
+
name: 'build',
|
|
72
|
+
usage: 'build <slug> ["<idea>"] run every step, world to verify, in order [--from STEP] [--to STEP] [--places N] [--photo room.jpg] [--hero-photo me.jpg] [--drawings DIR] [--told DIR] [--kids] [--language L] [--place KEY: only the place steps, for one place] [--repair off: leave errors for the agent and the person, no free fixes] [--max-take N: new paintings the build may ask for, default 2, 0 keeps them]',
|
|
73
|
+
async run(args) {
|
|
74
|
+
const from = takeOption(args, 'from', STEPS[0]) as Step;
|
|
75
|
+
const to = takeOption(args, 'to', STEPS[STEPS.length - 1]) as Step;
|
|
76
|
+
const places = takeOption(args, 'places', '4');
|
|
77
|
+
const photo = takeOption(args, 'photo', '');
|
|
78
|
+
const heroPhoto = takeOption(args, 'hero-photo', '');
|
|
79
|
+
// Children's drawings and recordings of their story, and the game's language, go to the world step as they are.
|
|
80
|
+
const told = takeOption(args, 'told', '');
|
|
81
|
+
const worldArgs = ['drawings', 'told', 'language'].flatMap((o) => {
|
|
82
|
+
const value = o === 'told' ? told : takeOption(args, o, '');
|
|
83
|
+
return value ? [`--${o}`, value] : [];
|
|
84
|
+
});
|
|
85
|
+
const kids = args.indexOf('--kids');
|
|
86
|
+
if (kids >= 0) (args.splice(kids, 1), worldArgs.push('--kids'));
|
|
87
|
+
const only = takeOption(args, 'place', '');
|
|
88
|
+
const repair = takeOption(args, 'repair', 'auto');
|
|
89
|
+
// The highest take of a painting the build may ask for by itself (FLOOR_LOST); 0 keeps every painting as it is.
|
|
90
|
+
const maxTake = Number(takeOption(args, 'max-take', '2'));
|
|
91
|
+
const [slug, idea] = args;
|
|
92
|
+
if (!slug) throw new Error('usage: diorama build <slug> ["<idea>"] [--from STEP] [--to STEP]');
|
|
93
|
+
const range = STEPS.slice(STEPS.indexOf(from), STEPS.indexOf(to) + 1).filter((s) => !only || PLACE_STEPS.includes(s));
|
|
94
|
+
if (range.length === 0) throw new Error(`steps run ${STEPS.join(' > ')}; --from ${from} --to ${to} selects none`);
|
|
95
|
+
const files = new WorldFiles(slug);
|
|
96
|
+
if (range[0] === 'world' && !idea && !told && toldFiles(files).length === 0 && drawingFiles(files).length === 0) throw new Error('the world step needs an idea: diorama build <slug> "<idea>" (or --told DIR, recordings of the story)');
|
|
97
|
+
|
|
98
|
+
if (only && !files.world().places.some((p) => p.key === only)) throw new Error(`${slug} has no place ${only}`);
|
|
99
|
+
const logFile = files.path('build.json');
|
|
100
|
+
const log = (fs.existsSync(logFile) ? files.readJson(logFile) : { steps: {} }) as { steps: Record<string, unknown>; places?: Report[]; repairs?: Array<Repair & { at: string }> };
|
|
101
|
+
// A place whose step threw is set aside with why; the other places go on, and so do the world's own steps.
|
|
102
|
+
const broken = new Map<string, { step: Step; error: string }>();
|
|
103
|
+
const placeStep = async (step: Step, place: string) => {
|
|
104
|
+
// A place step that lost the network (every retry of a request failed: "fetch failed") gets the step
|
|
105
|
+
// again after a pause, twice, before the place is set aside: an outage of a few minutes costs time, not a place.
|
|
106
|
+
for (let again = 0; ; again++) {
|
|
107
|
+
try {
|
|
108
|
+
await inStep(step, () => registry.get(step)!.run([slug, place]));
|
|
109
|
+
return;
|
|
110
|
+
} catch (reason) {
|
|
111
|
+
const error = reason instanceof Error ? reason.message : String(reason);
|
|
112
|
+
if (again < 2 && /fetch failed|ECONNRESET|ENOTFOUND|EAI_AGAIN|socket hang up/i.test(error)) {
|
|
113
|
+
console.log(`${place}: ${step} lost the network (${error.slice(0, 80)}); trying the step again in ${60 * (again + 1)} s`);
|
|
114
|
+
await new Promise((r) => setTimeout(r, 60_000 * (again + 1)));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw reason;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** Runs a place step for one place; a place that throws is set aside, the others go on. */
|
|
122
|
+
const runPlace = async (step: Step, place: string) => {
|
|
123
|
+
// A place an earlier job left unfinished (its paint refused, say) is set aside at the step it never got past,
|
|
124
|
+
// not failed here with a missing file, so the report and job_retry point at the step to redo.
|
|
125
|
+
const gap = placeGap(files.place(place), step, range);
|
|
126
|
+
if (gap) {
|
|
127
|
+
broken.set(place, { step: gap.step, error: gapError(place, gap) });
|
|
128
|
+
console.log(`${place}: ${step} skipped: ${gapError(place, gap)}`);
|
|
129
|
+
emit('place.fail', { place, step: gap.step, error: gapError(place, gap) });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await placeStep(step, place);
|
|
134
|
+
} catch (reason) {
|
|
135
|
+
const error = reason instanceof Error ? reason.message : String(reason);
|
|
136
|
+
broken.set(place, { step, error });
|
|
137
|
+
console.log(`${place}: ${step} failed, set aside: ${error}`);
|
|
138
|
+
emit('place.fail', { place, step, error });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/** Per place: whether it is playable, everything flagged and let through, and for each error what to try (the playbook). */
|
|
142
|
+
const reportOf = (place: string): Report | null => {
|
|
143
|
+
const gone = broken.get(place);
|
|
144
|
+
if (gone) {
|
|
145
|
+
const issue = withPlay({ code: 'STEP_FAILED', key: gone.step, detail: gone.error });
|
|
146
|
+
return { place, ok: false, errors: [line(issue)], flagged: [], issues: [issue], from: gone.step };
|
|
147
|
+
}
|
|
148
|
+
const file = files.place(place, 'verify.json');
|
|
149
|
+
if (!range.includes('verify') || !fs.existsSync(file)) return null;
|
|
150
|
+
const v = files.readJson(file) as Verdict;
|
|
151
|
+
return { place, ok: v.ok, errors: v.errors.map(line), flagged: v.flags.map(line), issues: v.errors.map((e) => withPlay(e, v.flags)) };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
emit('build.start', { slug, steps: range });
|
|
155
|
+
const saved = process.exitCode;
|
|
156
|
+
const optionalWarnings = new Map<Step,string[]>();
|
|
157
|
+
const run = (step: Step) => range.includes(step);
|
|
158
|
+
/** Each step's run: when it began, its longest single run (one place, or the world), what is still to do. */
|
|
159
|
+
const runs = new Map<Step, { at: string; started: number; longest: number; left: number; done: Promise<void>; end: () => void; fail: (error: unknown) => void }>();
|
|
160
|
+
for (const step of range) {
|
|
161
|
+
let end!: () => void;
|
|
162
|
+
let fail!: (error: unknown) => void;
|
|
163
|
+
const done = new Promise<void>((resolve, reject) => ((end = resolve), (fail = reject)));
|
|
164
|
+
// Whoever waits on it hears of a failure; nobody waiting is fine.
|
|
165
|
+
done.catch(() => {});
|
|
166
|
+
runs.set(step, { at: '', started: 0, longest: 0, left: 1, done, end, fail });
|
|
167
|
+
}
|
|
168
|
+
/** A world step failed: nothing waiting on an unfinished step starts, so the build ends with that failure. */
|
|
169
|
+
const abort = (error: unknown) => {
|
|
170
|
+
for (const r of runs.values()) r.fail(error);
|
|
171
|
+
};
|
|
172
|
+
/** How many runs make the step: one for the world's, one per place (known once world.json is). */
|
|
173
|
+
const open = (step: Step, units: number) => {
|
|
174
|
+
const r = runs.get(step)!;
|
|
175
|
+
r.left = units;
|
|
176
|
+
if (units === 0) r.end();
|
|
177
|
+
};
|
|
178
|
+
const begin = (step: Step) => {
|
|
179
|
+
const r = runs.get(step)!;
|
|
180
|
+
if (r.started) return Date.now();
|
|
181
|
+
r.started = Date.now();
|
|
182
|
+
r.at = new Date().toISOString();
|
|
183
|
+
console.log(`\n# ${step}`);
|
|
184
|
+
emit('step.start', { step });
|
|
185
|
+
return r.started;
|
|
186
|
+
};
|
|
187
|
+
/** One unit of a step finished (a place, or the world's own run); the last one ends the step. */
|
|
188
|
+
const finish = (step: Step, since?: number) => {
|
|
189
|
+
const r = runs.get(step)!;
|
|
190
|
+
if (since) r.longest = Math.max(r.longest, Date.now() - since);
|
|
191
|
+
if (--r.left > 0) return;
|
|
192
|
+
r.end();
|
|
193
|
+
if (!r.started) return;
|
|
194
|
+
// A step's time is what one place (or the world) took with it, so it adds up along the web, not the clock.
|
|
195
|
+
const done = { at: r.at, seconds: Math.round(r.longest / 100) / 10, ...stepCalls(files, step, r.at), ...(optionalWarnings.has(step) && { status: 'incomplete', warnings: optionalWarnings.get(step) }) };
|
|
196
|
+
// A one-place rerun does not stand for the whole step.
|
|
197
|
+
if (!only) {
|
|
198
|
+
log.steps[step] = done;
|
|
199
|
+
files.write(logFile, log);
|
|
200
|
+
}
|
|
201
|
+
emit('step.end', { step, ...done, ...(only && { place: only }) });
|
|
202
|
+
};
|
|
203
|
+
/** Resolves when `step` is done everywhere (every place has finished it or was set aside); at once when not run. */
|
|
204
|
+
const ready = (step: Step) => (run(step) ? runs.get(step)!.done : Promise.resolve());
|
|
205
|
+
const worldStep = async (step: Step, args: string[]) => {
|
|
206
|
+
await Promise.all(needs(step).map(ready));
|
|
207
|
+
const since = begin(step);
|
|
208
|
+
if (step === 'faces' && !facesState().enabled) {
|
|
209
|
+
console.log('Talking mouths are off (Setup): characters speak with voice and gestures.');
|
|
210
|
+
finish(step, since); return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await inStep(step, () => registry.get(step)!.run(args));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// Speaking faces never fail a build: whatever went wrong, the game plays with gestures.
|
|
216
|
+
if (step === 'faces') {
|
|
217
|
+
optionalWarnings.set(step,error instanceof FacePreparationError ? error.failures : [error instanceof Error ? error.message : String(error)]);
|
|
218
|
+
console.log(`Speaking animation incomplete; game build continues: ${optionalWarnings.get(step)!.join('; ')}`);
|
|
219
|
+
finish(step,since);return;
|
|
220
|
+
}
|
|
221
|
+
emit('step.fail', { step, error: error instanceof Error ? error.message : String(error) });
|
|
222
|
+
abort(error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
finish(step, since);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const worldSteps = STEPS.filter((s) => run(s) && !PLACE_STEPS.includes(s) && s !== 'icons');
|
|
229
|
+
const tasks: Array<Promise<void>> = [];
|
|
230
|
+
const go = (step: Step, args: string[]) => {
|
|
231
|
+
const task = worldStep(step, args);
|
|
232
|
+
// Every world step is awaited at the end; this only keeps a failure from being reported twice as unhandled.
|
|
233
|
+
task.catch(() => {});
|
|
234
|
+
tasks.push(task);
|
|
235
|
+
};
|
|
236
|
+
if (run('world')) go('world', [slug, ...(idea ? [idea] : []), '--places', places, ...worldArgs, ...(photo ? ['--photo', photo] : []), ...(heroPhoto ? ['--hero-photo', heroPhoto] : [])]);
|
|
237
|
+
// Speaking faces in a build: the portrait (what a conversation shows) and the 2D rig only. Talk sheets and 3D
|
|
238
|
+
// surfaces cost a Muse atlas and many vision checks each, about 20 minutes a character, for lips nobody watches.
|
|
239
|
+
for (const s of worldSteps.filter((s) => s !== 'world')) go(s, s === 'faces' ? [slug, '--image-only'] : [slug]);
|
|
240
|
+
|
|
241
|
+
// The places start once world.json says which there are; each goes through its steps on its own.
|
|
242
|
+
const reports: Report[] = [];
|
|
243
|
+
const repairs: Repair[] = [];
|
|
244
|
+
let keys: string[] = [];
|
|
245
|
+
const placesDone = ready('world').then(async () => {
|
|
246
|
+
keys = files.world().places.map((p) => p.key).filter((p) => !only || p === only);
|
|
247
|
+
for (const s of PLACE_STEPS.filter(run)) open(s, keys.length);
|
|
248
|
+
// Icons, per place once its things are cut out; items picked up nowhere once the story is there.
|
|
249
|
+
const story = () => (fs.existsSync(files.path('story.json')) ? Story.parse(files.readJson(files.path('story.json'))) : null);
|
|
250
|
+
if (run('icons')) open('icons', keys.length + 1);
|
|
251
|
+
let iconsQueue = Promise.resolve();
|
|
252
|
+
const iconTasks: Array<Promise<void>> = [];
|
|
253
|
+
/** items/items.json is read and written by each run, so icons runs one after another. */
|
|
254
|
+
const icons = (items: () => string[] | null) => {
|
|
255
|
+
const next = iconsQueue.then(async () => {
|
|
256
|
+
await ready('story');
|
|
257
|
+
const keys = items();
|
|
258
|
+
// null: no story to say which items, so icons is run as a whole (it says there is nothing to do).
|
|
259
|
+
if (keys && keys.length === 0) return finish('icons');
|
|
260
|
+
const since = begin('icons');
|
|
261
|
+
try {
|
|
262
|
+
await inStep('icons', () => registry.get('icons')!.run([slug, ...(keys ?? [])]));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
emit('step.fail', { step: 'icons', error: error instanceof Error ? error.message : String(error) });
|
|
265
|
+
abort(error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
finish('icons', since);
|
|
269
|
+
});
|
|
270
|
+
iconsQueue = next.catch(() => {});
|
|
271
|
+
iconTasks.push(next);
|
|
272
|
+
};
|
|
273
|
+
const itemsFrom = (place: string | null) => () => {
|
|
274
|
+
const s = story();
|
|
275
|
+
if (!s) return place === null ? null : [];
|
|
276
|
+
const placed = new Set(keys);
|
|
277
|
+
return Object.entries(itemSources(s)).filter(([, from]) => (place === null ? !from || !placed.has(from.place) : from?.place === place)).map(([item]) => item);
|
|
278
|
+
};
|
|
279
|
+
if (run('icons')) icons(itemsFrom(null));
|
|
280
|
+
|
|
281
|
+
const chains = await Promise.allSettled(keys.map(async (place) => {
|
|
282
|
+
for (const step of PLACE_STEPS.filter(run)) {
|
|
283
|
+
await Promise.all(NEEDS[step].map(ready));
|
|
284
|
+
if (!broken.has(place)) {
|
|
285
|
+
const since = begin(step);
|
|
286
|
+
emit('place.step', { place, step });
|
|
287
|
+
await runPlace(step, place);
|
|
288
|
+
// Said per place, so Studio can tell a place between steps (waiting on another one) from one being worked on.
|
|
289
|
+
emit('place.step', { place, step, done: true });
|
|
290
|
+
finish(step, since);
|
|
291
|
+
} else finish(step);
|
|
292
|
+
if (step === 'masks' && run('icons')) icons(itemsFrom(place));
|
|
293
|
+
}
|
|
294
|
+
const report = await repairPlace(place);
|
|
295
|
+
if (report) {
|
|
296
|
+
reports.push(report);
|
|
297
|
+
// Said as soon as the place is done, so it can be played while the others are still being made.
|
|
298
|
+
emit('place.verdict', report);
|
|
299
|
+
}
|
|
300
|
+
}));
|
|
301
|
+
if (run('icons') && !run('masks')) for (const p of keys) icons(itemsFrom(p));
|
|
302
|
+
for (const settled of [...chains, ...(await Promise.allSettled(iconTasks))]) if (settled.status === 'rejected') throw settled.reason;
|
|
303
|
+
});
|
|
304
|
+
placesDone.catch(abort);
|
|
305
|
+
tasks.push(placesDone);
|
|
306
|
+
// Everything runs to the end before a failure is told, so no step is cut off half written.
|
|
307
|
+
const settled = await Promise.allSettled(tasks);
|
|
308
|
+
process.exitCode = saved;
|
|
309
|
+
const failure = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected');
|
|
310
|
+
if (failure) throw failure.reason;
|
|
311
|
+
if (keys.length && keys.every((p) => broken.has(p))) {
|
|
312
|
+
const last = [...broken.values()].sort((a, b) => STEPS.indexOf(b.step) - STEPS.indexOf(a.step))[0].step;
|
|
313
|
+
const error = `${last} failed for every place: ${[...broken].map(([p, b]) => `${p}: ${b.error}`).join('; ')}`;
|
|
314
|
+
emit('step.fail', { step: last, error });
|
|
315
|
+
throw new Error(error);
|
|
316
|
+
}
|
|
317
|
+
if (reports.length) {
|
|
318
|
+
const report = keys.map((p) => reports.find((r) => r.place === p)).filter((r): r is Report => !!r);
|
|
319
|
+
if (repairs.length) log.repairs = [...(log.repairs ?? []), ...repairs.map((x) => ({ at: new Date().toISOString(), ...x }))].slice(-50);
|
|
320
|
+
log.places = [...(log.places ?? []).filter((r) => !report.some((n) => n.place === r.place)), ...report];
|
|
321
|
+
files.write(logFile, log);
|
|
322
|
+
console.log('\n# report');
|
|
323
|
+
for (const r of report) {
|
|
324
|
+
console.log(`${r.place}: ${r.ok ? 'playable' : 'NOT playable'}${r.flagged.length ? `, flagged and continued: ${r.flagged.length}` : ''}`);
|
|
325
|
+
for (const i of r.issues) {
|
|
326
|
+
console.log(` error ${line(i)}`);
|
|
327
|
+
if (i.play) console.log(` try: ${i.play.tools.join(' | ')}`);
|
|
328
|
+
}
|
|
329
|
+
for (const f of r.flagged) console.log(` flag ${f}`);
|
|
330
|
+
}
|
|
331
|
+
if (report.some((r) => !r.ok)) process.exitCode = 1;
|
|
332
|
+
}
|
|
333
|
+
// The cast and the sound, which verify does not look at: a character with no walk, a sound that stayed silent.
|
|
334
|
+
if (['cast', 'model', 'sprites', 'sound', 'voices'].some((s) => run(s as Step))) {
|
|
335
|
+
try {
|
|
336
|
+
const assets = assetReport(files);
|
|
337
|
+
if (assets.length) console.log(`\n# made\n${assets.map((l) => ` ${l}`).join('\n')}`);
|
|
338
|
+
} catch { /* a report only */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The place's verdict once its steps are done, after the free tier of repair: fixes the build can make itself
|
|
343
|
+
* with no model call (src/scene/autofix.ts), then the place is rebuilt from the step they feed. Whatever is
|
|
344
|
+
* left goes to the agent, then the person. Null when verify did not run and the place did not break.
|
|
345
|
+
*/
|
|
346
|
+
async function repairPlace(place: string): Promise<Report | null> {
|
|
347
|
+
let r = reportOf(place);
|
|
348
|
+
if (!r || repair === 'off') return r;
|
|
349
|
+
// Two passes: a bridge to one patch can bring the next patch within reach of a second bridge.
|
|
350
|
+
for (let pass = 0; pass < 2 && !r.ok && !r.from; pass++) {
|
|
351
|
+
const dir = files.place(place);
|
|
352
|
+
let fixes: ReturnType<typeof autoFixes>;
|
|
353
|
+
try {
|
|
354
|
+
const geometry = Geometry.parse(files.readJson(files.place(place, 'geometry.json')));
|
|
355
|
+
const floors = Layout.parse(files.readJson(files.place(place, 'layout.json'))).floors.map((f) => f.key);
|
|
356
|
+
// Walk-on masks: a bridge is drawn along stairs only when they reach both floors.
|
|
357
|
+
const masks: Record<string, Mask> = {};
|
|
358
|
+
for (const t of geometry.things) {
|
|
359
|
+
if (t.kind === 'walk-on' && t.mask && fs.existsSync(files.place(place, t.mask))) masks[t.key] = await maskFromPng(fs.readFileSync(files.place(place, t.mask)));
|
|
360
|
+
}
|
|
361
|
+
fixes = autoFixes(geometry, floors, r.issues, masks);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
fixes = [];
|
|
364
|
+
console.log(`${place}: could not look for a free fix: ${error instanceof Error ? error.message : error}`);
|
|
365
|
+
}
|
|
366
|
+
const before = readCorrections(dir);
|
|
367
|
+
const after = Corrections.parse(JSON.parse(JSON.stringify(before)));
|
|
368
|
+
for (const f of fixes) f.apply(after);
|
|
369
|
+
if (fixes.length === 0 || JSON.stringify(after) === JSON.stringify(before)) {
|
|
370
|
+
console.log(`\n# repair ${place}: no free fix for ${[...new Set(r.issues.map((i) => i.code))].join(', ')}; left for the agent`);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
writeCorrections(dir, after);
|
|
374
|
+
const start = fixes.some((f) => f.from === 'masks') ? 'masks' : 'derive';
|
|
375
|
+
console.log(`\n# repair ${place}: ${fixes.map((f) => f.what).join('; ')}`);
|
|
376
|
+
emit('repair.start', { place, by: 'auto', fixes: fixes.map((f) => f.what) });
|
|
377
|
+
for (const step of PLACE_STEPS.slice(PLACE_STEPS.indexOf(start))) {
|
|
378
|
+
if (!broken.has(place)) await runPlace(step, place);
|
|
379
|
+
}
|
|
380
|
+
r = reportOf(place)!;
|
|
381
|
+
repairs.push({ place, by: 'auto', fixes: fixes.map((f) => f.what), ok: r.ok });
|
|
382
|
+
emit('repair.end', { place, by: 'auto', ok: r.ok });
|
|
383
|
+
}
|
|
384
|
+
return recutCards(place, await paintMissing(place, await newTake(place, r)));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Do not paint a bridge to a snapped interaction spot and brush it open without measuring it.
|
|
388
|
+
// Paint now restores the plan's composition before masks; remaining gaps need observed floor evidence.
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* A card whose mask leaves out a painted part of its thing (CARD_CUT: the boat's bow, a lamp on its post) leaves
|
|
392
|
+
* that part smeared on the backdrop when the card slides. The flag's box says where; the thing's box is widened
|
|
393
|
+
* to take it in (as mask_fix box does), so masks asks SAM by the wider box and joins what it finds there that
|
|
394
|
+
* touches the thing; the card carries it and the backdrop is made again without it. Once per cut. A round dab
|
|
395
|
+
* brushed over the part instead took the sky round it too: a disc on the card, and a hover outline looping round it.
|
|
396
|
+
*/
|
|
397
|
+
async function recutCards(place: string, r: Report | null): Promise<Report | null> {
|
|
398
|
+
if (!r || r.from || repair === 'off') return r;
|
|
399
|
+
const file = files.place(place, 'verify.json');
|
|
400
|
+
if (!fs.existsSync(file)) return r;
|
|
401
|
+
const cuts = (files.readJson(file) as Verdict).flags.filter((f) => f.code === 'CARD_CUT' && f.key && f.box);
|
|
402
|
+
if (!cuts.length) return r;
|
|
403
|
+
const dir = files.place(place);
|
|
404
|
+
const before = readCorrections(dir);
|
|
405
|
+
const after = Corrections.parse(JSON.parse(JSON.stringify(before)));
|
|
406
|
+
const geometry = fs.existsSync(files.place(place, 'geometry.json')) ? Geometry.parse(files.readJson(files.place(place, 'geometry.json'))) : undefined;
|
|
407
|
+
const what: string[] = [];
|
|
408
|
+
for (const f of cuts) {
|
|
409
|
+
const was = after.things[f.key!]?.box ?? geometry?.things.find((t) => t.key === f.key)?.box;
|
|
410
|
+
if (!was) continue;
|
|
411
|
+
const [x0, y0, x1, y1] = f.box!;
|
|
412
|
+
const box: [number, number, number, number] = [Math.min(was[0], x0), Math.min(was[1], y0), Math.max(was[2], x1), Math.max(was[3], y1)].map(Math.round) as [number, number, number, number];
|
|
413
|
+
if (box.every((v, i) => v === Math.round(was[i]))) continue;
|
|
414
|
+
(after.things[f.key!] ??= { strokes: [] }).box = box;
|
|
415
|
+
what.push(`widened ${f.key}'s box to [${box.join(', ')}] to take in the part its card cut through at [${f.box!.join(', ')}]`);
|
|
416
|
+
}
|
|
417
|
+
if (!what.length) return r;
|
|
418
|
+
writeCorrections(dir, after);
|
|
419
|
+
console.log(`\n# repair ${place}: ${what.join('; ')}`);
|
|
420
|
+
emit('repair.start', { place, by: 'auto', fixes: what });
|
|
421
|
+
for (const step of PLACE_STEPS.slice(PLACE_STEPS.indexOf('masks'))) {
|
|
422
|
+
if (!broken.has(place)) await runPlace(step, place);
|
|
423
|
+
}
|
|
424
|
+
const next = reportOf(place) ?? r;
|
|
425
|
+
repairs.push({ place, by: 'auto', fixes: what, ok: next.ok });
|
|
426
|
+
emit('repair.end', { place, by: 'auto', ok: next.ok });
|
|
427
|
+
return next;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function newTake(place: string, r: Report | null): Promise<Report | null> {
|
|
431
|
+
if (!r || r.ok || r.from || !r.issues.some((i) => i.code === 'FLOOR_LOST')) return r;
|
|
432
|
+
const dir = files.place(place);
|
|
433
|
+
const before = readCorrections(dir);
|
|
434
|
+
const take = (before.take ?? 0) + 1;
|
|
435
|
+
if (take > maxTake) return r;
|
|
436
|
+
const after = Corrections.parse(JSON.parse(JSON.stringify(before)));
|
|
437
|
+
after.take = take;
|
|
438
|
+
after.repaint = after.repaint.filter((x) => !x.fix.startsWith(BUILD_REPAINT));
|
|
439
|
+
writeCorrections(dir, after);
|
|
440
|
+
const lost = r.issues.filter((i) => i.code === 'FLOOR_LOST').map((i) => i.key).join(', ');
|
|
441
|
+
const what = [`painted take ${take}: floor ${lost} was painted away from its layout (one Muse painting)`];
|
|
442
|
+
console.log(`\n# repair ${place}: ${what[0]}`);
|
|
443
|
+
emit('repair.start', { place, by: 'auto', fixes: what });
|
|
444
|
+
for (const step of PLACE_STEPS.slice(PLACE_STEPS.indexOf('paint'))) {
|
|
445
|
+
if (!broken.has(place)) await runPlace(step, place);
|
|
446
|
+
}
|
|
447
|
+
const next = reportOf(place) ?? r;
|
|
448
|
+
repairs.push({ place, by: 'auto', fixes: what, ok: next.ok });
|
|
449
|
+
emit('repair.end', { place, by: 'auto', ok: next.ok });
|
|
450
|
+
return next;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A thing the story needs that Spark finds nowhere in the painting: no mask fix can find what was never painted,
|
|
455
|
+
* so it gets one Muse edit at its layout box (the one repaint_region per place the agent may make unasked), and
|
|
456
|
+
* the place is rebuilt from paint. Once per thing: a repaint already asked for it is not asked again.
|
|
457
|
+
*/
|
|
458
|
+
async function paintMissing(place: string, r: Report | null): Promise<Report | null> {
|
|
459
|
+
if (!r || r.ok || r.from) return r;
|
|
460
|
+
const keys = [...new Set(r.issues.filter((i) => i.code === 'THING_NOT_PAINTED' && i.key).map((i) => i.key!))];
|
|
461
|
+
if (!keys.length) return r;
|
|
462
|
+
const dir = files.place(place);
|
|
463
|
+
const before = readCorrections(dir);
|
|
464
|
+
const after = Corrections.parse(JSON.parse(JSON.stringify(before)));
|
|
465
|
+
const asked: string[] = [];
|
|
466
|
+
try {
|
|
467
|
+
const layout = Layout.parse(files.readJson(files.place(place, 'layout.json')));
|
|
468
|
+
const listed = files.world().places.find((p) => p.key === place)?.things ?? [];
|
|
469
|
+
for (const key of keys) {
|
|
470
|
+
const t = layout.things.find((x) => x.key === key);
|
|
471
|
+
if (!t) continue;
|
|
472
|
+
const looks = (listed.find((x) => x.key === key)?.looks ?? key.replace(/-/g, ' ')).replace(/^(a|an|the)\s+/i, '');
|
|
473
|
+
const fix = `${BUILD_REPAINT} ${looks} here, clearly visible and whole, standing where it meets the floor, in the painting's own style and light; keep everything around it`;
|
|
474
|
+
if (after.repaint.some((x) => x.fix === fix)) continue;
|
|
475
|
+
// The layout box with room round it, so Muse can fit the thing in, at least 120 px each way.
|
|
476
|
+
const w = Math.max(120, t.width * 1.4), h = Math.max(120, t.height * 1.3);
|
|
477
|
+
const region: [number, number, number, number] = [
|
|
478
|
+
Math.max(0, Math.round(t.x - w / 2)), Math.max(0, Math.round(t.footY - h + 12)),
|
|
479
|
+
Math.min(1200, Math.round(t.x + w / 2)), Math.min(896, Math.round(t.footY + 24)),
|
|
480
|
+
];
|
|
481
|
+
after.repaint.push({ region, fix });
|
|
482
|
+
asked.push(key);
|
|
483
|
+
}
|
|
484
|
+
} catch (error) {
|
|
485
|
+
console.log(`${place}: could not ask for a repaint: ${error instanceof Error ? error.message : error}`);
|
|
486
|
+
return r;
|
|
487
|
+
}
|
|
488
|
+
if (!asked.length) return r;
|
|
489
|
+
writeCorrections(dir, after);
|
|
490
|
+
const what = asked.map((k) => `repainted ${k} at its layout box (one Muse edit)`);
|
|
491
|
+
console.log(`\n# repair ${place}: ${what.join('; ')}`);
|
|
492
|
+
emit('repair.start', { place, by: 'auto', fixes: what });
|
|
493
|
+
for (const step of PLACE_STEPS.slice(PLACE_STEPS.indexOf('paint'))) {
|
|
494
|
+
if (!broken.has(place)) await runPlace(step, place);
|
|
495
|
+
}
|
|
496
|
+
const next = reportOf(place) ?? r;
|
|
497
|
+
repairs.push({ place, by: 'auto', fixes: what, ok: next.ok });
|
|
498
|
+
emit('repair.end', { place, by: 'auto', ok: next.ok });
|
|
499
|
+
return next;
|
|
500
|
+
}
|
|
501
|
+
console.log(`\nbuild ${slug}: ${range.join(' > ')} done · ${files.path('build.json')}`);
|
|
502
|
+
},
|
|
503
|
+
});
|