diff options
| author | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
| commit | 458916fb78dcef3ff77cb29b7ba98d9f646819fc (patch) | |
| tree | 19cd8b8eef41a1aa00d155bf7953ad2770f91a4c /internal | |
| parent | 97a1a908b9e6e6d3516628c139c9b64262b4fe08 (diff) | |
| download | thehouseoficarus-458916fb78dcef3ff77cb29b7ba98d9f646819fc.tar.gz | |
feat: rough pharmacy skill added
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/combat/formulas.go | 83 | ||||
| -rw-r--r-- | internal/game/action.go | 2 | ||||
| -rw-r--r-- | internal/game/action_clean.go | 123 | ||||
| -rw-r--r-- | internal/game/action_production.go | 1 | ||||
| -rw-r--r-- | internal/game/action_state.go | 6 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 143 | ||||
| -rw-r--r-- | internal/game/cmd_clean.go | 78 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 60 | ||||
| -rw-r--r-- | internal/game/cmd_mix.go | 113 | ||||
| -rw-r--r-- | internal/game/cmd_stats.go | 70 | ||||
| -rw-r--r-- | internal/game/equip_stats.go | 31 | ||||
| -rw-r--r-- | internal/game/game.go | 14 | ||||
| -rw-r--r-- | internal/game/utils.go | 13 | ||||
| -rw-r--r-- | internal/object/item.go | 20 | ||||
| -rw-r--r-- | internal/player/player.go | 7 | ||||
| -rw-r--r-- | internal/player/store.go | 4 | ||||
| -rw-r--r-- | internal/world/mob.go | 39 |
17 files changed, 746 insertions, 61 deletions
diff --git a/internal/combat/formulas.go b/internal/combat/formulas.go index 0b3248b..963ee9c 100644 --- a/internal/combat/formulas.go +++ b/internal/combat/formulas.go @@ -2,27 +2,33 @@ package combat import "math/rand" -func AttackRoll(attackLevel int, styleBonus int, equipBonus int) int { - return (attackLevel + styleBonus + 8) * (equipBonus + 64) +func AttackRoll(level int, styleBonus int, equipBonus int) int { + effective := level + styleBonus + 8 + return effective * (equipBonus + 64) } -func DefenseRoll(defenseLevel int, styleBonus int, equipBonus int) int { - return (defenseLevel + styleBonus + 8) * (equipBonus + 64) +func DefenseRoll(level int, styleBonus int, equipBonus int) int { + effective := level + styleBonus + 8 + return effective * (equipBonus + 64) } -func HitCheck(attackRoll, defenseRoll int) bool { - if attackRoll > defenseRoll { - return true - } - if attackRoll < defenseRoll { - return false +func HitChance(attackRoll, defenseRoll int) float64 { + a := float64(attackRoll) + d := float64(defenseRoll) + if a > d { + return 1.0 - (d+2.0)/(2.0*(a+1.0)) } - return rand.Intn(2) == 1 + return a / (2.0 * (d + 1.0)) +} + +func HitCheck(attackRoll, defenseRoll int) bool { + chance := HitChance(attackRoll, defenseRoll) + return rand.Float64() < chance } -func MaxHit(strengthLevel int, styleBonus int, equipBonus int) int { - effective := (strengthLevel + styleBonus + 8) * (equipBonus + 64) - hit := effective / 512 +func MaxHit(level int, styleBonus int, equipBonus int) int { + effective := level + styleBonus + 8 + hit := (effective * (equipBonus + 64)) / 512 if hit < 1 { hit = 1 } @@ -50,3 +56,52 @@ func AttackStyleBonus(style string) (attack, strength, defense int) { return 0, 0, 0 } } + +func RangedStyleBonus(style string) (ranged, defense int) { + switch style { + case "accurate": + return 3, 0 + case "aggressive": + return 3, 0 + case "defensive": + return 0, 3 + case "balanced": + return 1, 1 + default: + return 0, 0 + } +} + +func SelectAttackBonus(attackType string, stab, slash, crush, science, ranged int) int { + switch attackType { + case "stab": + return stab + case "slash": + return slash + case "crush": + return crush + case "science": + return science + case "ranged": + return ranged + default: + return crush + } +} + +func SelectDefenseBonus(attackType string, stab, slash, crush, science, ranged int) int { + switch attackType { + case "stab": + return stab + case "slash": + return slash + case "crush": + return crush + case "science": + return science + case "ranged": + return ranged + default: + return crush + } +} diff --git a/internal/game/action.go b/internal/game/action.go index ca8506a..641f4dd 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -254,6 +254,8 @@ func (g *Game) AdvanceActions() { switch p.BackgroundAction.Type { case "fletch": g.advanceFletch(sess, p) + case "clean": + g.advanceClean(sess, p) } if p.BackgroundAction == nil { g.writePrompt(sess) diff --git a/internal/game/action_clean.go b/internal/game/action_clean.go new file mode 100644 index 0000000..dab3619 --- /dev/null +++ b/internal/game/action_clean.go @@ -0,0 +1,123 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) advanceClean(sess *net.Session, p *player.Player) { + phase, _ := p.BackgroundAction.Data["phase"].(int) + filter, _ := p.BackgroundAction.Data["filter"].(string) + + if phase == 0 { + p.BackgroundAction.Data["phase"] = 1 + p.BackgroundAction.WaitLeft = engine.ToTicks(2) + return + } + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + g.CancelBackgroundAction(p) + return + } + + var recipe *cleanMatch + for _, r := range allRecipes { + if r.Type != "clean" { + continue + } + if filter != "" { + if len(r.Consume) == 0 || len(r.Consume[0].Items) == 0 { + continue + } + if !strings.Contains(r.Consume[0].Items[0], filter) && + !strings.Contains(r.Output, filter) { + continue + } + } + if p.Level(player.Pharmacy) < r.Level { + continue + } + if !r.HasAllItems(p.HasItem) { + continue + } + recipe = &cleanMatch{r.Consume[0].Items[0], r.Output, r.XP, r.Message} + break + } + + if recipe == nil { + sess.WriteLine("\nYou've finished cleaning herbs.") + g.CancelBackgroundAction(p) + return + } + + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == recipe.consumeID { + slot.ItemID = recipe.outputID + break + } + } + + if recipe.xp > 0 { + if newLevel := p.AddSkillXP(player.Pharmacy, recipe.xp); newLevel > 0 { + sess.WriteLine(g.colorize(sess, "level_up", + fmt.Sprintf("*** You are now level %d pharmacy! ***", newLevel))) + } + } + g.AccountStore.SaveCharacter(p) + + msg := recipe.message + if msg == "" { + outDef, _ := g.ItemStore.Load(recipe.outputID) + outputName := recipe.outputID + if outDef != nil { + outputName = outDef.Name + } + msg = fmt.Sprintf("You clean a %s.", outputName) + } + if p.OptionBool("xp_drops") && recipe.xp > 0 { + abbr := player.SkillAbbr[player.Pharmacy] + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.xp, abbr)) + } + sess.WriteLine(msg) + + hasMore := false + for _, r := range allRecipes { + if r.Type != "clean" { + continue + } + if filter != "" { + if len(r.Consume) == 0 || len(r.Consume[0].Items) == 0 { + continue + } + if !strings.Contains(r.Consume[0].Items[0], filter) && + !strings.Contains(r.Output, filter) { + continue + } + } + if p.Level(player.Pharmacy) >= r.Level && r.HasAllItems(p.HasItem) { + hasMore = true + break + } + } + + if !hasMore { + sess.WriteLine("\nYou've finished cleaning herbs.") + g.CancelBackgroundAction(p) + return + } + + p.BackgroundAction.WaitLeft = engine.ToTicks(2) +} + +type cleanMatch struct { + consumeID string + outputID string + xp int + message string +} diff --git a/internal/game/action_production.go b/internal/game/action_production.go index df23198..c44f3d6 100644 --- a/internal/game/action_production.go +++ b/internal/game/action_production.go @@ -33,6 +33,7 @@ var productionTypes = map[string]productionTypeInfo{ "crafting": {"craft", "crafting"}, "combine": {"combine", "combining"}, "fletching": {"fletch", "fletching"}, + "pharmacy": {"mix", "mixing"}, } var productionActionTypes map[string]bool diff --git a/internal/game/action_state.go b/internal/game/action_state.go index 4510afd..1935703 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -22,6 +22,8 @@ const ( ActionProducing ActionType = "producing" ActionFletching ActionType = "fletching" ActionEating ActionType = "eating" + ActionCleaning ActionType = "cleaning" + ActionMixing ActionType = "mixing" ) type ActionState struct { @@ -73,6 +75,10 @@ func (a *ActionState) Description() string { return "fletching " + a.TargetName case ActionEating: return "eating " + a.TargetName + case ActionCleaning: + return "cleaning herbs" + case ActionMixing: + return "mixing " + a.TargetName } return "" } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 4c6941a..4d2d19f 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -46,6 +46,15 @@ func (g *Game) doAttack(sess *net.Session, input string) { return } + if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil && def.WeaponType == object.WeaponRanged { + if _, hasAmmo := p.Equipment[object.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 { + sess.WriteLine("You don't have any ammo equipped.") + return + } + } + } + g.startCombat(sess, p, mob) } @@ -177,23 +186,51 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI return } - attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) - - equipAtt := 0 - equipStr := 0 + attackType := "crush" + var weaponType object.WeaponType if itemID, ok := p.Equipment[object.SlotMainHand]; ok { - def, err := g.ItemStore.Load(itemID) - if err == nil { - equipAtt = def.Stats.AttackBonus - equipStr = def.Stats.StrengthBonus + if def, err := g.ItemStore.Load(itemID); err == nil { + if def.AttackType != "" { + attackType = def.AttackType + } else if def.WeaponType == object.WeaponRanged { + attackType = "ranged" + } else if def.WeaponType == object.WeaponScience { + attackType = "science" + } + weaponType = def.WeaponType } } - attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt) - defRoll := combat.DefenseRoll(mob.Defense, 0, 0) + totals := g.playerEquipBonuses(p) + isRanged := weaponType == object.WeaponRanged + + var attRoll, defRoll, maxHit int + + if isRanged { + rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) + equipAttack := totals.RangedAttack + attRoll = combat.AttackRoll(p.Level(player.Ranged), rangedBonus, equipAttack) + + mobDefBonus := combat.SelectDefenseBonus(attackType, + mob.StabDefense, mob.SlashDefense, mob.CrushDefense, + mob.ScienceDefense, mob.RangedDefense) + defRoll = combat.DefenseRoll(mob.Defense, 0, mobDefBonus) + maxHit = combat.MaxHit(p.Level(player.Ranged), rangedBonus, totals.RangedStrength) + } else { + attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) + equipAttack := combat.SelectAttackBonus(attackType, + totals.StabAttack, totals.SlashAttack, totals.CrushAttack, + totals.ScienceAttack, totals.RangedAttack) + attRoll = combat.AttackRoll(p.Level(player.Attack), attBonus, equipAttack) + + mobDefBonus := combat.SelectDefenseBonus(attackType, + mob.StabDefense, mob.SlashDefense, mob.CrushDefense, + mob.ScienceDefense, mob.RangedDefense) + defRoll = combat.DefenseRoll(mob.Defense, 0, mobDefBonus) + maxHit = combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus) + } if combat.HitCheck(attRoll, defRoll) { - maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, equipStr) dmg := combat.RollDamage(maxHit) mob.HP -= dmg @@ -203,7 +240,16 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI if mob.HP < mob.MaxHP && mob.HP > 0 { mob.StartRegen() } - gains, leveled := g.awardCombatXP(p, dmg) + gains, leveled := g.awardCombatXP(p, dmg, isRanged) + + if isRanged { + p.AmmoQty-- + if p.AmmoQty <= 0 { + delete(p.Equipment, object.SlotAmmo) + p.AmmoQty = 0 + } + g.AccountStore.SaveCharacter(p) + } for _, skill := range leveled { sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill))) @@ -230,27 +276,45 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")") } sess.WriteLine(line) + + if isRanged && p.AmmoQty <= 0 { + sess.WriteLine("You've run out of ammo!") + combat.LeaveCombat(p.Name) + } } else { sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true)))) + + if isRanged { + p.AmmoQty-- + if p.AmmoQty <= 0 { + delete(p.Equipment, object.SlotAmmo) + p.AmmoQty = 0 + g.AccountStore.SaveCharacter(p) + sess.WriteLine("You've run out of ammo!") + combat.LeaveCombat(p.Name) + } + } } } func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { - _, _, defBonus := combat.AttackStyleBonus(string(p.AttackStyle)) + _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle)) - equipDef := 0 - for _, itemID := range p.Equipment { - def, err := g.ItemStore.Load(itemID) - if err == nil { - equipDef += def.Stats.DefenseBonus - } + mobAttackType := mob.AttackType + if mobAttackType == "" { + mobAttackType = "crush" } - attRoll := combat.AttackRoll(mob.Attack, 0, 0) - defRoll := combat.DefenseRoll(p.Level(player.Defense), defBonus, equipDef) + attRoll := combat.AttackRoll(mob.Attack, 0, mob.AttackBonus) + + totals := g.playerEquipBonuses(p) + equipDef := combat.SelectDefenseBonus(mobAttackType, + totals.StabDefense, totals.SlashDefense, totals.CrushDefense, + totals.ScienceDefense, totals.RangedDefense) + defRoll := combat.DefenseRoll(p.Level(player.Defense), defStyleBonus, equipDef) if combat.HitCheck(attRoll, defRoll) { - maxHit := combat.MaxHit(mob.Strength, 0, 0) + maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus) dmg := combat.RollDamage(maxHit) p.HP -= dmg @@ -375,25 +439,32 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst } } -func (g *Game) awardCombatXP(p *player.Player, dmg int) ([]xpGain, []player.SkillName) { +func (g *Game) awardCombatXP(p *player.Player, dmg int, isRanged bool) ([]xpGain, []player.SkillName) { baseXP := dmg * 4 var gains []xpGain var leveledUp []player.SkillName - switch p.AttackStyle { - case player.Accurate: - gains = []xpGain{{string(player.Attack), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Aggressive: - gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Defensive: - gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} - case player.Balanced: - quarter := baseXP / 4 + if isRanged { gains = []xpGain{ - {string(player.Attack), quarter}, - {string(player.Strength), quarter}, - {string(player.Defense), quarter}, - {string(player.Hitpoints), quarter}, + {string(player.Ranged), baseXP * 3 / 4}, + {string(player.Hitpoints), baseXP / 4}, + } + } else { + switch p.AttackStyle { + case player.Accurate: + gains = []xpGain{{string(player.Attack), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Aggressive: + gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Defensive: + gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}} + case player.Balanced: + quarter := baseXP / 4 + gains = []xpGain{ + {string(player.Attack), quarter}, + {string(player.Strength), quarter}, + {string(player.Defense), quarter}, + {string(player.Hitpoints), quarter}, + } } } diff --git a/internal/game/cmd_clean.go b/internal/game/cmd_clean.go new file mode 100644 index 0000000..b3d8408 --- /dev/null +++ b/internal/game/cmd_clean.go @@ -0,0 +1,78 @@ +package game + +import ( + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) doClean(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + + if combat.GetCombat(p.Name) != nil { + sess.WriteLine("You can't do that during combat!") + return + } + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + sess.WriteLine("Error loading recipes.") + return + } + + filter := strings.TrimSpace(input) + + found := false + for _, r := range allRecipes { + if r.Type != "clean" { + continue + } + if filter != "" && !matchesCleanFilter(r, filter) { + continue + } + if p.Level(player.Pharmacy) < r.Level { + continue + } + if !r.HasAllItems(p.HasItem) { + continue + } + found = true + break + } + + if !found { + sess.WriteLine("You don't have any herbs to clean.") + return + } + + g.CancelBackgroundAction(p) + + p.BackgroundAction = &action.Action{ + Type: "clean", + TargetID: "clean_herbs", + Data: map[string]any{ + "phase": 0, + "filter": filter, + }, + WaitLeft: engine.ToTicks(1), + } + p.BackgroundActionState = &ActionState{Type: ActionCleaning} + + sess.WriteLine("\nYou begin cleaning herbs.") +} + +func matchesCleanFilter(r action.RecipeDef, filter string) bool { + if len(r.Consume) > 0 && len(r.Consume[0].Items) > 0 { + if strings.Contains(r.Consume[0].Items[0], filter) { + return true + } + } + if strings.Contains(r.Output, filter) { + return true + } + return false +} diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 9809e9b..1c9eb9c 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -455,6 +455,18 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { fmt.Sprintf(" Defense: %d", best.Defense), fmt.Sprintf(" HP: %d/%d", best.HP, best.MaxHP), ) + if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 || + best.ScienceDefense != 0 || best.RangedDefense != 0 { + sess.WriteLines( + "", + " Defense bonuses:", + fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense), + fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense), + ) + } + if best.Weakness != "" { + sess.WriteLine(fmt.Sprintf(" Weakness: %s", best.Weakness)) + } return } @@ -511,6 +523,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { fmt.Sprintf(" %s", def.Description), fmt.Sprintf(" Value: %d credits", def.Value), ) + g.showItemStats(sess, def) return } @@ -533,6 +546,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { lines = append(lines, fmt.Sprintf(" Has %d units of butane left.", slot.Quality)) } sess.WriteLines(lines...) + g.showItemStats(sess, def) return } @@ -711,3 +725,49 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine } } } + +func (g *Game) showItemStats(sess *net.Session, def *object.ItemDef) { + s := def.Stats + hasAttack := s.StabAttack != 0 || s.SlashAttack != 0 || s.CrushAttack != 0 || + s.ScienceAttack != 0 || s.RangedAttack != 0 + hasDefense := s.StabDefense != 0 || s.SlashDefense != 0 || s.CrushDefense != 0 || + s.ScienceDefense != 0 || s.RangedDefense != 0 + hasOther := s.StrengthBonus != 0 || s.RangedStrength != 0 || + s.ScienceDamage != 0 || s.TechnologyBonus != 0 + + if !hasAttack && !hasDefense && !hasOther { + return + } + + sess.WriteLine("") + if hasAttack || hasDefense { + sess.WriteLine(" Attack bonuses: Defense bonuses:") + sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense)) + sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense)) + sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense)) + sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense)) + sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense)) + } + if hasOther { + sess.WriteLine("") + sess.WriteLine(" Other bonuses:") + if s.StrengthBonus != 0 { + sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus)) + } + if s.RangedStrength != 0 { + sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength)) + } + if s.ScienceDamage != 0 { + sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage)) + } + if s.TechnologyBonus != 0 { + sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus)) + } + } + if def.AttackType != "" { + sess.WriteLine(fmt.Sprintf(" Attack type: %s", def.AttackType)) + } + if def.Speed > 0 { + sess.WriteLine(fmt.Sprintf(" Speed: %.0f", def.Speed)) + } +} diff --git a/internal/game/cmd_mix.go b/internal/game/cmd_mix.go new file mode 100644 index 0000000..3082870 --- /dev/null +++ b/internal/game/cmd_mix.go @@ -0,0 +1,113 @@ +package game + +import ( + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func (g *Game) doMix(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + g.CancelAction(p) + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + sess.WriteLine("Error loading recipes.") + return + } + + if input == "" { + g.showMixMenu(sess, p, allRecipes) + return + } + + qty, itemName := parseQty(input) + _ = qty + + var matched []action.RecipeDef + for _, r := range allRecipes { + if r.Type != "pharmacy" { + continue + } + outDef, _ := g.ItemStore.Load(r.Output) + name := r.Output + if outDef != nil { + name = outDef.Name + } + if world.WordPrefixMatch(itemName, name) { + matched = append(matched, r) + } + } + + if len(matched) == 0 { + sess.WriteLine("You can't mix that.") + return + } + + var available []action.RecipeDef + for _, r := range matched { + if r.HasAllItems(p.HasItem) && p.Level(player.Pharmacy) >= r.Level { + available = append(available, r) + } + } + + if len(available) == 0 { + sess.WriteLine("You don't have the materials for that.") + return + } + + if len(available) == 1 { + g.promptHowMany(sess, available[0].ID) + return + } + + sess.State = net.StateRecipeChoice + var names []string + for _, r := range available { + names = append(names, g.recipeName(sess, &r)) + } + g.showMenuTable(sess, "What would you like to mix?", names) + sess.PendingMenu = recipeMenuData(available) +} + +func (g *Game) showMixMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef) { + seen := make(map[string]bool) + var entries []recipeEntry + + for _, r := range allRecipes { + if r.Type != "pharmacy" { + continue + } + if seen[r.ID] { + continue + } + if !r.HasAllItems(p.HasItem) { + continue + } + seen[r.ID] = true + entries = append(entries, recipeEntry{g.recipeName(sess, &r), r}) + } + + if len(entries) == 0 { + sess.WriteLine("You don't have anything you can mix.") + return + } + + if len(entries) == 1 { + if p.OptionBool("mix_all") { + g.startProductionFromRecipe(sess, p, &entries[0].Recipe, 0) + return + } + g.promptHowMany(sess, entries[0].Recipe.ID) + return + } + + sess.State = net.StateRecipeChoice + var names []string + for _, e := range entries { + names = append(names, e.ItemName) + } + g.showMenuTable(sess, "What would you like to mix?", names) + sess.PendingMenu = entryMenuData(entries) +} diff --git a/internal/game/cmd_stats.go b/internal/game/cmd_stats.go new file mode 100644 index 0000000..0dc963e --- /dev/null +++ b/internal/game/cmd_stats.go @@ -0,0 +1,70 @@ +package game + +import ( + "fmt" + + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" +) + +func (g *Game) doStats(sess *net.Session) { + p := sess.Player.(*player.Player) + totals := g.playerEquipBonuses(p) + + attackType := "crush" + weaponName := "unarmed" + var weaponType object.WeaponType + if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil { + weaponName = def.Name + weaponType = def.WeaponType + if def.AttackType != "" { + attackType = def.AttackType + } else if def.WeaponType == object.WeaponRanged { + attackType = "ranged" + } else if def.WeaponType == object.WeaponScience { + attackType = "science" + } + } + } + + sess.WriteLines( + "", + fmt.Sprintf("Weapon: %s (attack type: %s)", weaponName, attackType), + "", + "Attack bonuses: Defense bonuses:", + fmt.Sprintf(" Stab: %+4d Stab: %+4d", totals.StabAttack, totals.StabDefense), + fmt.Sprintf(" Slash: %+4d Slash: %+4d", totals.SlashAttack, totals.SlashDefense), + fmt.Sprintf(" Crush: %+4d Crush: %+4d", totals.CrushAttack, totals.CrushDefense), + fmt.Sprintf(" Science:%+4d Science:%+4d", totals.ScienceAttack, totals.ScienceDefense), + fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", totals.RangedAttack, totals.RangedDefense), + "", + "Other bonuses:", + fmt.Sprintf(" Melee strength: %+d", totals.StrengthBonus), + fmt.Sprintf(" Ranged strength: %+d", totals.RangedStrength), + fmt.Sprintf(" Science damage: %+d", totals.ScienceDamage), + fmt.Sprintf(" Technology: %+d", totals.TechnologyBonus), + ) + + var attRoll, maxHitVal int + if weaponType == object.WeaponRanged { + rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) + attRoll = combat.AttackRoll(p.Level(player.Ranged), rangedBonus, totals.RangedAttack) + maxHitVal = combat.MaxHit(p.Level(player.Ranged), rangedBonus, totals.RangedStrength) + } else { + attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) + equipAtt := combat.SelectAttackBonus(attackType, + totals.StabAttack, totals.SlashAttack, totals.CrushAttack, + totals.ScienceAttack, totals.RangedAttack) + attRoll = combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt) + maxHitVal = combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus) + } + + sess.WriteLines( + "", + fmt.Sprintf("Style: %s Attack roll: %d Max hit: %d", + p.AttackStyle, attRoll, maxHitVal), + ) +} diff --git a/internal/game/equip_stats.go b/internal/game/equip_stats.go new file mode 100644 index 0000000..559671f --- /dev/null +++ b/internal/game/equip_stats.go @@ -0,0 +1,31 @@ +package game + +import ( + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" +) + +func (g *Game) playerEquipBonuses(p *player.Player) object.ItemStats { + var totals object.ItemStats + for _, itemID := range p.Equipment { + def, err := g.ItemStore.Load(itemID) + if err != nil { + continue + } + totals.StabAttack += def.Stats.StabAttack + totals.SlashAttack += def.Stats.SlashAttack + totals.CrushAttack += def.Stats.CrushAttack + totals.ScienceAttack += def.Stats.ScienceAttack + totals.RangedAttack += def.Stats.RangedAttack + totals.StabDefense += def.Stats.StabDefense + totals.SlashDefense += def.Stats.SlashDefense + totals.CrushDefense += def.Stats.CrushDefense + totals.ScienceDefense += def.Stats.ScienceDefense + totals.RangedDefense += def.Stats.RangedDefense + totals.StrengthBonus += def.Stats.StrengthBonus + totals.RangedStrength += def.Stats.RangedStrength + totals.ScienceDamage += def.Stats.ScienceDamage + totals.TechnologyBonus += def.Stats.TechnologyBonus + } + return totals +} diff --git a/internal/game/game.go b/internal/game/game.go index a1cba83..8b5f924 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -139,7 +139,7 @@ func classifyCommand(cmd string) CommandClass { "look", "l", "exits", "help", "map", "option", "options", "alias", "unalias", "description", "desc", "queued", "color", "colors", - "colortable", "prompt", "style": + "colortable", "prompt", "style", "stats": return ClassInstant case "equip", "eq", "equipment", "wear", "wield", "remove", "unwear", "unwield": return ClassFree @@ -147,9 +147,9 @@ func classifyCommand(cmd string) CommandClass { "attack", "kill", "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d", - "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft": + "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft", "mix": return ClassActive - case "eat", "fletch": + case "eat", "fletch", "clean": return ClassFree } if _, ok := verbAliases[cmd]; ok { @@ -314,6 +314,8 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI } case "sc", "score": g.doScore(sess) + case "stats": + g.doStats(sess) case "i", "inv", "inventory": g.doInventory(sess) case "quit": @@ -385,6 +387,12 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI case "fletch": g.doFletch(sess, strings.Join(args, " ")) return + case "clean": + g.doClean(sess, strings.Join(args, " ")) + return + case "mix": + g.doMix(sess, strings.Join(args, " ")) + return case "talk", "speak", "ask": g.CancelAction(p) if len(args) == 0 { diff --git a/internal/game/utils.go b/internal/game/utils.go index 55e48c6..6a294c5 100644 --- a/internal/game/utils.go +++ b/internal/game/utils.go @@ -45,7 +45,18 @@ func mobDisplayName(m *world.MobInstance, definite bool) string { } func mobCombatLevel(m *world.MobInstance) int { - return int(0.25*float64(m.Attack+m.Strength+m.Defense+m.MaxHP) + 0.5) + base := float64(m.Defense+m.MaxHP) / 4.0 + melee := float64(m.Attack+m.Strength) / 4.0 + ranged := float64(m.Ranged) * 3.0 / 8.0 + science := float64(m.Science) * 3.0 / 8.0 + best := melee + if ranged > best { + best = ranged + } + if science > best { + best = science + } + return int(base + best + 0.5) } func uniqueItemNames(matches []itemMatch) []string { diff --git a/internal/object/item.go b/internal/object/item.go index a6a06e1..14cb65c 100644 --- a/internal/object/item.go +++ b/internal/object/item.go @@ -39,6 +39,7 @@ type ItemDef struct { Stackable bool `yaml:"stackable"` EquipSlot EquipSlot `yaml:"equip_slot"` WeaponType WeaponType `yaml:"weapon_type"` + AttackType string `yaml:"attack_type"` Stats ItemStats `yaml:"stats"` Speed float64 `yaml:"speed"` ToolType string `yaml:"tool_type"` @@ -65,10 +66,21 @@ type MadeFromEntry struct { } type ItemStats struct { - AttackBonus int `yaml:"attack_bonus"` - StrengthBonus int `yaml:"strength_bonus"` - DefenseBonus int `yaml:"defense_bonus"` - ScienceBonus int `yaml:"science_bonus"` + StabAttack int `yaml:"stab_attack"` + SlashAttack int `yaml:"slash_attack"` + CrushAttack int `yaml:"crush_attack"` + ScienceAttack int `yaml:"science_attack"` + RangedAttack int `yaml:"ranged_attack"` + + StabDefense int `yaml:"stab_defense"` + SlashDefense int `yaml:"slash_defense"` + CrushDefense int `yaml:"crush_defense"` + ScienceDefense int `yaml:"science_defense"` + RangedDefense int `yaml:"ranged_defense"` + + StrengthBonus int `yaml:"strength_bonus"` + RangedStrength int `yaml:"ranged_strength"` + ScienceDamage int `yaml:"science_damage"` TechnologyBonus int `yaml:"technology_bonus"` } diff --git a/internal/player/player.go b/internal/player/player.go index edfe5dd..4b26a84 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -21,7 +21,7 @@ const ( Smithing SkillName = "smithing" Crafting SkillName = "crafting" Fletching SkillName = "fletching" - Alchemy SkillName = "alchemy" + Pharmacy SkillName = "pharmacy" Thieving SkillName = "thieving" Agility SkillName = "agility" Construction SkillName = "construction" @@ -34,7 +34,7 @@ const ( var AllSkills = []SkillName{ Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Fishing, Cooking, Woodcutting, Firemaking, Mining, Smithing, Crafting, Fletching, - Alchemy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming, + Pharmacy, Thieving, Agility, Construction, Scavenging, Hacking, Assassin, Farming, } var SkillAbbr = map[SkillName]string{ @@ -53,7 +53,7 @@ var SkillAbbr = map[SkillName]string{ Smithing: "smt", Crafting: "cft", Fletching: "flt", - Alchemy: "alc", + Pharmacy: "pha", Thieving: "thv", Agility: "agl", Construction: "con", @@ -131,6 +131,7 @@ var OptionDefs = []OptionDef{ {"smith_all", OptBool, false, nil, "Auto-start smithing when only one product is possible"}, {"cook_all", OptBool, false, nil, "Auto-start cooking when only one product is possible"}, {"smelt_all", OptBool, false, nil, "Auto-start smelting when only one product is possible"}, + {"mix_all", OptBool, false, nil, "Auto-start mixing when only one product is possible"}, } var optionByName map[string]*OptionDef diff --git a/internal/player/store.go b/internal/player/store.go index 04af594..c05b29d 100644 --- a/internal/player/store.go +++ b/internal/player/store.go @@ -89,6 +89,10 @@ func (s *AccountStore) LoadCharacter(name string) (*Player, error) { if p.Skills == nil { p.Skills = make(map[SkillName]int) } + if xp, ok := p.Skills["alchemy"]; ok { + p.Skills[Pharmacy] = xp + delete(p.Skills, "alchemy") + } if p.Equipment == nil { p.Equipment = make(map[object.EquipSlot]string) } diff --git a/internal/world/mob.go b/internal/world/mob.go index d801b59..daf73dc 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -28,12 +28,26 @@ type MobDef struct { Strength int `yaml:"strength"` Defense int `yaml:"defense"` HP int `yaml:"hp"` + Ranged int `yaml:"ranged"` + Science int `yaml:"science"` Speed float64 `yaml:"speed"` Aggressive bool `yaml:"aggressive"` Protected bool `yaml:"protected"` Unique bool `yaml:"unique"` RespawnTicks float64 `yaml:"respawn_ticks"` Drops DropTable `yaml:"drops"` + + AttackBonus int `yaml:"attack_bonus"` + StrengthBonus int `yaml:"strength_bonus"` + AttackType string `yaml:"attack_type"` + + StabDefense int `yaml:"stab_defense"` + SlashDefense int `yaml:"slash_defense"` + CrushDefense int `yaml:"crush_defense"` + ScienceDefense int `yaml:"science_defense"` + RangedDefense int `yaml:"ranged_defense"` + + Weakness string `yaml:"weakness"` } type MobInstance struct { @@ -46,6 +60,8 @@ type MobInstance struct { Attack int Strength int Defense int + Ranged int + Science int Speed float64 Aggressive bool Protected bool @@ -59,6 +75,18 @@ type MobInstance struct { WanderInterval float64 WanderTickCounter int regenerateTick int + + AttackBonus int + StrengthBonus int + AttackType string + + StabDefense int + SlashDefense int + CrushDefense int + ScienceDefense int + RangedDefense int + + Weakness string } const ( @@ -243,6 +271,8 @@ func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) { Attack: dw.def.Attack, Strength: dw.def.Strength, Defense: dw.def.Defense, + Ranged: dw.def.Ranged, + Science: dw.def.Science, Speed: dw.def.Speed, Aggressive: dw.def.Aggressive, Protected: dw.def.Protected, @@ -253,6 +283,15 @@ func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) { RoomID: roomID, HomeRoomID: roomID, Drops: dw.def.Drops, + AttackBonus: dw.def.AttackBonus, + StrengthBonus: dw.def.StrengthBonus, + AttackType: dw.def.AttackType, + StabDefense: dw.def.StabDefense, + SlashDefense: dw.def.SlashDefense, + CrushDefense: dw.def.CrushDefense, + ScienceDefense: dw.def.ScienceDefense, + RangedDefense: dw.def.RangedDefense, + Weakness: dw.def.Weakness, } inst.IdleDescription = pickIdleDescription(dw.def.IdleDescriptions) s.instances[instID] = inst |
