diff options
Diffstat (limited to 'internal/behavior')
| -rw-r--r-- | internal/behavior/behavior.go | 94 | ||||
| -rw-r--r-- | internal/behavior/desc.go | 27 | ||||
| -rw-r--r-- | internal/behavior/doc.go | 4 | ||||
| -rw-r--r-- | internal/behavior/store.go | 164 | ||||
| -rw-r--r-- | internal/behavior/types.go | 188 |
5 files changed, 477 insertions, 0 deletions
diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go new file mode 100644 index 0000000..57b2a7b --- /dev/null +++ b/internal/behavior/behavior.go @@ -0,0 +1,94 @@ +package behavior + +type GatherConfig struct { + Skill string `yaml:"skill"` + Level int `yaml:"level"` + XP int `yaml:"xp"` + BaseWait float64 `yaml:"base_wait"` + Tools []string `yaml:"tools"` + Bait string `yaml:"bait"` + Success SuccessFormula `yaml:"success"` + GatherMsg string `yaml:"gather_message"` + DepletedMessage string `yaml:"depleted_message"` + ExhaustedMessage string `yaml:"exhausted_message"` + FailMsg string `yaml:"fail_message"` + Drops []DropEntry `yaml:"drops"` + RespawnTimer float64 `yaml:"respawn_timer"` + RespawnBroadcast string `yaml:"respawn_broadcast"` + DepleteTimer float64 `yaml:"deplete_timer"` + NestChance int `yaml:"nest_chance"` +} + +type SuccessFormula struct { + Base float64 `yaml:"base"` + PerLevel float64 `yaml:"per_level"` + Cap float64 `yaml:"cap"` +} + +type TalkConfig struct { + Nodes map[string]TalkNode `yaml:"nodes"` +} + +type TalkNode struct { + Message string `yaml:"message"` + Options []TalkOption `yaml:"options"` + Action *NodeAction `yaml:"action"` +} + +type TalkOption struct { + Text string `yaml:"text"` + Goto string `yaml:"goto"` + End bool `yaml:"end"` + Condition *Condition `yaml:"condition"` +} + +type NodeAction struct { + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` + GiveItem string `yaml:"give_item"` + TakeItem string `yaml:"take_item"` + Teleport int `yaml:"teleport"` + Heal int `yaml:"heal"` + Cost int `yaml:"cost"` + Shop *ShopConfig `yaml:"shop"` + AssignTask bool `yaml:"assign_task"` + SkipTask bool `yaml:"skip_task"` + ExtendTask bool `yaml:"extend_task"` + ReputationCost int `yaml:"reputation_cost"` + Sawmill bool `yaml:"sawmill"` + ApsNode bool `yaml:"aps_node"` +} + +type ShopConfig struct { + Message string `yaml:"message"` + Items []ShopItem `yaml:"items"` +} + +type ShopItem struct { + ItemID string `yaml:"item_id"` + BuyPrice int `yaml:"buy_price"` + SellPrice int `yaml:"sell_price"` +} + +type Condition struct { + Flag string `yaml:"flag"` + Value any `yaml:"value"` + Not bool `yaml:"not"` + PlayerFlag string `yaml:"player_flag"` + HasItem string `yaml:"has_item"` + MinCredits int `yaml:"min_credits"` + AllOf []Condition `yaml:"all_of"` + AnyOf []Condition `yaml:"any_of"` +} + +type UseConfig struct { + Message string `yaml:"message"` + Wait float64 `yaml:"wait"` + Consume map[string]int `yaml:"consume"` + Reward DropEntry `yaml:"reward"` + FailMsg string `yaml:"fail_message"` + Success *SuccessFormula `yaml:"success"` + Skill string `yaml:"skill"` + Level int `yaml:"level"` + XP int `yaml:"xp"` +} diff --git a/internal/behavior/desc.go b/internal/behavior/desc.go new file mode 100644 index 0000000..ca4bf69 --- /dev/null +++ b/internal/behavior/desc.go @@ -0,0 +1,27 @@ +package behavior + +import "gopkg.in/yaml.v3" + +type DescVariant struct { + Text string `yaml:"text"` + Condition *Condition `yaml:"condition,omitempty"` +} + +type DescList []DescVariant + +func (dl *DescList) UnmarshalYAML(value *yaml.Node) error { + if value.Kind == yaml.ScalarNode { + var s string + if err := value.Decode(&s); err != nil { + return err + } + *dl = DescList{{Text: s}} + return nil + } + var entries []DescVariant + if err := value.Decode(&entries); err != nil { + return err + } + *dl = entries + return nil +} diff --git a/internal/behavior/doc.go b/internal/behavior/doc.go new file mode 100644 index 0000000..a1ea45f --- /dev/null +++ b/internal/behavior/doc.go @@ -0,0 +1,4 @@ +// Package behavior defines YAML-driven interactive behaviors for objects and +// mobs: gathering, talking, using, and the shared condition system. These are +// data structures — the runtime action lifecycle lives in the game package. +package behavior diff --git a/internal/behavior/store.go b/internal/behavior/store.go new file mode 100644 index 0000000..7695b8d --- /dev/null +++ b/internal/behavior/store.go @@ -0,0 +1,164 @@ +package behavior + +import ( + "fmt" + "math/rand" + "os" + "path/filepath" + "strings" + "sync" + + "gopkg.in/yaml.v3" +) + +func BuildPathIndex(dir string) map[string]string { + index := make(map[string]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + index[id] = path + return nil + }) + return index +} + +func BuildRoomIndex(dir string) map[int]string { + index := make(map[int]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + var n int + if _, scanErr := fmt.Sscanf(id, "%d", &n); scanErr == nil { + index[n] = path + } + return nil + }) + return index +} + +func WalkYAMLDir(dir string, fn func(path, id string, data []byte) error) error { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + data, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + return fn(path, id, data) + }) +} + +type Duplicate struct { + ID string + Paths []string +} + +func CheckDuplicateIDs(dir string) []Duplicate { + seen := make(map[string][]string) + filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { + return nil + } + id := strings.TrimSuffix(d.Name(), ".yaml") + seen[id] = append(seen[id], path) + return nil + }) + var dups []Duplicate + for id, paths := range seen { + if len(paths) > 1 { + dups = append(dups, Duplicate{ID: id, Paths: paths}) + } + } + return dups +} + +func SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64 { + chance := cfg.Base + float64(level-requiredLevel)*cfg.PerLevel + if chance > cfg.Cap { + chance = cfg.Cap + } + if chance < 0 { + chance = 0 + } + return chance +} + +var ( + dropIndex map[string]string + dropIndexMu sync.Mutex +) + +func loadDropIndex(dataDir string) map[string]string { + dropIndexMu.Lock() + defer dropIndexMu.Unlock() + if dropIndex == nil { + dropIndex = BuildPathIndex(filepath.Join(dataDir, "drops")) + } + return dropIndex +} + +func LoadDropTable(dataDir, id string) (*DropTableDef, error) { + index := loadDropIndex(dataDir) + path, ok := index[id] + if !ok { + path = filepath.Join(dataDir, "drops", id+".yaml") + } + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read drop table %s: %w", id, err) + } + var dt DropTableDef + if err := yaml.Unmarshal(data, &dt); err != nil { + return nil, fmt.Errorf("parse drop table %s: %w", id, err) + } + return &dt, nil +} + +func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry { + if len(drops) == 0 { + return nil + } + total := 0 + for _, d := range drops { + total += d.Weight + } + if total <= 0 { + return nil + } + roll := rand.Intn(total) + cumulative := 0 + for i := range drops { + cumulative += drops[i].Weight + if roll < cumulative { + if drops[i].Table != "" { + sub, err := LoadDropTable(dataDir, drops[i].Table) + if err == nil { + if resolved := ResolveDrop(dataDir, sub.Drops); resolved != nil { + qty := drops[i].Quantity + if qty <= 0 { + qty = resolved.Quantity + } + if qty <= 0 { + qty = 1 + } + return &DropEntry{ + ItemID: resolved.ItemID, + Quantity: qty, + Depletes: drops[i].Depletes, + Message: drops[i].Message, + } + } + } + return nil + } + return &drops[i] + } + } + return &drops[0] +} diff --git a/internal/behavior/types.go b/internal/behavior/types.go new file mode 100644 index 0000000..3c6e290 --- /dev/null +++ b/internal/behavior/types.go @@ -0,0 +1,188 @@ +package behavior + +import "strings" + +func WordPrefixMatch(input, name string) bool { + inputWords := strings.Fields(strings.ToLower(input)) + if len(inputWords) == 0 { + return false + } + nameWords := strings.Fields(strings.ToLower(name)) + for _, iw := range inputWords { + found := false + for _, nw := range nameWords { + if strings.HasPrefix(nw, iw) || strings.HasPrefix(iw, nw) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +type ActionType string + +const ( + TypeGather ActionType = "gather" + TypeSteal ActionType = "steal" + TypeTalk ActionType = "talk" + TypeUse ActionType = "use" + TypeBurn ActionType = "burn" + TypeStoke ActionType = "stoke" + TypeSearch ActionType = "search" + TypeIdentify ActionType = "identify" + TypePlant ActionType = "plant" + TypeHarvest ActionType = "harvest" + TypeRake ActionType = "rake" + TypeWater ActionType = "water" + TypeCure ActionType = "cure" + TypeObstacle ActionType = "obstacle" + TypeFletch ActionType = "fletch" + TypeClean ActionType = "clean" + TypeCook ActionType = "cook" + TypeSmelt ActionType = "smelt" + TypeSmith ActionType = "smith" + TypeCraft ActionType = "craft" + TypeCombine ActionType = "combine" + TypeMix ActionType = "mix" + TypeConstruct ActionType = "construct" +) + +type Action struct { + Type ActionType + TargetID string + TargetName string + WaitLeft int + Data any +} + +func (a *Action) Advance() bool { + if a.WaitLeft > 0 { + a.WaitLeft-- + return false + } + return true +} + +type GatherData struct { + ObjDefID string + InstanceKey string + InstanceIdx int + EffectiveWait float64 + DepleteTimer bool + Step int + Verb string + ToolName string +} + +type ProductionData struct { + ItemID string + Phase int + Wait float64 + StartMsg string + EndMsg string + Remaining int + NextStepIndex int + StepsAccumulatedTicks float64 +} + +type StealData struct { + TargetType string + StealTable string + StealLevel int + StealXP int + StealSpeed float64 + TargetName string + MobInstanceID string + ObjDefID string + GuardMob string + GuardWatching bool +} + +type TalkData struct { + Node string + Cfg *TalkConfig + EstateDirectory bool + StealGuard bool +} + +type UseData struct { + Cfg *UseConfig + Step int +} + +type BurnData struct { + ItemID string + ToolSpeed float64 + Level int + XP int + BurnTicks float64 + Phase int +} + +type IdentifyData struct { + JunkID string + XPPer int +} + +type ObstacleData struct { + CourseID string + Phase int + ObstacleIndex int + TotalObstacles int + NextRoom int + StartRoom int + ObstacleXP int + CompletionXP int + FailChance float64 + FailDamageMin int + FailDamageMax int + Messages []any + TicksPerPhase float64 + RequiredLevel int +} + +type FarmData struct { + Prefix string + SeedID string + XP float64 + Product string + MinYield float64 + MaxYield float64 +} + +type SearchData struct { + ItemID string + SlotIdx int + Started bool +} + +type FletchData struct { + ItemID string + Phase int + Wait float64 + Remaining int +} + +type CleanData struct { + Phase int + Filter string +} + +type DropEntry struct { + ItemID string `yaml:"item_id"` + Table string `yaml:"table"` + Weight int `yaml:"weight"` + Quantity int `yaml:"quantity"` + Depletes bool `yaml:"depletes"` + Message string `yaml:"message"` + Level int `yaml:"level"` + XP int `yaml:"xp"` +} + +type DropTableDef struct { + Drops []DropEntry `yaml:"drops"` +} |
