@myvillage/cli 1.64.1 → 1.64.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/utils/templates.js +611 -300
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.64.1",
3
+ "version": "1.64.3",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,13 +56,17 @@ const BRAND = {
56
56
  export function createGameProject(targetDir, options) {
57
57
  const { name, description, type, ageGroup, orientation = 'landscape' } = options;
58
58
 
59
- // A quiz is a 2D UI experience, not a 3D world — it gets a dedicated DOM/CSS
60
- // scaffold (no Three.js, no renderer, no game loop) instead of the shared
61
- // 3D shell the exploration/narrative/custom types use.
59
+ // Quiz and narrative are 2D UI experiences, not 3D worlds — they get dedicated
60
+ // DOM/CSS scaffolds (no Three.js, no renderer, no game loop) instead of the
61
+ // shared 3D shell the exploration/custom types use.
62
62
  if (type === 'quiz') {
63
63
  createQuizProject(targetDir, options);
64
64
  return;
65
65
  }
66
+ if (type === 'narrative') {
67
+ createNarrativeProject(targetDir, options);
68
+ return;
69
+ }
66
70
 
67
71
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
68
72
 
@@ -90,6 +94,7 @@ export function createGameProject(targetDir, options) {
90
94
  writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
91
95
  writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
92
96
  writeFileSync(join(targetDir, 'README.md'), generateReadme(name, description, type, ageGroup));
97
+ writeFileSync(join(targetDir, 'AGENTS.md'), generate3DGameAgents(name, type, orientation));
93
98
  writeFileSync(join(targetDir, 'index.html'), generateIndexHtml(name, orientation));
94
99
  writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
95
100
  copyTemplateArt(targetDir, type, orientation);
@@ -102,17 +107,13 @@ export function createGameProject(targetDir, options) {
102
107
  writeFileSync(join(targetDir, 'src/utils/AudioManager.js'), generateAudioManager());
103
108
  writeFileSync(join(targetDir, 'src/components/UI.js'), generateUI(type));
104
109
 
105
- // Write game-type-specific files
110
+ // Write the game-type-specific game logic. Quiz and narrative are handled
111
+ // earlier by their own 2D scaffolds, so only exploration and custom (default)
112
+ // reach this 3D path.
106
113
  switch (type) {
107
- case 'quiz':
108
- writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateQuizGameLogic());
109
- break;
110
114
  case 'exploration':
111
115
  writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateExplorationGameLogic());
112
116
  break;
113
- case 'narrative':
114
- writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateNarrativeGameLogic());
115
- break;
116
117
  default:
117
118
  writeFileSync(join(targetDir, 'src/components/GameLogic.js'), generateBaseGameLogic());
118
119
  break;
@@ -135,11 +136,12 @@ function createQuizProject(targetDir, options) {
135
136
  mkdirSync(join(targetDir, dir), { recursive: true });
136
137
  }
137
138
 
138
- writeFileSync(join(targetDir, 'package.json'), generateQuizPackageJson(name, description, slug, ageGroup, orientation));
139
+ writeFileSync(join(targetDir, 'package.json'), generate2DGamePackageJson(name, description, slug, ageGroup, orientation, 'quiz'));
139
140
  writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
140
141
  writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
141
- writeFileSync(join(targetDir, 'index.html'), generateQuizIndexHtml(name, orientation));
142
+ writeFileSync(join(targetDir, 'index.html'), generate2DGameIndexHtml(name, orientation));
142
143
  writeFileSync(join(targetDir, 'README.md'), generateQuizReadme(name, description, ageGroup));
144
+ writeFileSync(join(targetDir, 'AGENTS.md'), generate2DGameAgents(name, 'quiz', orientation));
143
145
 
144
146
  writeFileSync(join(targetDir, 'src/main.js'), generateQuizApp());
145
147
  writeFileSync(join(targetDir, 'src/questions.js'), generateQuizQuestions(name));
@@ -150,7 +152,9 @@ function createQuizProject(targetDir, options) {
150
152
  copyTemplateArt(targetDir, 'quiz', orientation);
151
153
  }
152
154
 
153
- function generateQuizPackageJson(name, description, slug, ageGroup, orientation) {
155
+ // Shared by the 2D game types (quiz, narrative) — a plain Vite project with no
156
+ // Three.js. `gameType` goes into the myvillage block the deploy command reads.
157
+ function generate2DGamePackageJson(name, description, slug, ageGroup, orientation, gameType) {
154
158
  return JSON.stringify({
155
159
  name: slug,
156
160
  version: '1.0.0',
@@ -162,14 +166,14 @@ function generateQuizPackageJson(name, description, slug, ageGroup, orientation)
162
166
  build: 'vite build',
163
167
  preview: 'vite preview',
164
168
  },
165
- // No Three.js — a quiz is pure DOM/CSS.
169
+ // No Three.js — these are pure DOM/CSS games.
166
170
  devDependencies: {
167
171
  vite: '^5.0.0',
168
172
  },
169
173
  myvillage: {
170
174
  name: name,
171
175
  gameId: null,
172
- gameType: 'quiz',
176
+ gameType,
173
177
  targetAge: ageGroup,
174
178
  orientation,
175
179
  aspectRatio: orientation === 'portrait' ? '9:16' : '16:9',
@@ -183,7 +187,8 @@ function generateQuizPackageJson(name, description, slug, ageGroup, orientation)
183
187
  }, null, 2) + '\n';
184
188
  }
185
189
 
186
- function generateQuizIndexHtml(name, orientation) {
190
+ // Shared by the 2D game types — links styles.css + main.js into a #app root.
191
+ function generate2DGameIndexHtml(name, orientation) {
187
192
  return `<!DOCTYPE html>
188
193
  <html lang="en">
189
194
  <head>
@@ -593,6 +598,595 @@ myvillage deploy
593
598
  `;
594
599
  }
595
600
 
601
+ // ── Narrative Game Project (2D) ────────────────────────────────────
602
+ // A story game is text + choices, so this scaffold is plain DOM/CSS — no
603
+ // Three.js. Devs edit src/story.js (the branching scenes) and src/styles.css
604
+ // (the look). src/main.js runs the story and reports the result through the
605
+ // shared MyVillage game contract.
606
+ function createNarrativeProject(targetDir, options) {
607
+ const { name, description, ageGroup, orientation = 'portrait' } = options;
608
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
609
+
610
+ const dirs = ['', 'src', 'assets', 'public'];
611
+ for (const dir of dirs) {
612
+ mkdirSync(join(targetDir, dir), { recursive: true });
613
+ }
614
+
615
+ writeFileSync(join(targetDir, 'package.json'), generate2DGamePackageJson(name, description, slug, ageGroup, orientation, 'narrative'));
616
+ writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
617
+ writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
618
+ writeFileSync(join(targetDir, 'index.html'), generate2DGameIndexHtml(name, orientation));
619
+ writeFileSync(join(targetDir, 'README.md'), generateNarrativeReadme(name, description, ageGroup));
620
+ writeFileSync(join(targetDir, 'AGENTS.md'), generate2DGameAgents(name, 'narrative', orientation));
621
+
622
+ writeFileSync(join(targetDir, 'src/main.js'), generateNarrativeApp());
623
+ writeFileSync(join(targetDir, 'src/story.js'), generateNarrativeStory(name));
624
+ writeFileSync(join(targetDir, 'src/styles.css'), generateNarrativeStyles());
625
+ writeFileSync(join(targetDir, 'src/myvillage-game.js'), generateMyVillageGameSdk());
626
+
627
+ writeFileSync(join(targetDir, 'assets/README.md'), generateAssetsReadme(orientation));
628
+ copyTemplateArt(targetDir, 'narrative', orientation);
629
+ }
630
+
631
+ function generateNarrativeApp() {
632
+ return `// Narrative Game — an interactive, choice-driven story.
633
+ //
634
+ // Edit your story in src/story.js (scenes, speakers, choices) and the look in
635
+ // src/styles.css. This file runs the story: it shows the start screen, each
636
+ // scene with its choices, branches on what the player picks, and an ending —
637
+ // then reports the result to the MyVillage host.
638
+
639
+ import { MyVillageGame } from './myvillage-game.js';
640
+ import { storyConfig, scenes } from './story.js';
641
+
642
+ class NarrativeGame {
643
+ constructor(root) {
644
+ this.root = root;
645
+ this.index = 0;
646
+ this.choicesMade = 0;
647
+
648
+ this.renderStart();
649
+
650
+ // Tell the MyVillage host the game is loaded and interactive.
651
+ MyVillageGame.ready();
652
+ }
653
+
654
+ renderStart() {
655
+ this.root.innerHTML = \`
656
+ <main class="screen">
657
+ <div class="card start-card">
658
+ <h1 class="title">\${storyConfig.title}</h1>
659
+ <p class="intro">\${storyConfig.intro}</p>
660
+ <button class="btn primary" id="begin-btn">Begin</button>
661
+ </div>
662
+ </main>
663
+ \`;
664
+ this.root.querySelector('#begin-btn').addEventListener('click', () => this.begin());
665
+ }
666
+
667
+ begin() {
668
+ this.index = storyConfig.startScene || 0;
669
+ this.choicesMade = 0;
670
+ this.renderScene();
671
+ }
672
+
673
+ renderScene() {
674
+ const scene = scenes[this.index];
675
+ if (!scene) {
676
+ this.renderEnding();
677
+ return;
678
+ }
679
+
680
+ const hasChoices = Array.isArray(scene.choices) && scene.choices.length > 0;
681
+ const buttons = hasChoices
682
+ ? scene.choices.map((c, i) => \`<button class="btn choice" data-index="\${i}">\${c.text}</button>\`).join('')
683
+ : \`<button class="btn primary" id="end-btn">The End</button>\`;
684
+
685
+ this.root.innerHTML = \`
686
+ <main class="screen">
687
+ <div class="card scene-card">
688
+ \${scene.speaker ? \`<p class="speaker">\${scene.speaker}</p>\` : ''}
689
+ <p class="scene-text">\${scene.text}</p>
690
+ <div class="choices">\${buttons}</div>
691
+ </div>
692
+ </main>
693
+ \`;
694
+
695
+ if (hasChoices) {
696
+ this.root.querySelectorAll('.choice').forEach((btn) => {
697
+ btn.addEventListener('click', () => this.choose(Number(btn.dataset.index)));
698
+ });
699
+ } else {
700
+ this.root.querySelector('#end-btn').addEventListener('click', () => this.renderEnding());
701
+ }
702
+ }
703
+
704
+ choose(choiceIndex) {
705
+ const scene = scenes[this.index];
706
+ const choice = scene.choices[choiceIndex];
707
+ this.choicesMade++;
708
+ // Jump to the linked scene. An undefined / out-of-range next ends the story.
709
+ this.index = choice && choice.next !== undefined ? choice.next : -1;
710
+ this.renderScene();
711
+ }
712
+
713
+ renderEnding() {
714
+ this.root.innerHTML = \`
715
+ <main class="screen">
716
+ <div class="card results-card">
717
+ <h2 class="title">The End</h2>
718
+ <p class="intro">You made \${this.choicesMade} choice\${this.choicesMade === 1 ? '' : 's'} along the way.</p>
719
+ <button class="btn primary" id="again-btn">Play Again</button>
720
+ </div>
721
+ </main>
722
+ \`;
723
+ this.root.querySelector('#again-btn').addEventListener('click', () => this.begin());
724
+
725
+ // REQUIRED: tell the MyVillage host the story is finished. The host records
726
+ // the session, awards this game's MVT reward, and returns the player to the
727
+ // catalog. During \`npm run dev\` there's no host, so this just logs.
728
+ MyVillageGame.complete({ won: true, score: this.choicesMade, custom: { choices: this.choicesMade } });
729
+ }
730
+ }
731
+
732
+ new NarrativeGame(document.getElementById('app'));
733
+ `;
734
+ }
735
+
736
+ function generateNarrativeStory(name) {
737
+ return `// Story content — edit this file to write your own branching narrative.
738
+ //
739
+ // Each scene has an optional \`speaker\`, the \`text\` to show, and \`choices\`.
740
+ // Each choice has \`text\` and \`next\`: the index of the scene to jump to.
741
+ // Set \`choices: null\` (or omit it) to mark a scene as the end of the story.
742
+
743
+ export const storyConfig = {
744
+ // Shown on the start screen.
745
+ title: ${JSON.stringify(name)},
746
+ intro: 'An interactive story — your choices shape what happens next.',
747
+ // Index of the scene the story opens on.
748
+ startScene: 0,
749
+ };
750
+
751
+ export const scenes = [
752
+ {
753
+ speaker: 'Narrator',
754
+ text: 'You find yourself at the entrance to a mysterious village. The golden gates shine in the sunlight.',
755
+ choices: [
756
+ { text: 'Enter the village', next: 1 },
757
+ { text: 'Look around first', next: 2 },
758
+ ],
759
+ },
760
+ {
761
+ speaker: 'Village Elder',
762
+ text: 'Welcome, young traveler! We have been expecting you. Our village needs your help.',
763
+ choices: [
764
+ { text: 'How can I help?', next: 3 },
765
+ { text: 'What happened here?', next: 3 },
766
+ ],
767
+ },
768
+ {
769
+ speaker: 'Narrator',
770
+ text: 'You notice a weathered signpost and colorful flowers lining a cobblestone path. The village looks peaceful, but there is an air of anticipation.',
771
+ choices: [
772
+ { text: 'Follow the path inside', next: 1 },
773
+ ],
774
+ },
775
+ {
776
+ speaker: 'Village Elder',
777
+ text: 'A great challenge lies ahead. Only someone with courage and wisdom can solve it. Will you accept the quest?',
778
+ choices: [
779
+ { text: 'I accept!', next: 4 },
780
+ { text: 'Tell me more first', next: 4 },
781
+ ],
782
+ },
783
+ {
784
+ speaker: 'Narrator',
785
+ text: 'And so your adventure begins. This is where your story continues — edit src/story.js to write your own!',
786
+ choices: null, // null (or omitted) = the end of the story
787
+ },
788
+ ];
789
+ `;
790
+ }
791
+
792
+ function generateNarrativeStyles() {
793
+ return `/* Narrative styling — tweak freely. Colors use the MyVillage palette. */
794
+
795
+ :root {
796
+ --gold: #FFD700;
797
+ --primary: #B07C00;
798
+ --cream: #E4DCCB;
799
+ --dark: #302017;
800
+ --deep-green: #043922;
801
+ --green: #228B22;
802
+ --teal: #799C9F;
803
+ }
804
+
805
+ * { margin: 0; padding: 0; box-sizing: border-box; }
806
+
807
+ html, body {
808
+ height: 100%;
809
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
810
+ }
811
+
812
+ body {
813
+ background: linear-gradient(160deg, var(--deep-green), var(--dark));
814
+ color: var(--cream);
815
+ display: flex;
816
+ -webkit-font-smoothing: antialiased;
817
+ }
818
+
819
+ #app { flex: 1; display: flex; }
820
+
821
+ .screen {
822
+ flex: 1;
823
+ display: flex;
824
+ flex-direction: column;
825
+ justify-content: center;
826
+ align-items: center;
827
+ padding: 24px;
828
+ width: 100%;
829
+ }
830
+
831
+ .card {
832
+ width: 100%;
833
+ max-width: 540px;
834
+ background: rgba(48, 32, 23, 0.92);
835
+ border: 2px solid var(--primary);
836
+ border-radius: 20px;
837
+ padding: 28px 24px;
838
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
839
+ }
840
+
841
+ .start-card, .results-card { text-align: center; }
842
+ .scene-card { text-align: left; }
843
+
844
+ .title { color: var(--gold); font-size: 30px; margin-bottom: 12px; }
845
+ .intro { font-size: 16px; line-height: 1.5; margin-bottom: 24px; }
846
+
847
+ .speaker {
848
+ color: var(--gold);
849
+ font-weight: 700;
850
+ font-size: 13px;
851
+ letter-spacing: 0.08em;
852
+ text-transform: uppercase;
853
+ margin-bottom: 10px;
854
+ }
855
+
856
+ .scene-text {
857
+ font-size: 18px;
858
+ line-height: 1.6;
859
+ margin-bottom: 24px;
860
+ color: var(--cream);
861
+ }
862
+
863
+ .choices { display: flex; flex-direction: column; gap: 12px; }
864
+
865
+ .btn {
866
+ font-family: inherit;
867
+ font-size: 16px;
868
+ font-weight: 600;
869
+ border-radius: 12px;
870
+ border: none;
871
+ cursor: pointer;
872
+ min-height: 52px;
873
+ padding: 14px 18px;
874
+ transition: transform 0.05s ease, background 0.2s ease, border-color 0.2s ease;
875
+ }
876
+
877
+ .btn:active { transform: scale(0.98); }
878
+
879
+ .btn.primary { background: var(--primary); color: #fff; width: 100%; font-size: 18px; }
880
+ .btn.primary:hover { background: #c98f0a; }
881
+
882
+ .choice {
883
+ background: var(--deep-green);
884
+ color: var(--cream);
885
+ border: 2px solid var(--green);
886
+ text-align: left;
887
+ width: 100%;
888
+ }
889
+
890
+ .choice:hover { border-color: var(--gold); }
891
+ `;
892
+ }
893
+
894
+ function generateNarrativeReadme(name, description, ageGroup) {
895
+ return `# ${name}
896
+
897
+ ${description}
898
+
899
+ - **Game Type**: Narrative
900
+ - **Target Age Group**: ${ageGroup}
901
+ - **Built with**: MyVillageOS CLI
902
+
903
+ An interactive, choice-driven story — no 3D engine. Edit two files to make it yours:
904
+
905
+ - **src/story.js** — your scenes, speakers, and branching choices
906
+ - **src/styles.css** — colors, fonts, and layout
907
+
908
+ ## Getting Started
909
+
910
+ \`\`\`bash
911
+ npm install
912
+ npm run dev # play it locally with live reload
913
+ npm run build # build for production (outputs to dist/)
914
+ \`\`\`
915
+
916
+ ## Writing your story
917
+
918
+ Open \`src/story.js\`. Each scene has an optional \`speaker\`, the \`text\` to show,
919
+ and \`choices\`. Each choice has \`text\` and \`next\` — the index of the scene to
920
+ jump to. Branch the story by pointing choices at different scenes:
921
+
922
+ \`\`\`js
923
+ {
924
+ speaker: 'Village Elder',
925
+ text: 'Will you accept the quest?',
926
+ choices: [
927
+ { text: 'I accept!', next: 4 },
928
+ { text: 'Not yet', next: 2 },
929
+ ],
930
+ }
931
+ \`\`\`
932
+
933
+ Set \`choices: null\` (or omit it) to make a scene the end of the story.
934
+
935
+ ## The game contract
936
+
937
+ When the story ends it calls \`MyVillageGame.complete({ won, score })\`, so the
938
+ MyVillage host records the session and awards the player this game's MVT reward.
939
+ \`MyVillageGame.ready()\` is called on load. During \`npm run dev\` there's no host
940
+ attached, so these just log to the console — that's expected.
941
+
942
+ ## Deploying
943
+
944
+ Starter cover art ships in \`assets/\` — swap in your own thumbnail.png + banner.png, then:
945
+
946
+ \`\`\`bash
947
+ myvillage deploy
948
+ \`\`\`
949
+
950
+ ## Learn More
951
+
952
+ - [Vite Documentation](https://vitejs.dev/)
953
+ - [MyVillageOS Developer Guide](https://portal.myvillageproject.ai)
954
+ `;
955
+ }
956
+
957
+ // ── AGENTS.md generators ───────────────────────────────────────────
958
+ // Each scaffold ships an AGENTS.md — a guide written for AI coding agents
959
+ // (Claude Code, Cursor, Codex, etc. read it automatically). It points the agent
960
+ // at the right files, documents the available APIs + the MyVillage game
961
+ // contract, and flags what must not break. Built as string arrays (not template
962
+ // literals) so the heavy inline-code backticks need no escaping.
963
+
964
+ function generate2DGameAgents(name, gameType, orientation) {
965
+ const isQuiz = gameType === 'quiz';
966
+ const kind = isQuiz ? 'quiz' : 'narrative';
967
+ const engine = isQuiz ? 'QuizGame' : 'NarrativeGame';
968
+ const contentFile = isQuiz ? 'src/questions.js' : 'src/story.js';
969
+ const endFn = isQuiz ? 'renderResults()' : 'renderEnding()';
970
+ const banner = orientation === 'portrait'
971
+ ? '9:16 (e.g. 1080x1920) for this portrait game'
972
+ : '16:9 (e.g. 1920x1080) for this landscape game';
973
+ const contentLine = isQuiz
974
+ ? '**Content — edit `src/questions.js` first.** It exports `quizConfig` (title, intro, `passThreshold`, `showFeedback`) and a `questions` array. Each question is `{ question, choices, correct }`, where `correct` is the 0-based index of the right choice.'
975
+ : '**Content — edit `src/story.js` first.** It exports `storyConfig` (title, intro, `startScene`) and a `scenes` array. Each scene is `{ speaker, text, choices }`; each choice is `{ text, next }`, where `next` is the index of the next scene. Set `choices: null` to end the story. Branch by pointing choices at different scenes.';
976
+ const flowLine = isQuiz
977
+ ? '**Flow & rules — `src/main.js`.** The `QuizGame` class renders each screen (start -> question -> results) and wires the buttons. Edit here for scoring, timing, or new screens.'
978
+ : '**Flow — `src/main.js`.** The `NarrativeGame` class renders each screen (start -> scene -> ending), follows each choice\'s `next`, and wires the buttons.';
979
+
980
+ return [
981
+ '# AGENTS.md — working on this MyVillage game with an AI agent',
982
+ '',
983
+ '**' + name + '** is a MyVillage ' + kind + ' game — a 2D web game (plain DOM + CSS, no 3D engine, no Three.js) built with Vite. It runs inside the MyVillage host (the muni mobile app or the web player) and standalone in a browser.',
984
+ '',
985
+ 'When editing: make the requested change, keep the **MyVillage game contract** intact (see below), and keep `npm run build` working.',
986
+ '',
987
+ '## Where common changes go',
988
+ '',
989
+ '- ' + contentLine,
990
+ '- **Look & feel — `src/styles.css`.** Colors are CSS variables in `:root` (the MyVillage palette). Cards, buttons, layout, fonts.',
991
+ '- ' + flowLine,
992
+ '',
993
+ '## File map',
994
+ '',
995
+ '| File | Purpose |',
996
+ '|------|---------|',
997
+ '| `index.html` | Entry point; mounts `#app`, loads `main.js` + `styles.css`. |',
998
+ '| `src/main.js` | The `' + engine + '` engine — lifecycle + rendering. |',
999
+ '| `' + contentFile + '` | **The content** — edit this first for content changes. |',
1000
+ '| `src/styles.css` | Styling (CSS variables in `:root`). |',
1001
+ '| `src/myvillage-game.js` | Host bridge — **do not edit.** |',
1002
+ '| `assets/` | Cover art shipped on deploy. |',
1003
+ '| `package.json` | The `myvillage` block holds metadata `myvillage deploy` reads. |',
1004
+ '',
1005
+ '## The MyVillage game contract — DO NOT BREAK',
1006
+ '',
1007
+ 'The game talks to its host through `src/myvillage-game.js`:',
1008
+ '',
1009
+ '- `MyVillageGame.ready()` — called on load. Leave it.',
1010
+ '- `MyVillageGame.complete({ score, won })` — **required.** Called when the game ends (in `' + endFn + '`). It is how the host records the session and awards the player\'s reward. If you change the end flow, keep this call wired to the real end condition.',
1011
+ '',
1012
+ 'Do **not** add an Exit button — the host always provides its own.',
1013
+ '',
1014
+ 'During `npm run dev` there is no host attached, so `ready()`/`complete()` just log to the console. That is expected — do not try to "fix" it.',
1015
+ '',
1016
+ '## Run / build / deploy',
1017
+ '',
1018
+ ' npm install',
1019
+ ' npm run dev # local dev server, live reload',
1020
+ ' npm run build # production build -> dist/ (must pass before deploy)',
1021
+ ' myvillage deploy # build, then upload + submit to MyVillage',
1022
+ '',
1023
+ 'Keep asset URLs relative (Vite `base: \'./\'`). The host serves the game from a sub-path, so absolute `/...` URLs will break.',
1024
+ '',
1025
+ '## Cover art (`assets/`)',
1026
+ '',
1027
+ 'Starter art ships here — replace with your own, keeping the same filenames and sizes:',
1028
+ '',
1029
+ '- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
1030
+ '- `banner.png` — ' + banner + '. Featured hero; optional.',
1031
+ '',
1032
+ '## Conventions',
1033
+ '',
1034
+ '- Orientation is **' + orientation + '** (set in `package.json` -> `myvillage.orientation`). Portrait runs on muni mobile **and** web; landscape is web-only.',
1035
+ '- Vanilla JS, no framework — keep it dependency-light.',
1036
+ '',
1037
+ ].join('\n');
1038
+ }
1039
+
1040
+ function generate3DGameAgents(name, type, orientation) {
1041
+ const isExploration = type === 'exploration';
1042
+ const banner = orientation === 'portrait'
1043
+ ? '9:16 (e.g. 1080x1920) for this portrait game'
1044
+ : '16:9 (e.g. 1920x1080) for this landscape game';
1045
+ const logicLine = isExploration
1046
+ ? 'Holds the gameplay — player movement (via `input.getMovementVector()`), collectible spawning + collision, the follow-camera, score, and win condition. It calls `input.enableJoystick()` in `start()` so mobile gets on-screen movement.'
1047
+ : 'A blank starter — `update(delta)` is empty with hints. Build your mechanics here; the 3D scene, input, audio, UI, and game loop are already wired.';
1048
+ const mobileLine = isExploration
1049
+ ? 'This game already calls `this.game.input.enableJoystick()` in `GameLogic.start()`, so touch devices get an on-screen movement thumbstick (no-op on desktop). Read movement with `this.game.input.getMovementVector()`.'
1050
+ : 'If your game uses movement, call `this.game.input.enableJoystick()` in `GameLogic.start()` to show an on-screen thumbstick on touch devices (no-op on desktop), then read it with `this.game.input.getMovementVector()`.';
1051
+ const uiExtra = isExploration
1052
+ ? ' This game also adds `showMinimap(pos, items)` and `showInventory(items)`.'
1053
+ : '';
1054
+
1055
+ return [
1056
+ '# AGENTS.md — working on this MyVillage game with an AI agent',
1057
+ '',
1058
+ '**' + name + '** is a MyVillage ' + (isExploration ? 'exploration' : 'custom') + ' game — a 3D web game built with **Three.js** and Vite. It runs inside the MyVillage host (the muni mobile app or the web player) and standalone in a browser.',
1059
+ '',
1060
+ 'When editing: make the requested change, keep the **MyVillage game contract** intact (see below), and keep `npm run build` working.',
1061
+ '',
1062
+ '## Where common changes go',
1063
+ '',
1064
+ '- **Gameplay — `src/components/GameLogic.js`.** ' + logicLine,
1065
+ '- **3D scene — `src/scenes/MainScene.js`.** Camera, lighting, ground, objects. The starter has a sky, a green ground plane, and a sample rotating cube (`this.sampleObject`). Add your meshes here.',
1066
+ '- **On-screen UI — `src/components/UI.js`.** HTML overlay (start menu, HUD, pause menu, game-over) rendered over the 3D canvas.',
1067
+ '- **Page/overlay styles are inline in `index.html`.**',
1068
+ '',
1069
+ '## File map',
1070
+ '',
1071
+ '| File | Purpose |',
1072
+ '|------|---------|',
1073
+ '| `src/main.js` | The `Game` class — Three.js renderer, state machine (`loading`/`menu`/`playing`/`paused`/`gameover`), the `animate()` loop, host wiring. |',
1074
+ '| `src/scenes/MainScene.js` | The 3D scene (camera, lights, ground, objects). |',
1075
+ '| `src/components/GameLogic.js` | **Your gameplay** — `start()`, `update(delta)`, `reset()`. |',
1076
+ '| `src/components/UI.js` | HTML overlay (menus, HUD). |',
1077
+ '| `src/utils/InputManager.js` | Keyboard / mouse / touch + mobile joystick. |',
1078
+ '| `src/utils/AudioManager.js` | Sound effects + music (Web Audio). |',
1079
+ '| `src/myvillage-game.js` | Host bridge — **do not edit.** |',
1080
+ '| `assets/` | Cover art shipped on deploy. |',
1081
+ '| `public/assets/` | Drop 3D models / textures / audio here. |',
1082
+ '',
1083
+ '## Engine API (reach it as `this.game` inside GameLogic)',
1084
+ '',
1085
+ '- `this.game.scene` — `.scene` (THREE.Scene), `.camera` (THREE.PerspectiveCamera), `.sampleObject`, `.update(delta)`.',
1086
+ '- `this.game.input` — `getMovementVector()` (returns `{ x, z }` from WASD/arrows or the joystick), `isKeyDown(code)`, `isKeyJustPressed(code)`, `enableJoystick()`, `clearFrame()`, `mouse`, `touches`.',
1087
+ '- `this.game.audio` — `playSFX(name)`, `playMusic(name)`, `stopMusic()`, `setMusicVolume(v)`, `setSFXVolume(v)`.',
1088
+ '- `this.game.ui` — `showMenu()`, `showHUD()`, `showPauseMenu()`, `showGameOver(result)`, `clearOverlay()`, `createElement(tag, styles)`.' + uiExtra,
1089
+ '- `this.game` — `startGame()`, `pauseGame()`, `resumeGame()`, `endGame(result)`, `state`.',
1090
+ '',
1091
+ '## Mobile controls',
1092
+ '',
1093
+ mobileLine,
1094
+ '',
1095
+ '## The MyVillage game contract — DO NOT BREAK',
1096
+ '',
1097
+ '- `MyVillageGame.ready()` — called after assets load in `src/main.js`. Leave it.',
1098
+ '- `this.game.endGame({ won, score })` — call at your win/lose condition. It calls `MyVillageGame.complete(...)`, which is **required**: the host records the session and awards the player\'s reward.',
1099
+ '',
1100
+ 'Do **not** add an Exit button — the host always provides its own. During `npm run dev` there is no host, so host calls just log to the console (expected).',
1101
+ '',
1102
+ '## Run / build / deploy',
1103
+ '',
1104
+ ' npm install',
1105
+ ' npm run dev # local dev server, live reload',
1106
+ ' npm run build # production build -> dist/ (must pass before deploy)',
1107
+ ' myvillage deploy # build, then upload + submit to MyVillage',
1108
+ '',
1109
+ 'Keep asset URLs relative (Vite `base: \'./\'`). The host serves from a sub-path, so absolute `/...` URLs break.',
1110
+ '',
1111
+ '## Cover art (`assets/`)',
1112
+ '',
1113
+ 'Starter art ships here — replace with your own, same filenames and sizes:',
1114
+ '',
1115
+ '- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
1116
+ '- `banner.png` — ' + banner + '. Featured hero; optional.',
1117
+ '',
1118
+ '## Conventions',
1119
+ '',
1120
+ '- Orientation is **' + orientation + '** (`package.json` -> `myvillage.orientation`). Portrait runs on muni mobile **and** web; landscape is web-only.',
1121
+ '- The follow-camera and game loop already exist — extend them, do not rebuild.',
1122
+ '',
1123
+ ].join('\n');
1124
+ }
1125
+
1126
+ function generateUnityAgents(name, className, orientation) {
1127
+ const isLandscape = orientation === 'landscape';
1128
+ const reach = isLandscape
1129
+ ? 'web (WebGL) only'
1130
+ : 'the muni mobile app and the web';
1131
+ const banner = isLandscape
1132
+ ? '16:9 (e.g. 1920x1080) for this landscape game'
1133
+ : '9:16 (e.g. 1080x1920) for this portrait game';
1134
+
1135
+ return [
1136
+ '# AGENTS.md — working on this MyVillage Unity game with an AI agent',
1137
+ '',
1138
+ '**' + name + '** is a MyVillage Unity game built on the **MyVillage GameKit SDK** (a UPM package pinned in `Packages/manifest.json`). The mission is C#; it builds to AssetBundles the M-UNI host loads.',
1139
+ '',
1140
+ 'When editing: change the mission logic, keep the **game contract** intact (`CompleteMission` / `FailMission`), and do not break the GameKit `MissionBase` lifecycle.',
1141
+ '',
1142
+ '## Where common changes go',
1143
+ '',
1144
+ '- **Mission logic — `Assets/Scripts/' + className + '.cs`.** Your `MissionBase` subclass. Override `OnBegin()` with your gameplay and call `CompleteMission(finalScore: n)` (or `FailMission(reason)`) when the player finishes. `OnEnd()` is for cleanup.',
1145
+ '- **Scene & objects — the Unity scene** (create via the editor menu `MyVillage -> Create Starter Mission Scene`). Add GameObjects, art, and audio in the Unity editor.',
1146
+ '- **Orientation — `Assets/Editor/MyVillageOrientation.cs`** pins it on first load.',
1147
+ '',
1148
+ '## File map',
1149
+ '',
1150
+ '| Path | Purpose |',
1151
+ '|------|---------|',
1152
+ '| `Assets/Scripts/' + className + '.cs` | **Your mission** — the `MissionBase` subclass. Edit here. |',
1153
+ '| `Assets/Editor/MyVillageOrientation.cs` | Pins player orientation on first load. |',
1154
+ '| `Packages/manifest.json` | Depends on the GameKit SDK (pinned version). Do not hand-edit the SDK. |',
1155
+ '| `myvillage.json` | Game metadata the CLI/deploy reads (slug, gameType, orientation, assets). |',
1156
+ '| `assets/` | Cover art shipped on deploy (thumbnail/banner). |',
1157
+ '| `builds/{ios,android,webgl}/` | Drop-zone for built AssetBundles the CLI deploys. |',
1158
+ '',
1159
+ '## The game contract (GameKit `MissionBase`) — DO NOT BREAK',
1160
+ '',
1161
+ '- The host drives your mission and runs `OnBegin()`. In the editor, `MockMissionHost` stands in and logs host calls to the Console.',
1162
+ '- **`CompleteMission(finalScore: n)`** — call when the player finishes. This is the native equivalent of the web games\' `MyVillageGame.complete()`; the host records the session and awards the player\'s reward. **Required to end the game properly.**',
1163
+ '- `FailMission(reason)` — the lose path. `Host.LogEvent("...")` — telemetry.',
1164
+ '- **Do not build an Exit button** — the host always shows its own and calls `OnEnd()` for cleanup when the player exits.',
1165
+ '',
1166
+ 'The same script runs unchanged in the real M-UNI host (a `MissionHostAdapter` replaces `MockMissionHost`). Do not add platform-specific host code.',
1167
+ '',
1168
+ '## Build & deploy',
1169
+ '',
1170
+ '1. In Unity: **`MyVillage -> Build All Platforms`** (ships in GameKit). Builds iOS, Android, and WebGL into `builds/`.',
1171
+ '2. From the terminal: `myvillage deploy` (scans `builds/`, uploads, submits).',
1172
+ '',
1173
+ 'This game\'s orientation is **' + orientation + '** — it runs on ' + reach + '.',
1174
+ '',
1175
+ '## Cover art (`assets/`)',
1176
+ '',
1177
+ 'Starter art ships here — replace with your own, same filenames and sizes:',
1178
+ '',
1179
+ '- `thumbnail.png` — square 1:1 (e.g. 512x512). The game card; required.',
1180
+ '- `banner.png` — ' + banner + '. Featured hero; optional.',
1181
+ '',
1182
+ '## Gotchas',
1183
+ '',
1184
+ '- Do not edit the GameKit SDK package — it is a pinned dependency. Use its `MissionBase` API.',
1185
+ '- Visual Scripting reflection nodes (`InvokeMember`/`GetMember`/`SetMember`) are rejected by the deploy preflight. Write C# instead.',
1186
+ '',
1187
+ ].join('\n');
1188
+ }
1189
+
596
1190
  // ── Unity Game Project ─────────────────────────────────────────────
597
1191
 
598
1192
  export async function createUnityGameProject(targetDir, options) {
@@ -675,6 +1269,7 @@ export async function createUnityGameProject(targetDir, options) {
675
1269
  copyTemplateArt(targetDir, type, orientation);
676
1270
  writeFileSync(join(targetDir, '.gitignore'), generateUnityGitignore());
677
1271
  writeFileSync(join(targetDir, 'README.md'), generateUnityReadme(name, slug, className, orientation, unityVersion));
1272
+ writeFileSync(join(targetDir, 'AGENTS.md'), generateUnityAgents(name, className, orientation));
678
1273
 
679
1274
  return { unityVersion, sdkVersion: GAMEKIT_SDK_VERSION, gameType: primaryGameType };
680
1275
  }
@@ -1833,52 +2428,6 @@ function generateUI(type) {
1833
2428
  let typeSpecificUI = '';
1834
2429
 
1835
2430
  switch (type) {
1836
- case 'quiz':
1837
- typeSpecificUI = `
1838
- showQuestion(question, choices, onAnswer) {
1839
- this.clearOverlay();
1840
-
1841
- const panel = this.createElement('div', {
1842
- position: 'absolute', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
1843
- width: '90%', maxWidth: '600px', background: 'rgba(48,32,23,0.95)',
1844
- borderRadius: '16px', padding: '24px', color: '#E4DCCB',
1845
- });
1846
-
1847
- const qText = this.createElement('p', {
1848
- fontSize: '18px', fontWeight: 'bold', marginBottom: '16px',
1849
- color: '#FFD700', textAlign: 'center',
1850
- });
1851
- qText.textContent = question;
1852
- panel.appendChild(qText);
1853
-
1854
- choices.forEach((choice, index) => {
1855
- const btn = this.createElement('button', {
1856
- display: 'block', width: '100%', padding: '12px',
1857
- margin: '8px 0', background: '#043922', color: '#E4DCCB',
1858
- border: '2px solid #228B22', borderRadius: '8px',
1859
- fontSize: '16px', cursor: 'pointer', textAlign: 'left',
1860
- });
1861
- btn.textContent = choice;
1862
- btn.addEventListener('click', () => onAnswer(index));
1863
- panel.appendChild(btn);
1864
- });
1865
-
1866
- this.overlay.appendChild(panel);
1867
- }
1868
-
1869
- showScore(score, total) {
1870
- this.clearOverlay();
1871
-
1872
- const panel = this.createElement('div', {
1873
- position: 'absolute', top: '10px', right: '10px',
1874
- background: 'rgba(48,32,23,0.85)', borderRadius: '12px',
1875
- padding: '10px 18px', color: '#FFD700', fontSize: '18px', fontWeight: 'bold',
1876
- });
1877
- panel.textContent = \`Score: \${score} / \${total}\`;
1878
- this.overlay.appendChild(panel);
1879
- }`;
1880
- break;
1881
-
1882
2431
  case 'exploration':
1883
2432
  typeSpecificUI = `
1884
2433
  showMinimap(playerPos, items) {
@@ -1941,57 +2490,6 @@ function generateUI(type) {
1941
2490
  }`;
1942
2491
  break;
1943
2492
 
1944
- case 'narrative':
1945
- typeSpecificUI = `
1946
- showDialogue(speaker, text, choices, onChoice) {
1947
- this.clearOverlay();
1948
-
1949
- const panel = this.createElement('div', {
1950
- position: 'absolute', bottom: '0', left: '0', width: '100%',
1951
- background: 'rgba(48,32,23,0.95)', padding: '20px 24px',
1952
- borderTop: '3px solid #B07C00', color: '#E4DCCB',
1953
- });
1954
-
1955
- if (speaker) {
1956
- const nameTag = this.createElement('span', {
1957
- color: '#FFD700', fontWeight: 'bold', fontSize: '16px',
1958
- marginBottom: '8px', display: 'block',
1959
- });
1960
- nameTag.textContent = speaker;
1961
- panel.appendChild(nameTag);
1962
- }
1963
-
1964
- const textEl = this.createElement('p', {
1965
- fontSize: '15px', lineHeight: '1.5', marginBottom: choices ? '16px' : '0',
1966
- });
1967
- textEl.textContent = text;
1968
- panel.appendChild(textEl);
1969
-
1970
- if (choices && choices.length > 0) {
1971
- choices.forEach((choice, index) => {
1972
- const btn = this.createElement('button', {
1973
- display: 'block', width: '100%', padding: '10px',
1974
- margin: '6px 0', background: 'transparent', color: '#FFD700',
1975
- border: '1px solid #B07C00', borderRadius: '8px',
1976
- fontSize: '14px', cursor: 'pointer', textAlign: 'left',
1977
- });
1978
- btn.textContent = \`> \${choice}\`;
1979
- btn.addEventListener('click', () => onChoice(index));
1980
- panel.appendChild(btn);
1981
- });
1982
- } else {
1983
- const hint = this.createElement('p', {
1984
- fontSize: '12px', color: '#799C9F', marginTop: '8px', textAlign: 'center',
1985
- });
1986
- hint.textContent = 'Click to continue...';
1987
- panel.appendChild(hint);
1988
- panel.addEventListener('click', () => onChoice(-1));
1989
- }
1990
-
1991
- this.overlay.appendChild(panel);
1992
- }`;
1993
- break;
1994
-
1995
2493
  default:
1996
2494
  typeSpecificUI = '';
1997
2495
  }
@@ -2162,89 +2660,6 @@ export class GameLogic {
2162
2660
  `;
2163
2661
  }
2164
2662
 
2165
- function generateQuizGameLogic() {
2166
- return `// Quiz Game Logic
2167
- // Manages questions, answers, scoring, and progress tracking.
2168
-
2169
- export class GameLogic {
2170
- constructor(game) {
2171
- this.game = game;
2172
- this.score = 0;
2173
- this.currentQuestion = 0;
2174
-
2175
- // Define your quiz questions here
2176
- this.questions = [
2177
- {
2178
- question: 'What is 2 + 2?',
2179
- choices: ['3', '4', '5', '6'],
2180
- correct: 1,
2181
- },
2182
- {
2183
- question: 'What is the capital of France?',
2184
- choices: ['London', 'Berlin', 'Paris', 'Madrid'],
2185
- correct: 2,
2186
- },
2187
- {
2188
- question: 'Which planet is closest to the Sun?',
2189
- choices: ['Venus', 'Mercury', 'Earth', 'Mars'],
2190
- correct: 1,
2191
- },
2192
- ];
2193
- }
2194
-
2195
- start() {
2196
- this.score = 0;
2197
- this.currentQuestion = 0;
2198
- this.showCurrentQuestion();
2199
- }
2200
-
2201
- reset() {
2202
- this.score = 0;
2203
- this.currentQuestion = 0;
2204
- }
2205
-
2206
- showCurrentQuestion() {
2207
- if (this.currentQuestion >= this.questions.length) {
2208
- // Quiz complete
2209
- this.game.endGame({
2210
- won: this.score > this.questions.length / 2,
2211
- score: this.score,
2212
- });
2213
- return;
2214
- }
2215
-
2216
- const q = this.questions[this.currentQuestion];
2217
- this.game.ui.showQuestion(q.question, q.choices, (answerIndex) => {
2218
- this.handleAnswer(answerIndex);
2219
- });
2220
- this.game.ui.showScore(this.score, this.questions.length);
2221
- }
2222
-
2223
- handleAnswer(answerIndex) {
2224
- const q = this.questions[this.currentQuestion];
2225
-
2226
- if (answerIndex === q.correct) {
2227
- this.score++;
2228
- // Play celebration animation - rotate the sample object faster
2229
- if (this.game.scene.sampleObject) {
2230
- this.game.scene.sampleObject.scale.set(1.5, 1.5, 1.5);
2231
- setTimeout(() => {
2232
- this.game.scene.sampleObject.scale.set(1, 1, 1);
2233
- }, 500);
2234
- }
2235
- }
2236
-
2237
- this.currentQuestion++;
2238
- this.showCurrentQuestion();
2239
- }
2240
-
2241
- update(delta) {
2242
- // Quiz games are event-driven, so update is minimal
2243
- }
2244
- }
2245
- `;
2246
- }
2247
-
2248
2663
  function generateExplorationGameLogic() {
2249
2664
  return `// Exploration Game Logic
2250
2665
  // Manages character movement, collision detection, and collectible items.
@@ -2375,107 +2790,3 @@ export class GameLogic {
2375
2790
  }
2376
2791
  `;
2377
2792
  }
2378
-
2379
- function generateNarrativeGameLogic() {
2380
- return `// Narrative Game Logic
2381
- // Manages dialogue, choices, scene transitions, and story branching.
2382
-
2383
- export class GameLogic {
2384
- constructor(game) {
2385
- this.game = game;
2386
- this.currentSceneIndex = 0;
2387
- this.score = 0;
2388
-
2389
- // Define your story scenes here.
2390
- // Each scene has speaker, text, and optional choices.
2391
- // Choices have 'text' and 'next' (index of the next scene to go to).
2392
- this.storyScenes = [
2393
- {
2394
- speaker: 'Narrator',
2395
- text: 'You find yourself at the entrance to a mysterious village. The golden gates shine in the sunlight.',
2396
- choices: [
2397
- { text: 'Enter the village', next: 1 },
2398
- { text: 'Look around first', next: 2 },
2399
- ],
2400
- },
2401
- {
2402
- speaker: 'Village Elder',
2403
- text: 'Welcome, young traveler! We have been expecting you. Our village needs your help.',
2404
- choices: [
2405
- { text: 'How can I help?', next: 3 },
2406
- { text: 'What happened here?', next: 3 },
2407
- ],
2408
- },
2409
- {
2410
- speaker: 'Narrator',
2411
- text: 'You notice a weathered signpost and colorful flowers lining a cobblestone path. The village looks peaceful but there is an air of anticipation.',
2412
- choices: [
2413
- { text: 'Follow the path inside', next: 1 },
2414
- ],
2415
- },
2416
- {
2417
- speaker: 'Village Elder',
2418
- text: 'A great challenge lies ahead. Only someone with courage and wisdom can solve it. Will you accept the quest?',
2419
- choices: [
2420
- { text: 'I accept!', next: 4 },
2421
- { text: 'Tell me more about the quest', next: 4 },
2422
- ],
2423
- },
2424
- {
2425
- speaker: 'Narrator',
2426
- text: 'And so your adventure begins... This is where your story continues! Edit the storyScenes array in GameLogic.js to create your own narrative.',
2427
- choices: null, // null choices = end of story
2428
- },
2429
- ];
2430
- }
2431
-
2432
- start() {
2433
- this.currentSceneIndex = 0;
2434
- this.score = 0;
2435
- this.showCurrentScene();
2436
- }
2437
-
2438
- reset() {
2439
- this.currentSceneIndex = 0;
2440
- this.score = 0;
2441
- }
2442
-
2443
- showCurrentScene() {
2444
- const scene = this.storyScenes[this.currentSceneIndex];
2445
- if (!scene) {
2446
- this.game.endGame({ won: true, score: this.score });
2447
- return;
2448
- }
2449
-
2450
- const choiceTexts = scene.choices ? scene.choices.map((c) => c.text) : null;
2451
-
2452
- this.game.ui.showDialogue(scene.speaker, scene.text, choiceTexts, (choiceIndex) => {
2453
- this.handleChoice(choiceIndex);
2454
- });
2455
- }
2456
-
2457
- handleChoice(choiceIndex) {
2458
- const scene = this.storyScenes[this.currentSceneIndex];
2459
-
2460
- if (!scene.choices || choiceIndex === -1) {
2461
- // End of story
2462
- this.game.endGame({ won: true, score: this.score });
2463
- return;
2464
- }
2465
-
2466
- const choice = scene.choices[choiceIndex];
2467
- if (choice && choice.next !== undefined) {
2468
- this.currentSceneIndex = choice.next;
2469
- this.score++;
2470
- this.showCurrentScene();
2471
- } else {
2472
- this.game.endGame({ won: true, score: this.score });
2473
- }
2474
- }
2475
-
2476
- update(delta) {
2477
- // Narrative games are event-driven via UI choices
2478
- }
2479
- }
2480
- `;
2481
- }