refactor: rename hammer→production throughout codebase

Production is production, hammers are weapons. Single source of truth.

Rust: hammer_cost→production_cost, hammers_invested→production_invested,
hammers params→production (with serde aliases for JSON backward compat)
GDScript: hammers vars→prod, comments updated
Guide: "Hammers" label→"Production"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-04-12 17:56:17 -07:00
parent d67d94e440
commit 0dc1fa5db8
7 changed files with 64 additions and 59 deletions

View file

@ -43,7 +43,7 @@ var owner_index: int = -1
## Each entry: {type: "building"|"unit", id: String, cost: int}
var production_queue: Array = []
## Accumulated hammers toward the current production_queue[0].
## Accumulated production toward the current production_queue[0].
var production_progress: int = 0
# ── Property accessors (used by renderers) ─────────────────────────
@ -249,10 +249,10 @@ func process_culture(tile_yields_json: String) -> bool:
return _gd_city.call("process_culture", tile_yields_json)
## Add production hammers.
func add_production(hammers: float) -> void:
## Add production points to the city's accumulator.
func add_production(production: float) -> void:
if _gd_city != null:
_gd_city.call("add_production", hammers)
_gd_city.call("add_production", production)
## Get food surplus (food yield - consumption).
@ -312,12 +312,12 @@ func add_to_queue(type: String, item_id: String) -> void:
production_queue.append({"type": type, "id": item_id, "cost": cost})
## Apply hammers to the building/unit production queue. Returns
## true if the current item completed this tick.
func apply_production(hammers: int) -> bool:
## Apply production to the building/unit queue. Returns true if the
## current item completed this tick.
func apply_production(production: int) -> bool:
if production_queue.is_empty():
return false
production_progress += hammers
production_progress += production
var current: Dictionary = production_queue[0]
var cost: int = int(current.get("cost", 0))
if production_progress >= cost:
@ -370,14 +370,14 @@ func enqueue_item(
## Tick a building's queue. Returns number of completed items.
func tick_building(
building: String,
hammers: int,
production: int,
treasury: RefCounted = null
) -> int:
if _gd_city == null:
_warn_missing_extension()
return 0
var completed_ids: PackedStringArray = _gd_city.call(
"tick_building", building, hammers
"tick_building", building, production
)
var count: int = completed_ids.size()
if count == 0:

View file

@ -70,14 +70,14 @@ func _process_production(player: RefCounted) -> void: # Player
var c: CityScript = city_ref as CityScript
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
var yields: Dictionary = c.get_yields(tile_json)
var hammers: int = int(yields.get("production", 1) * prod_modifier)
var prod: int = int(yields.get("production", 1) * prod_modifier)
# Capture current item before apply_production pops it on completion.
var current: Dictionary = (
c.production_queue.front() as Dictionary
if not c.production_queue.is_empty()
else {}
)
if not c.apply_production(hammers):
if not c.apply_production(prod):
continue
var item_type: String = current.get("type", "")
var item_id: String = current.get("id", "")

View file

@ -30,14 +30,14 @@ static func process_production(
var c: CityScript = city as CityScript
var tile_json: String = build_tile_yields_json(c, game_map)
var yields: Dictionary = c.get_yields(tile_json)
var hammers: int = int(yields.get("production", 1) * prod_modifier)
var prod: int = int(yields.get("production", 1) * prod_modifier)
# Capture current item before apply_production pops it on completion.
var current: Dictionary = (
c.production_queue.front() as Dictionary
if not c.production_queue.is_empty()
else {}
)
if not c.apply_production(hammers):
if not c.apply_production(prod):
continue
var item_type: String = current.get("type", "")
var item_id: String = current.get("id", "")

View file

@ -102,7 +102,7 @@ func test_happy_path_enqueue_tick_emits_item_crafted() -> void:
var treasury: RefCounted = ClassDB.instantiate("GdTreasury") as RefCounted
var completed: int = city.tick_building("smithy", 30, treasury)
assert_eq(completed, 1, "30 hammers should complete iron_axe")
assert_eq(completed, 1, "30 production should complete iron_axe")
assert_eq(city.queue_len("smithy"), 0)
assert_eq(_crafted_events.size(), 1, "item_crafted should fire exactly once")

View file

@ -819,7 +819,7 @@ impl GdCity {
}
/// Load items from a JSON array `[ { id, production: { building, secondary_building?,
/// hammer_cost, materials?, requires_tech? }, ... }, ... ]`. The DataLoader passes
/// production_cost, materials?, requires_tech? }, ... }, ... ]`. The DataLoader passes
/// the merged item list from `public/resources/items/*.json`. Items without a
/// `production` block (apex relics) are skipped.
#[func]
@ -829,7 +829,7 @@ impl GdCity {
building: String,
#[serde(default)]
secondary_building: Option<String>,
hammer_cost: u32,
production_cost: u32,
#[serde(default)]
materials: Vec<mc_city::MaterialCost>,
#[serde(default)]
@ -852,7 +852,7 @@ impl GdCity {
let Some(p) = doc.production else { continue };
self.item_registry.insert(mc_city::ItemDef {
id: doc.id,
hammer_cost: p.hammer_cost,
production_cost: p.production_cost,
building: p.building,
secondary_building: p.secondary_building,
requires_tech: p.requires_tech,
@ -895,13 +895,13 @@ impl GdCity {
}
}
/// Apply `hammers` to one building's queue. Returns an Array of completed
/// Apply `production` to one building's queue. Returns an Array of completed
/// item ids (in completion order). The caller should add each to the
/// civ Treasury and emit `EventBus.item_crafted(item_id, city, player)`.
/// Returns an empty array on error and logs to godot_error.
#[func]
fn tick_building(&mut self, building: GString, hammers: i64) -> PackedStringArray {
let h = hammers.max(0) as u32;
fn tick_building(&mut self, building: GString, production: i64) -> PackedStringArray {
let h = production.max(0) as u32;
match self.inner.tick_building(&building.to_string(), h) {
Ok(completed) => {
let mut out = PackedStringArray::new();
@ -928,7 +928,7 @@ impl GdCity {
}
/// Snapshot a building's queue as an Array of Dictionaries:
/// `[{ item_id, hammer_cost, hammers_invested, progress }, ...]`
/// `[{ item_id, production_cost, production_invested, progress }, ...]`
#[func]
fn queue_snapshot(&self, building: GString) -> Array<Dictionary> {
let mut out = Array::<Dictionary>::new();
@ -939,8 +939,8 @@ impl GdCity {
let mut d = Dictionary::new();
let mc_city::Queueable::Item { item_id } = &entry.queueable;
d.set("item_id", GString::from(item_id));
d.set("hammer_cost", entry.hammer_cost as i64);
d.set("hammers_invested", entry.hammers_invested as i64);
d.set("production_cost", entry.production_cost as i64);
d.set("production_invested", entry.production_invested as i64);
d.set("progress", entry.progress() as f64);
out.push(&d);
}
@ -1157,10 +1157,10 @@ impl GdCity {
self.inner.process_culture(&ty)
}
/// Add production hammers.
/// Add production production.
#[func]
fn add_production(&mut self, hammers: f64) {
self.inner.add_production(hammers);
fn add_production(&mut self, production: f64) {
self.inner.add_production(production);
}
/// Get food surplus for this turn (food yield - consumption).

View file

@ -358,9 +358,9 @@ impl City {
self.can_expand()
}
/// Add production hammers (from the city's production yield).
pub fn add_production(&mut self, hammers: f64) {
self.production_progress += hammers;
/// Add production points (from the city's production yield).
pub fn add_production(&mut self, production: f64) {
self.production_progress += production;
}
// ── Citizens ───────────────────────────────────────────────────
@ -519,8 +519,8 @@ impl City {
queueable: Queueable::Item {
item_id: def.id.clone(),
},
hammer_cost: def.hammer_cost,
hammers_invested: 0,
production_cost: def.production_cost,
production_invested: 0,
};
self.queues
.entry(def.building.clone())
@ -529,11 +529,11 @@ impl City {
Ok(())
}
/// Apply `hammers` to one building's queue. Returns completions.
/// Apply `production` to one building's queue. Returns completions.
pub fn tick_building(
&mut self,
building: &str,
hammers: u32,
production: u32,
) -> Result<Vec<CompletedEntry>, QueueError> {
if !self.has_building(building) {
return Err(QueueError::UnknownBuilding {
@ -544,7 +544,7 @@ impl City {
.queues
.entry(building.to_string())
.or_default()
.tick(hammers))
.tick(production))
}
}
@ -781,7 +781,7 @@ mod tests {
let mut registry = ItemRegistry::new();
registry.insert(ItemDef {
id: "iron_axe".into(),
hammer_cost: 30,
production_cost: 30,
building: "smithy".into(),
secondary_building: None,
requires_tech: Some("bronze_working".into()),

View file

@ -8,7 +8,9 @@
//! Validation happens at *enqueue* time, not completion time, and materials
//! are consumed up-front (so a queued item is "paid for" and cannot be
//! retroactively voided by stockpile drains). This mirrors Civ5's behavior
//! and matches the design doc.
//! and matches the design doc. Production points are the per-turn currency;
//! some JSON data files still use the legacy key `hammer_cost` (accepted via
//! serde alias).
use mc_economy::StockpileError;
use serde::{Deserialize, Serialize};
@ -20,7 +22,8 @@ use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemDef {
pub id: String,
pub hammer_cost: u32,
#[serde(alias = "hammer_cost")]
pub production_cost: u32,
pub building: String,
#[serde(default)]
pub secondary_building: Option<String>,
@ -73,19 +76,21 @@ pub enum Queueable {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueueEntry {
pub queueable: Queueable,
pub hammer_cost: u32,
pub hammers_invested: u32,
#[serde(alias = "hammer_cost")]
pub production_cost: u32,
#[serde(alias = "hammers_invested")]
pub production_invested: u32,
}
impl QueueEntry {
pub fn is_complete(&self) -> bool {
self.hammers_invested >= self.hammer_cost
self.production_invested >= self.production_cost
}
pub fn progress(&self) -> f32 {
if self.hammer_cost == 0 {
if self.production_cost == 0 {
1.0
} else {
(self.hammers_invested as f32 / self.hammer_cost as f32).min(1.0)
(self.production_invested as f32 / self.production_cost as f32).min(1.0)
}
}
}
@ -118,24 +123,24 @@ impl BuildingQueue {
self.entries.push(entry);
}
/// Apply `hammers` to the head entry. Returns any items that completed
/// (hammer overflow rolls into the next entry). Completed entries are
/// Apply `production` to the head entry. Returns any items that completed
/// (production overflow rolls into the next entry). Completed entries are
/// removed from the queue in FIFO order.
pub fn tick(&mut self, mut hammers: u32) -> Vec<CompletedEntry> {
pub fn tick(&mut self, mut production: u32) -> Vec<CompletedEntry> {
let mut completed = Vec::new();
while hammers > 0 && !self.entries.is_empty() {
while production > 0 && !self.entries.is_empty() {
let head = &mut self.entries[0];
let needed = head.hammer_cost.saturating_sub(head.hammers_invested);
let applied = hammers.min(needed);
head.hammers_invested += applied;
hammers -= applied;
let needed = head.production_cost.saturating_sub(head.production_invested);
let applied = production.min(needed);
head.production_invested += applied;
production -= applied;
if head.is_complete() {
let entry = self.entries.remove(0);
completed.push(CompletedEntry {
queueable: entry.queueable,
});
} else {
// Head not done — even if we have hammers left we can't be
// Head not done — even if we have production left we can't be
// mid-stuck; break is unreachable but defensive.
break;
}
@ -213,7 +218,7 @@ mod tests {
fn iron_axe() -> ItemDef {
ItemDef {
id: "iron_axe".into(),
hammer_cost: 30,
production_cost: 30,
building: "smithy".into(),
secondary_building: None,
requires_tech: Some("bronze_working".into()),
@ -227,7 +232,7 @@ mod tests {
fn dwarven_plate() -> ItemDef {
ItemDef {
id: "dwarven_plate".into(),
hammer_cost: 90,
production_cost: 90,
building: "forge".into(),
secondary_building: Some("tannery".into()),
requires_tech: Some("steelworking".into()),
@ -268,7 +273,7 @@ mod tests {
assert_eq!(stockpile.available("iron_ore"), 0);
assert_eq!(city.queue_for("smithy").unwrap().len(), 1);
// 30 hammers in one tick = complete.
// 30 production in one tick = complete.
let done = city.tick_building("smithy", 30).unwrap();
assert_eq!(done.len(), 1);
assert_eq!(
@ -368,11 +373,11 @@ mod tests {
city.enqueue_item("iron_axe", &registry(), &mut stockpile, &techs(&["bronze_working"]))
.unwrap();
// 10 hammers → not done.
// 10 production → not done.
let done = city.tick_building("smithy", 10).unwrap();
assert!(done.is_empty());
assert_eq!(
city.queue_for("smithy").unwrap().entries()[0].hammers_invested,
city.queue_for("smithy").unwrap().entries()[0].production_invested,
10
);
@ -392,7 +397,7 @@ mod tests {
.unwrap();
city.enqueue_item("iron_axe", &r, &mut stockpile, &techs(&["bronze_working"]))
.unwrap();
// Two entries × 30 hammers each = 60 total.
// Two entries x 30 production each = 60 total.
let done = city.tick_building("smithy", 60).unwrap();
assert_eq!(done.len(), 2);
}
@ -427,7 +432,7 @@ mod tests {
assert_eq!(done_axe.len(), 1);
assert!(done_plate.is_empty());
assert_eq!(
city.queue_for("forge").unwrap().entries()[0].hammers_invested,
city.queue_for("forge").unwrap().entries()[0].production_invested,
50
);
}