Add merit node type (Scene 8 ceremony + achievement award)
A merit node awards a configured achievement (awards_flag) to the player the moment the playthrough arrives on it, with node provenance, then presents a ceremony. Migration 028 extends node_type + the component_key CHECK and adds awards_flag; the runtime grants the flag in advance/goto/ new-game write paths; MeritHost renders the ceremony (a component_key can supply a bespoke one, e.g. a 3D model). Verified: teleport to a merit node auto-grants its achievement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+20
-1
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState, type FC } from 'react'
|
||||
import { audio } from './audio'
|
||||
|
||||
export type RuntimeUtterance = { id: string; utterer: 'npc' | 'player'; speaker: { name: string; role: string }; poseUrl: string | null; text: string; childIds: string[]; terminalKey: string | null }
|
||||
export type RuntimeNode = { id: string; kind: 'cutscene' | 'dialogue' | 'level'; label: string; componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null; musicVolume?: number; utterances?: RuntimeUtterance[]; rootId?: string | null }
|
||||
export type RuntimeNode = { id: string; kind: 'cutscene' | 'dialogue' | 'level' | 'merit'; label: string; componentKey?: string | null; levelSlug?: string | null; musicUrl?: string | null; musicVolume?: number; awardsFlag?: string | null; utterances?: RuntimeUtterance[]; rootId?: string | null }
|
||||
export type PlaythroughSummary = { id: string; mysterySlug: string; levelSlug: string | null; status: string }
|
||||
export type PlaythroughState = { playthrough: PlaythroughSummary; node: RuntimeNode | null }
|
||||
|
||||
@@ -48,6 +48,25 @@ const GlassHarbourDiversion: FC<{ onComplete: () => void }> = ({ onComplete }) =
|
||||
const CUTSCENE_REGISTRY: Record<string, FC<{ onComplete: () => void }>> = { 'glass-harbour-diversion': GlassHarbourDiversion }
|
||||
export const CUTSCENE_COMPONENT_KEYS = Object.keys(CUTSCENE_REGISTRY)
|
||||
|
||||
// Merit ceremony components, keyed by a merit node's component_key (e.g. a 3D
|
||||
// award model). The achievement itself is granted server-side on arrival; this is
|
||||
// purely the presentation of receiving it.
|
||||
const MERIT_REGISTRY: Record<string, FC<{ label: string; onComplete: () => void }>> = {}
|
||||
export const MERIT_COMPONENT_KEYS = Object.keys(MERIT_REGISTRY)
|
||||
|
||||
export function MeritHost({ componentKey, label, awardsFlag, onComplete }: { componentKey: string | null | undefined; label: string; awardsFlag?: string | null; onComplete: () => void }) {
|
||||
const Component = componentKey ? MERIT_REGISTRY[componentKey] : undefined
|
||||
if (Component) return <Component label={label} onComplete={onComplete} />
|
||||
return <div className="cutscene-card merit-card" onClick={onComplete}>
|
||||
<div className="title-card-inner">
|
||||
<small className="merit-eyebrow">◆ MERIT AWARDED ◆</small>
|
||||
<h1>{label}</h1>
|
||||
{awardsFlag && <p className="merit-flag">🏅 {awardsFlag}</p>}
|
||||
<button className="cutscene-begin" onClick={event => { event.stopPropagation(); onComplete() }}>Accept ▸</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function CutsceneHost({ componentKey, label, onComplete }: { componentKey: string | null | undefined; label: string; onComplete: () => void }) {
|
||||
const Component = componentKey ? CUTSCENE_REGISTRY[componentKey] : undefined
|
||||
if (Component) return <Component onComplete={onComplete} />
|
||||
|
||||
Reference in New Issue
Block a user