package game import ( "fmt" "math/rand" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) resolveCraftVar(sess *net.Session, varSpec string, craft *item.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { if !strings.Contains(varSpec, "%") { return varSpec } outDef, _ := g.ItemStore.Load(outputID) outputName := outputID if outDef != nil { outputName = g.itemColorize(sess, outDef, outDef.Name) } varSpec = strings.ReplaceAll(varSpec, "%n", outputName) for i, e := range craft.Ingredients { key := fmt.Sprintf("%%i%d", i+1) if !strings.Contains(varSpec, key) { continue } var matchedID string for _, id := range e.Items { if hasItem(id) { matchedID = id break } } if matchedID == "" && len(e.Items) > 0 { matchedID = e.Items[0] } if def, err := g.ItemStore.Load(matchedID); err == nil { varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) } else { varSpec = strings.ReplaceAll(varSpec, key, matchedID) } } for i, bpID := range byproducts { key := fmt.Sprintf("%%b%d", i+1) if !strings.Contains(varSpec, key) { continue } if def, err := g.ItemStore.Load(bpID); err == nil { varSpec = strings.ReplaceAll(varSpec, key, g.itemColorize(sess, def, def.Name)) } else { varSpec = strings.ReplaceAll(varSpec, key, bpID) } } return color.ExpandTags(g.colorMode(sess), varSpec) } func (g *Game) resolveCraftMessage(sess *net.Session, itemMsg, defaultMsg string, craft *item.CraftDef, outputID string, byproducts []string, hasItem func(string) bool) string { msg := itemMsg if msg == "" { msg = defaultMsg } if msg == "" { return "" } return g.resolveCraftVar(sess, msg, craft, outputID, byproducts, hasItem) } func (g *Game) startProduction(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, actionType behavior.ActionType, displayVerb, startMsg, endMsg string, count int) { g.cancelAction(p) g.cancelBgAction(p) skill := craft.EffectiveSkill() if skill != "" { skillLevel := p.Level(player.SkillName(skill)) if skillLevel < craft.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", craft.Level, skill)) g.reprompt(sess) return } } if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the required materials.") g.reprompt(sess) return } outDef, _ := g.ItemStore.Load(item.ID) wait := craft.TicksPerCycle if wait <= 0 { wait = 4 } outputName := item.ID if outDef != nil { outputName = outDef.Name } p.Action = &behavior.Action{ Type: actionType, TargetID: item.ID, TargetName: outputName, Data: &behavior.ProductionData{ ItemID: item.ID, Phase: 0, TicksPerCycle: wait, StartMessage: startMsg, EndMessage: endMsg, Remaining: count, }, WaitLeft: engine.ToTicks(1), } g.broadcastAction(sess, "%s starts %s.", p.Name, displayVerb) } func (g *Game) startProductionFromItem(sess *net.Session, p *player.Player, item *item.ItemDef, count int) { craft := item.FirstCraft() if craft == nil { return } if craft.Type == "combine" { g.startCombine(sess, p, item, craft, count) return } _, stationName := g.findStation(p.RoomID, craft.Station) if stationName == "" { stationName = "inventory" } cKey := craft.Subtype if cKey == "" { cKey = craft.Type } info, ok := productionTypes[cKey] if !ok { info = productionTypeInfo{ ActionType: behavior.ActionType(cKey), DisplayVerb: cKey, StartMessage: "You start " + cKey + " %i1.", SuccessMessage: "You produce %n.", EndMessage: "You've finished " + cKey + ".", } } startMsg := g.resolveCraftMessage(sess, craft.StartMessage, info.StartMessage, craft, item.ID, nil, p.HasItem) if stationName != "inventory" { startMsg += " on the " + stationName + "." } endMsg := g.resolveCraftMessage(sess, craft.EndMessage, info.EndMessage, craft, item.ID, nil, p.HasItem) g.startProduction(sess, p, item, craft, info.ActionType, info.DisplayVerb, startMsg, endMsg, count) } func (g *Game) loadCraftItem(itemID string) *item.ItemDef { item, err := g.ItemStore.Load(itemID) if err != nil || len(item.Craft) == 0 { return nil } return item } func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { d, ok := p.Action.Data.(*behavior.ProductionData) if !ok { g.cancelAction(p) return false } itemID := d.ItemID phase := d.Phase wait := d.TicksPerCycle startMsg := d.StartMessage endMsg := d.EndMessage remaining := d.Remaining item := g.loadCraftItem(itemID) if item == nil { g.cancelAction(p) return false } craft := item.FirstCraft() if phase == 0 { if startMsg != "" { sess.WriteLine(startMsg) } d.Phase = 1 p.Action.WaitLeft = engine.ToTicks(wait) return true } skill := craft.EffectiveSkill() skillLevel := p.Level(player.SkillName(skill)) chance := 1.0 if craft.Success != nil { chance = behavior.SuccessChance(behavior.SuccessFormula(*craft.Success), skillLevel, craft.Level) } cKey := craft.Subtype if cKey == "" { cKey = craft.Type } info, _ := productionTypes[cKey] if rand.Float64() < chance { outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } byproducts := g.collectByproducts(p, item) placed := false outDef, _ := g.ItemStore.Load(item.ID) if outDef != nil && outDef.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == item.ID { consumeIngredients(craft, p.HasItem, p.RemoveItem) slot.Quantity += outputQty placed = true break } } } if !placed { consumeIngredients(craft, p.HasItem, p.RemoveItem) freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is too full!") g.cancelAction(p) return false } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: item.ID, Quantity: outputQty}) } for _, bp := range byproducts { if slot := p.FirstFreeSlot(); slot >= 0 { p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) } } if craft.XP > 0 { if newLevel := p.AddSkillXP(player.SkillName(skill), craft.XP); newLevel > 0 { sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, skill))) } } g.AccountStore.SaveCharacter(p) var defaultSuccess string if info.SuccessMessage != "" { defaultSuccess = info.SuccessMessage } else { defaultSuccess = "You produce %n." } msg := g.resolveCraftMessage(sess, craft.SuccessMessage, defaultSuccess, craft, item.ID, byproducts, p.HasItem) if p.OptionBool("xp_drops") && craft.XP > 0 { msg += g.formatXpDropSingle(sess, p, player.SkillName(skill), craft.XP) } sess.WriteLine(msg) } else { consumeIngredients(craft, p.HasItem, p.RemoveItem) if craft.Fail != "" { freeSlot := p.FirstFreeSlot() if freeSlot >= 0 { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) } } g.AccountStore.SaveCharacter(p) msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) if msg != "" { sess.WriteLine(g.colorize(sess, "damage_taken", msg)) } } if remaining > 0 { remaining-- d.Remaining = remaining if remaining <= 0 { if endMsg != "" { sess.WriteLine(endMsg) } g.cancelAction(p) return false } } if g.canContinueProduction(p, item) { p.Action.WaitLeft = engine.ToTicks(wait) return true } if endMsg != "" { sess.WriteLine(endMsg) } g.cancelAction(p) return false } func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string { craft := item.FirstCraft() if craft == nil { return nil } var byproducts []string for _, e := range craft.Ingredients { if len(e.Byproducts) == 0 { continue } for i, id := range e.Items { if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" { byproducts = append(byproducts, e.Byproducts[i]) break } } } return byproducts } func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool { craft := item.FirstCraft() if craft == nil { return false } if !craftHasAllItemsQty(craft, p.CountItem) { return false } outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } outDef, _ := g.ItemStore.Load(item.ID) if outDef != nil && outDef.Stackable { for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == item.ID { return true } } } return p.FirstFreeSlot() >= 0 }