aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/grid_test.go4
-rw-r--r--internal/world/insert_remove.go3
-rw-r--r--internal/world/mob.go24
-rw-r--r--internal/world/room.go71
-rw-r--r--internal/world/room_migrate.go15
-rw-r--r--internal/world/room_migrate_test.go26
-rw-r--r--internal/world/trigger.go65
-rw-r--r--internal/world/trigger_store.go9
8 files changed, 92 insertions, 125 deletions
diff --git a/internal/world/grid_test.go b/internal/world/grid_test.go
index 9c2c7c6..c777f90 100644
--- a/internal/world/grid_test.go
+++ b/internal/world/grid_test.go
@@ -117,11 +117,11 @@ func TestBuildGridConflictsHypotheticalInsert(t *testing.T) {
copy.Exits = make(map[ExitDir]ExitDef, len(r.Exits))
for k, v := range r.Exits {
if id == 1 && k == East {
- copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, SetGlobalFlags: v.SetGlobalFlags, SetPlayerFlags: v.SetPlayerFlags, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked}
+ copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, OnTraverse: v.OnTraverse, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked}
continue
}
if id == 2 && k == West {
- copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, SetGlobalFlags: v.SetGlobalFlags, SetPlayerFlags: v.SetPlayerFlags, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked}
+ copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, OnTraverse: v.OnTraverse, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked}
continue
}
copy.Exits[k] = v
diff --git a/internal/world/insert_remove.go b/internal/world/insert_remove.go
index c55427c..05bfa22 100644
--- a/internal/world/insert_remove.go
+++ b/internal/world/insert_remove.go
@@ -20,8 +20,7 @@ func RewirePreserving(src ExitDef, newTarget int) ExitDef {
Room: newTarget,
Condition: src.Condition,
BlockedMessage: src.BlockedMessage,
- SetGlobalFlags: src.SetGlobalFlags,
- SetPlayerFlags: src.SetPlayerFlags,
+ OnTraverse: src.OnTraverse,
Hidden: src.Hidden,
AlwaysBlocked: src.AlwaysBlocked,
}
diff --git a/internal/world/mob.go b/internal/world/mob.go
index 6780c45..a9e2f7e 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -78,13 +78,13 @@ type MobCombat struct {
}
type MobDef struct {
- ID string `yaml:"id"`
- Name string `yaml:"name"`
- Description string `yaml:"description"`
- IdleDescriptions []string `yaml:"idle_descriptions"`
- Protected bool `yaml:"protected"`
- Unique bool `yaml:"unique"`
- Drops MobDropTable `yaml:"drops"`
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ IdleDescriptions []string `yaml:"idle_descriptions"`
+ Protected bool `yaml:"protected"`
+ Unique bool `yaml:"unique"`
+ Drops MobDropTable `yaml:"drops"`
Steal *MobSteal `yaml:"steal,omitempty"`
Task *MobTask `yaml:"task,omitempty"`
@@ -93,6 +93,8 @@ type MobDef struct {
Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
Shop *behavior.ShopConfig `yaml:"shop,omitempty"`
+
+ OnKill []behavior.Interaction `yaml:"on_kill,omitempty"`
}
func (d *MobDef) IsTalkable() bool { return d.Talk != nil }
@@ -165,6 +167,13 @@ type MobInstance struct {
TalkConfig *behavior.TalkConfig
+ // OnKill is copied from the def at spawn time and fires from endCombat
+ // when this mob is defeated — either by a combat kill or by a task mob's
+ // HP draining to zero (the "completion" of the work). The first entry
+ // whose item filter, Condition, and the first-match-wins rule all pass
+ // fires.
+ OnKill []behavior.Interaction
+
// Shop is the (shared, read-only) shop config from the def. Per-instance
// live stock is tracked in ShopStock; ShopRestock holds per-item countdown
// timers. All three are only mutated under MobStore.mu.
@@ -332,6 +341,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in
CompleteMessage: completeMessage,
CombatDescriptions: combatDescriptions,
TalkConfig: def.Talk,
+ OnKill: def.OnKill,
}
if def.Shop != nil {
inst.Shop = def.Shop
diff --git a/internal/world/room.go b/internal/world/room.go
index 2da40fb..08e18a4 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -75,13 +75,12 @@ type SpawnDef struct {
}
type ExitDef struct {
- Room int `yaml:"room"`
- Condition *behavior.Condition `yaml:"condition,omitempty"`
- BlockedMessage string `yaml:"blocked_message,omitempty"`
- SetGlobalFlags map[string]any `yaml:"set_global_flags,omitempty"`
- SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty"`
- Hidden bool `yaml:"hidden,omitempty"`
- AlwaysBlocked bool `yaml:"always_blocked,omitempty"`
+ Room int `yaml:"room"`
+ Condition *behavior.Condition `yaml:"condition,omitempty"`
+ BlockedMessage string `yaml:"blocked_message,omitempty"`
+ OnTraverse []behavior.Interaction `yaml:"on_traverse,omitempty"`
+ Hidden bool `yaml:"hidden,omitempty"`
+ AlwaysBlocked bool `yaml:"always_blocked,omitempty"`
}
func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error {
@@ -106,7 +105,7 @@ type Room struct {
Objects []RoomObject `yaml:"objects"`
ItemSpawns []SpawnDef `yaml:"item_spawns"`
Mobs []RoomMob `yaml:"mobs"`
- OnEnter []EnterStep `yaml:"on_enter"`
+ OnEnter []behavior.StepAction `yaml:"on_enter"`
Hazard string `yaml:"hazard"`
BlockTransport bool `yaml:"block_transport"`
Triggers []TriggerDef `yaml:"triggers"`
@@ -131,32 +130,6 @@ func (rm *RoomMob) UnmarshalYAML(value *yaml.Node) error {
return value.Decode((*raw)(rm))
}
-type EnterStep struct {
- Message string `yaml:"message"`
- Condition *behavior.Condition `yaml:"condition"`
- Delay int `yaml:"delay"`
- SetGlobalFlags map[string]any `yaml:"set_global_flags"`
- SetPlayerFlags map[string]any `yaml:"set_player_flags"`
- Broadcast string `yaml:"broadcast"`
- BroadcastGlobal string `yaml:"broadcast_global"`
- SpawnMob *SpawnMobConfig `yaml:"spawn_mob"`
- DespawnMob string `yaml:"despawn_mob"`
- GiveItem string `yaml:"give_item"`
- TakeItem string `yaml:"take_item"`
- Teleport int `yaml:"teleport"`
- Heal int `yaml:"heal"`
-}
-
-// IsTimed reports whether the step carries enter-sequence semantics (any
-// non-zero field means it needs per-tick scheduling rather than synchronous
-// printing).
-func (e EnterStep) IsTimed() bool {
- return e.Delay > 0 || len(e.SetGlobalFlags) > 0 || len(e.SetPlayerFlags) > 0 ||
- e.Message != "" || e.Broadcast != "" || e.BroadcastGlobal != "" ||
- e.SpawnMob != nil || e.DespawnMob != "" || e.GiveItem != "" ||
- e.TakeItem != "" || e.Teleport != 0 || e.Heal != 0
-}
-
// RoomObject is either a reference to a file-backed object definition (only
// `id`/wander fields set) or a fully local object definition (Local != nil).
// An entry is treated as local when it carries any passive content field
@@ -190,7 +163,7 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error {
return err
}
isLocal := def.Name != "" || len(def.Description) > 0 || def.InRoomDescription != "" ||
- len(def.Aliases) > 0 || def.Color != "" || def.OnLook != nil
+ len(def.Aliases) > 0 || def.Color != "" || len(def.OnLook) > 0
if isLocal {
ro.ID = NormalizeObjectName(def.Name)
ro.Local = &def
@@ -204,19 +177,19 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error {
func (ro RoomObject) MarshalYAML() (interface{}, error) {
if ro.Local != nil {
type inlineObj struct {
- Name string `yaml:"name"`
- Aliases []string `yaml:"aliases,omitempty"`
- Color string `yaml:"color,omitempty"`
- Hidden bool `yaml:"hidden,omitempty"`
- InRoomDescription string `yaml:"inroom_description,omitempty"`
- RemovalItem string `yaml:"removal_item,omitempty"`
- Description behavior.DescList `yaml:"description,omitempty"`
- UseInteractions []object.UseInteraction `yaml:"use_interactions,omitempty"`
- Steal *object.ObjectSteal `yaml:"steal,omitempty"`
- Gather *behavior.GatherConfig `yaml:"gather,omitempty"`
- Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
- Safespot *object.SafespotConfig `yaml:"safespot,omitempty"`
- OnLook *behavior.NodeAction `yaml:"on_look,omitempty"`
+ Name string `yaml:"name"`
+ Aliases []string `yaml:"aliases,omitempty"`
+ Color string `yaml:"color,omitempty"`
+ Hidden bool `yaml:"hidden,omitempty"`
+ InRoomDescription string `yaml:"inroom_description,omitempty"`
+ RemovalItem string `yaml:"removal_item,omitempty"`
+ Description behavior.DescList `yaml:"description,omitempty"`
+ OnUse []behavior.Interaction `yaml:"on_use,omitempty"`
+ Steal *object.ObjectSteal `yaml:"steal,omitempty"`
+ Gather *behavior.GatherConfig `yaml:"gather,omitempty"`
+ Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
+ Safespot *object.SafespotConfig `yaml:"safespot,omitempty"`
+ OnLook []behavior.Interaction `yaml:"on_look,omitempty"`
}
def := ro.Local
return inlineObj{
@@ -227,7 +200,7 @@ func (ro RoomObject) MarshalYAML() (interface{}, error) {
InRoomDescription: def.InRoomDescription,
RemovalItem: def.RemovalItem,
Description: def.Description,
- UseInteractions: def.UseInteractions,
+ OnUse: def.OnUse,
Steal: def.Steal,
Gather: def.Gather,
Talk: def.Talk,
diff --git a/internal/world/room_migrate.go b/internal/world/room_migrate.go
index 6d79499..946417f 100644
--- a/internal/world/room_migrate.go
+++ b/internal/world/room_migrate.go
@@ -16,7 +16,22 @@ func (r *Room) RewriteRoomIDs(idMap map[int]int) bool {
}
changed := false
for dir, exit := range r.Exits {
+ var exitChanged bool
if remapInt(idMap, &exit.Room) {
+ exitChanged = true
+ }
+ for i := range exit.OnTraverse {
+ if exit.OnTraverse[i].Action != nil {
+ if remapInt(idMap, &exit.OnTraverse[i].Action.Teleport) {
+ exitChanged = true
+ }
+ if exit.OnTraverse[i].Action.SpawnMob != nil &&
+ remapIntSlice(idMap, exit.OnTraverse[i].Action.SpawnMob.DespawnRooms) {
+ exitChanged = true
+ }
+ }
+ }
+ if exitChanged {
r.Exits[dir] = exit
changed = true
}
diff --git a/internal/world/room_migrate_test.go b/internal/world/room_migrate_test.go
index 5696a36..c54adf2 100644
--- a/internal/world/room_migrate_test.go
+++ b/internal/world/room_migrate_test.go
@@ -1,23 +1,29 @@
package world
-import "testing"
+import (
+ "testing"
+
+ "thehouseoficarus/internal/behavior"
+)
func TestRoomRewriteRoomIDs(t *testing.T) {
r := &Room{
Exits: map[ExitDir]ExitDef{
- North: {Room: 10, SetGlobalFlags: map[string]any{"last": 10}},
+ North: {Room: 10, OnTraverse: []behavior.Interaction{{Action: &behavior.StepAction{
+ NodeAction: behavior.NodeAction{SetGlobalFlags: map[string]any{"last": 10}, Teleport: 110},
+ }}}},
South: {Room: 99},
},
Triggers: []TriggerDef{
{
Room: 30,
- Steps: []TriggerStep{
- {Teleport: 40, SpawnMob: &SpawnMobConfig{DespawnRooms: []int{50, 60}}},
+ Steps: []behavior.StepAction{
+ {NodeAction: behavior.NodeAction{Teleport: 40}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{50, 60}}},
},
},
},
- OnEnter: []EnterStep{
- {Teleport: 70, SpawnMob: &SpawnMobConfig{DespawnRooms: []int{80}}},
+ OnEnter: []behavior.StepAction{
+ {NodeAction: behavior.NodeAction{Teleport: 70}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{80}}},
},
Mobs: []RoomMob{
{ID: "guard", WanderRooms: []int{90, 100}},
@@ -26,7 +32,7 @@ func TestRoomRewriteRoomIDs(t *testing.T) {
idMap := map[int]int{
10: 11, 20: 21, 30: 31, 40: 41, 50: 51, 60: 61,
- 70: 71, 80: 81, 90: 91, 100: 101,
+ 70: 71, 80: 81, 90: 91, 100: 101, 110: 111,
}
if !r.RewriteRoomIDs(idMap) {
t.Fatal("expected changed=true")
@@ -43,9 +49,13 @@ func TestRoomRewriteRoomIDs(t *testing.T) {
// Author set_flags values are NOT structural room references and must not
// be remapped (the old swapid regex wrongly rewrote these).
- if v := r.Exits[North].SetGlobalFlags["last"]; v != 10 {
+ if v := r.Exits[North].OnTraverse[0].Action.SetGlobalFlags["last"]; v != 10 {
t.Errorf("set_global_flags value remapped: got %v, want 10", v)
}
+ // Exit on_traverse teleport IS a structural room reference and must remap.
+ if r.Exits[North].OnTraverse[0].Action.Teleport != 111 {
+ t.Errorf("exit on_traverse teleport = %d, want 111", r.Exits[North].OnTraverse[0].Action.Teleport)
+ }
if r.Triggers[0].Room != 31 {
t.Errorf("trigger room = %d, want 31", r.Triggers[0].Room)
diff --git a/internal/world/trigger.go b/internal/world/trigger.go
index e31a5f9..e7f3b75 100644
--- a/internal/world/trigger.go
+++ b/internal/world/trigger.go
@@ -1,61 +1,20 @@
package world
-import (
- "gopkg.in/yaml.v3"
-)
+import "thehouseoficarus/internal/behavior"
+
+// SpawnMobConfig is an alias for the behavior.SpawnMobConfig so callers that
+// historically referenced world.SpawnMobConfig continue to compile.
+type SpawnMobConfig = behavior.SpawnMobConfig
type TriggerDef struct {
- ID string `yaml:"id"`
- OnPlayerFlag string `yaml:"on_player_flag"`
- OnGlobalFlag string `yaml:"on_global_flag"`
- Value any `yaml:"value"`
- Room int `yaml:"room"`
- Steps []TriggerStep `yaml:"steps"`
+ ID string `yaml:"id"`
+ OnPlayerFlag string `yaml:"on_player_flag"`
+ OnGlobalFlag string `yaml:"on_global_flag"`
+ Value any `yaml:"value"`
+ Room int `yaml:"room"`
+ Steps []behavior.StepAction `yaml:"steps"`
}
func (t *TriggerDef) IsPlayerFlagTrigger() bool { return t.OnPlayerFlag != "" }
-func (t *TriggerDef) IsGlobalFlagTrigger() bool { return t.OnGlobalFlag != "" }
-
-type TriggerStep struct {
- Delay int `yaml:"delay"`
- Message string `yaml:"message"`
- Broadcast string `yaml:"broadcast"`
- BroadcastGlobal string `yaml:"broadcast_global"`
- SetGlobalFlags map[string]any `yaml:"set_global_flags"`
- SetPlayerFlags map[string]any `yaml:"set_player_flags"`
- SpawnMob *SpawnMobConfig `yaml:"spawn_mob"`
- GiveItem string `yaml:"give_item"`
- TakeItem string `yaml:"take_item"`
- Teleport int `yaml:"teleport"`
- Heal int `yaml:"heal"`
- DespawnMob string `yaml:"despawn_mob"`
-}
-
-type SpawnMobConfig struct {
- ID string `yaml:"id"`
- OwnerOnly bool `yaml:"owner_only"`
- DespawnOnLeave bool `yaml:"despawn_on_leave"`
- DespawnRooms []int `yaml:"despawn_rooms"`
- DespawnTicks float64 `yaml:"despawn_ticks"`
-}
-
-func (s *SpawnMobConfig) UnmarshalYAML(value *yaml.Node) error {
- if value.Kind == yaml.ScalarNode {
- var id string
- if err := value.Decode(&id); err != nil {
- return err
- }
- s.ID = id
- return nil
- }
- type raw SpawnMobConfig
- return value.Decode((*raw)(s))
-}
-
-func (t TriggerStep) IsTimed() bool {
- return t.Delay > 0 || len(t.SetGlobalFlags) > 0 || len(t.SetPlayerFlags) > 0 ||
- t.Message != "" || t.Broadcast != "" || t.BroadcastGlobal != "" ||
- t.SpawnMob != nil || t.GiveItem != "" || t.TakeItem != "" ||
- t.Teleport != 0 || t.Heal != 0 || t.DespawnMob != ""
-}
+func (t *TriggerDef) IsGlobalFlagTrigger() bool { return t.OnGlobalFlag != "" } \ No newline at end of file
diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go
index e18ae80..04d77f4 100644
--- a/internal/world/trigger_store.go
+++ b/internal/world/trigger_store.go
@@ -8,6 +8,7 @@ import (
"sync"
"gopkg.in/yaml.v3"
+ "thehouseoficarus/internal/behavior"
)
type TriggerStore struct {
@@ -26,7 +27,7 @@ type PlayerTriggerSeq struct {
PlayerName string
TriggerID string
RoomID int
- Steps []TriggerStep
+ Steps []behavior.StepAction
Wait int
Started bool
FlagValue any
@@ -35,7 +36,7 @@ type PlayerTriggerSeq struct {
type GlobalTriggerSeq struct {
TriggerID string
RoomID int
- Steps []TriggerStep
+ Steps []behavior.StepAction
Wait int
FlagValue any
}
@@ -227,7 +228,7 @@ func (ts *TriggerStore) startPlayerSeqLocked(playerName string, t *TriggerDef, r
PlayerName: playerName,
TriggerID: t.ID,
RoomID: roomID,
- Steps: make([]TriggerStep, len(t.Steps)),
+ Steps: make([]behavior.StepAction, len(t.Steps)),
FlagValue: flagValue,
}
copy(seq.Steps, t.Steps)
@@ -246,7 +247,7 @@ func (ts *TriggerStore) startGlobalSeqLocked(t *TriggerDef, flagValue any) *Glob
seq := &GlobalTriggerSeq{
TriggerID: t.ID,
RoomID: t.Room,
- Steps: make([]TriggerStep, len(t.Steps)),
+ Steps: make([]behavior.StepAction, len(t.Steps)),
FlagValue: flagValue,
}
copy(seq.Steps, t.Steps)