pywargame 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. pywargame/__init__.py +2 -0
  2. pywargame/common/__init__.py +3 -0
  3. pywargame/common/collector.py +87 -0
  4. pywargame/common/dicedraw.py +363 -0
  5. pywargame/common/drawdice.py +40 -0
  6. pywargame/common/singleton.py +22 -0
  7. pywargame/common/test.py +25 -0
  8. pywargame/common/verbose.py +59 -0
  9. pywargame/common/verboseguard.py +53 -0
  10. pywargame/cyberboard/__init__.py +18 -0
  11. pywargame/cyberboard/archive.py +283 -0
  12. pywargame/cyberboard/base.py +63 -0
  13. pywargame/cyberboard/board.py +462 -0
  14. pywargame/cyberboard/cell.py +200 -0
  15. pywargame/cyberboard/collect.py +49 -0
  16. pywargame/cyberboard/collectgbx0pwd.py +30 -0
  17. pywargame/cyberboard/collectgbxext.py +30 -0
  18. pywargame/cyberboard/collectgsnexp.py +32 -0
  19. pywargame/cyberboard/collectgsnext.py +30 -0
  20. pywargame/cyberboard/draw.py +396 -0
  21. pywargame/cyberboard/exporter.py +1132 -0
  22. pywargame/cyberboard/extractor.py +240 -0
  23. pywargame/cyberboard/features.py +17 -0
  24. pywargame/cyberboard/gamebox.py +81 -0
  25. pywargame/cyberboard/gbxexp.py +76 -0
  26. pywargame/cyberboard/gbxext.py +64 -0
  27. pywargame/cyberboard/gsnexp.py +147 -0
  28. pywargame/cyberboard/gsnext.py +59 -0
  29. pywargame/cyberboard/head.py +111 -0
  30. pywargame/cyberboard/image.py +76 -0
  31. pywargame/cyberboard/main.py +47 -0
  32. pywargame/cyberboard/mark.py +102 -0
  33. pywargame/cyberboard/palette.py +36 -0
  34. pywargame/cyberboard/piece.py +169 -0
  35. pywargame/cyberboard/player.py +36 -0
  36. pywargame/cyberboard/scenario.py +115 -0
  37. pywargame/cyberboard/testgrid.py +156 -0
  38. pywargame/cyberboard/tile.py +121 -0
  39. pywargame/cyberboard/tray.py +68 -0
  40. pywargame/cyberboard/windows.py +41 -0
  41. pywargame/cyberboard/zeropwd.py +45 -0
  42. pywargame/cyberboard.py +2728 -0
  43. pywargame/gbx0pwd.py +2776 -0
  44. pywargame/gbxextract.py +2795 -0
  45. pywargame/gsnexport.py +16499 -0
  46. pywargame/gsnextract.py +2790 -0
  47. pywargame/latex/__init__.py +2 -0
  48. pywargame/latex/collect.py +34 -0
  49. pywargame/latex/latexexporter.py +4010 -0
  50. pywargame/latex/main.py +184 -0
  51. pywargame/vassal/__init__.py +66 -0
  52. pywargame/vassal/base.py +139 -0
  53. pywargame/vassal/board.py +243 -0
  54. pywargame/vassal/buildfile.py +60 -0
  55. pywargame/vassal/chart.py +79 -0
  56. pywargame/vassal/chessclock.py +197 -0
  57. pywargame/vassal/collect.py +98 -0
  58. pywargame/vassal/collectpatch.py +28 -0
  59. pywargame/vassal/command.py +21 -0
  60. pywargame/vassal/documentation.py +322 -0
  61. pywargame/vassal/dumpcollect.py +28 -0
  62. pywargame/vassal/dumpvsav.py +28 -0
  63. pywargame/vassal/element.py +439 -0
  64. pywargame/vassal/exporter.py +89 -0
  65. pywargame/vassal/extension.py +101 -0
  66. pywargame/vassal/folder.py +103 -0
  67. pywargame/vassal/game.py +940 -0
  68. pywargame/vassal/gameelements.py +1091 -0
  69. pywargame/vassal/globalkey.py +127 -0
  70. pywargame/vassal/globalproperty.py +433 -0
  71. pywargame/vassal/grid.py +573 -0
  72. pywargame/vassal/map.py +1061 -0
  73. pywargame/vassal/mapelements.py +1020 -0
  74. pywargame/vassal/merge.py +57 -0
  75. pywargame/vassal/merger.py +460 -0
  76. pywargame/vassal/moduledata.py +275 -0
  77. pywargame/vassal/mrgcollect.py +31 -0
  78. pywargame/vassal/patch.py +44 -0
  79. pywargame/vassal/patchcollect.py +28 -0
  80. pywargame/vassal/player.py +83 -0
  81. pywargame/vassal/save.py +495 -0
  82. pywargame/vassal/skel.py +380 -0
  83. pywargame/vassal/trait.py +224 -0
  84. pywargame/vassal/traits/__init__.py +36 -0
  85. pywargame/vassal/traits/area.py +50 -0
  86. pywargame/vassal/traits/basic.py +35 -0
  87. pywargame/vassal/traits/calculatedproperty.py +22 -0
  88. pywargame/vassal/traits/cargo.py +29 -0
  89. pywargame/vassal/traits/click.py +41 -0
  90. pywargame/vassal/traits/clone.py +28 -0
  91. pywargame/vassal/traits/delete.py +24 -0
  92. pywargame/vassal/traits/deselect.py +32 -0
  93. pywargame/vassal/traits/dynamicproperty.py +112 -0
  94. pywargame/vassal/traits/globalcommand.py +55 -0
  95. pywargame/vassal/traits/globalhotkey.py +26 -0
  96. pywargame/vassal/traits/globalproperty.py +54 -0
  97. pywargame/vassal/traits/hide.py +67 -0
  98. pywargame/vassal/traits/label.py +76 -0
  99. pywargame/vassal/traits/layer.py +105 -0
  100. pywargame/vassal/traits/mark.py +20 -0
  101. pywargame/vassal/traits/mask.py +85 -0
  102. pywargame/vassal/traits/mat.py +26 -0
  103. pywargame/vassal/traits/moved.py +35 -0
  104. pywargame/vassal/traits/movefixed.py +51 -0
  105. pywargame/vassal/traits/nonrect.py +95 -0
  106. pywargame/vassal/traits/nostack.py +55 -0
  107. pywargame/vassal/traits/place.py +104 -0
  108. pywargame/vassal/traits/prototype.py +20 -0
  109. pywargame/vassal/traits/report.py +34 -0
  110. pywargame/vassal/traits/restrictaccess.py +28 -0
  111. pywargame/vassal/traits/restrictcommand.py +32 -0
  112. pywargame/vassal/traits/return.py +40 -0
  113. pywargame/vassal/traits/rotate.py +62 -0
  114. pywargame/vassal/traits/sendto.py +59 -0
  115. pywargame/vassal/traits/sheet.py +129 -0
  116. pywargame/vassal/traits/skel.py +9 -0
  117. pywargame/vassal/traits/stack.py +28 -0
  118. pywargame/vassal/traits/submenu.py +27 -0
  119. pywargame/vassal/traits/trail.py +61 -0
  120. pywargame/vassal/traits/trigger.py +72 -0
  121. pywargame/vassal/turn.py +272 -0
  122. pywargame/vassal/upgrade.py +191 -0
  123. pywargame/vassal/vmod.py +323 -0
  124. pywargame/vassal/vsav.py +100 -0
  125. pywargame/vassal/widget.py +358 -0
  126. pywargame/vassal/withtraits.py +634 -0
  127. pywargame/vassal/xml.py +4 -0
  128. pywargame/vassal/zone.py +399 -0
  129. pywargame/vassal.py +12500 -0
  130. pywargame/vmodpatch.py +12548 -0
  131. pywargame/vsavdump.py +12533 -0
  132. pywargame/vslmerge.py +13015 -0
  133. pywargame/wgexport.py +16689 -0
  134. pywargame/ztexport.py +14351 -0
  135. pywargame/zuntzu/__init__.py +5 -0
  136. pywargame/zuntzu/base.py +82 -0
  137. pywargame/zuntzu/collect.py +38 -0
  138. pywargame/zuntzu/countersheet.py +250 -0
  139. pywargame/zuntzu/dicehand.py +48 -0
  140. pywargame/zuntzu/exporter.py +936 -0
  141. pywargame/zuntzu/gamebox.py +154 -0
  142. pywargame/zuntzu/map.py +36 -0
  143. pywargame/zuntzu/piece.py +37 -0
  144. pywargame/zuntzu/scenario.py +208 -0
  145. pywargame/zuntzu/ztexp.py +115 -0
  146. pywargame-0.3.1.dist-info/METADATA +353 -0
  147. pywargame-0.3.1.dist-info/RECORD +150 -0
  148. pywargame-0.3.1.dist-info/WHEEL +5 -0
  149. pywargame-0.3.1.dist-info/licenses/LICENSE +5 -0
  150. pywargame-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4010 @@
