-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy patharmcom.py
executable file
·15602 lines (12772 loc) · 559 KB
/
armcom.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
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Python 3.6.8
##########################################################################################
# Armoured Commander #
# The World War II Tank Commander Roguelike #
##########################################################################################
##########################################################################################
#
# Copyright 2015-2017 Gregory Adam Scott ([email protected])
#
# This file is part of Armoured Commander.
#
# Armoured Commander 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 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, in the form of a file named "LICENSE".
# If not, see <http://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.
#
##########################################################################################
import sys, os # for command line functions, for SDL window instruction, other OS-related stuff
if getattr(sys, 'frozen', False): # needed for pyinstaller
os.chdir(sys._MEIPASS)
os.environ['PYSDL2_DLL_PATH'] = os.getcwd() # set sdl2 dll path
##### Libraries #####
from datetime import datetime # for recording date and time in campaign journal
from math import atan2, degrees # more "
from math import pi, floor, ceil, sqrt # math functions
from operator import attrgetter # for list sorting
from textwrap import wrap # for breaking up game messages
import csv # for loading campaign info
import libtcodpy as libtcod # The Doryen Library
import random # for randomly selecting items from a list
import shelve # for saving and loading games
import time # for wait function
import xml.etree.ElementTree as xml # ElementTree library for xml
import xp_loader # for loading image files
import gzip # for loading image files
import zipfile, io # for loading from zip archive
MIXER_ACTIVE = True
try:
import sdl2.sdlmixer as mixer # sound effects
except:
MIXER_ACTIVE = False
from steamworks import STEAMWORKS # main steamworks library
from armcom_defs import * # general definitions
from armcom_vehicle_defs import * # vehicle stat definitions
##### Constants #####
DEBUG = False # enable in-game debug commands
NAME = 'Armoured Commander'
VERSION = '1.0' # determines saved game compatability
SUBVERSION = '9' # descriptive only, no effect on compatability
WEBSITE = 'www.armouredcommander.com'
GITHUB = 'github.com/sudasana/armcom'
COMPATIBLE_VERSIONS = ['Beta 3.0'] # list of older versions for which the savegame
# is compatible with this version
DATAPATH = 'data' + os.sep # path to data files
PI = pi
SCREEN_WIDTH = 149 # width of game window in characters
SCREEN_HEIGHT = 61 # height "
SCREEN_XM = int(SCREEN_WIDTH/2) # horizontal center "
SCREEN_YM = int(SCREEN_HEIGHT/2) # vertical "
TANK_CON_WIDTH = 73 # width of tank info console in characters
TANK_CON_HEIGHT = 37 # height "
MSG_CON_WIDTH = TANK_CON_WIDTH # width of message console in characters
MSG_CON_HEIGHT = 19 # height "
MAP_CON_WIDTH = 73 # width of encounter map console in characters
MAP_CON_HEIGHT = 51 # height "
MAP_CON_X = MSG_CON_WIDTH + 2 # x position of encounter map console
MAP_CON_Y = 2 # y "
MAP_X0 = int(MAP_CON_WIDTH/2) # centre of encounter map console
MAP_Y0 = int(MAP_CON_HEIGHT/2)
MAP_INFO_CON_WIDTH = MAP_CON_WIDTH # width of map info console in characters
MAP_INFO_CON_HEIGHT = 7 # height "
DATE_CON_WIDTH = TANK_CON_WIDTH # date, scenario type, etc. console
DATE_CON_HEIGHT = 1
MENU_CON_WIDTH = 139 # width of in-game menu console
MENU_CON_HEIGHT = 42 # height "
MENU_CON_XM = int(MENU_CON_WIDTH/2) # horizontal center of "
MENU_CON_YM = int(MENU_CON_HEIGHT/2) # vertical center of "
MENU_CON_X = SCREEN_XM - MENU_CON_XM # x and y location to draw
MENU_CON_Y = int(SCREEN_HEIGHT/2) - int(MENU_CON_HEIGHT/2) # menu console on screen
# text console, displays 84 x 50 characters
TEXT_CON_WIDTH = 86 # width of text display console (campaign journal, messages, etc.)
TEXT_CON_HEIGHT = 57 # height "
TEXT_CON_XM = int(TEXT_CON_WIDTH/2) # horizontal center "
TEXT_CON_X = SCREEN_XM - TEXT_CON_XM # x/y location to draw window
TEXT_CON_Y = 2
C_MAP_CON_WIDTH = 90 # width of campaign map console
C_MAP_CON_HEIGHT = 90 # height "
C_MAP_CON_WINDOW_W = 90 # width of how much of the campaign map is displayed on screen
C_MAP_CON_WINDOW_H = 57 # height "
C_MAP_CON_X = SCREEN_WIDTH - C_MAP_CON_WINDOW_W - 1 # x position of campaign map console
C_ACTION_CON_W = SCREEN_WIDTH - C_MAP_CON_WINDOW_W - 3 # width of campaign action console
C_ACTION_CON_H = 30 # height "
C_INFO_CON_W = SCREEN_WIDTH - C_MAP_CON_WINDOW_W - 3 # width of campaign info console
C_INFO_CON_H = SCREEN_HEIGHT - C_ACTION_CON_H - 4 # height "
C_INFO_CON_X = int(C_INFO_CON_W/2)
MAX_HS = 40 # maximum number of highscore entries to save
NAME_MAX_LEN = 17 # maximum length of crew names in characters
NICKNAME_MAX_LEN = 15 # maximum length of crew nicknames in characters
LIMIT_FPS = 50 # maximum screen refreshes per second
# Game defintions
EXTRA_AMMO = 30 # player tank can carry up to this many extra main gun shells
# Difficulty level
# Veteran=1(normal mode), Regular=2, Recruit=3
DIFFICULTY = 1
# Adjust leveling for difficulty level
BASE_EXP_REQ = int(30 / DIFFICULTY)
LVL_INFLATION = 10 / DIFFICULTY
# Adjust skill efficiency for difficulty level
for skill in SKILLS:
for k, v in enumerate(skill.levels):
skill.levels[k] *= DIFFICULTY
if skill.levels[k] > 100:
skill.levels[k] = 100
num_100 = len([x for x in skill.levels if x == 100])
if num_100 > 1:
skill.levels = skill.levels[:-(num_100 - 1)]
STONE_ROAD_MOVE_TIME = 30 # minutes required to move into a new area via an improved road
DIRT_ROAD_MOVE_TIME = 45 # " dirt road
NO_ROAD_MOVE_TIME = 60 # " no road
GROUND_MOVE_TIME_MODIFIER = 15 # additional time required if ground is muddy / rain / snow
# Colour Defintions
KEY_COLOR = libtcod.Color(255, 0, 255) # key color for transparency
# campaign map base colours
MAP_B_COLOR = libtcod.Color(100, 120, 100) # fields
MAP_D_COLOR = libtcod.Color(70, 90, 70) # woods
OPEN_GROUND_COLOR = libtcod.Color(100, 140, 100)
MUD_COLOR = libtcod.Color(80, 50, 30)
HEX_EDGE_COLOR = libtcod.Color(60, 100, 60)
ROAD_COLOR = libtcod.Color(160, 140, 100)
DIRT_COLOR = libtcod.Color(80, 50, 30)
CLEAR_SKY_COLOR = libtcod.Color(16, 180, 240)
OVERCAST_COLOR = libtcod.Color(150, 150, 150)
STONE_ROAD_COLOR = libtcod.darker_grey
FRONTLINE_COLOR = libtcod.red # highlight for hostile map areas
PLAYER_COLOR = libtcod.Color(10, 64, 10)
ENEMY_COLOR = libtcod.Color(80, 80, 80)
ROW_COLOR = libtcod.Color(30, 30, 30) # to highlight a line in a console
ROW_COLOR2 = libtcod.Color(20, 20, 20) # to highlight a line in a console
SELECTED_COLOR = libtcod.blue # selected option background
HIGHLIGHT_COLOR = libtcod.light_blue # to highlight important text
GREYED_COLOR = libtcod.Color(60, 60, 60) # greyed-out option
SKILL_ACTIVATE_COLOR = libtcod.Color(0, 255, 255) # skill activated message
MENU_TITLE_COLOR = libtcod.lighter_blue # title of menu console
KEY_HIGHLIGHT_COLOR = libtcod.Color(0, 200, 255) # highlight for key commands
HIGHLIGHT = (libtcod.COLCTRL_1, libtcod.COLCTRL_STOP) # constant for highlight pair
TITLE_GROUND_COLOR = libtcod.Color(26, 79, 5) # color of ground in main menu
SOUNDS = {} # sound effects
##########################################################################################
# Classes #
##########################################################################################
# Bones Class
# records high scores and other info between play sessions
class Bones:
def __init__(self):
self.score_list = []
self.graveyard = []
# tribute to David Bowie
self.graveyard.append(['Major', 'Jack Celliers', 'Brixton', 'January 10', ''])
# flags for having displayed help text windows
self.tutorial_message_flags = {}
for key in TUTORIAL_TEXT:
self.tutorial_message_flags[key] = False
# Saved Game Info Class
# holds basic information about a saved game, only read by main menu and only written to
# by SaveGame, doesn't impact gameplay otherwise
class SavedGameInfo:
def __init__(self, game_version, campaign_name, commander_name, tank_name, current_date):
self.game_version = game_version
self.campaign_name = campaign_name
self.commander_name = commander_name
self.tank_name = tank_name
self.current_date = current_date
# Campaign Day Map Class
# holds information about the campaign map used for in an action day
class CampaignDayMap:
def __init__(self):
self.seed = 0 # seed used for map painting, set during map
# generation
self.nodes = [] # list of map nodes
self.blocked_nodes = set() # set of impassible map nodes
self.char_locations = dict() # dictionary for character location parent nodes
self.player_node = None # pointer to player location
# Map Node Class
# holds information about a single location on the campaign map
class MapNode:
def __init__(self, x, y):
self.x = x # x coordinate of the area centre
self.y = y # y "
self.edges = set() # set of edge locations w/in the area
self.links = [] # list of adjacent nodes
self.node_type = '' # node terrain type
self.village_radius = 0 # radius of village buildings if village node
self.dirt_road_links = [] # list of nodes linked to this one by a dirt road
self.stone_road_links = [] # " an improved road
self.road_end = False # any roads should be extended to the edge
# of the map
self.extended = False # flag to note that this node has had its
# road extended to edge of map
self.top_edge = False # this area is on the top edge of the map
self.bottom_edge = False # " bottom "
self.left_edge = False # " left "
self.right_edge = False # " right "
self.start = False # start node
self.exit = False # exit node
self.resistance = None # area resistance level
self.res_known = False # resistance level is known to the player
self.friendly_control = False # area is controlled by player forces
self.arty_strike = False # friendly artillery has hit this area
self.air_strike = False # friendly air forces have hit this area
self.advancing_fire = False # player used advancing fire moving into this area
# Pathfinding stuff
self.parent = None
self.g = 0
self.h = 0
self.f = 0
# quest stuff
self.quest_type = None # type of active quest for this node
self.quest_time_limit = None # time limit to complete quest
self.quest_vp_bonus = None # VP bonus awarded for completing quest
# reset pathfinding info for this node
def ClearPathInfo(self):
self.parent = None
self.g = 0
self.h = 0
self.f = 0
# Skill Record Class
# holds information about a crewman's skill and its activation level
class SkillRecord:
def __init__(self, name, level):
self.name = name
self.level = level
# Hit Class
# holds information about a hit on an enemy unit with the player's main gun
class MainGunHit:
def __init__(self, gun_calibre, ammo_type, critical, area_fire):
self.gun_calibre = gun_calibre
self.ammo_type = ammo_type
self.critical = critical
self.area_fire = area_fire
# Weather Class
# holds information about weather conditions
class Weather:
def __init__(self):
self.clouds = 'Clear'
self.fog = False
self.precip = 'None'
self.ground = 'Dry'
# record of precip accumilation
self.rain_time = 0
self.snow_time = 0
self.dry_time = 0
# generate a totally new set of weather conditions upon moving to a new area
def GenerateNew(self):
self.clouds = 'Clear'
self.fog = False
self.precip = 'None'
self.ground = 'Dry'
self.rain_time = 0
self.snow_time = 0
self.dry_time = 0
# cloud cover
d1, d2, roll = Roll2D6()
month = campaign.current_date[1]
if 3 <= month <= 11:
roll -= 1
if 5 <= month <= 8:
roll -= 1
if roll > 6:
self.clouds = 'Overcast'
# precipitation and/or fog
if self.clouds == 'Overcast':
d1, d2, roll = Roll2D6()
if roll <= 4:
if month <= 2 or month == 12:
self.precip = 'Snow'
elif 5 <= month <= 9:
self.precip = 'Rain'
else:
# small chance of snow in march/april, oct/nov
d1, d2, roll = Roll2D6()
if roll >= 11:
self.precip = 'Snow'
else:
self.precip = 'Rain'
# fog
d1, d2, roll = Roll2D6()
if self.precip != 'None':
roll -= 2
if roll >= 10:
self.fog = True
# ground cover
d1, d2, roll = Roll2D6()
if self.precip == 'Snow':
if roll >= 11:
self.ground = 'Deep Snow'
elif roll >= 5:
self.ground = 'Snow'
elif self.precip == 'Rain':
if roll >= 8:
self.ground = 'Mud'
else:
# deep winter
if month <= 2 or month == 12:
if roll == 12:
self.ground = 'Deep Snow'
elif roll >= 8:
self.ground = 'Snow'
# warmer months
elif 5 <= month <= 9:
if roll >= 10:
self.ground = 'Mud'
# spring/autumn
else:
if roll == 12:
self.ground = 'Snow'
elif roll >= 8:
self.ground = 'Mud'
# check to see if weather changes, and apply effects if so
def CheckChange(self):
d1, d2, roll = Roll2D6()
month = campaign.current_date[1]
# check to see if precip stops; if so, this will be only change
# this cycle
if self.precip != 'None':
if roll <= 3:
if self.precip == 'Rain':
PopUp('The rain stops.')
else:
PopUp('The snow stops falling.')
self.precip = 'None'
return
# otherwise, if overcast, see if precip starts
elif self.clouds == 'Overcast':
if roll <= 3:
if month <= 2 or month == 12:
self.precip = 'Snow'
PopUp('Snow starts falling')
elif 5 <= month <= 9:
self.precip = 'Rain'
PopUp('Rain starts falling')
else:
# small chance of snow in march/april, oct/nov
d1, d2, roll = Roll2D6()
if roll >= 11:
self.precip = 'Snow'
PopUp('Snow starts falling')
else:
self.precip = 'Rain'
PopUp('Rain starts falling')
return
# if no precip change, check to see if cloud cover / fog changes
d1, d2, roll = Roll2D6()
if self.clouds == 'Clear':
if roll <= 3:
self.clouds = 'Overcast'
PopUp('Clouds roll in and the weather turns overcast')
return
else:
if roll <= 5:
# if foggy, fog lifts instead
if self.fog:
self.fog = False
PopUp('The fog lifts.')
return
# otherwise, the sky clears, stopping any precip
self.clouds = 'Clear'
self.precip = 'None'
PopUp('The sky clears.')
return
# chance of fog rolling in
d1, d2, roll = Roll2D6()
if roll <= 3 and not self.fog:
self.fog = True
PopUp('Fog rolls in.')
# check for a change in ground cover based on accumilated precip
# or lack thereof
def CheckGround(self, minutes_passed):
change = False
if self.precip == 'Rain':
if self.ground != 'Mud':
self.rain_time += minutes_passed
if self.rain_time >= 120:
PopUp('The rain has turned to ground to mud.')
self.ground = 'Mud'
self.dry_time = 0
change = True
elif self.precip == 'Snow':
if self.ground != 'Deep Snow':
self.snow_time += minutes_passed
if self.snow_time >= 120:
if self.ground == 'Snow':
PopUp('The snow on the ground has become deep.')
self.ground = 'Deep Snow'
change = True
elif self.ground in ['Dry', 'Mud']:
PopUp('The ground is covered in snow.')
self.ground = 'Snow'
change = True
else:
if self.ground == 'Mud':
self.dry_time += minutes_passed
if self.dry_time >= 120:
PopUp('The muddy ground dries out.')
self.ground = 'Dry'
self.rain_time = 0
change = True
# if there was a change, update consoles
if change:
if battle is not None:
UpdateMapOverlay()
else:
campaign.BuildActionList()
UpdateCActionCon()
UpdateCInfoCon(mouse.cx, mouse.cy)
# Hex Class
# holds information on a hex location in the battle encounter map
class MapHex:
def __init__(self, hx, hy, rng, sector):
self.hx = hx
self.hy = hy
self.rng = rng
self.sector = sector
self.smoke_factors = 0 # current number of smoke factors
self.x, self.y = Hex2Screen(hx, hy) # set x and y location to draw on screen
# smoke factor class
# holds information about a smoke factor on the battle encounter map
class SmokeFactor:
def __init__(self, hx, hy, num_factors):
self.hx = hx
self.hy = hy
self.num_factors = num_factors
# change position as a result of tank rotating
def RotatePosition(self, clockwise):
# convert present coordinate from axial to cube
x = self.hx
z = self.hy
y = -x-z
# do the rotation
if clockwise:
new_x = -y
new_z = -x
else:
new_x = -z
new_z = -y
# set the new hex location
self.hx = new_x
self.hy = new_z
# change position based on player tank moving forward or backward
def YMove(self, y_change):
# two special cases, if unit would end up in player hex
if self.hx == 0 and self.hy + y_change == 0:
if y_change == -1:
y_change = -2
else:
y_change = 2
self.hy = self.hy + y_change
# Campaign Class
# holds information on an ongoing campaign
class Campaign:
def __init__(self):
# Info set by the campaign xml file: defines the parameters of the campaign
# selected by the player
self.campaign_name = '' # name of the campaign (eg. Patton's Best)
self.campaign_file = '' # filename of the campaign xml file
self.player_nation = '' # three-letter code for player's nation
self.enemy_nation = '' # " for enemy nation
self.map_file = '' # XP file of campaign map
self.player_veh_list = [] # list of permitted player vehicle types
self.mission_activations = [] # list of activation chance dictionaries
# for advance, battle, counterattack
# missions
self.activation_modifiers = [] # list of activation modifiers
self.class_activations = [] # list of unit type and activation chance
# tuples for each unit class
# first item is always unit class name
self.ranks = None # list of ranks for current nation
self.decorations = None # list of decorations "
self.days = [] # list of calendar days: each one is
# a dictionary with keys and values
self.over = False # flag set when campaign has finished
(self.fs_res_x, self.fs_res_y) = FS_RES_LIST[0] # full screen resolution
self.fullscreen = False # full screen preference
self.exiting = False # flag for exiting out to main menu
self.mouseover = (-1, -1) # keeps track of mouse position
self.color_scheme = None # campaign map colour scheme, set at
# map generation
# campaign options
self.unlimited_tank_selection = False # freedom to select any available tank model
self.casual_commander = False # can replace commander and continue playing
self.difficulty = DIFFICULTY # campaign difficulty level
self.start_date = 0 # index of date in the calendar to start campaign
# game settings
self.animations = True # in-game animations
self.sounds = True # in-game sound effects
self.pause_labels = True # wait for enter after displaying a label
self.tutorial_message = True # display tutorial message windows
self.current_date = [0,0,0] # current year, month, date
self.day_vp = 0 # vp gained this campaign day
self.vp = 0 # current total player victory points
self.action_day = False # flag if player sees action today
self.saw_action = False # flag if player saw action already today
self.day_in_progress = False # flag if player is in the campaign map interface
self.scen_res = '' # string description of expected resistance for this day
self.scen_type = '' # " mission type
self.tank_on_offer = '' # new sherman model available during refitting
self.selected_crew = None # selected crew member
self.weather = Weather() # set up a new weather object
self.ClearAmmo() # clear rare ammo supplies\
self.gyro_skill_avail = False # gyrostabilier skill is available
self.stats = {} # campaign statistics, for display at end of campaign
self.campaign_journal = [] # list of text descriptions of campaign
self.record_day_vp = 0 # highest one-day VP score this month
def ResetForNewDay(self):
# Following are reset for each new campaign day where player sees action
self.day_map = None # day map, will be generated later
self.weather.GenerateNew() # reset weather for a new day
self.hour = 0 # current hour in 24-hour format
self.minute = 0 # current minute
self.c_map_y = 0 # offset for displaying campaign map on screen
self.selected_crew = None # pointer to currently selected crewmember
self.resupply = False # currently in resupply mode
self.input_mode = 'None' # current input mode in campaign
self.selected_node = None # selected node on the day map
self.adjacent_nodes = [] # list of adjacent nodes for moving, checking
self.free_check = False # player gets a free check adjacent area action
self.messages = [] # list of campaign messages
self.sunset = False # flag that the combat day is over
self.exiting = False # flag to exit to main menu
self.arty_chance = 9 # chance of calling in artillery
self.air_chance = 7 # chance of calling in air strike
self.time_of_last_event = (0,0) # hour, minute of last triggered event;
# 0,0 if no event has occured today
self.quest_active = False # campaign quest currently active
self.action_list = []
self.BuildActionList()
# check for enemy advances during a counterattack mission day
def DoEnemyAdvance(self):
# build list of candidate nodes
nodes = []
for node in self.day_map.nodes:
if node == campaign.day_map.player_node: continue
if not node.friendly_control: continue
if node in self.day_map.blocked_nodes: continue
if node.top_edge:
nodes.append(node)
continue
# check if adjacent to an enemy-held node
for link_node in node.links:
if not link_node.friendly_control:
nodes.append(node)
break
# if no candidate nodes, return
if len(nodes) == 0:
return
# run through candidate nodes and see if they get taken over
for node in nodes:
chance = 0
for link_node in node.links:
if not link_node.friendly_control:
if link_node.resistance == 'Light' and chance < 3:
chance = 3
elif link_node.resistance == 'Medium' and chance < 5:
chance = 5
elif link_node.resistance == 'Heavy' and chance < 7:
chance = 7
# at beginning of day, top-edge nodes won't have any enemy-held
# nodes adjacent, so we need to generate random chances for
# these ones
if chance == 0:
chance = random.choice([3, 5, 7])
# do advance roll
d1, d2, roll = Roll2D6()
# control is lost
if roll <= chance:
node.friendly_control = False
node.res_known = True
campaign.MoveViewTo(node)
UpdateCOverlay(highlight_node=node)
RenderCampaign()
Wait(500)
PopUp('A map area has been captured by an enemy advance.')
UpdateCOverlay()
RenderCampaign()
# if no possible path to 'exit' node, player is moved to nearest
# friendly node, spends 1-20 HE shells
if not campaign.day_map.player_node.exit:
for node in campaign.day_map.nodes:
if node.exit:
if len(GetPath(campaign.day_map.player_node, node, enemy_blocks=True)) > 0:
return
break
# HE shells expended during move
ammo_expended = Roll1D10() * 2
if tank.general_ammo['HE'] < ammo_expended:
ammo_expended = tank.general_ammo['HE']
tank.general_ammo['HE'] -= ammo_expended
# time required for move
time_req = 60
if campaign.weather.ground != 'Dry' or campaign.weather.precip != 'None' or campaign.weather.fog:
time_req += 15
campaign.SpendTime(0, time_req)
PopUp('You have been cut off from your allies and must reposition. You travel' +
' off-road to the nearest friendly map area, expending ' + str(ammo_expended) +
' HE shells to cover your withdrawl. This takes ' + str(time_req) +
' minutes.')
# player node captured by enemy
campaign.day_map.player_node.friendly_control = False
campaign.day_map.player_node.res_known = True
# find the target node
closest = None
closest_dist = 9000
for node in campaign.day_map.nodes:
if node.friendly_control:
dist = GetDistance(campaign.day_map.player_node.x,
campaign.day_map.player_node.y, node.x,
node.y)
if dist < closest_dist:
closest = node
closest_dist = dist
if closest is None:
print ('ERROR: Could not find a friendly node to move to')
return
# do the move
campaign.day_map.player_node = closest
campaign.MoveViewTo(closest)
UpdateCOverlay()
RenderCampaign()
Wait(500)
# check for sunset
campaign.CheckSunset()
# check to see if a random campaign event is triggered
def RandomCampaignEvent(self):
# highlight and move player view to event node
def ShowNode(node):
campaign.MoveViewTo(node)
UpdateCOverlay(highlight_node=node)
RenderCampaign()
Wait(1000)
# if current day mission is Counterattack, don't trigger any campaign events
if campaign.scen_type == 'Counterattack': return
# if sunset has already happened
if campaign.sunset: return
roll = Roll1D100()
# if no event yet today, set current time as 'time of last event' and return
if self.time_of_last_event == (0,0):
self.time_of_last_event = (self.hour, self.minute)
return
else:
h1, m1 = self.time_of_last_event
h, m = GetTimeUntil(h1, m1, self.hour, self.minute)
if h == 0:
# No event
return
elif h == 1:
if m <= 15:
roll -= 30
elif m <= 30:
roll -= 25
elif m <= 45:
roll -= 10
# No event
if roll <= 50:
return
# Ammo supply Discovered
elif roll <= 55:
WriteJournal('Friendly supply truck discovered.')
if PopUp('You have encountered a friendly supply truck. Restock ' +
'your ammunition? (15 mins.)', confirm=True):
self.SpendTime(0, 15)
tank.smoke_grenades = 6
tank.smoke_bombs = 15
self.resupply = True
MainGunAmmoMenu()
self.resupply = False
RenderCampaign()
self.time_of_last_event = (self.hour, self.minute)
return
# Quest Triggered
if roll <= 75:
# don't trigger another one if there's currently one in progress
if self.quest_active:
return
# determine quest type
d1, d2, roll = Roll2D6()
if roll <= 3:
# like CAPTURE but with a time limit
quest_type = 'RESCUE'
vp_bonus = 15
elif roll <= 7:
# enter an enemy-held area, automatic battle encounter
quest_type = 'CAPTURE'
vp_bonus = 10
elif roll <= 10:
# check an enemy-held area for resistance level
quest_type = 'RECON'
vp_bonus = 5
else:
# enter a friendly-held area, wait for attack
quest_type = 'DEFEND'
vp_bonus = 15
# find quest map node
player_y = campaign.day_map.player_node.y
nodes = []
for node in campaign.day_map.nodes:
if node in campaign.day_map.blocked_nodes: continue
# skip marshland nodes; should have been done by
# previous line but still seems to be getting through
if node.node_type == 'E': continue
if node == campaign.day_map.player_node: continue
if node.y > player_y: continue
if quest_type != 'DEFEND' and node.friendly_control: continue
if quest_type == 'DEFEND' and not node.friendly_control: continue
if quest_type == 'RECON' and node.res_known: continue
# node must be close to player
if len(GetPath(campaign.day_map.player_node, node)) > 3: continue
nodes.append(node)
if len(nodes) == 0:
return
WriteJournal(quest_type + ' quest triggered')
# set quest active flag
self.quest_active = True
# add campaign stat
campaign.AddStat('Quests Assigned', 1)
# select quest node
node = random.choice(nodes)
# set quest node settings
node.quest_type = quest_type
node.quest_vp_bonus = vp_bonus
if quest_type == 'RESCUE':
# determine time limit for quest
h = campaign.hour + libtcod.random_get_int(0, 2, 4)
m = campaign.minute
node.quest_time_limit = (h, m)
text = ('Commander, you are requested to head to the highlighted ' +
'map location. Allied units are pinned down in the area ' +
'and require your help. If completed at or before ' +
str(h) + ':' + str(m).zfill(2) + ', you will receive ' +
'a bonus of ' + str(vp_bonus) + ' VP.')
elif quest_type == 'RECON':
text = ('Commander, you are requested to head to the highlighted ' +
'map location and check it for estimated enemy resistance. ' +
'If completed, you will receive a bonus of ' +
str(vp_bonus) + ' VP.')
elif quest_type == 'CAPTURE':
text = ('Commander, you are requested to head to the highlighted ' +
'map location and capture it from enemy forces. ' +
'If completed, you will receive a bonus of ' +
str(vp_bonus) + ' VP.')
elif quest_type == 'DEFEND':
text = ('Commander, you are requested to head to the highlighted ' +
'map location and defend it from an anticipated enemy ' +
'counterattack. If completed, you will receive a bonus of ' +
str(vp_bonus) + ' VP.')
ShowNode(node)
PopUp(text)
# Exit Area Changed
elif roll <= 80:
# don't move if player within 2 nodes of current exit
for node in campaign.day_map.nodes:
if node.exit:
if len(GetPath(campaign.day_map.player_node, node)) <= 4:
return
break
# build list of top edge nodes that are reachable from current player
# location, note the pre-existing exit node but don't include it in the list
nodes = []
old_exit = None
for node in campaign.day_map.nodes:
if node.top_edge:
if node.friendly_control: continue
if node.exit:
old_exit = node
elif GetPath(campaign.day_map.player_node, node) != []:
nodes.append(node)
# no candidates found
if len(nodes) == 0: return
# select a random node from the list, make it the exit node, and clear the
# exit flag of the old exit node
node = random.choice(nodes)
node.exit = True
old_exit.exit = False
ShowNode(node)
PopUp('HQ has ordered us to proceed to a different target area.')
# Reconnaissance Report: Reveals expected resistance level in an adjacent area
elif roll <= 85:
# build list of possible nodes
nodes = []
for node in campaign.day_map.nodes:
if node in campaign.day_map.player_node.links and not node.res_known:
if not node.friendly_control and node.quest_type is None:
nodes.append(node)
# no candidates found
if len(nodes) == 0: return
# select a random node and reveal its resistance level
node = random.choice(nodes)
node.res_known = True
ShowNode(node)
PopUp('Reconnaissance teams have reported on a nearby area.')
# Enemy Reinforcements: Previously known resistance level is increased
elif roll <= 90:
# build list of possible nodes
nodes = []
for node in campaign.day_map.nodes:
if node in campaign.day_map.player_node.links and node.res_known:
if not node.friendly_control and node.resistance != 'Heavy':
nodes.append(node)