game

package
v1.4.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RelicGoldBonus  = 60
	RelicLivesBonus = 1
)

Campaign relic bonuses are also used when describing the reward in the UI.

View Source
const (
	StartingGold    = 220
	StartingLives   = 20
	MaxWaves        = 20
	SellRefund      = 0.7
	ProjectileSpeed = 14.0
	EarlyBonusBase  = 8
)
View Source
const (
	ForgeHalfWidth = .48
	HookRecovery   = 2.5
	MineArmDelay   = .6
)
View Source
const AllTowersMask uint32 = (1 << TowerCount) - 1
View Source
const ChainFalloff = 0.6
View Source
const ChainRange = 2.6
View Source
const MainTowersMask uint32 = (1 << TowerRuneforge) - 1
View Source
const MazeW, MazeH = 45, 13

MazeW and MazeH are the fixed playfield size used for procedural mazes.

View Source
const SampleStep = 0.25

Variables

View Source
var EnemySpecs = [EnemyCount]EnemySpec{
	{Name: copytext.Text("enemies.squire.name"), Short: 'o', HP: 16, Speed: 3.2, Bounty: 2},
	{Name: copytext.Text("enemies.rogue.name"), Short: 'r', HP: 24, Speed: 2.6, Bounty: 5, Evasion: 0.5},
	{Name: copytext.Text("enemies.mercenary.name"), Short: 'g', HP: 65, Speed: 1.6, Bounty: 8},
	{Name: copytext.Text("enemies.paladin.name"), Short: 't', HP: 240, Speed: 1.05, Bounty: 20, Plate: 0.75, ArrowPlate: 0.4},
	{Name: copytext.Text("enemies.necromancer.name"), Short: 's', HP: 110, Speed: 1.4, Bounty: 12, SplitN: 2, SplitHP: 0.5},
	{Name: copytext.Text("enemies.player.name"), Short: 'B', HP: 1600, Speed: 0.8, Bounty: 140, Lives: 6},
	{Name: copytext.Text("enemies.wizard.name"), Short: 'w', HP: 9, Speed: 2.3, Bounty: 3},
	{Name: copytext.Text("enemies.centurion.name"), Short: 'D', HP: 180, Speed: 1.15, Bounty: 24, Ward: 0.8},
}
View Source
var TowerSpecs = [TowerCount]TowerSpec{
	{
		Name: copytext.Text("defenders.gunner.name"), Short: 'G',
		Cost: [3]int{50, 45, 70}, Dmg: [3]float64{9, 14, 22},
		Range: [3]float64{3.0, 3.4, 3.8}, RoT: [3]float64{1.3, 1.5, 1.8},
	},
	{
		Name: copytext.Text("defenders.cannonier.name"), Short: 'C',
		Cost: [3]int{100, 90, 140}, Dmg: [3]float64{24, 40, 64},
		Range: [3]float64{2.8, 3.2, 3.6}, RoT: [3]float64{0.55, 0.6, 0.65},
		Splash: 1.3,
	},
	{
		Name: copytext.Text("defenders.frost.name"), Short: 'F',
		Cost: [3]int{75, 65, 100}, Dmg: [3]float64{5, 8, 12},
		Range: [3]float64{2.6, 2.9, 3.2}, RoT: [3]float64{0.9, 1.0, 1.1},
		SlowPct: 0.45, SlowDur: 1.6,
	},
	{
		Name: copytext.Text("defenders.ranger.name"), Short: 'S',
		Cost: [3]int{150, 130, 200}, Dmg: [3]float64{70, 120, 190},
		Range: [3]float64{6.0, 6.6, 7.2}, RoT: [3]float64{0.35, 0.4, 0.45},
	},
	{
		Name: copytext.Text("defenders.lightning.name"), Short: 'T',
		Cost: [3]int{200, 160, 240}, Dmg: [3]float64{30, 48, 75},
		Range: [3]float64{3.2, 3.5, 3.9}, RoT: [3]float64{0.8, 0.9, 1.0},
		Chain: [3]int{2, 3, 4},
	},
	{
		Name: copytext.Text("defenders.trebuchet.name"), Short: 'M',
		Cost: [3]int{250, 220, 320}, Dmg: [3]float64{70, 105, 160},
		Range: [3]float64{5.0, 5.5, 6.0}, RoT: [3]float64{0.45, 0.5, 0.55},
		Splash: 2.2,
	},
	{
		Name: copytext.Text("defenders.gnolls.name"), Short: 'L',
		Cost: [3]int{80, 70, 110}, Dmg: [3]float64{6, 9, 14},
		Range: [3]float64{2.2, 2.5, 2.8}, RoT: [3]float64{3.0, 3.5, 4.0},
	},
	{
		Name: copytext.Text("defenders.runeforge.name"), Short: 'R',
		Cost: [3]int{180, 140, 220}, Dmg: [3]float64{22, 34, 50},
		Range: [3]float64{7, 8, 9}, RoT: [3]float64{1, 1, 1},
	},
	{
		Name: copytext.Text("defenders.hookmaster.name"), Short: 'H',
		Cost: [3]int{140, 100, 160}, Dmg: [3]float64{8, 12, 18},
		Range: [3]float64{3.0, 3.4, 3.8}, RoT: [3]float64{.2, .23, .26},
		Pull: [3]float64{2, 2.6, 3.2},
	},
	{
		Name: copytext.Text("defenders.sappers.name"), Short: 'P',
		Cost: [3]int{125, 100, 160}, Dmg: [3]float64{90, 145, 220},
		Range: [3]float64{2.8, 3.2, 3.6}, RoT: [3]float64{.4, .45, .5},
		Splash: 1.1, MineCap: [3]int{3, 4, 5},
	},
	{
		Name: copytext.Text("defenders.witch.name"), Short: 'W',
		Cost: [3]int{160, 120, 180}, Dmg: [3]float64{0, 0, 0},
		Range: [3]float64{3.5, 3.9, 4.3}, RoT: [3]float64{.5, .5, .5},
		Mark: [3]float64{.25, .35, .45}, MarkDur: [3]float64{3, 3.5, 4},
	},
}

