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.
- pywargame/__init__.py +2 -0
- pywargame/common/__init__.py +3 -0
- pywargame/common/collector.py +87 -0
- pywargame/common/dicedraw.py +363 -0
- pywargame/common/drawdice.py +40 -0
- pywargame/common/singleton.py +22 -0
- pywargame/common/test.py +25 -0
- pywargame/common/verbose.py +59 -0
- pywargame/common/verboseguard.py +53 -0
- pywargame/cyberboard/__init__.py +18 -0
- pywargame/cyberboard/archive.py +283 -0
- pywargame/cyberboard/base.py +63 -0
- pywargame/cyberboard/board.py +462 -0
- pywargame/cyberboard/cell.py +200 -0
- pywargame/cyberboard/collect.py +49 -0
- pywargame/cyberboard/collectgbx0pwd.py +30 -0
- pywargame/cyberboard/collectgbxext.py +30 -0
- pywargame/cyberboard/collectgsnexp.py +32 -0
- pywargame/cyberboard/collectgsnext.py +30 -0
- pywargame/cyberboard/draw.py +396 -0
- pywargame/cyberboard/exporter.py +1132 -0
- pywargame/cyberboard/extractor.py +240 -0
- pywargame/cyberboard/features.py +17 -0
- pywargame/cyberboard/gamebox.py +81 -0
- pywargame/cyberboard/gbxexp.py +76 -0
- pywargame/cyberboard/gbxext.py +64 -0
- pywargame/cyberboard/gsnexp.py +147 -0
- pywargame/cyberboard/gsnext.py +59 -0
- pywargame/cyberboard/head.py +111 -0
- pywargame/cyberboard/image.py +76 -0
- pywargame/cyberboard/main.py +47 -0
- pywargame/cyberboard/mark.py +102 -0
- pywargame/cyberboard/palette.py +36 -0
- pywargame/cyberboard/piece.py +169 -0
- pywargame/cyberboard/player.py +36 -0
- pywargame/cyberboard/scenario.py +115 -0
- pywargame/cyberboard/testgrid.py +156 -0
- pywargame/cyberboard/tile.py +121 -0
- pywargame/cyberboard/tray.py +68 -0
- pywargame/cyberboard/windows.py +41 -0
- pywargame/cyberboard/zeropwd.py +45 -0
- pywargame/cyberboard.py +2728 -0
- pywargame/gbx0pwd.py +2776 -0
- pywargame/gbxextract.py +2795 -0
- pywargame/gsnexport.py +16499 -0
- pywargame/gsnextract.py +2790 -0
- pywargame/latex/__init__.py +2 -0
- pywargame/latex/collect.py +34 -0
- pywargame/latex/latexexporter.py +4010 -0
- pywargame/latex/main.py +184 -0
- pywargame/vassal/__init__.py +66 -0
- pywargame/vassal/base.py +139 -0
- pywargame/vassal/board.py +243 -0
- pywargame/vassal/buildfile.py +60 -0
- pywargame/vassal/chart.py +79 -0
- pywargame/vassal/chessclock.py +197 -0
- pywargame/vassal/collect.py +98 -0
- pywargame/vassal/collectpatch.py +28 -0
- pywargame/vassal/command.py +21 -0
- pywargame/vassal/documentation.py +322 -0
- pywargame/vassal/dumpcollect.py +28 -0
- pywargame/vassal/dumpvsav.py +28 -0
- pywargame/vassal/element.py +439 -0
- pywargame/vassal/exporter.py +89 -0
- pywargame/vassal/extension.py +101 -0
- pywargame/vassal/folder.py +103 -0
- pywargame/vassal/game.py +940 -0
- pywargame/vassal/gameelements.py +1091 -0
- pywargame/vassal/globalkey.py +127 -0
- pywargame/vassal/globalproperty.py +433 -0
- pywargame/vassal/grid.py +573 -0
- pywargame/vassal/map.py +1061 -0
- pywargame/vassal/mapelements.py +1020 -0
- pywargame/vassal/merge.py +57 -0
- pywargame/vassal/merger.py +460 -0
- pywargame/vassal/moduledata.py +275 -0
- pywargame/vassal/mrgcollect.py +31 -0
- pywargame/vassal/patch.py +44 -0
- pywargame/vassal/patchcollect.py +28 -0
- pywargame/vassal/player.py +83 -0
- pywargame/vassal/save.py +495 -0
- pywargame/vassal/skel.py +380 -0
- pywargame/vassal/trait.py +224 -0
- pywargame/vassal/traits/__init__.py +36 -0
- pywargame/vassal/traits/area.py +50 -0
- pywargame/vassal/traits/basic.py +35 -0
- pywargame/vassal/traits/calculatedproperty.py +22 -0
- pywargame/vassal/traits/cargo.py +29 -0
- pywargame/vassal/traits/click.py +41 -0
- pywargame/vassal/traits/clone.py +28 -0
- pywargame/vassal/traits/delete.py +24 -0
- pywargame/vassal/traits/deselect.py +32 -0
- pywargame/vassal/traits/dynamicproperty.py +112 -0
- pywargame/vassal/traits/globalcommand.py +55 -0
- pywargame/vassal/traits/globalhotkey.py +26 -0
- pywargame/vassal/traits/globalproperty.py +54 -0
- pywargame/vassal/traits/hide.py +67 -0
- pywargame/vassal/traits/label.py +76 -0
- pywargame/vassal/traits/layer.py +105 -0
- pywargame/vassal/traits/mark.py +20 -0
- pywargame/vassal/traits/mask.py +85 -0
- pywargame/vassal/traits/mat.py +26 -0
- pywargame/vassal/traits/moved.py +35 -0
- pywargame/vassal/traits/movefixed.py +51 -0
- pywargame/vassal/traits/nonrect.py +95 -0
- pywargame/vassal/traits/nostack.py +55 -0
- pywargame/vassal/traits/place.py +104 -0
- pywargame/vassal/traits/prototype.py +20 -0
- pywargame/vassal/traits/report.py +34 -0
- pywargame/vassal/traits/restrictaccess.py +28 -0
- pywargame/vassal/traits/restrictcommand.py +32 -0
- pywargame/vassal/traits/return.py +40 -0
- pywargame/vassal/traits/rotate.py +62 -0
- pywargame/vassal/traits/sendto.py +59 -0
- pywargame/vassal/traits/sheet.py +129 -0
- pywargame/vassal/traits/skel.py +9 -0
- pywargame/vassal/traits/stack.py +28 -0
- pywargame/vassal/traits/submenu.py +27 -0
- pywargame/vassal/traits/trail.py +61 -0
- pywargame/vassal/traits/trigger.py +72 -0
- pywargame/vassal/turn.py +272 -0
- pywargame/vassal/upgrade.py +191 -0
- pywargame/vassal/vmod.py +323 -0
- pywargame/vassal/vsav.py +100 -0
- pywargame/vassal/widget.py +358 -0
- pywargame/vassal/withtraits.py +634 -0
- pywargame/vassal/xml.py +4 -0
- pywargame/vassal/zone.py +399 -0
- pywargame/vassal.py +12500 -0
- pywargame/vmodpatch.py +12548 -0
- pywargame/vsavdump.py +12533 -0
- pywargame/vslmerge.py +13015 -0
- pywargame/wgexport.py +16689 -0
- pywargame/ztexport.py +14351 -0
- pywargame/zuntzu/__init__.py +5 -0
- pywargame/zuntzu/base.py +82 -0
- pywargame/zuntzu/collect.py +38 -0
- pywargame/zuntzu/countersheet.py +250 -0
- pywargame/zuntzu/dicehand.py +48 -0
- pywargame/zuntzu/exporter.py +936 -0
- pywargame/zuntzu/gamebox.py +154 -0
- pywargame/zuntzu/map.py +36 -0
- pywargame/zuntzu/piece.py +37 -0
- pywargame/zuntzu/scenario.py +208 -0
- pywargame/zuntzu/ztexp.py +115 -0
- pywargame-0.3.1.dist-info/METADATA +353 -0
- pywargame-0.3.1.dist-info/RECORD +150 -0
- pywargame-0.3.1.dist-info/WHEEL +5 -0
- pywargame-0.3.1.dist-info/licenses/LICENSE +5 -0
- 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
|
+
['←,→,↑↓','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
|
+
#
|