quoxcli 0.3.1 → 0.5.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/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {activityView as baseView} from '../hyperion-v3/activity.mjs';
|
|
4
|
+
|
|
5
|
+
export const CHARACTERS=Object.freeze({
|
|
6
|
+
lucid:{name:'Lucid',version:'recovery V5',review:'../../design/lucid/recovery/review.mjs',power:'sweep',powerName:'Horizon Sweep',powerDuration:3200,arrival:{scene:'attach',duration:3200}},
|
|
7
|
+
hyperion:{name:'Hyperion',version:'V2 acting',review:'../../design/hyperion/v2/review.mjs',power:'cluster',powerName:'Cluster Forge',powerDuration:4800,arrival:{scene:'attach',duration:3400}},
|
|
8
|
+
seer:{name:'Seer',version:'V4 flowing eight',review:'../../design/seer/v4/review.mjs',power:'pattern',powerName:'Pattern Sight',powerDuration:5200,arrival:{scene:'swim',duration:5800}},
|
|
9
|
+
sentinel:{name:'Sentinel',version:'A solid (provisional)',review:'../../design/stage3/companion-review.mjs',power:'scorch',powerName:'Aegis Inferno',powerDuration:3800,arrival:{scene:'greeting',duration:2400}},
|
|
10
|
+
cipher:{name:'Cipher',version:'V2 network acting',review:'../../design/cipher/v2/review.mjs',power:'trace',powerName:'Trace Route',powerDuration:4800,arrival:{scene:'attach',duration:3200}},
|
|
11
|
+
vulcan:{name:'Vulcan',version:'V3 forge salute',review:'../../design/vulcan/v3/review.mjs',power:'forge',powerName:'Brimstone Forge',powerDuration:9000,arrival:{scene:'attach',duration:3600}},
|
|
12
|
+
quoxperticus:{name:'Quoxperticus',version:'V4 fleet conductor',review:'../../design/quoxperticus/v4/review.mjs',power:'orchestrate',powerName:'Fleet Conductor',powerDuration:10000,arrival:{scene:'attach',duration:3000}},
|
|
13
|
+
drago:{name:'Drago',version:'V4 scoped inferno',review:'../../design/drago/v4/review.mjs',power:'fire',powerName:'Scoped Inferno',powerDuration:6800,arrival:{scene:'attach',duration:3800}},
|
|
14
|
+
});
|
|
15
|
+
const ids=Object.keys(CHARACTERS);
|
|
16
|
+
// Host scene -> art clip. Sentinel never shows 'all-clear' (needs verified evidence); Drago has no 'proud'/'curious'/'focus' clips.
|
|
17
|
+
const SCENES=Object.freeze({
|
|
18
|
+
sentinel:{curious:'scan',focus:'scan',concerned:'failure',proud:'idle'},
|
|
19
|
+
// Cipher claims no network facts: 'restored'/'delay'/'broken' routes are simulated, so completion stays on plain listening.
|
|
20
|
+
cipher:{curious:'inspect',focus:'inspect',standby:'idle',concerned:'broken',proud:'idle'},
|
|
21
|
+
// Vulcan has only idle, fire and forge clips besides the entrance. 'fire' is a decorative flourish, so nothing here claims a build or a fault: every host mood rests on idle and the forge is the working power.
|
|
22
|
+
vulcan:{curious:'idle',focus:'idle',standby:'idle',concerned:'idle',proud:'idle'},
|
|
23
|
+
// Quoxperticus: 'skill' (select a skill) stands for attention; the fleet scene is simulated, so it only appears as the working power.
|
|
24
|
+
quoxperticus:{curious:'skill',focus:'skill',standby:'idle',concerned:'idle',proud:'idle'},
|
|
25
|
+
drago:{curious:'inspect',focus:'active',concerned:'concerned',proud:'acknowledge'},
|
|
26
|
+
});
|
|
27
|
+
const SENTINEL_MOOD=Object.freeze({idle:'cute',greeting:'cute',standby:'cute',scan:'alert',approval:'alert',failure:'alert',warden:'warden',brace:'warden',scorch:'warden'});
|
|
28
|
+
export function character(id){
|
|
29
|
+
if(!Object.hasOwn(CHARACTERS,id))throw new Error(`Choose ${ids.slice(0,-1).join(', ')} or ${ids.at(-1)}.`);
|
|
30
|
+
return CHARACTERS[id];
|
|
31
|
+
}
|
|
32
|
+
export function modulePath(id){character(id);return fileURLToPath(new URL(`./${id}.mjs`,import.meta.url));}
|
|
33
|
+
export function reviewPath(id){return fileURLToPath(new URL(character(id).review,import.meta.url));}
|
|
34
|
+
/** Brief compact entrance on attach. Host facts that need attention (approval, issue) always pre-empt it. */
|
|
35
|
+
export function arrivalView(id,state,now){
|
|
36
|
+
const {arrival}=character(id),elapsed=now-state.started;
|
|
37
|
+
if(!arrival||!(elapsed>=0)||elapsed>=arrival.duration)return null;
|
|
38
|
+
if(state.approvals.length||state.issue||state.terminal)return null;
|
|
39
|
+
return {scene:arrival.scene,time:elapsed,motionTime:elapsed,caption:state.phase==='connecting'?'Connecting…':'Following the conversation.'};
|
|
40
|
+
}
|
|
41
|
+
export function activityView(id,state,now,{arrival=false}={}){
|
|
42
|
+
const spec=character(id);
|
|
43
|
+
const entrance=arrival?arrivalView(id,state,now):null;
|
|
44
|
+
if(entrance)return entrance;
|
|
45
|
+
const v=baseView(state,now);
|
|
46
|
+
if(v.scene==='cluster')return {...v,scene:spec.power,time:v.time*spec.powerDuration/4800,caption:`Working · ${spec.powerName} visual`};
|
|
47
|
+
if(id==='lucid')return {...v,scene:({concerned:'uncertain',proud:'delighted'})[v.scene]??v.scene};
|
|
48
|
+
if(SCENES[id])return {...v,scene:(id==='sentinel'&&state.approvals.length?'approval':SCENES[id][v.scene])??v.scene};
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
export async function loadCharacter(id){
|
|
52
|
+
const spec=character(id);
|
|
53
|
+
if(id==='lucid'){
|
|
54
|
+
const [{parseMaster},{renderLucidCharacter}]=await Promise.all([
|
|
55
|
+
import('../../design/lucid/recovery/render.mjs'),import('../../design/lucid/recovery/character.mjs'),
|
|
56
|
+
]);
|
|
57
|
+
const rig=parseMaster(await readFile(new URL('../../design/lucid/recovery/lucid-master.svg',import.meta.url),'utf8'));
|
|
58
|
+
return {...spec,render:(scene,time,options)=>renderLucidCharacter(rig,scene,time,options)};
|
|
59
|
+
}
|
|
60
|
+
if(id==='sentinel'){
|
|
61
|
+
const [{renderSentinelCharacter},{renderSentinelIcon}]=await Promise.all([
|
|
62
|
+
import('../../design/stage3/sentinel-character.js'),import('../../design/stage3/sentinel-icon.js'),
|
|
63
|
+
]);
|
|
64
|
+
return {...spec,render:(scene,time,{cols,rows,mono=false,still=false}={})=>{
|
|
65
|
+
// The authored character refuses below 48x16 cells (shrinking it was never approved). Smaller slots get the approved shield icon.
|
|
66
|
+
if(cols<48||rows<16){
|
|
67
|
+
const h=Math.max(6,Math.min(rows,10)),w=Math.max(12,Math.min(cols,h*2));
|
|
68
|
+
return renderSentinelIcon({cols:w,rows:h,mono,mood:SENTINEL_MOOD[scene]??'cute'});
|
|
69
|
+
}
|
|
70
|
+
// Scan is not a loop; bounce it so a long job keeps sweeping without a restart pop.
|
|
71
|
+
const t=scene==='scan'&&!still?(u=>u<2500?u:5000-u)(time%5000):time;
|
|
72
|
+
return renderSentinelCharacter(scene,t,{cols,rows,mono,still});
|
|
73
|
+
}};
|
|
74
|
+
}
|
|
75
|
+
const {parseSvgRig}=await import('../../factory/svg-rig.mjs');
|
|
76
|
+
if(id==='cipher'){
|
|
77
|
+
const [rig,{renderCipher}]=await Promise.all([
|
|
78
|
+
readFile(new URL('../../design/cipher/v1/cipher-master.svg',import.meta.url),'utf8').then(parseSvgRig),
|
|
79
|
+
import('../../design/cipher/v2/character.mjs'),
|
|
80
|
+
]);
|
|
81
|
+
return {...spec,render:(scene,time,options)=>renderCipher(rig,scene,time,options)};
|
|
82
|
+
}
|
|
83
|
+
if(id==='vulcan'){
|
|
84
|
+
const [rig,{renderVulcan}]=await Promise.all([
|
|
85
|
+
readFile(new URL('../../design/vulcan/v1/vulcan-master.svg',import.meta.url),'utf8').then(parseSvgRig),
|
|
86
|
+
import('../../design/vulcan/v3/character.mjs'),
|
|
87
|
+
]);
|
|
88
|
+
return {...spec,render:(scene,time,options)=>renderVulcan(rig,scene,time,options)};
|
|
89
|
+
}
|
|
90
|
+
if(id==='quoxperticus'){
|
|
91
|
+
const [rig,{renderQuoxperticus}]=await Promise.all([
|
|
92
|
+
readFile(new URL('../../design/quoxperticus/v2/quoxperticus-master.svg',import.meta.url),'utf8').then(parseSvgRig),
|
|
93
|
+
import('../../design/quoxperticus/v4/character.mjs'),
|
|
94
|
+
]);
|
|
95
|
+
return {...spec,render:(scene,time,options)=>renderQuoxperticus(rig,scene,time,options)};
|
|
96
|
+
}
|
|
97
|
+
if(id==='drago'){
|
|
98
|
+
const [rig,{renderDrago}]=await Promise.all([
|
|
99
|
+
readFile(new URL('../../design/drago/v1/drago-master.svg',import.meta.url),'utf8').then(parseSvgRig),
|
|
100
|
+
import('../../design/drago/v4/character.mjs'),
|
|
101
|
+
]);
|
|
102
|
+
return {...spec,render:(scene,time,options)=>renderDrago(rig,scene,time,options)};
|
|
103
|
+
}
|
|
104
|
+
const source=id==='seer'?'../../design/seer/v1/seer-master.svg':'../../design/hyperion/v1/hyperion-master.svg';
|
|
105
|
+
const rig=parseSvgRig(await readFile(new URL(source,import.meta.url),'utf8'));
|
|
106
|
+
const render=id==='seer'?(await import('../../design/seer/v4/character.mjs')).renderSeer:(await import('../../design/hyperion/v2/character.mjs')).renderHyperion;
|
|
107
|
+
return {...spec,render:(scene,time,options)=>render(rig,scene,time,options)};
|
|
108
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {composeMessage} from '../../design/lucid/recovery/layout.mjs';
|
|
3
|
+
import {character,CHARACTERS} from './characters.mjs';
|
|
4
|
+
const ENTRANCE_MS=Object.fromEntries(Object.entries(CHARACTERS).map(([k,v])=>[k,v.arrival.duration]));
|
|
5
|
+
import {loadDragonView} from './view.mjs';
|
|
6
|
+
import {initialActivity,activityEvent} from '../hyperion-v3/activity.mjs';
|
|
7
|
+
|
|
8
|
+
/** Shared display lifecycle. Host events choose expressions; art makes no decisions. */
|
|
9
|
+
export async function createDragonCompanion(id,{output=process.stdout,now=()=>performance.now(),still=false,arrival=true,mono=false,persist=true}={}){
|
|
10
|
+
character(id);
|
|
11
|
+
// Piped output does not need to load or render any art.
|
|
12
|
+
const actor=output.isTTY?await loadDragonView(id,{still,arrival,mono}):null;
|
|
13
|
+
let state=initialActivity(now()),height=0,boundary=true,closed=false,disabled=false,timer,closing;
|
|
14
|
+
const erase=()=>{
|
|
15
|
+
if(!height)return;
|
|
16
|
+
output.write(`\x1b[${height}A\r`);
|
|
17
|
+
for(let i=0;i<height;i++)output.write('\x1b[2K\n');
|
|
18
|
+
output.write(`\x1b[${height}A\r`);height=0;
|
|
19
|
+
};
|
|
20
|
+
const draw=()=>{
|
|
21
|
+
if(closed||disabled||!output.isTTY||!boundary)return;
|
|
22
|
+
const cols=output.columns||80,rows=output.rows||24;
|
|
23
|
+
if(rows<5||cols<16)return;
|
|
24
|
+
const started=state.started;
|
|
25
|
+
// greeting-only: after the entrance the block is withdrawn for good (host text is untouched).
|
|
26
|
+
if(!persist&&!still&&now()-started>=ENTRANCE_MS[id]&&!state.approvals.length){erase();disabled=true;clearInterval(timer);return;}
|
|
27
|
+
const v=actor.view(state,now(),{cols,rows});
|
|
28
|
+
const lines=composeMessage(v.frame,actor.name.toUpperCase()+'\n'+v.caption,{terminalCols:cols});
|
|
29
|
+
if(lines.length>=rows)return;
|
|
30
|
+
erase();output.write(lines.join('\n')+'\n');height=lines.length;
|
|
31
|
+
};
|
|
32
|
+
const safeDraw=()=>{try{draw();}catch{disabled=true;clearInterval(timer);try{erase();}catch{}}};
|
|
33
|
+
// Reflow invalidates old cursor coordinates. Do not erase after resize.
|
|
34
|
+
const resize=()=>{height=0;disabled=true;clearInterval(timer);};
|
|
35
|
+
output.on?.('resize',resize);
|
|
36
|
+
safeDraw();
|
|
37
|
+
if(output.isTTY&&!disabled){timer=setInterval(safeDraw,1000/6);timer.unref?.();}
|
|
38
|
+
return {
|
|
39
|
+
event(event){if(!closed&&!closing){state=activityEvent(state,event,now());safeDraw();}},
|
|
40
|
+
write(text){
|
|
41
|
+
if(closed)return;
|
|
42
|
+
erase();output.write(text);
|
|
43
|
+
if(text.length)boundary=text.endsWith('\n');
|
|
44
|
+
if(boundary)safeDraw();
|
|
45
|
+
},
|
|
46
|
+
suspend(){erase();disabled=true;clearInterval(timer);},
|
|
47
|
+
close(){
|
|
48
|
+
if(closing)return closing;
|
|
49
|
+
// Set closing before starting async work so concurrent closes share cleanup.
|
|
50
|
+
closing=Promise.resolve().then(async()=>{
|
|
51
|
+
state=activityEvent(state,{type:'stream.closed'},now());
|
|
52
|
+
try{
|
|
53
|
+
if(output.isTTY&&!disabled&&!still&&state.phase==='complete'&&!state.issue&&!state.approvals.length){
|
|
54
|
+
const remaining=Math.max(0,2600-(now()-state.changed));
|
|
55
|
+
if(remaining)await new Promise(resolve=>setTimeout(resolve,remaining));
|
|
56
|
+
}
|
|
57
|
+
safeDraw();
|
|
58
|
+
}finally{closed=true;clearInterval(timer);output.off?.('resize',resize);}
|
|
59
|
+
});
|
|
60
|
+
return closing;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {artSlot} from '../../design/lucid/recovery/layout.mjs';
|
|
2
|
+
import {initialActivity,activityEvent} from '../hyperion-v3/activity.mjs';
|
|
3
|
+
import {character,loadCharacter,activityView} from './characters.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure per-frame view for hosts that own their own screen (Quoxon's Ink, the CLI block).
|
|
7
|
+
* Same reducer, same renderer, same priorities as the line-based companion; no timers, no terminal control.
|
|
8
|
+
* view() returns {frame,caption,slot,name,scene}; frame is null when the terminal is too small for art.
|
|
9
|
+
*/
|
|
10
|
+
export async function loadDragonView(id,{still=false,arrival=true,mono=false,persist=true}={}){
|
|
11
|
+
character(id);
|
|
12
|
+
const actor=await loadCharacter(id);
|
|
13
|
+
return {
|
|
14
|
+
id,name:actor.name,arrivalMs:character(id).arrival.duration,
|
|
15
|
+
initial:initialActivity,
|
|
16
|
+
reduce:activityEvent,
|
|
17
|
+
view(state,now,{cols=80,rows=24}={}){
|
|
18
|
+
// Greeting-only: withdrawn for good once the entrance is over, unless an approval is pending.
|
|
19
|
+
if(!persist&&!still&&now-state.started>=character(id).arrival.duration&&!state.approvals.length)return {frame:null,caption:null,hidden:true,name:actor.name,scene:null};
|
|
20
|
+
const slot=artSlot(cols,Math.max(1,rows));
|
|
21
|
+
if(id==='seer'||id==='cipher'||id==='vulcan'||id==='quoxperticus'){slot.cols=Math.min(39,slot.cols);slot.rows=Math.min(11,slot.rows);}
|
|
22
|
+
const v=activityView(id,state,now,{arrival:arrival&&!still});
|
|
23
|
+
const result=slot.showArt?actor.render(v.scene,v.time,{...slot,still,mono,motionTime:v.motionTime}):null;
|
|
24
|
+
if(result&&!result.ok)throw new Error(result.code);
|
|
25
|
+
return {frame:result?.frame??null,caption:v.caption,slot,name:actor.name,scene:v.scene};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Host facts select expressions; the renderer never infers operational truth.
|
|
2
|
+
export function initialActivity(now = 0) {
|
|
3
|
+
return { started: now, changed: now, received: now, turnId: null, phase: 'connecting',
|
|
4
|
+
approvals: [], tools: [], issue: false, terminal: false };
|
|
5
|
+
}
|
|
6
|
+
const id = x => typeof x === 'string' && x.length <= 256 ? x : null;
|
|
7
|
+
export function activityEvent(s, e, now) {
|
|
8
|
+
if (!e || typeof e !== 'object' || !Number.isFinite(now) || now < s.received) return s;
|
|
9
|
+
if (e.turn_id && s.turnId && e.turn_id !== s.turnId && e.type !== 'turn.started') return s;
|
|
10
|
+
let n = { ...s, received: now };
|
|
11
|
+
const phase = p => { if (n.phase !== p) { n.phase = p; n.changed = now; } };
|
|
12
|
+
const key = id(e.call_id) || id(e.approval_id);
|
|
13
|
+
switch (e.type) {
|
|
14
|
+
case 'turn.started':
|
|
15
|
+
if (s.terminal || (id(e.turn_id) && e.turn_id !== s.turnId)) n = { ...initialActivity(now), approvals: s.approvals };
|
|
16
|
+
n.turnId = id(e.turn_id) || s.turnId; phase('thinking'); break;
|
|
17
|
+
case 'connected': phase('thinking'); break;
|
|
18
|
+
case 'thinking':
|
|
19
|
+
if (!s.terminal) phase(String(e.stage || '').includes('tool') ? 'working' : 'thinking'); break;
|
|
20
|
+
case 'chunk': case 'text.delta':
|
|
21
|
+
if (!s.terminal) phase('speaking'); break;
|
|
22
|
+
case 'tool.started':
|
|
23
|
+
if (s.terminal) return s;
|
|
24
|
+
if (key && !n.tools.includes(key)) n.tools = [...n.tools, key].slice(-256);
|
|
25
|
+
phase('working'); break;
|
|
26
|
+
case 'tool.finished':
|
|
27
|
+
if (s.terminal) return s;
|
|
28
|
+
n.tools = n.tools.filter(k => k !== key);
|
|
29
|
+
// Unknown and effect_unknown results are never promoted to success.
|
|
30
|
+
if (e.status !== 'ok') n.issue = true;
|
|
31
|
+
phase(n.tools.length ? 'working' : 'thinking'); break;
|
|
32
|
+
case 'approval.requested':
|
|
33
|
+
if (!key) return s;
|
|
34
|
+
if (!n.approvals.includes(key)) n.approvals = [...n.approvals, key].slice(-256);
|
|
35
|
+
break;
|
|
36
|
+
case 'approval.resolved':
|
|
37
|
+
if (!key || !n.approvals.includes(key)) return s;
|
|
38
|
+
n.approvals = n.approvals.filter(k => k !== key);
|
|
39
|
+
if (e.decision !== 'approved') n.issue = true;
|
|
40
|
+
break;
|
|
41
|
+
case 'error': n.issue = true; n.terminal = true; phase('error'); break;
|
|
42
|
+
case 'turn.finished':
|
|
43
|
+
if (s.terminal) return s;
|
|
44
|
+
n.terminal = true;
|
|
45
|
+
if (e.status !== 'completed' || n.tools.length) n.issue = true;
|
|
46
|
+
phase(e.status === 'completed' ? 'complete' : 'error'); break;
|
|
47
|
+
case 'done':
|
|
48
|
+
// Legacy text completion is not a verified task outcome.
|
|
49
|
+
if (!s.terminal) phase('response-ready'); break;
|
|
50
|
+
case 'stream.closed':
|
|
51
|
+
if (!s.terminal) { n.terminal = true; phase(s.phase === 'response-ready' ? 'response-ready' : 'disconnected'); }
|
|
52
|
+
break;
|
|
53
|
+
default: return s;
|
|
54
|
+
}
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function activityView(s, now) {
|
|
59
|
+
const age = Math.max(0, now - s.changed);
|
|
60
|
+
const view = (scene, caption, time = age) => ({ scene, caption, time, motionTime: Math.max(0, now - s.started) });
|
|
61
|
+
if (s.approvals.length) return view('curious', 'Waiting for your approval.');
|
|
62
|
+
if (s.issue) return view('concerned', 'Something needs attention.');
|
|
63
|
+
if (s.phase === 'disconnected') return view('standby', 'Stream ended; outcome unknown.');
|
|
64
|
+
if (!s.terminal && now - s.received > 30000) return view('standby', 'No recent activity update.');
|
|
65
|
+
if (s.phase === 'complete') return age < 2600 ? view('proud', 'Turn completed.') : view('idle', 'Ready for the next message.');
|
|
66
|
+
if (s.phase === 'response-ready') return view('idle', 'Response received.');
|
|
67
|
+
if (s.phase === 'connecting') return view('standby', 'Connecting…');
|
|
68
|
+
if (age < 250) return view('idle', 'Following the conversation.');
|
|
69
|
+
if (s.tools.length || s.phase === 'working') {
|
|
70
|
+
// A restrained visual flourish, at most once each 30 seconds of work.
|
|
71
|
+
const t = age % 30000;
|
|
72
|
+
return t >= 5000 && t < 9800 ? view('cluster', 'Working · Cluster Forge visual', t - 5000) : view('focus', 'Working.');
|
|
73
|
+
}
|
|
74
|
+
if (s.phase === 'thinking') return view('curious', 'Thinking.');
|
|
75
|
+
return view('focus', 'Responding.');
|
|
76
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transparent, terminal-cell-native signature power renderer.
|
|
3
|
+
*
|
|
4
|
+
* Draws Braille dots in a separate compositing layer. Does not alter Quox's
|
|
5
|
+
* vendored renderer, write terminal escapes, set backgrounds, use randomness
|
|
6
|
+
* or perform actions. Times are deterministic milliseconds.
|
|
7
|
+
*
|
|
8
|
+
* Coordinates are in terminal cells; 2x4 Braille dots per cell.
|
|
9
|
+
*/
|
|
10
|
+
import { POWER_BY_SCENE } from './power-registry.js';
|
|
11
|
+
|
|
12
|
+
const PI=Math.PI, TAU=2*PI;
|
|
13
|
+
const clamp=(x,a=0,b=1)=>Math.max(a,Math.min(b,x));
|
|
14
|
+
const ease=(u)=>{const q=clamp(u);return q*q*(3-2*q);};
|
|
15
|
+
const phase=(t,a,b)=>ease((t-a)/Math.max(1,b-a));
|
|
16
|
+
const rgb={
|
|
17
|
+
azure:[73,163,255],shield:[100,205,255],burn:[91,224,255],
|
|
18
|
+
threat:[250,87,91],ember:[255,165,78],gold:[255,190,86],
|
|
19
|
+
teal:[76,232,182],green:[85,245,153],
|
|
20
|
+
violet:[183,140,255],muted:[147,155,184],white:[225,236,255],
|
|
21
|
+
};
|
|
22
|
+
const bit=[
|
|
23
|
+
[0x01,0x08],
|
|
24
|
+
[0x02,0x10],
|
|
25
|
+
[0x04,0x20],
|
|
26
|
+
[0x40,0x80],
|
|
27
|
+
];
|
|
28
|
+
const fakeHash=n=>{
|
|
29
|
+
let x=Math.imul((n|0)^0x9e3779b9,0x85ebca6b);
|
|
30
|
+
x^=x>>>13;x=Math.imul(x,0xc2b2ae35);x^=x>>>16;
|
|
31
|
+
return (x>>>0)/4294967296;
|
|
32
|
+
};
|
|
33
|
+
function empty(c){return !c||c.ch===' '||c.ch==='\u2800';}
|
|
34
|
+
|
|
35
|
+
export function spriteBounds(frame){
|
|
36
|
+
const w=frame[0]?.length||0,h=frame.length;
|
|
37
|
+
let minX=w,maxX=0,minY=h,maxY=0,n=0;
|
|
38
|
+
for(let y=0;y<h;y++)for(let x=0;x<w;x++){
|
|
39
|
+
if(empty(frame[y][x]))continue;
|
|
40
|
+
minX=Math.min(minX,x);maxX=Math.max(maxX,x);
|
|
41
|
+
minY=Math.min(minY,y);maxY=Math.max(maxY,y);n++;
|
|
42
|
+
}
|
|
43
|
+
return n?{minX,maxX,minY,maxY,centerX:(minX+maxX)/2,centerY:(minY+maxY)/2,visible:n}:
|
|
44
|
+
{minX:Math.floor(w*.35),maxX:Math.floor(w*.65),minY:Math.floor(h*.2),maxY:Math.floor(h*.8),centerX:w/2,centerY:h/2,visible:0};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createCanvas(w,h,{mono=false,theme='dark'}={}){
|
|
48
|
+
const masks=new Uint8Array(w*h);
|
|
49
|
+
const ink=Array(w*h).fill(null);
|
|
50
|
+
const labels=new Map();
|
|
51
|
+
const col=c=>{
|
|
52
|
+
if(mono)return null;
|
|
53
|
+
if(theme==='light')return c.map(v=>Math.round(v*.68));
|
|
54
|
+
return c;
|
|
55
|
+
};
|
|
56
|
+
function dot(dx,dy,c=rgb.white){
|
|
57
|
+
const x=Math.round(dx),y=Math.round(dy);
|
|
58
|
+
// Keep a blank perimeter. Never put anything in the final column.
|
|
59
|
+
if(x<2||y<1||x>=w*2-2||y>=h*4-1)return;
|
|
60
|
+
const cx=Math.floor(x/2),cy=Math.floor(y/4),i=cy*w+cx;
|
|
61
|
+
masks[i]|=bit[y%4][x%2];ink[i]=col(c);
|
|
62
|
+
}
|
|
63
|
+
function point(x,y,c=rgb.white,r=0){
|
|
64
|
+
const xx=x*2,yy=y*4,rr=Math.max(0,r);
|
|
65
|
+
if(rr===0){dot(xx,yy,c);return;}
|
|
66
|
+
for(let dx=-rr;dx<=rr;dx++)for(let dy=-rr;dy<=rr;dy++){
|
|
67
|
+
if(dx*dx+dy*dy<=rr*rr+.15)dot(xx+dx,yy+dy,c);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function line(ax,ay,bx,by,c=rgb.white,thickness=0){
|
|
71
|
+
const dx=(bx-ax)*2,dy=(by-ay)*4,steps=Math.max(1,Math.ceil(Math.hypot(dx,dy)*1.8));
|
|
72
|
+
for(let i=0;i<=steps;i++){
|
|
73
|
+
const p=i/steps;
|
|
74
|
+
point(ax+(bx-ax)*p,ay+(by-ay)*p,c,thickness);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function arc(cx,cy,rx,ry,a0,a1,c=rgb.white,thickness=0){
|
|
78
|
+
const steps=Math.max(12,Math.ceil((Math.abs(rx)+Math.abs(ry)*2)*Math.abs(a1-a0)*2));
|
|
79
|
+
for(let i=0;i<=steps;i++){
|
|
80
|
+
const a=a0+(a1-a0)*(i/steps);
|
|
81
|
+
point(cx+Math.cos(a)*rx,cy+Math.sin(a)*ry,c,thickness);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function spark(cx,cy,t,r,c=rgb.white){
|
|
85
|
+
if(r<=0)return;
|
|
86
|
+
for(let k=0;k<7;k++){
|
|
87
|
+
const a=k*TAU/7+t*.0023;
|
|
88
|
+
line(cx+Math.cos(a)*.2,cy+Math.sin(a)*.14,
|
|
89
|
+
cx+Math.cos(a)*r,cy+Math.sin(a)*r*.5,c);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function glyph(x,y,ch,c=rgb.white){
|
|
93
|
+
const xx=Math.round(x),yy=Math.round(y);
|
|
94
|
+
if(xx<1||yy<0||xx>=w-1||yy>=h)return;
|
|
95
|
+
labels.set(yy*w+xx,{ch,rgb:col(c),bg:null});
|
|
96
|
+
}
|
|
97
|
+
function compose(base){
|
|
98
|
+
return base.map((row,y)=>row.map((cell,x)=>{
|
|
99
|
+
if(!empty(cell))return cell;
|
|
100
|
+
const i=y*w+x,mark=labels.get(i);
|
|
101
|
+
if(mark)return mark;
|
|
102
|
+
return masks[i]?{ch:String.fromCodePoint(0x2800+masks[i]),rgb:ink[i],bg:null}:cell;
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
return {w,h,point,line,arc,spark,glyph,compose};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function positions(g){
|
|
109
|
+
const {minX,maxX,centerY}=g;
|
|
110
|
+
// Accent zone on left of the body, where the Q usually has room for fire.
|
|
111
|
+
return {
|
|
112
|
+
left:minX,
|
|
113
|
+
near:Math.max(3,minX-3),
|
|
114
|
+
mouth:minX+Math.max(5,Math.min(12,(maxX-minX)*.29)),
|
|
115
|
+
y:centerY+1,
|
|
116
|
+
mx:(minX+maxX)/2,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function sentinel(canvas,g,t,anchors){
|
|
120
|
+
const {point,line,arc,spark}=canvas,p=positions(g);
|
|
121
|
+
const {left}=p,y=anchors?.mouth?.y??p.y;
|
|
122
|
+
const mouthX=anchors?.mouth?.x??p.mouth;
|
|
123
|
+
const h=canvas.h;
|
|
124
|
+
const approach=phase(t,100,920),barrier=phase(t,580,1060),flame=phase(t,1200,2800);
|
|
125
|
+
// Two approaching abstract threat fragments, never a person or actual target.
|
|
126
|
+
for(let k=0;k<3;k++){
|
|
127
|
+
const dx=Math.max(3,left-20+k*2), dest=Math.max(3,left-5);
|
|
128
|
+
const px=dx+(dest-dx)*approach;
|
|
129
|
+
const py=h*(.37+k*.115)+Math.sin(t*.003+k)*.3;
|
|
130
|
+
if(t<1650){
|
|
131
|
+
line(px-.7,py-.27,px+.7,py+.27,rgb.threat,1);
|
|
132
|
+
line(px-.3,py+.45,px+.65,py-.35,rgb.threat);
|
|
133
|
+
}else if(t<3200){
|
|
134
|
+
for(let j=0;j<7;j++){
|
|
135
|
+
const a=j*TAU/7+k*2, dist=(t-1600)/180*(.3+fakeHash(k*73+j)*.6);
|
|
136
|
+
const glow=t<2300?rgb.ember:rgb.muted;
|
|
137
|
+
point(dest+Math.cos(a)*dist,py+Math.sin(a)*dist*.42,glow);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Azure crescent shields the vulnerable LEFT face and flickers on collision.
|
|
142
|
+
if(t>480&&t<3480){
|
|
143
|
+
const sx=Math.max(4,Math.min(left+5,mouthX-2)),
|
|
144
|
+
rx=Math.max(2.5,Math.min(11,left*.34)),ry=Math.max(2.4,h*.36);
|
|
145
|
+
const sweep=phase(t,480,1000);
|
|
146
|
+
arc(sx,y,rx,ry,PI/2+(.6*(1-sweep)),3*PI/2-(.6*(1-sweep)),
|
|
147
|
+
t<1050?rgb.azure:rgb.shield, t>800&&t<1180?1:0);
|
|
148
|
+
if(t>800&&t<1520){
|
|
149
|
+
const intensity=1-phase(t,1120,1550);
|
|
150
|
+
spark(Math.max(2,sx-rx),y-.5,t,2.0*intensity,rgb.shield);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Forward-facing defensive blue fire. The Q's eyes/body are not overwritten.
|
|
154
|
+
// It is a DEMO of scorched hostile symbols, never an asserted mitigation.
|
|
155
|
+
if(t>1200&&t<3300){
|
|
156
|
+
const reach=Math.max(3,Math.min(25,mouthX-3));
|
|
157
|
+
for(let i=0;i<65;i++){
|
|
158
|
+
const f=i/65, born=1230+f*1550+(fakeHash(i*11+5)-.5)*180, age=t-born;
|
|
159
|
+
if(age<0||age>750)continue;
|
|
160
|
+
const u=clamp(age/700),dist=reach*u*(.8+fakeHash(i+9)*.3);
|
|
161
|
+
// Emitted at the actual dragon's snout, not at an assumed Q-ring.
|
|
162
|
+
const px=mouthX-dist,py=y-.2+Math.sin(i*3.17+u*11)*(.2+u*.85);
|
|
163
|
+
point(px,py,u<.45?rgb.burn:rgb.azure,(i%11===0)?1:0);
|
|
164
|
+
if(i%9===0)point(px-1,py-.2,rgb.white);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if(t>3250)point(left+5,y,rgb.azure,1);
|
|
168
|
+
}
|
|
169
|
+
function lucid(c,g,t){
|
|
170
|
+
const {point,arc,line,glyph}=c,p=positions(g),l=Math.max(3,p.left-4),y=c.h*.52;
|
|
171
|
+
const sweep=phase(t,450,2600);
|
|
172
|
+
// Observations radiate outward; the 'unknown' remains explicitly marked.
|
|
173
|
+
if(t>300){
|
|
174
|
+
for(let k=1;k<=3;k++){
|
|
175
|
+
const radius=2+k*2.2*sweep;
|
|
176
|
+
arc(l+1,y,radius,radius*.35,PI*.85,PI*1.9,rgb.violet);
|
|
177
|
+
}
|
|
178
|
+
line(l+1,y,l+1+Math.min(9,p.left*.42)*Math.cos(-1.9+sweep*TAU),
|
|
179
|
+
y+Math.min(3,c.h*.19)*Math.sin(-1.9+sweep*TAU),rgb.white);
|
|
180
|
+
if(t>1750){glyph(Math.max(2,l-4),Math.round(y)-2,'?',rgb.muted);point(Math.max(3,l-1),y+.4,rgb.violet);}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function hyperion(c,g,t){
|
|
184
|
+
const {line,point,arc,glyph}=c,p=positions(g);
|
|
185
|
+
const left=Math.max(3,p.left-12),y=c.h*.49;
|
|
186
|
+
const lift=phase(t,250,1100),built=phase(t,1100,2650);
|
|
187
|
+
// A tiny 3-D guest container built one edge at a time.
|
|
188
|
+
if(lift>0){
|
|
189
|
+
arc(p.left+3,y-1,Math.min(8,p.left*.25),Math.min(3,c.h*.2),PI,2*PI,rgb.gold);
|
|
190
|
+
}
|
|
191
|
+
const w=6+Math.min(5,p.left*.18),h=Math.min(3.2,c.h*.16);
|
|
192
|
+
line(left,y,left+w*built,y,rgb.gold);
|
|
193
|
+
line(left,y,left,y+h*built,rgb.gold);
|
|
194
|
+
if(t>1500){
|
|
195
|
+
line(left+w*built,y,left+w*built,y+h*built,rgb.ember);
|
|
196
|
+
line(left,y+h*built,left+w*built,y+h*built,rgb.ember);
|
|
197
|
+
}
|
|
198
|
+
if(t>2400&&t<3650){
|
|
199
|
+
const x=left+w/2,yy=y+h/2;point(x,yy,rgb.gold,1);glyph(Math.round(x),Math.round(yy),'◆',rgb.white);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function seer(c,g,t){
|
|
203
|
+
const {line,arc,point,glyph}=c,p=positions(g);
|
|
204
|
+
const x0=Math.max(2,p.left-21),x1=Math.max(x0+7,p.left-3),y=c.h*.53;
|
|
205
|
+
const progress=phase(t,350,2850);
|
|
206
|
+
const gap=x0+(x1-x0)*.58;
|
|
207
|
+
for(let i=0;i<7;i++){
|
|
208
|
+
const x=x0+(x1-x0)*i/6;
|
|
209
|
+
if((x-x0)/(x1-x0)>progress)continue;
|
|
210
|
+
if(Math.abs(x-gap)<2&&t<2200)continue;
|
|
211
|
+
point(x,y+Math.sin(i*.9)*.55,rgb.teal,1);
|
|
212
|
+
if(i&&i!==4&&progress>i/6)line(x-1.4,y+.1,x-.5,y+.1,rgb.green);
|
|
213
|
+
}
|
|
214
|
+
if(t>1600){
|
|
215
|
+
const rx=1.4+Math.sin(t/380)*.18;
|
|
216
|
+
arc(gap,y,rx,rx*.72,0,TAU,rgb.white);
|
|
217
|
+
if(t>2450)glyph(Math.round(gap),Math.round(y),'•',rgb.green);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function nova(c,g,t){
|
|
221
|
+
const {line,point,arc,spark}=c,p=positions(g);
|
|
222
|
+
const x0=Math.max(2,p.left-25),x1=Math.max(x0+8,p.left-2),y=c.h*.55;
|
|
223
|
+
const thrust=phase(t,500,2250),travel=x0+(x1-x0)*thrust;
|
|
224
|
+
for(let i=0;i<5;i++){
|
|
225
|
+
const x=x0+(x1-x0)*i/4;
|
|
226
|
+
point(x,y+(i%2)*.5,rgb.green,1);
|
|
227
|
+
if(i>0&&t>350)line(x-2,y,x-1,y,rgb.teal);
|
|
228
|
+
}
|
|
229
|
+
if(t>450&&t<2750){
|
|
230
|
+
spark(travel,y+.1,t,1.4,rgb.green);
|
|
231
|
+
for(let i=0;i<9;i++){
|
|
232
|
+
const trail=travel-i*1.1;
|
|
233
|
+
if(trail<=x0-2)continue;
|
|
234
|
+
point(trail,y+.4*Math.sin(t*.013+i),i%3?rgb.green:rgb.white);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if(t>2200)arc(x1,y,2.2,1.2,0,TAU*.8,rgb.green);
|
|
238
|
+
}
|
|
239
|
+
function vulcan(c,g,t){
|
|
240
|
+
const {point,line,spark,glyph}=c,p=positions(g);
|
|
241
|
+
const x=Math.max(4,p.left-13),y=c.h*.49;
|
|
242
|
+
glyph(x,Math.round(y),'〈',rgb.teal);glyph(x+8,Math.round(y),'〉',rgb.teal);
|
|
243
|
+
line(x+1,y+2,x+7,y+2,rgb.muted);
|
|
244
|
+
if(t>500&&t<1900){
|
|
245
|
+
const q=phase(t,500,1600);
|
|
246
|
+
line(x+4,y-3,x+4-q*2,y-1+q*3,rgb.teal,1);
|
|
247
|
+
point(x+4-q*2,y-1+q*3,rgb.white);
|
|
248
|
+
}
|
|
249
|
+
if(t>1600&&t<2700){
|
|
250
|
+
spark(x+4,y+2,t,2.1*(1-phase(t,2100,2700)),rgb.ember);
|
|
251
|
+
point(x+4,y+2,rgb.white,1);
|
|
252
|
+
}
|
|
253
|
+
if(t>2750) {
|
|
254
|
+
for(let i=0;i<3;i++)point(x+2+i*2,y+1.5,rgb.teal);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function quox(c,g,t){
|
|
258
|
+
const {arc,point,line,spark}=c,p=positions(g);
|
|
259
|
+
const left=Math.max(4,p.left-13),cx=Math.max(5,p.left-6),cy=c.h*.5;
|
|
260
|
+
for(let i=0;i<4;i++){
|
|
261
|
+
const a=i*TAU/4+phase(t,450,2850)*TAU*.8;
|
|
262
|
+
const r=3.5+phase(t,300,1650)*3;
|
|
263
|
+
const tx=cx+Math.cos(a)*r*1.7,ty=cy+Math.sin(a)*r*.8;
|
|
264
|
+
if(t>350) {
|
|
265
|
+
point(tx,ty,i%2?rgb.violet:rgb.azure,1);
|
|
266
|
+
if(t>1200&&t<3100)line(tx,ty,cx,cy,rgb.muted);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if(t>2100&&t<3300)spark(cx,cy,t,2.4,rgb.white);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const DRAWERS=Object.freeze({
|
|
273
|
+
quox,lucid,hyperion,seer,sentinel,nova,vulcan,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Visual-only signature action. Base frame is immutable.
|
|
278
|
+
* Even at 32 columns, clamps effects to a blank perimeter.
|
|
279
|
+
* A static image can be requested by using still=true.
|
|
280
|
+
*/
|
|
281
|
+
export function renderPower(scene,t,base,{still=false,theme='dark',mono=false,anchors=null}={}){
|
|
282
|
+
const profile=POWER_BY_SCENE.get(scene);
|
|
283
|
+
if(!profile)throw new RangeError('Unknown power: '+scene);
|
|
284
|
+
if(!Array.isArray(base)||!base.length||!Array.isArray(base[0]))
|
|
285
|
+
throw new TypeError('Power base must be a nonempty matrix of terminal cells.');
|
|
286
|
+
const h=base.length,w=base[0].length;
|
|
287
|
+
if(h<9||w<32||base.some(row=>row.length!==w))
|
|
288
|
+
throw new RangeError('Power viewport must be rectangular, at least 32×9.');
|
|
289
|
+
const canvas=createCanvas(w,h,{theme,mono});
|
|
290
|
+
const time=still?Math.min(profile.duration*.6,2100):clamp(Number.isFinite(t)?t:0,0,profile.duration);
|
|
291
|
+
DRAWERS[profile.id](canvas,spriteBounds(base),time,anchors);
|
|
292
|
+
const result=canvas.compose(base);
|
|
293
|
+
return result;
|
|
294
|
+
}
|