package game import ( "fmt" "strings" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) // scienceAttack resolves a player science-mod attack against a mob. // // Unlike melee/ranged (which roll the player's accuracy vs the mob's Defense), // science uses a symmetric science-vs-science model: both the attack roll and // the mob's defense roll use ScienceEffective, keyed off the player's Science // level and the mob's Science stat (plus ScienceDefense) respectively — the // mob's Defense stat is not involved. Science has no attack styles, so no style // bonus applies. func (g *Game) scienceAttack(sess *net.Session, p *player.Player, mob *world.MobInstance, mod *ModDef) bool { if p.Level(player.Science) < mod.Level { sess.WriteLine(fmt.Sprintf("You need level %d science to trigger %s.", mod.Level, mod.Name)) return false } if g.processConsumeQueue(p, sess) { return true } g.consumeJunkCost(p, mod) equipSciBonus := g.totalScienceAttack(p) effectiveScience := p.Level(player.Science) + g.techLevelBonus(p, "science") + g.buffLevelBonus(p, "science") attRoll := combat.AttackRoll(combat.ScienceEffective(effectiveScience), equipSciBonus) defRoll := combat.AttackRoll(combat.ScienceEffective(mob.Science), mob.ScienceDefense) if mob.Weakness == mod.Element && mob.WeaknessPercent > 0 { attRoll = int(float64(attRoll) * (1.0 + float64(mob.WeaknessPercent)/100.0)) } if combat.HitCheck(attRoll, defRoll) { totals := g.playerEquipBonuses(p) maxHit := mod.MaxHit if totals.ScienceDamage > 0 { maxHit = int(float64(maxHit) * (1.0 + float64(totals.ScienceDamage)/100.0)) } // Weakness is an additive bonus computed off the mod's base MaxHit (not // the science-damage-boosted maxHit), so equipment and weakness stack // additively. With no science-damage gear this equals a ×(1+pct) boost, // matching the multiplicative weakness applied to the attack roll above. if mob.Weakness == mod.Element && mob.WeaknessPercent > 0 { maxHit += int(float64(mod.MaxHit) * float64(mob.WeaknessPercent) / 100.0) } if maxHit < 1 { maxHit = 1 } dmg := combat.RollDamage(maxHit) if dmg < 0 { dmg = 0 } mob.HP -= dmg if mob.HP < 0 { mob.HP = 0 } if mob.HP < mob.MaxHP && mob.HP > 0 { mob.StartRegen() } sciXP := mod.BaseXP hpXP := mod.BaseXP / 3 var gains []xpGain g.awardSkillXP(sess, p, player.Science, sciXP) gains = append(gains, xpGain{string(player.Science), sciXP}) g.awardSkillXP(sess, p, player.Hitpoints, hpXP) gains = append(gains, xpGain{string(player.Hitpoints), hpXP}) g.AccountStore.SaveCharacter(p) mobName := mobDisplayName(mob, true) prefix := fmt.Sprintf("%s hits %s for %s damage.", g.colorize(sess, "science_mod", mod.Name), g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", fmt.Sprint(dmg))) 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) } else { sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s fails to connect.", mod.Name))) } return true } func (g *Game) findInventoryItem(p *player.Player, input string) (int, *player.InventorySlot) { lower := strings.ToLower(strings.TrimSpace(input)) for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil { continue } if def.MatchesName(lower) { return i, slot } } return -1, nil }