Functions

func AutoWaveDelayFor

func AutoWaveDelayFor(w int) float64

AutoWaveDelayFor returns the break after completing wave w (the delay before wave w+1 auto-starts). Tapers 11s -> ~4.5s so tempo rises as the game escalates, in place of a fixed 12s.

func BossMod

func BossMod(wave int) (hp, speed float64)

BossMod tweaks boss stats per wave: returns hp and speed multipliers. Bosses appear on wave 15 (debut), 18 (stronger), and 20 (finale, two of them). The finale bosses are the run's climax.

func EarlyBonus

func EarlyBonus(prevWave int) int

EarlyBonus is the gold paid for starting the next wave before the inter-wave timer ends. Modest on purpose: early-starting is a tempo choice, not a dominant income source.

func ForgeContains

func ForgeContains(start, end, p Pos) bool

func HPScale

func HPScale(wave int) float64

func HookBudget

func HookBudget(k EnemyKind) float64

func LevelNames

func LevelNames() []string

func SpeedScale

func SpeedScale(wave int) float64

func TacticalBrief

func TacticalBrief(m *Map) string

func WaveBonus

func WaveBonus(wave int) int

WaveBonus is the gold paid for clearing a wave. Front-loaded enough that even a tiny wave funds progress (BTD3's $99+round shape, scaled down).

func WavePreview

func WavePreview(wave int) string

func WavePreviewFor

func WavePreviewFor(m *Map, wave int) string

func WaveTelegraph

func WaveTelegraph(wave int) string

WaveTelegraph returns a short pre-wave message for the upcoming wave, shown during the break before it (BTD3's pre-round comment habit). It telegraphs new enemy types and the boss waves. Empty for unremarkable waves.

func WaveTelegraphFor

func WaveTelegraphFor(m *Map, wave int) string

func WaveTheme

func WaveTheme(wave int) string

WaveTheme returns the theme label for a wave (for the HUD).

Types

type Autoplay

type Autoplay struct {
	// contains filtered or unexported fields
}

func NewAutoplay

func NewAutoplay(s *State) *Autoplay

func (*Autoplay) Tick

func (a *Autoplay) Tick()

type Beam

type Beam struct {
	From []Pos
	TTL  float64
	Max  float64
	Kind TowerKind
}

type CellKind

type CellKind int
const (
	CellWall CellKind = iota
	CellGrass
	CellPath
)

type Difficulty

type Difficulty int
const (
	Normal Difficulty = iota
	Easy
	Hard
)

func (Difficulty) Gold

func (d Difficulty) Gold() int

func (Difficulty) HPMul

func (d Difficulty) HPMul() float64

func (Difficulty) Lives

func (d Difficulty) Lives() int

type Encounter

type Encounter string

Encounter is attached only by authored level loaders. A zero encounter keeps the original wave schedule, including every procedural maze.

const (
	EncounterRotunda Encounter = "rotunda"
	EncounterRift    Encounter = "rift"
	EncounterHalls   Encounter = "halls"
	EncounterGarden  Encounter = "garden"
	EncounterHeart   Encounter = "heart"
)

type Enemy

type Enemy struct {
	ID             int
	Kind           EnemyKind
	HP             float64
	MaxHP          float64
	Prog           float64
	PrevProg       float64
	Speed          float64
	Pos            Pos
	SlowUntil      float64
	SlowFactor     float64
	Bounty         int
	Lives          int
	Dead           bool
	Leaked         bool
	HitTTL         float64 // >0 while the enemy flashes from a recent hit
	PulledDistance float64
	HookUntil      float64 // shared recovery prevents several Ogres trapping a fighter
	HexUntil       float64
	HexBonus       float64
}

func (*Enemy) Damage

func (e *Enemy) Damage(d float64) bool

func (*Enemy) DamageMultiplier

func (e *Enemy) DamageMultiplier(k TowerKind, time float64, splash bool) float64

DamageMultiplier keeps counters deterministic. Evasion represents glancing shots, not random misses; frost disables it only for the active slow window. Splash is a property of the actual impact, not merely the tower's name.

func (*Enemy) Slowed

func (e *Enemy) Slowed(time float64) bool

type EnemyKind

type EnemyKind int
const (
	EnemyMinion EnemyKind = iota
	EnemyRunner
	EnemyGrunt
	EnemyTank
	EnemySplitter
	EnemyBoss
	EnemyWisp
	EnemyShield
	EnemyCount
)

type EnemySpec

type EnemySpec struct {
	Name       string
	Short      rune
	HP         float64
	Speed      float64
	Bounty     int
	Lives      int
	SplitN     int
	SplitHP    float64
	Plate      float64 // reduction against small shot
	ArrowPlate float64 // reduction against Ranger arrows
	Ward       float64 // reduction against frost and lightning
	Evasion    float64 // reduction against ordinary shots while not slowed
}

type Facing

type Facing int
const (
	FacingEast Facing = iota
	FacingSouth
	FacingWest
	FacingNorth
)

func (Facing) Arrow

func (d Facing) Arrow() rune

func (Facing) Next

func (d Facing) Next() Facing

func (Facing) Vector

func (d Facing) Vector() Vec

type Fx

type Fx struct {
	Pos   Pos
	TTL   float64
	Max   float64
	R     rune
	Color int
	Ring  float64 // if >0, render as an expanding ring of this radius (map units)
}

type GameStatus

type GameStatus int
const (
	StatusRunning GameStatus = iota
	StatusVictory
	StatusDefeat
)

type Map

type Map struct {
	W, H      int
	Encounter Encounter
	Cell      []CellKind

	Path     []Vec
	Dist     []float64
	TotalLen float64
	Spawn    Vec
	Exit     Vec
	PathSet  map[Vec]bool
	Samples  []Pos

	// HPMul is a per-map enemy health multiplier used to tune difficulty.
	HPMul float64
	// GoldMul scales starting gold and income, allowing spacious floors to
	// support broader defenses without changing tower prices. Zero means 1.
	GoldMul float64
}

func GenerateMap

func GenerateMap(seed int64, w, h int) (*Map, error)

GenerateMap builds a perfect maze and uses its unique route from the left edge to the right edge as the enemy path. All other open cells are buildable.

func LoadBoss

func LoadBoss() (*Map, error)

LoadBoss loads the heart chamber: the endgame defense behind the unsealed heart. It is deliberately not in LevelNames, so the level select never lists it — the lair unseals it.

func LoadLevel

func LoadLevel(name string) (*Map, error)

func LoadMap

func LoadMap(w, h int, rows []string) (*Map, error)

func MazeFromSeed

func MazeFromSeed(seed int64) (*Map, error)

MazeFromSeed builds the standard-size procedural maze for seed. It is the single place that knows mazes are MazeW×MazeH.

func (*Map) At

func (m *Map) At(v Vec) CellKind

func (*Map) InBounds

func (m *Map) InBounds(v Vec) bool

func (*Map) IsPath

func (m *Map) IsPath(v Vec) bool

func (*Map) PointAt

func (m *Map) PointAt(d float64) Pos

func (*Map) SamplePoints

func (m *Map) SamplePoints() []Pos

func (*Map) ScaleGold

func (m *Map) ScaleGold(gold int) int

ScaleGold applies the floor's economy to a gold amount, rounded to a coin.

type Mine

type Mine struct {
	Owner  int
	Prog   float64
	Pos    Pos
	ArmAt  float64
	Damage float64
	Radius float64
}

type Pos

type Pos struct {
	X, Y float64
}

func ForgeEnd

func ForgeEnd(m *Map, cell Vec, facing Facing, reach float64) Pos

ForgeEnd is shared by the simulation and the placement preview. Walls do not block attacks in TERMTD; the ray ends at its range or the map boundary.

func (Pos) Center

func (p Pos) Center() Vec

func (Pos) Dist

func (p Pos) Dist(q Pos) float64

type Projectile

type Projectile struct {
	ID      int
	Kind    TowerKind
	Pos     Pos
	Target  int
	LastPos Pos
	Dmg     float64
	Splash  float64
	SlowPct float64
	SlowDur float64
	Dead    bool
}

type Recruitment

type Recruitment struct {
	Kind      TowerKind
	AfterWave int
}

func RecruitmentOrder

func RecruitmentOrder(stage int) []Recruitment

A recruit arrives during the preceding break, before its first useful fight.

type SimResult

type SimResult struct {
	Won    bool
	Wave   int
	Lives  int
	Gold   int
	Kills  int
	Leaks  int
	Towers int
	Time   float64
	Level  string
}

func RunAutoplay

func RunAutoplay(m *Map, level string) SimResult

func RunAutoplayDiff

func RunAutoplayDiff(m *Map, level string, diff Difficulty) SimResult

type SpawnEntry

type SpawnEntry struct {
	At   float64
	Kind EnemyKind
}

func BuildWave

func BuildWave(wave int) []SpawnEntry

func BuildWaveFor

func BuildWaveFor(m *Map, wave int) []SpawnEntry

type State

type State struct {
	TrainingStage  int
	UnlockedTowers uint32
	Lesson         TowerKind
	LessonPending  bool
	Map            *Map
	Time           float64
	Diff           Difficulty

	Gold   int
	Lives  int
	Wave   int
	Status GameStatus

	// SeenEnemies also remembers enemies killed on their spawning tick.
	SeenEnemies [EnemyCount]bool
	Enemies     []*Enemy
	Towers      []*Tower
	Projectiles []*Projectile
	Beams       []*Beam
	Mines       []*Mine
	Fx          []*Fx
	LeakFlash   float64

	SpawnQueue []SpawnEntry
	SpawnIdx   int
	NextWaveAt float64
	WaveActive bool
	WaveStart  float64

	Score      int
	TotalKills int
	TotalLeaks int
	Combo      int
	MaxCombo   int

	NextID int

	// UI hints (headless-safe)
	Placing   TowerKind
	PlacingOn bool
	Cursor    Vec
	Selected  int
}

func NewState

func NewState(m *Map) *State

The simulation is fully deterministic: a run is determined entirely by (map, difficulty). There is no in-engine randomness, so no seed is kept here — the only seed in the game is the maze generator's, which happens before a State exists.

func NewStateDiff

func NewStateDiff(m *Map, diff Difficulty) *State

func (*State) Aim

func (s *State) Aim(t *Tower, facing Facing)

func (*State) Build

func (s *State) Build(v Vec, k TowerKind) *Tower

func (*State) CanBuild

func (s *State) CanBuild(v Vec, k TowerKind) bool

func (*State) CanBuildSite added in v1.4.1

func (s *State) CanBuildSite(v Vec, k TowerKind) bool

CanBuildSite checks placement rules independently of the player's gold.

func (*State) ConfigureTraining

func (s *State) ConfigureTraining(stage int, unlocked uint32)

func (*State) CycleTarget

func (s *State) CycleTarget(v Vec) (*Tower, TargetMode)

CycleTarget cycles the targeting mode of the tower at v, if any. Returns the tower (possibly nil) and its new mode.

func (*State) Enemy

func (s *State) Enemy(id int) *Enemy

func (*State) MineSites

func (s *State) MineSites(t *Tower) []Mine

MineSites are actual route positions, rather than nearby floor decoration. Exposing the candidates also lets selection previews and builders use the same legal placement geometry as the simulation.

func (*State) Sell

func (s *State) Sell(t *Tower) int

func (*State) SpecialistPlaced

func (s *State) SpecialistPlaced(k TowerKind) bool

SpecialistPlaced reports the one-per-kind limit for the current level. Selling a specialist makes its slot available again.

func (*State) StartWave

func (s *State) StartWave() int

StartWave begins the next wave. If it was started before the auto timer expired, the player gets an early-start bonus. Returns bonus gold.

The siege itself — the very first wave — will not open until the player has committed to the defense: at least one tower must be placed before it may start, whether by the auto timer or the early-start key.

func (*State) Step

func (s *State) Step(dt float64)

func (*State) TargetFor

func (s *State) TargetFor(t *Tower) *Enemy

TargetFor exposes the same priority used for a defender's next shot.

func (*State) Tower

func (s *State) Tower(id int) *Tower

func (*State) TowerAt

func (s *State) TowerAt(v Vec) *Tower

func (*State) TowerAvailable

func (s *State) TowerAvailable(k TowerKind) bool

func (*State) Upgrade

func (s *State) Upgrade(t *Tower) bool

func (*State) UpgradeCost

func (s *State) UpgradeCost(t *Tower) int

type TargetMode

type TargetMode int

TargetMode controls which in-range enemy a tower attacks.

const (
	TargetFirst     TargetMode = iota // furthest along the path
	TargetStrongest                   // highest current HP
	TargetClosest                     // nearest to the tower
	TargetModeCount
)

func (TargetMode) Name

func (m TargetMode) Name() string

func (TargetMode) Next

func (m TargetMode) Next() TargetMode

func (TargetMode) Short

func (m TargetMode) Short() string

type Tower

type Tower struct {
	ID         int
	Kind       TowerKind
	Level      int
	Cell       Vec
	CD         float64
	Invested   int
	Flash      float64
	FlashTo    Pos
	TargetMode TargetMode
	Facing     Facing
	BeamEnd    Pos
	Active     bool
	// contains filtered or unexported fields
}

func (*Tower) Dmg

func (t *Tower) Dmg() float64

func (*Tower) Pos

func (t *Tower) Pos() Pos

func (*Tower) Range

func (t *Tower) Range() float64

func (*Tower) RoT

func (t *Tower) RoT() float64

func (*Tower) Spec

func (t *Tower) Spec() *TowerSpec

type TowerKind

type TowerKind int
const (
	TowerGunner TowerKind = iota
	TowerCannon
	TowerFrost
	TowerSniper
	TowerTesla
	TowerMortar
	TowerFlak
	TowerRuneforge
	TowerHookmaster
	TowerSappers
	TowerWitch
	TowerCount
)

func (TowerKind) Valid

func (k TowerKind) Valid() bool

type TowerSpec

type TowerSpec struct {
	Name    string
	Short   rune
	Cost    [3]int
	Dmg     [3]float64
	Range   [3]float64
	RoT     [3]float64
	Splash  float64
	SlowPct float64
	SlowDur float64
	Chain   [3]int
	Pull    [3]float64
	Mark    [3]float64
	MarkDur [3]float64
	MineCap [3]int
}

type Vec

type Vec struct {
	X, Y int
}

func (Vec) Center

func (c Vec) Center() Pos

func (Vec) Man

func (a Vec) Man(b Vec) int

func (Vec) Sub

func (a Vec) Sub(b Vec) Vec

type WaveDef

type WaveDef struct {
	Theme  string
	Groups []spawnGroup
	Bosses int
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL