jengine-td
Tower defense: a maze the player builds, waves that path around it with the engine’s tilemap A*, towers and upgrades; or fixed roads with soldiers, a hero and spells.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-td |
| Version | 0.1.0 |
| Entry point | jengine_td::TdPackage |
| Script namespace | Td |
| Listed by | projects/arcade-classics, projects/strategy-classics |
Using it
Section titled “Using it”List it in the top-level "packages" array of project.jengine:
{ "packages": [ { "name": "jengine-td", "source": { "type": "path", "path": "../../packages/jengine-td" } } ]}Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the
editor’s Rebuild viewport banner). Native and web builds link it on their own.
A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field
for a package component and keeps it as inert custom data.
A script can check that the package is linked with if Td == nil then return end.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
td_game | TdGame | package only: needs a v2 scene |
td_road | TdRoad | package only: needs a v2 scene |
Script commands
Section titled “Script commands”Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.
| Call | Flat name |
|---|---|
Td.new_game(e) | td_new_game |
Td.build(e, col, row, tower) | td_build |
Td.upgrade(e, col, row) | td_upgrade |
Td.sell(e, col, row) | td_sell |
Td.target(e, col, row, mode) | td_target |
Td.wave(e) | td_wave |
Td.speed(e, x) | td_speed |
Td.auto(e, on, delay) | td_auto |
Td.road_start(e, level, hero, seed) | td_road_start |
Td.road_stop(e) | td_road_stop |
Td.road_build(e, spot, family) | td_road_build |
Td.road_upgrade(e, spot, choice) | td_road_upgrade |
Td.road_sell(e, spot) | td_road_sell |
Td.road_rally(e, spot, x, y) | td_road_rally |
Td.road_hero(e, x, y) | td_road_hero |
Td.road_cast(e, spell, x, y) | td_road_cast |
Td.road_call(e) | td_road_call |
Td.road_speed(e, x) | td_road_speed |
Td.road_pause(e, on) | td_road_pause |
Td.road_auto(e, on, delay) | td_road_auto |
Td.road_advise(e, on) | td_road_advise |
Scenes that use it
Section titled “Scenes that use it”projects/arcade-classics/assets/scenes/keep.sceneprojects/strategy-classics/assets/scenes/bastion.scene
From packages/jengine-td/README.md.
Tower defense, two ways: waves that walk a maze the player builds
(td_game), and Kingdom Rush’s model - fixed roads, build spots beside
them, soldiers who hold the road, a hero and spells (td_road, “The road
mode” below).
The maze is the point. A tower is built on the map itself, and the creeps
walk the shortest way round it - found with the engine’s own tilemap A*
(jengine_core::tilemap::find_path_on_layer). A tower that would seal the
way off is refused, so there is always a road; everything else you build
just makes the walk longer, and every extra second is another second under
fire.
Used by projects/arcade-classics (Bramble Keep, the maze) and
projects/strategy-classics (Hollow Bastion, the roads).
| Module | What it is |
|---|---|
content | Towers, creeps and waves, read from the project’s data tables. A new tower is a row. |
sim | The game: the maze, the walk, the shooting, the money, the waves. |
bot | A player that builds and upgrades by itself: attract modes, and the balance test. |
road | The road mode, with its own content, sim and bot. |
The map
Section titled “The map”The component goes on the entity that carries the tilemap. Whatever is
already in the named layer is the map’s own rock - ground a tower cannot be
built on and creeps have to walk around:
"td_game": { "tables": { "towers": "data/towers", "creeps": "data/creeps", "waves": "data/waves" }, "spawn": [0, 7], "goal": [27, 7], "blocked_layer": "rock", "start_gold": 260, "start_lives": 20, "build_time": 20.0 }Cell positions in what it publishes are tile coordinates, fractional for anything moving, so a script turns them into world positions with the tilemap’s own origin and tile size.
Content is data
Section titled “Content is data”- Tower rows:
cost,damage,rate(shots a second),range(in tiles),splash,slowandslow_time,chainandchain_range,shot_speed(0 is a beam that hits at once),hits_air/hits_ground,upgrade(the row it becomes), andart,color,textfor the game to draw and describe it with. - Creep rows:
hp,speed(tiles a second),armor(taken off every hit, to a minimum of 1),reward,leak(lives it costs at the gate),flying(goes straight over the maze),healandheal_range(mends others),splits_intoandsplits. - Wave rows:
number,bonus,boss,text, andgroups- each a creep, a count, the seconds between them and a delay before the first.
Loading checks every reference: a tower upgrading into nothing, a creep
splitting into nothing, a wave sending a creep that is not there. All of it
is an error at load (TdState.load_error), not a hole in the middle of a
game.
Playing
Section titled “Playing”- Build phase between waves, with a clock. Calling the next wave early pays a gold for every second left on it.
- Towers are bought, upgraded in place (the line is however deep the table makes it) and sold back for three quarters of what went in.
- Targeting: first, last, strongest, weakest or closest, per tower.
- A leak costs lives; at zero the keep falls. Clearing the last wave wins, and the lives left are worth score.
Script commands
Section titled “Script commands”| Call | Does |
|---|---|
Td.new_game(e) | start again |
Td.build(e, col, row, tower) | build one |
Td.upgrade(e, col, row) | upgrade the tower there |
Td.sell(e, col, row) | sell it |
Td.target(e, col, row, mode) | first, last, strongest, weakest, closest, or next |
Td.wave(e) | call the next wave early |
Td.speed(e, x) | run at x times speed |
Td.auto(e, on, delay) | the bot plays |
A refused build says why in the refused event (and in TdState.error):
the cell is taken, the gold is short, or it would seal the way.
What scripts read
Section titled “What scripts read”TdState every frame: phase (build, wave, victory, defeat),
wave, waves, lives, gold, score, build_left, speed, error,
the current path as cells, and the lists:
creeps:id,kind,x,y,hp,max_hp,slow,flying,facing,flashtowers:id,kind,col,row,aim,level,target,kills,spentshots:id,x,y,kind
TdLibrary once the tables load: the towers that can be built, each
with its cost, what it does, and the whole upgrade line behind it - what a
shop panel needs.
TdEvents.events: build, upgrade, sell, refused, wave_start,
wave_end, fire (with beam and where it aimed), kill, split,
leak, victory, defeat.
src/sim.rs: the road goes round the towers and may never be sealed; creeps walk it and cost lives at the gate; towers shoot and are paid; splash catches a crowd and a blob splits; fliers ignore the maze and only some towers reach them; the last wave won is a victory; calling a wave early pays; each targeting mode picks what it says.tests/balance.rs, on the arcade’s real tables: every reference resolves and the waves grow; the bot gets deep into the run and wins only narrowly; a keep with no towers falls in the first waves; and the same game played twice goes the same way.
The road mode (td_road)
Section titled “The road mode (td_road)”Kingdom Rush’s model beside the maze. The roads are fixed polylines - one or more, which may fork and merge (roads that share points share that stretch) - and towers go only on the build spots beside them. Positions are world units, so a road point is where it is drawn - Hollow Bastion paints its maps from the same points the package walks.
"td_road": { "tables": { "towers": "data/bastion_towers", "creeps": "data/bastion_creeps", "heroes": "data/bastion_heroes", "spells": "data/bastion_spells", "levels": "data/bastion_levels" } }Nothing plays until a script starts a level.
Content is data
Section titled “Content is data”- Towers come in four
familys -archer,mage,artillery,barracks- each a line of rows joined byupgrades: one upgrade for tiers 1 and 2, two at tier 3 (the specialisations). A row hascost(to build it or to upgrade into it),damage[min, max],kind(physical, stopped by armour;magic, stopped by magic resistance;true),rate(seconds between volleys),range,shot(arrow,bolt,shellfly in arcs;lightningstrikes at once),shot_speed,arc,splash,shots(targets a volley),hits_air, and effects:crit_chance/crit_mult,poison/poison_time,slow/slow_time,chain/chain_range,instakill,armor_shred,stun_chance/stun_time. A barracks fires nothing:soldiers,soldier_hp,soldier_damage,soldier_rate,soldier_armor,soldier_respawn,soldier_regenandsoldier_heal(mends the soldiers round it, even mid-fight). - Creeps:
hp,speed,armorandmagic_resist(0 to 1, the share shrugged off),reward,lives(lost at the gate),flying(nobody blocks it),damage/attack_rate(its melee against a blocker),regen,boss, and abilities on timers:spawn/spawn_count/spawn_every(calls others),heal/heal_range/heal_every, andstun_every/stun_range/stun_time(silences the nearest tower). - Heroes:
hp,damage,rate,rangedandrange(a ranged hero shoots what passes while nothing holds him),cleave,armor,speed,respawn,regen, cumulative experiencelevels, andhp_per_level/damage_per_level. - Spells: a
meteor(countmeteors ofdamageandradiusscattered about the aim) andreinforce(soldierswho hold the road forduration), each with acooldown. - Levels:
number,gold,lives,paths,spots,hero(where he stands, and comes back),first_wait,gap(seconds after a wave has all come in before the next comes by itself),call_rate(gold a second left on the clock when a wave is called early),road_width, andwaves- each a list ofgroups:creep,count,spacing,delayandpath(-1 takes the roads in turn).
Loading checks every reference, that each family is a proper line (one first tower, upgrades one tier up within the family, at most two), and that every wave sends real creeps down real roads.
Playing
Section titled “Playing”- A creep walks its road by distance, a little to one side. A soldier (from a barracks, or a reinforcement) or the hero picks a ground creep near its post - an unheld one first, the one furthest along - walks to it and holds it: the creep stops and the two fight until one falls. A fallen soldier comes back from its barracks, the hero to his post. Fliers go over all of them.
- Towers fire at whatever is furthest along within reach. Arrows and bolts follow their target; shells are aimed at where it will be and burst there.
- The first wave comes by itself after
first_wait, each later onegapseconds after the last has all arrived. Calling one early pays for the time left, and takes half of it off the spells’ cooldowns. - A sale gives back 60% of what went into a tower. Winning gives stars by the lives left: 3 for 90% or more, 2 for 30% or more, else 1.
Script commands
Section titled “Script commands”Spots count from 1 here, as Lua does.
| Call | Does |
|---|---|
Td.road_start(e, level, hero, seed) | start a level (a row id) with a hero (a row id) |
Td.road_stop(e) | end it (the state goes back to idle) |
Td.road_build(e, spot, family) | build the first tower of a family |
Td.road_upgrade(e, spot, choice) | upgrade into upgrade number choice (1, or 2 at the top) |
Td.road_sell(e, spot) | sell |
Td.road_rally(e, spot, x, y) | move a barracks’ rally point to the road nearest (x, y), within its reach |
Td.road_hero(e, x, y) | send the hero |
Td.road_cast(e, spell, x, y) | meteor or reinforce |
Td.road_call(e) | bring the next wave now |
Td.road_speed(e, x), Td.road_pause(e, on) | time |
Td.road_auto(e, on, delay) | the bot plays |
Td.road_advise(e, on) | the bot publishes its next move as advice without making it |
What scripts read
Section titled “What scripts read”TdRoadState every frame: phase (idle, ready, battle,
victory, defeat), gold, lives, wave, waves, next_in,
can_call, call_bonus, incoming (the roads the next wave takes, with
their first points), next_kinds, next_text, stars, error, advice,
and the lists creeps (position, health, armor, mr, flying, boss,
fighting, facing, flash, slow, poison, stun), towers (spot
counted from 0, kind, family, tier, range, rally_x / rally_y,
fired - seconds since its last volley - stunned, sell and its
upgrades), soldiers, shots (with h, the height above the ground,
and angle), meteors, hero and spells. TdRoadLibrary once the
tables load: every row, and every level’s roads and spots.
TdRoadEvents.events: build, upgrade, sell, refused, wave,
call, fire, hit (the damage dealt, crit, and by: a spot, -2 the
hero, -3 a spell, -4 a soldier), boom, bolt (a lightning’s points),
miss, kill, leak, summon, heal, stun, meteor, spell,
rally, smite, hero_move, hero_strike, hero_level, hero_dead,
hero_back, soldier_dead, victory, defeat.
The bot
Section titled “The bot”road::bot::decide ranks the spots by how much road they see, and gives
them families by what the level sends (fliers want arrows and magic, armour
wants magic). It spends in buckets - three towers, their first upgrades, more
towers, the third tier, the rest, the specialisations - saving for the next
thing rather than buying something worse; it rains meteors on crowds and
bosses, drops reinforcements in front of whatever is about to get through,
sends the hero there, and calls the next wave once the field is clear. Two
things tried and dropped, for whoever improves it: spending on every spot
before any third tier lost more games, and a greedy “damage a second per
gold” chooser lost more still - upgrades are worth more than they look.
Tests (road)
Section titled “Tests (road)”src/road/sim.rs: creeps walk the road and cost lives; the first wave comes by itself and calling early pays; armour turns arrows and magic goes through; a soldier holds a creep until one falls, and comes back; fliers go over soldiers and shells; a shell bursts on a crowd; towers climb into two specialisations and sell for 60%; a rally point snaps to the road within reach; the hero walks, fights, levels up and comes back; spells wait on their cooldowns and reinforcements leave; bosses call, heal and silence; stars by the lives left.tests/road_balance.rs, on Hollow Bastion’s real tables: every family climbs to two specialisations and every boss ability is used; the bot wins every level with both heroes over three seeds, with two stars or more on average; the hero alone loses every level; the same game goes the same way twice.TD_LEVEL=l5 TD_HERO=seren TD_SEED=21 cargo test -p jengine-td --test road_balance --release trace -- --ignored --nocaptureprints one game wave by wave (TD_LEAKS=1names every leak).