@@ -3,9 +3,9 @@ use crate::citymap::ffi;
33use std:: collections:: HashSet ;
44
55const CONSTRUCTION_COST : i32 = 5 ;
6+ const RESIDENTIAL_TAX_BASE_PER_RESIDENT : f64 = 100.0 ;
67
78// Values are shared with CategoryID::Type in src/Enums.hpp.
8- const CATEGORY_RESIDENTIAL : i32 = 19 ;
99const CATEGORY_COMMERCIAL : i32 = 20 ;
1010const CATEGORY_OFFICE : i32 = 21 ;
1111const CATEGORY_INDUSTRIAL : i32 = 22 ;
@@ -17,7 +17,6 @@ const CATEGORY_EDUCATION: i32 = 28;
1717
1818#[ derive( Default ) ]
1919struct FinanceObjectCounts {
20- residential : i64 ,
2120 commercial : i64 ,
2221 office : i64 ,
2322 industrial : i64 ,
@@ -53,17 +52,21 @@ impl SimulationState {
5352 )
5453 } ) ;
5554
56- let income = [
57- ( counts. residential , self . tax_residential ) ,
58- ( counts. commercial , self . tax_commercial ) ,
59- ( counts. office , self . tax_office ) ,
60- ( counts. industrial , self . tax_industrial ) ,
61- ( counts. farm , self . tax_farm ) ,
62- ]
63- . into_iter ( )
64- . map ( |( count, tax) | count as f64 * finite_nonnegative ( tax) )
65- . sum :: < f64 > ( )
66- . trunc ( ) as i64 ;
55+ let residential_income = f64:: from ( self . population . max ( 0 ) )
56+ * RESIDENTIAL_TAX_BASE_PER_RESIDENT
57+ * finite_nonnegative ( self . tax_residential )
58+ / 100.0 ;
59+ let income = residential_income
60+ + [
61+ ( counts. commercial , self . tax_commercial ) ,
62+ ( counts. office , self . tax_office ) ,
63+ ( counts. industrial , self . tax_industrial ) ,
64+ ( counts. farm , self . tax_farm ) ,
65+ ]
66+ . into_iter ( )
67+ . map ( |( count, tax) | count as f64 * finite_nonnegative ( tax) )
68+ . sum :: < f64 > ( ) ;
69+ let income = income. trunc ( ) as i64 ;
6770
6871 let updated_money = i64:: from ( self . money )
6972 . saturating_add ( income)
@@ -82,7 +85,6 @@ fn count_finance_objects(simulation_objects: &[ffi::SimulationObjectState]) -> F
8285 }
8386
8487 let categories = & object. category_ids ;
85- counts. residential += i64:: from ( categories. contains ( & CATEGORY_RESIDENTIAL ) ) ;
8688 counts. commercial += i64:: from ( categories. contains ( & CATEGORY_COMMERCIAL ) ) ;
8789 counts. office += i64:: from ( categories. contains ( & CATEGORY_OFFICE ) ) ;
8890 counts. industrial += i64:: from ( categories. contains ( & CATEGORY_INDUSTRIAL ) ) ;
@@ -109,11 +111,10 @@ mod tests {
109111 use super :: {
110112 CATEGORY_COMMERCIAL , CATEGORY_EDUCATION , CATEGORY_FARM , CATEGORY_FIRE_DEPARTMENT ,
111113 CATEGORY_INDUSTRIAL , CATEGORY_OFFICE , CATEGORY_POLICE , CATEGORY_POST_OFFICE ,
112- CATEGORY_RESIDENTIAL ,
113114 } ;
114115 use crate :: {
115116 citymap:: ffi,
116- simulation:: test_support:: { state_at , FixedRandom } ,
117+ simulation:: test_support:: { FixedRandom , residential_tile , state_at } ,
117118 } ;
118119
119120 fn simulation_objects (
@@ -146,8 +147,8 @@ mod tests {
146147 state. tax_office = 9.0 ;
147148 state. tax_industrial = 10.0 ;
148149 state. tax_farm = 11.0 ;
150+ let mut residential_tiles = vec ! [ residential_tile( 10 , 10 ) ] ;
149151 let objects = simulation_objects ( & [
150- ( CATEGORY_RESIDENTIAL , 10 ) ,
151152 ( CATEGORY_COMMERCIAL , 20 ) ,
152153 ( CATEGORY_OFFICE , 30 ) ,
153154 ( CATEGORY_INDUSTRIAL , 40 ) ,
@@ -159,7 +160,15 @@ mod tests {
159160 ] ) ;
160161 let mut random = FixedRandom :: new ( [ ] ) ;
161162
162- state. update_world_with_source ( 1 , & mut [ ] , & mut [ ] , & mut [ ] , & [ ] , & objects, & mut random) ;
163+ state. update_world_with_source (
164+ 1 ,
165+ & mut residential_tiles,
166+ & mut [ ] ,
167+ & mut [ ] ,
168+ & [ ] ,
169+ & objects,
170+ & mut random,
171+ ) ;
163172
164173 assert_eq ! ( ( state. time. month, state. time. date) , ( 5 , 1 ) ) ;
165174 assert_eq ! ( state. money, 71_450 ) ;
@@ -169,7 +178,7 @@ mod tests {
169178 fn does_not_update_finances_on_other_dates ( ) {
170179 let mut state = state_at ( 2024 , 5 , 1 , 23 , 59 ) ;
171180 state. money = 100_000 ;
172- let objects = simulation_objects ( & [ ( CATEGORY_RESIDENTIAL , 10 ) , ( CATEGORY_POLICE , 1 ) ] ) ;
181+ let objects = simulation_objects ( & [ ( CATEGORY_POLICE , 1 ) ] ) ;
173182 let mut random = FixedRandom :: new ( [ ] ) ;
174183
175184 state. update_world_with_source ( 1 , & mut [ ] , & mut [ ] , & mut [ ] , & [ ] , & objects, & mut random) ;
@@ -187,16 +196,16 @@ mod tests {
187196 state. budget_fire = 0 ;
188197 state. budget_post = 0 ;
189198 state. budget_education = 0 ;
190- let objects = simulation_objects ( & [ ( CATEGORY_RESIDENTIAL , 1 ) ] ) ;
199+ let mut residential_tiles = vec ! [ residential_tile ( 1 , 1 ) ] ;
191200 let mut random = FixedRandom :: new ( [ ] ) ;
192201
193202 state. update_world_with_source (
194203 30 * 24 * 60 ,
195- & mut [ ] ,
204+ & mut residential_tiles ,
196205 & mut [ ] ,
197206 & mut [ ] ,
198207 & [ ] ,
199- & objects ,
208+ & [ ] ,
200209 & mut random,
201210 ) ;
202211
@@ -213,10 +222,21 @@ mod tests {
213222 state. budget_fire = 0 ;
214223 state. budget_post = 0 ;
215224 state. budget_education = 0 ;
216- let objects = simulation_objects ( & [ ( CATEGORY_RESIDENTIAL , 1 ) ] ) ;
225+ let mut tile = residential_tile ( 1 , 1 ) ;
226+ tile. ages . clear ( ) ;
227+ tile. genders . clear ( ) ;
228+ let mut residential_tiles = vec ! [ tile] ;
217229 let mut random = FixedRandom :: new ( [ ] ) ;
218230
219- state. update_world_with_source ( 1 , & mut [ ] , & mut [ ] , & mut [ ] , & [ ] , & objects, & mut random) ;
231+ state. update_world_with_source (
232+ 1 ,
233+ & mut residential_tiles,
234+ & mut [ ] ,
235+ & mut [ ] ,
236+ & [ ] ,
237+ & [ ] ,
238+ & mut random,
239+ ) ;
220240
221241 assert_eq ! (
222242 ( state. time. year, state. time. month, state. time. date) ,
@@ -244,10 +264,19 @@ mod tests {
244264 farm : 0.0 ,
245265 } ,
246266 ) ;
247- let objects = simulation_objects ( & [ ( CATEGORY_RESIDENTIAL , 10 ) , ( CATEGORY_POLICE , 1 ) ] ) ;
267+ let mut residential_tiles = vec ! [ residential_tile( 10 , 10 ) ] ;
268+ let objects = simulation_objects ( & [ ( CATEGORY_POLICE , 1 ) ] ) ;
248269 let mut random = FixedRandom :: new ( [ ] ) ;
249270
250- state. update_world_with_source ( 1 , & mut [ ] , & mut [ ] , & mut [ ] , & [ ] , & objects, & mut random) ;
271+ state. update_world_with_source (
272+ 1 ,
273+ & mut residential_tiles,
274+ & mut [ ] ,
275+ & mut [ ] ,
276+ & [ ] ,
277+ & objects,
278+ & mut random,
279+ ) ;
251280
252281 assert_eq ! ( state. money, 150 ) ;
253282 }
@@ -258,14 +287,87 @@ mod tests {
258287 state. money = i32:: MAX - 1 ;
259288 state. tax_residential = f64:: MAX ;
260289 state. budget_police = 100 ;
261- let objects = simulation_objects ( & [ ( CATEGORY_RESIDENTIAL , 1 ) ] ) ;
290+ let mut residential_tiles = vec ! [ residential_tile ( 1 , 1 ) ] ;
262291 let mut random = FixedRandom :: new ( [ ] ) ;
263292
264- state. update_world_with_source ( 1 , & mut [ ] , & mut [ ] , & mut [ ] , & [ ] , & objects, & mut random) ;
293+ state. update_world_with_source (
294+ 1 ,
295+ & mut residential_tiles,
296+ & mut [ ] ,
297+ & mut [ ] ,
298+ & [ ] ,
299+ & [ ] ,
300+ & mut random,
301+ ) ;
265302
266303 assert_eq ! ( state. money, i32 :: MAX ) ;
267304 }
268305
306+ #[ test]
307+ fn residential_tax_uses_population_and_fractional_percent_rate ( ) {
308+ let mut state = state_at ( 2024 , 1 , 1 , 0 , 0 ) ;
309+ state. money = 100 ;
310+ state. population = 100 ;
311+ state. tax_residential = 7.5 ;
312+ state. tax_commercial = 0.0 ;
313+ state. tax_office = 0.0 ;
314+ state. tax_industrial = 0.0 ;
315+ state. tax_farm = 0.0 ;
316+ state. budget_police = 0 ;
317+ state. budget_fire = 0 ;
318+ state. budget_post = 0 ;
319+ state. budget_education = 0 ;
320+
321+ state. update_monthly_finances ( & [ ] ) ;
322+
323+ assert_eq ! ( state. money, 850 ) ;
324+ }
325+
326+ #[ test]
327+ fn residential_object_count_does_not_change_residential_tax ( ) {
328+ let mut without_homes = state_at ( 2024 , 1 , 1 , 0 , 0 ) ;
329+ without_homes. money = 0 ;
330+ without_homes. population = 10 ;
331+ without_homes. tax_residential = 7.0 ;
332+ without_homes. update_monthly_finances ( & [ ] ) ;
333+
334+ let mut with_multi_tile_home = state_at ( 2024 , 1 , 1 , 0 , 0 ) ;
335+ with_multi_tile_home. money = 0 ;
336+ with_multi_tile_home. population = 10 ;
337+ with_multi_tile_home. tax_residential = 7.0 ;
338+ with_multi_tile_home. update_monthly_finances ( & [
339+ ffi:: SimulationObjectState {
340+ object_id : 42 ,
341+ category_ids : vec ! [ 19 ] ,
342+ } ,
343+ ffi:: SimulationObjectState {
344+ object_id : 42 ,
345+ category_ids : vec ! [ 19 ] ,
346+ } ,
347+ ] ) ;
348+
349+ assert_eq ! ( without_homes. money, 70 ) ;
350+ assert_eq ! ( with_multi_tile_home. money, 70 ) ;
351+ }
352+
353+ #[ test]
354+ fn empty_homes_and_negative_population_produce_no_residential_tax ( ) {
355+ let residential_objects = simulation_objects ( & [ ( 19 , 10 ) ] ) ;
356+ let mut empty_city = state_at ( 2024 , 1 , 1 , 0 , 0 ) ;
357+ empty_city. money = 100 ;
358+ empty_city. population = 0 ;
359+ empty_city. tax_residential = 7.0 ;
360+
361+ empty_city. update_monthly_finances ( & residential_objects) ;
362+
363+ assert_eq ! ( empty_city. money, 100 ) ;
364+
365+ empty_city. population = -10 ;
366+ empty_city. update_monthly_finances ( & residential_objects) ;
367+
368+ assert_eq ! ( empty_city. money, 100 ) ;
369+ }
370+
269371 #[ test]
270372 fn counts_multi_tile_object_and_each_category_only_once ( ) {
271373 let objects = vec ! [
0 commit comments