go_games_collection/wayfarer_combat.go

479 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}