package pncdsl // Status is the outcome of one tick of a Runner. type Status int const ( StatusRunning Status = iota StatusDone StatusFailed ) // Ctx is the per-tick context passed to running actions. type Ctx struct { Game *Game DT float64 Scene *Scene Hotspot *Hotspot Item *Item } // Action is an immutable spec of work; Start makes a fresh Runner with // state. Storing an Action in a struct field (e.g. Hotspot.OnUse) is safe // because the engine always calls Start before ticking, so two invocations // can never share mutable state. type Action interface { Start() Runner } // Runner is one in-flight execution of an Action. Tick advances it one // frame and returns whether it's still running, done, or failed. type Runner interface { Tick(ctx *Ctx) Status } // ----- immediate action helper ----------------------------------------- type immediateAction struct { fn func(*Ctx) Status } func (a *immediateAction) Start() Runner { return a } func (a *immediateAction) Tick(ctx *Ctx) Status { if a.fn == nil { return StatusDone } return a.fn(ctx) } // Custom wraps a user function as an Action. Returns Done on first tick // unless the function itself returns StatusRunning. func Custom(fn func(*Ctx) Status) Action { return &immediateAction{fn: fn} } // ----- Seq -------------------------------------------------------------- type seqAction struct{ children []Action } type seqRunner struct { spec *seqAction idx int current Runner } func Seq(actions ...Action) Action { return &seqAction{children: flattenSeq(actions)} } func flattenSeq(in []Action) []Action { out := make([]Action, 0, len(in)) for _, a := range in { if a == nil { continue } if s, ok := a.(*seqAction); ok { out = append(out, s.children...) } else { out = append(out, a) } } return out } func (a *seqAction) Start() Runner { return &seqRunner{spec: a} } func (r *seqRunner) Tick(ctx *Ctx) Status { for { if r.idx >= len(r.spec.children) { return StatusDone } if r.current == nil { r.current = r.spec.children[r.idx].Start() } s := r.current.Tick(ctx) switch s { case StatusDone: r.idx++ r.current = nil // keep going only if the just-finished action consumed no time // (zero-dt would loop forever otherwise — that's fine here since // immediate actions complete in one Tick call). continue case StatusFailed: return StatusFailed default: return StatusRunning } } } // ----- Par -------------------------------------------------------------- type parAction struct{ children []Action } type parRunner struct { runners []Runner done []bool } func Par(actions ...Action) Action { return &parAction{children: actions} } func (a *parAction) Start() Runner { rs := make([]Runner, len(a.children)) for i, c := range a.children { rs[i] = c.Start() } return &parRunner{runners: rs, done: make([]bool, len(rs))} } func (r *parRunner) Tick(ctx *Ctx) Status { allDone := true for i, rn := range r.runners { if r.done[i] { continue } s := rn.Tick(ctx) if s == StatusFailed { return StatusFailed } if s == StatusDone { r.done[i] = true continue } allDone = false } if allDone { return StatusDone } return StatusRunning } // ----- If --------------------------------------------------------------- type ifAction struct { cond Condition then Action els Action } func If(cond Condition, then Action, els ...Action) Action { var e Action if len(els) > 0 { e = Seq(els...) } return &ifAction{cond: cond, then: then, els: e} } func (a *ifAction) Start() Runner { return &ifRunner{spec: a} } type ifRunner struct { spec *ifAction started bool inner Runner } func (r *ifRunner) Tick(ctx *Ctx) Status { if !r.started { r.started = true take := r.spec.then if r.spec.cond == nil || !r.spec.cond.Eval(ctx) { take = r.spec.els } if take == nil { return StatusDone } r.inner = take.Start() } if r.inner == nil { return StatusDone } return r.inner.Tick(ctx) } // ----- Wait ------------------------------------------------------------- type waitAction struct{ seconds float64 } type waitRunner struct { spec *waitAction elapsed float64 } func Wait(seconds float64) Action { return &waitAction{seconds: seconds} } func (a *waitAction) Start() Runner { return &waitRunner{spec: a} } func (r *waitRunner) Tick(ctx *Ctx) Status { r.elapsed += ctx.DT if r.elapsed >= r.spec.seconds { return StatusDone } return StatusRunning } // ----- Say -------------------------------------------------------------- type sayAction struct{ speaker, text string } type sayRunner struct { spec *sayAction elapsed float64 duration float64 started bool } func Say(speaker, text string) Action { return &sayAction{speaker: speaker, text: text} } func (a *sayAction) Start() Runner { return &sayRunner{spec: a} } func (r *sayRunner) Tick(ctx *Ctx) Status { if !r.started { r.started = true // duration scales with text length, with a 1.2s floor r.duration = 1.2 + float64(len(r.spec.text))*0.05 ctx.Game.SetSpeech(r.spec.speaker, r.spec.text) ctx.Game.LogResponse(r.spec.speaker, r.spec.text) } r.elapsed += ctx.DT // skip on click if ctx.Game.Input.consumedClick() { r.elapsed = r.duration } if r.elapsed >= r.duration { ctx.Game.ClearSpeech() return StatusDone } return StatusRunning } // ----- GoTo ------------------------------------------------------------- type gotoAction struct{ scene string } func GoTo(scene string) Action { return &gotoAction{scene: scene} } func (a *gotoAction) Start() Runner { return a } func (a *gotoAction) Tick(ctx *Ctx) Status { ctx.Game.changeScene(a.scene) return StatusDone } // ----- inventory -------------------------------------------------------- type giveAction struct{ item string } func Give(item string) Action { return &giveAction{item: item} } func (a *giveAction) Start() Runner { return a } func (a *giveAction) Tick(ctx *Ctx) Status { ctx.Game.Inventory.Add(a.item) return StatusDone } type takeAwayAction struct{ item string } func TakeAway(item string) Action { return &takeAwayAction{item: item} } func (a *takeAwayAction) Start() Runner { return a } func (a *takeAwayAction) Tick(ctx *Ctx) Status { ctx.Game.Inventory.Remove(a.item) return StatusDone } // RequireItem fails silently with a generic line if the player doesn't have // the named item selected or in inventory. Used at the top of Use handlers. type requireItemAction struct{ item string } func RequireItem(item string) Action { return &requireItemAction{item: item} } func (a *requireItemAction) Start() Runner { return a } func (a *requireItemAction) Tick(ctx *Ctx) Status { if ctx.Game.Inventory.Has(a.item) { return StatusDone } ctx.Game.FlashLine("Ehhez kell egy " + a.item + ".") return StatusFailed } // ----- flags / vars ----------------------------------------------------- type setFlagAction struct{ name string } func SetFlag(name string) Action { return &setFlagAction{name: name} } func (a *setFlagAction) Start() Runner { return a } func (a *setFlagAction) Tick(ctx *Ctx) Status { ctx.Game.State.SetFlag(a.name); return StatusDone } type clearFlagAction struct{ name string } func ClearFlag(name string) Action { return &clearFlagAction{name: name} } func (a *clearFlagAction) Start() Runner { return a } func (a *clearFlagAction) Tick(ctx *Ctx) Status { ctx.Game.State.ClearFlag(a.name) return StatusDone } type setVarAction struct { name string v any } func SetVar(name string, v any) Action { return &setVarAction{name: name, v: v} } func (a *setVarAction) Start() Runner { return a } func (a *setVarAction) Tick(ctx *Ctx) Status { ctx.Game.State.SetVar(a.name, a.v) return StatusDone } // ----- audio ------------------------------------------------------------ type playMusicAction struct{ name string } func PlayMusic(name string) Action { return &playMusicAction{name: name} } func (a *playMusicAction) Start() Runner { return a } func (a *playMusicAction) Tick(ctx *Ctx) Status { ctx.Game.Audio.PlayMusic(a.name) return StatusDone } type stopMusicAction struct{} func StopMusic() Action { return &stopMusicAction{} } func (a *stopMusicAction) Start() Runner { return a } func (a *stopMusicAction) Tick(ctx *Ctx) Status { ctx.Game.Audio.StopMusic(); return StatusDone } type playSoundAction struct{ name string } func PlaySound(name string) Action { return &playSoundAction{name: name} } func (a *playSoundAction) Start() Runner { return a } func (a *playSoundAction) Tick(ctx *Ctx) Status { ctx.Game.Audio.PlaySound(a.name) return StatusDone } // ----- dialogue --------------------------------------------------------- type runDialogueAction struct{ name string } func RunDialogue(name string) Action { return &runDialogueAction{name: name} } func (a *runDialogueAction) Start() Runner { return &runDialogueRunner{spec: a} } type runDialogueRunner struct { spec *runDialogueAction started bool } func (r *runDialogueRunner) Tick(ctx *Ctx) Status { if !r.started { r.started = true ctx.Game.startDialogue(r.spec.name) } if ctx.Game.dialogueActive() { return StatusRunning } return StatusDone } type endDialogueAction struct{} func EndDialogue() Action { return &endDialogueAction{} } func (a *endDialogueAction) Start() Runner { return a } func (a *endDialogueAction) Tick(ctx *Ctx) Status { ctx.Game.endDialogue() return StatusDone } type gotoNodeAction struct{ node string } func GotoNode(node string) Action { return &gotoNodeAction{node: node} } func (a *gotoNodeAction) Start() Runner { return a } func (a *gotoNodeAction) Tick(ctx *Ctx) Status { ctx.Game.gotoDialogueNode(a.node) return StatusDone } // ----- scripts ---------------------------------------------------------- type runScriptAction struct{ name string } func RunScript(name string) Action { return &runScriptAction{name: name} } func (a *runScriptAction) Start() Runner { return &runScriptRunner{spec: a} } type runScriptRunner struct { spec *runScriptAction inner Runner } func (r *runScriptRunner) Tick(ctx *Ctx) Status { if r.inner == nil { s, ok := ctx.Game.ScriptManager.Get(r.spec.name) if !ok || s.Actions == nil { return StatusFailed } r.inner = s.Actions.Start() } return r.inner.Tick(ctx) } // ----- character movement ---------------------------------------------- type walkAction struct { character string to Point } func Walk(character string, to Point) Action { return &walkAction{character: character, to: to} } func (a *walkAction) Start() Runner { return &walkRunner{spec: a} } type walkRunner struct { spec *walkAction started bool } func (r *walkRunner) Tick(ctx *Ctx) Status { if !r.started { r.started = true ctx.Game.walkCharacter(r.spec.character, r.spec.to) } if ctx.Game.characterMoving(r.spec.character) { return StatusRunning } return StatusDone } // ----- misc ------------------------------------------------------------- type showEndAction struct{ text string } func ShowEnd(text string) Action { return &showEndAction{text: text} } func (a *showEndAction) Start() Runner { return &showEndRunner{spec: a} } type showEndRunner struct { spec *showEndAction started bool } func (r *showEndRunner) Tick(ctx *Ctx) Status { if !r.started { r.started = true ctx.Game.showEndCard(r.spec.text) } return StatusRunning // never finishes; player closes the window }