package game import ( "fmt" "sort" "strconv" "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/combat" "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 combat.GetCombat(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.ActionState = nil p.WalkSequence = nil p.ClearMoveState() } func (g *Game) cancelBgAction(p *player.Player) { p.BackgroundAction = nil p.BackgroundActionState = 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[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 *action.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 } }