1use crate::clock::GameMode;
12use crate::econ::{classic_ladder, rent_config, rent_of};
13use crate::facilities::Kind;
14use crate::gameplay::GameplayEvent;
15use crate::sim::Simulation;
16use crate::tower::Unit;
17
18pub(crate) const AVERAGE_RUNG: usize = 2;
23
24#[doc(hidden)]
25pub fn snap_to_ladder(rungs: &[f64; 4], value: f64) -> f64 {
26 if !value.is_finite() {
27 return rungs[AVERAGE_RUNG];
28 }
29 let mut best = rungs[0];
30 let mut best_dist = f64::INFINITY;
31 for &r in rungs {
32 let d = (value - r).abs();
33 if d <= best_dist {
34 best_dist = d;
35 best = r;
36 }
37 }
38 best
39}
40
41pub(crate) fn ladder_level(rungs: &[f64; 4], value: f64) -> usize {
43 let snapped = snap_to_ladder(rungs, value);
44 rungs
45 .iter()
46 .position(|&r| r == snapped)
47 .unwrap_or(AVERAGE_RUNG)
48}
49
50pub(crate) fn price_options(mode: GameMode, kind: Kind) -> Option<Option<[f64; 4]>> {
53 match mode {
54 GameMode::Classic => classic_ladder(kind).map(Some),
55 GameMode::Modern => rent_config(kind).map(|_| None),
56 }
57}
58
59fn price_neutral(mode: GameMode, kind: Kind) -> Option<f64> {
60 match price_options(mode, kind)? {
61 Some(l) => Some(l[AVERAGE_RUNG]),
62 None => Some(rent_config(kind)?.default),
63 }
64}
65
66pub(crate) fn is_neutral_price(price: f64, neutral: f64) -> bool {
70 price == neutral
71}
72
73#[derive(Clone, Copy, Debug, PartialEq)]
75pub enum BatchTarget {
76 Default,
78 NoRate,
80 Price(f64),
83}
84
85#[derive(Clone, Debug, Default, PartialEq, Eq)]
87pub struct BatchRentResult {
88 pub matched: i64,
90 pub eligible: i64,
92 pub changed: i64,
94 pub skipped_sold: i64,
96 pub skipped_custom: i64,
99 pub custom_overwritten: i64,
101 pub clamped_low: i64,
103 pub clamped_high: i64,
105}
106
107impl Simulation {
108 pub fn apply_rent_batch(
126 &mut self,
127 kind: Kind,
128 target: BatchTarget,
129 only_default_priced: bool,
130 ) -> Option<BatchRentResult> {
131 let r = self.compute_batch(kind, target, only_default_priced, true);
132 if r.as_ref().is_some_and(|r| r.changed > 0) {
133 self.gameplay.push(GameplayEvent::PricingChanged { kind });
134 }
135 r
136 }
137
138 #[doc(hidden)]
139 pub fn compute_batch(
142 &mut self,
143 kind: Kind,
144 target: BatchTarget,
145 only_default_priced: bool,
146 mutate: bool,
147 ) -> Option<BatchRentResult> {
148 let cfg = rent_config(kind)?;
149 let ladder = price_options(self.mode, kind)?;
150 if target == BatchTarget::NoRate && ladder.is_none() {
151 return None;
152 }
153 if let BatchTarget::Price(p) = target {
154 if !p.is_finite() {
155 return None;
156 }
157 }
158 let neutral = price_neutral(self.mode, kind)?;
159 let mut r = BatchRentResult::default();
160 let rooms = self.tower.room_index();
163 for &i in rooms.iter() {
164 let u = &mut self.tower.units[i];
165 if u.kind != kind {
166 continue;
167 }
168 r.matched += 1;
169 if u.kind == Kind::Condo && u.ever_occupied {
170 r.skipped_sold += 1;
171 continue;
172 }
173 let neutral_priced =
177 !u.no_rate && is_neutral_price(u.rent.unwrap_or(cfg.default), neutral);
178 if only_default_priced && !neutral_priced {
179 r.skipped_custom += 1;
180 continue;
181 }
182 r.eligible += 1;
183 if target == BatchTarget::NoRate {
184 if !u.no_rate {
185 r.changed += 1;
186 }
187 if mutate {
188 if let Some(j) = self.tower.journal.as_mut() {
189 j.unit_edit(i, u);
190 }
191 u.no_rate = true;
192 }
193 continue;
194 }
195 if !neutral_priced && !u.no_rate {
196 r.custom_overwritten += 1;
197 }
198 let before = rent_of(u.kind, u.rent, u.no_rate);
199 let value = match (target, ladder) {
200 (BatchTarget::Default, _) => neutral,
201 (BatchTarget::Price(p), Some(l)) => snap_to_ladder(&l, p),
202 (BatchTarget::Price(p), None) => {
203 if p < cfg.min {
204 r.clamped_low += 1;
205 } else if p > cfg.max {
206 r.clamped_high += 1;
207 }
208 p.min(cfg.max).max(cfg.min)
209 }
210 (BatchTarget::NoRate, _) => unreachable!("handled above"),
211 };
212 if before != value {
213 r.changed += 1;
214 }
215 if mutate {
216 if let Some(j) = self.tower.journal.as_mut() {
217 j.unit_edit(i, u);
218 }
219 u.rent = if value == cfg.default {
222 None
223 } else {
224 Some(value)
225 };
226 u.no_rate = false;
227 }
228 }
229 Some(r)
230 }
231
232 #[doc(hidden)]
233 pub fn demand_factor(&self, u: &Unit) -> f64 {
235 let Some(neutral) = price_neutral(self.mode, u.kind) else {
236 return 1.0;
237 };
238 if u.no_rate {
239 return 0.0;
240 }
241 let ratio = rent_of(u.kind, u.rent, u.no_rate) / neutral;
242 (2.0 - ratio).min(1.6).max(0.15)
243 }
244
245 pub fn price_unit(&mut self, id: i64, target: f64) -> Option<f64> {
250 let mode = self.mode;
251 let u = self.tower.get_unit_mut(id)?;
252 let cfg = rent_config(u.kind)?;
253 let opts = price_options(mode, u.kind)?;
254 if !target.is_finite() {
255 return None;
256 }
257 if u.kind == Kind::Condo && u.ever_occupied {
258 return None;
259 }
260 let applied = match opts {
261 Some(l) => snap_to_ladder(&l, target),
262 None => target.min(cfg.max).max(cfg.min),
263 };
264 let before = (!u.no_rate).then(|| rent_of(u.kind, u.rent, u.no_rate));
265 u.rent = if applied == cfg.default {
266 None
267 } else {
268 Some(applied)
269 };
270 u.no_rate = false;
271 let changed = Some(rent_of(u.kind, u.rent, false)) != before;
273 let kind = u.kind;
274 if changed {
275 self.gameplay.push(GameplayEvent::PricingChanged { kind });
276 }
277 Some(applied)
278 }
279
280 pub fn set_no_rate(&mut self, id: i64) -> bool {
284 let mode = self.mode;
285 let Some(u) = self.tower.get_unit_mut(id) else {
286 return false;
287 };
288 if !matches!(price_options(mode, u.kind), Some(Some(_))) {
289 return false;
290 }
291 if u.kind == Kind::Condo && u.ever_occupied {
292 return false;
293 }
294 let was_off = u.no_rate;
295 u.no_rate = true;
296 let kind = u.kind;
297 if !was_off {
298 self.gameplay.push(GameplayEvent::PricingChanged { kind });
299 }
300 true
301 }
302
303 pub fn adjust_rent(&mut self, id: i64, dir: i64) -> Option<f64> {
307 let mode = self.mode;
308 let (kind, rent, no_rate) = {
309 let u = self.tower.get_unit(id)?;
310 (u.kind, u.rent, u.no_rate)
311 };
312 let cfg = rent_config(kind)?;
313 let opts = price_options(mode, kind)?;
314 let current = rent_of(kind, rent, no_rate);
315 match opts {
316 Some(l) => {
317 let idx = ladder_level(&l, current) as i64;
318 let next = l[(idx + dir).clamp(0, 3) as usize];
319 self.price_unit(id, next)
320 }
321 None => self.price_unit(id, current + dir as f64 * cfg.step),
322 }
323 }
324}