-
Notifications
You must be signed in to change notification settings - Fork 13
/
armcom2.py
18261 lines (14585 loc) · 589 KB
/
armcom2.py
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
# -*- coding: UTF-8 -*-
# Python 3.6.6 x64
# Libtcod 1.6.4 x64
##########################################################################################
# #
# Armoured Commander II #
# #
##########################################################################################
# Project Started February 23, 2016; Restarted July 25, 2016 #
# Restarted again January 11, 2018; Restarted again January 2, 2019 #
# First stable release March 14, 2020 #
##########################################################################################
#
# Copyright (c) 2016-2020 Gregory Adam Scott
#
# This file is part of Armoured Commander II.
#
# Armoured Commander II is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Armoured Commander II is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Armoured Commander II, in the form of a file named "gpl.txt".
# If not, see <https://www.gnu.org/licenses/>.
#
# xp_loader.py is covered under a MIT License (MIT) and is Copyright (c) 2015
# Sean Hagar; see XpLoader_LICENSE.txt for more info.
#
# Some sound samples from the C64 sample pack by Odo:
# <https://howtomakeelectronicmusic.com/270mb-of-free-c64-samples-made-by-odo/>
#
# Steam integration thanks to SteamworksPy, covered under a MIT License (MIT)
# Copyright (c) 2016 GP Garcia, CoaguCo Industries
# https://github.com/Gramps/SteamworksPy
#
##########################################################################################
##### Debug Flags #####
STEAM_ON = False # load steamworks
DEBUG = False # debug flag - set to False in all distribution versions
##### Libraries #####
import os, sys # OS-related stuff
if sys.platform == 'darwin': # OSX
import tcod as libtcod
elif os.name == 'posix': # linux
import libtcodpy_local as libtcod
else: # windows
import libtcodpy as libtcod
os.environ['PYSDL2_DLL_PATH'] = os.getcwd() + '/lib'.replace('/', os.sep) # set sdl2 dll path
from configparser import ConfigParser # saving and loading configuration settings
from random import choice, shuffle, sample # for the illusion of randomness
from math import floor, cos, sin, sqrt, degrees, atan2, ceil # math and heading calculations
import xp_loader, gzip # loading xp image files
import json # for loading JSON data
import time
from datetime import datetime, timedelta # for timestamping logs, date calculations
from textwrap import wrap # breaking up strings
import shelve # saving and loading games
import sdl2.sdlmixer as mixer # sound effects
from calendar import monthrange # for date calculations
if STEAM_ON:
from steamworks import STEAMWORKS # main steamworks library
##########################################################################################
# Constants #
##########################################################################################
NAME = 'Armoured Commander II' # game name
VERSION = '3.0.2' # game version
DISCLAIMER = 'This is a work of fiction and no endorsement of any historical ideologies or events depicted within is intended.'
DATAPATH = 'data/'.replace('/', os.sep) # path to data files
SAVEPATH = 'saved_campaigns/'.replace('/', os.sep) # path to saved campaign folders
SOUNDPATH = 'sounds/'.replace('/', os.sep) # path to sound samples
CAMPAIGNPATH = 'campaigns/'.replace('/', os.sep) # path to campaign files
if sys.platform == 'darwin':
RENDERER = libtcod.RENDERER_SDL2
elif os.name == 'posix': # linux (and OS X?) has to use SDL for some reason
RENDERER = libtcod.RENDERER_SDL
else:
RENDERER = libtcod.RENDERER_OPENGL2
LIMIT_FPS = 50 # maximum screen refreshes per second
ANIM_UPDATE_TIMER = 0.15 # number of seconds between animation frame checks
WINDOW_WIDTH, WINDOW_HEIGHT = 90, 60 # size of game window in character cells
WINDOW_XM, WINDOW_YM = int(WINDOW_WIDTH/2), int(WINDOW_HEIGHT/2) # center of game window
KEYBOARDS = ['QWERTY', 'AZERTY', 'QWERTZ', 'Dvorak', 'Custom'] # list of possible keyboard layout settings
MAX_TANK_NAME_LENGTH = 20 # maximum length of tank names
MAX_NICKNAME_LENGTH = 10 # " for crew nicknames
DEBUG_OPTIONS = [
'Regenerate CD Map Roads & Rivers', 'Attack Selected Crewman (Scenario)', 'Set Crewman Injury',
'Set Time to End of Day', 'End Current Scenario', 'Export Campaign Log',
'Regenerate Weather'
]
##### Hex geometry definitions #####
# directional and positional constants
DESTHEX = [(0,-1), (1,-1), (1,0), (0,1), (-1,1), (-1,0)] # change in hx, hy values for hexes in each direction
CD_DESTHEX = [(1,-1), (1,0), (0,1), (-1,1), (-1,0), (0,-1)] # same for pointy-top
PLOT_DIR = [(0,-1), (1,-1), (1,1), (0,1), (-1,1), (-1,-1)] # position of direction indicator
TURRET_CHAR = [254, 47, 92, 254, 47, 92] # characters to use for turret display
# relative locations of edge cells in a given direction for a map hex
HEX_EDGE_CELLS = {
0: [(-1,-2),(0,-2),(1,-2)],
1: [(1,-2),(2,-1),(3,0)],
2: [(3,0),(2,1),(1,2)],
3: [(1,2),(0,2),(-1,2)],
4: [(-1,2),(-2,1),(-3,0)],
5: [(-3,0),(-2,-1),(-1,-2)]
}
# same for campaign day hexes (pointy-topped)
CD_HEX_EDGE_CELLS = {
0: [(0,-4),(1,-3),(2,-2),(3,-1)],
1: [(3,-1),(3,0),(3,1)],
2: [(3,1),(2,2),(1,3),(0,4)],
3: [(0,4),(-1,3),(-2,2),(-3,1)],
4: [(-3,1),(-3,0),(-3,-1)],
5: [(-3,-1),(-2,-2),(-1,-3),(0,-4)]
}
# list of hexes on campaign day map
CAMPAIGN_DAY_HEXES = [
(0,0),(1,0),(2,0),(3,0),(4,0),
(0,1),(1,1),(2,1),(3,1),
(-1,2),(0,2),(1,2),(2,2),(3,2),
(-1,3),(0,3),(1,3),(2,3),
(-2,4),(-1,4),(0,4),(1,4),(2,4),
(-2,5),(-1,5),(0,5),(1,5),
(-3,6),(-2,6),(-1,6),(0,6),(1,6),
(-3,7),(-2,7),(-1,7),(0,7),
(-4,8),(-3,8),(-2,8),(-1,8),(0,8)
]
##### Colour Definitions #####
KEY_COLOR = libtcod.Color(255, 0, 255) # key color for transparency
ACTION_KEY_COL = libtcod.Color(51, 153, 255) # colour for key commands
HIGHLIGHT_MENU_COL = libtcod.Color(30, 70, 130) # background highlight colour for selected menu option
TITLE_COL = libtcod.Color(102, 178, 255) # menu titles
PORTRAIT_BG_COL = libtcod.Color(217, 108, 0) # background color for unit portraits
UNKNOWN_UNIT_COL = libtcod.grey # unknown enemy unit display colour
ENEMY_UNIT_COL = libtcod.Color(255, 20, 20) # known "
ALLIED_UNIT_COL = libtcod.Color(120, 120, 255) # allied unit display colour
GOLD_COL = libtcod.Color(255, 255, 100) # golden colour for awards
DIRT_ROAD_COL = libtcod.Color(80, 50, 20) # dirt roads on campaign day map
STONE_ROAD_COL = libtcod.Color(110, 110, 110) # stone "
RIVER_COL = libtcod.Color(0, 0, 140) # rivers "
BRIDGE_COL = libtcod.Color(40, 20, 10) # bridges/fords "
# text names for months
MONTH_NAMES = [
'', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
]
# order of turn phases
PHASE_COMMAND = 0
PHASE_SPOTTING = 1
PHASE_CREW_ACTION = 2
PHASE_MOVEMENT = 3
PHASE_SHOOTING = 4
PHASE_ALLIED_ACTION = 5
PHASE_ENEMY_ACTION = 6
# text names for scenario turn phases
SCEN_PHASE_NAMES = [
'Command', 'Spotting', 'Crew Action', 'Movement', 'Shooting', 'Allied Action',
'Enemy Action'
]
# colour associated with phases
SCEN_PHASE_COL = [
libtcod.yellow, libtcod.purple, libtcod.light_blue, libtcod.green, libtcod.red,
ALLIED_UNIT_COL, ENEMY_UNIT_COL
]
# list of campaign calendar menus and their highlight colours
CC_MENU_LIST = [
('Proceed', 1, libtcod.Color(70, 140, 0)),
('Crew and Tank', 2, libtcod.Color(140, 140, 0)),
('Journal', 3, libtcod.Color(0, 0, 150)),
('Field Hospital', 4, libtcod.Color(200, 0, 0))
]
# list of campaign day menus and their highlight colours
CD_MENU_LIST = [
('Supply', 1, libtcod.Color(128, 100, 64)),
('Crew', 2, libtcod.Color(140, 140, 0)),
('Travel', 3, libtcod.Color(70, 140, 0)),
('Group', 4, libtcod.Color(180, 0, 45))
]
# directional arrows for directions on the campaign day map
CD_DIR_ARROW = [228,26,229,230,27,231]
# list of commands for travel in campaign day
CD_TRAVEL_CMDS = [
('e',2,-2,228), ('d',2,0,26), ('c',2,2,229), ('z',-2,2,230), ('a',-2,0,27), ('q',-2,-2,231)
]
# order to display ammo types
AMMO_TYPES = ['HE', 'AP', 'Smoke']
# display colours for ammo types (FUTURE: store text descriptions here too)
AMMO_TYPE_COLOUR = {
'HE' : libtcod.lighter_grey,
'AP' : libtcod.yellow,
'Smoke' : libtcod.darker_grey
}
# list of MG-type weapons
MG_WEAPONS = ['Co-ax MG', 'Turret MG', 'Hull MG', 'AA MG', 'HMG']
# types of records to store for each combat day and for entire campaign
# also order in which they are displayed
RECORD_LIST = [
'Battles Fought', 'Map Areas Captured', 'Gun Hits', 'Vehicles Destroyed',
'Guns Destroyed', 'Infantry Destroyed'
]
# text descriptions for different types of Campaign Day missions
MISSION_DESC = {
'Advance' : 'Enemy resistance is scattered and we are pushing forward. Advance into enemy territory, destroy any resistance, and capture territory.',
'Battle' : 'Your group has been posted to the front line where there is heavy resistance. Break through the enemy defenses, destroy enemy units, and capture territory.',
'Counterattack' : 'After being on the defensive, your battlegroup has been ordered to mount a counterattack against the enemy advance.',
'Fighting Withdrawal' : 'The enemy is mounting a strong attack against our lines. Destroy enemy units but withdraw into friendly territory if necessary.',
'Spearhead' : 'You must pierce the enemy lines, driving forward as far as possible before the end of the day.'
}
##########################################################################################
# Game Engine Definitions #
##########################################################################################
# percent chance per day after minimum stay that a crewman in the Field Hospital will be released
# to active duty
FIELD_HOSPITAL_RELEASE_CHANCE = 4.0
# Spearhead mission zone capture VP values: adds one per this many hexrows reached
SPEARHEAD_HEXROW_LEVELS = 2
# level at which crew become eligible for promotion to the next rank
LEVEL_RANK_LIST = {
'2' : 1,
'4' : 2,
'10' : 3,
'15' : 4,
'20' : 5,
'25' : 6
}
# chance that eligible crew will receive a promotion
PROMOTION_CHANCE = 18.0
# chance that a weapon will jam upon use, chance that it will be unjammed if crewman is operating it
WEAPON_JAM_CHANCE = 0.5
WEAPON_UNJAM_CHANCE = 75.0
# chance of a direct hit during air or artillery attacks
DIRECT_HIT_CHANCE = 4.0
# base crew experience point and level system
BASE_EXP_REQUIRED = 10.0
EXP_EXPONENT = 1.1
# region definitions: set by campaigns, determine terrain odds on the campaign day map,
# types and odds of weather conditions at different times during the calendar year
REGIONS = {
'Northeastern Europe' : {
# campaign day map terrain type odds
# NEW: can be modified by campaign weeks
'cd_terrain_odds' : {
'Flat' : 50,
'Forest' : 10,
'Hills' : 15,
'Fields' : 10,
'Marsh' : 5,
'Villages' : 10
},
# odds of dirt road network being present on the map
'dirt_road_odds' : 40.0,
# odds of stone/improved road "
'stone_road_odds' : 10.0,
# odds of 1+ rivers being spawned (with crossing points)
'river_odds' : 50.0,
# seasons, end dates, and weather odds for each season
'season_weather_odds' : {
'Winter' : {
'end_date' : '03.31',
'ground_conditions' : {
'Dry' : 20.0, 'Wet' : 0.0, 'Muddy' : 5.0,
'Snow' : 60.0, 'Deep Snow' : 15.0
},
# if ground cover is rolled as 'Dry', chance that snow could later fall
'freezing' : 80.0,
'cloud_cover' : {
'Clear' : 40.0, 'Scattered' : 20.0,
'Heavy' : 20.0, 'Overcast' : 20.0
},
# if precipitation is rolled and cloud cover is clear, cloud cover will be set to scattered+
'precipitation' : {
'None' : 35.0, 'Mist' : 10.0,
'Rain' : 10.0, 'Heavy Rain' : 5.0,
'Light Snow' : 10.0, 'Snow' : 20.0,
'Blizzard' : 10.0
}
},
'Spring' : {
'end_date' : '06.14',
'ground_conditions' : {
'Dry' : 50.0, 'Wet' : 20.0, 'Muddy' : 25.0,
'Snow' : 5.0, 'Deep Snow' : 0.0
},
'freezing' : 10.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 30.0, 'Mist' : 10.0,
'Rain' : 30.0, 'Heavy Rain' : 20.0,
'Light Snow' : 10.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
},
'Summer' : {
'end_date' : '09.31',
'ground_conditions' : {
'Dry' : 75.0, 'Wet' : 10.0, 'Muddy' : 15.0,
'Snow' : 0.0, 'Deep Snow' : 0.0
},
'freezing' : 0.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 60.0, 'Mist' : 10.0,
'Rain' : 20.0, 'Heavy Rain' : 10.0,
'Light Snow' : 0.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
},
'Autumn' : {
'end_date' : '12.01',
'ground_conditions' : {
'Dry' : 65.0, 'Wet' : 10.0, 'Muddy' : 15.0,
'Snow' : 10.0, 'Deep Snow' : 0.0
},
'freezing' : 15.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 50.0, 'Mist' : 10.0,
'Rain' : 20.0, 'Heavy Rain' : 10.0,
'Light Snow' : 10.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
}
}
},
'Northwestern Europe' : {
'cd_terrain_odds' : {
'Flat' : 40,
'Forest' : 20,
'Hills' : 10,
'Fields' : 15,
'Marsh' : 5,
'Villages' : 10
},
'dirt_road_odds' : 80.0,
'stone_road_odds' : 50.0,
'river_odds' : 20.0,
'season_weather_odds' : {
'Winter' : {
'end_date' : '03.31',
'ground_conditions' : {
'Dry' : 25.0, 'Wet' : 0.0, 'Muddy' : 5.0,
'Snow' : 60.0, 'Deep Snow' : 10.0
},
'freezing' : 60.0,
'cloud_cover' : {
'Clear' : 40.0, 'Scattered' : 20.0,
'Heavy' : 20.0, 'Overcast' : 20.0
},
'precipitation' : {
'None' : 35.0, 'Mist' : 10.0,
'Rain' : 10.0, 'Heavy Rain' : 5.0,
'Light Snow' : 15.0, 'Snow' : 20.0,
'Blizzard' : 5.0
}
},
'Spring' : {
'end_date' : '06.14',
'ground_conditions' : {
'Dry' : 50.0, 'Wet' : 20.0, 'Muddy' : 25.0,
'Snow' : 0.0, 'Deep Snow' : 0.0
},
'freezing' : 5.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 30.0, 'Mist' : 10.0,
'Rain' : 30.0, 'Heavy Rain' : 20.0,
'Light Snow' : 0.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
},
'Summer' : {
'end_date' : '09.31',
'ground_conditions' : {
'Dry' : 75.0, 'Wet' : 10.0, 'Muddy' : 15.0,
'Snow' : 0.0, 'Deep Snow' : 0.0
},
'freezing' : 0.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 60.0, 'Mist' : 10.0,
'Rain' : 20.0, 'Heavy Rain' : 10.0,
'Light Snow' : 0.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
},
'Autumn' : {
'end_date' : '12.01',
'ground_conditions' : {
'Dry' : 65.0, 'Wet' : 10.0, 'Muddy' : 15.0,
'Snow' : 10.0, 'Deep Snow' : 0.0
},
'freezing' : 10.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 50.0, 'Mist' : 10.0,
'Rain' : 20.0, 'Heavy Rain' : 10.0,
'Light Snow' : 10.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
}
}
},
'Nordic' : {
'cd_terrain_odds' : {
'Flat' : 20,
'Forest' : 40,
'Hills' : 20,
'Fields' : 10,
'Marsh' : 5,
'Villages' : 5
},
'dirt_road_odds' : 20.0,
'stone_road_odds' : 2.0,
'river_odds' : 30.0,
'season_weather_odds' : {
'Winter' : {
'end_date' : '03.31',
'ground_conditions' : {
'Dry' : 0.0, 'Wet' : 0.0, 'Muddy' : 0.0,
'Snow' : 70.0, 'Deep Snow' : 30.0
},
'freezing' : 100.0,
'cloud_cover' : {
'Clear' : 20.0, 'Scattered' : 10.0,
'Heavy' : 10.0, 'Overcast' : 60.0
},
'precipitation' : {
'None' : 40.0, 'Mist' : 0.0,
'Rain' : 0.0, 'Heavy Rain' : 0.0,
'Light Snow' : 25.0, 'Snow' : 25.0,
'Blizzard' : 10.0
}
},
'Spring' : {
'end_date' : '06.14',
'ground_conditions' : {
'Dry' : 55.0, 'Wet' : 15.0, 'Muddy' : 5.0,
'Snow' : 15.0, 'Deep Snow' : 10.0
},
'freezing' : 20.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 30.0, 'Mist' : 15.0,
'Rain' : 10.0, 'Heavy Rain' : 5.0,
'Light Snow' : 20.0, 'Snow' : 15.0,
'Blizzard' : 5.0
}
},
'Summer' : {
'end_date' : '09.31',
'ground_conditions' : {
'Dry' : 75.0, 'Wet' : 10.0, 'Muddy' : 15.0,
'Snow' : 0.0, 'Deep Snow' : 0.0
},
'freezing' : 0.0,
'cloud_cover' : {
'Clear' : 50.0, 'Scattered' : 15.0,
'Heavy' : 20.0, 'Overcast' : 15.0
},
'precipitation' : {
'None' : 60.0, 'Mist' : 10.0,
'Rain' : 20.0, 'Heavy Rain' : 10.0,
'Light Snow' : 0.0, 'Snow' : 0.0,
'Blizzard' : 0.0
}
},
'Autumn' : {
'end_date' : '12.01',
'ground_conditions' : {
'Dry' : 10.0, 'Wet' : 20.0, 'Muddy' : 20.0,
'Snow' : 40.0, 'Deep Snow' : 10.0
},
'freezing' : 60.0,
'cloud_cover' : {
'Clear' : 20.0, 'Scattered' : 10.0,
'Heavy' : 30.0, 'Overcast' : 40.0
},
'precipitation' : {
'None' : 15.0, 'Mist' : 10.0,
'Rain' : 15.0, 'Heavy Rain' : 10.0,
'Light Snow' : 25.0, 'Snow' : 20.0,
'Blizzard' : 5.0
}
}
}
}
}
# base chance of a ground conditions change during weather update
GROUND_CONDITION_CHANGE_CHANCE = 3.0
# modifier for heavy rain/snow
HEAVY_PRECEP_MOD = 5.0
# list of crew stats
CREW_STATS = ['Perception', 'Morale', 'Grit', 'Knowledge']
# list of positions that player character can be (Commander, etc.)
PLAYER_POSITIONS = ['Commander', 'Commander/Gunner']
# list of possible new positions for a crew that is transferring to a different type of tank
# listed in order of preference
POSITION_TRANSFER_LIST = {
"Commander" : ["Commander", "Commander/Gunner"],
"Commander/Gunner" : ["Commander/Gunner", "Commander"],
"Gunner" : ["Gunner", "Gunner/Loader"],
"Gunner/Loader" : ["Gunner/Loader", "Gunner"],
"Loader" : ["Loader", "Gunner/Loader"],
"Driver" : ["Driver"],
"Assistant Driver" : ["Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Port MG Gunner" : ["Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Assistant Driver", "Radio Operator"],
"Starboard MG Gunner" : ["Starboard MG Gunner", "Port MG Gunner", "MG Gunner", "Assistant Driver", "Radio Operator"],
"MG Gunner" : ["MG Gunner", "Port MG Gunner", "Starboard MG Gunner", "Assistant Driver", "Radio Operator"],
"Radio Operator" : ["Radio Operator", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Assistant Driver"]
}
# list of possible new positions that can be temporarily taken by a crewman, in case of serious
# injury or death during a campaign day
POSITION_SWITCH_LIST = {
"Commander" : ["Commander", "Commander/Gunner", "Gunner", "Gunner/Loader", "Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Commander/Gunner" : ["Commander", "Commander/Gunner", "Gunner", "Gunner/Loader", "Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Gunner" : ["Gunner", "Gunner/Loader", "Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Gunner/Loader" : ["Gunner", "Gunner/Loader", "Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Loader" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Driver" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Assistant Driver" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Port MG Gunner" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Starboard MG Gunner" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"MG Gunner" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"],
"Radio Operator" : ["Loader", "Driver", "Assistant Driver", "Port MG Gunner", "Starboard MG Gunner", "MG Gunner", "Radio Operator"]
}
# length of scenario turn in minutes
TURN_LENGTH = 2
# maximum visible distance when buttoned up
MAX_BU_LOS = 1
# base chance to spot unit at distance 0,1,2,3
SPOT_BASE_CHANCE = [95.0, 85.0, 70.0, 50.0]
# each point of Perception increases chance to spot enemy unit by this much
PERCEPTION_SPOTTING_MOD = 5.0
# base chance of moving forward/backward into next hex
BASE_FORWARD_MOVE_CHANCE = 50.0
BASE_REVERSE_MOVE_CHANCE = 20.0
# bonus per unsuccessful move attempt
BASE_MOVE_BONUS = 15.0
# base critical hit and miss thresholds
CRITICAL_HIT = 3.0
CRITICAL_MISS = 97.0
# maximum range at which MG attacks have a chance to penetrate armour
MG_AP_RANGE = 1
# base success chances for point fire attacks
# first column is for vehicle targets, second is everything else
PF_BASE_CHANCE = [
[98.0, 88.0], # same hex
[83.0, 78.0], # 1 hex range
[72.0, 58.0], # 2 "
[58.0, 35.0] # 3 hex range
]
# acquired target bonus for level 1 and level 2 for point fire
AC_BONUS = [
[8.0, 15.0], # distance 0
[10.0, 25.0], # distance 1
[10.0, 25.0], # distance 2
[20.0, 35.0] # distance 3
]
# modifier for target size if target is known
PF_SIZE_MOD = {
'Very Small' : -28.0,
'Small' : -12.0,
'Large' : 12.0,
'Very Large' : 28.0
}
# base chances of partial effect for area fire attacks: infantry/gun and vehicle targets
INF_FP_BASE_CHANCE = 30.0
VEH_FP_BASE_CHANCE = 20.0
FP_CHANCE_STEP = 5.0 # each additional firepower beyond 1 adds this additional chance
FP_CHANCE_STEP_MOD = 0.95 # additional firepower modifier reduced by this much beyond 1
FP_FULL_EFFECT = 0.75 # multiplier for full effect
FP_CRIT_EFFECT = 0.1 # multipler for critical effect
RESOLVE_FP_BASE_CHANCE = 2.0 # base chance of a 1 firepower attack destroying a unit
RESOLVE_FP_CHANCE_STEP = 2.0 # each additional firepower beyond 1 adds this additional chance
RESOLVE_FP_CHANCE_MOD = 1.03 # additional firepower modifier increased by this much beyond 1
MORALE_CHECK_BASE_CHANCE = 70.0 # base chance of passing a morale check
# effective FP of an HE hit from different weapon calibres
HE_FP_EFFECT = [
(200, 36),(183, 34),(170, 32),(160, 31),(150, 30),(140, 28),(128, 26),(120, 24),
(107, 22),(105, 22),(100, 20),(95, 19),(88, 18),(85, 17),(80, 16),(75, 14),
(70, 12),(65, 10),(60, 8),(57, 7),(50, 6),(45, 5),(37, 4),(30, 2),(25, 2),(20, 1)
]
# odds of unarmoured vehicle destruction when resolving FP
VEH_FP_TK = [
(36, 110.0),(30, 100.0),(24, 97.0),(20, 92.0),(16, 83.0),(12, 72.0),(8, 58.0),(6, 42.0),
(4, 28.0),(2, 17.0),(1, 8.0)
]
# ap scores of various close combat weapons
CC_WEAPON_AP = {
'Grenades' : 6,
'Flame Thrower' : 8
}
# amount within an AFV armour save that will result in Stun tests for crew/unit
AP_STUN_MARGIN = 10.0
# for each campaign day hex terrain type, odds that a unit on the scenario map will be in
# a given type of terrain in its scenario hex
SCENARIO_TERRAIN_ODDS = {
'Flat' : {
'Open Ground' : 60.0,
'Broken Ground' : 20.0,
'Brush' : 10.0,
'Woods' : 5.0,
'Wooden Buildings' : 4.0,
'Rubble' : 1.0
},
'Forest' : {
'Open Ground' : 10.0,
'Broken Ground' : 15.0,
'Brush' : 25.0,
'Woods' : 45.0,
'Wooden Buildings' : 4.0,
'Rubble' : 1.0
},
'Hills' : {
'Open Ground' : 15.0,
'Broken Ground' : 10.0,
'Brush' : 5.0,
'Woods' : 5.0,
'Hills' : 50.0,
'Wooden Buildings' : 4.0,
'Rubble' : 1.0
},
'Fields' : {
'Open Ground' : 20.0,
'Broken Ground' : 5.0,
'Brush' : 5.0,
'Woods' : 5.0,
'Fields' : 50.0,
'Wooden Buildings' : 4.0,
'Rubble' : 1.0
},
'Marsh' : {
'Open Ground' : 10.0,
'Broken Ground' : 5.0,
'Brush' : 5.0,
'Woods' : 4.0,
'Marsh' : 60.0,
'Wooden Buildings' : 5.0,
'Rubble' : 1.0
},
'Villages' : {
'Open Ground' : 5.0,
'Broken Ground' : 10.0,
'Brush' : 10.0,
'Woods' : 5.0,
'Fields' : 15.0,
'Wooden Buildings' : 50.0,
'Rubble' : 5.0
}
}
# modifiers and effects for different types of terrain on the scenario layer
SCENARIO_TERRAIN_EFFECTS = {
'Open Ground' : {
'HD Chance' : 5.0,
'los_mod' : 0.0
},
'Broken Ground' : {
'TEM' : {
'Vehicle' : -10.0,
'Infantry' : -15.0,
'Deployed Gun' : -15.0
},
'HD Chance' : 10.0,
'Movement Mod' : -5.0,
'Bog Mod' : 1.0,
'los_mod' : 3.0
},
'Brush': {
'TEM' : {
'All' : -15.0
},
'HD Chance' : 10.0,
'Movement Mod' : -15.0,
'Bog Mod' : 2.0,
'Air Burst' : 10.0,
'Burnable' : True,
'los_mod' : 5.0
},
'Woods': {
'TEM' : {
'All' : -25.0
},
'HD Chance' : 20.0,
'Movement Mod' : -30.0,
'Bog Mod' : 5.0,
'Double Bog Check' : True, # player must test to bog before moving out of this terrain type
'Air Burst' : 20.0,
'Burnable' : True,
'los_mod' : 10.0
},
'Fields': {
'TEM' : {
'All' : -10.0
},
'HD Chance' : 5.0,
'Burnable' : True,
'los_mod' : 5.0
},
'Hills': {
'TEM' : {
'All' : -20.0
},
'HD Chance' : 40.0,
'los_mod' : 15.0
},
'Wooden Buildings': {
'TEM' : {
'Vehicle' : -20.0,
'Infantry' : -30.0,
'Deployed Gun' : -30.0
},
'HD Chance' : 30.0,
'los_mod' : 10.0,
'Burnable' : True
},
'Marsh': {
'TEM' : {
'All' : -10.0
},
'HD Chance' : 15.0,
'Movement Mod' : -30.0,
'Bog Mod' : 10.0,
'Double Bog Check' : True,
'los_mod' : 3.0,
},
'Rubble': {
'TEM' : {
'Vehicle' : -15.0,
'Infantry' : -30.0,
'Deployed Gun' : -30.0
},
'HD Chance' : 30.0,
'los_mod' : 10.0,
'Bog Mod' : 10.0,
'Double Bog Check' : True
}
}
# relative locations to draw greebles for terrain on scenario map
GREEBLE_LOCATIONS = [(-1,-1), (0,-1), (1,-1), (-1,0), (1,0), (-1,1), (0,1), (1,1)]
# modifier for base HD chance
HD_SIZE_MOD = {
'Very Small' : 12.0, 'Small' : 6.0, 'Normal' : 0.0, 'Large' : -6.0, 'Very Large' : -12.0
}
# base chance of a sniper attack being effective
BASE_SNIPER_TK_CHANCE = 45.0
# base chance of a random event in a scenario
BASE_RANDOM_EVENT_CHANCE = 5.0
# base chance of a random event in a campaign day
BASE_CD_RANDOM_EVENT_CHANCE = 3.0
# base number of minutes between weather update checks
BASE_WEATHER_UPDATE_CLOCK = 30
##########################################################################################
# Classes #
##########################################################################################
# Campaign: stores data about a campaign and calendar currently in progress
class Campaign:
def __init__(self):
self.filename = '' # record filename of campaign definitions
self.options = {
'permadeath' : True,
'fate_points' : True
}
# load skills from JSON file - they won't change over the course of a campaign
with open(DATAPATH + 'skill_defs.json', encoding='utf8') as data_file:
self.skills = json.load(data_file)
self.logs = {} # dictionary of campaign logs for each combat day
self.journal = {} # dictionary of events for each combat day
self.player_unit = None # placeholder for player unit
self.player_squad_max = 0 # maximum units in player squad in addition to player
self.player_vp = 0 # total player victory points
self.stats = {} # local copy of campaign stats
self.combat_calendar = [] # list of combat days
self.today = None # pointer to current day in calendar
self.current_week = None # " week
self.hospital = [] # holds crewmen currently in the field hospital
self.active_calendar_menu = 1 # currently active menu in the campaign calendar interface
self.active_journal_day = None # currently displayed journal day
self.journal_scroll_line = 0 # current level of scroll on the journal display
self.ended = False # campaign has ended due to player serious injury or death
self.player_oob = False # player was seriously injured or killed
self.decoration = '' # decoration awarded to player at end of campaign
# records for end-of-campaign summary
self.records = {}
for text in RECORD_LIST:
self.records[text] = 0
# check for the start of a new campaign week given the current date, apply any modifiers
def CheckForNewWeek(self):
week_index = self.stats['calendar_weeks'].index(self.current_week)
if week_index < len(self.stats['calendar_weeks']) - 1:
week_index += 1
if self.today >= self.stats['calendar_weeks'][week_index]['start_date']:
self.current_week = self.stats['calendar_weeks'][week_index]
# check for modified class spawn odds
if 'enemy_class_odds_modifier' in self.current_week:
for k, v in self.current_week['enemy_class_odds_modifier'].items():
if k in self.stats['enemy_unit_class_odds']:
self.stats['enemy_unit_class_odds'][k] = v
# check for promotions
for position in self.player_unit.positions_list:
if position.crewman is None: continue
position.crewman.PromotionCheck()
# handle a Player Commander heading to the field hospital for a period of time
def CommanderInAComa(self, crewman):
# clear all remaining crewmen from current player tank
for position in self.player_unit.positions_list:
position.crewman = None
# roll for actual length of hospital stay
(days_min, days_max) = crewman.field_hospital
days = days_min
for i in range(days_max-days_min):
if GetPercentileRoll() <= FIELD_HOSPITAL_RELEASE_CHANCE:
break
days += 1
# check to see if this would take the player beyond the end of the campaign
(year, month, day) = self.today.split('.')
a = datetime(int(year), int(month), int(day), 0, 0, 0) + timedelta(days=10)
release_day = str(a.year) + '.' + str(a.month).zfill(2) + '.' + str(a.day).zfill(2)
if release_day > self.stats['end_date']:
ShowMessage('While you are still recovering in the field hospital, you receive word that your campaign has ended.')
return True
# we're still in the campaign, set the current day to the next possible combat day
previous_day = self.today
i = self.combat_calendar.index(self.today)
for combat_day in self.combat_calendar[i+1:]:
# set the current campaign week and apply modified class spawn odds if any
self.CheckForNewWeek()
# check for player commander birthday
if previous_day < crewman.birthday <= self.today:
print('DEBUG: ' + crewman.last_name + ' is one year older!')
crewman.age += 1
# ignore refitting weeks
if 'refitting' in self.current_week:
continue
if combat_day >= release_day:
self.today = self.combat_calendar[i]
break
i+=1
previous_day = self.combat_calendar[i]
else:
# unable to find a combat day on or after the release date, campaign ends
ShowMessage('While you are still recovering in the field hospital, you receive word that your campaign has ended.')
return True
ShowMessage('After ' + str(days) + ' days in the field hospital, you recover and return to active duty on ' +
GetDateText(self.today))
self.hospital.remove(crewman)
crewman.field_hospital = None
# player selects a new tank, and generate a crew for it
(unit_id, tank_name) = self.TankSelectionMenu()
self.player_unit = Unit(unit_id)
self.player_unit.unit_name = tank_name