479 lines
19 KiB
Go
479 lines
19 KiB
Go
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
|
||
}
|