Skip to content

Defend the dungeon heart: workers and scouts rally when enemies approach - #152

Closed
Rokk001 wants to merge 4018 commits into
tomluchowski:shaders-improvementfrom
Rokk001:pr/dungeon-heart-h5
Closed

Rokk001 wants to merge 4018 commits into
tomluchowski:shaders-improvementfrom
Rokk001:pr/dungeon-heart-h5

Conversation

@Rokk001

@Rokk001 Rokk001 commented Sep 27, 2026 •

Copy link
Copy Markdown

What

Each turn the server looks for enemy creatures within seven tiles of the heart's outer perimeter. While one is there, workers and scouts (CaveHornet, Goblin) leave their work and run to the seat's fighters; when the heart itself takes damage, they drop their jobs and rush to it. When no enemy is left in range, everyone returns to normal work. An alarm call (Keeper/WeAreUnderAttack) sounds when the defence starts, instead of the combat music. The defence state is server-side only.

Saves

The defence state is not saved with the level; old maps and saves keep working.

Stacking

This PR stacks on #147 (pr/defeat-sequence-2026-09-25), #148 (pr/dungeon-heart-h1), #149 (pr/dungeon-heart-h2), #150 (pr/dungeon-heart-h3) and #151 (pr/dungeon-heart-h4). Until all are merged, the diff against the base branch also shows their commits; the new work is the last two commits. To see only this step, compare this branch against the head of #151.

Validation

  • Release build of the stacked branch succeeds (2026-09-27).
  • check_heart_defence.py (15 checks: the enemy at six tiles starts the defence with one alarm call and at eight tiles it ends; a dead or allied creature does not trigger it; a non-human owner gets the defence without the call; a destroyed heart is not defended; the rally goes to the nearest fighter, and with a damaged heart the runner goes to the heart even when a fighter is closer) and the whole heart fixture set keep passing.
  • Verified in game: workers and scouts rally when an enemy approaches and return to work when it leaves.

tomluchowski and others added 30 commits June 8, 2024 18:18
…r every creature owned by player, now the tangents are correctly re-generated for every model in the game, split of SelectionEntityWanted.h into separate file
Instanciacing of Dirt tiles and Fog planes meshes
Auto-generating perlin noise for fog -- little bug -- only a texture of screen size allowed
This packages the accepted implementation without private development notes. No release version or changelog change is required for this unreleased contribution.
This packages the accepted implementation without private development notes. No release version or changelog change is required for this unreleased contribution.
…62408a1c296287d00f9e647306654deb35ca' into HEAD
This packages the accepted implementation without private development notes. No release version or changelog change is required for this unreleased contribution.
Replace the auto declarations introduced by this branch with explicit types,
as requested in issue tomluchowski#42 and required by CLAUDE.md. No behavioural change.
When an enemy hits a heart that is at or below 11 % of its durability,
the human owner receives one event message. No warning on the killing
hit, for AI or defeated owners, or in the editor.
When a human player loses the last dungeon heart, the server sends two
owner-only messages appended to ServerNotificationType: playerDefeated
with the conqueror seat and the heart tile, and levelStatistics with the
elapsed time and six per-seat counters (keepers defeated, creatures
killed, heroes destroyed, rooms captured, items made, creatures
converted). The counters are kept per seat on the server and are not
saved. The client stores the statistics and hands playerDefeated to a
new GameMode::startDefeatSequence entry point.
On playerDefeated the client hides the interface and pointer, blocks
input and cuts the camera to the destroyed heart. A client-only copy of
the heart shakes and bursts into particles and a rubble pile, a swirl
in the conqueror's colour leaves the heart, and a red tint, two
subtitles and a fade to black follow. The timeline runs on a monotonic
clock, so repeated frame callbacks do not speed it up.

On the black screen a debriefing window shows the player, the outcome,
the elapsed time and a per-seat statistics table from levelStatistics.
Its confirm button returns to the main menu, which zooms in briefly and
opens the skirmish sub-menu.
When a heart's health reaches 0, only the heart object is removed. The
room keeps its floor tiles as an inert ruin with no health, so the seat
has no temple any more and the existing defeat path runs as before. The
ruin cannot be attacked, its floor is never released in game mode, and
a saved ruin gets no new heart object on load.
The heart took its health from the durability of its floor tiles, so a
3 by 3 heart fell to a few blows. It now has 10000 health per room tile
(90000 for a 3 by 3 heart); the critical warning stays at 11 % of this
maximum. Only fighters damage an enemy heart: workers deal no damage to
it, and a worker given a non-creature target drops its fight action.

