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/production_engine.go | |
| parent | 2725e2927a1595c7b100d942d1f14146252adeb7 (diff) | |
| download | thehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz | |
slop refactor
Diffstat (limited to 'internal/game/production_engine.go')
| -rw-r--r-- | internal/game/production_engine.go | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go new file mode 100644 index 0000000..c30e7bb --- /dev/null +++ b/internal/game/production_engine.go @@ -0,0 +1,359 @@ +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.Consume { + 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.Wait + 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, + Wait: wait, + StartMsg: startMsg, + EndMsg: 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 + } + + _, stationName := g.findStation(p.RoomID, craft.Station) + if stationName == "" { + stationName = "inventory" + } + + info, ok := productionTypes[craft.Type] + if !ok { + info = productionTypeInfo{ + ActionType: behavior.ActionType(craft.Type), + DisplayVerb: craft.Type, + StartMessage: "You start " + craft.Type + " %i1.", + SuccessMessage: "You produce %n.", + EndMessage: "You've finished " + craft.Type + ".", + } + } + + 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.Wait + startMsg := d.StartMsg + endMsg := d.EndMsg + 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) + } + if len(craft.Steps) > 0 { + d.NextStepIndex = 0 + d.StepsAccumulatedTicks = 0 + } + d.Phase = 1 + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + if stepsIdx := d.NextStepIndex; stepsIdx < len(craft.Steps) { + accTicks := d.StepsAccumulatedTicks + accTicks += wait + d.StepsAccumulatedTicks = accTicks + for i := stepsIdx; i < len(craft.Steps); i++ { + if accTicks >= craft.Steps[i].Tick { + sess.WriteLine(g.resolveCraftVar(sess, craft.Steps[i].Message, craft, item.ID, nil, p.HasItem)) + d.NextStepIndex = i + 1 + } + } + } + + 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) + } + + info, _ := productionTypes[craft.Type] + + 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 { + craftConsumeAll(craft, p.HasItem, p.RemoveItem) + slot.Quantity += outputQty + placed = true + break + } + } + } + if !placed { + craftConsumeAll(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.Message, 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 { + craftConsumeAll(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.Consume { + 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 +} |
