diff --git a/CardListing/9dd8b49f-d29e-4ebf-9476-b1209e89b9d5.json b/CardListing/9dd8b49f-d29e-4ebf-9476-b1209e89b9d5.json new file mode 100644 index 0000000..1596568 --- /dev/null +++ b/CardListing/9dd8b49f-d29e-4ebf-9476-b1209e89b9d5.json @@ -0,0 +1,79 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CardListing", + "module": "https://realms-staging.stack.cards/catalog/catalog-app/listing/listing" + } + }, + "type": "card", + "attributes": { + "name": "Create Your Own Adventure V4", + "images": [], + "summary": "The CreateYourOwnAdventureV4 module defines a card-based structure for managing customizable adventure games with a focus on user interface updates. Its core functions include setting up an adventure by linking scenarios or creating a one-shot narrative, managing game state (setup, playing, completed), and facilitating interactive storytelling with turn management. It provides embedded components for displaying adventures compactly and a full UI presentation with scenario selection, story narration, image generation, and chat integration. The primary purpose is to enable users to craft, run, and interact with personalized adventure narratives within a card-based UI, supporting multimedia elements and real-time communication features.", + "cardInfo": { + "name": null, + "notes": null, + "summary": null, + "cardThumbnailURL": null + } + }, + "relationships": { + "specs.0": { + "links": { + "self": "../Spec/8398f06e-0fa9-4427-8271-3ce9bd8c5e2f" + } + }, + "specs.1": { + "links": { + "self": "../Spec/0fa91427-0271-4ce9-bd8c-5e2f30583ef3" + } + }, + "specs.2": { + "links": { + "self": "../Spec/6e0fa914-2702-413c-a9bd-8c5e2f30583e" + } + }, + "skills": { + "links": { + "self": null + } + }, + "tags.0": { + "links": { + "self": "https://realms-staging.stack.cards/catalog/Tag/140feda8-625b-4a24-9ddb-6f4da891aef2" + } + }, + "license": { + "links": { + "self": "https://realms-staging.stack.cards/catalog/License/4c5a023b-a72c-4f90-930b-da60a1de5b2d" + } + }, + "publisher": { + "links": { + "self": null + } + }, + "categories": { + "links": { + "self": null + } + }, + "examples.0": { + "links": { + "self": "../CreateYourOwnAdventureV4/6afda2db-c1a1-4d42-9b0e-ecac1d10d28a" + } + }, + "examples.1": { + "links": { + "self": "../create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/CreateYourOwnAdventureV4/d43addec-1547-4772-8850-7c7432ec56ac" + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/CreateYourOwnAdventureV4/6afda2db-c1a1-4d42-9b0e-ecac1d10d28a.json b/CreateYourOwnAdventureV4/6afda2db-c1a1-4d42-9b0e-ecac1d10d28a.json new file mode 100644 index 0000000..2dec74b --- /dev/null +++ b/CreateYourOwnAdventureV4/6afda2db-c1a1-4d42-9b0e-ecac1d10d28a.json @@ -0,0 +1,54 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CreateYourOwnAdventureV4", + "module": "../create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4" + } + }, + "type": "card", + "attributes": { + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "startedAt": "2025-10-12T11:43:39.932Z", + "chatRoomId": "!LoYoPYTqKbPvlUDDhb:stack.cards", + "gameStatus": "playing", + "totalTurns": 1, + "completedAt": null, + "currentTurn": 1, + "oneShotTags": [], + "oneShotTitle": "Cyberpunk Malaysia", + "lastNarration": "The neon-soaked streets of Kuala Lumpur pulse with electric energy as rain slicks the asphalt. You're walking through a narrow alley behind Jalan Alor when the rumble of modified motorcycles echoes off the concrete walls.\n\nThree riders emerge from the shadows—the local mat rempit crew, their bikes gleaming with illegal neon underglow and quantum-tuned exhausts. Chrome helmets reflect the holographic advertisements floating overhead. The leader revs his engine, a cybernetic arm gripping the handlebars.\n\n\"Eh, stranger,\" he calls out in accented English, visor sliding up to reveal augmented eyes. \"You lost or what? This our territory, lah.\"\n\nThe other two flank you, blocking escape routes. Rain continues to fall, mixing with the smell of engine oil and street food from the nearby hawker stalls.\n\n**What do you do?**\n\n1. **Try to negotiate** - Show respect and explain you're just passing through\n2. **Run for it** - Sprint toward the crowded main street where there's safety in numbers \n3. **Stand your ground** - Face them directly and see what they really want\n4. **Or tell me your own approach...**", + "lastTimestamp": "2025-10-12T11:43:47.493Z", + "adventureTitle": null, + "lastTurnNumber": 1, + "lastImagePrompt": "cyberpunk alley Kuala Lumpur neon motorcycles rain mat rempit crew chrome helmets", + "lastIsPlayerTurn": false, + "lastPlayerChoice": null, + "selectedScenario": { + "key": "one-shot", + "cardTitle": "Cyberpunk Malaysia", + "cardDescription": "You are caught by the local mat rempit group..." + }, + "autoGenerateImages": true, + "oneShotDescription": "You are caught by the local mat rempit group...", + "oneShotImageStyles": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + }, + "linkedScenarios": { + "links": { + "self": null + } + } + } + } +} diff --git a/Spec/0fa91427-0271-4ce9-bd8c-5e2f30583ef3.json b/Spec/0fa91427-0271-4ce9-bd8c-5e2f30583ef3.json new file mode 100644 index 0000000..14d3147 --- /dev/null +++ b/Spec/0fa91427-0271-4ce9-bd8c-5e2f30583ef3.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4", + "name": "AdventureScenarioV4" + }, + "specType": "field", + "containedExamples": [], + "cardTitle": "Adventure Scenario V4", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/Spec/6e0fa914-2702-413c-a9bd-8c5e2f30583e.json b/Spec/6e0fa914-2702-413c-a9bd-8c5e2f30583e.json new file mode 100644 index 0000000..af0f860 --- /dev/null +++ b/Spec/6e0fa914-2702-413c-a9bd-8c5e2f30583e.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4", + "name": "CreateYourOwnAdventureV4" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "Create Your Own Adventure V4", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/Spec/8398f06e-0fa9-4427-8271-3ce9bd8c5e2f.json b/Spec/8398f06e-0fa9-4427-8271-3ce9bd8c5e2f.json new file mode 100644 index 0000000..90954d4 --- /dev/null +++ b/Spec/8398f06e-0fa9-4427-8271-3ce9bd8c5e2f.json @@ -0,0 +1,40 @@ +{ + "data": { + "type": "card", + "attributes": { + "readMe": null, + "ref": { + "module": "../create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-scenario", + "name": "AdventureScenario" + }, + "specType": "card", + "containedExamples": [], + "cardTitle": "Adventure Scenario", + "cardDescription": null, + "cardInfo": { + "name": null, + "summary": null, + "cardThumbnailURL": null, + "notes": null + } + }, + "relationships": { + "linkedExamples": { + "links": { + "self": null + } + }, + "cardInfo.theme": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/spec", + "name": "Spec" + } + } + } +} \ No newline at end of file diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/detective-mystery.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/detective-mystery.json new file mode 100644 index 0000000..82bf281 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/detective-mystery.json @@ -0,0 +1,38 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AdventureScenario", + "module": "../adventure-scenario" + } + }, + "type": "card", + "attributes": { + "key": "detective-mystery", + "tags": [ + "noir", + "mystery", + "gritty" + ], + "cardTitle": "The Vanishing Detective", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "cardDescription": "A famed sleuth disappears in 1947 Manhattan. The fog knows more.", + "imageStyles": [ + "film-noir", + "black-and-white" + ] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/enchanted-forest.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/enchanted-forest.json new file mode 100644 index 0000000..8010fbd --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/enchanted-forest.json @@ -0,0 +1,31 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AdventureScenario", + "module": "../adventure-scenario" + } + }, + "type": "card", + "attributes": { + "key": "enchanted-forest", + "tags": [], + "cardTitle": "The Enchanted Forest", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "cardDescription": "A magical wood seeks your help to break a moonlit curse.", + "imageStyles": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/space-station.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/space-station.json new file mode 100644 index 0000000..2fadcde --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/space-station.json @@ -0,0 +1,31 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AdventureScenario", + "module": "../adventure-scenario" + } + }, + "type": "card", + "attributes": { + "key": "space-station", + "tags": [], + "cardTitle": "Lost in Space", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "cardDescription": "You wake on a silent space station with flickering lights and no memory.", + "imageStyles": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/time-traveler.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/time-traveler.json new file mode 100644 index 0000000..bb83ef0 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/time-traveler.json @@ -0,0 +1,35 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AdventureScenario", + "module": "../adventure-scenario" + } + }, + "type": "card", + "attributes": { + "key": "time-traveler", + "tags": [ + "combat" + ], + "cardTitle": "Temporal Paradox", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "cardDescription": "Your machine misfires, stranding you with knowledge from the future.", + "imageStyles": [ + "time-relevant" + ] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/undersea-kingdom.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/undersea-kingdom.json new file mode 100644 index 0000000..194b4f8 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/AdventureScenario/undersea-kingdom.json @@ -0,0 +1,31 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "AdventureScenario", + "module": "../adventure-scenario" + } + }, + "type": "card", + "attributes": { + "key": "undersea-kingdom", + "tags": [], + "cardTitle": "The Sunken Kingdom", + "cardInfo": { + "notes": null, + "name": null, + "summary": null, + "cardThumbnailURL": null + }, + "cardDescription": "An ancient realm beneath the waves stirs once more.", + "imageStyles": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/CreateYourOwnAdventureV4/d43addec-1547-4772-8850-7c7432ec56ac.json b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/CreateYourOwnAdventureV4/d43addec-1547-4772-8850-7c7432ec56ac.json new file mode 100644 index 0000000..9ecf0e9 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/CreateYourOwnAdventureV4/d43addec-1547-4772-8850-7c7432ec56ac.json @@ -0,0 +1,74 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "CreateYourOwnAdventureV4", + "module": "../adventure-v4" + } + }, + "type": "card", + "attributes": { + "cardInfo": { + "name": "New Adventure", + "notes": null, + "summary": null, + "cardThumbnailURL": null + }, + "startedAt": "2025-10-12T11:39:37.144Z", + "chatRoomId": "!OVtpepPNVyUHZIyhoV:stack.cards", + "gameStatus": "playing", + "totalTurns": 1, + "completedAt": null, + "currentTurn": 1, + "oneShotTags": [], + "oneShotTitle": null, + "lastNarration": "# Turn 1: Awakening\n\nYour eyes flutter open to the harsh glare of emergency lighting. The metallic taste of recycled air fills your lungs as you struggle to sit up. Around you, the space station hums with an unsettling quiet—too quiet for a functioning facility.\n\nThe corridor stretches in both directions, its walls lined with dormant control panels. To your left, a faint blue glow emanates from what appears to be the command center. To your right, you hear the distant sound of machinery still running. Behind you, an airlock door bears warning symbols you can't quite remember the meaning of.\n\nYour head throbs as you try to recall how you got here. Your uniform bears no name tag, and your pockets are empty except for a small device blinking red.\n\n**What do you do?**\n\n1. **Head toward the blue glow** - investigate the command center\n2. **Follow the machinery sounds** - check the engineering section \n3. **Examine the airlock door** - see what lies beyond\n4. **Study the blinking device** - try to understand what it is\n\n*Or describe your own action...*", + "lastTimestamp": "2025-10-12T11:39:51.522Z", + "adventureTitle": "New Adventure", + "lastTurnNumber": 1, + "lastImagePrompt": "person awakening in dark space station corridor, emergency lights, metallic walls, blinking control panels", + "lastIsPlayerTurn": false, + "lastPlayerChoice": null, + "selectedScenario": { + "key": "space-station", + "cardTitle": "Lost in Space", + "cardDescription": "You wake on a silent space station with flickering lights and no memory." + }, + "autoGenerateImages": true, + "oneShotDescription": null, + "oneShotImageStyles": [] + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + }, + "linkedScenarios.0": { + "links": { + "self": "../AdventureScenario/space-station" + } + }, + "linkedScenarios.1": { + "links": { + "self": "../AdventureScenario/time-traveler" + } + }, + "linkedScenarios.2": { + "links": { + "self": "../AdventureScenario/enchanted-forest" + } + }, + "linkedScenarios.3": { + "links": { + "self": "../AdventureScenario/undersea-kingdom" + } + }, + "linkedScenarios.4": { + "links": { + "self": "../AdventureScenario/detective-mystery" + } + } + } + } +} \ No newline at end of file diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-scenario.gts b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-scenario.gts new file mode 100644 index 0000000..d5d2e41 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-scenario.gts @@ -0,0 +1,409 @@ +import { and } from '@cardstack/boxel-ui/helpers'; +/* + Adventure Scenario (CardDef) + - Standalone scenarios that can be re-used by Adventure cards + - Keeps fields compatible with the V3/V4 embedded scenario structure +*/ + +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { + CardDef, + Component, + field, + contains, + containsMany, // ¹ᵃ for tags/imageStyles/contentWarnings +} from 'https://cardstack.com/base/card-api'; // ¹ Core imports +import StringField from 'https://cardstack.com/base/string'; // ² Base fields +import BookOpenIcon from '@cardstack/boxel-icons/book-open'; // ³ Icon + +export class AdventureScenario extends CardDef { + // ⁴ CardDef + static displayName = 'Adventure Scenario'; + static icon = BookOpenIcon; + + // ⁵ Fields + @field key = contains(StringField); + @field cardTitle = contains(StringField); + @field cardDescription = contains(StringField); + + // Open-ended guidance + @field tags = containsMany(StringField); // narrative/style hints (free-form) + @field imageStyles = containsMany(StringField); // visual-only hints (e.g., "pixel-art") + + // ⁶ Isolated format (simple, readable) + static isolated = class Isolated extends Component { + + }; + + // ⁷ Embedded format (compact row) + static embedded = class Embedded extends Component { + + }; + + // ⁸ Fitted format (badge/strip/tile/card patterns) + static fitted = class Fitted extends Component { + + }; +} diff --git a/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4.gts b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4.gts new file mode 100644 index 0000000..9f3e4c6 --- /dev/null +++ b/create-your-own-adventure-game-31ffda5d-900b-483c-8a44-b8e7c4a099e1/adventure-v4.gts @@ -0,0 +1,1884 @@ +/* + Adventure V4 - UI-focused refresh + - Lighter header (scenario title primary) + - Turn Bar (Turn N • time • scenario tag) + - Compact action bar: Open Chat (primary), Generate Image (secondary), Reset (ghost) + - Latest-only story panel (image-first, zoom affordance placeholder) + - Mobile-first spacing and type + - Keeps V3 logic (latest-only fields, per-instance chat room, ephemeral images, optional auto loop) + - Legacy setupRoom + imageGenerationSkill preserved (not invoked) +*/ + +// ═══ [EDIT TRACKING: ON] Mark all changes with ⁿ ═══ +import { fn, get, hash } from '@ember/helper'; +import { + CardDef, + FieldDef, + Component, + field, + contains, + containsMany, + linksTo, + linksToMany, +} from 'https://cardstack.com/base/card-api'; // ¹ Core imports +import StringField from 'https://cardstack.com/base/string'; + +import MarkdownField from 'https://cardstack.com/base/markdown'; +import DatetimeField from 'https://cardstack.com/base/datetime'; +import BooleanField from 'https://cardstack.com/base/boolean'; +import NumberField from 'https://cardstack.com/base/number'; +import { Button } from '@cardstack/boxel-ui/components'; +import { + formatDateTime, + eq, + gt, + or, + not, + pick, +} from '@cardstack/boxel-ui/helpers'; +import { on } from '@ember/modifier'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { restartableTask, timeout } from 'ember-concurrency'; +import BookOpenIcon from '@cardstack/boxel-icons/book-open'; +import UseAiAssistantCommand from '@cardstack/boxel-host/commands/ai-assistant'; +import SetActiveLLMCommand from '@cardstack/boxel-host/commands/set-active-llm'; +import PatchCardInstanceCommand from '@cardstack/boxel-host/commands/patch-card-instance'; +import PatchFieldsCommand from '@cardstack/boxel-host/commands/patch-fields'; +import SendRequestViaProxyCommand from '@cardstack/boxel-host/commands/send-request-via-proxy'; + +import { AdventureScenario } from './adventure-scenario'; // ¹ᵇ Linked Scenarios + +// Optional compatibility FieldDefs (not used for display; kept for schema continuity) + +export class AdventureScenarioV4 extends FieldDef { + static displayName = 'Adventure Scenario V4'; + @field key = contains(StringField); + @field cardTitle = contains(StringField); + @field cardDescription = contains(StringField); +} + +export class CreateYourOwnAdventureV4 extends CardDef { + static displayName = 'Create Your Own Adventure V4'; + static icon = BookOpenIcon; + static prefersWideFormat = true; + + // Identity and state + @field adventureTitle = contains(StringField); + @field selectedScenario = contains(AdventureScenarioV4); // embedded (compat) + + // Linked scenarios + one-shot editor + @field linkedScenarios = linksToMany(AdventureScenario); // pick from real cards + @field oneShotTitle = contains(StringField); + @field oneShotDescription = contains(StringField); + @field oneShotTags = containsMany(StringField); + @field oneShotImageStyles = containsMany(StringField); + + @field gameStatus = contains(StringField); // 'setup' | 'playing' | 'completed' + @field currentTurn = contains(NumberField); + @field totalTurns = contains(NumberField); + @field startedAt = contains(DatetimeField); + @field completedAt = contains(DatetimeField); + @field chatRoomId = contains(StringField); + + // Legacy (kept, not used by default) + + // Latest-only model + @field lastTurnNumber = contains(NumberField); + @field lastNarration = contains(MarkdownField); + @field lastPlayerChoice = contains(StringField); + @field lastTimestamp = contains(DatetimeField); + @field lastIsPlayerTurn = contains(BooleanField); + @field lastImagePrompt = contains(StringField); + @field autoGenerateImages = contains(BooleanField); + + // Title + @field cardTitle = contains(StringField, { + computeVia: function (this: CreateYourOwnAdventureV4) { + try { + if (this.adventureTitle) return this.adventureTitle; + if (this.selectedScenario?.title) + return `Adventure V4: ${this.selectedScenario.title}`; + return 'Create Your Own Adventure V4'; + } catch (e) { + console.error('Adventure V4: Error computing title', e); + return 'Adventure Game V4'; + } + }, + }); + + static isolated = class Isolated extends Component< + typeof CreateYourOwnAdventureV4 + > { + @tracked isProcessing = false; + @tracked roomId: string | null = null; + @tracked selectedScenarioKey: string | null = null; + @tracked pendingChoice: string = ''; + @tracked currentImageData: string | null = null; // ephemeral render-only + @tracked lastImageRenderedForTurn: number | null = null; + @tracked isImageLoading: boolean = false; // image skeleton state + @tracked toastMessage: string | null = null; // inline toast + @tracked isOverlayVisible: boolean = true; // VN text overlay visibility + @tracked selectedLinkedScenario: any | null = null; // chosen linked scenario + // One‑shot chips editors (tags + image styles) + @tracked newTag: string = ''; + @tracked newStyle: string = ''; + @tracked showLinkedChooser: boolean = false; + + setNewTag = (value: string) => { + this.newTag = value ?? ''; + }; + setNewStyle = (value: string) => { + this.newStyle = value ?? ''; + }; + + addTag = (raw: string) => { + const v = (raw || '').trim(); + if (!v) return; + let arr = Array.isArray(this.args.model?.oneShotTags) + ? this.args.model.oneShotTags + : (this.args.model.oneShotTags = []); + if (!arr.includes(v)) arr.push(v); + this.newTag = ''; + }; + + addStyle = (raw: string) => { + const v = (raw || '').trim(); + if (!v) return; + let arr = Array.isArray(this.args.model?.oneShotImageStyles) + ? this.args.model.oneShotImageStyles + : (this.args.model.oneShotImageStyles = []); + if (!arr.includes(v)) arr.push(v); + this.newStyle = ''; + }; + + onTagKey = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + this.addTag(this.newTag); + } + }; + + onStyleKey = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + this.addStyle(this.newStyle); + } + }; + + removeTag = (idx: number) => { + let arr = Array.isArray(this.args.model?.oneShotTags) + ? this.args.model.oneShotTags + : null; + if (!arr) return; + if (idx >= 0 && idx < arr.length) { + arr.splice(idx, 1); + } + }; + + removeStyle = (idx: number) => { + let arr = Array.isArray(this.args.model?.oneShotImageStyles) + ? this.args.model.oneShotImageStyles + : null; + if (!arr) return; + if (idx >= 0 && idx < arr.length) { + arr.splice(idx, 1); + } + }; + + sanitizeOneShotArrays = () => { + try { + const m = this.args?.model; + if (!m) return; + if (Array.isArray(m.oneShotTags)) { + m.oneShotTags = m.oneShotTags.filter(Boolean); + } + if (Array.isArray(m.oneShotImageStyles)) { + m.oneShotImageStyles = m.oneShotImageStyles.filter(Boolean); + } + } catch {} + }; + + // Auto-dismiss toast after ~2.5s + dismissToast = restartableTask(async () => { + await timeout(2500); + this.toastMessage = null; + }); + + showToast = (msg: string) => { + this.toastMessage = msg; + this.dismissToast.perform(); + }; + + toggleOverlay = () => { + this.isOverlayVisible = !this.isOverlayVisible; + }; + showChooser = () => { + this.showLinkedChooser = true; + }; + hideChooser = () => { + this.showLinkedChooser = false; + }; + + constructor(owner: unknown, args: any) { + super(owner, args); + this.autoImageLoop.perform(); + this.sanitizeOneShotArrays(); + } + + scenarios = []; + + // Auto-image on new turn (UI-side) + autoImageLoop = restartableTask(async () => { + while (true) { + await timeout(800); + try { + const m = this.args?.model; + if (!m) continue; + if (m.gameStatus !== 'playing') continue; + const t = m.lastTurnNumber; + const p = m.lastImagePrompt; + const auto = m.autoGenerateImages !== false; + if (auto && t && p && this.lastImageRenderedForTurn !== t) { + await this.generateSceneImage(); + this.lastImageRenderedForTurn = t; + } + } catch { + /* swallow */ + } + } + }); + + // Start adventure (same orchestration as V3; UI refreshed) + @action + async startAdventure() { + const hasLinked = !!this.selectedLinkedScenario; + const hasDefault = false; + const hasOneShot = + !!this.args.model?.oneShotDescription && + (this.args.model.oneShotDescription.trim?.().length || 0) > 0; + + if (!hasLinked && !hasDefault && !hasOneShot) { + alert('Please pick a linked Scenario or craft a one‑shot first.'); + return; + } + + this.isProcessing = true; + try { + let chosen: any | null = null; + + if (hasLinked) { + chosen = this.selectedLinkedScenario; + } else if (hasOneShot) { + chosen = { + key: 'one-shot', + title: this.args.model.oneShotTitle || 'One‑Shot Scenario', + description: this.args.model.oneShotDescription || '', + }; + } + + if (!chosen) throw new Error('Scenario not found'); + + // Derive kickoff guidance (linked preferred, else one‑shot) + const kickoffTags = ( + hasLinked + ? this.selectedLinkedScenario?.tags ?? [] + : this.args.model?.oneShotTags ?? [] + ).filter(Boolean); + + const kickoffStyles = ( + hasLinked + ? this.selectedLinkedScenario?.imageStyles ?? [] + : this.args.model?.oneShotImageStyles ?? [] + ).filter(Boolean); + + const ctx = this.args.context?.commandContext; + if (!ctx) + throw new Error( + 'Command context does not exist. Please switch to Interact Mode', + ); + + // Patch selected scenario + initialize + const patch = new PatchCardInstanceCommand(ctx); + await patch.execute({ + cardId: this.args.model.id, + patch: { + attributes: { + selectedScenario: { + key: chosen.key, + title: chosen.title, + description: chosen.description, + }, + // Persist guidance for ongoing turns (linked preferred, else one‑shot) + oneShotTags: kickoffTags, + oneShotImageStyles: kickoffStyles, + + gameStatus: 'playing', + autoGenerateImages: true, + currentTurn: 1, + startedAt: new Date().toISOString(), + }, + }, + }); + + // Open assistant with GM skill (latest-only fields) + const gmSkillId = new URL( + './Skill/adventure-game-master', + import.meta.url, + ).href; + const kickoffPrompt = `You are the Adventure Game Master. Use the attached CreateYourOwnAdventureV4 card. + +Start Turn 1 using the selected scenario as seed. End with 2–4 choices and allow open-ended replies. + +GUIDANCE FIELDS (read from oneShotTags/oneShotImageStyles on the card): +- oneShotTags: Use as narrative guidance (tone, setting, atmosphere, POV, theme) +- oneShotImageStyles: Include in lastImagePrompt with concrete scene nouns + +PATCH ONLY latest fields via patch-fields (no arrays): +- lastTurnNumber = 1 +- lastNarration = your Markdown story (shaped by non-visual tags) +- lastIsPlayerTurn = false +- lastTimestamp = new ISO string +- lastImagePrompt = short, concrete prompt (include imageStyles + visual tags; ≤120 chars) +- currentTurn = 1 +- totalTurns = max(totalTurns, 1) + +Do NOT call image APIs; UI handles rendering from lastImagePrompt.`; + + const use = new UseAiAssistantCommand(ctx); + const opts: any = { + openRoom: true, + attachedCards: [this.args.model as CardDef], + prompt: kickoffPrompt, + llmModel: 'anthropic/claude-sonnet-4', + llmMode: 'act', + skillCardIds: [gmSkillId], + }; + if (this.args.model?.chatRoomId) { + opts.roomId = this.args.model.chatRoomId; + } else { + opts.roomId = 'new'; + opts.roomName = `Adventure V4: ${chosen.title}`; + } + const result = await use.execute(opts); + this.roomId = result.roomId; + + // Persist room id + await patch.execute({ + cardId: this.args.model.id, + patch: { attributes: { chatRoomId: this.roomId } }, + }); + + // Ensure agentic mode + const set = new SetActiveLLMCommand(ctx); + await set.execute({ roomId: this.roomId, mode: 'act' }); + + // Gentle toast + alert('🎉 Adventure started! Open the chat to continue.'); + } catch (e: any) { + console.error('Adventure V4 start error:', e); + alert(`Failed to start: ${e?.message || String(e)}`); + try { + const ctx = this.args.context?.commandContext; + const patch = new PatchCardInstanceCommand(ctx); + await patch.execute({ + cardId: this.args.model.id, + patch: { attributes: { gameStatus: 'setup' } }, + }); + } catch {} + } finally { + this.isProcessing = false; + } + } + + @action + async resetAdventure() { + if ( + !confirm( + 'Reset this adventure? Linked Scenarios remain linked; one‑shot fields stay as entered.', + ) + ) + return; + try { + const ctx = this.args.context?.commandContext; + const patch = new PatchCardInstanceCommand(ctx); + await patch.execute({ + cardId: this.args.model.id, + patch: { + attributes: { + gameStatus: 'setup', + selectedScenario: null, + currentTurn: 0, + startedAt: null, + completedAt: null, + chatRoomId: null, + lastTurnNumber: null, + lastNarration: null, + lastPlayerChoice: null, + lastTimestamp: null, + lastIsPlayerTurn: null, + lastImagePrompt: null, + }, + }, + }); + this.selectedScenarioKey = null; + this.roomId = null; + this.pendingChoice = ''; + this.currentImageData = null; + } catch (e: any) { + console.error('Reset error:', e); + alert(`Failed to reset: ${e?.message || String(e)}`); + } + } + + @action + selectScenario(key: string) { + this.selectedScenarioKey = key; + this.selectedLinkedScenario = null; // prefer explicit default pick + } + + @action + selectLinked(s: any) { + try { + this.selectedLinkedScenario = s || null; + this.selectedScenarioKey = s?.key || null; + // Sync one-shot guidance to the linked Scenario so UI and kickoff align + if (s) { + const tags = Array.isArray(s.tags) ? [...s.tags] : []; + const styles = Array.isArray(s.imageStyles) ? [...s.imageStyles] : []; + this.args.model.oneShotTags = tags; + this.args.model.oneShotImageStyles = styles; + this.newTag = ''; + this.newStyle = ''; + } + } catch { + this.selectedLinkedScenario = null; + } + } + + @action + async startWithLinked(s: any) { + try { + this.selectLinked(s); + await this.startAdventure(); + } catch (e) { + console.error('Failed to start with linked scenario', e); + } + } + + // Generate image (ephemeral render-only) + @action + async generateSceneImage() { + if (this.args.model.gameStatus !== 'playing') return; + const imagePrompt = this.args.model?.lastImagePrompt; + if (!imagePrompt) return; + + this.isProcessing = true; + this.isImageLoading = true; + // keep currentImageData until new one arrives to prevent UI flicker + + try { + const ctx = this.args.context?.commandContext; + if (!ctx) throw new Error('Please switch to Interact Mode'); + + const proxy = new SendRequestViaProxyCommand(ctx); + const res = await proxy.execute({ + url: 'https://openrouter.ai/api/v1/chat/completions', + method: 'POST', + requestBody: JSON.stringify({ + model: 'google/gemini-2.5-flash-image-preview', + messages: [{ role: 'user', content: imagePrompt }], + }), + }); + + if (!res.response?.ok) { + const text = res.response ? await res.response.text() : ''; + throw new Error( + `${res.response?.status} ${res.response?.statusText} • ${text}`, + ); + } + + const data = await res.response.json(); + const msg = data?.choices?.[0]?.message; + const url = Array.isArray(msg?.images) + ? msg.images + .map((i: any) => i?.image_url?.url) + .find((u: string) => u?.startsWith('data:image/')) + : null; + + if (!url) throw new Error('No data:image/* URL found'); + this.currentImageData = url; + } catch (e: any) { + console.error('Image generation error:', e); + this.showToast(`Image failed: ${e?.message || String(e)}`); + this.currentImageData = null; + } finally { + this.isProcessing = false; + this.isImageLoading = false; + } + } + + get gameInProgress() { + return this.args.model.gameStatus === 'playing'; + } + get gameNotStarted() { + return ( + !this.args.model.gameStatus || this.args.model.gameStatus === 'setup' + ); + } + + + }; + + static embedded = class Embedded extends Component< + typeof CreateYourOwnAdventureV4 + > { + + }; + + static fitted = class Fitted extends Component< + typeof CreateYourOwnAdventureV4 + > { + + }; +}