diff options
Diffstat (limited to 'internal/game')
| -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 |
12 files changed, 614 insertions, 40 deletions
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 { |
