package main import "math/rand" // WayfarerCombatOutcome — текущий/итоговый исход боя. type WayfarerCombatOutcome int const ( CombatOngoing WayfarerCombatOutcome = iota CombatWon // враг уничтожен CombatLost // корабль игрока уничтожен CombatFled // игрок сбежал CombatNegotiated // удалось договориться ) // WayfarerWeaponType — чем именно наносится урон в бою: обычный // лазер или один из двух видов ракет. type WayfarerWeaponType int const ( WayfarerWeaponLaser WayfarerWeaponType = iota // WayfarerWeaponMissileRegular — обычная ракета: слабее лазера, // пока цель прикрыта щитом, но сильнее лазера по голому корпусу. // От неё можно попытаться увернуться отдельным действием (см. // WayfarerCombatState.Dodge). WayfarerWeaponMissileRegular // WayfarerWeaponMissileHoming — самонаводящаяся ракета: та же // разница в уроне по щиту/корпусу, что и у обычной, но увернуться // от неё пилотированием нельзя — только обмануть тепловой // ловушкой (см. WayfarerCombatState.UseDecoy). WayfarerWeaponMissileHoming ) // WayfarerShieldFacing — одно из четырёх направлений, между которыми // распределяется энергия щита (и у игрока, и у врага — симметрично). type WayfarerShieldFacing int const ( WayfarerShieldFore WayfarerShieldFacing = iota // нос WayfarerShieldAft // хвост (корма) WayfarerShieldLeft // левый борт WayfarerShieldRight // правый борт ) // WayfarerApproach — с какого направления заходит враг для СЛЕДУЮЩЕЙ // атаки. Известно игроку заранее (см. NextApproach) и сужает набор // направлений щита, куда реально может прилететь удар — но не // называет конкретное направление напрямую, оставляя разведку // неполной. type WayfarerApproach int const ( WayfarerApproachFront WayfarerApproach = iota // строго спереди — удар точно в нос WayfarerApproachBehind // строго сзади — удар точно в хвост WayfarerApproachLeft // слева — удар в левый борт либо в хвост WayfarerApproachRight // справа — удар в правый борт либо в хвост WayfarerApproachVertical // сверху/снизу — удар в нос либо в хвост ) // wayfarerApproachCandidates — какие направления щита реально может // задеть атака при данном заходе врага (см. WayfarerApproach). var wayfarerApproachCandidates = map[WayfarerApproach][]WayfarerShieldFacing{ WayfarerApproachFront: {WayfarerShieldFore}, WayfarerApproachBehind: {WayfarerShieldAft}, WayfarerApproachLeft: {WayfarerShieldLeft, WayfarerShieldAft}, WayfarerApproachRight: {WayfarerShieldRight, WayfarerShieldAft}, WayfarerApproachVertical: {WayfarerShieldFore, WayfarerShieldAft}, } // Базовые вероятности успеха действий "бегство"/"переговоры"/"уклонение // от обычной ракеты" — в сумме с некоторой случайностью боя это // делает все три реальными, но не гарантированными альтернативами // прямому бою. const ( wayfarerFleeChance = 0.45 wayfarerNegotiateChance = 0.35 wayfarerDodgeChance = 0.5 ) // Ракеты (что обычные, что самонаводящиеся) наносят одинаково // изменённый урон относительно лазера: слабее, пока щит той стороны // ещё держится, и сильнее, если щита с этой стороны уже нет. const ( wayfarerMissileShieldMultiplier = 0.5 wayfarerMissileHullMultiplier = 1.5 ) // WayfarerCombatState — состояние одного пошагового боя. type WayfarerCombatState struct { EnemyNameKey string EnemyLaser int EnemyHull int EnemyMaxHull int EnemyWeapon WayfarerWeaponType // чем атакует враг — известно игроку с начала боя // EnemyShieldFore/Aft/Left/Right — щиты врага, устроены // симметрично щитам игрока (см. ниже). Игрок стреляет // прицельно — по конкретному направлению (Attack/AttackWithMissile). EnemyShieldFore int EnemyShieldAft int EnemyShieldLeft int EnemyShieldRight int EnemyMaxShield int PlayerLaser int PlayerHull int PlayerMaxHull int // PlayerShieldFore/Aft/Left/Right — энергия щита игрока по // четырём направлениям; в сумме не может превышать // PlayerMaxShield. Перераспределяется действием DistributeShields. PlayerShieldFore int PlayerShieldAft int PlayerShieldLeft int PlayerShieldRight int PlayerMaxShield int Negotiable bool // можно ли вообще пытаться договориться с этим противником Round int Outcome WayfarerCombatOutcome LastPlayerDamage int // урон, нанесённый игроком в последнем раунде (для лога) LastEnemyDamage int // урон, полученный игроком в последнем раунде LastHitFacing WayfarerShieldFacing // какое направление ЩИТА ИГРОКА приняло удар в последнем раунде LastPlayerHitFacing WayfarerShieldFacing // в какое направление ЩИТА ВРАГА стрелял игрок в последнем раунде // NextApproach — с какого направления зайдёт враг для СЛЕДУЮЩЕЙ // атаки; показывается игроку ДО выбора действия (см. // RollNextApproach), чтобы решение о распределении щита было // осмысленным, а не гаданием вслепую. NextApproach WayfarerApproach } // randomEnemyWeapon выбирает случайное вооружение врага при // генерации боевой встречи: чаще лазер, реже — один из видов ракет. func randomEnemyWeapon(rnd *rand.Rand) WayfarerWeaponType { roll := rnd.Float64() switch { case roll < 0.6: return WayfarerWeaponLaser case roll < 0.85: return WayfarerWeaponMissileRegular default: return WayfarerWeaponMissileHoming } } // wayfarerWeaponNameKey / wayfarerFacingNameKey / wayfarerApproachNameKey // — ключи переводов названий для отображения в бою. var wayfarerWeaponNameKey = map[WayfarerWeaponType]string{ WayfarerWeaponLaser: "wf.weapon.laser", WayfarerWeaponMissileRegular: "wf.weapon.missile_regular", WayfarerWeaponMissileHoming: "wf.weapon.missile_homing", } var wayfarerFacingNameKey = map[WayfarerShieldFacing]string{ WayfarerShieldFore: "wf.combat.facing.fore", WayfarerShieldAft: "wf.combat.facing.aft", WayfarerShieldLeft: "wf.combat.facing.left", WayfarerShieldRight: "wf.combat.facing.right", } var wayfarerApproachNameKey = map[WayfarerApproach]string{ WayfarerApproachFront: "wf.combat.approach.front", WayfarerApproachBehind: "wf.combat.approach.behind", WayfarerApproachLeft: "wf.combat.approach.left", WayfarerApproachRight: "wf.combat.approach.right", WayfarerApproachVertical: "wf.combat.approach.vertical", } // splitShieldFour раскладывает суммарный запас энергии щита на // четыре направления по умолчанию — поровну, остаток на нос. func splitShieldFour(total int) (fore, aft, left, right int) { quarter := total / 4 rest := total - quarter*3 return rest, quarter, quarter, quarter } // NewWayfarerCombat создаёт новое состояние боя. Щиты (и игрока, и // врага) по умолчанию распределяются по четырём направлениям // поровну (остаток — на нос). Сразу же определяется, с какого // направления зайдёт враг для самой первой атаки. func NewWayfarerCombat(enemyNameKey string, enemyHull, enemyLaser int, playerHull, playerMaxHull, playerShield, playerMaxShield, playerLaser int, negotiable bool, enemyWeapon WayfarerWeaponType, enemyShield int, rnd *rand.Rand) *WayfarerCombatState { c := &WayfarerCombatState{ EnemyNameKey: enemyNameKey, EnemyHull: enemyHull, EnemyMaxHull: enemyHull, EnemyLaser: enemyLaser, EnemyWeapon: enemyWeapon, EnemyMaxShield: enemyShield, PlayerHull: playerHull, PlayerMaxHull: playerMaxHull, PlayerMaxShield: playerMaxShield, PlayerLaser: playerLaser, Negotiable: negotiable, } c.PlayerShieldFore, c.PlayerShieldAft, c.PlayerShieldLeft, c.PlayerShieldRight = splitShieldFour(playerShield) c.EnemyShieldFore, c.EnemyShieldAft, c.EnemyShieldLeft, c.EnemyShieldRight = splitShieldFour(enemyShield) c.RollNextApproach(rnd) return c } // RollNextApproach выбирает случайное направление, с которого зайдёт // враг для СЛЕДУЮЩЕЙ атаки. func (c *WayfarerCombatState) RollNextApproach(rnd *rand.Rand) { c.NextApproach = WayfarerApproach(rnd.Intn(5)) } // TotalShield — суммарная энергия щита игрока по всем направлениям // (используется при синхронизации с кораблём после боя). func (c *WayfarerCombatState) TotalShield() int { return c.PlayerShieldFore + c.PlayerShieldAft + c.PlayerShieldLeft + c.PlayerShieldRight } // TotalEnemyShield — суммарная энергия щита врага по всем направлениям. func (c *WayfarerCombatState) TotalEnemyShield() int { return c.EnemyShieldFore + c.EnemyShieldAft + c.EnemyShieldLeft + c.EnemyShieldRight } func (c *WayfarerCombatState) playerShieldFor(facing WayfarerShieldFacing) *int { switch facing { case WayfarerShieldFore: return &c.PlayerShieldFore case WayfarerShieldAft: return &c.PlayerShieldAft case WayfarerShieldLeft: return &c.PlayerShieldLeft default: return &c.PlayerShieldRight } } func (c *WayfarerCombatState) enemyShieldFor(facing WayfarerShieldFacing) *int { switch facing { case WayfarerShieldFore: return &c.EnemyShieldFore case WayfarerShieldAft: return &c.EnemyShieldAft case WayfarerShieldLeft: return &c.EnemyShieldLeft default: return &c.EnemyShieldRight } } // rollDamage — базовый урон laser ± 30% случайного разброса. func rollDamage(laser int, rnd *rand.Rand) int { factor := 0.7 + rnd.Float64()*0.6 dmg := int(float64(laser) * factor) if dmg < 1 { dmg = 1 } return dmg } // applyDamage наносит урон dmg сначала по щиту, затем по корпусу // (щит поглощает урон первым, как в большинстве похожих игр). Для // обычного лазера — без изменений множителя. func applyDamage(shield, hull *int, dmg int) { if *shield > 0 { absorbed := dmg if absorbed > *shield { absorbed = *shield } *shield -= absorbed dmg -= absorbed } if dmg > 0 { *hull -= dmg if *hull < 0 { *hull = 0 } } } // applyWeaponDamage наносит dmg с учётом типа оружия: лазер — как и // раньше (applyDamage), ракета (любая) — слабее, пока щит с этой // стороны ещё держится (только часть dmg, по // wayfarerMissileShieldMultiplier, уходит на щит — до корпуса в тот // же раунд не доходит), и сильнее (dmg*wayfarerMissileHullMultiplier), // если щита с этой стороны уже нет вовсе. func applyWeaponDamage(shield, hull *int, dmg int, weapon WayfarerWeaponType) { if weapon == WayfarerWeaponLaser { applyDamage(shield, hull, dmg) return } if *shield > 0 { reduced := int(float64(dmg) * wayfarerMissileShieldMultiplier) if reduced < 1 { reduced = 1 } if reduced > *shield { reduced = *shield } *shield -= reduced return } boosted := int(float64(dmg) * wayfarerMissileHullMultiplier) if boosted < 1 { boosted = 1 } *hull -= boosted if *hull < 0 { *hull = 0 } } // enemyStrikes разыгрывает ответный удар врага: направление удара // выбирается случайно, но только среди вариантов, допустимых текущим // NextApproach (см. wayfarerApproachCandidates) — не полностью // произвольно среди всех четырёх. После удара сразу готовится // подсказка о заходе для следующего раунда. func (c *WayfarerCombatState) enemyStrikes(rnd *rand.Rand) { candidates := wayfarerApproachCandidates[c.NextApproach] facing := candidates[rnd.Intn(len(candidates))] c.LastHitFacing = facing dmg := rollDamage(c.EnemyLaser, rnd) applyWeaponDamage(c.playerShieldFor(facing), &c.PlayerHull, dmg, c.EnemyWeapon) c.LastEnemyDamage = dmg if c.PlayerHull == 0 { c.Outcome = CombatLost } if c.Outcome == CombatOngoing { c.RollNextApproach(rnd) } } // Attack — игрок атакует лазером выбранное направление щита врага; // если враг не уничтожен, враг атакует в ответ в тот же раунд. func (c *WayfarerCombatState) Attack(facing WayfarerShieldFacing, rnd *rand.Rand) { if c.Outcome != CombatOngoing { return } c.Round++ dmg := rollDamage(c.PlayerLaser, rnd) c.LastPlayerHitFacing = facing applyWeaponDamage(c.enemyShieldFor(facing), &c.EnemyHull, dmg, WayfarerWeaponLaser) c.LastPlayerDamage = dmg if c.EnemyHull == 0 { c.Outcome = CombatWon c.LastEnemyDamage = 0 return } c.enemyStrikes(rnd) } // AttackWithMissile — игрок стреляет ракетой в выбранное направление // щита врага вместо лазера, расходуя один боеприпас соответствующего // типа с корабля ship. func (c *WayfarerCombatState) AttackWithMissile(ship *WayfarerShip, weapon WayfarerWeaponType, facing WayfarerShieldFacing, rnd *rand.Rand) error { if c.Outcome != CombatOngoing { return nil } switch weapon { case WayfarerWeaponMissileRegular: if ship.RegularMissiles <= 0 { return errWayfarerNoMissiles } ship.RegularMissiles-- case WayfarerWeaponMissileHoming: if ship.HomingMissiles <= 0 { return errWayfarerNoMissiles } ship.HomingMissiles-- default: return errWayfarerInvalidWeapon } c.Round++ dmg := rollDamage(c.PlayerLaser, rnd) c.LastPlayerHitFacing = facing applyWeaponDamage(c.enemyShieldFor(facing), &c.EnemyHull, dmg, weapon) c.LastPlayerDamage = dmg if c.EnemyHull == 0 { c.Outcome = CombatWon c.LastEnemyDamage = 0 return nil } c.enemyStrikes(rnd) return nil } // Dodge — попытка увернуться от ОБЫЧНОЙ ракеты вместо атаки в этот // раунд (тратит ход, как и Flee/Negotiate). Имеет смысл (и вообще // доступно) только если EnemyWeapon == WayfarerWeaponMissileRegular — // от лазера или самонаводящейся ракеты так уклониться нельзя. func (c *WayfarerCombatState) Dodge(rnd *rand.Rand) bool { if c.Outcome != CombatOngoing || c.EnemyWeapon != WayfarerWeaponMissileRegular { return false } c.Round++ if rnd.Float64() < wayfarerDodgeChance { c.LastEnemyDamage = 0 return true } c.enemyStrikes(rnd) return false } // UseDecoy — расходует одну тепловую ловушку с корабля ship, чтобы // гарантированно обмануть САМОНАВОДЯЩУЮСЯ ракету в этом раунде // (тратит ход). Единственная защита от неё — обычное уклонение // (Dodge) на неё не действует. func (c *WayfarerCombatState) UseDecoy(ship *WayfarerShip) bool { if c.Outcome != CombatOngoing || c.EnemyWeapon != WayfarerWeaponMissileHoming || ship.Decoys <= 0 { return false } ship.Decoys-- c.Round++ c.LastEnemyDamage = 0 return true } // DistributeShields перераспределяет энергию щита игрока по четырём // направлениям (нос/хвост/левый борт/правый борт); суммарно не может // превышать PlayerMaxShield. Считается отдельным действием — // расходует ход, после чего враг всё равно атакует уже по новому // распределению. func (c *WayfarerCombatState) DistributeShields(fore, aft, left, right int, rnd *rand.Rand) error { if c.Outcome != CombatOngoing { return nil } if fore < 0 || aft < 0 || left < 0 || right < 0 || fore+aft+left+right > c.PlayerMaxShield { return errWayfarerInvalidShieldDistribution } c.PlayerShieldFore = fore c.PlayerShieldAft = aft c.PlayerShieldLeft = left c.PlayerShieldRight = right c.Round++ c.enemyStrikes(rnd) return nil } // Flee — попытка сбежать из боя. При неудаче враг наносит полный // удар (без ответа игрока в этом раунде — сосредоточились на // бегстве, не на атаке). func (c *WayfarerCombatState) Flee(rnd *rand.Rand) bool { if c.Outcome != CombatOngoing { return false } c.Round++ if rnd.Float64() < wayfarerFleeChance { c.Outcome = CombatFled c.LastEnemyDamage = 0 return true } c.enemyStrikes(rnd) return false } // Negotiate — попытка договориться (доступна только если // c.Negotiable). При неудаче враг наносит полный удар, как и при // неудачном бегстве. func (c *WayfarerCombatState) Negotiate(rnd *rand.Rand) bool { if c.Outcome != CombatOngoing || !c.Negotiable { return false } c.Round++ if rnd.Float64() < wayfarerNegotiateChance { c.Outcome = CombatNegotiated c.LastEnemyDamage = 0 return true } c.enemyStrikes(rnd) return false }