aboutsummaryrefslogtreecommitdiff
path: root/internal/behavior
diff options
context:
space:
mode:
Diffstat (limited to 'internal/behavior')
-rw-r--r--internal/behavior/behavior.go94
-rw-r--r--internal/behavior/desc.go27
-rw-r--r--internal/behavior/doc.go4
-rw-r--r--internal/behavior/store.go164
-rw-r--r--internal/behavior/types.go188
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"`
+}