aboutsummaryrefslogtreecommitdiff
path: root/internal/game/production_engine.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/production_engine.go
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/game/production_engine.go')
-rw-r--r--internal/game/production_engine.go359
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
+}