1
+ ## BEGIN_IMPORTS
2
+ from common import VerboseGuard, Verbose
3
+ from vassal.buildfile import BuildFile
4
+ from vassal.documentation import Documentation
5
+ from vassal.traits import *
6
+ from vassal.base import *
7
+ from vassal.moduledata import ModuleData
8
+ from vassal.exporter import Exporter
9
+ from pprint import pprint
10
+
11
+ ## END_IMPORTS
12
+
13
+ # ====================================================================
14
+ #
15
+ # Exporter class
16
+ #
17
+ class LaTeXExporter(Exporter):
18
+ class Specials:
19
+ BATTLE_MARK = 'wgBattleMarker'
20
+ BATTLE_CTRL = 'wgBattleCtrl'
21
+ BATTLE_CALC = 'wgBattleCalc'
22
+ BATTLE_UNIT = 'wgBattleUnit'
23
+ ODDS_MARK = 'wgOddsMarker'
24
+ DRM_MARK = 'wgDRMMarker'
25
+ HIDDEN_NAME = 'wg hidden unit'
26
+
27
+ class Keys:
28
+ MARK_BATTLE = key(NONE,0)+',wgMarkBattle'
29
+ CLEAR_BATTLE = key(NONE,0)+',wgClearBattle'
30
+ CLEAR_ALL_BATTLE = key(NONE,0)+',wgClearAllBattle'
31
+ ZERO_BATTLE = key(NONE,0)+',wgZeroBattle'
32
+ INCR_BATTLE = key(NONE,0)+',wgIncrBattle'
33
+ SET_BATTLE = key(NONE,0)+',wgSetBattle'
34
+ GET_BATTLE = key(NONE,0)+',wgGetBattle'
35
+ MARK_ODDS = key(NONE,0)+',wgMarkOdds'
36
+ MARK_RESULT = key(NONE,0)+',wgMarkResult'
37
+ CLEAR_MOVED = key(NONE,0)+',wgClearMoved'
38
+ ZERO_BATTLE_AF = key(NONE,0)+',wgZeroBattleAF'
39
+ ZERO_BATTLE_DF = key(NONE,0)+',wgZeroBattleDF'
40
+ ZERO_BATTLE_FRAC = key(NONE,0)+',wgZeroBattleFrac'
41
+ ZERO_BATTLE_ODDS = key(NONE,0)+',wgZeroBattleOdds'
42
+ ZERO_BATTLE_SHFT = key(NONE,0)+',wgZeroBattleShift'
43
+ ZERO_BATTLE_DRM = key(NONE,0)+',wgZeroBattleDRM'
44
+ ZERO_BATTLE_IDX = key(NONE,0)+',wgZeroBattleIdx'
45
+ CALC_BATTLE_AF = key(NONE,0)+',wgCalcBattleAF'
46
+ CALC_BATTLE_DF = key(NONE,0)+',wgCalcBattleDF'
47
+ CALC_BATTLE_FRAC = key(NONE,0)+',wgCalcBattleFrac'
48
+ CALC_BATTLE_ODDS = key(NONE,0)+',wgCalcBattleOdds'
49
+ CALC_BATTLE_SHFT = key(NONE,0)+',wgCalcBattleShift'
50
+ CALC_BATTLE_DRM = key(NONE,0)+',wgCalcBattleDRM'
51
+ CALC_BATTLE_IDX = key(NONE,0)+',wgCalcBattleIdx'
52
+ CALC_BATTLE_RES = key(NONE,0)+',wgCalcBattleResult'
53
+ CLEAR_BATTLE_PHS = key(NONE,0)+',wgClearBattlePhs'
54
+ RESOLVE_BATTLE = key(NONE,0)+',wgResolveBattle'
55
+ ROLL_DICE = key(NONE,0)+',wgRollDice'
56
+ DICE_INIT_KEY = key(NONE,0)+',wgInitDice'
57
+ TRAIL_TOGGLE_KEY = key(NONE,0)+',wgTrailToggle'
58
+ CLEAR_KEY = key('C')
59
+ CLEAR_ALL_KEY = key('C',CTRL_SHIFT)
60
+ DELETE_KEY = key('D')
61
+ ELIMINATE_KEY = key('E')
62
+ FLIP_KEY = key('F')
63
+ TRAIL_KEY = key('T')
64
+ RESTORE_KEY = key('R')
65
+ MARK_KEY = key('X')
66
+ RESOLVE_KEY = key('Y')
67
+ ROTATE_CCWKey = key('[')
68
+ ROTATE_CWKey = key(']')
69
+ CHARTS_KEY = key('A',ALT)
70
+ OOB_KEY = key('B',ALT)
71
+ COUNTERS_KEY = key('C',ALT)
72
+ DEAD_KEY = key('E',ALT)
73
+ DICE_KEY = key('6',ALT)
74
+ RECALC_ODDS = key('X',CTRL_SHIFT)
75
+ UNDO_KEY = key('Z')
76
+ PRINT_CMD = key(NONE,0)+'+wgPrint'
77
+
78
+ class Globals:
79
+ BATTLE_COUNTER = 'wgBattleCounter'
80
+ CURRENT_BATTLE = 'wgCurrentBattle'
81
+ PLACED_GLOBAL = 'wgOddsPlaced'
82
+ MARK_START = 'wgPlaceMarks'
83
+ CURRENT_ATTACKER = 'wgCurrentAttacker'
84
+ BATTLE_NO = 'wgBattleNo'
85
+ BATTLE_AF = 'wgBattleAF'
86
+ BATTLE_DF = 'wgBattleDF'
87
+ BATTLE_FRAC = 'wgBattleFrac'
88
+ BATTLE_IDX = 'wgBattleIdx'
89
+ BATTLE_ODDS = 'wgBattleOdds'
90
+ BATTLE_DRM = 'wgBattleDRM'
91
+ BATTLE_ODDSM = 'wgBattleOddsMarker'
92
+ BATTLE_SHIFT = 'wgBattleShift'
93
+ BATTLE_RESULT = 'wgBattleResult'
94
+ AUTO_ODDS = 'wgAutoOdds'
95
+ AUTO_RESULTS = 'wgAutoResults'
96
+ NO_CLEAR_MOVES = 'wgNoClearMoves'
97
+ NO_CLEAR_BATTLES = 'wgNoClearBattles'
98
+ DEBUG = 'wgDebug'
99
+ VERBOSE = 'wgVerbose'
100
+ TRAILS_FLAG = 'wgTrailsFlag'
101
+
102
+ def __init__(self,
103
+ vmodname = 'Draft.vmod',
104
+ pdfname = 'export.pdf',
105
+ infoname = 'export.json',
106
+ title = 'Draft',
107
+ version = 'draft',
108
+ imageFormat = 'png',
109
+ description = '',
110
+ rules = None,
111
+ tutorial = None,
112
+ patch = None,
113
+ visible = True,
114
+ vassalVersion = '3.6.7',
115
+ nonato = False,
116
+ nochit = False,
117
+ counterScale = 1,
118
+ resolution = 150):
119
+ '''Exports a PDF and associated JSON files to a VASSAL module.
120
+
121
+ Parameters
122
+ ----------
123
+ vmodname : str
124
+ Name of module file to write
125
+ pdfname : str
126
+ Name of PDF file to read images from
127
+ infoname : str
128
+ Name of JSON file to read meta data from
129
+ title : str
130
+ Name of module
131
+ version : str
132
+ Version of midule
133
+ description : str
134
+ Short description of the module
135
+ rules : str
136
+ Optional name PDF file to attach as rules
137
+ tutorial : str
138
+ Optional name of a VASSAL log file to use as tutorial
139
+ patch : str
140
+ Optional name of Python script to post process the module
141
+ visible : bool
142
+ Make grids visible
143
+ vassalVersion : str
144
+ VASSAL version to encode this module for
145
+ resolution : int
146
+ Resolution for images (default 150)
147
+ '''
148
+ self._vmodname = vmodname
149
+ self._pdfname = pdfname
150
+ self._infoname = infoname
151
+ self._title = title
152
+ self._version = version
153
+ self._description = description
154
+ self._rules = rules
155
+ self._tutorial = tutorial
156
+ self._patch = patch
157
+ self._visible = visible or version.lower() == 'draft'
158
+ self._vassalVersion = vassalVersion
159
+ self._nonato = nonato
160
+ self._nochit = nochit
161
+ self._resolution = resolution
162
+ self._counterScale = counterScale
163
+ self._img_format = imageFormat.lower()
164
+
165
+ self._battleMark = LaTeXExporter.Specials.BATTLE_MARK
166
+ self._oddsMark = LaTeXExporter.Specials.ODDS_MARK
167
+ self._drmMark = LaTeXExporter.Specials.DRM_MARK
168
+ self._battleCtrl = LaTeXExporter.Specials.BATTLE_CTRL
169
+ self._battleCalc = LaTeXExporter.Specials.BATTLE_CALC
170
+ self._battleUnit = LaTeXExporter.Specials.BATTLE_UNIT
171
+ self._hiddenName = LaTeXExporter.Specials.HIDDEN_NAME
172
+ self._markBattle = LaTeXExporter.Keys.MARK_BATTLE
173
+ self._clearBattle = LaTeXExporter.Keys.CLEAR_BATTLE
174
+ self._clearAllBattle = LaTeXExporter.Keys.CLEAR_ALL_BATTLE
175
+ self._zeroBattle = LaTeXExporter.Keys.ZERO_BATTLE
176
+ self._incrBattle = LaTeXExporter.Keys.INCR_BATTLE
177
+ self._setBattle = LaTeXExporter.Keys.SET_BATTLE
178
+ self._getBattle = LaTeXExporter.Keys.GET_BATTLE
179
+ self._markOdds = LaTeXExporter.Keys.MARK_ODDS
180
+ self._markResult = LaTeXExporter.Keys.MARK_RESULT
181
+ self._clearMoved = LaTeXExporter.Keys.CLEAR_MOVED
182
+ self._zeroBattleAF = LaTeXExporter.Keys.ZERO_BATTLE_AF
183
+ self._zeroBattleDF = LaTeXExporter.Keys.ZERO_BATTLE_DF
184
+ self._zeroBattleFrac = LaTeXExporter.Keys.ZERO_BATTLE_FRAC
185
+ self._zeroBattleOdds = LaTeXExporter.Keys.ZERO_BATTLE_ODDS
186
+ self._zeroBattleShft = LaTeXExporter.Keys.ZERO_BATTLE_SHFT
187
+ self._zeroBattleDRM = LaTeXExporter.Keys.ZERO_BATTLE_DRM
188
+ self._zeroBattleIdx = LaTeXExporter.Keys.ZERO_BATTLE_IDX
189
+ self._calcBattleAF = LaTeXExporter.Keys.CALC_BATTLE_AF
190
+ self._calcBattleDF = LaTeXExporter.Keys.CALC_BATTLE_DF
191
+ self._calcBattleFrac = LaTeXExporter.Keys.CALC_BATTLE_FRAC
192
+ self._calcBattleOdds = LaTeXExporter.Keys.CALC_BATTLE_ODDS
193
+ self._calcBattleShft = LaTeXExporter.Keys.CALC_BATTLE_SHFT
194
+ self._calcBattleIdx = LaTeXExporter.Keys.CALC_BATTLE_IDX
195
+ self._calcBattleDRM = LaTeXExporter.Keys.CALC_BATTLE_DRM
196
+ self._calcBattleRes = LaTeXExporter.Keys.CALC_BATTLE_RES
197
+ self._clearBattlePhs = LaTeXExporter.Keys.CLEAR_BATTLE_PHS
198
+ self._resolveBattle = LaTeXExporter.Keys.RESOLVE_BATTLE
199
+ self._rollDice = LaTeXExporter.Keys.ROLL_DICE
200
+ self._diceInitKey = LaTeXExporter.Keys.DICE_INIT_KEY
201
+ self._clearKey = LaTeXExporter.Keys.CLEAR_KEY
202
+ self._clearAllKey = LaTeXExporter.Keys.CLEAR_ALL_KEY
203
+ self._deleteKey = LaTeXExporter.Keys.DELETE_KEY
204
+ self._eliminateKey = LaTeXExporter.Keys.ELIMINATE_KEY
205
+ self._flipKey = LaTeXExporter.Keys.FLIP_KEY
206
+ self._trailKey = LaTeXExporter.Keys.TRAIL_KEY
207
+ self._trailToggleKey = LaTeXExporter.Keys.TRAIL_TOGGLE_KEY
208
+ self._restoreKey = LaTeXExporter.Keys.RESTORE_KEY
209
+ self._markKey = LaTeXExporter.Keys.MARK_KEY
210
+ self._resolveKey = LaTeXExporter.Keys.RESOLVE_KEY
211
+ self._rotateCCWKey = LaTeXExporter.Keys.ROTATE_CCWKey
212
+ self._rotateCWKey = LaTeXExporter.Keys.ROTATE_CWKey
213
+ self._chartsKey = LaTeXExporter.Keys.CHARTS_KEY
214
+ self._oobKey = LaTeXExporter.Keys.OOB_KEY
215
+ self._countersKey = LaTeXExporter.Keys.COUNTERS_KEY
216
+ self._deadKey = LaTeXExporter.Keys.DEAD_KEY
217
+ self._diceKey = LaTeXExporter.Keys.DICE_KEY
218
+ self._recalcOdds = LaTeXExporter.Keys.RECALC_ODDS
219
+ self._battleCounter = LaTeXExporter.Globals.BATTLE_COUNTER
220
+ self._currentBattle = LaTeXExporter.Globals.CURRENT_BATTLE
221
+ self._placedGlobal = LaTeXExporter.Globals.PLACED_GLOBAL
222
+ self._markStart = LaTeXExporter.Globals.MARK_START
223
+ self._currentAttacker = LaTeXExporter.Globals.CURRENT_ATTACKER
224
+ self._battleNo = LaTeXExporter.Globals.BATTLE_NO
225
+ self._battleAF = LaTeXExporter.Globals.BATTLE_AF
226
+ self._battleDF = LaTeXExporter.Globals.BATTLE_DF
227
+ self._battleFrac = LaTeXExporter.Globals.BATTLE_FRAC
228
+ self._battleIdx = LaTeXExporter.Globals.BATTLE_IDX
229
+ self._battleOdds = LaTeXExporter.Globals.BATTLE_ODDS
230
+ self._battleOddsM = LaTeXExporter.Globals.BATTLE_ODDSM
231
+ self._battleShift = LaTeXExporter.Globals.BATTLE_SHIFT
232
+ self._battleDRM = LaTeXExporter.Globals.BATTLE_DRM
233
+ self._battleResult = LaTeXExporter.Globals.BATTLE_RESULT
234
+ self._autoOdds = LaTeXExporter.Globals.AUTO_ODDS
235
+ self._autoResults = LaTeXExporter.Globals.AUTO_RESULTS
236
+ self._noClearMoves = LaTeXExporter.Globals.NO_CLEAR_MOVES
237
+ self._noClearBattles = LaTeXExporter.Globals.NO_CLEAR_BATTLES
238
+ self._debug = LaTeXExporter.Globals.DEBUG
239
+ self._verbose = LaTeXExporter.Globals.VERBOSE
240
+ self._trailsFlag = LaTeXExporter.Globals.TRAILS_FLAG
241
+ self._battleMarks = []
242
+ self._oddsMarks = []
243
+ self._resultMarks = []
244
+ self._hidden = None
245
+ self._dice = {}
246
+ self._diceInit = None
247
+ self._printCmd = LaTeXExporter.Keys.PRINT_CMD
248
+ self._undoKey = LaTeXExporter.Keys.UNDO_KEY
249
+
250
+ with VerboseGuard('Overall settings') as v:
251
+ v(f'Module file name: {self._vmodname}')
252
+ v(f'PDF file name: {self._pdfname}')
253
+ v(f'JSON file name: {self._infoname}')
254
+ v(f'Game title: {self._title}')
255
+ v(f'Game version: {self._version}')
256
+ v(f'Description: {self._description}')
257
+ v(f'Rules PDF file: {self._rules}')
258
+ v(f'Tutorial log: {self._tutorial}')
259
+ v(f'Patch scripts: {self._patch}')
260
+ v(f'Visible grids: {self._visible}')
261
+ v(f'Resolution: {self._resolution}')
262
+ v(f'Scale of counters: {self._counterScale}')
263
+ v(f'Image format: {self._img_format}')
264
+
265
+
266
+ def setup(self):
267
+ # Start the processing
268
+ self._info = self.convertPages()
269
+ self._categories, \
270
+ self._mains, \
271
+ self._echelons, \
272
+ self._commands = self.writeImages(self._counterScale)
273
+
274
+
275
+ def run(self):
276
+ super(LaTeXExporter,self).run(self._vmodname,self._patch)
277
+
278
+
279
+
280
+ # ================================================================
281
+ def createProcess(self,args):
282
+ '''Spawn a process and pipe output here
283
+
284
+ Parameters
285
+ ----------
286
+ args : list
287
+ List of process command line elements
288
+
289
+ Returns
290
+ -------
291
+ pipe : subprocess.Pipe
292
+ Pipe to read from
293
+ '''
294
+ from os import environ
295
+ from subprocess import Popen, PIPE
296
+
297
+ return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
298
+
299
+ # ----------------------------------------------------------------
300
+ def addPws(self,opw=None,upw=None):
301
+ '''Add a `Pws` element to arguments
302
+
303
+ Add password options
304
+
305
+ Parameters
306
+ ----------
307
+ kwargs : dict
308
+ Dictionary of attribute key-value pairs
309
+
310
+ Returns
311
+ -------
312
+ element : Pws
313
+ The added element
314
+ '''
315
+ args = []
316
+ if upw is not None: args.extend(['-upw',upw])
317
+ if opw is not None: args.extend(['-opw',opw])
318
+ return args
319
+
320
+ # ----------------------------------------------------------------
321
+ def getPdfInfo(self,upw=None,opw=None,timeout=None):
322
+ '''Get information about the PDF
323
+
324
+ Parameters
325
+ ----------
326
+ opw : str
327
+ Owner password (optional)
328
+ upw : str
329
+ User password (optional)
330
+ timeout : int
331
+ Time out in miliseconds for subprocesses
332
+
333
+ Returns
334
+ -------
335
+ info : dict
336
+ Image information
337
+ '''
338
+ args = ['pdfinfo', self._pdfname ]
339
+ args.extend(self.addPws(opw=opw,upw=upw))
340
+
341
+ with VerboseGuard(f'Getting information from PDF {self._pdfname}'):
342
+ proc = self.createProcess(args)
343
+ try:
344
+ out, err = proc.communicate(timeout=timeout)
345
+ except:
346
+ proc.kill()
347
+ proc.communicate()
348
+ raise RuntimeError(f'Failed to get PDF info: {e}')
349
+
350
+ d = {}
351
+ for field in out.decode('utf8','ignore').split('\n'):
352
+ if field == '':
353
+ continue
354
+ subfields = field.split(':')
355
+ key, value = subfields[0], ':'.join(subfields[1:])
356
+ if key != '':
357
+ d[key] = (int(value.strip()) if key == 'Pages'
358
+ else value.strip())
359
+
360
+ if 'Pages' not in d:
361
+ raise ValueError(f'Page count not found from {self._pdfname}')
362
+
363
+ return d
364
+
365
+ # ----------------------------------------------------------------
366
+ def getImagesInfo(self):
367
+ '''Read in JSON information, and return as dictionary'''
368
+ from json import load
369
+
370
+ with VerboseGuard(f'Getting information from JSON {self._infoname}'):
371
+ with open(self._infoname) as file:
372
+ info = load(file)
373
+
374
+ return info
375
+
376
+ # ================================================================
377
+ @classmethod
378
+ def parseLength(cls,value,def_unit='px'):
379
+ from re import match
380
+
381
+ scales = {
382
+ 'px': 1,
383
+ 'pt': 1.25,
384
+ 'pc': 15,
385
+ 'in': 90,
386
+ 'mm': 3.543307,
387
+ 'cm': 35.43307,
388
+ '%': -1/100
389
+ }
390
+
391
+ if not value:
392
+ return 0
393
+
394
+ parts = match(r'^\s*(-?\d+(?:\.\d+)?)\s*(px|in|cm|mm|pt|pc|%)?', value)
395
+ if not parts:
396
+ raise RuntimeError(f'Unknown length format: "{value}"')
397
+
398
+ number = float(parts.group(1))
399
+ unit = parts.group(2) or def_unit
400
+ factor = scales.get(unit,None)
401
+
402
+ if not factor:
403
+ raise RuntimeError(f'Unknown unit: "{unit}"')
404
+
405
+ return factor * number
406
+
407
+ # ----------------------------------------------------------------
408
+ @classmethod
409
+ def scaleSVG(cls,buffer,factor):
410
+ '''Buffer is bytes'''
411
+ #from xml.dom.minidom import parse
412
+ from re import split
413
+ from io import StringIO, BytesIO
414
+
415
+ if not LaTeXExporter.isSVG(buffer):
416
+ return buffer
417
+
418
+ with BytesIO(buffer) as stream:
419
+ doc = xmlns.parse(stream)
420
+
421
+ if not doc:
422
+ raise RuntimeError('Failed to parse buffer as XML')
423
+
424
+ root = doc.childNodes[0]
425
+ getA = lambda e,n,d=None : \
426
+ e.getAttribute(n) if e.hasAttribute(n) else d
427
+ setA = lambda e,n,v : e.setAttribute(n,v)
428
+ leng = LaTeXExporter.parseLength
429
+
430
+ width = leng(getA(root,'width', '0'))
431
+ height = leng(getA(root,'height','0'))
432
+ vport = getA(root,'viewBox','0 0 0 0').strip()
433
+ vp = [leng(v) for v in split('[ \t,]',vport)]
434
+ # print(f'Input WxH: {width}x{height} ({vp})')
435
+
436
+ width *= factor
437
+ height *= factor
438
+ vp = [factor * v for v in vp]
439
+
440
+ # print(f'Scaled WxH: {width}x{height} ({vp})')
441
+
442
+ if width <= 0 and vp:
443
+ width = vp[2] - vp[0]
444
+
445
+ if height <= 0 and vp:
446
+ height = vp[3] - vp[1]
447
+
448
+ if not vp:
449
+ vp = [0, 0, width, height]
450
+
451
+ setA(root,'transform',f'scale({factor})')
452
+ setA(root,'width', f'{width}')
453
+ setA(root,'height',f'{height}')
454
+ setA(root,'viewBox',' '.join([f'{v}' for v in vp]))
455
+
456
+
457
+ with StringIO() as out:
458
+ doc.writexml(out)
459
+ return out.getvalue().encode()
460
+
461
+
462
+ # ================================================================
463
+ def convertPage(self,page,opw=None,upw=None,timeout=None):
464
+ '''Convert a page from PDF into an image (bytes)
465
+
466
+ Parameters
467
+ ----------
468
+ page : int
469
+ Page number in the PDF to convert
470
+ opw : str
471
+ Owner password (optional)
472
+ upw : str
473
+ User password (optional)
474
+ timeout : int
475
+ Time out in miliseconds for subprocesses
476
+
477
+ Returns
478
+ -------
479
+ info : dict
480
+ Image information
481
+ '''
482
+ args = ['pdftocairo']
483
+ if self._img_format != 'svg':
484
+ args.extend([
485
+ '-transp',
486
+ '-singlefile'])
487
+
488
+ args.extend([
489
+ '-r', str(self._resolution),
490
+ '-f', str(page),
491
+ '-l', str(page),
492
+ f'-{self._img_format}' ])
493
+ args.extend(self.addPws(opw=opw,upw=upw))
494
+ args.append(self._pdfname)
495
+ args.append('-')
496
+
497
+ # print(f'Conversion command',' '.join(args))
498
+ proc = self.createProcess(args)
499
+
500
+ try:
501
+ out, err = proc.communicate(timeout=timeout)
502
+ except Exception as e:
503
+ proc.kill()
504
+ proc.communicate()
505
+ raise RuntimeError(f'Failed to convert page {page} of '
506
+ f'{self._pdfname}: {e}')
507
+
508
+ if len(out) <= 0:
509
+ raise RuntimeError(f'Failed to convert page {page} of '
510
+ f'{self._pdfname}: {err}')
511
+
512
+ # This does not seem to work - VASSAL (and Inkscape) does not
513
+ # apply the 'scale' transformation to the image!
514
+ #
515
+ # if self._img_format == 'svg':
516
+ # out = LaTeXExporter.scaleSVG(out,2)
517
+
518
+ return out
519
+
520
+
521
+ # ----------------------------------------------------------------
522
+ def ignoreEntry(self,info,ignores=['<<dummy>>','<<eol>>']):
523
+ '''Check if we should ignore an entry in the JSON file'''
524
+ return info['category'] in ignores
525
+
526
+ # ----------------------------------------------------------------
527
+ def scaleImage(self,buffer,factor):
528
+ from PIL import Image
529
+ from io import BytesIO
530
+ from math import isclose
531
+
532
+ if isclose(factor,1): return buffer
533
+
534
+ # print(f'Scaling image by factor {factor}')
535
+ with Image.open(BytesIO(buffer)) as img:
536
+ w, h = img.width, img.height
537
+ cpy = img.resize((int(factor*w),int(factor*h)))
538
+
539
+ with BytesIO() as out:
540
+ cpy.save(out,format='PNG')
541
+ return out.getvalue()
542
+
543
+
544
+ # ----------------------------------------------------------------
545
+ def convertPages(self,opw=None,upw=None,timeout=None):
546
+ '''Reads in JSON and pages from PDF and stores information
547
+ dictionary, which is returned
548
+
549
+ Parameters
550
+ ----------
551
+ opw : str
552
+ Owner password (optional)
553
+ upw : str
554
+ User password (optional)
555
+ timeout : int
556
+ Time out in miliseconds for subprocesses
557
+
558
+ Returns
559
+ -------
560
+ info : dict
561
+ Image information
562
+ '''
563
+ oargs = {'opw':opw,'upw':upw }
564
+ docinfo = self.getPdfInfo()
565
+ imgsinfo = self.getImagesInfo()
566
+
567
+ if len(imgsinfo) - 1 != docinfo['Pages']:
568
+ raise RuntimeError(f'Number of pages in {self._pdfname} '
569
+ f'{docinfo["Pages"]} not matched in JSON '
570
+ f'{self._infoname} -> {len(imgsinfo)}')
571
+
572
+ with VerboseGuard(f'Converting {docinfo["Pages"]} '
573
+ f'pages in {self._pdfname}') as v:
574
+ for i,info in enumerate(imgsinfo):
575
+ if self.ignoreEntry(info): continue
576
+
577
+ if i == 0: v(end='')
578
+ v(f'[{info["number"]}]',end=' ',flush=True)
579
+ info['img'] = self.convertPage(info['number'],**oargs)
580
+
581
+ v('done')
582
+
583
+ return imgsinfo
584
+
585
+ # ----------------------------------------------------------------
586
+ @classmethod
587
+ def isSVG(cls,buffer):
588
+ return buffer[:5] == b'<?xml'
589
+
590
+ # ----------------------------------------------------------------
591
+ def getBB(self,buffer):
592
+ '''Get bounding box of image
593
+
594
+ Parameters
595
+ ----------
596
+ buffer : bytes
597
+ The image bytes
598
+
599
+ Returns
600
+ -------
601
+ ulx, uly, lrx, lry : tuple
602
+ The coordinates of the bounding box
603
+ '''
604
+ from io import BytesIO
605
+
606
+ with BytesIO(buffer) as inp:
607
+ if LaTeXExporter.isSVG(buffer):
608
+ from svgelements import SVG
609
+
610
+ svg = SVG.parse(inp)
611
+ # bb = svg.bbox()
612
+ # if bb is None:
613
+ # print(f'No bounding box!')
614
+ # bb = [0, 0, 1, 1]
615
+ # else:
616
+ # bb = [int(b) for b in bb]
617
+ x, y, w, h = svg.x, svg.y, svg.width, svg.height
618
+ bb = (x,y,x+w,y+h)
619
+ else:
620
+ from PIL import Image
621
+
622
+ with Image.open(inp) as img:
623
+ bb = img.getbbox()
624
+
625
+ return bb
626
+
627
+ # ----------------------------------------------------------------
628
+ def getWH(self,buffer):
629
+ '''Get bounding box of image
630
+
631
+ Parameters
632
+ ----------
633
+ buffer : bytes
634
+ The image bytes
635
+
636
+ Returns
637
+ -------
638
+ ulx, uly, lrx, lry : tuple
639
+ The coordinates of the bounding box
640
+ '''
641
+ from io import BytesIO
642
+
643
+ with BytesIO(buffer) as inp:
644
+ if LaTeXExporter.isSVG(buffer):
645
+ from svgelements import SVG
646
+
647
+ svg = SVG.parse(inp)
648
+ w, h = svg.x, svg.y, svg.width, svg.height
649
+ # bb = svg.bbox()
650
+ # w, h = int(bb[2]-bb[0]),int(bb[3]-bb[1])
651
+ else:
652
+ from PIL import Image
653
+
654
+ with Image.open(inp) as img:
655
+ w, h = img.width, img.height
656
+
657
+ return w,h
658
+
659
+ # ----------------------------------------------------------------
660
+ def getOutline(self,buffer):
661
+ '''Get bounding box of image
662
+
663
+ Parameters
664
+ ----------
665
+ buffer : bytes
666
+ The image bytes
667
+
668
+ Returns
669
+ -------
670
+ ulx, uly, lrx, lry : tuple
671
+ The coordinates of the bounding box
672
+ '''
673
+ from PIL import Image
674
+ from io import BytesIO
675
+
676
+ # print(buffer)
677
+ with Image.open(BytesIO(buffer)) as img:
678
+ bb = img.getbbox()
679
+
680
+ for r in range(bb[0],bb[2]):
681
+ for c in range(bb[1],bb[3]):
682
+ pass #print(img.getpixel((c,r)))
683
+
684
+ return None
685
+
686
+
687
+ # ================================================================
688
+ def writeImages(self,counterScale=1):
689
+ '''From the information gathered about the images (including
690
+ their bitmap representation, generate image files in the
691
+ module
692
+
693
+ '''
694
+ categories = {}
695
+ unittypes = []
696
+ echelons = []
697
+ commands = []
698
+
699
+ with VerboseGuard(f'Writing images in VMod '
700
+ f'{self._vmod.fileName()}',end=' ') as v:
701
+ for info in self._info:
702
+ if self.ignoreEntry(info): continue
703
+
704
+ typ = info.get('category','counter')
705
+ sub = info.get('subcategory','all')
706
+ nam = info['name']
707
+ num = info['number']
708
+
709
+ info['filename'] = f'{nam.replace(" ","_")}.{self._img_format}'
710
+ imgfn = 'images/'+info['filename']
711
+ if imgfn not in self._vmod.getFileNames():
712
+ if typ == 'counter' and self._img_format != 'svg':
713
+ # print(f'Possibly scale file {imgfn}')
714
+ info['img'] = self.scaleImage(info['img'],
715
+ counterScale)
716
+ # self.message(f'Writing image {imgfn}')
717
+ self._vmod.addFile(imgfn,info['img'])
718
+
719
+ if sub == '':
720
+ info['subcategory'] = 'all'
721
+ sub = 'all'
722
+
723
+ # Add into catalogue
724
+ if typ not in categories:
725
+ v('')
726
+ v(f'Adding category "{typ}"')
727
+ v('',end=' ')
728
+ categories[typ] = {}
729
+ cat = categories[typ]
730
+
731
+ if sub not in cat:
732
+ v('')
733
+ v(f'Adding sub-category "{sub}"')
734
+ v('',end=' ')
735
+ cat[sub] = {}
736
+ tgt = cat[sub]
737
+
738
+ v(f'[{nam}]',end=' ',flush=True,noindent=True)
739
+ #self.message(f'Adding "{info["name"]}" to catalogue')
740
+ #
741
+ # Here we could handle multiple info's with the same
742
+ # name by adding a unique postfix - e.g., for dices
743
+ # what have non-uniform PMFs.
744
+ #
745
+ # if info['name'] in tgt:
746
+ # n = len([i for k,i in tgt.items() if k.startswith(info['name'])])
747
+ # info['name'] += '_' + str(n)
748
+ # info['filename'] = info['name'].replace(' ','_') + '.png'
749
+ unam = f'{nam}'
750
+ tgt[unam] = info
751
+
752
+ if self._nonato: continue
753
+
754
+ # Get NATO App6c information, if any
755
+ natoapp6c = info.get('natoapp6c',None)
756
+ if natoapp6c is not None:
757
+ from re import sub
758
+ def clean(s):
759
+ return sub('.*=','',
760
+ (sub(r'\[[^]]+\]','',s.strip())
761
+ .replace('{','')
762
+ .replace('}','')
763
+ .replace('/',' '))).strip()
764
+ mains = clean(natoapp6c.get('main', ''))
765
+ lower = clean(natoapp6c.get('lower', ''))
766
+ upper = clean(natoapp6c.get('upper', ''))
767
+ echelon = clean(natoapp6c.get('echelon',''))
768
+ command = clean(natoapp6c.get('command',''))
769
+
770
+
771
+ if mains is not None:
772
+ if len(lower) > 0: mains += ' '+lower
773
+ if len(upper) > 0: mains += ' '+upper
774
+ mains = sub(r'\[[^]]+\]','',mains)\
775
+ .replace('{','').replace('}','')#.split(',')
776
+ unittypes.append(mains.replace(',',' '))
777
+ unittypes.extend([s.strip().replace(',',' ')
778
+ for s in mains.split(',')])
779
+ #if len(mains) > 1:
780
+ # unittypes.append('+'.join(mains))
781
+ info['mains'] = mains
782
+
783
+ if len(echelon) > 0:
784
+ echelons.append(echelon)
785
+ info['echelon'] = echelon
786
+
787
+ if len(command) > 0:
788
+ commands.append(command)
789
+ info['command'] = command
790
+
791
+
792
+ # Finished loop over infos. Make unit types, echelons,
793
+ # commands unique
794
+ v('done')
795
+
796
+ return categories, set(unittypes), set(echelons), set(commands)
797
+
798
+ # ================================================================
799
+ def createModuleData(self):
800
+ '''Create the `moduleData` file in the module
801
+ '''
802
+ with VerboseGuard(f'Creating module data'):
803
+ self._moduleData = ModuleData()
804
+ data = self._moduleData.addData()
805
+ data.addVersion (version=self._version)
806
+ data.addVASSALVersion(version=self._vassalVersion)
807
+ data.addName (name=self._title)
808
+ data.addDescription (description=self._description)
809
+ data.addDateSaved ()
810
+
811
+ # ================================================================
812
+ def createBuildFile(self,
813
+ ignores = '(.*markers?|all|commons|.*hidden|[ ]+)'):
814
+ '''Create the `buildFile.xml` file in the module.
815
+
816
+ Parameters
817
+ ----------
818
+ ignores : str
819
+ Regular expression to match ignored categories for factions
820
+ determination. Python's re.fullmatch is applied to this
821
+ regular exression against chit categories. If the pattern
822
+ is matched, then the chit is not considered to belong to a
823
+ faction.
824
+
825
+ '''
826
+ from re import fullmatch, IGNORECASE
827
+ with VerboseGuard(f'Creating build file') as v:
828
+ self._build = BuildFile() # 'buildFile.xml')
829
+ self._game = self._build.addGame(name = self._title,
830
+ version = self._version,
831
+ description = self._description)
832
+ doc = self.addDocumentation()
833
+ self._game.addBasicCommandEncoder()
834
+
835
+ # Extract the sides
836
+ self._sides = [ k
837
+ for k in self._categories.get('counter',{}).keys()
838
+ if fullmatch(ignores, k, IGNORECASE) is None]
839
+ v(f'Got sides: {", ".join(self._sides)}')
840
+
841
+ v(f'Adding Global options')
842
+ go = self._game.addGlobalOptions(
843
+ autoReport = GlobalOptions.PROMPT,
844
+ centerOnMove = GlobalOptions.PROMPT,
845
+ nonOwnerUnmaskable = GlobalOptions.PROMPT,
846
+ playerIdFormat = '$playerName$')
847
+ go.addOption(name='undoHotKey',value=self._undoKey)
848
+ go.addOption(name='undoIcon', value='/images/Undo16.gif')
849
+ # go.addOptoin(name='stepHotKey',value='')
850
+ go.addBoolPreference(name = self._verbose,
851
+ default = True,
852
+ desc = 'Be verbose',
853
+ tab = self._title)
854
+ go.addBoolPreference(name = self._debug,
855
+ default = False,
856
+ desc = 'Show debug chat messages',
857
+ tab = self._title)
858
+ go.addBoolPreference(name = self._autoOdds,
859
+ default = False,
860
+ desc = 'Calculate Odds on battle declaration',
861
+ tab = self._title)
862
+ go.addBoolPreference(name = self._autoResults,
863
+ default = False,
864
+ desc = 'Resolve battle results automatically',
865
+ tab = self._title)
866
+ go.addBoolPreference(name = self._noClearMoves,
867
+ default = False,
868
+ desc = ('Do not remove moved markers '
869
+ 'on phase change'),
870
+ tab = self._title)
871
+ go.addBoolPreference(name = self._noClearBattles,
872
+ default = False,
873
+ desc = ('Do not remove battle markers '
874
+ 'on phase change'),
875
+ tab = self._title)
876
+
877
+ v(f'Adding player roster')
878
+ roster = self._game.addPlayerRoster()
879
+ for side in self._sides:
880
+ roster.addSide(side)
881
+
882
+ v(f'Adding global properties')
883
+ glob = self._game.addGlobalProperties()
884
+ glob.addProperty(name='TurnTracker.defaultDocked',
885
+ initialValue=True)
886
+ glob.addProperty(name=self._trailsFlag,
887
+ initialValue = False,
888
+ isNumeric = True,
889
+ description = 'Global trails on/off')
890
+
891
+ self._battleMarks = self._categories\
892
+ .get('counter',{})\
893
+ .get('BattleMarkers',[])
894
+ if len(self._battleMarks) > 0:
895
+ v(f'We have battle markers')
896
+
897
+ glob.addProperty(name = self._battleCounter,
898
+ initialValue = 0,
899
+ isNumeric = True,
900
+ min = 0,
901
+ max = len(self._battleMarks),
902
+ wrap = True,
903
+ description = 'Counter of battles')
904
+ glob.addProperty(name = self._currentBattle,
905
+ initialValue = 0,
906
+ isNumeric = True,
907
+ min = 0,
908
+ max = len(self._battleMarks),
909
+ wrap = True,
910
+ description = 'Current battle number')
911
+ glob.addProperty(name = self._placedGlobal,
912
+ initialValue = False,
913
+ isNumeric = True,
914
+ wrap = True,
915
+ description = 'Odds have been placed')
916
+ glob.addProperty(name = self._markStart,
917
+ initialValue = False,
918
+ isNumeric = True,
919
+ wrap = True,
920
+ description = 'Mark battle in progress')
921
+ glob.addProperty(name = self._currentAttacker,
922
+ initialValue = 0,
923
+ isNumeric = True,
924
+ min = 0,
925
+ max = 1,
926
+ wrap = True,
927
+ description = 'Current unit is attacker')
928
+ glob.addProperty(name = self._battleAF,
929
+ initialValue = 0,
930
+ isNumeric = True,
931
+ description = 'Current battle AF')
932
+ glob.addProperty(name = self._battleDF,
933
+ initialValue = 0,
934
+ isNumeric = True,
935
+ description = 'Current battle DF')
936
+ glob.addProperty(name = self._battleFrac,
937
+ initialValue = 0,
938
+ isNumeric = True,
939
+ description = 'Current battle fraction')
940
+ glob.addProperty(name = self._battleShift,
941
+ initialValue = 0,
942
+ isNumeric = True,
943
+ description = 'Current battle odds shift')
944
+ glob.addProperty(name = self._battleDRM,
945
+ initialValue = 0,
946
+ isNumeric = True,
947
+ description = 'Current battle die roll mod')
948
+ glob.addProperty(name = self._battleOdds,
949
+ initialValue = '',
950
+ isNumeric = False,
951
+ description = 'Current battle odds')
952
+ glob.addProperty(name = self._battleResult,
953
+ initialValue = '',
954
+ isNumeric = False,
955
+ description = 'Current battle results')
956
+ glob.addProperty(name = self._battleIdx,
957
+ initialValue = 0,
958
+ isNumeric = True,
959
+ description = 'Current battle odds index')
960
+
961
+ self._oddsMarks = self._categories\
962
+ .get('counter',{})\
963
+ .get('OddsMarkers',[])
964
+ if len(self._oddsMarks) > 0:
965
+ v(f'We have odds markers')
966
+
967
+ self._resultMarks = self._categories\
968
+ .get('counter',{})\
969
+ .get('ResultMarkers',[])
970
+ if len(self._resultMarks) > 0:
971
+ v(f'We have result markers')
972
+
973
+ self.addNotes()
974
+ v(f'Adding turn track')
975
+ turns = self._game.addTurnTrack(name='Turn',
976
+ counter={
977
+ 'property': 'Turn',
978
+ 'phases': {
979
+ 'property': 'Phase',
980
+ 'names': self._sides } })
981
+ turns.addHotkey(hotkey = self._clearMoved+'Phase',
982
+ name = 'Clear moved markers',
983
+ reportFormat = (f'{{{self._debug}?('
984
+ f'"`Clear all moved markers, "+'
985
+ f'""):""}}'))
986
+ if len(self._battleMarks) > 0:
987
+ turns.addHotkey(
988
+ hotkey = self._clearBattlePhs,
989
+ name = 'Clear battle markers',
990
+ reportFormat = (f'{{{self._debug}?('
991
+ f'"`Clear all battle markers, "+'
992
+ f'""):""}}'))
993
+
994
+ self._dice = self._categories\
995
+ .get('die-roll',{})
996
+ if len(self._dice) > 0:
997
+ v(f'We have symbolic dice')
998
+ self._diceInit = []
999
+ # from pprint import pprint
1000
+ # pprint(self._dice,depth=2)
1001
+ for die, faces in self._dice.items():
1002
+ ico = self.getIcon(die+'-die-icon','')
1003
+ # print(f'Die {die} icon="{ico}"')
1004
+
1005
+ dmin = +100000
1006
+ dmax = -100000
1007
+ symb = self._game.addSymbolicDice(
1008
+ name = die+'Dice',
1009
+ text = die if ico == '' else '',
1010
+ icon = ico,
1011
+ tooltip = f'{die} die roll',
1012
+ format = (f'{{"<b>"+PlayerSide+"</b> "+'
1013
+ f'"(<i>"+PlayerName+"</i>): "+'+
1014
+ f'"{die} die roll: "+result1'
1015
+ # f'+" <img src=\'{die}-"+result1'
1016
+ # f'+".png\' width=24 height=24>"'
1017
+ f'}}'),
1018
+ resultWindow = True,
1019
+ windowX = str(int(67 * self._resolution/150)),
1020
+ windowY = str(int(65 * self._resolution/150)));
1021
+ sdie = symb.addDie(name = die);
1022
+ w = 0
1023
+ h = 0
1024
+ for face, fdata in faces.items():
1025
+ fn = fdata['filename']
1026
+ img = fdata['img']
1027
+ iw,ih = self.getWH(img)
1028
+ w = max(w,iw)
1029
+ h = max(h,ih)
1030
+ val = sum([int(s) for s in
1031
+ fn.replace(f'.{self._img_format}','')
1032
+ .replace(die+'-','').split('-')])
1033
+ dmin = min(dmin,val)
1034
+ dmax = max(dmax,val)
1035
+ sdie.addFace(icon = fn,
1036
+ text = str(val),
1037
+ value = val);
1038
+ symb['windowX'] = w # self._resolution/150
1039
+ symb['windowY'] = h # self._resolution/150
1040
+
1041
+ self._diceInit.extend([
1042
+ GlobalPropertyTrait(
1043
+ ['',self._diceInitKey,
1044
+ GlobalPropertyTrait.DIRECT,
1045
+ f'{{{dmin}}}'],
1046
+ name = die+'Dice_result',
1047
+ numeric = True,
1048
+ min = dmin,
1049
+ max = dmax,
1050
+ description = f'Initialize {die}Dice'),
1051
+ ReportTrait(
1052
+ self._diceInitKey,
1053
+ report=(f'{{{self._debug}?("~Initialize '
1054
+ f'{die}Dice_result to {dmin}"):""}}'))
1055
+ ])
1056
+
1057
+
1058
+ # Add start-up key
1059
+ self._game.addStartupMassKey(
1060
+ name = 'Initialise dice results',
1061
+ hotkey = self._diceInitKey,
1062
+ target = '',
1063
+ filter = f'{{BasicName=="{self._hiddenName}"}}',
1064
+ whenToApply = StartupMassKey.EVERY_LAUNCH,
1065
+ reportFormat=f'{{{self._debug}?("`Init Dice"):""}}')
1066
+
1067
+
1068
+
1069
+
1070
+
1071
+
1072
+ self.addKeybindings(doc)
1073
+ self.addCounters()
1074
+ self.addInventory()
1075
+ self.addBoards()
1076
+ self.addDeadMap()
1077
+ self.addOOBs()
1078
+ self.addCharts()
1079
+ self.addDie()
1080
+
1081
+ # ----------------------------------------------------------------
1082
+ def addDocumentation(self):
1083
+ '''Add documentation to the module. This includes rules,
1084
+ key-bindings, and about elements.
1085
+ '''
1086
+ with VerboseGuard('Adding documentation') as v:
1087
+ doc = self._game.addDocumentation()
1088
+ if self._rules is not None:
1089
+ self._vmod.addExternalFile(self._rules,'rules.pdf')
1090
+ doc.addBrowserPDFFile(title = 'Show rules',
1091
+ pdfFile = 'rules.pdf')
1092
+
1093
+ if self._tutorial is not None:
1094
+ self._vmod.addExternalFile(self._tutorial,'tutorial.vlog')
1095
+ doc.addTutorial(name = 'Tutorial',
1096
+ logfile = 'tutorial.vlog',
1097
+ launchOnStartup = True)
1098
+
1099
+
1100
+ fronts = self._categories.get('front',{}).get('all',[])
1101
+ front = list(fronts.values())[0] if len(fronts) > 0 else None
1102
+ if front is not None:
1103
+ v(f'Adding about page')
1104
+ doc.addAboutScreen(title=f'About {self._title}',
1105
+ fileName = front['filename'])
1106
+
1107
+ return doc
1108
+
1109
+ # ----------------------------------------------------------------
1110
+ def addKeybindings(self,doc):
1111
+ keys = [
1112
+ ['Alt-A', '-', 'Show the charts panel'],
1113
+ ['Alt-B', '-', 'Show the OOBs'],
1114
+ ['Alt-C', '-', 'Show the counters panel'],
1115
+ ['Alt-E', '-', 'Show the eliminated units'],
1116
+ ['Alt-I', '-', 'Show/refresh inventory window'],
1117
+ ['Alt-M', '-', 'Show map'],
1118
+ ['Alt-T', '-', 'Increase turn track'],
1119
+ ['Alt-S', '-', 'Toggle movement trails'],
1120
+ ['Alt-Shift-T', '-', 'Decrease turn track'],
1121
+ ['Alt-6', '-', 'Roll the dice'],
1122
+ ['Ctrl-D', 'Board,Counter','Delete counters'],
1123
+ ['Ctrl-E', 'Board,Counter','Eliminate counters'],
1124
+ ['Ctrl-F', 'Board,Counter','Flip counters'],
1125
+ ['Ctrl-M', 'Board,Counter','Toggle "moved" markers'],
1126
+ ['Ctrl-O', 'Board', 'Hide/show counters'],
1127
+ ['Ctrl-R', 'Board,Counter','Restore unit'],
1128
+ ['Ctrl-T', 'Board,Counter','Toggle move trail'],
1129
+ ['Ctrl-Z', 'Board', 'Undo last move'],
1130
+ ['Ctrl-+', 'Board', 'Zoom in'],
1131
+ ['Ctrl--', 'Board', 'Zoom out'],
1132
+ ['Ctrl-=', 'Board', 'Select zoom'],
1133
+ ['Ctrl-Shift-O', 'Board','Show overview map'],
1134
+ ['&larr;,&rarr;,&uarr;&darr;','Board',
1135
+ 'Scroll board left, right, up, down (slowly)'],
1136
+ ['PnUp,PnDn','Board', 'Scroll board up/down (fast)'],
1137
+ ['Ctrl-PnUp,Ctrl-PnDn','Board', 'Scroll board left/right (fast)'],
1138
+ ['Mouse-scroll up/down', 'Board', 'Scroll board up//down'],
1139
+ ['Shift-Mouse-scroll up/down','Board','Scroll board right/leftown'],
1140
+ ['Ctrl-Mouse-scroll up/down','Board','Zoom board out/in'],
1141
+ ['Mouse-2', 'Board', 'Centre on mouse']]
1142
+ if self._battleMarks:
1143
+ for a,l in zip(['Ctrl-D','Ctrl-Shift-O', 'Ctrl-+', 'Ctrl-+'],
1144
+ [['Ctrl-C', 'Counter', 'Clear battle'],
1145
+ ['Ctrl-Shift-C','Board', 'Clear all battle'],
1146
+ ['Ctrl-X', 'Board,Counter','Mark battle'],
1147
+ ['Ctrl-Shift-X','Board,Counter','Recalculate Odds'],
1148
+ ['Ctrl-Y', 'Board,Counter','Resolve battle'],
1149
+ ]):
1150
+ ks = [k[0] for k in keys]
1151
+ didx = ks.index(a)
1152
+ keys.insert(didx,l)
1153
+
1154
+ self._vmod.addFile('help/keys.html',
1155
+ Documentation.createKeyHelp(
1156
+ keys,
1157
+ title=self._title,
1158
+ version=self._version))
1159
+ doc.addHelpFile(title='Key bindings',fileName='help/keys.html')
1160
+
1161
+ # ----------------------------------------------------------------
1162
+ def addNatoPrototypes(self,prototypes):
1163
+ # Add unit categories as prototypes
1164
+ for n,c in zip(['Type','Echelon','Command'],
1165
+ [self._mains, self._echelons, self._commands]):
1166
+ sc = set([cc.strip() for cc in c])
1167
+ with VerboseGuard(f'Adding prototypes for "{n}"') as vv:
1168
+ for i,cc in enumerate(sc):
1169
+ cc = cc.strip()
1170
+ if len(cc) <= 0: continue
1171
+ vv(f'[{cc}] ',end='',flush=True,noindent=True)
1172
+ p = prototypes.addPrototype(name = f'{cc} prototype',
1173
+ description = '',
1174
+ traits = [MarkTrait(n,cc),
1175
+ BasicTrait()])
1176
+ vv('')
1177
+
1178
+ # ----------------------------------------------------------------
1179
+ def addBattleControlPrototype(self,prototypes):
1180
+ # Control of battles.
1181
+ #
1182
+ # This has traits to
1183
+ #
1184
+ # - Zero battle counter
1185
+ # - Increment battle counter
1186
+ # - Set current battle number
1187
+ # - Mark battle
1188
+ # - Calculate odds
1189
+ #
1190
+ # When wgMarkBattle is issued to this piece, then
1191
+ #
1192
+ # - Increment battle counter
1193
+ # - Set global current battle
1194
+ # - Trampoline to GCK markBattle
1195
+ # - For all selected pieces, issue markBattle
1196
+ # - All wgBattleUnit pieces then
1197
+ # - Get current battle # and store
1198
+ # - Add marker on top of it self
1199
+ # - Issue calculateOddsAuto
1200
+ # - If auto odds on, go to calcOddsStart,
1201
+ # - Trampoline to GCK calcOddsAuto
1202
+ # - Which sends calcOddsStart to all markers
1203
+ # - For each mark
1204
+ # - Set current battle to current global
1205
+ # - Trampoline calcOdds via GKC
1206
+ # - Send calcBattleOdds to wgBattleCalc
1207
+ # - Zero odds
1208
+ # - Calculate fraction
1209
+ # - Zero fraction
1210
+ # - Calculate total AF
1211
+ # - Zero AF
1212
+ # - via trampoline to GKC
1213
+ # - Calculate total DF
1214
+ # - Zero DF
1215
+ # - via trampoline to GKC
1216
+ # - Real fraction calculation
1217
+ # - From calculate fraction
1218
+ # - Access via calculate trait
1219
+ # - Calculate shift
1220
+ # - Zero shift
1221
+ # - Trampoline to GKC
1222
+ # - Access via calculate trait
1223
+ # - Calculate index
1224
+ # - Via calculated OddsIndex
1225
+ # - Calculate odds real
1226
+ # - Via calculated Index to odds
1227
+ # - Calculate DRM
1228
+ # - Zero DRM
1229
+ # - Trampoline to GKC
1230
+ # - Access via calculate trait
1231
+ # - Do markOddsAuto which selects between odds
1232
+ # - Do markOddsReal+OddsIndex
1233
+ # - Set global battle #
1234
+ # - Place marker
1235
+ # - Take global battle #
1236
+ # - De-select all other marks to prevent
1237
+ # further propagation
1238
+ #
1239
+ if len(self._battleMarks) <= 0:
1240
+ return False
1241
+
1242
+ n = len(self._battleMarks)
1243
+ # --- Battle counter control - reset and increment -----------
1244
+ traits = [
1245
+ GlobalPropertyTrait(
1246
+ ['',self._zeroBattle,GlobalPropertyTrait.DIRECT,'{0}'],
1247
+ ['',self._incrBattle,GlobalPropertyTrait.DIRECT,
1248
+ f'{{({self._battleCounter}%{n})+1}}'],
1249
+ name = self._battleCounter,
1250
+ numeric = True,
1251
+ min = 0,
1252
+ max = n,
1253
+ wrap = True,
1254
+ description = 'Zero battle counter',
1255
+ ),
1256
+ # Set global property combat # from this
1257
+ GlobalPropertyTrait(
1258
+ ['',self._setBattle,GlobalPropertyTrait.DIRECT,
1259
+ f'{{{self._battleCounter}}}'],
1260
+ name = self._currentBattle,
1261
+ numeric = True,
1262
+ min = 0,
1263
+ max = n,
1264
+ wrap = True,
1265
+ description = 'Zero battle counter',
1266
+ ),
1267
+ ReportTrait(self._zeroBattle,
1268
+ report=(f'{{{self._debug}?'
1269
+ f'("~ "+BasicName+": zero battle counter: "'
1270
+ f'+{self._battleCounter}):""}}')),
1271
+ ReportTrait(self._incrBattle,
1272
+ report=(f'{{{self._debug}?'
1273
+ f'("~ "+BasicName+": '
1274
+ f'increment battle counter: "'
1275
+ f'+{self._battleCounter}):""}}')),
1276
+ ReportTrait(self._setBattle,
1277
+ report=(f'{{{self._debug}?'
1278
+ f'("~ "+BasicName+": set current battle: "+'
1279
+ f'{self._battleCounter}+" -> "+'
1280
+ f'{self._currentBattle}):""}}')),
1281
+ # Set global property combat # from this
1282
+ GlobalPropertyTrait(
1283
+ ['',self._markBattle+'ResetPlaced',GlobalPropertyTrait.DIRECT,
1284
+ f'{{false}}'],
1285
+ name = self._placedGlobal,
1286
+ numeric = True,
1287
+ description = 'Reset the placed marker flag',
1288
+ ),
1289
+ GlobalPropertyTrait(
1290
+ ['',self._markBattle+'ResetPlaced',GlobalPropertyTrait.DIRECT,
1291
+ f'{{true}}'],
1292
+ ['',self._calcBattleOdds+'Start',GlobalPropertyTrait.DIRECT,
1293
+ f'{{false}}'],
1294
+ name = self._markStart,
1295
+ numeric = True,
1296
+ description = 'Reset the placed marker flag',
1297
+ ),
1298
+ ReportTrait(self._markBattle+'ResetPlaced',
1299
+ report = (f'{{{self._debug}?("~"+BasicName+'
1300
+ f'" reset placed "+'
1301
+ f'{self._placedGlobal}+" markStart="+'
1302
+ f'{self._markStart}):""}}')),
1303
+ GlobalHotkeyTrait(name = '',
1304
+ key = self._markBattle+'Trampoline',
1305
+ globalHotkey = self._markBattle,
1306
+ description = 'Mark selected for battle'),
1307
+ ReportTrait(self._markBattle+'Trampoline',
1308
+ report=(f'{{{self._debug}?'
1309
+ f'("~ "+BasicName+": forward mark battle: "+'
1310
+ f'{self._battleCounter}):""}}')),
1311
+ GlobalHotkeyTrait(name = '',
1312
+ key = self._calcBattleOdds+'Start',
1313
+ globalHotkey = self._calcBattleOdds+'Auto',
1314
+ description = 'Trampoline to global'),
1315
+ ReportTrait(self._calcBattleOdds+'Start',
1316
+ report=(f'{{{self._debug}?'
1317
+ f'("~ "+BasicName+": start forward odds: "+'
1318
+ f'{self._battleCounter}+" markStart="+'
1319
+ f'{self._markStart}):""}}')),
1320
+ DeselectTrait(command = '',
1321
+ key = self._calcBattleOdds+'Deselect',
1322
+ deselect = DeselectTrait.ALL),
1323
+ ReportTrait(self._calcBattleOdds+'Deselect',
1324
+ report=(f'{{{self._debug}?'
1325
+ f'("~ "+BasicName+": select only this: "+'
1326
+ f'{self._battleCounter}):""}}')),
1327
+ TriggerTrait(command = '',
1328
+ key = self._calcBattleOdds+'Auto',
1329
+ actionKeys = [self._calcBattleOdds+'Start'],
1330
+ property = f'{{{self._autoOdds}==true}}'),
1331
+ ReportTrait(self._calcBattleOdds+'Auto',
1332
+ report=(f'{{{self._debug}?'
1333
+ f'("~ "+BasicName+": auto forward odds: "+'
1334
+ f'{self._battleCounter}):""}}')),
1335
+ TriggerTrait(command = '',
1336
+ key = self._markBattle,
1337
+ actionKeys = [self._incrBattle,
1338
+ self._setBattle,
1339
+ self._markBattle+'ResetPlaced',
1340
+ self._markBattle+'Trampoline',
1341
+ self._calcBattleOdds+'Auto']),
1342
+ ReportTrait(self._markBattle,
1343
+ report=(f'{{{self._debug}?'
1344
+ f'("~ "+BasicName+": mark battle: "+'
1345
+ f'{self._battleCounter}):""}}')),
1346
+ GlobalHotkeyTrait(name = '',
1347
+ key = self._clearAllBattle+'Trampoline',
1348
+ globalHotkey = self._clearAllBattle,
1349
+ description = 'Clear all battles'),
1350
+ TriggerTrait(command = '',
1351
+ key = self._clearAllBattle,
1352
+ actionKeys = [self._clearAllBattle+'Trampoline',
1353
+ self._zeroBattle]),
1354
+ ReportTrait(self._clearBattle,
1355
+ report=(f'{{{self._debug}?'
1356
+ f'("~ "+BasicName+": clear battle: "+'
1357
+ f'{self._battleCounter}):""}}')),
1358
+ GlobalHotkeyTrait(name = '',
1359
+ key = self._clearMoved+'Trampoline',
1360
+ globalHotkey = self._clearMoved,
1361
+ description = 'Clear moved markers'),
1362
+ MarkTrait(name=self._battleCtrl,value=True),
1363
+ BasicTrait()]
1364
+ prototypes.addPrototype(name = self._battleCtrl,
1365
+ description = '',
1366
+ traits = traits)
1367
+ return True
1368
+
1369
+ # ----------------------------------------------------------------
1370
+ def addBattleCalculatePrototype(self,prototypes):
1371
+ # --- Batttle AF, DF, Odds -----------------------------------
1372
+ # This calculate odds derivation from stated odds.
1373
+ with VerboseGuard(f'Making battle calculation prototype') as v:
1374
+ calcIdx = 0
1375
+ maxIdx = len(self._oddsMarks)+1
1376
+ minIdx = 0
1377
+ idx2Odds = '""'
1378
+ calcFrac = 1
1379
+ if len(self._oddsMarks) > 0:
1380
+ odds = [o.replace('odds marker','').strip() for
1381
+ o in self._oddsMarks]
1382
+ ratios = all([o == '0' or ':' in o for o in odds])
1383
+
1384
+ if ratios: # All is ratios!
1385
+ def calc(s):
1386
+ if s == '0': return 0
1387
+ num, den = [float(x.strip()) for x in s.split(':')]
1388
+ return num/den
1389
+ ratios = [[calc(s),s] for s in odds]
1390
+ ind = [i[0] for i in sorted(enumerate(ratios),
1391
+ key=lambda x:x[1][0])]
1392
+ #print(f'=== Rations: {ratios}, Index: {ind[::-1]}')
1393
+ calcIdx = ':'.join([f'{self._battleFrac}>={ratios[i][0]}?'
1394
+ f'({i+1})'
1395
+ for i in ind[::-1]]) + ':0'
1396
+ idx2Odds = ':'.join([f'OddsIndex=={i+1}?'
1397
+ f'"{ratios[i][1]}"'
1398
+ for i in ind[::-1]]) + ':""'
1399
+ calcFrac = (f'{{{self._battleDF}==0?0:'
1400
+ f'(((double)({self._battleAF}))'
1401
+ fr'\/{self._battleDF})}}')
1402
+ v(f'Calculate index: {calcIdx}')
1403
+ v(f'Index to odds: {idx2Odds}')
1404
+ else:
1405
+ try:
1406
+ nums = [[int(o),o] for o in odds]
1407
+ calcFrac = f'{{{self._battleAF}-{self._battleDF}}}'
1408
+ ind = [i[0] for i in sorted(enumerate(nums),
1409
+ key=lambda x:x[1])]
1410
+ calcIdx = ':'.join([f'{self._battleFrac}>={nums[i][0]}?'
1411
+ f'({i+1})'
1412
+ for i in ind[::-1]])+':0'
1413
+ idx2Odds = ':'.join([f'OddsIndex=={i+1}?"{nums[i][1]}"'
1414
+ for i in ind[::-1]]) + ':""'
1415
+ vidx2Odds = '\t'+idx2Odds.replace(':',':\n\t')
1416
+ #print(f'Index to odds: {vidx2Odds}')
1417
+ except:
1418
+ pass
1419
+
1420
+ traits = [
1421
+ CalculatedTrait(# This should be changed to game rules
1422
+ name = 'OddsShift',
1423
+ expression = f'{{{self._battleShift}}}',
1424
+ description = 'Calculated internal oddsshift'),
1425
+ CalculatedTrait(# This should be changed to game rules
1426
+ name = 'DRM',
1427
+ expression = f'{{{self._battleDRM}}}',
1428
+ description = 'Calculated internal oddsshift'),
1429
+ CalculatedTrait(# This should be changed to game rules
1430
+ name = 'OddsIndexRaw',
1431
+ expression = f'{{{calcIdx}}}',
1432
+ description = 'Calculated internal odds index'),
1433
+ CalculatedTrait(# This should be changed to game rules
1434
+ name = 'OddsIndexLimited',
1435
+ expression = (f'{{OddsIndexRaw>{maxIdx}?{maxIdx}:'
1436
+ f'OddsIndexRaw<{minIdx}?{minIdx}:'
1437
+ f'OddsIndexRaw}}'),
1438
+ description = 'Calculated internal limited odds index'),
1439
+ CalculatedTrait(# This should be changed to game rules
1440
+ name = 'OddsIndex',
1441
+ expression = (f'{{OddsIndexLimited+OddsShift}}'),
1442
+ description = 'Calculated internal odds index (with shift)'),
1443
+ CalculatedTrait(# This should be changed to game rules
1444
+ name = 'BattleFraction',
1445
+ expression = calcFrac,
1446
+ description = 'Calculated fraction off battle'),
1447
+ GlobalPropertyTrait(
1448
+ ['',self._zeroBattleShft,GlobalPropertyTrait.DIRECT,'{0}'],
1449
+ name = self._battleShift,
1450
+ numeric = True,
1451
+ description = 'Zero battle odds shift',
1452
+ ),
1453
+ GlobalPropertyTrait(
1454
+ ['',self._zeroBattleDRM,GlobalPropertyTrait.DIRECT,'{0}'],
1455
+ name = self._battleDRM,
1456
+ numeric = True,
1457
+ description = 'Zero battle die roll modifier',
1458
+ ),
1459
+ GlobalPropertyTrait(
1460
+ ['',self._zeroBattleAF,GlobalPropertyTrait.DIRECT,'{0}'],
1461
+ name = self._battleAF,
1462
+ numeric = True,
1463
+ description = 'Zero battle AF',
1464
+ ),
1465
+ GlobalPropertyTrait(
1466
+ ['',self._zeroBattleDF,GlobalPropertyTrait.DIRECT,'{0}'],
1467
+ name = self._battleDF,
1468
+ numeric = True,
1469
+ description = 'Zero battle AF',
1470
+ ),
1471
+ # {wgBattleDF==0?0:(double(wgBattleAF)/wgBattleDF)}
1472
+ GlobalPropertyTrait(
1473
+ ['',self._zeroBattleFrac,GlobalPropertyTrait.DIRECT,'{0}'],
1474
+ ['',self._calcBattleFrac+'Real',GlobalPropertyTrait.DIRECT,
1475
+ '{BattleFraction}'],
1476
+ name = self._battleFrac,
1477
+ description = 'Calculate battle fraction',
1478
+ ),
1479
+ GlobalPropertyTrait(
1480
+ ['',self._zeroBattleIdx,GlobalPropertyTrait.DIRECT,'{0}'],
1481
+ ['',self._calcBattleIdx,GlobalPropertyTrait.DIRECT,
1482
+ '{OddsIndex}'],
1483
+ name = self._battleIdx,
1484
+ description = 'Calculate battle odds index',
1485
+ ),
1486
+ GlobalPropertyTrait(
1487
+ ['',self._zeroBattleOdds,GlobalPropertyTrait.DIRECT,'{""}'],
1488
+ ['',self._calcBattleOdds+'Real',GlobalPropertyTrait.DIRECT,
1489
+ f'{{{idx2Odds}}}'],
1490
+ name = self._battleOdds,
1491
+ description = 'Calculate battle odds',
1492
+ ),
1493
+ GlobalHotkeyTrait(
1494
+ name = '',# Forward to units
1495
+ key = self._calcBattleAF+'Trampoline',
1496
+ globalHotkey = self._calcBattleAF,
1497
+ description = 'Calculate total AF'),
1498
+ GlobalHotkeyTrait(
1499
+ name = '',# Forward to units
1500
+ key = self._calcBattleDF+'Trampoline',
1501
+ globalHotkey = self._calcBattleDF,
1502
+ description = 'Calculate total DF'),
1503
+ GlobalHotkeyTrait(
1504
+ name = '',# Forward to units
1505
+ key = self._calcBattleShft+'Trampoline',
1506
+ globalHotkey = self._calcBattleShft,
1507
+ description = 'Calculate total shift'),
1508
+ GlobalHotkeyTrait(
1509
+ name = '',# Forward to units
1510
+ key = self._calcBattleDRM+'Trampoline',
1511
+ globalHotkey = self._calcBattleDRM,
1512
+ description = 'Calculate total DRM'),
1513
+ TriggerTrait(
1514
+ command = '',
1515
+ key = self._calcBattleAF,
1516
+ actionKeys = [self._zeroBattleAF,
1517
+ self._calcBattleAF+'Trampoline']),
1518
+ TriggerTrait(
1519
+ command = '',
1520
+ key = self._calcBattleDF,
1521
+ actionKeys = [self._zeroBattleDF,
1522
+ self._calcBattleDF+'Trampoline']),
1523
+ TriggerTrait(
1524
+ command = '',
1525
+ key = self._calcBattleShft,
1526
+ actionKeys = [self._zeroBattleShft,
1527
+ self._calcBattleShft+'Trampoline']),
1528
+ TriggerTrait(
1529
+ command = '',
1530
+ key = self._calcBattleDRM,
1531
+ actionKeys = [self._calcBattleDRM+'Trampoline']),
1532
+ TriggerTrait(
1533
+ command = '',
1534
+ key = self._calcBattleFrac,
1535
+ actionKeys = [self._zeroBattleFrac,
1536
+ self._zeroBattleDRM,
1537
+ self._calcBattleAF,
1538
+ self._calcBattleDF,
1539
+ self._calcBattleFrac+'Real']),
1540
+ # Entry point for calculations
1541
+ TriggerTrait(
1542
+ command = '',
1543
+ key = self._calcBattleOdds,
1544
+ actionKeys = [self._zeroBattleOdds,
1545
+ self._calcBattleFrac,
1546
+ self._calcBattleShft,
1547
+ self._calcBattleIdx,
1548
+ self._calcBattleDRM,
1549
+ self._calcBattleOdds+'Real']),
1550
+ ReportTrait(
1551
+ self._zeroBattleAF,
1552
+ report=(f'{{{self._debug}?'
1553
+ f'("~"+BasicName+" @ "+LocationName+'
1554
+ f'": Reset AF: "+'
1555
+ f'{self._battleAF}):""}}')),
1556
+ ReportTrait(
1557
+ self._zeroBattleDF,
1558
+ report=(f'{{{self._debug}?'
1559
+ f'("~"+BasicName+" @ "+LocationName+'
1560
+ f'": Reset DF: "+'
1561
+ f'{self._battleDF}):""}}')),
1562
+ ReportTrait(
1563
+ self._zeroBattleFrac,
1564
+ report=(f'{{{self._debug}?'
1565
+ f'("~"+BasicName+" @ "+LocationName+'
1566
+ f'": Reset fraction: "+'
1567
+ f'{self._battleFrac}):""}}')),
1568
+ ReportTrait(
1569
+ self._zeroBattleOdds,
1570
+ report=(f'{{{self._debug}?'
1571
+ f'("~"+BasicName+" @ "+LocationName+'
1572
+ f'": Reset odds: "+'
1573
+ f'{self._battleOdds}):""}}')),
1574
+ ReportTrait(
1575
+ self._zeroBattleShft,
1576
+ report=(f'{{{self._debug}?'
1577
+ f'("~"+BasicName+" @ "+LocationName+'
1578
+ f'": Reset Shift: "+'
1579
+ f'{self._battleShift}):""}}')),
1580
+ ReportTrait(
1581
+ self._zeroBattleDRM,
1582
+ report=(f'{{{self._debug}?'
1583
+ f'("~"+BasicName+" @ "+LocationName+'
1584
+ f'": Reset DRM: "+'
1585
+ f'{self._battleDRM}):""}}')),
1586
+ ReportTrait(
1587
+ self._calcBattleAF,
1588
+ report=(f'{{{self._debug}?'
1589
+ f'("~"+BasicName+" @ "+LocationName+'
1590
+ f'": Total AF: "+'
1591
+ f'{self._battleAF}):""}}')),
1592
+ ReportTrait(
1593
+ self._calcBattleDF,
1594
+ report=(f'{{{self._debug}?'
1595
+ f'("~"+BasicName+" @ "+LocationName+'
1596
+ f'": Total DF: "+'
1597
+ f'{self._battleDF}):""}}')),
1598
+ ReportTrait(
1599
+ self._calcBattleShft,
1600
+ report=(f'{{{self._debug}?'
1601
+ f'("~"+BasicName+" @ "+LocationName+'
1602
+ f'": Battle odds shift: "+'
1603
+ f'{self._battleShift}):""}}')),
1604
+ ReportTrait(
1605
+ self._calcBattleDRM,
1606
+ report=(f'{{{self._debug}?'
1607
+ f'("~"+BasicName+" @ "+LocationName+'
1608
+ f'": Battle DRM: "+'
1609
+ f'{self._battleDRM}):""}}')),
1610
+ ReportTrait(
1611
+ self._calcBattleFrac,
1612
+ report=(f'{{{self._debug}?'
1613
+ f'("~"+BasicName+" @ "+LocationName+'
1614
+ f'": Battle fraction: "+'
1615
+ f'{self._battleFrac}):""}}')),
1616
+ ReportTrait(
1617
+ self._calcBattleOdds,
1618
+ report=(f'{{{self._debug}?'
1619
+ f'("~"+BasicName+" @ "+LocationName+'
1620
+ f'": Battle odds: "+'
1621
+ f'{self._battleOdds}+" ("+'
1622
+ f'{self._battleIdx}+")"):""}}')),
1623
+ ReportTrait(
1624
+ self._calcBattleFrac+'Real',
1625
+ report=(f'{{{self._debug}?'
1626
+ f'("~"+BasicName+" @ "+LocationName+'
1627
+ f'": Battle fraction: "+'
1628
+ f'{self._battleFrac}+'
1629
+ f'" AF="+{self._battleAF}+'
1630
+ f'" DF="+{self._battleDF}'
1631
+ f'):""}}')),
1632
+ ReportTrait(
1633
+ self._calcBattleOdds+'Real',
1634
+ report=(f'{{{self._debug}?'
1635
+ f'("~"+BasicName+" @ "+LocationName+'
1636
+ f'": Battle odds: "+'
1637
+ f'{self._battleOdds}+'
1638
+ f'" ("+{self._battleIdx}+","+OddsShift+","+'
1639
+ f'" raw="+OddsIndexRaw+","+'
1640
+ f'" limited="+OddsIndexLimited+","+'
1641
+ f'" -> "+OddsIndex+","+'
1642
+ f'{self._battleShift}+")"+'
1643
+ f'" DRM="+{self._battleDRM}+'
1644
+ f'" Fraction="+{self._battleFrac}+'
1645
+ f'" AF="+{self._battleAF}+'
1646
+ f'" DF="+{self._battleDF}'
1647
+ f'):""}}')),
1648
+ ReportTrait(
1649
+ self._calcBattleOdds+'Real',
1650
+ report=(f'{{{self._verbose}?'
1651
+ f'("! Battle # "'
1652
+ f'+{self._battleNo}'
1653
+ f'+{self._currentBattle}'
1654
+ f'+" AF="+{self._battleAF}'
1655
+ f'+" DF="+{self._battleDF}'
1656
+ f'+" => "+{self._battleOdds}'
1657
+ f'+" DRM="+{self._battleDRM}'
1658
+ # f'+" <img src=\'odds_marker_"'
1659
+ # f'+{self._battleOdds}+".png\' "'
1660
+ # f'+" width=24 height=24>"'
1661
+ f'):""}}')),
1662
+ MarkTrait(name=self._battleCalc,value=True),
1663
+ BasicTrait()]
1664
+ prototypes.addPrototype(name = self._battleCalc,
1665
+ description = '',
1666
+ traits = traits)
1667
+
1668
+ # ----------------------------------------------------------------
1669
+ def addBattleUnitPrototype(self,prototypes):
1670
+ # --- Battle units that set battle markers -------------------
1671
+ #
1672
+ # - Trait to add battle number 1 to max
1673
+ #
1674
+ # - Trigger trait for each of these using the global property
1675
+ # for the current battle
1676
+ #
1677
+ traits = [
1678
+ # getBattle retrieves the battle number from the global property.
1679
+ # clearBattle sets piece battle to -1
1680
+ DynamicPropertyTrait(['',self._getBattle,
1681
+ DynamicPropertyTrait.DIRECT,
1682
+ f'{{{self._currentBattle}}}'],
1683
+ ['',self._clearBattle,
1684
+ DynamicPropertyTrait.DIRECT,
1685
+ f'{{-1}}'],
1686
+ name = self._battleNo,
1687
+ numeric = True,
1688
+ value = f'{{-1}}',
1689
+ description = 'Set battle number'),
1690
+ # This setBattle sets current attacker in global property
1691
+ GlobalPropertyTrait(['',self._setBattle,
1692
+ GlobalPropertyTrait.DIRECT,
1693
+ '{IsAttacker}'],
1694
+ name = self._currentAttacker,
1695
+ numeric = True,
1696
+ description = 'Set attacker'),
1697
+ ReportTrait(self._getBattle,
1698
+ report=(f'{{{self._debug}?'
1699
+ f'("~ "+BasicName+" current battle # "+'
1700
+ f'{self._currentBattle}+" -> "+'
1701
+ f'{self._battleNo}):""}}')),
1702
+ ReportTrait(self._clearBattle,
1703
+ report=(f'{{{self._debug}?'
1704
+ f'("~ "+BasicName+" Clear this global="+'
1705
+ f'{self._currentBattle}+" this="+'
1706
+ f'{self._battleNo}):""}}')),
1707
+ ]
1708
+ place = []
1709
+ trig = []
1710
+ rept = []
1711
+ for i, bm in enumerate(self._battleMarks):
1712
+ kn = self._markBattle+str(i+1)
1713
+ skel = PlaceTrait.SKEL_PATH()
1714
+ path = skel.format('BattleMarkers',bm)
1715
+
1716
+ place.append(
1717
+ PlaceTrait(command = '',#f'Add battle marker {i}',
1718
+ key = kn,
1719
+ markerSpec = path,
1720
+ markerText = 'null',
1721
+ xOffset = -8,
1722
+ yOffset = -16,
1723
+ matchRotation = False,
1724
+ afterKey = self._getBattle,
1725
+ gpid = self._game.nextPieceSlotId(),
1726
+ description = f'Add battle marker {i+1}',
1727
+ placement = PlaceTrait.ABOVE,
1728
+ above = False))
1729
+ # Get current global battle number
1730
+ # Set current battle
1731
+ # Filtered on current global battle # is equal to
1732
+ trig.append(
1733
+ TriggerTrait(command = '',#Mark battle',
1734
+ key = self._markBattle,
1735
+ actionKeys = [self._getBattle,
1736
+ self._setBattle,
1737
+ kn],
1738
+ property = f'{{{self._currentBattle}=={i+1}}}'))
1739
+ rept.append(
1740
+ ReportTrait(kn,
1741
+ report=(f'{{{self._debug}?'
1742
+ f'("~ "+BasicName+" placing marker ({i+1})'
1743
+ f' ="+ {self._currentBattle}+"'
1744
+ f'={kn}"):""}}')))
1745
+
1746
+ oth = [
1747
+ TriggerTrait(name = 'Declare combat',
1748
+ command = 'Declare battle',
1749
+ key = self._markKey,
1750
+ actionKeys = [self._markBattle+'Unit'],
1751
+ property = f'{{{self._battleNo}<=0}}'
1752
+ ),
1753
+ GlobalHotkeyTrait(name = '',#'Declare battle',
1754
+ key = self._markBattle+'Unit',
1755
+ globalHotkey = self._markBattle+'Unit',
1756
+ description = 'Mark for combat'),
1757
+ GlobalPropertyTrait(
1758
+ ['',self._calcBattleAF,GlobalPropertyTrait.DIRECT,
1759
+ f'{{EffectiveAF+{self._battleAF}}}'],
1760
+ name = self._battleAF,
1761
+ numeric = True,
1762
+ description = 'Update battle AF'),
1763
+ GlobalPropertyTrait(
1764
+ ['',self._calcBattleDF,GlobalPropertyTrait.DIRECT,
1765
+ f'{{EffectiveDF+{self._battleDF}}}'],
1766
+ name = self._battleDF,
1767
+ numeric = True,
1768
+ description = 'Update battle DF'),
1769
+ GlobalPropertyTrait(
1770
+ ['',self._calcBattleShft,GlobalPropertyTrait.DIRECT,
1771
+ f'{{OddsShift}}'],
1772
+ name = self._battleShift,
1773
+ numeric = True,
1774
+ description = 'Update battle shift',
1775
+ ),
1776
+ GlobalPropertyTrait(
1777
+ ['',self._calcBattleDRM,GlobalPropertyTrait.DIRECT,
1778
+ f'{{DRM+{self._battleDRM}}}'],
1779
+ name = self._battleDRM,
1780
+ numeric = True,
1781
+ description = 'Update battle die roll modifier',
1782
+ ),
1783
+ CalculatedTrait(#This could be redefined in module
1784
+ name = 'EffectiveAF',
1785
+ expression = '{CF}',
1786
+ description = 'Current attack factor'),
1787
+ CalculatedTrait(#This could be redefined in module
1788
+ name = 'EffectiveDF',
1789
+ expression = '{DF}',
1790
+ description = 'Current defence factor'),
1791
+ CalculatedTrait(#This could be redefined in module
1792
+ name = 'IsAttacker',
1793
+ expression = '{Phase.contains(Faction)}',
1794
+ description = 'Check if current phase belongs to faction'),
1795
+ CalculatedTrait(#This could be redefined in module
1796
+ name = 'OddsShift',
1797
+ expression = f'{{{self._battleShift}}}',
1798
+ description = 'Add to odds shift'),
1799
+ # CalculatedTrait(#This could be redefined in module
1800
+ # name = 'DRM',
1801
+ # expression = f'{{{self._battleDRM}}}',
1802
+ # description = 'Add die-roll modifer'),
1803
+ ReportTrait(
1804
+ self._markKey,
1805
+ report = (f'{{{self._debug}?("~"+BasicName'
1806
+ f'+" Mark battle trampoline global "'
1807
+ f'+" "+{self._markStart}):""}}')),
1808
+ ReportTrait(
1809
+ self._calcBattleAF,
1810
+ report=(f'{{{self._verbose}?'
1811
+ f'("! "+BasicName+'
1812
+ f'" add "+EffectiveAF+'
1813
+ f'" to total attack factor -> "+'
1814
+ f'{self._battleAF}'
1815
+ f'):""}}')),
1816
+ ReportTrait(
1817
+ self._calcBattleDF,
1818
+ report=(f'{{{self._verbose}?'
1819
+ f'("! "+BasicName+'
1820
+ f'" add "+EffectiveDF+'
1821
+ f'" to total defence factor -> "+'
1822
+ f'{self._battleDF}'
1823
+ f'):""}}')),
1824
+ ReportTrait(
1825
+ self._calcBattleShft,
1826
+ report=(f'{{{self._debug}?'
1827
+ f'("~ "+BasicName+'
1828
+ f'" Updating odds shift with "+OddsShift+'
1829
+ f'" -> "+{self._battleShift}):""}}')),
1830
+ ReportTrait(
1831
+ self._calcBattleDRM,
1832
+ report=(f'{{{self._debug}?'
1833
+ f'("~ "+BasicName+'
1834
+ f'" Updating DRM with "+DRM+'
1835
+ f'" -> "+{self._battleDRM}):""}}')),
1836
+ ]
1837
+ traits.extend(
1838
+ place+
1839
+ trig+
1840
+ oth+
1841
+ [MarkTrait(name=self._battleUnit,value=True),
1842
+ BasicTrait()])
1843
+ prototypes.addPrototype(name = self._battleUnit,
1844
+ description = '',
1845
+ traits = traits)
1846
+ # ----------------------------------------------------------------
1847
+ def addBattleCorePrototype(self,prototypes):
1848
+ # --- Core traits for battle markers (number, odds, results)
1849
+ # - Set the global current battle number
1850
+ # - Get the current global battle number
1851
+ # - Clear this counter
1852
+ # - Trampoline to global command to clear all marks for this battle
1853
+ traits = [
1854
+ # NoStackTrait(select = NoStackTrait.NORMAL_SELECT,
1855
+ # move = NoStackTrait.NORMAL_MOVE,
1856
+ # canStack = False,
1857
+ # ignoreGrid = False),
1858
+ GlobalPropertyTrait(['',self._setBattle,GlobalPropertyTrait.DIRECT,
1859
+ f'{{{self._battleNo}}}'],
1860
+ name = self._currentBattle,
1861
+ numeric = True,
1862
+ description = 'Set current battle'),
1863
+ GlobalPropertyTrait(['',self._setBattle,
1864
+ GlobalPropertyTrait.DIRECT, '{IsAttacker}'],
1865
+ name = self._currentAttacker,
1866
+ numeric = True,
1867
+ description = 'Set attacker'),
1868
+ GlobalPropertyTrait(['',self._markBattle+'ResetPlaced',
1869
+ GlobalPropertyTrait.DIRECT, '{false}'],
1870
+ name = self._placedGlobal,
1871
+ numeric = True,
1872
+ description = 'Clear Odds placed flag'),
1873
+ ReportTrait(self._markBattle+'ResetPlaced',
1874
+ report = (f'{{{self._debug}?("`"+BasicName+":"+'
1875
+ f'"Clear placed odds flags "+'
1876
+ f'{self._placedGlobal}):""}}')),
1877
+ DynamicPropertyTrait(['',self._getBattle,
1878
+ DynamicPropertyTrait.DIRECT,
1879
+ f'{{{self._currentBattle}}}'],
1880
+ name = self._battleNo,
1881
+ numeric = True,
1882
+ value = f'{{{self._battleNo}}}',
1883
+ description = 'Set battle number'),
1884
+ DynamicPropertyTrait(['',self._getBattle,
1885
+ DynamicPropertyTrait.DIRECT,
1886
+ f'{{{self._currentAttacker}}}'],
1887
+ name = 'IsAttacker',
1888
+ numeric = True,
1889
+ value = 'false',
1890
+ description = 'Set attacker'),
1891
+ DeleteTrait('',self._clearBattle),
1892
+ GlobalHotkeyTrait(name = '',
1893
+ key = self._clearBattle+'Trampo',
1894
+ globalHotkey = self._clearBattle,
1895
+ description = 'Clear selected battle'),
1896
+ TriggerTrait(command = 'Clear',
1897
+ key = self._clearKey,
1898
+ actionKeys = [self._setBattle,
1899
+ self._clearBattle+'Trampo']),
1900
+ ReportTrait(self._setBattle,
1901
+ report=(f'{{{self._debug}?'
1902
+ f'("~ "+BasicName+" @ "+LocationName+'
1903
+ f'": Set global current battle # "+'
1904
+ f'{self._battleNo}+" -> "+'
1905
+ f'{self._currentBattle}+" IsAttacker("+'
1906
+ f'IsAttacker+")="+{self._currentAttacker}+'
1907
+ f'" Marker="+{self._battleMark}+'
1908
+ f'" Odds="+{self._oddsMark}+'
1909
+ f'" Placed="+{self._placedGlobal}+'
1910
+ f'""):""}}')),
1911
+ ReportTrait(self._getBattle,
1912
+ report=(f'{{{self._debug}?'
1913
+ f'("~ "+BasicName+" @ "+LocationName+'
1914
+ f'": Get global current battle # "+'
1915
+ f'{self._currentBattle}+" -> "+'
1916
+ f'{self._battleNo}+'
1917
+ f'" IsAttacker="+IsAttacker+'
1918
+ f'" Marker="+{self._battleMark}+'
1919
+ f'" Odds="+{self._oddsMark}+'
1920
+ f'""):""}}')),
1921
+ ReportTrait(self._clearBattle,
1922
+ report=(f'{{{self._debug}?'
1923
+ f'("~ "+BasicName+" @ "+LocationName+'
1924
+ f'": Clear this global="+'
1925
+ f'{self._currentBattle}+" this="+'
1926
+ f'{self._battleNo}):""}}')),
1927
+ ReportTrait(self._clearKey,
1928
+ report=(f'{{{self._debug}?'
1929
+ f'("~ "+BasicName+" @ "+LocationName+'
1930
+ f'": To clear battle # global="+'
1931
+ f'{self._currentBattle}+" this="+'
1932
+ f'{self._battleNo}):""}}')),
1933
+ ReportTrait(self._clearBattle+'Trampo',
1934
+ report=(f'{{{self._debug}?'
1935
+ f'("~ "+BasicName+" @ "+LocationName+'
1936
+ f'": Forward clear battle # global="+'
1937
+ f'{self._currentBattle}+" this="+'
1938
+ f'{self._battleNo}):""}}')),
1939
+ MarkTrait(name=self._battleMark,value=True),
1940
+ # TriggerTrait(name = '',
1941
+ # command = 'Print',
1942
+ # key = self._printKey,
1943
+ # actionKeys = []),
1944
+ # ReportTrait(self._printKey,
1945
+ # report = (f'{{{self._debug}?("`"+BasicName+":"+'
1946
+ # f'" Battle no "+{self._battleNo}+'
1947
+ # f'" Current no "+{self._currentBattle}):""}}'
1948
+ # )),
1949
+ BasicTrait()
1950
+ ]
1951
+ prototypes.addPrototype(name = self._currentBattle,
1952
+ description = '',
1953
+ traits = traits)
1954
+
1955
+ # ----------------------------------------------------------------
1956
+ def addBattlePrototypes(self,prototypes):
1957
+ if not self.addBattleControlPrototype(prototypes):
1958
+ return
1959
+
1960
+ self.addBattleCalculatePrototype(prototypes)
1961
+ self.addBattleUnitPrototype(prototypes)
1962
+ self.addBattleCorePrototype(prototypes)
1963
+
1964
+ # ----------------------------------------------------------------
1965
+ def markerTraits(self):
1966
+ return [DeleteTrait(),
1967
+ SubMenuTrait(
1968
+ subMenu='Rotate',
1969
+ keys=['Clock-wise',
1970
+ 'Counter clock-wise']),
1971
+ RotateTrait(
1972
+ rotateCW = 'Clock-wise',
1973
+ rotateCCW = 'Counter clock-wise',
1974
+ )]
1975
+
1976
+ # ----------------------------------------------------------------
1977
+ def battleMarkerTraits(self,c):
1978
+ '''Derives from the CurrentBattle prototype and adds a submenu
1979
+ to place odds counter on the battle marker'''
1980
+ traits = [PrototypeTrait(name=self._currentBattle),
1981
+ NonRectangleTrait(filename = c['filename'],
1982
+ image = c['img'])]
1983
+
1984
+ subs = []
1985
+ ukeys = []
1986
+ place = []
1987
+ trig = []
1988
+ rept = []
1989
+ repp = []
1990
+ for i, odds in enumerate(self._oddsMarks):
1991
+ on = odds.replace('odds marker','').strip()
1992
+ om = odds.replace(':',r'\:')
1993
+ kn = self._markOdds+str(i+1)
1994
+ gpid = self._game.nextPieceSlotId()
1995
+ skel = PlaceTrait.SKEL_PATH()
1996
+ path = skel.format('OddsMarkers',om)
1997
+ subs.append(on)
1998
+
1999
+ place.append(
2000
+ PlaceTrait(command = '',
2001
+ key = kn,
2002
+ markerSpec = path,
2003
+ markerText = 'null',
2004
+ xOffset = -6,
2005
+ yOffset = -8,
2006
+ matchRotation = False,
2007
+ afterKey = self._getBattle+'Details',
2008
+ gpid = gpid,
2009
+ placement = PlaceTrait.ABOVE,
2010
+ description = f'Add odds marker {on}'))
2011
+ trig.append(
2012
+ TriggerTrait(name = '',
2013
+ command = on,
2014
+ key = kn+'real',
2015
+ actionKeys = [
2016
+ self._setBattle,
2017
+ kn]))
2018
+ rept.append(
2019
+ ReportTrait(kn+'real',
2020
+ report=(f'{{{self._debug}?'
2021
+ f'("~ "+BasicName+": Set odds '
2022
+ f'{on} ({kn})"):""}}')))
2023
+ repp.append(
2024
+ ReportTrait(kn,
2025
+ report=(f'{{{self._debug}?'
2026
+ f'("~ "+BasicName+": Place odds '
2027
+ f'{on} ({kn})"):""}}')))
2028
+ ukeys.append(kn+'real')
2029
+
2030
+ auto = []
2031
+ auton = []
2032
+ if len(self._oddsMarks) > 0:
2033
+ auton = ['Auto']
2034
+ for i, odds in enumerate(self._oddsMarks):
2035
+ trig.append(
2036
+ TriggerTrait(name = '',
2037
+ command = '',
2038
+ key = self._markOdds+'Auto',
2039
+ property = f'{{{self._battleIdx}=={i+1}}}',
2040
+ actionKeys = [self._markOdds+str(i+1)]))
2041
+
2042
+ auto = [
2043
+ GlobalHotkeyTrait(name = '',
2044
+ key = self._calcBattleOdds,
2045
+ globalHotkey = self._calcBattleOdds,
2046
+ description = 'Calculate fraction'),
2047
+ DeselectTrait(command = '',
2048
+ key = self._calcBattleOdds+'Deselect',
2049
+ deselect = DeselectTrait.ONLY),
2050
+ ReportTrait(self._calcBattleOdds+'Deselect',
2051
+ report=(f'{{{self._debug}?'
2052
+ f'("~ "+BasicName+": Select only this "'
2053
+ f'+" Attacker="+IsAttacker'
2054
+ f'):""}}')),
2055
+ GlobalPropertyTrait(
2056
+ ['',self._calcBattleOdds+'Placed',
2057
+ GlobalPropertyTrait.DIRECT, '{true}'],
2058
+ name = self._placedGlobal),
2059
+ GlobalPropertyTrait(
2060
+ ['',self._calcBattleOdds+'Placed',
2061
+ GlobalPropertyTrait.DIRECT, '{false}'],
2062
+ name = self._markStart),
2063
+ TriggerTrait(name = '',
2064
+ command = '',
2065
+ key = self._markOdds+'Trampoline',
2066
+ actionKeys = [
2067
+ self._calcBattleOdds+'Placed',
2068
+ self._calcBattleOdds,
2069
+ self._markOdds+'Auto',
2070
+ self._calcBattleOdds+'Deselect'],
2071
+ property = (f'{{IsAttacker!=true&&'
2072
+ f'{self._placedGlobal}!=true}}'
2073
+ )
2074
+ ),
2075
+ TriggerTrait(name = '',
2076
+ command = 'Auto',
2077
+ key = self._calcBattleOdds+'Start',
2078
+ actionKeys = [
2079
+ self._setBattle,
2080
+ self._markOdds+'Trampoline',
2081
+ ]),
2082
+ ReportTrait(self._markOdds+'Trampoline',
2083
+ report = (f'{{{self._debug}?("~"+BasicName+'
2084
+ f'" @ "+LocationName+'
2085
+ f'" Trampoline to mark odds"+'
2086
+ f'" IsAttacker="+IsAttacker)'
2087
+ f':""}}')),
2088
+ ReportTrait(self._calcBattleOdds+'Placed',
2089
+ report=(f'{{{self._debug}?'
2090
+ f'("~ "+BasicName+" @ "+LocationName+'
2091
+ f'": placed for odds "+'
2092
+ f'{self._placedGlobal}+" "+'
2093
+ f'{self._markStart}):""}}')),
2094
+ ReportTrait(self._calcBattleOdds,
2095
+ report=(f'{{{self._debug}?'
2096
+ f'("~ "+BasicName+'
2097
+ f'": to global Battle odds "):""}}')),
2098
+ ReportTrait(self._calcBattleOdds+'Start',
2099
+ report=(f'{{{self._debug}?'
2100
+ f'("~ "+BasicName+" @ "+LocationName+'
2101
+ f'": Battle calculate odds start ."+'
2102
+ f'{self._battleOdds}+"."):""}}')),
2103
+ ReportTrait(self._markOdds+'Auto',
2104
+ report=(f'{{{self._debug}?'
2105
+ f'("~"+BasicName+" : "+LocationName+'
2106
+ f'": Auto battle odds ."+'
2107
+ f'{self._battleOdds}+"."):""}}'))
2108
+ ]
2109
+
2110
+ traits.extend([
2111
+ RestrictCommandsTrait(
2112
+ name='Hide when auto-odds are enabled',
2113
+ hideOrDisable = RestrictCommandsTrait.HIDE,
2114
+ expression = f'{{{self._autoOdds}==true}}',
2115
+ keys = ukeys)]+
2116
+ place
2117
+ +trig
2118
+ +auto
2119
+ +rept
2120
+ +repp)
2121
+ if len(subs) > 0:
2122
+ traits.extend([
2123
+ SubMenuTrait(subMenu = 'Odds',
2124
+ keys = auton+subs),
2125
+ ])
2126
+
2127
+ return traits
2128
+
2129
+ # ----------------------------------------------------------------
2130
+ def oddsMarkerTraits(self,c=None):
2131
+ '''Derives from the CurrentBattle prototype and adds a submenu
2132
+ to replace odds counter with result marker'''
2133
+ gpid = self._game.nextPieceSlotId()
2134
+ traits = [
2135
+ PrototypeTrait(name=self._currentBattle),
2136
+ MarkTrait(self._oddsMark,'true'),
2137
+ NonRectangleTrait(filename = c['filename'],
2138
+ image = c['img']),
2139
+ DynamicPropertyTrait(
2140
+ ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT,
2141
+ (f'{{{self._battleAF}+" vs "+{self._battleDF}+'
2142
+ f'" (odds "+{self._battleOdds}+" shift "+'
2143
+ f'{self._battleShift}+" DRM "+{self._battleDRM}+'
2144
+ f'")"}}')],
2145
+ name = 'BattleDetails',
2146
+ value = '',
2147
+ numeric = False,
2148
+ description = 'Stored battle details'),
2149
+ DynamicPropertyTrait(
2150
+ ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT,
2151
+ f'{{{self._battleDRM}}}'],
2152
+ name = 'BattleDRM',
2153
+ value = 0,
2154
+ numeric = True,
2155
+ description = 'Stored DRM'),
2156
+ TriggerTrait(command = '',
2157
+ key = self._getBattle+'Details',
2158
+ actionKeys = [self._getBattle,
2159
+ self._getBattle+'More']),
2160
+ # DeleteTrait('',self._recalcOdds+'Delete'),
2161
+ # ReplaceTrait(command = '',
2162
+ # key = self._recalcOdds+'Replace',
2163
+ # markerSpec = '',
2164
+ # markerText = 'null',
2165
+ # xOffset = 0,
2166
+ # yOffset = 0,
2167
+ # matchRotation = False,
2168
+ # afterKey = '',
2169
+ # gpid = gpid,
2170
+ # description = f'Replace with nothing'),
2171
+ GlobalHotkeyTrait(
2172
+ name = '',
2173
+ key =self._calcBattleOdds+'Start',
2174
+ globalHotkey=self._calcBattleOdds+'ReAuto',
2175
+ description ='Trampoline to global'),
2176
+ # Recalculate odds
2177
+ # First setBatle to make battle No global
2178
+ # Then send global key command
2179
+ # Then delete this
2180
+ TriggerTrait(
2181
+ command = 'Recalculate',
2182
+ key = self._recalcOdds,
2183
+ actionKeys = [self._setBattle,
2184
+ self._markBattle+'ResetPlaced',
2185
+ self._calcBattleOdds+'Start',
2186
+ self._clearBattle,
2187
+ ]),
2188
+ ReportTrait(
2189
+ self._recalcOdds+'Delete',
2190
+ report=(f'{{{self._debug}?'
2191
+ f'("~"+BasicName+'
2192
+ f'": Deleting self"):""}}')),
2193
+ ReportTrait(
2194
+ self._clearBattle,
2195
+ report=(f'{{{self._debug}?'
2196
+ f'("~"+BasicName+'
2197
+ f'": Remove"):""}}')),
2198
+ ReportTrait(
2199
+ self._recalcOdds,
2200
+ report=(f'{{{self._debug}?'
2201
+ f'("! Recalculate Odds"):""}}')),
2202
+ ReportTrait(
2203
+ self._calcBattleOdds+'Start',
2204
+ report=(f'{{{self._debug}?'
2205
+ f'("~Start auto recalculate Odds"):""}}')),
2206
+ ReportTrait(
2207
+ self._getBattle+'More',
2208
+ report = (f'{{{self._debug}?('
2209
+ f'"~Getting more battle info: "+'
2210
+ f'BattleDetails+" "+BattleDRM'
2211
+ f'):""}}'))
2212
+ ]
2213
+
2214
+ subs = []
2215
+ place = []
2216
+ trig = []
2217
+ rept = []
2218
+ ukeys = [self._recalcOdds]
2219
+ first = ''
2220
+ for i, result in enumerate(self._resultMarks):
2221
+ r = result.replace('result marker','').strip()
2222
+ kn = self._markResult+str(i+1)
2223
+ gpid = self._game.nextPieceSlotId()
2224
+ ukeys.append(kn+'real')
2225
+ subs.append(r)
2226
+ if first == '': first = r
2227
+
2228
+ skel = PlaceTrait.SKEL_PATH()
2229
+ path = skel.format('ResultMarkers',result)
2230
+
2231
+ place.append(
2232
+ ReplaceTrait(command = '',
2233
+ key = kn,
2234
+ markerSpec = path,
2235
+ markerText = 'null',
2236
+ xOffset = -6,
2237
+ yOffset = -8,
2238
+ matchRotation = False,
2239
+ afterKey = self._getBattle,
2240
+ gpid = gpid,
2241
+ description = f'Add result marker {r}'))
2242
+ trig.append(
2243
+ TriggerTrait(name = '',
2244
+ command = r,
2245
+ key = kn+'real',
2246
+ actionKeys = [
2247
+ self._setBattle,
2248
+ kn]))
2249
+ rept.append(
2250
+ ReportTrait(kn+'real',
2251
+ report=(f'{{{self._debug}?'
2252
+ f'("~ "+BasicName+" setting result '
2253
+ f'{r}"):""}}')))
2254
+
2255
+ auto = []
2256
+ auton = []
2257
+ if len(self._resultMarks) > 0:
2258
+ auton = ['Auto']
2259
+ for i, res in enumerate(self._resultMarks):
2260
+ r = res.replace('result marker','').strip()
2261
+ trig.append(
2262
+ TriggerTrait(
2263
+ name = '',
2264
+ command = '',
2265
+ key = self._markResult+'Auto',
2266
+ property = f'{{{self._battleResult}=="{r}"}}',
2267
+ actionKeys = [self._markResult+str(i+1)]))
2268
+
2269
+ auto = [ # Override in the module
2270
+ CalculatedTrait(
2271
+ name = 'Die',
2272
+ expression = '{GetProperty("1d6_result")}',
2273
+ description = 'Die roll'),
2274
+ GlobalHotkeyTrait(
2275
+ name = '',
2276
+ key = self._rollDice,
2277
+ globalHotkey = self._diceKey,
2278
+ description = 'Roll dice'),
2279
+ CalculatedTrait(
2280
+ name = 'BattleResult',
2281
+ expression = f'{{"{first}"}}',
2282
+ ),
2283
+ GlobalPropertyTrait(
2284
+ ['',self._calcBattleRes+'real',GlobalPropertyTrait.DIRECT,
2285
+ '{BattleResult}'],
2286
+ name = self._battleResult,
2287
+ numeric = False,
2288
+ description = 'Set combat result'),
2289
+ TriggerTrait(
2290
+ name = '',
2291
+ command = 'Resolve',
2292
+ key = self._resolveKey,
2293
+ property = f'{{{self._autoResults}==true}}',
2294
+ actionKeys = [
2295
+ self._setBattle,
2296
+ self._rollDice,
2297
+ self._calcBattleRes+'real',
2298
+ self._markResult+'Auto',
2299
+ ]),
2300
+ ReportTrait(
2301
+ self._rollDice,
2302
+ report = (f'{{{self._debug}?("~"+BasicName+": "'
2303
+ f'+"Rolling the dice with DRM "'
2304
+ f'+BattleDRM):""}}')),
2305
+ ReportTrait(
2306
+ self._calcBattleRes,
2307
+ report=(f'{{{self._debug}?'
2308
+ f'("~ "+BasicName+" @ "+LocationName+'
2309
+ f'": Battle result "+'
2310
+ f'{self._battleOdds}):""}}')),
2311
+ ReportTrait(
2312
+ self._markResult+'Auto',
2313
+ report=(f'{{{self._debug}?'
2314
+ f'("~ "+BasicName+" @ "+LocationName+'
2315
+ f'": Auto battle result "+'
2316
+ f'{self._battleResult}):""}}')),
2317
+ ReportTrait(
2318
+ self._markResult+'Auto',
2319
+ report=(f'{{"` Battle # "+{self._battleNo}+": "+'
2320
+ f'BattleDetails+" with die roll "+Die+": "+'
2321
+ f'{self._battleResult}'
2322
+ # f'+ "<img src=\'result_marker_"'
2323
+ # f'+{self._battleResult}+".png\'"'
2324
+ # f'+" width=24 height=24>"'
2325
+ f'}}')),
2326
+ MarkTrait(name=self._battleOddsM,value='true')
2327
+ ]
2328
+
2329
+ traits.extend(
2330
+ [RestrictCommandsTrait(
2331
+ name='Hide when auto-results are enabled',
2332
+ hideOrDisable = RestrictCommandsTrait.HIDE,
2333
+ expression = f'{{{self._autoResults}==true}}',
2334
+ keys = ukeys)]+
2335
+ place
2336
+ +trig
2337
+ +auto)
2338
+
2339
+ if len(subs) > 0:
2340
+ traits.append(SubMenuTrait(subMenu = 'Result',
2341
+ keys = subs))
2342
+
2343
+ return traits
2344
+
2345
+ # ----------------------------------------------------------------
2346
+ def resultMarkerTraits(self,c=None):
2347
+ traits = [PrototypeTrait(name=self._currentBattle),
2348
+ NonRectangleTrait(filename = c['filename'],
2349
+ image = c['img'])]
2350
+
2351
+ return traits
2352
+
2353
+ # ----------------------------------------------------------------
2354
+ def factionTraits(self,faction):
2355
+ offX = 36 * self._counterScale * self._resolution/150
2356
+ offY = -38 * self._counterScale * self._resolution/150
2357
+ traits = [
2358
+ SubMenuTrait(subMenu = 'Movement',
2359
+ keys = ['Trail',
2360
+ 'Toggle mark']),
2361
+ TrailTrait(name = 'Trail',
2362
+ lineWidth = 5,
2363
+ radius = 10,
2364
+ key = self._trailKey,
2365
+ turnOn = self._trailToggleKey+'On',
2366
+ turnOff = self._trailToggleKey+'Off'),
2367
+ MovedTrait(name = 'Toggle mark',
2368
+ xoff = int(offX),
2369
+ yoff = int(offY)),
2370
+ RotateTrait(
2371
+ rotateCW = 'Clock-wise',
2372
+ rotateCCW = 'Counter clock-wise',
2373
+ ),
2374
+ SubMenuTrait(
2375
+ subMenu='Rotate',
2376
+ keys=['Clock-wise',
2377
+ 'Counter clock-wise']),
2378
+ SendtoTrait(mapName = 'DeadMap',
2379
+ boardName = f'{faction} pool',
2380
+ name = 'Eliminate',
2381
+ key = self._eliminateKey,
2382
+ restoreName = '', # 'Restore',
2383
+ restoreKey = '', # self._restoreKey,
2384
+ description = 'Eliminate unit'),
2385
+ PrototypeTrait(name=self._battleUnit),
2386
+ MarkTrait(name='Faction',value=faction),
2387
+ ReportTrait(
2388
+ self._trailToggleKey+'On',
2389
+ report = (f'{{{self._debug}?("~"+BasicName+'
2390
+ f'" turning ON movement trail"):""}}')),
2391
+ ReportTrait(
2392
+ self._trailToggleKey+'Off',
2393
+ report = (f'{{{self._debug}?("~"+BasicName+'
2394
+ f'" turning OFF movement trail"):""}}'))
2395
+ ]
2396
+
2397
+ return traits
2398
+
2399
+ # ----------------------------------------------------------------
2400
+ def getFactors(self,val):
2401
+ from re import sub
2402
+
2403
+ with VerboseGuard(f'Parsing factors: {val}') as v:
2404
+ cf = None
2405
+ mf = None
2406
+ df = None
2407
+ ra = None
2408
+ try:
2409
+ for rem in ['protect',
2410
+ 'leavevmode@ifvmode',
2411
+ 'kern',
2412
+ '[-0-9.]+em',
2413
+ 'relax',
2414
+ 'TU']:
2415
+ val = sub(rem,'',val)
2416
+
2417
+ paren = False
2418
+ while True:
2419
+ tt = sub(r'\(([^()]*)\)',r'\1',val)
2420
+ if tt == val:
2421
+ break
2422
+ val = tt
2423
+ paren = True
2424
+
2425
+ if val.startswith('['):
2426
+ eb = val.rindex(']')
2427
+ val = val[eb+1:]
2428
+
2429
+ v(f'Value to parse {val}')
2430
+ if 'chit/1 factor' in val:
2431
+ vv = val.replace('chit/1 factor=','')
2432
+ v(f'1 factor: {vv}')
2433
+ cf = float(vv)
2434
+ elif 'chit/2 factors artillery' in val:
2435
+ vv = val.replace('chit/2 factors artillery=','')
2436
+ vv = vv.replace('*','0')
2437
+ v(f'2 factors artillery: {vv}')
2438
+ cf,mf,ra = [float(v) for v in vv.strip('=').split()]
2439
+ elif 'chit/3 factors artillery' in val:
2440
+ vv = val.replace('chit/3 factors artillery=','')
2441
+ vv = vv.replace('*','0')
2442
+ v(f'3 factors artillery: {vv}')
2443
+ cf,df,mf,ra = [int(v) for v in vv.strip('=').split()]
2444
+ elif 'chit/2 factors defence' in val:
2445
+ vv = val.replace('chit/2 factors defence=','')
2446
+ cf = 0
2447
+ v(f'2 factors defence: {vv}')
2448
+ df,mf = [float(v) for v in vv.split()]
2449
+ elif 'chit/2 factors' in val:
2450
+ vv = val.replace('chit/2 factors=','')
2451
+ v(f'2 factors: {vv}')
2452
+ cf,mf = [float(v) for v in vv.split()]
2453
+ if paren:
2454
+ df = cf
2455
+ cf = 0
2456
+ elif 'chit/3 factors' in val:
2457
+ vv = val.replace('chit/3 factors=','')
2458
+ vv = vv.replace('*','0')
2459
+ v(f'3 factors: {vv}')
2460
+ cf,df,mf = [float(v) for v in vv.split()]
2461
+ else:
2462
+ v(f'Unknown factors: {val}')
2463
+
2464
+ # Set defensive factor to combat factor if not defined.
2465
+ if df is None and cf is not None:
2466
+ df = cf
2467
+
2468
+
2469
+
2470
+ except Exception as e:
2471
+ print(f'\nWarning when extracting factors: {e} '
2472
+ f'in "{val}" -> "{vv}"')
2473
+ return None,None,None,None
2474
+ pass
2475
+
2476
+ v(f'-> CF={cf} DF={df} MF={mf} RA={ra}')
2477
+ return cf,df,mf,ra
2478
+
2479
+ # ----------------------------------------------------------------
2480
+ def pieceTraits(self,subn,subc,cn,c):
2481
+ from re import sub
2482
+
2483
+ bb = self.getBB(c['img'])
2484
+ height = bb[3]-bb[1] if bb is not None else 1
2485
+ width = bb[2]-bb[0] if bb is not None else 1
2486
+
2487
+ cf = subc.get(cn + ' flipped', None)
2488
+ traits = [PrototypeTrait(name=f'{subn} prototype')]
2489
+
2490
+ def clean(s):
2491
+ return s.strip().replace(',',' ').replace('/',' ').strip()
2492
+
2493
+ if not self._nonato:
2494
+ mains = c.get('mains','')
2495
+ mm = clean(mains).strip()
2496
+ traits.append(PrototypeTrait(name=f'{mm} prototype'))
2497
+ # Commented code adds all 'main' types as prototypes, which
2498
+ # doesn't make so much sense
2499
+ #
2500
+ # m = set([clean(m) for m in mains.split(',')])
2501
+ # traits.extend([PrototypeTrait(name=f'{m.strip()} prototype')
2502
+ # for m in set(m)])
2503
+ for p in ['echelon','command']:
2504
+ val = c.get(p,None)
2505
+ if val is not None:
2506
+ pv = f'{val.strip()} prototype'
2507
+ traits.append(PrototypeTrait(name=pv))
2508
+
2509
+ if cf is not None:
2510
+ traits.extend([
2511
+ LayerTrait(images = [c['filename'],
2512
+ cf['filename']],
2513
+ newNames = ['','Reduced +'],
2514
+ activateName = '',
2515
+ decreaseName = '',
2516
+ increaseName = 'Flip',
2517
+ increaseKey = self._flipKey,
2518
+ decreaseKey = '',
2519
+ name = 'Step'),
2520
+ ReportTrait(self._flipKey)])
2521
+
2522
+ if not self._nochit:
2523
+ def clean(value):
2524
+ return sub(r'\[[^=]+\]=','',value)\
2525
+ .replace('{','')\
2526
+ .replace('}','')\
2527
+ .replace('/',' ')\
2528
+ .replace(',',' ')\
2529
+ .replace('\\',' ')
2530
+
2531
+ def factor_clean(value):
2532
+ tmp = sub(r'\[[^=]+\]=','',value)\
2533
+ .replace('{','')\
2534
+ .replace('}','')\
2535
+ .replace('/TU','')\
2536
+ .replace('\\',' ')
2537
+ return sub(r'\textonehalf','0.5',
2538
+ sub(r'([0-9])\textonehalf',r'\1.5',tmp))\
2539
+ .replace(',',' ')
2540
+
2541
+ # Add extra marks. This may be useful later on.
2542
+ for field in ['upper left', 'upper right',
2543
+ 'lower left', 'lower right',
2544
+ 'left', 'right',
2545
+ 'factors']:
2546
+ value = c.get('chit',{}).get(field,None)
2547
+ if value is None:
2548
+ continue
2549
+
2550
+ if field != 'factors':
2551
+ val = clean(value)
2552
+ val = val\
2553
+ .replace('chit identifier=','')\
2554
+ .replace('chit small identifier=','')
2555
+
2556
+ traits.append(MarkTrait(name = field, value = val))
2557
+ continue
2558
+
2559
+
2560
+ #print('\n'+f'Got factors "{value}" -> "{val}"')
2561
+ val = factor_clean(value)
2562
+
2563
+ af, df, mf, ra = self.getFactors(val)
2564
+ saf, sdf, smf, sra = None,None,None,None
2565
+ if cf is not None:
2566
+ value = cf.get('chit',{}).get(field,None)
2567
+ if value is not None:
2568
+ val = factor_clean(value)
2569
+ val = val\
2570
+ .replace('chit/identifier=','')\
2571
+ .replace('chit/small identifier=','')
2572
+ #print('\n'+f'Got s-factors "{value}" -> "{val}"')
2573
+ saf, sdf, smf, sra = self.getFactors(val)
2574
+
2575
+ rf = []
2576
+ srf = []
2577
+ for f,sf,n in [[af,saf,'CF'],
2578
+ [df,sdf,'DF'],
2579
+ [mf,smf,'MF'],
2580
+ [ra,sra,'Range']]:
2581
+ if f is None: continue
2582
+ try:
2583
+ # Try to make the factor an integer
2584
+ f = int(str(f))
2585
+ except:
2586
+ pass
2587
+
2588
+ if sf is None:
2589
+ rf.append(MarkTrait(name=n,value=f))
2590
+ else:
2591
+ try:
2592
+ # Try to make the factor an integer
2593
+ sf = int(str(sf))
2594
+ except:
2595
+ pass
2596
+
2597
+ rf .append(MarkTrait(name='Full'+n, value=f))
2598
+ srf.append(MarkTrait(name='Reduced'+n,value=sf))
2599
+ traits.append(CalculatedTrait(
2600
+ name = n,
2601
+ expression = (f'{{(Step_Level==2)?'
2602
+ f'Reduced{n}:Full{n}}}')))
2603
+
2604
+ # Perhaps modify in module
2605
+ srf.append(MarkTrait(name='DRM',value=0))
2606
+ traits.extend(rf+srf)
2607
+
2608
+
2609
+
2610
+
2611
+
2612
+
2613
+ return height, width, traits
2614
+
2615
+ # ----------------------------------------------------------------
2616
+ def addCounters(self):
2617
+ '''Add all counters (pieces) element to the module.
2618
+ Prototypes are also created as part of this.
2619
+ '''
2620
+ from re import sub
2621
+
2622
+ with VerboseGuard('Adding counters') as v:
2623
+ protos = self._game.addPrototypes()
2624
+
2625
+ self.addNatoPrototypes(protos)
2626
+ self.addBattlePrototypes(protos)
2627
+
2628
+ pieces = self._game.addPieceWindow(
2629
+ name = 'Counters',
2630
+ icon = self.getIcon('unit-icon',
2631
+ '/images/counter.gif'),
2632
+ hotkey = self._countersKey)
2633
+ tabs = pieces.addTabs(entryName='Counters')
2634
+
2635
+ for subn, subc in self._categories.get('counter',{}).items():
2636
+
2637
+ subn = subn.strip()
2638
+ panel = tabs.addPanel(entryName = subn, fixed = False)
2639
+ plist = panel.addList(entryName = f'{subn} counters')
2640
+
2641
+ traits = []
2642
+ if subn in ['BattleMarkers']:
2643
+ traits = self.battleMarkerTraits(list(subc.values())[0])
2644
+ elif subn in ['OddsMarkers']:
2645
+ traits = self.oddsMarkerTraits(list(subc.values())[0])
2646
+ elif subn in ['ResultMarkers']:
2647
+ traits = self.resultMarkerTraits(list(subc.values())[0])
2648
+ elif subn.lower() in ['marker', 'markers']:
2649
+ traits = self.markerTraits()
2650
+ else:
2651
+ traits = self.factionTraits(subn)
2652
+
2653
+ traits.append(BasicTrait())
2654
+
2655
+ p = protos.addPrototype(name = f'{subn} prototype',
2656
+ description = f'Prototype for {subn}',
2657
+ traits = traits)
2658
+ v('')
2659
+
2660
+ with VerboseGuard(f'Adding pieces for "{subn}"') as vv:
2661
+ for i, (cn, c) in enumerate(subc.items()):
2662
+ if cn.endswith('flipped'): continue
2663
+
2664
+ if i == 0: v('',end='')
2665
+ vv(f'[{cn}',end='',flush=True,noindent=True)
2666
+
2667
+ height, width, traits = self.pieceTraits(subn,subc,cn,c)
2668
+ if cn == self._hiddenName:
2669
+ traits = [
2670
+ PrototypeTrait(name=self._battleCtrl),
2671
+ PrototypeTrait(name=self._battleCalc),
2672
+ TriggerTrait(
2673
+ name = 'Toggle trails',
2674
+ command = '',
2675
+ key = self._trailKey,
2676
+ actionKeys = [
2677
+ self._trailToggleKey+'Value',
2678
+ self._trailToggleKey+'Cmd'],
2679
+ ),
2680
+ GlobalPropertyTrait(
2681
+ ['',self._trailToggleKey+'Value',
2682
+ GlobalPropertyTrait.DIRECT,
2683
+ f'{{!{self._trailsFlag}}}'],
2684
+ name = self._trailsFlag,
2685
+ description = 'State of global trails'),
2686
+ TriggerTrait(
2687
+ name = 'Turn on trails',
2688
+ command = '',
2689
+ key = self._trailToggleKey+'Cmd',
2690
+ actionKeys = [self._trailToggleKey+'On'],
2691
+ property = f'{{{self._trailsFlag}==true}}'),
2692
+ TriggerTrait(
2693
+ name = 'Turn off trails',
2694
+ command = '',
2695
+ key = self._trailToggleKey+'Cmd',
2696
+ actionKeys = [self._trailToggleKey+'Off'],
2697
+ property = f'{{{self._trailsFlag}!=true}}'),
2698
+ # ReportTrait(
2699
+ # self._trailToggleKey+'Cmd',
2700
+ # report = (f'{{{self._verbose}?('
2701
+ # f'BasicName+" toggle trails "'
2702
+ # f'+{self._trailsFlag}):""}}')),
2703
+ #GlobalCommandTrait
2704
+ GlobalHotkeyTrait
2705
+ (
2706
+ name = '',
2707
+ #commandName = '',
2708
+ key = self._trailToggleKey+'On',
2709
+ globalHotkey= self._trailToggleKey+'On',
2710
+ #properties = '{Moved!=""}',
2711
+ #ranged = False
2712
+ ),
2713
+ #GlobalCommandTrait
2714
+ GlobalHotkeyTrait
2715
+ (
2716
+ name = '',
2717
+ #commandName = '',
2718
+ key = self._trailToggleKey+'Off',
2719
+ globalHotkey= self._trailToggleKey+'Off',
2720
+ #properties = '{Moved!=""}',
2721
+ #ranged = False
2722
+ ),
2723
+ ReportTrait(
2724
+ self._trailKey,
2725
+ report = (f'{{{self._debug}?("~"+'
2726
+ f'BasicName+" toggle trails"'
2727
+ f'):""}}')),
2728
+ ReportTrait(
2729
+ self._trailToggleKey+'Value',
2730
+ report = (f'{{{self._debug}?("~"+'
2731
+ f'BasicName+" trail state: "+'
2732
+ f'TrailsFlag):""}}')),
2733
+ ReportTrait(
2734
+ self._trailToggleKey+'Cmd',
2735
+ report = (f'{{{self._debug}?("~"+'
2736
+ f'BasicName+" send cmd: "+'
2737
+ f'TrailsFlag):""}}')),
2738
+ ReportTrait(
2739
+ self._trailToggleKey+'On',
2740
+ report = (f'{{{self._debug}?("~"+'
2741
+ f'BasicName+" turn on: "+'
2742
+ f'TrailsFlag):""}}')),
2743
+ ReportTrait(
2744
+ self._trailToggleKey+'Off',
2745
+ report = (f'{{{self._debug}?("~"+'
2746
+ f'BasicName+" turn off: "+'
2747
+ f'TrailsFlag):""}}')),
2748
+ ]
2749
+ if self._diceInit is not None:
2750
+ traits.extend(self._diceInit)
2751
+ traits.append(
2752
+ RestrictAccessTrait(sides=[],
2753
+ description='Fixed'))
2754
+
2755
+
2756
+ #if cn.startswith('odds marker'):
2757
+ # cn = cn.replace(':','_')
2758
+
2759
+ gpid = self._game.nextPieceSlotId()
2760
+ traits.extend([BasicTrait(name = c['name'],
2761
+ filename = c['filename'],
2762
+ gpid = gpid)])
2763
+
2764
+ ps = plist.addPieceSlot(entryName = cn,
2765
+ gpid = gpid,
2766
+ height = height,
2767
+ width = width,
2768
+ traits = traits)
2769
+ if cn == self._hiddenName:
2770
+ self._hidden = ps
2771
+ vv('] ',end='',flush=True,noindent=True)
2772
+
2773
+ vv('')
2774
+
2775
+
2776
+ # ----------------------------------------------------------------
2777
+ def addNotes(self,**kwargs):
2778
+ '''Add a `Notes` element to the module
2779
+
2780
+ Parameters
2781
+ ----------
2782
+ kwargs : dict
2783
+ Dictionary of attribute key-value pairs
2784
+ '''
2785
+ self._game.addNotes(**kwargs)
2786
+
2787
+ # ----------------------------------------------------------------
2788
+ def addInventory(self,**kwargs):
2789
+ '''Add a `Inventory` element to module
2790
+
2791
+ Parameters
2792
+ ----------
2793
+ kwargs : dict
2794
+ Dictionary of attribute key-value pairs
2795
+ '''
2796
+ filt = '{' + '||'.join([f'Faction=="{s}"' for s in self._sides])+'}'
2797
+ grp = 'Faction,Command,Echelon,Type'
2798
+ self._game.addInventory(include = filt,
2799
+ groupBy = grp,
2800
+ sortFormat = '$PieceName$',
2801
+ tooltip ='Show inventory of all pieces',
2802
+ zoomOn = True,
2803
+ **kwargs)
2804
+
2805
+ # ----------------------------------------------------------------
2806
+ def addBoard(self,name,info,hasFlipped=False):
2807
+ '''Add a `Board` element to module
2808
+
2809
+ Parameters
2810
+ ----------
2811
+ name : str
2812
+ Name of board
2813
+ info : dict
2814
+ Information on board image
2815
+ hasFlipped : bool
2816
+ True if any piece can be flipped
2817
+ '''
2818
+ with VerboseGuard(f'Adding board {name}') as v:
2819
+ # from pprint import pprint
2820
+ # pprint(info)
2821
+ map = self._game.addMap(mapName=name,
2822
+ markUnmovedHotkey=self._clearMoved)
2823
+ map.addCounterDetailViewer(
2824
+ propertyFilter=f'{{{self._battleMark}!=true}}',
2825
+ fontSize = 14,
2826
+ summaryReportFormat = '<b>$LocationName$</b>',
2827
+ hotkey = key('\n'),
2828
+ stopAfterShowing = True
2829
+ )
2830
+ map.addHidePiecesButton()
2831
+ map.addGlobalMap()
2832
+ # Basics
2833
+ map.addStackMetrics()
2834
+ map.addImageSaver()
2835
+ map.addTextSaver()
2836
+ map.addForwardToChatter()
2837
+ map.addMenuDisplayer()
2838
+ map.addMapCenterer()
2839
+ map.addStackExpander()
2840
+ map.addPieceMover()
2841
+ map.addKeyBufferer()
2842
+ map.addForwardKeys()# Be careful - does duplicate!
2843
+ map.addSelectionHighlighters()
2844
+ map.addHighlightLastMoved()
2845
+ map.addZoomer()
2846
+
2847
+ # Forward
2848
+ # map.addMassKey(
2849
+ # name = 'Movement trails',
2850
+ # buttonHotkey = '',
2851
+ # hotkey = self._trailKey,
2852
+ # icon = self.getIcon('trail-icon',
2853
+ # '/images/recenter.gif'),
2854
+ # tooltip = 'Toggle movement trails',
2855
+ # #filter = '{Moved!=""}', # Filter on MarkMoved
2856
+ # filter = f'{{BasicName=="{self._hiddenName}"}}',
2857
+ # target = '',
2858
+ # reportFormat = (f'{{{self._debug}?'
2859
+ # f'("`Movement trails toggled"):""}}'))
2860
+ map.addMassKey(
2861
+ name = '',
2862
+ buttonHotkey = self._trailToggleKey+'On',
2863
+ hotkey = self._trailToggleKey+'On',
2864
+ icon = '',
2865
+ tooltip = '',
2866
+ filter = '{Moved!=""}', # Filter on MarkMoved
2867
+ target = '',
2868
+ reportFormat = (f'{{{self._verbose}?'
2869
+ f'("`Movement trails toggled on"):""}}'))
2870
+ map.addMassKey(
2871
+ name = '',
2872
+ buttonHotkey = self._trailToggleKey+'Off',
2873
+ hotkey = self._trailToggleKey+'Off',
2874
+ icon = '',
2875
+ tooltip = '',
2876
+ filter = '{Moved!=""}', # Filter on MarkMoved
2877
+ target = '',
2878
+ reportFormat = (f'{{{self._verbose}?'
2879
+ f'("`Movement trails toggled off"):""}}'))
2880
+ map.addMassKey(
2881
+ name = 'Eliminate',
2882
+ buttonHotkey = self._eliminateKey,
2883
+ hotkey = self._eliminateKey,
2884
+ icon = self.getIcon('eliminate-icon',
2885
+ '/icons/16x16/edit-undo.png'),
2886
+ tooltip = 'Eliminate selected units')
2887
+ map.addMassKey(
2888
+ name = 'Delete',
2889
+ buttonHotkey = self._deleteKey,
2890
+ hotkey = self._deleteKey,
2891
+ icon = self.getIcon('delete-icon',
2892
+ '/icons/16x16/no.png'),
2893
+ tooltip = 'Delete selected units')
2894
+ map.addMassKey(
2895
+ name = 'Trail',
2896
+ buttonHotkey = self._trailKey,
2897
+ hotkey = self._trailKey,
2898
+ icon = '',
2899
+ tooltip = '')
2900
+ # Forward
2901
+ # map.addMassKey(
2902
+ # name='Rotate CW',
2903
+ # buttonHotkey = self._rotateCWKey,
2904
+ # hotkey = self._rotateCWKey,
2905
+ # icon = '', #/icons/16x16/no.png',
2906
+ # tooltip = 'Rotate selected units')
2907
+ # map.addMassKey(
2908
+ # name='Rotate CCW',
2909
+ # buttonHotkey = self._rotateCCWKey,
2910
+ # hotkey = self._rotateCCWKey,
2911
+ # icon = '', #/icons/16x16/no.png',
2912
+ # tooltip = 'Rotate selected units')
2913
+ map.addMassKey(
2914
+ name='Phase clear moved markers',
2915
+ buttonHotkey = self._clearMoved+'Phase',
2916
+ hotkey = self._clearMoved+'Trampoline',
2917
+ canDisable = True,
2918
+ target = '',
2919
+ filter = f'{{{self._battleCtrl}==true}}',
2920
+ propertyGate = f'{self._noClearMoves}',
2921
+ icon = '', #/icons/16x16/no.png',
2922
+ tooltip = 'Phase clear moved markers',
2923
+ reportFormat = (f'{{{self._debug}?'
2924
+ f'("~ {name}: '
2925
+ f'Phase Clear moved markers "+'
2926
+ f'{self._noClearMoves})'
2927
+ f':""}}'))
2928
+ if hasFlipped:
2929
+ map.addMassKey(
2930
+ name = 'Flip',
2931
+ buttonHotkey = self._flipKey,
2932
+ hotkey = self._flipKey,
2933
+ icon = self.getIcon('flip-icon',
2934
+ '/images/Undo16.gif'),
2935
+ tooltip = 'Flip selected units')
2936
+
2937
+ if len(self._battleMarks) > 0:
2938
+ v(f'Adding battle mark interface')
2939
+ ctrlSel = f'{{{self._battleCtrl}==true}}'
2940
+ ctrlSl2 = (f'{{{self._battleCtrl}==true&&'
2941
+ f'{self._markStart}!=true}}')
2942
+ oddsSel = f'{{{self._battleMark}==true}}'
2943
+ calcSel = f'{{{self._battleCalc}==true}}'
2944
+ curSel = (f'{{{self._battleNo}=={self._currentBattle}}}')
2945
+ curAtt = (f'{{{self._battleNo}=={self._currentBattle}&&'
2946
+ f'{self._battleUnit}==true&&'
2947
+ f'IsAttacker==true}}')
2948
+ curDef = (f'{{{self._battleNo}=={self._currentBattle}&&'
2949
+ f'{self._battleUnit}==true&&'
2950
+ f'IsAttacker==false}}')
2951
+ curUnt = (f'{{{self._battleNo}=={self._currentBattle}&&'
2952
+ f'{self._battleUnit}==true}}')
2953
+ markSel = (f'{{{self._battleNo}=={self._currentBattle}&&'
2954
+ f'{self._battleMark}==true&&'
2955
+ f'{self._oddsMark}!=true}}')
2956
+ markSel = (f'{{{self._battleNo}=={self._currentBattle}&&'
2957
+ f'{self._placedGlobal}!=true&&'
2958
+ f'{self._battleMark}==true&&'
2959
+ f'{self._oddsMark}!=true}}')
2960
+
2961
+ # ctrlSel = '{BasicName=="wg hidden unit"}'
2962
+ map.addMassKey(
2963
+ # This can come from a unit
2964
+ name = 'User mark battle',
2965
+ buttonHotkey = self._markBattle+'Unit',
2966
+ buttonText = '',
2967
+ hotkey = self._markBattle,
2968
+ icon = f'battle-marker-icon.{self._img_format}',
2969
+ tooltip = 'Mark battle (Ctrl-X)',
2970
+ target = '',
2971
+ singleMap = True, # Was False,
2972
+ filter = ctrlSel,
2973
+ reportFormat = (f'{{{self._verbose}?'
2974
+ f'("~{name}: '
2975
+ f'User marks battle # "+'
2976
+ f'{self._currentBattle})'
2977
+ f':""}}'))
2978
+ map.addMassKey(
2979
+ name = 'Selected mark battle',
2980
+ buttonHotkey = self._markBattle,
2981
+ hotkey = self._markBattle,
2982
+ icon = '',
2983
+ tooltip = '',
2984
+ singleMap = True, # Was False,
2985
+ reportFormat = (f'{{{self._debug}?'
2986
+ f'("~{name}: '
2987
+ f'Mark battle # "+'
2988
+ f'{self._currentBattle})'
2989
+ f':""}}'))
2990
+ map.addMassKey(
2991
+ name = 'Clear current battle',
2992
+ buttonText = '',
2993
+ buttonHotkey = self._clearBattle,
2994
+ hotkey = self._clearBattle,
2995
+ icon = '',
2996
+ tooltip = '',
2997
+ target = '',
2998
+ singleMap = True, # Was False,
2999
+ filter = curSel,
3000
+ reportFormat = (f'{{{self._debug}?'
3001
+ f'("~{name}: '
3002
+ f'Clear battle # "+'
3003
+ f'{self._currentBattle})'
3004
+ f':""}}'))
3005
+ map.addMassKey(
3006
+ name = 'Clear selected battle',
3007
+ buttonText = '',
3008
+ buttonHotkey = '',#self._clearKey,
3009
+ hotkey = self._clearKey,
3010
+ icon = '',
3011
+ tooltip = '',
3012
+ singleMap = False,
3013
+ reportFormat = (f'{{{self._debug}?'
3014
+ f'("~ {name}: '
3015
+ f'Clear battle # "+'
3016
+ f'{self._currentBattle})'
3017
+ f':""}}'))
3018
+ map.addMassKey(
3019
+ name = 'Clear all battles',
3020
+ buttonText = '',
3021
+ buttonHotkey = self._clearAllBattle,
3022
+ hotkey = self._clearBattle,
3023
+ icon = '',
3024
+ tooltip = '',
3025
+ target = '',
3026
+ singleMap = True, # Was False,
3027
+ reportFormat = (f'{{{self._debug}?'
3028
+ f'("~ {name}: '
3029
+ f'Clear all battle markers")'
3030
+ f':""}}'))
3031
+ map.addMassKey(
3032
+ name = 'User clear all battles',
3033
+ buttonText = '',
3034
+ buttonHotkey = self._clearAllKey,
3035
+ hotkey = self._clearAllBattle,
3036
+ icon = f'clear-battles-icon.{self._img_format}',
3037
+ tooltip = 'Clear all battles',
3038
+ target = '',
3039
+ singleMap = True, # False,
3040
+ filter = ctrlSel,
3041
+ reportFormat = (f'{{{self._debug}?'
3042
+ f'("~ {name}: '
3043
+ f'User clears battle markers")'
3044
+ f':""}}'))
3045
+ map.addMassKey(
3046
+ name = 'Phase clear all battles',
3047
+ buttonText = '',
3048
+ buttonHotkey = self._clearBattlePhs,
3049
+ hotkey = self._clearAllBattle,
3050
+ icon = '',
3051
+ tooltip = 'Clear all battles',
3052
+ canDisable = True,
3053
+ propertyGate = f'{self._noClearBattles}',
3054
+ target = '',
3055
+ singleMap = True, # Was False,
3056
+ filter = ctrlSel,
3057
+ reportFormat = (f'{{{self._debug}?'
3058
+ f'("~ {name}: '
3059
+ f'Phase clears battle markers "+'
3060
+ f'{self._noClearBattles})'
3061
+ f':""}}'))
3062
+ map.addMassKey(
3063
+ name = 'Selected resolve battle',
3064
+ buttonHotkey = '',#self._resolveKey,
3065
+ hotkey = self._resolveKey,
3066
+ icon = f'resolve-battles-icon.{self._img_format}',
3067
+ tooltip = 'Resolve battle',
3068
+ singleMap = True, # False,
3069
+ filter = oddsSel,
3070
+ reportFormat = (f'{{{self._debug}?'
3071
+ f'("~ {name}: '
3072
+ f'Resolve battle # "+'
3073
+ f'{self._currentBattle})'
3074
+ f':""}}'))
3075
+ map.addMassKey(
3076
+ name = 'Sum AFs',
3077
+ buttonText = '',
3078
+ buttonHotkey = self._calcBattleAF,
3079
+ hotkey = self._calcBattleAF,
3080
+ icon = '',
3081
+ tooltip = '',
3082
+ target = '',
3083
+ singleMap = True, # False,
3084
+ filter = curAtt,
3085
+ reportFormat = (f'{{{self._debug}?'
3086
+ f'("~ {name}: '
3087
+ f'Calculate total AF"):""}}'))
3088
+ map.addMassKey(
3089
+ name = 'Sum DFs',
3090
+ buttonText = '',
3091
+ buttonHotkey = self._calcBattleDF,
3092
+ hotkey = self._calcBattleDF,
3093
+ icon = '',
3094
+ tooltip = '',
3095
+ target = '',
3096
+ singleMap = True, # Was False,
3097
+ filter = curDef,
3098
+ reportFormat = (f'{{{self._debug}?'
3099
+ f'("~ {name}: '
3100
+ f'Calculate total DF"):""}}'))
3101
+ map.addMassKey(
3102
+ name = 'Sum odds shifts',
3103
+ buttonText = '',
3104
+ buttonHotkey = self._calcBattleShft,
3105
+ hotkey = self._calcBattleShft,
3106
+ icon = '',
3107
+ tooltip = '',
3108
+ target = '',
3109
+ singleMap = True, # False,
3110
+ filter = curUnt,
3111
+ reportFormat = (f'{{{self._debug}?'
3112
+ f'("~ {name}: '
3113
+ f'Calculate odds shift"):""}}'))
3114
+ map.addMassKey(
3115
+ name = 'Sum die roll modifiers',
3116
+ buttonText = '',
3117
+ buttonHotkey = self._calcBattleDRM,
3118
+ hotkey = self._calcBattleDRM,
3119
+ icon = '',
3120
+ tooltip = '',
3121
+ target = '',
3122
+ singleMap = True, # False,
3123
+ filter = curUnt,
3124
+ reportFormat = (f'{{{self._debug}?'
3125
+ f'("~ {name}: '
3126
+ f'Calculate DRM"):""}}'))
3127
+ map.addMassKey(
3128
+ name = 'Calc battle odds',
3129
+ buttonText = '',
3130
+ buttonHotkey = self._calcBattleOdds,
3131
+ hotkey = self._calcBattleOdds,
3132
+ icon = '',
3133
+ tooltip = '',
3134
+ target = '',
3135
+ singleMap = True, # Was False,
3136
+ filter = calcSel,
3137
+ reportFormat = (f'{{{self._debug}?'
3138
+ f'("~ {name}: '
3139
+ f'Calculate odds"):""}}'))
3140
+ # If `target` is set to the nothing, then the command
3141
+ # is sent to all - which means that will get duplicate
3142
+ # odd markers. If set to selected, then we have
3143
+ # deselected all but one, and so we will not get
3144
+ # duplicate odds markers. However, we may get into the
3145
+ # situation where a battle marker isn't selected (for
3146
+ # example becuase the unit is in a different layer),
3147
+ # which means we will not get the right calculations.
3148
+ #
3149
+ # IF I can find a way to not get double ciate markers,
3150
+ # then it would be preferable to set "target" to the
3151
+ # empty string.
3152
+ #
3153
+ # Found that way - keep track ourselves of whether
3154
+ # this has been called, and only dispatch if it
3155
+ # hasn't. Requires a reset to be done before hand,
3156
+ # and a set of flag in battle marker. That is, we do
3157
+ # not rely on the VASSAL selection mechanism, which
3158
+ # has it's own quirks.
3159
+ map.addMassKey(
3160
+ name = 'Auto calc battle odds',
3161
+ buttonText = '',
3162
+ buttonHotkey = self._calcBattleOdds+'Auto',
3163
+ hotkey = self._calcBattleOdds+'Start',
3164
+ icon = '',
3165
+ tooltip = '',
3166
+ target = '', # Was commented ?
3167
+ singleMap = True, # Was False,
3168
+ filter = markSel,
3169
+ reportFormat = (
3170
+ f'{{{self._debug}?'
3171
+ f'("~{name}: Auto calculate odds # "+'
3172
+ f'{self._currentBattle}+" "+'
3173
+ f'{self._placedGlobal}+" "+'
3174
+ f'"{markSel}"):""}}'))
3175
+ map.addMassKey(
3176
+ name = 'User recalc',
3177
+ buttonHotkey = self._recalcOdds,
3178
+ buttonText = '',
3179
+ hotkey = self._recalcOdds,
3180
+ icon = '',
3181
+ tooltip = 'Recalculate odds',
3182
+ singleMap = False,
3183
+ filter = '',
3184
+ reportFormat = (f'{{{self._debug}?'
3185
+ f'("~ {name}: '
3186
+ f'Recalculate odds"):""}}'))
3187
+ map.addMassKey(
3188
+ name = 'Auto recalc battle odds',
3189
+ buttonText = '',
3190
+ buttonHotkey = self._calcBattleOdds+'ReAuto',
3191
+ hotkey = self._calcBattleOdds+'Start',
3192
+ icon = '',
3193
+ tooltip = '',
3194
+ target = '',
3195
+ singleMap = False,
3196
+ filter = markSel,
3197
+ reportFormat = (f'{{{self._debug}?'
3198
+ f'("~ {name}: '
3199
+ f'Auto re-calculate odds of "+'
3200
+ f'{self._currentBattle}):'
3201
+ f'""}}'))
3202
+
3203
+
3204
+ v(f'Getting the board dimensions')
3205
+ ulx,uly,lrx,lry = self.getBB(info['img'])
3206
+ width = int(abs(ulx - lrx))
3207
+ height = int(abs(uly - lry))
3208
+ # Why is it we take the width and height like this?
3209
+ # Do they every differ from the above?
3210
+ # This is the only place that we actually use this
3211
+ #
3212
+ # width, height = self.getWH(info['img'])
3213
+ height += 20
3214
+ width += 5
3215
+ # v(f'{ulx},{uly},{lrx},{lry}')
3216
+
3217
+ v(f'Board BB=({lrx},{lry})x({ulx},{uly}) {width}x{height}')
3218
+ picker = map.addBoardPicker()
3219
+ board = picker.addBoard(name = name,
3220
+ image = info['filename'],
3221
+ width = width,
3222
+ height = height)
3223
+ zoned = board.addZonedGrid()
3224
+ zoned.addHighlighter()
3225
+
3226
+ if not 'zones' in info:
3227
+ color = rgb(255,0,0)
3228
+ full = zoned.addZone(name = 'full',
3229
+ useParentGrid = False,
3230
+ path=(f'{ulx},{uly};' +
3231
+ f'{lrx},{uly};' +
3232
+ f'{lrx},{lry};' +
3233
+ f'{ulx},{lry}'))
3234
+ grid = full.addHexGrid(color = color,
3235
+ dx = HEX_WIDTH,
3236
+ dy = HEX_HEIGHT,
3237
+ visible = self._visible)
3238
+ grid.addNumbering(color = color,
3239
+ hType = 'A',
3240
+ hOff = -1,
3241
+ vType = 'N',
3242
+ vOff = -1,
3243
+ visible = self._visible)
3244
+ return
3245
+
3246
+ w = abs(ulx-lrx)
3247
+ h = abs(uly-lry)
3248
+ self.addZones(zoned,name,info['zones'],w,h)
3249
+
3250
+ if self._hidden is not None:
3251
+ v(f'Adding hidden unit to map {name}')
3252
+ at = map.addAtStart(name = self._hiddenName,
3253
+ location = '',
3254
+ useGridLocation = False,
3255
+ owningBoard = name,
3256
+ x = 0,
3257
+ y = 0)
3258
+ at.addPieces(self._hidden)
3259
+
3260
+
3261
+ # ----------------------------------------------------------------
3262
+ def addDeadMap(self):
3263
+ '''Add a "Dead Map" element to the module
3264
+ '''
3265
+ name = 'DeadMap'
3266
+ with VerboseGuard(f'Adding deadmap {name}') as v:
3267
+ map = self._game.addMap(
3268
+ mapName = name,
3269
+ buttonName = '',
3270
+ markMoved = 'Never',
3271
+ launch = True,
3272
+ icon = self.getIcon('pool-icon',
3273
+ '/images/playerAway.gif'),
3274
+ allowMultiple = True,
3275
+ hotkey = self._deadKey)
3276
+ # Basics
3277
+ map.addStackMetrics()
3278
+ map.addImageSaver()
3279
+ map.addTextSaver()
3280
+ map.addForwardToChatter()
3281
+ map.addMenuDisplayer()
3282
+ map.addMapCenterer()
3283
+ map.addStackExpander()
3284
+ map.addPieceMover()
3285
+ map.addKeyBufferer()
3286
+ map.addSelectionHighlighters()
3287
+ map.addHighlightLastMoved()
3288
+ map.addZoomer()
3289
+
3290
+ map.addMassKey(
3291
+ name='Restore',
3292
+ buttonHotkey = self._restoreKey,
3293
+ hotkey = self._restoreKey,
3294
+ icon = self.getIcon('restore-icon',
3295
+ '/images/Undo16.gif'),
3296
+ tooltip = 'Restore selected units')
3297
+
3298
+ picker = map.addBoardPicker()
3299
+ picker.addSetup(maxColumns=len(self._sides),mapName=name,
3300
+ boardNames=[s+' pool' for s in self._sides])
3301
+
3302
+ for i, s in enumerate(self._sides):
3303
+ v(f'Adding {s} pool')
3304
+ color = [0,0,0,64]
3305
+ color[i % 3] = 255
3306
+ w = 400
3307
+ h = 400
3308
+ c = rgba(*color)
3309
+ img = ''
3310
+ dimg = self._categories.get('pool',{}).get('all',{})\
3311
+ .get(s,None)
3312
+
3313
+ if dimg:
3314
+ bb = self.getBB(dimg['img'])
3315
+ w = bb[2] - bb[0]
3316
+ h = bb[3] - bb[1]
3317
+ c = ''
3318
+ img = dimg['filename']
3319
+ v(f'Using image provided by user {img}')
3320
+
3321
+ board = picker.addBoard(name = f'{s} pool',
3322
+ image = img,
3323
+ width = w,
3324
+ height = h,
3325
+ color = c)
3326
+
3327
+ if dimg is None or not 'zones' in dimg:
3328
+ continue
3329
+
3330
+ zoned = board.addZonedGrid()
3331
+ zoned.addHighlighter()
3332
+ w = abs(w)
3333
+ h = abs(h)
3334
+ self.addZones(zoned,board['name'],dimg['zones'],w,h)
3335
+
3336
+
3337
+ # --------------------------------------------------------------------
3338
+ def getPictureInfo(self,picture,name,width,height):
3339
+ '''
3340
+ Returns
3341
+ -------
3342
+ hex_width, hex_height : float, float
3343
+ Scale hex width
3344
+ scx, scy : float, float, float, float
3345
+ Scale to image and picture (x,y)
3346
+ rot90 : bool
3347
+ True if rotated +/-90 degrees
3348
+ tran : callable
3349
+ Translation function
3350
+ '''
3351
+ if picture is None:
3352
+ print(f'WARNING: No Tikz picture information.'
3353
+ f"Are you sure you used the `[zoned]' option for the "
3354
+ f"tikzpicture environment of {name}?")
3355
+ f = lambda x,y: (x,y)
3356
+ return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
3357
+
3358
+ # Get picture bounding box
3359
+ tll = picture['lower left']
3360
+ tur = picture['upper right']
3361
+ # Get picture transformation
3362
+ pa = picture['xx']
3363
+ pb = picture['xy']
3364
+ pc = picture['yx']
3365
+ pd = picture['yy']
3366
+ # Get picture offset (always 0,0?)
3367
+ pdx = picture['dx']
3368
+ pdy = picture['dy']
3369
+ # Define picture global transformation
3370
+ pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
3371
+ # Globally transform (rotate) picture bounding box
3372
+ pll = pr(*tll)
3373
+ pur = pr(*tur)
3374
+ # Calculate widht, height, and scaling factors
3375
+ pw = pur[0] - pll[0]
3376
+ ph = pur[1] - pll[1]
3377
+ scw = width / pw
3378
+ sch = height / ph
3379
+ # Extract picture scales and rotation
3380
+ # Courtesy of
3381
+ # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
3382
+ from math import sqrt, atan2, degrees, isclose
3383
+ psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
3384
+ psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
3385
+ prt = degrees(atan2(pc,pd))
3386
+ if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
3387
+ raise RuntimeException('Rotations of Tikz pictures other than '
3388
+ '0 or +/-90,+/- 180, or +/-270 not supported. '
3389
+ 'found {prt}')
3390
+ rot90 = int(prt // 90)
3391
+ if rot90 == 2: rot90 = -2
3392
+ # Now supported
3393
+ # if any([isclose(prt,a) for a in [90,270,180,-180]]):
3394
+ # print(f'WARNING: rotations by {prt} not fully supported')
3395
+
3396
+ from math import sqrt
3397
+ hex_width = psx * scw * 2 # HEX_WIDTH
3398
+ hex_height = psy * sch * sqrt(3) # HEX_HEIGHT
3399
+ with VerboseGuard('Picture') as v:
3400
+ v(f'Transformations: {pa},{pb},{pc},{pd}')
3401
+ v(f'Scale (x,y): {psx},{psy}')
3402
+ v(f'Rotation (degrees): {prt} ({rot90})')
3403
+ v(f'Scale to pixels (x,y): {scw},{sch}')
3404
+
3405
+ # When translating the Tikz coordinates, it is important to note
3406
+ # that the Tikz y-axis point upwards, while the picture y-axis
3407
+ # point downwards. This means that the upper right corner is at
3408
+ # (width,0) and the lower left corner is at (0,height).
3409
+ def tranx(x,off=-pll[0]):
3410
+ # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
3411
+ return int(scw * (x + off)+.5)
3412
+ def trany(y,off=-pur[1]):
3413
+ # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
3414
+ return -int(sch * (y + off)+.5)
3415
+ tran = lambda x,y : (tranx(x), trany(y))
3416
+
3417
+ return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
3418
+
3419
+ # --------------------------------------------------------------------
3420
+ def getHexParams(self,
3421
+ llx,
3422
+ lly,
3423
+ urx,
3424
+ ury,
3425
+ mx,
3426
+ my,
3427
+ hex_width,
3428
+ hex_height,
3429
+ rot90,
3430
+ labels,
3431
+ coords,
3432
+ targs,
3433
+ nargs):
3434
+ '''rot90 = 0 No rotation
3435
+ = 1 Rotated -90 (clock-wise)
3436
+ = -1 Rotated 90 (counter clock-wise)
3437
+ = -2 Rotated 180
3438
+ '''
3439
+ with VerboseGuard('Hex parameters') as v:
3440
+ from math import sqrt
3441
+ isodd = lambda x : (x % 2 == 1)
3442
+ iseven = lambda x : (x % 2 == 0)
3443
+ isfalse = lambda x : False
3444
+ shorts = {'isodd': isodd, 'iseven': iseven, 'isfalse': isfalse }
3445
+
3446
+ # Funny scaling needed by VASSAL. Seems like they only
3447
+ # really about the absolute value of 'dy' and then the
3448
+ # aspect ratio between dx and dy.
3449
+ pxfac = sqrt(3)/2
3450
+ hex_pw = hex_height * pxfac
3451
+ hex_ph = hex_width * pxfac
3452
+ stagger = False
3453
+ #
3454
+ # Get parameters from coordinates. These should always be set
3455
+ #
3456
+ rows = coords .get('row', {})
3457
+ columns = coords .get('column',{})
3458
+ top_short = columns .get('top short', 'isfalse')
3459
+ bot_short = columns .get('bottom short','isfalse')
3460
+ inv_col = columns .get('factor',1)
3461
+ inv_row = rows .get('factor',1)
3462
+ voff = -rows .get('offset',0) # 0: from 0 -> -1
3463
+ hoff = -columns.get('offset',0) # -1: from 1 -> -2
3464
+ vdesc = inv_row == 1
3465
+ hdesc = inv_col == -1
3466
+ #
3467
+ # Calculate total dimensions, and number of columns and rows
3468
+ #
3469
+ w = abs((urx-llx) - 2 * mx)
3470
+ h = abs((ury-lly) - 2 * my)
3471
+ if abs(rot90) == 1: h, w = w, h
3472
+ nc = int(w // (hex_width * 3 / 4))
3473
+ nr = int(h // (hex_height))
3474
+ namrot = {0: 'none - 0',
3475
+ -1: '-90 - CCW',
3476
+ 1: '90 CW',
3477
+ -2: '180 - half-turn'}
3478
+
3479
+ v(f'Width: {w}')
3480
+ v(f'Height: {h}')
3481
+ v(f'Margins: x={mx} y={my}')
3482
+ v(f'Rotation: {rot90} ({namrot[rot90]})')
3483
+ v(f'Labels: {labels}')
3484
+ v(f'Columns:')
3485
+ v(f' size: {nc}')
3486
+ v(f' start: {hoff}')
3487
+ v(f' direction: {inv_col}')
3488
+ v(f' top short: {top_short}')
3489
+ v(f' bottom short: {bot_short}')
3490
+ v(f'Rows:')
3491
+ v(f' size: {nr}')
3492
+ v(f' start: {voff}')
3493
+ v(f' direction: {inv_row}')
3494
+ v(f'Image:')
3495
+ v(f' BB: ({llx},{lly}) x ({urx},{ury})')
3496
+ #
3497
+ # X0 and Y0 are in the local (rotated) frame of the hex grid.
3498
+ # Thus X is always along hex breadth, and Y along the
3499
+ # height. Thus the base offset (rotated into the hex frame) differs.
3500
+ x0 = ury if abs(rot90) == 1 else llx
3501
+ y0 = llx if abs(rot90) == 1 else ury
3502
+ # Calculate column,row of corners
3503
+ llc = hoff
3504
+ ulc = hoff
3505
+ lrc = hoff+nc-1
3506
+ urc = hoff+nc-1
3507
+ #
3508
+ # Swap in directions
3509
+ if hdesc: llc, lrc, ulc, urc = lrc, llc, urc, ulc
3510
+ #
3511
+ is_short_top = shorts[columns.get('top short', 'isfalse')]
3512
+ is_short_bot = shorts[columns.get('bottom short','isfalse')]
3513
+ if is_short_top is isfalse:
3514
+ # Assume fully populated columns
3515
+ is_short_top = isodd if iseven(hoff) else iseven
3516
+ if is_short_bot is isfalse:
3517
+ is_short_bot = isodd if isodd(hoff) else iseven
3518
+
3519
+ #
3520
+ # Now we have the hex coordinates of the corners. We can
3521
+ # now check how things are offset. Before rotation, we
3522
+ # will have that the first column is offset by hex_pw / 2.
3523
+ x0 += hex_width / 2
3524
+ #
3525
+ # If the first column is _not_ short on top, then off set
3526
+ # is simply hex_ph / 2. Otherwise, the offset is hex_ph
3527
+ y0 += hex_ph / 2
3528
+ voff -= 1
3529
+ voff -= inv_row
3530
+ v(f' Initial offset of image {x0},{y0}')
3531
+
3532
+ # Treat each kind of rotation separately. Note that -90 and
3533
+ # 180 uses the `is_short_bot' while 0 and 90 uses
3534
+ # `is_short_top'. There might be a way to unify these, if
3535
+ # offsets and so on may warrent it, but it may be complete
3536
+ # overkill.
3537
+ is_off = False
3538
+ col_map = {0 : (ulc, is_short_top, is_short_bot),
3539
+ -1 : (urc, is_short_top, is_short_bot),
3540
+ 1 : (ulc, is_short_bot, is_short_top),
3541
+ -2 : (urc, is_short_bot, is_short_top) }
3542
+ col_chk, is_s1, is_s2 = col_map[rot90]
3543
+
3544
+ is_off = is_s1(col_chk)
3545
+ if is_off:
3546
+ y0 += hex_ph /2
3547
+
3548
+ v(f'Is first column off: {is_off}')
3549
+
3550
+ # For full columns, noting more is needed
3551
+ #
3552
+ # Below is if some columns are short both top and bottom.
3553
+ # VASSAL seems to start numbering from a given place, and
3554
+ # then use that for the rest numbering, and forgets to
3555
+ # take into account various offsets and the like. hence,
3556
+ # we need to hack it hard.
3557
+ if iseven(nc):
3558
+ v(f'Even number of columns, perhaps hacks')
3559
+ if rot90 == 0:
3560
+ # Hacks
3561
+ #
3562
+ # If the last column is short in both top and bottom,
3563
+ # and we have inverse columns, but not inverse rows,
3564
+ # then add to offset
3565
+ if inv_col == -1 and inv_row == 1 and \
3566
+ is_s1(urc) and is_s2(urc):
3567
+ voff += 1
3568
+ # If the column we check for short is short both top
3569
+ # and bottom, and we have inverse rows, but not
3570
+ # inverse columns, then add offset
3571
+ if inv_row == -1 and inv_col == 1 and \
3572
+ is_s2(col_chk) and is_off:
3573
+ voff += 1
3574
+
3575
+ if rot90 == -1:
3576
+ # If the last column is short in both top and bottom,
3577
+ # and we have inverse columns, then add to offset
3578
+ if is_s1(urc) and inv_col == -1 and is_s2(urc):
3579
+ voff -= inv_row
3580
+
3581
+ if rot90 == 1:
3582
+ voff += inv_row + (inv_row == 1)
3583
+ # If the first column is short in both top and bottom,
3584
+ # and we have inverse columns, then add to offset
3585
+ if is_s1(ulc) and is_s2(ulc) and inv_col == -1:
3586
+ voff += inv_row
3587
+
3588
+ if rot90 == -2:
3589
+ voff += inv_row * 2
3590
+ # Hacks If the column we check for short is short both
3591
+ # top and bottom, and we have either inverse rows and
3592
+ # inverse columns, or rows and columns are normal,
3593
+ # then add offset
3594
+ if inv_col == inv_row and is_s1(col_chk) and is_s2(col_chk):
3595
+ voff += 1
3596
+ # If the first column is short in both top and bottom,
3597
+ # and we have inverse columns and rows, then add to
3598
+ # offset
3599
+ if inv_col == inv_row and inv_col == -1 and \
3600
+ is_s1(ulc) and is_s2(ulc):
3601
+ voff += 1
3602
+ else:
3603
+ v(f'Odd number of columns')
3604
+ voff -= inv_row
3605
+ if rot90 == 1:
3606
+ # If we offset in the column direction, add the
3607
+ # inverse row direction, and if we have inverse rows,
3608
+ # substract one, otherwise add 2.
3609
+ voff += (inv_row * hoff + (-1 if inv_row == -1 else 2))
3610
+ # If we have a short column, and that column is even,
3611
+ # then add, otherwise subtract, the inverse row
3612
+ # direction, if the checked column is even.
3613
+ voff += ((1 if is_off else -1) *
3614
+ inv_row if is_short_bot(2) else 0)
3615
+ if rot90 == 2:
3616
+ voff += inv_row * (2 + is_off) # OK for odd
3617
+
3618
+
3619
+ if rot90 == 0:
3620
+ if inv_col == -1 and iseven(nc): # OK
3621
+ stagger = not stagger
3622
+ hoff -= (inv_col == -1) # OK
3623
+
3624
+ if rot90 == -1: # CCW
3625
+ if inv_col == 1 and iseven(nc): # OK
3626
+ stagger = not stagger
3627
+ vdesc, hdesc = hdesc, vdesc
3628
+ vdesc = not vdesc
3629
+ voff += (inv_row == 1)
3630
+ hoff -= (inv_col == 1) # OK
3631
+
3632
+ if rot90 == 1: # CW
3633
+ if (inv_col == 1 and iseven(nc)) or isodd(nc): # OK
3634
+ stagger = not stagger
3635
+ vdesc, hdesc = hdesc, vdesc
3636
+ hdesc = not hdesc
3637
+ hoff -= (inv_col == -1) # OK
3638
+
3639
+ if rot90 == -2:
3640
+ if (inv_col == -1 and iseven(nc)) or isodd(nc): # OK
3641
+ stagger = not stagger
3642
+ vdesc, hdesc = not vdesc, not hdesc
3643
+ hoff -= (inv_col == 1) # OK
3644
+
3645
+ # Labels
3646
+ if labels is not None:
3647
+ labmap = {
3648
+ 'auto': {
3649
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
3650
+ 'auto=numbers' : {
3651
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
3652
+ 'auto=alpha column': {
3653
+ 'hLeading': 0,'vLeading': 0,'hType': 'A','vType': 'N' },
3654
+ 'auto=alpha 2 column': {# Not supported
3655
+ 'hLeading': 1,'vLeading': 1,'hType': 'A','vType': 'N' },
3656
+ 'auto=inv y x plus 1': {
3657
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
3658
+ 'auto=x and y plus 1': {
3659
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }
3660
+ }
3661
+ for l in labels.split(','):
3662
+ nargs.update(labmap.get(l,{}))
3663
+ if 'alpha column' in l or 'alpha 2 column' in l:
3664
+ hoff -= 1 # VASSAL 0->A, wargame 1->A
3665
+ if l == 'auto=inv y x plus 1':
3666
+ hoff += 1
3667
+ #inv_row = not inv_row
3668
+ if l == 'auto=x and y plus 1':
3669
+ hoff -= 1
3670
+ voff -= 1
3671
+
3672
+ # Add margins
3673
+ x0 += int(mx)
3674
+ y0 += int(my)
3675
+
3676
+ targs['dx'] = hex_pw
3677
+ targs['dy'] = hex_ph
3678
+ nargs['vOff'] = voff
3679
+ nargs['hOff'] = hoff
3680
+ nargs['vDescend'] = vdesc
3681
+ nargs['hDescend'] = hdesc
3682
+ targs['edgesLegal'] = True
3683
+ targs['sideways'] = abs(rot90) == 1
3684
+ nargs['stagger'] = stagger
3685
+ targs['x0'] = int(x0+.5)
3686
+ targs['y0'] = int(y0+.5)
3687
+
3688
+ # --------------------------------------------------------------------
3689
+ def getRectParams(self,i,llx,ury,width,height,targs,nargs):
3690
+ targs['dx'] = width
3691
+ targs['dy'] = height
3692
+ targs['x0'] = int(llx - width/2)
3693
+ targs['y0'] = int(ury + height/2)
3694
+ targs['color'] = rgb(0,255,0)
3695
+ nargs['color'] = rgb(0,255,0)
3696
+ nargs['vDescend'] = True
3697
+ nargs['vOff'] = -3
3698
+ nargs.update({'sep':',','vLeading':0,'hLeading':0})
3699
+
3700
+ # ----------------------------------------------------------------
3701
+ def addZones(self,
3702
+ zoned,
3703
+ name,
3704
+ info,
3705
+ width,
3706
+ height,
3707
+ labels=None,
3708
+ coords=None,
3709
+ picinfo=None):
3710
+ '''Add zones to the Zoned element.
3711
+
3712
+ Parameters
3713
+ ----------
3714
+ zoned : Zoned
3715
+ Parent element
3716
+ name : str
3717
+ Name of Zoned
3718
+ info : dict
3719
+ Dictionary of zones informatio
3720
+ width : int
3721
+ Width of parent
3722
+ height : int
3723
+ Height of parent
3724
+ labels : list
3725
+ On recursive call, list of labels
3726
+ coords : list
3727
+ On recursive call, coordinates
3728
+ picinfo : dict
3729
+ On recursive call, picture information
3730
+ '''
3731
+ grids = []
3732
+ picture = None
3733
+
3734
+ with VerboseGuard(f'Adding zones to {name}') as v:
3735
+ for k, val in info.items():
3736
+ if k == 'labels': labels = val;
3737
+ if k == 'coords': coords = val
3738
+ if k == 'zoned': picture = val
3739
+ if 'zone' not in k or k == 'zoned':
3740
+ continue
3741
+
3742
+ grids = [[k,val]] + grids # Reverse order!
3743
+ # grids.append([k,v])
3744
+
3745
+ if len(grids) < 1:
3746
+ return
3747
+
3748
+ if picinfo is None:
3749
+ picinfo = self.getPictureInfo(picture,name,width,height)
3750
+
3751
+ hex_width, hex_height, scx, scy, rot90, tran = picinfo
3752
+
3753
+ for g in grids:
3754
+ n, i = g
3755
+ v(f'Adding zone {n}')
3756
+
3757
+ if 'scope' in n:
3758
+ llx,lly = tran(*i['global lower left'])
3759
+ urx,ury = tran(*i['global upper right'])
3760
+ path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
3761
+ nm = n.replace('zone scope ','')
3762
+ elif 'path' in n:
3763
+ path = [tran(*p) for p in i['path']]
3764
+ llx = min([px for px,py in path])
3765
+ ury = max([py for px,py in path])
3766
+ nm = n.replace('zone path ','')
3767
+
3768
+ # Checkf if we have "point" type elements in this object and
3769
+ # add them to dict.
3770
+ points = [ val for k,val in i.items()
3771
+ if (k.startswith('point') and
3772
+ isinstance(val,dict) and \
3773
+ val.get('type','') == 'point')]
3774
+
3775
+ pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
3776
+ v(f'Zone path ({llx},{ury}): {pathstr} ({len(points)})')
3777
+
3778
+ ispool = 'pool' in n.lower() and len(points) <= 0
3779
+ zone = zoned.addZone(name = nm,
3780
+ locationFormat = ("$name$"
3781
+ if ispool else
3782
+ "$gridLocation$"),
3783
+ useParentGrid = False,
3784
+ path = pathstr)
3785
+
3786
+ # Do not add grids to pools
3787
+ if ispool:
3788
+ v(f'Board {n} is pool with no points')
3789
+ continue
3790
+
3791
+ targs = {'color':rgb(255,0,0),'visible':self._visible}
3792
+ nargs = {'color':rgb(255,0,0),'visible':self._visible}
3793
+ # print(targs,nargs)
3794
+ if 'turn' in n.lower(): nargs['sep'] = 'T'
3795
+ if 'oob' in n.lower(): nargs['sep'] = 'O'
3796
+
3797
+ if len(points) > 0:
3798
+ with VerboseGuard('Using region grid') as vv:
3799
+ grid = zone.addRegionGrid(snapto = True,
3800
+ visible = self._visible)
3801
+ for j,p in enumerate(points):
3802
+ pn = p["name"].strip()
3803
+ pp = p.get('parent','').strip()
3804
+ pc = p["coords"]
3805
+ if j == 0: vv(f'',end='')
3806
+ vv(f'[{pn}] ',end='',flush=True,noindent=True)
3807
+
3808
+ if pn.endswith(' flipped'):
3809
+ pn = pn[:-len(' flipped')]
3810
+
3811
+ x, y = tran(*pc)
3812
+ r = grid.addRegion(name = pn,
3813
+ originx = x,
3814
+ originy = y,
3815
+ alsoPiece = True,
3816
+ prefix = pp)
3817
+ v('')
3818
+
3819
+ elif 'hex' in n.lower():
3820
+ margin = i.get('board frame',{}).get('margin',0)
3821
+ mx = scx * margin
3822
+ my = scy * margin
3823
+ # self.message(f'{margin} -> {scx},{scy} -> {mx},{my}')
3824
+ w = abs(urx - llx)-2*mx
3825
+ h = abs(ury - lly)-2*my
3826
+ self.getHexParams(llx = llx,
3827
+ lly = lly,
3828
+ urx = urx,
3829
+ ury = ury,
3830
+ mx = mx,
3831
+ my = my,
3832
+ hex_width = hex_width,
3833
+ hex_height = hex_height,
3834
+ rot90 = rot90,
3835
+ labels = labels,
3836
+ coords = coords,
3837
+ targs = targs,
3838
+ nargs = nargs)
3839
+
3840
+ v(f'Adding hex grid')
3841
+
3842
+ grid = zone.addHexGrid(**targs)
3843
+ grid.addNumbering(**nargs)
3844
+
3845
+ else:
3846
+ width = hex_width / HEX_WIDTH * RECT_WIDTH
3847
+ height = hex_height / HEX_HEIGHT * RECT_HEIGHT
3848
+ self.getRectParams(i,llx,ury,width,height,targs,nargs)
3849
+
3850
+ v(f'Adding rectangular grid')
3851
+
3852
+ grid = zone.addSquareGrid(**targs)
3853
+ grid.addNumbering(**nargs)
3854
+
3855
+
3856
+ # Once we've dealt with this grid, we should see if we have
3857
+ # any embedded zones we should deal with.
3858
+ self.addZones(zoned,name,i,width,height,
3859
+ labels=labels,
3860
+ coords=coords,
3861
+ picinfo=picinfo)
3862
+
3863
+
3864
+ # ----------------------------------------------------------------
3865
+ def addBoards(self):
3866
+ '''Add Boards to the module
3867
+ '''
3868
+ with VerboseGuard('Adding boards') as v:
3869
+ hasFlipped = False
3870
+ for cn,cd in self._categories.get('counter',{}).items():
3871
+ for sn in cd:
3872
+ if ' flipped' in sn:
3873
+ hasFlipped = True
3874
+ break
3875
+
3876
+ v(f'Has flipped? {hasFlipped}')
3877
+ for bn, b in self._categories.get('board',{}).get('all',{}).items():
3878
+ self.addBoard(bn, b,hasFlipped=hasFlipped)
3879
+
3880
+
3881
+ # ----------------------------------------------------------------
3882
+ def getIcon(self,name,otherwise):
3883
+ with VerboseGuard(f'Get Icon {name}') as v:
3884
+ icon = self._categories\
3885
+ .get('icon',{})\
3886
+ .get('all',{})\
3887
+ .get(name,{
3888
+ 'filename':otherwise})['filename']
3889
+ v(f'Using "{icon}"')
3890
+ return icon
3891
+
3892
+ # ----------------------------------------------------------------
3893
+ def addOOBs(self):
3894
+ '''Add OOBs to the game'''
3895
+ oobc = self._categories.get('oob',{}).get('all',{}).items()
3896
+ if len(oobc) < 1:
3897
+ return
3898
+
3899
+ with VerboseGuard(f'Adding OOBs') as v:
3900
+ icon = self.getIcon('oob-icon','/images/inventory.gif')
3901
+ v(f'Using icon "{icon}" for OOB')
3902
+ charts = \
3903
+ self._game.addChartWindow(name='OOBs',
3904
+ hotkey = self._oobKey,
3905
+ description = 'OOBs',
3906
+ text = '',
3907
+ icon = icon,
3908
+ tooltip = 'Show/hide OOBs')
3909
+ tabs = charts.addTabs(entryName='OOBs')
3910
+
3911
+ for on, o in oobc:
3912
+ widget = tabs.addMapWidget(entryName=on)
3913
+ self.addOOB(widget, on, o)
3914
+
3915
+
3916
+ # ----------------------------------------------------------------
3917
+ def addOOB(self,widget,name,info):
3918
+ '''Add a OOB elements to the game
3919
+
3920
+ Parameters
3921
+ ----------
3922
+ widget : Widget
3923
+ Widget to add to
3924
+ name : str
3925
+ Name
3926
+ info : dict
3927
+ Information on the OOB image
3928
+ '''
3929
+ map = widget.addWidgetMap(mapName = name,
3930
+ markMoved = 'Never',
3931
+ hotkey = '')
3932
+ map.addCounterDetailViewer()
3933
+ map.addStackMetrics()
3934
+ map.addImageSaver()
3935
+ map.addTextSaver()
3936
+ map.addForwardToChatter()
3937
+ map.addMenuDisplayer()
3938
+ map.addMapCenterer()
3939
+ map.addStackExpander()
3940
+ map.addPieceMover()
3941
+ map.addKeyBufferer()
3942
+ map.addSelectionHighlighters()
3943
+ map.addHighlightLastMoved()
3944
+ map.addZoomer()
3945
+
3946
+ picker = map.addPicker()
3947
+ ulx,uly,lrx,lry = self.getBB(info['img'])
3948
+ board = picker.addBoard(name = name,
3949
+ image = info['filename'])
3950
+ zoned = board.addZonedGrid()
3951
+ zoned.addHighlighter()
3952
+
3953
+ if not 'zones' in info:
3954
+ zone = zoned.addZone(name = 'full',
3955
+ useParentGrid = False,
3956
+ path=(f'{ulx},{uly};' +
3957
+ f'{lrx},{uly};' +
3958
+ f'{lrx},{lry};' +
3959
+ f'{ulx},{lry}'))
3960
+ grid = zone.addSquareGrid()
3961
+ grid.addNumbering()
3962
+
3963
+ return
3964
+
3965
+ # If we get here, we have board info!
3966
+ w = abs(ulx-lrx)
3967
+ h = abs(uly-lry)
3968
+ self.addZones(zoned,name,info['zones'],w,h)
3969
+
3970
+ # ----------------------------------------------------------------
3971
+ def addCharts(self):
3972
+ '''Add Charts elements to game
3973
+ '''
3974
+ chartc = self._categories.get('chart',{}).get('all',{}).items()
3975
+ if len(chartc) < 1:
3976
+ return
3977
+
3978
+ with VerboseGuard('Adding charts') as v:
3979
+ charts = self._game.addChartWindow(name = 'Charts',
3980
+ hotkey = self._chartsKey,
3981
+ description = '',
3982
+ text = '',
3983
+ tooltip = 'Show/hide charts',
3984
+ icon = self.getIcon('chart-icon',
3985
+ '/images/chart.gif'))
3986
+ tabs = charts.addTabs(entryName='Charts')
3987
+ for i, (cn, c) in enumerate(chartc):
3988
+ if i == 0: v('',end='')
3989
+ v(f'[{cn}] ',end='',flush=True,noindent=True)
3990
+
3991
+ tabs.addChart(chartName = cn,
3992
+ description = cn,
3993
+ fileName = c['filename'])
3994
+
3995
+ v('')
3996
+
3997
+ # ----------------------------------------------------------------
3998
+ def addDie(self):
3999
+ '''Add a `Die` element to the module
4000
+ '''
4001
+ if self._dice is not None and len(self._dice) > 0:
4002
+ return
4003
+ self._game.addDiceButton(name = '1d6',
4004
+ hotkey = self._diceKey)
4005
+
4006
+
4007
+
4008
+ #
4009
+ # EOF
4010
+ #