package game import ( "fmt" "math/rand" "sort" "strconv" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) func (g *Game) doAttack(sess *net.Session, input string) { p := sess.Player if g.Combat.Get(p.Name) != nil { sess.WriteLine("You are already in combat!") return } if p.Action != nil { g.cancelAction(p) } mob := g.findMob(sess, input, p.RoomID) if mob == nil { return } if mob.HP <= 0 { if mob.IsTask() { sess.WriteLine(fmt.Sprintf("%s is already complete.", mobDisplayName(mob, true))) } else { sess.WriteLine("That is already dead.") } return } if mob.Protected { sess.WriteLine(fmt.Sprintf("You can't attack %s!", mobDisplayName(mob, true))) return } if mob.AssassinLevel > 0 && p.Level(player.Assassin) < mob.AssassinLevel { sess.WriteLine(fmt.Sprintf("You need Assassin level %d to attack %s.", mob.AssassinLevel, mobDisplayName(mob, true))) return } if g.Combat.IsMobInCombat(mob.InstanceID) { sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false))) return } if itemID, ok := p.Equipment[item.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil && def.EquipmentType() == item.EquipRangedWeapon { if _, hasAmmo := p.Equipment[item.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 { sess.WriteLine("You don't have any ammo equipped.") return } } } g.startCombat(sess, p, mob) } func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobInstance { lower := strings.ToLower(input) mobs := g.MobStore.MobsInRoom(roomID) idx := -1 name := lower if dotPos := strings.Index(lower, "."); dotPos > 0 { if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 { idx = n name = lower[dotPos+1:] } } var exact []*world.MobInstance var prefix []*world.MobInstance for _, m := range mobs { q := m.MatchQuality(name) if q == world.MatchExact { exact = append(exact, m) } else if q == world.MatchPrefix { prefix = append(prefix, m) } } candidates := exact if len(candidates) == 0 { candidates = prefix } if len(candidates) == 0 { sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input)) return nil } sort.Slice(candidates, func(i, j int) bool { return candidates[i].InstanceID < candidates[j].InstanceID }) if idx > 0 { if idx-1 < len(candidates) { return candidates[idx-1] } return nil } if len(candidates) == 1 { return candidates[0] } seen := make(map[string]bool) for _, m := range candidates { seen[mobDisplayName(m, false)] = true } if len(seen) > 1 { sess.WriteLine("Which one?") return nil } return candidates[0] } func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { if mob.OwnerOnly && mob.Owner != "" && mob.Owner != p.Name { sess.WriteLine("They don't seem interested in you.") return } g.cancelRest(p.Name) g.cancelBgAction(p) g.Combat.Enter(p.Name, mob.InstanceID) p.AttackTimer = 0 isTask := mob.IsTask() autotriggerActive := p.AutotriggerMod != "" if !autotriggerActive { attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle)) var styleParts []string if attBonus > 0 { styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus)) } if strBonus > 0 { styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus)) } if defBonus > 0 { styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus)) } styleStr := "" if len(styleParts) > 0 { styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" } if isTask { sess.WriteLine(fmt.Sprintf("You begin to %s %s.%s", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) } else { sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr)) } } else { mod := GetMod(p.AutotriggerMod) modName := p.AutotriggerMod if mod != nil { modName = mod.Name } if isTask { sess.WriteLine(fmt.Sprintf("You begin to %s %s using %s!", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), g.colorize(sess, "science_mod", modName))) } else { sess.WriteLine(fmt.Sprintf("You attack %s with %s!", g.colorize(sess, "mob", mobDisplayName(mob, true)), g.colorize(sess, "science_mod", modName))) } } if isTask { g.broadcastAction(sess, "%s begins working on %s.", p.Name, mobDisplayName(mob, true)) } else { g.broadcastAction(sess, "%s attacks %s!", p.Name, mobDisplayName(mob, true)) } g.Ticks.Subscribe(1, func() bool { cs := g.Combat.Get(p.Name) if cs == nil || !cs.Active { return false } currentMob := g.MobStore.GetInstance(cs.MobID) if currentMob == nil || currentMob.HP <= 0 { g.endCombat(sess, p, currentMob) return false } if p.Action != nil && p.Action.Type == behavior.TypeTriggerModule { return true } p.AttackTimer++ if float64(p.AttackTimer) >= g.playerWeaponSpeed(p) { p.AttackTimer = 0 if g.hasDeckEquipped(p) { if p.AutotriggerMod != "" { mod := GetMod(p.AutotriggerMod) if mod != nil && g.hasJunkCost(p, mod) { g.scienceAttack(sess, p, currentMob, mod) } else if mod != nil { sess.WriteLine(g.colorize(sess, "error", fmt.Sprintf("You're out of junk for %s!! You flail with your fists in desperation!", mod.Name))) g.playerAttackUnarmed(sess, p, currentMob) } else { p.AutotriggerMod = "" sess.WriteLine(g.colorize(sess, "error", "No autotrigger set! Set a module with the 'autotrigger' command.")) g.playerAttackUnarmed(sess, p, currentMob) } } else { sess.WriteLine(g.colorize(sess, "error", "\nNo autotrigger set! Set a module with the 'autotrigger' command.")) g.playerAttackUnarmed(sess, p, currentMob) } } else { if p.QueuedTrigger != "" { mod := GetMod(p.QueuedTrigger) p.QueuedTrigger = "" if mod != nil && g.hasJunkCost(p, mod) { g.scienceAttack(sess, p, currentMob, mod) } else { g.playerAttack(sess, p, currentMob) } } else { g.playerAttack(sess, p, currentMob) } } } if currentMob.HP <= 0 { g.endCombat(sess, p, currentMob) return false } return true }) if !isTask { g.Ticks.Subscribe(engine.ToTicks(mob.Speed), func() bool { cs := g.Combat.Get(p.Name) if cs == nil || !cs.Active { return false } currentMob := g.MobStore.GetInstance(cs.MobID) if currentMob == nil || currentMob.HP <= 0 { return false } g.mobAttack(sess, p, currentMob) if p.HP <= 0 { g.endCombat(sess, p, currentMob) return false } return true }) } } func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) { if g.processConsumeQueue(p, sess) { return } attackType, weaponType, attRoll, defRoll, maxHit := g.calculatePlayerAttackRoll(p, mob) if g.isSafespotted(p.Name) && attackType != "ranged" && attackType != "science" { if ss, ok := g.safespot.Get(p.Name); ok { g.forceLeaveSafespot(sess, p, &ss, "You leave your cover to close in for melee!") } } if combat.HitCheck(attRoll, defRoll) { g.applyPlayerHit(sess, p, mob, attackType, weaponType, maxHit) } else { g.applyPlayerMiss(sess, p, mob, weaponType) } } // resolvePlayerAttackType determines the player's attack type and main-hand // weapon type (and the weapon def, if any) from their equipped weapon, // defaulting to crush when unarmed or when the weapon specifies no type. func (g *Game) resolvePlayerAttackType(p *player.Player) (attackType string, weaponType item.EquipmentType, def *item.ItemDef) { attackType = combat.DefaultAttackType if itemID, ok := p.Equipment[item.SlotMainHand]; ok { if d, err := g.ItemStore.Load(itemID); err == nil { def = d weaponType = d.EquipmentType() if at := d.AttackType(); at != "" { attackType = at } else if weaponType == item.EquipRangedWeapon { attackType = combat.AttackRanged } else if weaponType == item.EquipScienceWeapon { attackType = combat.AttackScience } } } return } func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType item.EquipmentType, attRoll int, defRoll int, maxHit int) { attackType, weaponType, _ = g.resolvePlayerAttackType(p) totals := g.playerEquipBonuses(p) isRanged := weaponType == item.EquipRangedWeapon if isRanged { rangedBonus, _ := combat.RangedStyleBonus(string(p.AttackStyle)) equipAttack := totals.RangedAttack effectiveRanged := p.Level(player.Ranged) + g.techLevelBonus(p, "ranged") + g.buffLevelBonus(p, "ranged") attRoll = combat.AttackRoll(combat.PlayerEffective(effectiveRanged, rangedBonus), equipAttack) mobDefBonus := combat.SelectBonus(attackType, mob.StabDefense, mob.SlashDefense, mob.CrushDefense, mob.ScienceDefense, mob.RangedDefense) defRoll = combat.AttackRoll(combat.NPCEffective(mob.Defense), mobDefBonus) maxHit = combat.MaxHit(combat.PlayerEffective(effectiveRanged, rangedBonus), totals.RangedStrength) } else { attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) equipAttack := combat.SelectBonus(attackType, totals.StabAttack, totals.SlashAttack, totals.CrushAttack, totals.ScienceAttack, totals.RangedAttack) effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") attRoll = combat.AttackRoll(combat.PlayerEffective(effectiveAccuracy, attBonus), equipAttack) mobDefBonus := combat.SelectBonus(attackType, mob.StabDefense, mob.SlashDefense, mob.CrushDefense, mob.ScienceDefense, mob.RangedDefense) defRoll = combat.AttackRoll(combat.NPCEffective(mob.Defense), mobDefBonus) maxHit = combat.MaxHit(combat.PlayerEffective(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus), totals.StrengthBonus) } return } func (g *Game) playerAttackUnarmed(sess *net.Session, p *player.Player, mob *world.MobInstance) { if g.processConsumeQueue(p, sess) { return } attackType, _, attRoll, defRoll, maxHit := g.calculateUnarmedAttackRoll(p, mob) if combat.HitCheck(attRoll, defRoll) { g.applyPlayerHit(sess, p, mob, attackType, item.EquipMeleeWeapon, maxHit) } else { g.applyPlayerMiss(sess, p, mob, item.EquipMeleeWeapon) } } func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstance) (attackType string, weaponType item.EquipmentType, attRoll int, defRoll int, maxHit int) { attackType = "crush" weaponType = item.EquipMeleeWeapon attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle)) effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy") attRoll = combat.AttackRoll(combat.PlayerEffective(effectiveAccuracy, attBonus), 0) mobDefBonus := combat.SelectBonus(attackType, mob.StabDefense, mob.SlashDefense, mob.CrushDefense, mob.ScienceDefense, mob.RangedDefense) defRoll = combat.AttackRoll(combat.NPCEffective(mob.Defense), mobDefBonus) maxHit = combat.MaxHit(combat.PlayerEffective(p.Level(player.Strength)+g.techLevelBonus(p, "strength")+g.buffLevelBonus(p, "strength"), strBonus), 0) return } func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.MobInstance, attackType string, weaponType item.EquipmentType, maxHit int) { dmg := combat.RollDamage(maxHit) mob.HP -= dmg if mob.HP < 0 { mob.HP = 0 } if mob.FinishingBlow != "" && mob.HP <= 0 { mob.HP = 1 } if mob.HP < mob.MaxHP && mob.HP > 0 { mob.StartRegen() } if mob.FinishingBlow != "" && mob.HP == 1 { sess.WriteLine(g.colorize(sess, "warning", fmt.Sprintf("%s resists death! Use %s on it to finish it off.", mobDisplayName(mob, false), g.itemDisplayName(mob.FinishingBlow)))) } isRanged := weaponType == item.EquipRangedWeapon gains := g.awardCombatXP(sess, p, dmg, isRanged) if isRanged { g.consumeAmmo(sess, p) } if mob.IsTask() { g.writeTaskProgress(sess, p, mob, dmg, gains) } else { mobName := mobDisplayName(mob, true) attacker := mobDisplayNameCap(mob, true) prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", g.dmgDisplay(sess, dmg))) prefix = padCombatPrefix(prefix, fmt.Sprintf("You hit %s for > %d < damage.", mobName, 999), fmt.Sprintf("%s hits you for > %d < damage.", attacker, 999)) hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP) line := prefix + hpSuffix + g.formatXpDrop(sess, p, gains) sess.WriteLine(line) } if isRanged && p.AmmoQty <= 0 { sess.WriteLine("You've run out of ammo!") g.Combat.Leave(p.Name) } } func (g *Game) applyPlayerMiss(sess *net.Session, p *player.Player, mob *world.MobInstance, weaponType item.EquipmentType) { sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("You miss %s.", mobDisplayName(mob, true)))) if weaponType == item.EquipRangedWeapon { g.consumeAmmo(sess, p) if p.AmmoQty <= 0 { sess.WriteLine("You've run out of ammo!") g.Combat.Leave(p.Name) } } } func (g *Game) consumeAmmo(sess *net.Session, p *player.Player) { ammoID := p.Equipment[item.SlotAmmo] p.AmmoQty-- if p.AmmoQty <= 0 { delete(p.Equipment, item.SlotAmmo) p.AmmoQty = 0 } if ammoID != "" { if def, err := g.ItemStore.Load(ammoID); err == nil && def.Recoverable { if rand.Float64() < 0.80 { g.World.AddGroundItem(p.RoomID, def.ID, 1) } } } g.AccountStore.SaveCharacter(p) }