diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/grid_test.go | 4 | ||||
| -rw-r--r-- | internal/world/insert_remove.go | 3 | ||||
| -rw-r--r-- | internal/world/mob.go | 24 | ||||
| -rw-r--r-- | internal/world/room.go | 71 | ||||
| -rw-r--r-- | internal/world/room_migrate.go | 15 | ||||
| -rw-r--r-- | internal/world/room_migrate_test.go | 26 | ||||
| -rw-r--r-- | internal/world/trigger.go | 65 | ||||
| -rw-r--r-- | internal/world/trigger_store.go | 9 |
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) |
