diff options
| author | historia <[not public]> | 2026-06-17 17:34:40 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-17 17:34:40 -0400 |
| commit | eef5ddaa94f8cff6010d092c30fed53d2be01524 (patch) | |
| tree | 3026ebb5fe34fd778c5f8f6b6d23aec337a99b99 /internal/game | |
| parent | 04998e3e4eff075eee6d00850c752c3c73ab66b5 (diff) | |
| download | thehouseoficarus-eef5ddaa94f8cff6010d092c30fed53d2be01524.tar.gz | |
feat: began implementing smithing, ground item display fixes.
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/action.go | 2 | ||||
| -rw-r--r-- | internal/game/action_smelt.go | 153 | ||||
| -rw-r--r-- | internal/game/action_state.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 125 | ||||
| -rw-r--r-- | internal/game/cmd_smelt.go | 230 | ||||
| -rw-r--r-- | internal/game/cmd_use.go | 33 | ||||
| -rw-r--r-- | internal/game/cmd_wear.go | 9 | ||||
| -rw-r--r-- | internal/game/game.go | 30 |
8 files changed, 538 insertions, 47 deletions
diff --git a/internal/game/action.go b/internal/game/action.go index c08295c..608528d 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -227,6 +227,8 @@ func (g *Game) AdvanceActions() { g.advanceSearch(sess, p) case "cook": g.advanceCook(sess, p) + case "smelt": + g.advanceSmelt(sess, p) } if p.Action == nil { g.writePrompt(sess) diff --git a/internal/game/action_smelt.go b/internal/game/action_smelt.go new file mode 100644 index 0000000..75fb8a0 --- /dev/null +++ b/internal/game/action_smelt.go @@ -0,0 +1,153 @@ +package game + +import ( + "fmt" + "math/rand" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) startSmelt(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) { + g.CancelAction(p) + + if recipe.Skill != "" { + skillLevel := p.Level(player.SkillName(recipe.Skill)) + if skillLevel < recipe.Level { + sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", recipe.Level, recipe.Skill)) + return + } + } + + if !recipe.HasAllItemsQty(p.CountItem) { + sess.WriteLine("You don't have the required ores.") + return + } + + if p.FirstFreeSlot() == -1 { + sess.WriteLine("Your inventory is too full!") + return + } + + wait := recipe.Wait + if wait <= 0 { + wait = 4 + } + + outputName := recipe.Output + if def, err := g.ItemStore.Load(recipe.Output); err == nil { + outputName = def.Name + } + + p.Action = &action.Action{ + Type: "smelt", + TargetID: recipe.ID, + TargetName: recipe.DisplayName(), + Data: map[string]any{ + "recipe_id": recipe.ID, + "station_name": stationName, + "phase": 0, + "wait": wait, + }, + WaitLeft: engine.ToTicks(1), + } + + p.ActionState = &ActionState{Type: ActionSmelting, TargetName: outputName} +} + +func (g *Game) advanceSmelt(sess *net.Session, p *player.Player) bool { + recipeID := p.Action.Data["recipe_id"].(string) + phase := p.Action.Data["phase"].(int) + wait := p.Action.Data["wait"].(float64) + + all, err := g.RecipeStore.LoadAll() + if err != nil { + g.CancelAction(p) + return false + } + var recipe *action.RecipeDef + for _, r := range all { + if r.ID == recipeID { + recipe = &r + break + } + } + if recipe == nil { + g.CancelAction(p) + return false + } + + if phase == 0 { + firstItem := recipe.FirstItemName(func(id string) (string, bool) { + def, err := g.ItemStore.Load(id) + if err != nil { + return id, false + } + return def.Name, true + }) + sess.WriteLine(fmt.Sprintf("\nYou begin to process %s in the furnace.", firstItem)) + p.Action.Data["phase"] = 1 + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + skillLevel := p.Level(player.SkillName(recipe.Skill)) + chance := 1.0 + if recipe.Success != nil { + chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level) + } + + if rand.Float64() < chance { + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + sess.WriteLine("Your inventory is too full!") + g.CancelAction(p) + return false + } + + recipe.ConsumeAll(p.HasItem, p.RemoveItem) + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Output, Quantity: 1}) + + if recipe.XP > 0 { + if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill))) + } + } + g.AccountStore.SaveCharacter(p) + + msg := recipe.Message + if msg == "" { + outputName := recipe.Output + if def, loadErr := g.ItemStore.Load(recipe.Output); loadErr == nil { + outputName = def.Name + } + msg = fmt.Sprintf("You remove a white hot %s!", outputName) + } + if p.OptionBool("xp_drops") && recipe.XP > 0 { + abbr := player.SkillAbbr[player.SkillName(recipe.Skill)] + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) + } + sess.WriteLine(msg) + } else { + recipe.ConsumeAll(p.HasItem, p.RemoveItem) + g.AccountStore.SaveCharacter(p) + + msg := recipe.FailMessage + if msg == "" { + msg = "You fail to smelt a usable bar." + } + sess.WriteLine(g.colorize(sess, "damage", msg)) + } + + if recipe.HasAllItemsQty(p.CountItem) && p.FirstFreeSlot() >= 0 { + p.Action.Data["phase"] = 0 + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + sess.WriteLine("\nYou've processed all the ore in your inventory.") + g.CancelAction(p) + return false +} diff --git a/internal/game/action_state.go b/internal/game/action_state.go index b28e170..6ce3734 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -20,6 +20,7 @@ const ( ActionResting ActionType = "resting" ActionWalking ActionType = "walking" ActionCooking ActionType = "cooking" + ActionSmelting ActionType = "smelting" ActionEating ActionType = "eating" ) @@ -68,6 +69,8 @@ func (a *ActionState) Description() string { return "walking somewhere with a purpose!" case ActionCooking: return "cooking some " + a.TargetName + case ActionSmelting: + return "smelting some " + a.TargetName case ActionEating: return "eating " + a.TargetName } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 8cadfd3..77c8776 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -214,27 +214,24 @@ func (g *Game) doLook(sess *net.Session) { ground := g.World.GroundItemsDetailed(p.RoomID) if len(ground) > 0 { - sort.Slice(ground, func(i, j int) bool { - ni := ground[i].ItemID - if def, err := g.ItemStore.Load(ground[i].ItemID); err == nil { - ni = def.Name - } - nj := ground[j].ItemID - if def, err := g.ItemStore.Load(ground[j].ItemID); err == nil { - nj = def.Name - } - return strings.ToLower(ni) < strings.ToLower(nj) - }) + showDespawn := p.OptionBool("despawn") + showReserve := p.OptionBool("reserve") - sess.WriteLine("") - sess.WriteLine("On the ground:") + type displayLine struct { + name string + quantity int + colorName string + annotation string + } - type groundLine struct { - prefix string - reserved string + type groupKey struct { + itemID string + despawnTimer int } - var lines []groundLine - maxPrefix := 0 + + var lines []displayLine + groups := make(map[groupKey]*displayLine) + var groupOrder []groupKey for _, info := range ground { def, err := g.ItemStore.Load(info.ItemID) @@ -243,37 +240,89 @@ func (g *Game) doLook(sess *net.Session) { name = def.Name } coloredName := g.itemColorize(sess, def, name) - prefix := "" - if info.Quantity > 1 { - prefix = fmt.Sprintf(" %d x %s", info.Quantity, coloredName) - } else { - prefix = fmt.Sprintf(" %s", coloredName) - } - if visibleLen(prefix) > maxPrefix { - maxPrefix = visibleLen(prefix) - } - var parts []string + if info.ReservedFor != "" { - if p.OptionBool("reserve") { + var parts []string + if showReserve { parts = append(parts, fmt.Sprintf("reserved for %s %dt", info.ReservedFor, info.ReserveTimer)) } else { parts = append(parts, "reserved") } + if showDespawn && !info.IsSpawn { + parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) + } + annotation := "" + if len(parts) > 0 { + annotation = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) + } + lines = append(lines, displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + }) + continue + } + + timerKey := -1 + if showDespawn && !info.IsSpawn { + timerKey = info.DespawnTimer + } + key := groupKey{itemID: info.ItemID, despawnTimer: timerKey} + + if existing, ok := groups[key]; ok { + existing.quantity += info.Quantity + } else { + annotation := "" + if showDespawn && !info.IsSpawn { + annotation = fmt.Sprintf(" (despawns %dt)", info.DespawnTimer) + } + dl := &displayLine{ + name: name, + quantity: info.Quantity, + colorName: coloredName, + annotation: annotation, + } + groups[key] = dl + groupOrder = append(groupOrder, key) } - if p.OptionBool("despawn") && !info.IsSpawn { - parts = append(parts, fmt.Sprintf("despawns %dt", info.DespawnTimer)) + } + + for _, key := range groupOrder { + lines = append(lines, *groups[key]) + } + + sort.Slice(lines, func(i, j int) bool { + return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name) + }) + + sess.WriteLine("") + sess.WriteLine("On the ground:") + + type fmtLine struct { + prefix string + annotation string + } + var fmtLines []fmtLine + maxPrefix := 0 + + for _, dl := range lines { + prefix := "" + if dl.quantity > 1 { + prefix = fmt.Sprintf(" %d x %s", dl.quantity, dl.colorName) + } else { + prefix = fmt.Sprintf(" %s", dl.colorName) } - reserved := "" - if len(parts) > 0 { - reserved = fmt.Sprintf(" (%s)", strings.Join(parts, ", ")) + if visibleLen(prefix) > maxPrefix { + maxPrefix = visibleLen(prefix) } - lines = append(lines, groundLine{prefix, reserved}) + fmtLines = append(fmtLines, fmtLine{prefix, dl.annotation}) } - for _, l := range lines { - if l.reserved != "" { + for _, l := range fmtLines { + if l.annotation != "" { pad := maxPrefix + 1 + (len(l.prefix) - visibleLen(l.prefix)) - sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.reserved)) + sess.WriteLine(fmt.Sprintf("%-*s%s", pad, l.prefix, l.annotation)) } else { sess.WriteLine(l.prefix) } diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go new file mode 100644 index 0000000..a308352 --- /dev/null +++ b/internal/game/cmd_smelt.go @@ -0,0 +1,230 @@ +package game + +import ( + "fmt" + "strconv" + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +type smeltableEntry struct { + ItemName string + Recipe action.RecipeDef +} + +func (g *Game) doSmelt(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + g.CancelAction(p) + + stationName := g.findFurnace(p.RoomID) + if stationName == "" { + sess.WriteLine("You need a furnace to smelt.") + return + } + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + sess.WriteLine("Error loading recipes.") + return + } + + if input == "" { + g.showSmeltMenu(sess, p, allRecipes, stationName) + return + } + + matches := g.findInventoryMatches(input, p) + if len(matches) == 0 { + sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input)) + return + } + + unique := uniqueItemNames(matches) + if len(unique) > 1 { + sess.WriteLine("Which one?") + for _, name := range unique { + sess.WriteLine(fmt.Sprintf(" - %s", name)) + } + return + } + + itemID := matches[0].ID + + var recipes []action.RecipeDef + for _, r := range allRecipes { + if r.Type != "smelting" { + continue + } + if !smeltStationOK(stationName, r.Station) { + continue + } + if !r.MatchesEntry(itemID) { + continue + } + if !r.HasAllItemsQty(p.CountItem) { + continue + } + skillLevel := p.Level(player.SkillName(r.Skill)) + if skillLevel < r.Level { + continue + } + recipes = append(recipes, r) + } + + if len(recipes) == 0 { + sess.WriteLine("You can't smelt that here.") + return + } + + if len(recipes) == 1 { + g.startSmelt(sess, p, &recipes[0], stationName) + return + } + + sess.PendingRecipeItem = itemID + sess.PendingCookMenu = smeltRecipesToMenuData(recipes) + sess.State = net.StateSmeltRecipe + sess.WriteLine("\nWhat would you like to smelt?") + for i, r := range recipes { + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r))) + } + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1)) +} + +func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) { + seen := make(map[string]bool) + var smeltables []smeltableEntry + + for _, r := range allRecipes { + if r.Type != "smelting" { + continue + } + if !smeltStationOK(stationName, r.Station) { + continue + } + if seen[r.ID] { + continue + } + if !r.HasAllItemsQty(p.CountItem) { + continue + } + skillLevel := p.Level(player.SkillName(r.Skill)) + if skillLevel < r.Level { + continue + } + seen[r.ID] = true + smeltables = append(smeltables, smeltableEntry{g.recipeName(sess, &r), r}) + } + + if len(smeltables) == 0 { + sess.WriteLine("You don't have anything you can smelt.") + return + } + + if len(smeltables) == 1 { + g.startSmelt(sess, p, &smeltables[0].Recipe, stationName) + return + } + + sess.PendingRecipeItem = "" + sess.State = net.StateSmeltRecipe + sess.WriteLine("\nWhat would you like to smelt?") + for i, s := range smeltables { + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, s.ItemName)) + } + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(smeltables)+1)) + + sess.PendingCookMenu = makeSmeltMenuData(smeltables) +} + +func (g *Game) handleSmeltRecipe(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + input = strings.TrimSpace(input) + if input == "" { + return + } + + choice, err := strconv.Atoi(input) + if err != nil { + sess.State = net.StateGame + sess.PendingRecipeItem = "" + sess.PendingCookMenu = nil + sess.WriteLine("You decide not to smelt anything.") + g.reprompt(sess) + return + } + + if len(sess.PendingCookMenu) > 0 { + menuData := sess.PendingCookMenu + sess.PendingCookMenu = nil + if choice <= 0 || choice > len(menuData)+1 { + sess.WriteLine("Invalid choice.") + return + } + sess.State = net.StateGame + if choice == len(menuData)+1 { + sess.WriteLine("You decide not to smelt anything.") + g.reprompt(sess) + return + } + entry := menuData[choice-1] + + all, loadErr := g.RecipeStore.LoadAll() + if loadErr != nil { + g.reprompt(sess) + return + } + for _, r := range all { + if r.ID == entry["recipe_id"] { + stationName := g.findFurnace(p.RoomID) + g.startSmelt(sess, p, &r, stationName) + return + } + } + g.reprompt(sess) + return + } + + sess.State = net.StateGame + g.reprompt(sess) +} + +func smeltStationOK(stationName string, recipeStations []string) bool { + for _, rs := range recipeStations { + if rs == "furnace" && stationName == "furnace" { + return true + } + } + return false +} + +func (g *Game) findFurnace(roomID int) string { + for _, obj := range g.World.AllObjInstances(roomID) { + if obj.Depleted { + continue + } + if obj.DefID == "furnace" { + return "furnace" + } + } + return "" +} + +func makeSmeltMenuData(smeltables []smeltableEntry) []map[string]string { + out := make([]map[string]string, len(smeltables)) + for i, s := range smeltables { + out[i] = map[string]string{"recipe_id": s.Recipe.ID} + } + return out +} + +func smeltRecipesToMenuData(recipes []action.RecipeDef) []map[string]string { + out := make([]map[string]string, len(recipes)) + for i, r := range recipes { + out[i] = map[string]string{"recipe_id": r.ID} + } + return out +} diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index 56aa5a1..247df72 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -74,6 +74,39 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, stationIDs := []string{def.ID} recipes, err := g.RecipeStore.FindByStation(itemAID, stationIDs, "") if err == nil && len(recipes) > 0 { + if recipes[0].Type == "smelting" { + filtered := recipes[:0:0] + for _, r := range recipes { + if r.Type != "smelting" { + continue + } + if !r.HasAllItemsQty(p.CountItem) { + continue + } + skillLevel := p.Level(player.SkillName(r.Skill)) + if skillLevel < r.Level { + continue + } + filtered = append(filtered, r) + } + if len(filtered) == 0 { + sess.WriteLine("You can't smelt that here.") + return + } + if len(filtered) == 1 { + g.startSmelt(sess, p, &filtered[0], def.Name) + return + } + sess.PendingRecipeItem = itemAID + sess.PendingCookMenu = smeltRecipesToMenuData(filtered) + sess.State = net.StateSmeltRecipe + sess.WriteLine("\nWhat would you like to smelt?") + for i, r := range filtered { + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r))) + } + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(filtered)+1)) + return + } if len(recipes) == 1 { g.startCook(sess, p, &recipes[0], def.Name) return diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go index cbc2ff3..8910e75 100644 --- a/internal/game/cmd_wear.go +++ b/internal/game/cmd_wear.go @@ -31,6 +31,15 @@ func (g *Game) doWear(sess *net.Session, input string) { return } + unique := uniqueItemNames(matches) + if len(unique) > 1 { + sess.WriteLine("Equip what?") + for _, name := range unique { + sess.WriteLine(fmt.Sprintf(" - %s", name)) + } + return + } + match := matches[0] slot := p.InvSlot(match.Slot) if slot == nil { diff --git a/internal/game/game.go b/internal/game/game.go index 5af35be..49d34b6 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -124,6 +124,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) { g.handleDropAllConfirm(sess, input) case net.StateCookRecipe: g.handleCookRecipe(sess, input) + case net.StateSmeltRecipe: + g.handleSmeltRecipe(sess, input) case net.StateColorChoice: g.handleColorChoice(sess, input) } @@ -132,18 +134,18 @@ func (g *Game) HandleSession(sess *net.Session, input string) { func classifyCommand(cmd string) CommandClass { switch cmd { case "say", "score", "sc", "inventory", "i", "inv", - "equipment", "eq", "look", "l", "exits", "help", + "look", "l", "exits", "help", "map", "option", "options", "alias", "unalias", "description", "desc", "queued", "color", "colors", "colortable", "prompt", "style": return ClassInstant - case "wear", "wield", "remove", "unwear", "unwield": + case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield": return ClassFree case "get", "take", "grab", "pick", "drop", "attack", "kill", "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d", - "quit", "use", "burn", "stoke", "search", "walk", "cook": + "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt": return ClassActive case "eat": return ClassFree @@ -188,6 +190,16 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { parts := strings.Fields(strings.ToLower(input)) cmd := parts[0] + + if len(parts) == 1 { + switch cmd { + case "equip", "eq", "equipment", "wear", "wield": + g.doEquipment(sess) + g.writePrompt(sess) + return + } + } + class := classifyCommand(cmd) if class == ClassInstant { @@ -302,8 +314,6 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI g.doScore(sess) case "i", "inv", "inventory": g.doInventory(sess) - case "eq", "equipment": - g.doEquipment(sess) case "quit": g.doQuit(sess) return @@ -358,6 +368,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI case "cook": g.doCook(sess, strings.Join(args, " ")) return + case "smelt": + g.doSmelt(sess, strings.Join(args, " ")) + return case "eat": g.doEat(sess, strings.Join(args, " ")) return @@ -394,7 +407,7 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI case "walk": g.doWalk(sess, args) return - case "wear", "wield": + case "eq", "equipment", "equip", "wear", "wield": g.doWear(sess, strings.Join(args, " ")) return case "remove", "unwear", "unwield": @@ -446,7 +459,7 @@ func (g *Game) ProcessQueuedCommands() { } switch as.Type { case ActionGathering, ActionCombating, ActionUsing, ActionTalking, - ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking: + ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking, ActionSmelting: default: if as.Type != ActionMoving || p.MoveTicks <= 0 { p.ActionState = nil @@ -488,13 +501,12 @@ func (g *Game) ProcessQueuedCommands() { if p.MoveTicks > 0 { p.ClearMoveState() } - wasBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0 _, isResting := g.restTimers[p.Name] - if !isResting && (wasBusy || !isBusy) { + if !isResting && !isBusy { g.writePrompt(qc.Session) } } |
