Compare commits
14 Commits
progressio
...
92ab1dab54
| Author | SHA1 | Date | |
|---|---|---|---|
| 92ab1dab54 | |||
| 94b5d941ba | |||
| 53af44db04 | |||
| cdf89ce0dd | |||
| e8786c3922 | |||
| fc622670d2 | |||
| e32d384c99 | |||
| 24e0999d8a | |||
| fcaee000fa | |||
| 751ef27a7b | |||
| 3c1376828c | |||
| bd0675db66 | |||
| 5b86b15c71 | |||
| c6db4bf24a |
@@ -56,12 +56,9 @@ production_time_seconds = 3
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Rm"
|
||||
|
||||
# damage 14 keeps a 60 HP drone at 5 hits — 15+ crosses a breakpoint that
|
||||
# silently adds ~25% effective DPS vs drones (docs/balancing/history.md,
|
||||
# round 6).
|
||||
[module.weapon]
|
||||
damage = 14
|
||||
attack_range_m = 80
|
||||
attack_range_m = 70
|
||||
attack_rate_hz = 1.5
|
||||
|
||||
|
||||
@@ -78,12 +75,9 @@ production_time_seconds = 4
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Rl"
|
||||
|
||||
# attack_range_m 130 deliberately exceeds the station range of 120
|
||||
# (stations.toml) — the l gun is the only weapon that can besiege stations
|
||||
# without tanking their fire (docs/balancing/history.md, playtest 1).
|
||||
[module.weapon]
|
||||
damage = 52
|
||||
attack_range_m = 130
|
||||
attack_range_m = 100
|
||||
attack_rate_hz = 0.8
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -116,7 +110,7 @@ glyph = "Rp"
|
||||
|
||||
[module.repair]
|
||||
repair_rate_hz = 1
|
||||
repair_amount_hp = 4
|
||||
repair_amount_hp = 9
|
||||
repair_range_m = 80
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
# using the verified fitted threat values from tools/threat_report.py:
|
||||
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
||||
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.5,
|
||||
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17,
|
||||
# railgun_s-spam cruiser (12 small guns) 178.
|
||||
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17.
|
||||
# Module arrays mirror the ships' default_modules loadouts unless a
|
||||
# doctrine variant is the point of the arena.
|
||||
#
|
||||
@@ -349,50 +348,6 @@ enemy_buffer_width_tiles = 10
|
||||
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena]]
|
||||
name = "Railgun_s-spam cruisers vs default cruisers (1424 vs 1401)"
|
||||
# Tracks the playtest-1 meta: cruiser hulls filled with 12 small guns
|
||||
# (max DPS/threat, no armor, range 50) against the default m-gun fit.
|
||||
# The concentration tax means the spam side SHOULD win a brawl somewhat;
|
||||
# this arena bounds its margin — a blowout here means the small-gun
|
||||
# premium or the m-gun range edge needs retuning.
|
||||
height_tiles = 10
|
||||
player_buffer_width_tiles = 10
|
||||
contest_zone_width_tiles = 50
|
||||
enemy_buffer_width_tiles = 10
|
||||
|
||||
[[arena.team]]
|
||||
name = "Spam"
|
||||
[[arena.team.ship]]
|
||||
schematic = "cruiser"
|
||||
count = 8
|
||||
modules = [
|
||||
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 3, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 0, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 3, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 3, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 3, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Default"
|
||||
[[arena.team.ship]]
|
||||
schematic = "cruiser"
|
||||
count = 6
|
||||
modules = [
|
||||
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena]]
|
||||
name = "Repair escort vs raw numbers (444 vs 444)"
|
||||
height_tiles = 10
|
||||
|
||||
@@ -21,12 +21,8 @@ REQ-* ids in [../requirements.md](../requirements.md).
|
||||
|
||||
First full balancing round complete (2026-07-06): targets → tree →
|
||||
numbers → threat-calculator parity → combat stats (arena-converged) →
|
||||
pacing. Playtesting in progress: playtest 1 (two full ~40-min wins,
|
||||
cruisers only) found the railgun_s-spam-cruiser meta, confirmed repair
|
||||
as overpowered, and exposed a 2.5–3× run-length gap; combat stats
|
||||
adjusted and re-checked in arena round 6 (see `history.md`). Next:
|
||||
playtest 2 with the new stats — pacing knobs (station scaling, threat
|
||||
rate, win pacing) wait for its result.
|
||||
pacing. Next step: full-game playtests against the run-shape targets in
|
||||
`targets.md`.
|
||||
|
||||
## Open action items
|
||||
|
||||
|
||||
@@ -94,16 +94,10 @@ geometry-validated against the hull grids)
|
||||
|
||||
## Combat stats
|
||||
|
||||
(arena-converged 2026-07, rounds 1–5; playtest-1 adjustments 2026-07-06 —
|
||||
see `history.md`)
|
||||
(arena-converged, 2026-07; see `history.md` rounds 1–5)
|
||||
|
||||
**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m;
|
||||
railgun_m 14 × 1.5 (21), range 80; railgun_l 52 × 0.8 (41.6), range 130.
|
||||
DPS per threat: s 0.62, m 0.48, l 0.41 — the concentration tax stands;
|
||||
reach is the bigger guns' compensation (ranges raised after playtest 1,
|
||||
which alone priced out the small-gun-spam meta). railgun_l deliberately
|
||||
outranges stations (120 m) to buy the siege role. railgun_m damage is
|
||||
breakpoint-sensitive: 15+ drops a 60 HP drone from 5 hits to 4.
|
||||
railgun_m 14 × 1.5 (21), range 70; railgun_l 52 × 0.8 (41.6), range 100.
|
||||
|
||||
**Hull HP** (15/threat prior + empirical trims): drone 60, frigate 300,
|
||||
destroyer 550, cruiser 1500, battlecruiser 2400, battleship 6300,
|
||||
@@ -115,9 +109,8 @@ destroyer 30|35|18|6|3, cruiser 24|25|12|4|2, battlecruiser 20|20|10|3|1.5,
|
||||
battleship 15|14|7|2|1, dreadnought/carrier 10|8|4|1|0.5.
|
||||
Sensors: 150/200/220/250/260/280/300/350 m.
|
||||
|
||||
**Other modules:** armor_plates +1200 HP; repair_tool 4 HP × 1 Hz,
|
||||
range 80 (halved after playtest 1 — free between-wave top-offs were never
|
||||
priced by the arena escort test); salvager range 60, cargo 20, 0.5 collections/s; afterburner
|
||||
**Other modules:** armor_plates +1200 HP; repair_tool 9 HP × 1 Hz,
|
||||
range 80; salvager range 60, cargo 20, 0.5 collections/s; afterburner
|
||||
×1.6 speed +60 accel; maneuvering_thrusters ×1.2 speed +10 maneuvering;
|
||||
sensor_booster +50 m; weapon_upgrade ×1.2 damage; weapon_primer ×1.2
|
||||
rate; weapon_stabilizer ×1.3 range ×0.8 rate.
|
||||
|
||||
@@ -94,68 +94,3 @@ range 200).
|
||||
— ~1 expansion per cycle mid-game, 2–3 cycles apart late.
|
||||
- **First full balancing round complete.** Next: full-game playtests
|
||||
against the run-shape targets.
|
||||
|
||||
## 2026-07-06 — playtest 1 (two full playthroughs)
|
||||
|
||||
Two complete runs, WON in ~40 min each with cruiser fleets only — never
|
||||
needing capitals. (Initially misread as "two pushes in 40 min, pacing on
|
||||
target"; corrected in round 6.) That is roughly cycle 8 against the
|
||||
win-cycle target of 20–24: a 2.5–3× pacing gap. Pacing knobs deliberately
|
||||
untouched this round — the runs rode 9 HP/s repair and stations nothing
|
||||
outranged, both nerfed below; playtest 2 measures the remaining gap.
|
||||
|
||||
- **Meta finding: cruisers filled with 12× railgun_s dominate.** Predicted
|
||||
by the numbers in hindsight: the concentration tax makes railgun_s the
|
||||
best DPS/threat (0.62 vs m 0.48, l 0.41), range is the big guns' only
|
||||
mechanical edge (armor is added HP, not damage reduction — no anti-swarm
|
||||
mechanic), repair sustain covers the closing distance, and stations
|
||||
outranged every ship gun (120 vs railgun_l's 100), so even capitals had
|
||||
to tank-and-brawl. The cruiser compounds it: first hull with a large
|
||||
1×1 canvas (12 cells) and a nearly quartz-free chain.
|
||||
- **Repair tool confirmed overpowered** (second signal after the
|
||||
persistent +24% arena escort margin): the arena only prices in-fight
|
||||
sustain; real runs add free full top-offs in every 15–45 s wave gap
|
||||
across the whole swarm. The 0.7 HP/s-per-threat prior is wrong for
|
||||
wave defence.
|
||||
- **Changes:** repair_tool 9→4 HP/s; railgun_l range 100→130 (now
|
||||
outranges stations — buys the siege role the capital ladder promises);
|
||||
railgun_m 14→16 dmg and range 70→80 (tax softened: m sits at 0.55
|
||||
DPS/threat, between s and l). Module threats unchanged (costs
|
||||
untouched), so no ladder recalculation needed.
|
||||
- New tracked arena added: railgun_s-spam cruisers (8× 178) vs default
|
||||
cruisers (6× 233.5) — the spam side should win a brawl somewhat, but a
|
||||
blowout means the small-gun premium needs retuning.
|
||||
- **Open:** re-run the arena suite to check the range/damage changes
|
||||
against the round 1–5 results; next playtest should verify big guns now
|
||||
feel worth climbing to and repair is merely good.
|
||||
|
||||
## 2026-07-06 — arena round 6 (checking the playtest-1 adjustments)
|
||||
|
||||
Mirrors healthy (5–8% margins, durations 24/66/91 s). Results:
|
||||
|
||||
- **Spam-cruiser arena: default cruisers +9% — the meta is priced out**,
|
||||
and the range buff alone did the work.
|
||||
- **Regression: drone swarm vs cruisers +47% for cruisers** (was +14%
|
||||
swarm in round 3). Besides the wider range gap, the damage buff crossed
|
||||
a breakpoint: 14 dmg kills a 60 HP drone in 5 hits, 16 in 4 — a hidden
|
||||
~25% effective-DPS gain vs drones. Change: **railgun_m damage 16→14**
|
||||
(range stays 80); the tax stands, reach is the compensation.
|
||||
- Battleship +30% and dreadnought +29% vs pure railgun_s fleets:
|
||||
**accepted as reach-doctrine texture** (BS was already accepted at
|
||||
+23%) — the l gun's 130 m standoff is exactly what the range buff
|
||||
bought; the counter is your own reach or 2:1 numbers, not equal-threat
|
||||
small guns. Watch, don't tune.
|
||||
- Repair escort flipped to raw +16%: **kept at 4 HP/s deliberately** —
|
||||
the arena cannot price the free between-wave top-offs, so slightly
|
||||
below par in-fight is the correct price for a module whose run-value
|
||||
includes them. Playtest 2 decides; 6 is the fallback if repair feels
|
||||
dead.
|
||||
- Station assault: the 3× swarm cracked the fortified position keeping
|
||||
45% EHP. No knob this round touched it; together with playtest 1's
|
||||
trivially easy pushes it flags **station strength as the first pacing
|
||||
lever** for the next pass.
|
||||
|
||||
**Pacing deferred:** playtest 1's 40-min wins predate the repair nerf
|
||||
and the l-gun siege range. If playtest 2 still wins by ~cycle 10, the
|
||||
levers are enemy station scaling (`3000 + 1500*x` likely too shallow),
|
||||
the threat rate, and possibly `artifact_win_count`.
|
||||
|
||||
@@ -31,6 +31,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
13
src/lib/eventsystem/event/PlayerCommandsAppliedEvent.h
Normal file
13
src/lib/eventsystem/event/PlayerCommandsAppliedEvent.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted by GameWorldView once per frame after queued player commands have been
|
||||
// drained and applied to the simulation. It lets presentation widgets refresh
|
||||
// even while the game is paused (no tick advances, so no TickAdvancedEvent), for
|
||||
// example so a shipyard's layout preview appears immediately after its schematic
|
||||
// is chosen. It is a UI notification only and never feeds back into the command
|
||||
// queue, so it has no effect on replay recording or determinism.
|
||||
class PlayerCommandsAppliedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -62,6 +62,7 @@
|
||||
#include "ExpansionCostChangedEvent.h"
|
||||
#include "GameSpeedChangedEvent.h"
|
||||
#include "SchematicChoicesAvailableEvent.h"
|
||||
#include "PlayerCommandsAppliedEvent.h"
|
||||
#include "TickAdvancedEvent.h"
|
||||
|
||||
namespace
|
||||
@@ -212,6 +213,7 @@ void GameWorldView::onFrame()
|
||||
// Drain queued player commands once per frame, before the tick batch. This
|
||||
// runs even at 0x so a paused player sees placed construction sites
|
||||
// immediately, while staying deterministic (see docs/replay_design.md).
|
||||
const bool commandsApplied = m_commandManager.hasPending();
|
||||
m_commandManager.drain();
|
||||
|
||||
// A drained Reset reinitialized the simulation; reset the view to match.
|
||||
@@ -221,6 +223,17 @@ void GameWorldView::onFrame()
|
||||
resetForNewGame();
|
||||
}
|
||||
|
||||
// Notify presentation widgets that queued commands were applied, so a
|
||||
// paused player still sees the effect (e.g. a shipyard's layout preview
|
||||
// after picking a schematic) even though no tick advances. UI-only: this
|
||||
// does not touch the command queue or simulation, so replay recording
|
||||
// and determinism are unaffected.
|
||||
if (commandsApplied)
|
||||
{
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<PlayerCommandsAppliedEvent>());
|
||||
}
|
||||
|
||||
const int ticks = m_tickDriver.advance(
|
||||
static_cast<double>(elapsed), m_gameSpeedMultiplier);
|
||||
for (int i = 0; i < ticks; ++i)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "ItemType.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "PlayerCommandsAppliedEvent.h"
|
||||
#include "RecipeSelectionDialog.h"
|
||||
#include "RecipeSelectionRequestedEvent.h"
|
||||
#include "Rotation.h"
|
||||
@@ -294,38 +295,12 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||
}
|
||||
m_recipeSelectButton->show();
|
||||
|
||||
if (type == BuildingType::Shipyard && !recipeId.empty())
|
||||
{
|
||||
const ShipDef* sDef = findShipDef(recipeId);
|
||||
if (sDef && !sDef->layout.empty())
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
if (shipLayout.has_value())
|
||||
{
|
||||
layout = *shipLayout;
|
||||
}
|
||||
m_layoutPreview->setShipAndLayout(
|
||||
sDef->layout, layout, &m_config->modules.modules);
|
||||
m_layoutPreview->show();
|
||||
m_configureLayoutBtn->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
updateShipyardLayoutWidgets(type, recipeId, shipLayout);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_recipeSelectButton->hide();
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
updateShipyardLayoutWidgets(type, recipeId, shipLayout);
|
||||
}
|
||||
|
||||
// Belt "Clear" removes items from a live belt tile; a construction site has
|
||||
@@ -363,32 +338,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||
|
||||
if (m_singleIsSite)
|
||||
{
|
||||
QString progress;
|
||||
if (s->completesAt == 0)
|
||||
{
|
||||
progress = tr("Queued");
|
||||
}
|
||||
else
|
||||
{
|
||||
const BuildingDef* def = nullptr;
|
||||
for (const BuildingDef& d : m_config->buildings.buildings)
|
||||
{
|
||||
if (d.type == s->type) { def = &d; break; }
|
||||
}
|
||||
if (def && def->constructionTimeSeconds > 0)
|
||||
{
|
||||
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
|
||||
const int pct = static_cast<int>(
|
||||
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
|
||||
progress = tr("%1% complete").arg(pct);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = tr("Building...");
|
||||
}
|
||||
}
|
||||
m_buffersLabel->setText(progress);
|
||||
refreshSiteProgress(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -396,6 +346,36 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||
}
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
|
||||
{
|
||||
QString progress;
|
||||
if (s->completesAt == 0)
|
||||
{
|
||||
progress = tr("Queued");
|
||||
}
|
||||
else
|
||||
{
|
||||
const BuildingDef* def = nullptr;
|
||||
for (const BuildingDef& d : m_config->buildings.buildings)
|
||||
{
|
||||
if (d.type == s->type) { def = &d; break; }
|
||||
}
|
||||
if (def && def->constructionTimeSeconds > 0)
|
||||
{
|
||||
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
|
||||
const int pct = static_cast<int>(
|
||||
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
|
||||
progress = tr("%1% complete").arg(pct);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = tr("Building...");
|
||||
}
|
||||
}
|
||||
m_buffersLabel->setText(progress);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(b);
|
||||
@@ -530,15 +510,38 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
|
||||
m_buffersLabel->setText(bufText);
|
||||
|
||||
if (b->type == BuildingType::Shipyard && shipDef && !shipDef->layout.empty())
|
||||
// The recipe/schematic is applied via a queued command that only drains on a
|
||||
// later frame, so the per-tick refresh must own the shipyard preview and the
|
||||
// Configure Layout button's visibility; otherwise they stay hidden until the
|
||||
// building is re-selected (which re-runs buildSingle).
|
||||
updateShipyardLayoutWidgets(b->type, b->recipeId, b->shipLayout);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::updateShipyardLayoutWidgets(
|
||||
BuildingType type,
|
||||
const std::string& recipeId,
|
||||
const std::optional<ShipLayoutConfig>& shipLayout)
|
||||
{
|
||||
const ShipDef* shipDef = (type == BuildingType::Shipyard)
|
||||
? findShipDef(recipeId)
|
||||
: nullptr;
|
||||
|
||||
if (shipDef && !shipDef->layout.empty())
|
||||
{
|
||||
ShipLayoutConfig layout;
|
||||
if (b->shipLayout.has_value())
|
||||
if (shipLayout.has_value())
|
||||
{
|
||||
layout = *b->shipLayout;
|
||||
layout = *shipLayout;
|
||||
}
|
||||
m_layoutPreview->setShipAndLayout(
|
||||
shipDef->layout, layout, &m_config->modules.modules);
|
||||
m_layoutPreview->show();
|
||||
m_configureLayoutBtn->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layoutPreview->hide();
|
||||
m_configureLayoutBtn->hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,6 +566,21 @@ const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
|
||||
{
|
||||
refreshSelectionDisplay(RefreshReason::PeriodicTick);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(
|
||||
std::shared_ptr<const PlayerCommandsAppliedEvent> /*event*/)
|
||||
{
|
||||
// Player commands (e.g. choosing a shipyard schematic) are applied by a
|
||||
// queued drain, not synchronously. When the game is paused no tick advances,
|
||||
// so TickAdvancedEvent never fires; refresh here too, otherwise the panel
|
||||
// would not reflect the change until the next tick or a re-selection.
|
||||
refreshSelectionDisplay(RefreshReason::CommandApplied);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
||||
{
|
||||
if (m_selectedEntity.has_value())
|
||||
{
|
||||
@@ -587,7 +605,18 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent>
|
||||
const ConstructionSite* s = m_sim->buildings().findSite(m_singleBuildingId);
|
||||
if (s)
|
||||
{
|
||||
rebuild();
|
||||
// A periodic tick only advances construction progress, so update just the
|
||||
// progress label. Rebuilding every tick would hide/re-show all widgets and
|
||||
// cancel any in-progress click on the recipe button. An applied command
|
||||
// may have changed the site's recipe/layout, so rebuild in that case.
|
||||
if (reason == RefreshReason::CommandApplied)
|
||||
{
|
||||
rebuild();
|
||||
}
|
||||
else
|
||||
{
|
||||
refreshSiteProgress(s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
buildEmpty();
|
||||
@@ -647,9 +676,11 @@ void SelectedBuildingPanel::onSelectRecipeClicked()
|
||||
return;
|
||||
}
|
||||
// The emit is synchronous: MainWindow pauses the game, runs the modal
|
||||
// selection dialog, applies the chosen recipe/schematic, and restores the
|
||||
// speed before this returns. rebuild() then refreshes the button caption,
|
||||
// tooltip, preview, and buffers for the new selection.
|
||||
// selection dialog, and restores the speed before this returns. The chosen
|
||||
// recipe/schematic is only *enqueued* as a command, though, and drains on a
|
||||
// later frame -- so this rebuild() still sees the old recipe. The per-tick
|
||||
// refreshBuffers() path picks up the new schematic (and shows the layout
|
||||
// preview + Configure Layout button) once the command has been applied.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<RecipeSelectionRequestedEvent>(m_singleBuildingId));
|
||||
rebuild();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "PlayerCommandsAppliedEvent.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "ShipLayout.h"
|
||||
@@ -33,6 +34,7 @@ class QVBoxLayout;
|
||||
|
||||
class SelectedBuildingPanel : public QWidget,
|
||||
public CombinedEventHandler<TickAdvancedEvent,
|
||||
PlayerCommandsAppliedEvent,
|
||||
EntitySelectedEvent,
|
||||
SelectionChangedEvent,
|
||||
DebugDrawToggledEvent>
|
||||
@@ -46,6 +48,7 @@ public:
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||
@@ -56,7 +59,18 @@ private slots:
|
||||
void onSplitterFilterChanged();
|
||||
|
||||
private:
|
||||
// Why the selection display is being refreshed. A periodic tick only needs a
|
||||
// lightweight content update (e.g. a construction site's progress label),
|
||||
// whereas an applied player command may have changed the configuration and
|
||||
// needs a full structural rebuild.
|
||||
enum class RefreshReason
|
||||
{
|
||||
PeriodicTick,
|
||||
CommandApplied
|
||||
};
|
||||
|
||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||
void refreshSelectionDisplay(RefreshReason reason);
|
||||
void rebuild();
|
||||
void hideAllWidgets();
|
||||
void clearContent();
|
||||
@@ -64,6 +78,10 @@ private:
|
||||
void buildSingle(BuildingId id);
|
||||
void buildMulti(const std::vector<BuildingId>& ids);
|
||||
void refreshBuffers(const Building* b);
|
||||
void refreshSiteProgress(const ConstructionSite* s);
|
||||
void updateShipyardLayoutWidgets(BuildingType type,
|
||||
const std::string& recipeId,
|
||||
const std::optional<ShipLayoutConfig>& shipLayout);
|
||||
void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info);
|
||||
const RecipeDef* findRecipe(const Building* b) const;
|
||||
const ShipDef* findShipDef(const std::string& id) const;
|
||||
|
||||
Reference in New Issue
Block a user