summaryrefslogtreecommitdiff
path: root/CorsixTH/Lua/world.lua
blob: 0a61640abd02170e543e51e9a444ab753a1df5fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
--[[ Copyright (c) 2009 Peter "Corsix" Cawley

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]

local pathsep = package.config:sub(1, 1)
local TH = require"TH"
local ipairs, _G, table_remove
    = ipairs, _G, table.remove

dofile "entities/patient"
dofile "entities/staff"
dofile "entities/vip"
dofile "entities/grim_reaper"
dofile "entities/inspector"
dofile "staff_profile"
dofile "hospital"
dofile "epidemic"
dofile "calls_dispatcher"
dofile "research_department"
dofile "entity_map"

--! Manages entities, rooms, and the date.
class "World"

---@type World
local World = _G["World"]

local local_criteria_variable = {
  {name = "reputation",       icon = 10, formats = 2},
  {name = "balance",          icon = 11, formats = 2},
  {name = "percentage_cured", icon = 12, formats = 2},
  {name = "num_cured" ,       icon = 13, formats = 2},
  {name = "percentage_killed",icon = 14, formats = 2},
  {name = "value",            icon = 15, formats = 2},
  {name = "population",       icon = 11, formats = 1},
}

function World:World(app)
  self.map = app.map
  self.wall_types = app.walls
  self.object_types = app.objects
  self.anims = app.anims
  self.animation_manager = app.animation_manager
  self.pathfinder = TH.pathfinder()
  self.pathfinder:setMap(app.map.th)
  self.entities = {}
  self.dispatcher = CallsDispatcher(self)
  self.objects = {}
  self.object_counts = {
    extinguisher = 0,
    radiator = 0,
    plant = 0,
    reception_desk = 0,
    bench = 0,
    general = 0,
  }
  self.objects_notify_occupants = {}
  self.rooms = {} -- List that can have gaps when a room is deleted, so use pairs to iterate.
  self.entity_map = EntityMap(self.map)

  -- Time
  self.hours_per_day = 50
  self.hours_per_tick = 1
  self.tick_rate = 3
  self.tick_timer = 0
  self.year = 1
  self.month = 1 -- January
  self.day = 1
  self.hour = 0

  self.room_information_dialogs_off = app.config.debug
  -- This is false when the game is paused.
  self.user_actions_allowed = true

  -- In Free Build mode?
  if tonumber(self.map.level_number) then
    self.free_build_mode = false
  else
    self.free_build_mode = app.config.free_build_mode
    self.debug_disable_salary_raise = self.free_build_mode
  end

  self.debug_disable_salary_raise = false
  self.idle_cache = {}
  -- List of which goal criterion means what, and what number the corresponding icon has.
  self.level_criteria = local_criteria_variable
  self.room_build_callbacks = {--[[a set rather than a list]]}
  self.room_remove_callbacks = {--[[a set rather than a list]]}
  self.room_built = {} -- List of room types that have been built
  self.hospitals = {}
  self.floating_dollars = {}
  self.game_log = {} -- saves list of useful debugging information
  self.savegame_version = app.savegame_version
  -- Also preserve this throughout future updates.
  self.original_savegame_version = app.savegame_version

  -- Initialize available rooms.
  local avail_rooms = self:getAvailableRooms()
  self.available_rooms = {} -- Both a list and a set, use ipairs to iterate through the available rooms.
  for _, avail_room in ipairs(avail_rooms) do
    local room = avail_room.room
    self.available_rooms[#self.available_rooms + 1] = room
    self.available_rooms[room.id] = room
  end

  -- Initialize available diseases and winning conditions.
  self:initLevel(app, avail_rooms)

  self.hospitals[1] = Hospital(self, avail_rooms, app.config.player_name) -- Player's hospital
  self:initCompetitors(avail_rooms)
  for _, hospital in ipairs(self.hospitals) do
    hospital.research:setResearchConcentration()
  end

  -- TODO: Add (working) AI and/or multiplayer hospitals
  -- TODO: Needs to be changed for multiplayer support
  self:initStaff()
  self.wall_id_by_block_id = {}
  for _, wall_type in ipairs(self.wall_types) do
    for _, set in ipairs{"inside_tiles", "outside_tiles", "window_tiles"} do
      for name, id in pairs(wall_type[set]) do
        self.wall_id_by_block_id[id] = wall_type.id
      end
    end
  end
  self.wall_set_by_block_id = {}
  for _, wall_type in ipairs(self.wall_types) do
    for _, set in ipairs{"inside_tiles", "outside_tiles", "window_tiles"} do
      for name, id in pairs(wall_type[set]) do
        self.wall_set_by_block_id[id] = set
      end
    end
  end
  self.wall_dir_by_block_id = {}
  for _, wall_type in ipairs(self.wall_types) do
    for _, set in ipairs{"inside_tiles", "outside_tiles", "window_tiles"} do
      for name, id in pairs(wall_type[set]) do
        self.wall_dir_by_block_id[id] = name
      end
    end
  end

  self.object_id_by_thob = {}
  for _, object_type in ipairs(self.object_types) do
    self.object_id_by_thob[object_type.thob] = object_type.id
  end
  self:makeAvailableStaff(0)
  self:calculateSpawnTiles()

  self:nextEmergency()
  self:nextVip()

  -- earthquakes
  -- current_map_earthquakes is a counter that tracks which number of earthquake
  -- we are currently on in maps which have information for earthquakes in them
  self.current_map_earthquake = 0
  self:nextEarthquake()

  -- Set initial spawn rate in people per month.
  -- Assumes that the first entry is always the first month.
  self.spawn_rate = self.map.level_config.popn[0].Change
  self.monthly_spawn_increase = self.spawn_rate

  self.spawn_hours = {}
  self.spawn_dates = {}
  self:updateSpawnDates()

  self.cheat_announcements = {
    "cheat001.wav", "cheat002.wav", "cheat003.wav",
  }

  self:gameLog("Created game with savegame version " .. self.savegame_version .. ".")
end

--! Register key shortcuts for controling the world (game speed, etc.)
function World:setUI(ui)
  self.ui = ui
  self.ui:addKeyHandler("P", self, self.pauseOrUnpause, "Pause")
  self.ui:addKeyHandler("1", self, self.setSpeed, "Slowest")
  self.ui:addKeyHandler("2", self, self.setSpeed, "Slower")
  self.ui:addKeyHandler("3", self, self.setSpeed, "Normal")
  self.ui:addKeyHandler("4", self, self.setSpeed, "Max speed")
  self.ui:addKeyHandler("5", self, self.setSpeed, "And then some more")

  self.ui:addKeyHandler("=", self, self.adjustZoom,  1)
  self.ui:addKeyHandler({"shift", "="}, self, self.adjustZoom, 5)
  self.ui:addKeyHandler("+", self, self.adjustZoom,  1)
  self.ui:addKeyHandler({"shift", "+"}, self, self.adjustZoom, 5)
  self.ui:addKeyHandler("-", self, self.adjustZoom, -1)
  self.ui:addKeyHandler({"shift", "-"}, self, self.adjustZoom, -5)
end

function World:adjustZoom(delta)
  local scr_w = self.ui.app.config.width
  local factor = self.ui.app.config.zoom_speed
  local virtual_width = scr_w / (self.ui.zoom_factor or 1)

  -- The modifier is a normal distribution to make it more difficult to zoom at the extremes
  local modifier = math.exp(1)^(-((self.ui.zoom_factor-1)^2)/(2 * 1))/(math.sqrt(2*math.pi)*1)

  if modifier < 0.05 or modifier > 1 then
    modifier = 0.05
  end

  virtual_width = virtual_width - delta * factor * modifier
  if virtual_width < 200 then
    return false
  end

  return self.ui:setZoom(scr_w/virtual_width)
end

--! Initialize the game level (available diseases, winning conditions).
--!param app Game application.
--!param avail_rooms (list) Available rooms in the level.
function World:initLevel(app, avail_rooms)
  local existing_rooms = {}
  for _, avail_room in ipairs(avail_rooms) do
    existing_rooms[avail_room.room.id] = true
  end

  -- Determine available diseases
  self.available_diseases = {}
  local level_config = self.map.level_config
  local visual = level_config.visuals
  local non_visual = level_config.non_visuals
  for _, disease in ipairs(app.diseases) do
    if not disease.pseudo then
      local vis = 1
      if visual and (visual[disease.visuals_id] or non_visual[disease.non_visuals_id]) then
        vis = disease.visuals_id and visual[disease.visuals_id].Value
        or non_visual[disease.non_visuals_id].Value
      end
      if vis ~= 0 then
        for _, room_id in ipairs(disease.treatment_rooms) do
          if existing_rooms[room_id] == nil then
            print("Warning: Removing disease \"" .. disease.id .. "\" due to missing treatment room \"" .. room_id .. "\".")
            vis = 0 -- Missing treatment room, disease cannot be treated. Remove it.
            break
          end
        end
      end
      -- TODO: Where the value is greater that 0 should determine the frequency of the patients
      if vis ~= 0 then
        self.available_diseases[#self.available_diseases + 1] = disease
        self.available_diseases[disease.id] = disease
      end
    end
  end
  if #self.available_diseases == 0 and not _MAP_EDITOR then
    -- No diseases are needed if we're actually in the map editor!
    print("Warning: This level does not contain any diseases")
  end

  self:determineWinningConditions()
end

function World:toggleInformation()
  self.room_information_dialogs_off = not self.room_information_dialogs_off
end

function World:initStaff()
  local level_config = self.map.level_config
  local hosp = self.hospitals[1]
  if level_config.start_staff then
    local i = 0
    for n, conf in ipairs(level_config.start_staff) do
      local profile
      local skill = 0
      local added_staff = true
      if conf.Skill then
        skill = conf.Skill / 100
      end

      if conf.Nurse == 1 then
        profile = StaffProfile(self, "Nurse", _S.staff_class["nurse"])
        profile:init(skill)
      elseif conf.Receptionist == 1 then
        profile = StaffProfile(self, "Receptionist", _S.staff_class["receptionist"])
        profile:init(skill)
      elseif conf.Handyman == 1 then
        profile = StaffProfile(self, "Handyman", _S.staff_class["handyman"])
        profile:init(skill)
      elseif conf.Doctor == 1 then
        profile = StaffProfile(self, "Doctor", _S.staff_class["doctor"])

        local shrink = 0
        local rsch = 0
        local surg = 0
        local jr, cons

        if conf.Shrink == 1 then shrink = 1 end
        if conf.Surgeon == 1 then surg = 1 end
        if conf.Researcher == 1 then rsch = 1 end

        if conf.Junior == 1 then jr = 1
        elseif conf.Consultant == 1 then cons = 1
        end
        profile:initDoctor(shrink,surg,rsch,jr,cons,skill)
      else
        added_staff = false
      end
      if added_staff then
        local staff = self:newEntity("Staff", 2)
        staff:setProfile(profile)
        -- TODO: Make a somewhat "nicer" placing algorithm.
        staff:setTile(self.map.th:getCameraTile(1))
        staff:onPlaceInCorridor()
        hosp.staff[#hosp.staff + 1] = staff
        staff:setHospital(hosp)
      end
    end
  end
end

--! Load goals to win and lose from the map, and store them in 'self.goals'.
--! Also set 'self.winning_goal_count'.
function World:determineWinningConditions()
  local winning_goal_count = 0
  -- No conditions if in free build mode!
  if self.free_build_mode then
    self.goals = {}
    self.winning_goal_count = winning_goal_count
    return
  end
  -- Determine winning and losing conditions
  local world_goals = {}

  -- There might be no winning criteria (i.e. the demo), then
  -- we don't have to worry about the progress report dialog
  -- since it doesn't exist anyway.
  local win = self.map.level_config.win_criteria
  if win then
    for _, values in pairs(win) do
      if values.Criteria ~= 0 then
        winning_goal_count = winning_goal_count + 1
        local crit_name = self.level_criteria[values.Criteria].name
        world_goals[crit_name] = {
          name = crit_name,
          win_value = values.Value,
          boundary = values.Bound,
          criterion = values.Criteria,
          max_min_win = values.MaxMin,
          group = values.Group,
          number = winning_goal_count,
        }
        world_goals[#world_goals + 1] = world_goals[crit_name]
      end
    end
  end
  -- Likewise there might be no losing criteria (i.e. the demo)
  local lose = self.map.level_config.lose_criteria
  if lose then
    for _, values in pairs(lose) do
      if values.Criteria ~= 0 then
        local crit_name = self.level_criteria[values.Criteria].name
        if not world_goals[crit_name] then
          world_goals[crit_name] = {number = #world_goals + 1, name = crit_name}
          world_goals[#world_goals + 1] = world_goals[crit_name]
        end
        world_goals[crit_name].lose_value = values.Value
        world_goals[crit_name].boundary = values.Bound
        world_goals[crit_name].criterion = values.Criteria
        world_goals[crit_name].max_min_lose = values.MaxMin
        world_goals[crit_name].group = values.Group
        world_goals[world_goals[crit_name].number].lose_value = values.Value
        world_goals[world_goals[crit_name].number].boundary = values.Bound
        world_goals[world_goals[crit_name].number].criterion = values.Criteria
        world_goals[world_goals[crit_name].number].max_min_lose = values.MaxMin
        world_goals[world_goals[crit_name].number].group = values.Group
      end
    end
  end

  -- Order the criteria (some icons in the progress report shouldn't be next to each other)
  table.sort(world_goals, function(a,b) return a.criterion < b.criterion end)
  self.goals = world_goals
  self.winning_goal_count = winning_goal_count
end

--! Find the rooms available at the level.
--!return (list) Available rooms, with discovery state at start, and build_cost.
function World:getAvailableRooms()
  local avail_rooms = {}

  local obj = self.map.level_config.objects
  local rooms = self.map.level_config.rooms
  for _, room in ipairs(TheApp.rooms) do
    -- Add build cost based on level files for all rooms.
    -- For now, sum it up so that the result is the same as before.
    -- TODO: Change the whole build process so that this value is
    -- the room cost only? (without objects)
    local build_cost = rooms[room.level_config_id].Cost
    local available = true
    local is_discovered = true
    -- Make sure that all objects needed for this room are available
    for name, no in pairs(room.objects_needed) do
      local spec = obj[TheApp.objects[name].thob]
      if spec.AvailableForLevel == 0 then
        -- It won't be possible to build this room at all on the level.
        available = false
      elseif spec.StartAvail == 0 then
        -- Ok, it will be available at some point just not from the beginning.
        is_discovered = false
      end
      -- Add cost for this object.
      build_cost = build_cost + obj[TheApp.objects[name].thob].StartCost * no
    end

    if available then
      avail_rooms[#avail_rooms + 1] = {room = room, is_discovered = is_discovered, build_cost = build_cost}
    end
  end
  return avail_rooms
end

--! Initialize competing hospitals
--!param avail_rooms (list) Available rooms in the level.
function World:initCompetitors(avail_rooms)
  -- Add computer players
  -- TODO: Right now they're only names
  local level_config = self.map.level_config
  for key, value in pairs(level_config.computer) do
    if value.Playing == 1 then
      self.hospitals[#self.hospitals + 1] = AIHospital(tonumber(key) + 1, self, avail_rooms)
    end
  end
end

--! Initializes variables carried from previous levels
function World:initFromPreviousLevel(carry)
  for object, tab in pairs(carry) do
    if object == "world" then
      for key, value in pairs(tab) do
        self[key] = value
      end
    elseif object == "hospital" then
      for key, value in pairs(tab) do
        self.hospitals[1][key] = value
      end
    end
  end
end

--! Get the hospital controlled by the (single) player.
--!return (Hospital) The hospital controlled by the (single) player.
function World:getLocalPlayerHospital()
  -- NB: UI code can get the hospital to use via ui.hospital
  -- TODO: Make this work in multiplayer?
  return self.hospitals[1]
end

--! Identify the tiles on the map suitable for spawning `Humanoid`s from.
function World:calculateSpawnTiles()
  self.spawn_points = {}
  local w, h = self.map.width, self.map.height
  for _, edge in ipairs{
    {direction = "north", origin = {1, 1}, step = { 1,  0}},
    {direction = "east" , origin = {w, 1}, step = { 0,  1}},
    {direction = "south", origin = {w, h}, step = {-1,  0}},
    {direction = "west" , origin = {1, h}, step = { 0, -1}},
  } do
    -- Find all possible spawn points on the edge
    local xs = {}
    local ys = {}
    local x, y = edge.origin[1], edge.origin[2]
    repeat
      if self.pathfinder:isReachableFromHospital(x, y) then
        xs[#xs + 1] = x
        ys[#ys + 1] = y
      end
      x = x + edge.step[1]
      y = y + edge.step[2]
    until x < 1 or x > w or y < 1 or y > h

    -- Choose at most 8 points for the edge
    local num = math.min(8, #xs)
    for i = 1, num do
      local index = math.floor((i - 0.5) / num * #xs + 1)
      self.spawn_points[#self.spawn_points + 1] = {x = xs[index], y = ys[index], direction = edge.direction}
    end
  end
end

--! Spawn a patient from a spawn point for the given hospital.
--!param hospital (Hospital) Hospital that the new patient should visit.
--!return (Patient entity) The spawned patient, or 'nil' if no patient spawned.
function World:spawnPatient(hospital)
  -- The level might not contain any diseases
  if #self.available_diseases < 1 then
    self.ui:addWindow(UIInformation(self.ui, {"There are no diseases on this level! Please add some to your level."}))
    return
  end
  if #self.spawn_points == 0 then
    self.ui:addWindow(UIInformation(self.ui, {"Could not spawn patient because no spawn points are available. Please place walkable tiles on the edge of your level."}))
    return
  end
  if not hospital then
    hospital = self:getLocalPlayerHospital()
  end
  --! What is the current month?
  local current_month = (self.year - 1) * 12 + self.month
  --! level files can delay visuals to a given month
  --! and / or until a given number of patients have arrived
  local hold_visual_months = self.map.level_config.gbv.HoldVisualMonths
  local hold_visual_peep_count = self.map.level_config.gbv.HoldVisualPeepCount
  --! Function to determine whether a given disease is visible and available.
  --!param disease (disease) Disease to test.
  --!return (boolean) Whether the disease is visible and available.
  local function isVisualDiseaseAvailable(disease)
    if not disease.visuals_id then
      return true
    end
    --! if the month is greater than either of these values then visuals will not appear in the game
    if hold_visual_months and hold_visual_months > current_month or
    hold_visual_peep_count and hold_visual_peep_count > hospital.num_visitors then
      return false
    end
    --! the value against #visuals_available determines from which month a disease can appear. 0 means it can show up anytime.
    local level_config = self.map.level_config
    if level_config.visuals_available[disease.visuals_id].Value >= current_month then
      return false
    end
    return true
  end

  if hospital:hasStaffedDesk() then
    local spawn_point = self.spawn_points[math.random(1, #self.spawn_points)]
    local patient = self:newEntity("Patient", 2)
    local disease = self.available_diseases[math.random(1, #self.available_diseases)]
    while disease.only_emergency or not isVisualDiseaseAvailable(disease) do
      disease = self.available_diseases[math.random(1, #self.available_diseases)]
    end
    patient:setDisease(disease)
    patient:setNextAction{name = "spawn", mode = "spawn", point = spawn_point}
    patient:setHospital(hospital)

    return patient
  end
end

--A VIP is invited (or he invited himself) to the player hospital.
--!param name Name of the VIP
function World:spawnVIP(name)
  local hospital = self:getLocalPlayerHospital()

  local spawn_point = self.spawn_points[math.random(1, #self.spawn_points)]
  local vip = self:newEntity("Vip", 2)
  vip:setType("VIP")
  vip.name = name
  vip.enter_deaths = hospital.num_deaths
  vip.enter_visitors = hospital.num_visitors
  vip.enter_cures = hospital.num_cured

  vip.enter_explosions = hospital.num_explosions

  local spawn_point = self.spawn_points[math.random(1, #self.spawn_points)]
  vip:setNextAction{name = "spawn", mode = "spawn", point = spawn_point}
  vip:setHospital(hospital)
  vip:updateDynamicInfo()
  hospital.announce_vip = hospital.announce_vip + 1
  vip:queueAction{name = "seek_reception"}
end

function World:createEarthquake()
  -- Sanity check
  if not self.earthquake_size then
    return false
  end

  -- the bigger the earthquake, the longer it lasts. We add one
  -- further day, as we use those to give a small earthquake first,
  -- before the bigger one begins
  local stop_day = math.round(self.earthquake_size / 3) + 1

  -- make sure the user has at least two days of an earthquake
  if stop_day < 2 then
    stop_day = 2
  end

  -- store the offsets so we can not shake the user to some too distant location
  self.currentX = self.ui.screen_offset_x
  self.currentY = self.ui.screen_offset_y

  -- we add an extra 1 at the end because we register the start of earthquakes at the end of the day
  self.earthquake_stop_day = self.day + stop_day + 1

  -- if the day the earthquake is supposed to stop on a day greater than the length of the current month (eg 34)
  if self.earthquake_stop_day > self:getCurrentMonthLength() then
    -- subtract the current length of the month so the earthquake will stop at the start of the next month
    self.earthquake_stop_day = self.earthquake_stop_day - self:getCurrentMonthLength()
  end

  -- Prepare machines for getting damage - at most as much as the severity of the earthquake +-1
  for _, room in pairs(self.rooms) do
    for object, value in pairs(room.objects) do
      if object.strength then
        object.quake_points = self.earthquake_size + math.random(-1, 1)
      end
    end
  end

  -- set a flag to indicate that we are now having an earthquake
  self.active_earthquake = true
  return true
end

--! Perform actions to simulate an active earthquake.
function World:tickEarthquake()
  -- check if this is the day that the earthquake is supposed to stop
  if self.day == self.earthquake_stop_day then
    self.active_earthquake = false
    self.ui.tick_scroll_amount = false
    -- if the earthquake measured more than 7 on the richter scale, tell the user about it
    if self.earthquake_size > 7 then
      self.ui.adviser:say(_A.earthquake.ended:format(math.floor(self.earthquake_size)))
    end
    -- Make sure that machines got all the damage they should get.
    for _, room in pairs(self.rooms) do
      for object, value in pairs(room.objects) do
        if object.strength and object.quake_points then
          while object.quake_points > 0 do
            object:machineUsed(room)
            object.quake_points = object.quake_points - 1
          end
        end
      end
    end

    -- set up the next earthquake date
    self:nextEarthquake()
  else
    -- Multiplier for how much the screen moves around during the quake.
    local multi = 4
    if (self.day > self.earthquake_stop_day) or (self.day < math.round(self.earthquake_size / 3)) then
      -- if we are in the first two days of the earthquake, make it smaller
      self.randomX = math.random(-(self.earthquake_size/2)*multi, (self.earthquake_size/2)*multi)
      self.randomY = math.random(-(self.earthquake_size/2)*multi, (self.earthquake_size/2)*multi)
    else
      -- otherwise, hit the user with the full earthquake
      self.randomX = math.random(-self.earthquake_size*multi, self.earthquake_size*multi)
      self.randomY = math.random(-self.earthquake_size*multi, self.earthquake_size*multi)
    end

    -- if the game is not paused
    if not self:isCurrentSpeed("Pause") then
      -- Play the earthquake sound. It has different names depending on language used though.
      if TheApp.audio:soundExists("quake2.wav") then
        self.ui:playSound("quake2.wav")
      else
        self.ui:playSound("quake.wav")
      end

      -- shake the screen randomly to give the appearance of an earthquake
      -- the greater the earthquake, the more the screen will shake

      -- restrict the amount the earthquake can shift the user left and right
      if self.ui.screen_offset_x > (self.currentX + 600) then
        if self.randomX > 0 then
          self.randomX = -self.randomX
        end
      elseif self.ui.screen_offset_x < (self.currentX - 600) then
        if self.randomX < 0 then
          self.randomX = -self.randomX
        end
      end

      -- restrict the amount the earthquake can shift the user up and down
      if self.ui.screen_offset_y > (self.currentY + 600) then
        if self.randomY > 0 then
          self.randomY = -self.randomY
        end
      elseif self.ui.screen_offset_y < (self.currentY - 600) then
        if self.randomY < 0 then
          self.randomY = -self.randomY
        end
      end

      self.ui.tick_scroll_amount = {x = self.randomX, y = self.randomY}

      local hospital = self:getLocalPlayerHospital()
      -- loop through the patients and allow the possibilty for them to fall over
      for _, patient in ipairs(hospital.patients) do
        local current = patient.action_queue[1]

        if not patient.in_room and patient.falling_anim then

          -- make the patients fall

          -- jpirie: this is currently disabled. Calling this function
          -- really screws up the action queue, sometimes the patients
          -- end up with nil action queues, and sometimes the resumed
          -- actions throw exceptions. Also, patients in the hospital
          -- who have not yet found reception throw exceptions after
          -- they visit reception. Some debugging needed here to get
          -- this working.

          -- patient:falling()
        end
      end
    end
  end
end

function World:debugDisableSalaryRaise(mode)
  self.debug_disable_salary_raise = mode
end

local staff_to_make = {
  {class = "Doctor",       name = "doctor",       conf = "Doctors"      },
  {class = "Nurse",        name = "nurse",        conf = "Nurses"       },
  {class = "Handyman",     name = "handyman",     conf = "Handymen"     },
  {class = "Receptionist", name = "receptionist", conf = "Receptionists"},
}
function World:makeAvailableStaff(month)
  local conf_entry = 0
  local conf = self.map.level_config.staff_levels
  while conf[conf_entry + 1] and conf[conf_entry + 1].Month <= month do
    conf_entry = conf_entry + 1
  end
  self.available_staff = {}
  for _, info in ipairs(staff_to_make) do
    local num
    local ind = conf_entry
    while not num do
      assert(ind >= 0, "Staff amount " .. info.conf .. " not existent (should at least be given by base_config).")
      num = conf[ind][info.conf]
      ind = ind - 1
    end
    local group = {}
    for i = 1, num do
      group[i] = StaffProfile(self, info.class, _S.staff_class[info.name])
      group[i]:randomise(month)
    end
    self.available_staff[info.class] = group
  end
end

--[[ Register a callback for when `Humanoid`s enter or leave a given tile.
! Note that only one callback may be registered to each tile.
!param x (integer) The 1-based X co-ordinate of the tile to monitor.
!param y (integer) The 1-based Y co-ordinate of the tile to monitor.
!param object (Object) Something with an `onOccupantChange` method, which will
be called whenever a `Humanoid` enters or leaves the given tile. The method
will receive one argument (after `self`), which will be `1` for an enter event
and `-1` for a leave event.
]]
function World:notifyObjectOfOccupants(x, y, object)
  local idx = (y - 1) * self.map.width + x
  self.objects_notify_occupants[idx] =  object or nil
end

function World:getObjectToNotifyOfOccupants(x, y)
  local idx = (y - 1) * self.map.width + x
  return self.objects_notify_occupants[idx]
end

local flag_cache = {}
function World:createMapObjects(objects)
  self.delayed_map_objects = {}
  local map = self.map.th
  for _, object in ipairs(objects) do repeat
    local x, y, thob, flags = unpack(object)
    local object_id = self.object_id_by_thob[thob]
    if not object_id then
      print("Warning: Map contained object with unrecognised THOB (" .. thob .. ") at " .. x .. "," .. y)
      break -- continue
    end
    local object_type = self.object_types[object_id]
    if not object_type or not object_type.supports_creation_for_map then
      print("Warning: Unable to create map object " .. object_id .. " at " .. x .. "," .. y)
      break -- continue
    end
    -- Delay making objects which are on plots which haven't been purchased yet
    local parcel = map:getCellFlags(x, y, flag_cache).parcelId
    if parcel ~= 0 and map:getPlotOwner(parcel) == 0 then
      self.delayed_map_objects[{object_id, x, y, flags, "map object"}] = parcel
    else
      self:newObject(object_id, x, y, flags, "map object")
    end
  until true end
end

function World:setPlotOwner(parcel, owner)
  self.map.th:setPlotOwner(parcel, owner)
  if owner ~= 0 and self.delayed_map_objects then
    for info, p in pairs(self.delayed_map_objects) do
      if p == parcel then
        self:newObject(unpack(info))
        self.delayed_map_objects[info] = nil
      end
    end
  end
  self.map.th:updateShadows()
end

function World:getAnimLength(anim)
  return self.animation_manager:getAnimLength(anim)
end

-- Register a function to be called whenever a room is built.
--!param callback (function) A function taking one argument: a `Room`.
function World:registerRoomBuildCallback(callback)
  self.room_build_callbacks[callback] = true
end

-- Unregister a function from being called whenever a room is built.
--!param callback (function) A function previously passed to
-- `registerRoomBuildCallback`.
function World:unregisterRoomBuildCallback(callback)
  self.room_build_callbacks[callback] = nil
end

-- Register a function to be called whenever a room has been deactivated (crashed or edited).
--!param callback (function) A function taking one argument: a `Room`.
function World:registerRoomRemoveCallback(callback)
  self.room_remove_callbacks[callback] = true
end

-- Unregister a function from being called whenever a room has been deactivated (crashed or edited).
--!param callback (function) A function previously passed to
-- `registerRoomRemoveCallback`.
function World:unregisterRoomRemoveCallback(callback)
  self.room_remove_callbacks[callback] = nil
end

function World:newRoom(x, y, w, h, room_info, ...)
  local id = #self.rooms + 1
  -- Note: Room IDs will be unique, but they may not form continuous values
  -- from 1, as IDs of deleted rooms may not be re-issued for a while
  local class = room_info.class and _G[room_info.class] or Room
  -- TODO: Take hospital based on the owner of the plot the room is built on
  local hospital = self.hospitals[1]
  local room = class(x, y, w, h, id, room_info, self, hospital, ...)

  self.rooms[id] = room
  self:clearCaches()
  return room
end

--! Called when a room has been completely built and is ready to use
function World:markRoomAsBuilt(room)
  room:roomFinished()
  local diag_disease = self.hospitals[1].disease_casebook["diag_" .. room.room_info.id]
  if diag_disease and not diag_disease.discovered then
    self.hospitals[1].disease_casebook["diag_" .. room.room_info.id].discovered = true
  end
  for callback in pairs(self.room_build_callbacks) do
    callback(room)
  end
end

--! Called when a room has been deactivated (crashed or edited)
function World:notifyRoomRemoved(room)
  self.dispatcher:dropFromQueue(room)
  for callback in pairs(self.room_remove_callbacks) do
    callback(room)
  end
end

--! Clear all internal caches which are dependant upon map state / object position
function World:clearCaches()
  self.idle_cache = {}
end

function World:getWallIdFromBlockId(block_id)
  -- Remove the transparency flag if present.
  if self.ui.transparent_walls then
    block_id = block_id - 1024
  end
  return self.wall_id_by_block_id[block_id]
end

function World:getWallSetFromBlockId(block_id)
  -- Remove the transparency flag if present.
  if self.ui.transparent_walls then
    block_id = block_id - 1024
  end
  return self.wall_set_by_block_id[block_id]
end

function World:getWallDirFromBlockId(block_id)
  -- Remove the transparency flag if present.
  if self.ui.transparent_walls then
    block_id = block_id - 1024
  end
  return self.wall_dir_by_block_id[block_id]
end

local month_length = {
  31, -- Jan
  28, -- Feb (29 in leap years, but TH doesn't have leap years)
  31, -- Mar
  30, -- Apr
  31, -- May
  30, -- Jun
  31, -- Jul
  31, -- Aug
  30, -- Sep
  31, -- Oct
  30, -- Nov
  31, -- Dec
}

function World:getDate()
  return self.month, self.day
end

-- Game speeds. The second value is the number of world clicks that pass for each
-- in-game tick and the first is the number of hours to progress when this
-- happens.
local tick_rates = {
  ["Pause"]              = {0, 1},
  ["Slowest"]            = {1, 9},
  ["Slower"]             = {1, 5},
  ["Normal"]             = {1, 3},
  ["Max speed"]          = {1, 1},
  ["And then some more"] = {3, 1},
  ["Speed Up"]           = {4, 1},
}

-- Return the length of the current month
function World:getCurrentMonthLength()
  return month_length[self.month]
end

function World:speedUp()
  self:setSpeed("Speed Up")
end

function World:previousSpeed()
  if self:isCurrentSpeed("Speed Up") then
    self:setSpeed(self.prev_speed)
  end
end

-- Return if the selected speed the same as the current speed.
function World:isCurrentSpeed(speed)
  local numerator, denominator = unpack(tick_rates[speed])
  return self.hours_per_tick == numerator and self.tick_rate == denominator
end

-- Return the name of the current speed, relating to a key in tick_rates.
function World:getCurrentSpeed()
  for name, rate in pairs(tick_rates) do
    if rate[1] == self.hours_per_tick and rate[2] == self.tick_rate then
      return name
    end
  end
end

-- Set the (approximate) number of seconds per tick.
--!param speed (string) One of: "Pause", "Slowest", "Slower", "Normal",
-- "Max speed", or "And then some more".
function World:setSpeed(speed)
  if self:isCurrentSpeed(speed) then
    return
  end
  local pause_state_changed = nil
  if speed == "Pause" then
    -- stop screen shaking if there was an earthquake in progress
    if self.active_earthquake then
      self.ui.tick_scroll_amount = {x = 0, y = 0}
    end
    -- By default actions are not allowed when the game is paused.
    self.user_actions_allowed = TheApp.config.allow_user_actions_while_paused
    pause_state_changed = true
  elseif self:getCurrentSpeed() == "Pause" then
    self.user_actions_allowed = true
  end

  local currentSpeed = self:getCurrentSpeed()
  if currentSpeed ~= "Pause" and currentSpeed ~= "Speed Up" then
    self.prev_speed = self:getCurrentSpeed()
  end

  local was_paused = currentSpeed == "Pause"
  local numerator, denominator = unpack(tick_rates[speed])
  self.hours_per_tick = numerator
  self.tick_rate = denominator

  if was_paused then
    TheApp.audio:onEndPause()
  end

  -- Set the blue filter according to whether the user can build or not.
  TheApp.video:setBlueFilterActive(not self.user_actions_allowed)
  return false
end

function World:isPaused()
  return self:isCurrentSpeed("Pause")
end

--! Dedicated function to allow unpausing by pressing 'p' again
function World:pauseOrUnpause()
  if not self:isCurrentSpeed("Pause") then
    self:setSpeed("Pause")
  elseif self.prev_speed then
    self:setSpeed(self.prev_speed)
  end
end

-- Outside (air) temperatures based on climate data for Oxford, taken from
-- Wikipedia. For scaling, 0 degrees C becomes 0 and 50 degrees C becomes 1
local outside_temperatures = {
   4.1  / 50, -- January
   4.4  / 50, -- February
   6.3  / 50, -- March
   8.65 / 50, -- April
  11.95 / 50, -- May
  15    / 50, -- June
  16.95 / 50, -- July
  16.55 / 50, -- August
  14.15 / 50, -- September
  10.5  / 50, -- October
   6.8  / 50, -- November
   4.75 / 50, -- December
}

--! World ticks are translated to game ticks (or hours) depending on the
-- current speed of the game. There are 50 hours in a TH day.
function World:onTick()
  if self.tick_timer == 0 then
    if self.autosave_next_tick then
      self.autosave_next_tick = nil
      local pathsep = package.config:sub(1, 1)
      local dir = TheApp.savegame_dir
      if not dir:sub(-1, -1) == pathsep then
        dir = dir .. pathsep
      end
      if not lfs.attributes(dir .. "Autosaves", "modification") then
        lfs.mkdir(dir .. "Autosaves")
      end
      local status, err = pcall(TheApp.save, TheApp, dir .. "Autosaves" .. pathsep .. "Autosave" .. self.month .. ".sav")
      if not status then
        print("Error while autosaving game: " .. err)
      end
    end
    if self.year == 1 and self.month == 1 and self.day == 1 and self.hour == 0 then
      if not self.ui.start_tutorial then
        self.ui:addWindow(UIWatch(self.ui, "initial_opening"))
        self.ui:showBriefing()
      end
    end
    self.tick_timer = self.tick_rate
    self.hour = self.hour + self.hours_per_tick

    -- if an earthquake is supposed to be going on, call the earthquake function
    if self.active_earthquake then
      self:tickEarthquake()
    end


    -- End of day/month/year
    if self.hour >= self.hours_per_day then
      for _, hospital in ipairs(self.hospitals) do
        hospital:onEndDay()
      end
      self:onEndDay()
      self.hour = self.hour - self.hours_per_day
      self.day = self.day + 1
      if self.day > month_length[self.month] then
        self.day = month_length[self.month]
        for _, hospital in ipairs(self.hospitals) do
          hospital:onEndMonth()
        end
        -- Let the hospitals do what they need to do at end of month first.
        if self:onEndMonth() then
          -- Bail out as the game has already been ended.
          return
        end
        self.day = 1
        self.month = self.month + 1
        if self.month > 12 then
          self.month = 12
          if self.year == 1 then
            for _, hospital in ipairs(self.hospitals) do
              hospital.initial_grace = false
            end
          end
          -- It is crucial that the annual report gets to initialize before onEndYear is called.
          -- Yearly statistics are reset there.
          self.ui:addWindow(UIAnnualReport(self.ui, self))
          self:onEndYear()
          self.year = self.year + 1
          self.month = 1
        end
      end
    end
    for i = 1, self.hours_per_tick do
      for _, hospital in ipairs(self.hospitals) do
        hospital:tick()
      end
      -- A patient might arrive to the player hospital.
      -- TODO: Multiplayer support.
      if self.spawn_hours[self.hour + i-1] and self.hospitals[1].opened then
        for k=1, self.spawn_hours[self.hour + i-1] do
          self:spawnPatient()
        end
      end
      for _, entity in ipairs(self.entities) do
        if entity.ticks then
          self.current_tick_entity = entity
          entity:tick()
        end
      end
      self.current_tick_entity = nil
      self.map:onTick()
      self.map.th:updateTemperatures(outside_temperatures[self.month],
        0.25 + self.hospitals[1].radiator_heat * 0.3)
      if self.ui then
        self.ui:onWorldTick()
      end
      self.dispatcher:onTick()
    end
  end
  if self.hours_per_tick > 0 and self.floating_dollars then
    for obj in pairs(self.floating_dollars) do
      obj:tick()
      if obj:isDead() then
        obj:setTile(nil)
        self.floating_dollars[obj] = nil
      end
    end
  end
  self.tick_timer = self.tick_timer - 1
end

function World:setEndMonth()
  self.day = month_length[self.month]
  self.hour = self.hours_per_day - 1
end

function World:setEndYear()
  self.month = 12
  self:setEndMonth()
end

-- Called immediately prior to the ingame day changing.
function World:onEndDay()
  for _, entity in ipairs(self.entities) do
    if entity.ticks and class.is(entity, Humanoid) then
      self.current_tick_entity = entity
      entity:tickDay()
    elseif class.is(entity, Plant) then
      entity:tickDay()
    end
  end
  self.current_tick_entity = nil

  --check if it's time for a VIP visit
  if (self.year - 1) * 12 + self.month == self.next_vip_month
  and self.day == self.next_vip_day then
    if #self.rooms > 0 and self.ui.hospital:hasStaffedDesk() then
      self.hospitals[1]:createVip()
    else
      self:nextVip()
    end
  end

  -- check if it's time for an earthquake, and the user is at least on level 5
  if (self.year - 1) * 12 + self.month == self.next_earthquake_month
  and self.day == self.next_earthquake_day then
    -- warn the user that an earthquake is on the way
    local announcements = {
      "quake001.wav", "quake002.wav", "quake003.wav", "quake004.wav",
    }
    self.ui:playAnnouncement(announcements[math.random(1, #announcements)])

    self:createEarthquake()
  end

  -- Maybe it's time for an emergency?
  if (self.year - 1) * 12 + self.month == self.next_emergency_month
  and self.day == self.next_emergency_day then
    -- Postpone it if anything clock related is already underway.
    if self.ui:getWindow(UIWatch) then
      self.next_emergency_month = self.next_emergency_month + 1
      local month_of_year = 1 + ((self.next_emergency_month - 1) % 12)
      self.next_emergency_day = math.random(1, month_length[month_of_year])
    else
      -- Do it only for the player hospital for now. TODO: Multiplayer
      local control = self.map.level_config.emergency_control
      if control[0].Mean or control[0].Random then
        -- The level uses random emergencies, so just create one.
        self.hospitals[1]:createEmergency()
      else
        control = control[self.next_emergency_no]
        -- Find out which disease the emergency patients have.
        local disease
        for _, dis in ipairs(self.available_diseases) do
          if dis.expertise_id == control.Illness then
            disease = dis
            break
          end
        end
        if not disease then
          -- Unknown disease! Create a random one instead.
          self.hospitals[1]:createEmergency()
        else
          local emergency = {
            disease = disease,
            victims = math.random(control.Min, control.Max),
            bonus = control.Bonus,
            percentage = control.PercWin/100,
            killed_emergency_patients = 0,
            cured_emergency_patients = 0,
          }
          self.hospitals[1]:createEmergency(emergency)
        end
      end
    end
  end
  -- Any patients tomorrow?
  self.spawn_hours = {}
  if self.spawn_dates[self.day] then
    for i = 1, self.spawn_dates[self.day] do
      local hour = math.random(1, self.hours_per_day)
      self.spawn_hours[hour] = self.spawn_hours[hour] and self.spawn_hours[hour] + 1 or 1
    end
  end
  -- TODO: Do other regular things? Such as checking if any room needs
  -- staff at the moment and making plants need water.
end

function World:checkIfGameWon()
  for i, hospital in ipairs(self.hospitals) do
    local res = self:checkWinningConditions(i)
    if res.state == "win" then
      self:winGame(i)
    end
  end
end

-- Called immediately prior to the ingame month changing.
-- returns true if the game was killed due to the player losing
function World:onEndMonth()
  -- Check if a player has won the level if the year hasn't ended, if it has the
  -- annual report window will perform this check when it has been closed.

  -- TODO.... this is a step closer to the way TH would check.
  -- What is missing is that if offer is declined then the next check should be
  -- either 6 months later or at the end of month 12 and then every 6 months
  if self.month % 3 == 0 and self.month < 12 then
    self:checkIfGameWon()
  end

  -- Change population share for the hospitals, TODO according to reputation.
  -- Since there are no competitors yet the player's hospital can be considered
  -- to be fairly good no matter what it looks like, so after gbv.AllocDelay
  -- months, change the share to half of the new people.
  if self.month >= self.map.level_config.gbv.AllocDelay then
    self:getLocalPlayerHospital().population = 0.5
  end

  -- Also possibly change world spawn rate according to the level configuration.
  local index = 0
  local popn = self.map.level_config.popn
  while popn[index] do
    if popn[index].Month == self.month + (self.year - 1)*12 then
      self.monthly_spawn_increase = popn[index].Change
      break
    end
    index = index + 1
  end
  -- Now set the new spawn rate
  self.spawn_rate = self.spawn_rate + self.monthly_spawn_increase
  self:updateSpawnDates()

  self:makeAvailableStaff((self.year - 1) * 12 + self.month)
  self.autosave_next_tick = true
  for _, entity in ipairs(self.entities) do
    if entity.checkForDeadlock then
      self.current_tick_entity = entity
      entity:checkForDeadlock()
    end
  end
  self.current_tick_entity = nil
end

-- Called when a month ends. Decides on which dates patients arrive
-- during the coming month.
function World:updateSpawnDates()
  -- Set dates when people arrive
  local no_of_spawns = math.n_random(self.spawn_rate, 2)
  -- Use ceil so that at least one patient arrives (unless population = 0)
  no_of_spawns = math.ceil(no_of_spawns*self:getLocalPlayerHospital().population)
  self.spawn_dates = {}
  for i = 1, no_of_spawns do
    -- We are interested in the coming month, pick days from it at random.
    local day = math.random(1, month_length[self.month % 12 + 1])
    self.spawn_dates[day] = self.spawn_dates[day] and self.spawn_dates[day] + 1 or 1
  end
end

-- Called when it is time to determine what the
-- next emergency should look like.
function World:nextEmergency()
  local control = self.map.level_config.emergency_control
  local current_month = (self.year - 1) * 12 + self.month
  -- Does this level use random emergencies?
  if control and (control[0].Random or control[0].Mean) then
    -- Support standard values for mean and variance
    local mean = control[0].Mean or 180
    local variance = control[0].Variance or 30
    -- How many days until next emergency?
    local days = math.round(math.n_random(mean, variance))
    local next_month = self.month

    -- Walk forward to get the resulting month and day.
    if days > month_length[next_month] - self.day then
      days = days - (month_length[next_month] - self.day)
      next_month = next_month + 1
    end
    while days > month_length[(next_month - 1) % 12 + 1] do
      days = days - month_length[(next_month - 1) % 12 + 1]
      next_month = next_month + 1
    end
    -- Make it the same format as for "controlled" emergencies
    self.next_emergency_month = next_month + (self.year - 1) * 12
    self.next_emergency_day = days
  else
    if not self.next_emergency_no then
      self.next_emergency_no = 0
    else
      repeat
        self.next_emergency_no = self.next_emergency_no + 1
        -- Level three is missing [5].
        if not control[self.next_emergency_no]
        and control[self.next_emergency_no + 1] then
          self.next_emergency_no = self.next_emergency_no + 1
        end
      until not control[self.next_emergency_no]
      or control[self.next_emergency_no].EndMonth >= current_month
    end

    local emergency = control[self.next_emergency_no]

    -- No more emergencies?
    if not emergency or emergency.EndMonth == 0 then
      self.next_emergency_month = 0
    else
      -- Generate the next month and day the emergency should occur at.
      -- Make sure it doesn't happen in the past.
      local start = math.max(emergency.StartMonth, self.month + (self.year - 1) * 12)
      local next_month = math.random(start, emergency.EndMonth)
      self.next_emergency_month = next_month
      local day_start = 1
      if start == emergency.EndMonth then
        day_start = self.day
      end
      local day_end = month_length[(next_month - 1) % 12 + 1]
      self.next_emergency_day = math.random(day_start, day_end)
    end
  end
end

-- Called when it is time to have another VIP
function World:nextVip()
  local current_month = (self.year - 1) * 12 + self.month

  -- Support standard values for mean and variance
  local mean = 180
  local variance = 30
  -- How many days until next vip?
  local days = math.round(math.n_random(mean, variance))
  local next_month = self.month

  -- Walk forward to get the resulting month and day.
  if days > month_length[next_month] - self.day then
    days = days - (month_length[next_month] - self.day)
    next_month = next_month + 1
  end
  while days > month_length[(next_month - 1) % 12 + 1] do
    days = days - month_length[(next_month - 1) % 12 + 1]
    next_month = next_month + 1
  end
  self.next_vip_month = next_month + (self.year - 1) * 12
  self.next_vip_day = days
end

-- Called when it is time to have another earthquake
function World:nextEarthquake()
  -- check carefully that no value that we are going to use is going to be nil
  if self.map.level_config.quake_control and
  self.map.level_config.quake_control[self.current_map_earthquake] and
  self.map.level_config.quake_control[self.current_map_earthquake].Severity ~= 0 then
      -- this map has rules to follow when making earthquakes, let's follow them
    local control = self.map.level_config.quake_control[self.current_map_earthquake]
    self.next_earthquake_month = math.random(control.StartMonth, control.EndMonth)
    self.next_earthquake_day = math.random(1, month_length[(self.next_earthquake_month % 12)+1])
    self.earthquake_size = control.Severity
    self.current_map_earthquake = self.current_map_earthquake + 1
  else
    if (tonumber(self.map.level_number) and tonumber(self.map.level_number) >= 5) or
    (not tonumber(self.map.level_number)) then
      local current_month = (self.year - 1) * 12 + self.month

      -- Support standard values for mean and variance
      local mean = 180
      local variance = 30
      -- How many days until next earthquake?
      local days = math.round(math.n_random(mean, variance))
      local next_month = self.month

      -- Walk forward to get the resulting month and day.
      if days > month_length[next_month] - self.day then
        days = days - (month_length[next_month] - self.day)
        next_month = next_month + 1
      end
      while days > month_length[(next_month - 1) % 12 + 1] do
        days = days - month_length[(next_month - 1) % 12 + 1]
        next_month = next_month + 1
      end
      self.next_earthquake_month = next_month + (self.year - 1) * 12
      self.next_earthquake_day = days

      -- earthquake can be between 1 and 10 (non-inclusive) on the richter scale
      -- Make quakes at around 4 more probable.
      self.earthquake_size = math.round(math.min(math.max(math.n_random(4,2), 1), 9))
    end
  end
end


--! Checks if all goals have been achieved or if the player has lost.
--! Returns a table that always contains a state string ("win", "lose" or "nothing").
--! If the state is "lose", the table also contains a reason string,
--! which corresponds to the criterion name the player lost to
--! (reputation, balance, percentage_killed) and a number limit which
--! corresponds to the limit the player passed.
--!param player_no The index of the player to check in the world's list of hospitals
function World:checkWinningConditions(player_no)
  -- If there are no goals at all, do nothing.
  if #self.goals == 0 then
    return {state = "nothing"}
  end

  -- Default is to win.
  -- As soon as a goal that doesn't support this is found it is changed.
  local result = {state = "win"}
  local hospital = self.hospitals[player_no]

  -- Go through the goals
  for i, goal in ipairs(self.goals) do
    local current_value = hospital[goal.name]
    -- If max_min is 1 the value must be > than the goal condition.
    -- If 0 it must be < than the goal condition.
    if goal.lose_value then
      local max_min = goal.max_min_lose == 1 and 1 or -1
      -- Is this a minimum/maximum that has been passed?
      -- This is actually not entirely correct. A lose condition
      -- for balance at -1000 will make you lose if you have exactly
      -- -1000 too, but how often does that happen? Probably not more often
      -- than having exactly e.g. 200 in reputation,
      -- which is handled correctly.
      if (current_value - goal.lose_value) * max_min > 0 then
        result.state = "lose"
        result.reason = goal.name
        result.limit = goal.lose_value
        break
      end
    end
    if goal.win_value then
      local max_min = goal.max_min_win == 1 and 1 or -1
      -- Special case for balance, subtract any loans!
      if goal.name == "balance" then
        current_value = current_value - hospital.loan
      end
      -- Is this goal not fulfilled yet?
      if (current_value - goal.win_value) * max_min <= 0 then
        result.state = "nothing"
      end
    end
  end
  return result
end

function World:winGame(player_no)
  if player_no == 1 then -- Player won. TODO: Needs to be changed for multiplayer
    local text = {}
    local choice_text, choice
    local bonus_rate = math.random(4,9)
    local with_bonus = self.ui.hospital.cheated and 0 or (self.ui.hospital.player_salary * bonus_rate) / 100
    self.ui.hospital.salary_offer = math.floor(self.ui.hospital.player_salary + with_bonus)
    if tonumber(self.map.level_number) then
      local no = tonumber(self.map.level_number)
      local repeated_offer = false -- TODO whether player was asked previously to advance and declined
      local has_next = no < 12 and not TheApp.using_demo_files
      -- Letters 1-4  normal
      -- Letters 5-8  repeated offer
      -- Letters 9-12 last level
      local letter_idx = math.random(1, 4) + (not has_next and 8 or repeated_offer and 4 or 0)
      for key, value in ipairs(_S.letter[letter_idx]) do
        text[key] = value
      end
      text[1] = text[1]:format(self.hospitals[player_no].name)
      text[2] = text[2]:format(self.hospitals[player_no].salary_offer)
      text[3] = text[3]:format(_S.level_names[self.map.level_number + 1])
      if has_next then
        choice_text = _S.fax.choices.accept_new_level
        choice = 1
      else
        choice_text = _S.fax.choices.return_to_main_menu
        choice = 2
      end
    else
      -- TODO: When custom levels can contain sentences this should be changed to something better.
      text[1] = _S.letter.dear_player:format(self.hospitals[player_no].name)
      text[2] = _S.letter.custom_level_completed
      text[3] = _S.letter.return_to_main_menu
      choice_text = _S.fax.choices.return_to_main_menu
      choice = 2
    end
    local message = {
      {text = text[1]},
      {text = text[2]},
      {text = text[3]},
      choices = {
        {text = choice_text,  choice = choice == 1 and "accept_new_level" or "return_to_main_menu"},
        {text = _S.fax.choices.decline_new_level, choice = "stay_on_level"},
      },
    }
    local --[[persistable:world_win_game_message_close_callback]] function callback ()
      local world = self.ui.app.world
      if world then
        world.hospitals[player_no].game_won = false
        if world:isCurrentSpeed("Pause") then
          world:setSpeed(world.prev_speed)
        end
      end
    end
    self.hospitals[player_no].game_won = true
    if self:isCurrentSpeed("Speed Up") then
      self:previousSpeed()
    end
    self:setSpeed("Pause")
    self.ui.app.video:setBlueFilterActive(false)
    self.ui.bottom_panel:queueMessage("information", message, nil, 0, 2, callback)
    self.ui.bottom_panel:openLastMessage()
  end
end

--! Cause the player with the player number player_no to lose.
--!param player_no (number) The number of the player which should lose.
--!param reason (string) [optional] The name of the criterion the player lost to.
--!param limit (number) [optional] The number the player went over/under which caused him to lose.
function World:loseGame(player_no, reason, limit)
  if player_no == 1 then -- TODO: Multiplayer
    self.ui.app.moviePlayer:playLoseMovie()
    local message = {_S.information.level_lost[1]}
    if reason then
      message[2] = _S.information.level_lost[2]
      message[3] = _S.information.level_lost[reason]:format(limit)
    else
      message[2] = _S.information.level_lost["cheat"]
     end
    self.ui.app:loadMainMenu(message)
  end
end

-- Called immediately prior to the ingame year changing.
function World:onEndYear()
  for _, hospital in ipairs(self.hospitals) do
    hospital:onEndYear()
  end
  -- This is done here instead of in onEndMonth so that the player gets
  -- the chance to receive money or reputation from trophies and awards first.
  for i, hospital in ipairs(self.hospitals) do
    local res = self:checkWinningConditions(i)
    if res.state == "lose" then
      self:loseGame(i, res.reason, res.limit)
      if i == 1 then
        return true
      end
    end
  end
end

-- Calculate the distance of the shortest path (along passable tiles) between
-- the two given map tiles. This operation is commutative (swapping (x1, y1)
-- with (x2, y2) has no effect on the result) if both tiles are passable.
--!param x1 (integer) X-cordinate of first tile's Lua tile co-ordinates.
--!param y1 (integer) Y-cordinate of first tile's Lua tile co-ordinates.
--!param x2 (integer) X-cordinate of second tile's Lua tile co-ordinates.
--!param y2 (integer) Y-cordinate of second tile's Lua tile co-ordinates.
--!return (integer, boolean) The distance of the shortest path, or false if
-- there is no path.
function World:getPathDistance(x1, y1, x2, y2)
  return self.pathfinder:findDistance(x1, y1, x2, y2)
end

function World:getPath(x, y, dest_x, dest_y)
  return self.pathfinder:findPath(x, y, dest_x, dest_y)
end

function World:getIdleTile(x, y, idx)
  local cache_idx = (y - 1) * self.map.width + x
  local cache = self.idle_cache[cache_idx]
  if not cache then
    cache = {
      x = {},
      y = {},
    }
    self.idle_cache[cache_idx] = cache
  end
  if not cache.x[idx] then
    local ix, iy = self.pathfinder:findIdleTile(x, y, idx)
    if not ix then
      return ix, iy
    end
    cache.x[idx] = ix
    cache.y[idx] = iy
  end
  return cache.x[idx], cache.y[idx]
end

--[[
This function checks if a tile has no entity on it and (optionally) if it is not
in a room.
!param x (integer) the queried tile's x coordinate.
!param y (integer) the queried tile's y coordinate.
!param not_in_room (boolean) If set, also check the tile is not in a room.
!return (boolean) whether all checks hold.
--]]
function World:isTileEmpty(x, y, not_in_room)
  for _, entity in ipairs(self.entities) do
    if entity.tile_x == x and entity.tile_y == y then
      return false
    end
  end
  if not_in_room then
    return self:getRoom(x, y) == nil
  end
  return true
end

local face_dir = {
  [0] = "south",
  [1] = "west",
  [2] = "north",
  [3] = "east",
}

function World:getFreeBench(x, y, distance)
  local bench, rx, ry, bench_distance
  local object_type = self.object_types.bench
  x, y, distance = math.floor(x), math.floor(y), math.ceil(distance)
  self.pathfinder:findObject(x, y, object_type.thob, distance, function(x, y, d, dist)
    local b = self:getObject(x, y, "bench")
    if b and not b.user and not b.reserved_for then
      local orientation = object_type.orientations[b.direction]
      if orientation.pathfind_allowed_dirs[d] then
        rx = x + orientation.use_position[1]
        ry = y + orientation.use_position[2]
        bench = b
        bench_distance = dist
        return true
      end
    end
  end)
  return bench, rx, ry, bench_distance
end

--! Checks whether the given tile is part of a nearby object (walkable tiles
--  count as part of the object)
--!param x X position of the given tile.
--!param y Y position of the given tile.
--!param distance The number of tiles away from the tile to search.
--!return (boolean) Whether the tile is part of a nearby object.
function World:isTilePartOfNearbyObject(x, y, distance)
  for o in pairs(self:findAllObjectsNear(x, y, distance)) do
    for _, xy in ipairs(o:getWalkableTiles()) do
      if xy[1] == x and xy[2] == y then
        return true
      end
    end
  end
  return false
end

-- Returns a set of all objects near the given position but if supplied only of the given object type.
--!param x The x-coordinate at which to originate the search
--!param y The y-coordinate
--!param distance The number of tiles away from the origin to search
--!param object_type_name The name of the objects that are being searched for
function World:findAllObjectsNear(x, y, distance, object_type_name)
  if not distance then
    -- Note that regardless of distance, only the room which the humanoid is in
    -- is searched (or the corridor if the humanoid is not in a room).
    distance = 2^30
  end
  local objects = {}
  local thob = 0
  if object_type_name then
    local obj_type = self.object_types[object_type_name]
    if not obj_type then
      error("Invalid object type name: " .. object_type_name)
    end
    thob = obj_type.thob
  end

  local callback = function(x, y, d)
    local obj = self:getObject(x, y, object_type_name)
    if obj then
      objects[obj] = true
    end
  end
  self.pathfinder:findObject(x, y, thob, distance, callback)
  return objects
end

--[[ Find all objects of the given type near the humanoid.
Note that regardless of distance, only the room which the humanoid is in
is searched (or the corridor if the humanoid is not in a room).

When no callback is specified then the first object found is returned,
along with its usage tile position. This may return an object already being
used - if you want to find an object not in use (in order to use it),
then call findFreeObjectNearToUse instead.

!param humanoid The humanoid to search around
!param object_type_name The objects to search for
!param distance Maximum L1 distance to search from humanoid. If nil then
       everywhere in range will be searched.
!param callback Function to call for each result. If it returns true then
       the search will be ended.
--]]
function World:findObjectNear(humanoid, object_type_name, distance, callback)
  if not distance then
    distance = 2^30
  end
  local obj, ox, oy
  if not callback then
    -- The default callback returns the first object found
    callback = function(x, y, d)
      obj = self:getObject(x, y, object_type_name)
      local orientation = obj.object_type.orientations
      if orientation then
        orientation = orientation[obj.direction]
        if not orientation.pathfind_allowed_dirs[d] then
          return
        end
        x = x + orientation.use_position[1]
        y = y + orientation.use_position[2]
      end
      ox = x
      oy = y
      return true
    end
  end
  local thob = 0
  if type(object_type_name) == "table" then
    local original_callback = callback
    callback = function(x, y, ...)
      local obj = self:getObject(x, y, object_type_name)
      if obj then
        return original_callback(x, y, ...)
      end
    end
  elseif object_type_name ~= nil then
    local obj_type = self.object_types[object_type_name]
    if not obj_type then
      error("Invalid object type name: " .. object_type_name)
    end
    thob = obj_type.thob
  end
  self.pathfinder:findObject(humanoid.tile_x, humanoid.tile_y, thob, distance,
    callback)
  -- These return values are only relevant for the default callback - are nil
  -- for custom callbacks
  return obj, ox, oy
end

function World:findFreeObjectNearToUse(humanoid, object_type_name, which, current_object)
  -- If which == nil or false, then the nearest object is taken.
  -- If which == "far", then the furthest object is taken.
  -- If which == "near", then the nearest object is taken with 50% probability, the second nearest with 25%, and so on
  -- Other values for which may be added in the future.
  -- Specify current_object if you want to exclude the currently used object from the search
  local object, ox, oy
  self:findObjectNear(humanoid, object_type_name, nil, function(x, y, d)
    local obj = self:getObject(x, y, object_type_name)
    if obj.user or (obj.reserved_for and obj.reserved_for ~= humanoid) or (current_object and obj == current_object) then
      return
    end
    local orientation = obj.object_type.orientations
    if orientation then
      orientation = orientation[obj.direction]
      if not orientation.pathfind_allowed_dirs[d] then
        return
      end
      x = x + orientation.use_position[1]
      y = y + orientation.use_position[2]
    end
    object = obj
    ox = x
    oy = y
    if which == "far" then
      -- just take the last found object, so don't ever abort
    elseif which == "near" then
      -- abort at each item with 50% probability
      local chance = math.random(1, 2)
      if chance == 1 then
        return true
      end
    else
      -- default: return at the first found item
      return true
    end
  end)
  return object, ox, oy
end

function World:findRoomNear(humanoid, room_type_id, distance, mode)
  -- If mode == "nearest" (or nil), the nearest room is taken
  -- If mode == "advanced", prefer a near room, but also few patients and fulfilled staff criteria
  local room
  local score
  if not mode then
    mode = "nearest" -- default mode
  end
  if not distance then
    distance = 2^30
  end
  for _, r in pairs(self.rooms) do repeat
    if r.built and (not room_type_id or r.room_info.id == room_type_id) and r.is_active and r.door.queue.max_size ~= 0 then
      local x, y = r:getEntranceXY(false)
      local d = self:getPathDistance(humanoid.tile_x, humanoid.tile_y, x, y)
      if not d or d > distance then
        break -- continue
      end
      local this_score = d
      if mode == "advanced" then
        this_score = this_score + r:getUsageScore()
      end
      if not score or this_score < score then
        score = this_score
        room = r
      end
    end
  until true end
  return room
end

--! Setup an animated floating money amount above a patient.
--!param patient Patient to float above.
--!param amount Amount of money to display.
function World:newFloatingDollarSign(patient, amount)
  if not self.floating_dollars then
    self.floating_dollars = {}
  end
  if self.free_build_mode then
    return
  end
  local spritelist = TH.spriteList()
  spritelist:setPosition(-17, -60)
  spritelist:setSpeed(0, -1):setLifetime(100)
  spritelist:setSheet(TheApp.gfx:loadSpriteTable("Data", "Money01V"))
  spritelist:append(1, 0, 0)
  local len = #("%i"):format(amount)
  local xbase = math.floor(10.5 + (20 - 5 * len) / 2)
  for i = 1, len do
    local digit = amount % 10
    amount = (amount - digit) / 10
    spritelist:append(2 + digit, xbase + 5 * (len - i), 5)
  end
  spritelist:setTile(self.map.th, patient.tile_x, patient.tile_y)

  self.floating_dollars[spritelist] = true
end

function World:newEntity(class, animation)
  local th = TH.animation()
  th:setAnimation(self.anims, animation)
  local entity = _G[class](th)
  self.entities[#self.entities + 1] = entity
  entity.world = self
  return entity
end

function World:destroyEntity(entity)
  for i, e in ipairs(self.entities) do
    if e == entity then
      table.remove(self.entities, i)
      break
    end
  end
  entity:onDestroy()
end

function World:newObjectType(new_object)
  self.object_types[new_object.id] = new_object
end

--! Creates a new object by finding the object_type from the "id" variable and
--  calls its class constructor.
--!param id (string) The unique id of the object to be created.
--!return The created object.
function World:newObject(id, ...)
  local object_type = self.object_types[id]
  local entity
  if object_type.class then
    entity = _G[object_type.class](self, object_type, ...)
  elseif object_type.default_strength then
    entity = Machine(self, object_type, ...)
    -- Tell the player if there is no handyman to take care of the new machinery.
    if not self.hospitals[1]:hasStaffOfCategory("Handyman") then
      self.ui.adviser:say(_A.staff_advice.need_handyman_machines)
    end
  else
    entity = Object(self, object_type, ...)
  end
  self:objectPlaced(entity, id)
  return entity
end

function World:canNonSideObjectBeSpawnedAt(x, y, objects_id, orientation, spawn_rooms_id)
  local object = self.object_types[objects_id]
  local objects_footprint = object.orientations[orientation].footprint
  for _, tile in ipairs(objects_footprint) do
    local tiles_world_x = x + tile[1]
    local tiles_world_y = y + tile[2]
    if self:areFootprintTilesCoardinatesInvalid(tiles_world_x, tiles_world_y) then
      return false
    end

    if not self:willObjectsFootprintTileBeWithinItsAllowedRoomIfLocatedAt(x, y, object, spawn_rooms_id).within_room then
      return false
    end

    if not self:isFootprintTileBuildableOrPassable(x, y, tile, objects_footprint, "buildable") then
      return false
    end
  end
  return not self:wouldNonSideObjectBreakPathfindingIfSpawnedAt(x, y, object, orientation, spawn_rooms_id)
end

function World:areFootprintTilesCoardinatesInvalid(x, y)
  return x < 1 or x > self.map.width or y < 1 or y > self.map.height
end

---
-- @param allowed_rooms_id_parameter Should be nil when the object is allowed to be placed in any room.
-- @return {within_room, roomId}
---
function World:willObjectsFootprintTileBeWithinItsAllowedRoomIfLocatedAt(x, y, object, allowed_rooms_id_parameter)
  local xy_rooms_id = self.map.th:getCellFlags(x, y, {}).roomId

  if allowed_rooms_id_parameter then
    return {within_room = allowed_rooms_id_parameter == xy_rooms_id, roomId = allowed_rooms_id_parameter}
  elseif xy_rooms_id == 0 then
    return {within_room = object.corridor_object ~= nil, roomId = xy_rooms_id}
  else
    for _, additional_objects_name in pairs(self.rooms[xy_rooms_id].room_info.objects_additional) do
      if TheApp.objects[additional_objects_name].thob == object.thob then
        return {within_room = true, roomId = xy_rooms_id}
      end
    end
    for needed_objects_name, _ in pairs(self.rooms[xy_rooms_id].room_info.objects_needed) do
      if TheApp.objects[needed_objects_name].thob == object.thob then
        return {within_room = true, roomId = xy_rooms_id}
      end
    end
    return {within_room = false, roomId = xy_rooms_id}
  end
end

---
-- A footprint tile will either need to be buildable or passable so this function
-- checks if its buildable/passable using the tile's appropriate flag and then returns this
-- flag's boolean value or false if the tile isn't valid.
---
function World:isFootprintTileBuildableOrPassable(x, y, tile, footprint, requirement_flag)
  local function isTileValid(x, y, complete_cell, flags, flag_name, need_side)
    if complete_cell or need_side then
      return flags[flag_name]
    end
    for _, tile in ipairs(footprint) do
      if(tile[1] == x and tile[2] == y) then
        return flags[flag_name]
      end
    end
    return true
  end

  local direction_parameters = {
      north = { x = 0, y = -1, buildable_flag = "buildableNorth", passable_flag = "travelNorth", needed_side = "need_north_side"},
      east = { x = 1, y = 0, buildable_flag =  "buildableEast", passable_flag = "travelEast", needed_side = "need_east_side"},
      south = { x = 0, y = 1, buildable_flag = "buildableSouth", passable_flag = "travelSouth", needed_side = "need_south_side"},
      west = { x = -1, y = 0, buildable_flag = "buildableWest", passable_flag = "travelWest", needed_side = "need_west_side"}
    }
  local flags = {}
  local requirement_met = self.map.th:getCellFlags(x, y, flags)[requirement_flag]

  if requirement_met then
    -- For each direction check that the tile is valid:
    for _, direction in pairs(direction_parameters) do
      local x1, y1 = tile[1] + direction["x"], tile[2] + direction["y"]
      if not isTileValid(x1, y1, tile.complete_cell, flags, direction["buildable_flag"], tile[direction["needed_side"]]) then
        return false
      end
    end
    return true
  else
    return false
  end
end

---
-- Check that pathfinding still works, i.e. that placing the object
-- wouldn't disconnect one part of the hospital from another. To do
-- this, we provisionally mark the footprint as unpassable (as it will
-- become when the object is placed), and then check that the cells
-- surrounding the footprint have not had their connectedness changed.
---
function World:wouldNonSideObjectBreakPathfindingIfSpawnedAt(x, y, object, objects_orientation, spawn_rooms_id)
  local objects_footprint = object.orientations[objects_orientation].footprint
  local map = self.map.th

  local function setFootprintTilesPassable(passable)
    for _, tile in ipairs(objects_footprint) do
      if not tile.only_passable then
        map:setCellFlags(x + tile[1], y + tile[2], {passable = passable})
      end
    end
  end

  local function isIsolated(x, y)
    setFootprintTilesPassable(false)
    local result = not self.pathfinder:isReachableFromHospital(x, y)
    setFootprintTilesPassable(true)
    return result
  end

  local all_good = true

  --1. Find out which footprint tiles are passable now before this function makes some unpassable
  --during its test:
  local tiles_passable_flags = {}
  for _, tile in ipairs(objects_footprint) do
    table.insert(tiles_passable_flags, map:getCellFlags(x + tile[1], y + tile[2], {}).passable)
  end

  --2. Find out which tiles adjacent to the footprint would become isolated:
  setFootprintTilesPassable(false)
  local prev_x, prev_y
  for _, tile in ipairs(object.orientations[objects_orientation].adjacent_to_solid_footprint) do
    local x = x + tile[1]
    local y = y + tile[2]
    local flags = {}
    if map:getCellFlags(x, y, flags).roomId == spawn_rooms_id and flags.passable then
      if prev_x then
        if not self.pathfinder:findDistance(x, y, prev_x, prev_y) then
          -- There is no route between the two map nodes. In most cases,
          -- this means that connectedness has changed, though there is
          -- one rare situation where the above test is insufficient. If
          -- (x, y) is a passable but isolated node outside the hospital
          -- and (prev_x, prev_y) is in the corridor, then the two will
          -- not be connected now, but critically, neither were they
          -- connected before.
          if not isIsolated(x, y) then
            if not isIsolated(prev_x, prev_y) then
              all_good = false
              break
            end
          else
            x = prev_x
            y = prev_y
          end
        end
      end
      prev_x = x
      prev_y = y
    end
  end

  -- 3. For each footprint tile passable flag set to false by step 2 undo this change:
  for tiles_index, tile in ipairs(objects_footprint) do
    map:setCellFlags(x + tile[1], y + tile[2], {passable = tiles_passable_flags[tiles_index]})
  end

  return not all_good
end

--! Notifies the world that an object has been placed, notifying
--  interested entities in the vicinity of the new arrival.
--!param entity (Entity) The entity that was just placed.
--!param id (string) That entity's id.
function World:objectPlaced(entity, id)
  -- If id is not supplied, we can use the entities internal id if it exists
  -- This is so the bench check below works
  -- see place_object.lua:UIPlaceObjects:placeObject for call w/o id --cgj
  if not id and entity.object_type.id then
    id = entity.object_type.id
  end

  self.entities[#self.entities + 1] = entity
  -- If it is a bench we're placing, notify queueing patients in the vicinity
  if id == "bench" and entity.tile_x and entity.tile_y then
    for _, patient in ipairs(self.entities) do
      if class.is(patient, Patient) then
        if math.abs(patient.tile_x - entity.tile_x) < 7 and
          math.abs(patient.tile_y - entity.tile_y) < 7 then
          patient:notifyNewObject(id)
        end
      end
    end
  end
  if id == "reception_desk" then
    if not self.ui.start_tutorial
    and not self.hospitals[1]:hasStaffOfCategory("Receptionist") then
      -- TODO: Will not work correctly for multiplayer
      self.ui.adviser:say(_A.room_requirements.reception_need_receptionist)
    elseif self.hospitals[1]:hasStaffOfCategory("Receptionist") and self.object_counts["reception_desk"] == 1
    and not self.hospitals[1].receptionist_msg and self.month > 3 then
      self.ui.adviser:say(_A.warnings.no_desk_5)
      self.hospitals[1].receptionist_msg = true
    end
    -- A new reception desk? Then add it to the reception desk set.
    self:getLocalPlayerHospital().reception_desks[entity] = true
  end
  -- If it is a plant it might be advisable to hire a handyman
  if id == "plant" and not self.hospitals[1]:hasStaffOfCategory("Handyman") then
    self.ui.adviser:say(_A.staff_advice.need_handyman_plants)
  end
  if id == "gates_to_hell" then
    entity:playSoundsAtEntityInRandomSequence("LAVA00*.WAV",
                                              {0,1350,1150,950,750,350},
                                              {0,1450,1250,1050,850,450},
                                              40)
    entity:setTimer(entity.world:getAnimLength(2550),
                    --[[persistable:lava_hole_spawn_animation_end]]
                    function(entity)
                      entity:setAnimation(1602)
                    end)
    entity:setAnimation(2550)
  end
end

--! Notify the world of an object being removed from a tile
--! See also `World:addObjectToTile`
--!param object (Object) The object being removed.
--!param x (integer) The X-coordinate of the tile which the object was on
--!param y (integer) The Y-coordinate of the tile which the object was on
function World:removeObjectFromTile(object, x, y)
  local index = (y - 1) * self.map.width + x
  local objects = self.objects[index]
  local thob = object.object_type.thob
  if objects then
    for k, v in ipairs(objects) do
      if v == object then
        table_remove(objects, k)
        self.map.th:removeObjectType(x, y, thob)
        local count_cat = object.object_type.count_category
        if count_cat then
          self.object_counts[count_cat] = self.object_counts[count_cat] - 1
        end
        return true
      end
    end
  end
  return false
end

--! Notify the world of a new object being placed somewhere in the world
--! See also `World:removeObjectFromTile`
--!param object (Object) The object being placed
--!param x (integer) The X-coordinate of the tile being placed upon
--!param y (integer) The Y-coordinate of the tile being placed upon
function World:addObjectToTile(object, x, y)
  local index = (y - 1) * self.map.width + x
  local objects = self.objects[index]
  if objects then
    self.map.th:setCellFlags(x, y, {thob = object.object_type.thob})
    objects[#objects + 1] = object
  else
    objects = {object}
    self.objects[index] = objects
    self.map.th:setCellFlags(x, y, {thob = object.object_type.thob})
  end
  local count_cat = object.object_type.count_category
  if count_cat then
    self.object_counts[count_cat] = self.object_counts[count_cat] + 1
  end
  return true
end

function World:getObjects(x, y)
  local index = (y - 1) * self.map.width + x
  return self.objects[index]
end

function World:getObject(x, y, id)
  local objects = self:getObjects(x, y)
  if objects then
    if not id then
      return objects[1]
    elseif type(id) == "table" then
      for _, obj in ipairs(objects) do
        if id[obj.object_type.id] then
          return obj
        end
      end
    else
      for _, obj in ipairs(objects) do
        if obj.object_type.id == id then
          return obj
        end
      end
    end
  end
  return -- nil
end

function World:getObjectsById(id)
  if not id then
      return self.objects
  end

  local ret = {}
  if type(id) == "table" then
    for position, obj_list in pairs(self.objects) do
      for _, obj in ipairs(obj_list) do
        if id[obj.object_type.id] then
          table.insert(ret, obj)
        end
      end
    end
  else
    for position, obj_list in pairs(self.objects) do
      for _, obj in ipairs(obj_list) do
        if obj.object_type.id == id then
          table.insert(ret, obj)
        end
      end
    end
  end

  return ret
end

--! Remove litter from a tile.
--!param obj (Litter) litter to remove.
--!param x (int) X position of the tile.
--!param y (int) Y position of the tile.
function World:removeLitter(obj, x, y)
  self:removeObjectFromTile(obj, x, y)
  self:destroyEntity(obj)
  self.map.th:setCellFlags(x, y, {buildable = true})
end

--! Get the room at a given tile location.
--!param x X position of the queried tile.
--!param y Y position of the queried tile.
--!return (Room) Room of the tile, or 'nil'.
function World:getRoom(x, y)
  return self.rooms[self.map:getRoomId(x, y)]
end

--! Returns localized name of the room, internal required staff name
-- and localized name of staff required.
function World:getRoomNameAndRequiredStaffName(room_id)
  local room_name, required_staff, staff_name
  for _, room in ipairs(TheApp.rooms) do
    if room.id == room_id then
      room_name = room.name
      required_staff = room.required_staff
    end
  end
  for key, _ in pairs(required_staff) do
    staff_name = key
  end
  required_staff = staff_name -- This is the "programmatic" name of the staff.
  if staff_name == "Nurse" then
    staff_name = _S.staff_title.nurse
  elseif staff_name == "Psychiatrist" then
    staff_name = _S.staff_title.psychiatrist
  elseif staff_name == "Researcher" then
    staff_name = _S.staff_title.researcher
  elseif staff_name == "Surgeon" then
    staff_name = _S.staff_title.surgeon
  elseif staff_name == "Doctor" then
    staff_name = _S.staff_title.doctor
  end
  return room_name, required_staff, staff_name
end

--! Append a message to the game log.
--!param message (string) The message to add.
function World:gameLog(message)
  self.game_log[#self.game_log + 1] = message
  -- If in debug mode also show it in the command prompt
  if TheApp.config.debug then
    print(message)
  end
end

--! Dump the contents of the game log into a file.
-- This is automatically done on each error.
function World:dumpGameLog()
  local config_path = TheApp.command_line["config-file"] or ""
  local pathsep = package.config:sub(1, 1)
  config_path = config_path:match("^(.-)[^".. pathsep .."]*$")
  local gamelog_path = config_path .. "gamelog.txt"
  local fi, err = io.open(gamelog_path, "w")
  if fi then
    for _, str in ipairs(self.game_log) do
      fi:write(str .. "\n")
    end
    fi:close()
  else
    print("Warning: Cannot dump game log: " .. tostring(err))
  end
end

--! Because the save file only saves one thob per tile if they are more that information
-- will be lost. To solve this after a load we need to set again all the thobs on each tile.
function World:resetAnimations()
  for _, entity in ipairs(self.entities) do
    entity:resetAnimation()
  end
end

--! Let the world react to and old save game. First it gets the chance to
-- do things for itself, and then it calls corresponding functions for
-- the hospitals, entities and rooms in that order.
--!param old The old version of the save game.
--!param new The current version of the save game format.
function World:afterLoad(old, new)

  if not self.original_savegame_version then
    self.original_savegame_version = old
  end
  -- If the original save game version is considerably lower than the current, warn the player.
  if new - 20 > self.original_savegame_version then
    self.ui:addWindow(UIInformation(self.ui, {_S.information.very_old_save}))
  end
  -- insert global compatibility code here
  if old < 4 then
    self.room_built = {}
  end
  if old < 6 then
    -- Calculate hospital value

    -- Initial value
    local value = self.map.parcelTileCounts[self.hospitals[1]:getPlayerIndex()] * 25 + 20000

    -- Add room values
    for _, room in pairs(self.rooms) do
      local valueChange = room.room_info.build_cost

      -- Subtract values of objects in rooms to avoid calculating those object values twice
      for obj, num in pairs(room.room_info.objects_needed) do
        valueChange = valueChange - num * TheApp.objects[obj].build_cost
      end
      value = value + valueChange
    end

    -- Add up all object values
    for _, object in ipairs(self.entities) do
        if class.is(object, Object) and object.object_type.build_cost then
          value = value + object.object_type.build_cost
        end
    end

    self.hospitals[1].value = value
  end

  if old < 7 then
    self.level_criteria = local_criteria_variable
    self:determineWinningConditions()
  end
  if old < 10 then
    self.object_counts = {
      extinguisher = 0,
      radiator = 0,
      plant = 0,
      general = 0,
    }
    for position, obj_list in pairs(self.objects) do
      for _, obj in ipairs(obj_list) do
        local count_cat = obj.object_type.count_category
        if count_cat then
          self.object_counts[count_cat] = self.object_counts[count_cat] + 1
        end
      end
    end
  end
  if old < 43 then
    self.object_counts.reception_desk = 0
    for position, obj_list in pairs(self.objects) do
      for _, obj in ipairs(obj_list) do
        local count_cat = obj.object_type.count_category
        if count_cat and count_cat == "reception_desk" then
          self.object_counts[count_cat] = self.object_counts[count_cat] + 1
        end
      end
    end
  end
  if old < 47 then
    self.object_counts.bench = 0
    for position, obj_list in pairs(self.objects) do
      for _, obj in ipairs(obj_list) do
        local count_cat = obj.object_type.count_category
        if count_cat and count_cat == "bench" then
          self.object_counts[count_cat] = self.object_counts[count_cat] + 1
        end
      end
    end
  end
  if old < 12 then
    self.animation_manager = TheApp.animation_manager
    self.anim_length_cache = nil
  end
  if old < 16 then
    self.ui:addKeyHandler("+", self, self.adjustZoom,  1)
    self.ui:addKeyHandler("-", self, self.adjustZoom, -1)
  end
  if old < 17 then
    -- Added another object
    local _, shield = pcall(dofile, "objects" .. pathsep .. "radiation_shield")
    local _, shield_b = pcall(dofile, "objects" .. pathsep .. "radiation_shield_b")
    shield.slave_type = shield_b
    shield.slave_type.master_type = shield
    Object.processTypeDefinition(shield)
    Object.processTypeDefinition(shield_b)

    self.object_id_by_thob[shield.thob] = shield.id
    self.object_id_by_thob[shield_b.thob] = shield_b.id
    self.object_types[shield.id] = shield
    self.object_types[shield_b.id] = shield_b
    self.ui.app.objects[shield.id] = shield
    self.ui.app.objects[shield_b.id] = shield_b
    self.ui.app.objects[#self.ui.app.objects + 1] = shield
    self.ui.app.objects[#self.ui.app.objects + 1] = shield_b
  end
  if old < 27 then
    -- Add callsDispatcher
    self.dispatcher = CallsDispatcher(self)
  end
  if old < 30 then
    self:nextEmergency()
  end
  if old < 31 then
    self.hours_per_day = 50
    self:setSpeed("Normal")
  end
  if old < 36 then
    self:determineWinningConditions()
  end
  if old < 37 then
    -- Spawn rate is taken from level files now.
    -- Make sure that all config values are present.
    if not self.map.level_config.popn then
      self.map.level_config.popn = {
        [0] = {Change = 3, Month = 0},
        [1] = {Change = 1, Month = 1},
      }
    end
    if not self.map.level_config.gbv.AllocDelay then
      self.map.level_config.gbv.AllocDelay = 3
    end
    local index = 0
    local popn = self.map.level_config.popn
    self.spawn_rate = popn[index].Change
    self.monthly_spawn_increase = self.spawn_rate

    -- Bring the spawn rate "up to speed".
    for month = 1, self.month + (self.year-1)*12 do
      -- Check if the next entry should be used.
      while popn[index + 1] and month >= popn[index + 1].Month do
        index = index + 1
      end
      self.monthly_spawn_increase = popn[index].Change
      self.spawn_rate = self.spawn_rate + self.monthly_spawn_increase
    end
    self.spawn_hours = {}
    self.spawn_dates = {}
    self:updateSpawnDates()
  end
  if old < 45 then
    self:nextVip()
  end
  if old < 52 then
    -- Litter was not properly removed from the world.
    for i = #self.entities, 1, -1 do
      if class.is(self.entities[i], Litter) then
        if not self.entities[i].tile_x then
          self:destroyEntity(self.entities[i])
        end
      end
    end
  end
  if old < 53 then
    self.current_map_earthquake = 0
    -- It may happen that the current game has gone on for a while
    if self.map.level_config.quake_control then
      while true do
        if self.map.level_config.quake_control[self.current_map_earthquake] and
        self.map.level_config.quake_control[self.current_map_earthquake] ~= 0 then
          -- Check to see if the start month has passed
          local control = self.map.level_config.quake_control[self.current_map_earthquake]
          if control.StartMonth <= self.month + 12 * (self.year - 1) then
            -- Then check the next one
            self.current_map_earthquake = self.current_map_earthquake + 1
          else
            -- We found an earthquake coming in the future!
            break
          end
        else
          -- No more earthquakes in the config file.
          break
        end
      end
    end
    -- Now set up the next earthquake.
    self:nextEarthquake()
  end
  if old < 57 then
    self.user_actions_allowed = true
  end
  if old < 61 then
    -- room remove callbacks added
    self.room_remove_callbacks = {}
  end
  if old < 64 then
    -- added reference to world for staff profiles
    for _, group in pairs(self.available_staff) do
      for _, profile in ipairs(group) do
        profile.world = self
      end
    end
  end
  if old < 66 then
    -- Unreserve objects which are not actually reserved for real in the staff room.
    -- This is a special case where reserved_for could be set just as a staff member was leaving
    for _, room in pairs(self.rooms) do
      if room.room_info.id == "staff_room" then
        -- Find all objects in the room
        local fx, fy = room:getEntranceXY(true)
        for obj, _ in pairs(self:findAllObjectsNear(fx, fy)) do
          if obj.reserved_for then
            local found = false
            for _, action in ipairs(obj.reserved_for.action_queue) do
              if action.name == "use_object" then
                if action.object == obj then
                  found = true
                  break
                end
              end
            end
            if not found then
              self:gameLog("Unreserved an object: " .. obj.object_type.id .. " at " .. obj.tile_x .. ":" .. obj.tile_y)
              obj.reserved_for = nil
            end
          end
        end
      end
    end
  end
  if old < 77 then
    self.ui:addKeyHandler({"shift", "+"}, self, self.adjustZoom,  5)
    self.ui:addKeyHandler({"shift", "-"}, self, self.adjustZoom, -5)
  end

  if old < 103 then
    -- If a room has patients who no longer exist in its
    -- humanoids_enroute table because of #133 remove them:
    for _, room in pairs(self.rooms) do
      for patient, _ in pairs(room.humanoids_enroute) do
        if patient.tile_x == nil then
          room.humanoids_enroute[patient] = nil
        end
      end
    end
  end

  -- Now let things inside the world react.
  for _, cat in pairs({self.hospitals, self.entities, self.rooms}) do
    for _, obj in pairs(cat) do
      obj:afterLoad(old, new)
    end
  end

  if old < 80 then
    self:determineWinningConditions()
  end

  if old >= 87 then
    self:playLoadedEntitySounds()
  end

  if old < 88 then
    --Populate the entity map
    self.entity_map = EntityMap(self.map)
    for _, e in ipairs(self.entities) do
      local x, y = e.tile_x, e.tile_y
      if x and y then
        self.entity_map:addEntity(x,y,e)
      end
    end
  end
  self.savegame_version = new
end

function World:playLoadedEntitySounds()
  for _, entity in pairs(self.entities) do
    entity:playAfterLoadSound()
  end
end

--[[ There is a problem with room editing in that it resets all the partial passable flags
(travelNorth, travelSouth etc.) in the corridor, a workaround is calling this function
after the room was edited so that all edge only objects, that set partial passable flags set
those flags again]]
function World:resetSideObjects()
  for _, objects in pairs(self.objects) do
    for _, obj in ipairs(objects) do
      if obj.object_type.class == "SideObject" then
        obj:setTile(obj.tile_x, obj.tile_y)
      end
    end
  end
end