Files
pncdsl/action.def.go
2026-05-25 21:28:17 +02:00

452 lines
11 KiB
Go

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
}