aboutsummaryrefslogtreecommitdiff
path: root/internal/game/act.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 15:40:48 -0400
committerhistoria <[not public]>2026-06-25 15:40:48 -0400
commitabd612c15799f604e671e83dc7c410ed2b44185f (patch)
tree0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/act.go
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/game/act.go')
-rw-r--r--internal/game/act.go376
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
+ }
+}