Skip to content

Commit 181dc6d

Browse files
authoredFeb 3, 2025··
Payment options (#148)
1 parent 638166a commit 181dc6d

24 files changed

+723
-571
lines changed
 

‎client/src/advance_ui.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ pub enum AdvanceState {
3030

3131
fn new_advance_payment(rc: &RenderContext, name: &str) -> Payment {
3232
let p = rc.shown_player;
33-
let model = &p.advance_cost(name);
33+
let options = &p.advance_cost(name);
3434
let available = &p.resources;
35-
Payment::new(model, available, name, false)
35+
Payment::new(options, available, name, false)
3636
}
3737

3838
pub fn show_paid_advance_menu(rc: &RenderContext) -> StateUpdate {

‎client/src/client_state.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -509,17 +509,17 @@ impl State {
509509
r.iter()
510510
.map(|p| {
511511
Payment::new(
512-
&p.model,
512+
&p.options,
513513
&game.get_player(game.active_player()).resources,
514514
&p.name,
515515
p.optional,
516516
)
517517
})
518518
.collect(),
519519
),
520-
CustomPhaseRequest::Reward(r) => ActiveDialog::CustomPhaseRewardRequest(
521-
Payment::new_gain(r.model.clone(), &r.name),
522-
),
520+
CustomPhaseRequest::Reward(r) => {
521+
ActiveDialog::CustomPhaseRewardRequest(Payment::new_gain(&r.options, &r.name))
522+
}
523523
};
524524
}
525525
match &game.state {

‎client/src/construct_ui.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use server::city::City;
77
use server::city_pieces::Building;
88
use server::content::custom_actions::CustomAction;
99
use server::map::Terrain;
10-
use server::payment::PaymentModel;
10+
use server::payment::PaymentOptions;
1111
use server::playing_actions::{Construct, PlayingAction, Recruit};
1212
use server::position::Position;
1313
use server::unit::UnitType;
@@ -113,7 +113,7 @@ impl ConstructionPayment {
113113
.unwrap()
114114
.cost
115115
.clone(),
116-
ConstructionProject::Units(sel) => PaymentModel::resources(
116+
ConstructionProject::Units(sel) => PaymentOptions::resources(
117117
sel.amount
118118
.units
119119
.clone()

‎client/src/happiness_ui.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ impl IncreaseHappinessConfig {
3434
let city = p.get_city(*pos).unwrap();
3535
p.increase_happiness_cost(city, *steps).unwrap()
3636
})
37-
.reduce(|a, b| a + b)
37+
.reduce(|mut a, b| {
38+
a.default += b.default;
39+
a
40+
})
3841
.unwrap();
3942

4043
let available = &p.resources;

‎client/src/payment_ui.rs

+23-121
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ use crate::resource_ui::{new_resource_map, resource_name};
66
use crate::select_ui;
77
use crate::select_ui::{CountSelector, HasCountSelectableObject};
88
use macroquad::math::{bool, vec2};
9-
use server::payment::{PaymentModel, SumPaymentOptions};
9+
use server::payment::PaymentOptions;
1010
use server::resource::ResourceType;
11-
use server::resource_pile::{PaymentOptions, ResourcePile};
12-
use std::cmp;
13-
use std::cmp::min;
11+
use server::resource_pile::ResourcePile;
1412

1513
#[derive(PartialEq, Eq, Debug, Clone)]
1614
pub struct ResourcePayment {
@@ -36,63 +34,32 @@ impl HasCountSelectableObject for ResourcePayment {
3634
#[derive(PartialEq, Eq, Debug, Clone)]
3735
pub struct Payment {
3836
pub name: String,
39-
pub model: PaymentModel,
37+
pub options: PaymentOptions,
4038
pub available: ResourcePile,
4139
pub optional: bool,
4240
pub current: Vec<ResourcePayment>,
43-
pub discount_used: u32,
4441
}
4542

4643
impl Payment {
4744
#[must_use]
4845
pub fn new(
49-
model: &PaymentModel,
46+
model: &PaymentOptions,
5047
available: &ResourcePile,
5148
name: &str,
5249
optional: bool,
5350
) -> Payment {
54-
let mut discount_used = 0;
55-
let resources = match model {
56-
PaymentModel::Sum(options) => sum_payment(options, available),
57-
PaymentModel::Resources(a) => {
58-
let options = a.get_payment_options(available);
59-
discount_used = a.discount - options.discount_left;
60-
resource_payment(&options)
61-
}
62-
};
63-
6451
Self {
6552
name: name.to_string(),
66-
model: model.clone(),
53+
options: model.clone(),
6754
available: available.clone(),
6855
optional,
69-
current: resources,
70-
discount_used,
56+
current: resource_payment(model, available),
7157
}
7258
}
7359

7460
#[must_use]
75-
pub fn new_gain(model: PaymentModel, name: &str) -> Payment {
76-
let sum = if let PaymentModel::Sum(m) = &model {
77-
m.clone()
78-
} else {
79-
panic!("No trade route reward")
80-
};
81-
let available = sum
82-
.types_by_preference
83-
.iter()
84-
.map(|t| ResourcePile::of(*t, sum.cost))
85-
.reduce(|a, b| a + b)
86-
.expect("sum");
87-
88-
Payment {
89-
name: name.to_string(),
90-
model,
91-
optional: false,
92-
current: sum_payment(&sum, &available),
93-
available,
94-
discount_used: 0,
95-
}
61+
pub fn new_gain(options: &PaymentOptions, name: &str) -> Payment {
62+
Self::new(options, &options.default, name, false)
9663
}
9764

9865
pub fn to_resource_pile(&self) -> ResourcePile {
@@ -157,12 +124,12 @@ pub fn multi_payment_dialog(
157124

158125
for (i, payment) in payments.iter().enumerate() {
159126
let name = &payment.name;
160-
let model = payment.model.clone();
161-
let types = show_types(&model);
127+
let options = payment.options.clone();
128+
let types = options.possible_resource_types();
162129
let offset = vec2(0., i as f32 * -100.);
163130
bottom_centered_text_with_offset(
164131
rc,
165-
&format!("{name} for {model}"),
132+
&format!("{name} for {options}"),
166133
offset + vec2(0., -30.),
167134
);
168135
let result = select_ui::count_dialog(
@@ -218,17 +185,17 @@ fn ok_tooltip(payments: &[Payment], mut available: ResourcePile) -> OkTooltip {
218185
let mut invalid: Vec<String> = vec![];
219186

220187
for payment in payments {
221-
let model = &payment.model;
188+
let options = &payment.options;
222189
let pile = payment.to_resource_pile();
223190
let name = &payment.name;
224191
let tooltip = if payment.optional && pile.is_empty() {
225192
OkTooltip::Valid(format!("Pay nothing for {name}"))
226-
} else if model.can_afford(&available) && model.is_valid_payment(&pile) {
193+
} else if options.can_afford(&available) && options.is_valid_payment(&pile) {
227194
// make sure that we can afford all the payments
228195
available -= payment.to_resource_pile();
229196
OkTooltip::Valid(format!("Pay {pile} for {name}"))
230197
} else {
231-
OkTooltip::Invalid(format!("You don't have {:?} for {}", payment.model, name))
198+
OkTooltip::Invalid(format!("You don't have {:?} for {}", payment.options, name))
232199
};
233200
match tooltip {
234201
OkTooltip::Valid(v) => valid.push(v),
@@ -255,46 +222,18 @@ fn replace_updated_payment(payment: &Payment, all: &[Payment]) -> Vec<Payment> {
255222
.collect::<Vec<_>>()
256223
}
257224

258-
fn sum_payment(a: &SumPaymentOptions, available: &ResourcePile) -> Vec<ResourcePayment> {
259-
let mut cost_left = a.cost;
260-
261-
a.types_by_preference
262-
.iter()
263-
.map(|t| {
264-
let have = available.get(*t);
265-
let used = min(have, cost_left);
266-
cost_left -= used;
267-
ResourcePayment::new(*t, used, 0, have)
268-
})
269-
.collect()
270-
}
271-
272225
#[must_use]
273-
fn resource_payment(options: &PaymentOptions) -> Vec<ResourcePayment> {
226+
fn resource_payment(options: &PaymentOptions, available: &ResourcePile) -> Vec<ResourcePayment> {
274227
let mut resources: Vec<ResourcePayment> = new_resource_map(&options.default)
275228
.into_iter()
276229
.map(|e| {
277230
let resource_type = e.0;
278-
let amount = e.1;
279-
match resource_type {
280-
ResourceType::Gold => ResourcePayment {
281-
resource: resource_type,
282-
selectable: CountSelector {
283-
current: amount,
284-
min: amount,
285-
max: amount,
286-
},
287-
},
288-
_ => ResourcePayment {
289-
resource: resource_type,
290-
selectable: CountSelector {
291-
current: amount,
292-
min: cmp::max(
293-
0,
294-
amount as i32 - options.discount_left as i32 - options.gold_left as i32,
295-
) as u32,
296-
max: amount,
297-
},
231+
ResourcePayment {
232+
resource: resource_type,
233+
selectable: CountSelector {
234+
current: e.1,
235+
min: 0,
236+
max: available.get(&resource_type),
298237
},
299238
}
300239
})
@@ -304,49 +243,12 @@ fn resource_payment(options: &PaymentOptions) -> Vec<ResourcePayment> {
304243
resources
305244
}
306245

307-
#[must_use]
308-
pub fn show_types(model: &PaymentModel) -> Vec<ResourceType> {
309-
match model {
310-
PaymentModel::Sum(options) => options.types_by_preference.clone(),
311-
PaymentModel::Resources(options) => options.cost.types(),
312-
}
313-
}
314-
315246
pub fn plus(mut payment: Payment, t: ResourceType) -> Payment {
316-
match payment.model {
317-
PaymentModel::Sum(_) => {
318-
payment.get_mut(t).selectable.current += 1;
319-
}
320-
PaymentModel::Resources(_) => {
321-
{
322-
let gold = payment.get_mut(ResourceType::Gold);
323-
if gold.selectable.current > 0 {
324-
gold.selectable.current -= 1;
325-
} else {
326-
payment.discount_used += 1;
327-
}
328-
}
329-
payment.get_mut(t).selectable.current += 1;
330-
}
331-
}
247+
payment.get_mut(t).selectable.current += 1;
332248
payment
333249
}
334250

335251
pub fn minus(mut payment: Payment, t: ResourceType) -> Payment {
336-
match payment.model {
337-
PaymentModel::Sum(_) => {
338-
payment.get_mut(t).selectable.current -= 1;
339-
}
340-
PaymentModel::Resources(_) => {
341-
{
342-
if payment.discount_used > 0 {
343-
payment.discount_used -= 1;
344-
} else {
345-
payment.get_mut(ResourceType::Gold).selectable.current += 1;
346-
}
347-
}
348-
payment.get_mut(t).selectable.current -= 1;
349-
}
350-
}
252+
payment.get_mut(t).selectable.current -= 1;
351253
payment
352254
}

‎server/src/ability_initializer.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ pub(crate) trait AbilityInitializerSetup: Sized {
193193
for (request, payment) in requests.iter().zip(payments.iter()) {
194194
let zero_payment = payment.is_empty() && request.optional;
195195
assert!(
196-
zero_payment || request.model.is_valid_payment(payment),
196+
zero_payment || request.options.is_valid_payment(payment),
197197
"Invalid payment"
198198
);
199199
game.players[player_index].loose_resources(payment.clone());
@@ -226,13 +226,13 @@ pub(crate) trait AbilityInitializerSetup: Sized {
226226
move |game, player_index, _player_name| {
227227
let req = request(game, player_index);
228228
if let Some(r) = &req {
229-
if r.model.possible_resource_types().len() == 1 {
229+
if r.options.possible_resource_types().len() == 1 {
230230
let player_name = game.players[player_index].get_name();
231231
g(
232232
game,
233233
player_index,
234234
&player_name,
235-
&r.model.default_payment(),
235+
&r.options.default_payment(),
236236
false,
237237
);
238238
return None;
@@ -243,7 +243,7 @@ pub(crate) trait AbilityInitializerSetup: Sized {
243243
move |game, player_index, player_name, action, request| {
244244
if let CustomPhaseRequest::Reward(request) = &request {
245245
if let CustomPhaseEventAction::Reward(reward) = action {
246-
assert!(request.model.is_valid_payment(&reward), "Invalid payment");
246+
assert!(request.options.is_valid_payment(&reward), "Invalid payment");
247247
gain_reward(game, player_index, player_name, &reward, true);
248248
return;
249249
}

‎server/src/content/advances.rs

+26-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::combat::{Combat, CombatModifier, CombatStrength};
88
use crate::content::custom_phase_actions::{CustomPhasePaymentRequest, CustomPhaseRewardRequest};
99
use crate::content::trade_routes::{gain_trade_route_reward, trade_route_reward};
1010
use crate::game::GameState;
11-
use crate::payment::PaymentModel;
11+
use crate::payment::{PaymentConversion, PaymentOptions};
1212
use crate::playing_actions::{PlayingAction, PlayingActionType};
1313
use crate::position::Position;
1414
use crate::resource::ResourceType;
@@ -27,7 +27,6 @@ pub const NAVIGATION: &str = "Navigation";
2727
pub const ROADS: &str = "Roads";
2828
pub const STEEL_WEAPONS: &str = "Steel Weapons";
2929
pub const METALLURGY: &str = "Metallurgy";
30-
pub const RITUALS: &str = "Rituals";
3130
pub const TACTICS: &str = "Tactics";
3231
pub const BARTERING: &str = "Bartering";
3332
pub const CURRENCY: &str = "Currency";
@@ -304,8 +303,8 @@ fn warfare() -> Vec<Advance> {
304303
|game, player_index| {
305304
let GameState::Combat(c) = &game.state else { panic!("Invalid state") };
306305

307-
let extra_die = PaymentModel::sum(2, vec![ResourceType::Wood, ResourceType::Gold]);
308-
let ignore_hit = PaymentModel::sum(2, vec![ResourceType::Ore, ResourceType::Gold]);
306+
let extra_die = PaymentOptions::sum(2, &[ResourceType::Wood, ResourceType::Gold]);
307+
let ignore_hit = PaymentOptions::sum(2, &[ResourceType::Ore, ResourceType::Gold]);
309308

310309
let player = &game.players[player_index];
311310
if game
@@ -315,12 +314,12 @@ fn warfare() -> Vec<Advance> {
315314
{
316315
Some(vec![
317316
CustomPhasePaymentRequest {
318-
model: extra_die,
317+
options: extra_die,
319318
name: "Cancel fortress ability to add an extra die in the first round of combat".to_string(),
320319
optional: true,
321320
},
322321
CustomPhasePaymentRequest {
323-
model: ignore_hit,
322+
options: ignore_hit,
324323
name: "Cancel fortress ability to ignore the first hit in the first round of combat".to_string(),
325324
optional: true,
326325
},
@@ -374,7 +373,7 @@ fn warfare() -> Vec<Advance> {
374373

375374
if player.can_afford(&cost) {
376375
Some(vec![CustomPhasePaymentRequest {
377-
model: cost,
376+
options: cost,
378377
name: "Use steel weapons".to_string(),
379378
optional: true,
380379
}])
@@ -407,14 +406,14 @@ fn add_steel_weapons(player_index: usize, c: &mut Combat) {
407406
}
408407

409408
#[must_use]
410-
fn steel_weapons_cost(game: &Game, combat: &Combat, player_index: usize) -> PaymentModel {
409+
fn steel_weapons_cost(game: &Game, combat: &Combat, player_index: usize) -> PaymentOptions {
411410
let player = &game.players[player_index];
412411
let attacker = &game.players[combat.attacker];
413412
let defender = &game.players[combat.defender];
414413
let both_steel_weapons =
415414
attacker.has_advance(STEEL_WEAPONS) && defender.has_advance(STEEL_WEAPONS);
416415
let cost = u32::from(!player.has_advance(METALLURGY) || both_steel_weapons);
417-
PaymentModel::sum(cost, vec![ResourceType::Ore, ResourceType::Gold])
416+
PaymentOptions::sum(cost, &[ResourceType::Ore, ResourceType::Gold])
418417
}
419418

420419
fn fortress(s: &mut CombatStrength, c: &Combat, game: &Game) {
@@ -464,8 +463,23 @@ fn spirituality() -> Vec<Advance> {
464463
Advance::builder("Myths", "not implemented")
465464
.with_advance_bonus(MoodToken)
466465
.with_unlocked_building(Temple),
467-
Advance::builder(RITUALS, "When you perform the Increase Happiness Action you may spend any Resources as a substitute for mood tokens. This is done at a 1:1 ratio")
468-
.with_advance_bonus(CultureToken),
466+
Advance::builder("Rituals", "When you perform the Increase Happiness Action you may spend any Resources as a substitute for mood tokens. This is done at a 1:1 ratio")
467+
.with_advance_bonus(CultureToken)
468+
.add_player_event_listener(
469+
|event| &mut event.happiness_cost,
470+
|cost, (), ()| {
471+
for r in &[
472+
ResourceType::Food,
473+
ResourceType::Wood,
474+
ResourceType::Ore,
475+
ResourceType::Ideas,
476+
ResourceType::Gold,
477+
] {
478+
cost.conversions.push(PaymentConversion::unlimited(vec![ResourcePile::mood_tokens(1)],ResourcePile::of(*r, 1)));
479+
}
480+
},
481+
0,
482+
),
469483
],
470484
)
471485
}
@@ -499,7 +513,7 @@ fn trade_routes() -> AdvanceBuilder {
499513
|game, _player_index| {
500514
trade_route_reward(game).map(|(reward, _routes)| {
501515
CustomPhaseRewardRequest {
502-
model: reward,
516+
options: reward,
503517
name: "Collect trade routes reward".to_string(),
504518
}
505519
})

‎server/src/content/custom_phase_actions.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::ability_initializer::EventOrigin;
22
use crate::game::{Game, UndoContext};
3-
use crate::payment::PaymentModel;
3+
use crate::payment::PaymentOptions;
44
use crate::resource_pile::ResourcePile;
55
use serde::{Deserialize, Serialize};
66

@@ -20,14 +20,14 @@ impl CustomPhaseEventType {
2020

2121
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
2222
pub struct CustomPhasePaymentRequest {
23-
pub model: PaymentModel,
23+
pub options: PaymentOptions,
2424
pub name: String,
2525
pub optional: bool,
2626
}
2727

2828
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
2929
pub struct CustomPhaseRewardRequest {
30-
pub model: PaymentModel,
30+
pub options: PaymentOptions,
3131
pub name: String,
3232
}
3333

‎server/src/content/trade_routes.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::city::{City, MoodState};
22
use crate::content::advances::CURRENCY;
33
use crate::game::Game;
4-
use crate::payment::PaymentModel;
4+
use crate::payment::PaymentOptions;
55
use crate::player::Player;
66
use crate::position::Position;
77
use crate::resource::ResourceType;
@@ -16,7 +16,7 @@ pub struct TradeRoute {
1616
}
1717

1818
#[must_use]
19-
pub fn trade_route_reward(game: &Game) -> Option<(PaymentModel, Vec<TradeRoute>)> {
19+
pub fn trade_route_reward(game: &Game) -> Option<(PaymentOptions, Vec<TradeRoute>)> {
2020
let p = game.current_player_index;
2121
let trade_routes = find_trade_routes(game, &game.players[p]);
2222
if trade_routes.is_empty() {
@@ -25,12 +25,12 @@ pub fn trade_route_reward(game: &Game) -> Option<(PaymentModel, Vec<TradeRoute>)
2525

2626
Some((
2727
if game.players[p].has_advance(CURRENCY) {
28-
PaymentModel::sum(
28+
PaymentOptions::sum(
2929
trade_routes.len() as u32,
30-
vec![ResourceType::Gold, ResourceType::Food],
30+
&[ResourceType::Gold, ResourceType::Food],
3131
)
3232
} else {
33-
PaymentModel::sum(trade_routes.len() as u32, vec![ResourceType::Food])
33+
PaymentOptions::sum(trade_routes.len() as u32, &[ResourceType::Food])
3434
},
3535
trade_routes,
3636
))

‎server/src/content/wonders.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::ability_initializer::AbilityInitializerSetup;
22
use crate::content::advances::IRRIGATION;
33
use crate::game::Game;
44
use crate::map::Terrain::Fertile;
5-
use crate::payment::PaymentModel;
5+
use crate::payment::PaymentOptions;
66
use crate::position::Position;
77
use crate::{resource_pile::ResourcePile, wonder::Wonder};
88
use std::collections::HashSet;
@@ -14,15 +14,15 @@ pub fn get_all() -> Vec<Wonder> {
1414
Wonder::builder(
1515
"Pyramids",
1616
"todo",
17-
PaymentModel::resources_with_discount(ResourcePile::new(3, 3, 3, 0, 0, 0, 4), 1),
17+
PaymentOptions::resources_with_discount(ResourcePile::new(3, 3, 3, 0, 0, 0, 4), 1),
1818
vec![],
1919
)
2020
.build(),
2121
// add other effects
2222
Wonder::builder(
2323
"Great Gardens",
2424
"The city with this wonder may Collect any type of resource from Grassland spaces including ideas and gold.",
25-
PaymentModel::resources_with_discount(ResourcePile::new(5, 5, 2, 0, 0, 0, 5), 0),
25+
PaymentOptions::resources(ResourcePile::new(5, 5, 2, 0, 0, 0, 5)),
2626
vec![IRRIGATION],
2727
)
2828
.add_player_event_listener(

‎server/src/game.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1859,7 +1859,7 @@ pub mod tests {
18591859

18601860
use super::{Game, GameState::Playing};
18611861
use crate::content::custom_phase_actions::CustomPhaseEventState;
1862-
use crate::payment::PaymentModel;
1862+
use crate::payment::PaymentOptions;
18631863
use crate::utils::tests::FloatEq;
18641864
use crate::{
18651865
city::{City, MoodState::*},
@@ -1909,7 +1909,7 @@ pub mod tests {
19091909
let old = Player::new(civilizations::tests::get_test_civilization(), 0);
19101910
let new = Player::new(civilizations::tests::get_test_civilization(), 1);
19111911

1912-
let wonder = Wonder::builder("wonder", "test", PaymentModel::free(), vec![]).build();
1912+
let wonder = Wonder::builder("wonder", "test", PaymentOptions::free(), vec![]).build();
19131913
let mut game = test_game();
19141914
game.players.push(old);
19151915
game.players.push(new);

‎server/src/payment.rs

+342-116
Large diffs are not rendered by default.

‎server/src/player.rs

+17-25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use crate::advance::Advance;
2-
use crate::content::advances::{get_advance_by_name, RITUALS};
2+
use crate::content::advances::get_advance_by_name;
33
use crate::game::CurrentMove;
44
use crate::game::GameState::Movement;
55
use crate::movement::move_routes;
66
use crate::movement::{is_valid_movement_type, MoveRoute};
7-
use crate::payment::PaymentModel;
7+
use crate::payment::PaymentOptions;
88
use crate::resource::ResourceType;
99
use crate::unit::{carried_units, get_current_move, MovementRestriction, UnitData};
1010
use crate::{
@@ -423,11 +423,11 @@ impl Player {
423423

424424
#[must_use]
425425
pub fn can_afford_resources(&self, cost: &ResourcePile) -> bool {
426-
self.can_afford(&PaymentModel::resources(cost.clone()))
426+
self.can_afford(&PaymentOptions::resources(cost.clone()))
427427
}
428428

429429
#[must_use]
430-
pub fn can_afford(&self, cost: &PaymentModel) -> bool {
430+
pub fn can_afford(&self, cost: &PaymentOptions) -> bool {
431431
cost.can_afford(&self.resources)
432432
}
433433

@@ -581,16 +581,16 @@ impl Player {
581581
}
582582

583583
#[must_use]
584-
pub fn construct_cost(&self, building: Building, city: &City) -> PaymentModel {
584+
pub fn construct_cost(&self, building: Building, city: &City) -> PaymentOptions {
585585
let mut cost = CONSTRUCT_COST;
586586
self.get_events()
587587
.construct_cost
588588
.trigger(&mut cost, city, &building);
589-
PaymentModel::resources(cost)
589+
PaymentOptions::resources(cost)
590590
}
591591

592592
#[must_use]
593-
pub fn wonder_cost(&self, wonder: &Wonder, city: &City) -> PaymentModel {
593+
pub fn wonder_cost(&self, wonder: &Wonder, city: &City) -> PaymentOptions {
594594
let mut cost = wonder.cost.clone();
595595
self.get_events()
596596
.wonder_cost
@@ -599,37 +599,29 @@ impl Player {
599599
}
600600

601601
#[must_use]
602-
pub fn increase_happiness_cost(&self, city: &City, steps: u32) -> Option<PaymentModel> {
602+
pub fn increase_happiness_cost(&self, city: &City, steps: u32) -> Option<PaymentOptions> {
603603
let max_steps = 2 - city.mood_state.clone() as u32;
604604
let cost = city.size() as u32 * steps;
605605
if steps > max_steps {
606606
None
607-
} else if self.has_advance(RITUALS) {
608-
Some(PaymentModel::sum(
609-
cost,
610-
vec![
611-
ResourceType::Food,
612-
ResourceType::Wood,
613-
ResourceType::Ore,
614-
ResourceType::Ideas,
615-
ResourceType::MoodTokens,
616-
ResourceType::Gold,
617-
],
618-
))
619607
} else {
620-
Some(PaymentModel::sum(cost, vec![ResourceType::MoodTokens]))
608+
let mut options = PaymentOptions::sum(cost, &[ResourceType::MoodTokens]);
609+
self.get_events()
610+
.happiness_cost
611+
.trigger(&mut options, &(), &());
612+
Some(options)
621613
}
622614
}
623615

624616
#[must_use]
625-
pub fn advance_cost(&self, advance: &str) -> PaymentModel {
617+
pub fn advance_cost(&self, advance: &str) -> PaymentOptions {
626618
let mut cost = ADVANCE_COST;
627619
self.get_events()
628620
.advance_cost
629621
.trigger(&mut cost, &advance.to_string(), &());
630-
PaymentModel::sum(
622+
PaymentOptions::sum(
631623
cost,
632-
vec![ResourceType::Ideas, ResourceType::Food, ResourceType::Gold],
624+
&[ResourceType::Ideas, ResourceType::Food, ResourceType::Gold],
633625
)
634626
}
635627

@@ -780,7 +772,7 @@ impl Player {
780772
if !city.can_activate() {
781773
return false;
782774
}
783-
let cost = PaymentModel::resources(units.iter().map(UnitType::cost).sum());
775+
let cost = PaymentOptions::resources(units.iter().map(UnitType::cost).sum());
784776
if !self.can_afford(&cost) {
785777
return false;
786778
}

‎server/src/player_events.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::content::custom_phase_actions::{
66
};
77
use crate::game::Game;
88
use crate::map::Terrain;
9-
use crate::payment::PaymentModel;
9+
use crate::payment::PaymentOptions;
1010
use crate::playing_actions::PlayingActionType;
1111
use crate::{
1212
city::City, city_pieces::Building, events::EventMut, player::Player, position::Position,
@@ -23,12 +23,13 @@ pub(crate) struct PlayerEvents {
2323
pub construct_cost: EventMut<ResourcePile, City, Building>,
2424
pub on_construct_wonder: EventMut<Player, Position, Wonder>,
2525
pub on_undo_construct_wonder: EventMut<Player, Position, Wonder>,
26-
pub wonder_cost: EventMut<PaymentModel, City, Wonder>,
26+
pub wonder_cost: EventMut<PaymentOptions, City, Wonder>,
2727
pub on_advance: EventMut<Player, String, ()>,
2828
pub on_undo_advance: EventMut<Player, String, ()>,
2929
pub after_execute_action: EventMut<Player, Action, ()>,
3030
pub before_undo_action: EventMut<Player, Action, ()>,
3131
pub advance_cost: EventMut<u32, String>,
32+
pub happiness_cost: EventMut<PaymentOptions, (), ()>,
3233
pub is_playing_action_available: EventMut<bool, PlayingActionType, Player>,
3334
pub terrain_collect_options: EventMut<HashMap<Terrain, HashSet<ResourcePile>>, (), ()>,
3435
pub collect_options: EventMut<HashMap<Position, HashSet<ResourcePile>>, CollectContext, Game>,

‎server/src/playing_actions.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::action::Action;
66
use crate::city::MoodState;
77
use crate::collect::{collect, undo_collect};
88
use crate::game::{CulturalInfluenceResolution, GameState};
9-
use crate::payment::PaymentModel;
9+
use crate::payment::PaymentOptions;
1010
use crate::unit::Unit;
1111
use crate::{
1212
city::City,
@@ -185,7 +185,7 @@ impl PlayingAction {
185185
collect(game, player_index, &c);
186186
}
187187
Recruit(r) => {
188-
let cost = PaymentModel::resources(
188+
let cost = PaymentOptions::resources(
189189
r.units.iter().map(UnitType::cost).sum::<ResourcePile>(),
190190
);
191191
let player = &mut game.players[player_index];
@@ -404,7 +404,7 @@ impl ActionType {
404404

405405
pub(crate) fn increase_happiness(game: &mut Game, player_index: usize, i: IncreaseHappiness) {
406406
let player = &mut game.players[player_index];
407-
let mut total_cost: PaymentModel = PaymentModel::free();
407+
let mut total_cost = PaymentOptions::free();
408408
let mut angry_activations = vec![];
409409
for (city_position, steps) in i.happiness_increases {
410410
let city = player.get_city(city_position).expect("Illegal action");
@@ -420,7 +420,7 @@ pub(crate) fn increase_happiness(game: &mut Game, player_index: usize, i: Increa
420420
if total_cost.is_free() {
421421
total_cost = cost;
422422
} else {
423-
total_cost = total_cost + cost;
423+
total_cost.default += cost.default;
424424
}
425425
let city = player.get_city_mut(city_position).expect("Illegal action");
426426
for _ in 0..steps {

‎server/src/resource_pile.rs

+16-114
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use crate::resource::ResourceType;
22
use crate::utils;
33
use serde::{Deserialize, Serialize};
44
use std::{
5-
cmp,
65
fmt::Display,
76
iter::Sum,
87
ops::{Add, AddAssign, Mul, SubAssign},
@@ -68,7 +67,7 @@ impl ResourcePile {
6867
}
6968

7069
#[must_use]
71-
pub fn get(&self, resource_type: ResourceType) -> u32 {
70+
pub fn get(&self, resource_type: &ResourceType) -> u32 {
7271
match resource_type {
7372
ResourceType::Food => self.food,
7473
ResourceType::Wood => self.wood,
@@ -80,6 +79,17 @@ impl ResourcePile {
8079
}
8180
}
8281

82+
#[must_use]
83+
pub fn has_at_least(&self, other: &ResourcePile, times: u32) -> bool {
84+
self.food >= other.food * times
85+
&& self.wood >= other.wood * times
86+
&& self.ore >= other.ore * times
87+
&& self.ideas >= other.ideas * times
88+
&& self.gold >= other.gold * times
89+
&& self.mood_tokens >= other.mood_tokens * times
90+
&& self.culture_tokens >= other.culture_tokens * times
91+
}
92+
8393
///
8494
/// # Panics
8595
/// Panics if `resource_type` is `Discount`
@@ -362,113 +372,27 @@ impl CostWithDiscount {
362372
&& available.mood_tokens >= cost.mood_tokens
363373
&& available.culture_tokens >= cost.culture_tokens
364374
}
365-
366-
//this function assumes that `self` can afford `cost`
367-
#[must_use]
368-
pub fn get_payment_options(&self, available: &ResourcePile) -> PaymentOptions {
369-
let cost = &self.cost;
370-
let mut jokers_left = self.discount;
371-
372-
let mut gold_left = available.gold;
373-
let mut gold_cost = cost.gold;
374-
gold_left -= gold_cost;
375-
376-
if cost.food > available.food {
377-
let joker_cost = cost.food - available.food;
378-
if joker_cost > jokers_left {
379-
gold_left -= joker_cost - jokers_left;
380-
gold_cost += joker_cost - jokers_left;
381-
}
382-
jokers_left = jokers_left.saturating_sub(joker_cost);
383-
}
384-
if cost.wood > available.wood {
385-
let joker_cost = cost.wood - available.wood;
386-
if joker_cost > jokers_left {
387-
gold_left -= joker_cost - jokers_left;
388-
gold_cost += joker_cost - jokers_left;
389-
}
390-
jokers_left = jokers_left.saturating_sub(joker_cost);
391-
}
392-
if cost.ore > available.ore {
393-
let joker_cost = cost.ore - available.ore;
394-
if joker_cost > jokers_left {
395-
gold_left -= joker_cost - jokers_left;
396-
gold_cost += joker_cost - jokers_left;
397-
}
398-
jokers_left = jokers_left.saturating_sub(joker_cost);
399-
}
400-
if cost.ideas > available.ideas {
401-
let joker_cost = cost.ideas - available.ideas;
402-
if joker_cost > jokers_left {
403-
gold_left -= joker_cost - jokers_left;
404-
gold_cost += joker_cost - jokers_left;
405-
}
406-
jokers_left = jokers_left.saturating_sub(joker_cost);
407-
}
408-
let default = ResourcePile::new(
409-
cmp::min(cost.food, available.food),
410-
cmp::min(cost.wood, available.wood),
411-
cmp::min(cost.ore, available.ore),
412-
cmp::min(cost.ideas, available.ideas),
413-
gold_cost,
414-
cost.mood_tokens,
415-
cost.culture_tokens,
416-
);
417-
PaymentOptions::new(default, gold_left, jokers_left)
418-
}
419-
}
420-
421-
#[derive(PartialEq, Eq, Debug, Clone)]
422-
pub struct PaymentOptions {
423-
pub default: ResourcePile,
424-
pub gold_left: u32,
425-
pub discount_left: u32,
426-
}
427-
428-
impl PaymentOptions {
429-
#[must_use]
430-
pub fn new(default: ResourcePile, gold_left: u32, discount_left: u32) -> Self {
431-
Self {
432-
default,
433-
gold_left,
434-
discount_left,
435-
}
436-
}
437375
}
438376

439377
#[cfg(test)]
440378
mod tests {
441-
use super::{CostWithDiscount, PaymentOptions, ResourcePile};
442-
use crate::payment::PaymentModel;
379+
use super::ResourcePile;
380+
use crate::payment::PaymentOptions;
443381

444382
fn assert_can_afford(name: &str, cost: &ResourcePile, discount: u32) {
445383
let player_has = ResourcePile::new(1, 2, 3, 4, 5, 6, 7);
446384
let can_afford =
447-
PaymentModel::resources_with_discount(cost.clone(), discount).can_afford(&player_has);
385+
PaymentOptions::resources_with_discount(cost.clone(), discount).can_afford(&player_has);
448386
assert!(can_afford, "{name}");
449387
}
450388

451389
fn assert_cannot_afford(name: &str, cost: &ResourcePile, discount: u32) {
452390
let player_has = ResourcePile::new(1, 2, 3, 4, 5, 6, 7);
453391
let can_afford =
454-
PaymentModel::resources_with_discount(cost.clone(), discount).can_afford(&player_has);
392+
PaymentOptions::resources_with_discount(cost.clone(), discount).can_afford(&player_has);
455393
assert!(!can_afford, "{name}");
456394
}
457395

458-
fn assert_payment_options(
459-
name: &str,
460-
cost: &ResourcePile,
461-
discount: u32,
462-
want: &PaymentOptions,
463-
) {
464-
let budget = ResourcePile::new(1, 2, 3, 4, 5, 6, 7);
465-
let c = CostWithDiscount {
466-
cost: cost.clone(),
467-
discount,
468-
};
469-
assert_eq!(want, &c.get_payment_options(&budget), "{name}");
470-
}
471-
472396
fn assert_to_string(resource_pile: &ResourcePile, expected: &str) {
473397
assert_eq!(
474398
expected.to_string(),
@@ -521,28 +445,6 @@ mod tests {
521445
assert_eq!(ResourcePile::new(0, 1, 2, 0, 0, 0, 0), waste);
522446
}
523447

524-
#[test]
525-
fn payment_options_test() {
526-
assert_payment_options(
527-
"no gold use",
528-
&ResourcePile::new(1, 1, 3, 2, 0, 2, 4),
529-
0,
530-
&(PaymentOptions::new(ResourcePile::new(1, 1, 3, 2, 0, 2, 4), 5, 0)),
531-
);
532-
assert_payment_options(
533-
"use some gold",
534-
&ResourcePile::new(2, 2, 3, 5, 2, 0, 0),
535-
0,
536-
&(PaymentOptions::new(ResourcePile::new(1, 2, 3, 4, 4, 0, 0), 1, 0)),
537-
);
538-
assert_payment_options(
539-
"jokers",
540-
&(ResourcePile::ore(4) + ResourcePile::ideas(4)),
541-
3,
542-
&(PaymentOptions::new(ResourcePile::ore(3) + ResourcePile::ideas(4), 5, 2)),
543-
);
544-
}
545-
546448
#[test]
547449
fn resource_pile_display_test() {
548450
assert_to_string(&ResourcePile::empty(), "nothing");

‎server/src/status_phase.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use itertools::Itertools;
22
use serde::{Deserialize, Serialize};
33

4-
use crate::payment::PaymentModel;
4+
use crate::payment::PaymentOptions;
55
use crate::{
66
content::advances,
77
game::{Game, GameState::*},
@@ -202,7 +202,7 @@ fn play_status_phase_for_player(
202202
player.cities.len() > 1 && player.cities.iter().any(|city| city.size() == 1)
203203
}
204204
StatusPhaseState::ChangeGovernmentType => {
205-
let cost = &PaymentModel::resources(CHANGE_GOVERNMENT_COST);
205+
let cost = &PaymentOptions::resources(CHANGE_GOVERNMENT_COST);
206206
player.can_afford(cost)
207207
&& player.government().is_some_and(|government| {
208208
advances::get_governments().iter().any(|(g, a)| {

‎server/src/wonder.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::ability_initializer::EventOrigin;
2-
use crate::payment::PaymentModel;
2+
use crate::payment::PaymentOptions;
33
use crate::{
44
ability_initializer::{self, AbilityInitializer, AbilityInitializerSetup},
55
game::Game,
@@ -11,7 +11,7 @@ type PlacementChecker = Box<dyn Fn(Position, &Game) -> bool>;
1111
pub struct Wonder {
1212
pub name: String,
1313
pub description: String,
14-
pub cost: PaymentModel,
14+
pub cost: PaymentOptions,
1515
pub required_advances: Vec<String>,
1616
pub placement_requirement: Option<PlacementChecker>,
1717
pub player_initializer: AbilityInitializer,
@@ -24,7 +24,7 @@ impl Wonder {
2424
pub fn builder(
2525
name: &str,
2626
description: &str,
27-
cost: PaymentModel,
27+
cost: PaymentOptions,
2828
required_advances: Vec<&str>,
2929
) -> WonderBuilder {
3030
WonderBuilder::new(
@@ -42,7 +42,7 @@ impl Wonder {
4242
pub struct WonderBuilder {
4343
name: String,
4444
descriptions: Vec<String>,
45-
cost: PaymentModel,
45+
cost: PaymentOptions,
4646
required_advances: Vec<String>,
4747
placement_requirement: Option<PlacementChecker>,
4848
player_initializers: Vec<AbilityInitializer>,
@@ -55,7 +55,7 @@ impl WonderBuilder {
5555
fn new(
5656
name: &str,
5757
description: &str,
58-
cost: PaymentModel,
58+
cost: PaymentOptions,
5959
required_advances: Vec<String>,
6060
) -> Self {
6161
Self {

‎server/tests/test_games/combat_all_modifiers.outcome.json

+46-38
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
{
22
"state": {
33
"Combat": {
4-
"initiation": {
5-
"Movement": {
6-
"movement_actions_left": 2,
7-
"moved_units": [
8-
0,
9-
1,
10-
2,
11-
3,
12-
4,
13-
5
14-
]
15-
}
16-
},
17-
"round": 1,
18-
"phase": "Retreat",
19-
"defender": 1,
20-
"defender_position": "C1",
21-
"attacker": 0,
22-
"attacker_position": "C2",
23-
"attackers": [
24-
0,
25-
1,
26-
2,
27-
3,
28-
4,
29-
5
30-
],
31-
"can_retreat": true
32-
}
4+
"initiation": {
5+
"Movement": {
6+
"movement_actions_left": 2,
7+
"moved_units": [
8+
0,
9+
1,
10+
2,
11+
3,
12+
4,
13+
5
14+
]
15+
}
16+
},
17+
"round": 1,
18+
"phase": "Retreat",
19+
"defender": 1,
20+
"defender_position": "C1",
21+
"attacker": 0,
22+
"attacker_position": "C2",
23+
"attackers": [
24+
0,
25+
1,
26+
2,
27+
3,
28+
4,
29+
5
30+
],
31+
"can_retreat": true
32+
}
3333
},
3434
"state_change_event_state": {
3535
"event_used": [],
@@ -41,14 +41,22 @@
4141
"request": {
4242
"Payment": [
4343
{
44-
"model": {
45-
"Sum": {
46-
"cost": 1,
47-
"types_by_preference": [
48-
"Ore",
49-
"Gold"
50-
]
51-
}
44+
"options": {
45+
"default": {
46+
"ore": 1
47+
},
48+
"conversions": [
49+
{
50+
"from": [
51+
{
52+
"ore": 1
53+
}
54+
],
55+
"to": {
56+
"gold": 1
57+
}
58+
}
59+
]
5260
},
5361
"name": "Use steel weapons",
5462
"optional": true
@@ -407,4 +415,4 @@
407415
}
408416
}
409417
]
410-
}
418+
}

‎server/tests/test_games/combat_all_modifiers.outcome1.json

+48-24
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,43 @@
4444
"request": {
4545
"Payment": [
4646
{
47-
"model": {
48-
"Sum": {
49-
"cost": 2,
50-
"types_by_preference": [
51-
"Wood",
52-
"Gold"
53-
]
54-
}
47+
"options": {
48+
"default": {
49+
"wood": 2
50+
},
51+
"conversions": [
52+
{
53+
"from": [
54+
{
55+
"wood": 1
56+
}
57+
],
58+
"to": {
59+
"gold": 1
60+
}
61+
}
62+
]
5563
},
5664
"name": "Cancel fortress ability to add an extra die in the first round of combat",
5765
"optional": true
5866
},
5967
{
60-
"model": {
61-
"Sum": {
62-
"cost": 2,
63-
"types_by_preference": [
64-
"Ore",
65-
"Gold"
66-
]
67-
}
68+
"options": {
69+
"default": {
70+
"ore": 2
71+
},
72+
"conversions": [
73+
{
74+
"from": [
75+
{
76+
"ore": 1
77+
}
78+
],
79+
"to": {
80+
"gold": 1
81+
}
82+
}
83+
]
6884
},
6985
"name": "Cancel fortress ability to ignore the first hit in the first round of combat",
7086
"optional": true
@@ -443,14 +459,22 @@
443459
"request": {
444460
"Payment": [
445461
{
446-
"model": {
447-
"Sum": {
448-
"cost": 1,
449-
"types_by_preference": [
450-
"Ore",
451-
"Gold"
452-
]
453-
}
462+
"options": {
463+
"default": {
464+
"ore": 1
465+
},
466+
"conversions": [
467+
{
468+
"from": [
469+
{
470+
"ore": 1
471+
}
472+
],
473+
"to": {
474+
"gold": 1
475+
}
476+
}
477+
]
454478
},
455479
"name": "Use steel weapons",
456480
"optional": true

‎server/tests/test_games/combat_all_modifiers.outcome2.json

+64-32
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,22 @@
4747
"request": {
4848
"Payment": [
4949
{
50-
"model": {
51-
"Sum": {
52-
"cost": 1,
53-
"types_by_preference": [
54-
"Ore",
55-
"Gold"
56-
]
57-
}
50+
"options": {
51+
"default": {
52+
"ore": 1
53+
},
54+
"conversions": [
55+
{
56+
"from": [
57+
{
58+
"ore": 1
59+
}
60+
],
61+
"to": {
62+
"gold": 1
63+
}
64+
}
65+
]
5866
},
5967
"name": "Use steel weapons",
6068
"optional": true
@@ -444,14 +452,22 @@
444452
"request": {
445453
"Payment": [
446454
{
447-
"model": {
448-
"Sum": {
449-
"cost": 1,
450-
"types_by_preference": [
451-
"Ore",
452-
"Gold"
453-
]
454-
}
455+
"options": {
456+
"default": {
457+
"ore": 1
458+
},
459+
"conversions": [
460+
{
461+
"from": [
462+
{
463+
"ore": 1
464+
}
465+
],
466+
"to": {
467+
"gold": 1
468+
}
469+
}
470+
]
455471
},
456472
"name": "Use steel weapons",
457473
"optional": true
@@ -476,27 +492,43 @@
476492
"request": {
477493
"Payment": [
478494
{
479-
"model": {
480-
"Sum": {
481-
"cost": 2,
482-
"types_by_preference": [
483-
"Wood",
484-
"Gold"
485-
]
486-
}
495+
"options": {
496+
"default": {
497+
"wood": 2
498+
},
499+
"conversions": [
500+
{
501+
"from": [
502+
{
503+
"wood": 1
504+
}
505+
],
506+
"to": {
507+
"gold": 1
508+
}
509+
}
510+
]
487511
},
488512
"name": "Cancel fortress ability to add an extra die in the first round of combat",
489513
"optional": true
490514
},
491515
{
492-
"model": {
493-
"Sum": {
494-
"cost": 2,
495-
"types_by_preference": [
496-
"Ore",
497-
"Gold"
498-
]
499-
}
516+
"options": {
517+
"default": {
518+
"ore": 2
519+
},
520+
"conversions": [
521+
{
522+
"from": [
523+
{
524+
"ore": 1
525+
}
526+
],
527+
"to": {
528+
"gold": 1
529+
}
530+
}
531+
]
500532
},
501533
"name": "Cancel fortress ability to ignore the first hit in the first round of combat",
502534
"optional": true

‎server/tests/test_games/combat_all_modifiers.outcome3.json

+64-32
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,22 @@
393393
"request": {
394394
"Payment": [
395395
{
396-
"model": {
397-
"Sum": {
398-
"cost": 1,
399-
"types_by_preference": [
400-
"Ore",
401-
"Gold"
402-
]
403-
}
396+
"options": {
397+
"default": {
398+
"ore": 1
399+
},
400+
"conversions": [
401+
{
402+
"from": [
403+
{
404+
"ore": 1
405+
}
406+
],
407+
"to": {
408+
"gold": 1
409+
}
410+
}
411+
]
404412
},
405413
"name": "Use steel weapons",
406414
"optional": true
@@ -425,27 +433,43 @@
425433
"request": {
426434
"Payment": [
427435
{
428-
"model": {
429-
"Sum": {
430-
"cost": 2,
431-
"types_by_preference": [
432-
"Wood",
433-
"Gold"
434-
]
435-
}
436+
"options": {
437+
"default": {
438+
"wood": 2
439+
},
440+
"conversions": [
441+
{
442+
"from": [
443+
{
444+
"wood": 1
445+
}
446+
],
447+
"to": {
448+
"gold": 1
449+
}
450+
}
451+
]
436452
},
437453
"name": "Cancel fortress ability to add an extra die in the first round of combat",
438454
"optional": true
439455
},
440456
{
441-
"model": {
442-
"Sum": {
443-
"cost": 2,
444-
"types_by_preference": [
445-
"Ore",
446-
"Gold"
447-
]
448-
}
457+
"options": {
458+
"default": {
459+
"ore": 2
460+
},
461+
"conversions": [
462+
{
463+
"from": [
464+
{
465+
"ore": 1
466+
}
467+
],
468+
"to": {
469+
"gold": 1
470+
}
471+
}
472+
]
449473
},
450474
"name": "Cancel fortress ability to ignore the first hit in the first round of combat",
451475
"optional": true
@@ -472,14 +496,22 @@
472496
"request": {
473497
"Payment": [
474498
{
475-
"model": {
476-
"Sum": {
477-
"cost": 1,
478-
"types_by_preference": [
479-
"Ore",
480-
"Gold"
481-
]
482-
}
499+
"options": {
500+
"default": {
501+
"ore": 1
502+
},
503+
"conversions": [
504+
{
505+
"from": [
506+
{
507+
"ore": 1
508+
}
509+
],
510+
"to": {
511+
"gold": 1
512+
}
513+
}
514+
]
483515
},
484516
"name": "Use steel weapons",
485517
"optional": true

‎server/tests/test_games/trade_routes_with_currency.outcome.json

+17-9
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@
99
"player_index": 1,
1010
"request": {
1111
"Reward": {
12-
"model": {
13-
"Sum": {
14-
"cost": 2,
15-
"types_by_preference": [
16-
"Gold",
17-
"Food"
18-
]
19-
}
12+
"options": {
13+
"default": {
14+
"gold": 2
15+
},
16+
"conversions": [
17+
{
18+
"from": [
19+
{
20+
"gold": 1
21+
}
22+
],
23+
"to": {
24+
"food": 1
25+
}
26+
}
27+
]
2028
},
2129
"name": "Collect trade routes reward"
2230
}
@@ -635,4 +643,4 @@
635643
}
636644
}
637645
]
638-
}
646+
}

‎server/tests/test_games/trade_routes_with_currency.outcome1.json

+17-9
Original file line numberDiff line numberDiff line change
@@ -627,14 +627,22 @@
627627
"player_index": 1,
628628
"request": {
629629
"Reward": {
630-
"model": {
631-
"Sum": {
632-
"cost": 2,
633-
"types_by_preference": [
634-
"Gold",
635-
"Food"
636-
]
637-
}
630+
"options": {
631+
"default": {
632+
"gold": 2
633+
},
634+
"conversions": [
635+
{
636+
"from": [
637+
{
638+
"gold": 1
639+
}
640+
],
641+
"to": {
642+
"food": 1
643+
}
644+
}
645+
]
638646
},
639647
"name": "Collect trade routes reward"
640648
}
@@ -647,4 +655,4 @@
647655
}
648656
}
649657
]
650-
}
658+
}

0 commit comments

Comments
 (0)
Please sign in to comment.