diff options
| author | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
| commit | abd612c15799f604e671e83dc7c410ed2b44185f (patch) | |
| tree | 0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/act.go | |
| parent | 2725e2927a1595c7b100d942d1f14146252adeb7 (diff) | |
| download | thehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz | |
slop refactor
Diffstat (limited to 'internal/game/act.go')
| -rw-r--r-- | internal/game/act.go | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/internal/game/act.go b/internal/game/act.go new file mode 100644 index 0000000..8920d0c --- /dev/null +++ b/internal/game/act.go @@ -0,0 +1,376 @@ +package game + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +var verbAliases = map[string]string{ + "mine": "gather", + "chop": "gather", + "fish": "gather", + "cut": "gather", + "shear": "gather", + "talk": "talk", + "speak": "talk", + "ask": "talk", + "steal": "steal", + "thieve": "steal", + "hide": "safespot", +} + +var verbSkill = map[string]string{ + "mine": "mining", + "chop": "woodcutting", + "cut": "woodcutting", + "fish": "fishing", + "shear": "crafting", + "steal": "thieving", + "thieve": "thieving", +} + +func normalizeVerb(v string) string { + if a, ok := verbAliases[v]; ok { + return a + } + return v +} + +func (g *Game) startAction(sess *net.Session, verb, target string) { + p := sess.Player + + verb = normalizeVerb(verb) + + if g.Combat.Get(p.Name) != nil { + sess.WriteLine("You can't do that during combat!") + return + } + + if p.Action != nil { + g.cancelAction(p) + } + g.cancelBgAction(p) + + lower := strings.ToLower(target) + instanceIdx := -1 + if dotPos := strings.Index(lower, "."); dotPos > 0 { + if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 { + instanceIdx = n - 1 + lower = lower[dotPos+1:] + } + } + + instances := g.World.FindObjInstances(p.RoomID, lower) + sort.Slice(instances, func(i, j int) bool { + return instances[i].Index < instances[j].Index + }) + + var obj *object.ObjectDef + var mob *world.MobInstance + var chosen *world.ObjState + + if len(instances) > 0 { + uniqueDefs := make(map[string]bool) + for _, ist := range instances { + uniqueDefs[ist.DefID] = true + } + if len(uniqueDefs) > 1 { + sess.WriteLine("That's ambiguous, which one?") + return + } + + if instanceIdx >= 0 && instanceIdx < len(instances) { + chosen = &instances[instanceIdx] + } else { + for i := range instances { + if !instances[i].Depleted { + chosen = &instances[i] + break + } + } + if chosen == nil { + chosen = &instances[0] + } + } + if chosen == nil { + sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb)) + return + } + var err error + obj, err = g.ObjectStore.Load(chosen.DefID) + if err != nil || !obj.IsInteractable() { + sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name)) + return + } + } else { + mobs := g.MobStore.MobsInRoom(p.RoomID) + var candidates []*world.MobInstance + for _, m := range mobs { + if !m.IsTalkable() { + continue + } + if q := m.MatchQuality(lower); q >= world.MatchPrefix { + candidates = append(candidates, m) + } + } + if len(candidates) == 0 { + sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb)) + return + } + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].InstanceID < candidates[j].InstanceID + }) + if instanceIdx >= 0 && instanceIdx < len(candidates) { + mob = candidates[instanceIdx] + } else if len(candidates) == 1 { + mob = candidates[0] + } else { + sess.WriteLine("Which one?") + return + } + } + + if obj != nil && obj.ID == "estate_directory" { + g.startEstateDirectory(sess, verb) + return + } + + bhType := "" + if obj != nil { + bhType = obj.BehaviorType() + } else if mob != nil { + if mob.IsTalkable() { + bhType = "talk" + } + } + + if bhType != verb { + if obj != nil { + sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name)) + } else { + sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) + } + return + } + + switch bhType { + case "gather": + if obj == nil || chosen == nil { + sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) + return + } + g.startGather(sess, p, obj, chosen) + case "talk": + if mob != nil { + g.startMobTalk(sess, p, mob) + } else { + g.startTalk(sess, p, obj) + } + case "use": + g.startUse(sess, p, obj) + default: + sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) + } +} + +func (g *Game) cancelAction(p *player.Player) { + p.Action = nil + p.WalkSequence = nil + p.ClearMoveState() +} + +func (g *Game) cancelBgAction(p *player.Player) { + p.BackgroundAction = nil +} + +func (g *Game) AdvanceActions() { + if g.Hub == nil { + return + } + for _, sess := range g.Hub.AllSessions() { + p := sess.Player + if p == nil || p.Action == nil { + continue + } + if !p.Action.Advance() { + continue + } + switch p.Action.Type { + case "gather": + g.advanceGather(sess, p) + case "use": + g.advanceUse(sess, p) + case "burn": + g.advanceBurn(sess, p) + case "stoke": + g.advanceStoke(sess, p) + case "search": + g.advanceSearch(sess, p) + case "identify": + g.advanceIdentify(sess, p) + case "steal": + g.advanceSteal(sess, p) + case "plant": + g.advancePlant(sess, p) + case "harvest": + g.advanceHarvest(sess, p) + case "rake": + g.advanceRake(sess, p) + case "water": + g.advanceWater(sess, p) + case "cure": + g.advanceCure(sess, p) + case "obstacle": + g.advanceObstacle(sess, p) + default: + if productionActionTypes[string(p.Action.Type)] { + g.advanceProduction(sess, p) + } + } + if p.Action == nil { + g.writePrompt(sess) + } + } + + for _, sess := range g.Hub.AllSessions() { + p := sess.Player + if p == nil || p.BackgroundAction == nil { + continue + } + if !p.BackgroundAction.Advance() { + continue + } + switch p.BackgroundAction.Type { + case "fletch": + g.advanceFletch(sess, p) + case "clean": + g.advanceClean(sess, p) + } + if p.BackgroundAction == nil { + g.writePrompt(sess) + } + } +} + +func (g *Game) checkCondition(sess *net.Session, c *behavior.Condition) bool { + p := sess.Player + + if c.AllOf != nil { + for _, sub := range c.AllOf { + if !g.checkCondition(sess, &sub) { + return false + } + } + return true + } + if c.AnyOf != nil { + for _, sub := range c.AnyOf { + if g.checkCondition(sess, &sub) { + return true + } + } + return false + } + + if c.PlayerFlag != "" { + var val any + present := false + if p != nil && p.Flags != nil { + val, present = p.Flags[c.PlayerFlag] + } + return flagMatches(present, val, c.Value, c.Not) + } + + if c.Flag != "" { + val, present := g.Flags.Get(c.Flag) + return flagMatches(present, val, c.Value, c.Not) + } + if c.HasItem != "" { + has := p != nil && p.HasItem(c.HasItem) + if c.Not { + return !has + } + return has + } + if c.MinCredits > 0 { + has := p != nil && p.Credits >= c.MinCredits + if c.Not { + return !has + } + return has + } + return true +} + +// flagMatches evaluates a flag/player_flag condition. +// +// If the condition specifies no `value`, the flag matches when it is present +// and truthy (so `{player_flag: x}` means "x is set" and +// `{player_flag: x, not: true}` means "x is not set"). If a `value` is given, +// the flag must be present and equal to it. `not` inverts the result. Numeric +// values are compared by coercing both sides through a common numeric type so +// that an int flag set in code matches an int64/float64 decoded from YAML. +func flagMatches(present bool, val, want any, not bool) bool { + var match bool + if want == nil { + match = present && isTruthy(val) + } else { + match = present && valuesEqual(val, want) + } + if not { + return !match + } + return match +} + +// valuesEqual compares two any-typed values, normalizing numeric types (int, +// int64, float64) so that e.g. int(3) == int64(3) == float64(3). Non-numeric +// types fall back to ==. +func valuesEqual(a, b any) bool { + ai, aok := numericValue(a) + bi, bok := numericValue(b) + if aok && bok { + return ai == bi + } + return a == b +} + +// numericValue returns the value as a float64 if it is a numeric type. +func numericValue(v any) (float64, bool) { + switch x := v.(type) { + case int: + return float64(x), true + case int64: + return float64(x), true + case float64: + return x, true + } + return 0, false +} + +func isTruthy(v any) bool { + switch x := v.(type) { + case nil: + return false + case bool: + return x + case int: + return x != 0 + case int64: + return x != 0 + case float64: + return x != 0 + case string: + return x != "" + default: + return true + } +} |