Saves write the heart health as "HeartHealth" on the new scale. An older
"HeartHP" record is converted to the same share of the new maximum, a
value above the maximum is limited to it, an unknown record is rejected
and a destroyed heart stays a ruin.
The green ring of the heart badge now shows the remaining heart health
of the local player as an arc, and the badge glows magenta for three
seconds after the heart was hit. The server sends the new owner-only
heartHealth notification (appended last to ServerNotificationType) when
the health fraction changed by at least one percentage point, when the
heart is destroyed and once when a game starts or is loaded. The client
redraws the badge into its existing texture with blitFromMemory, since
loadFromMemory would create a new texture that the badge never shows.

The badge drawing moves into drawBadgePixels, with the symbol test as a
named function instead of a lambda.
The badge icon still said "Your Mana". The heartHealth message now also
carries the current and maximum heart HP, the server sends it on every whole
HP change, and the client sets the icon tooltip and context line to
"Dungeon Heart: current/max" every frame. The badge tooltip in the layout
reads "Dungeon Heart Health" until the first message arrives.
…econd


The heart maximum no longer depends on the number of covered tiles. A living
damaged heart regains 2.5 / turnsPerSecond per turn up to the maximum, a
destroyed heart never heals. Saves write HeartHealth10000; the older HeartHealth
(per tile scale) and HeartHP records are converted by share, so a full heart
stays full and a ruin stays a ruin. Messages that only carry a new whole HP are
sent at most once per second; percentage point changes and destruction are
still sent at once.
…ght-click to the heart


The ring of the heart badge is drawn as six segments separated by spokes. One
segment stands for one sixth of the health and a partly covered segment is
filled in part. The badge tooltip reads "Dungeon heart health bar" and the
info line "Dungeon heart at 17 %. Right-click moves the view to the heart."
A right-click on the badge moves the camera to the own heart.
…rivets, emerald health gems, ruby heart and embossed gold coin
…ges, lit from height maps with soft 3x3 sampling
…emption and mana transfer on defeat


- Mana income is per second: 30 from the heart plus one per claimed tile,
  the tile part capped at 500 tiles, the tiles of the heart area not counted
  again (GameMap::updateSeatMana)
- Every worker costs 7 mana per second; mana never drops below 0 and is
  capped at MaxManaPerSeat (now 200000), casting is refused while the cost
  is not covered
- Dropping an own worker onto the tile of a living own heart redeems it:
  the worker is removed and half of its summoning price at the current
  worker count is paid back (Player::redemWorkerInHeart)
- On a single player defeat the remaining mana of the defeated seat goes
  to the conqueror, the defeated seat is left with 0
- The mana display shows income and upkeep per second separately
- New fixture source/tests/check_heart_mana.py for income, upkeep,
  redemption and transfer; check_heart_ruin.py keeps passing
…seat has four


- While a seat with a living dungeon heart has fewer than four workers, the
  heart creates one of the seat's worker type at its heart tile every five
  seconds, until there are four (GameMap::updateSeatAutoWorkers)
- A seat with four or more workers, or without a living heart, creates none
  and its clock resets; a long turn still creates at most up to the target
- The existing no-worker chat notice stays and only shows until the first
  worker appears; summoning a worker by spell is unchanged
- The creation clock lives on the seat (mAutoWorkerTimer, server side, not
  saved with the level: after a load the clock starts over at zero)
- New fixture source/tests/check_heart_auto_workers.py for the creation
  pace, the target cap and the blocked cases; check_heart_mana.py keeps
  passing
… an alarm call


- Each turn the server checks for a living enemy creature within seven tiles
  of the living heart of the seat; a dead enemy, an allied creature or a
  destroyed heart gives no defence (GameMap::updateSeatHeartDefense)
- While an enemy is in range, the workers and the scouts (CaveHornet, Goblin)
  leave their work and run to the nearest fighter of the seat
  (CreatureActionGoDefendHeart, GameMap::getHeartDefenceTargetTile)
- When the heart itself takes damage, the runners rush to the heart instead
- When no enemy is in range any more, the action pops and everyone returns
  to normal work
- An alarm call sounds once when the defence starts
  (Keeper/WeAreUnderAttack), not the combat music; a non human owner gets
  the defence without the call
- The defence state lives on the seat (mHeartDefenceActive,
  mHeartDefenceHeartDamaged, server side, not saved with the level: old maps
  and saves keep working)
- New fixture source/tests/check_heart_defence.py for the six and eight tile
  reach, the rally to the nearest fighter, the damaged heart, the destroyed
  heart, the alarm and the runners; the whole heart fixture set keeps
  passing
- check_level_statistics.py: the fake seat of the notification probe gets
  the mana members the production code needs
The runner action class asks the game map for the tile to defend, so the
lookup is a public member next to the other path queries; the per-turn
defence check stays private.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants