summaryrefslogtreecommitdiff
path: root/macros
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-03-30 03:01:43 +0000
committerNorbert Preining <norbert@preining.info>2023-03-30 03:01:43 +0000
commit0a426619b66178dd2257f1ece60ff80f86f40e88 (patch)
treefb35be40eb794817645905b4813c091d5f75dda9 /macros
parent395caca342df230d7d98a658477b7f54c3ee5845 (diff)
CTAN sync 202303300301
Diffstat (limited to 'macros')
-rw-r--r--macros/latex/contrib/songproj/Makefile8
-rwxr-xr-xmacros/latex/contrib/songproj/song2tex.py26
-rw-r--r--macros/latex/contrib/songproj/songproj.dtx67
-rw-r--r--macros/latex/contrib/songproj/songproj.pdfbin100783 -> 131017 bytes
-rw-r--r--macros/latex/contrib/wargame/README.md49
-rw-r--r--macros/latex/contrib/wargame/doc/compat.pdfbin432806 -> 432819 bytes
-rw-r--r--macros/latex/contrib/wargame/doc/symbols.pdfbin184493 -> 184525 bytes
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/Makefile15
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/export.tex199
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/game.pdfbin583838 -> 588181 bytes
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/game.sty42
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/game.tex27
-rw-r--r--macros/latex/contrib/wargame/doc/tutorial/patch.py50
-rw-r--r--macros/latex/contrib/wargame/doc/wargame.pdfbin1310006 -> 1444437 bytes
-rw-r--r--macros/latex/contrib/wargame/source/Makefile46
-rw-r--r--macros/latex/contrib/wargame/source/chit/battle.dtx77
-rw-r--r--macros/latex/contrib/wargame/source/chit/core.dtx8
-rw-r--r--macros/latex/contrib/wargame/source/chit/dice.dtx286
-rw-r--r--macros/latex/contrib/wargame/source/chit/elements.dtx33
-rw-r--r--macros/latex/contrib/wargame/source/chit/misc.dtx429
-rw-r--r--macros/latex/contrib/wargame/source/chit/modifiers.dtx42
-rw-r--r--macros/latex/contrib/wargame/source/chit/oob.dtx172
-rw-r--r--macros/latex/contrib/wargame/source/chit/shape.dtx220
-rw-r--r--macros/latex/contrib/wargame/source/chit/stack.dtx54
-rw-r--r--macros/latex/contrib/wargame/source/chit/table.dtx176
-rw-r--r--macros/latex/contrib/wargame/source/hex/board.dtx393
-rw-r--r--macros/latex/contrib/wargame/source/hex/coord.dtx39
-rw-r--r--macros/latex/contrib/wargame/source/hex/core.dtx1
-rw-r--r--macros/latex/contrib/wargame/source/hex/extra.dtx2
-rw-r--r--macros/latex/contrib/wargame/source/hex/paths.dtx100
-rw-r--r--macros/latex/contrib/wargame/source/hex/ridges.dtx80
-rw-r--r--macros/latex/contrib/wargame/source/hex/shape.dtx105
-rw-r--r--macros/latex/contrib/wargame/source/hex/split.dtx517
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain.dtx13
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/beach.dtx10
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/city.dtx3
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/rough.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/town.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/village.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/terrain/woods.dtx4
-rw-r--r--macros/latex/contrib/wargame/source/hex/towns.dtx11
-rw-r--r--macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx24
-rw-r--r--macros/latex/contrib/wargame/source/natoapp6c/shape.dtx19
-rw-r--r--macros/latex/contrib/wargame/source/util/bb.dtx226
-rw-r--r--macros/latex/contrib/wargame/source/util/compound.dtx242
-rw-r--r--macros/latex/contrib/wargame/source/util/core.dtx1340
-rw-r--r--macros/latex/contrib/wargame/source/util/export.dtx817
-rw-r--r--macros/latex/contrib/wargame/source/util/icons.dtx118
-rw-r--r--macros/latex/contrib/wargame/source/util/misc.dtx145
-rw-r--r--macros/latex/contrib/wargame/source/util/randomid.dtx22
-rw-r--r--macros/latex/contrib/wargame/source/util/tikz.dtx196
-rwxr-xr-xmacros/latex/contrib/wargame/source/utils/wgexport.py14663
-rwxr-xr-xmacros/latex/contrib/wargame/source/utils/wgsvg2tikz.py2
-rw-r--r--macros/latex/contrib/wargame/source/wargame.dtx265
-rw-r--r--macros/latex/contrib/wargame/source/wargame.ins28
-rw-r--r--macros/luatex/generic/luavlna/README.md2
-rw-r--r--macros/luatex/generic/luavlna/luavlna-doc.pdfbin92320 -> 92324 bytes
-rw-r--r--macros/luatex/generic/luavlna/luavlna.lua40
-rw-r--r--macros/luatex/generic/luavlna/luavlna.sty7
-rw-r--r--macros/luatex/generic/luavlna/luavlna.tex5
63 files changed, 15873 insertions, 5616 deletions
diff --git a/macros/latex/contrib/songproj/Makefile b/macros/latex/contrib/songproj/Makefile
index 48edf01b51..de8b57bc12 100644
--- a/macros/latex/contrib/songproj/Makefile
+++ b/macros/latex/contrib/songproj/Makefile
@@ -22,10 +22,12 @@ pdf: songproj.pdf
mostlyclean:
rm -f *.aux *.glo *.gls *.hd *.idx *.ilg *.ind *.log *.out
+ctanclean: mostlyclean
+ rm -f *.sty
+
clean: mostlyclean
rm -f *.pdf
-maintainer-clean: clean
- rm -f *.sty
+maintainer-clean: ctanclean clean
-.PHONY: all sty doc mostlyclean clean maintainer-clean
+.PHONY: all sty doc mostlyclean ctanclean clean maintainer-clean
diff --git a/macros/latex/contrib/songproj/song2tex.py b/macros/latex/contrib/songproj/song2tex.py
index dc32da2b3f..270a9ef721 100755
--- a/macros/latex/contrib/songproj/song2tex.py
+++ b/macros/latex/contrib/songproj/song2tex.py
@@ -8,6 +8,7 @@ import re
# The following regexp match the typical markers of refrain and couplet:
re_refrain = re.compile(r'(R(efrain)?|C(horus)?)( |[-./] ?)', re.IGNORECASE)
re_couplet = re.compile(r'\d+( |[-./] ?)')
+re_txt_ext = re.compile(r'\.te?xt$')
def match_rest(regexp, s):
@@ -41,35 +42,35 @@ def parse_song(lines, re_refrain, re_couplet):
yield '\\begin{refrain}\n'
state = State.REFRAIN
line = rest
- yield ' {} \\\\\n'.format(line)
+ yield ' {}'.format(line)
elif (rest := match_rest(re_couplet, line)) is not None:
# when a couplet start marker is found, enter a couplet.
yield '\\begin{couplet}\n'
state = State.COUPLET
line = rest
- yield ' {} \\\\\n'.format(line)
+ yield ' {}'.format(line)
elif line != '':
# when a non-empty line is found, also enter a couplet as all.
yield '\\begin{couplet}\n'
state = State.COUPLET
- yield ' {} \\\\\n'.format(line)
+ yield ' {}'.format(line)
elif state == State.REFRAIN:
# We are in the refrain. Depending on what we find:
if line != '':
# when a non-empty line is found, stay in the refrain.
- yield ' {} \\\\\n'.format(line)
+ yield ' \\\\\n {}'.format(line)
else:
# when an empty line is found, leave the refrain.
- yield '\\end{refrain}\n\n'
+ yield '\n\\end{refrain}\n\n'
state = State.LIMBO
elif state == State.COUPLET:
# We are in a couplet. Depending on what we find:
if line != '':
# when a non-empty line is found, stay in the couplet.
- yield ' {} \\\\\n'.format(line)
+ yield ' \\\\\n {}'.format(line)
else:
# when an empty line is found, leave the couplet.
- yield '\\end{couplet}\n\n'
+ yield '\n\\end{couplet}\n\n'
state = State.LIMBO
# No matter what we found, we have a song line, possibly empty, that we
# have to check to see if it could not be the new longest line seen so far.
@@ -78,9 +79,9 @@ def parse_song(lines, re_refrain, re_couplet):
# The song text file is now finished. We may still be in the refrain or in a
# couplet, that we have to close.
if state == State.REFRAIN:
- yield '\\end{refrain}\n\n'
+ yield '\n\\end{refrain}\n\n'
elif state == State.COUPLET:
- yield '\\end{couplet}\n\n'
+ yield '\n\\end{couplet}\n\n'
# Now print the longest line of the entire song.
yield '\\longest{{{}}}\n'.format(longest)
@@ -92,8 +93,13 @@ def main(args=None):
parser.add_argument("--couplet", '-c', type=re.compile, default=re_couplet, metavar="REGEXP",
help="specify the couplet marker (regexp, default \"{}\")".format(re_couplet.pattern))
parser.add_argument("infile", help="input song text file", type=argparse.FileType('r'))
- parser.add_argument("outfile", help="output LaTeX file", type=argparse.FileType('w'))
+ parser.add_argument("outfile", help="output LaTeX file", type=argparse.FileType('w'), nargs='?')
args = parser.parse_args(args)
+ if args.outfile is None:
+ try:
+ args.outfile = open(re_txt_ext.sub('', args.infile.name) + '.tex', 'w')
+ except AttributeError:
+ parser.error("the following argument is required: outfile")
for line in parse_song(args.infile, args.refrain, args.couplet):
args.outfile.write(line)
diff --git a/macros/latex/contrib/songproj/songproj.dtx b/macros/latex/contrib/songproj/songproj.dtx
index 7c1783fe15..6ed09318c0 100644
--- a/macros/latex/contrib/songproj/songproj.dtx
+++ b/macros/latex/contrib/songproj/songproj.dtx
@@ -34,7 +34,7 @@
%<package>\NeedsTeXFormat{LaTeX2e}[2020/10/01]
%<package>\ProvidesPackage{songproj}
%<*package>
- [2022/11/07 v1.0.1 Song projection]
+ [2023/03/29 v1.2.0 Song projection]
%</package>
%
%<*driver>
@@ -52,7 +52,7 @@
%</driver>
% \fi
%
-% \CheckSum{256}
+% \CheckSum{269}
%
% \CharacterTable
% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z
@@ -75,6 +75,8 @@
% \changes{v0.3.0}{2021/09/26}{Use \textsf{expl3} naming conventions}
% \changes{v1.0.0}{2021/11/07}{First public release}
% \changes{v1.0.1}{2021/11/07}{Add the generated extension file to Git}
+% \changes{v1.1.0}{2023/03/27}{Allow couplet numbering with \cs{numbercouplets}}
+% \changes{v1.2.0}{2023/03/29}{Allow couplet numbering with \cs{inputsong*}}
%
% \GetFileInfo{songproj.dtx}
%
@@ -147,6 +149,16 @@
% is only used to compute and record its length, and will not be typeset.
% \end{function}
%
+% \changes{v1.1.0}{2023/03/27}{Document the \cs{numbercouplets} command}
+% \begin{function}{\numbercouplets}
+% \label{doc-cs-numbercouplets}
+% Inside a \env{song} environment, the \cs{numbercouplets} command can be used
+% to enable couplet numbering. This can be useful when specific couplets have
+% been selected but the lead singer has a score a lyrics sheet that includes
+% all of them: indicating the couplet numbers in the projection will allow them
+% to check which couplet to sing.
+% \end{function}
+%
% \changes{v0.4.0}{2021/09/29}{Document \env{intro} and \env{final} environments}
%
% \paragraph{}
@@ -215,10 +227,12 @@
%
% \subsection{The \cs{inputsong} command}
% \changes{v1.0.0}{2021/11/07}{Document the \cs{inputsong} command}
+% \changes{v1.2.0}{2023/03/29}{Document the \cs{inputsong*} command}
%
-% \begin{function}{\inputsong}
+% \begin{function}{\inputsong, \inputsong*}
% \begin{syntax}
% \cs{inputsong}\marg{file}\marg{stanzas per slide}\oarg{couplet list}
+% \cs{inputsong*}\marg{file}\marg{stanzas per slide}\oarg{couplet list}
% \end{syntax}
%
% The \cs{inputsong} command environment is used as a shortcut for
@@ -235,6 +249,9 @@
% \end{verbatim}
% \end{function}
%
+% The starred version \cs{inputsong*} enables couplet numbering, as described
+% in \ref{doc-cs-numbercouplets}.
+%
% \subsection{The \env{refrain}, \env{couplet}, \env{intro} and \env{final} environments}
%
% \changes{v0.4.0}{2021/09/29}{Document \env{intro} and \env{final} environments}
@@ -293,6 +310,7 @@
% \end{verbatim}
%
% \subsubsection{Importing text lyrics}
+% \changes{v1.1.0}{2023/03/27}{Derivate output file name from input file name if not specified}
%
% Song lyrics are often found in text format with basic markup:
%
@@ -391,6 +409,8 @@
%
% \changes{v0.2.0}{2021/09/23}{Use \texttt{sp} prefix for functions and
% variables}
+% \changes{v1.1.0}{2023/03/27}{Add the \cs{g__sp_show_numbers_bool} variable to
+% number or not couplets}
%
% We define a number of internal variables, that are used when reading and
% formatting a song. All of these variables are meant to be set globally: since
@@ -402,6 +422,7 @@
\bool_new:N \g__sp_song_start_bool % are we at the start of a song?
\bool_new:N \g__sp_refrain_first_bool % does current song start with the
% refrain?
+\bool_new:N \g__sp_show_numbers_bool % should we show the couplet numbers?
\int_new:N \g__sp_stanzas_per_slide_int % number of stanzas to show on each
% slide (1 or 2)
\dim_new:N \g__sp_linewidth_dim % length of the longest line in current
@@ -500,8 +521,9 @@
% \end{macro}
%
% \begin{macro}{\__sp_song_couplet:n}
+% \changes{v1.1.0}{2023/03/27}{Print the couplet number when requested}
%
-% This macro uses the \env{__sp_couplet} environment to a specified couplet of
+% This macro uses the \env{__sp_couplet} environment to format a specified couplet of
% the current song. It takes a single argument:
%
% \begin{arguments}
@@ -515,6 +537,9 @@
\dim_compare:nNnTF \g__sp_linewidth_dim {=} {0pt}
{ \begin{__sp_couplet} }
{ \begin{__sp_couplet} [\g__sp_linewidth_dim] }
+ \bool_if:NTF \g__sp_show_numbers_bool
+ { \flagverse{#1.} }
+ {}
\seq_item:Nn \g__sp_couplets_seq {#1}
\end{__sp_couplet}
}
@@ -832,7 +857,20 @@
% \end{macrocode}
% \end{macro}
%
+% \begin{macro}{\numbercouplets}
+% \changes{v1.1.0}{2023/03/27}{New command to enable couplet numbering}
+%
+% This macro can be used within a song to indicate that its couplets should be
+% numbered.
+%
+% \begin{macrocode}
+ \NewDocumentCommand {\numbercouplets}{}
+ { \bool_gset_true:N \g__sp_show_numbers_bool }
+% \end{macrocode}
+% \end{macro}
+%
% \begin{environment}{song}
+% \changes{v1.1.0}{2023/03/27}{Do not number couplets by default}
%
% This environment is used as a container for entire songs. On opening, it does several things:
% \begin{enumerate}
@@ -847,6 +885,9 @@
% we are at the start of the song, which will allow the next
% \env{refrain} or \env{couplet} to tell if the song starts with the
% refrain or with a couplet;
+% \item it sets the \cs{g__sp_show_numbers_bool} variable to false, to
+% indicate that the couplets should not be numbered by default (the user
+% will be able to override this with the \cs{numbercouplets} command).
% \end{enumerate}
%
% This environment takes two arguments:
@@ -877,6 +918,9 @@
% Indicate that we are in a song, and at its start
\bool_gset_true:N \g__sp_song_bool
\bool_gset_true:N \g__sp_song_start_bool
+
+ % Couplets should not be numbered by default
+ \bool_gset_false:N \g__sp_show_numbers_bool
}
% \end{macrocode}
%
@@ -994,18 +1038,21 @@
% \end{environment}
%
% \begin{macro}{\inputsong}
-% \changes{v1.0.0}{2021/11/07}{Add a \cs{inputsong} command}
+% \changes{v1.0.0}{2021/11/07}{Add an \cs{inputsong} command}
+% \changes{v1.2.0}{2023/03/29}{Add an \cs{inputsong*} command}
%
% This macro starts a \env{song} environment and \cs{input}s the song content
% from an external file.
%
% \begin{macrocode}
-\NewDocumentCommand {\inputsong} { m m o }
+\NewDocumentCommand {\inputsong} { s m m o }
{
- \IfNoValueTF {#3}
- { \begin{song} {#2} }
- { \begin{song} {#2} [#3] }
- \input{#1}
+ \IfNoValueTF {#4}
+ { \begin{song} {#3} }
+ { \begin{song} {#3} [#4] }
+ \IfBooleanT {#1}
+ { \numbercouplets }
+ \input{#2}
\end{song}
}
% \end{macrocode}
diff --git a/macros/latex/contrib/songproj/songproj.pdf b/macros/latex/contrib/songproj/songproj.pdf
index 24216ce150..1329f0e986 100644
--- a/macros/latex/contrib/songproj/songproj.pdf
+++ b/macros/latex/contrib/songproj/songproj.pdf
Binary files differ
diff --git a/macros/latex/contrib/wargame/README.md b/macros/latex/contrib/wargame/README.md
index 000f3cd68b..3de32ab4d0 100644
--- a/macros/latex/contrib/wargame/README.md
+++ b/macros/latex/contrib/wargame/README.md
@@ -1,8 +1,6 @@
-A package to make Hex'n'Counter wargames in LaTeX
-=================================================
+# A package to make Hex'n'Counter wargames in LaTeX
-Version 0.3.2
--------------
+## Version 0.5
This package can help make classic [Hex'n'Counter
wargames](https://en.wikipedia.org/wiki/Wargame) using LaTeX. The
@@ -25,14 +23,12 @@ one will need for a game (rules, charts, boards, counter sheets).
- The package support exporting the game to a
[VASSAL](#vassal-support%20'Section%20below') module
-Sources
--------
+## Sources
The sources of the package are kept at
[GitLab](https://gitlab.com/wargames_tex/wargame_tex)
-Download from GitLab
---------------------
+## Download from GitLab
- [Zip file of package and support
files](https://gitlab.com/wargames_tex/wargame_tex/-/jobs/artifacts/master/download?job=dist)
@@ -67,14 +63,12 @@ to install in `~/texmf`. If you prefer another destination, say
make install DESTDIR=/usr/local/share/texmf
-Download from CTAN
-------------------
+## Download from CTAN
The package is available from
[CTAN](https://ctan.org/tex-archive/macros/latex/contrib/wargame) in the
-directory `/macros/latex/contrib/wargame`. As of this writing, the
-package has not hit any of the major distributions (e.g.,
-[TeXLive](https://tug.org/texlive/index.html)).
+directory `/macros/latex/contrib/wargame`. The package is part of the
+CTAN distribution [TeXLive](https://tug.org/texlive/index.html).
### From TDS zip archive
@@ -170,16 +164,14 @@ the module `PIL` for this. On Debian based systems, do
sudo apt install poppler-utils python3-pil
-Tutorial
---------
+## Tutorial
See the [tutorial](tutorial/README.md "Link works on GitLab only") page
for more
([here](https://ctan.org/tex-archive/macros/latex/contrib/wargame/doc/tutorial/)
if you browse from CTAN).
-Examples
---------
+## Examples
Below are some print'n'play board wargames made with this package. These
are not original games but rather revamps of existing games. All credits
@@ -192,6 +184,9 @@ goes to the original authors of these games.
- Western Front
- [D-Day](https://gitlab.com/wargames_tex/dday_tex) (Avalon
Hill classic)
+ - [Smithsonian
+ D-Day](https://gitlab.com/wargames_tex/sdday_tex) (Avalon
+ Hill)
- [Paul Koenig's Market
Garden](https://gitlab.com/wargames_tex/pkmg_tex)
- [Paul Koenig's
@@ -208,9 +203,10 @@ goes to the original authors of these games.
- [Strike Force One](https://gitlab.com/wargames_tex/sfo_tex)
- [Kriegspiel](https://gitlab.com/wargames_tex/kriegspiel_tex)
(Avalon Hill classic)
+ - [Port Stanley](https://gitlab.com/wargames_tex/portstanley_tex)
+ (from the *Wargamer* magazine)
-VASSAL support
---------------
+## VASSAL support
The packages has the script [`wgexport.py`](utils/wgexport.py) to
generate a draft [VASSAL](https://vassalengine.org) module from the
@@ -231,8 +227,14 @@ An example of this is given in the [Battle for
Moscow](https://gitlab.com/wargames_tex/bfm_tex) project, and of course
in the [tutorial](tutorial/ "Link works on GitLab only")
-NATO App6
----------
+The sources of this script is kept in a different project
+[`pywargame`](https://gitlab.com/wargames_tex/pywargame) also on GitLab.
+The script is generated in that package. The `pywargame` package allows
+one to manipulate VASSAL modules from Python, or to read (and convert)
+[CyberBoard](http://cyberboard.norsesoft.com/)
+([GitHub](https://github.com/CyberBoardPBEM/cbwindows)) scenarios.
+
+## NATO App6
The package supports
@@ -257,8 +259,7 @@ standard.
- Other LaTeX package for making NATO symbology
[XMilSymb](https://github.com/ralphieraccoon/MilSymb)
-Copyright and license
----------------------
+## Copyright and license
(c) 2022 Christian Holm Christensen
@@ -277,7 +278,7 @@ you generate using the package is *not* required to be licensed
according to the *ShareAlike* clause of CC-BY-SA-4.0. That is, you can
license your materials *any way you want*.
-However, that copyright rights on games is not as prohibitive as you may
+However, copyright rights on games is not as prohibitive as you may
think (see [this thread on
BGG](https://boardgamegeek.com/thread/493249/)). What you *can*
copyright is original, artistic expression. That is, the copyrighted
diff --git a/macros/latex/contrib/wargame/doc/compat.pdf b/macros/latex/contrib/wargame/doc/compat.pdf
index 70583eca2f..2ba344d4ab 100644
--- a/macros/latex/contrib/wargame/doc/compat.pdf
+++ b/macros/latex/contrib/wargame/doc/compat.pdf
Binary files differ
diff --git a/macros/latex/contrib/wargame/doc/symbols.pdf b/macros/latex/contrib/wargame/doc/symbols.pdf
index b7f8a0a6c0..cd8f0d3ddc 100644
--- a/macros/latex/contrib/wargame/doc/symbols.pdf
+++ b/macros/latex/contrib/wargame/doc/symbols.pdf
Binary files differ
diff --git a/macros/latex/contrib/wargame/doc/tutorial/Makefile b/macros/latex/contrib/wargame/doc/tutorial/Makefile
index 4947d84ee0..ed50fabad5 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/Makefile
+++ b/macros/latex/contrib/wargame/doc/tutorial/Makefile
@@ -8,7 +8,7 @@ LATEX_FLAGS := -interaction=nonstopmode \
-shell-escape
EXPORT := ../utils/wgexport.py
EXPORT_FLAGS :=
-
+PATCH := patch.py
ifdef VERBOSE
MUTE :=
REDIR :=
@@ -22,15 +22,15 @@ endif
%.aux:%.tex
@echo "LATEX $< -> $@"
- $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
+ $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
%.pdf:%.aux
@echo "LATEX $*.tex -> $@ (via $*.aux)"
- $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR)
+ $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR)
%.pdf:%.tex
@echo "LATEX $< -> $@"
- $(MUTE)TEXINPUTS=:..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
+ $(MUTE)TEXINPUTS=..: $(LATEX) $(LATEX_FLAGS) $< $(REDIR)
%.json:%.pdf
@@ -39,11 +39,12 @@ all: game.pdf Game.vmod
game.aux:game.tex game.sty export.tex
export.pdf:export.tex game.sty
-Game.vmod:export.pdf export.json patch.py game.pdf
+Game.vmod:export.pdf export.json $(PATCH) game.pdf
@echo "$(EXPORT) $< -> $@"
- $(MUTE)$(EXPORT) export.pdf export.json -p patch.py -r game.pdf \
+ $(MUTE)$(EXPORT) export.pdf export.json -p $(PATCH) -r game.pdf \
-d "Example module from LaTeX PnP game" \
- -t "LaTeX wargame tutorial" -v 0.1 -o $@
+ -t "LaTeX wargame tutorial" -v 0.1 -o $@ \
+ $(EXPORT_FLAGS)
clean:
rm -f *~ *.log *.out *.aux *.pdf *.vmod *.json
diff --git a/macros/latex/contrib/wargame/doc/tutorial/export.tex b/macros/latex/contrib/wargame/doc/tutorial/export.tex
index d51ecf0e5c..6d6722d9ac 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/export.tex
+++ b/macros/latex/contrib/wargame/doc/tutorial/export.tex
@@ -72,14 +72,14 @@
% For that we will use the macro \verb+\doublechitimages+ (if we had
% single sided counters, we would use \verb+\chitimages+). This macro
% takes two arguments: The list of counter definitions and the faction
-% of the chits. So lets go ahead and make those for factions A and B,
+% of the chits. Let's go ahead and make those for factions A and B,
% and the special faction ``Markers'' which will only be the game turn
% counter.
%
% \begin{macrocode}
\doublechitimages[A]{\alla}
\doublechitimages[B]{\allb}
-\doublechitimages[Markers]{{game turn}}
+\doublechitimages[Markers]{{game turn chit}}
% \end{macrocode}
%
% Again, the macros we made for the counters comes in handy. Note
@@ -87,6 +87,51 @@
% VASSAL module. We will, in fact, use them one more time to create
% the counter sheet.
%
+% To automatically make ``battle markers'' --- markers that identify
+% individual battles by placing a numbered marker on top of the
+% combatants, we can use the macro \verb+battlemarkers+. It takes one
+% argument, which is the number of unique markers to make and add.
+% The markers are round yellow circles with a number in
+% them\footnote{This can of course be customised as everything else
+% can.}.
+%
+% \begin{macrocode}
+\battlemarkers{12}
+% \end{macrocode}
+%
+% Furthermore, we can add ``odds'' and battle result markers. Odds
+% markers can be placed via the battle markers context menu, and then
+% later be replaced by result markers via the context menu of the odds
+% markers. This is done via the macros \verb+\oddsmarkers+ and
+% \verb+\resultmarkers+. Both macros accept a list of options (odds
+% and results, respectively) with a possible background colour to use
+% in the markers.
+%
+% \begin{macrocode}
+\oddsmarkers{%
+ 1:3/red!25!white,%
+ 1:2/red!15!white,%
+ 1:1/orange!15!white,%
+ 2:1/white,%
+ 3:1/green!15!white,%
+ 4:1/green!25!white}
+\resultmarkers{
+ AE/red!50!white,
+ AR/red!25!white,
+ EX/white,
+ DR/green!10!white,
+ DE/green!25!white}
+% \end{macrocode}
+%
+% By default, odds markers must be placed ``by-hand'', but via the
+% VASSAL preferences it is possible to calculate the odds and show
+% them when the battle is declared. By default, this calculation will
+% only be concerned with the combat factors of the involved units, and
+% features such as terrain is ignored. However, with a bit of Python
+% skill, one can flesh out the implementation of the game rules in the
+% optional patch (Python) script. We will not do that here, as a it
+% is a little beyond the scope of this tutorial.
+%
% Right, so that made our counter images and the associated meta
% data. Next thing is to add our board. For that, we use the
% environment \texttt{boardimage}. Inside that environment we must
@@ -116,7 +161,7 @@
% \end{macrocode}
%
% We have two charts that we would also like to be put in: The CRT and
-% the TE. These \emph{must} be put into \Tikz{} pictures, and we
+% the TEC. These \emph{must} be put into \Tikz{} pictures, and we
% precede those with the macro \verb+\info+. This macro takes three
% arguments: The name of the next image, the category, and
% sub-category. Again, the name is more or less free form, and the
@@ -153,7 +198,7 @@
\end{tikzpicture}
% \end{macrocode}
%
-% We can a splash page image. We have not defined such an image in
+% We can add splash page image. We have not defined such an image in
% the \texttt{game} package, but we could have, so we will make it
% here. Again, it should be a \Tikz{} picture. For this, we need to
% use the category \texttt{front}.
@@ -172,68 +217,67 @@
% the like. We will add a few such icons here, mainly to show how
% the \texttt{patch.py} script works.
%
-% First, a button icon for the pool window.
+% The \textsf{wgexport.cls} class provides a number of icons we may
+% use for this. These are
+%
+% \begin{center}
+% \begin{tikzpicture}
+% \pic {flip icon};
+% \pic at (2.0,0) {restore icon};
+% \pic at (3.5,0) {eliminate icon};
+% \pic at (5.0,0) {pool icon};
+% \pic at (6.5,0) {oob icon={black}{black}};
+% \end{tikzpicture}
+% \end{center}
+%
+%
+% We will add these as pictures to our export PDF. First, the image
+% for the pool of units.
%
% \begin{macrocode}
\info{pool-icon}{icon}{}
-\begin{tikzpicture}[scale=.15]
- \draw[scale line widths,line width=4mm]
- (0,1)--(0,-1) (-.7,.2)--(.7,.2);
+\begin{tikzpicture}[transform shape,scale=.4]
+ \pic{pool icon};
\end{tikzpicture}
% \end{macrocode}
%
-% The category \texttt{icon} has no special meaning. The icon will
-% look like \tikz[scale=.3]{\draw[scale line widths,line width=4mm]
-% (0,1)--(0,-1) (-.7,.2)--(.7,.2);}
+% The category \texttt{icon} has no special meaning.
%
% We also want to add a custom icon for the OOB button. VASSAL has a
% button icon for the piece inventory which is really quite
-% appropriate, but alas it is already in use, so we will make our own
-% (albeit similar).
+% appropriate, but alas it is already in use, so we will make our
+% own. Here, we use the \TikZ{} picture \texttt{oob icon} provided by
+% \textsf{wgexport}. This picture needs two arguments: the left hand
+% and right hand sides fill colours. We will use our background
+% colours.
%
% \begin{macrocode}
\info{oob-icon}{icon}{}
-\begin{tikzpicture}[scale=.6]
- \draw[fill=friendly] (.1,0) rectangle(.5,-.1);
- \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25);
- \draw[fill=friendly] (.1,-.3) rectangle(.2,-.4);
- \draw[fill=hostile] (-.1,0) rectangle(-.3,-.25);
+\begin{tikzpicture}[transform shape,scale=.4]
+ \pic{oob icon={a-bg}{b-bg}};
\end{tikzpicture}
% \end{macrocode}
-%
-% This looks like
-% \tikz{
-% \draw[fill=friendly] (.1,0) rectangle(.5,-.1);
-% \draw[fill=friendly] (.1,-.15) rectangle(.3,-.25);
-% \draw[fill=friendly] (.1,-.3) rectangle(.2,-.4);
-% \draw[fill=hostile] (-.1,0) rectangle(-.3,-.25);}
%
-% Normally the \texttt{wgexport.py} script used the \emph{undo} mage
+% Normally the \texttt{wgexport.py} script uses the \emph{undo} image
% for the flip button. However, that may be a bit confusing, so we
-% will use a large ``F''
-% instead. \tikz{\node[color=blue!75!black,font=\sffamily\bfseries\LARGE]{F}}
+% will use a custom image for that. We will use the pictures provided
+% by \textsf{wgexport}.
%
-% And similarly for the ``Eliminate''\tikz{\node[color=red!75!black,font=\sffamily\bfseries\LARGE]{E}}
-%
-% and ``Restore'' buttons. \tikz{\node[color=green!75!black,font=\sffamily\bfseries\LARGE]{R}}
%
% \begin{macrocode}
\info{flip-icon}{icon}{}
-\begin{tikzpicture}
- \node[color=blue!75!black,inner sep=0pt,
- font=\sffamily\bfseries\large]{F};
+\begin{tikzpicture}[transform shape,scale=.4]
+ \pic{flip icon};
\end{tikzpicture}
%
\info{eliminate-icon}{icon}{}
-\begin{tikzpicture}
- \node[color=red!75!black,inner sep=0pt,
- font=\sffamily\bfseries\large]{E};
+\begin{tikzpicture}[transform shape,scale=.4]
+ \pic{eliminate icon};
\end{tikzpicture}
%
\info{restore-icon}{icon}{}
-\begin{tikzpicture}
- \node[color=green!75!black,inner sep=0pt,
- font=\sffamily\bfseries\large]{R};
+\begin{tikzpicture}[transform shape,scale=.4]
+ \pic{restore icon};
\end{tikzpicture}
% \end{macrocode}
%
@@ -262,7 +306,7 @@
% course Un*x-like syntax. For other OSs, consult your \TeX{}
% distribution's documentation for how to find files in the \TeX{}
% installation.
-
+%
% You can open the module in the VASSAL editor and see what was
% generated. On the board, all areas marked with the \texttt{zone
% scope} (or \texttt{zone path}) should be defined as zones. In the
@@ -328,10 +372,9 @@
% done the heavy work for us. Not least be because we have taken care
% to add in \texttt{zone} styles where needed.
%
-% We will
-% put all the counters on the OOB chart, and adjust some grids - most
-% notably the OOB and turn track grids. Other than that, we will not
-% do much.
+% We will put all the counters on the OOB chart, and adjust some grids
+% - most notably the OOB and turn track grids. Other than that, we
+% will not do much.
%
% \begin{verbatim}
% `kpsewhich wgexport.py` export.pdf export.json \
@@ -344,37 +387,61 @@
% Now for the script:
%
% \begin{verbatim}
-% def patch(build,data,vmod,verbose=False):
-% # We need to import the export module :-)
-% import wgexport as wg
+% # --- We may need to import the export module ---
+% #
+% # from wgexport import *
%
-% # Get the game and last free GPID
-% game = wg.get_game(build)
-% gpid = int(game.getAttribute('nextPieceSlotId'))
+% def patch(build,data,vmod,verbose=False,**kwargs):
+% # --- Get the game ---
+% game = build.getGame()
%
-% # Get all boards
-% maps = wg.get_maps(build)
-% board = maps['Board']
+% # --- Get the maps ---
+% maps = game.getMaps()
%
-% mkeys = wg.get_masskey(board,'name')
-% wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png')
-% wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png')
+% # --- Get the main board ---
+% board = maps['Board']
+%
+% # --- Get the mass keys ---
+% mkeys = board.getMassKeys()
+% mkeys['Eliminate']['icon'] = 'eliminiate-icon.png'
+% mkeys['Flip'] ['icon'] = 'flip-icon.png'
%
-% # Get main map
+% # --- Get the dead-pool map ---
% pool = maps['DeadMap']
-% wg.set_node_attr(pool,icon='pool-icon.png')
+% pool['icon'] = 'pool-icon.png'
+% pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png'
%
-% mkeys = wg.get_masskey(pool,'name')
-% wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png')
-%
-% # Get the OOB map
-% oob = wg.get_chartwindows(game)['OOBs']
-% wg.set_node_attr(oob,icon='oob-icon.png')
+% # --- Get the OOB map ---
+% oob = game.getChartWindows()['OOBs']
+% oob['icon'] = 'oob-icon.png'
+%
+%
+% #
+% # EOF
+% #
% \end{verbatim}
%
% In the \texttt{patch.py} script we can use all the functionality
% provided by \texttt{wgexport.py}. Elements are XML elements that we
-% apply \texttt{xml.dom.minidom} operations on.
+% apply \texttt{xml.dom.minidom} operations on.
+%
+% Note, if we need to use classes, etc.\ from \texttt{wgexport.py},
+% then we ought to import that module into our patch script, as shown
+% in the top comment above. In the example patch script we do not
+% need that, so we leave it out.
+%
+% One can do quite complicated things in VASSAL, which can be set-up
+% in the patch script. By default, the \texttt{wgexport} script sets
+% up the pieces and boards, and if one has defined regions with
+% specific piece names, then the script will likewise place the pieces
+% there (as with our \texttt{game turn} example above).
+%
+% One can, for example, setup the module so that the game turn marker
+% automatically flips or progresses when the turn track widget reach
+% specific points. One can in principle also set it up so that when a
+% specific turn or phase is reach, then pieces are moved from the OOB
+% to the board. The more one can do like that, the more the game is
+% automated.
%
% \iffalse
% Local Variables:
diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.pdf b/macros/latex/contrib/wargame/doc/tutorial/game.pdf
index 14bcde8f98..f4e8a22e2a 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/game.pdf
+++ b/macros/latex/contrib/wargame/doc/tutorial/game.pdf
Binary files differ
diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.sty b/macros/latex/contrib/wargame/doc/tutorial/game.sty
index 0c8fcc0165..83a5c02185 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/game.sty
+++ b/macros/latex/contrib/wargame/doc/tutorial/game.sty
@@ -899,7 +899,7 @@
% \begin{figure}
% \centering
% \begin{tikzpicture}
-% \chit[game turn]
+% \chit[game turn chit]
% \end{tikzpicture}
% \caption{The \textsf{wargame} game turn counter}
% \label{fig:plain-game-turn}
@@ -910,8 +910,8 @@
%
% \begin{macrocode}
\tikzset{
- game turn/.append style={a},
- game turn flipped/.style={game turn,b}
+ game turn chit/.append style={a},
+ game turn chit flipped/.append style={b}
}
% \end{macrocode}
%
@@ -920,7 +920,7 @@
% \begin{figure}
% \centering
% \begin{tikzpicture}
-% \doublechits{{game turn}}{1}{1.24}
+% \doublechits{{game turn chit}}{1}{1.24}
% \end{tikzpicture}
% \caption{Modified game turn, front and back}
% \label{fig:game-turn}
@@ -1346,6 +1346,8 @@
text=titlefg,
font=\sffamily\bfseries\Huge,
scale=1.5,
+ inner sep=0pt,
+ outer sep=0pt,
transform shape,
},
sub title/.style={
@@ -1354,6 +1356,8 @@
scale=1.5,
transform shape,
text width=5cm,
+ inner sep=0pt,
+ outer sep=0pt,
align=right
}
}
@@ -1859,7 +1863,7 @@
turn,
transform shape,
anchor=east,
- outer sep=5mm,
+ outer sep=2mm,
scale=.7},
}
% \end{macrocode}
@@ -1921,7 +1925,7 @@
%
% \begin{verbatim}
% \begin{tikzpicture}[oob turn/.append style={anchor=west}]
-% \oob{\alla}{4}{1.24}{0}
+% \oob*{\alla}{4}{1.24}{0}
% \end{tikzpicture}
% \begin{tikzpicture}
% \oob{\allb}{4}{1.24}{0}
@@ -1950,31 +1954,31 @@
at (0,2.5) {};%
% Game title
\node[%
- below right=5mm and 5mm of oob frame.north west,
- title,scale=.8]{A Game};
+ below right=5mm and 5mm of oob frame.north west,
+ title,scale=.8]{A Game};
% Chart title
\node[%
- below left=5mm and 5mm of oob frame.north east,
- sub title,scale=1.2,transform shape]{%
+ below left=5mm and 5mm of oob frame.north east,
+ sub title,scale=1.2,transform shape]{%
Order of Battle};
% Turn title
\node[font=\sffamily\bfseries\Large,
- white,transform shape]
- at (0,1) {Turn};
+ anchor=south,
+ white,transform shape] at (0,1) {Turn};
%
\begin{scope}[%
- transform shape,
- oob turn/.append style={anchor=west},%
- shift={(-1.35,0)},
- zone scope=A OOB]%
+ transform shape,
+ oob turn/.append style={anchor=west},%
+ shift={(-1.4,0)},
+ zone scope=A OOB]%
\oobemptyturn%
\oob*{\alla}{4}{1.4}{.2}%
\end{scope}%
%
\begin{scope}[%
- transform shape,
- shift={(1.35,0)},
- zone scope=B OOB]%
+ transform shape,
+ shift={(1.4,0)},
+ zone scope=B OOB]%
\oobrealturn%
\oob{\allb}{4}{1.4}{.2}%
\end{scope}%
diff --git a/macros/latex/contrib/wargame/doc/tutorial/game.tex b/macros/latex/contrib/wargame/doc/tutorial/game.tex
index 6db72bff27..062ef41c1a 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/game.tex
+++ b/macros/latex/contrib/wargame/doc/tutorial/game.tex
@@ -3,6 +3,7 @@
\usepackage{game}
\usepackage[margin=1cm,bottom=2cm,top=2cm,a4paper]{geometry}
\newcommand\Tikz{Ti{\itshape k}Z/PGF}
+\let\TikZ\Tikz
\setlength{\parskip}{.5ex}
\setlength{\parindent}{0em}
\newcommand\tightlist{\setlength{\itemsep}{3pt}
@@ -113,8 +114,8 @@ the macros \verb+\alla+ and \verb+\allb+ yet again.
\end{verbatim}
As a final bonus, we also make a board that includes our three charts:
-The OOB, CRT, and TE. We will not demonstrate the code here, but take
-a look in the sources if you are curious.
+The OOB, CRT, and TEC. We will not demonstrate the code here, but take
+a look in the sources if you are curious.
\newcommand\fullboard[1][]{%
\begin{tikzpicture}[#1,rotate=90]%
@@ -126,13 +127,23 @@ a look in the sources if you are curious.
\inneroob
\end{scope}
%
- \node[anchor=south west,minimum width=6.2cm,minimum height=10cm,
- line width=1pt,sub title,scale=.66,draw,align=center,
- text depth=9.7cm,fill=neutral,transform shape]
- at (oob frame.south west)
+ \node[
+ anchor=south west,
+ minimum width=6.2cm,
+ minimum height=10.5cm,
+ line width=1pt,
+ sub title,
+ scale=.66,
+ draw,
+ align=center,
+ text depth=9.7cm,
+ fill=neutral,
+ transform shape,
+ above right=1pt of oob frame.south west
+ ] (charts)
{{\LARGE Charts}};
%
- \node[above right=5mm and 4mm of oob frame.south west,
+ \node[above right=5mm and 4mm of charts.south west,
inner sep=0pt,
transform shape,scale=.75,anchor=south west] (crt) {\crt};
%
@@ -185,7 +196,7 @@ corrections, and so on are very welcomed.
\end{tikzpicture}
\begin{tikzpicture}
- \doublechits{{game turn}}{1}{1.24}
+ \doublechits{{game turn chit}}{1}{1.24}
\end{tikzpicture}
\end{center}
diff --git a/macros/latex/contrib/wargame/doc/tutorial/patch.py b/macros/latex/contrib/wargame/doc/tutorial/patch.py
index 0fcdd49645..b01cba6d14 100644
--- a/macros/latex/contrib/wargame/doc/tutorial/patch.py
+++ b/macros/latex/contrib/wargame/doc/tutorial/patch.py
@@ -1,31 +1,33 @@
-def patch(build,data,vmod,verbose=False):
- # We need to import the export module :-)
- import wgexport as wg
+# --- We may need to import the export module ---
+#
+# from wgexport import *
- # Get the game and last free GPID
- game = wg.get_game(build)
- gpid = int(game.getAttribute('nextPieceSlotId'))
+def patch(build,data,vmod,verbose=False,**kwargs):
+ # --- Get the game ---
+ game = build.getGame()
- # Get all boards
- maps = wg.get_maps(build)
- board = maps['Board']
+ # --- Get the maps ---
+ maps = game.getMaps()
- mkeys = wg.get_masskey(board,'name')
- wg.set_node_attr(mkeys['Eliminate'],icon='eliminate-icon.png')
- wg.set_node_attr(mkeys['Flip'],icon='flip-icon.png')
+ # --- Get the main board ---
+ board = maps['Board']
+
+ # --- Get the mass keys ---
+ mkeys = board.getMassKeys()
+ mkeys['Eliminate']['icon'] = 'eliminate-icon.png'
+ mkeys['Flip'] ['icon'] = 'flip-icon.png'
- # Get main map
+ # --- Get the dead-pool map ---
pool = maps['DeadMap']
- wg.set_node_attr(pool,icon='pool-icon.png')
+ pool['icon'] = 'pool-icon.png'
+ pool.getMassKeys()['Restore']['icon'] = 'restore-icon.png'
- mkeys = wg.get_masskey(pool,'name')
- wg.set_node_attr(mkeys['Restore'],icon='restore-icon.png')
-
- # Get the OOB map
- oob = wg.get_chartwindows(game)['OOBs']
- wg.set_node_attr(oob,icon='oob-icon.png')
-
-
+ # --- Get the OOB map ---
+ oob = game.getChartWindows()['OOBs']
+ oob['icon'] = 'oob-icon.png'
+
+
+#
+# EOF
+#
-
-
diff --git a/macros/latex/contrib/wargame/doc/wargame.pdf b/macros/latex/contrib/wargame/doc/wargame.pdf
index c25e83c175..01a58c597b 100644
--- a/macros/latex/contrib/wargame/doc/wargame.pdf
+++ b/macros/latex/contrib/wargame/doc/wargame.pdf
Binary files differ
diff --git a/macros/latex/contrib/wargame/source/Makefile b/macros/latex/contrib/wargame/source/Makefile
index faca441091..e11c477bf7 100644
--- a/macros/latex/contrib/wargame/source/Makefile
+++ b/macros/latex/contrib/wargame/source/Makefile
@@ -2,7 +2,7 @@
#
#
NAME := wargame
-VERSION := 0.3
+VERSION := 0.5
LATEX_FLAGS := -interaction=nonstopmode \
-file-line-error \
--synctex=15 \
@@ -16,8 +16,21 @@ SOURCES := wargame.ins \
wargame.dtx \
package.dtx \
util/core.dtx \
+ util/misc.dtx \
+ util/compound.dtx \
+ util/bb.dtx \
+ util/tikz.dtx \
+ util/randomid.dtx \
+ util/icons.dtx \
+ util/export.dtx \
chit/shape.dtx \
chit/misc.dtx \
+ chit/modifiers.dtx \
+ chit/stack.dtx \
+ chit/oob.dtx \
+ chit/table.dtx \
+ chit/battle.dtx \
+ chit/dice.dtx \
chit/elements.dtx \
chit/core.dtx \
hex/shape.dtx \
@@ -42,6 +55,7 @@ SOURCES := wargame.ins \
hex/terrain/rough.dtx \
hex/board.dtx \
hex/coord.dtx \
+ hex/split.dtx \
natoapp6c/shape.dtx \
natoapp6c/symbols.dtx \
natoapp6c/list.dtx \
@@ -105,15 +119,6 @@ PKG_FILES := wargame.sty \
wgexport.cls \
$(TILES_PDF)
-# beach.png \
-# city.png \
-# mountains.png \
-# sea.png \
-# woods.png \
-# light_woods.png \
-# rough.png \
-# swamp.png \
-# town.png
DOC_FILES := wargame.pdf symbols.pdf compat.pdf
ifdef VERBOSE
@@ -130,6 +135,10 @@ else
ifdef := $(CI_JOB_ID)
endif
+%.pdf:%.tex
+ @echo "LATEX $< -> $@"
+ $(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR)
+
%.aux:%.tex
@echo "LATEX $< -> $@"
$(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR)
@@ -152,10 +161,6 @@ endif
@echo "LATEX $*.tex -> $@ (via $<)"
$(MUTE)$(LATEX) $(LATEX_FLAGS) $*.tex $(REDIR)
-%.pdf:%.tex
- @echo "LATEX $< -> $@"
- $(MUTE)$(LATEX) $(LATEX_FLAGS) $< $(REDIR)
-
%.ind:%.idx
@echo "INDEX $< -> $@ $<"
@@ -174,8 +179,9 @@ package: $(PKG_FILES)
everything: all symbols.pdf compat.pdf tests/test.pdf
tutorial: tutorial/game.pdf
-tutorial/game.pdf: all
- $(MUTE)$(MAKE) -C tutorial Game.vmod $(REDIR)
+tutorial/game.pdf: all
+ @echo "MAKE -C tutorial Game.vmod"
+ $(MUTE)$(MAKE) -C tutorial Game.vmod
clean:
@echo "CLEAN"
@@ -198,6 +204,7 @@ clean:
$(MUTE)rm -rf __pycache__
$(MUTE)rm -rf $(ctandir) tmp
$(MUTE)rm -f ctan.tex $(NAME).ctan.tar.gz
+ $(MUTE)$(MAKE) -C tutorial clean
@@ -220,7 +227,9 @@ wargame.idx: wargame.dtx wargame.sty $(TILES_PDF)
wargame.pdf: wargame.aux wargame.ind
fast: wargame.idx
symbols.aux: symbols.tex wargame.sty
+symbols.pdf: symbols.aux
compat.aux: compat.tex $(TABLES:%=cmp_%.tex) wargame.sty
+compat.pdf: compat.aux
test.aux: test.tex wargame.sty
beach.pdf: beach.tex
@@ -269,11 +278,11 @@ bindist:install
logo.png:logo.pdf
- pdftocairo -png $<
+ pdftocairo -transp -png $<
mv logo-1.png logo.png
README.md.version:README.md
- @echo "Versioned README.md"
+ @echo "Versioned README.md $(VERSION)"
$(MUTE)$(SED) 's/^# \(.*\)/# \1\n## Version $(VERSION)/' \
< $< | pandoc --to markdown > $@
@@ -289,6 +298,7 @@ ctandir: all tutorial/game.pdf README.md.version
$(MUTE)mkdir -p $(ctandir)/$(NAME)
$(MUTE)cp tmp/$(docdir)/README.md $(ctandir)/$(NAME)/
$(MUTE)(cd tmp && zip -q -r ../$(ctandir)/$(NAME).tds.zip *)
+ $(MUTE)rm tmp/$(docdir)/README.md
$(MUTE)(cd tmp && cp -a $(docdir) ../$(ctandir)/$(NAME)/doc)
$(MUTE)(cd tmp && cp -a $(srcdir) ../$(ctandir)/$(NAME)/source)
$(MUTE) rm -rf tmp
diff --git a/macros/latex/contrib/wargame/source/chit/battle.dtx b/macros/latex/contrib/wargame/source/chit/battle.dtx
new file mode 100644
index 0000000000..5057d620a5
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/battle.dtx
@@ -0,0 +1,77 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Battle markers}
+%
+% Takes 1 arguments - the identifier.
+%
+% Define \spec{every battle marker} to change the style.
+%
+% \begin{macrocode}
+\tikzset{%
+ battle marker/.pic={
+ \node[shape=circle,
+ font=\sffamily\bfseries,
+ inner sep=0pt,
+ minimum size=5mm,
+ draw=black,
+ fill=yellow!85!black,
+ every battle marker/.try] at (-.3,.3) {%
+ \ifnum#1>0\relax #1\fi%
+ };
+ },
+ battle marker/.style={
+ chit={full={battle marker=#1},frame={draw=none}}},
+}
+% \end{macrocode}
+%
+% Takes two arguments - the odds and the fill colour. The latter is
+% useful to differentiate the severity of an attack.
+%
+% Define \spec{every odds marker} to change the style.
+%
+% \begin{macrocode}
+\tikzset{%
+ pics/odds marker/.style args={#1,#2}{
+ code={
+ \node[shape=circle,
+ font=\sffamily\bfseries\large,
+ inner sep=0pt,
+ minimum size=8mm,
+ draw=black,
+ fill=#2,
+ every odds marker/.try] at (.2,-.2) {#1};
+ }
+ },
+ odds marker/.style args={#1,#2}{
+ chit={full={odds marker={#1,#2}},frame={draw=none}}},
+}
+% \end{macrocode}
+%
+% Takes two arguments - the result and the fill colour. The latter is
+% useful to differentiate the severity of an attack.
+%
+% Define \spec{every result marker} to change the style.
+%
+% \begin{macrocode}
+\tikzset{
+ pics/result marker/.style args={#1,#2}{
+ code={
+ \message{^^JResults marker #1 (#2)}
+ \node[shape=circle,
+ font=\sffamily\bfseries\large,
+ inner sep=0pt,
+ minimum size=8mm,
+ draw=black,
+ fill=#2,
+ every result marker/.try] at (0,0) {#1};}},
+ result marker/.style args={#1,#2}{
+ chit={full={result marker={#1,#2}},frame={draw=none}}}
+}
+% \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/core.dtx b/macros/latex/contrib/wargame/source/chit/core.dtx
index 7a0189074e..9181e6ede7 100644
--- a/macros/latex/contrib/wargame/source/chit/core.dtx
+++ b/macros/latex/contrib/wargame/source/chit/core.dtx
@@ -39,8 +39,14 @@
%
% \input{chit/shape.dtx}
% \input{chit/elements.dtx}
+% \input{chit/modifiers.dtx}
+% \input{chit/stack.dtx}
+% \input{chit/oob.dtx}
+% \input{chit/table.dtx}
+% \input{chit/battle.dtx}
+% \input{chit/dice.dtx}
% \input{chit/misc.dtx}
-%
+%
% \iffalse
%</chit>
% --------------------------------------------------------------------
diff --git a/macros/latex/contrib/wargame/source/chit/dice.dtx b/macros/latex/contrib/wargame/source/chit/dice.dtx
new file mode 100644
index 0000000000..0c07c91d4f
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/dice.dtx
@@ -0,0 +1,286 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Dice}
+%
+% First, a regular 6-sided dice with configurable number of dots. Use
+% like
+%
+% \begin{Syntax}
+% \cs{pic}\oarg{pic options}\{dice=\meta{eyes}\}
+% \end{Syntax}
+%
+% For example:
+%
+% \begin{enumerate}
+% \item \tikz{\pic[transform shape]{dice=1};}
+% \item \tikz{\pic[transform shape]{dice=2};}
+% \item \tikz{\pic[transform shape]{dice=3};}
+% \item \tikz{\pic[transform shape]{dice=4};}
+% \item \tikz{\pic[transform shape]{dice=5};}
+% \item \tikz{\pic[transform shape]{dice=6};}
+% \end{enumerate}
+%
+% \begin{macrocode}
+\tikzset{
+ dice bg/.style={
+ % /utils/exec={
+ % \pgfgettransformentries{%
+ % \wg@jaca}{%
+ % \wg@jacb}{%
+ % \wg@jacc}{%
+ % \wg@jacd}{%
+ % \wg@tmp}{%
+ % \wg@tmp}%
+ % \pgfmathsetmacro{\wg@tmp}{%
+ % sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))}
+ % \xdef\wg@tmp{\wg@tmp}},%
+ fill=black,
+ draw=none,
+ minimum width=1cm,
+ minimum height=1cm,
+ scale rounded corners,
+ rounded corners=.1cm,
+ inner sep=0pt,
+ transform shape},
+ dice fg/.style={
+ fill=white,
+ shape=circle,
+ inner sep=0pt,
+ minimum size=.2cm,
+ transform shape},
+ pics/dice/.style={
+ code={
+ \node[dice bg] (dice bg) {};
+ \ifodd#1\node[dice fg] at (dice bg) {};\fi
+ \ifnum#1>1%
+ \node[dice fg] at ($(dice bg)+(-45:.4)$){};%
+ \node[dice fg] at ($(dice bg)+(135:.4)$){};%
+ \fi%
+ \ifnum#1>3%
+ \node[dice fg] at ($(dice bg)+( 45:.4)$){};%
+ \node[dice fg] at ($(dice bg)+(-135:.4)$){};%
+ \fi%
+ \ifnum#1=6%
+ \node[dice fg] at ($(dice bg)+(-.282,0)$){};
+ \node[dice fg] at ($(dice bg)+( .282,0)$){};
+ \fi
+ }
+ },
+ pics/dice/.default=3
+}
+\newcommand\dicemark[2][scale=.5]{%
+ \tikz[baseline={($(dice bg.south east)!.25!(dice bg.north east)$)},#1]{
+ \pic[transform shape]{dice=3};}}
+% \end{macrocode}
+%
+% Now some shapes of different dice. This was originally done by
+% \href{https://tex.stackexchange.com/questions/144193/how-do-i-put-a-die-roll-at-the-top-of-each-page#answer-144474}{David
+% Carlisle}. Usage is for example
+%
+% \begin{Syntax}
+% \cs{node}[shape=\meta{dice},\meta{node options}] \marg{value};
+% \end{Syntax}
+%
+% where \meta{dice} is one of \texttt{d4}, \texttt{d6}, \texttt{d8},
+% \texttt{d10}, \texttt{d12}, or \texttt{d20}.
+%
+% \def\DiceExample#1{%
+% \begin{tikzpicture}[
+% die/.style={
+% shape=#1,
+% scale line widths,
+% line width=1pt,
+% transform shape,
+% scale=.7
+% },
+% fg die/.style={
+% die,
+% fill=##1,
+% draw=gray!25!white,
+% text=white},
+% die shadow/.style={
+% die,
+% fill=black,
+% draw=none,
+% opacity=.5}]
+% \node[die shadow,rotate=10] at (.20,.07){};
+% \node[fg die=red,rotate=10] at (.15,.10){};
+% \node[die shadow,rotate=-10] at (-.08,-.11){};
+% \node[fg die=blue,rotate=-10] at (-.15,-.1){};
+% \draw(-.5,-.5)rectangle(.6,.5);
+% \end{tikzpicture}
+% }
+%
+% \paragraph{Tetrahedron}
+%
+% \DiceExample{d4}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d4}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{
+ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathmoveto{\pgfpoint{0cm}{.4cm}}
+ \pgfpathlineto{\pgfpoint{.433cm}{-.35cm}}
+ \pgfpathlineto{\pgfpoint{-.433cm}{-.35cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.4cm}}
+ % \pgfusepath{draw} %draw border
+ % \pgfusepath{draw} %draw rectangle
+ }}
+% \end{macrocode}
+%
+% \paragraph{Cubic}
+%
+% \DiceExample{d6}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d6}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{
+ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathrectanglecorners{\pgfpoint{.4cm}{.4cm}}{\pgfpoint{-.4cm}{-.4cm}}
+ % \pgfusepath{draw} %draw rectangle
+ }}
+% \end{macrocode}
+%
+% \paragraph{Octahedron}
+%
+% \DiceExample{d8}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d8}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{
+ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{.433cm}{.25cm}}
+ \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+ \pgfpathlineto{\pgfpoint{-.433cm}{.25cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{.433cm}{-.25cm}}
+ \pgfpathlineto{\pgfpoint{-.433cm}{-.25cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ % \pgfusepath{draw} %draw interiaor
+ }}
+% \end{macrocode}
+%
+% \paragraph{Decahedron}
+%
+% \DiceExample{d10}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d10}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{
+ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{.294cm}{-.154cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+ \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{.475cm}{.1cm}}
+ \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{-.475cm}{-.1cm}}
+ \pgfpathlineto{\pgfpoint{-.475cm}{.1cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathmoveto{\pgfpoint{.294cm}{-.154cm}}
+ \pgfpathlineto{\pgfpoint{.475cm}{-.1cm}}
+ \pgfpathmoveto{\pgfpoint{-.475cm}{-.1cm}}
+ \pgfpathlineto{\pgfpoint{-.294cm}{-.154cm}}
+ \pgfpathmoveto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.3cm}}
+ % \pgfusepath{draw} %draw interiaor
+ }}
+% \end{macrocode}
+%
+% \paragraph{Dodecahedron}
+%
+% \DiceExample{d12}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d12}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{0.294cm}{.405cm}}
+ \pgfpathlineto{\pgfpoint{.475cm}{.173cm}}
+ \pgfpathlineto{\pgfpoint{.475cm}{-.173cm}}
+ \pgfpathlineto{\pgfpoint{.294cm}{-.405cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{-.294cm}{-.405cm}}
+ \pgfpathlineto{\pgfpoint{-.475cm}{-.173cm}}
+ \pgfpathlineto{\pgfpoint{-.475cm}{.173cm}}
+ \pgfpathlineto{\pgfpoint{-.294cm}{.405cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+ \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+ \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+ \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+ \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.349cm}}
+ \pgfpathmoveto{\pgfpoint{.475cm}{.173cm}}
+ \pgfpathlineto{\pgfpoint{.332cm}{.108cm}}
+ \pgfpathmoveto{\pgfpoint{.294cm}{-.405cm}}
+ \pgfpathlineto{\pgfpoint{.205cm}{-.282cm}}
+ \pgfpathmoveto{\pgfpoint{-.294cm}{-.405cm}}
+ \pgfpathlineto{\pgfpoint{-.205cm}{-.282cm}}
+ \pgfpathmoveto{\pgfpoint{-.475cm}{.173cm}}
+ \pgfpathlineto{\pgfpoint{-.332cm}{.108cm}}
+ % \pgfusepath{draw} %draw interiaor
+ }}
+% \end{macrocode}
+%
+% \paragraph{Icosohedron}
+%
+% \DiceExample{d20}
+%
+% \begin{macrocode}
+\pgfdeclareshape{d20}{
+ \anchor{center}{\pgfpointorigin} % within the node, (0,0) is the center
+ \anchor{text}{ % this is used to center the text in the node
+ \pgfpoint{-.5\wd\pgfnodeparttextbox}{-.5\ht\pgfnodeparttextbox}}
+ \backgroundpath{ % draw border
+ \pgfpathmoveto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+ \pgfpathlineto{\pgfpoint{.454cm}{-.262cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{-.454cm}{-.262cm}}
+ \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.5cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+ \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+ \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+ \pgfpathlineto{\pgfpoint{.454cm}{.262cm}}
+ \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{-.5cm}}
+ \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+ \pgfpathlineto{\pgfpoint{-.454cm}{.262cm}}
+ \pgfpathlineto{\pgfpoint{0cm}{.292cm}}
+ \pgfpathmoveto{\pgfpoint{.454cm}{-.262cm}}
+ \pgfpathlineto{\pgfpoint{.253cm}{-.146cm}}
+ \pgfpathmoveto{\pgfpoint{-.454cm}{-.262cm}}
+ \pgfpathlineto{\pgfpoint{-.253cm}{-.146cm}}
+ % \pgfusepath{draw} %draw interiaor
+ }}
+% \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/elements.dtx b/macros/latex/contrib/wargame/source/chit/elements.dtx
index 8708ca2a15..6585c94064 100644
--- a/macros/latex/contrib/wargame/source/chit/elements.dtx
+++ b/macros/latex/contrib/wargame/source/chit/elements.dtx
@@ -5,6 +5,12 @@
%
% \subsubsection{Predefined \texttt{chit} element pictures}
%
+% \begin{macrocode}
+\DeclareRobustCommand\chit@sep[2][/]{%
+ \foreach[count=\is] \s in {#2}{%
+ \ifnum\is>1\relax#1\fi%
+ \s}}
+% \end{macrocode}
% \begin{TikzKey}{/tikz/pics/chit/1 factor,
% /tikz/pics/chit/2 factors,
% /tikz/pics/chit/2 factors artillery,
@@ -28,24 +34,32 @@
pics/chit/2 factors artillery/.style args={#1,#2,#3}{%
code={
\chit@dbg{4}{ Chit 2 factors w/artillery: `#1' `#2' `#3'}%
- \node[chit/factor,chit/2 factors,pic actions]{%
- #1$\overset{\text{\scriptsize #3}}{\text{--}}$#2};}},
+ \node[chit/factor,chit/2 factors]{%
+ {#1}$\overset{\text{\scriptsize #3}}{\text{--}}${#2}};}},
pics/chit/3 factors/.style args={#1,#2,#3}{%
code={
\chit@dbg{4}{ Chit 3 factors: `#1' `#2' `#3'}%
- \node[chit/factor,chit/3 factors,pic actions]{#1-#2-#3};}},
+ \node[chit/factor,chit/3 factors]{#1-#2-#3};}},
pics/chit/4 factors/.style args={#1,#2,#3,#4}{%
code={
\chit@dbg{4}{ Chit 3 factors: `#1' `#2' `#3' `#4'}%
- \node[chit/factor,chit/4 factors,pic actions]{#1-#2-#3-#4};}},
+ \node[chit/factor,chit/4 factors]{#1-#2-#3-#4};}},
chit/identifier/.pic={
\chit@dbg{4}{ Chit identifier: `#1'}%
\node[chit/identifier,pic actions]{#1};
},
+ chit/identifiers/.pic={
+ \chit@dbg{4}{ Chit identifiers: `#1'}%
+ \node[chit/identifier,pic actions]{\chit@sep{#1}};
+ },
chit/small identifier/.pic={
\chit@dbg{4}{ Chit small identifier: `#1'}%
\node[chit/small identifier,pic actions]{#1};
},
+ chit/small identifiers/.pic={
+ \chit@dbg{4}{ Chit small identifiers: `#1'}%
+ \node[chit/small identifier,pic actions]{\chit@sep{#1}};
+ },
chit/identifier macro/.pic={%
\chit@dbg{4}{ Chit identifier macro: \meaning#1}
\edef\chit@i@tmp{#1}
@@ -69,32 +83,35 @@
\tikzset{%
chit/factor/.style={
shape=rectangle,
- font=\sffamily\bfseries\large,
+ font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
anchor=base,
inner sep=0,
%text=pgfstrokecolor,
draw=none,
fill=none,
+ transform shape,
},
chit/1 factor/.style={},
chit/2 factors/.style={},
chit/3 factors/.style={},
- chit/4 factors/.style={text/.append style=\small},
+ chit/4 factors/.style={text/.append style=\fontsize{10}{12}\selectfont},
chit/identifier/.style={
shape=rectangle,
- font=\sffamily\bfseries\scriptsize,
+ font=\sffamily\bfseries\fontsize{8}{9}\selectfont,
inner sep=0,
% text=pgfstrokecolor,
draw=none,
fill=none,
+ transform shape,
},
chit/small identifier/.style={
shape=rectangle,
- font=\sffamily\bfseries\tiny,
+ font=\sffamily\bfseries\fontsize{6}{7}\selectfont,
inner sep=0,
% text=pgfstrokecolor,
draw=none,
fill=none,
+ transform shape,
},
}
% \end{macrocode}
diff --git a/macros/latex/contrib/wargame/source/chit/misc.dtx b/macros/latex/contrib/wargame/source/chit/misc.dtx
index de373864e6..a5b9a79b24 100644
--- a/macros/latex/contrib/wargame/source/chit/misc.dtx
+++ b/macros/latex/contrib/wargame/source/chit/misc.dtx
@@ -1,391 +1,5 @@
% \iffalse
-% --------------------------------------------------------------------
-%<*chit>
-% \fi
-% \subsubsection{Other pictures}
-% Pictures for frame, factors, left, right, and below.
-% \begin{macrocode}
-\tikzset{
- pics/chit/shade/.style={
- code={%
- \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
- pics/chit/eliminate/.style={
- code={%
- \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
- pics/chit/shade/.default=0.5,
- pics/chit/eliminate/.default=0.25,
-}
-\def\shadechit(#1){%
- \pic at (#1) {chit/shade};}
-\def\eliminatechit(#1){%
- \pic at (#1) {chit/eliminate};}
-% \end{macrocode}
-%
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Stacking of chits}
-%
-% Stacking of chits. The key \spec{chit/stack direction} sets the
-% default direction to make the stack in.
-%
-% \begin{macrocode}
-% offset, location, direction, list
-\tikzset{%
- chit/stack direction/.store in=\chit@stack@dir,
- chit/stack direction/.initial={(.3,.3)},
-}
-% \end{macrocode}
-%
-% Now the code
-%
-% \begin{macrocode}
-\def\chit@stack@dir{(.3,.3)}
-\def\stackchits(#1){%
- \@ifnextchar({\st@ckchits{#1}}{\st@ckchits{#1}(.3,.3)}%)
-}
-\def\st@ckchits#1(#2)#3{
- \chit@dbg{2}{Stacking chits `#1', `#2', `#3'}
- \edef\xy{#1}
- \chit@dbg{4}{Stack start at \xy}
- \foreach[count=\i from 0] \c/\o in {#3} {%
- \ifx\c\empty\else%
- \edef\ccc{\c}
- \chit@dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}
- \expandafter\ccc(\xy)
- %%
- \ifx\c\o\else
- %\chit@dbg{0}{Option: \o}
- \edef\ccc{\o}
- \expandafter\ccc(\xy)
- \fi
- \expandafter\ccc(\xy)
- \tikzmath{%
- coordinate \cc;
- \cc = (\xy) + (#2);}
- \xdef\xy{\cc}
- \fi
- }
-}
-% \end{macrocode}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Making order of battle charts}
-%
-%
-% Macros for making OOBs
-%
-% Style for turns
-%
-% \begin{macrocode}
-\tikzset{
- chit/oob turn/.pic={\node[pic actions]{#1};}}
-% \end{macrocode}
-%
-%
-% current c, current r, n-columns, cell size, y
-%
-% \begin{macrocode}
-\def\chit@oob@cellupdate(#1,#2)#3#4#5{%
- \edef\f{\ifwg@oob@inv-1\else1\fi}%
- \chit@dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)}
- \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}%
- \xdef#5{\pgfmathresult}
- \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}%
- \xdef#2{\pgfmathresult}%
- \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}%
- \xdef#1{\pgfmathresult}%
- \chit@dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1}
-}
-% \end{macrocode}
-%
-% current c, current r, cell size, extra vertical spacing
-%
-% \begin{macrocode}
-\def\chit@oob@rowupdate(#1,#2)#3#4{%
- \chit@dbg{2}{ Row update c=`#1',r=`#2',s=`#3',e=`#4'}
- %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}%
- \pgfmathparse{#2-#3)}%
- \xdef#2{\pgfmathresult}%
- %\xdef#1{0}
- \chit@dbg{2}{ \space\space-> update `\string#2'=#2}
-}
-% \end{macrocode}
-% current c, current r, cell size, extra spacing
-%
-% \begin{macrocode}
-\def\chit@oob@turnupdate(#1,#2)#3#4{%
- \chit@dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'}
- % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}%
- \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)}
- \xdef#2{\pgfmathresult}%
- \xdef#1{0}%
- \chit@dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2}
-}
-% \end{macrocode}
-% chit list, n-colls, cell size, extra vertical spacing
-%
-% This expects a list of lists of chits, one list per turn; the
-% maximum number of columns; the size of cells, extra spacing between
-% turns.
-%
-% Note, the list of lists leaf elements should be styles for the
-% chits.
-%
-% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the
-% number as argument.
-%
-%
-% \begin{macrocode}
-\newif\ifwg@oob@inv\wg@oob@invfalse
-\def\wg@star@oob{\wg@oob@invtrue\wg@oob}
-\def\wg@nostar@oob{\wg@oob@invfalse\wg@oob}
-\def\oob{%
- \@ifstar{\wg@star@oob%
- }{\wg@nostar@oob%
- }%
-}
-\def\wg@oob#1#2#3#4{
- \def\r{0}
- \chit@dbg{1}{OOB: `#1'}
- \foreach[count=\ti from 0] \t/\y in #1{
- \xdef\o{\r}
- \def\c{0}
- \ifx\t\y\def\y{0}\fi
- \chit@dbg{1}{Turn \ti\space(\r,\t,y=\y):'}
- \ifwg@oob@inv%
- \pic at ( .5,\r) {chit/oob turn=\ti};%
- \else
- \pic at (-.5,\r) {chit/oob turn=\ti};%
- \fi%
- \ifx\t\empty\else%
- \foreach \u/\m in \t{
- %% \chit@dbg{2}{ `\u'=`\m'}
- \ifx\u\empty\else
- \ifx\m\@empty\def\m{1}\fi
- \ifx\u\m\def\m{1}\fi
- \foreach \n in {1,...,\m}{%
- \ifx\u\chit@blank\else
- \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);
- \fi
- \chit@oob@cellupdate(\c,\r){#2}{#3}{\y}
- }
- \fi
- }
- \fi
- \chit@dbg{1}{ End of chits in turn
- \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
- % IF no units where given, then we force \c to be non-zero so that
- % \chit@oob@turnupdate increments the row
- \ifx\t\@empty
- \def\c{#3}
- \chit@dbg{2}{ Turn is empty, set c=`\c'}
- \fi
- %\ifnum\y<0% No explicit number of rows given
- % \def\c{#3}
- % \chit@dbg{2}{ No explicit number of rows given, set c=`\c'}
- %\fi
- % In case the user gave and explicit number of rows, add the rows
- % that are missing. \y is initially set to the number of
- % requested rows, and then decremented every time we go down one
- % row. So if the number of rows we did so far is N, and the
- % requested number of rows is M, then the loop below adds M-N
- % rows.
- \ifnum\y>0%
- \chit@dbg{2}{ Looping rows from 2 to \y, break when row > \y}%
- \foreach \rr in {2,...,\y}{
- %\ifnum\rr>\y% A little funny, but \y can be negative!
- % \chit@dbg{2}{ \space Breaking loop \rr\space > \y}%
- % \breakforeach%
- %\else%
- \chit@oob@rowupdate(\c,\r){#3}{#4}
- %\fi
- }
- \fi
- % This will zero \c. However, if on entry |\c|>0, then we also
- % increment the row
- \chit@oob@turnupdate(\c,\r){#3}{#4}
- \chit@dbg{1}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
- }
- \chit@dbg{2}{End of OOB (c=`\c',r=`\r',y=`\y')}
-}
-% \end{macrocode}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-% \subsubsection{Table of chits}
-%
-% \begin{macrocode}
-\tikzset{
- chit/cell background/.style={fill=black},
- blank chit/.style={/chit/frame={draw=none,fill=none}},
-}
-% \end{macrocode}
-%
-% These macros are used when we set tables of chits. This allows us
-% to define blank spaces in the table by giving the element
-% \texttt{blank~chit}.
-%
-% \begin{macrocode}
-\def\chit@blank{blank chit}
-\def\chit@cellbg(#1,#2)#3{%
- \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3);
-}
-% \end{macrocode}
-%
-% \begin{Macro}{\ifchits@reset}
-%
-% This `if' controls whether to reset the coordinates to the origin
-% when \cs{chits} is called. If true, then reset for a new table.
-%
-% \begin{macrocode}
-\newif\ifchits@reset\chits@resettrue
-% \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\chits,\@chits,\chit@sng@cellupdate}
-% \begin{macrocode}
-\def\chit@sng@cellupdate(#1,#2)#3#4{%
- \chit@dbg{2}{Current `#1' vs `#4'*(`#3'+1)}
- \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}%
- \xdef#2{\pgfmathresult}%
- \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}%
- \xdef#1{\pgfmathresult}%
-}
-% \end{macrocode}
-%
-% The stared version (\cs{chits*}) of this macro continues the
-% previously set chit table.
-%
-% \begin{macrocode}
-\def\chits{%
- \@ifstar{\chits@resetfalse\@chits}{\chits@resettrue\@chits}}
-% \end{macrocode}
-%
-% \begin{macrocode}
-\def\@chits#1#2#3{
- \ifchits@reset
- \def\r{0}%
- \def\c{0}%
- \fi
- \chit@dbg{1}{Chits to make: #1}%
- \foreach[count=\ti from 0] \t/\x in #1{%
- \chit@dbg{2}{Turn `\t' with option `\x'}
- \ifx\t\empty\else%
- \foreach \u/\m in \t{%
- \ifx\u\empty\else%
- \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
- \ifx\m\@empty\def\m{1}\fi%
- \ifx\u\m\def\m{1}\fi%
- \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
- \foreach \n in {1,...,\m}{%
- \ifx\u\chit@blank%
- \chit@dbg{3}{Ignoring blank chit:\u}%
- \else%
- \chit@cellbg(\c,\r){#3}%
- \chit[\u=\ti](\c,\r)%
- \chit@sng@cellupdate(\c,\r){#2}{#3}%
- \fi%
- }%
- \fi%
- }%
- \fi%
- }%
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\doublechits,
-% \@doublechits,
-% \chit@dbl@cellupdate,
-% \chit@dbl@flip}
-% \begin{enumerate}
-% \item coordinates
-% \item coordinates
-% \item cell-size
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\chit@dbl@flip(#1,#2)#3{%
- \pgfmathparse{-#1}%
- \xdef\mc{\pgfmathresult}%
-}
-% \end{macrocode}
-%
-% \begin{enumerate}
-% \item coordinates
-% \item coordinates
-% \item Number of columns
-% \item cell-size
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\chit@dbl@cellupdate(#1,#2)#3#4{%
- \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}%
- \xdef#2{\pgfmathresult}%
- \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}%
- \xdef#1{\pgfmathresult}%
-}
-% \end{macrocode}
-%
-% \begin{enumerate}
-% \item List of list of keys
-% \item Number of columns
-% \item size of each cell
-% \end{enumerate}
-%
-% The stared version (\cs{doublechits*}) of this macro continues the
-% previously set chit table.
-%
-% \begin{macrocode}
-\def\doublechits{%
- \@ifstar{\chits@resetfalse\@doublechits}{\chits@resettrue\@doublechits}}
-% \end{macrocode}
-%
-% \begin{macrocode}
-\def\@doublechits#1#2#3{%
- \chit@dbg{1}{Setting double-sided chits: #1}
- \ifchits@reset
- \pgfmathparse{-(#2-.5)*#3}
- \xdef\c{\pgfmathresult}
- \def\r{0}
- \fi
-
- \foreach[count=\ti from 0] \t/\x in #1{
- \ifx\t\empty\else%
- \foreach \u/\m in \t{
- \ifx\u\empty\else
- \ifx\m\@empty\def\m{1}\else%
- \ifx\u\m\def\m{1}\fi\fi
- \chit@dbg{2}{`\u'=`\m' (\c,\r)}
- \foreach \n in {1,...,\m}{%
- \ifx\u\chit@blank
- \chit@dbg{3}{Ignoring blank chit:\u}
- \else
- \chit@cellbg(\c,\r){#3}
- \chit[\u=\ti](\c,\r)
- \chit@dbl@flip(\c,\r){#3}
- \chit@cellbg(\mc,\r){#3}
- \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r)
- \chit@dbl@cellupdate(\c,\r){#2}{#3}
- \fi
- }
- \fi
- }
- \fi
- }
- \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);
- \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[anchor=west]{Back};
- \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[anchor=east]{Front};
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% \iffalse
+% <*chit>
% --------------------------------------------------------------------
% \fi
% \subsubsection{Some utilities}
@@ -395,17 +9,32 @@
%
% \begin{macrocode}
\tikzset{
- number chit/.pic={
- \node[shape=rectangle,font=\sffamily\bfseries\LARGE]{%
- \begin{tabular}{c} #1\end{tabular}};},
- game turn/.pic={
- \node[shape=rectangle,font=\sffamily\bfseries]{%
- \begin{tabular}{c} Game\\Turn\end{tabular}};},
- game turn/.style={
- /chit/full={game turn},
+ chit/text base/.style={
+ shape=rectangle,
+ inner sep=0pt,
+ align=center,
+ text width=1.1cm},
+ chit/number/.style={
+ chit/text base,
+ font=\sffamily\bfseries\fontsize{12}{14}\selectfont},
+ chit/game turn/.style={
+ chit/text base,
+ font=\sffamily\bfseries},
+ chit/text/.style={
+ chit/text base,
+ font=\sffamily\bfseries},
+ chit/small text/.style={
+ chit/text base,
+ font=\sffamily\bfseries\fontsize{9}{10}\selectfont},
+ chit/number/.pic={\node[chit/number]{#1};},
+ chit/game turn/.pic={\node[chit/game turn]{Game\\Turn};},
+ chit/text/.pic={\node[chit/text]{#1};},
+ chit/small text/.pic={\node[chit/small text]{#1};},
+ game turn chit/.style={
+ /chit/full={chit/game turn},
color=black,
fill=white},
- game turn flipped/.style={game turn},
+ game turn chit flipped/.style={game turn chit},
dummy chit/.style={fill=white},
}
% \end{macrocode}
@@ -435,7 +64,7 @@
%
% \begin{macrocode}
\DeclareRobustCommand\zocmark[1][]{%
- \tikz[baseline=(current bounding box.center),scale=.1,#1]{%
+ \tikz[baseline=($(current bounding box.center)!.5!(current bounding box.south)$),scale=.1,#1]{%
\begin{scope}[hex/first row and column are=0,
hex/row direction is=normal,
hex/column direction is=normal,
@@ -450,6 +79,10 @@
\end{scope}}}
% \end{macrocode}
%
+% Dummy implementations of zones hooks when exporting. Here, these do
+% nothing, but in the \textsf{wgexport} class these are
+% re-implemented.
+%
% \begin{macrocode}
\tikzset{
zone point/.code n args={3}{},
@@ -457,5 +90,5 @@
% \end{macrocode}
%
% \iffalse
-%</chit>
+% </chit>
% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/modifiers.dtx b/macros/latex/contrib/wargame/source/chit/modifiers.dtx
new file mode 100644
index 0000000000..b42ee5790f
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/modifiers.dtx
@@ -0,0 +1,42 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*chit>
+% \fi
+% \subsubsection{Modifications to chits}
+%
+% These defines overlays one can add on top of chits, for example to
+% shade a chit, put a semi-transparent red cover to indicate
+% elimination, and similar.
+%
+% \begin{macrocode}
+\tikzset{
+ pics/chit/shade/.style={
+ code={%
+ \path[fill=white,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
+ pics/chit/eliminate/.style={
+ code={%
+ \path[fill=red,opacity=#1,pic actions] (-.6,-.6) rectangle(.6,.6);}},
+ pics/chit/shade/.default=0.5,
+ pics/chit/eliminate/.default=0.25,
+ dummy chit/.style={draw=none,fill=none,chit={}},
+}
+\def\shadechit{%
+ \@ifnextchar[{\sh@dechit}{\sh@dechit[.5]}%]
+}
+\def\eliminatechit{%
+ \@ifnextchar[{\elimin@techit}{\elimin@techit[.25]}%]
+}
+\def\sh@dechit[#1](#2){%
+ % \message{^^JShading chit with opacity `#1'}%
+ \pic[transform shape] at (#2) {chit/shade=#1};%
+ \@ifnextchar;{\@gobble}{}}
+\def\elimin@techit[#1](#2){%
+ \pic[transform shape] at (#2) {chit/eliminate=#1};%
+ \@ifnextchar;{\@gobble}{}}
+% \end{macrocode}
+%
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </chit>
+% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/oob.dtx b/macros/latex/contrib/wargame/source/chit/oob.dtx
new file mode 100644
index 0000000000..d640dadebf
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/oob.dtx
@@ -0,0 +1,172 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Making order of battle charts}
+%
+%
+% Macros for making OOBs
+%
+% Style for turns
+%
+% \begin{macrocode}
+\tikzset{
+ chit/oob turn/.pic={\node[pic actions]{#1};}}
+% \end{macrocode}
+%
+%
+% current c, current r, n-columns, cell size, y
+%
+% \begin{macrocode}
+\def\chit@oob@cellupdate(#1,#2)#3#4#5{%
+ \edef\f{\ifwg@oob@inv-1\else1\fi}%
+ \chit@dbg{1}{ \space Cell update `c=|#1|' vs `#4'*(`#3'-1)}
+ \pgfmathparse{int(ifthenelse(abs(#1)>=#4*(#3-1),#5-1,#5))}%
+ \xdef#5{\pgfmathresult}
+ \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),#2-#4,#2)}%
+ \xdef#2{\pgfmathresult}%
+ \pgfmathparse{ifthenelse(abs(#1)>=#4*(#3-1),0,#1+\f*#4)}%
+ \xdef#1{\pgfmathresult}%
+ \chit@dbg{1}{ \space\space-> `\string#5'=#5 `\string#2'=#2 `\string#1'=#1}
+}
+% \end{macrocode}
+%
+% current c, current r, cell size, extra vertical spacing
+%
+% \begin{macrocode}
+\def\chit@oob@rowupdate(#1,#2)#3#4{%
+ \chit@dbg{2}{ Row update c=`#1',r=`#2',s=`#3',e=`#4'}
+ %\pgfmathparse{ifthenelse(#1>0,#2-#3,#2)}%
+ \pgfmathparse{#2-#3)}%
+ \xdef#2{\pgfmathresult}%
+ %\xdef#1{0}
+ \chit@dbg{2}{ \space\space-> update `\string#2'=#2}
+}
+% \end{macrocode}
+% current c, current r, cell size, extra spacing
+%
+% \begin{macrocode}
+\def\chit@oob@turnupdate(#1,#2)#3#4{%
+ \chit@dbg{2}{ Turn update c=`#1',r=`#2',s=`#3',e=`#4'}
+ % \pgfmathparse{#2-ifthenelse(#1>0,#3,0)-#4}%
+ \pgfmathparse{#2-#4-ifthenelse(abs(#1)>0.0001,#3,0)}
+ \xdef#2{\pgfmathresult}%
+ \xdef#1{0}%
+ \chit@dbg{2}{ \space\space-> update `\string#1'=#1,`\string#2'=#2}
+}
+% \end{macrocode}
+% chit list, n-colls, cell size, extra vertical spacing
+%
+% This expects a list of lists of chits, one list per turn; the
+% maximum number of columns; the size of cells, extra spacing between
+% turns.
+%
+% Note, the list of lists leaf elements should be styles for the
+% chits.
+%
+% This depends on the Tikz pic \texttt{chit/oob~turn} which takes the
+% number as argument.
+%
+%
+% \begin{macrocode}
+\newif\ifwg@oob@inv\wg@oob@invfalse
+\def\chit@oob@spacer{hspace}
+\def\wg@star@oob{\wg@oob@invtrue\wg@oob}
+\def\wg@nostar@oob{\wg@oob@invfalse\wg@oob}
+\def\oob{%
+ \@ifstar{\wg@star@oob%
+ }{\wg@nostar@oob%
+ }%
+}
+% \end{macrocode}
+%
+% The inner macro of \cs{oob}. The arguments are
+% \begin{enumerate}
+% \item The list of lists of chits styles
+% \item The maximum number of columns
+% \item The width of each cell
+% \item Additional row spacing between turns
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\wg@oob#1#2#3#4{
+ \def\r{0}
+ \chit@dbg{2}{OOB: `#1'}
+ \foreach[count=\ti from 0] \t/\y in #1{
+ \xdef\o{\r}
+ \def\c{0}
+ \ifx\t\y\def\y{0}\fi
+ \chit@dbg{2}{Turn \ti\space(\r,\t,y=\y):'}
+ \ifwg@oob@inv%
+ \pic[transform shape] at ( .5*#3,\r) {chit/oob turn=\ti};% was dx=0.5
+ \else
+ \pic[transform shape] at (-.5*#3,\r) {chit/oob turn=\ti};% was dx=-0.5
+ \fi%
+ \ifx\t\empty\else%
+ \foreach \u/\m in \t{
+ %% \chit@dbg{2}{ `\u'=`\m'}
+ \ifx\u\empty\else
+ \ifx\m\@empty\def\m{1}\fi
+ \ifx\u\m\def\m{1}\fi
+ \foreach \n in {1,...,\m}{%
+ \chit@dbg{2}{OOB Chit is `\u'}%
+ \ifx\u\chit@oob@spacer%
+ \chit@dbg{3}{Chit `\u' is spacer `\chit@oob@spacer'}
+ \pgfmathparse{\c+#4}%
+ \xdef\c{\pgfmathresult}%
+ \else
+ \ifnum\chitdbglvl>2%
+ \node[minimum width=#3cm,minimum height=#3cm,
+ draw,transform shape] at (\c,\r) {};
+ \fi
+ \ifx\u\chit@blank\else%
+ \chit[\u=\ti,zone oob point={\u}{\c}{\r}](\c,\r);%
+ \fi%
+ \chit@oob@cellupdate(\c,\r){#2}{#3}{\y}
+ \fi
+ }
+ \fi
+ }
+ \fi
+ \chit@dbg{1}{ End of chits in turn
+ \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
+ % IF no units where given, then we force \c to be non-zero so that
+ % \chit@oob@turnupdate increments the row
+ \ifx\t\@empty
+ \def\c{#3}
+ \chit@dbg{2}{ Turn is empty, set c=`\c'}
+ \fi
+ %\ifnum\y<0% No explicit number of rows given
+ % \def\c{#3}
+ % \chit@dbg{2}{ No explicit number of rows given, set c=`\c'}
+ %\fi
+ % In case the user gave and explicit number of rows, add the rows
+ % that are missing. \y is initially set to the number of
+ % requested rows, and then decremented every time we go down one
+ % row. So if the number of rows we did so far is N, and the
+ % requested number of rows is M, then the loop below adds M-N
+ % rows.
+ \ifnum\y>0%
+ \chit@dbg{2}{ Looping rows from 2 to \y, break when row > \y}%
+ \foreach \rr in {2,...,\y}{
+ %\ifnum\rr>\y% A little funny, but \y can be negative!
+ % \chit@dbg{2}{ \space Breaking loop \rr\space > \y}%
+ % \breakforeach%
+ %\else%
+ \chit@oob@rowupdate(\c,\r){#3}{#4}
+ %\fi
+ }
+ \fi
+ % This will zero \c. However, if on entry |\c|>0, then we also
+ % increment the row
+ \chit@oob@turnupdate(\c,\r){#3}{#4}
+ \chit@dbg{2}{End of turn \ti\space(c=`\c',r=`\r',o='\o',y='\y')}
+ }
+ \chit@dbg{3}{End of OOB (c=`\c',r=`\r',y=`\y')}
+ \@ifnextchar;{\@gobble}{}}
+% \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/shape.dtx b/macros/latex/contrib/wargame/source/chit/shape.dtx
index 8ad0614cb8..8a3d70fbd0 100644
--- a/macros/latex/contrib/wargame/source/chit/shape.dtx
+++ b/macros/latex/contrib/wargame/source/chit/shape.dtx
@@ -4,6 +4,19 @@
% \fi
%
% \subsubsection{The \texttt{chit} key namespace}
+%
+% Some stuff to consider wrt.~line widths. Setting the line width in
+% the \texttt{chit} scope overrides frame settings. The frame stroke
+% can be larger but not smaller. Setting the stroke width in the
+% symbol scope sets it for the symbol only. Thus, to get a thin
+% border, we need to
+% \begin{itemize}
+% \item Set a small line width in the top chit scope.
+% \item Possible set a larger line width in the frame sub-scope.
+% \item Set a larger line width in the symbol sub-scope.
+% \end{itemize}
+% I do not know why this is.
+%
% \begin{TikzKey}{/chit/full,
% /chit/symbol,
% /chit/left,
@@ -13,12 +26,15 @@
% /chit/lower left,
% /chit/lower right,
% /chit/factors,
+% /chit/extra,
% /chit/setup,
+% /chit/bevel,
% /chit/id}
%
% The parts of a chit
%
% \begin{macrocode}
+\newif\ifchit@clip\chit@cliptrue
\tikzset{%
/chit/.search also={/tikz},
/chit/.cd,
@@ -36,6 +52,21 @@
setup/.store in=\chit@setup, setup/.initial=,%
id/.store in=\chit@id, id/.initial=,%
frame/.store in=\chit@frame, frame/.initial=,%
+ extra/.store in=\chit@extra, extra/.initial=,%
+ bev/.store in=\chit@bevel, bev/.initial=,
+ bevel fraction/.store in=\chit@bevel@frac,bevel fraction/.initial=10,
+ bevel/.is choice,
+ bevel/none/.style = {/chit/bev=},
+ bevel/north west/.style = {/chit/bev=1},
+ bevel/north east/.style = {/chit/bev=2},
+ bevel/south west/.style = {/chit/bev=3},
+ bevel/south east/.style = {/chit/bev=4},
+ bevel/NW/.style = {/chit/bev=1},
+ bevel/NE/.style = {/chit/bev=2},
+ bevel/SW/.style = {/chit/bev=3},
+ bevel/SE/.style = {/chit/bev=4},
+ bevel/.default = north west,
+ clip/.is if=chit@clip%
}
% \end{macrocode}
% \end{TikzKey}
@@ -69,7 +100,28 @@
chit/lower right/.style={chit/parts,anchor=south east},
chit/setup/.style={chit/parts},
chit/full/.style={chit/parts},
+ chit/frame/.try={draw=pgfstrokecolor},
+ chit/bevel highlight/.style={fill=white,opacity=.25},
+ chit/bevel shadow/.style={fill=black,opacity=.25},
}
+\def\chit@bevel@frac{10}
+\newif\ifchit@draw@frame\chit@draw@frametrue
+\tikzset{
+ chit/frame style/.search also={/tikz},
+ chit/frame style/.cd,
+ none/.code={\chit@draw@framefalse},
+ draw/.code={%
+ \chit@dbg{2}{Frame draw option `#1'}
+ \edef\tikz@temp{#1}%
+ \ifx\tikz@temp\tikz@nonetext%
+ \chit@draw@framefalse%
+ \else%
+ \chit@draw@frametrue%
+ \tikzset{/tikz/draw=#1}
+ \fi
+ }
+}
+
% \end{macrocode}
% \end{TikzKey}
%
@@ -116,7 +168,7 @@
% \begin{macrocode}
\def\chit@n@to#1#2{%
%% Without a following start square bracket '[' by-pass to final
- \chit@dbg{4}{Chit NATO App6(c) first step `#1' `#2'}
+ \chit@dbg{1}{Chit NATO App6(c) first step `#1' `#2'}
\@ifnextchar[{%
%\message{^^JStart square bracket}%
\@chit@n@to{#1}{#2}}{%
@@ -129,7 +181,7 @@
%
% \begin{macrocode}
\def\@chit@n@to@#1#2#3\@end@chit@n@to{%
- \chit@dbg{4}{Chit NATO App6(c) w/o offset:
+ \chit@dbg{1}{Chit NATO App6(c) w/o offset:
^^J Options: #3
^^J ID: #1
^^J Position: #2}
@@ -143,7 +195,7 @@
%
% \begin{macrocode}
\def\@chit@n@to#1#2[#3]{%
- \chit@dbg{4}{Chit NATO App6(c) second step `#1' `#2' `#3'}
+ \chit@dbg{1}{Chit NATO App6(c) second step `#1' `#2' `#3'}
\@ifnextchar({\@@chit@n@to{#1}{#2}{#3}}{\@@chit@n@to{#1}{#2}{#3}(0,0)}%)
}
% \end{macrocode}
@@ -152,7 +204,7 @@
%
% \begin{macrocode}
\def\@@chit@n@to#1#2#3(#4)\@end@chit@n@to{%
- \chit@dbg{4}{Chit NATO App6(c) w/offset:
+ \chit@dbg{1}{Chit NATO App6(c) w/offset:
^^J Options: #3
^^J ID: #1
^^J Position: #2
@@ -186,8 +238,39 @@
%
% Now follows the actual \text{chit} shape. This is rather long, so
% we will break it up a bit
-%
-%
+% \begin{macrocode}
+\def\chit@bevel@path#1{
+ \scope[#1]
+ \wg@tmpc=\wg@tmpa\multiply\wg@tmpc by \chit@bevel@frac
+ \wg@tmpd=\wg@tmpb\multiply\wg@tmpd by \chit@bevel@frac
+ \divide\wg@tmpc100
+ \divide\wg@tmpd100
+ \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Move down along edge
+ \wg@tmpb=-\wg@tmpb
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Move left along edge
+ \wg@tmpa=-\wg@tmpa
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Move in and up
+ \advance\wg@tmpa\wg@tmpc%
+ \advance\wg@tmpb\wg@tmpd%
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Move right, but in
+ \advance\wg@tmpa-\wg@tmpc\wg@tmpa=-\wg@tmpa%
+ \advance\wg@tmpa-\wg@tmpc%
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Move up but down
+ \advance\wg@tmpb-\wg@tmpd\wg@tmpb=-\wg@tmpb%
+ \advance\wg@tmpb-\wg@tmpd%
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \pgfclosepath%
+ \pgfusepath{fill}
+ \endscope
+}
+
+% \end{macrocode}
+%
% The first thing is we declare some saved anchors. These are
% computed (and defined as internal macros) when the shape is
% instantised. The anchors give the centre and north east corner of
@@ -224,7 +307,7 @@
\let\chitframeopt\pgfutil@empty%
\@ifundefined{chit@frame}{}{%
\edef\chitframeopt{\chit@frame}}
- \n@to@pp@dbg{3}{Chit Frame options: \meaning\chitframeopt}%
+ \chit@dbg{3}{Chit Frame options: \meaning\chitframeopt}%
}
% \end{macrocode}
%
@@ -296,6 +379,7 @@
\backgroundpath{%
%% This is the outline of the chit only. The rest of the chit is
%% made on the foreground "path".
+ \chit@dbg{1}{Chit drawing background path}
\northeast%
\wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
\pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
@@ -314,29 +398,38 @@
%
% \begin{macrocode}
\behindforegroundpath{%
- \chit@dbg{4}{%
- Chit foreground: \meaning\id
- ^^J ID (set): \meaning\chit@id
- ^^J Symbol: \meaning\chit@symbol
- ^^J Full: \meaning\chit@full
- ^^J Factors: \meaning\chit@factors
- ^^J Left: \meaning\chit@left
- ^^J Right: \meaning\chit@right
- ^^J Upper left: \meaning\chit@upper@left
- ^^J Lower left: \meaning\chit@lower@left
- ^^J Upper right: \meaning\chit@upper@right
- ^^J Lower right: \meaning\chit@lower@right}
+ \chit@dbg{1}{Chit drawing foreground path}
+ % \chit@dbg{4}{%
+ % Chit foreground: \meaning\id
+ % ^^J ID (set): \meaning\chit@id
+ % ^^J Symbol: \meaning\chit@symbol
+ % ^^J Full: \meaning\chit@full
+ % ^^J Factors: \meaning\chit@factors
+ % ^^J Left: \meaning\chit@left
+ % ^^J Right: \meaning\chit@right
+ % ^^J Upper left: \meaning\chit@upper@left
+ % ^^J Lower left: \meaning\chit@lower@left
+ % ^^J Upper right: \meaning\chit@upper@right
+ % ^^J Lower right: \meaning\chit@lower@right
+ % ^^J Extra: \meaning\chit@extra
+ % ^^J Bevel: \meaning\chit@bevel
+ % ^^J Frame: \meaning\chit@frame}
+ \chit@dbg{1}{Chit report}
\chit@report{}
+ \chit@dbg{1}{Chit start scope}
\pgfscope
%
- \northeast%
- \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
- \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \pgfclosepath
- \pgfusepath{clip}
+ \ifchit@clip%
+ \chit@dbg{1}{Chit clip path}
+ \northeast%
+ \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
+ \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \pgfclosepath%
+ \pgfusepath{clip}%
+ \fi%
% \end{macrocode}
%
% If we do not have the \texttt{symbol} key set, then we set the
@@ -346,19 +439,22 @@
\@ifundefined{chit@symbol}{%
%% Draw full stuff
\@ifundefined{chit@full}{}{%
+ \chit@dbg{1}{Chit draw full image: `\meaning\chit@full'}
\center\wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
\wg@pic@all{\chit@full}{}{\the\wg@tmpa,\the\wg@tmpb}{chit/full}}%
- }{%
+ }{% With NATO symbol
% \end{macrocode}
%
% Otherwise, we put in a node with shape \texttt{natoapp6c} and pass
% the \texttt{symbol} key--value pairs as options.
%
% \begin{macrocode}
+ \chit@dbg{1}{Chit draw symbol image}
\edef\symid{\id symbol}%
\symbol%
\edef\args{{\symid}{\the\pgf@x,\the\pgf@y}\chit@symbol}%
\chit@dbg{6}{Arguments to chit NATO symbol: \meaning\args}%
+ \chit@dbg{1}{Chit draw nato image}
\expandafter\chit@n@to\args\@end@chit@n@to%
\chit@dbg{6}{After making NATO symbol in chit}%
% \end{macrocode}
@@ -378,6 +474,7 @@
% \begin{macrocode}
% Put in left of symbol
\@ifundefined{chit@left}{}{%
+ \chit@dbg{1}{Chit draw left: `\meaning\chit@left'}
\begin{scope}[]
\pgfpointanchor{\symid}{west}%
\wg@tmpa=\pgf@x\advance\wg@tmpa-\margin%
@@ -386,6 +483,7 @@
\end{scope}}%
% Put in right of symbol
\@ifundefined{chit@right}{}{%
+ \chit@dbg{1}{Chit draw left: `\meaning\chit@right'}
\begin{scope}[]
\pgfpointanchor{\symid}{east}%
\wg@tmpa=\pgf@x\advance\wg@tmpa+\margin%
@@ -416,24 +514,28 @@
% \begin{macrocode}
% Put in upper left corner
\@ifundefined{chit@upper@left}{}{%
+ \chit@dbg{1}{Chit draw upper left: `\meaning\chit@upper@left'}
\begin{scope}[]
\wg@pic@all{\chit@upper@left}{}{-\the\wg@tmpa,\the\wg@tmpb}{%
chit/upper left}%
\end{scope}}
% Put in upper right corner
\@ifundefined{chit@upper@right}{}{%
+ \chit@dbg{1}{Chit draw upper right: `\meaning\chit@upper@right'}
\begin{scope}[]
\wg@pic@all{\chit@upper@right}{}{\the\wg@tmpa,\the\wg@tmpb}{%
chit/upper right}%
\end{scope}}
% Put in lower left corner
\@ifundefined{chit@lower@left}{}{%
+ \chit@dbg{1}{Chit draw lower left: `\meaning\chit@lower@left'}
\begin{scope}[]
\wg@pic@all{\chit@lower@left}{}{-\the\wg@tmpa,-\the\wg@tmpb}{%
chit/lower left}%
\end{scope}}
% Put in lower right corner
\@ifundefined{chit@lower@right}{}{%
+ \chit@dbg{1}{Chit draw lower right: `\meaning\chit@lower@right'}
\begin{scope}[]
\wg@pic@all{\chit@lower@right}{}{\the\wg@tmpa,-\the\wg@tmpb}{%
chit/lower right}%
@@ -448,24 +550,62 @@
% \begin{macrocode}
% Put in factors
\@ifundefined{chit@factors}{}{%
+ \chit@dbg{1}{Chit draw factors: `\meaning\chit@factors'}
\advance\wg@tmpb-\margin%
\begin{scope}[]
\wg@pic@all{\chit@factors}{}{0,-\the\wg@tmpb}{chit/factors}%
\end{scope}}%
- }%
+ % Put in extra
+ \@ifundefined{chit@extra}{}{%
+ \chit@dbg{1}{Chit draw extra: `\meaning\chit@extra'}
+ \begin{scope}[]
+ \wg@pic@all{\chit@extra}{}{0,0}{chit/factors}%
+ \end{scope}}%
+ }% End of full or symbol
\endpgfscope%
+ % Make bevel?
+ \@ifundefined{chit@bevel}{\let\chit@bevel\empty}{}
+ \ifx\chit@bevel\empty\else%
+ \chit@dbg{1}{Chit draw bevel}
+ %% South east bevel
+ \northeast%
+ \wg@tmpa=-\pgf@x\wg@tmpb=-\pgf@y%
+ \ifcase\chit@bevel\relax%
+ \or% 1
+ \or\wg@tmpa=-\wg@tmpa% 2
+ \or\wg@tmpb=-\wg@tmpb% 3
+ \or\wg@tmpa=-\wg@tmpa\wg@tmpb=-\wg@tmpb%4
+ \fi
+ \chit@bevel@path{chit/bevel highlight}
+ %% North west bevel
+ \northeast%
+ \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
+ \ifcase\chit@bevel\relax%
+ \or% 1
+ \or\wg@tmpa=-\wg@tmpa% 2
+ \or\wg@tmpb=-\wg@tmpb% 3
+ \or\wg@tmpa=-\wg@tmpa\wg@tmpb=-\wg@tmpb%4
+ \fi
+ \chit@bevel@path{chit/bevel shadow}
+ \fi
% Draw frame?
- \edef\tmp@opt{[\chitframeopt]}
+ \chit@dbg{1}{Chit draw frame: `\meaning\chitframeopt'}
+ \edef\tmp@opt{[chit/frame style/.cd,chit/frame/.try,\chitframeopt]}
+ \chit@dbg{1}{Chit draw frame: `\meaning\tmp@opt}
\expandafter\scope\tmp@opt
- \northeast%
- \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
- \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
- \pgfclosepath
- \pgfusepath{stroke}
- \endscope
+ \northeast%
+ \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
+ \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpb=-\wg@tmpb \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \wg@tmpa=-\wg@tmpa \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ \pgfclosepath%
+ \chit@dbg{3}{Line width for frame: `\the\pgflinewidth'}
+ \ifchit@draw@frame\pgfusepath{stroke}\fi%
+ \chit@draw@frametrue%
+ %\iftikz@mode@fill\pgfusepath{fill}\fi%
+ \endscope%
+ \chit@dbg{1}{Chit end of shape}
}
}
% \end{macrocode}
@@ -527,7 +667,7 @@
^^J Name: `#3'}
\let\name\pgfutil@empty%
\chit@dbg{1}{=== Before chit node}%
- \node[draw,chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
+ \node[chit={every chit/.try,id=#3,#1}] (tmp) at (#2) {};
\chit@dbg{2}{=== After chit node}%
\ifx|#3|\relax%
\else%
diff --git a/macros/latex/contrib/wargame/source/chit/stack.dtx b/macros/latex/contrib/wargame/source/chit/stack.dtx
new file mode 100644
index 0000000000..bc8f480e31
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/stack.dtx
@@ -0,0 +1,54 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Stacking of chits}
+%
+% Stacking of chits. The key \spec{chit/stack direction} sets the
+% default direction to make the stack in.
+%
+% \begin{macrocode}
+% offset, location, direction, list
+\tikzset{%
+ chit/stack direction/.store in=\chit@stack@dir,
+ chit/stack direction/.initial={(.3,.3)},
+}
+% \end{macrocode}
+%
+% Now the code
+%
+% \begin{macrocode}
+\def\chit@stack@dir{(.3,.3)}
+\def\stackchits(#1){%
+ \@ifnextchar({\st@ckchits{#1}}{\st@ckchits{#1}(.3,.3)}%)
+}
+\def\st@ckchits#1(#2)#3{%
+ \chit@dbg{2}{Stacking chits `#1', `#2', `#3'}%
+ \edef\xy{#1}%
+ \chit@dbg{4}{Stack start at \xy}%
+ \foreach[count=\i from 0] \c/\o in {#3} {%
+ \ifx\c\empty\else%
+ \edef\ccc{\c}%
+ \chit@dbg{2}{Adding \meaning\ccc\space to stack at (\xy)' `\o'}%
+ \expandafter\ccc(\xy)%
+ %%
+ \ifx\c\o\else%
+ %\chit@dbg{0}{Option: \o}
+ \edef\ccc{\o}%
+ \expandafter\ccc(\xy)%
+ \fi
+ \expandafter\ccc(\xy)%
+ \tikzmath{%
+ coordinate \cc;%
+ \cc = (\xy) + (#2);}
+ \xdef\xy{\cc}%
+ \fi%
+ }%
+ \@ifnextchar;{\@gobble}{}%
+}
+% \end{macrocode}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/chit/table.dtx b/macros/latex/contrib/wargame/source/chit/table.dtx
new file mode 100644
index 0000000000..7094e436ed
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/chit/table.dtx
@@ -0,0 +1,176 @@
+% \iffalse
+% <*chit>
+% --------------------------------------------------------------------
+% \fi
+% \subsubsection{Table of chits}
+%
+% \begin{macrocode}
+\tikzset{
+ chit/cell background/.style={fill=black},
+ blank chit/.style={/chit/frame={draw=none,fill=none}},
+}
+% \end{macrocode}
+%
+% These macros are used when we set tables of chits. This allows us
+% to define blank spaces in the table by giving the element
+% \texttt{blank~chit}.
+%
+% \begin{macrocode}
+\def\chit@blank{blank chit}
+\def\chit@cellbg(#1,#2)#3{%
+ \draw[chit/cell background](#1-#3/2,#2-#3/2) rectangle++(#3,#3);
+}
+% \end{macrocode}
+%
+% \begin{Macro}{\ifchits@reset}
+%
+% This `if' controls whether to reset the coordinates to the origin
+% when \cs{chits} is called. If true, then reset for a new table.
+%
+% \begin{macrocode}
+\newif\ifchits@reset\chits@resettrue
+% \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\chits,\@chits,\chit@sng@cellupdate}
+% \begin{macrocode}
+\def\chit@sng@cellupdate(#1,#2)#3#4{%
+ \chit@dbg{2}{Current `#1' vs `#4'*(`#3'+1)}
+ \pgfmathparse{ifthenelse(#1>=#4*(#3-1),#2-#4,#2)}%
+ \xdef#2{\pgfmathresult}%
+ \pgfmathparse{ifthenelse(#1>=#4*(#3-1),0,#1+#4)}%
+ \xdef#1{\pgfmathresult}%
+}
+% \end{macrocode}
+%
+% The stared version (\cs{chits*}) of this macro continues the
+% previously set chit table.
+%
+% \begin{macrocode}
+\def\chits{%
+ \@ifstar{\chits@resetfalse\@chits}{\chits@resettrue\@chits}}
+% \end{macrocode}
+%
+% \begin{macrocode}
+\def\@chits#1#2#3{
+ \ifchits@reset
+ \def\r{0}%
+ \def\c{0}%
+ \fi
+ \chit@dbg{1}{Chits to make: #1}%
+ \foreach[count=\ti from 0] \t/\x in #1{%
+ \chit@dbg{2}{Turn `\t' with option `\x'}
+ \ifx\t\empty\else%
+ \foreach \u/\m in \t{%
+ \ifx\u\empty\else%
+ \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+ \ifx\m\@empty\def\m{1}\fi%
+ \ifx\u\m\def\m{1}\fi%
+ \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
+ \foreach \n in {1,...,\m}{%
+ \ifx\u\chit@blank%
+ \chit@dbg{3}{Ignoring blank chit:\u}%
+ \else%
+ \chit@cellbg(\c,\r){#3}%
+ \chit[\u=\ti](\c,\r)%
+ \chit@sng@cellupdate(\c,\r){#2}{#3}%
+ \fi%
+ }%
+ \fi%
+ }%
+ \fi%
+ }%
+ \@ifnextchar;{\@gobble}{}}
+% \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\doublechits,
+% \@doublechits,
+% \chit@dbl@cellupdate,
+% \chit@dbl@flip}
+% \begin{enumerate}
+% \item coordinates
+% \item coordinates
+% \item cell-size
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\chit@dbl@flip(#1,#2)#3{%
+ \pgfmathparse{-#1}%
+ \xdef\mc{\pgfmathresult}%
+}
+% \end{macrocode}
+%
+% \begin{enumerate}
+% \item coordinates
+% \item coordinates
+% \item Number of columns
+% \item cell-size
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\chit@dbl@cellupdate(#1,#2)#3#4{%
+ \pgfmathparse{ifthenelse(#1<-#4/2,#2,#4+#2)}%
+ \xdef#2{\pgfmathresult}%
+ \pgfmathparse{ifthenelse(#1<-#4/2,#4+#1,-(#3-.5)*#4)}%
+ \xdef#1{\pgfmathresult}%
+}
+% \end{macrocode}
+%
+% \begin{enumerate}
+% \item List of list of keys
+% \item Number of columns
+% \item size of each cell
+% \end{enumerate}
+%
+% The stared version (\cs{doublechits*}) of this macro continues the
+% previously set chit table.
+%
+% \begin{macrocode}
+\def\doublechits{%
+ \@ifstar{\chits@resetfalse\@doublechits}{\chits@resettrue\@doublechits}}
+% \end{macrocode}
+%
+% \begin{macrocode}
+\def\@doublechits#1#2#3{%
+ \chit@dbg{1}{Setting double-sided chits: #1}
+ \ifchits@reset
+ \pgfmathparse{-(#2-.5)*#3}
+ \xdef\c{\pgfmathresult}
+ \def\r{0}
+ \fi
+
+ \foreach[count=\ti from 0] \t/\x in #1{
+ \ifx\t\empty\else%
+ \foreach \u/\m in \t{
+ \ifx\u\empty\else
+ \ifx\m\@empty\def\m{1}\else%
+ \ifx\u\m\def\m{1}\fi\fi
+ \chit@dbg{2}{`\u'=`\m' (\c,\r)}
+ \foreach \n in {1,...,\m}{%
+ \ifx\u\chit@blank
+ \chit@dbg{3}{Ignoring blank chit:\u}
+ \else
+ \chit@cellbg(\c,\r){#3}
+ \chit[\u=\ti](\c,\r)
+ \chit@dbl@flip(\c,\r){#3}
+ \chit@cellbg(\mc,\r){#3}
+ \chit[\u\space flipped=\ti,zone turn=\t,zone mult=\n](\mc,\r)
+ \chit@dbl@cellupdate(\c,\r){#2}{#3}
+ \fi
+ }
+ \fi
+ }
+ \fi
+ }
+ \draw[dashed](0,-3*#3/4)--(0,\r-#3/4);%
+ \draw[dashed,<-] (#3/5,-2*#3/3)--(#3/2,-2*#3/3) node[transform shape,anchor=west]{Back};%
+ \draw[dashed,<-] (-#3/5,-2*#3/3)--(-#3/2,-2*#3/3) node[transform shape,anchor=east]{Front};%
+ \@ifnextchar;{\@gobble}{}}
+% \end{macrocode}
+% \end{Macro}
+%
+% \iffalse
+% </chit>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/hex/board.dtx b/macros/latex/contrib/wargame/source/hex/board.dtx
index fd0af295fb..7c791a72b3 100644
--- a/macros/latex/contrib/wargame/source/hex/board.dtx
+++ b/macros/latex/contrib/wargame/source/hex/board.dtx
@@ -18,82 +18,261 @@
%
%
% \begin{macrocode}
-\tikzset{
- hex/board frame/.style={draw}
-}
\def\boardframe{%
- \@ifnextchar[{\bo@rdframe}{\bo@rdframe[0]}%}
+ \@ifnextchar[{\bo@rdframe}{\bo@rdframe[0]}%]
+}
+% \end{macrocode}
+%
+% Below is our new implementation of \cs{boardframe}. This is split
+% into parts.
+%
+%
+% First, a macro that will define the path around rectangular placed
+% hexes. This takes 4 mandatory arguments: lower left column and row,
+% and upper right column and row, in that order. It also accepts an
+% optional argument. If this is not empty, then it is assumed to be a
+% style to apply, and hexes will be drawn using that style. The style
+% will be passed the hex coordinates and can react accordingly.
+%
+% \begin{macrocode}
+\def\bo@rdfr@me{
+ \@ifnextchar[{\bo@rdfr@me@}{\bo@rdfr@me@[]}%]
+}
+\def\bo@rdfr@me@u(#1)#2#3#4#5{
+ \hex@coords@conv{#1}
+ % \hex@dbg{0}{#1 -> `\hex@x',`\hex@y'}
+ \pgfmathparse{min(#2,\hex@x)}\xdef#2{\pgfmathresult}
+ \pgfmathparse{min(#3,\hex@y)}\xdef#3{\pgfmathresult}
+ \pgfmathparse{max(#4,\hex@x)}\xdef#4{\pgfmathresult}
+ \pgfmathparse{max(#5,\hex@y)}\xdef#5{\pgfmathresult}
+ \hex@dbg{2}{#1 -> ll=`#2',`#3', ur=`#4',`#5'}
+}
+\def\bo@rdfr@me@[#1]#2#3#4#5{
+ % Define rtmp and a ctmp to by directions
+ \pgfmathparse{int(\hex@coords@row@fac)}\edef\rtmp{\pgfmathresult}
+ \pgfmathparse{int(\hex@coords@col@fac)}\edef\ctmp{\pgfmathresult}
+ % Define vertices for path
+ \def\ctfv{SW}
+ \def\ctsv{SE}
+ \def\cbfv{NE}
+ \def\cbsv{NW}
+ \def\rrfv{E}
+ \def\rrsv{NE}
+ \def\rlfv{W}
+ \def\rlsv{SW}
+ % Swap around some definitions based on the row direction
+ \ifnum\rtmp<0
+ \let\max@short\hex@bot@short@col
+ \let\min@short\hex@top@short@col
+ \let\swp\ctfv\let\ctfv\cbsv\let\cbsv\swp
+ \let\swp\ctsv\let\ctsv\cbfv\let\cbfv\swp
+ \def\rrsv{SE}
+ \def\rlsv{NW}
+ \else
+ \let\max@short\hex@top@short@col
+ \let\min@short\hex@bot@short@col
+ \fi
+ % Swap around some definitions based on the column direction
+ \ifnum\ctmp<0
+ \let\swp\ctfv\let\ctfv\ctsv\let\ctsv\swp
+ \let\swp\cbfv\let\cbfv\cbsv\let\cbsv\swp
+ \let\swp\rrfv\let\rrfv\rlsv\let\rlsv\swp
+ \let\swp\rrsv\let\rrsv\rlfv\let\rlfv\swp
+ \fi
+ % Define tmp = 0 if no shorts, 1 if top short, 2 if both
+ \pgfmathparse{ifthenelse(\hex@got@top@short,
+ ifthenelse(\hex@got@bot@short,2,1),0)}\edef\tmp{\pgfmathresult}
+ % If top-short, set factors
+ \ifnum\tmp=1
+ \def\mnf{-1}
+ \def\mxf{-1}
+ \def\mnn{}
+ \def\mxn{}
+ % If both short, set factors
+ \else\ifnum\tmp=2
+ \def\mnf{\rtmp}
+ \def\mxf{(-\rtmp)}
+ % If inverse rows, set factors
+ \ifnum\rtmp<0
+ \def\mnn{}
+ \def\mxn{not}
+ \else
+ \def\mnn{not}
+ \def\mxn{}
+ \fi
+ % If none is short
+ \else
+ \def\mnf{1}
+ \def\mxf{1}
+ \def\mnn{not}
+ \def\mxn{not}
+ \fi\fi
+ % Define row@mn to give least row of column
+ \def\row@mn##1{%
+ \pgfmathparse{int(#3+\mnf*
+ \hex@coords@row@fac*\min@short(##1)*
+ \mnn(\min@short(\hex@coords@col@off)))}
+ \edef\lr{\pgfmathresult}}
+ % Define row@mx to give largest row of column
+ \def\row@mx##1{%
+ \pgfmathparse{int(#5+\mxf*
+ \hex@coords@row@fac*\max@short(##1)*
+ \mxn(\max@short(\hex@coords@col@off)))}
+ \edef\ur{\pgfmathresult}}
+ %
+ %
+ % Below defines a path around the perimeter of the hexes.
+ %
+ \def\@llx{10000}
+ \def\@lly{10000}
+ \def\@urx{-10000}
+ \def\@ury{-10000}
+ % Start with an empty path
+ \def\p{}
+ % Loop across least row (can be top if \rtmp<0)
+ \foreach \c in {#2,...,#4}{%
+ \row@mn{\c}
+ \row@mx{\c}
+ % \message{^^JColumn: `\c' -> `\lr',`\ur' (#3,#5)}
+ }
+ \foreach \c in {#2,...,#4}{%
+ \row@mn{\c}
+ \xdef\p{\p
+ (hex cs:c=\c,r=\lr,v=\ctfv)--
+ (hex cs:c=\c,r=\lr,v=\ctsv)--}
+ \bo@rdfr@me@u(c=\c,r=\lr,v=\ctfv)\@llx\@lly\@urx\@ury
+ \bo@rdfr@me@u(c=\c,r=\lr,v=\ctsv)\@llx\@lly\@urx\@ury
+ }
+ % Go up (down if \rtmp<0) right side
+ \row@mn{#4}
+ \row@mx{#4}
+ \foreach \r in {\lr,...,\ur}{%
+ \xdef\p{\p
+ (hex cs:c=#4,r=\r,v=\rrfv)--
+ (hex cs:c=#4,r=\r,v=\rrsv)--}
+ \bo@rdfr@me@u(c=#4,r=\r,v=\rrfv)\@llx\@lly\@urx\@ury
+ \bo@rdfr@me@u(c=#4,r=\r,v=\rrsv)\@llx\@lly\@urx\@ury
+ }
+ % Go across largest row (can be bottom if \rtmp<0)
+ \foreach \c in {#4,...,#2}{%
+ \row@mx{\c}
+ % \message{^^JColumn: `\c', max:`\ur'}
+ \xdef\p{\p
+ (hex cs:c=\c,r=\ur,v=\cbfv)--
+ (hex cs:c=\c,r=\ur,v=\cbsv)--}
+ \bo@rdfr@me@u(c=\c,r=\ur,v=\cbfv)\@llx\@lly\@urx\@ury
+ \bo@rdfr@me@u(c=\c,r=\ur,v=\cbsv)\@llx\@lly\@urx\@ury
+ }
+ % Go up (down if \rtmp<0) left side.
+ \row@mn{#2}
+ \row@mx{#2}
+ \foreach \r in {\ur,...,\lr}{%
+ \xdef\p{\p
+ (hex cs:c=#2,r=\r,v=\rlfv)--
+ (hex cs:c=#2,r=\r,v=\rlsv)--}
+ \bo@rdfr@me@u(c=#2,r=\r,v=\rlfv)\@llx\@lly\@urx\@ury
+ \bo@rdfr@me@u(c=#2,r=\r,v=\rlsv)\@llx\@lly\@urx\@ury
+ }
+ % End path with cycle
+ \edef\p{\p cycle}
+ % Define global path
+ \global\let\hex@board@path\p
+ \hex@dbg{3}{Hex board path: `\meaning\hex@board@path'}
+ % If an optional argument was given, then use that to actually make
+ % hexes.
+ \ifx|#1|\else
+ \foreach[count=\nc] \c in {#2,...,#4}{%
+ \row@mn{\c}
+ \row@mx{\c}
+ \foreach \r in {\lr,...,\ur}{%
+ \hex[#1={\c,\r}](c=\c,r=\r)
+ }
+ }
+ \fi
+}
+% \end{macrocode}
+%
+%
+% This is a no operations style used as default for the macro
+% \cs{boardhexes} below.
+%
+% \begin{macrocode}
+\tikzset{%
+ /hex/board/no op/.style args={#1,#2}{}}
+% \end{macrocode}
+%
+% This macro will make the actual hexes using the specified, optional,
+% style. It builds on \cs{bo@rdfr@me} above.
+%
+% \begin{macrocode}
+\def\boardhexes{%
+ \@ifnextchar[{\bo@rdhexes}{\bo@rdhexes[board/no op]}%]
}
+\def\bo@rdhexes[#1](#2)(#3){%
+ \hex@coords@conv{#2}
+ \edef\llc{\hex@col}
+ \edef\llr{\hex@row}
+ \hex@coords@conv{#3}
+ \edef\urc{\hex@col}
+ \edef\urr{\hex@row}
+ \bo@rdfr@me[#1]{\llc}{\llr}{\urc}{\urr}}
+% \end{macrocode}
+%
+% Creates a board frame using \cs{bo@rdfr@me}.
+%
+% \begin{macrocode}
+\tikzset{board frame bb/.code={
+ \pgfkeys{
+ %/tikz/local bounding box=tmp board frame,
+ /tikz/transform shape,
+ /tikz/execute at end scope={%
+ % \hex@dbg{1}{Getting board frame BB}
+ %\wg@get@bb{tmp board frame}
+ \global\let\llx\@llx
+ \global\let\lly\@lly
+ \global\let\urx\@urx
+ \global\let\ury\@ury
+ % \hex@dbg{0}{Board bounding box (\llx,\lly)x(\urx,\ury)}
+ }}}}
+
\def\bo@rdframe[#1](#2)(#3){%
\hex@coords@conv{#2}
- \edef\llx{\hex@x}
- \edef\lly{\hex@y}
\edef\llc{\hex@col}
\edef\llr{\hex@row}
- \edef\ellc{\hex@eff@col}
- \edef\ellr{\hex@eff@row}
%
\hex@coords@conv{#3}
- \edef\urx{\hex@x}
- \edef\ury{\hex@y}
\edef\urc{\hex@col}
\edef\urr{\hex@row}
- \edef\eurc{\hex@eff@col}
- \edef\eurr{\hex@eff@row}
%
\def\margin{#1}
%
- \hex@dbg{2}{%
- Board Hex range: (\llc,\llr)x(\urc,\urr)
- ^^JEffective range: (\ellc,\ellr)x(\eurc,\eurr)
- ^^JBB: (\llx,\lly)x(\urx,\ury)}%
- \ifnum\hexdbglvl>1
- %\draw[red,very thick](hex cs:c=\llc,r=\llr) rectangle(hex cs:c=\urc,r=\urr);
- \draw[red,ultra thick,dashed](\llx,\lly) rectangle(\urx,\ury);
- \draw[->,very thick,blue] (0,0) -- (0,1) (0,0) -- (1,0);
- \fi
- % Calculate how many half hex hides to add to the "bottom"
+ % This will store the bounding box in tmp node `board frame'
+ \bo@rdfr@me{\llc}{\llr}{\urc}{\urr}%
+ \begin{scope}[board frame bb]
+ \expandafter\path\hex@board@path;
+ \end{scope}
+ \hex@dbg{1}{Board frame LL: -> `\llx',`\lly'}
+ \pgfmathparse{\llx+ifthenelse(\llx<0,-1,1)*\margin}\edef\llx{\pgfmathresult}
+ \pgfmathparse{\lly+ifthenelse(\lly<0,-1,1)*\margin}\edef\lly{\pgfmathresult}
%
- \def\oddeven{isodd}
- \ifnum\hex@coords@row@fac<0\def\oddeven{iseven}\fi%
- \pgfmathparse{
- ifthenelse(\hex@got@bot@short(\ellc),
- ifthenelse(\hex@bot@short@col(\llc)*not(\oddeven(\ellc)),2,
- ifthenelse(\hex@bot@short@col(\llc),0,1)),
- ifthenelse(\oddeven(\ellc),1,2))}
- \edef\olly{\pgfmathresult}%
- \hex@dbg{2}{Delta lly: \olly half heights}
- % Calculate how many half hex heights to add to the "top"
- \def\oddeven{iseven}
- \ifnum\hex@coords@row@fac<0\def\oddeven{isodd}\fi%
+ \hex@dbg{1}{Board frame UR: -> `\urx',`\ury'}
+ \pgfmathparse{\urx+ifthenelse(\urx<0,-1,1)*\margin}\edef\urx{\pgfmathresult}
+ \pgfmathparse{\ury+ifthenelse(\ury<0,-1,1)*\margin}\edef\ury{\pgfmathresult}
%
- \pgfmathparse{
- ifthenelse(\hex@got@top@short(\urc),
- ifthenelse(\hex@top@short@col(\urc)*\oddeven(\eurc),0,
- ifthenelse(\hex@top@short@col(\urc),2,1)),
- ifthenelse(\oddeven(\eurc),1,2))}
- \edef\oury{\pgfmathresult}%
- \hex@dbg{2}{Delta ury: \oury half heights}
- % Calculate new LLY and URY
- \pgfmathparse{\lly-\hex@coords@row@fac*(\olly*\hex@yy+\margin)}
- \edef\lly{\pgfmathresult}
- \pgfmathparse{\ury+\hex@coords@row@fac*(\oury*\hex@yy+\margin)}
- \edef\ury{\pgfmathresult}
- % Calculate new LLX and URX
- \pgfmathparse{\llx-1-\margin}\edef\llx{\pgfmathresult}
- \pgfmathparse{\urx+1+\margin}\edef\urx{\pgfmathresult}
- % Calculate width and height
\pgfmathparse{\urx-\llx}\edef\w{\pgfmathresult}
\pgfmathparse{\ury-\lly}\edef\h{\pgfmathresult}
+ %% Print to the log
\hex@dbg{0}{Board Frame: (\llx,\lly)x(\urx,\ury) (\w x\h) (\llc,\llr)x(\urc,\urr)}
+ %% Possibly draw
\draw[hex/board frame/.try](\llx,\lly) rectangle(\urx,\ury);
+ %% Store macros
\xdef\boardXmin{\llx}%
\xdef\boardYmin{\lly}%
\xdef\boardXmax{\urx}%
\xdef\boardYmax{\ury}%
-}
-
+}
% \end{macrocode}
+%
% \end{Macro}
%
% \begin{Macro}{\boardclip}
@@ -104,108 +283,28 @@
% \cs{boardclip}\marg{nx}\marg{ny}\marg{preaction}
% \end{Syntax}
% \begin{macrocode}
-\def\boardpath(#1)(#2){%
+\def\boardpath(#1)(#2){
\hex@coords@reset%
\tikzset{/hex/coords/.cd, #1}
- \edef\llx{\hex@col}
- \edef\lly{\hex@row}
+ \edef\llc{\hex@col}
+ \edef\llr{\hex@row}
%%
\hex@coords@reset%
\tikzset{/hex/coords/.cd, #2}
- \edef\urx{\hex@col}
- \edef\ury{\hex@row}
- \let\board@odd\@undefined%
- \hex@dbg{1}{Board BB in hex: (\llx,\lly)x(\urx,\ury)}
- %%
- \def\fv{south}
- \def\sv{north}
- \ifnum\hex@coords@row@fac<0
- \def\fv{north}
- \def\sv{south}
- \fi
-
- \edef\hex@board@path{(hex cs:c=\llx,r=\lly,v=\fv\space west)}
- %% First the left side
- \foreach \r in {\lly,...,\ury} {%
- \edef\t{
- --(hex cs:c=\llx,r=\r,v=west)
- --(hex cs:c=\llx,r=\r,v=\sv\space west)}
- \wg@addto@macro{\hex@board@path}{\t}}
- %% Then for top of board
- \foreach \c in {\llx,...,\urx} {%
- % To be done
- %\pgfmathparse{int(ifthenelse(\hex@bot@short@col(\c),1,0))}
- %\edef\tmp{\pgfmathresult}
- %\ifnum\tmp>0
- %\edef\t{
- % --(hex cs:c=\c,r=\ury,v=\sv\space west)
- % --(hex cs:c=\c,r=\ury,v=\sv\space east)}
- %\else
- \edef\t{
- --(hex cs:c=\c,r=\ury,v=\fv\space east)
- --(hex cs:c=\c,r=\ury,v=\fv\space west)}
- %\fi
- \wg@addto@macro{\hex@board@path}{\t}}
- %% Then for right of board
- \foreach \r in {\ury,...,\lly} {%
- \edef\t{
- --(hex cs:c=\urx,r=\r,v=east)
- --(hex cs:c=\urx,r=\r,v=\fv\space east)}
- \wg@addto@macro{\hex@board@path}{\t}}
-
- %% Then for bottom of board
- \edef\t{--(hex cs:r=\lly,c=\urx,v=\fv\space west)}
- \wg@addto@macro{\hex@board@path}{\t}
- \foreach \c in {\urx,...,\llx} {%
- \pgfmathparse{int(ifthenelse(\hex@bot@short@col(\c),1,0))}
- \edef\tmp{\pgfmathresult}
- \ifnum\tmp>0
- \edef\t{
- --(hex cs:c=\c,r=\lly,v=\sv\space east)
- --(hex cs:c=\c,r=\lly,v=\sv\space west)}
- \else
- \edef\t{
- --(hex cs:c=\c,r=\lly,v=\fv\space east)
- --(hex cs:c=\c,r=\lly,v=\fv\space west)}
- \fi
- \wg@addto@macro{\hex@board@path}{\t}}
-
- \def\t{--cycle}
- \wg@addto@macro{\hex@board@path}{\t}
+ \edef\urc{\hex@col}
+ \edef\urr{\hex@row}
+
+ % This will store the bounding box in tmp node `board frame'
+ \bo@rdfr@me{\llc}{\llr}{\urc}{\urr}%
+ %% Use the path to extract the bounding box
+ %\begin{scope}[local bounding box=board frame]
+ % \expandafter\path\hex@board@path;
+ %\end{scope}
\global\let\hexboardpath\hex@board@path
}
-%\def\boardclip#1#2#3{%
-% \pgfmathparse{int(#1-1)}\xdef\board@range{\pgfmathresult,...,0}%
-% %% \show\board@range
-% \draw \ifx|#3|\else[preaction={#3}]\fi%
-% [clip]
-% % [decorate,decoration={show path construction,
-% % moveto code={\fill[red](\tikzinputsegmentfirst) circle(2pt)
-% % node [fill=none,below]{moveto};},
-% % lineto code={\draw[thick,blue,->](\tikzinputsegmentfirst)--
-% % (\tikzinputsegmentlast) node [above] {lineto};},
-% % curveto code={\draw[thick,green,->](\tikzinputsegmentfirst)..
-% % controls(\tikzinputsegmentsupporta) and
-% % (\tikzinputsegmentsupportb)
-% % ..(\tikzinputsegmentlast) node[above]{curveto};},
-% % closepath code={\draw[thick,orange,->](\tikzinputsegmentfirst)--
-% % (\tikzinputsegmentlast) node [above] {closepath};}
-% % }]
-% (hex cs:r=0,c=0,v=south west)
-% %% First the left side
-% \foreach \r in {0,1,...,#2} {%
-% --(hex cs:c=0,r=\r,v=west)--(hex cs:c=0,r=\r,v=north west)}
-% %% Then for top of board
-% \foreach \c in {0,1,...,#1} {%
-% --(hex cs:r=#2,c=\c,v=north west)--(hex cs:c=\c,r=#2,v=north east)}
-% %% Then for right of board
-% \foreach \r in {#2,...,0} {%
-% --(hex cs:c=#1,r=\r,v=east)--(hex cs:c=#1,r=\r,v=south east)}
-% %% Then for bottom of board
-% --(hex cs:r=0,c=#1,v=south west) \foreach \c in \board@range {%
-% --(hex cs:r=0,c=\c,v=south east) --(hex cs:c=\c,r=0,v=south west) }
-% --cycle; }
-%% New definition - much simpler
+
+% \end{macrocode}
+% \begin{macrocode}
\def\boardclip(#1)(#2)#3{%
\boardpath(#1)(#2)
\draw \ifx|#3|\else[preaction={#3}]\fi%
diff --git a/macros/latex/contrib/wargame/source/hex/coord.dtx b/macros/latex/contrib/wargame/source/hex/coord.dtx
index b996ffa3c2..147ae82892 100644
--- a/macros/latex/contrib/wargame/source/hex/coord.dtx
+++ b/macros/latex/contrib/wargame/source/hex/coord.dtx
@@ -278,11 +278,10 @@
% \begin{macrocode}
\pgfmathparse{int(\hex@coords@col@fac*(\hex@col+\hex@coords@col@off))}%
\xdef\hex@eff@col{\pgfmathresult}%
- \hex@dbg{2}{Effective column: \hex@coords@col@fac * (\hex@col -
+ \hex@dbg{2}{Effective column: \hex@coords@col@fac * (\hex@col +
\hex@coords@col@off) -> \hex@eff@col}%
\pgfmathparse{\hex@eff@col*1.5}%
\xdef\hex@x{\pgfmathresult}%
- \expandafter\pgf@x=\hex@x cm%
% \end{macrocode}
%
% And then for the $y$ coordinate and set the dimension \cs{pgf@y}.
@@ -304,17 +303,21 @@
\hex@dbg{2}{Effective row: \hex@coords@row@fac * (\hex@row +
\hex@coords@row@off) -> \hex@eff@row}%
\pgfmathparse{(2*\hex@eff@row-mod(round((\hex@col+\hex@coords@col@off)),2))*\hex@yy}%
+ \pgfmathparse{(2*\hex@eff@row-mod(abs(round(\hex@col+\hex@coords@col@off)),2))*\hex@yy}%
\xdef\hex@y{\pgfmathresult}%
- \expandafter\pgf@x=\hex@y cm%
% \end{macrocode}
%
% If we have a vertex specification add that location to the current
% coordinates. If not, set the point.
%
% \begin{macrocode}
- \ifx\hex@vtx\@empty\pgfpointxy{\hex@x}{\hex@y}\else%
- \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{%
- \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@vtx}{1}}}\fi%
+ \ifx\hex@vtx\@empty\else%
+ \pgfmathparse{\hex@x+\hex@off*cos(\hex@vtx)}\xdef\hex@x{\pgfmathresult}
+ \pgfmathparse{\hex@y+\hex@off*sin(\hex@vtx)}\xdef\hex@y{\pgfmathresult}
+ \fi%
+ % \ifx\hex@vtx\@empty\pgfpointxy{\hex@x}{\hex@y}\else%
+ % \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{%
+ % \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@vtx}{1}}}\fi%
% \end{macrocode}
%
% If we have an edge specification add that location to the current
@@ -322,20 +325,28 @@
%
% \begin{macrocode}
\ifx\hex@edg\@empty\else%
- \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{%
- \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@edg}{\hex@yy}}}\fi
+ \pgfmathparse{\hex@x+\hex@off*\hex@yy*cos(\hex@edg)}%
+ \xdef\hex@x{\pgfmathresult}%
+ \pgfmathparse{\hex@y+\hex@off*\hex@yy*sin(\hex@edg)}%
+ \xdef\hex@y{\pgfmathresult}%
+ \fi%
+ % \ifx\hex@edg\@empty\else%
+ % \pgfpointadd{\pgfpointxy{\hex@x}{\hex@y}}{%
+ % \pgfpointscale{\hex@off}{\pgfpointpolarxy{\hex@edg}{\hex@yy}}}\fi
% \end{macrocode}
%
% For debugging, we can print out stuff.
%
% \begin{macrocode}
+ \pgfpointxy{\hex@x}{\hex@y}
\hex@dbg{2}{Hex coordinates: #1
- ^^J c=\hex@col
- ^^J r=\hex@row
- ^^J v=\hex@vtx
- ^^J e=\hex@edg
- ^^J x=\hex@x
- ^^J y=\hex@y}%
+ ^^J c=`\hex@col'
+ ^^J r=`\hex@row'
+ ^^J v=`\hex@vtx'
+ ^^J e=`\hex@edg'
+ ^^J o=`\hex@off'
+ ^^J x=`\hex@x'
+ ^^J y=`\hex@y'}%
\global\let\hex@x\hex@x%
\global\let\hex@y\hex@y%
\global\let\hex@row\hex@row%
diff --git a/macros/latex/contrib/wargame/source/hex/core.dtx b/macros/latex/contrib/wargame/source/hex/core.dtx
index ee04290955..89179ef9fd 100644
--- a/macros/latex/contrib/wargame/source/hex/core.dtx
+++ b/macros/latex/contrib/wargame/source/hex/core.dtx
@@ -89,6 +89,7 @@
% \input{hex/extra.dtx}
% \input{hex/paths.dtx}
% \input{hex/board.dtx}
+% \input{hex/split.dtx}
% \iffalse
%</hex>
% \fi
diff --git a/macros/latex/contrib/wargame/source/hex/extra.dtx b/macros/latex/contrib/wargame/source/hex/extra.dtx
index 05f7215297..f2e6e1cb75 100644
--- a/macros/latex/contrib/wargame/source/hex/extra.dtx
+++ b/macros/latex/contrib/wargame/source/hex/extra.dtx
@@ -18,7 +18,7 @@
% \begin{macrocode}
\tikzset{%
hex/fortress/.pic={
- \path[draw,pic actions]
+ \path[draw,solid,pic actions]
(0: .9) --
(0: .7) --
(60: .7) -- ( 60:.9) -- ( 60:.7) --
diff --git a/macros/latex/contrib/wargame/source/hex/paths.dtx b/macros/latex/contrib/wargame/source/hex/paths.dtx
index 833a4a245a..813ab869fa 100644
--- a/macros/latex/contrib/wargame/source/hex/paths.dtx
+++ b/macros/latex/contrib/wargame/source/hex/paths.dtx
@@ -45,19 +45,45 @@
}
}%
}%
-
+% \end{macrocode}
+%
+% A decoration to make a fortification line
+%
+% \begin{macrocode}
+\pgfdeclaredecoration{fortification}{initial}
+{
+ \state{initial}[width=4\pgflinewidth]
+ {
+ \pgfpathlineto{\pgfpoint{2\pgflinewidth}{0}}
+ \pgfpathlineto{\pgfpoint{2\pgflinewidth}{2\pgflinewidth}}
+ \pgfpathlineto{\pgfpoint{4\pgflinewidth}{2\pgflinewidth}}
+ \pgfpathlineto{\pgfpoint{4\pgflinewidth}{0}}
+ }
+ \state{final}
+ {
+ \pgfpathlineto{\pgfpointdecoratedpathlast}
+ }
+}
+% \end{macrocode}
+%
+% Roads, railroads, rivers, borders.
+%
+% \begin{macrocode}
\tikzset{
hex/road/.style={
rounded corners=3\pgflinewidth,% .25cm,
color=black,
transform shape,
scale line widths,
- thick},
+ thick,
+ every hex road/.try,
+ },
hex/railroad/.style={
%scale line widths,
rounded corners=.25cm,
color=gray!50!black,
transform shape,
+ every hex railroad/.try,
postaction={draw,decorate},
decoration={ticks,
segment length=9\pgflinewidth,
@@ -67,29 +93,42 @@
hex/river/.style={
color=blue,
scale line widths,
+ scale rounded corners,
line width=3pt,
transform shape,
+ every hex river/.try,
decorate,
decoration={random steps,
- segment length=.3cm,
- amplitude=.15cm,
+ segment length=3\pgflinewidth,
+ amplitude=1.5\pgflinewidth,
pre=lineto,
post=lineto,
- pre length=.05cm,
- post length=.05cm},
- rounded corners=.08cm},
+ pre length=.5\pgflinewidth,
+ post length=.5\pgflinewidth},
+ rounded corners=.75\pgflinewidth},
hex/border/.style={
color=gray,
- rounded corners=3pt,
dashed,
transform shape,
scale line widths,
- very thick
+ very thick,
+ rounded corners=3\pgflinewidth,
+ every hex border/.try
},
- every river/.style={},
- every road/.style={},
- every railroad/.style={},
- every border/.style={},
+ %
+ % Fortification line
+ %
+ hex/fortified line/.style={
+ draw=brown!50!black,
+ scale line widths,
+ line width=2pt,
+ every hex fortification line/.try,
+ decoration={fortification,raise=-2\pgflinewidth},
+ decorate},
+ % every river/.style={},
+ % every road/.style={},
+ % every railroad/.style={},
+ % every border/.style={},
}
% \end{macrocode}
%
@@ -97,25 +136,30 @@
%
% \begin{macrocode}
\def\road{%
- \hex@dbg{3}{Road}
+ %\hex@dbg{3}{Road}
\@ifnextchar[{\road@}{\road@[]}%]
}
\def\road@[#1]{\draw[hex/road,every hex road/.try,#1]}
\def\railroad{%
- \hex@dbg{3}{Rail road}
+ %\hex@dbg{3}{Rail road}
\@ifnextchar[{\railroad@}{\railroad@[]}%]
}
\def\railroad@[#1]{\draw[hex/railroad,every hex railroad/.try,#1]}
\def\river{%
- \hex@dbg{3}{River}
+ %\hex@dbg{3}{River}
\@ifnextchar[{\river@}{\river@[]}%]
}
-\def\river@[#1]{\draw[hex/river,every hex river/.try,#1]}
+\def\river@[#1]{\draw[hex/river,#1]}
\def\border{%
\hex@dbg{3}{Border}
\@ifnextchar[{\border@}{\border@[]}%]
}
\def\border@[#1]{\draw[hex/border,every hex border/.try,#1]}
+\def\fortifiedline{%
+ \@ifnextchar[{\fortifiedline@}{\fortifiedline@[]}%]
+}%
+\def\fortifiedline@[#1]{%
+ \draw[hex/fortified line,every hex fortified line/.try,#1]}
% \end{macrocode}
% \end{Macro}
%
@@ -209,10 +253,10 @@
markings,
mark=between positions 0 and 1 step 0.75*\hex@scale*\hex@dy with {
\node [single arrow,
- single arrow head extend=3pt,
+ single arrow head extend=.1*\hex@scale*\hex@dy,
fill=#1,
- inner sep=\hex@scale*.5mm,
- minimum width=\hex@scale*2mm,
+ inner sep=0.05*\hex@scale*\hex@dy,
+ minimum width=0.02*\hex@scale*\hex@dy,
minimum height=\hex@scale*\hex@dy/2,
transform shape]{};
}
@@ -236,10 +280,10 @@
markings,
mark=between positions 0 and 1 step 0.5*\hex@scale*\hex@dy with {
\node [single arrow,
- single arrow head extend=3pt,
+ single arrow head extend=.1*\hex@scale*\hex@dy,
fill=#1,
- inner sep=\hex@scale*.5mm,
- minimum width=\hex@scale*2mm,
+ inner sep=0.05*\hex@scale*\hex@dy,
+ minimum width=0.02*\hex@scale*\hex@dy,
minimum height=\hex@scale*\hex@dy/3,
transform shape]{};
}
@@ -270,7 +314,7 @@
anchor=west,
inner sep=\hex@scale*.25mm,
outer sep=.3*\hex@scale*\hex@dy,
- minimum width=\hex@scale*2mm,
+ minimum width=0.02*\hex@scale*\hex@dy,
minimum height=1.4*\hex@scale*\hex@dy,
transform shape]{};
}
@@ -300,10 +344,10 @@
transform shape] {};},
mark=between positions 0 and 1 step 0.75*\hex@scale*\hex@dy with {
\node [single arrow,
- single arrow head extend=\hex@scale*3pt,
+ single arrow head extend=.1*\hex@scale*\hex@dy,
fill=#1,
- inner sep=\hex@scale*1mm,
- minimum width=\hex@scale*3mm,
+ inner sep=0.05*\hex@scale*\hex@dy,
+ minimum width=0.02*\hex@scale*\hex@dy,
minimum height=\hex@scale*\hex@dy/2,
transform shape]{};
}
@@ -331,7 +375,7 @@
fill=#2,
transform shape,
text=#1,
- font=\sffamily\bfseries\Large},
+ font=\sffamily\bfseries\fontsize{14.4}{17}\selectfont},
hex/move cost/.default={black}{none},
% Argument is fill colour
% \end{macrocode}
diff --git a/macros/latex/contrib/wargame/source/hex/ridges.dtx b/macros/latex/contrib/wargame/source/hex/ridges.dtx
index 3914ff6ce5..24552e36e2 100644
--- a/macros/latex/contrib/wargame/source/hex/ridges.dtx
+++ b/macros/latex/contrib/wargame/source/hex/ridges.dtx
@@ -9,23 +9,31 @@
% A hex can be decorated with up to 6 ridges --- one for each edge of
% the hexagon. The first thing is to set up the graphics style to use
% for the ridges. We use the \spec{wave} decoration.
+%
+% If rounded corners are set for ridges, (e.g., via \spec{every hex
+% ridges}), then it should be \spec{0pt} or \spec{4pt} (roughly 2mm)
+% or larger. Otherwise, one will get a ``dimension too large''
+% error.
%
% \begin{macrocode}
\tikzset{%
- hex/ridges/.style={
+ hex/ridges pre/.style={
line cap=round,
draw=pgfstrokecolor,
- rounded corners=.25cm,
- scale line widths,
+ solid,
+ /hex/ridges/.cd,%
+ radius=0.85,%
+ n=4,
+ R=.25,
+ },
+ hex/ridges/.style={
+ get scale,
decoration={
path has corners=true,
- pre=waves,
- post=waves,
- pre length=-.1cm,
- post length=0cm,
waves,
- radius=.2cm,
- segment length=.2cm},
+ radius=\wg@scale\hex@r@R,
+ segment length=\wg@scale\hex@r@s,
+ },
decorate}}
% \end{macrocode}
%
@@ -57,6 +65,7 @@
south/.is if=hex@r@s,
south east/.is if=hex@r@se,
radius/.store in=\hex@r@r,
+ curve radius/.store in=\hex@r@w,
NE/.is if=hex@r@ne,
N/.is if=hex@r@n,
NW/.is if=hex@r@nw,
@@ -64,6 +73,8 @@
S/.is if=hex@r@s,
SE/.is if=hex@r@se,
r/.store in=\hex@r@r,
+ n/.store in=\hex@r@n,
+ R/.store in=\hex@r@w,
}
% \end{macrocode}
%
@@ -74,10 +85,11 @@
% the routine below is handcrafted since it is relatively simple.
%
% \begin{macrocode}
+\newdimen\hex@r@s
+\newdimen\hex@r@R
\def\hex@do@ridges{%
\edef\hex@r@tmp{[
- /hex/ridges/.cd,%
- radius=0.8,%
+ hex/ridges pre,
/tikz/every hex ridges/.try,
\hex@ridges]}
\expandafter\scope\hex@r@tmp%
@@ -89,44 +101,70 @@
^^Jsouth =\ifhex@r@s yes\else no\fi
^^Jsouth east=\ifhex@r@se yes\else no\fi
^^Jradius =\hex@r@r
+ ^^Jn =\hex@r@n
}
+ \pgfmathparse{\hex@r@r/\hex@r@n}\xdef\hex@r@t{\pgfmathresult}
+ \hex@r@s=\hex@r@t cm
+ \hex@r@R=\hex@r@w cm
\def\hex@r@p{}
% Hand written algorithm
\ifhex@r@ne
- \def\hex@r@p{(0:\hex@r@r)--(60:\hex@r@r)}
+ \ifhex@r@se
+ \xdef\hex@r@p{(0:\hex@r@r)--(60:\hex@r@r)}
+ \else
+ \xdef\hex@r@p{($(0:\hex@r@r)+(-60:\hex@r@t/2)$)--(60:\hex@r@r)}
+ \fi
+ \hex@dbg{4}{Ridge along north east edge: `\hex@r@p'}
\fi
\ifhex@r@n
- \hex@dbg{4}{Ridge along north edge: `\hex@r@p'}
\ifhex@r@ne\else
- \xdef\hex@r@p{\hex@r@p ( 60:\hex@r@r)}
+ \xdef\hex@r@p{\hex@r@p ($( 60:\hex@r@r)+(0:\hex@r@t/2)$)}
\fi
\xdef\hex@r@p{\hex@r@p --(120:\hex@r@r)}
+ \hex@dbg{4}{Ridge along north edge: `\hex@r@p'}
\fi
\ifhex@r@nw
\ifhex@r@n\else
- \xdef\hex@r@p{\hex@r@p (120:\hex@r@r)}
+ \xdef\hex@r@p{\hex@r@p ($(120:\hex@r@r)+(60:\hex@r@t/2)$)}
\fi
\xdef\hex@r@p{\hex@r@p --(180:\hex@r@r)}
+ \hex@dbg{4}{Ridge along north west: `\hex@r@p'}
\fi
\ifhex@r@sw
\ifhex@r@nw\else
- \xdef\hex@r@p{\hex@r@p (180:\hex@r@r)}
+ \xdef\hex@r@p{\hex@r@p ($(180:\hex@r@r)+(120:\hex@r@t/2)$)}
+ \fi
+ \ifhex@r@s
+ \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)}
+ \else
+ \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)}
\fi
- \xdef\hex@r@p{\hex@r@p --(240:\hex@r@r)}
+ \hex@dbg{4}{Ridge along south west: `\hex@r@p'}
\fi
\ifhex@r@s
\ifhex@r@sw\else
- \xdef\hex@r@p{\hex@r@p (240:\hex@r@r)}
+ \xdef\hex@r@p{\hex@r@p ($(240:\hex@r@r)+(-\hex@r@t/2,0)$)}
\fi
- \xdef\hex@r@p{\hex@r@p --(300:\hex@r@r)}
+ \ifhex@r@se
+ \xdef\hex@r@p{\hex@r@p --(300:\hex@r@r)}
+ \else
+ \xdef\hex@r@p{\hex@r@p --(300.5:\hex@r@r)}
+ \fi
+ \hex@dbg{4}{Ridge along south: `\hex@r@p'}
\fi
\ifhex@r@se
\ifhex@r@s\else
- \xdef\hex@r@p{\hex@r@p (300:\hex@r@r)}
+ \xdef\hex@r@p{\hex@r@p ($(300:\hex@r@r)+(-120:\hex@r@t/2)$)}
+ \fi
+ \ifhex@r@ne
+ \xdef\hex@r@p{\hex@r@p --cycle}
+ \else
+ \xdef\hex@r@p{\hex@r@p --(.5:\hex@r@r)}
\fi
- \xdef\hex@r@p{\hex@r@p --(360:\hex@r@r)}
+ \hex@dbg{4}{Ridge along se: `\hex@r@p'}
\fi
\hex@dbg{3}{ Ridges path: \hex@r@p}
+ % \draw[red] \hex@r@p;
\draw[hex/ridges] \hex@r@p;
\endscope% End of ridges scope
}
diff --git a/macros/latex/contrib/wargame/source/hex/shape.dtx b/macros/latex/contrib/wargame/source/hex/shape.dtx
index 0d31da837f..104dbf23c6 100644
--- a/macros/latex/contrib/wargame/source/hex/shape.dtx
+++ b/macros/latex/contrib/wargame/source/hex/shape.dtx
@@ -14,6 +14,77 @@
% such as terrain and so on.
%
% \begin{macrocode}
+\tikzset{%
+ /hex/.cd,
+ bev/.store in=\hex@bevel, bev/.initial=,
+ bevel fraction/.store in=\hex@bevel@frac,bevel fraction/.initial=10,
+ bevel/.is choice,
+ bevel/none/.style = {/hex/bev=},
+ bevel/north west/.style = {/hex/bev=1},
+ bevel/north east/.style = {/hex/bev=2},
+ bevel/south west/.style = {/hex/bev=3},
+ bevel/south east/.style = {/hex/bev=4},
+ bevel/NW/.style = {/hex/bev=1},
+ bevel/NE/.style = {/hex/bev=2},
+ bevel/SW/.style = {/hex/bev=3},
+ bevel/SE/.style = {/hex/bev=4},
+ bevel/.default = {north west},
+}
+\def\hex@bevel@frac{10}
+\tikzset{
+ hex/bevel highlight/.style={fill=white,opacity=.25},
+ hex/bevel shadow/.style={fill=black,opacity=.25},
+}
+% \end{macrocode}
+%
+%
+%
+% \begin{macrocode}
+\newdimen\wg@tmpe
+\newdimen\wg@tmpf
+\newdimen\wg@tmpg
+\def\hex@bevel@path#1{%
+ \scope[#1]
+ \wg@tmpe=\wg@tmpa\multiply\wg@tmpe by \hex@bevel@frac
+ \wg@tmpf=\wg@tmpb\multiply\wg@tmpf by \hex@bevel@frac
+ \wg@tmpg=\wg@tmpc\multiply\wg@tmpg by \hex@bevel@frac
+ \divide\wg@tmpe100
+ \divide\wg@tmpf100
+ \divide\wg@tmpg100
+ % Start
+ \pgfpathmoveto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Left
+ \pgfpathlineto{\pgfqpoint{-\wg@tmpa}{\wg@tmpb}}%
+ % Left-down
+ \pgfpathlineto{\pgfqpoint{\wg@tmpc}{\wg@tmpd}}%
+ % Right down
+ \wg@tmpa=-\wg@tmpa%
+ \wg@tmpb=-\wg@tmpb%
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Up, in
+ \advance\wg@tmpa\wg@tmpe%
+ \advance\wg@tmpb\wg@tmpf%
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Left-down, in
+ \advance\wg@tmpc-\wg@tmpg
+ \pgfpathlineto{\pgfqpoint{\wg@tmpc}{\wg@tmpd}}%
+ % Left, down in
+ \advance\wg@tmpb-\wg@tmpf\wg@tmpb-\wg@tmpb%
+ \advance\wg@tmpb-\wg@tmpf
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % Start, down in
+ \advance\wg@tmpa-\wg@tmpe\wg@tmpa-\wg@tmpa%
+ \advance\wg@tmpa-\wg@tmpe
+ \pgfpathlineto{\pgfqpoint{\wg@tmpa}{\wg@tmpb}}%
+ % %
+ \pgfclosepath%
+ \pgfusepath{fill}
+ \endscope}%
+% \end{macrocode}
+%
+%
+%
+% \begin{macrocode}
\hex@dbg{5}{Base vertex: \hex@xx,\hex@yy}
\hex@dbg{5}{Base edges: \hex@e@xx,\hex@e@yy}
\pgfdeclareshape{hex/hex}{%
@@ -187,6 +258,40 @@
% \begin{macrocode}
\@ifundefined{hex@label}{\let\hex@label\empty}{}
\ifx\hex@label\empty\else\hex@do@label\fi%
+% \end{macrocode}
+%
+% \begin{macrocode}
+ \@ifundefined{hex@bevel}{\let\hex@bevel\empty}{}
+ \ifx\hex@bevel\empty\else%
+ \northeast
+ \wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
+ \west
+ \wg@tmpc=\pgf@x\wg@tmpd=\pgf@y%
+ \ifcase\hex@bevel\relax
+ \or%1
+ \or\wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%2
+ \or\wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%3
+ \or% 4
+ \wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%
+ \wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%
+ \fi
+ \hex@bevel@path{chit/bevel highlight}
+ \northeast
+ \wg@tmpa=-\pgf@x\wg@tmpb=-\pgf@y%
+ \west
+ \wg@tmpc=-\pgf@x\wg@tmpd=-\pgf@y%
+ \ifcase\hex@bevel\relax
+ \or%1
+ \or\wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%2
+ \or\wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%3
+ \or% 4
+ \wg@tmpa=-\wg@tmpa\wg@tmpc=-\wg@tmpc%
+ \wg@tmpb=-\wg@tmpb\wg@tmpd=-\wg@tmpd%
+ \fi
+ \hex@bevel@path{chit/bevel shadow}
+ \fi
+% \end{macrocode}
+% \begin{macrocode}
\endscope%
% \end{macrocode}
%
diff --git a/macros/latex/contrib/wargame/source/hex/split.dtx b/macros/latex/contrib/wargame/source/hex/split.dtx
new file mode 100644
index 0000000000..3996b8848f
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/hex/split.dtx
@@ -0,0 +1,517 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*hex>
+% \fi
+%
+% \subsubsection{Board splitting}
+% \label{sec:impl:hex:split}
+%
+% \begin{Macro}{\splitboard}
+%
+% Calculates how to split a board into sheets of paper.
+%
+% \begin{Syntax}
+% \cs{splitboard}\oarg{options}
+% \end{Syntax}
+%
+% where options are
+%
+% \begin{itemize}
+% \item \spec{paper}=\meta{format}: Specifies the paper format. One
+% of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default
+% is \spec{a4}.
+% \item \spec{landscape}: Sets the paper format to be in landscape
+% mode (default is portrait).
+% \item \spec{margin}=\meta{size in centimetres}: Size of margins on
+% each sheet in centimetres \emph{without} unit. That is put
+% \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}.
+% This should be \emph{slightly} larger (by roughly 5\%) than the
+% \emph{least} margin required by the printer used. \emph{Must} be
+% given \emph{before} \spec{paper} to have any effect.
+% \item \spec{ncol}=\meta{number of columns}: Sets the number of
+% columns of sheets.
+% \item \spec{nrow}=\meta{number of rows}: Set the number of rows of
+% sheets.
+% \item \spec{overlap}=\meta{size in centimetres}: Sets the size of
+% the overlap between sheets in centimetres \emph{without} unit.
+% That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is
+% \spec{2}.
+% \item \spec{image}=\meta{file name}: File name of the board image
+% (a PDF). Default is \spec{board}
+% \item \spec{output}=\meta{file name}: File name (without
+% \spec{.tex} ending) to write calculated split to.
+% \item \spec{standalone}: Boolean flag. If true, then output file
+% will be a standalone document (i.e., has a \cs{documentclass}).
+% \item \spec{scale}=\meta{scale}: Set scale of board.
+% \end{itemize}
+%
+% The macro will produce a file named \cs{jobname}\spec{\_out.tex}
+% which can be included in another document to generate the split
+% board PDF.
+%
+% To use, make, for example, the file \spec{calcsplit.tex} with the
+% content
+%
+% \begin{verbatim}
+% \documentclass[11pt]{standalone}
+% \usepackage{wargame}
+% \usepackage{mystyle}
+% \begin{document}
+% \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1}
+% \end{document}
+% \end{verbatim}
+%
+% to calculate the split of \spec{board.pdf} over $2\times2$ letter
+% paper sheets, with a non-printable margin of 7mm, and an overlap
+% between the segments of 1cm.
+%
+% The final split document can then be
+%
+% \begin{verbatim}
+% \documentclass[11pt]{article}
+% \usepackage[letterpaper,margin=7mm]{geometry}
+% \begin{document}
+% \input{calcsplit_out}
+% \end{document}
+% \end{verbatim}
+%
+%
+% If you need to scale down the board, define the style \spec{board
+% scale}. E.g.,
+%
+% \begin{verbatim}
+% \tikzset{board scale/.style={scale=.9}}
+% \end{verbatim}
+%
+% \end{Macro}
+%
+%
+% Styles used for drawing things.
+%
+% \begin{macrocode}
+\tikzset{%
+ % Margin must be <1cm
+ split/paper outline/.style={
+ shape=rectangle,
+ draw=red!50!black,
+ line width=.5mm},
+ split/effective outline/.style={
+ shape=rectangle,
+ draw=green!50!black,
+ dashed,
+ line width=.5mm},
+ split/board outline/.style={%
+ draw=magenta,
+ line width=.5mm,
+ dotted},
+}
+% \end{macrocode}
+%
+% A scratch dimension used
+%
+% \begin{macrocode}
+\newdimen\split@tmp
+% \end{macrocode}
+%
+% Get upper right and lower left corners of node. Argument is node name.
+%
+% \begin{macrocode}
+\def\split@getem#1{%
+ \draw (#1.north east);%
+ \pgfgetlastxy{\split@ulx}{\split@uly}%
+ \xdef\split@ulx{\split@ulx}%
+ \xdef\split@ulx{\split@ulx}%
+ \draw (#1.south west);%
+ \pgfgetlastxy{\split@lrx}{\split@lry}%
+ \xdef\split@lrx{\split@lrx}%
+ \xdef\split@lry{\split@lry}%
+}
+% \end{macrocode}
+%
+% Get board dimensions. Argument is node name.
+%
+% \begin{macrocode}
+\def\split@getboard#1{%
+ \split@getem{#1}%
+ \xdef\split@bulx{\split@ulx}%
+ \xdef\split@buly{\split@uly}%
+ \xdef\split@blrx{\split@lrx}%
+ \xdef\split@blry{\split@lry}%
+ \split@w{\@percentchar\space Board:
+ (\split@bulx,\split@buly)(\split@blrx,\split@blry)}}
+% \end{macrocode}
+%
+% Adjust placement of markers and cut lines.
+% \begin{enumerate}
+% \item Dimension to adjust
+% \item Overlap dimension (with units)
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\split@adj#1#2{%
+ \split@tmp=#2%
+ \divide\split@tmp by 2%
+ \advance\split@tmp by #1%
+ \edef\t{\the\split@tmp}}
+% \end{macrocode}
+%
+% Get initial offset in a direction.
+%
+% \begin{enumerate}
+% \item Number of segments in direction
+% \item Overlap in centimetres (without unit)
+% \item Effective size, in centimetres (without unit), of sheets in
+% direction
+% \item Full size, in centimetres (without unit), of board in
+% direction.
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\split@get@init#1#2#3#4{%
+ \pgfmathparse{((#1 * #3 - (#1 - 1) * #2) - #4)/2}%
+ \xdef\split@off{\pgfmathresult}%
+ \hex@dbg{2}{((#1 * #3 - (#1 - 1) * #2) - #4)/2 -> `\split@off'}}
+% \end{macrocode}
+%
+% Get initial offset of first segment.
+%
+% \begin{enumerate}
+% \item Number of rows
+% \item Number of columns
+% \item Overlap in centimetres (without unit)
+% \item Effective height, in centimetres (without unit), of sheets
+% \item Effective width, in centimetres (without unit), of sheets
+% \item Full height, in centimetres (without unit), of board
+% \item Full width, in centimetres (without unit), of board
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\split@getinit#1#2#3#4#5#6#7{%
+ \split@get@init{#1}{#3}{#4}{#6}\xdef\dy{\split@off cm}
+ \split@get@init{#2}{#3}{#5}{#7}\xdef\dx{\split@off cm}}
+% \end{macrocode}
+%
+% Get coordinates of a segment
+%
+% \begin{enumerate}
+% \item Column number
+% \item Row number
+% \item Overlap, in centimetres (without unit)
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\split@getcoords#1#2#3{%
+ \hex@dbg{2}{Getting coords `c#1r#2'}%
+ \split@getem{c#1r#2}%
+ \edef\sulx{\split@ulx}%
+ \edef\suly{\split@uly}%
+ \edef\slrx{\split@lrx}%
+ \edef\slry{\split@lry}%
+ \edef\mlx{\split@blrx}%
+ \edef\mrx{\split@bulx}%
+ \edef\mty{\split@buly}%
+ \edef\mby{\split@blry}%
+ \pgfmathparse{int(#1-1)}\edef\pc{\pgfmathresult}%
+ \pgfmathparse{int(#2-1)}\edef\pr{\pgfmathresult}%
+ \pgfmathparse{int(#1+1)}\edef\nc{\pgfmathresult}%
+ \pgfmathparse{int(#2+1)}\edef\nr{\pgfmathresult}%
+ \pgfutil@ifundefined{pgf@sh@ns@c\pc r#2}{}{% Left
+ \hex@dbg{3}{\space Getting left `c\pc r#2'}%
+ \split@getem{c\pc r#2}\split@adj{\split@ulx}{-#3}\edef\mlx{\t}}%
+ \pgfutil@ifundefined{pgf@sh@ns@c\nc r#2}{}{% Right
+ \hex@dbg{3}{\space Getting right `c\nc r#2'}%
+ \split@getem{c\nc r#2}\split@adj{\split@lrx}{#3}\edef\mrx{\t}}%
+ \pgfutil@ifundefined{pgf@sh@ns@c#1r\pr}{}{% Above
+ \hex@dbg{3}{\space Getting above `c#1 r\pr'}%
+ \split@getem{c#1r\pr}\split@adj{\split@lry}{#3} \edef\mty{\t}}%
+ \pgfutil@ifundefined{pgf@sh@ns@c#1r\nr}{}{% Below
+ \hex@dbg{3}{\space Getting below `c#1 r\nr'}%
+ \split@getem{c#1r\nr}\split@adj{\split@uly}{-#3}\edef\mby{\t}}%
+ \draw[fill=red] (\mlx,\mty) circle(.2);%
+ \draw[fill=green](\mrx,\mty) circle(.4);%
+ \draw[fill=blue] (\mlx,\mby) circle(.6);%
+ \draw[fill=cyan] (\mrx,\mby) circle(.8);%
+ \split@w{%
+ \@percentchar^^J%
+ \string\segment(\sulx,\suly)(\slrx,\slry){\mlx}{\mrx}{\mby}{\mty}
+ \@percentchar\space c#1r#2}
+}
+% \end{macrocode}
+%
+% Stream to write to
+%
+% \begin{macrocode}
+\newwrite\split@calcout
+% \end{macrocode}
+%
+% Short-hand for write outs.
+%
+% \begin{macrocode}
+\def\split@w{\immediate\write\split@calcout}
+% \end{macrocode}
+%
+% Open stream and set-up
+%
+% \begin{macrocode}
+\def\split@header#1{%
+ \immediate\openout\split@calcout=#1.tex
+ \ifsplit@standalone
+ \pgfmathparse{\split@margin*.95}\edef\tmp{\pgfmathresult}
+ \split@w{\@percentchar\@percentchar\space These are made with
+ `calcsplit' with `-jobname \jobname'}
+ \split@w{
+ ^^J\string\documentclass[twoside]{article}
+ ^^J\string\usepackage{geometry}
+ ^^J\string\geometry{papersize={\the\paperwidth,\the\paperheight},margin=\tmp cm}
+ ^^J\string\usepackage{wargame}
+ ^^J\string\setlength{\string\parindent}{0pt}
+ ^^J\string\setlength{\string\parskip}{0pt}
+ ^^J\string\begin{document}
+ ^^J\string\ignorespaces\@percentchar}
+ \fi
+ \split@w{\string\def\string\boardfile{\split@img}\@percentchar}
+ \split@w{\string\def\string\boardscale{\split@scale}\@percentchar}
+}
+% \end{macrocode}
+%
+% Write final stuff and close stream
+%
+% \begin{macrocode}
+\def\split@footer{%
+ \ifsplit@standalone
+ \split@w{^^J\string\end{document}}
+ \fi
+ \split@w{^^J\@percentchar\@percentchar End of `\jobname'^^J}
+ \immediate\closeout\split@calcout
+}
+% \end{macrocode}
+%
+% Initial calculations. This draws the board and then extracts the
+% dimensions of the board. It also defines some styles for drawing
+% the board segments.
+%
+% \begin{macrocode}
+\def\split@init#1{%
+ \node[scale=\split@scale,
+ inner sep=0pt,
+ outer sep=0pt,
+ anchor=north west,
+ transform shape](b){\includegraphics{#1}};
+ \split@getboard{b}
+ %x
+ \split@tmp=\split@blrx cm\advance\split@tmp by -\split@bulx%
+ \wg@pt@to@cm{\split@tmp}\edef\split@bw{\pgfmathresult}%
+ \pgfmathparse{abs(\split@bw)}\edef\split@bw{\pgfmathresult}%
+ %
+ \split@tmp=\split@buly cm\advance\split@tmp by -\split@blry%
+ \wg@pt@to@cm{\split@tmp}\edef\split@bh{\pgfmathresult}%
+ \pgfmathparse{abs(\split@bh)}\edef\split@bh{\pgfmathresult}%
+ %
+ \wg@pt@to@cm{\paperwidth}\edef\split@pw{\pgfmathresult}%
+ \wg@pt@to@cm{\paperheight}\edef\split@ph{\pgfmathresult}%
+ %
+ \wg@pt@to@cm{\textwidth}\edef\split@ew{\pgfmathresult}%
+ \wg@pt@to@cm{\textheight}\edef\split@eh{\pgfmathresult}%
+ %
+ \hex@dbg{1}{Board:
+ (\split@bulx,\split@buly)(\split@blrx,\split@blry) \split@bw x\split@bh
+ ^^JPaper: \split@pw x\split@ph
+ ^^JEffective: \split@ew x\split@eh
+ }
+ \tikzset{
+ split/paper size/.style={
+ shape=rectangle,
+ minimum width=\paperwidth,
+ minimum height=\paperheight,
+ split/paper outline,
+ },
+ split/effective size/.style={
+ shape=rectangle,
+ minimum width=\textwidth,
+ minimum height=\textheight,
+ split/effective outline},
+ split/board size/.style={
+ shape=rectangle,
+ minimum width=\split@bw cm,
+ minimum height=\split@bh cm,
+ split/board outline}}
+ \node[board/.try,split/board size,anchor=north west] {};
+}
+% \end{macrocode}
+%
+% Calculate effective sheet sizes from sheet dimensions and the
+% defined margin.
+%
+% \begin{macrocode}
+\def\split@text@dim#1{%
+ \textwidth=\paperwidth%
+ \textheight=\paperheight%
+ \advance\textwidth by -#1cm%
+ \advance\textwidth by -#1cm%
+ \advance\textheight by -#1cm%
+ \advance\textheight by -#1cm%
+ \global\textwidth=\textwidth%
+ \global\textheight=\textheight%
+}
+% \end{macrocode}
+%
+% Options for the \cs{splitboard} macro.
+%
+% \begin{macrocode}
+\newif\ifsplit@standalone\split@standalonetrue
+\tikzset{%
+ split/.search also={/tikz},%
+ split/.cd,%
+ margin/.store in=\split@margin,
+ paper/.is choice,%
+ paper/a4/.code={%
+ \hex@dbg{3}{A4 paper for split}%
+ \global\paperwidth=21cm%
+ \global\paperheight=29.7cm%
+ \split@text@dim{\split@margin}},
+ paper/a3/.code={%
+ \hex@dbg{3}{A3 paper for split}%
+ \global\paperheight=42cm%
+ \global\paperwidth=29.7cm%
+ \split@text@dim{\split@margin}},
+ paper/letter/.code={%
+ \hex@dbg{3}{Letter paper for split}
+ \paperheight=27.9cm,%
+ \paperwidth=21.6cm,%
+ \split@text@dim{\split@margin}},%
+ paper/tabloid/.code={%
+ \hex@dbg{3}{Tabloid paper for split}%
+ \paperheight=43.2cm,%
+ \paperwidth=27.9cm,%
+ \split@text@dim{\split@margin}},
+ landscape/.code={%
+ \hex@dbg{3}{Landscape option for split}
+ \split@tmp=\paperheight
+ \global\paperheight=\paperwidth
+ \global\paperwidth=\split@tmp
+ \split@tmp=\textheight
+ \global\textheight=\textwidth
+ \global\textwidth=\split@tmp},
+ standalone/.is if=split@standalone,
+ scale/.store in=\split@scale,
+ output/.store in=\split@out,
+ ncol/.store in=\split@ncol,
+ nrow/.store in=\split@nrow,
+ overlap/.store in=\split@ov, % Centimeter, no unit
+ image/.store in=\split@img,
+ paper/.default=a4, paper/.initial=a4,
+ margin/.default=.6, margin/.initial=.6,
+ ncol/.default=0, ncol/.initial=0,
+ nrow/.default=0, nrow/.initial=0,
+ overlap/.default=2, overlap/.initial=2,
+ image/.default=board, image/.initial=board,
+ output/.default=\jobname_out,
+ standalone/.default=true,
+ scale/.default=1,
+}
+% \end{macrocode}
+%
+% The actual macro. The argument is key-value pairs of options.
+%
+% \begin{macrocode}
+\def\splitboard#1{%
+ \pgfkeys{/tikz/split/.cd,%
+ standalone,%
+ output,%
+ margin,%
+ paper,%
+ image,%
+ overlap,%
+ scale,%
+ ncol,%
+ nrow,%
+ #1}
+ \hex@dbg{1}{%
+ Paper: `\the\paperwidth'x`\the\paperheight'
+ ^^JEffective: `\the\textwidth'x`\the\textheight'
+ ^^JNcols: `\split@ncol'
+ ^^JNrows: `\split@nrow'
+ ^^JOverlap: `\split@ov' cm}
+ \split@header{\split@out}
+ \begin{tikzpicture}
+ \split@init{\split@img}
+ \split@getinit{%
+ \split@nrow}{%
+ \split@ncol}{%
+ \split@ov}{\split@eh}{\split@ew}{\split@bh}{\split@bw}
+ \node[split/effective size,
+ above left=\dy and \dx of b.north west,
+ anchor=north west] (c1r1) {};
+ \node[split/paper size] at (c1r1) {};
+ %
+ \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split@nrow}{%
+ \ifnum\r>1
+ \hex@dbg{3}{Placing first column of row `\r'}
+ \node[split/effective size,
+ below=-\split@ov cm of c1r\pr.south west,anchor=north west] (c1r\r){};
+ \node[split/paper size] at (c1r\r) {};
+ \fi
+ \foreach \c [remember=\c as \pc (initially 1)] in {2,...,\split@ncol}{%
+ \ifnum\c>\split@ncol\else%
+ \ifnum\c>\pc
+ \hex@dbg{3}{Placing column `\c' (`\pc') of row `\r'}
+ \node[split/effective size,
+ right=-\split@ov cm of c\pc r\r.north east,anchor=north west]
+ (c\c r\r) {};
+ \node[split/paper size] at (c\c r\r) {};
+ \fi
+ \fi
+ }
+ }
+ \foreach \r [remember=\r as \pr (initially 0)] in {1,...,\split@nrow}{%
+ \foreach \c [remember=\c as \pc (initially 0)] in {1,...,\split@ncol}{%
+ \split@getcoords{\c}{\r}{\split@ov cm}}}
+ \end{tikzpicture}
+ \split@footer
+}
+% \end{macrocode}
+%
+% Macro used by the written file.
+%
+%
+% \begin{enumerate}
+% \item first coordinate (e.g., \spec{(hex ak:c=C,r=17)})
+% \item second coordinate (e.g., \spec{(hex ak:c=M,r=33)})
+% \item Crop mark left
+% \item Crop mark right
+% \item Crop mark bottom
+% \item Crop mark top
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\segment(#1)(#2)#3#4#5#6{%
+ \begin{tikzpicture}%
+ \begin{scope}
+ \clip (#1) rectangle (#2);
+ \node[scale=\boardscale,
+ inner sep=0pt,
+ outer sep=0pt,
+ anchor=north west,
+ transform shape]{\includegraphics{\boardfile}};
+ \end{scope}
+ \pgfinterruptboundingbox
+ \draw(#3,#6)--++( 0.0, 0.3);
+ \draw(#3,#6)--++(-0.3, 0.0);
+ \draw(#3,#5)--++( 0.0,-0.3);
+ \draw(#3,#5)--++(-0.3, 0.0);
+ \draw(#4,#6)--++( 0.0, 0.3);
+ \draw(#4,#6)--++( 0.3, 0.0);
+ \draw(#4,#5)--++( 0.0,-0.3);
+ \draw(#4,#5)--++( 0.3, 0.0);
+ \endpgfinterruptboundingbox
+ \end{tikzpicture}%
+ \cleardoublepage}%
+% \end{macrocode}
+%
+%
+%
+%
+% \iffalse
+%</hex>
+% --------------------------------------------------------------------
+% \fi
diff --git a/macros/latex/contrib/wargame/source/hex/terrain.dtx b/macros/latex/contrib/wargame/source/hex/terrain.dtx
index 42155ca5c9..4c486adf77 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain.dtx
@@ -47,7 +47,7 @@
pic/.default=,
image/.default=,
code/.default=,
- clip/.default,
+ clip/.default=,
}
\iffalse
\tikzset{
@@ -111,8 +111,9 @@
% \begin{macrocode}
\@ifundefined{hex@t@clip}{\let\hex@t@clip\empty}{}
\ifx\hex@t@clip\empty\else%
+ \edef\hex@t@cc{\hex@t@clip}%
\def\hex@t@c{}
- \foreach \c in \hex@t@clip{%
+ \foreach \c in \hex@t@cc{%
\hex@dbg{5}{Clipping to `\c'}
\expandafter\wg@pic\c\@endwg@pic {}{\wg@tmpa,\wg@tmpb}{%
save path=\hex@t@tmp}%
@@ -132,8 +133,13 @@
%% macros are undefined, define them to be empty
\@ifundefined{hex@t@pic}{\let\hex@t@pic\empty}{}
\@ifundefined{hex@t@image}{\let\hex@t@image\empty}{}
+ \@ifundefined{hex@t@code}{\let\hex@t@code\empty}{}
+ \@ifundefined{hex@t@code}{\let\hex@t@code\empty}{}
% \end{macrocode}
%
+% \begin{macrocode}
+ \ifx\hex@t@code\empty\else\hex@t@code\fi%
+% \end{macrocode}
% First we check if we have not got terrain images, but terrain
% pictures. If we have that, we process these in turn. Note, the
% user can give options to each terrain picture by preceding the
@@ -147,7 +153,8 @@
% We have pictures
\hex@dbg{5}{Terrain pictures}%
\pgfpointorigin\wg@tmpa=\pgf@x\wg@tmpb=\pgf@y%
- \wg@pic@all{\hex@t@pic}{}{\the\wg@tmpa,\the\wg@tmpb}{}%
+ \foreach \i in \hex@t@pic{%
+ \wg@pic@all{\i}{}{\the\wg@tmpa,\the\wg@tmpb}{}}%
\fi% We have pictures.
% \end{macrocode}
%
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx b/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx
index 98e990c15c..a6a6905e1a 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/beach.dtx
@@ -21,10 +21,14 @@
% \end{macrocode}
% \end{TikzKey}
%
-% Now for the actual patterns. We go in the same order as above ---
-% i.e, we start with the beach pattern. This is rather long.
-%
% \begin{TikzKey}{hex/terrain/beach}
+% Now for the actual patterns. We go in the same order as above ---
+% i.e, we start with the beach pattern. This is rather long.
+%
+% \begin{center}
+% \includegraphics{wargame.beach}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/city.dtx b/macros/latex/contrib/wargame/source/hex/terrain/city.dtx
index 1d373ea19d..0f5f88eea6 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/city.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/city.dtx
@@ -5,6 +5,9 @@
% \begin{TikzKey}{hex/terrain/city}
% And finally a city
%
+% \begin{center}
+% \includegraphics{wargame.city}
+% \end{center}
%
% \begin{macrocode}
\ifhex@terrain@pic
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx b/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx
index ee15f5ce78..ededa87541 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/light_woods.dtx
@@ -19,6 +19,10 @@
% \begin{TikzKey}{hex/terrain/light woods}
% Next, we have light woods.
%
+% \begin{center}
+% \includegraphics{wargame.light_woods}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx b/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx
index 04f935bd92..f80b991e27 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/mountains.dtx
@@ -21,6 +21,10 @@
% And the mountains pattern. This is the same as the beach pattern,
% only filled with a darker brown colour.
%
+% \begin{center}
+% \includegraphics{wargame.mountains}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx b/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx
index fe716add2f..a5fd0d6a06 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/rough.dtx
@@ -19,6 +19,10 @@
% \begin{TikzKey}{hex/terrain/rough}
% Roughs. Again, a bit long.
%
+% \begin{center}
+% \includegraphics{wargame.rough}
+% \end{center}
+%
%
% \begin{macrocode}
\ifhex@terrain@pic
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx b/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx
index b361789fa3..57ad7e9e65 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/swamp.dtx
@@ -18,6 +18,10 @@
% \begin{TikzKey}{hex/terrain/swamp}
% Swamps. This is probably the shortest of the terrain patterns.
%
+% \begin{center}
+% \includegraphics{wargame.swamp}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/town.dtx b/macros/latex/contrib/wargame/source/hex/terrain/town.dtx
index 1186b54c81..5956583ba6 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/town.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/town.dtx
@@ -5,6 +5,10 @@
% \begin{TikzKey}{hex/terrain/town}
% A town.
%
+% \begin{center}
+% \includegraphics{wargame.town}
+% \end{center}
+%
%
% \begin{macrocode}
\ifhex@terrain@pic
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/village.dtx b/macros/latex/contrib/wargame/source/hex/terrain/village.dtx
index 837b92642c..8aeddb2b9d 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/village.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/village.dtx
@@ -9,6 +9,10 @@
% houses, and separate styles for regular and small roads. Note
% that we draw using the stroke colour for roads and houses.
%
+% \begin{center}
+% \includegraphics{wargame.village}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx b/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx
index 9b0d08499b..a8faccddf3 100644
--- a/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx
+++ b/macros/latex/contrib/wargame/source/hex/terrain/woods.dtx
@@ -19,6 +19,10 @@
% \begin{TikzKey}{hex/terrain/woods}
% Regular woods.
%
+% \begin{center}
+% \includegraphics{wargame.woods}
+% \end{center}
+%
% \begin{macrocode}
\ifhex@terrain@pic
\tikzset{
diff --git a/macros/latex/contrib/wargame/source/hex/towns.dtx b/macros/latex/contrib/wargame/source/hex/towns.dtx
index a73c5eed5c..222b5f58da 100644
--- a/macros/latex/contrib/wargame/source/hex/towns.dtx
+++ b/macros/latex/contrib/wargame/source/hex/towns.dtx
@@ -13,6 +13,7 @@
\tikzset{%
hex/town/.style={
scale line widths,
+ solid,
thin,
fill=pgfstrokecolor,
color=pgfstrokecolor},
@@ -21,7 +22,7 @@
shape=rectangle,
above right=.1,
color=pgfstrokecolor,
- font=\sffamily\normalsize}
+ font=\sffamily\fontsize{11}{13}\selectfont}
}
% \end{macrocode}
%
@@ -46,11 +47,11 @@
%
% \begin{macrocode}
\tikzset{%
- hex/town/village/.pic={\path[fill,pic actions] circle(.1);},
- hex/town/town/.pic={\path[fill,pic actions] circle(.2);},
+ hex/town/village/.pic={\path[fill,solid,pic actions] circle(.1);},
+ hex/town/town/.pic={\path[fill,solid,pic actions] circle(.2);},
hex/town/city/.pic={%
- \path[fill,pic actions] circle(.25);
- \path[draw,pic actions] circle(.35);}
+ \path[fill,solid,pic actions] circle(.25);
+ \path[draw,solid,pic actions] circle(.35);}
}
% \end{macrocode}
%
diff --git a/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx b/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx
index d8d0c4fba7..22cb99cc97 100644
--- a/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx
+++ b/macros/latex/contrib/wargame/source/natoapp6c/frames/friendly.dtx
@@ -419,6 +419,30 @@
}
}
% \end{macrocode}
+%
+% \begin{macrocode}
+\pgfdeclareshape{natoapp6c friendly none}{%
+ \inheritsavedanchors[from=natoapp6c base]
+ \savedanchor\northeast{%
+ \pgf@x=1.5\n@to@pp@r%
+ \pgf@y=\n@to@pp@r}
+ \anchor{north east}{\northeast}
+ \anchor{north west}{\northeast\pgf@x=-\pgf@x}
+ \anchor{south east}{\northeast\pgf@y=-\pgf@y}
+ \anchor{south west}{\northeast\pgf@x=-\pgf@x\pgf@y=-\pgf@y}
+ \anchor{north}{\northeast\pgf@x=0cm}
+ \anchor{south}{\northeast\pgf@x=0cm\pgf@y=-\pgf@y}
+ \anchor{east}{\northeast\pgf@y=0cm}
+ \anchor{west}{\northeast\pgf@x=-\pgf@x\pgf@y=0cm}
+ \inheritanchor[from=natoapp6c base]{upper}
+ \inheritanchor[from=natoapp6c base]{lower}
+ \inheritanchor[from=natoapp6c base]{left}
+ \inheritanchor[from=natoapp6c base]{right}
+ \inheritanchor[from=natoapp6c base]{center}
+ \backgroundpath{}
+ \behindforegroundpath{}
+}
+% \end{macrocode}
% \end{NatoAppFrame}
%
% \iffalse
diff --git a/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx b/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx
index 029be911b7..3e6d41821d 100644
--- a/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx
+++ b/macros/latex/contrib/wargame/source/natoapp6c/shape.dtx
@@ -81,6 +81,7 @@
command/space/.style={cmd=space},
command/sub surface/.style={cmd=sub surface},
command/sea mine/.style={cmd=sub surface},
+ command/none/.style={cmd=none},
}
% \end{macrocode}
% \end{NatoAppKey}
@@ -164,7 +165,11 @@
%
% \begin{macrocode}
\tikzset{
- natoapp6c/parts/.style={draw,shape=rectangle,transform shape},
+ natoapp6c/parts/.style={
+ scale line widths,
+ draw,
+ shape=rectangle,
+ transform shape},
natoapp6c/main/.style={natoapp6c/parts},
natoapp6c/modifiers/.style={natoapp6c/parts,scale=.6},
natoapp6c/lower/.style={natoapp6c/parts},
@@ -607,13 +612,21 @@
%
% \begin{macrocode}
\providecommand\natoappmark[2][]{%
- \tikz[scale=.25,#1]{\natoapp[faction=friendly,command=land,main=#2]}}
+ \tikz[transform shape,
+ scale=.25,
+ baseline=(natoapp6c mark.south east),
+ natoapp6c mark/.try,
+ #1]{%1
+ \node[draw,transform
+ shape,natoapp6c={faction=friendly,command=land,
+ main=#2}] (natoapp6c mark){}}}
+% \natoapp[faction=friendly,command=land,main=#2](0,0)(natoapp6c mark)}}
% \end{macrocode}
% \end{Macro}
%
% \begin{Macro}{\echelonmark}
% \begin{macrocode}
-\providecommand\echelonmark[2][]{\tikz[scale=.5,#1]{%
+\providecommand\echelonmark[2][]{\tikz[transform shape,scale=.5,#1]{%
\pic[scale line widths,line width=1pt] {natoapp6c/s/echelon=#2};}}
% \end{macrocode}
% \end{Macro}
diff --git a/macros/latex/contrib/wargame/source/util/bb.dtx b/macros/latex/contrib/wargame/source/util/bb.dtx
new file mode 100644
index 0000000000..83e72da7a9
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/bb.dtx
@@ -0,0 +1,226 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+%
+% \subsubsection{Bounding boxes}
+%
+% Bounding box dimensions
+%
+%
+% \begin{macrocode}
+\newdimen\wg@bb@minx
+\newdimen\wg@bb@miny
+\newdimen\wg@bb@maxx
+\newdimen\wg@bb@maxy
+% \end{macrocode}
+%
+%
+%
+% Enable or disable bounding box tracking
+%
+% \begin{macrocode}
+\newif\ifwg@notrelevantforpathsize\wg@notrelevantforpathsizefalse
+% \end{macrocode}
+%
+%
+% \begin{Macro}{wg@resetbb}
+% Reset the bounding box tracking dimensions
+%
+% \begin{macrocode}
+\def\wg@resetbb{%
+ \global\wg@bb@minx=16000pt\relax%
+ \global\wg@bb@miny=16000pt\relax%
+ \global\wg@bb@maxx=-16000pt\relax%
+ \global\wg@bb@maxy=-16000pt\relax%
+}
+% \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\old@pgf@protocolsize}
+% Save PGF's bounding box algorithm
+%
+% \begin{macrocode}
+\let\old@pgf@protocolsize\pgf@protocolsizes
+% \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\wg@protocolsizes}
+% Our bounding box algorithm
+%
+% \begin{macrocode}
+\def\wg@protocolsizes#1#2{%
+ \old@pgf@protocolsize{#1}{#2}
+ \ifwg@notrelevantforpathsize\else%
+ \ifdim#1<\wg@bb@minx\global\wg@bb@minx#1\fi%
+ \ifdim#1>\wg@bb@maxx\global\wg@bb@maxx#1\fi%
+ \ifdim#2<\wg@bb@miny\global\wg@bb@miny#2\fi%
+ \ifdim#2>\wg@bb@maxy\global\wg@bb@maxy#2\fi%
+ \fi
+}
+% \end{macrocode}
+% \end{Macro}
+%
+% % \begin{environment}{getbbl}
+% Environment that tracks the local bounding box
+%
+% \begin{macrocode}
+\newenvironment{getbbl}{%
+ \wg@resetbb%
+ \wg@notrelevantforpathsizefalse%
+ \global\let\pgf@protocolsizes\wg@protocolsizes}{%
+ \gdef\pgf@sh@ns@L{rectangle}
+ \gdef\pgf@sh@np@L{%
+ \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}%
+ \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}%
+ }
+ \gdef\pgf@sh@nt@L{{1}{0}{0}{1}{0pt}{0pt}}
+ \gdef\pgf@sh@pi@L{\pgfpictureid}
+ \global\let\pgf@protocolsizes\old@pgf@protocolsize
+}
+% \end{macrocode}
+% \end{environment}
+%
+% \begin{environment}{getbb}
+% Environment to track global bounding box
+%
+% \begin{macrocode}
+\newenvironment{getbb}{%
+ \wg@resetbb%
+ \wg@notrelevantforpathsizefalse%
+ \global\let\pgf@protocolsizes\wg@protocolsizes}{%
+ \gdef\pgf@sh@ns@M{rectangle}
+ \gdef\pgf@sh@np@M{%
+ \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}%
+ \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}%
+ }
+ \gdef\pgf@sh@nt@M{{1}{0}{0}{1}{0pt}{0pt}}
+ % \pgfgettransform\pgf@temp%
+ % \xdef\pgf@sh@nt@M{\pgf@temp}
+ % \pgfgettransformentries{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{\pgf@temp}{\pgf@temp}
+ % \message{^^JTransform of M: \meaning\pgf@temp}
+ % \xdef\pgf@sh@nt@M{{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{0pt}{0pt}}%
+ % \message{^^JTransform of M: \meaning\pgf@sh@nt@M}
+ \gdef\pgf@sh@pi@M{\pgfpictureid}
+ \global\let\pgf@protocolsizes\old@pgf@protocolsize
+}
+% \end{macrocode}
+% \end{environment}
+%
+% \iffalse
+% --------------------------------------------------------------------
+% \fi
+%
+% \subsubsection{Some utilities to get bounding boxes and the like}
+%
+%
+% All coordinates, and such are recorded in centimetres. It is worth
+% remembering that the Tikz coordinate system has the $y$ axis point
+% upward, while typical image software has the $y$ axis point down.
+% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch)
+% resolution.
+%
+% That means that scaling factor becomes
+%
+% $$
+% \frac{150\mathrm{pixel}}{2.54\mathrm{cm}} =
+% 59.055\frac{\mathrm{pixel}}{\mathrm{cm}}
+% $$
+%
+% \iffalse Using definition in terms of printers feet - the one to
+% use!
+%
+% PNG: 1674 x 1101
+% PDF: "lower left": [-0.02107,-0.02107],
+% "upper right": [28.31705,18.60843]
+% Width: 28.31705+0.02107 = 28.33812
+% Height: 18.60843+0.02107 = 18.62950
+% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166
+% 1101 / 18.62950 = 59.09981480984460130438
+% Average = 59.08609399739914809302
+%
+% Calculated = 150 / 2.54 = 59.05511811023622047244
+%
+% Using 1/72.27
+%
+% "lower left": [-0.02109,-0.02109],
+% "upper right": [28.321,18.61102]
+% Width: 28.321+0.02109=28.34209
+% Height: 18.61102+0.02109=18.63211
+% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267
+% 1101 / 18.63211 = 59.09153606328000425072
+% Average = 59.07781736180584381169
+% \fi
+%
+% Since we want to write all dimensions in centimetres, we need to be
+% able to convert \texttt{pt} dimensions to centimetres. We make two
+% macros to do that for us.
+%
+% The exact definition of 1pt is
+%
+% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1"
+% = 0.03513\overline{6}$$
+%
+% \begin{macrocode}
+% 2.54 / 72.27 = .03514598035145980351
+% \def\wg@pt@to@cm#1{\pgfmathparse{#1 * 0.0351460}}
+\def\wg@pt@to@cm#1{\pgfmathparse{#1 * 0.0351367}}
+\def\ptpoint@to@cm#1#2{%
+ \wg@pt@to@cm{#1}\edef\x{\pgfmathresult}%
+ \wg@pt@to@cm{#2}\edef\y{\pgfmathresult}}
+% \end{macrocode}
+%
+% The next macro gets an anchors coordinates and stores them (in units
+% of centimetres) in \cs{tmp@x} and \cs{tmp@y}
+%
+% \begin{macrocode}
+\def\wg@get@nchor#1#2{%
+ \wg@dbg{2}{Get anchor coordinates #1.#2}
+ \pgfpointanchor{#1}{#2}%
+ \wg@dbg{2}{ `\the\pgf@x',`\the\pgf@y'}
+ \pgfgetlastxy\tmp@x\tmp@y%
+ \wg@dbg{2}{ `\tmp@x',`\tmp@y'}
+ \wg@pt@to@cm{\tmp@x}\edef\tmp@x{\pgfmathresult}
+ \wg@pt@to@cm{\tmp@y}\edef\tmp@y{\pgfmathresult}
+}
+% \end{macrocode}
+%
+% This does the same as above, but transform to the global coordinate
+% system.
+%
+% \begin{macrocode}
+\def\wg@get@global@nchor#1#2{%
+ \pgfpointanchor{#1}{#2}%
+ \pgfgetlastxy\tmp@x\tmp@y%
+ \pgfpointtransformed{\pgfpoint{\tmp@x}{\tmp@y}}
+ \pgf@xa=\pgf@x
+ \pgf@ya=\pgf@y
+ %% \message{^^JAnchor #1.#2 @ (\the\pgf@xa,\the\pgf@ya)}
+ \wg@pt@to@cm{\the\pgf@xa}\edef\tmp@x{\pgfmathresult}
+ \wg@pt@to@cm{\the\pgf@ya}\edef\tmp@y{\pgfmathresult}
+}
+% \end{macrocode}
+%
+% This records the bounding box given by a named node. The result is
+% stored in the macros \cs{llx}, \cs{lly}, \cs{urx}, and \cs{ury}.
+%
+% \begin{macrocode}
+\def\wg@get@bb#1{%
+ \wg@get@nchor{#1}{south west}
+ \edef\llx{\tmp@x}
+ \edef\lly{\tmp@y}
+ \wg@get@nchor{#1}{north east}
+ \edef\urx{\tmp@x}
+ \edef\ury{\tmp@y}
+}
+\def\wglogbb#1{%
+ \wg@get@bb{#1}%
+ \message{^^J`#1' BB: (\llx,\lly) x (\urx,\ury)^^J}}
+% \end{macrocode}
+%
+%
+%
+% \iffalse
+% --------------------------------------------------------------------
+%</utils>
+%\fi
diff --git a/macros/latex/contrib/wargame/source/util/compound.dtx b/macros/latex/contrib/wargame/source/util/compound.dtx
new file mode 100644
index 0000000000..aca6800644
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/compound.dtx
@@ -0,0 +1,242 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+%
+% \subsubsection{Pictures in compound nodes}
+%
+% \begin{Macro}{\wg@pic}
+% The macro \cs{wg@pic} will render a \texttt{pic}. This is used by
+% the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node
+% shapes extensively.
+%
+% The arguments are
+% \begin{enumerate}
+% \item Prefix
+% \item Position
+% \item Fixed options
+% \item User options
+% \item Picture.
+% \end{enumerate}
+%
+% That is, the macro expects calls like
+% \begin{Syntax}
+% \cs{wg@pic}\oarg{options}\meta{picture}\cs{@endwg@pic}\marg{prefix}\marg{position}\marg{options}
+% \end{Syntax}
+%
+% Note the \cs{@endwg@pic} at the end of the call to swallow up
+% \meta{picture}. Typically this macro is used as
+%
+% \begin{Syntax}
+% \cs{edef}\cs{args}\{\meta{something}\}
+% \cs{expandafter}\cs{wg@pic}\cs{args}\cs{@endwg@pic}\marg{prefix}\parg{position}\marg{options}
+% \end{Syntax}
+%
+% where \meta{something} typically expands to \oarg{user
+% option}\meta{picture}
+%
+% First, the top-level macro \cs{wg@pic} that looks for user
+% options.
+%
+% \begin{macrocode}
+\def\wg@pic{%
+ \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%]
+}
+% \end{macrocode}
+%
+% This macro then forwards to \cs{wg@@pic} to gobble up
+% \meta{picture}.
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\wg@@pic[#1]#2\@endwg@pic{%
+ \wg@dbg{2}{Options: `#1', picture: `#2'}%
+ \wg@@@pic{#1}{#2}%
+}
+% \end{macrocode}
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \item Prefix
+% \item Coordinates
+% \item Fixed options
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\wg@@@pic#1#2#3#4#5{%
+ \ifx|#2|\wg@dbg{3}{No picture given}%
+ \else%
+ \wg@dbg{3}{^^JWG Pic:
+ ^^J User options: #1
+ ^^J Picture: #2
+ ^^J Prefix: #3
+ ^^J Coordinates: #4
+ ^^J Fixed options: #5}%
+ % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
+ \pic[#5,#1] at (#4) {#3#2};%
+ \ifwg@s@ve%
+ \pgf@relevantforpicturesizetrue%
+ \begin{getbbl}%
+ \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};%
+ \end{getbbl}%
+ \wg@dbg{5}{Clipping to local bounding box}%
+ \clip (L.south west) rectangle (L.north east);%
+ \pgf@relevantforpicturesizefalse \global\wg@s@vefalse%
+ \fi
+ \fi%
+ \wg@dbg{3}{End of WG Pic}
+}
+% \end{macrocode}
+% \end{Macro}
+%
+%
+% \begin{Macro}{\wg@pic@all}
+%
+% This macro sets all pictures in a list.
+%
+% \begin{enumerate}
+% \item List
+% \item Prefix
+% \item Position
+% \item Styles
+% \end{enumerate}
+%
+%
+% \begin{macrocode}
+\def\wg@pic@all#1#2#3#4{%
+ \wg@dbg{2}{WG picture loop
+ ^^J List: \meaning#1
+ ^^J Prefix: `#2'
+ ^^J Position: `#3'
+ ^^J Styles: `#4'}
+ \foreach \p in #1{%
+ \wg@dbg{2}{WG picture element: \meaning\p}%
+ \expandafter\wg@pic\p\@endwg@pic {#2}{#3}{#4}%
+ }%
+}
+% \end{macrocode}
+% \end{Macro}
+%
+% \iffalse
+% --------------------------------------------------------------------
+% \fi
+%
+% \subsubsection{Nodes in compound nodes}
+%
+% \begin{Macro}{\wg@node}
+% The macro \cs{wg@node} will render a \texttt{node}. This can be
+% used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex}
+% node shapes.
+%
+% The arguments are
+% \begin{enumerate}
+% \item Prefix
+% \item Position
+% \item Fixed options
+% \item User options
+% \item Body.
+% \end{enumerate}
+%
+% That is, the macro expects calls like
+% \begin{Syntax}
+% \cs{wg@node}\oarg{options}\meta{body}\cs{@endwg@node}\marg{prefix}\marg{position}\marg{options}
+% \end{Syntax}
+%
+% Note the \cs{@endwg@node} at the end of the call to swallow up
+% \meta{body}. Typically this macro is used as
+%
+% \begin{Syntax}
+% \cs{edef}\cs{args}\{\meta{something}\}
+% \cs{expandafter}\cs{wg@node}\cs{args}\cs{@endwg@node}\marg{prefix}\parg{position}\marg{options}
+% \end{Syntax}
+%
+% where \meta{something} typically expands to \oarg{user
+% option}\meta{body}
+%
+% First, the top-level macro \cs{wg@node} that looks for user
+% options.
+%
+% \begin{macrocode}
+\def\wg@node{%
+ \@ifnextchar[{\wg@@node}{\wg@@node[]}%]
+}
+% \end{macrocode}
+%
+% This macro then forwards to \cs{wg@@node} to gobble up
+% \meta{body}.
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\wg@@node[#1]#2\@endwg@node{%
+ \wg@dbg{2}{Options: `#1', body: `#2'}%
+ \wg@@@node{#1}{#2}%
+}
+% \end{macrocode}
+%
+% \begin{enumerate}
+% \item User options
+% \item Arguments
+% \item Prefix
+% \item Coordinates
+% \item Fixed options
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\wg@@@node#1#2#3#4#5{%
+ \ifx|#2|\wg@dbg{3}{No body given}%
+ \else%
+ \wg@dbg{3}{^^JWG Pic:
+ ^^J User options: #1
+ ^^J Body: #2
+ ^^J Prefix: #3
+ ^^J Coordinates: #4
+ ^^J Fixed options: #5}%
+ % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
+ \node[#5,#1] at (#4) {#3#2};%
+ \fi%
+ \wg@dbg{3}{End of WG Node}
+}
+% \end{macrocode}
+% \end{Macro}
+%
+%
+% \begin{Macro}{\wg@node@all}
+%
+% This macro sets all pictures in a list.
+%
+% \begin{enumerate}
+% \item List
+% \item Prefix
+% \item Position
+% \item Styles
+% \end{enumerate}
+%
+%
+% \begin{macrocode}
+\def\wg@node@all#1#2#3#4{%
+ \wg@dbg{2}{WG picture loop
+ ^^J List: \meaning#1
+ ^^J Prefix: `#2'
+ ^^J Position: `#3'
+ ^^J Styles: `#4'}
+ \foreach \p in #1{%
+ \wg@dbg{2}{WG picture element: \meaning\p}%
+ \expandafter\wg@node\p\@endwg@node {#2}{#3}{#4}%
+ }%
+}
+% \end{macrocode}
+% \end{Macro}
+%
+% \iffalse
+% --------------------------------------------------------------------
+%</utils>
+%\fi
diff --git a/macros/latex/contrib/wargame/source/util/core.dtx b/macros/latex/contrib/wargame/source/util/core.dtx
index 1d95aa38c6..8ee6f84c85 100644
--- a/macros/latex/contrib/wargame/source/util/core.dtx
+++ b/macros/latex/contrib/wargame/source/util/core.dtx
@@ -12,1342 +12,20 @@
%<*utils>
%\fi
%
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Miscellaneous macros}
-%
-% \begin{Macro}{\wg@dbg}
-% Debugging support. The counter \cs{wargamedbglvl} sets the debug
-% level. The package code then uses \cs{wg@dbg} to print out
-% debugging messages. This macro takes two arguments --- the first
-% is the \emph{least} debug level at which the message is printed, and
-% the second is the message it self.
-%
-% \begin{macrocode}
-\newcount\wargamedbglvl\wargamedbglvl=0
-\def\wg@dbg#1#2{%
- \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi}
-% \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\wg@addto@macro}
-%
-% The macro \cs{wg@addto@macro}\marg{macro}\marg{other} adds the
-% definition of the macro \meta{other} to the macro \meta{macro}.
-% This uses the \cs{toks} trick of storing the \emph{tokens} of the
-% definition of a \meta{macro} and \meta{other} into \spec{@} and
-% expanding that token into the definition of \meta{macro}.
-% Effectively, this means that the top-level definition of
-% \meta{macro} and \meta{other} are expanded (i.e., macros used in
-% the definition of either macro is \emph{not} expanded) and then
-% that becomes the new definition of \meta{macro}.
-%
-% We will use this macro to do \emph{shallow} definitions of macros
-% to contain keys and such.
-%
-% \begin{macrocode}
-\long\def\wg@addto@macro#1#2{%
- \begingroup
- \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}%
- \xdef#1{\the\toks@}%
- \endgroup}
-% \end{macrocode}
-% \end{Macro}
-%
-%
-% \begin{Macro}{\wg@sub@nchor}
-% Get anchor from sub node. We cannot use \cs{pgfpointanchor} since
-% that returns the anchor coordinates in the global coordinate
-% system.
-%
-% \begin{macrocode}
-\def\wg@sub@nchor#1#2{%
- \wg@dbg{3}{^^JGet `#2' in `#1'}%
- \@ifundefined{pgf@sh@ns@#1}{%
- \pgf@x=0cm\pgf@y=0cm}{%
- \pgf@process{%
- \csname pgf@sh@ma@#1\endcsname% MW
- \csname pgf@sh@np@#1\endcsname%
- \pgf@sh@reanchor{\csname pgf@sh@ns@#1\endcsname}{#2}}}%
- \wg@dbg{10}{-> \the\pgf@x,\the\pgf@y}%
-}
-% \end{macrocode}
-% \end{Macro}
-%
-%
-% Scratch dimensions
-%
-% \begin{macrocode}
-\newdimen\wg@tmpa
-\newdimen\wg@tmpb
-\newdimen\wg@tmpc
-\newdimen\wg@tmpd
-% \end{macrocode}
-%
-%
-% Macro to easy restore a saved path
-%
-% \begin{macrocode}
-\def\settosave#1{
- \pgfsyssoftpath@setcurrentpath{#1}}
-% \end{macrocode}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Pictures in compound nodes}
-%
-% \begin{Macro}{\wg@pic}
-% The macro \cs{wg@pic} will render a \texttt{pic}. This is used by
-% the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex} node
-% shapes extensively.
-%
-% The arguments are
-% \begin{enumerate}
-% \item Prefix
-% \item Position
-% \item Fixed options
-% \item User options
-% \item Picture.
-% \end{enumerate}
-%
-% That is, the macro expects calls like
-% \begin{Syntax}
-% \cs{wg@pic}\oarg{options}\meta{picture}\cs{@endwg@pic}\marg{prefix}\marg{position}\marg{options}
-% \end{Syntax}
-%
-% Note the \cs{@endwg@pic} at the end of the call to swallow up
-% \meta{picture}. Typically this macro is used as
-%
-% \begin{Syntax}
-% \cs{edef}\cs{args}\{\meta{something}\}
-% \cs{expandafter}\cs{wg@pic}\cs{args}\cs{@endwg@pic}\marg{prefix}\parg{position}\marg{options}
-% \end{Syntax}
-%
-% where \meta{something} typically expands to \oarg{user
-% option}\meta{picture}
-%
-% First, the top-level macro \cs{wg@pic} that looks for user
-% options.
-%
-% \begin{macrocode}
-\def\wg@pic{%
- \@ifnextchar[{\wg@@pic}{\wg@@pic[]}%]
-}
-% \end{macrocode}
-%
-% This macro then forwards to \cs{wg@@pic} to gobble up
-% \meta{picture}.
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\wg@@pic[#1]#2\@endwg@pic{%
- \wg@dbg{2}{Options: `#1', picture: `#2'}%
- \wg@@@pic{#1}{#2}%
-}
-% \end{macrocode}
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \item Prefix
-% \item Coordinates
-% \item Fixed options
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\wg@@@pic#1#2#3#4#5{%
- \ifx|#2|\wg@dbg{3}{No picture given}%
- \else%
- \wg@dbg{3}{^^JWG Pic:
- ^^J User options: #1
- ^^J Picture: #2
- ^^J Prefix: #3
- ^^J Coordinates: #4
- ^^J Fixed options: #5}%
- % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
- \pic[#5,#1] at (#4) {#3#2};%
- \ifwg@s@ve%
- \pgf@relevantforpicturesizetrue%
- \begin{getbbl}%
- \pic[draw=none,fill=none,transform shape] at (#4) {#3#2};%
- \end{getbbl}%
- \wg@dbg{5}{Clipping to local bounding box}%
- \clip (L.south west) rectangle (L.north east);%
- \pgf@relevantforpicturesizefalse \global\wg@s@vefalse%
- \fi
- \fi%
- \wg@dbg{3}{End of WG Pic}
-}
-% \end{macrocode}
-% \end{Macro}
-%
-%
-% \begin{Macro}{\wg@pic@all}
-%
-% This macro sets all pictures in a list.
-%
-% \begin{enumerate}
-% \item List
-% \item Prefix
-% \item Position
-% \item Styles
-% \end{enumerate}
-%
-%
-% \begin{macrocode}
-\def\wg@pic@all#1#2#3#4{%
- \wg@dbg{2}{WG picture loop
- ^^J List: \meaning#1
- ^^J Prefix: `#2'
- ^^J Position: `#3'
- ^^J Styles: `#4'}
- \foreach \p in #1{%
- \wg@dbg{2}{WG picture element: \meaning\p}%
- \expandafter\wg@pic\p\@endwg@pic {#2}{#3}{#4}%
- }%
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Nodes in compound nodes}
-%
-% \begin{Macro}{\wg@node}
-% The macro \cs{wg@node} will render a \texttt{node}. This can be
-% used by the \texttt{natoapp6cs}, \texttt{chit}, and \texttt{hex}
-% node shapes.
-%
-% The arguments are
-% \begin{enumerate}
-% \item Prefix
-% \item Position
-% \item Fixed options
-% \item User options
-% \item Body.
-% \end{enumerate}
-%
-% That is, the macro expects calls like
-% \begin{Syntax}
-% \cs{wg@node}\oarg{options}\meta{body}\cs{@endwg@node}\marg{prefix}\marg{position}\marg{options}
-% \end{Syntax}
-%
-% Note the \cs{@endwg@node} at the end of the call to swallow up
-% \meta{body}. Typically this macro is used as
-%
-% \begin{Syntax}
-% \cs{edef}\cs{args}\{\meta{something}\}
-% \cs{expandafter}\cs{wg@node}\cs{args}\cs{@endwg@node}\marg{prefix}\parg{position}\marg{options}
-% \end{Syntax}
-%
-% where \meta{something} typically expands to \oarg{user
-% option}\meta{body}
-%
-% First, the top-level macro \cs{wg@node} that looks for user
-% options.
-%
-% \begin{macrocode}
-\def\wg@node{%
- \@ifnextchar[{\wg@@node}{\wg@@node[]}%]
-}
-% \end{macrocode}
-%
-% This macro then forwards to \cs{wg@@node} to gobble up
-% \meta{body}.
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\wg@@node[#1]#2\@endwg@node{%
- \wg@dbg{2}{Options: `#1', body: `#2'}%
- \wg@@@node{#1}{#2}%
-}
-% \end{macrocode}
-%
-% \begin{enumerate}
-% \item User options
-% \item Arguments
-% \item Prefix
-% \item Coordinates
-% \item Fixed options
-% \end{enumerate}
-%
-% \begin{macrocode}
-\def\wg@@@node#1#2#3#4#5{%
- \ifx|#2|\wg@dbg{3}{No body given}%
- \else%
- \wg@dbg{3}{^^JWG Pic:
- ^^J User options: #1
- ^^J Body: #2
- ^^J Prefix: #3
- ^^J Coordinates: #4
- ^^J Fixed options: #5}%
- % \wg@dbg{2}{\string\pic[#5,#1] at (#4) {#3#2}}%
- \node[#5,#1] at (#4) {#3#2};%
- \fi%
- \wg@dbg{3}{End of WG Node}
-}
-% \end{macrocode}
-% \end{Macro}
-%
-%
-% \begin{Macro}{\wg@node@all}
-%
-% This macro sets all pictures in a list.
-%
-% \begin{enumerate}
-% \item List
-% \item Prefix
-% \item Position
-% \item Styles
-% \end{enumerate}
-%
-%
-% \begin{macrocode}
-\def\wg@node@all#1#2#3#4{%
- \wg@dbg{2}{WG picture loop
- ^^J List: \meaning#1
- ^^J Prefix: `#2'
- ^^J Position: `#3'
- ^^J Styles: `#4'}
- \foreach \p in #1{%
- \wg@dbg{2}{WG picture element: \meaning\p}%
- \expandafter\wg@node\p\@endwg@node {#2}{#3}{#4}%
- }%
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Bounding boxes}
-%
-% Bounding box dimensions
-%
-%
-% \begin{macrocode}
-\newdimen\wg@bb@minx
-\newdimen\wg@bb@miny
-\newdimen\wg@bb@maxx
-\newdimen\wg@bb@maxy
-% \end{macrocode}
-%
-%
-%
-% Enable or disable bounding box tracking
-%
-% \begin{macrocode}
-\newif\ifwg@notrelevantforpathsize\wg@notrelevantforpathsizefalse
-% \end{macrocode}
-%
-%
-% \begin{Macro}{wg@resetbb}
-% Reset the bounding box tracking dimensions
-%
-% \begin{macrocode}
-\def\wg@resetbb{%
- \global\wg@bb@minx=16000pt\relax%
- \global\wg@bb@miny=16000pt\relax%
- \global\wg@bb@maxx=-16000pt\relax%
- \global\wg@bb@maxy=-16000pt\relax%
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\old@pgf@protocolsize}
-% Save PGF's bounding box algorithm
-%
-% \begin{macrocode}
-\let\old@pgf@protocolsize\pgf@protocolsizes
-% \end{macrocode}
-% \end{Macro}
-%
-% \begin{Macro}{\wg@protocolsizes}
-% Our bounding box algorithm
-%
-% \begin{macrocode}
-\def\wg@protocolsizes#1#2{%
- \old@pgf@protocolsize{#1}{#2}
- \ifwg@notrelevantforpathsize\else%
- \ifdim#1<\wg@bb@minx\global\wg@bb@minx#1\fi%
- \ifdim#1>\wg@bb@maxx\global\wg@bb@maxx#1\fi%
- \ifdim#2<\wg@bb@miny\global\wg@bb@miny#2\fi%
- \ifdim#2>\wg@bb@maxy\global\wg@bb@maxy#2\fi%
- \fi
-}
-% \end{macrocode}
-% \end{Macro}
-%
-% % \begin{environment}{getbbl}
-% Environment that tracks the local bounding box
-%
-% \begin{macrocode}
-\newenvironment{getbbl}{%
- \wg@resetbb%
- \wg@notrelevantforpathsizefalse%
- \global\let\pgf@protocolsizes\wg@protocolsizes}{%
- \gdef\pgf@sh@ns@L{rectangle}
- \gdef\pgf@sh@np@L{%
- \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}%
- \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}%
- }
- \gdef\pgf@sh@nt@L{{1}{0}{0}{1}{0pt}{0pt}}
- \gdef\pgf@sh@pi@L{\pgfpictureid}
- \global\let\pgf@protocolsizes\old@pgf@protocolsize
-}
-% \end{macrocode}
-% \end{environment}
-%
-% \begin{environment}{getbb}
-% Environment to track global bounding box
-%
-% \begin{macrocode}
-\newenvironment{getbb}{%
- \wg@resetbb%
- \wg@notrelevantforpathsizefalse%
- \global\let\pgf@protocolsizes\wg@protocolsizes}{%
- \gdef\pgf@sh@ns@M{rectangle}
- \gdef\pgf@sh@np@M{%
- \def\southwest{\pgfqpoint{\the\wg@bb@minx}{\the\wg@bb@miny}}%
- \def\northeast{\pgfqpoint{\the\wg@bb@maxx}{\the\wg@bb@maxy}}%
- }
- \gdef\pgf@sh@nt@M{{1}{0}{0}{1}{0pt}{0pt}}
- % \pgfgettransform\pgf@temp%
- % \xdef\pgf@sh@nt@M{\pgf@temp}
- % \pgfgettransformentries{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{\pgf@temp}{\pgf@temp}
- % \message{^^JTransform of M: \meaning\pgf@temp}
- % \xdef\pgf@sh@nt@M{{\wg@tmp@a}{\wg@tmp@b}{\wg@tmp@c}{\wg@tmp@d}{0pt}{0pt}}%
- % \message{^^JTransform of M: \meaning\pgf@sh@nt@M}
- \gdef\pgf@sh@pi@M{\pgfpictureid}
- \global\let\pgf@protocolsizes\old@pgf@protocolsize
-}
-% \end{macrocode}
-% \end{environment}
+% \input{util/misc.dtx}
+% \input{util/compound.dtx}
+% \input{util/bb.dtx}
+% \input{util/tikz.dtx}
+% \input{util/randomid.dtx}
+% \input{util/icons.dtx}
%
% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Other Tikz utilities}
-%
-% \begin{TikzKey}{tikz/reverseclip}
-%
-% A reverse clipping path. This is used to cut out stuff outside of
-% path defined.
-%
-% \begin{macrocode}
-\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) --
- (current bounding box.south east) --
- (current bounding box.south west) --
- (current bounding box.north west) --
- (current bounding box.north east)}]
-% \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{tikz/clip even odd rule}
-% A reverse clipping path
-%
-% \begin{macrocode}
-\tikzset{
- clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey
-}
-% \end{macrocode}
-% \end{TikzKey}
-%
-%
-% \begin{TikzKey}{tikz/invclip}
-%
-% Inverse clipping. This should be an option \emph{after} the path to
-% do the inverse clipping by. This works by adding a \emph{large}
-% (page) path to the current path, and then use that as clipping.
-%
-% \begin{macrocode}
-\tikzset{
- invclip/.style={
- clip,insert path=
- [clip even odd rule]{
- [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen)
- }
- },
-}
-% \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{save clip}
-%
-% An option for use with sub-elements of NATO App 6(c) or chit
-% nodes. This will save the current path as a clipping path for the
-% next paths to be drawn in the sub-element
-%
-% \begin{macrocode}
-\newif\ifwg@s@ve\wg@s@vefalse
-\tikzset{
- save clip/.is choice,
- save clip/true/.code={\global\wg@s@vetrue},
- save clip/false/.code={\global\wg@s@vefalse},
- save clip/.default={true},
- save clip/.initial={false},
-}
-% \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{scale line widths}
-%
-% Scales any line width specified in the node options.
-%
-% Use like
-%
-% \begin{verbatim}
-% \tikzset{
-% some/.style={
-% scale line widths,
-% line width=1pt}
-% }
-% \end{verbatim}
-%
-% Note that the order is important.
-%
-% \begin{macrocode}
-\tikzset{
- scale line widths/.style={%
- /utils/exec=\def\tikz@semiaddlinewidth##1{%
- \pgfgettransformentries{%
- \wg@jaca}{%
- \wg@jacb}{%
- \wg@jacc}{%
- \wg@jacd}{%
- \wg@tmp}{%
- \wg@tmp}%
- \pgfmathsetmacro{\wg@jac}{sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))}%
- \wg@dbg{4}{Scaling line width ##1 by \wg@jac}
- \pgfmathsetmacro{\wg@lw}{\wg@jac*##1}%
- \wg@dbg{4}{Scaled ##1 -> \wg@lw}
- \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}%
- \wg@dbg{4}{Added scaled option \wg@lw}
- \pgfmathsetlength\pgflinewidth{\wg@lw pt}
- \wg@dbg{4}{Did set line width \wg@lw pt}
- }},
- relative line width/.style={%
- /utils/exec=\def\tikz@semiaddlinewidth##1{%
- \wg@dbg{4}{Relative line width #1 times ##1}%
- \pgfmathsetmacro{\wg@lv}{#1*##1}%
- \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}%
- \pgfmathsetlength\pgflinewidth{\wg@lw pt}}}
-}
-% \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{sub pic actions}
-%
-% This is key that propagates actions to sub pictures of pictures.
-% The normal \texttt{pic actions} cannot be used as it causes an
-% infinite loop.
-%
-% \begin{macrocode}
-\tikzset{
- sub pic actions/.code={%
- \tikz@picmode%
- \edef\opts{%
- \iftikz@mode@draw draw,\else draw=none,\fi
- \iftikz@mode@fill fill\else fill=none\fi}
- \wg@dbg{5}{^^JSub Mode: \meaning\tikz@picmode \meaning\opts}
- \pgfset{/tikz/.cd}
- \pgfkeysalsofrom{\opts}
- }}
-% \end{macrocode}
-% \end{TikzKey}
-%
-% \begin{TikzKey}{wg/debug show}
-%
-% Show debugging information
-%
-% \begin{macrocode}
-\tikzset{
- wg/debug show/.code={%
- \extractcolorspec{pgfstrokecolor}{\wg@tmp@fg}
- \def\wg@tmp@bg{none}
- \@ifundefinedcolor{pgffillcolor}{}{
- \extractcolorspec{pgffillcolor}{\wg@tmp@bg}}
- \begingroup
- \tikz@mode
- \wargamedbglvl=#1
- \wg@dbg{3}{Drawing with w/stroke `\wg@tmp@fg'
- (\tikz@strokecolor,\iftikz@mode@draw\else not\space\fi drawing)
- and fill `\wg@tmp@bg' (\tikz@fillcolor,\iftikz@mode@fill\else
- not\space\fi filling)}
- \endgroup
- }
-}
-% \end{macrocode}
-% \end{TikzKey}
-%
-%
-% \iffalse
-% --------------------------------------------------------------------
-% \fi
-%
-% \subsubsection{Random IDs}
-%
-%
-% \begin{macrocode}
-\def\wg@r@ndom@id{%
- \def\wg@uuid{}
- \foreach \i in {1,...,8}{%
- \pgfmathparse{Hex(random(0,15))}
- \xdef\wg@uuid{\wg@uuid\pgfmathresult}}}
-% \end{macrocode}
-%
-% \iffalse
%</utils>
%\fi
-% \iffalse
-% ====================================================================
-% \fi
-%
-% \subsection{The \texttt{wgexport} class}
-% \label{sec:impl:util}
-%
-% This document class is used for exporting game component to be used
-% in a VASSAL module
-% libraries.
-%
-% \iffalse
-%<*exportcls>
-%\fi
-%
-% Class identification and load \texttt{wargame} package
-%
-% \begin{macrocode}
-\ProvidesClass{wgexport}
-\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone}
-\DeclareOption{noterrainpic}{%
- \PassOptionsToPackage{\CurrentOption}{wargame}}
-\DeclareOption{terrainpic}{%
- \PassOptionsToPackage{\CurrentOption}{wargame}}
-\DeclareOption*{%
- \PassOptionsToClass{\CurrentOption}{standalone}}
-\ProcessOptions\relax
-\LoadClass{standalone}
-\RequirePackage{wargame}
-% \end{macrocode}
-%
-% We need a few utilities before we get to the actual environment.
-% First, we need a tools to write out literal left and right curly
-% braces. We do a bit of catcode hackery to accomplish that.
-%
-% \begin{macrocode}
-\begingroup
-\catcode`\^^I=12
-\def\@tabchar{^^I}
-\catcode`<=1 \catcode`>=2
-\catcode`{=12 \catcode`}=12
-\gdef\@lbchar<{>
-\gdef\@rbchar<}>
-\endgroup
-% \end{macrocode}
-%
-% Above, we temporarily set the tab, and left and right curly brace
-% characters to be regular letters (12), and the catcodes of less than
-% and greater than to be those of left and right curly braces
-% respectively. We then define the macros \cs{@tabchar},
-% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters.
-% \LaTeX already has \cs{@percentchar}.
-%
-% Everything we do should go inside this environment. The single
-% optional argument is the file name stem of the output JSON file.
-%
-% \begin{macrocode}
-\newenvironment{imagelist}[1][\jobname]{%
- \newwrite\mk@out%
- \def\mk@i{}%
- \def\mk@w{\immediate\write\mk@out}%
- \immediate\openout\mk@out=#1.json
- \mk@w{[}
-}{
- \mk@w{\mk@i \@lbchar "name":"End of list", "category": "<<eol>>",
- "subcategory": "" \@rbchar }
- \mk@w{]}
- \immediate\closeout\mk@out
-}
-% \end{macrocode}
-%
-%
-% Preceed all images (\textsf{tikzpicture}) with this command
-%
-% First argument is the name of the image. This can be anything.
-% Note that for counters, if the name ends in \texttt{flipped} then it
-% is considered the backside of a counter.
-%
-% Second argument is the type of image. Recognised types are
-%
-% \begin{itemize}
-% \item \texttt{board} for boards
-% \item \texttt{oob} for OOBs
-% \item \texttt{chart} for charts
-% \item \texttt{counter} for counters
-% \item \texttt{front} for front page
-% \end{itemize}
-%
-% Other types can be used, and the images will be exported, but the
-% Python script pays no particular attention to those then. Use for
-% example to prepare images for help or the like.
-%
-% The third argument is the sub type. This is most relevant for the
-% counters. Sub types can be anything, but since the counters will
-% receive different prototypes based on the sub type, it makes sense
-% to divide into sub types a la
-%
-% \begin{itemize}
-% \item factions
-% \item common markers
-% \end{itemize}
-%
-% The faction sub types should just be the name of the faction.
-% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact. Spaces should not
-% matter.
-%
-% For common markers, there are a few names that are recognised
-% specifically by the Python script. These are
-%
-% \begin{itemize}
-% \item \texttt{common}
-% \item \texttt{all}
-% \item \texttt{marker}
-% \item \texttt{markers}
-% \end{itemize}
-%
-% Counters that has these sub-types will no be considered to belong
-% to any faction.
-%
-% Note that the Python script uses the faction names to guess the
-% players of the game, and uses them in several places.
-%
-%
-% \begin{macrocode}
-\def\info{%
- \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}}
-\def\@@info#1#2#3#4{%
- \chit@dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}%
- \mk@w{ \@lbchar}%
- \mk@w{ \space "name": "#2",}%
- \mk@w{ \space "category": "#3",}%
- \mk@w{ \space "subcategory": "#4", }%
- \mk@w{ \space "number": \thepage #1}%
- \let\oldmk@i\mk@i%
- \ifx#1,\relax\edef\mk@i{\mk@i\space\space}\fi}
-\def\end@info{%
- \let\mk@i\oldmk@i%
- \mk@w{ \space \@rbchar,}}
-% \end{macrocode}
-%
-% Make separate images for each counter (single sided).
-%
-% \begin{macrocode}
-\newcommand\chitimages[2][]{%
- \begingroup%
- \let\chit@report\do@chit@report%
- \let\natoapp@report\do@natoapp@report%
- \chit@dbg{2}{chits to make images of `#2'}%
- \foreach[count=\ti from 0] \t/\x in #2{%
- \ifx\t\empty\else% Ignore empty rows
- \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')}
- \ifx\t\x\def\x{#1}\fi% Take sub-category or default
- \foreach \u/\m in \t{%
- \ifx\u\empty\else% Ignore empty cells
- \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
- \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
- \ifx\u\m\def\m{1}\fi% If the same as unit
- \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
- %% We only make one copy of the chit, since we can duplicate
- %% it in VASSAL
- \info*{\u}{counter}{\x}
- \begin{tikzpicture}
- \chit[\u=\ti]%
- \end{tikzpicture}
- \end@info%
- %% \foreach \n in {1,...,\m}{% Make a number of copies
- %% \ifx\u\chit@blank%
- %% \chit@dbg{3}{Ignoring blank chit:\u}%
- %% \else%
- %% \info{\u}{counter}{#2}
- %% \begin{tikzpicture}
- %% \chit[\u=\ti](\c,\r)%
- %% \end{tikzpicture}
- %% \fi%
- %% }%
- \fi%
- }%
- \chit@dbg{2}{End of inner loop}%
- \fi%
- }%
- \chit@dbg{2}{End of outer loop}%
- \endgroup%
-}
-% \end{macrocode}
-%
-% Make separate images for each counter (double sided). The back-side
-% counters must be defined by append `\texttt{ flipped}' the front
-% face name
-%
-% \begin{macrocode}
-\newcommand\doublechitimages[2][]{%
- \begingroup%
- \let\chit@report\do@chit@report%
- \let\natoapp@report\do@natoapp@report%
- \foreach[count=\ti from 0] \t/\x in #2{%
- \ifx\t\empty\else% Ignore empty rows
- \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')}
- \ifx\t\x\def\x{#1}\fi% Take sub-category or default
- \foreach \u/\m in \t{%
- \ifx\u\empty\else% Ignore empty cells
- \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
- \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
- \ifx\u\m\def\m{1}\fi% If the same as unit
- \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
- %% Flipped chit
- \edef\s{\u\space flipped}%
- %% We only make one copy of the chit, since we can duplicate
- %% it in VASSAL
- \info*{\u}{counter}{\x}%
- \begin{tikzpicture}%
- \chit[\u=\ti]%
- \end{tikzpicture}%
- \end@info%
- \info*{\s}{counter}{\x}%
- \begin{tikzpicture}%
- \chit[\s=\ti]%
- \end{tikzpicture}%
- \end@info%
- %% \foreach \n in {1,...,\m}{% Make a number of copies
- %% \ifx\u\chit@blank%
- %% \chit@dbg{3}{Ignoring blank chit:\u}%
- %% \else%
- %% \info{\u}{counter}{#2}
- %% \begin{tikzpicture}
- %% \chit[\u=\ti](\c,\r)%
- %% \end{tikzpicture}
- %% \fi%
- %% }%
- \fi%
- }%
- \fi%
- }%
- \endgroup%
-}
-% \end{macrocode}
-%
-% Special for boards, we have the environment \textsf{boardimage}.
-% Like \cs{info} we must specify the name and sub-category of the
-% board, but the category is assumed to be \texttt{board} (though the
-% optional argument can specify a different category).
-%
-% Within this environment some specific styles are defined that allows
-% the user to specify VASSAL zones on the board. For this to work
-% properly, the parent \textsf{tikzpicture} \emph{must} have the style
-% \texttt{zoned}. This style will record the bounding box of the
-% picture which we will need to calculate VASSAL coordinates later
-% on.
-%
-% Other styles are \texttt{zone scope}, to be applied to
-% \texttt{scope}s in the picture, and \texttt{zone path} to be applied
-% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the
-% picture. These will record coordinates of these elements in side
-% the picture. The Python script will then define VASSAL zones based
-% on these coordinates.
%
-% For \texttt{zone scope} applied to a \texttt{scope}, what is
-% recorded are
+% \input{util/export.dtx}
%
-% \begin{itemize}
-% \item The current coordinate transformation matrix
-% \item The current translation
-% \item The bounding box, within the current transformation and
-% translation.
-% \end{itemize}
-%
-% To define a zone in the board, simply enclose it in a
-%
-% \begin{verbatim}
-% \begin{scope}[zone scope=name]
-% ...
-% \end{scope}
-% \end{verbatim}
-%
-% The \meta{name} will be the name of the scope. If this contains the
-% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone
-% will get a hex grid with numbering attached to it.
-%
-% If the \meta{name} contains the sub-string \texttt{turn} (any case),
-% then it is assumed to be a turn track and a rectangular grid will be
-% attached. The column and row separator will be set to \texttt{T},
-% so that it won't collide with the main zone. Similar if \meta{name}
-% contains \texttt{oob}, except the separator is set to \texttt{O}.
-%
-% If \meta{name} contains the sub-string \texttt{pool}, then it is
-% assumed to be a pool of counters, and \emph{no} grid is attached.
-%
-% For \texttt{zone path} applied to a \texttt{path}, what is recorded
-% is the path coordinates (as straight line segments) in the global
-% coordinate system.
-%
-% Both styles take one argument --- the name of the zone. If that
-% name contains the sub-string \texttt{hex} anywhere in the name, then
-% the zone is assumed to contain a hex grid. Otherwise, a rectangular
-% grid (of fixed size) will be applied to it.
-%
-% The environment \texttt{boardimage} also records the coordinate
-% options currently in use (keys \texttt{hex/first row is},
-% \texttt{hex/row direction is}, and so on), as well as the current
-% label option (as defined by \texttt{every hex} or \texttt{every hex
-% node}).
-%
-% All coordinates, and such are recorded in centimetres. It is worth
-% remembering that the Tikz coordinate system has the $y$ axis point
-% upward, while typical image software has the $y$ axis point down.
-% \texttt{pdftocairo} typically assumes a 150 PPI (pixels-per-inch)
-% resolution.
-%
-% That means that scaling factor becomes
-%
-% $$\frac{150\mathrm{pixel}}{2.54\mathrm{cm}}=59.055\frac{\mathrm{pixel}}{\mathrm{cm}}$$
-%
% \iffalse
-% Using definition in terms of printers feet - the one to use!
-%
-% PNG: 1674 x 1101
-% PDF: "lower left": [-0.02107,-0.02107],
-% "upper right": [28.31705,18.60843]
-% Width: 28.31705+0.02107 = 28.33812
-% Height: 18.60843+0.02107 = 18.62950
-% Pixel / cm: 1674 / 28.33812 = 59.07237318495369488166
-% 1101 / 18.62950 = 59.09981480984460130438
-% Average = 59.08609399739914809302
-%
-% Calculated = 150 / 2.54 = 59.05511811023622047244
-%
-% Using 1/72.27
-%
-% "lower left": [-0.02109,-0.02109],
-% "upper right": [28.321,18.61102]
-% Width: 28.321+0.02109=28.34209
-% Height: 18.61102+0.02109=18.63211
-% Pixel / cm: 1674 / 28.34209 = 59.06409866033168337267
-% 1101 / 18.63211 = 59.09153606328000425072
-% Average = 59.07781736180584381169
+% EOF
% \fi
-%
-% The information extracted is written to the
-% \cs{jobname}\texttt{.json} file as a sub-object (with name given by
-% the first optional argument) of the image object. In that way, we
-% can later on easily get the information from our catalogue of
-% images.
-%
-% Note, the styles \texttt{zoned}, \texttt{zone scope}, and
-% \texttt{zone path} are defined in \texttt{wargame} to be dummies so
-% that one can have them in the definition of the board without
-% impact.
-%
-% Since we want to write all dimensions in centimetres, we need to be
-% able to convert \texttt{pt} dimensions to centimetres. We make two
-% macros to do that for us.
-%
-% The exact definition of 1pt is
-%
-% $$1\,\mathrm{pt} = \frac{249}{250}12"\frac{1}{864}=\frac{83}{6000}1"
-% = 0.03513\overline{6}$$
-%
-% \begin{macrocode}
-% 2.54 / 72.27 = .03514598035145980351
-% \def\pt@to@cm#1{\pgfmathparse{#1 * 0.0351460}}
-\def\pt@to@cm#1{\pgfmathparse{#1 * 0.0351367}}
-\def\ptpoint@to@cm#1#2{%
- \pt@to@cm{#1}\edef\x{\pgfmathresult}%
- \pt@to@cm{#2}\edef\y{\pgfmathresult}}
-% \end{macrocode}
-% \begin{macrocode}
-\def\mk@get@anchor#1#2{%
- \pgfpointanchor{#1}{#2}%
- \pgfgetlastxy\tmp@x\tmp@y%
- \pt@to@cm{\tmp@x}\edef\tmp@x{\pgfmathresult}
- \pt@to@cm{\tmp@y}\edef\tmp@y{\pgfmathresult}
-}
-\def\mk@get@global@anchor#1#2{%
- \pgfpointanchor{#1}{#2}%
- \pgfgetlastxy\tmp@x\tmp@y%
- \pgfpointtransformed{\pgfpoint{\tmp@x}{\tmp@y}}
- \pgf@xa=\pgf@x
- \pgf@ya=\pgf@y
- \pt@to@cm{\the\pgf@xa}\edef\tmp@x{\pgfmathresult}
- \pt@to@cm{\the\pgf@ya}\edef\tmp@y{\pgfmathresult}
-}
-\def\get@bb#1{%
- % \pgfpointanchor{#1}{south west}%
- % \pgfgetlastxy\tmp@llx\tmp@lly%
- % \pgfpointanchor{#1}{north east}%
- % \pgfgetlastxy\tmp@urx\tmp@ury%
- % \pt@to@cm{\tmp@llx}\edef\llx{\pgfmathresult}
- % \pt@to@cm{\tmp@lly}\edef\lly{\pgfmathresult}
- % \pt@to@cm{\tmp@urx}\edef\urx{\pgfmathresult}
- % \pt@to@cm{\tmp@ury}\edef\ury{\pgfmathresult}
- \mk@get@anchor{#1}{south west}
- \edef\llx{\tmp@x}
- \edef\lly{\tmp@y}
- \mk@get@anchor{#1}{north east}
- \edef\urx{\tmp@x}
- \edef\ury{\tmp@y}
-}
-% \end{macrocode}
-% \begin{macrocode}
-\def\mk@transform{%
- \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy}
- \pt@to@cm{\ptdx}\edef\dx{\pgfmathresult}
- \pt@to@cm{\ptdy}\edef\dy{\pgfmathresult}
- \mk@w{ \mk@i "xx": \mxx,}
- \mk@w{ \mk@i "xy": \mxy,}
- \mk@w{ \mk@i "yx": \myx,}
- \mk@w{ \mk@i "yy": \myy,}
- \mk@w{ \mk@i "dx": \dx,}
- \mk@w{ \mk@i "dy": \dy,}
-}
-% \end{macrocode}
-% \begin{macrocode}
-\def\mk@bb#1{%
- \get@bb{#1}
- \mk@w{ \mk@i "lower left": [\llx,\lly],}
- \mk@w{ \mk@i "upper right": [\urx,\ury],}
- \begingroup
- % \pgftransforminvert
- % \pgfpointanchor{#1}{south west}%
- % \pgfgetlastxy\tmp@llx\tmp@lly%
- % \pgfpointtransformed{\pgfpoint{\tmp@llx}{\tmp@lly}}
- % \pgf@xa=\pgf@x
- % \pgf@ya=\pgf@y
- % %
- % \pgfpointanchor{#1}{north east}%
- % \pgfgetlastxy\tmp@urx\tmp@ury%
- % \pgfgetlastxy\tmp@llx\tmp@lly%
- % \pgfpointtransformed{\pgfpoint{\tmp@urx}{\tmp@ury}}
- % \pgf@xb=\pgf@x
- % \pgf@yb=\pgf@y
- % \pt@to@cm{\the\pgf@xa}\edef\llx{\pgfmathresult}
- % \pt@to@cm{\the\pgf@ya}\edef\lly{\pgfmathresult}
- % \pt@to@cm{\the\pgf@xb}\edef\urx{\pgfmathresult}
- % \pt@to@cm{\the\pgf@yb}\edef\ury{\pgfmathresult}x
- \mk@get@global@anchor{#1}{south west}
- \mk@w{ \mk@i "global lower left": [\tmp@x,\tmp@y],}
- \mk@get@global@anchor{#1}{north east}
- \mk@w{ \mk@i "global upper right": [\tmp@x,\tmp@y]}
- \endgroup
-}
-\def\mk@pos#1(#2){%
- \hex@dbg{10}{^^JMarking `#2' with `#1' - start}
- \coordinate[transform shape] (tmp) at (#2) {};
- \mk@get@anchor{tmp}{center}
- \hex@dbg{3}{^^JMarking `#2' with `#1' - `\tmp@x',\tmp@y'}
- \tikzset{zone point={#1}{\tmp@x}{\tmp@y}}
-}
-% \end{macrocode}
-%
-% For the key \texttt{zone path} to work, we need to be able to record
-% the path as it moves along. To that end, we make a custom
-% decoration that will do that for us, and, once the path is finished,
-% write the path to our JSON file.
-%
-% \begin{macrocode}
-\pgfdeclaredecoration{record path construction}{initial}{%
- \state{initial}[width=0pt,next state=more]{
- \begingroup
- \pgf@decorate@inputsegment@first
- \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
- \xdef\wg@path{[\x,\y]}
- \endgroup
- }%
- \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{%
- \begingroup
- \pgf@decorate@inputsegment@last
- \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
- \xdef\wg@path{\wg@path,[\x,\y]}
- \endgroup
- }
- \state{final}{%
- \begingroup
- \pgf@decorate@inputsegment@last
- \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
- \xdef\wg@path{\wg@path,[\x,\y]}
- \endgroup
- \mk@w{ \mk@i "zone path \wg@record@path@name": \@lbchar}
- \mk@w{ \mk@i\space "path": [\wg@path] \@rbchar,}
- }
-}%
-% \end{macrocode}
-%
-% Now we can make our environment
-%
-% The first thing we do is to use the \cs{info} macro to mark the
-% image. Then we open our JSON file. We make a short-hand macro for
-% writing to that file. The macro \cs{bd@i} records the current
-% indention (which is important in JSON)
-%
-% \begin{macrocode}
-\newenvironment{boardimage}[3][board]{%
- \def\bd@n{#2}
- \newcount\mk@point
- \mk@point=0
- \let\oomk@i\mk@i%
- \let\markpos\mk@pos%
-% \end{macrocode}
-%
-%
-% Then, to extract the label option, we make a dummy \texttt{node}
-% with the styles \texttt{every hex} and \texttt{every hex node}, so
-% we can extract that option.
-%
-% \begin{macrocode}
- \info{dummy}{<<dummy>>}{}%
- %\tikz{}%
- \tikz{\scoped[every hex/.try,every hex node/.try]{%
- \node[inner sep=0,outer sep=0]{%
- \global\let\mk@label\hex@label}}}%
-% \end{macrocode}
-%
-% The next thing we do is to make an object. The first things we put
-% in are the units used (``cm''), and the grid options.
-%
-% \begin{macrocode}
- \info*{#2}{#1}{#3}%
- \mk@w{ \mk@i "zones": \@lbchar}%
- \edef\mk@i{\mk@i\space}
- %% Everything is made into centimeters
- \mk@w{ \mk@i "units": "cm",}
- \@ifundefined{mk@label}{}{\mk@w{ \mk@i "labels": "\mk@label",}}
- %% Write out coordinate options as "coords" object
- \mk@w{ \mk@i"coords": \@lbchar}%
- \mk@w{ \mk@i "row": \@lbchar}%
- \mk@w{ \mk@i\space "offset": \hex@coords@row@off,}%
- \mk@w{ \mk@i\space "factor": \hex@coords@row@fac \@rbchar,}%
- \mk@w{ \mk@i "column": \@lbchar}%
- \mk@w{ \mk@i\space "offset": \hex@coords@col@off,}%
- \mk@w{ \mk@i\space "factor": \hex@coords@col@fac,}%
- \mk@w{ \mk@i\space "top short": "\hex@top@short@col",}%
- \mk@w{ \mk@i\space "bottom short": "\hex@bot@short@col" \@rbchar}%
- \mk@w{ \mk@i\@rbchar,}%
-% \end{macrocode}
-%
-% We then monkey-patch \cs{boardframe} to also output coordinates to
-% our JSON file. Note that this will probably be embedded in a
-% different object.
-%
-% \begin{macrocode}
- %%
- \let\oldbo@rdframe\bo@rdframe%
- \def\bo@rdframe[##1](##2)(##3){%
- \oldbo@rdframe[##1](##2)(##3)%
- \mk@w{ \mk@i"board frame": \@lbchar}
- \mk@w{ \mk@i\space "lower left": [\llx,\lly],}
- \mk@w{ \mk@i\space "upper right": [\urx,\ury],}
- \mk@w{ \mk@i\space "margin": \margin,}
- \mk@w{ \mk@i\space "width": \w,}
- \mk@w{ \mk@i\space "height": \h \@rbchar,}}%
-% \end{macrocode}
-%
-% Next, we make the style \texttt{zoned} to be applied to the
-% \texttt{tikzpicture} environment. This records the bounding box of
-% the full picture.
-%
-% \begin{macrocode}
- \tikzset{
- zoned/.code={% Apply to whole picture
- \pgfkeys{%
- % This needs to be done in the picture!
- /tikz/execute at end picture={%
- \mk@w{ \mk@i "zoned": \@lbchar}
- \mk@transform%
- \mk@bb{current bounding box}
- \mk@w{ \mk@i \@rbchar,}
- }
- }
- },
-% \end{macrocode}
-%
-% The next style is the \texttt{zone scope}. At the start of the
-% scope we record the current transformation matrix. Then we install
-% a handler to extract the bounding box at the end of the scope. Note
-% that we increase indention here.
-%
-% \begin{macrocode}
- zone scope/.code={%
- \mk@w{ \mk@i"zone scope ##1": \@lbchar}
- \let\omk@i\mk@i
- \edef\mk@i{\mk@i\space}
- \mk@transform%
- %\bd@w{ \@rbchar,}
- \gdef\wg@export@box{##1}%
- \pgfkeys{%
- /tikz/local bounding box=wg export box,
- /tikz/execute at end scope={
- \mk@bb{wg export box}
- \let\mk@i\omk@i
- \mk@w{ \mk@i\@rbchar,}},
- } % pgfkeys
- }, % zone scope
-% \end{macrocode}
-% The next style gets the global coordinates of the current (0,0)
-% point - f.ex. in a node - and outputs that
-% \begin{macrocode}
- zone point/.code n args={3}{
- \pgf@xa=##2 cm
- \pgf@ya=##3 cm
- \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}}
- % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
- \pgf@xa=\pgf@x
- \pgf@ya=\pgf@y
- \pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult}
- \pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult}
- \advance\mk@point1
- \global\mk@point=\mk@point
- \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py]
- \@rbchar, }
- %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py}
- },
- zone oob point/.code n args={3}{
- \pgf@xa=##2 cm
- \pgf@ya=##3 cm
- \advance\pgf@xa.1cm
- \advance\pgf@ya.1cm
- \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}}
- % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
- \pgf@xa=\pgf@x
- \pgf@ya=\pgf@y
- \pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult}
- \pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult}
- \advance\mk@point1
- \global\mk@point=\mk@point
- \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py]
- \@rbchar, }
- %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py}
- },
- zone global point/.code n args={3}{
- \advance\mk@point1
- \global\mk@point=\mk@point
- \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py]
- \@rbchar, }
- },
-% \end{macrocode}
-%
-% The \texttt{zone path} style is a bit more simple, but only because
-% the bulk of the work is done in a decoration. We need to be able to
-% pass a name to that decoration, s we make a key for that. The user
-% need not think about that though.
-%
-% \begin{macrocode}
- /pgf/decoration/record path name/.store in=\wg@record@path@name,
- zone path/.style={%
- postaction={decorate,decoration={
- record path construction,
- record path name=##1}}
- } % zone path
- }% tikzset
-}
-% \end{macrocode}
-%
-% That finishes the first part of the environment. At the end of the
-% environment, we simple write the name of the picture, and close our
-% JSON output.
-%
-% \begin{macrocode}
-{%
- \mk@w{ \mk@i "name": "\bd@n" }%
- \let\mk@i\oomk@i%
- \mk@w{ \mk@i\@rbchar}%
- \end@info%
-}
-% \end{macrocode}
-%
-%
-% TO BE DONE: We could add hooks to both the \texttt{hex} and
-% \texttt{chit} shapes that would allow us to write out the settings
-% for each of these. This would allow us to make data files that
-% contain the information available in the \LaTeX{} code. For
-% example, we could write a counters
-%
-% \begin{itemize}
-% \item Left and right identifiers
-% \item Upper left, upper right, lower left, and lower right
-% identifiers. (some care must be taken if these contains graphics
-% and not just text.)
-% \item Factors
-% \item NATO symbol
-% \begin{itemize}
-% \item Faction, command, echelon
-% \item Mains
-% \item Left, right, top, and bottom attributes and modifiers
-% \item Below attribute
-% \end{itemize}
-% \end{itemize}
-%
-% If one then assumed that for example the upper left corner holds the
-% start-up hex, then one could use that information.
-%
-% The code below exports the chit information to the JSON file. Not
-% sure how to use it though.
-%
-% \begin{macrocode}
-\tikzset{
- zone turn/.store in=\zone@turn,
- zone mult/.store in=\zone@mult
-}
-\def\do@chit@report{%
- \mk@w{ \mk@i "chit": \@lbchar}
- \@ifundefined{id}{} {\mk@w{ \mk@i\space "id": "\id", }}%
- \@ifundefined{chit@symbol}{} {\mk@w{ \mk@i\space "symbol": "true", }}%
- \@ifundefined{chit@full}{} {\mk@w{ \mk@i\space "full": "\chit@full", }}
- \@ifundefined{chit@factors}{} {\mk@w{ \mk@i\space "factors": "\chit@factors", }}%
- \@ifundefined{chit@left}{} {\mk@w{ \mk@i\space "left": "\chit@left", }}%
- \@ifundefined{chit@right}{} {\mk@w{ \mk@i\space "right": "\chit@right", }}%
- \@ifundefined{chit@upper@left}{} {\mk@w{ \mk@i\space "upper left": "\chit@upper@left", }}%
- \@ifundefined{chit@lower@left}{} {\mk@w{ \mk@i\space "lower left": "\chit@lower@left", }}%
- \@ifundefined{chit@upper@right}{}{\mk@w{ \mk@i\space "upper right": "\chit@upper@right", }}%
- \@ifundefined{chit@lower@right}{}{\mk@w{ \mk@i\space "lower right": "\chit@lower@right}", }%
- \mk@w{ \mk@i\space "end": 0}
- \@ifundefined{chit@symbol}{
- \mk@w{ \mk@i \@rbchar }
- }{
- \mk@w{ \mk@i \@rbchar, }% NATOAPP6c will follow
- }%
-}
-\def\do@natoapp@report{%
- \mk@w{ \mk@i "natoapp6c": \@lbchar}
- \@ifundefined{id}{}{\mk@w{ \mk@i\space "id": "\id", }}
- \@ifundefined{natoapp@fac}{}{\mk@w{ \mk@i\space "faction": "\natoapp@fac", }}
- \@ifundefined{natoapp@cmd}{}{\mk@w{ \mk@i\space "command": "\natoapp@cmd", }}
- \@ifundefined{natoapp@ech}{}{\mk@w{ \mk@i\space "echelon": "\natoapp@ech", }}
- \@ifundefined{natoapp@main}{}{\mk@w{ \mk@i\space "main": "\natoapp@main", }}
- \@ifundefined{natoapp@left}{}{\mk@w{ \mk@i\space "left": "\natoapp@left", }}
- \@ifundefined{natoapp@right}{}{\mk@w{ \mk@i\space "right": "\natoapp@right", }}
- \@ifundefined{natoapp@upper}{}{\mk@w{ \mk@i\space "upper": "\natoapp@upper", }}
- \@ifundefined{natoapp@lower}{}{\mk@w{ \mk@i\space "lower": "\natoapp@lower", }}
- \@ifundefined{natoapp@below}{}{\mk@w{ \mk@i\space "below": "\natoapp@below", }}
- \mk@w{ \mk@i\space "end": 0}
- \mk@w{ \mk@i \@rbchar}
-}
-% \end{macrocode}
-% \iffalse
-%</exportcls>
-%\fi
-%
+
diff --git a/macros/latex/contrib/wargame/source/util/export.dtx b/macros/latex/contrib/wargame/source/util/export.dtx
new file mode 100644
index 0000000000..fde067dfe8
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/export.dtx
@@ -0,0 +1,817 @@
+% \iffalse
+% ====================================================================
+% \fi
+%
+% \subsection{The \texttt{wgexport} class}
+% \label{sec:impl:util}
+%
+% This document class is used for exporting game component to be used
+% in a VASSAL module
+% libraries.
+%
+% \iffalse
+%<*exportcls>
+%\fi
+%
+% Class identification and load \texttt{wargame} package
+%
+% \begin{macrocode}
+\ProvidesClass{wgexport}
+\PassOptionsToClass{multi=tikzpicture,varwidth=false}{standalone}
+\DeclareOption{noterrainpic}{%
+ \PassOptionsToPackage{\CurrentOption}{wargame}}
+\DeclareOption{terrainpic}{%
+ \PassOptionsToPackage{\CurrentOption}{wargame}}
+\DeclareOption*{%
+ \PassOptionsToClass{\CurrentOption}{standalone}}
+\ProcessOptions\relax
+\LoadClass{standalone}
+\RequirePackage{wargame}
+% \end{macrocode}
+%
+% We need a few utilities before we get to the actual environment.
+% First, we need a tools to write out literal left and right curly
+% braces. We do a bit of catcode hackery to accomplish that.
+%
+% \begin{macrocode}
+\begingroup
+\catcode`\^^I=12
+\def\@tabchar{^^I}
+\catcode`<=1 \catcode`>=2
+\catcode`{=12 \catcode`}=12
+\gdef\@lbchar<{>
+\gdef\@rbchar<}>
+\endgroup
+% \end{macrocode}
+%
+% Above, we temporarily set the tab, and left and right curly brace
+% characters to be regular letters (12), and the catcodes of less than
+% and greater than to be those of left and right curly braces
+% respectively. We then define the macros \cs{@tabchar},
+% \cs{@lbchar}, and \cs{@rbchar} to produce literal characters.
+% \LaTeX already has \cs{@percentchar}.
+%
+% Everything we do should go inside this environment. The single
+% optional argument is the file name stem of the output JSON file.
+%
+% \begin{macrocode}
+\newenvironment{imagelist}[1][\jobname]{%
+ \newwrite\mk@out%
+ \def\mk@i{}%
+ \def\mk@w{\immediate\write\mk@out}%
+ \immediate\openout\mk@out=#1.json
+ \mk@w{[}
+}{
+ \mk@w{\mk@i \@lbchar "name":"End of list", "category": "<<eol>>",
+ "subcategory": "" \@rbchar }
+ \mk@w{]}
+ \immediate\closeout\mk@out
+}
+% \end{macrocode}
+%
+%
+% Preceed all images (\textsf{tikzpicture}) with this command
+%
+% First argument is the name of the image. This can be anything.
+% Note that for counters, if the name ends in \texttt{flipped} then it
+% is considered the backside of a counter.
+%
+% Second argument is the type of image. Recognised types are
+%
+% \begin{itemize}
+% \item \texttt{board} for boards
+% \item \texttt{oob} for OOBs
+% \item \texttt{chart} for charts
+% \item \texttt{counter} for counters
+% \item \texttt{front} for front page
+% \end{itemize}
+%
+% Other types can be used, and the images will be exported, but the
+% Python script pays no particular attention to those then. Use for
+% example to prepare images for help or the like.
+%
+% The third argument is the sub type. This is most relevant for the
+% counters. Sub types can be anything, but since the counters will
+% receive different prototypes based on the sub type, it makes sense
+% to divide into sub types a la
+%
+% \begin{itemize}
+% \item factions
+% \item common markers
+% \end{itemize}
+%
+% The faction sub types should just be the name of the faction.
+% E.g., Allies, Axis, Soviet, NATO, Warsaw Pact. Spaces should not
+% matter.
+%
+% For common markers, there are a few names that are recognised
+% specifically by the Python script. These are
+%
+% \begin{itemize}
+% \item \texttt{common}
+% \item \texttt{all}
+% \item \texttt{marker}
+% \item \texttt{markers}
+% \end{itemize}
+%
+% Counters that has these sub-types will no be considered to belong
+% to any faction.
+%
+% Note that the Python script uses the faction names to guess the
+% players of the game, and uses them in several places.
+%
+%
+% \begin{macrocode}
+\def\info{%
+ \@ifstar{\@@info{,}}{\@@info{\@rbchar,}}}
+\def\@@info#1#2#3#4{%
+ \chit@dbg{2}{Making image `#2' of type `#3'/`#4' on page \thepage}%
+ \mk@w{ \@lbchar}%
+ \mk@w{ \space "name": "#2",}%
+ \mk@w{ \space "category": "#3",}%
+ \mk@w{ \space "subcategory": "#4", }%
+ \mk@w{ \space "number": \thepage #1}%
+ \let\oldmk@i\mk@i%
+ \ifx#1,\relax\edef\mk@i{\mk@i\space\space}\fi}
+\def\end@info{%
+ \let\mk@i\oldmk@i%
+ \mk@w{ \space \@rbchar,}}
+% \end{macrocode}
+%
+% Make separate images for each counter (single sided).
+%
+% \begin{macrocode}
+\newcommand\chitimages[2][]{%
+ \begingroup%
+ \let\chit@report\do@chit@report%
+ \let\natoapp@report\do@natoapp@report%
+ \chit@dbg{2}{chits to make images of `#2'}%
+ \foreach[count=\ti from 0] \t/\x in #2{%
+ \chit@dbg{2}{^^JRow: `\t' (`\x')}
+ \ifx\t\empty\else% Ignore empty rows
+ \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')}
+ % Take sub-category or default
+ \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
+ \foreach \u/\m in \t{%
+ \ifx\u\empty\else% Ignore empty cells
+ \ifx\u\chit@blank\else%
+ \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+ \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
+ \ifx\u\m\def\m{1}\fi% If the same as unit
+ \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
+ %% We only make one copy of the chit, since we can duplicate
+ %% it in VASSAL
+ \info*{\u}{counter}{\x}
+ \begin{tikzpicture}
+ \chit[\u=\ti]%
+ \end{tikzpicture}
+ \end@info%
+ %% \foreach \n in {1,...,\m}{% Make a number of copies
+ %% \ifx\u\chit@blank%
+ %% \chit@dbg{3}{Ignoring blank chit:\u}%
+ %% \else%
+ %% \info{\u}{counter}{#2}
+ %% \begin{tikzpicture}
+ %% \chit[\u=\ti](\c,\r)%
+ %% \end{tikzpicture}
+ %% \fi%
+ %% }%
+ \fi%
+ \fi%
+ }%
+ \chit@dbg{2}{End of inner loop}%
+ \fi%
+ }%
+ \chit@dbg{2}{End of outer loop}%
+ \endgroup%
+}
+% \end{macrocode}
+%
+% Make separate images for each counter (double sided). The back-side
+% counters must be defined by append `\texttt{ flipped}' the front
+% face name
+%
+% \begin{macrocode}
+\newcommand\doublechitimages[2][]{%
+ \begingroup%
+ \let\chit@report\do@chit@report%
+ \let\natoapp@report\do@natoapp@report%
+ \foreach[count=\ti from 0] \t/\x in #2{%
+ \ifx\t\empty\else% Ignore empty rows
+ \chit@dbg{5}{^^JSubcategory: `\x' (default `#1')}
+ % Take sub-category or default
+ \ifx\t\x\def\x{#1}\else\ifx\x\empty\def\x{#1}\fi\fi
+ \foreach \u/\m in \t{%
+ \ifx\u\empty\else% Ignore empty cells
+ \ifx\u\chit@blank\else%
+ \chit@dbg{2}{Next chit `\u' with possible multiplicity `\m'}%
+ \ifx\m\@empty\def\m{1}\fi% If not multiplicity defined
+ \ifx\u\m\def\m{1}\fi% If the same as unit
+ \chit@dbg{2}{Next chit `\u' multiplicity `\m'}%
+ %% Flipped chit
+ \edef\s{\u\space flipped}%
+ %% We only make one copy of the chit, since we can duplicate
+ %% it in VASSAL
+ \info*{\u}{counter}{\x}%
+ \begin{tikzpicture}%
+ \chit[\u=\ti]%
+ \end{tikzpicture}%
+ \end@info%
+ \info*{\s}{counter}{\x}%
+ \begin{tikzpicture}%
+ \chit[\s=\ti]%
+ \end{tikzpicture}%
+ \end@info%
+ %% \foreach \n in {1,...,\m}{% Make a number of copies
+ %% \ifx\u\chit@blank%
+ %% \chit@dbg{3}{Ignoring blank chit:\u}%
+ %% \else%
+ %% \info{\u}{counter}{#2}
+ %% \begin{tikzpicture}
+ %% \chit[\u=\ti](\c,\r)%
+ %% \end{tikzpicture}
+ %% \fi%
+ %% }%
+ \fi%
+ \fi%
+ }%
+ \fi%
+ }%
+ \endgroup%
+}
+% \end{macrocode}
+%
+% Special for boards, we have the environment \textsf{boardimage}.
+% Like \cs{info} we must specify the name and sub-category of the
+% board, but the category is assumed to be \texttt{board} (though the
+% optional argument can specify a different category).
+%
+% Within this environment some specific styles are defined that allows
+% the user to specify VASSAL zones on the board. For this to work
+% properly, the parent \textsf{tikzpicture} \emph{must} have the style
+% \texttt{zoned}. This style will record the bounding box of the
+% picture which we will need to calculate VASSAL coordinates later
+% on.
+%
+% Other styles are \texttt{zone scope}, to be applied to
+% \texttt{scope}s in the picture, and \texttt{zone path} to be applied
+% to \texttt{path}s (or \cs{draw}, \cs{fill}, or the like) in the
+% picture. These will record coordinates of these elements in side
+% the picture. The Python script will then define VASSAL zones based
+% on these coordinates.
+%
+% For \texttt{zone scope} applied to a \texttt{scope}, what is
+% recorded are
+%
+% \begin{itemize}
+% \item The current coordinate transformation matrix
+% \item The current translation
+% \item The bounding box, within the current transformation and
+% translation.
+% \end{itemize}
+%
+% To define a zone in the board, simply enclose it in a
+%
+% \begin{verbatim}
+% \begin{scope}[zone scope=name]
+% ...
+% \end{scope}
+% \end{verbatim}
+%
+% The \meta{name} will be the name of the scope. If this contains the
+% sub-string \texttt{hex} (upper, lower, or mixed case), then the zone
+% will get a hex grid with numbering attached to it.
+%
+% If the \meta{name} contains the sub-string \texttt{turn} (any case),
+% then it is assumed to be a turn track and a rectangular grid will be
+% attached. The column and row separator will be set to \texttt{T},
+% so that it won't collide with the main zone. Similar if \meta{name}
+% contains \texttt{oob}, except the separator is set to \texttt{O}.
+%
+% If \meta{name} contains the sub-string \texttt{pool}, then it is
+% assumed to be a pool of counters, and \emph{no} grid is attached.
+%
+% For \texttt{zone path} applied to a \texttt{path}, what is recorded
+% is the path coordinates (as straight line segments) in the global
+% coordinate system.
+%
+% Both styles take one argument --- the name of the zone. If that
+% name contains the sub-string \texttt{hex} anywhere in the name, then
+% the zone is assumed to contain a hex grid. Otherwise, a rectangular
+% grid (of fixed size) will be applied to it.
+%
+% The environment \texttt{boardimage} also records the coordinate
+% options currently in use (keys \texttt{hex/first row is},
+% \texttt{hex/row direction is}, and so on), as well as the current
+% label option (as defined by \texttt{every hex} or \texttt{every hex
+% node}).
+%
+%
+% The information extracted is written to the
+% \cs{jobname}\texttt{.json} file as a sub-object (with name given by
+% the first optional argument) of the image object. In that way, we
+% can later on easily get the information from our catalogue of
+% images.
+%
+% Note, the styles \texttt{zoned}, \texttt{zone scope}, and
+% \texttt{zone path} are defined in \texttt{wargame} to be dummies so
+% that one can have them in the definition of the board without
+% impact.
+% \begin{macrocode}
+\def\mk@transform{%
+ \pgfgettransformentries{\mxx}{\mxy}{\myx}{\myy}{\ptdx}{\ptdy}
+ \wg@pt@to@cm{\ptdx}\edef\dx{\pgfmathresult}
+ \wg@pt@to@cm{\ptdy}\edef\dy{\pgfmathresult}
+ \mk@w{ \mk@i "xx": \mxx,}
+ \mk@w{ \mk@i "xy": \mxy,}
+ \mk@w{ \mk@i "yx": \myx,}
+ \mk@w{ \mk@i "yy": \myy,}
+ \mk@w{ \mk@i "dx": \dx,}
+ \mk@w{ \mk@i "dy": \dy,}
+}
+% \end{macrocode}
+% \begin{macrocode}
+\def\mk@bb#1{%
+ \wg@get@bb{#1}
+ \mk@w{ \mk@i "lower left": [\llx,\lly],}
+ \mk@w{ \mk@i "upper right": [\urx,\ury],}
+ \begingroup
+ \wg@get@global@nchor{#1}{south west}
+ \mk@w{ \mk@i "global lower left": [\tmp@x,\tmp@y],}
+ \wg@get@global@nchor{#1}{north east}
+ \mk@w{ \mk@i "global upper right": [\tmp@x,\tmp@y]}
+ \endgroup
+}
+\def\mk@pos#1(#2){%
+ \wg@dbg{10}{^^JMarking `#2' with `#1' - start}
+ \coordinate[transform shape] (tmp) at (#2) {};
+ \wg@get@nchor{tmp}{center}
+ \wg@dbg{3}{^^JMarking `#2' with `#1' - `\tmp@x',\tmp@y'}
+ \tikzset{zone point={#1}{\tmp@x}{\tmp@y}}
+}
+% \end{macrocode}
+%
+% For the key \texttt{zone path} to work, we need to be able to record
+% the path as it moves along. To that end, we make a custom
+% decoration that will do that for us, and, once the path is finished,
+% write the path to our JSON file.
+%
+% \begin{macrocode}
+\pgfdeclaredecoration{record path construction}{initial}{%
+ \state{initial}[width=0pt,next state=more]{
+ \begingroup
+ \pgf@decorate@inputsegment@first
+ \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
+ \xdef\wg@path{[\x,\y]}
+ \endgroup
+ }%
+ \state{more}[width=\pgfdecoratedinputsegmentremainingdistance]{%
+ \begingroup
+ \pgf@decorate@inputsegment@last
+ \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
+ \xdef\wg@path{\wg@path,[\x,\y]}
+ \endgroup
+ }
+ \state{final}{%
+ \begingroup
+ \pgf@decorate@inputsegment@last
+ \ptpoint@to@cm{\the\pgf@x}{\the\pgf@y}
+ \xdef\wg@path{\wg@path,[\x,\y]}
+ \endgroup
+ \mk@w{ \mk@i "zone path \wg@record@path@name": \@lbchar}
+ \mk@w{ \mk@i\space "path": [\wg@path] \@rbchar,}
+ }
+}%
+% \end{macrocode}
+%
+% Now we can make our environment
+%
+% The first thing we do is to use the \cs{info} macro to mark the
+% image. Then we open our JSON file. We make a short-hand macro for
+% writing to that file. The macro \cs{bd@i} records the current
+% indention (which is important in JSON)
+%
+% \begin{macrocode}
+\newenvironment{boardimage}[3][board]{%
+ \def\bd@n{#2}
+ \newcount\mk@point
+ \mk@point=0
+ \let\oomk@i\mk@i%
+ \let\markpos\mk@pos%
+% \end{macrocode}
+%
+%
+% Then, to extract the label option, we make a dummy \texttt{node}
+% with the styles \texttt{every hex} and \texttt{every hex node}, so
+% we can extract that option.
+%
+% \begin{macrocode}
+ \info{dummy}{<<dummy>>}{}%
+ %\tikz{}%
+ \tikz{\scoped[%
+ every hex/.try,every hex node/.try,
+ ]{%
+ \def\hex@col{0}%
+ \def\hex@row{0}%
+ \node[hex,inner sep=0,outer sep=0]{%
+ \message{^^JHex label: `\meaning\hex@label'}%
+ \global\let\mk@label\hex@label}}}%
+% \end{macrocode}
+%
+% The next thing we do is to make an object. The first things we put
+% in are the units used (``cm''), and the grid options.
+%
+% \begin{macrocode}
+ \info*{#2}{#1}{#3}%
+ \mk@w{ \mk@i "zones": \@lbchar}%
+ \edef\mk@i{\mk@i\space}
+ %% Everything is made into centimeters
+ \mk@w{ \mk@i "units": "cm",}
+ \hex@dbg{0}{Label: `\meaning\mk@label'}
+ \@ifundefined{mk@label}{}{\mk@w{ \mk@i "labels": "\mk@label",}}
+ %% Write out coordinate options as "coords" object
+ \mk@w{ \mk@i"coords": \@lbchar}%
+ \mk@w{ \mk@i "row": \@lbchar}%
+ \mk@w{ \mk@i\space "offset": \hex@coords@row@off,}%
+ \mk@w{ \mk@i\space "factor": \hex@coords@row@fac \@rbchar,}%
+ \mk@w{ \mk@i "column": \@lbchar}%
+ \mk@w{ \mk@i\space "offset": \hex@coords@col@off,}%
+ \mk@w{ \mk@i\space "factor": \hex@coords@col@fac,}%
+ \mk@w{ \mk@i\space "top short": "\hex@top@short@col",}%
+ \mk@w{ \mk@i\space "bottom short": "\hex@bot@short@col" \@rbchar}%
+ \mk@w{ \mk@i\@rbchar,}%
+% \end{macrocode}
+%
+% We then monkey-patch \cs{boardframe} to also output coordinates to
+% our JSON file. Note that this will probably be embedded in a
+% different object.
+%
+% \begin{macrocode}
+ %%
+ \let\oldbo@rdframe\bo@rdframe%
+ \def\bo@rdframe[##1](##2)(##3){%
+ \oldbo@rdframe[##1](##2)(##3)%
+ \mk@w{ \mk@i"board frame": \@lbchar}
+ \mk@w{ \mk@i\space "lower left": [\llx,\lly],}
+ \mk@w{ \mk@i\space "upper right": [\urx,\ury],}
+ \mk@w{ \mk@i\space "margin": \margin,}
+ \mk@w{ \mk@i\space "width": \w,}
+ \mk@w{ \mk@i\space "height": \h \@rbchar,}}%
+% \end{macrocode}
+%
+% Next, we make the style \texttt{zoned} to be applied to the
+% \texttt{tikzpicture} environment. This records the bounding box of
+% the full picture.
+%
+% \begin{macrocode}
+ \tikzset{
+ zoned/.code={% Apply to whole picture
+ \pgfkeys{%
+ % This needs to be done in the picture!
+ /tikz/execute at end picture={%
+ \mk@w{ \mk@i "zoned": \@lbchar}
+ \mk@transform%
+ \mk@bb{current bounding box}
+ \mk@w{ \mk@i \@rbchar,}
+ }
+ }
+ },
+% \end{macrocode}
+%
+% The next style is the \texttt{zone scope}. At the start of the
+% scope we record the current transformation matrix. Then we install
+% a handler to extract the bounding box at the end of the scope. Note
+% that we increase indention here.
+%
+% \begin{macrocode}
+ zone scope/.code={%
+ \mk@w{ \mk@i"zone scope ##1": \@lbchar}
+ \let\omk@i\mk@i
+ \edef\mk@i{\mk@i\space}
+ \mk@transform%
+ %\bd@w{ \@rbchar,}
+ \gdef\wg@export@box{##1}%
+ \pgfkeys{%
+ /tikz/local bounding box=wg export box,
+ /tikz/execute at end scope={
+ \mk@bb{wg export box}
+ \let\mk@i\omk@i
+ \mk@w{ \mk@i\@rbchar,}},
+ } % pgfkeys
+ }, % zone scope
+% \end{macrocode}
+% The next style gets the global coordinates of the current (0,0)
+% point - f.ex. in a node - and outputs that
+% \begin{macrocode}
+ zone point/.code n args={3}{
+ \pgf@xa=##2 cm
+ \pgf@ya=##3 cm
+ \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}}
+ % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
+ \pgf@xa=\pgf@x
+ \pgf@ya=\pgf@y
+ \wg@pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult}
+ \wg@pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult}
+ \advance\mk@point1
+ \global\mk@point=\mk@point
+ \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py]
+ \@rbchar, }
+ %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py}
+ },
+ zone oob point/.code n args={3}{
+ \pgf@xa=##2 cm
+ \pgf@ya=##3 cm
+ \advance\pgf@xa.1cm
+ \advance\pgf@ya.1cm
+ \pgfpointtransformed{\pgfpoint{\pgf@xa}{\pgf@ya}}
+ % \pgfpointtransformed{\pgfpoint{0pt}{0pt}}
+ \pgf@xa=\pgf@x
+ \pgf@ya=\pgf@y
+ \wg@pt@to@cm{\the\pgf@xa}\edef\px{\pgfmathresult}
+ \wg@pt@to@cm{\the\pgf@ya}\edef\py{\pgfmathresult}
+ \advance\mk@point1
+ \global\mk@point=\mk@point
+ \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1",
+ "parent": "\wg@export@box", "type": "point", "coords": [\px,\py]
+ \@rbchar, }
+ %\message{^^JZone point \the\mk@point\space ##1: ##2,##3 -> \px,\py}
+ },
+ zone global point/.code n args={3}{
+ \advance\mk@point1
+ \global\mk@point=\mk@point
+ \mk@w{ \mk@i "point\the\mk@point": \@lbchar "name": "##1", "type": "point", "coords": [\px,\py]
+ \@rbchar, }
+ },
+% \end{macrocode}
+%
+% The \texttt{zone path} style is a bit more simple, but only because
+% the bulk of the work is done in a decoration. We need to be able to
+% pass a name to that decoration, s we make a key for that. The user
+% need not think about that though.
+%
+% \begin{macrocode}
+ /pgf/decoration/record path name/.store in=\wg@record@path@name,
+ zone path/.style={%
+ postaction={decorate,decoration={
+ record path construction,
+ record path name=##1}}
+ } % zone path
+ }% tikzset
+}
+% \end{macrocode}
+%
+% That finishes the first part of the environment. At the end of the
+% environment, we simple write the name of the picture, and close our
+% JSON output.
+%
+% \begin{macrocode}
+{%
+ \mk@w{ \mk@i "name": "\bd@n" }%
+ \let\mk@i\oomk@i%
+ \mk@w{ \mk@i\@rbchar}%
+ \end@info%
+}
+% \end{macrocode}
+%
+% Make battle markers. Mandatory argument is how many markers,
+% optional is the group to add the markers to.
+%
+% \begin{macrocode}
+\def\wg@gennumberm@rkers#1#2#3{
+ \message{^^JNumbered markers: Type=`#1' Max=`#2' Category=`#3'}
+ \def\markers{}
+ \def\keys{}
+ \foreach \i in {1,...,#2}{%
+ \xdef\keys{/tikz/#1 \i/.style={/tikz/#1=\i},\keys}
+ \xdef\markers{\markers,#1 \i}}
+ {%
+ \nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
+\tikzset{
+ wg hidden unit/.pic={},
+ wg hidden unit/.style={
+ chit={
+ frame={draw=none,fill=none},
+ full=wg hidden unit}}}
+\DeclareRobustCommand\battlemarkers[2][BattleMarkers]{%
+ \wg@gennumberm@rkers{battle marker}{#2}{#1}%
+ \message{^^JMake a hidden unit and add to Markers category}
+ {%
+ \nopagecolor%
+ \chitimages[Markers]{{wg hidden unit}}%
+ %
+ \info{battle-marker-icon}{icon}{}%
+ \tikz[scale=.7,transform shape]{\pic{battle marker=0};}%
+ \info{clear-battles-icon}{icon}{}
+ \tikz[scale=.4,transform shape]{%
+ \pic{eliminate icon};
+ \pic[scale=.7,transform shape] at (-.3,0) {battle marker=0};}%
+ }%
+}
+% \end{macrocode}
+%
+% Make odds markers. Mandatory argument is a list of odds and fill colours.
+% Optional is the group to add the markers to.
+%
+% \begin{macrocode}
+\def\wg@gencolorm@rkers#1#2#3{%
+ \def\markers{}
+ \def\keys{}
+ \foreach \o/\f in {#2}{%
+ \ifx\o\f\def\f{white}\fi%
+ \message{^^JOdds marker `#1 \o' w/fill `\f'}%
+ \xdef\keys{/tikz/#1 \o/.style={/tikz/#1={\o,\f}},\keys}
+ \xdef\markers{\markers,#1 \o}}
+ {\nopagecolor\pgfkeysalsofrom{\keys}\chitimages[#3]{\markers}}}%
+\DeclareRobustCommand\oddsmarkers[2][OddsMarkers]{%
+ \wg@gencolorm@rkers{odds marker}{#2}{#1}%
+ \info{odds-battles-icon}{icon}{}
+ \tikz[scale=.5,transform shape]{\pic{odds marker={?:?,white}}}
+ \info{resolve-battles-icon}{icon}{}
+ \tikz[scale=.3,transform shape]{%
+ \pic{dice};
+ \pic[scale=1.2,transform shape] at (-.2,-.2) {battle marker=0};}%
+}
+% \end{macrocode}
+%
+% Make results markers. Mandatory argument is a list of results and
+% fill colours. Optional is the group to add the markers to.
+%
+% \begin{macrocode}
+\DeclareRobustCommand\resultmarkers[2][ResultMarkers]{%
+ \wg@gencolorm@rkers{result marker}{#2}{#1}}
+% \end{macrocode}
+%
+% Common icons used by many modules
+%
+% \begin{macrocode}
+\DeclareRobustCommand\commonicons[2]{%
+ \begingroup%
+ \nopagecolor%
+ \tikzset{icon/.style={scale=.4,transform shape}}%
+ %
+ \info{pool-icon}{icon}{}
+ \tikz[icon]{\pic{pool icon};}
+ %
+ \info{oob-icon}{icon}{}%
+ \tikz[icon]{\pic{oob icon={#1}{#2}};}%
+ %
+ \info{flip-icon}{icon}{}%
+ \tikz[icon]{\pic{flip icon};}%
+ %
+ \info{eliminate-icon}{icon}{}%
+ \tikz[icon]{\pic{eliminate icon};}%
+ %
+ \info{restore-icon}{icon}{}%
+ \tikz[icon]{\pic{restore icon};}%
+ %
+ \info{dice-icon}{icon}{}%
+ \tikz[icon,scale=.9]{\pic{dice};}%
+ %
+ \info{unit-icon}{icon}{}%
+ \tikz[icon,scale=.7]{%
+ \chit[fill=#1,
+ symbol={[
+ scale line widths,
+ line width=1pt,
+ faction=friend,
+ command=land,
+ main=infantry,
+ scale=1.3](0,-.15)}]}%
+ \endgroup%
+}
+% \end{macrocode}
+%
+% \subsubsection{Making dice}
+% \begin{Syntax}
+% \cs{dice}\oarg{tikz-options}\oarg{node-options}\marg{name}\marg{name}\marg{list}
+% \end{Syntax}
+% \begin{enumerate}
+% \item \meta{tikz-options}
+% \item \meta{node-options}
+% \item \meta{name} - an identifier - e.g., the same as \meta{shape}.
+% \item \meta{shape} - one of \texttt{d4}, \texttt{d6}, \texttt{d8},
+% \texttt{d10}, \texttt{d12}, or \texttt{d20}.
+% \item \meta{list} - list of pairs
+% \meta{value}\texttt{/}\meta{printed}, where \meta{value} is the
+% value, and \meta{printed} is the shown value. If \meta{printed} is
+% left out, then \meta{value} is used.
+% \end{enumerate}
+%
+% \begin{macrocode}
+\def\dice{%
+ \@ifnextchar[{\wg@dice}{\wg@dice[]}%]
+}
+\def\wg@dice[#1]{%
+ \@ifnextchar[{\wg@@dice{#1}}{\wg@@dice{#1}[]} %]
+}
+\def\wg@@dice#1[#2]#3#4#5{%
+ \foreach \v/\p in {#5}{%
+ \info{#3-\v}{die-roll}{#3}
+ \tikz[#1]{
+ \node[shape=#4,transform shape,draw=none,fill=black,opacity=.5]
+ at (.05,-.03){};
+ \node[shape=#4,#2,transform shape]{\p};}}}
+% \end{macrocode}
+%
+% \subsubsection{Hooks into chits, etc.}
+%
+% TO BE DONE: We could add hook the \texttt{hex} shape that would
+% allow us to write out the settings for each of these. This would
+% allow us to make data files that contain the information available
+% in the \LaTeX{} code.
+%
+% If one then assumed that for example the upper left corner holds the
+% start-up hex, then one could use that information.
+%
+% The code below exports the chit information to the JSON file.
+% Together with the battle, odds, and result markers stuff above, this
+% allows the exporter to almost automatically set up battle odds and
+% result calculations. The fields exported are
+%
+% \begin{itemize}
+% \item Left and right identifiers
+% \item Upper left, upper right, lower left, and lower right
+% identifiers. (some care must be taken if these contains graphics
+% and not just text.)
+% \item Factors
+% \item NATO symbol
+% \begin{itemize}
+% \item Faction, command, echelon
+% \item Mains
+% \item Left, right, top, and bottom attributes and modifiers
+% \item Below attribute
+% \end{itemize}
+% \end{itemize}
+%
+% The exporter can set up prototypes for NATO types, echelons,
+% etc. The exporter can also set factors as marks on the units.
+%
+% \begin{macrocode}
+\tikzset{
+ zone turn/.store in=\zone@turn,
+ zone mult/.store in=\zone@mult
+}
+\def\@chit@rep@line#1#2{%
+ \@ifundefined{#2}{}{
+ \edef\wg@chit@tmp{\csname #2\endcsname}
+ {\escapechar=`/
+ \xdef\tmp{\detokenize\expandafter{\wg@chit@tmp} \@empty}}
+ % \message{^^J\meaning\@tmp -> \meaning\tmp}
+ \mk@w{ \mk@i\space "#1": "\tmp",}}}
+
+\def\do@chit@report{%
+ \chit@dbg{3}{Start of Chit Report}
+ \mk@w{ \mk@i "chit": \@lbchar}
+ \chit@dbg{3}{Report - ID}
+ \@ifundefined{id}{} {\mk@w{ \mk@i\space "id": "\id", }}%
+ \chit@dbg{3}{Report - Symbol: `\meaning\chit@symbol'}
+ \@ifundefined{chit@symbol}{}{\mk@w{ \mk@i\space "symbol": "true", }}%
+ \chit@dbg{3}{Report - Full: `\meaning\chit@full'}
+ \@chit@rep@line{full}{chit@full}
+ \chit@dbg{3}{Report - Factors: `\meaning\chit@factors'}
+ \@chit@rep@line{factors}{chit@factors}%
+ \chit@dbg{3}{Report - Left: `\meaning\chit@left'}
+ \@chit@rep@line{left}{chit@left}%
+ \chit@dbg{3}{Report - Right: : `\meaning\chit@right'}
+ \@chit@rep@line{right}{chit@right}%
+ \chit@dbg{3}{Report - Upper left: `\meaning\chit@upper@left'}
+ \@chit@rep@line{upper left}{chit@upper@left}%
+ \chit@dbg{3}{Report - Lower left: `\meaning\chit@lower@left'}
+ \@chit@rep@line{lower left}{chit@lower@left}%
+ \chit@dbg{3}{Report - Upper right: `\meaning\chit@upper@right}
+ \@chit@rep@line{upper right}{chit@upper@right}%
+ \chit@dbg{3}{Report - Lower right: `\meaning\chit@lower@right'}
+ \@chit@rep@line{lower right}{chit@lower@right}%
+ \chit@dbg{3}{Report - End comma}
+ \mk@w{ \mk@i\space "end": 0}
+ \@ifundefined{chit@symbol}{
+ \mk@w{ \mk@i \@rbchar }
+ }{
+ \mk@w{ \mk@i \@rbchar, }% NATOAPP6c will follow
+ }%
+ \chit@dbg{3}{End of Chit Report}
+}
+% \end{macrocode}
+% Report out NATO App6 symbol settings
+% \begin{macrocode}
+\def\do@natoapp@report{%
+ \mk@w{ \mk@i "natoapp6c": \@lbchar}
+ \@chit@rep@line{id}{\id}
+ \@chit@rep@line{faction}{natoapp@fac}
+ \@chit@rep@line{command}{natoapp@cmd}
+ \@chit@rep@line{echelon}{natoapp@ech}
+ \@chit@rep@line{main}{natoapp@main}
+ \@chit@rep@line{left}{natoapp@left}
+ \@chit@rep@line{right}{natoapp@right}
+ \@chit@rep@line{upper}{natoapp@upper}
+ \@chit@rep@line{lower}{natoapp@lower}
+ \@chit@rep@line{below}{natoapp@below}
+ \mk@w{ \mk@i\space "end": 0}
+ \mk@w{ \mk@i \@rbchar}
+}
+% \end{macrocode}
+%
+%
+% \iffalse
+%</exportcls>
+%\fi
diff --git a/macros/latex/contrib/wargame/source/util/icons.dtx b/macros/latex/contrib/wargame/source/util/icons.dtx
new file mode 100644
index 0000000000..14e2dc6d56
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/icons.dtx
@@ -0,0 +1,118 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+%
+% \subsubsection{VASSAL icons}
+%
+%
+% Some icons that may be useful in VASSAL. We put them here so they
+% may be used in manuals and the like too.
+%
+%
+% First, the line style
+% \begin{macrocode}
+\tikzset{
+ trash can line/.style={scale line widths,scale rounded corners,
+ line width=.5mm,->},
+}
+% \end{macrocode}
+%
+% Then, the body and lid of a trash can.
+%
+% \begin{macrocode}
+\tikzset{
+ trash can body/.pic={%
+ \path[fill=black,scale line widths,scale rounded corners,
+ rounded corners=.05cm]
+ (-.3,.2) --++(.6,0) --++(-.1,-.7) --++(-.4,0) --cycle;
+ \path[fill=white]
+ (-.025,-.4) arc(180:360:.025) --++( 0,.5) arc(0:180:.025) --cycle;
+ \path[fill=white]
+ (-.125,-.4) arc(180:360:.025) --++(-.07,.5) arc(0:180:.025) --cycle;
+ \path[fill=white]
+ ( .075,-.4) arc(180:360:.025) --++( .07,.5) arc(0:180:.025) --cycle;
+ },
+ trash can lid/.pic={%
+ \path[fill=black,scale line widths,scale rounded corners,
+ rounded corners=.05cm]
+ (-.35,.23)--++(.7,0)--++(-.07,.07)--++(-.56,0)--cycle;
+ \path[fill=black]
+ (-.15,.3) --++(.05,0) --++(0,.05) --++(.2,0) --++(0,-.05)
+ --++(.05,0) --++(0,.05) arc(0:90:.05) --++(-.2,0) arc(90:180:.05)
+ --cycle;
+ },
+}
+% \end{macrocode}
+%
+% Then, a closed and open trash can
+%
+% \begin{macrocode}
+\tikzset{
+ trash can/.pic={
+ \pic{trash can body};
+ \pic{trash can lid};
+ },
+ trash can open/.pic={
+ \pic{trash can body};
+ \pic[rotate=-30] at (0,.1) {trash can lid};
+ },
+}
+% \end{macrocode}
+%
+% Now we can use that to generate some useful icons.
+%
+% \begin{macrocode}
+\tikzset{
+ eliminate icon/.pic={
+ \pic{trash can open};
+ \draw[trash can line,color=red!50!black]
+ (-.5,.2) to[looseness=1.5] (-.1,.23);
+ },
+ restore icon/.pic={
+ \pic{trash can open};
+ \draw[trash can line,<-,color=green!50!black]
+ (-.5,.2) to[looseness=1.5] (-.1,.23);
+ },
+ pool icon/.pic={
+ \pic{trash can};
+ },
+}
+% \end{macrocode}
+%
+% These icons does not use the trash can picture.
+%
+% \begin{macrocode}
+\tikzset{
+ flip icon/.pic={
+ \draw[scale line widths,scale rounded corners,
+ line width=1mm,->,color=blue!50!black]
+ (-.5,-.5) arc(180:0:.5);% (.5,-.5);
+ },
+ pics/oob icon/.style n args={2}{code={%
+ \begin{scope}[box/.style args={##1,##2,##3,##4}{
+ minimum width=##1cm,
+ minimum height=##2cm,
+ fill=##3,
+ anchor=##4,
+ draw=gray!50!black,
+ scale line widths,
+ line width=.5pt,
+ transform shape},
+ under/.style={
+ below=.05cm of ##1}
+ ]
+ \node[box={.5,.2,#1,north west,fill=#1}] (r1) at (.05,.45) {};
+ \node[under=r1.south west,box={.3,.25,#1,north west}] (r2) {};
+ \node[under=r2.south west,box={.2,.3, #1,north west}] (r3) {};
+ \node[box={.2,.4,#2,north east}] (l1) at (-.05,.45) {};
+ \end{scope}
+ }
+ }
+}
+% \end{macrocode}
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi
diff --git a/macros/latex/contrib/wargame/source/util/misc.dtx b/macros/latex/contrib/wargame/source/util/misc.dtx
new file mode 100644
index 0000000000..eee931a164
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/misc.dtx
@@ -0,0 +1,145 @@
+% \iffalse
+% --------------------------------------------------------------------
+%<*utils>
+% \fi
+%
+% \subsubsection{Miscellaneous macros}
+% \begin{Macro}{\wargamelogo}
+%
+% This will produce the logo for this package.
+%
+%
+% \begin{center}
+% \tikz{\wargamelogo}
+% \end{center}
+%
+% \begin{macrocode}
+\tikzset{
+ wargame logo text/.style={
+ font=\sffamily\bfseries\fontsize{12}{14}\selectfont,
+ scale=2.8,
+ inner sep=0,
+ text width=1.8cm,
+ transform shape,
+ align=center},
+ wargame logo text content/.store in=\wg@logo@text@content,
+ wargame logo text content={{\huge\LaTeX} wargame},
+ wargame logo chit/.style={
+ chit={symbol={[
+ faction=friendly,
+ command=land,
+ echelon=division,
+ main=infantry]},
+ factors={chit/2 factors={4,3}},
+ left={chit/identifier=III},
+ right={chit/small identifier={10\textsuperscript{th}}},
+ color=white,
+ fill=red!50!black
+ }
+ },
+ wargame logo/.style={
+ transform shape,
+ every hex/.style={fill=gray!5!white,draw=gray!75!black},
+ hex/first row is=0,
+ hex/first column is=0,
+ hex/short top columns=none,
+ hex/short bottom columns=none,
+ hex/row direction is=normal,
+ hex/column direction is=normal
+ }
+}
+\newcommand\wargamelogo[1][]{%
+ \begin{scope}[wargame logo,#1]
+ \node[hex={fill=gray!30!white}] (logo center) at (hex cs:c=0,r=0) {};
+ \node[hex={terrain=light woods}](logo light woods) at (hex cs:c=0,r=1) {};
+ \node[hex={terrain=city}] (logo city) at (hex cs:c=0,r=-1){};
+ \node[hex={terrain=woods}] (logo woods) at (hex cs:c=-1,r=0){};
+ \node[hex={terrain=mountains}] (logo mountains) at (hex cs:c=-1,r=1){};
+ \node[hex={terrain=beach}] (logo beach) at (hex cs:c=1,r=1) {};
+ \node[hex={terrain=swamp}] (logo swamp) at (hex cs:c=1,r=0) {};
+ \node[wargame logo chit] (logo chit) at (hex cs:) {};
+ \node[wargame logo text] (logo text) {\wg@logo@text@content};
+ \end{scope}}
+% \end{macrocode}
+%
+% \end{Macro}
+% \begin{Macro}{\wg@dbg}
+% Debugging support. The counter \cs{wargamedbglvl} sets the debug
+% level. The package code then uses \cs{wg@dbg} to print out
+% debugging messages. This macro takes two arguments --- the first
+% is the \emph{least} debug level at which the message is printed, and
+% the second is the message it self.
+%
+% \begin{macrocode}
+\newcount\wargamedbglvl\wargamedbglvl=0
+\def\wg@dbg#1#2{%
+ \ifnum#1>\wargamedbglvl\relax\else\message{^^J#2}\fi}
+% \end{macrocode}
+% \end{Macro}
+%
+% \begin{Macro}{\wg@addto@macro}
+%
+% The macro \cs{wg@addto@macro}\marg{macro}\marg{other} adds the
+% definition of the macro \meta{other} to the macro \meta{macro}.
+% This uses the \cs{toks} trick of storing the \emph{tokens} of the
+% definition of a \meta{macro} and \meta{other} into \spec{@} and
+% expanding that token into the definition of \meta{macro}.
+% Effectively, this means that the top-level definition of
+% \meta{macro} and \meta{other} are expanded (i.e., macros used in
+% the definition of either macro is \emph{not} expanded) and then
+% that becomes the new definition of \meta{macro}.
+%
+% We will use this macro to do \emph{shallow} definitions of macros
+% to contain keys and such.
+%
+% \begin{macrocode}
+\long\def\wg@addto@macro#1#2{%
+ \begingroup
+ \toks@\expandafter\expandafter\expandafter{\expandafter#1#2}%
+ \xdef#1{\the\toks@}%
+ \endgroup}
+% \end{macrocode}
+% \end{Macro}
+%
+%
+% \begin{Macro}{\wg@sub@nchor}
+% Get anchor from sub node. We cannot use \cs{pgfpointanchor} since
+% that returns the anchor coordinates in the global coordinate
+% system.
+%
+% \begin{macrocode}
+\def\wg@sub@nchor#1#2{%
+ \wg@dbg{3}{^^JGet `#2' in `#1'}%
+ \@ifundefined{pgf@sh@ns@#1}{%
+ \pgf@x=0cm\pgf@y=0cm}{%
+ \pgf@process{%
+ \csname pgf@sh@ma@#1\endcsname% MW
+ \csname pgf@sh@np@#1\endcsname%
+ \pgf@sh@reanchor{\csname pgf@sh@ns@#1\endcsname}{#2}}}%
+ \wg@dbg{10}{-> \the\pgf@x,\the\pgf@y}%
+}
+% \end{macrocode}
+% \end{Macro}
+%
+%
+% Scratch dimensions
+%
+% \begin{macrocode}
+\newdimen\wg@tmpa
+\newdimen\wg@tmpb
+\newdimen\wg@tmpc
+\newdimen\wg@tmpd
+% \end{macrocode}
+%
+%
+% Macro to easy restore a saved path
+%
+% \begin{macrocode}
+\def\settosave#1{
+ \pgfsyssoftpath@setcurrentpath{#1}}
+% \end{macrocode}
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi
diff --git a/macros/latex/contrib/wargame/source/util/randomid.dtx b/macros/latex/contrib/wargame/source/util/randomid.dtx
new file mode 100644
index 0000000000..be986c8857
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/randomid.dtx
@@ -0,0 +1,22 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+%
+% \subsubsection{Random IDs}
+%
+% This macro sets the macro \cs{wg@uuid} to some random hex number.
+%
+% \begin{macrocode}
+\def\wg@r@ndom@id{%
+ \def\wg@uuid{}
+ \foreach \i in {1,...,8}{%
+ \pgfmathparse{Hex(random(0,15))}
+ \xdef\wg@uuid{\wg@uuid\pgfmathresult}}}
+% \end{macrocode}
+%
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi
diff --git a/macros/latex/contrib/wargame/source/util/tikz.dtx b/macros/latex/contrib/wargame/source/util/tikz.dtx
new file mode 100644
index 0000000000..fa31a7b58c
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/util/tikz.dtx
@@ -0,0 +1,196 @@
+% \iffalse
+% --------------------------------------------------------------------
+% <*utils>
+% \fi
+%
+% \subsubsection{Other Tikz utilities}
+%
+% \begin{TikzKey}{tikz/reverseclip}
+%
+% A reverse clipping path. This is used to cut out stuff outside of
+% path defined.
+%
+% \begin{macrocode}
+\tikzstyle{reverseclip}=[insert path={(current bounding box.north east) --
+ (current bounding box.south east) --
+ (current bounding box.south west) --
+ (current bounding box.north west) --
+ (current bounding box.north east)}]
+% \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{tikz/clip even odd rule}
+% A reverse clipping path
+%
+% \begin{macrocode}
+\tikzset{
+ clip even odd rule/.code={\pgfseteorule}, % Credit to Andrew Stacey
+}
+% \end{macrocode}
+% \end{TikzKey}
+%
+%
+% \begin{TikzKey}{tikz/invclip}
+%
+% Inverse clipping. This should be an option \emph{after} the path to
+% do the inverse clipping by. This works by adding a \emph{large}
+% (page) path to the current path, and then use that as clipping.
+%
+% \begin{macrocode}
+\tikzset{
+ invclip/.style={
+ clip,insert path=
+ [clip even odd rule]{
+ [reset cm](-\maxdimen,-\maxdimen)rectangle(\maxdimen,\maxdimen)
+ }
+ },
+}
+% \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{save clip}
+%
+% An option for use with sub-elements of NATO App 6(c) or chit
+% nodes. This will save the current path as a clipping path for the
+% next paths to be drawn in the sub-element
+%
+% \begin{macrocode}
+\newif\ifwg@s@ve\wg@s@vefalse
+\tikzset{
+ save clip/.is choice,
+ save clip/true/.code={\global\wg@s@vetrue},
+ save clip/false/.code={\global\wg@s@vefalse},
+ save clip/.default={true},
+ save clip/.initial={false},
+}
+% \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{scale line widths}
+%
+% Scales any line width specified in the node options.
+%
+% Use like
+%
+% \begin{verbatim}
+% \tikzset{
+% some/.style={
+% scale line widths,
+% line width=1pt}
+% }
+% \end{verbatim}
+%
+% Note that the order is important.
+%
+% \begin{macrocode}
+% Save pgf rounded corners macro
+% \let\wg@pgfsetcornersarced\pgfsetcornersarced
+\def\wg@setcornersarched#1{%
+ \ifx|#1|\else%
+ \edef\pgf@corner@arc{{#1}{#1}}%
+ \pgf@arccornerstrue%
+ \ifdim#1=0pt%
+ \pgf@arccornersfalse%
+ \fi\fi}
+\newdimen\wg@lw@scaled\wg@lw@scaled=1pt
+\def\wg@getscale{%
+ \pgfgettransformentries{%
+ \wg@jaca}{%
+ \wg@jacb}{%
+ \wg@jacc}{%
+ \wg@jacd}{%
+ \wg@tmp}{%
+ \wg@tmp}%
+ \pgfmathsetmacro{\wg@jac}{sqrt(abs(\wg@jaca*\wg@jacd-\wg@jacb*\wg@jacc))}%
+ \wg@dbg{4}{Scale is \wg@jac}
+ \xdef\wg@scale{\wg@jac}}
+\def\wg@scaled#1{%
+ \wg@getscale%
+ \wg@dbg{4}{Scaling #1 by \wg@scale}
+ \pgfmathsetmacro{\wg@tmp}{\wg@scale*#1}%
+ \xdef\wg@tmp{\wg@tmp}%
+ \xdef\wg@lw@scale{\wg@tmp}%
+ \wg@dbg{4}{Scaled #1 -> \wg@tmp}}
+%% \message{^^JRounded corners: \meaning\pgfsetcornersarced}
+\tikzset{
+ %% Get current scale and store in \wg@scale
+ get scale/.code={\wg@getscale},
+ scale line widths/.style={%
+ /utils/exec=\def\tikz@semiaddlinewidth##1{%
+ \wg@scaled{##1}
+ \wg@lw@scaled=\wg@tmp pt
+ \tikz@addoption{\pgfsetlinewidth{\wg@lw@scaled}}%
+ \wg@dbg{4}{Added scaled option \wg@tmp}
+ \pgfmathsetlength\pgflinewidth{\wg@tmp pt}
+ \wg@dbg{4}{Did set line width \wg@tmp pt}
+ }
+ },
+ scale rounded corners/.style={%
+ /utils/exec=\def\pgfsetcornersarced##1{%
+ \pgf@process{##1}%
+ \pgf@xa=\pgf@x%
+ \wg@scaled{\the\pgf@xa}%
+ % \tikz@addoption{\wg@setcornersarched{\wg@tmp pt}}%
+ \wg@dbg{4}{Scaled rounded corners: \the\pgf@xa -> \wg@tmp}%
+ \wg@setcornersarched{\wg@tmp pt}%
+ }
+ },
+ relative line width/.style={%
+ /utils/exec=\def\tikz@semiaddlinewidth##1{%
+ \wg@dbg{4}{Relative line width #1 times ##1}%
+ \pgfmathsetmacro{\wg@lv}{#1*##1}%
+ \tikz@addoption{\pgfsetlinewidth{\wg@lw pt}}%
+ \pgfmathsetlength\pgflinewidth{\wg@lw pt}}}
+}
+% \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{sub pic actions}
+%
+% This is key that propagates actions to sub pictures of pictures.
+% The normal \texttt{pic actions} cannot be used as it causes an
+% infinite loop.
+%
+% \begin{macrocode}
+\tikzset{
+ sub pic actions/.code={%
+ \tikz@picmode%
+ \edef\opts{%
+ \iftikz@mode@draw draw,\else draw=none,\fi
+ \iftikz@mode@fill fill\else fill=none\fi}
+ \wg@dbg{5}{^^JSub Mode: \meaning\tikz@picmode \meaning\opts}
+ \pgfset{/tikz/.cd}
+ \pgfkeysalsofrom{\opts}
+ }}
+% \end{macrocode}
+% \end{TikzKey}
+%
+% \begin{TikzKey}{wg/debug show}
+%
+% Show debugging information
+%
+% \begin{macrocode}
+\tikzset{
+ wg/debug show/.code={%
+ \extractcolorspec{pgfstrokecolor}{\wg@tmp@fg}
+ \def\wg@tmp@bg{none}
+ \@ifundefinedcolor{pgffillcolor}{}{
+ \extractcolorspec{pgffillcolor}{\wg@tmp@bg}}
+ \begingroup
+ \tikz@mode
+ \wargamedbglvl=#1
+ \wg@dbg{3}{Drawing with w/stroke `\wg@tmp@fg'
+ (\tikz@strokecolor,\iftikz@mode@draw\else not\space\fi drawing)
+ and fill `\wg@tmp@bg' (\tikz@fillcolor,\iftikz@mode@fill\else
+ not\space\fi filling)}
+ \endgroup
+ }
+}
+% \end{macrocode}
+% \end{TikzKey}
+%
+%
+% \iffalse
+% --------------------------------------------------------------------
+% </utils>
+% \fi
diff --git a/macros/latex/contrib/wargame/source/utils/wgexport.py b/macros/latex/contrib/wargame/source/utils/wgexport.py
index 9ad370cfcd..a09a7e4257 100755
--- a/macros/latex/contrib/wargame/source/utils/wgexport.py
+++ b/macros/latex/contrib/wargame/source/utils/wgexport.py
@@ -1,3661 +1,11515 @@
#!/usr/bin/env python
+# Script collected from other scripts
#
-from pprint import pprint
+# ../vassal/vassal.py
+# latexexporter.py
+# main.py
+#
+# ====================================================================
+# From ../vassal/vassal.py
+# Script collected from other scripts
+#
+# ../common/singleton.py
+# ../common/verbose.py
+# ../common/verboseguard.py
+# base.py
+# element.py
+# globalkey.py
+# gameelements.py
+# mapelements.py
+# globalproperty.py
+# turn.py
+# documentation.py
+# player.py
+# chessclock.py
+# widget.py
+# grid.py
+# zone.py
+# board.py
+# map.py
+# chart.py
+# command.py
+# trait.py
+# withtraits.py
+# traits/dynamicproperty.py
+# traits/globalproperty.py
+# traits/prototype.py
+# traits/place.py
+# traits/report.py
+# traits/calculatedproperty.py
+# traits/restrictcommand.py
+# traits/label.py
+# traits/layer.py
+# traits/globalcommand.py
+# traits/globalhotkey.py
+# traits/nostack.py
+# traits/deselect.py
+# traits/restrictaccess.py
+# traits/rotate.py
+# traits/stack.py
+# traits/mark.py
+# traits/mask.py
+# traits/trail.py
+# traits/delete.py
+# traits/sendto.py
+# traits/moved.py
+# traits/skel.py
+# traits/submenu.py
+# traits/basic.py
+# traits/trigger.py
+# traits/nonrect.py
+# traits/click.py
+# game.py
+# buildfile.py
+# moduledata.py
+# save.py
+# vsav.py
+# vmod.py
+# exporter.py
+#
+# ====================================================================
+# From ../common/singleton.py
+# ====================================================================
+class Singleton(type):
+ '''Meta base class for singletons'''
+ _instances = {}
+ def __call__(cls, *args, **kwargs):
+ '''Create the singleton object or returned existing
+
+ Parameters
+ ----------
+ args : tuple
+ Arguments
+ kwargs : dict
+ Keyword arguments
+ '''
+ if cls not in cls._instances:
+ cls._instances[cls] = \
+ super(Singleton, cls).__call__(*args, **kwargs)
+ return cls._instances[cls]
+#
+# EOF
+#
# ====================================================================
-def add_pws(opw=None,upw=None):
- '''Add password options to the poppler commands
+# From ../common/verbose.py
+# --------------------------------------------------------------------
+
+class Verbose(metaclass=Singleton):
+ def __init__(self,verbose=False):
+ '''Singleton for writing message to screen, contigent on setting
+
+ Parameters
+ ----------
+ verbose : bool
+ Whether to show messages or not
+ '''
+ self._indent = ''
+ self._verbose = verbose
+
+ def setVerbose(self,verbose):
+ '''Set whether to print or not
+
+ Parameters
+ ----------
+ verbose : bool
+ Whether to show messages or not
+ '''
+ self._verbose = verbose
+
+ @property
+ def verbose(self):
+ '''Test if this is verbose'''
+ return self._verbose
+
+ def message(self,*args,**kwargs):
+ '''Write messsage if verbose
+
+ Parameters
+ ----------
+ args : tuple
+ Arguments
+ kwargs : dict
+ Keyword arguments
+ '''
+ if not self._verbose: return
+ if not kwargs.pop('noindent', False):
+ print(self._indent,end='')
+ print(*args,**kwargs)
+
+ def incr(self):
+ '''Increment indention'''
+ self._indent += ' '
+
+ def decr(self):
+ '''Decrement indention'''
+ if len(self._indent) > 0:
+ self._indent = self._indent[:-1]
- Parameters
- ----------
- opw : str
- Owner password
- upw : str
- User password
+#
+# EOF
+#
+# ====================================================================
+# From ../common/verboseguard.py
+# --------------------------------------------------------------------
- Returns
- -------
- opts : list
- List of arguments
- '''
- args = []
- if upw is not None:
- args.extend(['-upw',upw])
+class VerboseGuard:
+ def __init__(self,*args,**kwargs):
+ '''A guard pattern that increases verbose indention
- if opw is not None:
- args.extend(['-opw',opw])
+ This is a context manager. The arguments passed are used for
+ an initial message, before increasinig indention.
- return args
+ Parameters
+ ----------
+ args : tuple
+ Arguments
+ kwargs : dict
+ Keyword arguments
+ '''
+ Verbose().message(*args,**kwargs)
-# --------------------------------------------------------------------
-def create_proc(args):
- '''Create a process
+ def __bool_(self):
+ '''Test if verbose'''
+ return Verbose().verbose
+
+ def __enter__(self):
+ '''Enter context'''
+ Verbose().incr()
+ return self
+
+ def __exit__(self,*args):
+ '''Exit context'''
+ Verbose().decr()
+
+ def __call__(self,*args,**kwargs):
+ '''Write a message at current indention level
+
+ Parameters
+ ----------
+ args : tuple
+ Arguments
+ kwargs : dict
+ Keyword arguments
+ '''
+ Verbose().message(*args,**kwargs)
+
+#
+# EOF
+#
+# ====================================================================
+# From base.py
+# ====================================================================
+# Key encoding
+SHIFT = 65
+CTRL = 130
+ALT = 520
+CTRL_SHIFT = CTRL+SHIFT
+ALT_SHIFT = ALT+SHIFT
+NONE = '\ue004'
+NONE_MOD = 0
+def key(let,mod=CTRL):
+
+ '''Encode a key sequence
+
+ Parameters
+ ----------
+ let : str
+ Key code (Letter)
+ mod : int
+ Modifier mask
+ '''
+ return f'{ord(let)},{mod}'
+#
+def hexcolor(s):
+ if isinstance(s,str):
+ s = s.replace('0x','')
+ if len(s) == 3:
+ r, g, b = [int(si,16)/16 for si in s]
+ elif len(s) == 6:
+ r = int(s[0:2],16) / 256
+ g = int(s[2:4],16) / 256
+ b = int(s[4:6],16) / 256
+ else:
+ raise RuntimeError('3 or 6 hexadecimal digits for color string')
+ elif isinstance(s,int):
+ r = ((s >> 16) & 0xFF) / 256
+ g = ((s >> 8) & 0xFF) / 256
+ b = ((s >> 0) & 0xFF) / 256
+ else:
+ raise RuntimeError('Hex colour must be string or integer')
+
+ return rgb(int(r*256),int(g*256),int(b*256))
+
+# Colour encoding
+def rgb(r,g,b):
+ '''Encode RGB colour
Parameters
----------
- args : list
- List of commmand line arguments
+ r : int
+ Red channel
+ g : int
+ Green channel
+ b : int
+ Blue channel
Returns
-------
- proc : subprocess.Process
- Process handle
+ colour : str
+ RGB colour as a string
'''
- from os import environ
- from subprocess import Popen, PIPE, TimeoutExpired
+ return ','.join([str(r),str(g),str(b)])
- return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
-
-# --------------------------------------------------------------------
-def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None):
- '''Get dictionary of PDF information
+def rgba(r,g,b,a):
+ '''Encode RGBA colour
Parameters
----------
- pdfname : str
- File name
- opw : str
- Owner password
- upw : str
- User password
- timeout : int
- Timeout in seconds or None
+ r : int
+ Red channel
+ g : int
+ Green channel
+ b : int
+ Blue channel
+ a : int
+ Alpha channel
Returns
-------
- info : dict
- PDF information
+ colour : str
+ RGBA colour as a string
'''
- from subprocess import TimeoutExpired
-
- args = ['pdfinfo', pdfname]
- args.extend(add_pws(opw=opw,upw=upw))
-
- proc = create_proc(args)
-
- try:
- out, err = proc.communicate(timeout=timeout)
- except Exception as e:
- proc.kill()
- proc.communicate()
- raise RuntimeError(f'Failed to get PDF info: {e}')
-
- d = {}
- for field in out.decode("utf8", "ignore").split("\n"):
- sf = field.split(":")
- key, value = sf[0], ":".join(sf[1:])
- if key != "":
- d[key] = (int(value.strip()) if key == 'Pages' else value.strip())
-
- if "Pages" not in d:
- raise ValueError
+ return ','.join([str(r),str(g),str(b),str(a)])
- return d
-
-# --------------------------------------------------------------------
-def get_images_info(infofile):
- '''Read in image information from file
+def dumpTree(node,ind=''):
+ '''Dump the tree of nodes
Parameters
----------
- infofile : str
- file name
-
- Returns
- -------
- images : list
- List of image information
+ node : xml.dom.Node
+ Node to dump
+ ind : str
+ Current indent
'''
- #from csv import DictReader
- from json import load
+ print(f'{ind}{node}')
+ for c in node.childNodes:
+ dumpTree(c,ind+' ')
- with open(infofile,'r') as file:
- info = load(file)
- #reader = DictReader(file,fieldnames=['page','name','type','sub type'])
- #return [l for l in reader]
+#
+# EOF
+#
+# ====================================================================
+# From element.py
- return info
+# ====================================================================
+class Element:
+ BUILD = 'VASSAL.build.'
+ MODULE = BUILD + 'module.'
+ WIDGET = BUILD + 'widget.'
+ MAP = MODULE + 'map.'
+ PICKER = MAP + 'boardPicker.'
+ BOARD = PICKER + 'board.'
+ def __init__(self,parent,tag,node=None,**kwargs):
+ '''Create a new element
+
+ Parameters
+ ----------
+ parent : Element
+ Parent element to add this element to
+ tag : str
+ Element tag
+ node : xml.dom.Node
+ If not None, then read attributes from that. Otherwise
+ set elements according to kwargs
+ kwargs : dict
+ Attribute keys and values. Only used if node is None
+ '''
+ if parent is not None:
+ self._root = parent._root
+ self._node = (node if node is not None else
+ parent.addNode(tag,**kwargs))
+ else:
+ self._root = None
+ self._node = None
+
+ # ----------------------------------------------------------------
+ # Attributes
+ def __contains__(self,key):
+ '''Check if element has attribute key'''
+ return self.hasAttribute(key)
+ def __getitem__(self,key):
+ '''Get attribute key value'''
+ return self.getAttribute(key)
-# --------------------------------------------------------------------
-def convert_page(pdfname,page,opw=None,upw=None,timeout=None):
- '''Convert a single PDF page to an image
+ def __setitem__(self,key,value):
+ '''Set attribute key value'''
+ self.setAttribute(key,value)
+
+ def hasAttribute(self,k):
+ '''Check if element has attribute '''
+ return self._node.hasAttribute(k)
+
+ def getAttribute(self,k):
+ '''Get attribute key value'''
+ return self._node.getAttribute(k)
+
+ def setAttribute(self,k,v):
+ '''Set attribute key value'''
+ self._node.setAttribute(k,str(v).lower()
+ if isinstance(v,bool) else str(v))
+
+ def setAttributes(self,**kwargs):
+ '''Set attributes to dictionary key and value'''
+ for k,v in kwargs.items():
+ self.setAttribute(k,v)
+
+ def getAttributes(self):
+ '''Get attributes as dict'''
+ return self._node.attributes
+
+ # ----------------------------------------------------------------
+ # Plain nodes
+ def getChildren(self):
+ '''Get child nodes (xml.dom.Node)'''
+ return self._node.childNodes
+
+ # ----------------------------------------------------------------
+ # Getters
+ #
+ # First generics
+ def getAsDict(self,tag='',key=None,enable=True):
+ '''Get elements with a specific tag as a dictionary
+ where the key is given by attribute key'''
+ cont = self._node.getElementsByTagName(tag)
+ if not enable or key is None:
+ return cont
+
+ return {e.getAttribute(key): e for e in cont}
+
+ def getAsOne(self,tag='',single=True):
+ '''Get elements with a specific tag, as a list.
+ If single is true, then assume we only have one such
+ child element, or fail.'''
+ cont = self._node.getElementsByTagName(tag)
+ if single and len(cont) != 1:
+ return None
+ return cont
- Parameters
- ----------
- pdfname : str
- File name
- page : int
- Page number
- opw : str
- Owner password
- upw : str
- User password
- timeout : int
- Timeout in seconds or None
+ def getElementsByKey(self,cls,key='',asdict=True):
+ '''Get elments of a specific class as a dictionary,
+ where the key is set by the key attribute.'''
+ cont = self.getAsDict(cls.TAG,key,asdict)
+ if cont is None: return None
+
+ if not asdict: return [cls(self,node=n) for n in cont]
+
+ return {k : cls(self,node=n) for k, n in cont.items()}
+
+ def getAllElements(self,cls,single=True):
+ '''Get elements with a specific tag, as a list. If single is
+ true, then assume we only have one such child element, or
+ fail.
+
+ '''
+ cont = self.getAsOne(cls.TAG,single=single)
+ if cont is None: return None
+ return [cls(self,node=n) for n in cont]
+
+ def getSpecificElements(self,cls,key,*names,asdict=True):
+ '''Get all elements of specific class and that has the
+ attribute key, and the attribute value is in names
+
+ '''
+ cont = self.getAsOne(cls.TAG,single=False)
+ cand = [cls(self,node=n) for n in cont
+ if n.getAttribute(key) in names]
+ if asdict:
+ return {c[key] : c for c in cand}
+ return cand
- Returns
- -------
- img : bytes
- The bytes of the image
- '''
- args = ['pdftocairo',
- '-singlefile',
- '-f', str(page),
- '-l', str(page),
- '-png']
- args.extend(add_pws(opw=opw,upw=upw))
- args.append(pdfname)
- args.append('-')
-
- proc = create_proc(args)
+ def getParent(self,cls,checkTag=True):
+ if self._node.parentNode is None:
+ return None
+ if checkTag and self._node.parentNode.tagName != cls.TAG:
+ return None
+ return cls(self,node=self._node.parentNode)
+
+ def getParentOfClass(self,cls):
+ '''Searches back until we find the parent with the right
+ class, or none
+ '''
+ t = {c.TAG: c for c in cls}
+ p = self._node.parentNode
+ while p is not None:
+ c = t.get(p.tagName,None)
+ if c is not None: return c(self,node=p)
+ p = p.parentNode
+ return None
+
+ # ----------------------------------------------------------------
+ # Adders
+ def addNode(self,tag,**attr):
+ '''Add a note to this element
+
+ Parameters
+ ----------
+ tag : str
+ Node tag name
+ attr : dict
+ Attributes to set
+ '''
+ e = self._root.createElement(tag)
+ if self._node: self._node.appendChild(e)
+
+ for k, v in attr.items():
+ e.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
+
+ return e
+
+ def addText(self,text):
+ '''Add a text child node to an element'''
+ t = self._root.createTextNode(text)
+ self._node.appendChild(t)
+ return t
+
+ def getText(self):
+ '''Get contained text node content'''
+ if self._node.firstChild is None or \
+ self._node.firstChild.nodeType != self._node.firstChild.TEXT_NODE:
+ return ''
+ return self._node.firstChild.nodeValue
+
- try:
- out, err = proc.communicate(timeout=timeout)
- except Exception as e:
- proc.kill()
- proc.communicate()
- raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}')
+ def add(self,cls,**kwargs):
+ '''Add an element and return wrapped in cls object'''
+ return cls(self,node=None,**kwargs)
- from io import BytesIO
- from PIL import Image
-
- img = out #Image.open(BytesIO(out))
+ def append(self,elem):
+ '''Append and element'''
+ if self._node.appendChild(elem._node):
+ return elem
+ return False
- return img
+ # ----------------------------------------------------------------
+ def remove(self,elem):
+ '''Remove an element'''
+ try:
+ self._node.removeChild(elem._node)
+ except:
+ return None
+ return elem
+ # ----------------------------------------------------------------
+ def insertBefore(self,toadd,ref):
+ '''Insert an element before another element'''
+ try:
+ self._node.insertBefore(toadd._node,ref._node)
+ except:
+ return None
+ return toadd
# --------------------------------------------------------------------
-def ignore_entry(entry):
- return entry['category'] in ['<<dummy>>','<<eol>>']
+class DummyElement(Element):
+ def __init__(self,parent,node=None,**kwargs):
+ '''A dummy element we can use to select elements of different
+ classes
-# --------------------------------------------------------------------
-def convert_pages(pdfname,infoname,
- opw=None,upw=None,timeout=None):
- '''Convert pages in a PDF to images
+ '''
+ super(DummyElement,self).__init__(parent,'Dummy',node=node)
- This creates a list of dictionaries with the image name and data
- (as bytes)
- Parameters
- ----------
- pdfname : str
- File name of PDF to read images from
- infoname : str
- File name of CSV to read image info from
- opw : str
- Owner password
- upw : str
- User password
- timeout : int
- Timeout in seconds or None
+#
+# EOF
+#
+# ====================================================================
+# From globalkey.py
+
+# --------------------------------------------------------------------
+class GlobalKey(Element):
+ SELECTED = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS'
+ def __init__(self,
+ parent,
+ tag,
+ node = None,
+ name = '',
+ buttonHotkey = '',
+ hotkey = '',
+ buttonText = '',
+ canDisable = False,
+ deckCount = '-1',
+ filter = '',
+ propertyGate = '',
+ reportFormat = '',
+ reportSingle = False,
+ singleMap = True,
+ target = SELECTED,
+ tooltip = '',
+ icon = ''):
+ '''
+ Parameters
+ ----------
+ - tag The XML tag to use
+ - parent Parent node
+ - node Optionally existing node
+ - name Name of key
+ - buttonHotkey Key in "global" scope
+ - hotkey Key to send to targeted pieces
+ - buttonText Text on button
+ - canDisable If true, disabled when propertyGate is true
+ - deckCount Number of decks (-1 is all)
+ - filter Which units to target
+ - propertyGate When true, disable
+ - reportFormat Chat message
+ - reportSingle Also show single piece reports
+ - singleMap Only originating map if True
+ - target Preselection filter (default selected pieces)
+ - tooltip Hover-over message
+ - icon Image to use as icon
+
+ Default targets are selected units
+ '''
+ nme = (name if len(name) > 0 else
+ tooltip if len(tooltip) > 0 else '')
+ tip = (tooltip if len(tooltip) > 0 else
+ name if len(name) > 0 else '')
+ super(GlobalKey,self).\
+ __init__(parent,
+ tag,
+ node = node,
+ name = nme,
+ buttonHotkey = buttonHotkey, # This hot key
+ hotkey = hotkey, # Target hot key
+ buttonText = buttonText,
+ canDisable = canDisable,
+ deckCount = deckCount,
+ filter = filter,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportSingle = reportSingle,
+ singleMap = singleMap,
+ target = target,
+ tooltip = tip,
+ icon = icon)
+#
+# EOF
+#
+# ====================================================================
+# From gameelements.py
+
+# --------------------------------------------------------------------
+class GameElementService:
+ def getGame(self):
+ return self.getParent(Game)
+
+# --------------------------------------------------------------------
+class GameElement(Element,GameElementService):
+ def __init__(self,game,tag,node=None,**kwargs):
+ super(GameElement,self).__init__(game,tag,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class Notes(GameElement):
+ TAG = Element.MODULE+'NotesWindow'
+ def __init__(self,elem,node=None,
+ name = 'Notes',
+ buttonText = '',
+ hotkey = key('N',ALT),
+ icon = '/images/notes.gif',
+ tooltip = 'Show notes window'):
+ super(Notes,self).__init__(elem,self.TAG,node=node,
+ name = name,
+ buttonText = buttonText,
+ hotkey = hotkey,
+ icon = icon,
+ tooltip = tooltip)
+ def encode(self):
+ return ['NOTES\t\\','PNOTES']
+
+# --------------------------------------------------------------------
+class PredefinedSetup(GameElement):
+ TAG = Element.MODULE+'PredefinedSetup'
+ def __init__(self,elem,node=None,
+ name = '',
+ file = '',
+ useFile = False,
+ isMenu = False,
+ description = ''):
+ useFile = ((useFile or not isMenu) and
+ (file is not None and len(file) > 0))
+ if file is None: file = ''
+ super(PredefinedSetup,self).__init__(elem,self.TAG,node=node,
+ name = name,
+ file = file,
+ useFile = useFile,
+ isMenu = isMenu,
+ description = description)
+ def addPredefinedSetup(self,**kwargs):
+ '''Add a `PredefinedSetup` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PredefinedSetup
+ The added element
+ '''
+ return self.add(PredefinedSetup,**kwargs)
+ def getPredefinedSetups(self,asdict=True):
+ '''Get all PredefinedSetup element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `PredefinedSetup` elements. If `False`, return a list of all PredefinedSetup` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `PredefinedSetup` children
+ '''
+ return self.getElementsByKey(PredefinedSetup,'name',asdict)
+
- Returns
- -------
- imgs : list
- List of images as dicts, info and the bytes of the images
- '''
+
+
+
+# --------------------------------------------------------------------
+class GlobalTranslatableMessages(GameElement):
+ TAG=Element.MODULE+'properties.GlobalTranslatableMessages'
+ def __init__(self,elem,node=None):
+ '''Translations
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ '''
+ super(GlobalTranslatableMessages,self).\
+ __init__(elem,self.TAG,node=node)
+
+# --------------------------------------------------------------------
+class Language(GameElement):
+ TAG = 'VASSAL.i18n.Language'
+ def __init__(self,elem,node=None,**kwargs):
+ super(Languate,self).__init__(sele,self.TAG,node=none,**kwargs)
+
+# --------------------------------------------------------------------
+class Chatter(GameElement):
+ TAG=Element.MODULE+'Chatter'
+ def __init__(self,elem,node=None,**kwargs):
+ '''Chat
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ kwargs : dict
+ Attributes
+ '''
+ super(Chatter,self).__init__(elem,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class KeyNamer(GameElement):
+ TAG=Element.MODULE+'KeyNamer'
+ def __init__(self,elem,node=None,**kwargs):
+ '''Key namer (or help menu)
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ kwargs : dict
+ Attributes
+ '''
+ super(KeyNamer,self).__init__(elem,self.TAG,node=node,**kwargs)
+
- oargs = {'opw':opw,'upw':upw,'timeout':timeout}
- docinfo = get_pdf_info(pdfname,**oargs)
- info = get_images_info(infoname)
+# --------------------------------------------------------------------
+# <VASSAL.build.module.GlobalOptions
+# autoReport="Always"
+# centerOnMove="Use Preferences Setting"
+# chatterHTMLSupport="Always"
+# hotKeysOnClosedWindows="Always"
+# inventoryForAll="Never"
+# nonOwnerUnmaskable="Always"
+# playerIdFormat="$PlayerName$"
+# promptString="Opponents can unmask pieces"
+# sendToLocationMoveTrails="Always"
+# storeLeadingZeroIntegersAsStrings="true">
+# <option name="stepIcon">/images/StepForward16.gif</option>
+# <option name="stepHotKey">39,130</option>
+# <option name="undoIcon">/images/Undo16.gif</option>
+# <option name="undoHotKey">90,130</option>
+# <option name="serverControlsIcon">/images/connect.gif</option>
+# <option name="serverControlsHotKey">65,195</option>
+# <option name="debugControlsIcon"/>
+# <option name="debugControlsHotKey">68,195</option>
+# </VASSAL.build.module.GlobalOptions>
+class GlobalOptions(GameElement):
+ NEVER = 'Never'
+ ALWAYS = 'Always'
+ PROMPT = 'Use Preferences Setting'
+ TAG = Element.MODULE+'GlobalOptions'
+ def __init__(self,doc,node=None,
+ autoReport = PROMPT,
+ centerOnMove = PROMPT,
+ chatterHTMLSupport = ALWAYS,
+ hotKeysOnClosedWindows = NEVER,
+ inventoryForAll = ALWAYS,
+ nonOwnerUnmaskable = PROMPT,
+ playerIdFormat = "$playerName$",
+ promptString = "Opponents can unmask pieces",
+ sendToLocationMoveTrails = NEVER,
+ storeLeadingZeroIntegersAsStrings = False,
+ description = 'Global options',
+ dragThreshold = 10):
+ '''Set global options on the module
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+
+ autoReport : str='always'
+ centerOnMove : str Option
+ chatterHTMLSupport : str='never'
+ hotKeysOnClosedWindows : str='never'
+ inventoryForAll : str='always'
+ nonOwnerUnmaskable : str='never'
+ playerIdFormat : str='$PlayerName$'
+ promptString : str=?
+ sendToLocationMoveTrails : bool=false
+ storeLeadingZeroIntegersAsStrings : bool=False
+ '''
+ super(GlobalOptions,self).\
+ __init__(doc,self.TAG,node=node,
+ autoReport = autoReport,
+ centerOnMove = centerOnMove,
+ chatterHTMLSupport = chatterHTMLSupport,
+ hotKeysOnClosedWindows = hotKeysOnClosedWindows,
+ inventoryForAll = inventoryForAll,
+ nonOwnerUnmaskable = nonOwnerUnmaskable,
+ playerIdFormat = playerIdFormat,
+ promptString = promptString,
+ sendToLocationMoveTrails = sendToLocationMoveTrails,
+ storeLeadingZeroIntegersAsStrings = storeLeadingZeroIntegersAsStrings,
+ dragThreshold = dragThreshold,
+ description = description)
+
+ def addOption(self,**kwargs):
+ '''Add a `Option` element to this
+
+ Options known
+ - stepIcon - image file name (/images/StepForward16.gif)
+ - stepHotKey - key
+ - undoIcon - image file name (/images/Undo16.gif)
+ - undoHotKey - key
+ - serverControlsIcon - image file name (/images/connect.gif)
+ - serverControlsHotKey - key
+ - debugControlsIcon - image file name
+ - debugControlsHotKey - key
+
- if len(info)-1 != docinfo['Pages']:
- raise RuntimeError(f'Number of pages in {pdfname} '
- f'({docinfo["Pages"]}) not {len(info)-1}')
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Option
+ The added element
+ '''
+ return self.add(Option,**kwargs)
+ def getOptions(self):
+ return self.getElementsByKey(Option,'name')
+
+ def addPreference(self,cls,**kwargs):
+ return self.add(cls,**kwargs)
+
+ def addIntPreference(self,**kwargs):
+ return self.add(IntPreference,**kwargs)
+
+ def addFloatPreference(self,**kwargs):
+ return self.add(FloatPreference,**kwargs)
+
+ def addBoolPreference(self,**kwargs):
+ return self.add(BoolPreference,**kwargs)
+
+ def addStrPreference(self,**kwargs):
+ return self.add(StrPreference,**kwargs)
+
+ def addTextPreference(self,**kwargs):
+ return self.add(TextPreference,**kwargs)
+
+ def addEnumPreference(self,**kwargs):
+ return self.add(EnumPreference,**kwargs)
+
+ def getIntPreferences(self):
+ return self.getElementsByKey(IntPreference,'name')
- for i in info:
- if ignore_entry(i): continue
- i['img'] = convert_page(pdfname,i['number'],**oargs)
+ def getFloatPreferences(self):
+ return self.getElementsByKey(FloatPreference,'name')
- return info
+ def getBoolPreferences(self):
+ return self.getElementsByKey(BoolPreference,'name')
-# ====================================================================
-def remove_and_get(zipfname,*filenames):
- '''Open zip file, and make a copy of it without the files listed
- in entries. Return the data of files listed in entry.'''
- from tempfile import mkdtemp
- from zipfile import ZipFile
- from shutil import move, rmtree
- from os import path
+ def getStrPreferences(self):
+ return self.getElementsByKey(StrPreference,'name')
- tempdir = mkdtemp()
- ret = {}
- try:
- tempname = path.join(tempdir, 'new.zip')
- with ZipFile(zipfname, 'r') as zipread:
+ def getTextPreferences(self):
+ return self.getElementsByKey(TextPreference,'name')
- with ZipFile(tempname, 'w') as zipwrite:
+ def getEnumPreferences(self):
+ return self.getElementsByKey(EnumPreference,'name')
- for item in zipread.infolist():
- data = zipread.read(item.filename)
+ def getPreferences(self):
+ retd = {}
+ for cls in [IntPreference,
+ FloatPreference,
+ BoolPreference,
+ StrPreference,
+ TextPreference,
+ EnumPreference]:
+ retd.update(self.getElementsByKey(cls,'name'))
- if item.filename not in filenames:
- zipwrite.writestr(item, data)
- else:
- ret[item.filename] = data
-
- move(tempname, zipfname)
- finally:
- rmtree(tempdir)
+ return retd
+
- return ret
+# --------------------------------------------------------------------
+class Option(Element):
+ TAG = 'option'
+ def __init__(self,doc,node=None,name='',value=''):
+ super(Option,self).__init__(doc,tag=self.TAG,node=node,name=name)
+ self.addText(value)
+
+ def getGlobalOptions(self):
+ return self.getParent(GlobalOptions)
+
+# --------------------------------------------------------------------
+class Preference(Element):
+ PREFS = 'VASSAL.preferences.'
+ def __init__(self,
+ doc,
+ tag,
+ node = None,
+ name = '',
+ default = '',
+ desc = '',
+ tab = '',
+ **kwargs):
+ '''Add a preference
+
+ Parameters
+ ----------
+ name : str
+ Name of property
+ default : str
+ Default value
+ desc : str
+ Description
+ tab : str
+ Preference tab to put in to
+ '''
+ super(Preference,self).__init__(doc,
+ tag = tag,
+ node = node,
+ name = name,
+ default = default,
+ desc = desc,
+ tab = tab)
+
+ def getGlobalOptions(self):
+ return self.getParent(GlobalOptions)
+
+# --------------------------------------------------------------------
+class IntPreference(Preference):
+ TAG = Preference.PREFS+'IntegerPreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ default = 0,
+ desc = '',
+ tab = ''):
+ super(IntPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = str(default),
+ desc = desc,
+ tab = tab)
+# --------------------------------------------------------------------
+class FloatPreference(Preference):
+ TAG = Preference.PREFS+'DoublePreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ default = 0.,
+ desc = '',
+ tab = ''):
+ super(FloatPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = str(default),
+ desc = desc,
+ tab = tab)
+# --------------------------------------------------------------------
+class BoolPreference(Preference):
+ TAG = Preference.PREFS+'BooleanPreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ default = False,
+ desc = '',
+ tab = ''):
+ super(BoolPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = ('true' if default
+ else 'false'),
+ desc = desc,
+ tab = tab)
+# --------------------------------------------------------------------
+class StrPreference(Preference):
+ TAG = Preference.PREFS+'StringPreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ default = '',
+ desc = '',
+ tab = ''):
+ super(StrPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = default,
+ desc = desc,
+ tab = tab)
+# --------------------------------------------------------------------
+class TextPreference(Preference):
+ TAG = Preference.PREFS+'TextPreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ default = '',
+ desc = '',
+ tab = ''):
+ super(TextPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = (default
+ .replace('\n','&#10;')),
+ desc = desc,
+ tab = tab)
+# --------------------------------------------------------------------
+class EnumPreference(Preference):
+ TAG = Preference.PREFS+'EnumPreference'
+ def __init__(self,
+ doc,
+ node = None,
+ name = '',
+ values = [],
+ default = '',
+ desc = '',
+ tab = ''):
+ ce = lambda v : str(v).replace(',',r'\,')
+ sl = [ce(v) for v in values]
+ df = ce(v)
+ assert df in sl, \
+ f'Default value "{default}" not in list {":".join(values)}'
+ super(EnumPreference,self).__init__(doc,
+ tag = self.TAG,
+ node = node,
+ name = name,
+ default = df,
+ desc = desc,
+ tab = tab,
+ list = sl)
+
# --------------------------------------------------------------------
-def add_back(zipfname,files):
- '''Open a zip file for appending and add data in files
+# CurrentMap == &quot;Board&quot;
+class Inventory(GameElement):
+ TAG = Element.MODULE+'Inventory'
+ def __init__(self,doc,node=None,
+ canDisable = False,
+ centerOnPiece = True,
+ disabledIcon = '',
+ drawPieces = True,
+ foldersOnly = False,
+ forwardKeystroke = True,
+ groupBy = '',
+ hotkey = key('I',ALT),
+ icon = '/images/inventory.gif',
+ include = '{}',
+ launchFunction = 'functionHide',
+ leafFormat = '$PieceName$',
+ name = '',
+ nonLeafFormat = '$PropertyValue$',
+ pieceZoom = '0.33',
+ pieceZoom2 = '0.5',
+ pieceZoom3 = '0.6',
+ propertyGate = '',
+ refreshHotkey = key('I',ALT_SHIFT),
+ showMenu = True,
+ sides = '',
+ sortFormat = '$PieceName$',
+ sortPieces = True,
+ sorting = 'alpha',
+ text = '',
+ tooltip = 'Show inventory of all pieces',
+ zoomOn = False):
+ super(Inventory,self).__init__(doc,self.TAG,node=node,
+ canDisable = canDisable,
+ centerOnPiece = centerOnPiece,
+ disabledIcon = disabledIcon,
+ drawPieces = drawPieces,
+ foldersOnly = foldersOnly,
+ forwardKeystroke = forwardKeystroke,
+ groupBy = groupBy,
+ hotkey = hotkey,
+ icon = icon,
+ include = include,
+ launchFunction = launchFunction,
+ leafFormat = leafFormat,
+ name = name,
+ nonLeafFormat = nonLeafFormat,
+ pieceZoom = pieceZoom,
+ pieceZoom2 = pieceZoom2,
+ pieceZoom3 = pieceZoom3,
+ propertyGate = propertyGate,
+ refreshHotkey = refreshHotkey,
+ showMenu = showMenu,
+ sides = sides,
+ sortFormat = sortFormat,
+ sortPieces = sortPieces,
+ sorting = sorting,
+ text = text,
+ tooltip = tooltip,
+ zoomOn = zoomOn)
+
+
+
- Files is a dictionary of filenames mapping to file content,
- as returned by remove_and_get
- '''
- from zipfile import ZipFile
- with ZipFile(zipfname, 'a') as zipadd:
- for filename,data in files.items():
- zipadd.writestr(filename,data)
+
-# ====================================================================
-# Contants used for tag names
-BUILD = 'VASSAL.build.'
-MODULE = BUILD + 'module.'
-WIDGET = BUILD + 'widget.'
-MAP = MODULE + 'map.'
-PICKER = MAP + 'boardPicker.'
-# ====================================================================
-def get_asdict(elem,tag,key,enable=True):
- cont = elem.getElementsByTagName(tag)
- if not enable:
- return cont
- return {e.getAttribute(key): e for e in cont}
-# --------------------------------------------------------------------
-def get_asone(elem,tag,enable=True):
- cont = elem.getElementsByTagName(tag)
- if enable and len(cont) != 1:
- return None
- return cont
-
-# --------------------------------------------------------------------
-def get_doc(string):
- from xml.dom.minidom import parseString
- return parseString(string)
# --------------------------------------------------------------------
-def get_game(doc):
- return doc.getElementsByTagName('VASSAL.build.GameModule')[0]
+class Prototypes(GameElement):
+ TAG = Element.MODULE+'PrototypesContainer'
+ def __init__(self,game,node=None,**kwargs):
+ super(Prototypes,self).\
+ __init__(game,self.TAG,node=node,**kwargs)
-# --------------------------------------------------------------------
-def get_globalproperties(doc,one=True):
- return get_asone(doc,MODULE+'properties.GlobalProperties',one)
+ def addPrototype(self,**kwargs):
+ '''Add a `Prototype` element to this
-# --------------------------------------------------------------------
-def get_globalproperty(doc,asdict=True):
- '''A little confusing - this returns a dictionary'''
- return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict)
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Prototype
+ The added element
+ '''
+ return self.add(Prototype,**kwargs)
+ def getPrototypes(self,asdict=True):
+ '''Get all Prototype element(s) from this
-# --------------------------------------------------------------------
-def get_turntrack(doc,asdict=True):
- return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict)
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Prototype` elements. If `False`, return a list of all Prototype` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Prototype` children
+ '''
+ return self.getElementsByKey(Prototype,'name',asdict=asdict)
+
+
-# --------------------------------------------------------------------
-def get_turncounter(doc,asdict=True):
- return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict)
# --------------------------------------------------------------------
-def get_turnlist(doc,asdict=True):
- return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict)
-
+class DiceButton(GameElement):
+ TAG=Element.MODULE+'DiceButton'
+ def __init__(self,elem,node=None,
+ addToTotal = 0,
+ canDisable = False,
+ hotkey = key('6',ALT),
+ icon = '/images/die.gif',
+ keepCount = 1,
+ keepDice = False,
+ keepOption = '>',
+ lockAdd = False,
+ lockDice = False,
+ lockPlus = False,
+ lockSides = False,
+ nDice = 1,
+ nSides = 6,
+ name = '1d6',
+ plus = 0,
+ prompt = False,
+ propertyGate = '',
+ reportFormat = '** $name$ = $result$ *** <$PlayerName$>;',
+ reportTotal = False,
+ sortDice = False,
+ text = '1d6',
+ tooltip = 'Roll a 1d6'):
+ super(DiceButton,self).\
+ __init__(elem,self.TAG,node=node,
+ addToTotal = addToTotal,
+ canDisable = canDisable,
+ hotkey = hotkey,
+ icon = icon,
+ keepCount = keepCount,
+ keepDice = keepDice,
+ keepOption = keepOption,
+ lockAdd = lockAdd,
+ lockDice = lockDice,
+ lockPlus = lockPlus,
+ lockSides = lockSides,
+ nDice = nDice,
+ nSides = nSides,
+ name = name,
+ plus = plus,
+ prompt = prompt,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportTotal = reportTotal,
+ sortDice = sortDice,
+ text = text,
+ tooltip = tooltip)
+
+# --------------------------------------------------------------------
+class GameMassKey(GlobalKey,GameElementService):
+ TAG = Element.MODULE+'GlobalKeyCommand'
+ def __init__(self,map,node=None,
+ name = '',
+ buttonHotkey = '',
+ hotkey = '',
+ buttonText = '',
+ canDisable = False,
+ deckCount = '-1',
+ filter = '',
+ propertyGate = '',
+ reportFormat = '',
+ reportSingle = False,
+ singleMap = True,
+ target = GlobalKey.SELECTED,
+ tooltip = '',
+ icon = ''):
+ '''Default targets are selected units'''
+ super(GameMassKey,self).\
+ __init__(map,
+ self.TAG,
+ node = node,
+ name = name,
+ buttonHotkey = buttonHotkey, # This hot key
+ hotkey = hotkey, # Target hot key
+ buttonText = buttonText,
+ canDisable = canDisable,
+ deckCount = deckCount,
+ filter = filter,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportSingle = reportSingle,
+ singleMap = singleMap,
+ target = target,
+ tooltip = tooltip,
+ icon = icon)
+
# --------------------------------------------------------------------
-def get_turnhotkey(doc,asdict=True):
- return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict)
-
+class StartupMassKey(GlobalKey,GameElementService):
+ TAG = Element.MODULE+'StartupGlobalKeyCommand'
+ FIRST_LAUNCH = 'firstLaunchOfSession'
+ EVERY_LAUNCH = 'everyLaunchOfSession'
+ START_GAME = 'startOfGameOnly'
+ def __init__(self,
+ map,
+ node = None,
+ name = '',
+ buttonHotkey = '',
+ hotkey = '',
+ buttonText = '',
+ canDisable = False,
+ deckCount = '-1',
+ filter = '',
+ propertyGate = '',
+ reportFormat = '',
+ reportSingle = False,
+ singleMap = True,
+ target = GlobalKey.SELECTED,
+ tooltip = '',
+ icon = '',
+ whenToApply = EVERY_LAUNCH):
+ '''Default targets are selected units'''
+ super(StartupMassKey,self).\
+ __init__(map,
+ self.TAG,
+ node = node,
+ name = name,
+ buttonHotkey = buttonHotkey, # This hot key
+ hotkey = hotkey, # Target hot key
+ buttonText = buttonText,
+ canDisable = canDisable,
+ deckCount = deckCount,
+ filter = filter,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportSingle = reportSingle,
+ singleMap = singleMap,
+ target = target,
+ tooltip = tooltip,
+ icon = icon)
+ if node is None:
+ self['whenToApply'] = whenToApply
+
+# --------------------------------------------------------------------
+class Menu(GameElement):
+ TAG = Element.MODULE+'ToolbarMenu'
+ def __init__(self,
+ game,
+ node = None,
+ canDisable = False,
+ description = '',
+ disabledIcon = '',
+ hotkey = '',
+ icon = '',
+ menuItems = [],# Button texts
+ propertyGate = '',
+ text = '',# Menu name
+ tooltip = ''):
+ if len(description) <= 0 and len(tooltip) > 0:
+ description = tooltip
+ if len(tooltip) <= 0 and len(description) > 0:
+ tooltip = description
+ super(Menu,self).\
+ __init__(game,
+ self.TAG,
+ node = node,
+ canDisable = canDisable,
+ description = description,
+ disabledIcon = disabledIcon,
+ hotkey = hotkey,
+ icon = icon,
+ menuItems = ','.join(menuItems),
+ propertyGate = propertyGate,
+ text = text,
+ tooltip = tooltip)
+
+
# --------------------------------------------------------------------
-def get_documentation(doc,one=True):
- return get_asone(doc,MODULE+'Documentation',one)
-
+class SymbolicDice(GameElement):
+ TAG = Element.MODULE+'SpecialDiceButton'
+ def __init__(self,
+ game,
+ node = None,
+ canDisable = False,
+ disabledIcon = '',
+ hotkey = key('6',ALT),
+ name = "Dice", # GP prefix
+ text = '', # Text on button
+ icon = '/images/die.gif', # Icon on button
+ format = '{name+": "+result1}', # Report
+ tooltip = 'Die roll', # Help
+ propertyGate = '', # Property to disable when T
+ resultButton = False, # Result on button?
+ resultChatter = True, # Result in Chatter?
+ resultWindow = False, # Result window?
+ backgroundColor = rgb(0xdd,0xdd,0xdd), # Window background
+ windowTitleResultFormat = "$name$", # Window title
+ windowX = '67', # Window size
+ windowY = '65'):
+ super(SymbolicDice,self).\
+ __init__(game,
+ self.TAG,
+ node = node,
+ canDisable = canDisable,
+ disabledIcon = disabledIcon,
+ hotkey = hotkey,
+ name = name,
+ text = text,
+ icon = icon,
+ format = format,
+ tooltip = tooltip,
+ propertyGate = propertyGate,
+ resultButton = resultButton,
+ resultChatter = resultChatter,
+ resultWindow = resultWindow,
+ backgroundColor = backgroundColor,
+ windowTitleResultFormat = windowTitleResultFormat,
+ windowX = windowX,
+ windowY = windowY)
+
+ def addDie(self,**kwargs):
+ return self.add(SpecialDie,**kwargs)
+
+ def getSymbolicDice(self):
+ return self.getParent(SymbolicDice)
+
+
# --------------------------------------------------------------------
-def get_helpfiles(doc,key='fileName',asdict=True):
- return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict)
+class SpecialDie(GameElement):
+ TAG = Element.MODULE+'SpecialDie'
+ def __init__(self,
+ symbolic, # Symblic dice
+ node = None,
+ name = '', # Name of dice (no GP)
+ report = '{name+": "+result}',
+ faces = None):
+ super(SpecialDie,self).\
+ __init__(symbolic,
+ self.TAG,
+ node = node,
+ name = name,
+ report = report)
+ if node is not None or faces is None:
+ return
+ if isinstance(faces,list):
+ faces = {i+1: f for i,f in enumerate(faces)}
+ for v,f in faces:
+ self.addFace(text = str(v), value = v, icon = f)
+
+ def addFace(self,**kwargs):
+ self.add(DieFace,**kwargs)
+
+ def getSymbolicDice(self):
+ return self.getParent(SymbolicDice)
+
-# --------------------------------------------------------------------
-def get_tutorials(doc,key='name',asdict=True):
- return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict)
+
+
+# --------------------------------------------------------------------
+class DieFace(GameElement):
+ TAG = Element.MODULE+'SpecialDieFace'
+ def __init__(self,
+ special, # Special dice
+ node, # existing node
+ icon = '', # graphical representation
+ text = '', # Text representation
+ value = 0): # Value representation
+ super(DieFace,self).\
+ __init__(special,
+ self.TAG,
+ node = node,
+ icon = icon,
+ text = text,
+ value = value)
+
+ def getSpecialDie(self):
+ return self.getParent(SpecialDie)
+#
+# EOF
+#
+# ====================================================================
+# From mapelements.py
+
+# --------------------------------------------------------------------
+class MapElementService:
+ def getMap(self):
+ '''Get map - either a Map or WidgetMap'''
+ return self.getParentOfClass([WidgetMap,Map])
+ # if self._parent is None:
+ # return None
+ #
+ # if 'WidgetMap' in self._parent.tagName:
+ # return self.getParent(WidgetMap)
+ #
+ # return self.getParent(Map)
+ def getGame(self):
+ m = self.getMap()
+ if m is not None: return m.getGame()
+ return None
# --------------------------------------------------------------------
-def get_inventories(doc,key='name',asdict=False):
- return get_asdict(doc,MODULE+'Inventory',key,asdict)
+class MapElement(Element,MapElementService):
+ def __init__(self,map,tag,node=None,**kwargs):
+ super(MapElement,self).__init__(map,tag,node=node,**kwargs)
-# --------------------------------------------------------------------
-def get_maps(doc,asdict=True):
- return get_asdict(doc,MODULE+'Map','mapName',asdict)
# --------------------------------------------------------------------
-def get_widgetmap(doc,asdict=True):
- return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict)
-
+class StackMetrics(MapElement):
+ TAG=Element.MAP+'StackMetrics'
+ def __init__(self,map,node=None,
+ bottom = key('(',0),
+ down = key('%',0),
+ top = key('&',0),
+ up = key("'",0),
+ disabled = False,
+ exSepX = 6, # Expanded (after double click)
+ exSepY = 18, # Expanded (after double click)
+ unexSepX = 8, # Compact
+ unexSepY = 16): # Compact
+ super(StackMetrics,self).__init__(map,self.TAG,node=node,
+ bottom = bottom,
+ disabled = disabled,
+ down = down,
+ exSepX = exSepX,
+ exSepY = exSepY,
+ top = top,
+ unexSepX = unexSepX,
+ unexSepY = unexSepY,
+ up = up)
+
+# --------------------------------------------------------------------
+class ImageSaver(MapElement):
+ TAG=Element.MAP+'ImageSaver'
+ def __init__(self,map,node=None,
+ buttonText = '',
+ canDisable = False,
+ hotkey = '',
+ icon = '/images/camera.gif',
+ propertyGate = '',
+ tooltip = 'Save map as PNG image'):
+ super(ImageSaver,self).__init__(map,self.TAG,node=node,
+ buttonText = buttonText,
+ canDisable = canDisable,
+ hotkey = hotkey,
+ icon = icon,
+ propertyGate = propertyGate,
+ tooltip = tooltip)
+# --------------------------------------------------------------------
+class TextSaver(MapElement):
+ TAG=Element.MAP+'TextSaver'
+ def __init__(self,map,node=None,
+ buttonText = '',
+ canDisable = False,
+ hotkey = '',
+ icon = '/images/camera.gif',
+ propertyGate = '',
+ tooltip = 'Save map as text'):
+ super(TextSaver,self).__init__(map,self.TAG,node=node,
+ buttonText = buttonText,
+ canDisable = canDisable,
+ hotkey = hotkey,
+ icon = icon,
+ propertyGate = propertyGate,
+ tooltip = tooltip)
+
+
+# --------------------------------------------------------------------
+class ForwardToChatter(MapElement):
+ TAG=Element.MAP+'ForwardToChatter'
+ def __init__(self,map,node=None,**kwargs):
+ super(ForwardToChatter,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class MenuDisplayer(MapElement):
+ TAG=Element.MAP+'MenuDisplayer'
+ def __init__(self,map,node=None,**kwargs):
+ super(MenuDisplayer,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class MapCenterer(MapElement):
+ TAG=Element.MAP+'MapCenterer'
+ def __init__(self,map,node=None,**kwargs):
+ super(MapCenterer,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class StackExpander(MapElement):
+ TAG=Element.MAP+'StackExpander'
+ def __init__(self,map,node=None,**kwargs):
+ super(StackExpander,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class PieceMover(MapElement):
+ TAG=Element.MAP+'PieceMover'
+ def __init__(self,map,node=None,**kwargs):
+ super(PieceMover,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class SelectionHighlighters(MapElement):
+ TAG=Element.MAP+'SelectionHighlighters'
+ def __init__(self,map,node=None,**kwargs):
+ super(SelectionHighlighters,self).\
+ __init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class KeyBufferer(MapElement):
+ TAG=Element.MAP+'KeyBufferer'
+ def __init__(self,map,node=None,**kwargs):
+ super(KeyBufferer,self).__init__(map,self.TAG,node=node,**kwargs)
+
+# --------------------------------------------------------------------
+class HighlightLastMoved(MapElement):
+ TAG=Element.MAP+'HighlightLastMoved'
+ def __init__(self,map,node=None,
+ color = rgb(255,0,0),
+ enabled = True,
+ thickness = 2):
+ super(HighlightLastMoved,self).__init__(map,self.TAG,node=node,
+ color = color,
+ enabled = enabled,
+ thickness = thickness)
+
+# --------------------------------------------------------------------
+class CounterDetailViewer(MapElement):
+ TAG=Element.MAP+'CounterDetailViewer'
+ TOP_LAYER = 'from top-most layer only'
+ ALL_LAYERS = 'from all layers'
+ INC_LAYERS = 'from listed layers only'
+ EXC_LAYERS = 'from layers other than those listed'
+ FILTER = 'by using a property filter'
+ def __init__(self,map,node=None,
+ borderWidth = 0,
+ centerAll = False,
+ centerText = False,
+ combineCounterSummary = False,
+ counterReportFormat = '',
+ delay = 700,
+ description = '',
+ display = TOP_LAYER,
+ emptyHexReportForma = '$LocationName$',
+ enableHTML = True,
+ extraTextPadding = 0,
+ fgColor = rgb(0,0,0),
+ fontSize = 9,
+ graphicsZoom = 1.0,
+ hotkey = key('\n',0),
+ layerList = '',
+ minDisplayPieces = 2,
+ propertyFilter = '',
+ showDeck = False,
+ showDeckDepth = 1,
+ showDeckMasked = False,
+ showMoveSelectde = False,
+ showNoStack = False,
+ showNonMovable = False,
+ showOverlap = False,
+ showgraph = True,
+ showgraphsingle = False,
+ showtext = False,
+ showtextsingle = False,
+ stretchWidthSummary = False,
+ summaryReportFormat = '$LocationName$',
+ unrotatePieces = False,
+ version = 3,
+ verticalOffset = 0,
+ verticalTopText = 5,
+ zoomlevel = 1.0): # showTerrain attributes
+ super(CounterDetailViewer,self)\
+ .__init__(map,self.TAG,node=node,
+ borderWidth = borderWidth,
+ centerAll = centerAll,
+ centerText = centerText,
+ combineCounterSummary = combineCounterSummary,
+ counterReportFormat = counterReportFormat,
+ delay = delay,
+ description = description,
+ display = display,
+ emptyHexReportForma = emptyHexReportForma,
+ enableHTML = enableHTML,
+ extraTextPadding = extraTextPadding,
+ fgColor = fgColor,
+ fontSize = fontSize,
+ graphicsZoom = graphicsZoom,
+ hotkey = hotkey,
+ layerList = layerList,
+ minDisplayPieces = minDisplayPieces,
+ propertyFilter = propertyFilter,
+ showDeck = showDeck,
+ showDeckDepth = showDeckDepth,
+ showDeckMasked = showDeckMasked,
+ showMoveSelectde = showMoveSelectde,
+ showNoStack = showNoStack,
+ showNonMovable = showNonMovable,
+ showOverlap = showOverlap,
+ showgraph = showgraph,
+ showgraphsingle = showgraphsingle,
+ showtext = showtext,
+ showtextsingle = showtextsingle,
+ stretchWidthSummary = stretchWidthSummary,
+ summaryReportFormat = summaryReportFormat,
+ unrotatePieces = unrotatePieces,
+ version = version,
+ verticalOffset = verticalOffset,
+ verticalTopText = verticalTopText,
+ zoomlevel = zoomlevel)
+
+# --------------------------------------------------------------------
+class GlobalMap(MapElement):
+ TAG=Element.MAP+'GlobalMap'
+ def __init__(self,map,node=None,
+ buttonText = '',
+ color = rgb(255,0,0),
+ hotkey = key('O',CTRL_SHIFT),
+ icon = '/images/overview.gif',
+ scale = 0.2,
+ tooltip = 'Show/Hide overview window'):
+ super(GlobalMap,self).\
+ __init__(map,self.TAG,node=node,
+ buttonText = buttonText,
+ color = color,
+ hotkey = hotkey,
+ icon = icon,
+ scale = scale,
+ tooltip = 'Show/Hide overview window')
+
+# --------------------------------------------------------------------
+class Zoomer(MapElement):
+ TAG = Element.MAP+'Zoomer'
+ def __init__(self,map,node=None,
+ inButtonText = '',
+ inIconName = '/images/zoomIn.gif',
+ inTooltip = 'Zoom in',
+ outButtonText = '',
+ outIconName = '/images/zoomOut.gif',
+ outTooltip = 'Zoom out',
+ pickButtonText = '',
+ pickIconName = '/images/zoom.png',
+ pickTooltip = 'Select Zoom',
+ zoomInKey = key('=',CTRL_SHIFT),
+ zoomLevels = [0.2,0.25,0.333,0.4,0.5,
+ 0.555,0.625,0.75,1.0,1.25,1.6],
+ zoomOutKey = key('-'),
+ zoomPickKey = key('='),
+ zoomStart = 3):
+ '''Zoom start is counting from the back (with default zoom levels,
+ and zoom start, the default zoom is 1'''
+ lvls = ','.join([str(z) for z in zoomLevels])
+ super(Zoomer,self).\
+ __init__(map,self.TAG,node=node,
+ inButtonText = inButtonText,
+ inIconName = inIconName,
+ inTooltip = inTooltip,
+ outButtonText = outButtonText,
+ outIconName = outIconName,
+ outTooltip = outTooltip,
+ pickButtonText = pickButtonText,
+ pickIconName = pickIconName,
+ pickTooltip = pickTooltip,
+ zoomInKey = zoomInKey,
+ zoomLevels = lvls,
+ zoomOutKey = zoomOutKey,
+ zoomPickKey = zoomPickKey,
+ zoomStart = zoomStart)
+
+# --------------------------------------------------------------------
+class HidePiecesButton(MapElement):
+ TAG=Element.MAP+'HidePiecesButton'
+ def __init__(self,map,node=None,
+ buttonText = '',
+ hiddenIcon = '/images/globe_selected.gif',
+ hotkey = key('O'),
+ showingIcon = '/images/globe_unselected.gif',
+ tooltip = 'Hide all pieces on this map'):
+ super(HidePiecesButton,self).\
+ __init__(map,self.TAG,node=node,
+ buttonText = buttonText,
+ hiddenIcon = hiddenIcon,
+ hotkey = hotkey,
+ showingIcon = showingIcon,
+ tooltip = tooltip)
+
# --------------------------------------------------------------------
-def get_chartwindows(doc,asdict=True):
- return get_asdict(doc,MODULE+'ChartWindow','name',asdict)
+class MassKey(GlobalKey,MapElementService):
+ TAG = Element.MAP+'MassKeyCommand'
+ def __init__(self,map,node=None,
+ name = '',
+ buttonHotkey = '',
+ hotkey = '',
+ buttonText = '',
+ canDisable = False,
+ deckCount = '-1',
+ filter = '',
+ propertyGate = '',
+ reportFormat = '',
+ reportSingle = False,
+ singleMap = True,
+ target = GlobalKey.SELECTED,
+ tooltip = '',
+ icon = ''):
+ '''Default targets are selected units'''
+ super(MassKey,self).\
+ __init__(map,self.TAG,node=node,
+ name = name,
+ buttonHotkey = buttonHotkey, # This hot key
+ hotkey = hotkey, # Target hot key
+ buttonText = buttonText,
+ canDisable = canDisable,
+ deckCount = deckCount,
+ filter = filter,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportSingle = reportSingle,
+ singleMap = singleMap,
+ target = target,
+ tooltip = tooltip,
+ icon = icon)
+
+
+# --------------------------------------------------------------------
+class Flare(MapElement):
+ TAG=Element.MAP+'Flare'
+ def __init__(self,map,node=None,
+ circleColor = rgb(255,0,0),
+ circleScale = True,
+ circleSize = 100,
+ flareKey = 'keyAlt',
+ flareName = 'Map Flare',
+ flarePulses = 6,
+ flarePulsesPerSec = 3,
+ reportFormat = ''):
+ super(Flare,self).__init__(map,self.TAG,node=node,
+ circleColor = circleColor,
+ circleScale = circleScale,
+ circleSize = circleSize,
+ flareKey = flareKey,
+ flareName = flareName,
+ flarePulses = flarePulses,
+ flarePulsesPerSec = flarePulsesPerSec,
+ reportFormat = '')
+
+# --------------------------------------------------------------------
+class AtStart(MapElement):
+ TAG = Element.MODULE+'map.SetupStack'
+ def __init__(self,map,node=None,
+ name = '',
+ location = '',
+ useGridLocation = True,
+ owningBoard = '',
+ x = 0,
+ y = 0):
+ '''Pieces are existing PieceSlot elements'''
+ super(AtStart,self).\
+ __init__(map,self.TAG,node=node,
+ name = name,
+ location = location,
+ owningBoard = owningBoard,
+ useGridLocation = useGridLocation,
+ x = x,
+ y = y)
+
+ def addPieces(self,*pieces):
+ '''Add a `Pieces` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Pieces
+ The added element
+ '''
+ # copy pieces here
+ copies = []
+ for p in pieces:
+ c = self.addPiece(p)
+ if c is not None:
+ copies.append(c)
+ return copies
+
+ def addPiece(self,piece):
+ '''Add a `Piece` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Piece
+ The added element
+ '''
+ if not isinstance(piece,PieceSlot):
+ print(f'Trying to add {type(piece)} to AtStart')
+ return None
+
+ p = piece.clone(self)
+ # self._node.appendChild(p._node)
+ return p
+
+ def getPieces(self,asdict=True):
+ '''Get all Piece element(s) from this
-# --------------------------------------------------------------------
-def get_map(doc,name):
- maps = get_maps(doc,True)
- return maps.get(name, None) if maps else None
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Piece`
+ elements. If `False`, return a list of all Piece`
+ children.
-# --------------------------------------------------------------------
-def get_boards(doc,asdict=True):
- return get_asdict(doc,PICKER+'Board','name',asdict)
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Piece` children
-# --------------------------------------------------------------------
-def get_zoned(doc,one=True):
- return get_asone(doc,PICKER+'board.ZonedGrid',one)
+ '''
+ return self.getElementsWithKey(PieceSlot,'entryName',asdict)
-# --------------------------------------------------------------------
-def get_zone(doc,asdict=True):
- return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict)
+#
+# EOF
+#
+# ====================================================================
+# From globalproperty.py
# --------------------------------------------------------------------
-def get_hexgrid(doc,one=True):
- return get_asone(doc,PICKER+'board.HexGrid',one)
+class GlobalProperties(Element):
+ TAG = Element.MODULE+'properties.GlobalProperties'
+ def __init__(self,elem,node=None,**named):
+ super(GlobalProperties,self).__init__(elem,self.TAG,node=node)
+
+ for n, p in named:
+ self.addProperty(n, **p)
+
+ def getGame(self):
+ return self.getParent(Game)
+ def addProperty(self,**kwargs):
+ '''Add a `Property` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Property
+ The added element
+ '''
+ return GlobalProperty(self,node=None,**kwargs)
+ def getProperties(self):
+ return getElementsByKey(GlobalProperty,'name')
+
# --------------------------------------------------------------------
-def get_squaregrid(doc,one=True):
- return get_asone(doc,PICKER+'board.SquareGrid',one)
+class GlobalProperty(Element):
+ TAG = Element.MODULE+'properties.GlobalProperty'
+ def __init__(self,elem,node=None,
+ name = '',
+ initialValue = '',
+ isNumeric = False,
+ min = "null",
+ max = "null",
+ wrap = False,
+ description = ""):
+ super(GlobalProperty,self).__init__(elem,self.TAG,
+ node = node,
+ name = name,
+ initialValue = initialValue,
+ isNumeric = isNumeric,
+ min = min,
+ max = max,
+ wrap = wrap,
+ description = description)
+
+ def getGlobalProperties(self):
+ return self.getParent(GlobalProperties)
-# --------------------------------------------------------------------
-def get_regiongrid(doc,one=True):
- return get_asone(doc,PICKER+'board.RegionGrid',one)
-# --------------------------------------------------------------------
-def get_hexnumbering(doc,one=True):
- return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one)
+#
+# EOF
+#
+# ====================================================================
+# From turn.py
+
+# --------------------------------------------------------------------
+class TurnLevel(Element):
+ def __init__(self,elem,tag,node=None,**kwargs):
+ super(TurnLevel,self).__init__(elem,tag,node=node,**kwargs)
+
+ def addLevel(self,counter=None,phases=None):
+ '''Add a `Level` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Level
+ The added element
+ '''
+ if counter is None and phases is None:
+ return self
-# --------------------------------------------------------------------
-def get_regions(doc,asdict=True):
- return get_asdict(doc,PICKER+'board.Region', 'name', asdict)
+ t = TurnCounter if counter is not None else TurnList
+ o = counter if counter is not None else phases
+
+ subcounter = o.pop('counter',None)
+ subphases = o.pop('phases',None)
+
+ s = t(self,node=None,**o)
+
+ return s.addLevel(subcounter, subphases)
+
+ def getUp(self):
+ return self.getParent(TurnLevel)
+ def addCounter(self,**kwargs):
+ '''Add a `Counter` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Counter
+ The added element
+ '''
+ return self.add(self,TurnCounter,**kwargs)
+ def addList(self,**kwargs):
+ '''Add a `List` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : List
+ The added element
+ '''
+ return self.add(self,TurnList,**kwargs)
+ def getCounter(self):
+ return self.getAllElements(TurnCounter)
+ def getList(self):
+ return self.getAllElements(TurnList)
+
+# --------------------------------------------------------------------
+class TurnTrack(TurnLevel):
+ TAG = Element.MODULE+'turn.TurnTracker'
+ def __init__(self,elem,node=None,
+ name = '',
+ buttonText = 'Turn',
+ hotkey = '',
+ icon = '',
+ length = -1,
+ lengthStyle = 'Maximum',
+ nexthotkey = key('T',ALT),
+ plusButtonSize = 22,
+ prevhotkey = key('T',ALT_SHIFT),
+ reportFormat = 'Turn updated from $oldTurn$ to $newTurn$',
+ turnButtonHeight = 22,
+ fwdOnly = True,
+ turnFormat = None,
+ counter = None,
+ phases = None):
+ levels = (counter if counter is not None else
+ phases if phases is not None else None)
+ if levels is not None:
+ lvl = 1
+ lvls = [f'$level{lvl}$']
+ sub = levels
+ while True:
+ sub = sub.get('counter',sub.get('phases',None))
+ if sub is None:
+ break
+ lvl += 1
+ lvls.append(f'$level{lvl}$')
+
+ turnFormat = ' '.join(lvls)
+
+ if turnFormat is None:
+ turnFormat = '$level1$ $level2$ $level3$ $level4$'
+
+ super(TurnTrack,self).__init__(elem, self.TAG,
+ node = node,
+ name = name,
+ buttonText = buttonText,
+ hotkey = hotkey,
+ icon = icon,
+ length = length,
+ lengthStyle = lengthStyle,
+ nexthotkey = nexthotkey,
+ plusButtonSize = plusButtonSize,
+ prevhotkey = prevhotkey,
+ reportFormat = reportFormat,
+ turnButtonHeight = turnButtonHeight,
+ turnFormat = turnFormat)
+
+ self.addLevel(counter=counter, phases=phases)
+
+ def getGame(self):
+ return self.getParent(Game)
+ def getLists(self,asdict=True):
+ '''Get all List element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `List`
+ elements. If `False`, return a list of all List`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `List` children
+
+ '''
+ return self.getElementsByKey(TurnList,'property',asdict=asdict)
+ def getCounters(self,asdict=True):
+ '''Get all Counter element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Counter`
+ elements. If `False`, return a list of all Counter`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Counter` children
+
+ '''
+ return self.getElementsByKey(TurnCounter,'property',asdict=asdict)
+ def addHotkey(self,**kwargs):
+ '''Add a `Hotkey` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Hotkey
+ The added element
+ '''
+ return self.add(TurnGlobalHotkey,**kwargs)
+ def getHotkeys(self,asdict):
+ return self.getElementsByKey(TurnGlobalHotkey,'name',asdict=asdict)
+ def encode(self):
+ ret = f'TURN{self["name"]}\t'
+
+ return []
+
# --------------------------------------------------------------------
-def get_squarenumbering(doc,one=True):
- return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one)
-
+class TurnCounter(TurnLevel):
+ TAG = Element.MODULE+"turn.CounterTurnLevel"
+ def __init__(self,elem,node=None,
+ property = '',
+ start = 1,
+ incr = 1,
+ loop = False,
+ loopLimit = -1,
+ turnFormat = "$value$"):
+ super(TurnCounter,self).__init__(elem,self.TAG,node=node,
+ property = property,
+ start = start,
+ incr = incr,
+ loop = loop,
+ loopLimit = loopLimit,
+ turnFormat = turnFormat)
+
# --------------------------------------------------------------------
-def get_pieces(doc,asdict=True):
- return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict)
-
+class TurnList(TurnLevel):
+ TAG = Element.MODULE+"turn.ListTurnLevel"
+ def __init__(self,elem,node=None,
+ property = '',
+ names = [],
+ configFirst = False,
+ configList = False,
+ turnFormat = '$value$'):
+ super(TurnList,self).\
+ __init__(elem,self.TAG,node=node,
+ property = property,
+ list = ','.join([str(p) for p in names]),
+ configFirst = configFirst,
+ configList = configList,
+ turnFormat = turnFormat)
+
# --------------------------------------------------------------------
-def get_prototypecontainer(doc,one=True):
- return get_asone(doc,MODULE+'PrototypesContainer',one)
+class TurnGlobalHotkey(Element):
+ TAG = Element.MODULE+'turn.TurnGlobalHotkey'
+ def __init__(self,elem,
+ node = None,
+ hotkey = '',
+ match = '{true}',
+ reportFormat = '',
+ name = ''):
+ '''Global key activated by turn change
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ hotkey : str
+ What to send (global command)
+ match : str
+ When to send
+ reportFormat : str
+ What to what
+ name : str
+ A free form name
+ '''
+ super(TurnGlobalHotkey,self).__init__(elem,self.TAG,
+ node = node,
+ hotkey = hotkey,
+ match = match,
+ reportFormat = reportFormat,
+ name = name)
+
+ def getTurnTrack(self):
+ '''Get the turn track'''
+ return self.getParent(TurnTrack)
-# --------------------------------------------------------------------
-def get_prototypes(doc,asdict=True):
- return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict)
+#
+# EOF
+#
+# ====================================================================
+# From documentation.py
-# --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
- return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+# ====================================================================
+def createKeyHelp(*args,**kwargs):
+ '''Creates a help file with key-bindings
+ See Documentation.createKeyHelp
+ '''
+ return Documentation.createKeyHelp(*args,**kwargs)
+
+# --------------------------------------------------------------------
+class Documentation(GameElement):
+ TAG=Element.MODULE+'Documentation'
+ def __init__(self,doc,node=None,**kwargs):
+ '''Documentation (or help menu)
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ kwargs : dict
+ Attributes
+ '''
+ super(Documentation,self).__init__(doc,self.TAG,node=node,**kwargs)
+
+ def addAboutScreen(self,**kwargs):
+ '''Add a `AboutScreen` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : AboutScreen
+ The added element
+ '''
+ return self.add(AboutScreen,**kwargs)
+ def addHelpFile(self,**kwargs):
+ '''Add a `HelpFile` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : HelpFile
+ The added element
+ '''
+ return self.add(HelpFile,**kwargs)
+ def addBrowserHelpFile(self,**kwargs):
+ '''Add a `BrowserHelpFile` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : BrowserHelpFile
+ The added element
+ '''
+ return self.add(HelpBrowserFile,**kwargs)
+ def addBrowserPDFFile(self,**kwargs):
+ '''Add a `BrowserPDFFile` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : BrowserPDFFile
+ The added element
+ '''
+ return self.add(BrowserPDFFile,**kwargs)
+ def addTutorial(self,**kwargs):
+ '''Add a `Tutorial` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Tutorial
+ The added element
+ '''
+ return self.add(Tutorial,**kwargs)
+ def getAboutScreens(self):
+ return self.getElementsByKey(AboutScreen,'title')
+ def getHelpFiles(self):
+ return self.getElementsByKey(HelpFile,'title')
+ def getBrowserHelpFiles(self):
+ return self.getElementsByKey(BrowserHelpFile,'title')
+ def getBrowserPDFFiles(self):
+ return self.getElementsByKey(BrowserPDFFile,'title')
+ def getTutorials(self):
+ return self.getElementsByKey(Tutorial,'name')
+
+ @classmethod
+ def createKeyHelp(cls,keys,title='',version=''):
+ '''Creates a help file with key-bindings
+
+ Parameters
+ ----------
+ keys : list of list of str
+ List of key-binding documentation
+ title : str
+ Title of help file
+ version : str
+ Version number
+
+ Returns
+ -------
+ txt : str
+ File content
+ '''
+ txt = f'''
+ <html>
+ <body>
+ <h1>{title} (Version {version}) Key bindings</h1>
+ <table>
+ <tr><th>Key</th><th>Where</th><th>Effect</th></tr>'''
+
+ for key, where, description in keys:
+ txt += (f'<tr><td>{key}</td>'
+ f'<td>{where}</td>'
+ f'<td>{description}</td></tr>')
+
+ txt += '''
+ </table>
+ </body>
+ </html>'''
+
+ return txt
+
+
+# --------------------------------------------------------------------
+class AboutScreen(Element):
+ TAG = Element.MODULE+'documentation.AboutScreen'
+ def __init__(self,doc,node=None,title='',fileName=""):
+ '''Create an about screen element that shows image
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ title : str
+ Entry title
+ fileName : str
+ Internal file name
+ '''
+ super(AboutScreen,self).__init__(doc,
+ self.TAG,
+ node = node,
+ fileName = fileName,
+ title = title)
+ def getDocumentation(self):
+ '''Get Parent element'''
+ return self.getParent(Documentation)
+
+# --------------------------------------------------------------------
+class BrowserPDFFile(Element):
+ TAG = Element.MODULE+'documentation.BrowserPDFFile'
+ def __init__(self,doc,node=None,title='',pdfFile=''):
+ '''Create help menu item that opens an embedded PDF
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ title : str
+ Entry title
+ pdfFile : str
+ Internal file name
+ '''
+ super(BrowserPDFFile,self).__init__(doc,self.TAG,
+ node = node,
+ pdfFile = pdfFile,
+ title = title)
+ def getDocumentation(self):
+ '''Get Parent element'''
+ return self.getParent(Documentation)
+
+# --------------------------------------------------------------------
+class HelpFile(Element):
+ TAG = Element.MODULE+'documentation.HelpFile'
+ ARCHIVE = 'archive'
+ def __init__(self,doc,node=None,
+ title='',
+ fileName='',
+ fileType=ARCHIVE):
+ '''Create a help menu entry that opens an embeddded file
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ title : str
+ Entry title
+ fileName : str
+ Internal file name
+ fileType : str
+ How to find the file
+ '''
+ super(HelpFile,self).__init__(doc,self.TAG,node=node,
+ fileName = fileName,
+ fileType = fileType,
+ title = title)
+
+ def getDocumentation(self):
+ '''Get Parent element'''
+ return self.getParent(Documentation)
+
+# --------------------------------------------------------------------
+class BrowserHelpFile(Element):
+ TAG = Element.MODULE+'documentation.BrowserHelpFile'
+ def __init__(self,doc,node=None,
+ title='',
+ startingPage='index.html'):
+ '''Create a help menu entry that opens an embeddded HTML
+ page (with possible sub-pages) file
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ title : str
+ Entry title
+ startingPage : str
+ which file to start at
+ '''
+ super(BrowserHelpFile,self).__init__(doc,self.TAG,node=node,
+ startingPage=startingPage,
+ title=title)
+
+ def getDocumentation(self):
+ '''Get Parent element'''
+ return self.getParent(Documentation)
+
# --------------------------------------------------------------------
-def get_masskey(map,asdict=True):
- return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+class Tutorial(Element):
+ TAG = Element.MODULE+'documentation.Tutorial'
+ def __init__(self,doc,node=None,
+ name = 'Tutorial',
+ logfile = 'tutorial.vlog',
+ promptMessage = 'Load the tutorial?',
+ welcomeMessage = 'Press "Step forward" (PnDn) to step through the tutorial',
+ launchOnStartup = True):
+ '''Add a help menu item that loads the tutorial
+
+ Also adds the start-up option to run the tutorial
+
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ name : str
+ Name of entry
+ logfile : str
+ Internal file name
+ promptMessage : str
+ What to show
+ launchOnStartup : bool
+ By default, launch tutorial first time running module
+ '''
+ super(Tutorial,self).__init__(doc,self.TAG,node=node,
+ name = name,
+ logfile = logfile,
+ promptMessage = promptMessage,
+ welcomeMessage = welcomeMessage,
+ launchOnStartup = launchOnStartup)
+
+ def getDocumentation(self):
+ '''Get Parent element'''
+ return self.getParent(Documentation)
+
+#
+# EOF
+#
+# ====================================================================
+# From player.py
# --------------------------------------------------------------------
-def get_piece_parts(code):
- '''Takes the code part of a pieceslot (the text node) and decodes
- it into traits.
+class PlayerRoster(GameElement):
+ TAG = Element.MODULE+'PlayerRoster'
+ def __init__(self,doc,node=None,buttonKeyStroke='',
+ buttonText='Retire',
+ buttonToolTip='Switch sides, become observer, or release faction'):
+ '''Add a player roster to the module
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ buttonText : str
+ Text on button
+ buttonTooltip : str
+ Tool tip to show when hovering over button
+ '''
+ super(PlayerRoster,self).__init__(doc,self.TAG,node=node,
+ buttonKeyStroke = buttonKeyStroke,
+ buttonText = buttonText,
+ buttonToolTip = buttonToolTip)
+ def addSide(self,name):
+ '''Add a `Side` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Side
+ The added element
+ '''
+ return self.add(PlayerSide,name=name)
+ def getSides(self):
+ '''Get all sides'''
+ return self.getAllElements(PlayerSide,False)
+ def encode(self):
+ '''Encode for save'''
+ return ['PLAYER\ta\ta\t<observer>']
+
+# --------------------------------------------------------------------
+class PlayerSide(Element):
+ TAG = 'entry'
+ def __init__(self,doc,node=None,name=''):
+ '''Adds a side to the player roster
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ name : str
+ Name of side
+ '''
+ super(PlayerSide,self).__init__(doc,self.TAG,node=node)
+ if node is None:
+ self.addText(name)
+
+ def getPlayerRoster(self):
+ '''Get Parent element'''
+ return self.getParent(PlayerRoster)
+
- One can use this to modify the pieces. One could for example
- remove a trait by calling this function, and remove the element
- corresponding to the trait from the returned list. Then one must
- collect up the definitions and states separately and pass each of
- them to `enc_parts', which gives us two strings that can be passed
- to `add_piece' (or `add_proto') function to get the new text node
- content of the piece slot or prototype definition. For example
+#
+# EOF
+#
+# ====================================================================
+# From chessclock.py
- piece = get_pieceslots(root,pieces)['foo']
- code = piece.childNodes[0].nodeValue
- traits = get_piece_parts(code)
- ... # Modify the list
+# ====================================================================
+class ChessClock(Element):
+ TAG=Element.MODULE+'chessclockcontrol.ChessClock'
+ def __init__(self,
+ doc,
+ node = None,
+ icon = '',
+ description = '',
+ side = '',
+ tooltip = 'Individual clock control',
+ buttonText = '',
+ startHotkey = '',
+ stopHotkey = '',
+ tickingBackgroundColor = rgb(255,255,0),
+ tickingFontColor = rgb(0,0,0),
+ tockingFontColor = rgb(51,51,51)):
+ '''Individual clock for a side
+
+ When the clock is running, the background colour may be
+ changed, and the colour of the numbers alternate between
+ `tickingFontColor` and `tockingFontColor`.
+
+ Parameters
+ ----------
+ doc : Element
+ Parent element
+ node : xml.dom.Element
+ Read from this node
+ icon : str
+ File name of button icon
+ description : str
+ Note on this clock
+ side : str
+ Name of side this clock belongs to
+ tooltop : str
+ Hover help text
+ buttonText : str
+ Text on button
+ startHotkey : str (key code)
+ Key or command to start timer
+ stopHotkey : str (key code)
+ Key or command to stop timer
+ tickingBackgroundColor : str (color)
+ Background color of time display when clock is running
+ tickingFontColor : str (color)
+ First color of numbers in display when clock is running.
+ tockingFontColor : str (color)
+ Second color of numbers in display when clock is running.
+ '''
+ super(ChessClock,self).__init__(#ChessClock
+ doc,
+ self.TAG,
+ node = node,
+ icon = icon,
+ description = description,
+ side = side,
+ tooltip = tooltip,
+ buttonText = buttonText,
+ startHotkey = startHotkey,
+ stopHotkey = stopHotkey,
+ tickingBackgroundColor = tickingBackgroundColor,
+ tickingFontColor = tickingFontColor,
+ tockingFontColor = tockingFontColor)
+
+ def getControl(self):
+ '''Get Parent element'''
+ return self.getParent(ChessClockControl)
+
+# ====================================================================
+class ChessClockControl(GameElement):
+ TAG=Element.MODULE+'ChessClockControl'
+ ALWAYS = 'Always'
+ AUTO = 'Auto'
+ NEVER = 'Never'
+ def __init__(self,
+ doc,
+ node = None,
+ name = 'Chess clock',
+ description = '',
+ buttonIcon = 'chess_clock.png',
+ buttonText = '',
+ buttonTooltip = 'Show/stop/hide chess clocks',
+ showHotkey = key('U',ALT),
+ pauseHotkey = key('U',CTRL_SHIFT),
+ nextHotkey = key('U'),
+ startOpponentKey = '',
+ showTenths = AUTO,
+ showSeconds = AUTO,
+ showHours = AUTO,
+ showDays = AUTO,
+ allowReset = False,
+ addClocks = True):
+ '''A set of chess clocs
+
+ Parameters
+ ----------
+ doc : Element
+ Parent
+ node : xml.dom.Element
+ Node to read state from
+ name : str
+ Name of clock control
+ description : str
+ Note on the chess clocks control
+ buttonIcon : str
+ Icon file name for button (chess_clock.png)
+ buttonText : str
+ Text on button
+ buttonTooltip : str
+ Hower help
+ showHotkey : str (key code)
+ Show or hide interface hot key
+ nextHotkey : str (key code)
+ Start the next clock hot key
+ pauseHotkey : str (key code)
+ Pause all clocks hot key
+ startOpponentKey : str (key code)
+ Start opponens clock
+ showTenths : one of AUTO, ALWAYS, NEVER
+ Whether to show tenths of seconds
+ showSeconds : one of AUTO, ALWAYS, NEVER
+ Whether to show seconds in clock
+ showHours : one of AUTO, ALWAYS, NEVER
+ Whether to show hours in clock
+ showDays : one of AUTO, ALWAYS, NEVER
+ Whether to show days in clock
+ allowReset : boolean
+ If true, allow manual reset of all clocks
+ '''
+ super(ChessClockControl,self).__init__(# ChessclockControl
+ doc,
+ self.TAG,
+ node = node,
+ name = name,
+ description = description,
+ buttonIcon = buttonIcon,
+ buttonText = buttonText,
+ buttonTooltip = buttonTooltip,
+ showHotkey = showHotkey,
+ pauseHotkey = pauseHotkey,
+ nextHotkey = nextHotkey,
+ startOpponentKey = startOpponentKey,
+ showTenths = showTenths,
+ showSeconds = showSeconds,
+ showHours = showHours,
+ showDays = showDays,
+ allowReset = allowReset)
+ print(node,addClocks)
+ if node is not None or not addClocks:
+ return
+
+ print('--- Will add clocks')
+ game = self.getGame()
+ roster = game.getPlayerRoster()[0]
+ sides = roster.getSides()
+ for side in sides:
+ name = side.getText()
+ self.addClock(side = name,
+ tooltip = f'Clock for {name}',
+ buttonText = name,
+ startHotkey = key('U'),
+ stopHotkey = key('U'))
- # Separate out definitions and states
- defs = [enc_def(e['def']) for e in traits]
- states = [enc_def(e['state']) for e in traits]
+ def addClock(self,**kwargs):
+ '''Add a clock element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : AboutScreen
+ The added element
+ '''
+ return self.add(ChessClock,**kwargs)
+ def getClocks(self,asdict=True):
+ '''Return dictionary of clocs'''
+ return self.getElementsByKey(ChessClock,'side',asdict)
- # Encode definitions and states as a single string, and then form body
- def = enc_parts(*defs)
- state = enc_parts(*states)
- body = add_piece(def,state)
- # Set new definition
- piece.childNodes[0].nodeValue = body
+#
+# EOF
+#
+# ====================================================================
+# From widget.py
+
+# --------------------------------------------------------------------
+class WidgetElement:
+ def __init__(self):
+ pass
+
+ def addTabs(self,**kwargs):
+ '''Add a `Tabs` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Tabs
+ The added element
+ '''
+ return self.add(TabWidget,**kwargs)
+ def addCombo(self,**kwargs):
+ '''Add a drop-down menu to this
+
+ Parameters
+ ----------
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Combo
+ The added element
+ '''
+ return self.add(ComboWidget,**kwargs)
+ def addPanel(self,**kwargs):
+ '''Add a `Panel` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Panel
+ The added element
+ '''
+ return self.add(PanelWidget,**kwargs)
+ def addList(self,**kwargs):
+ '''Add a `List` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : List
+ The added element
+ '''
+ return self.add(ListWidget,**kwargs)
+ def addMapWidget(self,**kwargs):
+ '''Add a `MapWidget` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : MapWidget
+ The added element
+ '''
+ return self.add(MapWidget,**kwargs)
+ def addChart(self,**kwargs):
+ '''Add a `Chart` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Chart
+ The added element
+ '''
+ return self.add(Chart,**kwargs)
+ def addPieceSlot(self,**kwargs):
+ '''Add a `PieceSlot` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PieceSlot
+ The added element
+ '''
+ return self.add(PieceSlot,**kwargs)
+ def addPiece(self,piece):
+ '''Add a `Piece` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Piece
+ The added element
+ '''
+ if not isinstance(piece,PieceSlot):
+ print(f'Trying to add {type(piece)} to ListWidget')
+ return None
+
+ p = piece.clone(self)
+ return p
+ def getTabs(self,asdict=True):
+ '''Get all Tab element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Tab` elements. If `False`, return a list of all Tab` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Tab` children
+ '''
+ return self.getElementsByKey(TabWidget,'entryName',asdict)
+ def getCombos(self,asdict=True):
+ '''Get all Combo element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Tab` elements. If `False`, return a list of all Tab` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Tab` children
+ '''
+ return self.getElementsByKey(ComboWidget,'entryName',asdict)
+ def getLists(self,asdict=True):
+ '''Get all List element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `List` elements. If `False`, return a list of all List` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `List` children
+ '''
+ return self.getElementsByKey(ListWidget,'entryName',asdict)
+ def getPanels(self,asdict=True):
+ '''Get all Panel element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Panel` elements. If `False`, return a list of all Panel` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Panel` children
+ '''
+ return self.getElementsByKey(PanelWidget,'entryName',asdict)
+ def getMapWidgets(self,asdict=True):
+ '''Get all MapWidget element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `MapWidget` elements. If `False`, return a list of all MapWidget` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `MapWidget` children
+ '''
+ return self.getElementsByKey(MapWidget,'entryName',asdict)
+ def getCharts(self,asdict=True):
+ '''Get all Chart element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Chart` elements. If `False`, return a list of all Chart` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Chart` children
+ '''
+ return self.getElementsByKey(Chart,'chartName',asdict)
+ def getPieceSlots(self,asdict=True):
+ '''Get all PieceSlot element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `PieceSlot` elements. If `False`, return a list of all PieceSlot` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `PieceSlot` children
+ '''
+ return self.getElementsByKey(PieceSlot,'entryName',asdict)
+
+# --------------------------------------------------------------------
+class PieceWindow(GameElement,WidgetElement):
+ TAG=Element.MODULE+'PieceWindow'
+ def __init__(self,elem,node=None,
+ name = '',
+ defaultWidth = 0,
+ hidden = False,
+ hotkey = key('C',ALT),
+ scale = 1.,
+ text = '',
+ tooltip = 'Show/hide piece window',
+ icon = '/images/counter.gif'):
+ super(PieceWindow,self).__init__(elem,self.TAG,node=node,
+ name = name,
+ defaultWidth = defaultWidth,
+ hidden = hidden,
+ hotkey = hotkey,
+ scale = scale,
+ text = text,
+ tooltip = tooltip,
+ icon = icon)
+
+# --------------------------------------------------------------------
+class ComboWidget(Element,WidgetElement):
+ TAG=Element.WIDGET+'BoxWidget'
+ def __init__(self,elem,node=None,entryName='',width=0,height=0):
+ super(ComboWidget,self).__init__(elem,
+ self.TAG,
+ node = node,
+ entryName = entryName,
+ width = width,
+ height = height)
- See also dicts_to_piece
+# --------------------------------------------------------------------
+class TabWidget(Element,WidgetElement):
+ TAG=Element.WIDGET+'TabWidget'
+ def __init__(self,elem,node=None,entryName=''):
+ super(TabWidget,self).__init__(elem,
+ self.TAG,
+ node = node,
+ entryName = entryName)
+
+# --------------------------------------------------------------------
+class ListWidget(Element,WidgetElement):
+ TAG=Element.WIDGET+'ListWidget'
+ def __init__(self,elem,node = None,
+ entryName = '',
+ height = 0,
+ width = 300,
+ scale = 1.,
+ divider = 150):
+ super(ListWidget,self).__init__(elem,self.TAG,node=node,
+ entryName = entryName,
+ height = height,
+ width = width,
+ scale = scale,
+ divider = divider)
+
+# --------------------------------------------------------------------
+class PanelWidget(Element,WidgetElement):
+ TAG=Element.WIDGET+'PanelWidget'
+ def __init__(self,elem,node=None,
+ entryName = '',
+ fixed = False,
+ nColumns = 1,
+ vert = False):
+ super(PanelWidget,self).__init__(elem,self.TAG,node=node,
+ entryName = entryName,
+ fixed = fixed,
+ nColumns = nColumns,
+ vert = vert)
+
+# --------------------------------------------------------------------
+class MapWidget(Element):
+ TAG=Element.WIDGET+'MapWidget'
+ def __init__(self,elem,node=None,entryName=''):
+ super(MapWidget,self).__init__(elem,self.TAG,
+ node = node,
+ entryName = entryName)
+
+ def addWidgetMap(self,**kwargs):
+ '''Add a `WidgetMap` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : WidgetMap
+ The added element
+ '''
+ return self.add(WidgetMap,**kwargs)
+ def getWidgetMaps(self,asdict=True):
+ '''Get all WidgetMap element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `WidgetMap` elements. If `False`, return a list of all WidgetMap` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `WidgetMap` children
+ '''
+ return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
- Parameters
- ----------
- code : str
- Text node content of <PieceSlot> or <PrototypeDefinition> tags
+#
+# EOF
+#
+# ====================================================================
+# From grid.py
+
+# --------------------------------------------------------------------
+HEX_WIDTH = 88.50779626676963
+HEX_HEIGHT = 102.2
+RECT_WIDTH = 80
+RECT_HEIGHT = 80
+# --------------------------------------------------------------------
+class BaseGrid(Element):
+ def __init__(self,zone,tag,node=None,
+ color = rgb(0,0,0),
+ cornersLegal = False,
+ dotsVisible = False,
+ dx = HEX_WIDTH, # Meaning seems reversed!
+ dy = HEX_HEIGHT,
+ edgesLegal = False,
+ sideways = False,
+ snapTo = True,
+ visible = True,
+ x0 = 0,
+ y0 = 32):
+ super(BaseGrid,self).__init__(zone,tag,node=node,
+ color = color,
+ cornersLegal = cornersLegal,
+ dotsVisible = dotsVisible,
+ dx = dx,
+ dy = dy,
+ edgesLegal = edgesLegal,
+ sideways = sideways,
+ snapTo = snapTo,
+ visible = visible,
+ x0 = x0,
+ y0 = y0)
+ def getZone(self):
+ z = self.getParent(Zone)
+ return z
+ def getZonedGrid(self):
+ z = self.getZone()
+ if z is not None:
+ return z.getZonedGrid()
+ return None
+ def getBoard(self):
+ z = self.getZonedGrid()
+ if z is not None:
+ return z.getBoard()
+ return self.getParent(Board)
+ def getPicker(self):
+ z = self.getBoard()
+ if z is not None:
+ return z.getPicker()
+ return None
+ def getMap(self):
+ b = self.getPicker()
+ if b is not None:
+ return b.getMap()
+ return None
+ def getNumbering(self):
+ pass
+ def getLocation(self,loc):
+ numbering = self.getNumbering()
+ if numbering is None or len(numbering) < 1:
+ return None
+
+ return numbering[0].getLocation(loc)
- Returns
- -------
- traits : list of dict
- All the traits in a list. Each element of the list is a
- dictionary with the keys 'def' and 'status'.
+# --------------------------------------------------------------------
+class BaseNumbering(Element):
+ def __init__(self,grid,tag,node=None,
+ color = rgb(255,0,0),
+ first = 'H',
+ fontSize = 24,
+ hDescend = False,
+ hDrawOff = 0,
+ hLeading = 1,
+ hOff = 0,
+ hType = 'A',
+ locationFormat = '$gridLocation$',
+ rotateText = 0,
+ sep = '',
+ stagger = True,
+ vDescend = False,
+ vDrawOff = 32,
+ vLeading = 0,
+ vOff = 0,
+ vType = 'N',
+ visible = True):
+ super(BaseNumbering,self).__init__(grid,tag,node=node,
+ color = color,
+ first = first,
+ fontSize = fontSize,
+ hDescend = hDescend,
+ hDrawOff = hDrawOff,
+ hLeading = hLeading,
+ hOff = hOff,
+ hType = hType,
+ locationFormat = locationFormat,
+ rotateText = rotateText,
+ sep = sep,
+ stagger = stagger,
+ vDescend = vDescend,
+ vDrawOff = vDrawOff,
+ vLeading = vLeading,
+ vOff = vOff,
+ vType = vType,
+ visible = visible)
+ def getGrid(self): return getParent(BaseGrid)
+
+ def _getMatcher(self,tpe,leading):
+ if tpe == 'A':
+ return \
+ '-?(?:A+|B+|C+|D+|E+|F+|G+|H+|I+|' + \
+ 'J+|K+|L+|M+|N+|O+|P+|Q+|R+|S+|T+|' + \
+ 'U+|V+|W+|X+|Y+|Z+)'
+
+ return f'-?[0-9]{{{int(leading)+1},}}'
+
+ def _getIndex(self,name,tpe):
+ if tpe == 'A':
+ negative = name.startswith('-')
+ if negative:
+ name = name[1:]
+
+ value = 0
+ for num,let in enumerate(name):
+ if not let.isupper():
+ continue
+ if num < len(name) - 1:
+ value += 26
+ else:
+ value += ord(let)-ord('A')
+
+ if negative:
+ value *= -1
+
+ return value
+
+ return int(name)
+
+ def _getCenter(self,col,row):
+ '''Convert col and row index to picture coordinates'''
+ print('Dummy GetCenter')
+ pass
+
+ def getLocation(self,loc):
+ '''Get picture coordinates from grid location'''
+ from re import match
+
+ first = self['first']
+ vType = self['vType']
+ hType = self['hType']
+ vOff = int(self['vOff'])
+ hOff = int(self['hOff'])
+ colPat = self._getMatcher(hType,self['hLeading'])
+ rowPat = self._getMatcher(vType,self['vLeading'])
+ patts = ((colPat,rowPat) if first == 'H' else (rowPat,colPat))
+ colGrp = 1 if first == 'H' else 2
+ rowGrp = 2 if first == 'H' else 1
+ patt = ''.join([f'({p})' for p in patts])
+ matched = match(patt,loc)
+ if not matched:
+ return None
+
+ rowStr = matched[rowGrp]
+ colStr = matched[colGrp]
+ rowNum = self._getIndex(rowStr,vType)
+ colNum = self._getIndex(colStr,hType)
+
+ return self._getCenter(colNum-hOff, rowNum-vOff);
+
+
+# --------------------------------------------------------------------
+class HexGrid(BaseGrid):
+ TAG = Element.BOARD+'HexGrid'
+ def __init__(self,zone,node=None,**kwargs):
+ super(HexGrid,self).__init__(zone,self.TAG,node=node,**kwargs)
+
+ def addNumbering(self,**kwargs):
+ '''Add a `Numbering` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Numbering
+ The added element
+ '''
+ return self.add(HexNumbering,**kwargs)
+ def getNumbering(self):
+ return self.getAllElements(HexNumbering)
+ def getDeltaX(self):
+ return float(self['dx'])
+ def getDeltaY(self):
+ return float(self['dy'])
+ def getXOffset(self):
+ return int(self['x0'])
+ def getYOffset(self):
+ return int(self['y0'])
+ def getMaxRows(self):
+ from math import floor
+ height = self.getZone().getHeight()
+ return floor(height / self.getDeltaX() + .5)
+ def getMaxCols(self):
+ from math import floor
+ width = self.getZone().getWidth()
+ return floor(width / self.getDeltaY() + .5)
+
+# --------------------------------------------------------------------
+class SquareGrid(BaseGrid):
+ TAG = Element.BOARD+'SquareGrid'
+ def __init__(self,zone,node=None,
+ dx = RECT_WIDTH,
+ dy = RECT_HEIGHT,
+ edgesLegal = False,
+ x0 = 0,
+ y0 = int(0.4*RECT_HEIGHT),
+ **kwargs):
+ super(SquareGrid,self).__init__(zone,self.TAG,node=node,
+ dx = dx,
+ dy = dy,
+ edgesLegal = edgesLegal,
+ x0 = 0,
+ y0 = int(0.4*RECT_HEIGHT),
+ **kwargs)
+ def addNumbering(self,**kwargs):
+ '''Add a `Numbering` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Numbering
+ The added element
+ '''
+ return self.add(SquareNumbering,**kwargs)
+ def getNumbering(self):
+ return self.getAllElements(SquareNumbering)
+ def getDeltaX(self):
+ return float(self['dx'])
+ def getDeltaY(self):
+ return float(self['dy'])
+ def getXOffset(self):
+ return int(self['x0'])
+ def getYOffset(self):
+ return int(self['y0'])
+ def getMaxRows(self):
+ from math import floor
+ height = self.getZone().getHeight()
+ return floor(height / self.getDeltaY() + .5)
+ def getMaxCols(self):
+ from math import floor
+ width = self.getZone().getWidth()
+ return floor(width / self.getDeltaX() + .5)
+
+# --------------------------------------------------------------------
+class HexNumbering(BaseNumbering):
+ TAG = Element.BOARD+'mapgrid.HexGridNumbering'
+ def __init__(self,grid,node=None,**kwargs):
+ super(HexNumbering,self).__init__(grid,self.TAG,node=node,**kwargs)
+ def getGrid(self):
+ g = self.getParent(HexGrid)
+ return g
+
+ def _getCenter(self,col,row):
+ '''Convert col and row index to picture coordinates'''
+ from math import floor
+
+ stagger = self['stagger'] == 'true'
+ sideways = self.getGrid()['sideways'] == 'true'
+ hDesc = self['hDescend'] == 'true'
+ vDesc = self['vDescend'] == 'true'
+ xOff = self.getGrid().getXOffset()
+ yOff = self.getGrid().getYOffset()
+ hexW = self.getGrid().getDeltaX()
+ hexH = self.getGrid().getDeltaY()
+ zxOff = self.getGrid().getZone().getXOffset()
+ zyOff = self.getGrid().getZone().getYOffset()
+ maxRows = self.getGrid().getMaxRows()
+ maxCols = self.getGrid().getMaxCols()
+ # print(f' Col: {col}')
+ # print(f' Row: {row}')
+ # print(f' Stagger: {stagger}')
+ # print(f' Sideways: {sideways}')
+ # print(f' hDesc: {hDesc}')
+ # print(f' vDesc: {vDesc}')
+ # print(f' maxRows: {maxRows}')
+ # print(f' maxCols: {maxCols}')
+
+ if sideways:
+ maxRows, maxCols = maxCols, maxRows
+
+ if stagger:
+ if sideways:
+ if col % 2 != 0:
+ row += 1 if hDesc else -1
+ else:
+ if col % 2 != 0:
+ row += 1 if vDesc else -1
+
+ if hDesc:
+ col = maxCols - col
+ if vDesc:
+ row = maxRows - row
+
+ x = col * hexW + xOff
+ y = row * hexH + yOff + (hexH/2 if col % 2 != 0 else 0)
+
+ x = int(floor(x + .5))
+ y = int(floor(y + .5))
+ if sideways:
+ # print(f'Swap coordinates because {sideways}')
+ x, y = y, x
+
+ return x,y
+
+# --------------------------------------------------------------------
+class SquareNumbering(BaseNumbering):
+ TAG = Element.BOARD+'mapgrid.SquareGridNumbering'
+ def __init__(self,grid,node=None,hType='N',**kwargs):
+ super(SquareNumbering,self).__init__(grid,self.TAG,node=node,
+ hType=hType,**kwargs)
+ def getGrid(self):
+ return self.getParent(SquareGrid)
+
+ def getCenter(self,col,row):
+ hDesc = self['hDescend'] == 'true'
+ vDesc = self['vDescend'] == 'true'
+ xOff = self.getGrid().getXOffset()
+ yOff = self.getGrid().getYOffset()
+ squareW = self.getGrid().getDeltaX()
+ squareH = self.getGrid().getDeltaY()
+ maxRows = self.getGrid().getMaxRows()
+ maxCols = self.getGrid().getMaxCols()
+
+ if vDesc: row = maxRows - row
+ if hDesc: col = maxCols - col
+
+ x = col * squareW + xOff
+ y = row * squareH + yOff
+
+ return x,y
+
+
+# --------------------------------------------------------------------
+class RegionGrid(Element):
+ TAG = Element.BOARD+'RegionGrid'
+ def __init__(self,zone,node=None,snapto=True,fontsize=9,visible=True):
+ super(RegionGrid,self).__init__(zone,self.TAG,node=node,
+ snapto = snapto,
+ fontsize = fontsize,
+ visible = visible)
+
+ def getZone(self):
+ return self.getParent(Zone)
+ def getZoneGrid(self):
+ z = self.getZone()
+ if z is not None:
+ return z.getBoard()
+ return None
+ def getBoard(self):
+ z = self.getZonedGrid()
+ if z is not None:
+ return z.getBoard()
+ return self.getParent(Board)
+ def getMap(self):
+ b = self.getBoard()
+ if b is not None:
+ return b.getMap()
+ return None
+ def getRegions(self):
+ return self.getElementsByKey(Region,'name')
+ def checkName(self,name):
+ '''Get unique name'''
+ poss = len([e for e in self.getRegions()
+ if e == name or e.startswith(name+'_')])
+ if poss == 0:
+ return name
+
+ return name + f'_{poss}'
+ def addRegion(self,**kwargs):
+ '''Add a `Region` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Region
+ The added element
+ '''
+ return self.add(Region,**kwargs)
+ def getLocation(self,loc):
+ for r in self.getRegions().values():
+ if loc == r['name']:
+ return int(r['originx']),int(r['originy'])
- The key 'def' points to a list of the trait attributes used to
- define the trait type. The first element of the 'def' list is
- the trait identifier. The rest are the arguments for that
- trait type. Note, all fields are encoded as strings.
+ return None
+
+# --------------------------------------------------------------------
+class Region(Element):
+ TAG = Element.BOARD+'Region'
+ def __init__(self,grid,node=None,
+ name = '',
+ originx = 0,
+ originy = 0,
+ alsoPiece = True,
+ piece = None,
+ prefix = ''):
+ fullName = name + ("@"+prefix if len(prefix) else "")
+ realName = grid.checkName(fullName) if node is None else fullName
+ super(Region,self).__init__(grid,
+ self.TAG,
+ node = node,
+ name = realName,
+ originx = originx,
+ originy = originy)
+
+ if node is None and alsoPiece:
+ m = self.getMap()
+ b = self.getBoard()
+ if m is not None and b is not None:
+ if piece is None:
+ g = m.getGame()
+ pieces = g.getSpecificPieces(name,asdict=False)
+ piece = pieces[0] if len(pieces) > 0 else None
+
+ if piece is not None:
+ # bname = m['mapName']
+ bname = b['name']
+ #print(f'Adding at-start name={name} location={realName} '
+ # f'owning board={bname}')
+ a = m.addAtStart(name = name,
+ location = realName,
+ useGridLocation = True,
+ owningBoard = bname,
+ x = 0,
+ y = 0)
+ p = a.addPiece(piece)
+ if p is None:
+ print(f'EEE Failed to add piece {name} ({piece}) to add-start {a}')
+ #if p is not None:
+ # print(f'Added piece {name} in region')
+ #else:
+ # print(f'Could not find piece {name}')
+
+ def getGrid(self):
+ return self.getParent(RegionGrid)
+ def getZone(self):
+ g = self.getGrid()
+ if g is not None:
+ return g.getZone()
+ return None
+ def getZonedGrid(self):
+ z = self.getZone()
+ if z is not None:
+ return z.getZonedGrid()
+ return None
+ def getBoard(self):
+ z = self.getZonedGrid()
+ if z is not None:
+ return z.getBoard()
+ return self.getParent(Board)
+ def getPicker(self):
+ z = self.getBoard()
+ if z is not None:
+ return z.getPicker()
+ return None
+ def getMap(self):
+ b = self.getPicker()
+ if b is not None:
+ return b.getMap()
+ return None
- The key 'status' encodes the status of the trait. The format
- of this depends on the trait.Note, all fields are encoded as
- strings.
+#
+# EOF
+#
+# ====================================================================
+# From zone.py
+
+# --------------------------------------------------------------------
+class ZonedGrid(Element):
+ TAG=Element.BOARD+'ZonedGrid'
+ def __init__(self,board,node=None):
+ super(ZonedGrid,self).__init__(board,self.TAG,node=node)
+
+ def getBoard(self):
+ b = self.getParent(Board)
+ # print(f'Get Board of Zoned: {b}')
+ return b
+ def getPicker(self):
+ z = self.getBoard()
+ if z is not None:
+ return z.getPicker()
+ return None
+ def getMap(self):
+ z = self.getPicker()
+ if z is not None:
+ return z.getMap()
+ return None
+ def addHighlighter(self,**kwargs):
+ '''Add a `Highlighter` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Highlighter
+ The added element
+ '''
+ return self.add(ZonedGridHighlighter,**kwargs)
+ def getHighlighters(self,single=True):
+ '''Get all or a sole `ZonedGridHighlighter` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Highligter` child, otherwise fail.
+ If `False` return all `Highligter` children in this element
+ Returns
+ -------
+ children : list
+ List of `Highligter` children (even if `single=True`)
+ '''
+ return self.getAllElements(ZonedGridHighliger,single=single)
+ def addZone(self,**kwargs):
+ '''Add a `Zone` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Zone
+ The added element
+ '''
+ return self.add(Zone,**kwargs)
+ def getZones(self,asdict=True):
+ '''Get all Zone element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Zone` elements. If `False`, return a list of all Zone` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Zone` children
+ '''
+ return self.getElementsByKey(Zone,'name',asdict=asdict)
+
+# --------------------------------------------------------------------
+class ZonedGridHighlighter(Element):
+ TAG=Element.BOARD+'mapgrid.ZonedGridHighlighter'
+ def __init__(self,zoned,node=None):
+ super(ZonedGridHighlighter,self).__init__(zoned,self.TAG,node=node)
+ def getZonedGrid(self): return self.getParent(ZonedGrid)
+
+
+# --------------------------------------------------------------------
+class Zone(Element):
+ TAG = Element.BOARD+'mapgrid.Zone'
+ def __init__(self,zoned,node=None,
+ name = "",
+ highlightProperty = "",
+ locationFormat = "$gridLocation$",
+ path = "0,0;976,0;976,976;0,976",
+ useHighlight = False,
+ useParentGrid = False):
+ super(Zone,self).\
+ __init__(zoned,self.TAG,node=node,
+ name = name,
+ highlightProperty = highlightProperty,
+ locationFormat = locationFormat,
+ path = path,
+ useHighlight = useHighlight,
+ useParentGrid = useParentGrid)
+
+ def getZonedGrid(self):
+ z = self.getParent(ZonedGrid)
+ # print(f'Get Zoned of Zone {self["name"]}: {z}')
+ return z
+
+ def getBoard(self):
+ z = self.getZonedGrid()
+ if z is not None:
+ return z.getBoard()
+ return None
+ def getPicker(self):
+ z = self.getBoard()
+ if z is not None:
+ return z.getPicker()
+ return None
+ def getMap(self):
+ z = self.getPicker()
+ if z is not None:
+ return z.getMap()
+ return None
+ def addHexGrid(self,**kwargs):
+ '''Add a `HexGrid` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : HexGrid
+ The added element
+ '''
+ return self.add(HexGrid,**kwargs)
+ def addSquareGrid(self,**kwargs):
+ '''Add a `SquareGrid` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : SquareGrid
+ The added element
+ '''
+ return self.add(SquareGrid,**kwargs)
+ def addRegionGrid(self,**kwargs):
+ '''Add a `RegionGrid` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : RegionGrid
+ The added element
+ '''
+ return self.add(RegionGrid,**kwargs)
+ def getHexGrids(self,single=True):
+ '''Get all or a sole `HexGrid` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `HexGrid` child, otherwise fail.
+ If `False` return all `HexGrid` children in this element
+ Returns
+ -------
+ children : list
+ List of `HexGrid` children (even if `single=True`)
+ '''
+ return self.getAllElements(HexGrid,single=single)
+ def getSquareGrids(self,single=True):
+ '''Get all or a sole `SquareGrid` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `SquareGrid` child, otherwise fail.
+ If `False` return all `SquareGrid` children in this element
+ Returns
+ -------
+ children : list
+ List of `SquareGrid` children (even if `single=True`)
+ '''
+ return self.getAllElements(SquareGrid,single=single)
+ def getRegionGrids(self,single=True):
+ '''Get all or a sole `RegionGrid` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `RegionGrid` child, otherwise fail.
+ If `False` return all `RegionGrid` children in this element
+ Returns
+ -------
+ children : list
+ List of `RegionGrid` children (even if `single=True`)
+ '''
+ return self.getAllElements(RegionGrid,single=single)
+ def getGrids(self,single=True):
+ '''Get all or a sole `Grid` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Grid` child, otherwise fail.
+ If `False` return all `Grid` children in this element
+ Returns
+ -------
+ children : list
+ List of `Grid` children (even if `single=True`)
+ '''
+ g = self.getHexGrids(single=single)
+ if g is not None: return g
+
+ g = self.getSquareGrids(single=single)
+ if g is not None: return g
+
+ g = self.getRegionGrids(single=single)
+ if g is not None: return g
+
+ return g
+
+ def getPath(self):
+ p = self['path'].split(';')
+ r = []
+ for pp in p:
+ c = pp.split(',')
+ r.append([int(c[0]),int(c[1])])
+ return r
+ def getBB(self):
+ from functools import reduce
+ path = self.getPath()
+ llx = reduce(lambda old,point:min(point[0],old),path,100000000000)
+ lly = reduce(lambda old,point:min(point[1],old),path,100000000000)
+ urx = reduce(lambda old,point:max(point[0],old),path,-1)
+ ury = reduce(lambda old,point:max(point[1],old),path,-1)
+ return llx,lly,urx,ury
+ def getWidth(self):
+ llx,_,urx,_ = self.getBB()
+ return urx-llx
+ def getHeight(self):
+ _,lly,_,ury = self.getBB()
+ return ury-lly
+ def getXOffset(self):
+ return self.getBB()[0]
+ def getYOffset(self):
+ return self.getBB()[1]
- '''
- cmd,iden,typ,sta = code.split('/')
- defs = typ.split(r' ')
- stas = sta.split(r' ')
- defs = [d.rstrip("\\").split(';') for d in defs]
- stas = [s.rstrip("\\").split(';') for s in stas]
- ret = [{'def': d, 'state': s} for d,s in zip(defs,stas)]
- return ret
+#
+# EOF
+#
+# ====================================================================
+# From board.py
+
+# --------------------------------------------------------------------
+class BoardPicker(MapElement):
+ TAG = Element.MAP+'BoardPicker'
+ def __init__(self,doc,node=None,
+ addColumnText = 'Add column',
+ addRowText = 'Add row',
+ boardPrompt = 'Select board',
+ slotHeight = 125,
+ slotScale = 0.2,
+ slotWidth = 350,
+ title = 'Choose Boards'):
+ super(BoardPicker,self).__init__(doc,self.TAG,node=node,
+ addColumnText = addColumnText,
+ addRowText = addRowText,
+ boardPrompt = boardPrompt,
+ slotHeight = slotHeight,
+ slotScale = slotScale,
+ slotWidth = slotWidth,
+ title = title)
+
+ def addSetup(self,**kwargs):
+ '''Add a `Setup` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Setup
+ The added element
+ '''
+ if 'mapName' not in kwargs:
+ m = self.getMap()
+ kwargs['mapName'] = m.getAttribute('mapName')
+
+ return self.add(Setup,**kwargs)
+ def getSetups(self,single=False):
+ '''Get all or a sole `Setup` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Setup` child, otherwise fail.
+ If `False` return all `Setup` children in this element
+ Returns
+ -------
+ children : list
+ List of `Setup` children (even if `single=True`)
+ '''
+ return self.getAllElements(Setup,single=single)
+ def addBoard(self,**kwargs):
+ '''Add a `Board` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Board
+ The added element
+ '''
+ return self.add(Board,**kwargs)
+ def getBoards(self,asdict=True):
+ '''Get all Board element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board` elements. If `False`, return a list of all Board` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+ '''
+ return self.getElementsByKey(Board,'name',asdict=asdict)
+ def encode(self):
+ setups = self.getSetups()
+ if setups is not None and len(setups)>0:
+ return [setups[0]._node.childNodes[0].nodeValue]
+
+ ret = []
+ for bn in self.getBoards().keys():
+ ret.append(bn+'BoardPicker\t'+bn+'\t0\t0')
+
+ return ret
+
+# --------------------------------------------------------------------
+class Setup(Element):
+ TAG = 'setup'
+ def __init__(self,picker,node=None,
+ mapName = '',
+ maxColumns = 1,
+ boardNames = []):
+ super(Setup,self).__init__(picker,self.TAG,node=node)
+ col = 0
+ row = 0
+ lst = [f'{mapName}BoardPicker']
+ for bn in boardNames:
+ lst.extend([bn,str(col),str(row)])
+ col += 1
+ if col >= maxColumns:
+ col = 0
+ row += 1
+
+ txt = r' '.join(lst)
+ self.addText(txt)
+ def getPicker(self): return self.getParent(BoardPicker)
+
# --------------------------------------------------------------------
-def dicts_to_piece(traits):
- # Separate out definitions and states
- defs = [enc_def(e['def']) for e in traits]
- states = [enc_def(e['state']) for e in traits]
+class Board(Element):
+ TAG = Element.PICKER+'Board'
+ def __init__(self,picker,node=None,
+ name = '',
+ image = '',
+ reversible = False,
+ color = rgb(255,255,255),
+ width = 0,
+ height = 0):
+ super(Board,self).__init__(picker,self.TAG,node=node,
+ image = image,
+ name = name,
+ reversible = reversible,
+ color = color,
+ width = width,
+ height = height)
+
+ def getPicker(self): return self.getParent(BoardPicker)
+ def getMap(self):
+ z = self.getPicker()
+ if z is not None:
+ return z.getMap()
+ return None
+ def addZonedGrid(self,**kwargs):
+ '''Add a `ZonedGrid` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : ZonedGrid
+ The added element
+ '''
+ return self.add(ZonedGrid,**kwargs)
+ def getZonedGrids(self,single=True):
+ '''Get all or a sole `ZonedGrid` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `ZonedGrid` child, otherwise fail.
+ If `False` return all `ZonedGrid` children in this element
+ Returns
+ -------
+ children : list
+ List of `ZonedGrid` children (even if `single=True`)
+ '''
+ return self.getAllElements(ZonedGrid,single=single)
+ def getZones(self,asdict=True):
+ '''Get all Zone element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Zone` elements. If `False`, return a list of all Zone` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Zone` children
+ '''
+ zoned = self.getZonedGrids(single=True)
+ if zoned is None: return None
+
+ return zoned[0].getZones(asdict=asdict)
+
+ def getWidth(self):
+ # print(f'Getting width of {self}: {self["width"]}')
+ if 'width' in self and int(self['width']) != 0:
+ return int(self['width'])
+ return 0
+
+ def getHeight(self):
+ # print(f'Getting height of {self}: {self["height"]}')
+ if 'height' in self and int(self['height']) != 0:
+ return int(self['height'])
+ return 0
- # Encode definitions and states as a single string, and then form body
- df = enc_parts(*defs)
- state = enc_parts(*states)
- body = add_piece(df,state)
- return body
-
-
+#
+# EOF
+#
# ====================================================================
-# Key encoding
-SHIFT = 65
-CTRL = 130
-ALT = 520
-CTRL_SHIFT = CTRL+SHIFT
-ALT_SHIFT = ALT+SHIFT
-NONE = '\ue004'
-NONE_MOD = 0
-def key(let,mod=CTRL):
-
- '''Encode a key sequence
+# From map.py
+
+# --------------------------------------------------------------------
+class BaseMap(Element):
+ def __init__(self,doc,tag,node=None,
+ mapName = '',
+ allowMultiple = 'false',
+ backgroundcolor = rgb(255,255,255),
+ buttonName = '',
+ changeFormat = '$message$',
+ color = rgb(0,0,0), # Selected pieces
+ createFormat = '$pieceName$ created in $location$ *',
+ edgeHeight = '0',
+ edgeWidth = '0',
+ hideKey = '',
+ hotkey = key('M',ALT),
+ icon = '/images/map.gif',
+ launch = 'false',
+ markMoved = 'Always',
+ markUnmovedHotkey = '',
+ markUnmovedIcon = '/images/unmoved.gif',
+ markUnmovedReport = '',
+ markUnmovedText = '',
+ markUnmovedTooltip = 'Mark all pieces on this map as not moved',
+ moveKey = '',
+ moveToFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
+ moveWithinFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
+ showKey = '',
+ thickness = '3'):
+ super(BaseMap,self).__init__(doc,tag,node=node,
+ allowMultiple = allowMultiple,
+ backgroundcolor = backgroundcolor,
+ buttonName = buttonName,
+ changeFormat = changeFormat,
+ color = color,
+ createFormat = createFormat,
+ edgeHeight = edgeHeight,
+ edgeWidth = edgeWidth,
+ hideKey = hideKey,
+ hotkey = hotkey,
+ icon = icon,
+ launch = launch,
+ mapName = mapName,
+ markMoved = markMoved,
+ markUnmovedHotkey = markUnmovedHotkey,
+ markUnmovedIcon = markUnmovedIcon,
+ markUnmovedReport = markUnmovedReport,
+ markUnmovedText = markUnmovedText,
+ markUnmovedTooltip = markUnmovedTooltip,
+ moveKey = moveKey,
+ moveToFormat = moveToFormat,
+ moveWithinFormat = moveWithinFormat,
+ showKey = showKey,
+ thickness = thickness)
+
+ def getGame(self):
+ '''Get the game'''
+ return self.getParentOfClass([Game])
+ def addPicker(self,**kwargs):
+ '''Add a `Picker` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Picker
+ The added element
+ '''
+ return self.add(BoardPicker,**kwargs)
+ def getBoardPicker(self,single=True):
+ '''Get all or a sole `BoardPicker` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `BoardPicker` child, otherwise fail.
+ If `False` return all `BoardPicker` children in this element
+ Returns
+ -------
+ children : list
+ List of `BoardPicker` children (even if `single=True`)
+ '''
+ return self.getAllElements(BoardPicker,single)
+ def getPicker(self,single=True):
+ '''Get all or a sole `BoardPicker` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `BoardPicker` child, otherwise fail.
+ If `False` return all `BoardPicker` children in this element
+ Returns
+ -------
+ children : list
+ List of `BoardPicker` children (even if `single=True`)
+ '''
+ return self.getAllElements(BoardPicker,single)
+ def getStackMetrics(self,single=True):
+ '''Get all or a sole `StackMetric` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `StackMetric` child, otherwise fail.
+ If `False` return all `StackMetric` children in this element
+ Returns
+ -------
+ children : list
+ List of `StackMetric` children (even if `single=True`)
+ '''
+ return self.getAllElements(StackMetrics,single)
+ def getImageSaver(self,single=True):
+ '''Get all or a sole `ImageSaver` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `ImageSaver` child, otherwise fail.
+ If `False` return all `ImageSaver` children in this element
+ Returns
+ -------
+ children : list
+ List of `ImageSaver` children (even if `single=True`)
+ '''
+ return self.getAllElements(ImageSaver,single)
+ def getTextSaver(self,single=True):
+ '''Get all or a sole `TextSaver` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `TextSaver` child, otherwise fail.
+ If `False` return all `TextSaver` children in this element
+ Returns
+ -------
+ children : list
+ List of `TextSaver` children (even if `single=True`)
+ '''
+ return self.getAllElements(TextSaver,single)
+ def getForwardToChatter(self,single=True):
+ '''Get all or a sole `ForwardToChatter` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `ForwardToChatter` child, otherwise fail.
+ If `False` return all `ForwardToChatter` children in this element
+ Returns
+ -------
+ children : list
+ List of `ForwardToChatter` children (even if `single=True`)
+ '''
+ return self.getAllElements(ForwardToChatter,single)
+ def getMenuDisplayer(self,single=True):
+ '''Get all or a sole `MenuDi` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `MenuDi` child, otherwise fail.
+ If `False` return all `MenuDi` children in this element
+ Returns
+ -------
+ children : list
+ List of `MenuDi` children (even if `single=True`)
+ '''
+ return self.getAllElements(MenuDisplayer,single)
+ def getMapCenterer(self,single=True):
+ '''Get all or a sole `MapCenterer` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `MapCenterer` child, otherwise fail.
+ If `False` return all `MapCenterer` children in this element
+ Returns
+ -------
+ children : list
+ List of `MapCenterer` children (even if `single=True`)
+ '''
+ return self.getAllElements(MapCenterer,single)
+ def getStackExpander(self,single=True):
+ '''Get all or a sole `StackExpander` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `StackExpander` child, otherwise fail.
+ If `False` return all `StackExpander` children in this element
+ Returns
+ -------
+ children : list
+ List of `StackExpander` children (even if `single=True`)
+ '''
+ return self.getAllElements(StackExpander,single)
+ def getPieceMover(self,single=True):
+ '''Get all or a sole `PieceMover` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `PieceMover` child, otherwise fail.
+ If `False` return all `PieceMover` children in this element
+ Returns
+ -------
+ children : list
+ List of `PieceMover` children (even if `single=True`)
+ '''
+ return self.getAllElements(PieceMover,single)
+ def getSelectionHighlighters(self,single=True):
+ '''Get all or a sole `SelectionHighlighter` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `SelectionHighlighter` child, otherwise fail.
+ If `False` return all `SelectionHighlighter` children in this element
+ Returns
+ -------
+ children : list
+ List of `SelectionHighlighter` children (even if `single=True`)
+ '''
+ return self.getAllElements(SelectionHighlighters,single)
+ def getKeyBufferer(self,single=True):
+ return self.getAllElements(KeyBufferer,single)
+ def getHighlightLastMoved(self,single=True):
+ '''Get all or a sole `HighlightLa` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `HighlightLa` child, otherwise fail.
+ If `False` return all `HighlightLa` children in this element
+ Returns
+ -------
+ children : list
+ List of `HighlightLa` children (even if `single=True`)
+ '''
+ return self.getAllElements(HighlightLastMoved,single)
+ def getCounterDetailViewer(self,single=True):
+ '''Get all or a sole `CounterDetailViewer` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `CounterDetailViewer` child, otherwise fail.
+ If `False` return all `CounterDetailViewer` children in this element
+ Returns
+ -------
+ children : list
+ List of `CounterDetailViewer` children (even if `single=True`)
+ '''
+ return self.getAllElements(CounterDetailViewer,single)
+ def getGlobalMap(self,single=True):
+ '''Get all or a sole `GlobalMap` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `GlobalMap` child, otherwise fail.
+ If `False` return all `GlobalMap` children in this element
+ Returns
+ -------
+ children : list
+ List of `GlobalMap` children (even if `single=True`)
+ '''
+ return self.getAllElements(GlobalMap,single)
+ def getZoomer(self,single=True):
+ '''Get all or a sole `Zoomer` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Zoomer` child, otherwise fail.
+ If `False` return all `Zoomer` children in this element
+ Returns
+ -------
+ children : list
+ List of `Zoomer` children (even if `single=True`)
+ '''
+ return self.getAllElements(Zoomer,single)
+ def getHidePiecesButton(self,single=True):
+ '''Get all or a sole `HidePiece` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `HidePiece` child, otherwise fail.
+ If `False` return all `HidePiece` children in this element
+ Returns
+ -------
+ children : list
+ List of `HidePiece` children (even if `single=True`)
+ '''
+ return self.getAllElements(HidePiecesButton,single)
+ def getMassKeys(self,asdict=True):
+ '''Get all MassKey element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `MassKey` elements. If `False`, return a list of all MassKey` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `MassKey` children
+ '''
+ return self.getElementsByKey(MassKey,'name',asdict)
+ def getFlare(self,single=True):
+ '''Get all or a sole `Flare` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Flare` child, otherwise fail.
+ If `False` return all `Flare` children in this element
+ Returns
+ -------
+ children : list
+ List of `Flare` children (even if `single=True`)
+ '''
+ return self.getAllElements(Flare,single)
+ def getAtStarts(self,single=True):
+ '''Get all or a sole `AtStart` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `AtStart` child, otherwise fail.
+ If `False` return all `AtStart` children in this element
+ Returns
+ -------
+ children : list
+ List of `AtStart` children (even if `single=True`)
+ '''
+ return self.getAllElements(AtStart,single)
+ def getBoards(self,asdict=True):
+ '''Get all Board element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board` elements. If `False`, return a list of all Board` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+ '''
+ picker = self.getPicker()
+ if picker is None: return None
+ return picker[0].getBoards(asdict=asdict)
+ def addBoardPicker(self,**kwargs):
+ '''Add a `BoardPicker` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : BoardPicker
+ The added element
+ '''
+ return self.add(BoardPicker,**kwargs)
+ def addStackMetrics(self,**kwargs):
+ '''Add a `StackMetrics` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : StackMetrics
+ The added element
+ '''
+ return self.add(StackMetrics,**kwargs)
+ def addImageSaver(self,**kwargs):
+ '''Add a `ImageSaver` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : ImageSaver
+ The added element
+ '''
+ return self.add(ImageSaver,**kwargs)
+ def addTextSaver(self,**kwargs):
+ '''Add a `TextSaver` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : TextSaver
+ The added element
+ '''
+ return self.add(TextSaver,**kwargs)
+ def addForwardToChatter(self,**kwargs):
+ '''Add a `ForwardToChatter` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : ForwardToChatter
+ The added element
+ '''
+ return self.add(ForwardToChatter,**kwargs)
+ def addMenuDisplayer(self,**kwargs):
+ '''Add a `MenuDisplayer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : MenuDisplayer
+ The added element
+ '''
+ return self.add(MenuDisplayer,**kwargs)
+ def addMapCenterer(self,**kwargs):
+ '''Add a `MapCenterer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : MapCenterer
+ The added element
+ '''
+ return self.add(MapCenterer,**kwargs)
+ def addStackExpander(self,**kwargs):
+ '''Add a `StackExpander` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : StackExpander
+ The added element
+ '''
+ return self.add(StackExpander,**kwargs)
+ def addPieceMover(self,**kwargs):
+ '''Add a `PieceMover` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PieceMover
+ The added element
+ '''
+ return self.add(PieceMover,**kwargs)
+ def addSelectionHighlighters(self,**kwargs):
+ '''Add a `SelectionHighlighters` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : SelectionHighlighters
+ The added element
+ '''
+ return self.add(SelectionHighlighters,**kwargs)
+ def addKeyBufferer(self,**kwargs):
+ '''Add a `KeyBufferer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : KeyBufferer
+ The added element
+ '''
+ return self.add(KeyBufferer,**kwargs)
+ def addHighlightLastMoved(self,**kwargs):
+ '''Add a `HighlightLastMoved` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : HighlightLastMoved
+ The added element
+ '''
+ return self.add(HighlightLastMoved,**kwargs)
+ def addCounterDetailViewer(self,**kwargs):
+ '''Add a `CounterDetailViewer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : CounterDetailViewer
+ The added element
+ '''
+ return self.add(CounterDetailViewer,**kwargs)
+ def addGlobalMap(self,**kwargs):
+ '''Add a `GlobalMap` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : GlobalMap
+ The added element
+ '''
+ return self.add(GlobalMap,**kwargs)
+ def addZoomer(self,**kwargs):
+ '''Add a `Zoomer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Zoomer
+ The added element
+ '''
+ return self.add(Zoomer,**kwargs)
+ def addHidePiecesButton(self,**kwargs):
+ '''Add a `HidePiecesButton` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : HidePiecesButton
+ The added element
+ '''
+ return self.add(HidePiecesButton,**kwargs)
+ def addMassKey(self,**kwargs):
+ '''Add a `MassKey` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : MassKey
+ The added element
+ '''
+ return self.add(MassKey,**kwargs)
+ def addStartupMassKey(self,**kwargs):
+ '''Add a `StartupMassKey` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : StartupMassKey
+ The added element
+ '''
+ return self.add(MassKey,**kwargs)
+ def addFlare(self,**kwargs):
+ '''Add a `Flare` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Flare
+ The added element
+ '''
+ return self.add(Flare,**kwargs)
+ def addAtStart(self,**kwargs):
+ '''Add a `AtStart` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : AtStart
+ The added element
+ '''
+ return self.add(AtStart,**kwargs)
+
+# --------------------------------------------------------------------
+class Map(BaseMap):
+ TAG = Element.MODULE+'Map'
+ def __init__(self,doc,node=None,
+ mapName = '',
+ allowMultiple = 'false',
+ backgroundcolor = rgb(255,255,255),
+ buttonName = '',
+ changeFormat = '$message$',
+ color = rgb(0,0,0),
+ createFormat = '$pieceName$ created in $location$ *',
+ edgeHeight = '0',
+ edgeWidth = '0',
+ hideKey = '',
+ hotkey = key('M',ALT),
+ icon = '/images/map.gif',
+ launch = 'false',
+ markMoved = 'Always',
+ markUnmovedHotkey = '',
+ markUnmovedIcon = '/images/unmoved.gif',
+ markUnmovedReport = '',
+ markUnmovedText = '',
+ markUnmovedTooltip = 'Mark all pieces on this map as not moved',
+ moveKey = '',
+ moveToFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
+ moveWithinFormat = '$pieceName$ moves $previousLocation$ &rarr; $location$ *',
+ showKey = '',
+ thickness = '3'):
+ super(Map,self).__init__(doc,self.TAG,node=node,
+ allowMultiple = allowMultiple,
+ backgroundcolor = backgroundcolor,
+ buttonName = buttonName,
+ changeFormat = changeFormat,
+ color = color,
+ createFormat = createFormat,
+ edgeHeight = edgeHeight,
+ edgeWidth = edgeWidth,
+ hideKey = hideKey,
+ hotkey = hotkey,
+ icon = icon,
+ launch = launch,
+ mapName = mapName,
+ markMoved = markMoved,
+ markUnmovedHotkey = markUnmovedHotkey,
+ markUnmovedIcon = markUnmovedIcon,
+ markUnmovedReport = markUnmovedReport,
+ markUnmovedText = markUnmovedText,
+ markUnmovedTooltip = markUnmovedTooltip,
+ moveKey = moveKey,
+ moveToFormat = moveToFormat,
+ moveWithinFormat = moveWithinFormat,
+ showKey = showKey,
+ thickness = thickness)
+
+ def getGame(self):
+ return self.getParent(Game)
+
+# --------------------------------------------------------------------
+class WidgetMap(BaseMap):
+ TAG = Element.WIDGET+'WidgetMap'
+ def __init__(self,doc,node=None,**attr):
+ super(WidgetMap,self).__init__(doc,self.TAG,node=node,**attr)
- Parameters
- ----------
- let : str
- Key code (Letter)
- mod : int
- Modifier mask
- '''
- return f'{ord(let)},{mod}'
-# Colour encoding
-def rgb(r,g,b):
- return ','.join([str(r),str(g),str(b)])
+ def getGame(self):
+ return self.getParentOfClass([Game])
+ def getMapWidget(self):
+ return self.getParent(MapWidget)
-def rgba(r,g,b,a):
- return ','.join([str(r),str(g),str(b),str(a)])
+#
+# EOF
+#
+# ====================================================================
+# From chart.py
# --------------------------------------------------------------------
-def set_node_attr(node,**attr):
- for k,v in attr.items():
- node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
-
-# --------------------------------------------------------------------
-def add_node(root,parent,tag,**attr):
- e = root.createElement(tag)
- if parent: parent.appendChild(e)
- set_node_attr(e,**attr)
-
- return e
-
-
-# --------------------------------------------------------------------
-def add_text(root,parent,text):
- t = root.createTextNode(text)
- parent.appendChild(t)
- return t
-
-# --------------------------------------------------------------------
-def add_game(root,doc,
- name,
- version,
- ModuleOther1 = "",
- ModuleOther2 = "",
- VassalVersion = "3.6.7",
- description = "",
- nextPieceSlotId = 0):
- return add_node(root,doc,BUILD+'GameModule',
- name = name,
- version = version,
- ModuleOther1 = ModuleOther1,
- ModuleOther2 = ModuleOther2,
- VassalVersion = VassalVersion,
- description = description,
- nextPieceSlotId = nextPieceSlotId)
-
-# --------------------------------------------------------------------
-def add_basiccommandencoder(root,doc):
- return add_node(root,doc,MODULE+'BasicCommandEncoder')
-
-# --------------------------------------------------------------------
-def add_globalproperties(root,elem,*props,**named):
- gp = add_node(root,elem,MODULE+'properties.GlobalProperties')
- # Add elements where each is a dict with _at least_
- # "{'name':NAME, 'initialValue':VALUE}"
- for p in props:
- add_globalproperty(root,gp,**p)
- # Add elements where each is a named dict with _at least_
- # "{'initialValue':VALUE}"
- for n, p in named:
- add_globalproperty(root,gp,name=n,**p)
-
- return gp
-
-
-# --------------------------------------------------------------------
-def add_globalproperty(root,elem,name,initialValue,
- isNumeric = False,
- min = "null",
- max = "null",
- wrap = False,
- description = ""):
- return add_node(root,elem,MODULE+'properties.GlobalProperty',
- name = name,
- initialValue = initialValue,
- isNumeric = isNumeric,
- min = min,
- max = max,
- wrap = wrap,
- description = description)
-
-# --------------------------------------------------------------------
-def add_turntrack(root, elem, name,
- buttonText = 'Turn',
- hotkey = '',
- icon = '',
- length = -1,
- lengthStyle = 'Maximum',
- nexthotkey = key('T',ALT),
- plusButtonSize = 22,
- prevhotkey = key('T',ALT_SHIFT),
- reportFormat = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$',
- turnButtonHeight = 22,
- turnFormat = None,
- counter = None,
- phases = None):
-
- levels = (counter if counter is not None else
- phases if phases is not None else None)
- if levels is not None:
- lvl = 1
- lvls = [f'$level{lvl}$']
- sub = levels
- while True:
- sub = sub.get('counter',sub.get('phases',None))
- if sub is None:
- break
- lvl += 1
- lvls.append(f'$level{lvl}$')
-
- turnFormat = ' '.join(lvls)
-
- if turnFormat is None:
- turnFormat = '$level1$ $level2$ $level3$ $level4$'
-
- t = add_node(root, elem, MODULE+'turn.TurnTracker',
- name = name,
- buttonText = buttonText,
- hotkey = hotkey,
- icon = icon,
- length = length,
- lengthStyle = lengthStyle,
- nexthotkey = nexthotkey,
- plusButtonSize = plusButtonSize,
- prevhotkey = prevhotkey,
- reportFormat = reportFormat,
- turnButtonHeight = turnButtonHeight,
- turnFormat = turnFormat)
-
- add_turnlevel(root, t, counter=counter, phases=phases)
-
- return t
-
-# --------------------------------------------------------------------
-def add_turnlevel(root,parent,counter=None,phases=None):
- f,s = ((add_turncounter,counter) if counter is not None else
- (add_turnlist,phases) if phases is not None else (None,None))
- if f is None: return parent
-
- subcounter = s.pop('counter',None)
- subphases = s.pop('phases',None)
-
- sub = f(root,parent,**s)
-
- return add_turnlevel(root,sub,counter=subcounter,phases=subphases)
-
-# --------------------------------------------------------------------
-def add_turncounter(root,elem,property,
- incr = 1,
- loop = False,
- loopLimit = -1,
- turnFormat = "$value$"):
- c = add_node(root,elem,MODULE+"turn.CounterTurnLevel",
- property = property,
- incr = incr,
- loop = loop,
- loopLimit = loopLimit,
- turnFormat = turnFormat)
- return c
-
-# --------------------------------------------------------------------
-def add_turnlist(root,elem,property,names,
- configFirst = False,
- configList = False,
- turnFormat = '$value$'):
- c = add_node(root,elem,MODULE+"turn.ListTurnLevel",
- property = property,
- list = ','.join([str(p) for p in names]),
- configFirst = configFirst,
- configList = configList,
- turnFormat = turnFormat)
- return c
-
-# --------------------------------------------------------------------
-def add_turnhotkey(root,elem,
- hotkey,
- match = '{phase=="first"}',
- reportFormat = '{PlayerName}',
- name = ''):
- return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey',
- hotkey = hotkey,
- match = match,
- reportFormat = reportFormat,
- name = name)
+class ChartWindow(GameElement,WidgetElement):
+ TAG=Element.MODULE+'ChartWindow'
+ def __init__(self,elem,node=None,
+ name = '',
+ hotkey = key('A',ALT),
+ description = '',
+ text = '',
+ tooltip = 'Show/hide Charts',
+ icon = '/images/chart.gif'):
+ super(ChartWindow,self).__init__(elem,self.TAG,node=node,
+ name = name,
+ hotkey = hotkey,
+ description = description,
+ text = text,
+ tooltip = tooltip,
+ icon = icon)
+
+ def addChart(self,**kwargs):
+ '''Add a `Chart` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Chart
+ The added element
+ '''
+ return self.add(Chart,**kwargs)
+ def getCharts(self,asdict=True):
+ '''Get all Chart element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Chart` elements. If `False`, return a list of all Chart` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Chart` children
+ '''
+ return self.getElementsById(Chart,'chartName',asdict=asdict)
+
+
-
-
# --------------------------------------------------------------------
-def add_translatable(root,elem):
- return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages')
+class Chart(Element):
+ TAG=Element.WIDGET+'Chart'
+ def __init__(self,elem,node=None,
+ chartName = '',
+ fileName = '',
+ description = ''):
+ super(Chart,self).__init__(elem,self.TAG,node=node,
+ chartName = chartName,
+ description = description,
+ fileName = fileName)
-# --------------------------------------------------------------------
-def add_language(root,elem):
- return add_node(root,elem,'VASSAL.i18n.Language')
+#
+# EOF
+#
+# ====================================================================
+# From command.py
# --------------------------------------------------------------------
-def add_chatter(root,elem):
- return add_node(root,elem,MODULE+'Chatter')
-
+class Command:
+ def __init__(self,what,iden,tpe,state):
+ self.cmd = '/'.join([what,iden,tpe,state])
+
# --------------------------------------------------------------------
-def add_keynamer(root,elem):
- return add_node(root,elem,MODULE+'KeyNamer')
+class AddCommand(Command):
+ ID = '+'
+ def __init__(self,iden,tpe,state):
+ super(AddCommand,self).__init__(self.ID,iden,tpe,state)
+
-# --------------------------------------------------------------------
-def add_documentation(root,doc):
- return add_node(root,doc,MODULE+'Documentation')
+#
+# EOF
+#
+# ====================================================================
+# From trait.py
+# ====================================================================
+class Trait:
+ known_traits = []
+ def __init__(self):
+ '''Base class for trait capture. Unlike the Element classes,
+ this actually holds state that isn't reflected elsewhere in
+ the DOM. This means that the data here is local to the
+ object. So when we do
+
+ piece = foo.getPieceSlots()[0]
+ traits = p.getTraits()
+ for trait in traits:
+ if trait.ID == 'piece':
+ trait["gpid"] = newPid
+ trait["lpid"] = newPid
+
+ we do not actually change anything in the DOM. To do that, we
+ must add back _all_ the traits as
+
+ piece.setTraits(traits)
+
+ We can add traits to a piece, like
+
+ piece.addTrait(MarkTrait('Hello','World'))
+
+ But it is not particularly efficient. Better to do
+ (continuing from above)
+
+ traits.append(MarkTrait('Hello','World;)
+ piece.setTraits(traits)
+
+ '''
+ self._type = None
+ self._state = None
+
+ def setType(self,**kwargs):
+ '''Set types. Dictionary of names and values. Dictonary keys
+ defines how we access the fields, which is internal here.
+ What matters is the order of the values.
+
+ '''
+ self._type = list(kwargs.values())
+ self._tnames = list(kwargs.keys())
+ def setState(self,**kwargs):
+ '''Set states. Dictionary of names and values. Dictonary keys
+ defines how we access the fields, which is internal here.
+ What matters is the order of the values.
+ '''
+ self._state = list(kwargs.values())
+ self._snames = list(kwargs.keys())
-# --------------------------------------------------------------------
-def add_about(root,doc,title,fileName=""):
- return add_node(root,doc,MODULE+'documentation.AboutScreen',
- fileName = fileName,
- title = title)
-# --------------------------------------------------------------------
-def add_pdffile(root,doc,title,pdfFile):
- return add_node(root,doc,MODULE+'documentation.BrowserPDFFile',
- pdfFile = pdfFile, title=title)
-# --------------------------------------------------------------------
-def add_helpfile(root,doc,title,fileName,fileType="archive"):
- return add_node(root,doc,MODULE+'documentation.HelpFile',
- fileName = fileName,
- fileType = fileType,
- title = title)
+
+ def __getitem__(self,key):
+ '''Look up item in either type or state'''
+ try:
+ return self._type[self._tnames.index(key)]
+ except:
+ pass
+ return self._state[self._snames.index(key)]
+
+ def __setitem__(self,key,value):
+ '''Set item in either type or state'''
+ try:
+ self._type[self._tnames.index(key)] = value
+ return
+ except:
+ pass
+ self._state[self._snames.index(key)] = value
+
+ def encode(self,term=False):
+ '''
+ returns type and state encoded'''
+ t = self.encodeFields(self.ID,*self._type,term=term)
+ s = self.encodeFields(*self._state,term=term)
+ return t,s
+
+ @classmethod
+ def findTrait(cls,traits,ID,key=None,value=None,verbose=False):
+ for trait in traits:
+ if trait.ID != ID:
+ continue
+ if verbose:
+ print(f' {trait.ID}')
+ if key is None or value is None:
+ if verbose:
+ print(f' Return {trait.ID}')
+ return trait
+ if verbose:
+ print(f' Check {key}={value}: {trait[key]}')
+ if trait[key] == value:
+ return trait
+ if verbose:
+ print(f' Trait of type {ID} with {key}={value} not found')
+ return None
+
+ @classmethod
+ def take(cls,iden,t,s):
+ '''If the first part of the string t matches the ID, then take it.
+
+ t and s are lists of strings.
+ '''
+ if iden != cls.ID: return None
+
+ ret = cls()
+ ret._type = t
+ ret._state = s
+ ret.check() # Check if we're reasonable, or raise
+ #print(f'Took {iden} {cls}\n'
+ # f' {ret._tnames}\n'
+ # f' {ret._snames}')
+ return ret
+
+ def check(self):
+ '''Implement if trait should check that all is OK when cloning'''
+ pass
+
+ @classmethod
+ def encodeFields(cls,*args,term=False):
+ return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
+ for e in args])+(';' if term else '')
+
+ @classmethod
+ def decodeFields(cls,s):
+ from re import split
+ return split(r'(?<!\\);',s)
+ # return s.split(';') # Probably too simple-minded
+
+ @classmethod
+ def encodeKeys(cls,keys,sep=','):
+ return sep.join([k.replace(',','\\'+f'{sep}') for k in keys])
+
+ @classmethod
+ def decodeKeys(cls,keys,sep=','):
+ from re import split
+ ks = split(r'(?<!\\)'+f'{sep}',keys)
+ return [k.replace('\\'+f'{sep}',f'{sep}') for k in ks]
+
+ @classmethod
+ def flatten(cls,traits,game=None,prototypes=None):
+ if prototypes is None:
+ if game is None:
+ print(f'Warning: Game or prototypes not passed')
+ return None
+ prototypes = game.getPrototypes()[0].getPrototypes()
+
+ ret = []
+ return cls._flatten(traits,ret,prototypes,False)
+
+ @classmethod
+ def _flatten(cls,traits,ret,prototypes,nobasic=True):
+ '''Expand all prototype traits in traits'''
+ for trait in traits:
+ # Ignore recursive basic traits
+ if nobasic and trait.ID == BasicTrait.ID:
+ continue
+ # Add normal traits
+ if trait.ID != PrototypeTrait.ID:
+ ret.append(trait)
+ continue
+
+ # Find prototype
+ proto = prototypes.get(trait['name'],None)
+ if proto is None:
+ print(f'Warning, prototype {trait["name"]} not found')
+ continue
+
+ # Recursive call to add prototype traits (and possibly
+ # more recursive calls
+ cls._flatten(proto.getTraits(), ret, prototypes,not nobasic)
+
+ return ret
+#
+# EOF
+#
+# ====================================================================
+# From withtraits.py
# --------------------------------------------------------------------
-def add_htmlfile(root,doc,title,startingPage='index.html'):
- return add_node(root,doc,MODULE+'documentation.BrowserHelpFile',
- startingPage=startingPage,
- title=title)
+#
+# Traits of this kind of object are
+#
+# - Evaluated from the start of the list to the end of the list,
+# skipping over report and trigger traits
+# - Then evaluated from the end of the list to the start, only
+# evaluating report and trigger traits
+# - The list _must_ end in a BasicTrait
+#
+# Traits are copied when making a copy of objects of this class, and
+# are done so using a full decoding and encoding. This means that
+# copying is a bit susceptible to expansions of the strings of the traits,
+# in particular if they contain special characters such as ',' or '/'.
+#
+class WithTraits(Element):
+ def __init__(self,parent,tag,node=None,traits=[],**kwargs):
+ '''Base class for things that have traits
+
+ Parameters
+ ----------
+ parent : Element
+ Parent to add this to
+ node : xml.minidom.Element
+ If not None, XML element to read definition from.
+ Rest of the arguments are ignored if not None.
+ traits : list of Trait objects
+ The traits to set on this object
+ kwargs : dict
+ More attributes to set on element
+ '''
+ super(WithTraits,self).__init__(parent,tag,node=node,**kwargs)
+ if node is None: self.setTraits(*traits)
+
+ def addTrait(self,trait):
+ '''Add a `Trait` element to this. Note that this re-encodes
+ all traits.
+
+ Parameters
+ ----------
+ trait : Trait
+ The trait to add
+ '''
+ traits = self.getTraits()
+ traits.append(trait)
+ self.setTraits(*traits)
+
+ def getTraits(self):
+ '''Get all element traits as objects. This decodes the trait
+ definitions. This is useful if we have read the element from
+ the XML file, or similar.
+
+ Note that the list of traits returned are _not_ tied to the
+ XML nodes content. Therefore, if one makes changes to the list,
+ or to elements of the list, and these changes should be
+ reflected in this object, then we _must_ call
+
+ setTraits(traits)
+
+ with the changed list of traits.
+
+ Returns
+ -------
+ traits : list of Trait objects
+ The decoded traits
+
+ '''
+ from re import split
+ code = self._node.childNodes[0].nodeValue
+ cmd, iden, typ, sta = split(fr'(?<!\\)/',code) #code.split('/')
+ types = typ.split(r' ')
+ states = sta.split(r' ')
+ types = [t.strip('\\').split(';') for t in types]
+ states = [s.strip('\\').split(';') for s in states]
+ traits = []
+
+ for t, s in zip(types,states):
+ tid = t[0]
+ trem = t[1:]
+ known = False
+
+ for c in Trait.known_traits:
+ t = c.take(tid,trem,s) # See if we have it
+ if t is not None:
+ traits.append(t) # Got it
+ known = True
+ break
+
+ if not known:
+ print(f'Warning: Unknown trait {tid}')
+ return traits
+
+ def setTraits(self,*traits,iden='null'):
+ '''Set traits on this element. This encodes the traits into
+ this object.
+
+ Parameters
+ ----------
+ traits : tuple of Trait objects
+ The traits to set on this object.
+ iden : str
+ Identifier
+
+ '''
+ types = []
+ states = []
+ for trait in traits:
+ if trait is None:
+ print(f'Trait is None (traits: {traits})')
+ continue
+ tpe, state = trait.encode()
+ types.append(tpe)
+ states.append(state)
+
+ tpe = WithTraits.encodeParts(*types)
+ state = WithTraits.encodeParts(*states)
+ add = AddCommand(str(iden),tpe,state)
+ if len(self._node.childNodes) < 1:
+ self.addText('')
+ self._node.childNodes[0].nodeValue = add.cmd
+
+ def removeTrait(self,ID,key=None,value=None,verbose=False):
+ '''Remove a trait from this object.
+
+ Parameters
+ ----------
+ ID : str
+ The type of trait to remove. Must be a valid
+ ID of a class derived from Trait.
+ key : str
+ Optional key to inspect to select trait that has
+ this key and the traits key value is the argument value,
+ value :
+ If specified, then only traits which key has this value
+ are removed
+ verbose : bool
+ Be verbose if True
+
+ Returns
+ -------
+ trait : Trait
+ The removed trait or None
+ '''
+ traits = self.getTraits()
+ trait = Trait.findTrait(traits,ID,key,value,verbose)
+ if trait is not None:
+ traits.remove(trait)
+ self.setTraits(traits)
+ return trait
+
+ def addTraits(self,*toadd):
+ '''Add traits to this. Note that this will
+ decode and reencode the traits. Only use this when
+ adding traits on-mass. Repeated use of this is inefficient.
+
+ This member function takes care to push any basic trait to
+ the end of the list.
+
+ The added traits will not override existing triats.
+
+ Paramters
+ ---------
+ toAdd : tuple of Trait objects
+ The traits to add
+
+ '''
+ traits = self.getTraits()
+ basic = Trait.findTrait(traits,BasicTrait.ID)
+ if basic:
+ traits.remove(basic)
+ traits.extend(toAdd)
+ if basic:
+ traits.append(basic)
+ self.setTraits(traits)
+
+
+ @classmethod
+ def encodeParts(cls,*parts):
+ '''Encode parts of a full piece definition
+
+ Each trait (VASSAL.counter.Decorator,
+ VASSAL.counter.BasicPiece) definition or state is separated by
+ a litteral TAB character. Beyond the first TAB separator,
+ additional escape characters (BACKSLAH) are added in front of
+ the separator. This is to that VASSAL.utils.SequenceDecoder
+ does not see consequitive TABs as a single TAB.
+ '''
+ ret = ''
+ sep = r' '
+ for i, p in enumerate(parts):
+ if i != 0:
+ ret += '\\'*(i-1) + sep
+ ret += str(p)
+
+ return ret
+
+
+ def cloneNode(self,parent):
+ '''This clones the underlying XML node.
+
+ Parameters
+ ----------
+ parent : Element
+ The element to clone this element into
+
+ Returns
+ -------
+ copy : xml.minidom.Element
+ The newly created clone of this object's node
+ '''
+ copy = self._node.cloneNode(deep=True)
+ if parent is not None:
+ parent._node.appendChild(copy)
+ else:
+ print('WARNING: No parent to add copy to')
+ return copy
+
# --------------------------------------------------------------------
-def add_tutorial(root,doc,
- name = 'Tutorial',
- logfile = 'tutorial.vlog',
- promptMessage = 'Load the tutorial?',
- welcomeMessage = 'Press "Step forward" (PnDn) to step through the tutorial',
- launchOnStartup = True):
- return add_node(root,doc,MODULE+'documentation.Tutorial',
- name = name,
- logfile = logfile,
- promptMessage = promptMessage,
- welcomeMessage = welcomeMessage,
- launchOnStartup = launchOnStartup)
+class DummyWithTraits(WithTraits):
+ TAG = 'dummy'
+ def __init__(self,parent,node=None,traits=[]):
+ '''An empty element. Used when making searching'''
+ super(DummyWithTraits,self).__init__(tag = self.TAG,
+ parent = parent,
+ node = node,
+ traits = traits)
+ if parent is not None:
+ parent.remove(self)
+
# --------------------------------------------------------------------
-def add_roster(root,doc,buttonKeyStroke='',
- buttonText='Retire',
- buttonToolTip='Switch sides, become observer, or release faction'):
- return add_node(root,doc,MODULE+'PlayerRoster',
- buttonKeyStroke = buttonKeyStroke,
- buttonText = buttonText,
- buttonToolTip = buttonToolTip)
-
-# --------------------------------------------------------------------
-def add_entry(root,doc,text):
- n = add_node(root,doc,'entry')
- add_text(root,n,text)
- return n
-
-# --------------------------------------------------------------------
-def add_globaloptions(root,doc,
- autoReport = "Use Preferences Setting",
- centerOnMove = "Use Preferences Setting",
- chatterHTMLSupport = "Always",
- hotKeysOnClosedWindows = "Never",
- inventoryForAll = "Always",
- nonOwnerUnmaskable = "Use Preferences Setting",
- playerIdFormat = "$playerName$",
- promptString = "Opponents can unmask pieces",
- sendToLocationMoveTrails = "Never"):
- return add_node(root,doc,MODULE+'GlobalOptions',
- autoReport = autoReport,
- centerOnMove = centerOnMove,
- chatterHTMLSupport = chatterHTMLSupport,
- hotKeysOnClosedWindows = hotKeysOnClosedWindows,
- inventoryForAll = inventoryForAll,
- nonOwnerUnmaskable = nonOwnerUnmaskable,
- playerIdFormat = playerIdFormat,
- promptString = promptString,
- sendToLocationMoveTrails = sendToLocationMoveTrails)
-
-# --------------------------------------------------------------------
-def add_option(root,doc,name,value):
- n = add_node(root,doc,'option',name=name)
- add_text(n,root,value)
- return n
+class PieceSlot(WithTraits):
+ TAG = Element.WIDGET+'PieceSlot'
+ def __init__(self,parent,node=None,
+ entryName = '',
+ traits = [],
+ gpid = 0,
+ height = 72,
+ width = 72,
+ icon = ''):
+ '''A piece slot. Used all the time.
+
+ Parameters
+ ----------
+ parent : Element
+ Parent to add this to
+ node : xml.minidom.Element
+ If not None, XML element to read definition from.
+ Rest of the arguments are ignored if not None.
+ entryName : str
+ Name of this
+ traits : list of Trait objects
+ The traits to set on this object
+ gpid : int
+ Global Piece identifier. If 0, will be set by Game
+ height : int
+ Height size of the piece (in pixels)
+ width : int
+ Width size of the piece (in pixels)
+ icon : str
+ Piece image file name within 'image' sub-dir of archive
+ '''
+ super(PieceSlot,self).\
+ __init__(parent,self.TAG,node=node,
+ traits = traits,
+ entryName = entryName,
+ gpid = gpid,
+ height = height,
+ width = width,
+ icon = icon)
+
+ def clone(self,parent):
+ '''Adds copy of self to parent, possibly with new GPID'''
+ game = self.getParentOfClass([Game])
+ gpid = game.nextPieceSlotId()
+ #opid = int(self.getAttribute('gpid'))
+ #print(f'Using GPID={gpid} for clone {opid}')
+
+ node = self.cloneNode(parent)
+ piece = PieceSlot(parent,node=node)
+ piece.setAttribute('gpid',gpid)
+
+ traits = piece.getTraits()
+ for trait in traits:
+ if isinstance(trait,BasicTrait):
+ trait['gpid'] = gpid
+
+ piece.setTraits(*traits)
+ return piece
+
# --------------------------------------------------------------------
-# CurrentMap == &quot;Board&quot;
-def add_inventory(root,doc,
- canDisable = False,
- centerOnPiece = True,
- disabledIcon = '',
- drawPieces = True,
- foldersOnly = False,
- forwardKeystroke = True,
- groupBy = '',
- hotkey = key('I',ALT),
- icon = '/images/inventory.gif',
- include = '{}',
- launchFunction = 'functionHide',
- leafFormat = '$PieceName$',
- name = '',
- nonLeafFormat = '$PropertyValue$',
- pieceZoom = '0.33',
- pieceZoom2 = '0.5',
- pieceZoom3 = '0.6',
- propertyGate = '',
- refreshHotkey = key('I',ALT_SHIFT),
- showMenu = True,
- sides = '',
- sortFormat = '$PieceName$',
- sortPieces = True,
- sorting = 'alpha',
- text = '',
- tooltip = 'Show inventory of all pieces',
- zoomOn = False):
- return add_node(root,doc,MODULE+'Inventory',
- canDisable = canDisable,
- centerOnPiece = centerOnPiece,
- disabledIcon = disabledIcon,
- drawPieces = drawPieces,
- foldersOnly = foldersOnly,
- forwardKeystroke = forwardKeystroke,
- groupBy = groupBy,
- hotkey = hotkey,
- icon = icon,
- include = include,
- launchFunction = launchFunction,
- leafFormat = leafFormat,
- name = name,
- nonLeafFormat = nonLeafFormat,
- pieceZoom = pieceZoom,
- pieceZoom2 = pieceZoom2,
- pieceZoom3 = pieceZoom3,
- propertyGate = propertyGate,
- refreshHotkey = refreshHotkey,
- showMenu = showMenu,
- sides = sides,
- sortFormat = sortFormat,
- sortPieces = sortPieces,
- sorting = sorting,
- text = text,
- tooltip = tooltip,
- zoomOn = zoomOn)
+class Prototype(WithTraits):
+ TAG = Element.MODULE+'PrototypeDefinition'
+ def __init__(self,cont,node=None,
+ name = '',
+ traits = [],
+ description = ''):
+ '''A prototype. Used all the time.
+
+ Parameters
+ ----------
+ cont : Element
+ Parent to add this to
+ node : xml.minidom.Element
+ If not None, XML element to read definition from.
+ Rest of the arguments are ignored if not None.
+ name : str
+ Name of this
+ traits : list of Trait objects
+ The traits to set on this object
+ description : str
+ A free-form description of this prototype
+ '''
+ super(Prototype,self).__init__(cont,self.TAG,node=node,
+ traits = traits,
+ name = name,
+ description = description)
-
+#
+# EOF
+#
+# ====================================================================
+# From traits/dynamicproperty.py
+
+# --------------------------------------------------------------------
+# Base class for property (piece or global) change traits. Encodes
+# constraints and commands.
+class ChangePropertyTrait(Trait):
+ DIRECT = 'P'
+ def __init__(self,
+ *commands,
+ numeric = False,
+ min = 0,
+ max = 100,
+ wrap = False):
+ # assert name is not None and len(name) > 0, \
+ # 'No name specified for ChangePropertyTriat'
+ super(ChangePropertyTrait,self).__init__()
+ self._constraints = self.encodeConstraints(numeric,wrap,min,max)
+ self._commands = self.encodeCommands(commands)
+
+ def encodeConstraints(self,numeric,wrap,min,max):
+ isnum = f'{numeric}'.lower()
+ iswrap = f'{wrap}'.lower()
+ return f'{isnum},{min},{max},{iswrap}'
+
+ def decodeConstraints(self,constraints):
+ f = Trait.decodeKeys(constraints)
+ return f[0]=='true',f[3]=='true',int(f[1]),int(f[2])
+
+ def encodeCommands(self,commands):
+ cmds = []
+ for cmd in commands:
+ # print(cmd)
+ com = cmd[0] + ':' + cmd[1].replace(',',r'\,') + ':' + cmd[2]
+ if cmd[2] == self.DIRECT:
+ com += f'\,'+cmd[3].replace(':',r'\:')
+ cmds.append(com)
+ return ','.join(cmds)
+
+ def decodeCommands(self,commands):
+ cmds = Trait.decodeKeys(commands)
+ ret = []
+ for cmd in cmds:
+ parts = Trait.decodeKeys(cmd,':')
+ # print('parts',parts)
+ if parts[-1][0] == self.DIRECT:
+ parts = parts[:-1]+Trait.decodeKeys(parts[-1],',')
+ ret.append(parts)
+ # print(commands,parts)
+ return ret
+
+ def getCommands(self):
+ return self.decodeCommands(self['commands'])
+ def setCommands(self,commands):
+ self['commands'] = self.encodeCommands(commands)
+
+ def check(self):
+ assert len(self['name']) > 0,\
+ f'No name given for ChangePropertyTrait'
+
+
# --------------------------------------------------------------------
-def add_map(root,doc,
- mapName,
- allowMultiple = 'false',
- backgroundcolor = rgb(255,255,255),
- buttonName = '',
- changeFormat = '$message$',
- color = rgb(0,0,0),
- createFormat = '$pieceName$ created in $location$ *',
- edgeHeight = '0',
- edgeWidth = '0',
- hideKey = '',
- hotkey = key('M',ALT),
- icon = '/images/map.gif',
- launch = 'false',
- markMoved = 'Always',
- markUnmovedHotkey = '',
- markUnmovedIcon = '/images/unmoved.gif',
- markUnmovedReport = '',
- markUnmovedText = '',
- markUnmovedTooltip = 'Mark all pieces on this map as not moved',
- moveKey = '',
- moveToFormat = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
- moveWithinFormat = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
- showKey = '',
- thickness = '3',
- cls = MODULE+'Map'):
- return add_node(root,doc,cls,
- allowMultiple = allowMultiple,
- backgroundcolor = backgroundcolor,
- buttonName = buttonName,
- changeFormat = changeFormat,
- color = color,
- createFormat = createFormat,
- edgeHeight = edgeHeight,
- edgeWidth = edgeWidth,
- hideKey = hideKey,
- hotkey = hotkey,
- icon = icon,
- launch = launch,
- mapName = mapName,
- markMoved = markMoved,
- markUnmovedHotkey = markUnmovedHotkey,
- markUnmovedIcon = markUnmovedIcon,
- markUnmovedReport = markUnmovedReport,
- markUnmovedText = markUnmovedText,
- markUnmovedTooltip = markUnmovedTooltip,
- moveKey = moveKey,
- moveToFormat = moveToFormat,
- moveWithinFormat = moveWithinFormat,
- showKey = showKey,
- thickness = thickness)
-
-# --------------------------------------------------------------------
-def add_widgetmap(root,doc, mapName,**attr):
- return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr)
-
-# --------------------------------------------------------------------
-def add_boardpicker(root,doc,
- addColumnText = 'Add column',
- addRowText = 'Add row',
- boardPrompt = 'Select board',
- slotHeight = 125,
- slotScale = 0.2,
- slotWidth = 350,
- title = 'Choose Boards'):
- return add_node(root,doc,MAP+'BoardPicker',
- addColumnText = addColumnText,
- addRowText = addRowText,
- boardPrompt = boardPrompt,
- slotHeight = slotHeight,
- slotScale = slotScale,
- slotWidth = slotWidth,
- title = title)
-
-# --------------------------------------------------------------------
-def add_setup(root,picker,mapName,maxColumns,boardNames):
- '''Add given boards in row-first up to maxColumns per row'''
- s = add_node(root,picker,'setup')
- col = 0
- row = 0
- lst = [f'{mapName}BoardPicker']
- for bn in boardNames:
- lst.extend([bn,str(col),str(row)])
- col += 1
- if col >= maxColumns:
- col = 0
- row += 1
- txt = r' '.join(lst)
- add_text(root,s,txt)
-
-# --------------------------------------------------------------------
-def add_board(root,picker,name,
- image = '',
- reversible = False,
- color = rgb(255,255,255),
- width = 0,
- height = 0):
- return add_node(root,picker,PICKER+'Board',
- image = image,
- name = name,
- reversible = reversible,
- color = color,
- width = width,
- height = height)
-
-# --------------------------------------------------------------------
-def add_zoned(root,board):
- return add_node(root,board,PICKER+'board.ZonedGrid')
-
-# --------------------------------------------------------------------
-def add_zonehighlighter(root,zoned):
- return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter')
-
-# --------------------------------------------------------------------
-def add_zone(root,zoned,
- name = "",
- highlightProperty = "",
- locationFormat = "$gridLocation$",
- path = "0,0;976,0;976,976;0,976",
- useHighlight = False,
- useParentGrid = False):
- return add_node(root,zoned,PICKER+'board.mapgrid.Zone',
- name = name,
- highlightProperty = highlightProperty,
- locationFormat = locationFormat,
- path = path,
- useHighlight = useHighlight,
- useParentGrid = useParentGrid)
+class DynamicPropertyTrait(ChangePropertyTrait):
+ ID = 'PROP'
+ def __init__(self,
+ *commands,
+ name = '',
+ value = 0,
+ numeric = False,
+ min = 0,
+ max = 100,
+ wrap = False,
+ description = ''):
+ '''Commands are
+
+ - menu
+ - key
+ - Type (only 'P' for now)
+ - Expression
+ '''
+ super(DynamicPropertyTrait,self).__init__(*commands,
+ numeric = numeric,
+ min = min,
+ max = max,
+ wrap = wrap)
+ # print(commands,'Name',name)
+ self.setType(name = name,
+ constraints = self._constraints,
+ commands = self._commands,
+ description = description)
+ self.setState(value=value)
-# --------------------------------------------------------------------
-HEX_WIDTH = 88.50779626676963
-HEX_HEIGHT = 102.2
-def add_hexgrid(root,zone,
- color = rgb(0,0,0),
- cornersLegal = False,
- dotsVisible = False,
- dx = HEX_WIDTH,
- dy = HEX_HEIGHT,
- edgesLegal = False,
- sideways = False,
- snapTo = True,
- visible = True,
- x0 = 0,
- y0 = 32,
- cls = PICKER+'board.HexGrid'):
- return add_node(root,zone,cls,
- color = color,
- cornersLegal = cornersLegal,
- dotsVisible = dotsVisible,
- dx = dx,
- dy = dy,
- edgesLegal = edgesLegal,
- sideways = sideways,
- snapTo = snapTo,
- visible = visible,
- x0 = x0,
- y0 = y0)
-
-# --------------------------------------------------------------------
-def add_hexnumbering(root,grid,
- color = rgb(255,0,0),
- first = 'H',
- fontSize = 24,
- hDescend = False,
- hDrawOff = 0,
- hLeading = 1,
- hOff = 0,
- hType = 'A',
- locationFormat = '$gridLocation$',
- rotateText = 0,
- sep = '',
- stagger = True,
- vDescend = False,
- vDrawOff = 32,
- vLeading = 0,
- vOff = 0,
- vType = 'N',
- visible = True,
- cls = PICKER+'board.mapgrid.HexGridNumbering'):
- return add_node(root,grid,cls,
- color = color,
- first = first,
- fontSize = fontSize,
- hDescend = hDescend,
- hDrawOff = hDrawOff,
- hLeading = hLeading,
- hOff = hOff,
- hType = hType,
- locationFormat = locationFormat,
- rotateText = rotateText,
- sep = sep,
- stagger = stagger,
- vDescend = vDescend,
- vDrawOff = vDrawOff,
- vLeading = vLeading,
- vOff = vOff,
- vType = vType,
- visible = visible)
+
+Trait.known_traits.append(DynamicPropertyTrait)
-# --------------------------------------------------------------------
-RECT_WIDTH = 80
-RECT_HEIGHT = 80
-def add_squaregrid(root,zone,
- dx = RECT_WIDTH,
- dy = RECT_HEIGHT,
- edgesLegal = False,
- x0 = 0,
- y0 = int(0.4*RECT_HEIGHT),
- **attr):
- return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid',
- dx = dx,
- dy = dy,
- edgesLegal = edgesLegal,
- x0 = x0,
- y0 = y0,
- **attr)
-
-# --------------------------------------------------------------------
-def add_squarenumbering(root,grid,
- hType = 'N',
- **attr):
- return add_hexnumbering(root,grid,
- cls=PICKER+'board.mapgrid.SquareGridNumbering',
- hType = hType,
- **attr)
-
-# --------------------------------------------------------------------
-def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True):
- # print(f'Make region grid visible: {visible}')
- return add_node(root,zone,PICKER+'board.RegionGrid',
- fontsize = fontsize,
- snapto = snapto,
- visble = visible)
-
-# --------------------------------------------------------------------
-def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False):
- if not alsoPiece:
- return None, None
-
- # Find the parent map element by stepping up the tree
- from functools import reduce
-
- map = reduce(lambda nn, _: None if nn is None else nn.parentNode,
- range(5), grid)
- if map is None or \
- map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']:
- if map is None:
- print(f' = Map not found from grid')
- return None, None
-
- if piece is not None:
- print(f' Using supplied piece')
- return piece, map
-
- # Find the piece by searching from the top of the tree
- piece = get_pieces(root,root).get(name,None)
- # if piece is None:
- # print(list(get_pieces(root,root).keys()))
- if verbose:
- print(f' Possible piece for region {name}: {piece}')
- return piece, map
-
-# --------------------------------------------------------------------
-def check_region_name(grid,name):
- post = ''
- targ = name
- same = 0
- regs = list(get_regions(grid).keys())
- poss = len([e for e in regs if e.startswith(name)])
- if poss == 0:
- return name
-
- return name + f' {poss}'
- # Loop over keys, sorted
- # regs.sort()
- # for r in regs:
- # if targ == r:
- # same += 1
- # targ = name + f' {same:02d}'
- # print(f' Found region named {r} -> {targ}')
-
- return targ
-
-# --------------------------------------------------------------------
-def add_region(root,grid,name,originx,originy,gpid,
- alsoPiece=True,piece=None,verbose=True):
- # First thing is to check if we have a piece
- piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose)
- if verbose:
- print(f' Region {name} -> piece {piece}')
-
-
- nam = check_region_name(grid,name)
- if verbose:
- print(f' Region {name} -> real name {nam}')
- n = add_node(root,grid,PICKER+'board.Region',
- name = nam,
- originx = originx,
- originy = originy)
- if piece is None:
- return n, gpid
-
- if verbose:
- print(f' Add piece {name} to region')
- _, gpid = add_atstart(root,map,name,piece,
- location = nam,
- useGridLocation = True,
- owningBoard = map.getAttribute('mapName'),
- x = 0,
- y = 0,
- gpid = gpid)
- return n, gpid
-
-
-# --------------------------------------------------------------------
-def add_stacking(root,map,
- bottom = '40,0',
- disabled = False,
- down = '37,0',
- exSepX = 6,
- exSepY = 18,
- top = '38,0',
- unexSepX = 8,
- unexSepY = 16,
- up = '39,0'):
- return add_node(root,map,MAP+'StackMetrics',
- bottom = bottom,
- disabled = disabled,
- down = down,
- exSepX = exSepX,
- exSepY = exSepY,
- top = top,
- unexSepX = unexSepX,
- unexSepY = unexSepY,
- up = up)
-
-# --------------------------------------------------------------------
-def add_camera(root,map,
- buttonText = '',
- canDisable = False,
- hotkey = '',
- icon = '/images/camera.gif',
- propertyGate = '',
- tooltip = 'Save map as PNG image'):
- return add_node(root,map,MAP+'ImageSaver',
- buttonText = buttonText,
- canDisable = canDisable,
- hotkey = hotkey,
- icon = icon,
- propertyGate = propertyGate,
- tooltip = tooltip)
-
-# --------------------------------------------------------------------
-def add_forwardtochatter(root,map):
- return add_node(root,map,MAP+'ForwardToChatter')
-
-# --------------------------------------------------------------------
-def add_menudisplayer(root,map):
- return add_node(root,map,MAP+'MenuDisplayer')
-
-# --------------------------------------------------------------------
-def add_mapcenterer(root,map):
- return add_node(root,map,MAP+'MapCenterer')
-
-# --------------------------------------------------------------------
-def add_stackexpander(root,map):
- return add_node(root,map,MAP+'StackExpander')
-
-# --------------------------------------------------------------------
-def add_piecemover(root,map):
- return add_node(root,map,MAP+'PieceMover')
-
-# --------------------------------------------------------------------
-def add_selectionhighlighters(root,map):
- return add_node(root,map,MAP+'SelectionHighlighters')
-
-# --------------------------------------------------------------------
-def add_keybufferer(root,map):
- return add_node(root,map,MAP+'KeyBufferer')
-
-# --------------------------------------------------------------------
-def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2):
- return add_node(root,map,MAP+'HighlightLastMoved',
- color=color,enabled=enabled,thickness=thickness)
-
-# --------------------------------------------------------------------
-def add_counterviewer(root,map,
- borderWidth = 0,
- centerAll = False,
- centerText = False,
- combineCounterSummary = False,
- counterReportFormat = '',
- delay = 700,
- description = '',
- display = 'from top-most layer only',
- emptyHexReportForma = '$LocationName$',
- enableHTML = True,
- extraTextPadding = 0,
- fgColor = rgb(0,0,0),
- fontSize = 9,
- graphicsZoom = 1.0,
- hotkey = key('\n',0),
- layerList = '',
- minDisplayPieces = 2,
- propertyFilter = '',
- showDeck = False,
- showDeckDepth = 1,
- showDeckMasked = False,
- showMoveSelectde = False,
- showNoStack = False,
- showNonMovable = False,
- showOverlap = False,
- showgraph = True,
- showgraphsingle = False,
- showtext = False,
- showtextsingle = False,
- stretchWidthSummary = False,
- summaryReportFormat = '$LocationName$',
- unrotatePieces = False,
- version = 3,
- verticalOffset = 0,
- verticalTopText = 5,
- zoomlevel = 1.0):
- return add_node(root,map,MAP+'CounterDetailViewer',
- borderWidth = borderWidth,
- centerAll = centerAll,
- centerText = centerText,
- combineCounterSummary = combineCounterSummary,
- counterReportFormat = counterReportFormat,
- delay = delay,
- description = description,
- display = display,
- emptyHexReportForma = emptyHexReportForma,
- enableHTML = enableHTML,
- extraTextPadding = extraTextPadding,
- fgColor = fgColor,
- fontSize = fontSize,
- graphicsZoom = graphicsZoom,
- hotkey = hotkey,
- layerList = layerList,
- minDisplayPieces = minDisplayPieces,
- propertyFilter = propertyFilter,
- showDeck = showDeck,
- showDeckDepth = showDeckDepth,
- showDeckMasked = showDeckMasked,
- showMoveSelectde = showMoveSelectde,
- showNoStack = showNoStack,
- showNonMovable = showNonMovable,
- showOverlap = showOverlap,
- showgraph = showgraph,
- showgraphsingle = showgraphsingle,
- showtext = showtext,
- showtextsingle = showtextsingle,
- stretchWidthSummary = stretchWidthSummary,
- summaryReportFormat = summaryReportFormat,
- unrotatePieces = unrotatePieces,
- version = version,
- verticalOffset = verticalOffset,
- verticalTopText = verticalTopText,
- zoomlevel = zoomlevel)
-
-# --------------------------------------------------------------------
-def add_globalmap(root,map,
- buttonText = '',
- color = rgb(255,0,0),
- hotkey = key('O',CTRL_SHIFT),
- icon = '/images/overview.gif',
- scale = 0.2,
- tooltip = 'Show/Hide overview window'):
- return add_node(root,map,MAP+'GlobalMap',
- buttonText = buttonText,
- color = color,
- hotkey = hotkey,
- icon = icon,
- scale = scale,
- tooltip = 'Show/Hide overview window')
-
-# --------------------------------------------------------------------
-def add_zoomer(root,map,
- inButtonText = '',
- inIconName = '/images/zoomIn.gif',
- inTooltip = 'Zoom in',
- outButtonText = '',
- outIconName = '/images/zoomOut.gif',
- outTooltip = 'Zoom out',
- pickButtonText = '',
- pickIconName = '/images/zoom.png',
- pickTooltip = 'Select Zoom',
- zoomInKey = key('=',CTRL_SHIFT),
- zoomLevels = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6],
- zoomOutKey = key('-'),
- zoomPickKey = key('='),
- zoomStart = 3):
- '''Zoom start is counting from the back (with default zoom levels,
- and zoom start, the default zoom is 1'''
- lvls = ','.join([str(z) for z in zoomLevels])
- return add_node(root,map,MAP+'Zoomer',
- inButtonText = inButtonText,
- inIconName = inIconName,
- inTooltip = inTooltip,
- outButtonText = outButtonText,
- outIconName = outIconName,
- outTooltip = outTooltip,
- pickButtonText = pickButtonText,
- pickIconName = pickIconName,
- pickTooltip = pickTooltip,
- zoomInKey = zoomInKey,
- zoomLevels = lvls,
- zoomOutKey = zoomOutKey,
- zoomPickKey = zoomPickKey,
- zoomStart = zoomStart)
-
-# --------------------------------------------------------------------
-def add_hidepieces(root,map,
- buttonText = '',
- hiddenIcon = '/images/globe_selected.gif',
- hotkey = key('O'),
- showingIcon = '/images/globe_unselected.gif',
- tooltip = 'Hide all pieces on this map'):
- return add_node(root,map,MAP+'HidePiecesButton',
- buttonText = buttonText,
- hiddenIcon = hiddenIcon,
- hotkey = hotkey,
- showingIcon = showingIcon,
- tooltip = tooltip)
-# --------------------------------------------------------------------
-def add_masskey(root,map,
- name,
- buttonHotkey,
- hotkey,
- buttonText = '',
- canDisable = False,
- deckCount = '-1',
- filter = '',
- propertyGate = '',
- reportFormat = '',
- reportSingle = False,
- singleMap = True,
- target = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS',
- tooltip = '',
- icon = ''):
- '''Default targets are selected units'''
- return add_node(root,map,MAP+'MassKeyCommand',
- name = name,
- buttonHotkey = buttonHotkey, # This hot key
- hotkey = hotkey, # Target hot key
- buttonText = buttonText,
- canDisable = canDisable,
- deckCount = deckCount,
- filter = filter,
- propertyGate = propertyGate,
- reportFormat = reportFormat,
- reportSingle = reportSingle,
- singleMap = singleMap,
- target = target,
- tooltip = tooltip,
- icon = icon)
-
-
-# --------------------------------------------------------------------
-def add_flare(root,map,
- circleColor = rgb(255,0,0),
- circleScale = True,
- circleSize = 100,
- flareKey = 'keyAlt',
- flareName = 'Map Flare',
- flarePulses = 6,
- flarePulsesPerSec = 3,
- reportFormat = ''):
- return add_node(root,map,MAP+'Flare',
- circleColor = circleColor,
- circleScale = circleScale,
- circleSize = circleSize,
- flareKey = flareKey,
- flareName = flareName,
- flarePulses = flarePulses,
- flarePulsesPerSec = flarePulsesPerSec,
- reportFormat = '')
-
-# --------------------------------------------------------------------
-def add_atstart(root,map,name,*pieces,
- location = '',
- useGridLocation = True,
- owningBoard = '',
- x = 0,
- y = 0,
- gpid = None):
- '''Pieces are existing PieceSlot elements'''
- a = add_node(root,map,MODULE+'map.SetupStack',
- name = name,
- location = location,
- owningBoard = owningBoard,
- useGridLocation = useGridLocation,
- x = x,
- y = y)
- # copy pieces here
- for p in pieces:
- c = p.cloneNode(True)
- a.appendChild(c)
- if gpid is not None:
- # Update gpid in element
- c.setAttribute('gpid',str(gpid))
- # Update gpid in traits state
- traits = get_piece_parts(p.childNodes[0].nodeValue)
- for t in traits:
- code = t['def']
- stat = t['state']
- if code[0] == 'piece':
- stat[3] = gpid
- # Put back the traits
- c.childNodes[0].nodeValue = dicts_to_piece(traits)
-
- gpid += 1
-
- return a, gpid
-
-# --------------------------------------------------------------------
-def add_pieceslot(root,elem,
- entryName,
- gpid,
- code,
- height=72,
- width=72,
- icon = ''):
- p = add_node(root,elem,WIDGET+'PieceSlot',
- entryName = entryName,
- gpid = gpid,
- height = height,
- width = width,
- icon = icon)
- add_text(root,p,code)
- return p
-
-# --------------------------------------------------------------------
-def add_prototypes(root,elem,*defs):
- '''Defs are existing definition elements'''
- p = add_node(root,elem,MODULE+'PrototypesContainer')
-
- for d in defs:
- c = d.cloneNode(True)
- p.appendChild(c)
-
- return p
-
-# --------------------------------------------------------------------
-def add_prototype(root,elem,name,code,description=''):
- d = add_node(root,elem,MODULE+'PrototypeDefinition',
- name = name,
- description = description)
- add_text(root,d,code)
- return d
-
-# --------------------------------------------------------------------
-def add_piecewindow(root,elem,name,
- defaultWidth = 0,
- hidden = False,
- hotkey = key('C',ALT),
- scale = 1.,
- text = '',
- tooltip = 'Show/hide piece window',
- icon = '/images/counter.gif'):
- return add_node(root,elem,MODULE+'PieceWindow',
- name = name,
- defaultWidth = defaultWidth,
- hidden = hidden,
- hotkey = hotkey,
- scale = scale,
- text = text,
- tooltip = tooltip,
- icon = icon)
-
-# --------------------------------------------------------------------
-def add_tabs(root,elem,entryName):
- return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName)
-
-# --------------------------------------------------------------------
-def add_list(root,elem,
- entryName,
- height = 0,
- width = 0,
- scale = 1.,
- divider = 100):
- return add_node(root,elem,WIDGET+'ListWidget',
- entryName = entryName,
- height = height,
- width = width,
- scale = scale,
- divider = divider)
-
-# --------------------------------------------------------------------
-def add_panel(root,elem,
- entryName,
- fixed = False,
- nColumns = 1,
- vert = False):
- return add_node(root,elem,WIDGET+'PanelWidget',
- entryName = entryName,
- fixed = fixed,
- nColumns = nColumns,
- vert = vert)
-
-# --------------------------------------------------------------------
-def add_mapwidget(root,elem,entryName=''):
- return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName)
-
-# --------------------------------------------------------------------
-def add_chartwindow(root,elem,
- name,
- hotkey = key('A',ALT),
- description = '',
- text = '',
- tooltip = 'Show/hide Charts',
- icon = '/images/chart.gif'):
- return add_node(root,elem,MODULE+'ChartWindow',
- name = name,
- hotkey = hotkey,
- description = description,
- text = text,
- tooltip = tooltip,
- icon = icon)
-
-# --------------------------------------------------------------------
-def add_chart(root,elem,
- chartName,
- fileName,
- description = ''):
- return add_node(root,elem,WIDGET+'Chart',
- chartName = chartName,
- description = description,
- fileName = fileName)
-
-# --------------------------------------------------------------------
-def add_dice(root,elem,
- addToTotal = 0,
- canDisable = False,
- hotkey = key('6',ALT),
- icon = '/images/die.gif',
- keepCount = 1,
- keepDice = False,
- keepOption = '&gt;',
- lockAdd = False,
- lockDice = False,
- lockPlus = False,
- lockSides = False,
- nDice = 1,
- nSides = 6,
- name = '1d6',
- plus = 0,
- prompt = False,
- propertyGate = '',
- reportFormat = '** $name$ = $result$ *** &amp;lt;$PlayerName$&amp;gt;',
- reportTotal = False,
- sortDice = False,
- text = '1d6',
- tooltip = 'Roll a 1d6'):
- return add_node(root,elem,MODULE+'DiceButton',
- addToTotal = addToTotal,
- canDisable = canDisable,
- hotkey = hotkey,
- icon = icon,
- keepCount = keepCount,
- keepDice = keepDice,
- keepOption = keepOption,
- lockAdd = lockAdd,
- lockDice = lockDice,
- lockPlus = lockPlus,
- lockSides = lockSides,
- nDice = nDice,
- nSides = nSides,
- name = name,
- plus = plus,
- prompt = prompt,
- propertyGate = propertyGate,
- reportFormat = reportFormat,
- reportTotal = reportTotal,
- sortDice = sortDice,
- text = text,
- tooltip = tooltip)
-
-# ====================================================================
-def enc_def(args,term=False):
- '''Encode a piece (decorator) definition
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalproperty.py
+
+# --------------------------------------------------------------------
+class GlobalPropertyTrait(ChangePropertyTrait):
+ # The real value of CURRENT_ZONE causes problems when copying the
+ # trait, since it contains slashes. Maybe a solition is to make
+ # it a raw string with escaped slashes? No, that's already done
+ # below when setting the type. However, the default in the Java
+ # code is the CURRENT_ZONE real value, so setting this to the
+ # empty string should make it be that value.
+ ID = 'setprop'
+ CURRENT_ZONE = 'Current Zone/Current Map/Module'
+ NAMED_ZONE = 'Named Zone'
+ NAMED_MAP = 'Named Map'
+ DIRECT = 'P'
+ def __init__(self,
+ *commands,
+ name = '',
+ numeric = False,
+ min = 0,
+ max = 100,
+ wrap = False,
+ description = '',
+ level = CURRENT_ZONE,
+ search = ''):
+ '''Commands are
+
+ - menu
+ - key
+ - Type (only 'P' for now)
+ - Expression
+ '''
+ super(GlobalPropertyTrait,self).__init__(*commands,
+ numeric = numeric,
+ min = min,
+ max = max,
+ wrap = wrap)
+ self.setType(name = name,
+ constraints = self._constraints,
+ commands = self._commands,
+ description = description,
+ level = level.replace('/',r'\/'),
+ search = search)
+ self.setState()
+
+Trait.known_traits.append(GlobalPropertyTrait)
- Parameters
- ----------
- args : list
- List of fields
- term : bool
- If true, add terminating ';'
+#
+# EOF
+#
+# ====================================================================
+# From traits/prototype.py
- Returns
- -------
- code : str
- The encoded definition
- '''
- return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
- for e in args])+(';' if term else '')
-
-# --------------------------------------------------------------------
-def layer_trait(images,
- newNames = None,
- activateName = 'Activate',
- activateMask = CTRL,
- activateChar = 'A',
- increaseName = 'Increase',
- increaseMask = CTRL,
- increaseChar = '[',
- decreaseName = '',
- decreaseMask = CTRL,
- decreaseChar = ']',
- resetName = '',
- resetKey = '',
- resetLevel = 1,
- under = False,
- underXoff = 0,
- underYoff = 0,
- loop = True,
- name = '',
- description = '',
- randomKey = '',
- randomName = '',
- follow = False,
- expression = '',
- first = 1,
- version = 1, # 1:new, 0:old
- always = 'true',
- activateKey = key('A'),
- increaseKey = key('['),
- decreaseKey = key(']'),
- scale = 1.):
- '''Create a layer trait (VASSAL.counter.Embellishment)
+# --------------------------------------------------------------------
+class PrototypeTrait(Trait):
+ ID = 'prototype'
+ def __init__(self,name=''):
+ '''Create a prototype trait (VASSAL.counter.UsePrototype)'''
+ super(PrototypeTrait,self).__init__()
+ self.setType(name = name)
+ self.setState(ignored = '')
- Parameters
- ----------
- main : dict
- Main image dictionary
- flipped : dict
- Flipped image dictionary
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state
- '''
- if newNames is None:
- newNames = ['']*len(images)
- args = ['emb2', # Code
- activateName, activateMask, activateChar, # ACTIVATE ; MASK ; KEY
- increaseName, increaseMask, increaseChar,
- decreaseName, decreaseMask, decreaseChar,
- resetName, resetKey, resetLevel,
- under, underXoff, underYoff,
- ','.join(images),
- ','.join(newNames),
- loop,
- name,
- randomKey, randomName,
- follow, expression,
- first,
- version,
- always,
- activateKey,
- increaseKey,
- decreaseKey,
- description,
- scale ]
- stat = '1' # CURRENT LEVEL
- return enc_def(args), stat
-
-# --------------------------------------------------------------------
-def prototype_trait(name):
- '''Create a prototype trait (VASSAL.counter.UsePrototype)
+Trait.known_traits.append(PrototypeTrait)
- Parameters
- ----------
- family : str
- Side in game
-
- Return
- ------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['prototype', name ]
- stat = '' # State is nothing
- return enc_def(args), stat
+#
+# EOF
+#
+# ====================================================================
+# From traits/place.py
+
+# --------------------------------------------------------------------
+class PlaceTrait(Trait):
+ ID = 'placemark'
+ STACK_TOP = 0
+ STACK_BOTTOM = 1
+ ABOVE = 2
+ BELOW = 3
+
+ # How the LaTeX exporter organises the units. Format with
+ # 0: the group
+ # 1: the piece name
+ # SKEL_PATH = (PieceWindow.TAG +r':Counters\/' +
+ # TabWidget.TAG +r':Counters\/' +
+ # PanelWidget.TAG +':{0}' +r'\/'+
+ # ListWidget.TAG +':{0} counters'+r'\/'+
+ # PieceSlot.TAG +':{1}')
+ @classmethod
+ @property
+ def SKEL_PATH(cls):
+
+ return (PieceWindow.TAG +r':Counters\/' +
+ TabWidget.TAG +r':Counters\/' +
+ PanelWidget.TAG +':{0}' +r'\/'+
+ ListWidget.TAG +':{0} counters'+r'\/'+
+ PieceSlot.TAG +':{1}')
+
+ def __init__(self,
+ command = '', # Context menu name
+ key = '', # Context menu key
+ markerSpec = '', # Full path in module
+ markerText = 'null', # Hard coded message
+ xOffset = 0,
+ yOffset = 0,
+ matchRotation = True,
+ afterKey = '',
+ description = '',
+ gpid = '', # Set in JAVA, but with warning
+ placement = ABOVE,
+ above = False):
+ '''Create a place marker trait (VASSAL.counter.PlaceMarker)'''
+ super(PlaceTrait,self).__init__()
+ self.setType(command = command, # Context menu name
+ key = key, # Context menu key
+ markerSpec = markerSpec,
+ markerText = markerText,
+ xOffset = xOffset,
+ yOffset = yOffset,
+ matchRotation = matchRotation,
+ afterKey = afterKey,
+ description = description,
+ gpid = gpid,
+ placement = placement,
+ above = above)
+ self.setState()
+
+Trait.known_traits.append(PlaceTrait)
+
+# --------------------------------------------------------------------
+class ReplaceTrait(PlaceTrait):
+ ID = 'replace'
+ def __init__(self,
+ command = '', # Context menu name
+ key = '', # Context menu key
+ markerSpec = '', # Full path in module
+ markerText = 'null', # Hard message
+ xOffset = 0,
+ yOffset = 0,
+ matchRotation = True,
+ afterKey = '',
+ description = '',
+ gpid = '', # Set in JAVA
+ placement = PlaceTrait.ABOVE,
+ above = False):
+ super(ReplaceTrait,self).__init__(command = command,
+ key = key,
+ markerSpec = markerSpec,
+ markerText = markerText,
+ xOffset = xOffset,
+ yOffset = yOffset,
+ matchRotation = matchRotation,
+ afterKey = afterKey,
+ description = description,
+ gpid = gpid,
+ placement = placement,
+ above = above)
+
-# --------------------------------------------------------------------
-def mark_trait(key,value):
- '''Create a marker trait (VASSAL.counter.Marker)
+Trait.known_traits.append(ReplaceTrait)
- Parameters
- ----------
- family : str
- Side in game
-
- Return
- ------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['mark', key ]
- stat = value # State is nothing
- return enc_def(args), stat
+#
+# EOF
+#
+# ====================================================================
+# From traits/report.py
# --------------------------------------------------------------------
-def report_trait(*keys,
+class ReportTrait (Trait):
+ ID = 'report'
+ def __init__(self,
+ *keys,
nosuppress = True,
description = '',
report = '$location$: $newPieceName$ $menuCommand$ *',
cyclekeys = '',
cyclereps = ''):
- '''Create a report trait (VASSAL.counters.ReportActon)
+ '''Create a report trait (VASSAL.counters.ReportActon)'''
+ super(ReportTrait,self).__init__()
+ esckeys = ','.join([k.replace(',',r'\,') for k in keys])
+ esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
+ escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
+
+ self.setType(keys = esckeys,
+ report = report,
+ cycleKeys = esccycl,
+ cycleReports = escreps,
+ description = description,
+ nosuppress = nosuppress)
+ self.setState(cycle = -1)
- Parameters
- ----------
- *keys : tuple of str
- The keys to report actions for
-
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- esckeys = ','.join([k.replace(',',r'\,') for k in keys])
- esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
- escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
- args = ['report', # CODE
- esckeys, # ESCAPED KEYS (commas escaped)
- report, # REPORT
- esccycl, # CYCLE KEYS (commas escaped)
- escreps, # CYCKE REPORTS (commas escaped)
- description, # DESCRIPTION
- nosuppress] # NOSUPPRESS
- stat = '-1' # CYCLE STATE
- return enc_def(args), stat
-
-# --------------------------------------------------------------------
-def moved_trait(image='moved.gif',
- xoff = 36,
- yoff = -38,
- name = 'Mark Moved',
- key = key('M')):
- '''Create a moved trait (VASSAL.counters.MovementMarkable)
+Trait.known_traits.append(ReportTrait)
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['markmoved', # CODE
- image, # IMAGE
- xoff, yoff, # XOFF ; YOFF
- name, # MENU
- key, # KEY, MODIFIER
- ''] # ?
- stat = False # MOVED?
- return enc_def(args), stat
-
-# --------------------------------------------------------------------
-def trail_trait(key = key('T'),
- name = 'Movement Trail',
- localVisible = False,
- globalVisible = False,
- radius = 10,
- fillColor = rgb(255,255,255),
- lineColor = rgb(0,0,0),
- activeOpacity = 100,
- inactiveOpacity = 50,
- edgesBuffer = 20,
- displayBuffer = 30,
- lineWidth = 1,
- turnOn = '',
- turnOff = '',
- reset = '',
- description = ''):
- ''' Create a movement trail trait ( VASSAL.counters.Footprint)
-
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state
- '''
- args = ['footprint', # CODE
- key, # ENABLE KEY
- name, # MENU
- localVisible, # LOCAL VISABLE
- globalVisible, # GLOBAL VISABLE
- radius, # RADIUS
- fillColor, # FILL COLOR
- lineColor, # LINE COLOR
- activeOpacity, # ACTIVE OPACITY
- inactiveOpacity, # INACTIVE OPACITY
- edgesBuffer, # EDGES BUFFER
- displayBuffer, # DISPLAY BUFFER
- lineWidth, # LINE WIDTH
- turnOn, # TURN ON KEY
- turnOff, # TURN OFF KEY
- reset, # RESET KEY
- description] # DESC
- stat = [False, # GLOBAL VIS
- '', # MAP
- 0] # POINTS (followed by [; [X,Y]*]
- return enc_def(args), enc_def(stat)
-
-# --------------------------------------------------------------------
-def delete_trait(name = 'Delete', key = key('D')):
- '''Create a delete trait (VASSAL.counters.Delete)
-
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['delete', name, key, '']
- stat = '' # Nothing
- return enc_def(args), stat
+#
+# EOF
+#
+# ====================================================================
+# From traits/calculatedproperty.py
# --------------------------------------------------------------------
-def sendto_trait(mapName,
- boardName,
- name = '',
- key = key('E'),
- restoreName = 'Restore',
- restoreKey = key('R'),
- x = 200,
- y = 200,
- xidx = 0,
- yidx = 0,
- xoff = 1,
- yoff = 1,
- description = '',
- destination = 'L',
- zone = '',
- region = '',
- expression = '',
- position = ''):
- '''Create a send to trait (VASSAL.counter.SendToLocation)
+class CalculatedTrait(Trait):
+ ID = 'calcProp'
+ def __init__(self,name='',expression='',description=''):
+ '''Define a trait that calculates a property'''
+ super(CalculatedTrait,self).__init__()
+ self.setType(name = name,
+ expression = expression,
+ description = description)
+ self.setState()
- Parameters
- ----------
- side : str
- Sub-map to move to
-
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['sendto', # CODE
- name, # NAME
- key, # KEY , MODIFIER
- mapName, # MAP
- boardName, # BOARD
- x, y, # X ; Y
- restoreName, # BACK
- restoreKey, # KEY , MODIFIER
- xidx, yidx, # XIDX ; YIDX
- xoff, yoff, # XOFF ; YOFF
- description, # DESC
- destination, # DEST
- zone, # ZONE
- region, # REGION
- expression, # EXPRESSION
- position ] # GRIDPOS
- stat = ['', # BACKMAP
- '', # X
- ''] # Y
- return enc_def(args), enc_def(stat)
-
-# --------------------------------------------------------------------
-def basic_trait(name,
- filename, # Can be empty
- gpid, # Can be empty
- cloneKey='', # Deprecated
- deleteKey=''):# Deprecated
- '''Create a basic unit (VASSAL.counters.BasicPiece)
- Parameters
- ----------
- main : dict
- Information
- gpid : int
- Piece global identifier
- image : bool
- If true, add image
+Trait.known_traits.append(CalculatedTrait)
- Returns
- -------
- code : str
- Encoded trait
- stat : str
- Encoded state (empty string)
- '''
- args = ['piece', # CODE
- cloneKey, # CLONEKEY
- deleteKey, # DELETEKEY
- filename, # IMAGE
- name] # NAME
- stat = ['null', # MAPID (possibly 'null')
- 0, # X
- 0, # Y
- gpid, # GLOBAL PIECE ID
- 0] # PROPERTY COUNT (followed by [; KEY; VALUE]+)
- return enc_def(args), enc_def(stat)
-
-# --------------------------------------------------------------------
-def enc_parts(*parts):
- '''Encode parts of a full piece definition
-
- Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece)
- definition or state is separated by a litteral TAB character.
- Beyond the first TAB separator, additional escape characters
- (BACKSLAH) are added in front of the separator. This is to that
- VASSAL.utils.SequenceDecoder does not see consequitive TABs as a
- single TAB.
-
- Parameters
- ----------
- parts : tuple
- The various trait definitions or states
-
- Returns
- -------
- code : str
- The encoded piece
- '''
- ret = ''
- sep = r' '
- for i, p in enumerate(parts):
- if i != 0:
- ret += '\\'*(i-1) + sep
- ret += str(p)
+#
+# EOF
+#
+# ====================================================================
+# From traits/restrictcommand.py
+
+# --------------------------------------------------------------------
+class RestrictCommandsTrait(Trait):
+ ID = 'hideCmd'
+ HIDE = 'Hide'
+ DISABLE = 'Disable'
+ def __init__(self,
+ name = '',
+ hideOrDisable = HIDE,
+ expression = '',# Restrict when true
+ keys = []):
+ '''Create a layer trait (VASSAL.counter.RestrictCommands)'''
+ super(RestrictCommandsTrait,self).__init__()
+ encKeys = ','.join([k.replace(',',r'\,') for k in keys])
+ self.setType(name = name,
+ hideOrDisable = hideOrDisable,
+ expression = expression,
+ keys = encKeys)
+ self.setState(state='')
+ def setKeys(self,keys):
+ self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
+
- return ret
+Trait.known_traits.append(RestrictCommandsTrait)
+#
+# EOF
+#
+# ====================================================================
+# From traits/label.py
+
+class LabelTraitCodes:
+ TOP = 't'
+ BOTTOM = 'b'
+ CENTER = 'c'
+ LEFT = 'l'
+ RIGHT = 'r'
+ PLAIN = 0
+ BOLD = 1
+ ITALIC = 2
+
# --------------------------------------------------------------------
-def add_piece(typ, state):
- '''Create and add piece command (VASSAL.command.AddPiece)
+class LabelTrait(Trait):
+ ID = 'label'
+ def __init__(self,
+ label = None,
+ labelKey = '',
+ menuCommand ='Change label',
+ fontSize = 10,
+ background = 'none',
+ foreground = '255,255,255',
+ vertical = LabelTraitCodes.TOP,
+ verticalOff = 0,
+ horizontal = LabelTraitCodes.CENTER,
+ horizontalOff = 0,
+ verticalJust = LabelTraitCodes.BOTTOM,
+ horizontalJust = LabelTraitCodes.CENTER,
+ nameFormat = '$pieceName$ ($label$)',
+ fontFamily = 'Dialog',
+ fontStyle = LabelTraitCodes.PLAIN,
+ rotate = 0,
+ propertyName = 'TextLabel',
+ description = '',
+ alwaysUseFormat = False):
+ '''Create a label trait (can be edited property)'''
+ super(LabelTrait,self).__init__()
+ self.setType(labelKey = labelKey,
+ menuCommand = menuCommand,
+ fontSize = fontSize,
+ background = background,
+ foreground = foreground,
+ vertical = vertical,
+ verticalOff = verticalOff,
+ horizontal = horizontal,
+ horizontalOff = horizontalOff,
+ verticalJust = verticalJust,
+ horizontalJust = horizontalJust,
+ nameFormat = nameFormat,
+ fontFamily = fontFamily,
+ fontStyle = fontStyle,
+ rotate = rotate,
+ propertyName = propertyName,
+ description = description,
+ alwaysUseFormat = alwaysUseFormat)
+ self.setState(label = (nameFormat if label is None else label))
+
+
+Trait.known_traits.append(LabelTrait)
- Each part is separated by a SLASH ('/'). The first part is the
- command type (PLUS '+') to perform. Second is the IDENTIFIER,
- possibly 'null'. Then comes the fully encoded type of the piece,
- followed by the state of the piece (also fully encoded).
+#
+# EOF
+#
+# ====================================================================
+# From traits/layer.py
+
+# --------------------------------------------------------------------
+class LayerTrait(Trait):
+ ID = 'emb2'
+ def __init__(self,
+ images = [''],
+ newNames = None,
+ activateName = 'Activate',
+ activateMask = CTRL,
+ activateChar = 'A',
+ increaseName = 'Increase',
+ increaseMask = CTRL,
+ increaseChar = '[',
+ decreaseName = '',
+ decreaseMask = CTRL,
+ decreaseChar = ']',
+ resetName = '',
+ resetKey = '',
+ resetLevel = 1,
+ under = False,
+ underXoff = 0,
+ underYoff = 0,
+ loop = True,
+ name = '',
+ description = '',
+ randomKey = '',
+ randomName = '',
+ follow = False,
+ expression = '',
+ first = 1,
+ version = 1, # 1:new, 0:old
+ always = True,
+ activateKey = key('A'),
+ increaseKey = key('['),
+ decreaseKey = key(']'),
+ scale = 1.):
+ '''Create a layer trait (VASSAL.counter.Embellishment)'''
+ super(LayerTrait,self).__init__()
+ if newNames is None and images is not None:
+ newNames = ['']*len(images)
+ self.setType(
+ activateName = activateName,
+ activateMask = activateMask,
+ activateChar = activateChar,
+ increaseName = increaseName,
+ increaseMask = increaseMask,
+ increaseChar = increaseChar,
+ decreaseName = decreaseName,
+ decreaseMask = decreaseMask,
+ decreaseChar = decreaseChar,
+ resetName = resetName,
+ resetKey = resetKey,
+ resetLevel = resetLevel,
+ under = under,
+ underXoff = underXoff,
+ underYoff = underYoff,
+ images = ','.join(images),
+ newNames = ','.join(newNames),
+ loop = loop,
+ name = name,
+ randomKey = randomKey,
+ randomName = randomName,
+ follow = follow,
+ expression = expression,
+ first = first,
+ version = version,
+ always = always,
+ activateKey = activateKey,
+ increaseKey = increaseKey,
+ decreaseKey = decreaseKey,
+ description = description,
+ scale = scale)
+ self.setState(level=1)
+
+Trait.known_traits.append(LayerTrait)
- Parameters
- ----------
- typ : str
- Encoded piece definition
- state : str
- Encoded state definition
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalcommand.py
+
+# --------------------------------------------------------------------
+class GlobalCommandTrait(Trait):
+ ID = 'globalkey'
+ def __init__(self,
+ commandName = '',
+ key = '', # Command received
+ globalKey = '', # Command to send to targets
+ properties = '', # Filter target on this expression
+ ranged = False,
+ range = 1,
+ reportSingle = True,
+ fixedRange = True,
+ rangeProperty = '',
+ description = '',
+ deskSelect = '-1',
+ target = ''):
+ '''Create a global key command in piece
+ (VASSAL.counters.CounterGlobalKeyCommand)'''
+ self.setType(commandName = commandName,
+ key = key,
+ globalKey = globalKey,
+ properties = properties,
+ ranged = ranged,
+ range = range,
+ reportSingle = reportSingle,
+ fixedRange = fixedRange,
+ rangeProperty = rangeProperty,
+ description = description,
+ deskSelect = deskSelect,
+ target = target)
+ self.setState()
+
+Trait.known_traits.append(GlobalCommandTrait)
- Returns
- -------
- cmd : str
- The add command encoded
- '''
- args = ['+', # Type
- 'null', # IDENTIFER
- typ, # DEFINITION
- state] # STATE
- return '/'.join(args)
+#
+# EOF
+#
+# ====================================================================
+# From traits/globalhotkey.py
+
+# --------------------------------------------------------------------
+class GlobalHotkeyTrait(Trait):
+ ID = 'globalhotkey'
+ def __init__(self,
+ name = '', # Command received
+ key = '', # Command key received
+ globalHotkey = '', # Key to send
+ description = ''):
+ '''Create a global key command in piece
+ (VASSAL.counters.GlobalHotkey)'''
+ self.setType(name = name,
+ key = key,
+ globalHotkey = globalHotkey,
+ description = description)
+ self.setState()
+
+Trait.known_traits.append(GlobalHotkeyTrait)
-# --------------------------------------------------------------------
-def add_proto(typ,state):
- '''Create an add prototype command. This is the same as 'add_piece'
+#
+# EOF
+#
+# ====================================================================
+# From traits/nostack.py
+
+# --------------------------------------------------------------------
+class NoStackTrait(Trait):
+ ID = 'immob'
+ NORMAL_SELECT = ''
+ SHIFT_SELECT = 'i'
+ CTRL_SELECT = 't'
+ ALT_SELECT = 'c'
+ NEVER_SELECT = 'n'
+ NORMAL_BAND_SELECT = ''
+ ALT_BAND_SELECT = 'A'
+ ALT_SHIFT_BAND_SELECT = 'B'
+ NEVER_BAND_SELECT = 'Z'
+ NORMAL_MOVE = 'N'
+ SELECT_MOVE = 'I'
+ NEVER_MOVE = 'V'
+ NORMAL_STACK = 'L'
+ NEVER_STACK = 'R'
+ IGNORE_GRID = 'g'
+ def __init__(self,
+ select = NORMAL_SELECT,
+ bandSelect = NORMAL_BAND_SELECT,
+ move = NORMAL_MOVE,
+ canStack = False,
+ ignoreGrid = False,
+ description = ''):
+
+ selectionOptions = (select +
+ (self.IGNORE_GRID if ignoreGrid else '') +
+ bandSelect)
+ movementOptions = move
+ stackingOptions = self.NORMAL_STACK if canStack else self.NEVER_STACK
+
+ '''Create a mark trait (static property)'''
+ super(NoStackTrait,self).__init__()
+
+ self.setType(selectionOptions = selectionOptions,
+ movementOptions = movementOptions,
+ stackingOptions = stackingOptions,
+ description = description)
+ self.setState()
- Parameters
- ----------
- typ : str
- Encoded piece definition
- state : str
- Encoded state definition
- Returns
- -------
- cmd : str
- The add command encoded
- '''
- return add_piece(typ,state)
-
+Trait.known_traits.append(NoStackTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/deselect.py
# --------------------------------------------------------------------
-def add_proto_code(decs):
- typs = [d[0] for d in decs]
- stts = [d[1] for d in decs]
- typ = enc_parts(*typs)
- stt = enc_parts(*stts)
+class DeselectTrait(Trait):
+ ID = 'deselect'
+ THIS = 'D' # Deselect only this piece
+ ALL = 'A' # Deselect all pieces
+ ONLY = 'S' # Select this piece only
+ def __init__(self,
+ command = '',
+ key = '',
+ description = '',
+ unstack = False,
+ deselect = THIS):
+ '''Create a deselect trait'''
+ super(DeselectTrait,self).__init__()
+ self.setType(command = command,
+ key = key,
+ description = description,
+ unstack = unstack,
+ deselect = deselect)
+ self.setState()
+
- return add_proto(typ,stt)
+Trait.known_traits.append(DeselectTrait)
+#
+# EOF
+#
# ====================================================================
+# From traits/restrictaccess.py
+
+# --------------------------------------------------------------------
+class RestrictAccessTrait(Trait):
+ ID = 'restrict'
+ def __init__(self,
+ sides = [],
+ byPlayer = False,
+ noMovement = True,
+ description = '',
+ owner = '',):
+ '''Create a layer trait (VASSAL.counter.Restricted)'''
+ super(RestrictAccessTrait,self).__init__()
+ encSides = ','.join(sides)
+ self.setType(sides = encSides,
+ byPlayer = byPlayer,
+ noMovement = noMovement,
+ description = description)
+ self.setState(owner=owner)
+
+Trait.known_traits.append(RestrictAccessTrait)
+
#
-# Below specific to the export app
+# EOF
#
# ====================================================================
-def get_bb(buffer):
- '''Get bounding box of image
+# From traits/rotate.py
+
+# --------------------------------------------------------------------
+class RotateTrait(Trait):
+ ID = 'rotate'
+ def __init__(self,
+ nangles = 6,
+ rotateCWKey = key(']'),
+ rotateCCWKey = key('['),
+ rotateCW = 'Rotate CW',
+ rotateCCW = 'Rotate CCW',
+ rotateRndKey = '',
+ rotateRnd = '',
+ name = 'Rotate',
+ description = 'Rotate piece',
+ rotateDirectKey = '',
+ rotateDirect = '',
+ directExpression = '',
+ directIsFacing = True,
+ angle = 0):
+ '''Create a Rotate trait'''
+ super(RotateTrait,self).__init__()
+ if nangles == 1:
+ self.setType(nangles = nangles,
+ rotateKey = rotateCWKey,
+ rotate = rotateCW,
+ rotateRndKey = rotateRndKey,
+ rotateRnd = rotateRnd,
+ name = name,
+ description = description,
+ rotateDirectKey = rotateDirectKey,
+ rotateDirect = rotateDirect,
+ directExpression = directExpression,
+ directIsFacing = directIsFacing)
+ else:
+ self.setType(nangles = nangles,
+ rotateCWKey = rotateCWKey,
+ rotateCCWKey = rotateCCWKey,
+ rotateCW = rotateCW,
+ rotateCCW = rotateCCW,
+ rotateRndKey = rotateRndKey,
+ rotateRnd = rotateRnd,
+ name = name,
+ description = description,
+ rotateDirectKey = rotateDirectKey,
+ rotateDirect = rotateDirect,
+ directExpression = directExpression,
+ directIsFacing = directIsFacing)
+
+ self.setState(angle = int(angle) if nangles > 1 else float(angle))
- Parameters
- ----------
- buffer : bytes
- The image bytes
+Trait.known_traits.append(RotateTrait)
- Returns
- -------
- ulx, uly, lrx, lry : tuple
- The coordinates of the bounding box
- '''
- from PIL import Image
- from io import BytesIO
+#
+# EOF
+#
+# ====================================================================
+# From traits/stack.py
+
+# --------------------------------------------------------------------
+class StackTrait(Trait):
+ ID = 'stack'
+ def __init__(self,
+ board = '',
+ x = '',
+ y = '',
+ pieceIds = [],
+ layer = -1):
+ '''Create a stack trait in a save file'''
+ self.setType() # NAME
+ # print('Piece IDs:',pieceIds)
+ self.setState(board = board,
+ x = x,
+ y = y,
+ pieceIds = ';'.join([str(p) for p in pieceIds]),
+ layer = f'@@{layer}')
+
+Trait.known_traits.append(StackTrait)
- with Image.open(BytesIO(buffer)) as img:
- bb = img.getbbox()
+#
+# EOF
+#
+# ====================================================================
+# From traits/mark.py
- return bb
+# --------------------------------------------------------------------
+class MarkTrait(Trait):
+ ID = 'mark'
+ def __init__(self,name='',value=''):
+ '''Create a mark trait (static property)'''
+ super(MarkTrait,self).__init__()
+ self.setType(name = name)
+ self.setState(value = value)
-# ====================================================================
-def piece_body(family,main,flipped,gpid):
- '''Create an full add piece command from the side, image, and ID
- information. This adds (in reverse order)
- - BasicPiece
- - Side prototype
- - Flip layer (if 'flipped' is not none)
+Trait.known_traits.append(MarkTrait)
- Parameters
- ----------
- family : str
- Side in the game
- main : dict
- Main image information
- flipped : dict
- Flipped image information, possibly None
- gpid : int
- Global piece identifier
+#
+# EOF
+#
+# ====================================================================
+# From traits/mask.py
- Returns
- -------
- cmd : str
- The command to add the piece
- '''
- # Create the layer, prototype, and basic piece traits
- decs = []
- if flipped is not None:
- imgs = [ main['filename'], flipped['filename'] ]
- decs.extend([ layer_trait(images=imgs,
- newNames=['','Reduced +'],
- activateName = '',
- decreaseName = '',
- increaseName = 'Flip',
- increaseKey = key('F'),
- decreaseKey = '',
- name='Step'),
- report_trait(key('F')) ])
- for m in main.get('mains',[]):
- decs.append(prototype_trait(m + ' prototype'))
- ech = main.get('echelon',None)
- cmd = main.get('command',None)
- if ech is not None: decs.append(prototype_trait(ech + ' prototype'))
- if cmd is not None: decs.append(prototype_trait(cmd + ' prototype'))
-
- decs.extend([ prototype_trait(family+' prototype'),
- basic_trait(main['name'], main['filename'],gpid)
- ])
-
- typs = [d[0] for d in decs]
- stts = [d[1] for d in decs]
- typ = enc_parts(*typs)
- stt = enc_parts(*stts)
-
- return add_piece(typ,stt)
-
-# --------------------------------------------------------------------
-def proto_body(family):
- '''Create an full add prototype com from the side information.
- This adds (in reverse order)
-
- - An empty BasicPiece
- - An eliminate (or sendto) trait
- - A delete trait
- - A mark moved trait
- - A move trail trait
+# --------------------------------------------------------------------
+# Inset
+# obs;88,130;ag_hide_1.png;Reveal;I;?;sides:Argentine;Peek;;true;;
+# obs;88,130;ag_hide_1.png;Reveal;I;?;side:Argentine;;;true;;
+#
+# Peek
+# obs;88,130;ag_hide_1.png;Reveal;P89,130;?;sides:Argentine;Peek;;true;;
+#
+# Image
+#
+class MaskTrait(Trait):
+ ID = 'obs'
+ INSET = 'I'
+ BACKGROUND = 'B'
+ PEEK = 'P'
+ IMAGE = 'G'
+ INSET2 = '2'
+ PLAYER = 'player:'
+ SIDE = 'side:'
+ SIDES = 'sides:'
+ def __init__(self,
+ keyCommand = '',
+ imageName = '',
+ hideCommand = '',
+ displayStyle = '',
+ peekKey = '',
+ ownerImage = '',
+ maskedName = '?',
+ access = '',#?
+ peekCommand = '',
+ description = '',
+ autoPeek = True,
+ dealKey = '',
+ dealExpr = ''):
+ '''Create a mark trait (static property)'''
+ super(MaskTrait,self).__init__()
+ disp = displayStyle
+ if displayStyle == self.PEEK:
+ disp += peekKey
+ elif displayStyle == self.IMAGE:
+ disp += ownerImage
+
+ acc = self.PLAYER
+ if isinstance(access,list):
+ acc = self.SIDES + ':'.join(access)
+ elif access.startswith('player'):
+ acc = self.PLAYER
+ elif access.startswith('side'):
+ acc = self.SIDE
+
+ self.setType(keyCommand = keyCommand,
+ imageImage = imageName,
+ hideCommand = hideCommand,
+ displayStyle = disp,
+ maskedName = maskedName,
+ access = acc, # ?
+ peekCommand = peekCommand,
+ description = description,
+ autoPeek = autoPeek,
+ dealKey = dealKey,
+ dealExpr = dealExpr)
+ self.setState(value='null')
+
+ @classmethod
+ def peekDisplay(cls,key):#Encoded key
+ return cls.PEEK + key
+
+ @classmethod
+ def peekImage(cls,ownerImage):
+ return cls.IMAGE + ownerImage
- Parameters
- ----------
- family : str
- Side in the game
+ @classmethod
+ def sides(cls,*names):
+ return cls.SIDES+':'.join(names)
- Returns
- -------
- cmd : str
- The command to add the piece
- '''
- # Dummy image
- decs = [ report_trait(key('E'),key('R'),key('M')),
- moved_trait(),
- trail_trait(),
- delete_trait(),
- sendto_trait('DeadMap',family+' pool','Eliminate',key('E'),
- 'Restore',key('R'),description="Eliminate unit"),
- mark_trait('Faction',family),
- basic_trait('','','') ]
- typs = [d[0] for d in decs]
- stts = [d[1] for d in decs]
- typ = enc_parts(*typs)
- stt = enc_parts(*stts)
+Trait.known_traits.append(MaskTrait)
- return add_proto(typ,stt)
+#
+# EOF
+#
+# ====================================================================
+# From traits/trail.py
+
+# --------------------------------------------------------------------
+class TrailTrait(Trait):
+ ID = 'footprint'
+ def __init__(self,
+ key = key('T'),
+ name = 'Movement Trail',
+ localVisible = False,
+ globalVisible = False,
+ radius = 10,
+ fillColor = rgb(255,255,255),
+ lineColor = rgb(0,0,0),
+ activeOpacity = 100,
+ inactiveOpacity = 50,
+ edgesBuffer = 20,
+ displayBuffer = 30,
+ lineWidth = 1,
+ turnOn = '',
+ turnOff = '',
+ reset = '',
+ description = ''):
+ ''' Create a movement trail trait ( VASSAL.counters.Footprint)'''
+ super(TrailTrait,self).__init__()
+ self.setType(key = key,# ENABLE KEY
+ name = name,# MENU
+ localVisible = localVisible,# LOCAL VISABLE
+ globalVisible = globalVisible,# GLOBAL VISABLE
+ radius = radius,# RADIUS
+ fillColor = fillColor,# FILL COLOR
+ lineColor = lineColor,# LINE COLOR
+ activeOpacity = activeOpacity,# ACTIVE OPACITY
+ inactiveOpacity = inactiveOpacity,# INACTIVE OPACITY
+ edgesBuffer = edgesBuffer,# EDGES BUFFER
+ displayBuffer = displayBuffer,# DISPLAY BUFFER
+ lineWidth = int(lineWidth),# LINE WIDTH
+ turnOn = turnOn,# TURN ON KEY
+ turnOff = turnOff,# TURN OFF KEY
+ reset = reset,# RESET KEY
+ description = description) # DESC
+ self.setState(isGlobal = False,
+ map = '',
+ points = 0, # POINTS (followed by [; [X,Y]*]
+ init = False)
+
+Trait.known_traits.append(TrailTrait)
+#
+# EOF
+#
+# ====================================================================
+# From traits/delete.py
# --------------------------------------------------------------------
-def add_splash(root,doc,categories,title='',verbose=False):
- '''Add an about page
+class DeleteTrait(Trait):
+ ID = 'delete'
+ def __init__(self,
+ name = 'Delete',
+ key = key('D')):
+ '''Create a delete trait (VASSAL.counters.Delete)'''
+ super(DeleteTrait,self).__init__()
+ self.setType(name = name,
+ key = key,
+ dummy = '')
+ self.setState()
- Parametersg
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- doc : xml.dom.minidom.Element
- The documentation element
- categories : dict
- Catalog of images
- title : str
- Title of the game
- '''
- fronts = categories.get('front',{}).get('all',[])
- front = list(fronts.values())[0] if len(fronts) > 0 else None
- if front is None:
- return
+Trait.known_traits.append(DeleteTrait)
- if verbose:
- print(f'Adding about page with {title}')
+#
+# EOF
+#
+# ====================================================================
+# From traits/sendto.py
+
+# --------------------------------------------------------------------
+class SendtoTrait(Trait):
+ ID = 'sendto'
+ LOCATION = 'L'
+ ZONE = 'Z'
+ REGION = 'R'
+ GRID = 'G'
+ def __init__(self,
+ mapName = '',
+ boardName = '',
+ name = '',
+ key = key('E'),
+ restoreName = 'Restore',
+ restoreKey = key('R'),
+ x = 200,
+ y = 200,
+ xidx = 0,
+ yidx = 0,
+ xoff = 1,
+ yoff = 1,
+ description = '',
+ destination = LOCATION,
+ zone = '',
+ region = '',
+ expression = '',
+ position = ''):
+ '''Create a send to trait (VASSAL.counter.SendToLocation)'''
+ self.setType(name = name,# NAME
+ key = key,# KEY , MODIFIER
+ mapName = mapName,# MAP
+ boardName = boardName,# BOARD
+ x = x,
+ y = y,# X ; Y
+ restoreName = restoreName,# BACK
+ restoreKey = restoreKey,# KEY , MODIFIER
+ xidx = xidx,
+ yidx = yidx,# XIDX ; YIDX
+ xoff = xoff,
+ yoff = yoff,# XOFF ; YOFF
+ description = description,# DESC
+ destination = destination,# DEST type
+ zone = zone,# ZONE
+ region = region,# REGION
+ expression = expression,# EXPRESSION
+ position = position) # GRIDPOS
+ self.setState(backMap = '', backX = '', backY = '')
+
+Trait.known_traits.append(SendtoTrait)
- add_about(root,doc, f'About {title}', fileName = front['filename'])
+#
+# EOF
+#
+# ====================================================================
+# From traits/moved.py
+
+# --------------------------------------------------------------------
+class MovedTrait(Trait):
+ ID = 'markmoved'
+ def __init__(self,
+ image = 'moved.gif',
+ xoff = 36,
+ yoff = -38,
+ name = 'Mark moved',
+ key = key('M'),
+ dummy = '' # Description
+ # ignoreSame = True
+ ):
+ '''Create a moved trait (VASSAL.counters.MovementMarkable)'''
+ super(MovedTrait,self).__init__()
+ self.setType(image = image,
+ xoff = xoff,
+ yoff = yoff,
+ name = name,
+ key = key,
+ dummy = dummy, # Description
+ # ignoreSame = ignoreSame
+ )
+ self.setState(moved = False)
+
+Trait.known_traits.append(MovedTrait)
-# --------------------------------------------------------------------
-def add_rules(root,doc,zipfile,rules,verbose=False):
- '''Add rules to the module
+#
+# EOF
+#
+# ====================================================================
+# From traits/skel.py
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- doc : xml.dom.minidom.Element
- The documentation element
- zipfile : zipfile.ZipFile
- The module archive
- rules : str
- File name of rules (PDF)
- '''
- if rules is None or rules == '':
- return
- if verbose:
- print('Adding rules PDF to module')
+#
+# EOF
+#
+# ====================================================================
+# From traits/submenu.py
+
+# --------------------------------------------------------------------
+class SubMenuTrait(Trait):
+ ID = 'submenu'
+ def __init__(self,
+ subMenu = '', # Title
+ keys = [], # Keys
+ description = ''):
+ '''Create a sub menu (VASSAL.counters.SubMenu)'''
+ self.setType(subMenu = subMenu, # CLONEKEY
+ keys = ','.join([k.replace(',',r'\,')
+ for k in keys]),
+ description = description)
+ self.setState() # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+ def setKeys(self,keys):
+ '''Set the keys'''
+ self['keys'] = ','.join([k.replace(',',r'\,') for k in keys])
- zipfile.write(rules,'rules.pdf')
+Trait.known_traits.append(SubMenuTrait)
- add_pdffile(root,doc,title='Show rules',pdfFile='rules.pdf')
+#
+# EOF
+#
+# ====================================================================
+# From traits/basic.py
+
+# --------------------------------------------------------------------
+class BasicTrait(Trait):
+ ID = 'piece'
+ def __init__(self,
+ name = '',
+ filename = '', # Can be empty
+ gpid = '', # Can be empty
+ cloneKey = '', # Deprecated
+ deleteKey = ''): # Deprecated
+ '''Create a basic unit (VASSAL.counters.BasicPiece)'''
+ self.setType(cloneKey = cloneKey, # CLONEKEY
+ deleteKey = deleteKey, # DELETEKEY
+ filename = filename, # IMAGE
+ name = name) # NAME
+ self.setState(map = 'null', # MAPID (possibly 'null')
+ x = 0,
+ y = 0,
+ gpid = gpid,
+ properties = 0) # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+
+Trait.known_traits.append(BasicTrait)
-# --------------------------------------------------------------------
-def add_notes(root,doc,zipfile,title,version,sides,pdffile,infofile,rules,
- verbose=False):
- '''Add rules to the module
+#
+# EOF
+#
+# ====================================================================
+# From traits/trigger.py
+
+# --------------------------------------------------------------------
+class TriggerTrait(Trait):
+ ID = 'macro'
+ WHILE = 'while'
+ UNTIL = 'until'
+ COUNTED = 'counted' # - Always one "do ... while"
+ def __init__(self,
+ name = '',
+ command = '', # Context menu name
+ key = '', # Context menu key
+ property = '', # Enable/Disable
+ watchKeys = [],
+ actionKeys = [], # What to do
+ loop = False,
+ preLoop = '', # Key
+ postLoop = '', # Key
+ loopType = COUNTED, # Loop type
+ whileExpression = '',
+ untilExpression = '',
+ count = 0,
+ index = False,
+ indexProperty = '',
+ indexStart = '',
+ indexStep = ''):
+ '''Create a layer trait (VASSAL.counter.Trigger)'''
+ super(TriggerTrait,self).__init__()
+ encWKeys = Trait.encodeKeys(watchKeys, ',')
+ encAKeys = Trait.encodeKeys(actionKeys,',')
+ self.setType(name = name,
+ command = command, # Context menu name
+ key = key, # Context menu key
+ property = property, # Enable/Disable
+ watchKeys = encWKeys,
+ actionKeys = encAKeys, # What to do
+ loop = loop,
+ preLoop = preLoop, # Key
+ postLoop = postLoop, # Key
+ loopType = loopType, # Loop type
+ whileExpression = whileExpression,
+ untilExpression = untilExpression,
+ count = count,
+ index = index,
+ indexProperty = indexProperty,
+ indexStart = indexStart,
+ indexStep = indexStep)
+ self.setState(state='')
+
+ def getActionKeys(self):
+ return Trait.decodeKeys(self['actionKeys'],',')
+
+ def getWatchKeys(self):
+ return Trait.decodeKeys(self['watchKeys'],',')
+
+ def setActionKeys(self,keys):
+ self['actionKeys'] = Trait.encodeKeys(keys,',')
+
+ def setWatchKeys(self,keys):
+ self['watchKeys'] = Trait.encodeKeys(keys,',')
+
+
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- doc : xml.dom.minidom.Element
- The documentation element
- zipfile : zipfile.ZipFile
- The module archive
- sides : list
- Sides in the game
- pdffile : str
- The images PDF
- infofile : str
- The images information
- rules : str
- File name of rules (PDF)
- '''
- factions = "".join([f'<li>{s}</li>' for s in sides])
- notes = f'''
- <html>
- <body>
- <h1>Draft VASSAL module of {title} (Version {version})</h1>
- <p>
- This is a draft VASSAL module of {title} generated from
- </p>
- <ul>
- <li>images in {pdffile}, and</li>
- <li>information in {infofile}</li>
- </ul>
- <p>
- both generated by LaTeX using the <b>wargame</b> [1] package, and
- processed by the Python script <code>wgexport.py</code> from that
- package.
- </p>
- <p>
- The module is <i>not</i> complete and the developer should edit
- the module as needed in the VASSAL module editor.
- </p>
- <p>
- [1] <code>https://gitlab.com/wargames_tex/wargame_tex</code>
- </p>
- <h2>Content of the module</h2>
- <p>
- The <code>wgexport.py</code> Python script has identified the
- factions of the game to be
- </p>
- <ul>
- '''+factions+f'''
- </ul>
- <p>
- and generated prototype pieces for these (along with possible
- non-faction pieces. The script also generated a map ("dead map")
- that contains boards for each faction and is meant to store
- eliminated units ("&lt;faction&gt; pool"). These pools have a
- generic shape and colour, and the developer should take care to
- correct these as needed.
- </p>
- <p>
- Boards of the game present in the {pdffile} and {infofile} files
- have been generated. Each board has a single zone in it that
- encompasses the full board, and a hex grid is added to that
- zone. The module developer should adjust the zone and embedded
- hex grid according to the actual map, using the VASSAL module
- editor. The hex grid (as well as coordinates) is drawn by
- default so as to make this obvious to the developer. The hex
- sizes should be correct, and mainly the offset needs to be
- adjusted.
- </p>
- <p>
- Images marked as OOBs have also been added as maps to the
- module. These also have a single zone with a rectangular grid
- imposed in it. This grid, and the grid coordinates are drawn by
- default, so it is easier to see and to remember to adjust it. The
- module developer should adjust this, possibly adding zones, so
- that it matches up with the graphics. The developer can add
- units to the OOB via 'At-start' stacks in the VASSAL editor.
- </p>
- <p>
- All pieces identified by {infofile} and present in {pdffile} have
- been generated. Double sided pieces (identified by images which
- also has a suffix-"flipped" image) are created as such.
- </p>
- <p>
- Charts identified by the {infofile} and {pdffile} have been added
- to the module. The are added as separate (pop-up) frames, but
- the module developer can change this in the VASSAL module editor.
- </p>
- <p>
- If the {infofile} and {pdffile} identified a front page, then that
- has been added to module as the splash screen.
- </p>
- <p>
- If a PDF rules file ({rules}) was passed to the Python script,
- then that has been included in the module and is accessible from
- the "Help" menu.
- </p>
- <h2>After finalising the module</h2>
- <p>
- After the developer has finalised the module (added more pieces,
- fixed board zones a and grids, add in more commands), then this
- Help menu entry should be deleted.
- </p>
- </body>
- </html>
- '''
- if version.lower() == 'draft':
- if verbose:
- print(f' Adding draft notes {version}')
- zipfile.writestr('help/notes.html',notes)
- add_helpfile(root,doc,title='Draft Module notes',
- fileName='help/notes.html')
-
- keys = f'''
- <html>
- <body>
- <h1>{title} (Version {version}) Key bindings</h1>
-
- <table>
- <tr>
- <th>Key </th>
- <th>Where </th>
- <th>Effect</th>
- </tr>
- <tr>
- <td>Alt-A</td>
- <td>-</td>
- <td>Show the charts panel</td>
- </tr>
- <tr>
- <td>Alt-B</td>
- <td>-</td>
- <td>Show the OOBs</td>
- </tr>
- <tr>
- <td>Alt-C</td>
- <td>-</td>
- <td>Show the counters panel</td>
- </tr>
- <tr>
- <td>Alt-E</td>
- <td>-</td>
- <td>Show the eliminated units</td>
- </tr>
- <tr>
- <td>Alt-I</td>
- <td>-</td>
- <td>Show/refresh inventory window</td>
- </tr>
- <tr>
- <td>Alt-M</td>
- <td>-</td>
- <td>Show map</td>
- </tr>
- <tr>
- <td>Alt-T</td>
- <td>-</td>
- <td>Increase turn track</td>
- </tr>
- <tr>
- <td>Alt-Shift-T</td>
- <td>-</td>
- <td>Decrease turn track</td>
- </tr>
- <tr>
- <td>Alt-6</td>
- <td>-</td>
- <td>Roll the dice</td>
- </tr>
- <tr>
- <td>Ctrl-D</td>
- <td>Board,Counter</td>
- <td>Delete selected counters</td>
- </tr>
- <tr>
- <td>Ctrl-E</td>
- <td>Board,Counter</td>
- <td>Eliminate selected counters</td>
- </tr>
- <tr>
- <td>Ctrl-F</td>
- <td>Board,Counter</td>
- <td>Flip selected counters</td>
- </tr>
- <tr>
- <td>Ctrl-M</td>
- <td>Board,Counter</td>
- <td>Toggle "moved" markers on selected counters</td>
- </tr>
- <tr>
- <td>Ctrl-O</td>
- <td>Board</td>
- <td>Hide/show counters</td>
- </tr>
- <tr>
- <td>Ctrl-R</td>
- <td>Board,Counter</td>
- <td>Restore unit (from dead pool)</td>
- </tr>
- <tr>
- <td>Ctrl-T</td>
- <td>Board,Counter</td>
- <td>Toggle move trail</td>
- </tr>
- <tr>
- <td>Ctrl-+</td>
- <td>Board</td>
- <td>Zoom in</td>
- </tr>
- <tr>
- <td>Ctrl--</td>
- <td>Board</td>
- <td>Zoom out</td>
- </tr>
- <tr>
- <td>Ctrl-=</td>
- <td>Board</td>
- <td>Select zoom</td>
- </tr>
- <tr>
- <td>Ctrl-Shift-O</td>
- <td>Board</td>
- <td>Show overview map</td>
- </tr>
- <tr>
- <td>&larr;,&rarr;,&uarr;&darr;</td>
- <td>Board</td>
- <td>Scroll board left, right, up, down (slowly)</td>
- </tr>
- <tr>
- <td>PnUp,PnDn</td>
- <td>Board</td>
- <td>Scroll board up/down (fast)</td>
- </tr>
- <tr>
- <td>Ctrl-PnUp,Ctrl-PnDn</td>
- <td>Board</td>
- <td>Scroll board left/right (fast)</td>
- </tr>
- <tr>
- <td>Mouse-scroll up/down</td>
- <td>Board</td>
- <td>Scroll board up//down</td>
- </tr>
- <tr>
- <td>Shift-Mouse-scroll up/down</td>
- <td>Board</td>
- <td>Scroll board right/leftown</td>
- </tr>
- <tr>
- <td>Ctrl-Mouse-scroll up/down</td>
- <td>Board</td>
- <td>Zoom board out/in</td>
- </tr>
- <tr>
- <td>Mouse-2</td>
- <td>Board</td>
- <td>Centre on mouse</td>
- </tr>
- </table>
- </body>
- </html>
- '''
- if verbose:
- print(f'Add key bindings help file')
- zipfile.writestr('help/keys.html',keys)
- add_helpfile(root,doc,title='Key bindings',
- fileName='help/keys.html')
+Trait.known_traits.append(TriggerTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/nonrect.py
+
+# --------------------------------------------------------------------
+class NonRectangleTrait(Trait):
+ ID = 'nonRect2'
+ CLOSE = 'c'
+ MOVETO = 'm'
+ LINETO = 'l'
+ CUBICTO = 'l'
+ QUADTO = 'l'
+ def __init__(self,
+ scale = 1.,
+ filename = '',
+ path = [],
+ image = None):
+ '''Create a NonRectangle trait (static property)'''
+ super(NonRectangleTrait,self).__init__()
+ l = []
+ if len(filename) > 0:
+ l.append(f'n{filename}')
+
+ if len(path) <= 0:
+ path = self.getShape(image)
+
+ if len(path) > 0:
+ # print(path)
+ l += [f'{p[0]},{int(p[1])},{int(p[2])}' if len(p) > 2 else p
+ for p in path]
- return notes
+ self.setType(scale = scale,
+ code = ','.join(l))
+ self.setState()
+
+ @classmethod
+ def getShape(cls,image):
+ if image is None:
+ return []
+
+ from PIL import Image
+ from io import BytesIO
+
+ code = []
+ with Image.open(BytesIO(image)) as img:
+ alp = img.getchannel('A') # Alpha channel
+ # Find least and largest non-transparent pixel in each row
+ rows = []
+ w = alp.width
+ h = alp.height
+ bb = alp.getbbox()
+ for r in range(bb[1],bb[3]):
+ ll, rr = bb[2], bb[0]
+ for c in range(bb[0],bb[2]):
+ if alp.getpixel((c,r)) != 0:
+ ll = min(c,ll)
+ rr = max(c,rr)
+ rows += [[r-h//2,ll-w//2,rr-w//2]]
+
+ # Now produce the code - we start with the top line
+ code = [(cls.MOVETO,rows[0][1],rows[0][0]-1),
+ (cls.LINETO,rows[0][2],rows[0][0]-1)]
+
+ # Now loop down right side of image
+ for c in rows:
+ last = code[-1]
+ if last[1] != c[2]:
+ code += [(cls.LINETO, c[2], last[2])]
+ code += [(cls.LINETO, c[2], c[0])]
+
+ # Now loop up left side of image
+ for c in rows[::-1]:
+ last = code[-1]
+ if last[1] != c[1]:
+ code += [(cls.LINETO,c[1],last[2])]
+ code += [(cls.LINETO,c[1],c[0])]
+
+ # Terminate with close
+ code += [(cls.CLOSE)]
+
+ return code
+
+
+Trait.known_traits.append(NonRectangleTrait)
+
+#
+# EOF
+#
+# ====================================================================
+# From traits/click.py
# --------------------------------------------------------------------
-def add_tut(root,doc,zipfile,tutorial,verbose=False):
- if tutorial is None:
- return None
+class ClickTrait(Trait):
+ ID = 'button'
+ def __init__(self,
+ key = '',
+ x = 0,
+ y = 0,
+ width = 0,
+ height = 0,
+ description = '',
+ context = False,
+ whole = True,
+ version = 1,
+ points = []):
+ '''Create a click trait (static property)'''
+ super(ClickTrait,self).__init__()
+ self.setType(key = key,
+ x = x,
+ y = y,
+ width = width,
+ height = height,
+ description = description,
+ context = context,
+ whole = whole,
+ version = version,
+ npoints = len(points),
+ points = ';'.join([f'{p[0]};{p[1]}'
+ for p in points]))
+ self.setState()
+
+
+Trait.known_traits.append(ClickTrait)
- if verbose:
- print(f'Adding tutorial {tutorial}')
- zipfile.write(tutorial,'tutorial.vlog')
+#
+# EOF
+#
+# ====================================================================
+# From game.py
+
+# --------------------------------------------------------------------
+class Game(Element):
+ TAG = Element.BUILD+'GameModule'
+ def __init__(self,build,node=None,
+ name = '',
+ version = '',
+ ModuleOther1 = "",
+ ModuleOther2 = "",
+ VassalVersion = "3.6.7",
+ description = "",
+ nextPieceSlotId = 20):
+ '''Create a new Game object
+
+ Parameters
+ ----------
+ build : xml.dom.Document
+ root note
+ node : xml.dom.Node
+ To read from, or None
+ name : str
+ Name of module
+ version : str
+ Version of module
+ ModuleOther1 : str
+ Free form string
+ ModuleOther2 : str
+ Free form string
+ VassalVersion : str
+ VASSAL version this was created for
+ description : str
+ Speaks volumes
+ nextPieceSlotId : int
+ Starting slot ID.
+ '''
+ super(Game,self).__init__(build, self.TAG,
+ node = node,
+ name = name,
+ version = version,
+ ModuleOther1 = ModuleOther1,
+ ModuleOther2 = ModuleOther2,
+ VassalVersion = VassalVersion,
+ description = description,
+ nextPieceSlotId = nextPieceSlotId)
+ def nextPieceSlotId(self):
+ '''Increment next piece slot ID'''
+ ret = int(self.getAttribute('nextPieceSlotId'))
+ self.setAttribute('nextPieceSlotId',str(ret+1))
+ return ret
+ #
+ def addBasicCommandEncoder(self,**kwargs):
+ '''Add a `BasicCommandEncoder` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : BasicCommandEncoder
+ The added element
+ '''
+ return self.add(BasicCommandEncoder,**kwargs)
+ def addGlobalTranslatableMessages(self,**kwargs):
+ '''Add a `GlobalTranslatableMessages` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : GlobalTranslatableMessages
+ The added element
+ '''
+ return self.add(GlobalTranslatableMessages,**kwargs)
+ def addPlayerRoster(self,**kwargs):
+ '''Add a `PlayerRoster` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PlayerRoster
+ The added element
+ '''
+ return self.add(PlayerRoster,**kwargs)
+ def addChessClock(self,**kwargs):
+ '''Add a `ChessClockControl` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PlayerRoster
+ The added element
+ '''
+ return self.add(ChessClockControl,**kwargs)
+ def addLanguage(self,**kwargs):
+ '''Add a `Language` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Language
+ The added element
+ '''
+ return self.add(Language,**kwargs)
+ def addChatter(self,**kwargs):
+ '''Add a `Chatter` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Chatter
+ The added element
+ '''
+ return self.add(Chatter,**kwargs)
+ def addKeyNamer(self,**kwargs):
+ '''Add a `KeyNamer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : KeyNamer
+ The added element
+ '''
+ return self.add(KeyNamer,**kwargs)
+ def addNotes(self,**kwargs):
+ '''Add a `Notes` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Notes
+ The added element
+ '''
+ return self.add(Notes,**kwargs)
+ def addLanguage(self,**kwargs):
+ '''Add a `Language` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Language
+ The added element
+ '''
+ return self.add(Language,**kwargs)
+ def addChatter(self,**kwargs):
+ '''Add a `Chatter` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Chatter
+ The added element
+ '''
+ return self.add(Chatter,**kwargs)
+ def addKeyNamer(self,**kwargs):
+ '''Add a `KeyNamer` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : KeyNamer
+ The added element
+ '''
+ return self.add(KeyNamer,**kwargs)
+ def addGlobalProperties(self,**kwargs):
+ '''Add a `GlobalProperties` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : GlobalProperties
+ The added element
+ '''
+ return self.add(GlobalProperties,**kwargs)
+ def addGlobalOptions(self,**kwargs):
+ '''Add a `GlobalOptions` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : GlobalOptions
+ The added element
+ '''
+ return self.add(GlobalOptions,**kwargs)
+ def addTurnTrack(self,**kwargs):
+ '''Add a `TurnTrack` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : TurnTrack
+ The added element
+ '''
+ return self.add(TurnTrack,**kwargs)
+ def addDocumentation(self,**kwargs):
+ '''Add a `Documentation` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Documentation
+ The added element
+ '''
+ return self.add(Documentation,**kwargs)
+ def addPrototypes(self,**kwargs):
+ '''Add a `Prototypes` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Prototypes
+ The added element
+ '''
+ return self.add(Prototypes,**kwargs)
+ def addPieceWindow(self,**kwargs):
+ '''Add a `PieceWindow` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PieceWindow
+ The added element
+ '''
+ return self.add(PieceWindow,**kwargs)
+ def addChartWindow(self,**kwargs):
+ '''Add a `ChartWindow` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : ChartWindow
+ The added element
+ '''
+ return self.add(ChartWindow,**kwargs)
+ def addInventory(self,**kwargs):
+ '''Add a `Inventory` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Inventory
+ The added element
+ '''
+ return self.add(Inventory,**kwargs)
+ def addMap(self,**kwargs):
+ '''Add a `Map` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Map
+ The added element
+ '''
+ return self.add(Map,**kwargs)
+ def addDiceButton(self,**kwargs):
+ '''Add a `DiceButton` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : DiceButton
+ The added element
+ '''
+ return self.add(DiceButton,**kwargs)
+ def addPredefinedSetup(self,**kwargs):
+ '''Add a `PredefinedSetup` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : PredefinedSetup
+ The added element
+ '''
+ return self.add(PredefinedSetup,**kwargs)
+ def addGameMassKey(self,**kwargs):
+ '''Add a `GameMassKey` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : GameMassKey
+ The added element
+ '''
+ return self.add(GameMassKey,**kwargs)
+ def addStartupMassKey(self,**kwargs):
+ '''Add a `StartupMassKey` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : StartupMassKey
+ The added element
+ '''
+ return self.add(StartupMassKey,**kwargs)
+ def addMenu(self,**kwargs):
+ '''Add a `Menu` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Menu
+ The added element
+ '''
+ return self.add(Menu,**kwargs)
+ def addSymbolicDice(self,**kwargs):
+ '''Add a `SymbolicDice` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : SymbolicDice
+ The added element
+ '''
+ return self.add(SymbolicDice,**kwargs)
+
- return add_tutorial(root,doc,name='Tutorial',logfile='tutorial.vlog')
+ # ----------------------------------------------------------------
+ def getGlobalProperties(self,single=True):
+ '''Get all or a sole `GlobalPropertie` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `GlobalPropertie` child, otherwise fail.
+ If `False` return all `GlobalPropertie` children in this element
+ Returns
+ -------
+ children : list
+ List of `GlobalPropertie` children (even if `single=True`)
+ '''
+ return self.getAllElements(GlobalProperties,single)
+ def getBasicCommandEncoder(self,single=True):
+ '''Get all or a sole `Ba` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Ba` child, otherwise fail.
+ If `False` return all `Ba` children in this element
+ Returns
+ -------
+ children : list
+ List of `Ba` children (even if `single=True`)
+ '''
+ return self.getAllElements(BasicCommandEncoder,single)
+ def getGlobalTranslatableMessages(self,single=True):
+ '''Get all or a sole `GlobalTranslatableMessage` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `GlobalTranslatableMessage` child, otherwise fail.
+ If `False` return all `GlobalTranslatableMessage` children in this element
+ Returns
+ -------
+ children : list
+ List of `GlobalTranslatableMessage` children (even if `single=True`)
+ '''
+ return self.getAllElements(GlobalTranslatableMessages,single)
+ def getLanguages(self,single=False):
+ '''Get all or a sole `Language` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Language` child, otherwise fail.
+ If `False` return all `Language` children in this element
+ Returns
+ -------
+ children : list
+ List of `Language` children (even if `single=True`)
+ '''
+ return self.getAllElements(Language,single)
+ def getChessClocks(self,asdict=False):
+ '''Get all or a sole `Language` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Language` child, otherwise fail.
+ If `False` return all `Language` children in this element
+ Returns
+ -------
+ children : list
+ List of `Language` children (even if `single=True`)
+ '''
+ return self.getElementsByKey(ChessClockControl,'name',asdict)
+ def getChatter(self,single=True):
+ '''Get all or a sole `Chatter` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Chatter` child, otherwise fail.
+ If `False` return all `Chatter` children in this element
+ Returns
+ -------
+ children : list
+ List of `Chatter` children (even if `single=True`)
+ '''
+ return self.getAllElements(Chatter,single)
+ def getKeyNamer(self,single=True):
+ '''Get all or a sole `KeyNamer` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `KeyNamer` child, otherwise fail.
+ If `False` return all `KeyNamer` children in this element
+ Returns
+ -------
+ children : list
+ List of `KeyNamer` children (even if `single=True`)
+ '''
+ return self.getAllElements(KeyNamer,single)
+ def getDocumentation(self,single=True):
+ '''Get all or a sole `Documentation` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Documentation` child, otherwise fail.
+ If `False` return all `Documentation` children in this element
+ Returns
+ -------
+ children : list
+ List of `Documentation` children (even if `single=True`)
+ '''
+ return self.getAllElements(Documentation,single)
+ def getPrototypes(self,single=True):
+ '''Get all or a sole `Prototypes` (i.e., the containers of
+ prototypes, not a list of actual prototypes) element(s) from
+ this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Prototypes` child, otherwise fail.
+ If `False` return all `Prototypes` children in this element
+ Returns
+ -------
+ children : list
+ List of `Prototype` children (even if `single=True`)
+
+ '''
+ return self.getAllElements(Prototypes,single)
+ def getPlayerRoster(self,single=True):
+ '''Get all or a sole `PlayerRo` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `PlayerRo` child, otherwise fail.
+ If `False` return all `PlayerRo` children in this element
+ Returns
+ -------
+ children : list
+ List of `PlayerRo` children (even if `single=True`)
+ '''
+ return self.getAllElements(PlayerRoster,single)
+ def getGlobalOptions(self,single=True):
+ '''Get all or a sole `GlobalOption` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `GlobalOption` child, otherwise fail.
+ If `False` return all `GlobalOption` children in this element
+ Returns
+ -------
+ children : list
+ List of `GlobalOption` children (even if `single=True`)
+ '''
+ return self.getAllElements(GlobalOptions,single)
+ def getInventories(self,asdict=True):
+ '''Get all Inventorie element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Inventorie` elements. If `False`, return a list of all Inventorie` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Inventorie` children
+ '''
+ return self.getElementsByKey(Inventory,'name',single)
+ def getPieceWindows(self,asdict=True):
+ '''Get all PieceWindow element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `PieceWindow` elements. If `False`, return a list of all PieceWindow` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `PieceWindow` children
+ '''
+ return self.getElementsByKey(PieceWindow,'name',asdict)
+ def getChartWindows(self,asdict=True):
+ '''Get all ChartWindow element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `ChartWindow` elements. If `False`, return a list of all ChartWindow` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `ChartWindow` children
+ '''
+ return self.getElementsByKey(ChartWindow,'name',asdict)
+ def getDiceButtons(self,asdict=True):
+ '''Get all DiceButton element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `DiceButton` elements. If `False`, return a list of all DiceButton` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `DiceButton` children
+ '''
+ return self.getElementsByKey(DiceButton,'name',asdict)
+ def getPredefinedSetups(self,asdict=True):
+ '''Get all PredefinedSetup element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `PredefinedSetup` elements. If `False`, return a list of all PredefinedSetup` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `PredefinedSetup` children
+ '''
+ return self.getElementsByKey(PredefinedSetup,'name',asdict)
+ def getNotes(self,single=True):
+ '''Get all or a sole `Note` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Note` child, otherwise fail.
+ If `False` return all `Note` children in this element
+ Returns
+ -------
+ children : list
+ List of `Note` children (even if `single=True`)
+ '''
+ return self.getAllElements(Notes,single)
+ def getTurnTracks(self,asdict=True):
+ '''Get all TurnTrack element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `TurnTrack` elements. If `False`, return a list of all TurnTrack` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `TurnTrack` children
+ '''
+ return self.getElementsByKey(TurnTrack,'name',asdict)
+ def getPieces(self,asdict=False):
+ '''Get all Piece element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Piece` elements. If `False`, return a list of all Piece` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Piece` children
+ '''
+ return self.getElementsByKey(PieceSlot,'entryName',asdict)
+ def getSpecificPieces(self,*names,asdict=False):
+ '''Get all SpecificPiece element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `SpecificPiece` elements. If `False`, return a list of all SpecificPiece` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `SpecificPiece` children
+ '''
+ return self.getSpecificElements(PieceSlot,'entryName',
+ *names,asdict=asdict)
+ def getMap(self,asdict=False):
+ return self.getElementsByKey(Map,'mapName',asdict)
+ def getWidgetMaps(self,asdict=True):
+ '''Get all WidgetMap element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `WidgetMap` elements. If `False`, return a list of all WidgetMap` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `WidgetMap` children
+ '''
+ return self.getElementsByKey(WidgetMap,'mapName',asdict=asdict)
+ def getMaps(self,asdict=True):
+ '''Get all Map element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Map` elements. If `False`, return a list of all Map` children.
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Map` children
+ '''
+ maps = self.getMap(asdict=asdict)
+ wmaps = self.getWidgetMaps(asdict=asdict)
+ if asdict:
+ maps.update(wmaps)
+ else:
+ maps.extend(wmaps)
+ return maps
+ def getBoards(self,asdict=True):
+ '''Get all Board element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board`
+ elements. If `False`, return a list of all Board`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+
+ '''
+ return self.getElementsByKey(Board,'name',asdict)
+ def getGameMassKeys(self,asdict=True):
+ '''Get all GameMassKey element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board`
+ elements. If `False`, return a list of all Board`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+
+ '''
+ return self.getElementsByKey(GameMassKey,'name',asdict)
+ def getStartupMassKeys(self,asdict=True):
+ '''Get all StartupMassKey element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board`
+ elements. If `False`, return a list of all Board`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+
+ '''
+ return self.getElementsByKey(StartupMassKey,'name',asdict)
+ def getMenus(self,asdict=True):
+ '''Get all Menu element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `Board`
+ elements. If `False`, return a list of all Board`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `Board` children
+
+ '''
+ return self.getElementsByKey(Menu,'text',asdict)
+ def getSymbolicDices(self,asdict=True):
+ '''Get all Menu element(s) from this
+
+ Parameters
+ ----------
+ asdict : bool
+ If `True`, return a dictonary that maps key to `SymbolicDice`
+ elements. If `False`, return a list of all `SymbolicDice`
+ children.
+
+ Returns
+ -------
+ children : dict or list
+ Dictionary or list of `SymbolicDice` children
+
+ '''
+ return self.getElementsByKey(SymbolicDice,'name',asdict)
+ def getAtStarts(self,single=False):
+ '''Get all or a sole `AtStart` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `AtStart` child, otherwise fail.
+ If `False` return all `AtStart` children in this element
+ Returns
+ -------
+ children : list
+ List of `AtStart` children (even if `single=True`)
+ '''
+ return self.getAllElements(AtStart,single)
+
+
+# --------------------------------------------------------------------
+class BasicCommandEncoder(GameElement):
+ TAG = Element.MODULE+'BasicCommandEncoder'
+ def __init__(self,doc,node=None):
+ super(BasicCommandEncoder,self).__init__(doc,self.TAG,node=node)
-# --------------------------------------------------------------------
-def add_basics_to_map(root,map,**attr):
- '''Add a map to the document
+#
+# EOF
+#
+# ====================================================================
+# From buildfile.py
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- parent : xml.dom.minidom.Element
- The parent element
- button : str
- Button text
- name : str
- Name of the map
- stacking : bool
- Whether to enable stacking
+# --------------------------------------------------------------------
+class BuildFile(Element):
+ def __init__(self,root=None):
+ '''Construct from a DOM object, if given, otherwise make new'''
+ from xml.dom.minidom import Document
+ super(BuildFile,self).__init__(None,'',None)
+
+ self._root = root
+ if self._root is None:
+ self._root = Document()
+
+ self._node = self._root
+
+ def addGame(self,**kwargs):
+ '''Add a `Game` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Game
+ The added element
+ '''
+ return Game(self,**kwargs)
+
+ def getGame(self):
+ '''Get the `Game`'''
+ return Game(self,
+ node=self._root.\
+ getElementsByTagName('VASSAL.build.GameModule')[0])
+
+ def encode(self):
+ '''Encode into XML'''
+ return self._root.toprettyxml(indent=' ',
+ encoding="UTF-8",
+ standalone=False)
- Returns
- -------
- map : xml.dom.minidom.Element
- The map element
- bpicker : xml.dom.minidom.Element
- The board picker in map
- '''
- stacking = add_stacking(root,map)
- add_camera (root,map)
- add_forwardtochatter (root,map)
- add_menudisplayer (root,map)
- add_mapcenterer (root,map)
- add_stackexpander (root,map)
- add_piecemover (root,map)
- add_keybufferer (root,map)
- add_selectionhighlighters(root,map) # TBD
- add_highlightlastmoved (root,map)
- add_zoomer (root,map)
- return stacking
+#
+# EOF
+#
+# ====================================================================
+# From moduledata.py
# --------------------------------------------------------------------
-def add_elim(root,game,sides,categories,verbose=False):
- '''Add an eliminated map
+class Data(Element):
+ TAG = 'data'
+ def __init__(self,doc,node=None,version='1'):
+ super(Data,self).__init__(doc,self.TAG,node=node,version=version)
+
+ def addVersion(self,**kwargs):
+ '''Add a `Version` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Version
+ The added element
+ '''
+ return self.add(Version,**kwargs)
+ def addVASSALVersion(self,**kwargs):
+ '''Add a `VASSALVersion` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : VASSALVersion
+ The added element
+ '''
+ return self.add(VASSALVersion,**kwargs)
+ def addName(self,**kwargs):
+ '''Add a `Name` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Name
+ The added element
+ '''
+ return self.add(Name,**kwargs)
+ def addDescription(self,**kwargs):
+ '''Add a `Description` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Description
+ The added element
+ '''
+ return self.add(Description,**kwargs)
+ def addDateSaved(self,**kwargs):
+ '''Add a `DateSaved` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : DateSaved
+ The added element
+ '''
+ return self.add(DateSaved,**kwargs)
+ def getVersion(self,single=True):
+ '''Get all or a sole `Version` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Version` child, otherwise fail.
+ If `False` return all `Version` children in this element
+ Returns
+ -------
+ children : list
+ List of `Version` children (even if `single=True`)
+ '''
+ return self.getAllElements(Version,single=single)
+ def getVASSALVersion(self,single=True):
+ '''Get all or a sole `VASSALVersion` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `VASSALVersion` child,
+ otherwise fail. If `False` return all `VASSALVersion`
+ children in this element
+ Returns
+ -------
+ children : list
+ List of `VASSALVersion` children (even if `single=True`)
+
+ '''
+ return self.getAllElements(VASSALVersion,single=single)
+ def getName(self,single=True):
+ return self.getAllElements(Name,single=single)
+ def getDescription(self,single=True):
+ '''Get all or a sole `Description` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `Description` child,
+ otherwise fail. If `False` return all `De` children in
+ this element
+ Returns
+ -------
+ children : list
+ List of `De` children (even if `single=True`)
+
+ '''
+ return self.getAllElements(Description,single=single)
+ def getDateSaved(self,single=True):
+ '''Get all or a sole `DateSaved` element(s) from this
+
+ Parameters
+ ----------
+ single : bool
+ If `True`, there can be only one `DateSaved` child, otherwise fail.
+ If `False` return all `DateSaved` children in this element
+ Returns
+ -------
+ children : list
+ List of `DateSaved` children (even if `single=True`)
+ '''
+ return self.getAllElements(DateSaved,single=single)
+
+# --------------------------------------------------------------------
+class DataElement(Element):
+ def __init__(self,data,tag,node=None,**kwargs):
+ super(DataElement,self).__init__(data,tag,node=node,**kwargs)
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The parent element
- sides : list of str
- List of sides in the game
- '''
- if verbose:
- print(' Adding eliminated map and boards')
-
- if sides is None or len(sides) == 0:
- return
-
- map = add_map(root,game,'DeadMap',
- buttonName = '', #'Eliminated units',
- icon = '/images/playerAway.gif',
- markMoved = 'Never',
- launch = True,
- allowMultiple = True,
- hotkey=key('E',ALT))
- bpicker = add_boardpicker(root,map,
- slotHeight = 400,
- slotWidth = 400)
- add_setup(root,bpicker,'DeadMap',2,[s+' pool' for s in sides])
- add_masskey(root,map,'Restore',
- key('R'),
- key('R'),
- icon = '/images/Undo16.gif',
- tooltip = "Restore selected units")
-
- for i,s in enumerate(sides):
- if verbose:
- print(f' Adding {s} pool')
-
- color = [0,0,0,64]
- color[i % 3] = 255
-
- w = 400
- h = 400
- c = ','.join([str(c) for c in color])
- dimg = categories.get('pool',{}).get('all',{}).get(s,None)
- img = ''
-
- if dimg:
- bb = get_bb(dimg['img'])
- w = bb[2] - bb[0]
- h = bb[3] - bb[1]
- c = ''
- img = dimg['filename']
- if verbose:
- print(f' Using image provided by the user: {img}')
+ def getData(self):
+ return self.getParent(Data)
- add_board(root,bpicker,f'{s} pool',
- image = img,
- color = c,
- width = w,
- height = h)
+# --------------------------------------------------------------------
+class Version(DataElement):
+ TAG = 'version'
+ def __init__(self,data,node=None,version=''):
+ super(Version,self).__init__(data,self.TAG,node=node)
+ if node is None:
+ self.addText(version)
- add_basics_to_map(root,map)
+# --------------------------------------------------------------------
+class VASSALVersion(DataElement):
+ TAG = 'VassalVersion'
+ def __init__(self,data,node=None,version='3.6.7'):
+ super(VASSALVersion,self).__init__(data,self.TAG,node=node)
+ if node is None:
+ self.addText(version)
# --------------------------------------------------------------------
-def add_hex_grid(root,parent,color=rgb(255,0,0),visible=True,grid=True):
- '''Add a hex grid, either to a zone or to the multizone container'''
+class Name(DataElement):
+ TAG = 'name'
+ def __init__(self,data,node=None,name=''):
+ super(Name,self).__init__(data,self.TAG,node=node)
+ if node is None:
+ self.addText(name)
+
+# --------------------------------------------------------------------
+class Description(DataElement):
+ TAG = 'description'
+ def __init__(self,data,node=None,description=''):
+ super(Description,self).__init__(data,self.TAG,node=node)
+ if node is None:
+ self.addText(description)
+
+# --------------------------------------------------------------------
+class DateSaved(DataElement):
+ TAG = 'dateSaved'
+ def __init__(self,data,node=None,milisecondsSinceEpoch=-1):
+ super(DateSaved,self).__init__(data,self.TAG,node=node)
+ if node is None:
+ from time import time
+ s = f'{int(time()*1000)}' if milisecondsSinceEpoch < 0 else \
+ str(milisecondsSinceEpoch)
+ self.addText(s)
+
+# --------------------------------------------------------------------
+class ModuleData(Element):
- hexes = add_hexgrid(root,parent,color=color,visible=visible,
- edgesLegal=True)
- if not grid:
- return hexes
+ def __init__(self,root=None):
+ '''Construct from a DOM object, if given, otherwise make new'''
+ from xml.dom.minidom import Document
+ super(ModuleData,self).__init__(None,'',None)
+
+ self._root = root
+ if self._root is None:
+ self._root = Document()
+
+ self._node = self._root
+
+ def addData(self,**kwargs):
+ '''Add a `Data` element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Data
+ The added element
+ '''
+ return Data(self,**kwargs)
+
+ def getData(self):
+ return Data(self,
+ node=self._root.getElementsByTagName(Data.TAG)[0])
+
+ def encode(self):
+ return self._root.toprettyxml(indent=' ',
+ encoding="UTF-8",
+ standalone=False)
- add_hexnumbering(root,hexes,
- color = color,
- hType = 'A',
- hOff = -1,
- vType = 'N',
- vOff = -1,
- visible = visible)
+#
+# EOF
+#
+# ====================================================================
+# From save.py
- return hexes
+# ====================================================================
+class SaveIO:
+ '''Wrapper around a save file
+
+ Save file is
+
+ "!VCSK" KEY content
+
+ Key is two bytes drawn as a random number in 0-255. Content is
+ two bytes per character. Content characters are encoded with the
+ random key.
+
+ Save file (.vsav) content is
+
+ "begin_save" ESC
+ "\" ESC
+ [commands]* ESC
+ "PLAYER" name password side ESC
+ [map+"BoardPicker" name x y ESC]+
+ "SETUP_STACK" ESC
+ "TURN"+name state ESC
+ "end_save"
+
+ Commands are
+
+ "+/" id "/" body "\"
+
+ where body are
+
+ "stack" "/" mapName ; x ; y ; ids "\"
+ piece_type "/" piece_state (x and y set here too) "\"
+
+ x and y are pixel coordinates (sigh!). This means we have to know
+
+ - the pixel location of a hex
+ - the hex coordinates of that hex
+ - whether rows and columns are descending
+ - if even hexes are higher or not
+
+ The two first items _must_ be user supplied (I think). When we
+ add stacks or pieces, we must then get the numerical hex
+ coordinates - not what the user specified in the VASSAL editor or
+ the like. Of course, this means opening the module before writing
+ the patch.py script.
+
+ It seems like every piece is added in a stack.
+
+ The id is a numerical value. Rather big (e.g., 1268518000806). It
+ is the current number of miliseconds since epoch, with offset to
+ disambiguate.
+
+ The ID is the current time, taken from a milisecond clock,
+ possibly adjusted up if there is a clash. This is all managed by
+ the GameState class.
-# --------------------------------------------------------------------
-def get_picture_info(picture,name,width,height,verbose):
'''
- Returns
- -------
- hex_width, hex_height : float, float
- Scale hex width
- scx, scy : float, float, float, float
- Scale to image and picture (x,y)
- rot90 : bool
- True if rotated +/-90 degrees
- tran : callable
- Translation function
- '''
- if picture is None:
- print(f'WARNING: No Tikz picture information.'
- f"Are you sure you used the `[zoned]' option for the "
- f"tikzpicture environment of {name}?")
- f = lambda x,y: (x,y)
- return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
-
- # Get picture bounding box
- tll = picture['lower left']
- tur = picture['upper right']
- # Get picture transformation
- pa = picture['xx']
- pb = picture['xy']
- pc = picture['yx']
- pd = picture['yy']
- # Get picture offset (always 0,0?)
- pdx = picture['dx']
- pdy = picture['dy']
- # Define picture global transformation
- pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
- # Globally transform (rotate) picture bounding box
- pll = pr(*tll)
- pur = pr(*tur)
- # Calculate widht, height, and scaling factors
- pw = pur[0] - pll[0]
- ph = pur[1] - pll[1]
- scw = width / pw
- sch = height / ph
- # Extract picture scales and rotation
- # Courtesy of
- # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
- from math import sqrt, atan2, degrees, isclose
- psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
- psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
- prt = degrees(atan2(pc,pd))
- if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
- raise RuntimeException('Rotations of Tikz pictures other than '
- '0 or +/-90,+/- 180, or +/-270 not supported. '
- 'found {prt}')
- rot90 = isclose(abs(prt),90)
- if any([isclose(prt,a) for a in [90,270,180,-180]]):
- print(f'WARNING: rotations by {prt} not fully supported')
-
- hex_width = psx * HEX_WIDTH
- hex_height = psy * HEX_HEIGHT
- if verbose:
- print(f' Picture transformations: {pa},{pb},{pc},{pd}\n'
- f' Scale (x,y): {psx},{psy}\n'
- f' Rotation (degrees): {prt}')
-
- # When translating the Tikz coordinates, it is important to note
- # that the Tikz y-axis point upwards, while the picture y-axis
- # point downwards. This means that the upper right corner is at
- # (width,0) and the lower left corner is at (0,height).
- def tranx(x,off=-pll[0]):
- # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
- return int(scw * (x + off)+.5)
- def trany(y,off=-pur[1]):
- # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
- return -int(sch * (y + off)+.5)
- tran = lambda x,y : (tranx(x), trany(y))
-
- return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
-
-# --------------------------------------------------------------------
-def get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
- labels,coords,targs,nargs):
- targs['dx'] = hex_width
- targs['dy'] = hex_height
- if labels is not None:
- labmap = {
- 'auto': {
- 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
- 'auto=numbers' : {
- 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
- 'auto=alpha column': {
- 'hLeading': 0, 'vLeading': 0, 'hType': 'A', 'vType': 'N',
- 'hOff': -1 },
- 'auto=alpha 2 column': {# Not supported
- 'hLeading': 1, 'vLeading': 1, 'hType': 'A', 'vType': 'N' },
- 'auto=inv y x plus 1': {
- 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
- 'hOff': 0, 'vDescend': True },
- 'auto=x and y plus 1': {
- 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
- 'hOff': 0, 'vOff': 0 }
- }
- for l in labels.split(','):
- nargs.update(labmap.get(l,{}))
-
- # Field/Direction | Normal | Rotated |
- # ----------------|--------|---------|
- # hOff | column | column |
- # vOff | row | row |
- # hDescend | column | row | True, incr to left
- # vDescend | row | column | True, incr upward
- # First='H' | column | column |
-
- # In wargame offset=0 means that the first coordinates are 0,0.
- # But offset 0,0 in VASSAL means start at 1,1
- horiz = 'column'
- vert = 'row'
- nargs['vOff'] = nargs.get('vOff',0)-coords[vert] ['offset']-1
- nargs['hOff'] = nargs.get('hOff',0)-coords[horiz]['offset']-1
-
- # In VASSAL, vDesend=False means rows increase downward,
- # while in wargame, factor=-1 means rows increase downward
- horiz = 'row' if rot90 else 'column'
- vert = 'column' if rot90 else 'row'
- nargs['hDescend'] = coords[horiz]['factor'] != 1
- nargs['vDescend'] = coords[vert] ['factor'] != -1 # flipped y
- targs['edgesLegal'] = True
-
- # Check for inversed columns
- inv_col = coords['column']['factor'] == -1
- ell = hex_width if inv_col else 0
-
- # Some special stuff in case of 90 degree rotation
- if rot90:
- targs.update({'sideways': True})
- nargs['hOff'] -= 1
- nargs['hDescend'] = not nargs['hDescend']
- if inv_col:
- nargs['hOff'] += 3
-
- margin = i.get('board frame',{}).get('margin',0)
- mx = scx * margin
- my = scy * margin
- if inv_col: mx = 0 # Bug in wargame?
-
- # Grid offset is defined relative to the full image,
- # not the zone:
- #
- # X,Y offset: The horizontal and vertical position of
- # the center of the first hex of the grid.
- #
- # That is, (x,y)=(0,0) is in the top-left corner of the
- # image, while in TikZ it is in the bottom left corner.
- # This means we shift by the boudning box offsets plus
- # some stuff to align with the zone.
- #
- # In case of inversed columns, we need to add some extra stuff.
- x0, y0 = llx-hex_width/3 + ell + mx, ury + my
-
- # Rotated?
- if rot90:
- x0, y0 = ury+2*hex_width/3 + ell + mx, llx + my
-
- # Below should be double-checked.
-
- if not rot90:
- if coords.get('column',{}).get('top short','') == 'isodd' \
- and coords.get('column',{}).get('offset',0) == -1:
- y0 -= hex_width/2
- elif coords.get('column',{}).get('top short','') == 'iseven' \
- and coords.get('column',{}).get('offset',-1) == 0:
- y0 -= hex_width/2
-
- targs.update({'x0': int(x0),'y0': int(y0)})
-
-# --------------------------------------------------------------------
-def get_rectparams(i,llx,ury,width,height,rot90,targs,nargs):
- targs['dx'] = width
- targs['dy'] = height
- targs['x0'] = int(llx - width/2)
- targs['y0'] = int(ury + height/2)
- targs['color'] = rgb(0,255,0)
- nargs['color'] = rgb(0,255,0)
- nargs['vDescend'] = True
- nargs['vOff'] = -3
- nargs.update({'sep':',','vLeading':0,'hLeading':0})
-
-# --------------------------------------------------------------------
-def add_zones(root,zoned,name,board,width,height,gpid,
- labels=None,coords=None,picinfo=None,
- verbose=False,visible=True):
- grids = []
- picture = None
- if verbose:
- print(f' Adding zones to {name} zoned={"zoned" in board}')
-
- for k, v in board.items():
- if k == 'labels': labels = v
- if k == 'coords': coords = v
- if k == 'zoned': picture = v
- if 'zone' not in k or k == 'zoned':
- continue
-
- gridtype = (add_hexgrid
- if 'hex' in k.lower()
- else add_squaregrid)
- gridnum = (add_hexnumbering
- if 'hex' in k.lower()
- else add_squarenumbering)
- grids.append([k,v,gridtype,gridnum])
-
- if len(grids) < 1: return gpid
- if verbose:
- print(f' Got zones: {[k[0] for k in grids]}')
- # Either get picture information from argument, or deduce from the
- # found 'zoned' key.
- if picinfo is None:
- picinfo = get_picture_info(picture,name,width,height,verbose)
-
- hex_width, hex_height, scx, scy, rot90, tran = picinfo
-
-
- for g in grids:
- n, i, typ, num = g
- if verbose:
- print(f' Adding zone {n}')
- if 'scope' in n:
- llx,lly = tran(*i['global lower left'])
- urx,ury = tran(*i['global upper right'])
- path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
- nm = n.replace('zone scope ','')
- elif 'path' in n:
- path = [tran(*ii) for ii in i['path']]
- llx = min([px for px,py in path])
- ury = max([py for px,py in path])
- nm = n.replace('zone path ','')
-
- # Checkf if we have "point" type elements in this object and
- # add them to dict.
- points = [ v for k,v in i.items()
- if (k.startswith('point') and
- isinstance(v,dict) and \
- v.get('type','') == 'point')]
-
- pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
- if verbose:
- print(f' Zone path ({llx},{ury}): {pathstr}')
- zone = add_zone(root,zoned,
- name=nm,
- locationFormat = ("$name$"
- if 'pool' in n.lower() else
- "$gridLocation$"),
- useParentGrid=False,
- path=pathstr)
-
- # Do not add grids to pools
- if 'pool' in n.lower():
- continue
-
- targs = {'color':rgb(255,0,0),'visible':visible}
- nargs = {'color':rgb(255,0,0),'visible':visible}
- if 'hex' in n.lower():
- get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
- labels,coords,targs,nargs)
+ VCS_HEADER = b'!VCSK'
+ VK_ESC = chr(27)
+ DEC_MAP = {
+ # 0-9
+ 0x30: 0x30,
+ 0x31: 0x30,
+ 0x32: 0x30,
+ 0x33: 0x30,
+ 0x34: 0x30,
+ 0x35: 0x30,
+ 0x36: 0x30,
+ 0x37: 0x30,
+ 0x38: 0x30,
+ 0x39: 0x30,
+ # A-F
+ 0x41: 0x37,
+ 0x42: 0x37,
+ 0x43: 0x37,
+ 0x44: 0x37,
+ 0x45: 0x37,
+ 0x46: 0x37,
+ # a-f
+ 0x61: 0x57,
+ 0x62: 0x57,
+ 0x63: 0x57,
+ 0x64: 0x57,
+ 0x65: 0x57,
+ 0x66: 0x57
+ }
+ ENC_MAP = [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9',
+ b'a',b'b',b'c',b'd',b'e',b'f']
+
+ # ----------------------------------------------------------------
+ @classmethod
+ def decHex(cls,b):
+ '''Decode a single char into a number
+
+ If the encoded number is b, then the decoded number is
+
+ b - off
+
+ where off is an offset that depends on b
+
+ off = 0x30 if 0x30 <= b <= 0x39
+ 0x37 if 0x41 <= b <= 0x46
+ 0x57 if 0x61 <= b <= 0x66
+ '''
+ return b - cls.DEC_MAP[b]
+ # --------------------------------------------------------------------
+ @classmethod
+ def readByte(cls,inp,key):
+ '''Read a single byte of information from input stream
+
+ Two characters (c1 and c2) are read from input stream, and the
+ decoded byte is then
+
+ ((dechex(c1) << 4 | dechex(c2)) ^ key) & 0xFF
+
+ Parameters
+ ----------
+ inp : stream
+ Input to read from
+ key : int
+ Key to decode the input
+
+ Returns
+ -------
+ b : int
+ The read byte
+ '''
+ try:
+ pair = inp.read(2)
+ except Exception as e:
+ from sys import stderr
+ print(e,file=stderr)
+ return None
+
+ if len(pair) < 2:
+ return None
+
+ return ((cls.decHex(pair[0]) << 4 | cls.decHex(pair[1])) ^ key) & 0xFF
+ # --------------------------------------------------------------------
+ @classmethod
+ def readSave(cls,file,alsometa=False):
+ '''Read data from save file. The data is read into lines
+ returned as a list.
+
+ '''
+ from zipfile import ZipFile
+
+ # We open the save file as a zip file
+ with ZipFile(file,'r') as z:
+ # open the save file in the archive
+ save = z.open('savedGame','r')
+
+ # First, we check the header
+ head = save.read(len(cls.VCS_HEADER))
+ assert head == cls.VCS_HEADER, \
+ f'Read header {head} is not {cls.VCS_HEADER}'
+
+ # Then, read the key
+ pair = save.read(2)
+ key = (cls.decHex(pair[0]) << 4 | cls.decHex(pair[1]))
+
+ # Now read content, one byte at a time
+ content = ''
+ while True:
+ byte = cls.readByte(save,key)
+ if byte is None:
+ break
+
+ # Convert byte to character
+ content += chr(byte)
+
+ lines = content.split(cls.VK_ESC)
+
+ if alsometa:
+ savedata = z.read(VSav.SAVE_DATA)
+ moduledata = z.read(VMod.MODULE_DATA)
+
+ if not alsometa:
+ return key, lines
+
+ return key,lines,savedata,moduledata
+
+ # --------------------------------------------------------------------
+ @classmethod
+ def writeByte(cls,out,byte,key):
+ '''Write a single byte
+
+ Parameters
+ ----------
+ out : IOStream
+ Stream to write to
+ byte : char
+ Single byte to write
+ key : int
+ Key to encode with (defaults to 0xAA - alternating 0's and 1's)
+ '''
+ b = ord(byte) ^ key
+ pair = cls.ENC_MAP[(b & 0xF0) >> 4], cls.ENC_MAP[b & 0x0F]
+ out.write(pair[0])
+ out.write(pair[1])
+
+ # --------------------------------------------------------------------
+ @classmethod
+ def writeInZip(cls,z,key,lines,filename='savedGame'):
+ '''Write a save file in a zip file (VMod)'''
+ # open the save file in the archive
+ with z.open(filename,'w') as save:
+ # Write header
+ save.write(cls.VCS_HEADER)
+
+ # Split key
+ pair = cls.ENC_MAP[(key & 0xF0) >> 4], cls.ENC_MAP[(key & 0x0F)]
+ save.write(pair[0])
+ save.write(pair[1])
+
+ # Form content
+ content = cls.VK_ESC.join(lines)
+
+ # Write each character as two
+ for c in content:
+ cls.writeByte(save, c, key)
+
+ # --------------------------------------------------------------------
+ @classmethod
+ def writeSave(cls,file,key,lines,savedata=None,moduledata=None):
+ '''Write a save file'''
+ from zipfile import ZipFile, ZIP_DEFLATED
+
+ # We open the save file as a zip file
+ with ZipFile(file,'w',ZIP_DEFLATED) as z:
+ cls.writeInZip(z,key,lines,filename='savedGame')
+
+ if savedata is not None:
+ z.writestr(VSav.SAVE_DATA,savedata)
+ z.writestr(VMod.MODULE_DATA,moduledata)
+
+# ====================================================================
+#
+# VSave file
+#
+class SaveFile:
+ def __init__(self,game,firstid=None):
+ '''Creates a save file to add positions to'''
+ from time import time
+ self._game = game
+ self._counters = {}
+ self._stacks = {}
+ self._pieces = self._game.getPieces(asdict=True)
+ self._nextId = (int(time()*1000) - 360000
+ if firstid is None else firstid)
+
+ def add(self,grid,**kwargs):
+ '''Add pieces to the save.
+
+ Parameters
+ ----------
+ grid : BaseGrid
+ Grid to add pieces to
+ kwargs : dict
+ Either a map from piece name to hex position,
+ Or a map from hex position to list of pieces
+ '''
+ for k,v in kwargs.items():
+ # print('Add to save',k,v)
+ self._add(grid,k,v)
+
+ def _add(self,grid,k,v):
+ '''Add to the save'''
+ # print(f'Adding {k} -> {v}')
+ loc = None
+ piece = self._pieces.get(k,None)
+ pieces = []
+ boardName = grid.getBoard()['name']
+ # print(f'Board name: {boardName}')
+ if piece is not None:
+ # print(f'Key is piece: {k}->{piece}')
+ pieces.append(piece)
+ loc = v
else:
- width = hex_width / HEX_WIDTH * RECT_WIDTH
- height = hex_height / HEX_HEIGHT * RECT_HEIGHT
- get_rectparams(i,llx,ury,width,height,rot90,targs,nargs)
+ # Key is not a piece name, so a location
+ loc = k
+ # Convert value to iterable
+ try:
+ iter(v)
+ except:
+ v = list(v)
- # print(targs,nargs)
- if 'turn' in n.lower(): nargs['sep'] = 'T'
- if 'oob' in n.lower(): nargs['sep'] = 'O'
+ for vv in v:
+ if isinstance(vv,PieceSlot):
+ pieces.append(vv)
+ continue
+ if isinstance(vv,str):
+ piece = self._pieces.get(vv,None)
+ if piece is None:
+ continue
+ pieces.append(piece)
+
+ # print(f'Loc: {loc}')
+ if len(pieces) < 1:
+ return
+
+ if (boardName,loc) not in self._stacks:
+ # print(f'Adding stack {boardName},{loc}')
+ coord = grid.getLocation(loc)
+ if coord is None:
+ print(f'did not get coordinates from {loc}')
+ return
+ self._stacks[(boardName,loc)] = {
+ 'x': coord[0],
+ 'y': coord[1],
+ 'pids': [] }
+
+ place = self._stacks[(boardName,loc)]
+ for piece in pieces:
+ name = piece['entryName']
+ count = self._counters.get(name,None)
+
+ if count is None:
+ count = {'pid': self._nextId,
+ 'piece': piece,
+ 'board': boardName,
+ 'x': place['x'],
+ 'y': place['y'],
+ }
+ self._counters[name] = count
+ self._nextId += 1
- if verbose:
- print(f' Adding grid')
+
+ # print(f'Adding to stack {boardName},{loc}: {count[0]}')
+ place['pids'].append(count['pid'])
- if len(points) <= 0:
- grid = typ(root,zone,**targs)
- num(root,grid,**nargs)
- else:
- if verbose:
- print(f' Use a region grid')
- grid = add_regiongrid(root,zone,snapto=True,visible=visible)
- for p in points:
- pn = p["name"]
- pc = p["coords"]
- if verbose:
- print(f' Add region {pn} {p}')
+ def getLines(self):
+ '''Get the final lines of code'''
+ key = 0xAA # fixed key
+
+ lines = ['begin_save',
+ '',
+ '\\']
- n, gpid = add_region(root,grid,pn,*tran(*pc),gpid,
- alsoPiece=True,verbose=verbose)
+ self._pieceLines(lines)
+ self._otherLines(lines)
+
+ lines.append('end_save')
+ return lines
+
+ def _pieceLines(self,lines):
+ '''Add piece lines to save file
+
+ Parameters
+ ----------
+ lines : list
+ The lines to add
+ '''
+ # print(self._counters)
+ for counter in self._counters.values():
+ iden = counter['pid']
+ piece = counter['piece']
+ traits = piece.getTraits()
+ traits = Trait.flatten(traits,self._game)
+ # Get last - trait (basic piece), and modify coords
+ basic = traits[-1]
+ basic['map'] = counter['board']
+ basic['x'] = counter['x']
+ basic['y'] = counter['y']
+ # Set old location if possible
+ parent = piece.getParent(DummyElement,checkTag=False)
+ if parent is not None and parent._node.nodeName == AtStart.TAG:
+ oldLoc = parent['location']
+ oldBoard = parent['owningBoard']
+ oldMap = self._game.getBoards()[oldBoard].getMap()['mapName']
+ oldX = parent['x']
+ oldY = parent['y']
+ oldZone = None
+ zones = self._game.getBoards()[oldBoard].getZones()
+ for zone in zones.values():
+ grid = zone.getGrids()[0]
+ if grid is None: continue
+
+ coord = grid.getLocation(oldLoc)
+ if coord is None: continue
+
+ oldZone = zone['name']
+ oldX = coord[0]
+ oldY = coord[1]
+ break
+
+ if oldZone is not None:
+ basic['properties'] = \
+ f'6;OldZone;{oldZone};OldLocationName;{oldLoc};'+\
+ f'OldX;{oldX};OldY;{oldY};'+\
+ f'OldBoard;{oldBoard};OldMap;{oldMap}'
+ else:
+ basic['properties'] = \
+ f'5;OldLocationName;{oldLoc};'+\
+ f'OldX;{oldX};OldY;{oldY};'+\
+ f'6;OldBoard;{oldBoard};OldMap;{oldMap}'
+
+ for trait in traits:
+ if trait.ID == TrailTrait.ID:
+ trait['map'] = oldMap
+ trait['points'] = f'1;{oldX},{oldY}'
+ trait['init'] = True
+
+ # Wrapper
+ wrap = DummyWithTraits(self._game,traits=[])
+ wrap.setTraits(*traits,iden=str(iden))
+ lines.append(wrap._node.childNodes[0].nodeValue+'\\')
+
+ layer = -1
+ for key,dat in self._stacks.items():
+ pids = dat.get('pids',None)
+ x = dat['x']
+ y = dat['y']
+ if pids is None or len(pids) < 1:
+ print(f'No pieces at {key[0]},{key[1]}')
+ continue
+
+ iden = self._nextId
+ self._nextId += 1
+ stack = StackTrait(board=key[0],x=x,y=y,pieceIds=pids,layer=layer)
+ layer = 1
+ wrap = DummyWithTraits(self._game,traits=[])
+ wrap.setTraits(stack,iden=iden)
+ lines.append(wrap._node.childNodes[0].nodeValue+'\\')
+
+ def _otherLines(self,lines):
+ '''Add other lines to save'''
+ lines.append('UNMASK\tnull')
+ for r in self._game.getPlayerRoster():
+ lines.extend(r.encode())
+ for n in self._game.getNotes(single=False):
+ lines.extend(n.encode())
+ setupStack = False
+ for m in self._game.getMaps(asdict=False):
+ for bp in m.getBoardPicker(single=False):
+ lines.extend(bp.encode())
+ if not setupStack:
+ atstart = m.getAtStarts(single=False)
+ if atstart and len(atstart) > 0:
+ lines.append('SETUP_STACK')
+ setupStack = True
+ # for tk,tt in self._game.getTurnTracks(asdict=True):
+ # lines.extend(tt.encode())
- # Once we've dealt with this grid, we should see if we have
- # any embedded zones we should deal with.
- add_zones(root,zoned,name,i,width,height,gpid,
- labels=labels,coords=coords,picinfo=picinfo,
- verbose=verbose)
-
- # Adding a global grid
- #targs['color'] = rgb(0,255,0)
- #nargs['color'] = rgb(0,255,0)
- #grid = typ(root,zoned,**targs)
- #num(root,grid,**nargs)
-
- return gpid
-
-
-# --------------------------------------------------------------------
-def add_inv(root,game,sides,**attr):
- '''Add inventory to module'''
- filt = '{' + '||'.join([f'Faction=="{s}"' for s in sides])+'}'
- grp = 'Faction,Command,Echelon,Type'
- inv = add_inventory(root,game,
- include = filt,
- groupBy = grp,
- sortFormat = '$PieceName$',
- tooltip = 'Show inventory of all pieces',
- zoomOn = True,
- **attr)
- return inv
-
-
+
# --------------------------------------------------------------------
-def add_map_board(root,game,name,board,gpid,verbose=False,visible=False):
- '''A add a single board to the module
- '''
+class SaveData(ModuleData):
+ def __init__(self,root=None):
+ '''Convinience wrapper'''
+ super(SaveData,self).__init__(root=root)
+# ====================================================================
+# From vsav.py
- map = add_map(root,game, name)
- bpicker = add_boardpicker(root,map)
-
- add_counterviewer (root,map)
- add_hidepieces (root,map)
- add_globalmap (root,map)
- add_basics_to_map (root,map)
- add_masskey (root,map,'Flip', key('F'),key('F'),
- icon = '/images/Undo16.gif',
- tooltip='Flip selected units')
- add_masskey (root,map,'Eliminate',key('E'),key('E'),
- icon = '/images/playerAway.gif',
- tooltip='Eliminate selected units')
-
- # Get the image so we may make a rectangle
- if verbose:
- print(f' Adding board {board["name"]} -> {board["filename"]}')
- ulx,uly,lrx,lry = get_bb(board['img'])
- eboard = add_board(root,bpicker,name,image=board['filename'])
- zoned = add_zoned(root,eboard)
- #hexes = add_hexgrid(root,zoned)
- add_zonehighlighter(root,zoned)
-
- if not 'zones' in board:
- full = add_zone(root,zoned,
- name='full',
- useParentGrid=False,
- path=(f'{ulx},{uly};' +
- f'{lrx},{uly};' +
- f'{lrx},{lry};' +
- f'{ulx},{lry}'))
-
- # We do not have board info, so add a grid to the whole thing
- add_hex_grid(root,full,rgb(255,0,0),True,True,visible=visible)
- return
-
- # If we get here, we have board info!
- w = abs(ulx-lrx)
- h = abs(uly-lry)
- return add_zones(root,zoned,name,board['zones'],w,h,gpid,
- verbose=verbose,visible=visible)
-
-# --------------------------------------------------------------------
-def add_boards(root,game,categories,sides,gpid,verbose=False,visible=False):
- '''Add all boards to the module, including eliminated map
+# --------------------------------------------------------------------
+class VSav:
+ SAVE_DATA = 'savedata'
+
+ def __init__(self,build,vmod):
+ '''Create a VASSAL save file programmatically
+
+ Parameters
+ ----------
+ build : xml.dom.Document
+ `buildFile.xml` as XML
+ vmod : VMod
+ Module file
+ '''
+ from time import time
+ self._vmod = vmod
+ self._game = build.getGame()
+ self._start = int(time()*1000)
+
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The parent element
- categories : dict
- Catalogue of images
- sides : list of str
- List of sides in the game
- Name of the map
- '''
- if verbose:
- print('Adding maps and boards')
+ def createSaveData(self,description=None):
+ '''Create `savedgame`'''
+ desc = (self._game['description']
+ if description is None else description)
+ self._saveData = SaveData(root=None)
+ data = self._saveData.addData()
+ data.addVersion (version =self._game['version'])
+ data.addVASSALVersion(version =self._game['VassalVersion'])
+ data.addDescription (description=desc)
+ data.addDateSaved (milisecondsSinceEpoch=self._start)
+ return self._saveData
+
+ def createModuleData(self):
+ '''Create `moduleData`'''
+ self._moduleData = ModuleData()
+ data = self._moduleData.addData()
+ data.addVersion (version =self._game['version'])
+ data.addVASSALVersion(version =self._game['VassalVersion'])
+ data.addName (name =self._game['name'])
+ data.addDescription (description=self._game['description'])
+ data.addDateSaved (milisecondsSinceEpoch=self._start)
+ return self._moduleData
+
+ def addSaveFile(self):
+ '''Add a save file to the module
+
+ Returns
+ -------
+ vsav : SaveFile
+ Save file to add content to
+ '''
+ self._saveFile = SaveFile(game=self._game,firstid=self._start)
+ return self._saveFile
+
+ def run(self,savename='Save.vsav',description=None):
+ '''Run this to generate the save file
+
+ Parameters
+ ----------
+ savename : str
+ Name of save file to write
+ description : str
+ Short description of the save file
+ '''
+ from zipfile import ZipFile, ZIP_DEFLATED
+
+ self.createSaveData(description=description)
+ self.createModuleData()
+
+ with self._vmod.getInternalFile(savename,'w') as vsav:
+ with ZipFile(vsav,'w',ZIP_DEFLATED) as zvsav:
+ # The key is set to 0xAA (alternating ones and zeros)
+ SaveIO.writeInZip(zvsav,0xAA,self._saveFile.getLines())
+
+ zvsav.writestr(VMod.MODULE_DATA, self._moduleData.encode())
+ zvsav.writestr(VSav.SAVE_DATA, self._saveData.encode())
+
+#
+# EOF
+#
+# ====================================================================
+# From vmod.py
+# ====================================================================
+#
+# Wrapper around a module
+#
+class VMod:
+ BUILD_FILE = 'buildFile.xml'
+ MODULE_DATA = 'moduledata'
+
+ def __init__(self,filename,mode):
+ '''Interface to VASSAL Module (a Zip file)'''
+ self._mode = mode
+ self._vmod = self._open(filename,mode)
+
+ def __enter__(self):
+ '''Enter context'''
+ return self
+
+ def __exit__(self,*e):
+ '''Exit context'''
+ self._vmod.close()
+ return None
+
+ def _open(self,filename,mode):
+ '''Open a file in VMod'''
+ from zipfile import ZipFile, ZIP_DEFLATED
- # Loop over defined boards
- for bn, b in categories.get('board',{}).get('all',{}).items():
- gpid = add_map_board(root,game,bn,b,gpid,verbose,visible)
+ return ZipFile(filename,mode,compression=ZIP_DEFLATED)
- add_elim(root,game,sides,categories,verbose)
+ def removeFiles(self,*filenames):
+ '''Open a temporary zip file, and copy content from there to
+ that file, excluding filenames mentioned in the arguments.
+ Then close current file, rename the temporary file to this,
+ and reopen in 'append' mode. The deleted files are returned
+ as a dictionary.
+
+ Parameters
+ ----------
+ filenames : tuple
+ List of files to remove from the VMOD
+
+ Returns
+ -------
+ files : dict
+ Dictionary from filename to content of the removed files.
+
+ Note, the VMOD is re-opened in append mode after this
+ '''
+ from tempfile import mkdtemp
+ from zipfile import ZipFile
+ from shutil import move, rmtree
+ from os import path
+
+ tempdir = mkdtemp()
+ ret = {}
- return gpid
+ try:
+ tempname = path.join(tempdir, 'new.zip')
+ with self._open(tempname, 'w') as tmp:
-# --------------------------------------------------------------------
-def add_oob(root,widget,name,oob,gpid,hotkey='',verbose=False,visible=False):
- '''A add a single board to the module
- '''
- # Short hand
- cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
+ for item in self._vmod.infolist():
+ data = self._vmod.read(item.filename)
- if verbose:
- print(f' Adding oob {name}')
+ if item.filename not in filenames:
+ tmp.writestr(item, data)
+ else:
+ ret[item.filename] = data
- map = add_widgetmap(root,widget, name,markMoved='Never',hotkey='')
- bpicker = add_boardpicker(root,map)
+ name = self._vmod.filename
+ self._vmod.close()
+ move(tempname, name)
- add_counterviewer (root,map)
- add_basics_to_map (root,map)
-
+ self._mode = 'a'
+ self._vmod = self._open(name,'a')
+ finally:
+ rmtree(tempdir)
- # Get the image so we may make a rectangle
- if verbose:
- print(f' Adding OOB {oob["name"]} -> {oob["filename"]}')
+ # Return the removed files
+ return ret
+
+ def fileName(self):
+ '''Get name of VMod file'''
+ return self._vmod.filename
+
+ def addFiles(self,**files):
+ '''Add a set of files to this
+
+ Parameters
+ ----------
+ files : dict
+ Dictionary that maps file name to content.
+ '''
+ for filename,data in files.items():
+ self.addFile(filename,data)
+
+ def addFile(self,filename,content):
+ '''Add a file to this
+
+ Parameters
+ ----------
+ filename : str
+ File name in module
+ content : str
+ File content
+ Returns
+ -------
+ element : File
+ The added element
+ '''
+ self._vmod.writestr(filename,content)
+
+ def addExternalFile(self,filename,target=None):
+ '''Add an external file element to this
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : ExternalFile
+ The added element
+ '''
+ if target is None: target = filename
+ self._vmod.write(filename,target)
+
+ def getFileNames(self):
+ '''Get all filenames in module'''
+ return self._vmod.namelist()
+
+ def getFiles(self,*filenames):
+ '''Return named files as a dictionary.
+
+ Parameters
+ ----------
+ filenames : tuple
+ The files to get
+ Returns
+ -------
+ files : dict
+ Mapping of file name to file content
+ '''
+ fn = self.getFileNames()
+ ret = {}
+ for f in filenames:
+ if f not in fn:
+ continue
+
+ ret[f] = self._vmod.read(f)
+
+ return ret
+
+ def getDOM(self,filename):
+ '''Get content of a file decoded as XML DOM
+
+ Parameters
+ ----------
+ filename : str
+ Name of file in module
+ '''
+ from xml.dom.minidom import parseString
+
+ r = self.getFiles(filename)
+ if filename not in r:
+ raise RuntimeException(f'No {filename} found!')
+
+ return parseString(r[filename])
- ulx,uly,lrx,lry = get_bb(oob['img'])
- board = add_board(root,bpicker,name,image=oob['filename'])
- zoned = add_zoned(root,board)
- add_zonehighlighter(root,zoned)
+ def getBuildFile(self):
+ '''Get the buildFile.xml decoded as a DOM tree'''
+ return self.getDOM(VMod.BUILD_FILE)
- if not 'zones' in oob:
- zone = add_zone(root,zoned,
- name='full',
- useParentGrid=False,
- path=(f'{ulx},{uly};' +
- f'{lrx},{uly};' +
- f'{lrx},{lry};' +
- f'{ulx},{lry}'))
- # We do not have board info, so add a grid to the whole thing
- grid = add_squaregrid(root,zone,visible=visible)
- add_squarenumbering(root,grid,visible=visible)
- return
+ def getModuleData(self):
+ '''Get the moduledata decoded as a DOM tree'''
+ return self.getDOM(VMod.MODULE_DATA)
- # If we get here, we have board info!
- w = abs(ulx-lrx)
- h = abs(uly-lry)
- return add_zones(root,zoned,name,oob['zones'],w,h,gpid,
- verbose=verbose,visible=visible)
-
-# --------------------------------------------------------------------
-def add_oobs(root,game,categories,gpid,verbose=False,visible=False):
- '''Add all OObs to the module
+ def getInternalFile(self,filename,mode):
+ return self._vmod.open(filename,mode)
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The parent element
- categories : dict
- Catalogue of images
- '''
- # Short hand
- oobc = categories.get('oob',{}).get('all',{}).items()
- if len(oobc) < 1:
- return gpid
-
- oobs = add_chartwindow(root,game, 'OOBs',
- hotkey = key('B',ALT),
- description = 'OOBs',
- text = '',
- icon = '/images/inventory.gif',
- tooltip = 'Show/hide OOBs')
- tabs = add_tabs(root,oobs,'OOBs')
-
- if verbose:
- print('Adding OOBs')
-
- # Loop over defined boards
- for on, o in oobc:
- widget = add_mapwidget(root,tabs, entryName=on)
- gpid = add_oob(root,widget,on,o,gpid,
- verbose = verbose,
- hotkey = key('B',ALT),
- visible = visible)
-
- return gpid
-
-# --------------------------------------------------------------------
-def add_markproto(root,protos,key,value):
- decs = [ mark_trait(key,value.capitalize()),
- basic_trait('','','') ]
- typs = [d[0] for d in decs]
- stts = [d[1] for d in decs]
- typ = enc_parts(*typs)
- stt = enc_parts(*stts)
- body = add_proto(typ,stt)
- add_prototype(root,protos,value+' prototype',body)
-
-# --------------------------------------------------------------------
-def add_counters(root,game,categories,
- unittypes,echelons,commands,verbose=False):
- '''Add all counters to the module.
+ def addVSav(self,build):
+ '''Add a `VSav` element to this
- This will add all counters to the module, as well as prototypes
- for each sub-category.
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : VSav
+ The added element
+ '''
+ return VSav(build=build,vmod=self)
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The game element
- categories : dict
- Catalogue of images
- '''
- # Short hand
- # Create prototypes container before adding pieces
- protos = add_prototypes(root,game)
- for utype in unittypes: add_markproto(root,protos,'Type',utype)
- for ech in echelons: add_markproto(root,protos,'Echelon',ech)
- for cmd in commands: add_markproto(root,protos,'Command',cmd)
-
- # Stating global piece ID
- gpid = 20
-
- # A window to hold the pieces
- piecew = add_piecewindow(root,game,'Counters',
- hotkey = key('C',ALT))
- # Tab container for each sub-category
- piecet = add_tabs(root,piecew,entryName='Counters')
-
- if verbose:
- print('Adding counters and prototypes')
-
- # Loop over sub-categories
- for subn, subc in categories.get('counter',{}).items():
- # Friendly message
- if verbose:
- print(f' Adding {subn} counters and prototype')
- # Create a panel for this sub-category
- piecep = add_panel(root,piecet,
- entryName = subn,
- fixed = False)
- # Add a list to the panel
- piecel = add_list(root,piecep,
- entryName=f'{subn} counters')
+#
+# EOF
+#
+# ====================================================================
+# From exporter.py
+
+class Exporter:
+ def __init__(self):
+ '''Base class for exporters'''
+ pass
+
+
+ def setup(self):
+ '''Should be defined to set-up for processing, for example
+ generating images and such. This is executed in a context
+ where the VMod file has been opened for writing via
+ `self._vmod`. Thus, files can be added to the module at this
+ stage.
+ '''
+ pass
+
+ def createBuildFile(self,ignores='(.*markers?|all|commons|[ ]+)'):
+ '''Should be defined to make the `buildFile.xml` document
+
+ Parameters
+ ----------
+ ignores : str
+ Regular expression to match ignored categories for factions
+ determination. Python's re.fullmatch is applied to this
+ regular exression against chit categories. If the pattern
+ is matched, then the chit is not considered to belong to a
+ faction.
+
+ '''
+ pass
+
+ def createModuleData(self):
+ '''Should be defined to make the `moduledata` document'''
+ pass
+
+ def run(self,vmodname,patch=None):
+ '''Run the exporter to generate the module
+ '''
+ with VMod(vmodname,'w') as vmod:
+ self._vmod = vmod
+ self.setup()
+ self.createBuildFile()
+ self.createModuleData()
+ self.runPatch(patch)
+ self._vmod.addFiles(**{VMod.BUILD_FILE :
+ self._build.encode(),
+ VMod.MODULE_DATA :
+ self._moduleData.encode()})
+ Verbose().message('Created VMOD')
+
- if verbose:
- print(f' Prototype: {subn} prototype')
- # Create prototype for this sub-category
- proto = add_prototype(root,protos,
- name = f'{subn} prototype',
- code = proto_body(subn),
- description = f'Prototype for {subn} counters')
-
- # Loop over counters in this sub-category
- for cn, c in subc.items():
- # Flipped counters are not added directly
- if cn.endswith('flipped'): continue
+ def runPatch(self,patch):
+ '''Run user specified patch script. The script should define
- if verbose:
- print(f' counter: {cn}')
-
- # Get image bounding box
- bb = get_bb(c['img'])
+ ```
+ def patch(buildFile,moduleData,vmod,verbose):
+ ```
+
+ where `buildFile` is the `buildFile.xml` and `moduleData` are
+ the XML documents as `xml.dom.Document` objects, `vmod` is a
+ `VMod` instance, and `verbose` is a `bool` selecting verbose
+ mode or not.
+ '''
+ if patch is None or patch == '':
+ return
+
+ from importlib.util import spec_from_file_location, module_from_spec
+ from pathlib import Path
+ from sys import modules
+
+ p = Path(patch)
+ with VerboseGuard(f'Will patch module with {p.stem}.patch function') \
+ as v:
+
+ spec = spec_from_file_location(p.stem, p.absolute())
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ modules[p.stem] = module
+
+ # Patch must accept xml.dom.document,xml.dom.document,ZipFile
+ module.patch(self._build,
+ self._moduleData,
+ self._vmod,
+ Verbose().verbose)
+
+# ====================================================================
+# From latexexporter.py
- # Get flipped image, if any
- cf = subc.get(cn + ' flipped', None)
+# ====================================================================
+#
+# Exporter class
+#
+class LaTeXExporter(Exporter):
+ def __init__(self,
+ vmodname = 'Draft.vmod',
+ pdfname = 'export.pdf',
+ infoname = 'export.json',
+ title = 'Draft',
+ version = 'draft',
+ description = '',
+ rules = None,
+ tutorial = None,
+ patch = None,
+ visible = True,
+ vassalVersion = '3.6.7',
+ nonato = False,
+ nochit = False,
+ counterScale = 1):
+ '''Exports a PDF and associated JSON files to a VASSAL module.
+
+ Parameters
+ ----------
+ vmodname : str
+ Name of module file to write
+ pdfname : str
+ Name of PDF file to read images from
+ infoname : str
+ Name of JSON file to read meta data from
+ title : str
+ Name of module
+ version : str
+ Version of midule
+ description : str
+ Short description of the module
+ rules : str
+ Optional name PDF file to attach as rules
+ tutorial : str
+ Optional name of a VASSAL log file to use as tutorial
+ patch : str
+ Optional name of Python script to post process the module
+ visible : bool
+ Make grids visible
+ vassalVersion : str
+ VASSAL version to encode this module for
+ '''
+ self._vmodname = vmodname
+ self._pdfname = pdfname
+ self._infoname = infoname
+ self._title = title
+ self._version = version
+ self._description = description
+ self._rules = rules
+ self._tutorial = tutorial
+ self._patch = patch
+ self._visible = visible or version.lower() == 'draft'
+ self._vassalVersion = vassalVersion
+ self._nonato = nonato
+ self._nochit = nochit
+ self._counterScale = counterScale
+ self._battleMark = 'wgBattleMarker'
+ self._battleMarks = []
+ self._oddsMarks = []
+ self._resultMarks = []
+ self._markBattle = key(NONE,0)+',wgMarkBattle'
+ self._clearBattle = key(NONE,0)+',wgClearBattle'
+ self._clearAllBattle = key(NONE,0)+',wgClearAllBattle'
+ self._zeroBattle = key(NONE,0)+',wgZeroBattle'
+ self._incrBattle = key(NONE,0)+',wgIncrBattle'
+ self._setBattle = key(NONE,0)+',wgSetBattle'
+ self._getBattle = key(NONE,0)+',wgGetBattle'
+ self._markOdds = key(NONE,0)+',wgMarkOdds'
+ self._markResult = key(NONE,0)+',wgMarkResult'
+ self._clearMoved = key(NONE,0)+',wgClearMoved'
+ self._zeroBattleAF = key(NONE,0)+',wgZeroBattleAF'
+ self._zeroBattleDF = key(NONE,0)+',wgZeroBattleDF'
+ self._zeroBattleFrac = key(NONE,0)+',wgZeroBattleFrac'
+ self._zeroBattleOdds = key(NONE,0)+',wgZeroBattleOdds'
+ self._zeroBattleShft = key(NONE,0)+',wgZeroBattleShift'
+ self._zeroBattleIdx = key(NONE,0)+',wgZeroBattleIdx'
+ self._calcBattleAF = key(NONE,0)+',wgCalcBattleAF'
+ self._calcBattleDF = key(NONE,0)+',wgCalcBattleDF'
+ self._calcBattleFrac = key(NONE,0)+',wgCalcBattleFrac'
+ self._calcBattleOdds = key(NONE,0)+',wgCalcBattleOdds'
+ self._calcBattleShft = key(NONE,0)+',wgCalcBattleShift'
+ self._calcBattleIdx = key(NONE,0)+',wgCalcBattleIdx'
+ self._calcBattleRes = key(NONE,0)+',wgCalcBattleResult'
+ self._clearBattlePhs = key(NONE,0)+',wgClearBattlePhs'
+ self._resolveBattle = key(NONE,0)+',wgResolveBattle'
+ self._rollDice = key(NONE,0)+',wgRollDice'
+ self._diceInitKey = key(NONE,0)+',wgInitDice'
+ self._clearKey = key('C')
+ self._clearAllKey = key('C',CTRL_SHIFT)
+ self._deleteKey = key('D')
+ self._eliminateKey = key('E')
+ self._flipKey = key('F')
+ self._trailKey = key('M')
+ self._restoreKey = key('R')
+ self._markKey = key('X')
+ self._resolveKey = key('Y')
+ self._rotateCCWKey = key('[')
+ self._rotateCWKey = key(']')
+ self._chartsKey = key('A',ALT)
+ self._oobKey = key('B',ALT)
+ self._countersKey = key('C',ALT)
+ self._deadKey = key('E',ALT)
+ self._diceKey = key('6',ALT)
+ self._battleCounter = 'wgBattleCounter'
+ self._currentBattle = 'wgCurrentBattle'
+ self._currentAttacker = 'wgCurrentAttacker'
+ self._battleNo = 'wgBattleNo'
+ self._battleAF = 'wgBattleAF'
+ self._battleDF = 'wgBattleDF'
+ self._battleFrac = 'wgBattleFrac'
+ self._battleIdx = 'wgBattleIdx'
+ self._battleOdds = 'wgBattleOdds'
+ self._battleOddsM = 'wgBattleOddsMarker'
+ self._battleShift = 'wgBattleShift'
+ self._battleCtrl = 'wgBattleCtrl'
+ self._battleCalc = 'wgBattleCalc'
+ self._battleUnit = 'wgBattleUnit'
+ self._battleResult = 'wgBattleResult'
+ self._autoOdds = 'wgAutoOdds'
+ self._autoResults = 'wgAutoResults'
+ self._noClearMoves = 'wgNoClearMoves'
+ self._noClearBattles = 'wgNoClearBattles'
+ self._debug = 'wgDebug'
+ self._verbose = 'wgVerbose'
+ self._hidden = None
+ self._hiddenName = 'wg hidden unit'
+ self._dice = {}
+ self._diceInit = None
+
+ with VerboseGuard('Overall settings') as v:
+ v(f'Module file name: {self._vmodname}')
+ v(f'PDF file name: {self._pdfname}')
+ v(f'JSON file name: {self._infoname}')
+ v(f'Game title: {self._title}')
+ v(f'Game version: {self._version}')
+ v(f'Description: {self._description}')
+ v(f'Rules PDF file: {self._rules}')
+ v(f'Tutorial log: {self._tutorial}')
+ v(f'Patch scripts: {self._patch}')
+ v(f'Visible grids: {self._visible}')
+ v(f'Scale of counters: {self._counterScale}')
+
+ def setup(self):
+ # Start the processing
+ self._info = self.convertPages()
+ self._categories, \
+ self._mains, \
+ self._echelons, \
+ self._commands = self.writeImages(self._counterScale)
- # Create a slot for this piece
- sl = add_pieceslot(root,piecel,
- entryName = cn,
- gpid = gpid,
- code = piece_body(subn,c,cf,gpid),
- height = bb[3]-bb[1],
- width = bb[2]-bb[0])
- # Store the global piece identifier - do we need this?
- c['gpid'] = gpid
+ def run(self):
+ super(LaTeXExporter,self).run(self._vmodname,self._patch)
+
+
- # Increment global piece identifier
- gpid += 1
+ # ================================================================
+ def createProcess(self,args):
+ '''Spawn a process and pipe output here
+
+ Parameters
+ ----------
+ args : list
+ List of process command line elements
+ Returns
+ -------
+ pipe : subprocess.Pipe
+ Pipe to read from
+ '''
+ from os import environ
+ from subprocess import Popen, PIPE
+
+ return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
+
+ # ----------------------------------------------------------------
+ def addPws(self,opw=None,upw=None):
+ '''Add a `Pws` element to arguments
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ Returns
+ -------
+ element : Pws
+ The added element
+ '''
+ '''Add password options'''
+ args = []
+ if upw is not None: args.extend(['-upw',upw])
+ if opw is not None: args.extend(['-opw',opw])
+ return args
+
+ # ----------------------------------------------------------------
+ def getPdfInfo(self,upw=None,opw=None,timeout=None):
+ '''Get information about the PDF
+
+ Parameters
+ ----------
+ opw : str
+ Owner password (optional)
+ upw : str
+ User password (optional)
+ timeout : int
+ Time out in miliseconds for subprocesses
+
+ Returns
+ -------
+ info : dict
+ Image information
+ '''
+ args = ['pdfinfo', self._pdfname ]
+ args.extend(self.addPws(opw=opw,upw=upw))
+
+ with VerboseGuard(f'Getting information from PDF {self._pdfname}'):
+ proc = self.createProcess(args)
+ try:
+ out, err = proc.communicate(timeout=timeout)
+ except:
+ proc.kill()
+ proc.communicate()
+ raise RuntimeError(f'Failed to get PDF info: {e}')
+
+ d = {}
+ for field in out.decode('utf8','ignore').split('\n'):
+ if field == '':
+ continue
+ subfields = field.split(':')
+ key, value = subfields[0], ':'.join(subfields[1:])
+ if key != '':
+ d[key] = (int(value.strip()) if key == 'Pages'
+ else value.strip())
+
+ if 'Pages' not in d:
+ raise ValueError(f'Page count not found from {self._pdfname}')
+
+ return d
+
+ # ----------------------------------------------------------------
+ def getImagesInfo(self):
+ '''Read in JSON information, and return as dictionary'''
+ from json import load
+
+ with VerboseGuard(f'Getting information from JSON {self._infoname}'):
+ with open(self._infoname) as file:
+ info = load(file)
+
+ return info
+
+ # ================================================================
+ def convertPage(self,page,opw=None,upw=None,timeout=None):
+ '''Convert a page from PDF into an image (bytes)
+
+ Parameters
+ ----------
+ page : int
+ Page number in the PDF to convert
+ opw : str
+ Owner password (optional)
+ upw : str
+ User password (optional)
+ timeout : int
+ Time out in miliseconds for subprocesses
+
+ Returns
+ -------
+ info : dict
+ Image information
+ '''
+ args = ['pdftocairo',
+ '-transp',
+ '-singlefile',
+ '-f', str(page),
+ '-l', str(page),
+ '-png' ]
+ args.extend(self.addPws(opw=opw,upw=upw))
+ args.append(self._pdfname)
+ args.append('-')
+
+ proc = self.createProcess(args)
- # Return next free piece identifier
- return gpid
+ try:
+ out, err = proc.communicate(timeout=timeout)
+ except Exception as e:
+ proc.kill()
+ proc.communicate()
+ raise RuntimeError(f'Failed to convert page {page} of '
+ f'{self._pdfname}: {e}')
+
+ # Just return the bytes
+ return out
+
+
+ # ----------------------------------------------------------------
+ def ignoreEntry(self,info,ignores=['<<dummy>>','<<eol>>']):
+ '''Check if we should ignore an entry in the JSON file'''
+ return info['category'] in ignores
-# --------------------------------------------------------------------
-def add_charts(root,game,categories,verbose=False):
- '''Add all boards to the module
+ # ----------------------------------------------------------------
+ def scaleImage(self,buffer,factor):
+ from PIL import Image
+ from io import BytesIO
+ from math import isclose
- This will select all 'chart' elements from the catalogue and add
- them as individual (popup) panels to the module.
-
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The game element
- categories : dict
- Catalogue of images
+ if isclose(factor,1): return buffer
- '''
- chartc = categories.get('chart',{}).get('all',{}).items()
- if len(chartc) < 1:
- return
-
- # Short hand
- cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
- cw = lambda parent,tag,**attr : create_elem(root,parent,vw(tag),**attr)
+ # print(f'Scaling image by factor {factor}')
+ with Image.open(BytesIO(buffer)) as img:
+ w, h = img.width, img.height
+ cpy = img.resize((int(factor*w),int(factor*h)))
- if verbose:
- print('Adding charts')
+ with BytesIO() as out:
+ cpy.save(out,format='PNG')
+ return out.getvalue()
- charts = add_chartwindow(root,game, 'Charts',
- hotkey = key('A',ALT),
- description = 'Charts',
- tooltip = 'Show/hide Charts')
- tabs = add_tabs(root,charts,'Charts')
+
+ # ----------------------------------------------------------------
+ def convertPages(self,opw=None,upw=None,timeout=None):
+ '''Reads in JSON and pages from PDF and stores information
+ dictionary, which is returned
+
+ Parameters
+ ----------
+ opw : str
+ Owner password (optional)
+ upw : str
+ User password (optional)
+ timeout : int
+ Time out in miliseconds for subprocesses
+
+ Returns
+ -------
+ info : dict
+ Image information
+ '''
+ oargs = {'opw':opw,'upw':upw }
+ docinfo = self.getPdfInfo()
+ imgsinfo = self.getImagesInfo()
+
+ if len(imgsinfo) - 1 != docinfo['Pages']:
+ raise RuntimeError(f'Number of pages in {pdfname} '
+ f'(doc["Pages"]) not matched in JSON '
+ f'{self._infoname} -> {len(imagesinfo)}')
+
+ with VerboseGuard(f'Converting {docinfo["Pages"]} '
+ f'pages in {self._pdfname}') as v:
+ for i,info in enumerate(imgsinfo):
+ if self.ignoreEntry(info): continue
+
+ if i == 0: v(end='')
+ v(f'[{info["number"]}]',end=' ',flush=True)
+ info['img'] = self.convertPage(info['number'],**oargs)
+
+ v('done')
+
+ return imgsinfo
+
+ # ----------------------------------------------------------------
+ def getBB(self,buffer):
+ '''Get bounding box of image
+ Parameters
+ ----------
+ buffer : bytes
+ The image bytes
- # Loop over defined charts
- for cn, c in chartc:
- if verbose:
- print(f' Chart {cn} ({c["filename"]})')
- add_chart(root,tabs,
- chartName = cn,
- description = cn,
- fileName = c['filename'])
-
-# --------------------------------------------------------------------
-def add_diceroll(root,game,categories,verbose=False):
- '''Add a dice button (1d6)
+ Returns
+ -------
+ ulx, uly, lrx, lry : tuple
+ The coordinates of the bounding box
+ '''
+ from PIL import Image
+ from io import BytesIO
- Parameters
- ----------
- root : xml.dom.minidom.Document
- Root of the document
- game : xml.dom.minidom.Element
- The game element
- categories : dict
- Catalogue of images
- '''
- if verbose:
- print('Adding dice')
-
- add_dice(root,game,
- hotkey = key('6',ALT),
- nDice = 1,
- nSides = 6,
- text = '1d6',
- tooltip = 'Roll a 1d6',
- name = '1d6')
-
-
-# ====================================================================
-def create_build(info,
- categories,
- unittypes,
- echelons,
- commands,
- zipfile,
- title,
- version,
- desc = '',
- rules = None,
- tutorial = None,
- pdffile = '',
- infofile = '',
- verbose = False,
- visible = False):
- '''Create the buildFile.xml document
+ with Image.open(BytesIO(buffer)) as img:
+ bb = img.getbbox()
+
+ return bb
- Parameters
- ----------
- info : dict
- Information dicationary
- categories : dict
- Catalogue of images
- unittypes : set
- Set of used unit types
- zipfile : zipfile.ZipFile
- Module archive
- title : str
- Title of module
- version : str
- Version number of module
- desc : str
- Short description of the game
- rules : str
- Filename of PDF rules document (if any)
+ # ----------------------------------------------------------------
+ def getWH(self,buffer):
+ '''Get bounding box of image
+
+ Parameters
+ ----------
+ buffer : bytes
+ The image bytes
+
+ Returns
+ -------
+ ulx, uly, lrx, lry : tuple
+ The coordinates of the bounding box
+ '''
+ from PIL import Image
+ from io import BytesIO
+
+ with Image.open(BytesIO(buffer)) as img:
+ w, h = img.width, img.height
+
+ return w,h
+
+ # ----------------------------------------------------------------
+ def getOutline(self,buffer):
+ '''Get bounding box of image
+
+ Parameters
+ ----------
+ buffer : bytes
+ The image bytes
+
+ Returns
+ -------
+ ulx, uly, lrx, lry : tuple
+ The coordinates of the bounding box
+ '''
+ from PIL import Image
+ from io import BytesIO
+
+ # print(buffer)
+ with Image.open(BytesIO(buffer)) as img:
+ bb = img.getbbox()
+
+ for r in range(bb[0],bb[2]):
+ for c in range(bb[1],bb[3]):
+ pass #print(img.getpixel((c,r)))
+
+ return None
+
+
+ # ================================================================
+ def writeImages(self,counterScale=1):
+ '''From the information gathered about the images (including
+ their bitmap representation, generate image files in the
+ module
+
+ '''
+ categories = {}
+ unittypes = []
+ echelons = []
+ commands = []
+
+ with VerboseGuard(f'Writing images in VMod '
+ f'{self._vmod.fileName()}',end=' ') as v:
+ for info in self._info:
+ if self.ignoreEntry(info): continue
+
+ typ = info.get('category','counter')
+ sub = info.get('subcategory','all')
+
+ info['filename'] = info['name'].replace(' ','_') + '.png'
+ imgfn = 'images/'+info['filename']
+ if imgfn not in self._vmod.getFileNames():
+ if typ == 'counter':
+ # print(f'Possibly scale file {imgfn}')
+ info['img'] = self.scaleImage(info['img'],
+ counterScale)
+ # self.message(f'Writing image {imgfn}')
+ self._vmod.addFile(imgfn,info['img'])
+
+ if sub == '':
+ info['subcategory'] = 'all'
+ sub = 'all'
+
+ # Add into catalogue
+ if typ not in categories:
+ v('')
+ v(f'Adding category "{typ}"')
+ v('',end=' ')
+ categories[typ] = {}
+ cat = categories[typ]
+
+ if sub not in cat:
+ v('')
+ v(f'Adding sub-category "{sub}"')
+ v('',end=' ')
+ cat[sub] = {}
+ tgt = cat[sub]
+
+ v(f'[{info["name"]}]',end=' ',flush=True,noindent=True)
+ #self.message(f'Adding "{info["name"]}" to catalogue')
+ tgt[info['name']] = info
+
+ if self._nonato: continue
+
+ # Get NATO App6c information, if any
+ natoapp6c = info.get('natoapp6c',None)
+ if natoapp6c is not None:
+ from re import sub
+ def clean(s):
+ return sub('.*=','',
+ (sub(r'\[[^]]+\]','',s.strip())
+ .replace('{','')
+ .replace('}','')
+ .replace('/',' '))).strip()
+ mains = clean(natoapp6c.get('main', ''))
+ lower = clean(natoapp6c.get('lower', ''))
+ upper = clean(natoapp6c.get('upper', ''))
+ echelon = clean(natoapp6c.get('echelon',''))
+ command = clean(natoapp6c.get('command',''))
- Returns
- -------
- xml : str
- The encoded document
- '''
- from xml.dom.minidom import Document
-
- if verbose:
- print(f'Creating buildFile.xml: {title},{version}')
-
- # Create document
- root = Document()
-
- # Add game element
- game = add_game(root,root,
- name = title,
- version = version,
- description = desc)
-
- # Extract the sides of the game from the counter sub-categories
- # Should be a little more clever about the case here
- dsides = {k.lower() : k for k in categories.get('counter',{}).keys()}
- sides = None
- for skip in ['all','common', 'marker', 'markers']:
- if skip in dsides: del dsides[skip]
- sides = list(dsides.values())
- if verbose:
- print('Identified factions: ',sides)
-
- # Use the standard command encoder
- add_basiccommandencoder(root,game)
-
- # Add document parts
- doc = add_documentation(root,game)
- add_splash(root, doc, categories, title,verbose)
- add_rules(root, doc, zipfile, rules, verbose)
- add_tut(root, doc, zipfile, tutorial, verbose)
- notes = add_notes(root,doc,zipfile,title,version,
- sides,pdffile,infofile,rules,verbose)
-
-
- # Create roster or player sides
- roster = add_roster(root,game)
- for s in sides:
- add_entry(root,roster,s)
-
- # Add global options - mainly defer to user settings
- add_globaloptions(root,game,
- autoReport = 'Use Preferences Setting',
- centerOnMove = 'Use Preferences Setting',
- nonOwnerUnmaskable = 'Use Preferences Setting',
- playerIdFormat = '$playerName$')
- # Add global properties
- gp = add_globalproperties(root,game)
- # Add a turn tracker
- tt = add_turntrack(root,game,'Turn',counter={
- 'property': 'Turn',
- 'phases': {
- 'property': 'Phase',
- 'names': sides }})
- add_globalproperty(root,gp,'TurnTracker.defaultDocked',True)
-
- # Add all pieces to the module (including prototypes)
- gpid = add_counters(root,game,categories,
- unittypes,echelons,commands,verbose)
- # Add an inventory
- add_inv(root,game,sides)
- # Add all boards to the module (including eliminated map)
- gpid = add_boards(root,game,categories,sides,gpid,
- verbose=verbose,visible=visible)
- # Add all OOBs to the module
- gpid = add_oobs(root,game,categories,gpid,
- verbose=verbose,visible=visible)
- # Add charts to the module
- add_charts(root,game,categories,verbose=verbose)
- # Add a dice rool
- add_diceroll(root,game,categories,verbose=verbose)
-
- # Set next available piece identifier on the game
- game.setAttribute('nextPieceSlotId',str(gpid+1))
-
- # Return the document
- return root, notes
-
-# --------------------------------------------------------------------
-def create_data(title,version,desc='',verbose=False):
- '''Create the module data XML
+
+ if mains is not None:
+ if len(lower) > 0: mains += ' '+lower
+ if len(upper) > 0: mains += ' '+upper
+ mains = sub(r'\[[^]]+\]','',mains)\
+ .replace('{','').replace('}','')#.split(',')
+ unittypes.append(mains.replace(',',' '))
+ unittypes.extend([s.strip().replace(',',' ')
+ for s in mains.split(',')])
+ #if len(mains) > 1:
+ # unittypes.append('+'.join(mains))
+ info['mains'] = mains
+
+ if len(echelon) > 0:
+ echelons.append(echelon)
+ info['echelon'] = echelon
+
+ if len(command) > 0:
+ commands.append(command)
+ info['command'] = command
+
+
+ # Finished loop over infos. Make unit types, echelons,
+ # commands unique
+ v('done')
+
+ return categories, set(unittypes), set(echelons), set(commands)
+
+ # ================================================================
+ def createModuleData(self):
+ '''Create the `moduleData` file in the module
+ '''
+ with VerboseGuard(f'Creating module data'):
+ self._moduleData = ModuleData()
+ data = self._moduleData.addData()
+ data.addVersion (version=self._version)
+ data.addVASSALVersion(version=self._vassalVersion)
+ data.addName (name=self._title)
+ data.addDescription (description=self._description)
+ data.addDateSaved ()
+
+ # ================================================================
+ def createBuildFile(self,
+ ignores = '(.*markers?|all|commons|.*hidden|[ ]+)'):
+ '''Create the `buildFile.xml` file in the module.
+
+ Parameters
+ ----------
+ ignores : str
+ Regular expression to match ignored categories for factions
+ determination. Python's re.fullmatch is applied to this
+ regular exression against chit categories. If the pattern
+ is matched, then the chit is not considered to belong to a
+ faction.
+
+ '''
+ from re import fullmatch, IGNORECASE
+ with VerboseGuard(f'Creating build file') as v:
+ self._build = BuildFile() # 'buildFile.xml')
+ self._game = self._build.addGame(name = self._title,
+ version = self._version,
+ description = self._description)
+ doc = self.addDocumentation()
+ self._game.addBasicCommandEncoder()
+
+ # Extract the sides
+ self._sides = [ k
+ for k in self._categories.get('counter',{}).keys()
+ if fullmatch(ignores, k, IGNORECASE) is None]
+ v(f'Got sides: {", ".join(self._sides)}')
+
+ v(f'Adding Global options')
+ go = self._game.addGlobalOptions(
+ autoReport = GlobalOptions.PROMPT,
+ centerOnMove = GlobalOptions.PROMPT,
+ nonOwnerUnmaskable = GlobalOptions.PROMPT,
+ playerIdFormat = '$playerName$')
+ go.addOption(name='undoHotKey',value=key('Z'))
+ go.addOption(name='undoIcon', value='/images/Undo16.gif')
+ # go.addOptoin(name='stepHotKey',value='')
+ go.addBoolPreference(name = self._verbose,
+ default = True,
+ desc = 'Be verbose',
+ tab = self._title)
+ go.addBoolPreference(name = self._debug,
+ default = False,
+ desc = 'Show debug chat messages',
+ tab = self._title)
+ go.addBoolPreference(name = self._autoOdds,
+ default = False,
+ desc = 'Calculate Odds on battle declaration',
+ tab = self._title)
+ go.addBoolPreference(name = self._autoResults,
+ default = False,
+ desc = 'Resolve battle results automatically',
+ tab = self._title)
+ go.addBoolPreference(name = self._noClearMoves,
+ default = False,
+ desc = ('Do not remove moved markers '
+ 'on phase change'),
+ tab = self._title)
+ go.addBoolPreference(name = self._noClearBattles,
+ default = False,
+ desc = ('Do not remove battle markers '
+ 'on phase change'),
+ tab = self._title)
+
+ v(f'Adding player roster')
+ roster = self._game.addPlayerRoster()
+ for side in self._sides:
+ roster.addSide(side)
+
+ v(f'Adding global properties')
+ glob = self._game.addGlobalProperties()
+ glob.addProperty(name='TurnTracker.defaultDocked',
+ initialValue=True)
+
+ self._battleMarks = self._categories\
+ .get('counter',{})\
+ .get('BattleMarkers',[])
+ if len(self._battleMarks) > 0:
+ v(f'We have battle markers')
+
+ glob.addProperty(name = self._battleCounter,
+ initialValue = 0,
+ isNumeric = True,
+ min = 0,
+ max = len(self._battleMarks),
+ wrap = True,
+ description = 'Counter of battles')
+ glob.addProperty(name = self._currentBattle,
+ initialValue = 0,
+ isNumeric = True,
+ min = 0,
+ max = len(self._battleMarks),
+ wrap = True,
+ description = 'Current battle number')
+ glob.addProperty(name = self._currentAttacker,
+ initialValue = 0,
+ isNumeric = True,
+ min = 0,
+ max = 1,
+ wrap = True,
+ description = 'Current unit is attacker')
+ glob.addProperty(name = self._battleAF,
+ initialValue = 0,
+ isNumeric = True,
+ description = 'Current battle AF')
+ glob.addProperty(name = self._battleDF,
+ initialValue = 0,
+ isNumeric = True,
+ description = 'Current battle DF')
+ glob.addProperty(name = self._battleFrac,
+ initialValue = 0,
+ isNumeric = True,
+ description = 'Current battle fraction')
+ glob.addProperty(name = self._battleShift,
+ initialValue = 0,
+ isNumeric = True,
+ description = 'Current battle odds shift')
+ glob.addProperty(name = self._battleOdds,
+ initialValue = '',
+ isNumeric = False,
+ description = 'Current battle odds')
+ glob.addProperty(name = self._battleResult,
+ initialValue = '',
+ isNumeric = False,
+ description = 'Current battle results')
+ glob.addProperty(name = self._battleIdx,
+ initialValue = 0,
+ isNumeric = True,
+ description = 'Current battle odds index')
+
+ self._oddsMarks = self._categories\
+ .get('counter',{})\
+ .get('OddsMarkers',[])
+ if len(self._oddsMarks) > 0:
+ v(f'We have odds markers')
+
+ self._resultMarks = self._categories\
+ .get('counter',{})\
+ .get('ResultMarkers',[])
+ if len(self._resultMarks) > 0:
+ v(f'We have result markers')
+
+ self.addNotes()
+ v(f'Adding turn track')
+ turns = self._game.addTurnTrack(name='Turn',
+ counter={
+ 'property': 'Turn',
+ 'phases': {
+ 'property': 'Phase',
+ 'names': self._sides } })
+ turns.addHotkey(hotkey = self._clearMoved+'Phase',
+ name = 'Clear moved markers')
+ if len(self._battleMarks) > 0:
+ turns.addHotkey(hotkey = self._clearBattlePhs,
+ name = 'Clear battle markers')
+
+ self._dice = self._categories\
+ .get('die-roll',{})
+ if len(self._dice) > 0:
+ v(f'We have symbolic dice')
+ self._diceInit = []
+ # from pprint import pprint
+ # pprint(self._dice,depth=3)
+ for die, faces in self._dice.items():
+ ico = self.getIcon(die+'-die-icon','')
+ # print(f'Die {die} icon="{ico}"')
+
+ dmin = +100000
+ dmax = -100000
+ symb = self._game.addSymbolicDice(
+ name = die+'Dice',
+ text = die if ico == '' else '',
+ icon = ico,
+ tooltip = f'{die} die roll',
+ format = (f'{{"{die} die roll: "+result1'
+ # f'+" <img src=\'{die}-"+result1'
+ # f'+".png\' width=24 height=24>"'
+ f'}}'),
+ resultWindow = True);
+ sdie = symb.addDie(name = die);
+ for face, fdata in faces.items():
+ fn = fdata['filename']
+ val = int(fn.replace('.png','').replace(die+'-',''))
+ dmin = min(dmin,val)
+ dmax = min(dmax,val)
+ sdie.addFace(icon = fn,
+ text = str(val),
+ value = val);
+
+ self._diceInit.extend([
+ GlobalPropertyTrait(
+ ['',self._diceInitKey,
+ GlobalPropertyTrait.DIRECT,
+ f'{{{dmin}}}'],
+ name = die+'Dice_result',
+ numeric = True,
+ min = dmin,
+ max = dmax,
+ description = f'Initialize {die}Dice'),
+ ReportTrait(
+ self._diceInitKey,
+ report=(f'{{{self._debug}?("Initialize '
+ f'{die}Dice_result to {dmin}"):""}}'))
+ ])
+
- Parameters
- ----------
- title : str
- Title of the game
- version : str
- Version number of the module
- desc : str
- Short description
-
- Returns
- -------
- xml : str
- The encoded document
- '''
- from xml.dom.minidom import Document
- from time import time
-
- if verbose:
- print('Create module data')
-
- # The document
- root = Document()
-
- # Top of the document
- data = root.createElement('data')
- data.setAttribute("version","1")
- root.appendChild(data)
-
- # Version number of module
- vers = root.createElement('version')
- vers.appendChild(root.createTextNode(str(version)))
- data.appendChild(vers)
-
- # Name of the game
- name = root.createElement('name')
- name.appendChild(root.createTextNode(title))
- data.appendChild(name)
-
- # VASSAL version (should get this in some other way)
- vvers = root.createElement('VassalVersion')
- vvers.appendChild(root.createTextNode("3.6.7"))
- data.appendChild(vvers)
-
- # Description, if any
- desc = root.createElement('description')
- data.appendChild(desc)
-
- # Automatically set the save data
- save = root.createElement('dateSaved')
- save.appendChild(root.createTextNode(f'{int(time())}'))
- data.appendChild(save)
-
- return root
-
-
-# ====================================================================
-def create_vmod(vmodname,
- info,
- title,
- version,
- desc = '',
- rules = None,
- tutorial = None,
- pdfname = '',
- infoname = '',
- patch = None,
- verbose = False,
- visible = False):
- '''Create a VASSAL module from the information passed
+ # Add start-up key
+ self._game.addStartupMassKey(
+ name = 'Initialise dice results',
+ hotkey = self._diceInitKey,
+ target = '',
+ filter = f'{{BasicName=="{self._hiddenName}"}}',
+ whenToApply = StartupMassKey.EVERY_LAUNCH,
+ reportFormat=f'{{{self._debug}?("Init Dice results"):""}}')
+
+
+
- Parameters
- ----------
- vmodname : str
- Name of the module
- info : list
- List of images and image data
- title : str
- Name of the game
- version : str
- Version of the module
- desc : str
- Short description of the game
- rules : str
- Rules PDF (optional)
+
+
+ self.addKeybindings(doc)
+ self.addCounters()
+ self.addInventory()
+ self.addBoards()
+ self.addDeadMap()
+ self.addOOBs()
+ self.addCharts()
+ self.addDie()
+
+ # ----------------------------------------------------------------
+ def addDocumentation(self):
+ '''Add documentation to the module. This includes rules,
+ key-bindings, and about elements.
+ '''
+ with VerboseGuard('Adding documentation') as v:
+ doc = self._game.addDocumentation()
+ if self._rules is not None:
+ self._vmod.addExternalFile(self._rules,'rules.pdf')
+ doc.addBrowserPDFFile(title = 'Show rules',
+ pdfFile = 'rules.pdf')
+
+ if self._tutorial is not None:
+ self._vmod.addExternalFile(self._tutorial,'tutorial.vlog')
+ doc.addTutorial(name = 'Tutorial',
+ logfile = 'tutorial.vlog',
+ launchOnStartup = True)
+
+
+ fronts = self._categories.get('front',{}).get('all',[])
+ front = list(fronts.values())[0] if len(fronts) > 0 else None
+ if front is not None:
+ v(f'Adding about page')
+ doc.addAboutScreen(title=f'About {self._title}',
+ fileName = front['filename'])
+
+ return doc
+
+ # ----------------------------------------------------------------
+ def addKeybindings(self,doc):
+ keys = [
+ ['Alt-A', '-', 'Show the charts panel'],
+ ['Alt-B', '-', 'Show the OOBs'],
+ ['Alt-C', '-', 'Show the counters panel'],
+ ['Alt-E', '-', 'Show the eliminated units'],
+ ['Alt-I', '-', 'Show/refresh inventory window'],
+ ['Alt-M', '-', 'Show map'],
+ ['Alt-T', '-', 'Increase turn track'],
+ ['Alt-Shift-T', '-', 'Decrease turn track'],
+ ['Alt-6', '-', 'Roll the dice'],
+ ['Ctrl-D', 'Board,Counter','Delete counters'],
+ ['Ctrl-E', 'Board,Counter','Eliminate counters'],
+ ['Ctrl-F', 'Board,Counter','Flip counters'],
+ ['Ctrl-M', 'Board,Counter','Toggle "moved" markers'],
+ ['Ctrl-O', 'Board', 'Hide/show counters'],
+ ['Ctrl-R', 'Board,Counter','Restore unit'],
+ ['Ctrl-T', 'Board,Counter','Toggle move trail'],
+ ['Ctrl-Z', 'Board', 'Undo last move'],
+ ['Ctrl-+', 'Board', 'Zoom in'],
+ ['Ctrl--', 'Board', 'Zoom out'],
+ ['Ctrl-=', 'Board', 'Select zoom'],
+ ['Ctrl-Shift-O', 'Board','Show overview map'],
+ ['&larr;,&rarr;,&uarr;&darr;','Board',
+ 'Scroll board left, right, up, down (slowly)'],
+ ['PnUp,PnDn','Board', 'Scroll board up/down (fast)'],
+ ['Ctrl-PnUp,Ctrl-PnDn','Board', 'Scroll board left/right (fast)'],
+ ['Mouse-scroll up/down', 'Board', 'Scroll board up//down'],
+ ['Shift-Mouse-scroll up/down','Board','Scroll board right/leftown'],
+ ['Ctrl-Mouse-scroll up/down','Board','Zoom board out/in'],
+ ['Mouse-2', 'Board', 'Centre on mouse']]
+ if self._battleMarks:
+ for a,l in zip(['Ctrl-D','Ctrl-Shift-O', 'Ctrl-+', 'Ctrl-+'],
+ [['Ctrl-C', 'Counter', 'Clear battle'],
+ ['Ctrl-Shift-C','Board', 'Clear all battle'],
+ ['Ctrl-X', 'Board,Counter','Mark battle'],
+ ['Ctrl-Y', 'Board,Counter','Resolve battle'],
+ ]):
+ ks = [k[0] for k in keys]
+ didx = ks.index(a)
+ keys.insert(didx,l)
+
+ self._vmod.addFile('help/keys.html',
+ Documentation.createKeyHelp(
+ keys,
+ title=self._title,
+ version=self._version))
+ doc.addHelpFile(title='Key bindings',fileName='help/keys.html')
+
+ # ----------------------------------------------------------------
+ def addNatoPrototypes(self,prototypes):
+ # Add unit categories as prototypes
+ for n,c in zip(['Type','Echelon','Command'],
+ [self._mains, self._echelons, self._commands]):
+ sc = set([cc.strip() for cc in c])
+ with VerboseGuard(f'Adding prototypes for "{n}"') as vv:
+ for i,cc in enumerate(sc):
+ cc = cc.strip()
+ if len(cc) <= 0: continue
+ vv(f'[{cc}] ',end='',flush=True,noindent=True)
+ p = prototypes.addPrototype(name = f'{cc} prototype',
+ description = '',
+ traits = [MarkTrait(n,cc),
+ BasicTrait()])
+ vv('')
+
+ # ----------------------------------------------------------------
+ def addBattleControlPrototype(self,prototypes):
+ # Control of battles.
+ #
+ # This has traits to
+ #
+ # - Zero battle counter
+ # - Increment battle counter
+ # - Set current battle number
+ # - Mark battle
+ # - Calculate odds
+ #
+ # When wgMarkBattle is issued to this piece, then
+ #
+ # - Increment battle counter
+ # - Set global current battle
+ # - Trampoline to GCK markBattle
+ # - For all selected pieces, issue markBattle
+ # - All wgBattleUnit pieces then
+ # - Get current battle # and store
+ # - Add marker on top of it self
+ # - Issue calculateOddsAuto
+ # - If auto odds on, go to calcOddsStart,
+ # - Trampoline to GCK calcOddsAuto
+ # - Which sends calcOddsStart to all markers
+ # - For each mark
+ # - Set current battle to current global
+ # - Trampoline calcOdds via GKC
+ # - Send calcBattleOdds to wgBattleCalc
+ # - Zero odds
+ # - Calculate fraction
+ # - Zero fraction
+ # - Calculate total AF
+ # - Zero AF
+ # - via trampoline to GKC
+ # - Calculate total DF
+ # - Zero DF
+ # - via trampoline to GKC
+ # - Real fraction calculation
+ # - From calculate fraction
+ # - Access via calculate trait
+ # - Calculate shift
+ # - Zero shift
+ # - Trampoline to GKC
+ # - Access via calculate trait
+ # - Calculate index
+ # - Via calculated OddsIndex
+ # - Calculate odds real
+ # - Via calculated Index to odds
+ # - Do markOddsAuto which selects between odds
+ # - Do markOddsReal+OddsIndex
+ # - Set global battle #
+ # - Place marker
+ # - Take global battle #
+ # - De-select all other marks to prevent
+ # further propagation
+ #
+ if len(self._battleMarks) <= 0:
+ return False
+
+ n = len(self._battleMarks)
+ # --- Battle counter control - reset and increment -----------
+ traits = [
+ GlobalPropertyTrait(
+ ['',self._zeroBattle,GlobalPropertyTrait.DIRECT,'{0}'],
+ ['',self._incrBattle,GlobalPropertyTrait.DIRECT,
+ f'{{({self._battleCounter}%{n})+1}}'],
+ name = self._battleCounter,
+ numeric = True,
+ min = 0,
+ max = n,
+ wrap = True,
+ description = 'Zero battle counter',
+ ),
+ GlobalPropertyTrait(
+ ['',self._setBattle,GlobalPropertyTrait.DIRECT,
+ f'{{{self._battleCounter}}}'],
+ name = self._currentBattle,
+ numeric = True,
+ min = 0,
+ max = n,
+ wrap = True,
+ description = 'Zero battle counter',
+ ),
+ ReportTrait(self._zeroBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": zero battle counter: "'
+ f'+{self._battleCounter}):""}}')),
+ ReportTrait(self._incrBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": '
+ f'increment battle counter: "'
+ f'+{self._battleCounter}):""}}')),
+ ReportTrait(self._setBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": set current battle: "+'
+ f'{self._battleCounter}+" -> "+'
+ f'{self._currentBattle}):""}}')),
+ GlobalHotkeyTrait(name = '',
+ key = self._markBattle+'Trampoline',
+ globalHotkey = self._markBattle,
+ description = 'Mark selected for battle'),
+ ReportTrait(self._markBattle+'Trampoline',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": forward mark battle: "+'
+ f'{self._battleCounter}):""}}')),
+ GlobalHotkeyTrait(name = '',
+ key = self._calcBattleOdds+'Start',
+ globalHotkey = self._calcBattleOdds+'Auto',
+ description = 'Trampoline to global'),
+ ReportTrait(self._calcBattleOdds+'Start',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": forward odds: "+'
+ f'{self._battleCounter}):""}}')),
+ DeselectTrait(command = '',
+ key = self._calcBattleOdds+'Deselect',
+ deselect = DeselectTrait.ALL),
+ ReportTrait(self._calcBattleOdds+'Deselect',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": select only this: "+'
+ f'{self._battleCounter}):""}}')),
+ TriggerTrait(command = '',
+ key = self._calcBattleOdds+'Auto',
+ actionKeys = [self._calcBattleOdds+'Start'],
+ property = f'{{{self._autoOdds}==true}}'),
+ ReportTrait(self._calcBattleOdds+'Auto',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": forward odds: "+'
+ f'{self._battleCounter}):""}}')),
+ TriggerTrait(command = '',
+ key = self._markBattle,
+ actionKeys = [self._incrBattle,
+ self._setBattle,
+ self._markBattle+'Trampoline',
+ self._calcBattleOdds+'Auto']),
+ ReportTrait(self._markBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": mark battle: "+'
+ f'{self._battleCounter}):""}}')),
+ GlobalHotkeyTrait(name = '',
+ key = self._clearAllBattle+'Trampoline',
+ globalHotkey = self._clearAllBattle,
+ description = 'Mark selected for battle'),
+ TriggerTrait(command = '',
+ key = self._clearAllBattle,
+ actionKeys = [self._clearAllBattle+'Trampoline',
+ self._zeroBattle]),
+ ReportTrait(self._clearBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": clear battle: "+'
+ f'{self._battleCounter}):""}}')),
+ GlobalHotkeyTrait(name = '',
+ key = self._clearMoved+'Trampoline',
+ globalHotkey = self._clearMoved,
+ description = 'Clear moved markers'),
+ MarkTrait(name=self._battleCtrl,value=True),
+ BasicTrait()]
+ prototypes.addPrototype(name = self._battleCtrl,
+ description = '',
+ traits = traits)
+ return True
+
+ # ----------------------------------------------------------------
+ def addBattleCalculatePrototype(self,prototypes):
+ # --- Batttle AF, DF, Odds -----------------------------------
+ # This calculate odds derivation from stated odds.
+ calcIdx = 0
+ maxIdx = len(self._oddsMarks)+1
+ minIdx = 0
+ idx2Odds = '""'
+ calcFrac = 1
+ if len(self._oddsMarks) > 0:
+ odds = [o.replace('odds marker','').strip() for
+ o in self._oddsMarks]
+ ratios = all([':' in o for o in odds])
+ if ratios: # All is ratios!
+ def calc(s):
+ num, den = [float(x.strip()) for x in s.split(':')]
+ return num/den
+ ratios = [[calc(s),s] for s in odds]
+ ind = [i[0] for i in sorted(enumerate(ratios),
+ key=lambda x:x[1][0])]
+ #print(f'=== Rations: {ratios}, Index: {ind[::-1]}')
+ calcIdx = ':'.join([f'{self._battleFrac}>={ratios[i][0]}?'
+ f'({i+1})'
+ for i in ind[::-1]]) + ':0'
+ idx2Odds = ':'.join([f'OddsIndex=={i+1}?'
+ f'"{ratios[i][1]}"'
+ for i in ind[::-1]]) + ':""'
+ calcFrac = (f'{{{self._battleDF}==0?0:'
+ f'(((double)({self._battleAF}))'
+ fr'\/{self._battleDF})}}')
+ #print(calcIdx,idx2Odds)
+ else:
+ try:
+ nums = [[int(o),o] for o in odds]
+ calcFrac = f'{{{self._battleAF}-{self._battleDF}}}'
+ ind = [i[0] for i in sorted(enumerate(nums),
+ key=lambda x:x[1])]
+ calcIdx = ':'.join([f'{self._battleFrac}>={nums[i][0]}?'
+ f'({i+1})'
+ for i in ind[::-1]])+':0'
+ idx2Odds = ':'.join([f'OddsIndex=={i+1}?"{nums[i][1]}"'
+ for i in ind[::-1]]) + ':""'
+ vidx2Odds = '\t'+idx2Odds.replace(':',':\n\t')
+ #print(f'Index to odds: {vidx2Odds}')
+ except:
+ pass
+
+ traits = [
+ CalculatedTrait(# This should be changed to game rules
+ name = 'OddsShift',
+ expression = f'{{{self._battleShift}}}',
+ description = 'Calculated internal oddsshift'),
+ CalculatedTrait(# This should be changed to game rules
+ name = 'OddsIndexRaw',
+ expression = f'{{{calcIdx}}}',
+ description = 'Calculated internal odds index'),
+ CalculatedTrait(# This should be changed to game rules
+ name = 'OddsIndexLimited',
+ expression = (f'{{OddsIndexRaw>{maxIdx}?{maxIdx}:'
+ f'OddsIndexRaw<{minIdx}?{minIdx}:'
+ f'OddsIndexRaw}}'),
+ description = 'Calculated internal limited odds index'),
+ CalculatedTrait(# This should be changed to game rules
+ name = 'OddsIndex',
+ expression = (f'{{OddsIndexLimited+OddsShift}}'),
+ description = 'Calculated internal odds index (with shift)'),
+ CalculatedTrait(# This should be changed to game rules
+ name = 'BattleFraction',
+ expression = calcFrac,
+ description = 'Calculated fraction off battle'),
+ GlobalPropertyTrait(
+ ['',self._zeroBattleShft,GlobalPropertyTrait.DIRECT,'{0}'],
+ name = self._battleShift,
+ numeric = True,
+ description = 'Zero battle odds shift',
+ ),
+ GlobalPropertyTrait(
+ ['',self._zeroBattleAF,GlobalPropertyTrait.DIRECT,'{0}'],
+ name = self._battleAF,
+ numeric = True,
+ description = 'Zero battle AF',
+ ),
+ GlobalPropertyTrait(
+ ['',self._zeroBattleDF,GlobalPropertyTrait.DIRECT,'{0}'],
+ name = self._battleDF,
+ numeric = True,
+ description = 'Zero battle AF',
+ ),
+ # {wgBattleDF==0?0:(double(wgBattleAF)/wgBattleDF)}
+ GlobalPropertyTrait(
+ ['',self._zeroBattleFrac,GlobalPropertyTrait.DIRECT,'{0}'],
+ ['',self._calcBattleFrac+'Real',GlobalPropertyTrait.DIRECT,
+ '{BattleFraction}'],
+ name = self._battleFrac,
+ description = 'Calculate battle fraction',
+ ),
+ GlobalPropertyTrait(
+ ['',self._zeroBattleIdx,GlobalPropertyTrait.DIRECT,'{0}'],
+ ['',self._calcBattleIdx,GlobalPropertyTrait.DIRECT,
+ '{OddsIndex}'],
+ name = self._battleIdx,
+ description = 'Calculate battle odds index',
+ ),
+ GlobalPropertyTrait(
+ ['',self._zeroBattleOdds,GlobalPropertyTrait.DIRECT,'{""}'],
+ ['',self._calcBattleOdds+'Real',GlobalPropertyTrait.DIRECT,
+ f'{{{idx2Odds}}}'],
+ name = self._battleOdds,
+ description = 'Calculate battle odds',
+ ),
+ GlobalHotkeyTrait(name = '',# Forward to units
+ key = self._calcBattleAF+'Trampoline',
+ globalHotkey = self._calcBattleAF,
+ description = 'Calculate total AF'),
+ GlobalHotkeyTrait(name = '',# Forward to units
+ key = self._calcBattleDF+'Trampoline',
+ globalHotkey = self._calcBattleDF,
+ description = 'Calculate total DF'),
+ GlobalHotkeyTrait(name = '',# Forward to units
+ key = self._calcBattleShft+'Trampoline',
+ globalHotkey = self._calcBattleShft,
+ description = 'Calculate total shift'),
+ TriggerTrait(command = '',
+ key = self._calcBattleAF,
+ actionKeys = [self._zeroBattleAF,
+ self._calcBattleAF+'Trampoline']),
+ TriggerTrait(command = '',
+ key = self._calcBattleDF,
+ actionKeys = [self._zeroBattleDF,
+ self._calcBattleDF+'Trampoline']),
+ TriggerTrait(command = '',
+ key = self._calcBattleShft,
+ actionKeys = [self._zeroBattleShft,
+ self._calcBattleShft+'Trampoline']),
+ TriggerTrait(command = '',
+ key = self._calcBattleFrac,
+ actionKeys = [self._zeroBattleFrac,
+ self._calcBattleAF,
+ self._calcBattleDF,
+ self._calcBattleFrac+'Real']),
+ TriggerTrait(command = '',
+ key = self._calcBattleOdds,
+ actionKeys = [self._zeroBattleOdds,
+ self._calcBattleFrac,
+ self._calcBattleShft,
+ self._calcBattleIdx,
+ self._calcBattleOdds+'Real']),
+ ReportTrait(self._zeroBattleAF,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Reset AF: "+'
+ f'{self._battleAF}):""}}')),
+ ReportTrait(self._zeroBattleDF,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Reset DF: "+'
+ f'{self._battleDF}):""}}')),
+ ReportTrait(self._zeroBattleFrac,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Reset fraction: "+'
+ f'{self._battleFrac}):""}}')),
+ ReportTrait(self._zeroBattleOdds,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Reset odds: "+'
+ f'{self._battleOdds}):""}}')),
+ ReportTrait(self._calcBattleAF,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Total AF: "+'
+ f'{self._battleAF}):""}}')),
+ ReportTrait(self._calcBattleDF,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Total DF: "+'
+ f'{self._battleDF}):""}}')),
+ ReportTrait(self._calcBattleShft,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle odds shift: "+'
+ f'{self._battleShift}):""}}')),
+ ReportTrait(self._calcBattleFrac,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle fraction: "+'
+ f'{self._battleFrac}):""}}')),
+ ReportTrait(self._calcBattleOdds,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle odds: "+'
+ f'{self._battleOdds}+" ("+'
+ f'{self._battleIdx}+")"):""}}')),
+ ReportTrait(self._calcBattleFrac+'Real',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle fraction: "+'
+ f'{self._battleFrac}+'
+ f'" AF="+{self._battleAF}+'
+ f'" DF="+{self._battleDF}'
+ f'):""}}')),
+ ReportTrait(self._calcBattleOdds+'Real',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle odds: "+'
+ f'{self._battleOdds}+'
+ f'" ("+{self._battleIdx}+","+OddsShift+","+'
+ f'" raw="+OddsIndexRaw+","+'
+ f'" limited="+OddsIndexLimited+","+'
+ f'" -> "+OddsIndex+","+'
+ f'{self._battleShift}+")"+'
+ f'" Fraction="+{self._battleFrac}+'
+ f'" AF="+{self._battleAF}+'
+ f'" DF="+{self._battleDF}'
+ f'):""}}')),
+ ReportTrait(self._calcBattleOdds+'Real',
+ report=(f'{{{self._verbose}?'
+ f'("! Battle # "'
+ f'+{self._battleNo}'
+ f'+" AF="+{self._battleAF}'
+ f'+" DF="+{self._battleDF}'
+ f'+" => "+{self._battleOdds}'
+ # f'+" <img src=\'odds_marker_"'
+ # f'+{self._battleOdds}+".png\' "'
+ # f'+" width=24 height=24>"'
+ f'):""}}')),
+ MarkTrait(name=self._battleCalc,value=True),
+ BasicTrait()]
+ prototypes.addPrototype(name = self._battleCalc,
+ description = '',
+ traits = traits)
+
+ # ----------------------------------------------------------------
+ def addBattleUnitPrototype(self,prototypes):
+ # --- Battle units that set battle markers -------------------
+ #
+ # - Trait to add battle number 1 to max
+ #
+ # - Trigger trait for each of these using the global property
+ # for the current battle
+ #
+ traits = [
+ DynamicPropertyTrait(['',self._getBattle,
+ DynamicPropertyTrait.DIRECT,
+ f'{{{self._currentBattle}}}'],
+ ['',self._clearBattle,
+ DynamicPropertyTrait.DIRECT,
+ f'{{-1}}'],
+ name = self._battleNo,
+ numeric = True,
+ value = f'{{-1}}',
+ description = 'Set battle number'),
+ GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
+ '{IsAttacker}'],
+ name = self._currentAttacker,
+ numeric = True,
+ description = 'Set attacker'),
+ ReportTrait(self._getBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" current battle # "+'
+ f'{self._currentBattle}+" -> "+'
+ f'{self._battleNo}):""}}')),
+ ReportTrait(self._clearBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" Clear this global="+'
+ f'{self._currentBattle}+" this="+'
+ f'{self._battleNo}):""}}')),
+ ]
+ place = []
+ trig = []
+ rept = []
+ for i, bm in enumerate(self._battleMarks):
+ kn = self._markBattle+str(i+1)
+ path = PlaceTrait.SKEL_PATH.format('BattleMarkers',bm)
+
+ place.append(
+ PlaceTrait(command = '',#f'Add battle marker {i}',
+ key = kn,
+ markerSpec = path,
+ markerText = 'null',
+ xOffset = -8,
+ yOffset = -16,
+ matchRotation = False,
+ afterKey = self._getBattle,
+ gpid = self._game.nextPieceSlotId(),
+ description = f'Add battle marker {i+1}',
+ placement = PlaceTrait.ABOVE,
+ above = False))
+ trig.append(
+ TriggerTrait(command = '',#Mark battle',
+ key = self._markBattle,
+ actionKeys = [self._getBattle,
+ self._setBattle,kn],
+ property = f'{{{self._currentBattle}=={i+1}}}'))
+ rept.append(
+ ReportTrait(kn,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" placing marker ({i+1})'
+ f' ="+ {self._currentBattle}+"'
+ f'={kn}"):""}}')))
+
+ oth = [
+ GlobalHotkeyTrait(name = 'Declare battle',
+ key = self._markKey,
+ globalHotkey = self._markKey,
+ description = 'Mark for combat'),
+ GlobalPropertyTrait(
+ ['',self._calcBattleAF,GlobalPropertyTrait.DIRECT,
+ f'{{EffectiveAF+{self._battleAF}}}'],
+ name = self._battleAF,
+ numeric = True,
+ description = 'Update battle AF'),
+ GlobalPropertyTrait(
+ ['',self._calcBattleDF,GlobalPropertyTrait.DIRECT,
+ f'{{EffectiveDF+{self._battleDF}}}'],
+ name = self._battleDF,
+ numeric = True,
+ description = 'Update battle AF'),
+ GlobalPropertyTrait(
+ ['',self._calcBattleShft,GlobalPropertyTrait.DIRECT,
+ f'{{OddsShift}}'],
+ name = self._battleShift,
+ numeric = True,
+ description = 'Update battle shift',
+ ),
+ CalculatedTrait(#This could be redefined in module
+ name = 'EffectiveAF',
+ expression = '{CF}',
+ description = 'Current attack factor'),
+ CalculatedTrait(#This could be redefined in module
+ name = 'EffectiveDF',
+ expression = '{DF}',
+ description = 'Current defence factor'),
+ CalculatedTrait(#This could be redefined in module
+ name = 'IsAttacker',
+ expression = '{Phase.contains(Faction)}',
+ description = 'Check if current phase belongs to faction'),
+ CalculatedTrait(#This could be redefined in module
+ name = 'OddsShift',
+ expression = f'{{{self._battleShift}}}',
+ description = 'Check if current phase belongs to faction'),
+ ReportTrait(self._calcBattleAF,
+ report=(f'{{{self._verbose}?'
+ f'("! "+BasicName+'
+ f'" add "+EffectiveAF+'
+ f'" to total attack factor -> "+'
+ f'{self._battleAF}'
+ f'):""}}')),
+ ReportTrait(self._calcBattleDF,
+ report=(f'{{{self._verbose}?'
+ f'("! "+BasicName+'
+ f'" add "+EffectiveDF+'
+ f'" to total defence factor -> "+'
+ f'{self._battleDF}'
+ f'):""}}')),
+ ReportTrait(self._calcBattleShft,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+'
+ f'" Updating odds shift with "+OddsShift+'
+ f'" -> "+{self._battleShift}):""}}')),
+ ]
+ traits.extend(
+ place+
+ trig+
+ oth+
+ [MarkTrait(name=self._battleUnit,value=True),
+ BasicTrait()])
+ prototypes.addPrototype(name = self._battleUnit,
+ description = '',
+ traits = traits)
+ # ----------------------------------------------------------------
+ def addBattleCorePrototype(self,prototypes):
+ # --- Core traits for battle markers (number, odds, results)
+ # - Set the global current battle number
+ # - Get the current global battle number
+ # - Clear this counter
+ # - Trampoline to global command to clear all marks for this battle
+ traits = [
+ # NoStackTrait(select = NoStackTrait.NORMAL_SELECT,
+ # move = NoStackTrait.NORMAL_MOVE,
+ # canStack = False,
+ # ignoreGrid = False),
+ GlobalPropertyTrait(['',self._setBattle,GlobalPropertyTrait.DIRECT,
+ f'{{{self._battleNo}}}'],
+ name = self._currentBattle,
+ numeric = True,
+ description = 'Set current battle'),
+ GlobalPropertyTrait(['',self._setBattle, GlobalPropertyTrait.DIRECT,
+ '{IsAttacker}'],
+ name = self._currentAttacker,
+ numeric = True,
+ description = 'Set attacker'),
+ DynamicPropertyTrait(['',self._getBattle,
+ DynamicPropertyTrait.DIRECT,
+ f'{{{self._currentBattle}}}'],
+ name = self._battleNo,
+ numeric = True,
+ value = f'{{{self._battleNo}}}',
+ description = 'Set battle number'),
+ DynamicPropertyTrait(['',self._getBattle,
+ DynamicPropertyTrait.DIRECT,
+ f'{{{self._currentAttacker}}}'],
+ name = 'IsAttacker',
+ numeric = True,
+ value = 'false',
+ description = 'Set attacker'),
+ DeleteTrait('',self._clearBattle),
+ GlobalHotkeyTrait(name = '',
+ key = self._clearBattle+'Trampo',
+ globalHotkey = self._clearBattle,
+ description = 'Clear selected battle'),
+ TriggerTrait(command = 'Clear',
+ key = self._clearKey,
+ actionKeys = [self._setBattle,
+ self._clearBattle+'Trampo']),
+ ReportTrait(self._setBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" battle # "+'
+ f'{self._battleNo}+" -> "+'
+ f'{self._currentBattle}):""}}')),
+ ReportTrait(self._getBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" current battle # "+'
+ f'{self._currentBattle}+" -> "+'
+ f'{self._battleNo}):""}}')),
+ ReportTrait(self._clearBattle,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" Clear this global="+'
+ f'{self._currentBattle}+" this="+'
+ f'{self._battleNo}):""}}')),
+ ReportTrait(self._clearKey,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" To clear battle # global="+'
+ f'{self._currentBattle}+" this="+'
+ f'{self._battleNo}):""}}')),
+ ReportTrait(self._clearBattle+'Trampo',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" '
+ f'Forward clear battle # global="+'
+ f'{self._currentBattle}+" this="+'
+ f'{self._battleNo}):""}}')),
+ MarkTrait(name=self._battleMark,value=True),
+ BasicTrait()
+ ]
+ prototypes.addPrototype(name = self._currentBattle,
+ description = '',
+ traits = traits)
+
+ # ----------------------------------------------------------------
+ def addBattlePrototypes(self,prototypes):
+ if not self.addBattleControlPrototype(prototypes):
+ return
+
+ self.addBattleCalculatePrototype(prototypes)
+ self.addBattleUnitPrototype(prototypes)
+ self.addBattleCorePrototype(prototypes)
+
+ # ----------------------------------------------------------------
+ def markerTraits(self):
+ return [DeleteTrait(),
+ RotateTrait()]
+
+ # ----------------------------------------------------------------
+ def battleMarkerTraits(self,c):
+ '''Derives from the CurrentBattle prototype and adds a submenu
+ to place odds counter on the battle marker'''
+ traits = [PrototypeTrait(name=self._currentBattle),
+ NonRectangleTrait(filename = c['filename'],
+ image = c['img'])]
+
+ subs = []
+ ukeys = []
+ place = []
+ trig = []
+ rept = []
+ repp = []
+ for i, odds in enumerate(self._oddsMarks):
+ on = odds.replace('odds marker','').strip()
+ om = odds.replace(':',r'\:')
+ kn = self._markOdds+str(i+1)
+ gpid = self._game.nextPieceSlotId()
+ path = PlaceTrait.SKEL_PATH.format('OddsMarkers',om)
+ subs.append(on)
+
+ place.append(
+ PlaceTrait(command = '',
+ key = kn,
+ markerSpec = path,
+ markerText = 'null',
+ xOffset = -6,
+ yOffset = -8,
+ matchRotation = False,
+ afterKey = self._getBattle+'Details',
+ gpid = gpid,
+ placement = PlaceTrait.ABOVE,
+ description = f'Add odds marker {on}'))
+ trig.append(
+ TriggerTrait(name = '',
+ command = on,
+ key = kn+'real',
+ actionKeys = [
+ self._setBattle,
+ kn]))
+ rept.append(
+ ReportTrait(kn+'real',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Set odds '
+ f'{on} ({kn})"):""}}')))
+ repp.append(
+ ReportTrait(kn,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Place odds '
+ f'{on} ({kn})"):""}}')))
+ ukeys.append(kn+'real')
+
+ auto = []
+ auton = []
+ if len(self._oddsMarks) > 0:
+ auton = ['Auto']
+ for i, odds in enumerate(self._oddsMarks):
+ trig.append(
+ TriggerTrait(name = '',
+ command = '',
+ key = self._markOdds+'Auto',
+ property = f'{{{self._battleIdx}=={i+1}}}',
+ actionKeys = [self._markOdds+str(i+1)]))
+
+ auto = [GlobalHotkeyTrait(name = '',
+ key = self._calcBattleOdds,
+ globalHotkey = self._calcBattleOdds,
+ description = 'Calculate fraction'),
+ DeselectTrait(command = '',
+ key = self._calcBattleOdds+'Deselect',
+ deselect = DeselectTrait.ONLY),
+ ReportTrait(self._calcBattleOdds+'Deselect',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Select only this "'
+ f'+" Attacker="+IsAttacker'
+ f'):""}}')),
+ TriggerTrait(name = '',
+ command = '',
+ key = self._markOdds+'Trampoline',
+ actionKeys = [
+ self._calcBattleOdds,
+ self._markOdds+'Auto',
+ self._calcBattleOdds+'Deselect'],
+ property = f'{{!IsAttacker}}'
+ ),
+ TriggerTrait(name = '',
+ command = 'Auto',
+ key = self._calcBattleOdds+'Start',
+ actionKeys = [
+ self._setBattle,
+ self._markOdds+'Trampoline',
+ ]),
+ ReportTrait(self._calcBattleOdds+'Start',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle odds "+'
+ f'{self._battleOdds}):""}}')),
+ ReportTrait(self._markOdds+'Auto',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": '
+ f'Auto battle odds "+'
+ f'{self._battleOdds}):""}}'))
+ ]
+
+ traits.extend([
+ RestrictCommandsTrait(
+ name='Hide when auto-odds are enabled',
+ hideOrDisable = RestrictCommandsTrait.HIDE,
+ expression = f'{{{self._autoOdds}==true}}',
+ keys = ukeys)]+
+ place
+ +trig
+ +auto
+ +rept
+ +repp)
+ if len(subs) > 0:
+ traits.extend([
+ SubMenuTrait(subMenu = 'Odds',
+ keys = auton+subs),
+ ])
+
+ return traits
+
+ # ----------------------------------------------------------------
+ def oddsMarkerTraits(self,c=None):
+ '''Derives from the CurrentBattle prototype and adds a submenu
+ to replace odds counter with result marker'''
+ traits = [PrototypeTrait(name=self._currentBattle),
+ NonRectangleTrait(filename = c['filename'],
+ image = c['img']),
+ DynamicPropertyTrait(
+ ['',self._getBattle+'More',DynamicPropertyTrait.DIRECT,
+ (f'{{{self._battleAF}+" vs "+{self._battleDF}+'
+ f'" (odds "+{self._battleOdds}+" shift "+'
+ f'{self._battleShift}+")"}}')],
+ name = 'BattleDetails',
+ value = '',
+ numeric = False,
+ description = 'Stored battle details'),
+ TriggerTrait(command = '',
+ key = self._getBattle+'Details',
+ actionKeys = [self._getBattle,
+ self._getBattle+'More'])]
+
+ subs = []
+ place = []
+ trig = []
+ rept = []
+ ukeys = []
+ first = ''
+ for i, result in enumerate(self._resultMarks):
+ r = result.replace('result marker','').strip()
+ kn = self._markResult+str(i+1)
+ gpid = self._game.nextPieceSlotId()
+ ukeys.append(kn+'real')
+ subs.append(r)
+ if first == '': first = r
+
+ path = PlaceTrait.SKEL_PATH.format('ResultMarkers',result)
+
+ place.append(
+ ReplaceTrait(command = '',
+ key = kn,
+ markerSpec = path,
+ markerText = 'null',
+ xOffset = -6,
+ yOffset = -8,
+ matchRotation = False,
+ afterKey = self._getBattle,
+ gpid = gpid,
+ description = f'Add result marker {r}'))
+ trig.append(
+ TriggerTrait(name = '',
+ command = r,
+ key = kn+'real',
+ actionKeys = [
+ self._setBattle,
+ kn]))
+ rept.append(
+ ReportTrait(kn+'real',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+" setting result '
+ f'{r}"):""}}')))
+
+ auto = []
+ auton = []
+ if len(self._resultMarks) > 0:
+ auton = ['Auto']
+ for i, res in enumerate(self._resultMarks):
+ r = res.replace('result marker','').strip()
+ trig.append(
+ TriggerTrait(
+ name = '',
+ command = '',
+ key = self._markResult+'Auto',
+ property = f'{{{self._battleResult}=="{r}"}}',
+ actionKeys = [self._markResult+str(i+1)]))
+
+ auto = [ # Override in the module
+ CalculatedTrait(
+ name = 'Die',
+ expression = '{GetProperty("1d6_result")}',
+ description = 'Die roll'),
+ GlobalHotkeyTrait(
+ name = '',
+ key = self._rollDice,
+ globalHotkey = self._diceKey,
+ description = 'Roll dice'),
+ CalculatedTrait(
+ name = 'BattleResult',
+ expression = f'{{"{first}"}}',
+ ),
+ GlobalPropertyTrait(
+ ['',self._calcBattleRes+'real',GlobalPropertyTrait.DIRECT,
+ '{BattleResult}'],
+ name = self._battleResult,
+ numeric = False,
+ description = 'Set combat result'),
+ TriggerTrait(name = '',
+ command = 'Resolve',
+ key = self._resolveKey,
+ property = f'{{{self._autoResults}==true}}',
+ actionKeys = [
+ self._setBattle,
+ self._rollDice,
+ self._calcBattleRes+'real',
+ self._markResult+'Auto',
+ ]),
+ ReportTrait(self._calcBattleRes,
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Battle result "+'
+ f'{self._battleOdds}):""}}')),
+ ReportTrait(self._markResult+'Auto',
+ report=(f'{{{self._debug}?'
+ f'("~ "+BasicName+": Auto battle result "+'
+ f'{self._battleResult}):""}}')),
+ ReportTrait(self._markResult+'Auto',
+ report=(f'{{"` Battle # "+{self._battleNo}+": "+'
+ f'BattleDetails+'
+ f'" with die roll "+Die+": "+'
+ f'{self._battleResult}'
+ # f'+ "<img src=\'result_marker_"'
+ # f'+{self._battleResult}+".png\'"'
+ # f'+" width=24 height=24>"'
+ f'}}')),
+ MarkTrait(name=self._battleOddsM,value='true')
+ ]
+
+ traits.extend(
+ [RestrictCommandsTrait(
+ name='Hide when auto-results are enabled',
+ hideOrDisable = RestrictCommandsTrait.HIDE,
+ expression = f'{{{self._autoResults}==true}}',
+ keys = ukeys)]+
+ place
+ +trig
+ +auto)
+
+ if len(subs) > 0:
+ traits.append(SubMenuTrait(subMenu = 'Result',
+ keys = subs))
+
+ return traits
+
+ # ----------------------------------------------------------------
+ def resultMarkerTraits(self,c=None):
+ traits = [PrototypeTrait(name=self._currentBattle),
+ NonRectangleTrait(filename = c['filename'],
+ image = c['img'])]
+
+ return traits
+
+ # ----------------------------------------------------------------
+ def factionTraits(self,faction):
+ traits = [ReportTrait(self._eliminateKey,
+ self._restoreKey,
+ self._trailKey),
+ TrailTrait(),
+ RotateTrait(),
+ MovedTrait(xoff = int( 36 * self._counterScale),
+ yoff = int(-38 * self._counterScale)),
+ DeleteTrait(),
+ SendtoTrait(mapName = 'DeadMap',
+ boardName = f'{faction} pool',
+ name = 'Eliminate',
+ key = self._eliminateKey,
+ restoreName = 'Restore',
+ restoreKey = self._restoreKey,
+ description = 'Eliminate unit'),
+ PrototypeTrait(name=self._battleUnit),
+ MarkTrait(name='Faction',value=faction)]
+
+ return traits
+
+ # ----------------------------------------------------------------
+ def getFactors(self,val):
+ cf = None
+ mf = None
+ df = None
+ ra = None
+ try:
+ if 'chit 1 factor' in val:
+ vv = val.replace('chit 1 factor=','')
+ cf = int(vv)
+ elif 'chit 2 factors artillery' in val:
+ vv = val.replace('chit 2 factors artillery=','')
+ cf,mf,ra = [int(v) for v in vv.strip('=').split()]
+ elif 'chit 2 factors' in val:
+ vv = val.replace('chit 2 factors=','')
+ cf,mf = [int(v) for v in vv.split()]
+ elif 'chit 3 factors' in val:
+ vv = val.replace('chit 3 factors=','')
+ cf,df,mf = [int(v) for v in vv.split()]
+
+ # Set defensive factor combat factor if not defined.
+ if df is None and cf is not None:
+ df = cf
+
+
+ except Exception as e:
+ print(f'\nWarning when extracting factors: {e} '
+ f'in "{val}" -> "{vv}"')
+ return None,None,None,None
+ pass
- Returns
- -------
- '''
- from zipfile import ZipFile, ZIP_DEFLATED
- from pprint import pprint
-
- categories = {}
- unittypes = []
- echelons = []
- commands = []
- with ZipFile(vmodname,'w',compression=ZIP_DEFLATED) as vmod:
- # Create images in ZIP file
- for i in info:
- if ignore_entry(i): continue
-
- # Store the name of the image file in catalogue
- i['filename'] = i['name'].replace(' ','_') + '.png'
- imgfn = 'images/'+i['filename']
- if imgfn not in vmod.namelist():
- vmod.writestr(imgfn,i['img'])
-
- # Categorize - assume counter
- typ = i.get('category','counter')
- sub = i.get('subcategory','all')
- if sub == '':
- i['subcategory'] = 'all'
- sub = 'all'
-
- # Add to catalogue
- if typ not in categories:
- categories[typ] = {}
- cat = categories[typ]
-
- if sub not in cat:
- cat[sub] = {}
- tgt = cat[sub]
-
- tgt[i['name']] = i
-
- natoapp6c = i.get('natoapp6c',None)
- if natoapp6c is not None:
- from re import sub
- mains = natoapp6c.get('main',None)
- echelon = natoapp6c.get('echelon',None)
- command = natoapp6c.get('command',None)
-
- if mains is not None:
- mains = sub(r'\[[^]]+\]','',mains)\
- .replace('{','').replace('}','').split(',')
- unittypes.extend(mains)
- i['mains'] = mains
+ return cf,df,mf,ra
+
+ # ----------------------------------------------------------------
+ def pieceTraits(self,subn,subc,cn,c):
+ from re import sub
+
+ bb = self.getBB(c['img'])
+ height = bb[3]-bb[1] if bb is not None else 1
+ width = bb[2]-bb[0] if bb is not None else 1
+ cf = subc.get(cn + ' flipped', None)
+ traits = [PrototypeTrait(name=f'{subn} prototype')]
+
+ def clean(s):
+ return s.strip().replace(',',' ').replace('/',' ').strip()
+
+ if not self._nonato:
+ mains = c.get('mains','')
+ m = set([clean(mains)] +
+ [clean(m) for m in mains.split(',')])
+ traits.extend([PrototypeTrait(name=f'{m.strip()} prototype')
+ for m in set(m)])
+ for p in ['echelon','command']:
+ val = c.get(p,None)
+ if val is not None:
+ pv = f'{val.strip()} prototype'
+ traits.append(PrototypeTrait(name=pv))
- if echelon is not None:
- echelons.append(natoapp6c['echelon'])
- i['echelon'] = echelon
-
- if command is not None:
- commands.append(command)
- i['command'] = command
-
- # Friendly message
- # if verbose:
- # print(f'{i["name"]} -> {i["type"]}/{i["sub type"]} {typ}/{sub}')
-
- unittypes = set(unittypes)
- echelons = set(echelons)
- commands = set(commands)
-
- # Now create the `buildFile.xml` file
- build, notes = create_build(info,
- categories,
- unittypes,
- echelons,
- commands,
- vmod,
- title,
- version,
- desc,
- rules,
- tutorial,
- pdfname,
- infoname,
- verbose,
- visible)
- # Now create the `moduledata` file
- data = create_data(title, version, desc,verbose)
-
-
- if patch:
- from importlib.util import spec_from_file_location, \
- module_from_spec
- from pathlib import Path
- from sys import modules
-
- p = Path(patch)
- if verbose:
- pn = str(p.stem)
- print(f'Will patch module with {pn}.patch function')
-
- spec = spec_from_file_location(p.stem, p.absolute())
- module = module_from_spec(spec)
- spec.loader.exec_module(module)
- modules[p.stem] = module
+ if cf is not None:
+ traits.extend([
+ LayerTrait(images = [c['filename'],
+ cf['filename']],
+ newNames = ['','Reduced +'],
+ activateName = '',
+ decreaseName = '',
+ increaseName = 'Flip',
+ increaseKey = self._flipKey,
+ decreaseKey = '',
+ name = 'Step'),
+ ReportTrait(self._flipKey)])
+
+ if not self._nochit:
+ def clean(value):
+ return sub(r'\[[^=]+\]=','',value)\
+ .replace('{','')\
+ .replace('}','')\
+ .replace('/',' ')\
+ .replace(',',' ')\
+ .replace('\\',' ')
+
+ # Add extra marks. This may be useful later on.
+ for field in ['upper left', 'upper right',
+ 'lower left', 'lower right',
+ 'left', 'right',
+ 'factors']:
+ value = c.get('chit',{}).get(field,None)
+ if value is None:
+ continue
+ val = clean(value)
+ val = val\
+ .replace('chit identifier=','')\
+ .replace('chit small identifier=','')
+
+ traits.append(MarkTrait(name = field, value = val))
+
+ if field != 'factors': continue
+
+ af, df, mf, ra = self.getFactors(val)
+ saf, sdf, smf, sra = None,None,None,None
+ if cf is not None:
+ value = cf.get('chit',{}).get(field,None)
+ if value is not None:
+ val = clean(value)
+ val = val\
+ .replace('chit identifier=','')\
+ .replace('chit small identifier=','')
+ saf, sdf, smf, sra = self.getFactors(val)
+
+ rf = []
+ srf = []
+ for f,sf,n in [[af,saf,'CF'],
+ [df,sdf,'DF'],
+ [mf,smf,'MF'],
+ [ra,sra,'Range']]:
+ if f is None: continue
+
+ if sf is None:
+ rf.append(MarkTrait(name=n,value=f))
+ else:
+ rf .append(MarkTrait(name='Full'+n, value=f))
+ srf.append(MarkTrait(name='Reduced'+n,value=sf))
+ traits.append(CalculatedTrait(
+ name = n,
+ expression = (f'{{(Step_Level==2)?'
+ f'Reduced{n}:Full{n}}}')))
+
+ traits.extend(rf+srf)
+
+
- # Patch must accept xml.dom.document,xml.dom.document,ZipFile
- module.patch(build,data,vmod,verbose)
-
- buildstr = build.toprettyxml(indent=' ',
- encoding="UTF-8",
- standalone=False)
- datastr = data.toprettyxml(indent=' ',
- encoding="UTF-8",
- standalone=False)
+
+
+ return height, width, traits
+
+ # ----------------------------------------------------------------
+ def addCounters(self):
+ '''Add all counters (pieces) element to the module.
+ Prototypes are also created as part of this.
+ '''
+ from re import sub
+
+ with VerboseGuard('Adding counters') as v:
+ protos = self._game.addPrototypes()
+
+ self.addNatoPrototypes(protos)
+ self.addBattlePrototypes(protos)
+
+ pieces = self._game.addPieceWindow(name = 'Counters',
+ icon = self.getIcon('unit-icon',
+ '/images/counter.gif'),
+ hotkey = self._countersKey)
+ tabs = pieces.addTabs(entryName='Counters')
+
+ for subn, subc in self._categories.get('counter',{}).items():
+
+ subn = subn.strip()
+ panel = tabs.addPanel(entryName = subn, fixed = False)
+ plist = panel.addList(entryName = f'{subn} counters')
+
+ traits = []
+ if subn in ['BattleMarkers']:
+ traits = self.battleMarkerTraits(list(subc.values())[0])
+ elif subn in ['OddsMarkers']:
+ traits = self.oddsMarkerTraits(list(subc.values())[0])
+ elif subn in ['ResultMarkers']:
+ traits = self.resultMarkerTraits(list(subc.values())[0])
+ elif subn.lower() in ['marker', 'markers']:
+ traits = self.markerTraits()
+ else:
+ traits = self.factionTraits(subn)
+
+ traits.append(BasicTrait())
+
+ p = protos.addPrototype(name = f'{subn} prototype',
+ description = f'Prototype for {subn}',
+ traits = traits)
+ v('')
+
+ with VerboseGuard(f'Adding pieces for "{subn}"') as vv:
+ for i, (cn, c) in enumerate(subc.items()):
+ if cn.endswith('flipped'): continue
+
+ if i == 0: v('',end='')
+ vv(f'[{cn}',end='',flush=True,noindent=True)
+
+ height, width, traits = self.pieceTraits(subn,subc,cn,c)
+ if cn == self._hiddenName:
+ traits = [
+ PrototypeTrait(name=self._battleCtrl),
+ PrototypeTrait(name=self._battleCalc)]
+ if self._diceInit is not None:
+ traits.extend(self._diceInit)
+ traits.append(
+ RestrictAccessTrait(sides=[],
+ description='Fixed'))
+
+
+ #if cn.startswith('odds marker'):
+ # cn = cn.replace(':','_')
+
+ gpid = self._game.nextPieceSlotId()
+ traits.extend([BasicTrait(name = c['name'],
+ filename = c['filename'],
+ gpid = gpid)])
+
+ ps = plist.addPieceSlot(entryName = cn,
+ gpid = gpid,
+ height = height,
+ width = width,
+ traits = traits)
+ if cn == self._hiddenName:
+ self._hidden = ps
+ vv('] ',end='',flush=True,noindent=True)
- vmod.writestr('buildFile.xml',buildstr)
- vmod.writestr('moduledata',datastr)
+ vv('')
+
- return notes
+ # ----------------------------------------------------------------
+ def addNotes(self,**kwargs):
+ '''Add a `Notes` element to the module
-# ====================================================================
-def export(vmodname = 'Draft.vmod',
- pdfname = 'export.pdf',
- infoname = 'export.json',
- title = 'Test',
- version = 'Draft',
- desc = '',
- rules = None,
- tutorial = None,
- patch = None,
- verbose = False,
- visible = False):
- '''Do the whole thing
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ '''
+ self._game.addNotes(**kwargs)
+
+ # ----------------------------------------------------------------
+ def addInventory(self,**kwargs):
+ '''Add a `Inventory` element to module
+
+ Parameters
+ ----------
+ kwargs : dict
+ Dictionary of attribute key-value pairs
+ '''
+ filt = '{' + '||'.join([f'Faction=="{s}"' for s in self._sides])+'}'
+ grp = 'Faction,Command,Echelon,Type'
+ self._game.addInventory(include = filt,
+ groupBy = grp,
+ sortFormat = '$PieceName$',
+ tooltip ='Show inventory of all pieces',
+ zoomOn = True,
+ **kwargs)
+
+ # ----------------------------------------------------------------
+ def addBoard(self,name,info,hasFlipped=False):
+ '''Add a `Board` element to module
+
+ Parameters
+ ----------
+ name : str
+ Name of board
+ info : dict
+ Information on board image
+ hasFlipped : bool
+ True if any piece can be flipped
+ '''
+ with VerboseGuard(f'Adding board {name}') as v:
+ map = self._game.addMap(mapName=name,
+ markUnmovedHotkey=self._clearMoved)
+ map.addCounterDetailViewer(
+ propertyFilter=f'{{{self._battleMark}!=true}}')
+ map.addHidePiecesButton()
+ map.addGlobalMap()
+ # Basics
+ map.addStackMetrics()
+ map.addImageSaver()
+ map.addTextSaver()
+ map.addForwardToChatter()
+ map.addMenuDisplayer()
+ map.addMapCenterer()
+ map.addStackExpander()
+ map.addPieceMover()
+ map.addKeyBufferer()
+ map.addSelectionHighlighters()
+ map.addHighlightLastMoved()
+ map.addZoomer()
+
+ map.addMassKey(name='Eliminate',
+ buttonHotkey = self._eliminateKey,
+ hotkey = self._eliminateKey,
+ icon = self.getIcon('eliminate-icon',
+ '/icons/16x16/edit-undo.png'),
+ tooltip = 'Eliminate selected units')
+ map.addMassKey(name='Delete',
+ buttonHotkey = self._deleteKey,
+ hotkey = self._deleteKey,
+ icon = self.getIcon('delete-icon',
+ '/icons/16x16/no.png'),
+ tooltip = 'Delete selected units')
+ map.addMassKey(name='Rotate CW',
+ buttonHotkey = self._rotateCWKey,
+ hotkey = self._rotateCWKey,
+ icon = '', #/icons/16x16/no.png',
+ tooltip = 'Rotate selected units')
+ map.addMassKey(name='Rotate CCW',
+ buttonHotkey = self._rotateCCWKey,
+ hotkey = self._rotateCCWKey,
+ icon = '', #/icons/16x16/no.png',
+ tooltip = 'Rotate selected units')
+ map.addMassKey(name='Phase clear moved markers',
+ buttonHotkey = self._clearMoved+'Phase',
+ hotkey = self._clearMoved+'Trampoline',
+ canDisable = True,
+ target = '',
+ filter = f'{{{self._battleCtrl}==true}}',
+ propertyGate = f'{self._noClearMoves}',
+ icon = '', #/icons/16x16/no.png',
+ tooltip = 'Phase clear moved markers',
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Phase Clear moved markers "+'
+ f'{self._noClearMoves})'
+ f':""}}'))
+ if hasFlipped:
+ map.addMassKey(name='Flip',
+ buttonHotkey = self._flipKey,
+ hotkey = self._flipKey,
+ icon = self.getIcon('flip-icon',
+ '/images/Undo16.gif'),
+ tooltip = 'Flip selected units')
+
+ if len(self._battleMarks) > 0:
+ v(f'Adding battle mark interface')
+ ctrlSel = f'{{{self._battleCtrl}==true}}'
+ oddsSel = f'{{{self._battleMark}==true}}'
+ calcSel = f'{{{self._battleCalc}==true}}'
+ curSel = (f'{{{self._battleNo}=={self._currentBattle}}}')
+ curAtt = (f'{{{self._battleNo}=={self._currentBattle}&&'
+ f'{self._battleUnit}==true&&'
+ f'IsAttacker==true}}')
+ curDef = (f'{{{self._battleNo}=={self._currentBattle}&&'
+ f'{self._battleUnit}==true&&'
+ f'IsAttacker==false}}')
+ curUnt = (f'{{{self._battleNo}=={self._currentBattle}&&'
+ f'{self._battleUnit}==true}}')
+ markSel = (f'{{{self._battleNo}=={self._currentBattle}&&'
+ f'{self._battleMark}==true}}')
+
+ # ctrlSel = '{BasicName=="wg hidden unit"}'
+ map.addMassKey(name = 'User mark battle',
+ buttonHotkey = self._markKey,
+ buttonText = '',
+ hotkey = self._markBattle,
+ icon = 'battle-marker-icon.png',
+ tooltip = 'Mark battle',
+ target = '',
+ singleMap = False,
+ filter = ctrlSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'User marks battle # "+'
+ f'{self._currentBattle})'
+ f':""}}'))
+ map.addMassKey(name = 'Selected mark battle',
+ buttonHotkey = self._markBattle,
+ hotkey = self._markBattle,
+ icon = '',
+ tooltip = '',
+ singleMap = False,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Mark battle # "+'
+ f'{self._currentBattle})'
+ f':""}}'))
+ map.addMassKey(name = 'Clear current battle',
+ buttonText = '',
+ buttonHotkey = self._clearBattle,
+ hotkey = self._clearBattle,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ filter = curSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Clear battle # "+'
+ f'{self._currentBattle})'
+ f':""}}'))
+ map.addMassKey(name = 'Clear selected battle',
+ buttonText = '',
+ buttonHotkey = self._clearKey,
+ hotkey = self._clearKey,
+ icon = '',
+ tooltip = '',
+ singleMap = False,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Clear battle # "+'
+ f'{self._currentBattle})'
+ f':""}}'))
+ map.addMassKey(name = 'Clear all battles',
+ buttonText = '',
+ buttonHotkey = self._clearAllBattle,
+ hotkey = self._clearBattle,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Clear all battle markers")'
+ f':""}}'))
+ map.addMassKey(name = 'User clear all battles',
+ buttonText = '',
+ buttonHotkey = self._clearAllKey,
+ hotkey = self._clearAllBattle,
+ icon = 'clear-battles-icon.png',
+ tooltip = 'Clear all battles',
+ target = '',
+ singleMap = False,
+ filter = ctrlSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'User clears battle markers")'
+ f':""}}'))
+ map.addMassKey(name = 'Phase clear all battles',
+ buttonText = '',
+ buttonHotkey = self._clearBattlePhs,
+ hotkey = self._clearAllBattle,
+ icon = '',
+ tooltip = 'Clear all battles',
+ canDisable = True,
+ propertyGate = f'{self._noClearBattles}',
+ target = '',
+ singleMap = False,
+ filter = ctrlSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Phase clears battle markers "+'
+ f'{self._noClearBattles})'
+ f':""}}'))
+ map.addMassKey(name = 'Selected resolve battle',
+ buttonHotkey = self._resolveKey,
+ hotkey = self._resolveKey,
+ icon = 'resolve-battles-icon.png',
+ tooltip = 'Resolve battle',
+ singleMap = False,
+ filter = oddsSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Resolve battle # "+'
+ f'{self._currentBattle})'
+ f':""}}'))
+ map.addMassKey(name = 'Sum AFs',
+ buttonText = '',
+ buttonHotkey = self._calcBattleAF,
+ hotkey = self._calcBattleAF,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ filter = curAtt,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Calculate total AF"):""}}'))
+ map.addMassKey(name = 'Sum DFs',
+ buttonText = '',
+ buttonHotkey = self._calcBattleDF,
+ hotkey = self._calcBattleDF,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ filter = curDef,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Calculate total DF"):""}}'))
+ map.addMassKey(name = 'Sum odds shifts',
+ buttonText = '',
+ buttonHotkey = self._calcBattleShft,
+ hotkey = self._calcBattleShft,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ filter = curUnt,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Calculate odds shift"):""}}'))
+ map.addMassKey(name = 'Calc battle odds',
+ buttonText = '',
+ buttonHotkey = self._calcBattleOdds,
+ hotkey = self._calcBattleOdds,
+ icon = '',
+ tooltip = '',
+ target = '',
+ singleMap = False,
+ filter = calcSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Calculate odds"):""}}'))
+ map.addMassKey(name = 'Auto calc battle odds',
+ buttonText = '',
+ buttonHotkey = self._calcBattleOdds+'Auto',
+ hotkey = self._calcBattleOdds+'Start',
+ icon = '',
+ tooltip = '',
+ # target = '',
+ singleMap = False,
+ filter = markSel,
+ reportFormat = (f'{{{self._debug}?'
+ f'("~ {name}: '
+ f'Auto calculate odds"):""}}'))
+
+
+ ulx,uly,lrx,lry = self.getBB(info['img'])
+ width = abs(ulx - lrx)
+ height = abs(uly - lry)
+ width, height = self.getWH(info['img'])
+ height += 20
+ width += 5
+
+ picker = map.addBoardPicker()
+ board = picker.addBoard(name = name,
+ image = info['filename'],
+ width = width,
+ height = height)
+ zoned = board.addZonedGrid()
+ zoned.addHighlighter()
+
+ if not 'zones' in info:
+ full = zoned.addZone(name = full,
+ useParentGrid = False,
+ path=(f'{ulx},{uly};' +
+ f'{lrx},{uly};' +
+ f'{lrx},{lry};' +
+ f'{ulx},{lry}'))
+ grid = zone.addHexGrid(color = color,
+ dx = HEX_WIDTH,
+ dy = HEX_HEIGHT,
+ visible = self._visible)
+ grid.addNumbering(color = color,
+ hType = 'A',
+ hOff = -1,
+ vType = 'N',
+ vOff = -1,
+ visible = self._visible)
+ return
+
+ w = abs(ulx-lrx)
+ h = abs(uly-lry)
+ self.addZones(zoned,name,info['zones'],w,h)
+
+ if self._hidden is not None:
+ v(f'Adding hidden unit to map {name}')
+ at = map.addAtStart(name = self._hiddenName,
+ location = '',
+ useGridLocation = False,
+ owningBoard = name,
+ x = 0,
+ y = 0)
+ at.addPieces(self._hidden)
+
- - Read in information form the 'infoname' file (a CSV file
- generated by LaTeX)
+ # ----------------------------------------------------------------
+ def addDeadMap(self):
+ '''Add a "Dead Map" element to the module
+ '''
+ name = 'DeadMap'
+ with VerboseGuard(f'Adding board {name}') as v:
+ map = self._game.addMap(mapName = name,
+ buttonName = '',
+ markMoved = 'Never',
+ launch = True,
+ icon = self.getIcon('pool-icon',
+ '/images/playerAway.gif'),
+ allowMultiple = True,
+ hotkey = self._deadKey)
+ # Basics
+ map.addStackMetrics()
+ map.addImageSaver()
+ map.addTextSaver()
+ map.addForwardToChatter()
+ map.addMenuDisplayer()
+ map.addMapCenterer()
+ map.addStackExpander()
+ map.addPieceMover()
+ map.addKeyBufferer()
+ map.addSelectionHighlighters()
+ map.addHighlightLastMoved()
+ map.addZoomer()
+
+ map.addMassKey(name='Restore',
+ buttonHotkey = self._restoreKey,
+ hotkey = self._restoreKey,
+ icon = self.getIcon('restore-icon',
+ '/images/Undo16.gif'),
+ tooltip = 'Restore selected units')
+
+ picker = map.addBoardPicker()
+ picker.addSetup(maxColumns=len(self._sides),mapName=name,
+ boardNames=[s+' pool' for s in self._sides])
+
+ for i, s in enumerate(self._sides):
+ v(f'Adding {s} pool')
+ color = [0,0,0,64]
+ color[i % 3] = 255
+ w = 400
+ h = 400
+ c = rgba(*color)
+ img = ''
+ dimg = self._categories.get('pool',{}).get('all',{})\
+ .get(s,None)
+
+ if dimg:
+ bb = self.getBB(dimg['img'])
+ w = bb[2] - bb[0]
+ h = bb[3] - bb[1]
+ c = ''
+ img = dimg['filename']
+ v(f'Using image provided by user {img}')
+
+ board = picker.addBoard(name = f'{s} pool',
+ image = img,
+ width = w,
+ height = h,
+ color = c)
+
+ if dimg is None or not 'zones' in dimg:
+ continue
+
+ zoned = board.addZonedGrid()
+ zoned.addHighlighter()
+ w = abs(w)
+ h = abs(h)
+ self.addZones(zoned,board['name'],dimg['zones'],w,h)
+
+
+ # --------------------------------------------------------------------
+ def getPictureInfo(self,picture,name,width,height):
+ '''
+ Returns
+ -------
+ hex_width, hex_height : float, float
+ Scale hex width
+ scx, scy : float, float, float, float
+ Scale to image and picture (x,y)
+ rot90 : bool
+ True if rotated +/-90 degrees
+ tran : callable
+ Translation function
+ '''
+ if picture is None:
+ print(f'WARNING: No Tikz picture information.'
+ f"Are you sure you used the `[zoned]' option for the "
+ f"tikzpicture environment of {name}?")
+ f = lambda x,y: (x,y)
+ return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
+
+ # Get picture bounding box
+ tll = picture['lower left']
+ tur = picture['upper right']
+ # Get picture transformation
+ pa = picture['xx']
+ pb = picture['xy']
+ pc = picture['yx']
+ pd = picture['yy']
+ # Get picture offset (always 0,0?)
+ pdx = picture['dx']
+ pdy = picture['dy']
+ # Define picture global transformation
+ pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
+ # Globally transform (rotate) picture bounding box
+ pll = pr(*tll)
+ pur = pr(*tur)
+ # Calculate widht, height, and scaling factors
+ pw = pur[0] - pll[0]
+ ph = pur[1] - pll[1]
+ scw = width / pw
+ sch = height / ph
+ # Extract picture scales and rotation
+ # Courtesy of
+ # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
+ from math import sqrt, atan2, degrees, isclose
+ psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
+ psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
+ prt = degrees(atan2(pc,pd))
+ if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
+ raise RuntimeException('Rotations of Tikz pictures other than '
+ '0 or +/-90,+/- 180, or +/-270 not supported. '
+ 'found {prt}')
+ rot90 = int(prt // 90)
+ if rot90 == 2: rot90 = -2
+ # Now supported
+ # if any([isclose(prt,a) for a in [90,270,180,-180]]):
+ # print(f'WARNING: rotations by {prt} not fully supported')
+
+ from math import sqrt
+ hex_width = psx * scw * 2 # HEX_WIDTH
+ hex_height = psy * sch * sqrt(3) # HEX_HEIGHT
+ with VerboseGuard('Picture') as v:
+ v(f'Transformations: {pa},{pb},{pc},{pd}')
+ v(f'Scale (x,y): {psx},{psy}')
+ v(f'Rotation (degrees): {prt} ({rot90})')
+ v(f'Scale to pixels (x,y): {scw},{sch}')
+
+ # When translating the Tikz coordinates, it is important to note
+ # that the Tikz y-axis point upwards, while the picture y-axis
+ # point downwards. This means that the upper right corner is at
+ # (width,0) and the lower left corner is at (0,height).
+ def tranx(x,off=-pll[0]):
+ # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
+ return int(scw * (x + off)+.5)
+ def trany(y,off=-pur[1]):
+ # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
+ return -int(sch * (y + off)+.5)
+ tran = lambda x,y : (tranx(x), trany(y))
+
+ return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
+
+ # --------------------------------------------------------------------
+ def getHexParams(self,
+ llx,
+ lly,
+ urx,
+ ury,
+ mx,
+ my,
+ hex_width,
+ hex_height,
+ rot90,
+ labels,
+ coords,
+ targs,
+ nargs):
+ '''rot90 = 0 No rotation
+ = 1 Rotated -90 (clock-wise)
+ = -1 Rotated 90 (counter clock-wise)
+ = -2 Rotated 180
+ '''
+ with VerboseGuard('Hex parameters') as v:
+ from math import sqrt
+ isodd = lambda x : (x % 2 == 1)
+ iseven = lambda x : (x % 2 == 0)
+ isfalse = lambda x : False
+ shorts = {'isodd': isodd, 'iseven': iseven, 'isfalse': isfalse }
+
+ # Funny scaling needed by VASSAL. Seems like they only
+ # really about the absolute value of 'dy' and then the
+ # aspect ratio between dx and dy.
+ pxfac = sqrt(3)/2
+ hex_pw = hex_height * pxfac
+ hex_ph = hex_width * pxfac
+ stagger = False
+ #
+ # Get parameters from coordinates. These should always be set
+ #
+ rows = coords .get('row', {})
+ columns = coords .get('column',{})
+ top_short = columns .get('top short', 'isfalse')
+ bot_short = columns .get('bottom short','isfalse')
+ inv_col = columns .get('factor',1)
+ inv_row = rows .get('factor',1)
+ voff = -rows .get('offset',0) # 0: from 0 -> -1
+ hoff = -columns.get('offset',0) # -1: from 1 -> -2
+ vdesc = inv_row == 1
+ hdesc = inv_col == -1
+ #
+ # Calculate total dimensions, and number of columns and rows
+ #
+ w = abs((urx-llx) - 2 * mx)
+ h = abs((ury-lly) - 2 * my)
+ if abs(rot90) == 1: h, w = w, h
+ nc = int(w // (hex_width * 3 / 4))
+ nr = int(h // (hex_height))
+ namrot = {0: 'none - 0',
+ -1: '-90 - CCW',
+ 1: '90 CW',
+ -2: '180 - half-turn'}
+
+ v(f'Width: {w}')
+ v(f'Height: {h}')
+ v(f'Margins: x={mx} y={my}')
+ v(f'Rotation: {rot90} ({namrot[rot90]})')
+ v(f'Labels: {labels}')
+ v(f'Columns:')
+ v(f' size: {nc}')
+ v(f' start: {hoff}')
+ v(f' direction: {inv_col}')
+ v(f' top short: {top_short}')
+ v(f' bottom short: {bot_short}')
+ v(f'Rows:')
+ v(f' size: {nr}')
+ v(f' start: {voff}')
+ v(f' direction: {inv_row}')
+ v(f'Image:')
+ v(f' BB: ({llx},{lly}) x ({urx},{ury})')
+ #
+ # X0 and Y0 are in the local (rotated) frame of the hex grid.
+ # Thus X is always along hex breadth, and Y along the
+ # height. Thus the base offset (rotated into the hex frame) differs.
+ x0 = ury if abs(rot90) == 1 else llx
+ y0 = llx if abs(rot90) == 1 else ury
+ # Calculate column,row of corners
+ llc = hoff
+ ulc = hoff
+ lrc = hoff+nc-1
+ urc = hoff+nc-1
+ #
+ # Swap in directions
+ if hdesc: llc, lrc, ulc, urc = lrc, llc, urc, ulc
+ #
+ is_short_top = shorts[columns.get('top short', 'isfalse')]
+ is_short_bot = shorts[columns.get('bottom short','isfalse')]
+ if is_short_top is isfalse:
+ # Assume fully populated columns
+ is_short_top = isodd if iseven(hoff) else iseven
+ if is_short_bot is isfalse:
+ is_short_bot = isodd if isodd(hoff) else iseven
+
+ #
+ # Now we have the hex coordinates of the corners. We can
+ # now check how things are offset. Before rotation, we
+ # will have that the first column is offset by hex_pw / 2.
+ x0 += hex_width / 2
+ #
+ # If the first column is _not_ short on top, then off set
+ # is simply hex_ph / 2. Otherwise, the offset is hex_ph
+ y0 += hex_ph / 2
+ voff -= 1
+ voff -= inv_row
+ v(f' Initial offset of image {x0},{y0}')
+
+ # Treat each kind of rotation separately. Note that -90 and
+ # 180 uses the `is_short_bot' while 0 and 90 uses
+ # `is_short_top'. There might be a way to unify these, if
+ # offsets and so on may warrent it, but it may be complete
+ # overkill.
+ is_off = False
+ col_map = {0 : (ulc, is_short_top, is_short_bot),
+ -1 : (urc, is_short_top, is_short_bot),
+ 1 : (ulc, is_short_bot, is_short_top),
+ -2 : (urc, is_short_bot, is_short_top) }
+ col_chk, is_s1, is_s2 = col_map[rot90]
+
+ is_off = is_s1(col_chk)
+ if is_off:
+ y0 += hex_ph /2
+
+ v(f'Is first column off: {is_off}')
+
+ # For full columns, noting more is needed
+ #
+ # Below is if some columns are short both top and bottom.
+ # VASSAL seems to start numbering from a given place, and
+ # then use that for the rest numbering, and forgets to
+ # take into account various offsets and the like. hence,
+ # we need to hack it hard.
+ if iseven(nc):
+ v(f'Even number of columns, perhaps hacks')
+ if rot90 == 0:
+ # Hacks
+ #
+ # If the last column is short in both top and bottom,
+ # and we have inverse columns, but not inverse rows,
+ # then add to offset
+ if inv_col == -1 and inv_row == 1 and \
+ is_s1(urc) and is_s2(urc):
+ voff += 1
+ # If the column we check for short is short both top
+ # and bottom, and we have inverse rows, but not
+ # inverse columns, then add offset
+ if inv_row == -1 and inv_col == 1 and \
+ is_s2(col_chk) and is_off:
+ voff += 1
+
+ if rot90 == -1:
+ # If the last column is short in both top and bottom,
+ # and we have inverse columns, then add to offset
+ if is_s1(urc) and inv_col == -1 and is_s2(urc):
+ voff -= inv_row
+
+ if rot90 == 1:
+ voff += inv_row + (inv_row == 1)
+ # If the first column is short in both top and bottom,
+ # and we have inverse columns, then add to offset
+ if is_s1(ulc) and is_s2(ulc) and inv_col == -1:
+ voff += inv_row
+
+ if rot90 == -2:
+ voff += inv_row * 2
+ # Hacks If the column we check for short is short both
+ # top and bottom, and we have either inverse rows and
+ # inverse columns, or rows and columns are normal,
+ # then add offset
+ if inv_col == inv_row and is_s1(col_chk) and is_s2(col_chk):
+ voff += 1
+ # If the first column is short in both top and bottom,
+ # and we have inverse columns and rows, then add to
+ # offset
+ if inv_col == inv_row and inv_col == -1 and \
+ is_s1(ulc) and is_s2(ulc):
+ voff += 1
+ else:
+ v(f'Odd number of columns')
+ voff -= inv_row
+ if rot90 == 1:
+ # If we offset in the column direction, add the
+ # inverse row direction, and if we have inverse rows,
+ # substract one, otherwise add 2.
+ voff += (inv_row * hoff + (-1 if inv_row == -1 else 2))
+ # If we have a short column, and that column is even,
+ # then add, otherwise subtract, the inverse row
+ # direction, if the checked column is even.
+ voff += ((1 if is_off else -1) *
+ inv_row if is_short_bot(2) else 0)
+ if rot90 == 2:
+ voff += inv_row * (2 + is_off) # OK for odd
+
+
+ if rot90 == 0:
+ if inv_col == -1 and iseven(nc): # OK
+ stagger = not stagger
+ hoff -= (inv_col == -1) # OK
+
+ if rot90 == -1: # CCW
+ if inv_col == 1 and iseven(nc): # OK
+ stagger = not stagger
+ vdesc, hdesc = hdesc, vdesc
+ vdesc = not vdesc
+ voff += (inv_row == 1)
+ hoff -= (inv_col == 1) # OK
+
+ if rot90 == 1: # CW
+ if (inv_col == 1 and iseven(nc)) or isodd(nc): # OK
+ stagger = not stagger
+ vdesc, hdesc = hdesc, vdesc
+ hdesc = not hdesc
+ hoff -= (inv_col == -1) # OK
+
+ if rot90 == -2:
+ if (inv_col == -1 and iseven(nc)) or isodd(nc): # OK
+ stagger = not stagger
+ vdesc, hdesc = not vdesc, not hdesc
+ hoff -= (inv_col == 1) # OK
+
+ # Labels
+ if labels is not None:
+ labmap = {
+ 'auto': {
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+ 'auto=numbers' : {
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+ 'auto=alpha column': {
+ 'hLeading': 0,'vLeading': 0,'hType': 'A','vType': 'N' },
+ 'auto=alpha 2 column': {# Not supported
+ 'hLeading': 1,'vLeading': 1,'hType': 'A','vType': 'N' },
+ 'auto=inv y x plus 1': {
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' },
+ 'auto=x and y plus 1': {
+ 'hLeading': 1,'vLeading': 1,'hType': 'N','vType': 'N' }
+ }
+ for l in labels.split(','):
+ nargs.update(labmap.get(l,{}))
+ if 'alpha column' in l or 'alpha 2 column' in l:
+ hoff -= 1 # VASSAL 0->A, wargame 1->A
+ if l == 'auto=inv y x plus 1':
+ hoff += 1
+ #inv_row = not inv_row
+ if l == 'auto=x and y plus 1':
+ hoff -= 1
+ voff -= 1
+
+ # Add margins
+ x0 += int(mx)
+ y0 += int(my)
+
+ targs['dx'] = hex_pw
+ targs['dy'] = hex_ph
+ nargs['vOff'] = voff
+ nargs['hOff'] = hoff
+ nargs['vDescend'] = vdesc
+ nargs['hDescend'] = hdesc
+ targs['edgesLegal'] = True
+ targs['sideways'] = abs(rot90) == 1
+ nargs['stagger'] = stagger
+ targs['x0'] = int(x0+.5)
+ targs['y0'] = int(y0+.5)
+
+ # --------------------------------------------------------------------
+ def getRectParams(self,i,llx,ury,width,height,targs,nargs):
+ targs['dx'] = width
+ targs['dy'] = height
+ targs['x0'] = int(llx - width/2)
+ targs['y0'] = int(ury + height/2)
+ targs['color'] = rgb(0,255,0)
+ nargs['color'] = rgb(0,255,0)
+ nargs['vDescend'] = True
+ nargs['vOff'] = -3
+ nargs.update({'sep':',','vLeading':0,'hLeading':0})
+
+ # ----------------------------------------------------------------
+ def addZones(self,
+ zoned,
+ name,
+ info,
+ width,
+ height,
+ labels=None,
+ coords=None,
+ picinfo=None):
+ '''Add zones to the Zoned element.
+
+ Parameters
+ ----------
+ zoned : Zoned
+ Parent element
+ name : str
+ Name of Zoned
+ info : dict
+ Dictionary of zones informatio
+ width : int
+ Width of parent
+ height : int
+ Height of parent
+ labels : list
+ On recursive call, list of labels
+ coords : list
+ On recursive call, coordinates
+ picinfo : dict
+ On recursive call, picture information
+ '''
+ grids = []
+ picture = None
+
+ with VerboseGuard(f'Adding zones to {name}') as v:
+ for k, val in info.items():
+ if k == 'labels': labels = val;
+ if k == 'coords': coords = val
+ if k == 'zoned': picture = val
+ if 'zone' not in k or k == 'zoned':
+ continue
+
+ grids = [[k,val]] + grids # Reverse order!
+ # grids.append([k,v])
+
+ if len(grids) < 1:
+ return
+
+ if picinfo is None:
+ picinfo = self.getPictureInfo(picture,name,width,height)
+
+ hex_width, hex_height, scx, scy, rot90, tran = picinfo
+
+ for g in grids:
+ n, i = g
+ v(f'Adding zone {n}')
+
+ if 'scope' in n:
+ llx,lly = tran(*i['global lower left'])
+ urx,ury = tran(*i['global upper right'])
+ path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
+ nm = n.replace('zone scope ','')
+ elif 'path' in n:
+ path = [tran(*p) for p in i['path']]
+ llx = min([px for px,py in path])
+ ury = max([py for px,py in path])
+ nm = n.replace('zone path ','')
+
+ # Checkf if we have "point" type elements in this object and
+ # add them to dict.
+ points = [ val for k,val in i.items()
+ if (k.startswith('point') and
+ isinstance(val,dict) and \
+ val.get('type','') == 'point')]
+
+ pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
+ v(f'Zone path ({llx},{ury}): {pathstr} ({len(points)})')
+
+ ispool = 'pool' in n.lower() and len(points) <= 0
+ zone = zoned.addZone(name = nm,
+ locationFormat = ("$name$"
+ if ispool else
+ "$gridLocation$"),
+ useParentGrid = False,
+ path = pathstr)
+
+ # Do not add grids to pools
+ if ispool:
+ v('Board {n} is pool with no points')
+ continue
+
+ targs = {'color':rgb(255,0,0),'visible':self._visible}
+ nargs = {'color':rgb(255,0,0),'visible':self._visible}
+ # print(targs,nargs)
+ if 'turn' in n.lower(): nargs['sep'] = 'T'
+ if 'oob' in n.lower(): nargs['sep'] = 'O'
+
+ if len(points) > 0:
+ with VerboseGuard('Using region grid') as vv:
+ grid = zone.addRegionGrid(snapto = True,
+ visible = self._visible)
+ for j,p in enumerate(points):
+ pn = p["name"].strip()
+ pp = p.get('parent','').strip()
+ pc = p["coords"]
+ if j == 0: vv(f'',end='')
+ vv(f'[{pn}] ',end='',flush=True,noindent=True)
+
+ x, y = tran(*pc)
+ r = grid.addRegion(name = pn,
+ originx = x,
+ originy = y,
+ alsoPiece = True,
+ prefix = pp)
+ v('')
+
+ elif 'hex' in n.lower():
+ margin = i.get('board frame',{}).get('margin',0)
+ mx = scx * margin
+ my = scy * margin
+ # self.message(f'{margin} -> {scx},{scy} -> {mx},{my}')
+ w = abs(urx - llx)-2*mx
+ h = abs(ury - lly)-2*my
+ self.getHexParams(llx = llx,
+ lly = lly,
+ urx = urx,
+ ury = ury,
+ mx = mx,
+ my = my,
+ hex_width = hex_width,
+ hex_height = hex_height,
+ rot90 = rot90,
+ labels = labels,
+ coords = coords,
+ targs = targs,
+ nargs = nargs)
+
+ v(f'Adding hex grid')
+
+ grid = zone.addHexGrid(**targs)
+ grid.addNumbering(**nargs)
+
+ else:
+ width = hex_width / HEX_WIDTH * RECT_WIDTH
+ height = hex_height / HEX_HEIGHT * RECT_HEIGHT
+ self.getRectParams(i,llx,ury,width,height,targs,nargs)
+
+ v(f'Adding rectangular grid')
+
+ grid = zone.addSquareGrid(**targs)
+ grid.addNumbering(**nargs)
+
+
+ # Once we've dealt with this grid, we should see if we have
+ # any embedded zones we should deal with.
+ self.addZones(zoned,name,i,width,height,
+ labels=labels,
+ coords=coords,
+ picinfo=picinfo)
+
+
+ # ----------------------------------------------------------------
+ def addBoards(self):
+ '''Add Boards to the module
+ '''
+ with VerboseGuard('Adding boards') as v:
+ hasFlipped = False
+ for cn,cd in self._categories.get('counter',{}).items():
+ for sn in cd:
+ if ' flipped' in sn:
+ hasFlipped = True
+ break
+
+ v(f'Has flipped? {hasFlipped}')
+ for bn, b in self._categories.get('board',{}).get('all',{}).items():
+ self.addBoard(bn, b,hasFlipped=hasFlipped)
+
+
+ # ----------------------------------------------------------------
+ def getIcon(self,name,otherwise):
+ with VerboseGuard(f'Get Icon {name}') as v:
+ icon = self._categories\
+ .get('icon',{})\
+ .get('all',{})\
+ .get(name,{
+ 'filename':otherwise})['filename']
+ v(f'Using "{icon}"')
+ return icon
+
+ # ----------------------------------------------------------------
+ def addOOBs(self):
+ '''Add OOBs to the game'''
+ oobc = self._categories.get('oob',{}).get('all',{}).items()
+ if len(oobc) < 1:
+ return
+
+ with VerboseGuard(f'Adding OOBs') as v:
+ icon = self.getIcon('oob-icon','/images/inventory.gif')
+ v(f'Using icon "{icon}" for OOB')
+ charts = \
+ self._game.addChartWindow(name='OOBs',
+ hotkey = self._oobKey,
+ description = 'OOBs',
+ text = '',
+ icon = icon,
+ tooltip = 'Show/hide OOBs')
+ tabs = charts.addTabs(entryName='OOBs')
+
+ for on, o in oobc:
+ widget = tabs.addMapWidget(entryName=on)
+ self.addOOB(widget, on, o)
+
+
+ # ----------------------------------------------------------------
+ def addOOB(self,widget,name,info):
+ '''Add a OOB elements to the game
+
+ Parameters
+ ----------
+ widget : Widget
+ Widget to add to
+ name : str
+ Name
+ info : dict
+ Information on the OOB image
+ '''
+ map = widget.addWidgetMap(mapName = name,
+ markMoved = 'Never',
+ hotkey = '')
+ map.addCounterDetailViewer()
+ map.addStackMetrics()
+ map.addImageSaver()
+ map.addTextSaver()
+ map.addForwardToChatter()
+ map.addMenuDisplayer()
+ map.addMapCenterer()
+ map.addStackExpander()
+ map.addPieceMover()
+ map.addKeyBufferer()
+ map.addSelectionHighlighters()
+ map.addHighlightLastMoved()
+ map.addZoomer()
+
+ picker = map.addPicker()
+ ulx,uly,lrx,lry = self.getBB(info['img'])
+ board = picker.addBoard(name = name,
+ image = info['filename'])
+ zoned = board.addZonedGrid()
+ zoned.addHighlighter()
+
+ if not 'zones' in info:
+ zone = zoned.addZone(name = 'full',
+ useParentGrid = False,
+ path=(f'{ulx},{uly};' +
+ f'{lrx},{uly};' +
+ f'{lrx},{lry};' +
+ f'{ulx},{lry}'))
+ grid = zone.addSquareGrid()
+ grid.addNumbering()
+
+ return
+
+ # If we get here, we have board info!
+ w = abs(ulx-lrx)
+ h = abs(uly-lry)
+ self.addZones(zoned,name,info['zones'],w,h)
+
+ # ----------------------------------------------------------------
+ def addCharts(self):
+ '''Add Charts elements to game
+ '''
+ chartc = self._categories.get('chart',{}).get('all',{}).items()
+ if len(chartc) < 1:
+ return
+
+ with VerboseGuard('Adding charts') as v:
+ charts = self._game.addChartWindow(name = 'Charts',
+ hotkey = self._chartsKey,
+ description = '',
+ text = '',
+ tooltip = 'Show/hide charts',
+ icon = self.getIcon('chart-icon',
+ '/images/chart.gif'))
+ tabs = charts.addTabs(entryName='Charts')
+ for i, (cn, c) in enumerate(chartc):
+ if i == 0: v('',end='')
+ v(f'[{cn}] ',end='',flush=True,noindent=True)
+
+ tabs.addChart(chartName = cn,
+ description = cn,
+ fileName = c['filename'])
+
+ v('')
- - Convert each page in the 'pdfname' file (a PDF generated by
- LaTex) into images
+ # ----------------------------------------------------------------
+ def addDie(self):
+ '''Add a `Die` element to the module
+ '''
+ if self._dice is not None:
+ return
+ self._game.addDiceButton(name = '1d6',
+ hotkey = self._diceKey)
- - Create the VASSAL module
+#
+# EOF
+#
+# ====================================================================
+# From main.py
- '''
- if verbose:
- print(f'Module name : {vmodname}\n'
- f'PDF name : {pdfname}\n'
- f'Info name : {infoname}\n'
- f'Title : {title}\n'
- f'Version : {version}\n'
- f'Description : {desc}\n'
- f'Rules : {rules}\n'
- f'Tutorial : {tutorial}')
- info = convert_pages(pdfname=pdfname,infoname=infoname)
- return create_vmod(vmodname = vmodname,
- info = info,
- title = title,
- version = version,
- desc = desc,
- rules = rules,
- tutorial = tutorial,
- pdfname = pdfname,
- infoname = infoname,
- patch = patch,
- verbose = verbose,
- visible = visible)
-
# ====================================================================
if __name__ == '__main__':
from argparse import ArgumentParser, FileType
@@ -3695,15 +11549,25 @@ if __name__ == '__main__':
help='Short description of module',
type=str,
default='draft of module')
+ ap.add_argument('-W','--vassal-version',
+ help='Vassal version number',
+ type=str,
+ default='3.6.7')
ap.add_argument('-V','--verbose',
help='Be verbose',
action='store_true')
ap.add_argument('-G','--visible-grids',
action='store_true',
help='Make grids visible in the module')
- ap.add_argument('-n','--notes',
- help='Show notes',
- action='store_true')
+ ap.add_argument('-N','--no-nato-prototypes',
+ action='store_true',
+ help='Do not make prototypes for types,echelons,commands')
+ ap.add_argument('-C','--no-chit-information',
+ action='store_true',
+ help='Do not make properties from chit information')
+ ap.add_argument('-S','--counter-scale',
+ type=float, default=1,
+ help='Scale counters by factor')
args = ap.parse_args()
@@ -3720,27 +11584,27 @@ if __name__ == '__main__':
if args.version.lower() == 'draft':
args.visible_grids = True
+ Verbose().setVerbose(args.verbose)
+
try:
- notes = export(vmodname = vmodname,
- pdfname = args.pdffile.name,
- infoname = args.infofile.name,
- title = args.title,
- version = args.version,
- desc = args.description,
- rules = rulesname,
- tutorial = tutname,
- patch = patchname,
- verbose = args.verbose,
- visible = args.visible_grids)
-
- if args.notes:
- print(f'Module {vmodname} created. '
- 'Here are the notes from the module')
- print(notes)
-
+ exporter = LaTeXExporter(vmodname = vmodname,
+ pdfname = args.pdffile.name,
+ infoname = args.infofile.name,
+ title = args.title,
+ version = args.version,
+ description = args.description,
+ rules = rulesname,
+ tutorial = tutname,
+ patch = patchname,
+ visible = args.visible_grids,
+ vassalVersion = args.vassal_version,
+ nonato = args.no_nato_prototypes,
+ nochit = args.no_chit_information,
+ counterScale = args.counter_scale)
+ exporter.run()
except Exception as e:
from sys import stderr
- print(f'Failed to build {vmodname}: e',file=stderr)
+ print(f'Failed to build {vmodname}: {e}',file=stderr)
from os import unlink
try:
unlink(vmodname)
@@ -3748,3 +11612,10 @@ if __name__ == '__main__':
pass
raise e
+
+#
+# EOF
+#
+##
+# End of generated script
+##
diff --git a/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py b/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py
index a9f3c60203..0800e18130 100755
--- a/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py
+++ b/macros/latex/contrib/wargame/source/utils/wgsvg2tikz.py
@@ -204,7 +204,7 @@ def path_to_tikz(path,f='8.5f'):
'''
from svg.path import Line, Move, Close, CubicBezier, Path
- code = []
+ code = [f'% ']
for e in path:
if isinstance(e, Move): code.append(tikz_move(e, f))
elif isinstance(e, Line): code.append(tikz_line(e, f))
diff --git a/macros/latex/contrib/wargame/source/wargame.dtx b/macros/latex/contrib/wargame/source/wargame.dtx
index d721befdb6..e8a3b9b1e1 100644
--- a/macros/latex/contrib/wargame/source/wargame.dtx
+++ b/macros/latex/contrib/wargame/source/wargame.dtx
@@ -44,6 +44,11 @@
\date{\today}
\begin{document}
\maketitle
+\begin{center}
+ \begin{tikzpicture}
+ \wargamelogo
+ \end{tikzpicture}
+\end{center}
\begin{abstract}
This package provides tools to typesetting manuals, board, and
counters for wargames using \LaTeX{}.
@@ -92,6 +97,10 @@
% typeset using a similar interface. Unit types are identified using
% the NATO Joint Military Symbology \cite{app6c} standard.
%
+% This document is meant as a reference manual (although far from
+% complete). A separate tutorial is available, and may be the best
+% starting point.
+%
% \iffalse
% --------------------------------------------------------------------
%
@@ -127,17 +136,26 @@
% \item[\keyval{town}{\meta{town-keys}}] specifies that a town (or
% similar) is present in the hex. The various keys are described in
% \sectionname~\ref{sec:hex:town}.
+% \item[\keyval{bevel}{\meta{bevel-keys}}] specifies that a bevel
+% should be added to the hex. The various keys are described in
+% \sectionname~\ref{sec:hex:bevel}.
% \item[\keyval{extra}{\meta{extra-keyx}} \textrm{and} \keyval{extra
% clipped}{\meta{extra-keyx}}] allows the user to put custom
% graphics in the hexes. See also \sectionname~\ref{sec:hex:extra}
% for more.
+% \item[\keyval{row}{\meta{row}} \textrm{and}
+% \keyval{column}{\meta{column}}] Keys to set hex coordinates.
+% Mainly used when using \cs{node} rather than \cs{hex}. These
+% coordinates should be specified in the \spec{hex cs} coordinate
+% system (\sectionname~\ref{sec:hex:cs}).
% \item any style key defined for \TikZ{} pictures.
% \end{description}
%
% The \meta{location} argument specifies the coordinates, in the hex
% coordinate system where to put the hex. More about the coordinate
% system is given in \sectionname~\ref{sec:hex:cs}. Note, the numbers
-% starts from the lower--left corner.
+% by default starts from the lower--left corner, but can be changed
+% via options.
%
% The elements are rendered in the following order
% \begin{enumerate}
@@ -146,6 +164,7 @@
% \item The ridges, if any
% \item The label, if any
% \item Extra graphics clipped to the hex
+% \item Bevel if selected
% \item Town, if any
% \item Extra graphics which may extend beyond the confines of the
% hex.
@@ -180,7 +199,46 @@
% indicate two units of length.}
% \label{fig:hex:parts}
% \end{figure}
+%
+% Note that the macro \cs{hex} is really a short hand for \TikZ{}'s
+% \cs{node} macro, but with preset options. An alternative to using
+% the \cs{hex} macro is to do
%
+% \begin{MacroSyntax}{\hex}
+% \cs{node}[hex=\marg{key-value-pairs}] \parg{name} at \parg{location};
+% \end{MacroSyntax}
+%
+% This can be useful when placing explanatory graphics or the like.
+% The main difference between using \cs{hex} and the raw
+% \cs{node}\texttt{[hex=\textellipsis]} is that the former can
+% automatically generate labels and set shape coordinates in the
+% picture. If you want that for your board, it is recommended to use
+% \cs{hex}. For example, if one does
+%
+% \begin{verbatim}
+% \begin{tikzpicture}[
+% every hex={label={auto=alpha column}},
+% hex/labels is name=true]
+% \hex(c=1,r=1);
+% \end{tikzpicture}
+% \end{verbatim}
+%
+% then one can refer to the location of the hex by its label i.e.,
+% \texttt{(A1)}. Since the hex is really a \TikZ{} \texttt{node}, we
+% can also use anchors defined for \texttt{hex} node shape, such as
+% \texttt{(A1.west)}, \texttt{(A1.north edge)}, and so on. This is
+% not possible if one uses the \cs{node} macro.
+%
+% \subsection{Hex bevels}
+% \label{sec:hex:bevel}
+%
+% A bevel (or ``shadow-effect'') can be added to hexes using the key
+% \spec{bevel}, with a value that specifies where the light comes from
+% (e.g., north west or NW). The percentage of the half width of a
+% chit of the bevel can be specified by the key \spec{bevel fraction}
+% (default 10\%).
+%
+%
% \subsection{Styling hexes}
%
% Typical \TikZ{} options can be passed to the \cs{hex} macro. For
@@ -200,6 +258,18 @@
% \end{scope}
% \end{tikzpicture}
% \end{verbatim}
+%
+% For example, to render only the corners of the hexes, as popular
+% among some designers, one can do
+%
+% \begin{verbatim}
+% every hex/.style={
+% dash pattern=on .2cm off .6cm on .2cm off 0cm
+% },
+% \end{verbatim}
+% Note that the dash pattern should be 1cm long and the last element
+% should be \spec{off 0cm} so the dash pattern is started afresh on
+% each hex edge.
%
% \subsection{Hex coordinate system}
% \label{sec:hex:cs}
@@ -216,7 +286,7 @@
% where \meta{vertex} and \meta{edge} are optional. The hex row and
% column defaults both to 0 and can be decimal numbers. The
% \spec{row}, \spec{column}, \spec{vertex}, and \spec{edge} keywords
-% may be shorted to \spec{r}, \spec{c}, \spec{v}, and \spec{e},
+% may be shortened to \spec{r}, \spec{c}, \spec{v}, and \spec{e},
% respectively. Possible vertexes and edges are listed in
% \tablename~\ref{tab:hex:ve}.
%
@@ -320,9 +390,15 @@
% coordinates are shifted upward or downward for smaller or larger
% numbers. Figure~\ref{fig:hex:coord:nb} illustrates. this. This can
% make it a little hard to specify coordinates relative to a hex
-% centre. Alternatively one may use vertex or edge specifciations
-% together with a relative offset in those directions.
-% \
+% centre. Alternatively one may use vertex or edge specifications
+% together with a relative offset in those directions. If one
+% require even more flexibility, one can use the \TikZ{} library
+% \spec{calc} to add arbitrary offsets, e.g.,
+%
+% \begin{verbatim}
+% \coordinate at ($(hex cs:c=1,r=10)+(.2,.2)$);
+% \end{verbatim}
+%
% \begin{figure}
% \centering
% \begin{tikzpicture}
@@ -1014,6 +1090,15 @@
% \caption{Adding rivers, boarders, and roads}
% \end{figure}
%
+% \subsubsection{Styling paths}
+%
+% Rivers, roads, railroads, and borders are styled by
+% \spec{hex/river}, \spec{hex/road}, \spec{hex/railroad}, and
+% \spec{border}, respectively, and the keys \spec{every hex river},
+% \spec{every hex road}, \spec{every hex railroad}, and \spec{every
+% hex border} will also be applied. The latter can be defined by the
+% user.
+%
% \subsection{Board clipping and frame}
%
% In the river, border, and road example above, the roads extend beyond
@@ -1117,6 +1202,134 @@
% The \cs{boardframe} macro prints the position of the rectangle to
% the log output, if one needs to do some more stuff around the board.
%
+% \subsection{Constructing the physical board}
+%
+% If the board is not too large, so that it may fit on a paper format
+% that can easily be printed (say A4, A3, Letter, or Tabloid), one can
+% simply print the board and glue it onto a sturdy surface (say
+% 1\textonehalf\,mm poster carton). However, if the board is large,
+% meaning it does not fit on a piece of printable paper, then one has
+% two options.
+% \begin{description}
+% \item[\textit{Either}] scale the board down so that it fits. Use
+% the \TikZ{} key \spec{scale=}\meta{factor} as an argument to the
+% \spec{tikzpicture} environment in which you create the board. In
+% this case, you should make sure you also scale the chits by the
+% same \meta{factor}, again via the \spec{scale} key.
+% \item[\textit{Or}] you can split the board over several pages. The
+% package provides a number of tools to help with this.
+% \end{description}
+%
+% \subsubsection{Split the board over multiple sheets}
+%
+% First, make sure you produce a standalone PDF of the board only.
+%
+% \begin{verbatim}
+% \documentclass{standalone}
+% \usepackage{wargame}
+% \begin{tikzpicture}[scale=SCALE]
+% % Define the board here.
+% \end{tikzpicture}
+% \end{verbatim}
+%
+% and that you have created this PDF --- say \texttt{board.pdf}.
+%
+% Next, prepare another document in which we will do the calculations.
+% For example
+%
+% \begin{verbatim}
+% \documentclass[11pt]{standalone}
+% \usepackage{wargame}
+% \begin{document}
+% \splitboard{paper=letter,margin=.7,ncol=2,nrow=2,overlap=1}
+% \end{document}
+% \end{verbatim}
+%
+% to calculate the split of \spec{board.pdf} over $2\times2$ letter
+% paper sheets, with a non-printable margin of 7mm, and an overlap
+% between the segments of 1cm.
+%
+% The possible keys for the \cs{splitboard} macro are
+%
+% \begin{itemize}
+% \item \spec{paper}=\meta{format}: Specifies the paper format. One
+% of \spec{a4}, \spec{a3}, \spec{letter}, \spec{tabloid}. Default
+% is \spec{a4}.
+% \item \spec{landscape}: Sets the paper format to be in landscape
+% mode (default is portrait).
+% \item \spec{margin}=\meta{size in centimetres}: Size of margins on
+% each sheet in centimetres \emph{without} unit. That is put
+% \spec{0.6} for 6mm, \emph{not} \spec{6mm}. Default is \spec{0.6}.
+% This should be \emph{slightly} larger (by roughly 5\%) than the
+% \emph{least} margin required by the printer used. \emph{Must} be
+% given \emph{before} \spec{paper} to have any effect.
+% \item \spec{ncol}=\meta{number of columns}: Sets the number of
+% columns of sheets.
+% \item \spec{nrow}=\meta{number of rows}: Set the number of rows of
+% sheets.
+% \item \spec{overlap}=\meta{size in centimetres}: Sets the size of
+% the overlap between sheets in centimetres \emph{without} unit.
+% That is put \spec{2} for 2cm, \emph{not} \spec{2cm}. Default is
+% \spec{2}.
+% \item \spec{image}=\meta{image file name}: File name of the board
+% image (a PDF). Default is \spec{board}
+% \item \spec{output}=\meta{output file name}: File name (without
+% \spec{.tex} ending) to write calculated split to.
+% \item \spec{standalone}: Boolean flag. If true, then output file
+% will be a standalone document (i.e., has a \cs{documentclass}).
+% \item \spec{scale}=\meta{scale}: Set scale of board.
+% \end{itemize}
+%
+% The macro will produce a file named \meta{output file
+% name}\spec{.tex} which can be included in another document to
+% generate the split board PDF. Crop marks will be added to the
+% board segments to make it easier to align the parts.
+%
+% \subsubsection{Foldable board}
+%
+% To make a fold-able board use for example the below template to
+% create grooves and cuts.
+%
+% \begin{tikzpicture}[
+% cut/.style={red!50!black,thick},
+% front groove/.style={green!50!black,dashed,thick},
+% back groove/.style={blue!50!black,dash dot,thick},
+% ]
+% \node[minimum width=8.4cm,
+% minimum height=5.9cm,
+% inner sep=0pt,
+% draw,
+% thick] (a) {};
+% \coordinate (middle) at (a);
+% \coordinate (top) at (a.north);
+% \coordinate (top left) at ($(a.north west)!.5!(a.north)$);
+% \coordinate (middle left) at ($(a.west)!.5!(a)$);
+% \coordinate (bottom left) at ($(a.south west)!.5!(a.south)$);
+% \coordinate (top right) at ($(a.north)!.5!(a.north east)$);
+% \coordinate (middle right) at ($(a)!.5!(a.east)$);
+% \coordinate (bottom right) at ($(a.south)!.5!(a.south east)$);
+% \coordinate (bottom) at (a.south);
+% \draw[cut] (middle)--(bottom);
+% \draw[cut] (top left)--(middle left);
+% \draw[cut] (top right)--(middle right);
+% \draw[back groove] (middle)--(top);
+% \draw[back groove] (bottom left)--(middle left);
+% \draw[back groove] (bottom right)--(middle right);
+% \draw[front groove] (a.west)--(a.east);
+%
+% \draw[cut] ($(a.south west)+(0,-1)$)--++(2,0) node[anchor=west]{%
+% Cut through carton};
+% \draw[back groove] ($(a.south west)+(0,-1.5)$)--++(2,0) node[anchor=west]{%
+% Cut groove (\textonehalf{} through) in carton on \emph{back} side};
+% \draw[front groove] ($(a.south west)+(0,-2)$)--++(2,0) node[anchor=west]{%
+% Cut groove (\textonehalf{} through) in carton on \emph{front} side};
+% \end{tikzpicture}
+%
+% This will fold the board down to a fourth of the size of the full
+% map. For example, if the board is A1
+% ($84\,\mathrm{cm}\times59.4\,\mathrm{cm}$) it will fold down to A4
+% ($21\,\mathrm{cm}\times29.7\,\mathrm{cm}$) for easier storage.
+%
% \iffalse
% --------------------------------------------------------------------
%
@@ -1228,7 +1441,22 @@
% \caption{Example of chits fit within hexes. }
% \label{fig:chit:hex}
% \end{figure}
+%
+% Just as \cs{hex} is really a wrapper around \TikZ{}'s \cs{node}
+% macro, so it is with \cs{chit}. This means that an alternative way
+% of making a chit is to do
+%
+% \begin{MacroSyntax}{\chit}
+% \cs{node}[chit=\marg{key-value-pairs}] \parg{name} at \parg{location};
+% \end{MacroSyntax}
+%
+% Since chits are really \TikZ{} \texttt{node}s we can use anchors
+% on the chit. Unlike for \cs{hex} where there are additional
+% features available when using the dedicated macro, there really
+% isn't much difference between \cs{chit} and
+% \cs{node}\texttt{[chit=\textellipsis]}.
%
+%
% \subsection{Styling chits}
%
% Typical \TikZ{} options can be passed to the \cs{chit} macro. For
@@ -1269,9 +1497,21 @@
% similar keys. For example, the picture \spec{chit/identifier} is
% styled by \spec{tikz/chit/identifier}.
%
+%
+% A bevel (or ``shadow-effect'') can be added to chits using the key
+% \spec{bevel}, with a value that specifies where the light comes from
+% (e.g., north west or NW). The percentage of the half width of a
+% chit of the bevel can be specified by the key \spec{bevel fraction}
+% (default 10\%). This can be used for both symbol or full chits.
+%
% In addition, one can define the key \spec{tikz/every chit} to be the
% default options for all chits.
%
+% By default, the outer ``frame'' of a chit is drawn with the same
+% graphics options as the chit it self (i.e., same fill and stroke
+% colour). To change that, one can pass \spec{frame}=\marg{options}
+% as part of the chit options.
+%
% \subsection{Defining preset chit types}
%
% One can conveniently pre-define some chit styles. For example,
@@ -1314,7 +1554,7 @@
% The NATO markers are designed to fit within the template shown in
% \figurename~\ref{fig:natoapp:template}. The template is serves as a
% placement guide of the the various parts of the NATO marker as
-% illustrated in \figurename{fig:natoapp:usage}.
+% illustrated in \figurename~\ref{fig:natoapp:usage}.
%
% \begin{figure}[htbp]
% \centering
@@ -1688,6 +1928,9 @@
%
% \subsection{Unit type identification}
%
+%
+% See \tablename~\ref{tab:natoapp6c:abbr}.
+%
% \begin{table}
% \centering%
% \def\typemark#1{%
@@ -1737,7 +1980,7 @@
% & FA\\
% \typemark{main=infantry}
% & Infantry
-% & IN\\
+% & IN\footnote{Sometimes just `I'}\\
% \rowcolor{altcol}
% \typemark{main={infantry,armoured}}
% & Mechanised infantry
@@ -1775,7 +2018,7 @@
% \rowcolor{altcol}
% \sizemark{division}
% & Division
-% & D\\
+% & D\footnote{Sometimes `DIV'}\\
% \sizemark{brigade}
% & Brigade
% & BD\\
@@ -1828,7 +2071,9 @@
%
% \begin{thebibliography}{99}
% \bibitem{fb} Hanover,C., Hendrix,C.E., \& Llewelyn,S.,
-% \textit{First Blood}, 1997, \url{https://grognard.com/fb/}
+% \textit{First Blood}, 1997, \url{https://grognard.com/fb/}. See
+% also implementation using this package at
+% \url{https://gitlab.com/wargames_tex/firstblood_tex}.
% \bibitem{app6c} \textit{NATO Joint Military Symbology}, APP-6(C),
% May 2011,
% \url{https://en.wikipedia.org/wiki/NATO_Joint_Military_Symbology}.
@@ -2453,3 +2698,5 @@
% TeX-command-extra-options: "-shell-escape"
% End:
% \fi
+
+
diff --git a/macros/latex/contrib/wargame/source/wargame.ins b/macros/latex/contrib/wargame/source/wargame.ins
index 23ec10e251..44acad580e 100644
--- a/macros/latex/contrib/wargame/source/wargame.ins
+++ b/macros/latex/contrib/wargame/source/wargame.ins
@@ -81,10 +81,17 @@ End:
\file{symbols.tex} {\from{wargame.dtx}{driver,symbols}}
%
\file{tikzlibrarywargame.util.code.tex}{%
- \from{util/core.dtx}{utils}}
+ \from{util/core.dtx} {utils}
+ \from{util/misc.dtx} {utils}
+ \from{util/compound.dtx}{utils}
+ \from{util/bb.dtx} {utils}
+ \from{util/tikz.dtx} {utils}
+ \from{util/randomid.dtx}{utils}
+ \from{util/icons.dtx} {utils}
+ }
%
\file{wgexport.cls}{%
- \from{util/core.dtx}{exportcls}}
+ \from{util/export.dtx}{exportcls}}
%
\file{tikzlibrarywargame.hex.code.tex}{
\from{hex/core.dtx} {hex}
@@ -107,7 +114,8 @@ End:
\from{hex/labels.dtx} {hex}
\from{hex/extra.dtx} {hex}
\from{hex/paths.dtx} {hex}
- \from{hex/board.dtx} {hex}}
+ \from{hex/board.dtx} {hex}
+ \from{hex/split.dtx} {hex}}
%
\file{tikzlibrarywargame.natoapp6c.code.tex}{
\from{natoapp6c/core.dtx} {natoapp6c}
@@ -125,10 +133,16 @@ End:
\from{natoapp6c/list.dtx} {natoapp6c}}
%
\file{tikzlibrarywargame.chit.code.tex}{
- \from{chit/core.dtx} {chit}
- \from{chit/shape.dtx} {chit}
- \from{chit/elements.dtx}{chit}
- \from{chit/misc.dtx} {chit}}
+ \from{chit/core.dtx} {chit}
+ \from{chit/shape.dtx} {chit}
+ \from{chit/elements.dtx} {chit}
+ \from{chit/modifiers.dtx}{chit}
+ \from{chit/stack.dtx} {chit}
+ \from{chit/oob.dtx} {chit}
+ \from{chit/table.dtx} {chit}
+ \from{chit/battle.dtx} {chit}
+ \from{chit/dice.dtx} {chit}
+ \from{chit/misc.dtx} {chit}}
%
\file{wargame.beach.tex} {\from{hex/tile.dtx}{tile,beach}}
\file{wargame.city.tex} {\from{hex/tile.dtx}{tile,city}}
diff --git a/macros/luatex/generic/luavlna/README.md b/macros/luatex/generic/luavlna/README.md
index df27afa648..870552cd0f 100644
--- a/macros/luatex/generic/luavlna/README.md
+++ b/macros/luatex/generic/luavlna/README.md
@@ -1,4 +1,4 @@
-# Luavlna, version 0.1j, 2022-08-15
+# Luavlna, version 0.1k, 2023-03-28
# Introduction
diff --git a/macros/luatex/generic/luavlna/luavlna-doc.pdf b/macros/luatex/generic/luavlna/luavlna-doc.pdf
index 175e8032d0..900d6c3714 100644
--- a/macros/luatex/generic/luavlna/luavlna-doc.pdf
+++ b/macros/luatex/generic/luavlna/luavlna-doc.pdf
Binary files differ
diff --git a/macros/luatex/generic/luavlna/luavlna.lua b/macros/luatex/generic/luavlna/luavlna.lua
index f13fe6bd59..5bbe2c060b 100644
--- a/macros/luatex/generic/luavlna/luavlna.lua
+++ b/macros/luatex/generic/luavlna/luavlna.lua
@@ -5,10 +5,12 @@
-- 1. It is possible to turn this functionality only for some letters
-- 2. Code now works even for single letters after brackets etc.
--
+--
+
local M = {}
-local utf_match = unicode.utf8.match
-local utf_char = unicode.utf8.char
-local utf_len = unicode.utf8.len
+-- local utf_match = unicode.utf8.match
+local utf_char = utf8.char -- unicode.utf8.char
+local utf_len = utf8.len -- unicode.utf8.len
local glue_id = node.id "glue"
local glyph_id = node.id "glyph"
@@ -18,10 +20,36 @@ local penalty_id = node.id "penalty"
local math_id = node.id "math"
local period_char = string.byte(".")
+-- we must process only glues with the spaceskip or xspaceskip subtypes
+local allowed_space_names = {spaceskip = true, xspaceskip = true}
+local allowed_space_types = {}
+local glue_subtypes = node.subtypes("glue")
+-- convert allowed subtypes names to numbers used in nodes
+for k,v in pairs(glue_subtypes) do
+ if allowed_space_names[v] then
+ allowed_space_types[k] = true
+ end
+end
+
+local unicode_alphas = "0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05270531-055605590561-058705D0-05EA05F0-05F20620-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280840-085808A008A2-08AC0904-0939093D09500958-09610971-09770979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10CF10CF20D05-0D0C0D0E-0D100D12-0D3A0D3D0D4E0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC-0EDF0F000F40-0F470F49-0F6C0F88-0F8C1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510C710CD10D0-10FA10FC-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1BBA-1BE51C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11CF51CF61D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209C21022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2CF22CF32D00-2D252D272D2D2D30-2D672D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31BA31F0-31FF3400-4DB54E00-9FCCA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78B-A78EA790-A793A7A0-A7AAA7F8-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDAAE0-AAEAAAF2-AAF4AB01-AB06AB09-AB0EAB11-AB16AB20-AB26AB28-AB2EABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC0030-0039"
+
+
+-- construct table with unicode alphas for later testing
+local alpha_tbl = {}
+for start, finish in unicode_alphas:gmatch("(....)-(....)") do
+ for i = tonumber(start, 16), tonumber(finish, 16) do
+ alpha_tbl[utf_char(i)] = true
+ end
+end
+
+
+
+
local alpha = string.char(37).."a" -- alpha class, entering
-- percent char directly caused error
local alphas = {}
-local match_char = function(x) return utf_match(x,alpha) end
+-- the following function is not used anymore
+-- local match_char = function(x) return utf_match(x,alpha) end
local match_table = function(x, chars)
local chars=chars or {}
return chars[x]
@@ -141,7 +169,7 @@ end
local is_alpha = function(c)
local status = alphas[c]
if not status then
- status = utf_match(c, alpha)
+ status = alpha_tbl[c]
alphas[c] = status
end
return status
@@ -267,7 +295,7 @@ local function prevent_single_letter (head)
end
end
if skip ~= 1 and not in_math then
- if id == glue_id then
+ if id == glue_id and allowed_space_types[head.subtype] then
if wasnumber then
if word ~= "" then
wasnumber = false
diff --git a/macros/luatex/generic/luavlna/luavlna.sty b/macros/luatex/generic/luavlna/luavlna.sty
index 31c5e6f284..f8af19876c 100644
--- a/macros/luatex/generic/luavlna/luavlna.sty
+++ b/macros/luatex/generic/luavlna/luavlna.sty
@@ -1,4 +1,9 @@
-\ProvidesPackage{luavlna}[2022-08-15 luavlna]
+\ProvidesPackage{luavlna}[2023-03-28 luavlna]
+
+\ifdefined\directlua\else
+\PackageWarning{luavlna}{Luavlna needs LuaLaTeX, aborting}
+\endinput
+\fi
\RequirePackage{kvoptions}
diff --git a/macros/luatex/generic/luavlna/luavlna.tex b/macros/luatex/generic/luavlna/luavlna.tex
index dd7415534d..e86df0f325 100644
--- a/macros/luatex/generic/luavlna/luavlna.tex
+++ b/macros/luatex/generic/luavlna/luavlna.tex
@@ -22,7 +22,6 @@ end
% By default, spaces for all single letters are inserted
% This can be modified with \singlechars macro
set_singlechars = function(lang,chars)
- local utf_gmatch = unicode.utf8.gmatch
% local langnumbers = languages.names
local langid = get_lang_id(lang)
if not langid then return nil, "Cannot find language number for: "..lang end
@@ -30,8 +29,8 @@ set_singlechars = function(lang,chars)
local singlechars = {}
local percent = string.char(37)
local alpha = percent .. "a"
- for char in utf_gmatch(chars,"("..alpha..")") do
- singlechars[char] = true
+ for pos, char in utf8.codes(chars) do
+ singlechars[utf8.char(char)] = true
end
luavlna.singlechars(langid,singlechars)
end