Compare commits
502 Commits
@ -0,0 +1,8 @@ |
|||||||
|
* |
||||||
|
|
||||||
|
! setup.py |
||||||
|
! requirements*.txt |
||||||
|
! vasl_templates/ |
||||||
|
! vassal-shim/release/ |
||||||
|
! docker/ |
||||||
|
! LICENSE.txt |
@ -1,40 +1,86 @@ |
|||||||
# To build the image: |
# NOTE: Use the run-container.sh script to build and launch this container. |
||||||
# docker build --tag vasl-templates . |
|
||||||
# Add "--build-arg ENABLE_TESTS=1" to allow the test suite to be run against a container. |
|
||||||
# |
|
||||||
# To run a container: |
|
||||||
# docker run --rm -it --name vasl-templates \ |
|
||||||
# -p 5010:5010 \ |
|
||||||
# -v .../vasl-6.4.3.vmod:/data/vasl.vmod \ |
|
||||||
# -v .../vasl-extensions:/data/vasl-extensions \ |
|
||||||
# vasl-templates |
|
||||||
# If you have Chapter H data, add the following: |
|
||||||
# -v .../chapter-h-notes:/data/chapter-h-notes |
|
||||||
|
|
||||||
FROM python:alpine3.6 |
|
||||||
|
|
||||||
# NOTE: pillow needs zlib and jpeg, lxml needs libxslt, we need build-base for gcc, etc. |
|
||||||
RUN apk add --no-cache build-base zlib-dev jpeg-dev libxslt-dev |
|
||||||
ENV LIBRARY_PATH=/lib:/usr/lib |
|
||||||
|
|
||||||
WORKDIR /app |
# NOTE: Multi-stage builds require Docker >= 17.05. |
||||||
|
FROM rockylinux:9.1 AS base |
||||||
|
|
||||||
|
# update packages and install requirements |
||||||
|
RUN dnf -y upgrade-minimal && \ |
||||||
|
dnf install -y python3.11 |
||||||
|
|
||||||
|
# NOTE: We don't need the following stuff for the build step, but it's nice to not have to re-install |
||||||
|
# it all every time we change the requirements :-/ |
||||||
|
|
||||||
|
# install Java |
||||||
|
RUN dnf install -y java-17-openjdk |
||||||
|
|
||||||
|
# install Firefox |
||||||
|
# NOTE: We could install this using dnf, but the version of geckodriver needs to match it. |
||||||
|
ARG FIREFOX_URL=https://ftp.mozilla.org/pub/firefox/releases/117.0.1/linux-x86_64/en-US/firefox-117.0.1.tar.bz2 |
||||||
|
RUN dnf install -y bzip2 xorg-x11-server-Xvfb gtk3 dbus-glib && \ |
||||||
|
curl -s "$FIREFOX_URL" | tar -jx -C /usr/local/ && \ |
||||||
|
ln -s /usr/local/firefox/firefox /usr/bin/firefox && \ |
||||||
|
echo "exclude=firefox" >>/etc/dnf/dnf.conf |
||||||
|
|
||||||
|
# install geckodriver |
||||||
|
ARG GECKODRIVER_URL=https://github.com/mozilla/geckodriver/releases/download/v0.33.0/geckodriver-v0.33.0-linux64.tar.gz |
||||||
|
RUN curl -sL "$GECKODRIVER_URL" | tar -xz -C /usr/bin/ |
||||||
|
|
||||||
|
# clean up |
||||||
|
RUN dnf clean all |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
ARG ENABLE_TESTS |
FROM base AS build |
||||||
|
|
||||||
# install the Python requirements |
# set up a virtualenv |
||||||
COPY requirements.txt requirements-dev.txt ./ |
RUN python3.11 -m venv /opt/venv |
||||||
RUN pip install -r requirements.txt ; \ |
ENV PATH="/opt/venv/bin:$PATH" |
||||||
if [ "$ENABLE_TESTS" ]; then pip install -r requirements-dev.txt ; fi |
RUN pip install --upgrade pip |
||||||
|
|
||||||
|
# install the application requirements |
||||||
|
COPY requirements.txt requirements-dev.txt /tmp/ |
||||||
|
RUN pip3 install -r /tmp/requirements.txt |
||||||
|
ARG CONTROL_TESTS_PORT |
||||||
|
RUN if [ -n "$CONTROL_TESTS_PORT" ]; then \ |
||||||
|
pip3 install -r /tmp/requirements-dev.txt \ |
||||||
|
; fi |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
FROM base |
||||||
|
|
||||||
|
# copy the virtualenv from the build image |
||||||
|
COPY --from=build /opt/venv /opt/venv |
||||||
|
ENV PATH="/opt/venv/bin:$PATH" |
||||||
|
|
||||||
# install the application |
# install the application |
||||||
ADD vasl_templates vasl_templates |
WORKDIR /app |
||||||
COPY setup.py LICENSE.txt ./ |
COPY vasl_templates/ ./vasl_templates/ |
||||||
RUN pip install -e . |
COPY vassal-shim/release/vassal-shim.jar ./vassal-shim/release/ |
||||||
|
COPY setup.py requirements.txt requirements-dev.txt LICENSE.txt ./ |
||||||
|
RUN pip3 install --editable . |
||||||
|
|
||||||
|
# install the config files |
||||||
|
COPY vasl_templates/webapp/config/logging.yaml.example ./vasl_templates/webapp/config/logging.yaml |
||||||
|
COPY docker/config/ ./vasl_templates/webapp/config/ |
||||||
|
|
||||||
|
# create a new user |
||||||
|
# NOTE: It would be nice to just specify the UID/GID in the "docker run" command, but VASSAL has problems |
||||||
|
# if there is no user :-/ We could specify these here, but that would bake them into the image. |
||||||
|
# In general, this is not a problem, since the application doesn't need to access files outside the container, |
||||||
|
# but if the user wants to e.g. keep the cached scenario index files outside the container, and they are |
||||||
|
# running with a non-default UID/GID, they will have to manage permissions themselves. Sigh... |
||||||
|
RUN useradd --create-home app |
||||||
|
USER app |
||||||
|
|
||||||
# copy the config files |
# FUDGE! We need this to stop spurious warning messages: |
||||||
COPY docker/config/* vasl_templates/webapp/config/ |
# Fork support is only compatible with the epoll1 and poll polling strategies |
||||||
RUN if [ "$ENABLE_TESTS" ]; then echo "ENABLE_REMOTE_TEST_CONTROL = 1" >>vasl_templates/webapp/config/debug.cfg ; fi |
# Setting the verbosity to ERROR should suppress these, but doesn't :-/ |
||||||
|
# https://github.com/grpc/grpc/issues/17253 |
||||||
|
# https://github.com/grpc/grpc/blob/master/doc/environment_variables.md |
||||||
|
ENV GRPC_VERBOSITY=NONE |
||||||
|
|
||||||
|
# run the application |
||||||
EXPOSE 5010 |
EXPOSE 5010 |
||||||
COPY docker/run.sh . |
COPY docker/run.sh ./ |
||||||
CMD ./run.sh |
CMD ./run.sh |
||||||
|
@ -1,20 +1,21 @@ |
|||||||
# VASL Templates |
# VASL Templates |
||||||
|
|
||||||
<a href="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/hill-621.png" target="_blank"> |
[<img src="vasl_templates/webapp/static/help/images/hill-621.small.png" width="200" align="right" hspace="10">](vasl_templates/webapp/static/help/images/hill-621.png) |
||||||
<img src="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/hill-621.small.png" width="200" align="right" hspace="10"> |
|
||||||
</a> |
|
||||||
|
|
||||||
*VASL Templates* makes it easy to set up attractive VASL scenarios, with loads of useful information embedded to assist with game play. |
*VASL Templates* makes it easy to set up attractive VASL scenarios, with loads of useful information embedded to assist with game play. |
||||||
|
|
||||||
Simply enter the scenario information into the UI, and the program will generate HTML snippets that you can transfer into VASL labels in your scenario. |
Simply enter the scenario information into the UI, and the program will generate HTML snippets that you can transfer into VASL labels in your scenario. |
||||||
|
|
||||||
<img src="https://github.com/pacman-ghost/vasl-templates/raw/master/vasl_templates/webapp/static/help/images/ob_setup.png" width="200"> |
[<img src="vasl_templates/webapp/static/help/images/ob_setup.png" width="200">](vasl_templates/webapp/static/help/images/ob_setup.png) |
||||||
|
|
||||||
You can find more examples of the program in action [here](https://github.com/pacman-ghost/vasl-templates/tree/master/examples/). |
You can find more examples of the program in action [here](examples/). |
||||||
|
|
||||||
### Documentation |
### Documentation |
||||||
|
|
||||||
* [User Guide](https://rawgit.com/pacman-ghost/vasl-templates/master/vasl_templates/webapp/static/help/index.html?tab=userguide) |
* [User Guide](https://vasl-templates.org/help?tab=userguide) |
||||||
* [Installation](https://rawgit.com/pacman-ghost/vasl-templates/master/vasl_templates/webapp/static/help/index.html?tab=installation) |
* [Installation](https://vasl-templates.org/help?tab=installation) |
||||||
* [Writing your own templates](https://rawgit.com/pacman-ghost/vasl-templates/master/vasl_templates/webapp/static/help/index.html?tab=templatepacks) |
* [Quick Start Guide](https://vasl-templates.org/help?tab=quickstart) |
||||||
* [For developers](https://rawgit.com/pacman-ghost/vasl-templates/master/vasl_templates/webapp/static/help/index.html?tab=fordevelopers) |
* [Setting up Chapter H data](https://vasl-templates.org/help?tab=chapterh) |
||||||
|
* [Writing your own templates](https://vasl-templates.org/help?tab=templatepacks) |
||||||
|
* [For developers](https://vasl-templates.org/help?tab=fordevelopers) |
||||||
|
* [FAQ](https://vasl-templates.org/FAQ) |
||||||
|
@ -0,0 +1 @@ |
|||||||
|
logging.yaml |
@ -1,4 +1,5 @@ |
|||||||
[Debug] |
[Debug] |
||||||
|
|
||||||
TEST_VASL_MODS = /test-data/vasl-vmods/ |
; NOTE: These need to be mapped in if you want to run the test suite against a container. |
||||||
TEST_VASL_EXTNS_DIR = /test-data/vasl-extensions/ |
TEST_VASSAL_ENGINES = /test-data/vassal/ |
||||||
|
TEST_VASL_MODS = /test-data/vasl-mods/ |
||||||
|
@ -1,31 +0,0 @@ |
|||||||
version: 1 |
|
||||||
|
|
||||||
formatters: |
|
||||||
standard: |
|
||||||
format: "%(asctime)s.%(msecs)03d | %(message)s" |
|
||||||
datefmt: "%H:%M:%S" |
|
||||||
|
|
||||||
handlers: |
|
||||||
console: |
|
||||||
class: "logging.StreamHandler" |
|
||||||
formatter: "standard" |
|
||||||
stream: "ext://sys.stdout" |
|
||||||
file: |
|
||||||
class: "logging.FileHandler" |
|
||||||
formatter: "standard" |
|
||||||
filename: "/tmp/vasl-templates.log" |
|
||||||
mode: "w" |
|
||||||
|
|
||||||
loggers: |
|
||||||
werkzeug: |
|
||||||
level: "WARNING" |
|
||||||
handlers: [ "console" ] |
|
||||||
vasl_mod: |
|
||||||
level: "WARNING" |
|
||||||
handlers: [ "console", "file" ] |
|
||||||
update_vsav: |
|
||||||
level: "WARNING" |
|
||||||
handlers: [ "console", "file" ] |
|
||||||
control_tests: |
|
||||||
level: "DEBUG" |
|
||||||
handlers: [ "console", "file" ] |
|
@ -1,8 +1,5 @@ |
|||||||
[Site Config] |
[Site Config] |
||||||
|
|
||||||
FLASK_HOST = 0.0.0.0 |
|
||||||
IS_CONTAINER = 1 |
IS_CONTAINER = 1 |
||||||
|
|
||||||
VASL_MOD = /data/vasl.vmod |
WEBDRIVER_PATH = /usr/bin/geckodriver |
||||||
VASL_EXTNS_DIR = /data/vasl-extensions/ |
|
||||||
CHAPTER_H_NOTES_DIR = /data/chapter-h-notes/ |
|
||||||
|
@ -1,3 +1,15 @@ |
|||||||
#!/bin/sh |
#!/bin/sh |
||||||
|
|
||||||
python /app/vasl_templates/webapp/run_server.py |
# set up the display (so we can run VASSAL and a webdriver) |
||||||
|
export ENV=10 |
||||||
|
export DISPLAY=:10.0 |
||||||
|
Xvfb :10 -ac 1>/tmp/xvfb.log 2>/tmp/xvfb.err & |
||||||
|
|
||||||
|
# run the webapp server |
||||||
|
# IMPORTANT! This script runs as PID 1, which is the only process that will receive signals, |
||||||
|
# so we must replace it with the Python webserver process if it is to receive e.g. SIGTERM, |
||||||
|
# which we must handle if "docker stop" and scaling down in Kubernetes is to work (otherwise |
||||||
|
# things timeout and we get SIGKILL'ed). |
||||||
|
exec python3 /app/vasl_templates/webapp/run_server.py \ |
||||||
|
--addr 0.0.0.0 \ |
||||||
|
--force-init-delay 30 |
||||||
|
@ -1 +1,189 @@ |
|||||||
{"SCENARIO_NAME":"Hill 621","SCENARIO_ID":"ASL E","SCENARIO_LOCATION":"Near Minsk, Russia","SCENARIO_DATE":"1944-06-29","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"240px","SSR_WIDTH":"300px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"The Russians win at Game End if they Control ≥ five Level 3 hill hexes on Board 2.","PLAYER_1":"russian","PLAYER_1_ELR":"4","PLAYER_1_SAN":"3","PLAYER_2":"german","PLAYER_2_ELR":"3","PLAYER_2_SAN":"4","SSR":["EC are Moderate, with no wind at start.","After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.","The Germans receive one module of 80+mm Battalion Mortar OBA (HE and Smoke) with the radio in the initial OB.","The Germans receive one module of 100+mm OBA (HE and Smoke) with the Turn 4 reinforcements."],"OB_VEHICLES_1":[{"name":"T-34 M43"},{"name":"SU-152"},{"name":"SU-122"},{"name":"ZIS-5"}],"OB_VEHICLES_2":[{"name":"PzKpfw IVH"},{"name":"PzKpfw IIIN"},{"name":"StuG IIIG (L)"},{"name":"StuH 42"},{"name":"SPW 250/1"},{"name":"SPW 251/1"},{"name":"SPW 251/sMG"}],"OB_ORDNANCE_2":[{"name":"7.5cm PaK 40"},{"name":"5cm PaK 38"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":"300px"}],"OB_SETUPS_1":[{"caption":"Set up on any whole hex of Board 3","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the east edge of Board 3","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\non the east edge of Board 3","width":""}],"OB_SETUPS_2":[{"caption":"Set up in any whole hex of Board 4","width":""},{"caption":"Enter on Turn 1 on any single road hex <br>\non any edge of Board 2","width":""},{"caption":"Enter on Turn 2 on any single road hex <br>\non the north <i>or</i> south edge of Board 4","width":""},{"caption":"Enter on Turn 4 on any single road hex <br>\non the west edge of Board 2","width":""},{"caption":"Enter on Turn 5 on any single road hex <br>\nalong the north, south or west edge of Board 2","width":""},{"caption":"Enter on Turn 8 along <br>\nthe west edge of Board 2","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[{"caption":"80+mm Battalion Mortar <br> OBA (HE/Smoke)","width":""},{"caption":"100+mm OBA (HE/Smoke)","width":""}]} |
{ |
||||||
|
"COMPASS": "right", |
||||||
|
"SCENARIO_DATE": "1944-07-01", |
||||||
|
"SCENARIO_WIDTH": "", |
||||||
|
"ASA_ID": "56512", |
||||||
|
"ROAR_ID": "129", |
||||||
|
"PLAYERS_WIDTH": "", |
||||||
|
"VICTORY_CONDITIONS_WIDTH": "", |
||||||
|
"SSR_WIDTH": "400px", |
||||||
|
"OB_VEHICLES_WIDTH_1": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_1": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_VEHICLES_WIDTH_2": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_2": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"SCENARIO_NAME": "Hill 621", |
||||||
|
"SCENARIO_ID": "ASL E", |
||||||
|
"SCENARIO_LOCATION": "Near Minsk, Russia", |
||||||
|
"PLAYER_1_DESCRIPTION": "Retreating elements of 170th Infantry Division", |
||||||
|
"PLAYER_2_DESCRIPTION": "Elements of 5th Guards Army", |
||||||
|
"VICTORY_CONDITIONS": "The Russians win at Game End if they Control<br>≥ five Level 3 hill hexes on Board 2.", |
||||||
|
"SCENARIO_THEATER": "ETO", |
||||||
|
"PLAYER_1": "russian", |
||||||
|
"PLAYER_1_ELR": "4", |
||||||
|
"PLAYER_1_SAN": "3", |
||||||
|
"PLAYER_2": "german", |
||||||
|
"PLAYER_2_ELR": "3", |
||||||
|
"PLAYER_2_SAN": "4", |
||||||
|
"TURN_TRACK": { |
||||||
|
"NTURNS": "10", |
||||||
|
"WIDTH": "", |
||||||
|
"VERTICAL": false, |
||||||
|
"SHADING": "", |
||||||
|
"REINFORCEMENTS_1": "2,5", |
||||||
|
"REINFORCEMENTS_2": "1,2,4,5,8", |
||||||
|
"SWAP_PLAYERS": false |
||||||
|
}, |
||||||
|
"SSR": [ |
||||||
|
"EC are Moderate, with no wind at start.", |
||||||
|
"After \"At Start\" placement, each German infantry unit must take a TC. The only possible consequence of failure is that the unit must begin the scenario broken. Those units which break during this pre-game TC are not subject to DM in the initial German RPh.", |
||||||
|
"The Germans receive one module of 80+mm Battalion Mortar OBA (HE and Smoke) with the radio in the initial OB.", |
||||||
|
"The Germans receive one module of 100+mm OBA (HE and Smoke) with the Turn 4 reinforcements." |
||||||
|
], |
||||||
|
"OB_VEHICLES_1": [ |
||||||
|
{ |
||||||
|
"id": "ru/v:025", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "T-34 M43" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ru/v:047", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "SU-152" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ru/v:046", |
||||||
|
"seq_id": 3, |
||||||
|
"name": "SU-122" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ru/v:068", |
||||||
|
"seq_id": 4, |
||||||
|
"name": "ZIS-5" |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_VEHICLES_2": [ |
||||||
|
{ |
||||||
|
"id": "ge/v:027", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "PzKpfw IVH" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:019", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "PzKpfw IIIN" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:038", |
||||||
|
"seq_id": 3, |
||||||
|
"name": "StuG IIIG (L)" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:039", |
||||||
|
"seq_id": 4, |
||||||
|
"name": "StuH 42" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:065", |
||||||
|
"seq_id": 5, |
||||||
|
"name": "SPW 250/1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:071", |
||||||
|
"seq_id": 6, |
||||||
|
"name": "SPW 251/1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:072", |
||||||
|
"seq_id": 7, |
||||||
|
"name": "SPW 251/sMG" |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_ORDNANCE_2": [ |
||||||
|
{ |
||||||
|
"id": "ge/o:009", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "7.5cm PaK 40" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/o:007", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "5cm PaK 38" |
||||||
|
} |
||||||
|
], |
||||||
|
"SCENARIO_NOTES": [ |
||||||
|
{ |
||||||
|
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||||
|
"width": "300px", |
||||||
|
"id": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_1": [ |
||||||
|
{ |
||||||
|
"caption": "Set up on any whole hex of Board 3", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 2 on any single road hex <br>\non the east edge of Board 3", |
||||||
|
"width": "", |
||||||
|
"id": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 5 on any single road hex <br>\non the east edge of Board 3", |
||||||
|
"width": "", |
||||||
|
"id": 3 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_2": [ |
||||||
|
{ |
||||||
|
"caption": "Set up in any whole hex of Board 4", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 1 on any single road hex <br>\non any edge of Board 2", |
||||||
|
"width": "", |
||||||
|
"id": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 2 on any single road hex <br>\non the north <i>or</i> south edge of Board 4", |
||||||
|
"width": "", |
||||||
|
"id": 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 4 on any single road hex <br>\non the west edge of Board 2", |
||||||
|
"width": "", |
||||||
|
"id": 4 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 5 on any single road hex <br>\nalong the north, south or west edge of Board 2", |
||||||
|
"width": "", |
||||||
|
"id": 5 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 8 along <br>\nthe west edge of Board 2", |
||||||
|
"width": "", |
||||||
|
"id": 6 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_NOTES_1": [], |
||||||
|
"OB_NOTES_2": [ |
||||||
|
{ |
||||||
|
"caption": "80+mm Battalion Mortar <br> OBA (HE/Smoke)", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "100+mm OBA (HE/Smoke)", |
||||||
|
"width": "", |
||||||
|
"id": 2 |
||||||
|
} |
||||||
|
], |
||||||
|
"_app_version": "v1.10", |
||||||
|
"_last_update_time": "2022-09-12T02:46:18.035Z", |
||||||
|
"_creation_time": "2020-09-27T03:46:56.089Z" |
||||||
|
} |
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.8 MiB |
After Width: | Height: | Size: 109 KiB |
@ -1 +1,126 @@ |
|||||||
{"SCENARIO_NAME":"Hube's Pocket","SCENARIO_ID":"ASL G","SCENARIO_LOCATION":"Near Buchach, Southern Russia","SCENARIO_DATE":"1944-04-05","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"300px","SSR_WIDTH":"330px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"The Germans win immediately by exiting ≥ 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).","PLAYER_1":"german","PLAYER_1_ELR":"4","PLAYER_1_SAN":"2","PLAYER_2":"russian","PLAYER_2_ELR":"3","PLAYER_2_SAN":"2","SSR":["The SPW 251/sMG inherent HS is a 3-4-8.","German inherent crews have a morale of 9.","No German unit may enter any hex of Board 4 prior to Turn 2.","All units of the 1st Panzer Army must enter in Convoy (E11.) on/after Turn 5 (some, none, or all may enter each Turn) along any single road hex along the east edge."],"OB_VEHICLES_1":[{"name":"PzKpfw IVH"},{"name":"PzKpfw VG"},{"name":"SPW 251/sMG"},{"name":"SPW 251/1"},{"name":"Buessing-NAG 4500"},{"name":"Opel 6700 (Blitz)"},{"name":"SdKfz 7"}],"OB_VEHICLES_2":[{"name":"T-34/85"},{"name":"T-34 M43"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":""}],"OB_SETUPS_1":[{"caption":"Enter on Turn 1 along the west edge of Boards 2/5 (see SSR 3)","width":""},{"caption":"Enter per SSR 4","width":"200px"}],"OB_SETUPS_2":[{"caption":"Enter on Turn 1 along the north edge","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[]} |
{ |
||||||
|
"COMPASS": "down", |
||||||
|
"SCENARIO_DATE": "1944-04-06", |
||||||
|
"SCENARIO_WIDTH": "", |
||||||
|
"ASA_ID": "56514", |
||||||
|
"ROAR_ID": "131", |
||||||
|
"PLAYERS_WIDTH": "", |
||||||
|
"VICTORY_CONDITIONS_WIDTH": "", |
||||||
|
"SSR_WIDTH": "330px", |
||||||
|
"OB_VEHICLES_WIDTH_1": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_1": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_VEHICLES_WIDTH_2": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_2": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"SCENARIO_NAME": "Hube's Pocket", |
||||||
|
"SCENARIO_ID": "ASL G", |
||||||
|
"SCENARIO_LOCATION": "Near Buchach, Southern Russia", |
||||||
|
"PLAYER_1_DESCRIPTION": "Advance elements of 5th Tank Corps", |
||||||
|
"PLAYER_2_DESCRIPTION": "10th SS Panzer Division \"Frundsberg\" and the First Panzer Army", |
||||||
|
"VICTORY_CONDITIONS": "The Germans win immediately by exiting ≥ 10 vehicles <br>\noff the west edge in either one or two Convoys (see SSR 4).", |
||||||
|
"SCENARIO_THEATER": "ETO", |
||||||
|
"PLAYER_1": "german", |
||||||
|
"PLAYER_1_ELR": "4", |
||||||
|
"PLAYER_1_SAN": "2", |
||||||
|
"PLAYER_2": "russian", |
||||||
|
"PLAYER_2_ELR": "3", |
||||||
|
"PLAYER_2_SAN": "2", |
||||||
|
"TURN_TRACK": { |
||||||
|
"NTURNS": "14", |
||||||
|
"WIDTH": "5", |
||||||
|
"VERTICAL": false, |
||||||
|
"SHADING": "", |
||||||
|
"REINFORCEMENTS_1": "1,5", |
||||||
|
"REINFORCEMENTS_2": "1", |
||||||
|
"SWAP_PLAYERS": true |
||||||
|
}, |
||||||
|
"SSR": [ |
||||||
|
"The SPW 251/sMG inherent HS is a 3-4-8.", |
||||||
|
"German inherent crews have a morale of 9.", |
||||||
|
"No German unit may enter any hex of Board 4 prior to Turn 2.", |
||||||
|
"All units of the 1st Panzer Army must enter in Convoy (E11.) on/after Turn 5 (some, none, or all may enter each Turn) along any single road hex along the east edge." |
||||||
|
], |
||||||
|
"OB_VEHICLES_1": [ |
||||||
|
{ |
||||||
|
"id": "ge/v:027", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "PzKpfw IVH" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:030", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "PzKpfw VG" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:072", |
||||||
|
"seq_id": 3, |
||||||
|
"name": "SPW 251/sMG" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:071", |
||||||
|
"seq_id": 4, |
||||||
|
"name": "SPW 251/1" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:116", |
||||||
|
"seq_id": 5, |
||||||
|
"name": "Buessing-NAG 4500" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:115", |
||||||
|
"seq_id": 6, |
||||||
|
"name": "Opel 6700 (Blitz)" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:118", |
||||||
|
"seq_id": 7, |
||||||
|
"name": "SdKfz 7" |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_VEHICLES_2": [ |
||||||
|
{ |
||||||
|
"id": "ru/v:027", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "T-34/85" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ru/v:025", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "T-34 M43" |
||||||
|
} |
||||||
|
], |
||||||
|
"SCENARIO_NOTES": [ |
||||||
|
{ |
||||||
|
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_1": [ |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 1 along the west edge of Boards 2/5 (see SSR 3)", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter per SSR 4", |
||||||
|
"width": "200px", |
||||||
|
"id": 2 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_2": [ |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 1 along the north edge", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_NOTES_1": [], |
||||||
|
"OB_NOTES_2": [], |
||||||
|
"_app_version": "v1.10", |
||||||
|
"_last_update_time": "2022-09-12T02:22:47.511Z", |
||||||
|
"_creation_time": "2020-09-27T04:11:07.200Z" |
||||||
|
} |
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 79 KiB |
@ -1 +1,173 @@ |
|||||||
{"SCENARIO_NAME":"The Streets Of Stalingrad","SCENARIO_ID":"ASL C","SCENARIO_LOCATION":"Stalingrad, Russia","SCENARIO_DATE":"1942-10-04","SCENARIO_WIDTH":"","VICTORY_CONDITIONS_WIDTH":"400px","SSR_WIDTH":"500px","OB_VEHICLES_WIDTH_1":"","OB_ORDNANCE_WIDTH_1":"","OB_VEHICLES_WIDTH_2":"","OB_ORDNANCE_WIDTH_2":"","VICTORY_CONDITIONS":"Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n<li> If a player fulfills one Victory Condition and draws the other, he wnis.\n<li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control ≥ 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n<p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</ul>","PLAYER_1":"russian","PLAYER_1_ELR":"3","PLAYER_1_SAN":"6","PLAYER_2":"german","PLAYER_2_ELR":"4","PLAYER_2_SAN":"6","SSR":["Roll a die to determine who moves first.","Set up the forces of Scenario A prior to placing the units of Scenario B.","Each non-prisoner Russian unit is Fanatic (A10.8) while in building X3.","Building X3 is a Factory.","German armor may delay entry one Game Turn and thereafter enter on any southern or eastern mapboard hex.","Prior to play, both players may agree that if the game is a draw by the standard victory conditions, then the Russian loses unless he has a favorable 3:1 ratio of unbroken squads at the end of play."],"OB_VEHICLES_1":[{"name":"T-34 M43"},{"name":"T-34 M41"}],"OB_VEHICLES_2":[{"name":"StuG IIIG"},{"name":"StuG IIIB"}],"SCENARIO_NOTES":[{"caption":"Download the scenario card from <a href=\"http://www.multimanpublishing.com/Support/ASLASLSK/ASLOfficialDownloads/tabid/109/Default.aspx\">Multi-Man Publishing</a> (ASL Classic pack).","width":""}],"OB_SETUPS_1":[{"caption":"Set up in building N4","width":""},{"caption":"Set up in building J2","width":""},{"caption":"Set up in building M2","width":""},{"caption":"Set up in building N2","width":""},{"caption":"Set up in building F3","width":"180px"},{"caption":"Set up first in building X3","width":"190px"},{"caption":"Set up last in buildings P8, P5, Q4 and R1","width":""},{"caption":"Enter on Turn 2 on I1","width":""}],"OB_SETUPS_2":[{"caption":"Set up in building F5","width":""},{"caption":"Set up in building K5","width":""},{"caption":"Set up in building I7","width":""},{"caption":"Set up in building M7","width":"170px"},{"caption":"Set up in building M9","width":""},{"caption":"Set up in buildings AA4, CC3 and/or Y8","width":""},{"caption":"Set up in buildings U3, T4, R7 and/or T7","width":""},{"caption":"Set up in buildings Y8, CC7 and/or AA4","width":""},{"caption":"Enter on Turn 3 on Y10 <br>\nand/or GG5-GG6","width":""}],"OB_NOTES_1":[],"OB_NOTES_2":[]} |
{ |
||||||
|
"COMPASS": "up", |
||||||
|
"SCENARIO_DATE": "1942-10-06", |
||||||
|
"SCENARIO_WIDTH": "", |
||||||
|
"ASA_ID": "56510", |
||||||
|
"ROAR_ID": "127", |
||||||
|
"PLAYERS_WIDTH": "", |
||||||
|
"VICTORY_CONDITIONS_WIDTH": "450px", |
||||||
|
"SSR_WIDTH": "500px", |
||||||
|
"OB_VEHICLES_WIDTH_1": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_1": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px", |
||||||
|
"OB_VEHICLES_WIDTH_2": "", |
||||||
|
"OB_VEHICLES_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"OB_ORDNANCE_WIDTH_2": "", |
||||||
|
"OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px", |
||||||
|
"SCENARIO_NAME": "The Streets Of Stalingrad", |
||||||
|
"SCENARIO_ID": "ASL C", |
||||||
|
"SCENARIO_LOCATION": "Stalingrad, Russia", |
||||||
|
"PLAYER_1_DESCRIPTION": "308th Rifle Division / 295th Rifle Division / 2nd Battalion, 37th Guards Division", |
||||||
|
"PLAYER_2_DESCRIPTION": "389th Infantry Division", |
||||||
|
"VICTORY_CONDITIONS": "Victory is based upon satisfying the Victory Conditions of Scenarios A and B:\n<ul style=\"margin:0 0 10px 10px;\">\n<li> If each side fulfills one Victory Condition, the game is a draw.\n</li><li> If a player fulfills one Victory Condition and draws the other, he wnis.\n</li><li> A decisive victory is achieved when a player fulfills both Victory Conditions.\n</li></ul>\n\n<p> <b>Scenario A:</b> The Russians win at Game End if they Control ≥ 2 more buildings initially occupied by the Germans than they lose of their own initially-held stone buildings to German Control, and/or have a favorable 3:1 ratio of unbroken squad-equivalents.\n</p><p> <b>Scenario B:</b> At Game End, the player with undisputed control of at least 6 hexes of building X3 wins. A hex containing a Melee is controlled by neither player. If only one player has an unbroken unit in the building at Game End, that player is the winner. Any other result is a draw.\n</p>", |
||||||
|
"SCENARIO_THEATER": "ETO", |
||||||
|
"PLAYER_1": "russian", |
||||||
|
"PLAYER_1_ELR": "3", |
||||||
|
"PLAYER_1_SAN": "6", |
||||||
|
"PLAYER_2": "german", |
||||||
|
"PLAYER_2_ELR": "4", |
||||||
|
"PLAYER_2_SAN": "6", |
||||||
|
"TURN_TRACK": { |
||||||
|
"NTURNS": "7", |
||||||
|
"WIDTH": "", |
||||||
|
"VERTICAL": false, |
||||||
|
"SHADING": "", |
||||||
|
"REINFORCEMENTS_1": "2", |
||||||
|
"REINFORCEMENTS_2": "3", |
||||||
|
"SWAP_PLAYERS": false |
||||||
|
}, |
||||||
|
"SSR": [ |
||||||
|
"Roll a die to determine who moves first.", |
||||||
|
"Set up the forces of Scenario A prior to placing the units of Scenario B.", |
||||||
|
"Each non-prisoner Russian unit is Fanatic (A10.8) while in building X3.", |
||||||
|
"Building X3 is a Factory.", |
||||||
|
"German armor may delay entry one Game Turn and thereafter enter on any southern or eastern mapboard hex.", |
||||||
|
"Prior to play, both players may agree that if the game is a draw by the standard victory conditions, then the Russian loses unless he has a favorable 3:1 ratio of unbroken squads at the end of play." |
||||||
|
], |
||||||
|
"OB_VEHICLES_1": [ |
||||||
|
{ |
||||||
|
"id": "ru/v:025", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "T-34 M43" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ru/v:023", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "T-34 M41" |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_VEHICLES_2": [ |
||||||
|
{ |
||||||
|
"id": "ge/v:037", |
||||||
|
"seq_id": 1, |
||||||
|
"name": "StuG IIIG" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"id": "ge/v:036", |
||||||
|
"seq_id": 2, |
||||||
|
"name": "StuG IIIB" |
||||||
|
} |
||||||
|
], |
||||||
|
"SCENARIO_NOTES": [ |
||||||
|
{ |
||||||
|
"caption": "Download the scenario card from <a href=\"https://mmpgamers.com/asl-downloads-ezp-3#scenarios\">Multi-Man Publishing</a> (ASL Classic pack).", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_1": [ |
||||||
|
{ |
||||||
|
"caption": "Set up in building N4", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building J2", |
||||||
|
"width": "", |
||||||
|
"id": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building M2", |
||||||
|
"width": "", |
||||||
|
"id": 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building N2", |
||||||
|
"width": "", |
||||||
|
"id": 4 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building F3", |
||||||
|
"width": "180px", |
||||||
|
"id": 5 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up first in building X3", |
||||||
|
"width": "190px", |
||||||
|
"id": 6 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up last in buildings P8, P5, Q4 and R1", |
||||||
|
"width": "", |
||||||
|
"id": 7 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 2 on I1", |
||||||
|
"width": "", |
||||||
|
"id": 8 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_SETUPS_2": [ |
||||||
|
{ |
||||||
|
"caption": "Set up in building F5", |
||||||
|
"width": "", |
||||||
|
"id": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building K5", |
||||||
|
"width": "", |
||||||
|
"id": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building I7", |
||||||
|
"width": "", |
||||||
|
"id": 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building M7", |
||||||
|
"width": "170px", |
||||||
|
"id": 4 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in building M9", |
||||||
|
"width": "", |
||||||
|
"id": 5 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in buildings AA4, CC3 and/or Y8", |
||||||
|
"width": "", |
||||||
|
"id": 6 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in buildings U3, T4, R7 and/or T7", |
||||||
|
"width": "", |
||||||
|
"id": 7 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Set up in buildings Y8, CC7 and/or AA4", |
||||||
|
"width": "", |
||||||
|
"id": 8 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"caption": "Enter on Turn 3 on Y10 <br>\nand/or GG5-GG6", |
||||||
|
"width": "", |
||||||
|
"id": 9 |
||||||
|
} |
||||||
|
], |
||||||
|
"OB_NOTES_1": [], |
||||||
|
"OB_NOTES_2": [], |
||||||
|
"_app_version": "v1.10", |
||||||
|
"_last_update_time": "2022-09-12T02:58:13.237Z", |
||||||
|
"_creation_time": "2020-09-27T04:44:48.473Z" |
||||||
|
} |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 902 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,107 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" Freeze the vasl-templates loader program. |
||||||
|
|
||||||
|
This script is called by the main freeze script. |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import shutil |
||||||
|
import tempfile |
||||||
|
import getopt |
||||||
|
|
||||||
|
from PyInstaller.__main__ import run as run_pyinstaller |
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
APP_ICON = os.path.join( |
||||||
|
os.path.abspath( os.path.dirname( __file__ ) ), |
||||||
|
"../vasl_templates/webapp/static/images/app.ico" |
||||||
|
) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def main( args ): |
||||||
|
"""Main processing.""" |
||||||
|
|
||||||
|
# parse the command-line options |
||||||
|
output_fname = "./loader" |
||||||
|
work_dir = os.path.join( tempfile.gettempdir(), "freeze-loader" ) |
||||||
|
cleanup = True |
||||||
|
opts,args = getopt.getopt( args, "o:w:", ["output=","work=","no-clean"] ) |
||||||
|
for opt, val in opts: |
||||||
|
if opt in ["-o","--output"]: |
||||||
|
output_fname = val.strip() |
||||||
|
elif opt in ["-w","--work"]: |
||||||
|
work_dir = val.strip() |
||||||
|
elif opt in ["--no-clean"]: |
||||||
|
cleanup = False |
||||||
|
else: |
||||||
|
raise RuntimeError( "Unknown argument: {}".format( opt ) ) |
||||||
|
|
||||||
|
# freeze the loader program |
||||||
|
freeze_loader( output_fname, work_dir, cleanup ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def freeze_loader( output_fname, work_dir, cleanup ): |
||||||
|
"""Freeze the loader program.""" |
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as dist_dir: |
||||||
|
|
||||||
|
# initialize |
||||||
|
base_dir = os.path.abspath( os.path.dirname( __file__ ) ) |
||||||
|
assets_dir = os.path.join( base_dir, "assets" ) |
||||||
|
|
||||||
|
# convert the app icon to an image |
||||||
|
if not os.path.isdir( work_dir ): |
||||||
|
os.makedirs( work_dir ) |
||||||
|
app_icon_fname = os.path.join( work_dir, "app-icon.png" ) |
||||||
|
_convert_app_icon( app_icon_fname ) |
||||||
|
|
||||||
|
# initialize |
||||||
|
app_name = "loader" |
||||||
|
args = [ |
||||||
|
"--distpath", dist_dir, |
||||||
|
"--workpath", work_dir, |
||||||
|
"--specpath", work_dir, |
||||||
|
"--onefile", |
||||||
|
"--name", app_name, |
||||||
|
] |
||||||
|
args.extend( [ |
||||||
|
"--add-data", app_icon_fname + os.pathsep + "assets/", |
||||||
|
"--add-data", os.path.join(assets_dir,"loading.gif") + os.pathsep + "assets/" |
||||||
|
] ) |
||||||
|
if sys.platform == "win32": |
||||||
|
args.append( "--noconsole" ) |
||||||
|
args.extend( [ "--icon", APP_ICON ] ) |
||||||
|
args.append( os.path.join( base_dir, "main.py" ) ) |
||||||
|
|
||||||
|
# freeze the program |
||||||
|
run_pyinstaller( args ) |
||||||
|
|
||||||
|
# save the generated artifact |
||||||
|
fname = app_name+".exe" if sys.platform == "win32" else app_name |
||||||
|
shutil.move( |
||||||
|
os.path.join( dist_dir, fname ), |
||||||
|
output_fname |
||||||
|
) |
||||||
|
|
||||||
|
# clean up |
||||||
|
if cleanup: |
||||||
|
shutil.rmtree( work_dir ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def _convert_app_icon( save_fname ): |
||||||
|
"""Convert the app icon to an image.""" |
||||||
|
# NOTE: Tkinter's PhotoImage doesn't handle .ico files, so we convert the app icon |
||||||
|
# to an image, then insert it into the PyInstaller-generated executable (so that |
||||||
|
# we don't have to bundle Pillow into the release). |
||||||
|
img = Image.open( APP_ICON ) |
||||||
|
img = img.convert( "RGBA" ).resize( (48, 48) ) |
||||||
|
img.save( save_fname, "png" ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main( sys.argv[1:] ) |
@ -0,0 +1,207 @@ |
|||||||
|
""" Load the main vasl-templates program. |
||||||
|
|
||||||
|
vasl-templates can be slow to start (especially on Windows), since it has to unpack the PyInstaller-generated EXE, |
||||||
|
then startup Qt. We want to show a splash screen while all this happening, but we can't just put it in vasl-templates, |
||||||
|
since it would only happen *after* all the slow stuff has finished :-/ So, we have this stub program that shows |
||||||
|
a splash screen, launches the main vasl-templates program, and waits for it to finish starting up. |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import subprocess |
||||||
|
import threading |
||||||
|
import itertools |
||||||
|
import urllib.request |
||||||
|
from urllib.error import URLError |
||||||
|
import time |
||||||
|
import configparser |
||||||
|
|
||||||
|
# NOTE: It's important that this program start up quickly (otherwise it becomes pointless), |
||||||
|
# so we use tkinter, instead of PyQt (and also avoid bundling a 2nd copy of PyQt :-/). |
||||||
|
import tkinter |
||||||
|
import tkinter.messagebox |
||||||
|
|
||||||
|
if getattr( sys, "frozen", False ): |
||||||
|
BASE_DIR = sys._MEIPASS #pylint: disable=no-member,protected-access |
||||||
|
else: |
||||||
|
BASE_DIR = os.path.abspath( os.path.dirname( __file__ ) ) |
||||||
|
|
||||||
|
STARTUP_TIMEOUT = 60 # how to long to wait for vasl-templates to start (seconds) |
||||||
|
|
||||||
|
main_window = None |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def main( args ): |
||||||
|
"""Load the main vasl-templates program.""" |
||||||
|
|
||||||
|
# initialize Tkinter |
||||||
|
global main_window |
||||||
|
main_window = tkinter.Tk() |
||||||
|
main_window.option_add( "*Dialog.msg.font", "Helvetica 12" ) |
||||||
|
|
||||||
|
# load the app icon |
||||||
|
# NOTE: This image file doesn't exist in source control, but is created dynamically from |
||||||
|
# the main app icon by the freeze script, and inserted into the PyInstaller-generated executable. |
||||||
|
# We do things this way so that we don't have to bundle Pillow into the release. |
||||||
|
app_icon = tkinter.PhotoImage( |
||||||
|
file = make_asset_path( "app-icon.png" ) |
||||||
|
) |
||||||
|
|
||||||
|
# locate the main vasl-templates executable |
||||||
|
fname = os.path.join( os.path.dirname( sys.executable ), "vasl-templates-main" ) |
||||||
|
if sys.platform == "win32": |
||||||
|
fname += ".exe" |
||||||
|
if not os.path.isfile( fname ): |
||||||
|
show_error_msg( "Can't find the main vasl-templates program.", withdraw=True ) |
||||||
|
return -1 |
||||||
|
|
||||||
|
# launch the main vasl-templates program |
||||||
|
try: |
||||||
|
proc = subprocess.Popen( itertools.chain( [fname], args ) ) #pylint: disable=consider-using-with |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
show_error_msg( "Can't start vasl-templates:\n\n{}".format( ex ), withdraw=True ) |
||||||
|
return -2 |
||||||
|
|
||||||
|
# get the webapp port number |
||||||
|
port = 5010 |
||||||
|
fname = os.path.join( os.path.dirname( fname ), "config/app.cfg" ) |
||||||
|
if os.path.isfile( fname ): |
||||||
|
config_parser = configparser.ConfigParser() |
||||||
|
config_parser.optionxform = str # preserve case for the keys :-/ |
||||||
|
config_parser.read( fname ) |
||||||
|
args = dict( config_parser.items( "System" ) ) |
||||||
|
port = args.get( "FLASK_PORT_NO", port ) |
||||||
|
|
||||||
|
# create the splash window |
||||||
|
create_window( app_icon ) |
||||||
|
|
||||||
|
# start a background thread to check on the main vasl-templates process |
||||||
|
threading.Thread( |
||||||
|
target = check_startup, |
||||||
|
args = ( proc, port ) |
||||||
|
).start() |
||||||
|
|
||||||
|
# run the main loop |
||||||
|
main_window.mainloop() |
||||||
|
|
||||||
|
return 0 |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def create_window( app_icon ): |
||||||
|
"""Create the splash window.""" |
||||||
|
|
||||||
|
# create the splash window |
||||||
|
main_window.geometry( "275x64" ) |
||||||
|
main_window.title( "vasl-templates loader" ) |
||||||
|
main_window.overrideredirect( 1 ) # nb: "-type splash" doesn't work on Windows :-/ |
||||||
|
main_window.eval( "tk::PlaceWindow . center" ) |
||||||
|
main_window.wm_attributes( "-topmost", 1 ) |
||||||
|
main_window.tk.call( "wm", "iconphoto", main_window._w, app_icon ) #pylint: disable=protected-access |
||||||
|
main_window.protocol( "WM_DELETE_WINDOW", lambda: None ) |
||||||
|
|
||||||
|
# add the app icon |
||||||
|
label = tkinter.Label( main_window, image=app_icon ) |
||||||
|
label.grid( row=0, column=0, rowspan=2, padx=8, pady=8 ) |
||||||
|
|
||||||
|
# add the caption |
||||||
|
label = tkinter.Label( main_window, text="Loading vasl-templates...", font=("Helvetica",12) ) |
||||||
|
label.grid( row=0, column=1, padx=5, pady=(8,0) ) |
||||||
|
|
||||||
|
# add the "loading" image (we have to animate it ourself :-/) |
||||||
|
anim_label = tkinter.Label( main_window ) |
||||||
|
anim_label.grid( row=1, column=1, sticky=tkinter.N, padx=0, pady=0 ) |
||||||
|
fname = make_asset_path( "loading.gif" ) |
||||||
|
nframes = 13 |
||||||
|
frames = [ |
||||||
|
tkinter.PhotoImage( file=fname, format="gif -index {}".format( i ) ) |
||||||
|
for i in range(nframes) |
||||||
|
] |
||||||
|
frame_index = 0 |
||||||
|
def next_frame(): |
||||||
|
nonlocal frame_index |
||||||
|
frame = frames[ frame_index ] |
||||||
|
frame_index = ( frame_index + 1 ) % nframes |
||||||
|
anim_label.configure( image=frame ) |
||||||
|
main_window.after( 75, next_frame ) |
||||||
|
main_window.after( 0, next_frame ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def check_startup( proc, port ): |
||||||
|
"""Check the startup of the main vasl-templates process.""" |
||||||
|
|
||||||
|
def do_check(): |
||||||
|
|
||||||
|
# check if we've waited for too long |
||||||
|
if time.time() - start_time > STARTUP_TIMEOUT: |
||||||
|
# yup - give up |
||||||
|
raise RuntimeError( "Couldn't start vasl-templates." ) |
||||||
|
|
||||||
|
# check if the main vasl-templates process has gone away |
||||||
|
if proc.poll() is not None: |
||||||
|
raise RuntimeError( "The vasl-templates program ended unexpectedly." ) |
||||||
|
|
||||||
|
# check if the webapp is responding |
||||||
|
url = "http://localhost:{}/ping".format( port ) |
||||||
|
try: |
||||||
|
with urllib.request.urlopen( url ) as resp: |
||||||
|
_ = resp.read() |
||||||
|
except URLError: |
||||||
|
# no response - the webapp is probably still starting up |
||||||
|
return False |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
raise RuntimeError( "Couldn't communicate with vasl-templates:\n\n{}".format( ex ) ) from ex |
||||||
|
|
||||||
|
# the main vasl-templates program has started up and is responsive - our job is done! |
||||||
|
if sys.platform == "win32": |
||||||
|
# FUDGE! There is a short amount of time between the webapp server starting and |
||||||
|
# the main window appearing. We delay here for a bit, to try to synchronize |
||||||
|
# our window fading out with the main vasl-templates window appearing. |
||||||
|
time.sleep( 1 ) |
||||||
|
return True |
||||||
|
|
||||||
|
def on_done( msg ): |
||||||
|
if msg: |
||||||
|
show_error_msg( msg, withdraw=True ) |
||||||
|
fade_out( main_window, main_window.quit ) |
||||||
|
|
||||||
|
# run the main loop |
||||||
|
start_time = time.time() |
||||||
|
while True: |
||||||
|
try: |
||||||
|
if do_check(): |
||||||
|
on_done( None ) |
||||||
|
break |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
on_done( str(ex) ) |
||||||
|
return |
||||||
|
time.sleep( 0.25 ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def fade_out( target, on_done ): |
||||||
|
"""Fade out the target window.""" |
||||||
|
alpha = target.attributes( "-alpha" ) |
||||||
|
if alpha > 0: |
||||||
|
alpha -= 0.1 |
||||||
|
target.attributes( "-alpha", alpha ) |
||||||
|
target.after( 50, lambda: fade_out( target, on_done ) ) |
||||||
|
else: |
||||||
|
on_done() |
||||||
|
|
||||||
|
def make_asset_path( fname ): |
||||||
|
"""Generate the path to an asset file.""" |
||||||
|
return os.path.join( BASE_DIR, "assets", fname ) |
||||||
|
|
||||||
|
def show_error_msg( error_msg, withdraw=False ): |
||||||
|
"""Show an error dialog.""" |
||||||
|
if withdraw: |
||||||
|
main_window.withdraw() |
||||||
|
tkinter.messagebox.showinfo( "vasl-templates loader error", error_msg ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
sys.exit( main( sys.argv[1:] ) ) |
@ -1,2 +1,3 @@ |
|||||||
[pytest] |
[pytest] |
||||||
addopts = --pylint |
addopts = --pylint |
||||||
|
norecursedirs = _work_ |
||||||
|
@ -1,6 +1,7 @@ |
|||||||
pytest==3.6.0 |
pytest==7.4.2 |
||||||
tabulate==0.8.2 |
grpcio-tools==1.58.0 |
||||||
lxml==4.2.4 |
tabulate==0.9.0 |
||||||
pylint==1.9.2 |
lxml==4.9.3 |
||||||
pytest-pylint==0.9.0 |
pylint==2.17.5 |
||||||
pyinstaller==3.4 |
pytest-pylint==0.19.0 |
||||||
|
pyinstaller==5.13.2 |
||||||
|
@ -1,5 +1,10 @@ |
|||||||
flask==1.0.2 |
# python 3.11.4 |
||||||
pyyaml==3.13 |
|
||||||
pillow==5.3.0 |
flask==2.3.3 |
||||||
selenium==3.12.0 |
pyyaml==6.0.1 |
||||||
click==6.7 |
# NOTE: Pillow 9.5.0 is the last version that provides 32-bit wheels. |
||||||
|
pillow==9.5.0 |
||||||
|
selenium==4.12.0 |
||||||
|
waitress==2.1.2 |
||||||
|
appdirs==1.4.4 |
||||||
|
click==8.1.7 |
||||||
|
@ -0,0 +1,396 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
# Helper script that builds and launches the Docker container. |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
function main |
||||||
|
{ |
||||||
|
# initialize |
||||||
|
cd `dirname "$0"` |
||||||
|
PORT=5010 |
||||||
|
VASSAL= |
||||||
|
VASL_MOD= |
||||||
|
VASL_EXTNS= |
||||||
|
VASL_BOARDS= |
||||||
|
CHAPTER_H_NOTES= |
||||||
|
USER_FILES= |
||||||
|
LOGGING_CONFIG= |
||||||
|
VASSAL_SHIM_LOGGING_CONFIG= |
||||||
|
ASA_INDEX= |
||||||
|
ROAR_INDEX= |
||||||
|
VO_NOTES_IMAGE_CACHE= |
||||||
|
TEMPLATE_PACK= |
||||||
|
IMAGE_TAG=latest |
||||||
|
CONTAINER_NAME=vasl-templates |
||||||
|
DETACH= |
||||||
|
NO_BUILD= |
||||||
|
BUILD_ARGS= |
||||||
|
BUILD_NETWORK= |
||||||
|
RUN_NETWORK= |
||||||
|
CONTROL_TESTS_PORT= |
||||||
|
TEST_DATA_VASSAL= |
||||||
|
TEST_DATA_VASL_MODS= |
||||||
|
|
||||||
|
# parse the command-line arguments |
||||||
|
if [ $# -eq 0 ]; then |
||||||
|
print_help |
||||||
|
exit 0 |
||||||
|
fi |
||||||
|
params="$(getopt -o p:v:e:k:t:d -l port:,control-tests-port:,vassal:,vasl:,vasl-extensions:,boards:,chapter-h:,template-pack:,user-files:,logging:,vassal-shim-logging:,asa-index:,roar-index:,vo-notes-image-cache:,tag:,name:,detach,no-build,build-arg:,build-network:,run-network:,test-data-vassal:,test-data-vasl-mods:,help --name "$0" -- "$@")" |
||||||
|
if [ $? -ne 0 ]; then exit 1; fi |
||||||
|
eval set -- "$params" |
||||||
|
while true; do |
||||||
|
case "$1" in |
||||||
|
-p | --port) |
||||||
|
PORT=$2 |
||||||
|
shift 2 ;; |
||||||
|
--vassal) |
||||||
|
VASSAL=$2 |
||||||
|
shift 2 ;; |
||||||
|
-v | --vasl) |
||||||
|
VASL_MOD=$2 |
||||||
|
shift 2 ;; |
||||||
|
-e | --vasl-extensions) |
||||||
|
VASL_EXTNS=$2 |
||||||
|
shift 2 ;; |
||||||
|
--boards) |
||||||
|
VASL_BOARDS=$2 |
||||||
|
shift 2 ;; |
||||||
|
--chapter-h) |
||||||
|
CHAPTER_H_NOTES=$2 |
||||||
|
shift 2 ;; |
||||||
|
--user-files) |
||||||
|
USER_FILES=$2 |
||||||
|
shift 2 ;; |
||||||
|
-k | --template-pack) |
||||||
|
TEMPLATE_PACK=$2 |
||||||
|
shift 2 ;; |
||||||
|
--logging) |
||||||
|
LOGGING_CONFIG=$2 |
||||||
|
shift 2 ;; |
||||||
|
--vassal-shim-logging) |
||||||
|
VASSAL_SHIM_LOGGING_CONFIG=$2 |
||||||
|
shift 2 ;; |
||||||
|
--asa-index) |
||||||
|
ASA_INDEX=$2 |
||||||
|
shift 2 ;; |
||||||
|
--roar-index) |
||||||
|
ROAR_INDEX=$2 |
||||||
|
shift 2 ;; |
||||||
|
--vo-notes-image-cache) |
||||||
|
VO_NOTES_IMAGE_CACHE=$2 |
||||||
|
shift 2 ;; |
||||||
|
-t | --tag) |
||||||
|
IMAGE_TAG=$2 |
||||||
|
shift 2 ;; |
||||||
|
--name) |
||||||
|
CONTAINER_NAME=$2 |
||||||
|
shift 2 ;; |
||||||
|
-d | --detach ) |
||||||
|
DETACH=--detach |
||||||
|
shift 1 ;; |
||||||
|
--no-build ) |
||||||
|
NO_BUILD=1 |
||||||
|
shift 1 ;; |
||||||
|
--build-arg ) |
||||||
|
BUILD_ARGS="$BUILD_ARGS --build-arg $2" |
||||||
|
shift 2 ;; |
||||||
|
--build-network ) |
||||||
|
# FUDGE! We sometimes can't get out to the internet from the container (DNS problems) using the default |
||||||
|
# "bridge" network, so we offer the option of using an alternate network (e.g. "host"). |
||||||
|
BUILD_NETWORK="--network $2" |
||||||
|
shift 2 ;; |
||||||
|
--run-network ) |
||||||
|
RUN_NETWORK="--network $2" |
||||||
|
shift 2 ;; |
||||||
|
--control-tests-port) |
||||||
|
CONTROL_TESTS_PORT=$2 |
||||||
|
shift 2 ;; |
||||||
|
--test-data-vassal ) |
||||||
|
target=$( realpath --no-symlinks "$2" ) |
||||||
|
TEST_DATA_VASSAL="--volume $target:/test-data/vassal/" |
||||||
|
shift 2 ;; |
||||||
|
--test-data-vasl-mods ) |
||||||
|
target=$( realpath --no-symlinks "$2" ) |
||||||
|
TEST_DATA_VASL_MODS="--volume $target:/test-data/vasl-mods/" |
||||||
|
shift 2 ;; |
||||||
|
--help ) |
||||||
|
print_help |
||||||
|
exit 0 ;; |
||||||
|
-- ) shift ; break ;; |
||||||
|
* ) |
||||||
|
echo "Unknown option: $1" >&2 |
||||||
|
exit 1 ;; |
||||||
|
esac |
||||||
|
done |
||||||
|
|
||||||
|
# check if a VASSAL directory has been specified |
||||||
|
if [ -n "$VASSAL" ]; then |
||||||
|
target=$( get_target DIR "$VASSAL" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the VASSAL directory: $VASSAL" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/vassal/ |
||||||
|
VASSAL_VOLUME="--volume $target:$mpoint" |
||||||
|
VASSAL_ENV="--env VASSAL_DIR=$mpoint --env VASSAL_DIR_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a VASL module file has been specified |
||||||
|
if [ -n "$VASL_MOD" ]; then |
||||||
|
target=$( get_target FILE "$VASL_MOD" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the VASL .vmod file: $VASL_MOD" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/vasl.vmod |
||||||
|
VASL_MOD_VOLUME="--volume $target:$mpoint" |
||||||
|
VASL_MOD_ENV="--env VASL_MOD=$mpoint --env VASL_MOD_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a VASL extensions directory has been specified |
||||||
|
if [ -n "$VASL_EXTNS" ]; then |
||||||
|
target=$( get_target DIR "$VASL_EXTNS" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the VASL extensions directory: $VASL_EXTNS" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/vasl-extensions/ |
||||||
|
VASL_EXTNS_VOLUME="--volume $target:$mpoint" |
||||||
|
VASL_EXTNS_ENV="--env VASL_EXTNS_DIR=$mpoint --env VASL_EXTNS_DIR_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a VASL boards directory has been specified |
||||||
|
if [ -n "$VASL_BOARDS" ]; then |
||||||
|
target=$( get_target DIR "$VASL_BOARDS" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the VASL boards directory: $VASL_BOARDS" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/boards/ |
||||||
|
VASL_BOARDS_VOLUME="--volume $target:$mpoint" |
||||||
|
VASL_BOARDS_ENV="--env BOARDS_DIR=$mpoint --env BOARDS_DIR_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a Chapter H notes directory has been specified |
||||||
|
if [ -n "$CHAPTER_H_NOTES" ]; then |
||||||
|
target=$( get_target DIR "$CHAPTER_H_NOTES" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the Chapter H notes directory: $CHAPTER_H_NOTES" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/chapter-h-notes/ |
||||||
|
CHAPTER_H_NOTES_VOLUME="--volume $target:$mpoint" |
||||||
|
CHAPTER_H_NOTES_ENV="--env CHAPTER_H_NOTES_DIR=$mpoint --env CHAPTER_H_NOTES_DIR_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a user files directory has been specified |
||||||
|
if [ -n "$USER_FILES" ]; then |
||||||
|
target=$( get_target DIR "$USER_FILES" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the user files directory: $USER_FILES" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/user-files/ |
||||||
|
USER_FILES_VOLUME="--volume $target:$mpoint" |
||||||
|
USER_FILES_ENV="--env USER_FILES_DIR=$mpoint --env USER_FILES_DIR_TARGET=$target" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if a template pack has been specified |
||||||
|
if [ -n "$TEMPLATE_PACK" ]; then |
||||||
|
# NOTE: The template pack can either be a file (ZIP) or a directory. |
||||||
|
target=$( get_target FILE-OR-DIR "$TEMPLATE_PACK" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the template pack: $TEMPLATE_PACK" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/data/template-pack |
||||||
|
TEMPLATE_PACK_VOLUME="--volume $target:$mpoint" |
||||||
|
TEMPLATE_PACK_ENV="--env DEFAULT_TEMPLATE_PACK=$mpoint" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if logging has been configured |
||||||
|
if [ -n "$LOGGING_CONFIG" ]; then |
||||||
|
target=$( get_target FILE "$LOGGING_CONFIG" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the logging config file: $LOGGING_CONFIG" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/app/vasl_templates/webapp/config/logging.yaml |
||||||
|
LOGGING_CONFIG_VOLUME="--volume $target:$mpoint" |
||||||
|
fi |
||||||
|
if [ -n "$VASSAL_SHIM_LOGGING_CONFIG" ]; then |
||||||
|
target=$( get_target FILE "$VASSAL_SHIM_LOGGING_CONFIG" ) |
||||||
|
if [ -z "$target" ]; then |
||||||
|
echo "Can't find the VASSAL shim logging config file: $VASSAL_SHIM_LOGGING_CONFIG" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
mpoint=/app/vassal-shim/release/logback-test.xml |
||||||
|
VASSAL_SHIM_LOGGING_CONFIG_VOLUME="--volume $target:$mpoint" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if external ASA/ROAR index files have been specified |
||||||
|
# NOTE: We don't need to pass env.vars into the container, or anything like that. The code already |
||||||
|
# saves the downloaded files in /tmp/ (inside the container), so all we need to do is map these files |
||||||
|
# to the specified external files. |
||||||
|
if [ -n "$ASA_INDEX" ]; then |
||||||
|
target=$( realpath --no-symlinks "$ASA_INDEX" ) |
||||||
|
if [ ! -f "$target" ]; then |
||||||
|
if ! touch "$target" 2>/dev/null; then |
||||||
|
echo "Can't find the ASA index file: $ASA_INDEX" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
fi |
||||||
|
mpoint=/tmp/asl-scenario-archive.json |
||||||
|
ASA_INDEX_VOLUME="--volume $target:$mpoint" |
||||||
|
fi |
||||||
|
if [ -n "$ROAR_INDEX" ]; then |
||||||
|
target=$( realpath --no-symlinks "$ROAR_INDEX" ) |
||||||
|
if [ ! -f "$target" ]; then |
||||||
|
if ! touch "$target" 2>/dev/null ; then |
||||||
|
echo "Can't find the ROAR index file: $ASA_INDEX" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
fi |
||||||
|
mpoint=/tmp/roar-scenario-index.json |
||||||
|
ROAR_INDEX_VOLUME="--volume $target:$mpoint" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if an external v/o notes image cache directory has been specified |
||||||
|
if [ -n "$VO_NOTES_IMAGE_CACHE" ]; then |
||||||
|
target=$( realpath --no-symlinks "$VO_NOTES_IMAGE_CACHE" ) |
||||||
|
if [ ! -d "$target" ]; then |
||||||
|
if ! mkdir "$target" 2>/dev/null; then |
||||||
|
echo "Can't find the V/O notes image cache directory: $VO_NOTES_IMAGE_CACHE" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
fi |
||||||
|
mpoint=/tmp/vo-notes-image-cache/ |
||||||
|
VO_NOTES_IMAGE_CACHE_VOLUME="--volume $target:$mpoint" |
||||||
|
fi |
||||||
|
|
||||||
|
# check if testing has been enabled |
||||||
|
if [ -n "$CONTROL_TESTS_PORT" ]; then |
||||||
|
BUILD_ARGS="$BUILD_ARGS --build-arg CONTROL_TESTS_PORT=$CONTROL_TESTS_PORT" |
||||||
|
CONTROL_TESTS_PORT_RUN="--env CONTROL_TESTS_PORT=$CONTROL_TESTS_PORT --publish $CONTROL_TESTS_PORT:$CONTROL_TESTS_PORT" |
||||||
|
fi |
||||||
|
|
||||||
|
# build the image |
||||||
|
if [ -z "$NO_BUILD" ]; then |
||||||
|
echo Building the \"$IMAGE_TAG\" image... |
||||||
|
docker build \ |
||||||
|
--tag vasl-templates:$IMAGE_TAG \ |
||||||
|
$BUILD_ARGS \ |
||||||
|
$BUILD_NETWORK \ |
||||||
|
. 2>&1 \ |
||||||
|
| sed -e 's/^/ /' |
||||||
|
if [ ${PIPESTATUS[0]} -ne 0 ]; then exit 10 ; fi |
||||||
|
echo |
||||||
|
fi |
||||||
|
|
||||||
|
# launch the container |
||||||
|
echo Launching the \"$IMAGE_TAG\" image as \"$CONTAINER_NAME\"... |
||||||
|
docker run \ |
||||||
|
--name $CONTAINER_NAME \ |
||||||
|
--publish $PORT:5010 \ |
||||||
|
--env DOCKER_IMAGE_NAME="vasl-templates:$IMAGE_TAG" \ |
||||||
|
--env DOCKER_IMAGE_TIMESTAMP="$(date --utc +"%Y-%m-%d %H:%M:%S %:z")" \ |
||||||
|
--env BUILD_GIT_INFO="$(get_git_info)" \ |
||||||
|
--env DOCKER_CONTAINER_NAME="$CONTAINER_NAME" \ |
||||||
|
$CONTROL_TESTS_PORT_RUN \ |
||||||
|
$VASSAL_VOLUME $VASL_MOD_VOLUME $VASL_EXTNS_VOLUME $VASL_BOARDS_VOLUME $CHAPTER_H_NOTES_VOLUME $TEMPLATE_PACK_VOLUME $USER_FILES_VOLUME \ |
||||||
|
$LOGGING_CONFIG_VOLUME $VASSAL_SHIM_LOGGING_CONFIG_VOLUME \ |
||||||
|
$ASA_INDEX_VOLUME $ROAR_INDEX_VOLUME $VO_NOTES_IMAGE_CACHE_VOLUME \ |
||||||
|
$VASSAL_ENV $VASL_MOD_ENV $VASL_EXTNS_ENV $VASL_BOARDS_ENV $CHAPTER_H_NOTES_ENV $TEMPLATE_PACK_ENV $USER_FILES_ENV \ |
||||||
|
$RUN_NETWORK $DETACH \ |
||||||
|
$TEST_DATA_VASSAL $TEST_DATA_VASL_MODS \ |
||||||
|
-it --rm \ |
||||||
|
vasl-templates:$IMAGE_TAG \ |
||||||
|
2>&1 \ |
||||||
|
| sed -e 's/^/ /' |
||||||
|
exit ${PIPESTATUS[0]} |
||||||
|
} |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
function get_git_info { |
||||||
|
# NOTE: We assume the source code has a git repo, and git is installed, etc. etc., which should |
||||||
|
# all be true, but in the event we can't get the current branch and commit ID, we return nothing, |
||||||
|
# and nothing will be shown in the Program Info dialog in the UI. |
||||||
|
cd "${0%/*}" |
||||||
|
local branch=$( git branch | grep "^\*" | cut -c 3- ) |
||||||
|
local commit=$( git log | head -n 1 | cut -f 2 -d " " | cut -c 1-8 ) |
||||||
|
if [[ -n "$branch" && -n "$commit" ]]; then |
||||||
|
echo "$branch:$commit" |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
function get_target { |
||||||
|
local type=$1 |
||||||
|
local target=$2 |
||||||
|
|
||||||
|
# check that the target exists |
||||||
|
if [ "$type" == "FILE" ]; then |
||||||
|
test -f "$target" || return |
||||||
|
elif [ "$type" == "DIR" ]; then |
||||||
|
test -d "$target" || return |
||||||
|
elif [ "$type" == "FILE-OR-DIR" ]; then |
||||||
|
ls "$target" >/dev/null 2>&1 || return |
||||||
|
fi |
||||||
|
|
||||||
|
# convert the target to a full path |
||||||
|
# FUDGE! I couldn't get the "docker run" command to work with spaces in the volume targets (although |
||||||
|
# copying the generated command into the terminal worked fine) (and no, using ${var@Q} didn't help). |
||||||
|
# So, the next best thing is to allow users to create symlinks to the targets :-/ |
||||||
|
echo $( realpath --no-symlinks "$target" ) |
||||||
|
} |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
function print_help { |
||||||
|
echo "`basename "$0"` {options}" |
||||||
|
cat <<EOM |
||||||
|
Build and launch the "vasl-templates" container. |
||||||
|
|
||||||
|
-p --port Web server port number. |
||||||
|
--vassal VASSAL installation directory. |
||||||
|
-v --vasl Path to the VASL module file (.vmod). |
||||||
|
-e --vasl-extensions Path to the VASL extensions directory. |
||||||
|
--boards Path to the VASL boards. |
||||||
|
--chapter-h Path to the Chapter H notes directory. |
||||||
|
--user-files Path to the user files directory. |
||||||
|
-k --template-pack Path to a user-defined template pack. |
||||||
|
|
||||||
|
-t --tag Docker image tag. |
||||||
|
--name Docker container name. |
||||||
|
-d --detach Detach from the container and let it run in the background. |
||||||
|
--no-build Launch the container as-is (i.e. without rebuilding the image first). |
||||||
|
--build-network Docker network to use when building the image. |
||||||
|
--run-network Docker network to use when running the container. |
||||||
|
|
||||||
|
Options for storing data files outside the container (so that they can be re-used): |
||||||
|
--asa-index Path to the ASL Scenario Archive index file (downloaded). |
||||||
|
--roar-index Path to the ROAR index file (downloaded). |
||||||
|
--vo-notes-image-cache Cache directory for images generated for vehicle/ordnance notes. |
||||||
|
|
||||||
|
Options for the test suite: |
||||||
|
--control-tests-port Remote test control port number. |
||||||
|
--test-data-vassal Directory containing VASSAL releases. |
||||||
|
--test-data-vasl-mods Directory containing VASL modules. |
||||||
|
|
||||||
|
NOTE: If the port the webapp server is listening on *inside* the container is different |
||||||
|
to the port exposed *outside* the container, webdriver image generation (e.g. Shift-Click |
||||||
|
on a snippet button, or Chapter H content as images) may not work properly. This is because |
||||||
|
a web browser is launched internally with snippet HTML and a screenshot taken of it, but |
||||||
|
the HTML will contain links to the webapp server that work from outside the container, |
||||||
|
but if those links don't resolve from inside the container, you will get broken images. |
||||||
|
In this case, you will need to make such links resolve from inside the container e.g. by |
||||||
|
port-forwarding, or via DNS. |
||||||
|
EOM |
||||||
|
} |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
main "$@" |
@ -0,0 +1,189 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" Manage VASL build files. """ |
||||||
|
|
||||||
|
import os |
||||||
|
import zipfile |
||||||
|
import itertools |
||||||
|
|
||||||
|
from lxml import etree |
||||||
|
import click |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
class BuildFile: |
||||||
|
"""Wrapper around a VASL module's buildFile.""" |
||||||
|
|
||||||
|
def __init__( self, build_file ): |
||||||
|
self.doc_root = etree.fromstring( build_file ) #pylint: disable=c-extension-no-member |
||||||
|
self.attribs = self.doc_root.attrib |
||||||
|
|
||||||
|
def dump( self, line_nos=False, images=False ): |
||||||
|
"""Dump the BuildFile.""" |
||||||
|
|
||||||
|
# dump the module header |
||||||
|
click.echo( "Name: {}".format( self.attribs.get( "name" ) ) ) |
||||||
|
click.echo( "Description: {}".format( self.attribs.get( "description" ) ) ) |
||||||
|
click.echo( "Version: {}".format( self.attribs.get( "version" ) ) ) |
||||||
|
click.echo( "VASSAL: {}".format( self.attribs.get( "VassalVersion" ) ) ) |
||||||
|
click.echo( "Next slot ID: {}".format( self.attribs.get( "nextPieceSlotId" ) ) ) |
||||||
|
click.echo() |
||||||
|
|
||||||
|
# initialize |
||||||
|
opts = { "extract_images": images } |
||||||
|
|
||||||
|
def dump_node( node, depth=0 ): |
||||||
|
"""Dump an XML node and its children.""" |
||||||
|
|
||||||
|
# dump each child node |
||||||
|
for child in node: |
||||||
|
|
||||||
|
# get the attributes we want to dump |
||||||
|
attribs = get_attrib_vals( child, opts ) |
||||||
|
|
||||||
|
if depth == 0: |
||||||
|
# this is a top-level node, show it with a header |
||||||
|
header = click.style( "===", fg="green" ) |
||||||
|
val = click.style( child.tag, fg="green" ) |
||||||
|
if line_nos: |
||||||
|
val += ":{}".format( click.style( str(child.sourceline), fg="cyan" ) ) |
||||||
|
click.echo( "{header} {} {header}".format( val, header=header ) ) |
||||||
|
click.echo() |
||||||
|
# dump any attributes |
||||||
|
if attribs: |
||||||
|
for key,val in attribs: |
||||||
|
click.echo( "{} = {}".format( key, val ) ) |
||||||
|
click.echo() |
||||||
|
else: |
||||||
|
# this a lower-level node, show it normally |
||||||
|
val = click.style( child.tag, fg="yellow" ) |
||||||
|
tab = " " * (depth-1) |
||||||
|
click.echo( tab+val, nl=False ) |
||||||
|
if line_nos: |
||||||
|
click.echo( ":{}".format( click.style( str(child.sourceline), fg="cyan" ) ), nl=False ) |
||||||
|
if attribs: |
||||||
|
attribs = [ "{}={}".format( k, v ) for k,v in attribs ] |
||||||
|
click.echo( ": {}".format( " ; ".join( attribs ) ) ) |
||||||
|
else: |
||||||
|
click.echo() |
||||||
|
|
||||||
|
# dump child nodes |
||||||
|
dump_node( child, depth+1 ) |
||||||
|
|
||||||
|
if depth == 1 and len(list(node.getchildren())) > 0: #pylint: disable=len-as-condition |
||||||
|
click.echo() |
||||||
|
|
||||||
|
# dump the XML document |
||||||
|
dump_node( self.doc_root ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def _get_node_cdata( node, opts ): #pylint: disable=unused-argument |
||||||
|
"""Get the CDATA for a node.""" |
||||||
|
return "cdata", node.text |
||||||
|
|
||||||
|
def _get_pieceslot_images( node, opts ): |
||||||
|
"""Get any image paths in a PieceSlot.""" |
||||||
|
|
||||||
|
# check if we need to do this |
||||||
|
if not opts["extract_images"]: |
||||||
|
return None, None |
||||||
|
|
||||||
|
# IMPORTANT! The data in the build file looks like a serialized object, so we use |
||||||
|
# a bunch of heuristics to try to identify the fields we want :-/ This means that |
||||||
|
# we might sometimes return the wrong results :-( |
||||||
|
|
||||||
|
# split the data into fields |
||||||
|
val = node.text.replace( "\\/", "/" ) |
||||||
|
fields = val.split( ";" ) # fields seem to be semicolon-separated |
||||||
|
fields = [ f.split(",") for f in fields ] # fields can have comma-separated sub-fields |
||||||
|
fields = [ f.strip() for f in itertools.chain(*fields) ] |
||||||
|
fields = [ f for f in fields if f ] |
||||||
|
|
||||||
|
# identify fields that look like an image path |
||||||
|
valid_prefixes = ( "ru/", "ge/", "am/", "br/", "it/", "ja/", "ch/", "sh/", "fr/", "al/", "ax/", "hu/", "fi/", |
||||||
|
"po/", "ss/", # nb: for BFP |
||||||
|
"nk/", # nb: for K:FW |
||||||
|
) |
||||||
|
def is_image_path( val ): |
||||||
|
"""Check if a value looks like an image path.""" |
||||||
|
if val.endswith( (".gif",".png") ): |
||||||
|
return True |
||||||
|
if val.startswith( valid_prefixes ): |
||||||
|
return True |
||||||
|
return False |
||||||
|
fields = [ f for f in fields if is_image_path(f) ] |
||||||
|
|
||||||
|
# return the final results |
||||||
|
return "images", ";".join(fields) if fields else None |
||||||
|
|
||||||
|
# which attributes to dump for each type of XML node in the build file |
||||||
|
NODE_ATTRIBS_TO_DUMP = { |
||||||
|
"VASL.build.module.ASLMap":[ "mapName" ], |
||||||
|
"VASSAL.build.module.ChartWindow": [ "name" ], |
||||||
|
"VASSAL.build.module.Map": [ "mapName"], |
||||||
|
"VASSAL.build.module.PieceWindow": [ "name" ], |
||||||
|
"VASSAL.build.widget.TabWidget": [ "entryName?" ], |
||||||
|
"VASSAL.build.widget.ListWidget": [ "entryName?" ], |
||||||
|
"VASSAL.build.widget.Chart": [ "chartName", "fileName" ], |
||||||
|
"VASSAL.build.widget.PanelWidget": [ "entryName?", "nColumns" ], |
||||||
|
"VASSAL.build.widget.BoxWidget": [ "entryName" ], |
||||||
|
"VASSAL.build.widget.PieceSlot": [ "gpid", "entryName", _get_pieceslot_images ], |
||||||
|
"VASSAL.build.module.PrototypeDefinition": [ "name" ], |
||||||
|
"VASSAL.build.module.documentation.HelpFile": [ "title", "fileName" ], |
||||||
|
"VASSAL.build.module.documentation.AboutScreen": [ "title", "fileName" ], |
||||||
|
"VASSAL.build.module.documentation.BrowserHelpFile": [ "title", "fileName" ], |
||||||
|
"option": [ "name" ], # nb: these appear under VASSAL.build.module.GlobalOptions |
||||||
|
"entry": [ "name", _get_node_cdata ], # nb: these appear under VASL.build.module.map.MassRemover |
||||||
|
} |
||||||
|
|
||||||
|
def get_attrib_vals( node, opts ): |
||||||
|
"""Get the attribute values we're interested in from an XML node.""" |
||||||
|
|
||||||
|
# figure out which attributes we're interested in |
||||||
|
attribs = NODE_ATTRIBS_TO_DUMP.get( node.tag, [] ) |
||||||
|
if attribs == "*": |
||||||
|
attribs = node.attrib.keys() |
||||||
|
|
||||||
|
# get the attribute values |
||||||
|
def get_attr_val( attr ): |
||||||
|
"""Get the value for the specified attribute.""" |
||||||
|
if callable( attr ): |
||||||
|
return attr( node , opts ) |
||||||
|
if attr.endswith( "?" ): |
||||||
|
# nb: this is an optional attribute (we don't show it if not present) |
||||||
|
attr = attr[:-1] |
||||||
|
return attr, node.attrib.get( attr ) |
||||||
|
else: |
||||||
|
# nb: we expect this attribute to be present, return a "missing" marker if it's not |
||||||
|
return attr, node.attrib.get( attr, "???" ) |
||||||
|
vals = [ get_attr_val(a) for a in attribs ] |
||||||
|
|
||||||
|
# return the final results |
||||||
|
return [ (k,v) for k,v in vals if v is not None ] |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
@click.command() |
||||||
|
@click.argument( "input-file", type=click.File("rb") ) |
||||||
|
@click.option( "-l","--line-nos", is_flag=True, help="Include line numbers for each XML node." ) |
||||||
|
@click.option( "-i","--images", is_flag=True, help="Show images paths for each PieceSlot." ) |
||||||
|
def main( input_file, line_nos, images ): |
||||||
|
"""Dump a VASL build file.""" |
||||||
|
|
||||||
|
# check if we've been given a .vmod file |
||||||
|
if os.path.splitext( input_file.name )[1] == ".vmod": |
||||||
|
# yup - extract the build file |
||||||
|
with zipfile.ZipFile( input_file.name, "r" ) as zf: |
||||||
|
build_file = zf.read( "buildFile" ) |
||||||
|
else: |
||||||
|
# nope - read the build file from the specified file |
||||||
|
build_file = input_file.read() |
||||||
|
|
||||||
|
# load and dump the build file |
||||||
|
build_file = BuildFile( build_file ) |
||||||
|
build_file.dump( line_nos=line_nos, images=images ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() #pylint: disable=no-value-for-parameter |
@ -0,0 +1,64 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" Check how scenarios at the ASL Scenario Archive are connected to those at ROAR. """ |
||||||
|
|
||||||
|
import sys |
||||||
|
import json |
||||||
|
|
||||||
|
from vasl_templates.webapp.scenarios import _match_roar_scenario, \ |
||||||
|
_asa_scenarios, _build_asa_scenario_index, _roar_scenarios, _build_roar_scenario_index |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def asa_string( s ): |
||||||
|
"""Return an ASL Scenario Archive scenario as a string.""" |
||||||
|
return "[{}] {} ({})".format( |
||||||
|
s["scenario_id"], s.get("title"), s.get("sc_id") |
||||||
|
) |
||||||
|
|
||||||
|
def roar_string( s ): |
||||||
|
"""Return ROAR scenario as a string.""" |
||||||
|
return "[{}] {} ({})".format( |
||||||
|
s["roar_id"], s.get("name"), s.get("scenario_id") |
||||||
|
) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
# load the ASL Scenario Archive scenarios |
||||||
|
fname = sys.argv[1] |
||||||
|
with open( fname, "r", encoding="utf-8" ) as fp: |
||||||
|
asa_data = json.load( fp ) |
||||||
|
_build_asa_scenario_index( _asa_scenarios, asa_data, None ) |
||||||
|
|
||||||
|
# load the ROAR scenarios |
||||||
|
fname = sys.argv[2] |
||||||
|
with open( fname, "r", encoding="utf-8" ) as fp: |
||||||
|
roar_data = json.load( fp ) |
||||||
|
_build_roar_scenario_index( _roar_scenarios, roar_data, None ) |
||||||
|
|
||||||
|
# try to connect each ASA scenario to ROAR |
||||||
|
exact_matches, multiple_matches, unmatched = [], [], [] |
||||||
|
for scenario in asa_data["scenarios"]: |
||||||
|
matches = _match_roar_scenario( scenario ) |
||||||
|
if not matches: |
||||||
|
unmatched.append( scenario ) |
||||||
|
elif len(matches) == 1: |
||||||
|
exact_matches.append( scenario ) |
||||||
|
else: |
||||||
|
multiple_matches.append( [ scenario, matches ] ) |
||||||
|
|
||||||
|
# output the results |
||||||
|
print( "ASL Scenario Archive scenarios: {}".format( len(asa_data["scenarios"]) ) ) |
||||||
|
print() |
||||||
|
print( "Exact matches: {}".format( len(exact_matches) ) ) |
||||||
|
print() |
||||||
|
print( "Multiple matches: {}".format( len(multiple_matches) ) ) |
||||||
|
if multiple_matches: |
||||||
|
for scenario,matches in multiple_matches: |
||||||
|
print( " {}:".format( asa_string(scenario) ) ) |
||||||
|
for match in matches: |
||||||
|
print( " - {}".format( roar_string( match ) ) ) |
||||||
|
print() |
||||||
|
print( "Unmatched: {}".format( len(unmatched) ) ) |
||||||
|
if unmatched: |
||||||
|
for scenario in unmatched: |
||||||
|
print( " {}".format( asa_string(scenario) ) ) |
@ -0,0 +1,268 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
"""Dump the log file analysis reports generated by the VASSAL shim.""" |
||||||
|
|
||||||
|
import os |
||||||
|
import itertools |
||||||
|
|
||||||
|
import click |
||||||
|
import tabulate |
||||||
|
|
||||||
|
from vasl_templates.webapp.lfa import parse_analysis_report, DEFAULT_LFA_DICE_HOTNESS_WEIGHTS |
||||||
|
|
||||||
|
EXPECTED_DISTRIB = { |
||||||
|
"DR": { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 }, |
||||||
|
"dr": { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 }, |
||||||
|
} |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
@click.command() |
||||||
|
@click.option( "--file","-f","fname", required=True, help="Log file analysis report." ) |
||||||
|
@click.option( "--players/--no-players","-p", help="Dump the players." ) |
||||||
|
@click.option( "--events/--no-events","-e", help="Dump the events." ) |
||||||
|
@click.option( "--roll-type","-r","roll_type", help="Roll type filter (e.g. IFT or MC)." ) |
||||||
|
@click.option( "--window","-w","window_size", default=1, help="Moving average window size." ) |
||||||
|
def main( fname, players, events, roll_type, window_size ): |
||||||
|
"""Dump a Log File Analysis report (generated by the VASSAL shim).""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
if not os.path.isfile( fname ): |
||||||
|
raise RuntimeError( "Can't find the report file: {}".format( fname ) ) |
||||||
|
|
||||||
|
# parse the report |
||||||
|
report = parse_analysis_report( fname ) |
||||||
|
|
||||||
|
# dump each log file |
||||||
|
for log_file in report["logFiles"]: |
||||||
|
|
||||||
|
# output a header for the next log file |
||||||
|
print( "=== {} {}".format( log_file["filename"], 80*"=" )[ :80 ] ) |
||||||
|
print() |
||||||
|
|
||||||
|
# dump the scenario details |
||||||
|
scenario_name = log_file["scenario"].get( "scenarioName" ) |
||||||
|
if scenario_name: |
||||||
|
print( "Scenario: {}".format( scenario_name ), end="" ) |
||||||
|
scenario_id = log_file["scenario"].get( "scenarioId" ) |
||||||
|
if scenario_id: |
||||||
|
print( " ({})".format( scenario_id ), end="" ) |
||||||
|
print() |
||||||
|
|
||||||
|
# dump the players |
||||||
|
if players: |
||||||
|
print( "Players:" ) |
||||||
|
max_id_len = max( len(k) for k in report["players"] ) |
||||||
|
fmt = "- {:%d} = {}" % max_id_len |
||||||
|
for player_id,player_name in report["players"].items(): |
||||||
|
print( fmt.format( player_id, player_name ) ) |
||||||
|
|
||||||
|
# dump the DR/dr distributions |
||||||
|
dump_distrib( report["players"], log_file, roll_type ) |
||||||
|
print() |
||||||
|
|
||||||
|
# dump the time-plot |
||||||
|
if events or roll_type: |
||||||
|
dump_time_plot( report["players"], log_file, roll_type, window_size ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def dump_distrib( players, log_file, roll_type ): #pylint: disable=too-many-locals,too-many-branches,too-many-statements |
||||||
|
"""Dump the DR/dr distributions.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
stats = { p: { |
||||||
|
"DR": { "nRolls": 0, "rollTotal": 0 }, |
||||||
|
"dr": { "nRolls": 0, "rollTotal": 0 }, |
||||||
|
} for p in players |
||||||
|
} |
||||||
|
distrib = { p: { |
||||||
|
"DR": { k: 0 for k in range(2,12+1) }, |
||||||
|
"dr": { k: 0 for k in range(1,6+1) }, |
||||||
|
} for p in players |
||||||
|
} |
||||||
|
|
||||||
|
# process events |
||||||
|
for evt in log_file["events"]: |
||||||
|
|
||||||
|
# check if we should process the next event |
||||||
|
if evt["eventType"] != "roll": |
||||||
|
continue |
||||||
|
if roll_type and evt["rollType"].lower() != roll_type.lower(): |
||||||
|
continue |
||||||
|
|
||||||
|
# update the stats |
||||||
|
player_id = evt["playerId"] |
||||||
|
key = "DR" if isinstance( evt["rollValue"], list ) else "dr" |
||||||
|
stats[ player_id ][ key ][ "nRolls" ] += 1 |
||||||
|
val = roll_total( evt["rollValue"] ) |
||||||
|
stats[ player_id ][ key ][ "rollTotal" ] += val |
||||||
|
distrib[ player_id ][ key ][ val ] += 1 |
||||||
|
|
||||||
|
# calculate averages |
||||||
|
avg = lambda x, y: x / y if y != 0 else 0 |
||||||
|
for player_id in players: |
||||||
|
for key in ["DR","dr"]: |
||||||
|
stats[ player_id ][ key ][ "rollAverage" ] = avg( |
||||||
|
stats[player_id][key].pop("rollTotal"), |
||||||
|
stats[player_id][key]["nRolls"] |
||||||
|
) |
||||||
|
|
||||||
|
# calculate chi-squared and hotness |
||||||
|
for player_id in players: |
||||||
|
for key in ["DR","dr"]: |
||||||
|
stats[ player_id ][ key ][ "chiSquared" ] = chi_squared( |
||||||
|
distrib[player_id][ key ], |
||||||
|
EXPECTED_DISTRIB[ key ] |
||||||
|
) |
||||||
|
stats[ player_id ][ key ][ "hotness" ] = hotness( |
||||||
|
distrib[player_id][ key ], |
||||||
|
EXPECTED_DISTRIB[ key ], |
||||||
|
DEFAULT_LFA_DICE_HOTNESS_WEIGHTS[ key ], |
||||||
|
) |
||||||
|
|
||||||
|
# output the results |
||||||
|
for key in ["dr","DR"]: |
||||||
|
print() |
||||||
|
print( "=== {} distribution ===".format( key ) ) |
||||||
|
vals = range(2,12+1) if key == "DR" else range(1,6+1) |
||||||
|
results = [ itertools.chain( [""], vals, ["total","average","chi2","hotness"] ) ] |
||||||
|
total_rolls = sum( stats[p][key]["nRolls"] for p in players ) |
||||||
|
for player_id,player_name in players.items(): |
||||||
|
# add a row for the stats |
||||||
|
row = [ player_name ] |
||||||
|
has_vals = False |
||||||
|
for val in vals: |
||||||
|
nRolls = distrib[player_id][key][val] |
||||||
|
if nRolls != 0: |
||||||
|
row.append( nRolls ) |
||||||
|
has_vals = True |
||||||
|
else: |
||||||
|
row.append( "" ) |
||||||
|
val2 = stats[player_id][key]["nRolls"] |
||||||
|
val2a = val2 / total_rolls if total_rolls != 0 else 0 |
||||||
|
row.append( "{} ({}%)".format( val2, int(100*val2a+0.5) ) ) |
||||||
|
row.append( fpfmt( stats[player_id][key]["rollAverage"], 1 ) ) |
||||||
|
row.append( fpfmt( stats[player_id][key]["chiSquared"], 3 ) ) |
||||||
|
results.append( row ) |
||||||
|
# add a row for the averages |
||||||
|
if has_vals: |
||||||
|
row = [ "" ] |
||||||
|
for val in vals: |
||||||
|
nRolls = distrib[player_id][key][val] |
||||||
|
if nRolls: |
||||||
|
val2 = avg( distrib[player_id][key][val], stats[player_id][key]["nRolls"] ) |
||||||
|
row.append( fpfmt( 100*val2, 1 ) ) |
||||||
|
else: |
||||||
|
row.append( "" ) |
||||||
|
results.append( row ) |
||||||
|
# add a row for the dice hotness |
||||||
|
row = [ "" ] |
||||||
|
partials = stats[ player_id ][ key ][ "hotness" ] |
||||||
|
if partials: |
||||||
|
for val in partials: |
||||||
|
row.append( fpfmt( val, 3 ) ) |
||||||
|
row.extend( [ "", "", "" ] ) |
||||||
|
row.append( fpfmt( sum(partials), 3 ) ) |
||||||
|
results.append( row ) |
||||||
|
print( tabulate.tabulate( results, headers="firstrow" ) ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def dump_time_plot( players, log_file, roll_type, window_size ): |
||||||
|
"""Dump the time-plot values.""" |
||||||
|
|
||||||
|
# initialize |
||||||
|
rolls = [] |
||||||
|
windows = { p: [] for p in players } |
||||||
|
|
||||||
|
def dump_rolls(): |
||||||
|
"""Dump the buffered ROLL events.""" |
||||||
|
print( tabulate.tabulate( rolls, tablefmt="plain" ) ) |
||||||
|
|
||||||
|
def onTurnTrack( evt ): #pylint: disable=unused-variable,possibly-unused-variable |
||||||
|
"""Process a TURN TRACK event.""" |
||||||
|
nonlocal rolls |
||||||
|
if rolls: |
||||||
|
dump_rolls() |
||||||
|
rolls = [] |
||||||
|
print() |
||||||
|
print( "--- {} Turn {} {} ---".format( evt["side"], evt["turnNo"], evt["phase"] ) ) |
||||||
|
print() |
||||||
|
|
||||||
|
def onRoll( evt ) : #pylint: disable=unused-variable,possibly-unused-variable |
||||||
|
"""Process a ROLL event""" |
||||||
|
# check if we should process this ROLL event |
||||||
|
if roll_type: |
||||||
|
if evt["rollType"].lower() != roll_type.lower(): |
||||||
|
return |
||||||
|
player_id = evt[ "playerId" ] |
||||||
|
if window_size == 1: |
||||||
|
# add the raw roll |
||||||
|
if isinstance( evt["rollValue"], int ): |
||||||
|
val = evt["rollValue"] |
||||||
|
else: |
||||||
|
val = ", ".join( str(v) for v in evt["rollValue"] ) |
||||||
|
rolls.append( [ players[player_id], evt["rollType"], val ] ) |
||||||
|
else: |
||||||
|
# add the moving average |
||||||
|
windows[ player_id ].append( roll_total( evt["rollValue"] ) ) |
||||||
|
if len(windows[player_id]) < window_size: |
||||||
|
return |
||||||
|
val = sum( windows[player_id] ) / len(windows[player_id]) |
||||||
|
del windows[player_id][0] |
||||||
|
rolls.append( [ players[player_id], val ] ) |
||||||
|
|
||||||
|
# process events |
||||||
|
print( "=== EVENTS ===" ) |
||||||
|
print() |
||||||
|
for evt in log_file["events"]: |
||||||
|
eventType = evt["eventType"] |
||||||
|
locals()[ "on" + eventType[0].upper() + eventType[1:] ]( evt ) |
||||||
|
if rolls: |
||||||
|
dump_rolls() |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def chi_squared( observed, expected ): |
||||||
|
"""Calculate the chi-squared for a set of values.""" |
||||||
|
nRolls = sum( observed.values() ) |
||||||
|
if nRolls == 0: |
||||||
|
return None |
||||||
|
assert observed.keys() == expected.keys() |
||||||
|
return sum( |
||||||
|
( observed[val]/nRolls - expected[val]/100 ) ** 2 / (expected[val]/100) |
||||||
|
for val in expected |
||||||
|
) |
||||||
|
|
||||||
|
def hotness( observed, expected, weights ): |
||||||
|
"""Calculate the hotness for a set of values.""" |
||||||
|
nRolls = sum( observed.values() ) |
||||||
|
if nRolls == 0: |
||||||
|
return None |
||||||
|
assert observed.keys() == expected.keys() == weights.keys() |
||||||
|
partials = [] |
||||||
|
sign = lambda val: -1 if val < 0 else +1 |
||||||
|
for val in expected: |
||||||
|
diff = observed[val]/nRolls - expected[val]/100 |
||||||
|
partials.append( sign(diff) * diff**2 * weights[val] / (expected[val]/100) ) |
||||||
|
return partials |
||||||
|
|
||||||
|
def roll_total( roll ): |
||||||
|
"""Calculate the total of a roll.""" |
||||||
|
if isinstance( roll, list ): |
||||||
|
assert all( isinstance(r,int) for r in roll ) |
||||||
|
return sum( roll ) |
||||||
|
else: |
||||||
|
assert isinstance( roll, int ) |
||||||
|
return roll |
||||||
|
|
||||||
|
def fpfmt( val, nDigits ): |
||||||
|
"""Format a floating point number.""" |
||||||
|
if val is None: |
||||||
|
return "-" |
||||||
|
return ("{:.%df}" % nDigits).format( val ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() #pylint: disable=no-value-for-parameter |
@ -0,0 +1,83 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" Prepare the piece info for a VASL module. |
||||||
|
|
||||||
|
The main program used to identify 5/8" counters by reading a module's buildFile and checking the height |
||||||
|
attribute of the PieceSlot nodes, but it turns out this is the wrong thing to do (this field actually |
||||||
|
controls the size of the piece's entry in the counter palette): |
||||||
|
https://github.com/vasl-developers/vasl/issues/1195 |
||||||
|
|
||||||
|
For each version of VASL supported, run vassal-shim (getPieceInfo command) to analyze the module's |
||||||
|
buildFile and get the correct counter sizes. Then pass the output into this script, to generate |
||||||
|
the final data file that should be saved in the $/data/vasl-$VERSION/ directory, where it will |
||||||
|
be read by the main program. |
||||||
|
|
||||||
|
NOTE: Introducing this process opens the possibility of also extracting the image file paths |
||||||
|
within the .vmod file, instead of the current messy parsing of the PieceSlot CDATA... :-/ |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import json |
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
# initialize |
||||||
|
report = {} |
||||||
|
|
||||||
|
# figure out which GPID's we're interested in |
||||||
|
gpids = set() |
||||||
|
def get_gpids( vo_type ): |
||||||
|
"""Get the GPID's from our data files.""" |
||||||
|
dname = os.path.join( os.path.dirname(__file__), "../webapp/data", vo_type ) |
||||||
|
for root,_,fnames in os.walk( dname ): |
||||||
|
for fname in fnames: |
||||||
|
if os.path.splitext( fname )[1] != ".json": |
||||||
|
continue |
||||||
|
fname = os.path.join( root, fname ) |
||||||
|
with open( fname, "r", encoding="utf-8" ) as fp: |
||||||
|
entries = json.load( fp ) |
||||||
|
for entry in entries: |
||||||
|
entry_gpid = entry.get( "gpid" ) |
||||||
|
if not entry_gpid: |
||||||
|
continue |
||||||
|
if isinstance( entry_gpid, list ): |
||||||
|
gpids.update( str(g) for g in entry_gpid ) |
||||||
|
else: |
||||||
|
gpids.add( str( entry_gpid ) ) |
||||||
|
get_gpids( "vehicles" ) |
||||||
|
get_gpids( "ordnance" ) |
||||||
|
|
||||||
|
# parse the piece info generated by vassal-shim |
||||||
|
doc = ET.parse( sys.stdin ) |
||||||
|
for piece_info in doc.getroot(): |
||||||
|
gpid = piece_info.attrib["gpid"] |
||||||
|
if gpid not in gpids: |
||||||
|
continue |
||||||
|
info = {} |
||||||
|
# check if the next piece is small |
||||||
|
# FUDGE! We used to check for <= 48, but what we get is GamePiece.boundingBox(), which is |
||||||
|
# the click zone for the counter, not the actual size of the counter's image :-/ |
||||||
|
if int( piece_info.attrib["height"] ) <= 55: |
||||||
|
info["is_small"] = True |
||||||
|
if info: |
||||||
|
report[ gpid ] = info |
||||||
|
|
||||||
|
# FUDGE! These are from extensions - it's not worth trying to figure these out programtically. |
||||||
|
report[ "adf:1948" ] = { "is_small": True } # BFP Blood & Jungle: Dutch Brandt 47mm Mortar |
||||||
|
report[ "adf:75" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 89 Heavy Grenade Launcher |
||||||
|
report[ "adf:77" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Type 97 Automatic Gun |
||||||
|
report[ "adf:76" ] = { "is_small": True } # BFP Blood & Jungle: Indonesian Year-11 Flat-Trajectory INF Gun |
||||||
|
report[ "adf:1407" ] = { "is_small": True } # BFP Poland In Flames: German 2cm Tankbusche S-18 |
||||||
|
report[ "08d:75" ] = { "is_small": True } # Fight For Seoul: American M20(L) 75mm Recoilless Rifle |
||||||
|
|
||||||
|
# output the final report |
||||||
|
print( "{" ) |
||||||
|
lines = [] |
||||||
|
for gpid, piece_info in report.items(): |
||||||
|
lines.append( "\"{}\": {}".format( |
||||||
|
gpid, json.dumps( piece_info ) |
||||||
|
) ) |
||||||
|
print( ",\n".join( lines ) ) |
||||||
|
print( "}" ) |
@ -0,0 +1,27 @@ |
|||||||
|
"""Test generating the Chapter H placeholder files.""" |
||||||
|
|
||||||
|
import os |
||||||
|
from zipfile import ZipFile |
||||||
|
|
||||||
|
from vasl_templates.tools.make_chapter_h_placeholders import make_chapter_h_placeholders |
||||||
|
from vasl_templates.webapp.utils import TempFile |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def test_make_chapter_h_placeholders(): |
||||||
|
"""Test generating the Chapter H placeholder files.""" |
||||||
|
|
||||||
|
with TempFile() as temp_file: |
||||||
|
|
||||||
|
# generate the Chapter H placeholder files |
||||||
|
make_chapter_h_placeholders( temp_file.name ) |
||||||
|
|
||||||
|
# get the expected results |
||||||
|
fname = os.path.join( os.path.split(__file__)[0], "fixtures/chapter-h-placeholders.txt" ) |
||||||
|
with open( fname, "r", encoding="utf-8" ) as fp: |
||||||
|
expected = [ line.strip() for line in fp ] |
||||||
|
|
||||||
|
# check the results |
||||||
|
with ZipFile( temp_file.name, "r" ) as zip_file: |
||||||
|
zip_fnames = sorted( zip_file.namelist() ) |
||||||
|
assert zip_fnames == expected |
@ -0,0 +1,10 @@ |
|||||||
|
[Debug] |
||||||
|
|
||||||
|
; Set this if you want to run the test suite (allows the webapp server to be controlled using gRPC). |
||||||
|
; CONTROL_TESTS_PORT = -1 |
||||||
|
|
||||||
|
; Set this to a directory containing the VASSAL releases to run the test suite with. |
||||||
|
; TEST_VASSAL_ENGINES = ... |
||||||
|
|
||||||
|
; Set this to a directory containing the VASL modules (.vmod files) to run the test suite with. |
||||||
|
; TEST_VASL_MODS = ... |
@ -0,0 +1,48 @@ |
|||||||
|
{ |
||||||
|
|
||||||
|
"_comment_": "This section maps theaters from those at the ASL Scenario Archive to ours.", |
||||||
|
"_comment2_": "CBI is handled in getEffectiveTheater().", |
||||||
|
"theater-mappings": { |
||||||
|
"WTO": "ETO", |
||||||
|
"MTO": "ETO", |
||||||
|
"Normandy": "ETO", |
||||||
|
"KW": "Korea" |
||||||
|
}, |
||||||
|
|
||||||
|
"_comment_": "This section maps player nationalities from those at the ASL Scenario Archive (must be lower-case) to our nationality ID's.", |
||||||
|
"nat-mappings": { |
||||||
|
"australian": "anzac", |
||||||
|
"belgians": "belgian", |
||||||
|
"canada": "british~canadian", |
||||||
|
"canadian": "british~canadian", |
||||||
|
"china cmd": "chinese~gmd", |
||||||
|
"commonwealth": "anzac", |
||||||
|
"filipinos": "filipino", |
||||||
|
"finland": "finnish", |
||||||
|
"free french": "free-french", |
||||||
|
"germany": "german", |
||||||
|
"gurkha": "british", |
||||||
|
"gurkhas": "british", |
||||||
|
"ina": "indonesian", |
||||||
|
"japan": "japanese", |
||||||
|
"kpa": "kfw-kpa", |
||||||
|
"nkpa": "kfw-kpa", |
||||||
|
"north korea": "kfw-kpa", |
||||||
|
"philippine": "filipino", |
||||||
|
"poland": "polish", |
||||||
|
"republic of korea": "kfw-rok", |
||||||
|
"rok": "kfw-rok", |
||||||
|
"rumanian": "romanian", |
||||||
|
"russia": "russian", |
||||||
|
"russians": "russian", |
||||||
|
"slovak": "slovakian", |
||||||
|
"siamese": "thai", |
||||||
|
"soviet": "russian", |
||||||
|
"ss": "german", |
||||||
|
"u.s.": "american", |
||||||
|
"usmc": "american", |
||||||
|
"yugoslav": "yugoslavian", |
||||||
|
"vichy": "french" |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> {{CSS:common}} </style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}Anti-Tank Magnetic Mines |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:2px 5px;"> |
||||||
|
CC Attack -2 DRM |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:2px 5px;"> |
||||||
|
<b>ATMM check</b>: dr ≤ {%if SCENARIO_YEAR < 1944%} 2 {%else%} 3 {%endif%} (△) <br> |
||||||
|
<table style="margin-left:10px;"> |
||||||
|
<tr> |
||||||
|
<td style="width:25px;"> +1 <td> HS |
||||||
|
<tr> |
||||||
|
<td> +1 <td> 1st Line |
||||||
|
<tr> |
||||||
|
<td> +1 <td> CX |
||||||
|
<tr> |
||||||
|
<td> +1 <td> vs. non-armored vehicle |
||||||
|
</table> |
||||||
|
original 6 = pinned (CCV reduced by 1) <br> |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,55 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}Bazooka '44 |
||||||
|
|
||||||
|
<tr> |
||||||
|
|
||||||
|
<td style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td class="c"> Range <td class="c" width="35"> <b>TH#</b> |
||||||
|
<tr> |
||||||
|
<td class="c"> 0 <td class="c"> 10 |
||||||
|
<tr> |
||||||
|
<td class="c"> 1 <td class="c"> 8 |
||||||
|
<tr> |
||||||
|
<td class="c"> 2 <td class="c"> 7 |
||||||
|
<tr> |
||||||
|
<td class="c"> 3 <td class="c"> 6 |
||||||
|
<tr> |
||||||
|
<td class="c"> 4 <td class="c"> 3 |
||||||
|
</table> |
||||||
|
|
||||||
|
<td valign="top" style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td> <b>TK#:</b> |
||||||
|
<td class="r"> 16 |
||||||
|
<tr> |
||||||
|
<td colspan="2" class="r"> 8-4 |
||||||
|
<tr> |
||||||
|
<td> |
||||||
|
<tr> |
||||||
|
<td> <b>X#:</b> |
||||||
|
<td class="r"> 11 |
||||||
|
</table> |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,57 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}Bazooka Type 51 |
||||||
|
|
||||||
|
<tr> |
||||||
|
|
||||||
|
<td style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td class="c"> Range <td class="c" width="35"> <b>TH#</b> |
||||||
|
<tr> |
||||||
|
<td class="c"> 0 <td class="c"> 10 |
||||||
|
<tr> |
||||||
|
<td class="c"> 1 <td class="c"> 9 |
||||||
|
<tr> |
||||||
|
<td class="c"> 2 <td class="c"> 8 |
||||||
|
<tr> |
||||||
|
<td class="c"> 3 <td class="c"> 7 |
||||||
|
<tr> |
||||||
|
<td class="c"> 4 <td class="c"> 5 |
||||||
|
<tr> |
||||||
|
<td class="c"> 5 <td class="c"> 3 |
||||||
|
</table> |
||||||
|
|
||||||
|
<td valign="top" style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td> <b>TK#:</b> |
||||||
|
<td class="r"> 22 |
||||||
|
<tr> |
||||||
|
<td colspan="2" class="r"> 12-5 |
||||||
|
<tr> |
||||||
|
<td> |
||||||
|
<tr> |
||||||
|
<td> <b>X#:</b> |
||||||
|
<td class="r"> 10 |
||||||
|
</table> |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,60 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}Bazooka '45 |
||||||
|
|
||||||
|
<tr> |
||||||
|
|
||||||
|
<td style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td class="c"> Range <td class="c" width="35"> <b>TH#</b> |
||||||
|
<tr> |
||||||
|
<td class="c"> 0 <td class="c"> 11 |
||||||
|
<tr> |
||||||
|
<td class="c"> 1 <td class="c"> 10 |
||||||
|
<tr> |
||||||
|
<td class="c"> 2 <td class="c"> 9 |
||||||
|
<tr> |
||||||
|
<td class="c"> 3 <td class="c"> 8 |
||||||
|
<tr> |
||||||
|
<td class="c"> 4 <td class="c"> 6 |
||||||
|
<tr> |
||||||
|
<td class="c"> 5 <td class="c"> 4 |
||||||
|
</table> |
||||||
|
|
||||||
|
<td valign="top" style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td> <b>TK#:</b> |
||||||
|
<td class="r"> 16 |
||||||
|
<tr> |
||||||
|
<td colspan="2" class="r"> 8-5 |
||||||
|
<tr> |
||||||
|
<td> |
||||||
|
<tr> |
||||||
|
<td> <b>X#:</b> |
||||||
|
<td class="r"> 11 |
||||||
|
<tr> |
||||||
|
<td> <b>WP#:</b> |
||||||
|
<td class="r"> 6 |
||||||
|
</table> |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,62 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}Bazooka '50 |
||||||
|
|
||||||
|
<tr> |
||||||
|
|
||||||
|
<td style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td class="c"> Range <td class="c" width="35"> <b>TH#</b> |
||||||
|
<tr> |
||||||
|
<td class="c"> 0 <td class="c"> 11 |
||||||
|
<tr> |
||||||
|
<td class="c"> 1 <td class="c"> 10 |
||||||
|
<tr> |
||||||
|
<td class="c"> 2 <td class="c"> 9 |
||||||
|
<tr> |
||||||
|
<td class="c"> 3 <td class="c"> 8 |
||||||
|
<tr> |
||||||
|
<td class="c"> 4 <td class="c"> 6 |
||||||
|
<tr> |
||||||
|
<td class="c"> 5 <td class="c"> 4 |
||||||
|
</table> |
||||||
|
|
||||||
|
<td valign="top" style="padding:0 5px;"> |
||||||
|
<table> |
||||||
|
<tr> |
||||||
|
<td> <b>TK#:</b> |
||||||
|
<td class="r"> 32 |
||||||
|
<tr> |
||||||
|
<td colspan="2" class="r"> 12-5 |
||||||
|
<tr> |
||||||
|
<td> |
||||||
|
<tr> |
||||||
|
<td> <b>X#:</b> |
||||||
|
<td class="r"> 11 |
||||||
|
{%if SCENARIO_YEAR >= 1952%} |
||||||
|
<tr> |
||||||
|
<td> <b>WP#:</b> |
||||||
|
<td class="r"> 6 |
||||||
|
{%endif%} |
||||||
|
</table> |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,25 @@ |
|||||||
|
body { |
||||||
|
{%if SNIPPET_FONT_FAMILY%} font-family: "{{SNIPPET_FONT_FAMILY}}" ; {%endif%} |
||||||
|
{%if SNIPPET_FONT_SIZE%} font-size: {{SNIPPET_FONT_SIZE}} ; {%endif%} |
||||||
|
} |
||||||
|
|
||||||
|
p { margin-top: 5px ; margin-bottom: 0 ; } |
||||||
|
|
||||||
|
ul { margin: 0 ; padding: 0 0 0 10px ; } |
||||||
|
ol { margin: 0 ; padding: 0 0 0 21px ; } |
||||||
|
{%if CUSTOM_LIST_BULLETS%} |
||||||
|
ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet.png") ; } |
||||||
|
ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet2.png") ; } |
||||||
|
ul ul ul { list-style-image: url("{{IMAGES_BASE_URL}}/bullet3.png") ; } |
||||||
|
ol { list-style-image: none ; } |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
td { margin: 0 ; padding: 0 ; } |
||||||
|
td.c { text-align: center ; } |
||||||
|
td.l { text-align: left ; } |
||||||
|
td.r { text-align: right ; } |
||||||
|
|
||||||
|
sup { font-size: 75% ; } |
||||||
|
sub { vertical-align: sub ; font-size: 80% ; line-height: 0.5em ; } |
||||||
|
|
||||||
|
.exc { font-style: italic ; color: #404040 ; } |
@ -0,0 +1,5 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<img src="{{IMAGES_BASE_URL}}/compass/{{COMPASS}}.png"> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,41 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Booby Traps --> |
||||||
|
<!-- vasl-templates:description Data chart for Booby Traps. --> |
||||||
|
|
||||||
|
<!-- player = {{PLAYER_DROPLIST:|Player}} |
||||||
|
<!-- boards = {{BOARDS*:/8|Board(s)}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
.header { |
||||||
|
background: {{PLAYER_COLORS[PLAYER_DROPLIST][0]}} ; |
||||||
|
border-bottom: 1px solid {{PLAYER_COLORS[PLAYER_DROPLIST][2]}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
} |
||||||
|
.header .level { font-size: 90% ; font-style: italic ; } |
||||||
|
{{CSS:common}} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td class="header"> |
||||||
|
<img src="{{PLAYER_FLAGS[PLAYER_DROPLIST]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}"> Booby Traps <span class="level">(Level {{LEVEL:A::B::C/3|Level}})</span> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:2px 5px;"> |
||||||
|
<b> {%if BOARDS%} Boards: {{BOARDS}} {%else%} Entire map {%endif%} </b> |
||||||
|
<ul> |
||||||
|
<li> Original TC |
||||||
|
{% if LEVEL == "A" %} ≥ 11 |
||||||
|
{% elif LEVEL == "B" %} 11 |
||||||
|
{% elif LEVEL == "C" %} 12 |
||||||
|
{%else%} ??? {%endif%} |
||||||
|
<li> Search Casualties |
||||||
|
</ul> |
||||||
|
|
||||||
|
</table> |
@ -0,0 +1,47 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Count remaining --> |
||||||
|
<!-- vasl-templates:description Add the snippet as the label of a counter (e.g. Panzerfaust or Tank-Hunter Hero), then press <i>Ctrl-L</i> when you need to update how many are left. Or add a caption, and use it as a stand-alone label. --> |
||||||
|
<!-- vasl-templates:comment The HTML is deliberately malformed, so that the number remaining is the last thing in snippet, which makes it easier to change during the course of a game. --> |
||||||
|
<!-- vasl-templates:footer <table> <tr> |
||||||
|
{% set HILITE_STYLE = 'style="background:#ffffe0;border-color:#888;"' %} |
||||||
|
<td> |
||||||
|
<h3>Panzerfaust</h3> |
||||||
|
<table class="pf"> |
||||||
|
<tr> <td class="key"> German (-'44) |
||||||
|
<td class="val" {%if (PLAYER_1 == "german" or PLAYER_2 == "german") and SCENARIO_YEAR and SCENARIO_YEAR < 1944%} {{HILITE_STYLE}} {%endif%} > #squads |
||||||
|
<tr> <td class="key"> German ('44) |
||||||
|
<td class="val" {%if (PLAYER_1 == "german" or PLAYER_2 == "german") and SCENARIO_YEAR == 1944%} {{HILITE_STYLE}} {%endif%}> 1½ × #squads (FRD) |
||||||
|
<tr> <td class="key"> German ('45) |
||||||
|
<td class="val" {%if (PLAYER_1 == "german" or PLAYER_2 == "german") and SCENARIO_YEAR == 1945%} {{HILITE_STYLE}} {%endif%}> 2 × #squads |
||||||
|
<tr> <td class="key"> Finnish (7/44+) |
||||||
|
<td class="val" {%if (PLAYER_1 == "finnish" or PLAYER_2 == "finnish") and (SCENARIO_YEAR >= 1945 or (SCENARIO_YEAR == 1944 and SCENARIO_MONTH >= 7))%} {{HILITE_STYLE}} {%endif%}> 1½ × # Elite/1<sup>st</sup> Line MMC squads (FRD) |
||||||
|
<tr> <td class="key"> Hungarian (6/44+) |
||||||
|
<td class="val" {%if (PLAYER_1 == "hungarian" or PLAYER_2 == "hungarian") and (SCENARIO_YEAR >= 1945 or (SCENARIO_YEAR == 1944 and SCENARIO_MONTH >= 6))%} {{HILITE_STYLE}} {%endif%}> #squads |
||||||
|
<tr> <td class="key"> Romanian (3-12/44) |
||||||
|
<td class="val" {%if (PLAYER_1 == "romanian" or PLAYER_2 == "romanian") and SCENARIO_YEAR == 1944 and SCENARIO_MONTH >= 3%} {{HILITE_STYLE}} {%endif%}> 1½ × #squads |
||||||
|
<tr> <td class="key"> Romanian ('45) |
||||||
|
<td class="val" {%if (PLAYER_1 == "romanian" or PLAYER_2 == "romanian") and SCENARIO_YEAR == 1945%} {{HILITE_STYLE}} {%endif%}> #squads |
||||||
|
</table> |
||||||
|
<em> Squads or squad-equivalents. </em> |
||||||
|
|
||||||
|
<td style="padding-left: 1em;"> |
||||||
|
<h3>Tank-Hunter Heroes</h3> |
||||||
|
<table class="thh"> |
||||||
|
<tr> <td class="key"> before '43 |
||||||
|
<td class="val" {%if (PLAYER_1 == "japanese" or PLAYER_2 == "japanese") and SCENARIO_YEAR and SCENARIO_YEAR < 1943%} {{HILITE_STYLE}} {%endif%}> 10% of #squads (FRU) <br> <small><em>(20% vs. Russians)</em></small> |
||||||
|
<tr> <td class="key"> '43 |
||||||
|
<td class="val" {%if (PLAYER_1 == "japanese" or PLAYER_2 == "japanese") and SCENARIO_YEAR == 1943%} {{HILITE_STYLE}} {%endif%}> 20% of #squads (FRU) |
||||||
|
<tr> <td class="key"> '44 |
||||||
|
<td class="val" {%if (PLAYER_1 == "japanese" or PLAYER_2 == "japanese") and SCENARIO_YEAR == 1944%} {{HILITE_STYLE}} {%endif%}> 33% of #squads (FRU) |
||||||
|
<tr> <td class="key"> '45 |
||||||
|
<td class="val" {%if (PLAYER_1 == "japanese" or PLAYER_2 == "japanese") and SCENARIO_YEAR == 1945%} {{HILITE_STYLE}} {%endif%}> 50% of #squads (FRU) |
||||||
|
</table> |
||||||
|
<em> Squads only, not squad-equivalents. </em> |
||||||
|
</table> --> |
||||||
|
|
||||||
|
<!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. --> |
||||||
|
<!-- vasl-templates:comment {{CAPTION:(none)::Panzerfaust::Tank-Hunter Heroes/11|Caption}} --> |
||||||
|
{# NOTE: We specify the font size in pixels, rather than as a percentage, since this label should be added to a counter. #} |
||||||
|
<div style="font-size:12px;font-weight:bold;"> |
||||||
|
{%if CAPTION != "(none)"%}{{CAPTION}}:{%endif%} {{COUNT:/2|Number}} |
@ -0,0 +1,49 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Grid --> |
||||||
|
<!-- vasl-templates:description Generates a grid. --> |
||||||
|
|
||||||
|
<!-- caption = {{CAPTION*:/25|Caption}} --> |
||||||
|
<!-- #cols = {{COLS:3/1|# columns}} ; #rows = {{ROWS:2/1|# rows}} --> |
||||||
|
<!-- cell size = {{CELL_WIDTH:180px/5|Cell width}} x {{CELL_HEIGHT:70px/5|Cell height}} --> |
||||||
|
<!-- cell labels = {{CELL_LABELS:none::letters::numbers/6|Cell labels}} --> |
||||||
|
<!-- color = {{PLAYER_COLOR_DROPLIST:|Border color}} ; border = {{BORDER_STYLE:solid::dotted::dashed::double::groove::ridge::inset::outset|Border style}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
td { |
||||||
|
width: {{CELL_WIDTH}} ; |
||||||
|
height: {{CELL_HEIGHT}} ; |
||||||
|
border: 1px {{BORDER_STYLE}} {{PLAYER_COLOR2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
} |
||||||
|
td.caption { |
||||||
|
height: 1px ; padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; text-align: center ; |
||||||
|
background: {{PLAYER_COLOR0}} ; border: none ; |
||||||
|
} |
||||||
|
.cell-label { font-size: 120% ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
{% set RANGE = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50] %} |
||||||
|
{% set LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" %} |
||||||
|
<table> |
||||||
|
{%if CAPTION%} <tr> <td class="caption" colspan="{{COLS}}"> {{CAPTION}} {%endif%} |
||||||
|
{% for row in RANGE %} {% if row <= ROWS %} |
||||||
|
<tr> {% for col in RANGE %} |
||||||
|
{% if col <= COLS %} |
||||||
|
{% set CELL_NO = (row - 1) * COLS + (col - 1) %} |
||||||
|
<td valign="top"> |
||||||
|
{% if CELL_LABELS == "letters" %} |
||||||
|
<span class="cell-label"> {{LETTERS[CELL_NO]}} </span> |
||||||
|
{% elif CELL_LABELS == "numbers" %} |
||||||
|
<span class="cell-label"> {{CELL_NO + 1}} </span> |
||||||
|
{%endif%} |
||||||
|
{%endif%} |
||||||
|
{%endfor%} |
||||||
|
{%endif%} {%endfor%} |
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,49 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Kakazu Ridge --> |
||||||
|
<!-- vasl-templates:description Generates a box to hold units hidden away in a Cave Complex. --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
/* {{CAVE_COMPLEX:Kakazu West::Kakazu Saddle::Kakazu Center::Kakazu Front::Kakazu Reverse::Kakazu East::Kakazu Village::(other)/10|Cave Complex}} */ |
||||||
|
.box { |
||||||
|
width: {{WIDTH:270px/5|Width}} ; |
||||||
|
height: {{HEIGHT:150px/5|Height}} ; |
||||||
|
border: 1px solid #ffdb00 ; |
||||||
|
color: #404040 ; |
||||||
|
} |
||||||
|
.header { background: #fff200 ; padding: 2px 5px ; } |
||||||
|
.header .name { font-size: 105% ; font-weight: bold ; } |
||||||
|
.hexes { font-size: 90% ; font-style: italic ; color: #606060 ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<div class="box"> |
||||||
|
<div class="header"> |
||||||
|
{% if CAVE_COMPLEX == "Kakazu West" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (15) |
||||||
|
<div class="hexes"> E10, F10-12, G10-14, H10-14, I11-12 <div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu Saddle" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (12) |
||||||
|
<div class="hexes"> J11-12, K11-14, L10-14, M9-13, N8-12 </div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu Center" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (20) |
||||||
|
<div class="hexes"> N13, O9-13, P8-12, Q8-13, R7-9 </div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu Front" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (20) |
||||||
|
<div class="hexes"> R10-11, S8-12, T8-12, U9-12, V8-12, W9-12, X9-11 </div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu Reverse" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (15) |
||||||
|
<div class="hexes"> R12-13, S13-15, T13-15, U13-15, V13 </div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu East" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (15) |
||||||
|
<div class="hexes"> W13, X12-13, Y11-14, Z11-14, AA11-14, BB13 </div> |
||||||
|
{% elif CAVE_COMPLEX == "Kakazu Village" %} |
||||||
|
<span class="name"> {{CAVE_COMPLEX}} </span> (12) |
||||||
|
<div class="hexes"> L15-17, M14-18, N14-18, O15-19, P15-19, Q16-20, R16-19, S17-19 </div> |
||||||
|
{% else %} |
||||||
|
<span class="name"> Cave Complex </span> |
||||||
|
{%endif%} |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,87 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Kampfgruppe Scherer --> |
||||||
|
<!-- vasl-templates:description Data charts for Grenade Bundles and Molotov Cocktails. --> |
||||||
|
<!-- {{TYPE:Grenade Bundles::Molotov Cocktails/10|Data chart}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> {{CSS:common}} </style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{PLAYER_COLORS["german"][0]}} ; |
||||||
|
border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||||
|
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}?prefh={{PLAYER_FLAG_SIZE_LARGE}}" width="{{PLAYER_FLAG_SIZE_LARGE}}" height="{{PLAYER_FLAG_SIZE_LARGE}}"> {%endif%}{{TYPE}} |
||||||
|
|
||||||
|
{% if TYPE == "Grenade Bundles" %} |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
CC Attack -2 DRM |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
<b>ATMM check</b>: dr ≤ 3 (△) |
||||||
|
<table style="margin-left:10px;"> |
||||||
|
<tr> |
||||||
|
<td style="width:20px;"> +1 |
||||||
|
<td> HS/crew |
||||||
|
<tr> |
||||||
|
<td> +2 |
||||||
|
<td> SMC |
||||||
|
<tr> |
||||||
|
<td> +1 |
||||||
|
<td> CX |
||||||
|
<tr> |
||||||
|
<td> +1 |
||||||
|
<td> vs. non-armored vehicle |
||||||
|
</table> |
||||||
|
original 6 = pinned (CCV reduced by 1) |
||||||
|
|
||||||
|
{% elif TYPE == "Molotov Cocktails" %} |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
Against AFV only. |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
<b>MOL check</b>: dr ≤ 3 (△) |
||||||
|
<table style="margin-left:10px;"> |
||||||
|
<tr> |
||||||
|
<td style="width:20px;"> +1 |
||||||
|
<td> HS/crew |
||||||
|
<tr> |
||||||
|
<td> +2 |
||||||
|
<td> SMC |
||||||
|
<tr> |
||||||
|
<td> +1 |
||||||
|
<td> CX |
||||||
|
</table> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
<b>IFT DR original colored dr</b>: |
||||||
|
<ul> |
||||||
|
<li> 1 = Flame in target Location |
||||||
|
<li> 6 = thrower breaks, Flame in their Location |
||||||
|
</ul> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td style="padding:3px 5px 0 5px;"> |
||||||
|
<b>Kindling Attempt</b>: +2 DRM |
||||||
|
|
||||||
|
{%endif%} |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
||||||
|
|
@ -1,42 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<!-- vasl-templates:name KGS Grenade Bundles --> |
|
||||||
<!-- vasl-templates:description Data chart for Grenade Bundles in <i>Kampfgruppe Scherer</i>. --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
td { margin: 0 ; padding: 0 ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style="background: {{PLAYER_COLORS["german"][0]}} ; border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; padding: 2px 5px ; font-weight: bold ;"> |
|
||||||
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}"> {%endif%}Grenade Bundles |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td style="padding:2px 5px;"> |
|
||||||
-2 CC Attack DRM <br> |
|
||||||
ATMM check: dr ≤ 3 (△) <br> |
|
||||||
<table style="margin-left:10px;"> |
|
||||||
<tr> |
|
||||||
<td style="width:20px;"> +1 |
|
||||||
<td> HS/crew |
|
||||||
<tr> |
|
||||||
<td> +2 |
|
||||||
<td> SMC |
|
||||||
<tr> |
|
||||||
<td> +1 |
|
||||||
<td> CX |
|
||||||
<tr> |
|
||||||
<td> +1 |
|
||||||
<td> vs. non-armored vehicle |
|
||||||
</table> |
|
||||||
original 6 = pinned (CCV reduced by 1) |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
||||||
|
|
@ -1,45 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<!-- vasl-templates:name KGS Molotov Cocktails --> |
|
||||||
<!-- vasl-templates:description Data chart for Molotov Cocktails in <i>Kampfgruppe Scherer</i>. --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
td { margin: 0 ; padding: 0 ; } |
|
||||||
ul { margin: 0 0 0 10px ; padding: 0 ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style="background: {{PLAYER_COLORS["german"][0]}} ; border-bottom: 1px solid {{PLAYER_COLORS["german"][2]}} ; padding: 2px 5px ; font-weight: bold ;"> |
|
||||||
{%if PLAYER_FLAGS["german"]%}<img src="{{PLAYER_FLAGS["german"]}}"> {%endif%}Molotov Cocktails |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td style="padding:0 5px;"> |
|
||||||
vs. AFV only <br> |
|
||||||
MOL check: dr ≤ 3 (△) <br> |
|
||||||
<table style="margin-left:10px;"> |
|
||||||
<tr> |
|
||||||
<td style="width:20px;"> +1 |
|
||||||
<td> HS/crew |
|
||||||
<tr> |
|
||||||
<td> +2 |
|
||||||
<td> SMC |
|
||||||
<tr> |
|
||||||
<td> +1 |
|
||||||
<td> CX |
|
||||||
</table> |
|
||||||
IFT DR original colored dr: |
|
||||||
<ul> |
|
||||||
<li> 1 = Flame in target Location |
|
||||||
<li> 6 = thrower breaks, Flame in their Location |
|
||||||
</ul> |
|
||||||
Kindling Attempt: +2 DRM |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
||||||
|
|
@ -1,7 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<!-- vasl-templates:name PF count --> |
|
||||||
<!-- vasl-templates:description Add the snippet as the label of a Panzerfaust counter, then press <i>Ctrl-L</i> when you need to update the number of remaining shots. --> |
|
||||||
<!-- vasl-templates:comment The HTML is deliberately malformed, so that the number of remaining shots is the last thing in snippet, which makes it easier to change during the course of a game. --> |
|
||||||
|
|
||||||
<div style="font-size:12px;font-weight:bold;"> {{PF_COUNT:/3|Number of PF shots}} |
|
@ -0,0 +1,9 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<!-- vasl-templates:name Victory Points --> |
||||||
|
<!-- vasl-templates:description Add a label to keep track of your victory points, and press <i>Ctrl-L</i> when you need to update them. --> |
||||||
|
<!-- vasl-templates:comment The HTML is deliberately malformed, so that the number remaining is the last thing in snippet, which makes it easier to change during the course of a game. --> |
||||||
|
|
||||||
|
<!-- vasl-templates:comment We don't include common.css because it's not necessary, and we want to keep the generated snippet short. --> |
||||||
|
<div style="font-size:110%"> |
||||||
|
<b>{{TYPE:Victory Points::Casualty VP::Exit VP/9|Type}}:</b> 0 |
@ -0,0 +1,57 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
td { padding: 2px 5px ; } |
||||||
|
li.comment { font-size: 96% ; font-style: italic ; color: #404040 ; } |
||||||
|
span.comment { font-size: 85% ; font-style: italic ; color: #404040 ; } |
||||||
|
.note-group { margin-top: 5px ; padding-top: 3px ; border-top: 1px solid #ccc ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table> |
||||||
|
|
||||||
|
<tr> <td style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
<nobr>{{INCLUDE:player_flag_large}}{{PLAYER_NAME|nbsp}} Capabilities</nobr> |
||||||
|
|
||||||
|
<tr> <td> |
||||||
|
|
||||||
|
{%if NAT_CAPS%} |
||||||
|
|
||||||
|
<ul> |
||||||
|
{%if NAT_CAPS.GRENADES%} <li class="grenades"> {{NAT_CAPS.GRENADES}} {%endif%} |
||||||
|
{%if NAT_CAPS.HOB_DRM%} <li class="hob-drm"> Heat of Battle: {{NAT_CAPS.HOB_DRM}} {%endif%} |
||||||
|
{%if NAT_CAPS.TH_COLOR%} <li class="th-color"> {{NAT_CAPS.TH_COLOR}} {%endif%} |
||||||
|
{%if NAT_CAPS.OBA_BLACK%} |
||||||
|
<li> OBA: <span class="oba-black">{{NAT_CAPS.OBA_BLACK}}</span> <span class="oba-red">{{NAT_CAPS.OBA_RED}}</span> |
||||||
|
{%if NAT_CAPS.OBA_ACCESS%} <span class="oba-access">(access: {{NAT_CAPS.OBA_ACCESS}})</span> {%endif%} |
||||||
|
{%if NAT_CAPS.OBA_COMMENTS%} |
||||||
|
<ul class="oba-comments"> {%for cmt in NAT_CAPS.OBA_COMMENTS%} <li class="comment"> {{cmt}} {%endfor%} </ul> |
||||||
|
{%endif%} |
||||||
|
{%endif%} |
||||||
|
</ul> |
||||||
|
|
||||||
|
{% for group in NAT_CAPS.NOTE_GROUPS %} |
||||||
|
<div class="note-group" {%if not group.CAPTION%}style="border-top:none;"{%endif%} > |
||||||
|
{%if group.CAPTION %}<div class="caption"> {{group.CAPTION}} </div>{%endif%} |
||||||
|
{%if group.NOTES %}<ul> {%for note in group.NOTES%} |
||||||
|
<li> {{note}} {%endfor%} |
||||||
|
</ul> {%endif%} |
||||||
|
</div> |
||||||
|
{%endfor%} |
||||||
|
|
||||||
|
{%else%} |
||||||
|
|
||||||
|
Not available. |
||||||
|
|
||||||
|
{%endif%} |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,467 @@ |
|||||||
|
{ |
||||||
|
|
||||||
|
"german": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "8B", "3R" ], "oba_access": "≤ 2", |
||||||
|
"hob_drm": "0 DRM", |
||||||
|
"grenades": "Smoke", |
||||||
|
"notes": [ |
||||||
|
"{? 10/1943- | Inherent PF | No Inherent PF | Inherent PF<sup>10/43+</sup> ?}", |
||||||
|
"{? 01/1944- | Inherent ATMM | No Inherent ATMM | Inherent ATMM<sup>44+</sup> ?}", |
||||||
|
{ "caption": "SS", "notes": [ |
||||||
|
"Disrupt & RtPh Surrender NA <br> vs Russians", |
||||||
|
"Massacre OK", |
||||||
|
"{? 01/1944- | Squad Assault Fire | No Squad Assault Fire | Squad Assault Fire<sup>44+</sup> ?}" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"russian": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "5B", "2R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+2 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Massacre OK", |
||||||
|
"Deploy NA", |
||||||
|
"Entrench -1 DRM", |
||||||
|
"{? -10/1942 | Commissars | Commissars NA | Commissars<sup>-10/42</sup> ?}", |
||||||
|
"Human Wave", |
||||||
|
"{? 01/1942- | Riders OK | Riders NA | Riders<sup>42+</sup> ?}" |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"american": { |
||||||
|
"th_color": "{? 01/1944- | Black | Red | Black<sup>44+</sup> ?}", |
||||||
|
"oba": [ "10B", "3R", "Plentiful Ammo included" ], |
||||||
|
"oba_access": "≤ 2", |
||||||
|
"hob_drm": "0 DRM", |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
{ "caption": "U.S.M.C.", "notes": [ |
||||||
|
"Disruption NA", |
||||||
|
"7-6-8 can Self-Deploy", |
||||||
|
"Vehicle [EXC: LC] Crew: Army 1-2-6" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
"kfw-american": { |
||||||
|
"th_color": "{! 06/1950-08/1950 = Red | 09/1950- = Black | ??? !}", |
||||||
|
"oba": [ "{! 06/1950-08/1950 = 9B | 09/1950- = 10B | ??? !}", "3R", |
||||||
|
"{! 09/1950- = Plentiful Ammo included !}" |
||||||
|
], |
||||||
|
"oba_access": "≤ 2", |
||||||
|
"hob_drm": [ "0 DRM", "+3 for Katusa; NA for TACP" ], |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
"{! 06/1950-08/1950 = Early KW U.S. Army rules: <ul> <li> Always Lax <li> Ammo Shortage <li> SW repair only on \"1\" <li> Radio/Phone Contact reduced by 1 <li> AFV Inherent Crews have Morale 7 <li> All motorized vehicles have Red MP </ul> !}", |
||||||
|
"Disruption NA", |
||||||
|
"7-6-8 can Self-Deploy", |
||||||
|
"Use 5-5-8 when: <ul> <li> U.S.M.C. ELR Replacement is in effect <li> U.S.M.C. MMC re-arms </ul>", |
||||||
|
{ "caption": "Rangers (6-6-8)", "notes": [ |
||||||
|
"Self-Rally OK", |
||||||
|
"Self-Deploy (1TC) & Self-Recombine OK", |
||||||
|
"Cowering NA", |
||||||
|
"Commandos", |
||||||
|
"No Non-Qualified Use penalty for RCL", |
||||||
|
"No Captured Use penalty for Communist SW" |
||||||
|
] }, |
||||||
|
{ "caption": "Airborne (6-6-7)", "allow_empty": true }, |
||||||
|
{ "caption": "Katusa", "notes": [ |
||||||
|
"As U.S. Army MMC", |
||||||
|
"HoB +3 DRM", |
||||||
|
"Leader Creation +1 drm", |
||||||
|
"{! 09/1950-10/1951 = ELR 2 !}", |
||||||
|
"{! 09/1950-10/1951 = Allied Troop penalties with U.S. leaders !}" |
||||||
|
] }, |
||||||
|
{ "caption": "Tactical Air Control Party", "notes": [ |
||||||
|
"Inherent Radio (Contact = 9)", |
||||||
|
"May set up HIP" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"british": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "8B", "2R" ], "oba_access": "≤ 2", |
||||||
|
"hob_drm": "-1 DRM", |
||||||
|
"grenades": "{? 01/1944- | SMOKE | Smoke | SMOKE<sup>44+</sup> ?}", |
||||||
|
"notes": [ |
||||||
|
{ "caption": "Elite & 1st Line", "notes": [ |
||||||
|
"Cowering NA" |
||||||
|
] }, |
||||||
|
{ "caption": "ANZAC", "notes": [ |
||||||
|
"Stealthy (unless Green)" |
||||||
|
] }, |
||||||
|
{ "caption": "Gurkha", "notes": [ |
||||||
|
"-1 CC DRM", |
||||||
|
"Disrupt & RtPh Surrender NA", |
||||||
|
"Commando (unless Green)", |
||||||
|
"Stealthy" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"french": { |
||||||
|
"th_color": [ "Black", "AFV use Red TH#" ], |
||||||
|
"oba": [ "6B", "2R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+1 DRM", |
||||||
|
"grenades": "Smoke" |
||||||
|
}, |
||||||
|
"free-french": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "8B", "2R" ], "oba_access": "≤ 2", |
||||||
|
"hob_drm": "-1 DRM", |
||||||
|
"grenades": "{? 01/1944- | SMOKE | Smoke | SMOKE<sup>44+</sup> ?}", |
||||||
|
"notes": [ |
||||||
|
"{? 12/1943- | Assault Fire | No Assault Fire | Assault Fire<sup>12/43+</sup> ?}", |
||||||
|
"{? 12/1943-05/1945 | Inherent Crews as British for Morale | | Inherent Crews as British for Morale<sup>12/43-5/45</sup> ?}", |
||||||
|
{ "caption": "Elite & 1st Line", "notes": [ |
||||||
|
"Cowering NA" |
||||||
|
] }, |
||||||
|
{ "caption": "No Captured Use penalty", "notes": [ |
||||||
|
"U.S. MTR/BAZ", |
||||||
|
"Vichy French SW", |
||||||
|
"{? -11/1943 | British (f) vehicles/Guns/SW | | British (f) vehicles/Guns/SW <sup>-11/43</sup> ?}", |
||||||
|
"{? 12/1943-05/1945 | British/French (a)/(f) SW | | British/French (a)/(f) SW<sup>12/43-5/45</sup> ?}" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
|
||||||
|
"italian": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "7B", "3R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+3 DRM", |
||||||
|
"grenades": "Smoke", |
||||||
|
"notes": [ |
||||||
|
"Escape NA", |
||||||
|
{ "caption": "1st Line & Conscript", "notes": [ |
||||||
|
"Surrender on HoB Final DR ≥ 10", |
||||||
|
"Deploy NA", |
||||||
|
"+1 CC Capture DRM NA", |
||||||
|
"Always Lax", |
||||||
|
"1 PAATC" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"finnish": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ |
||||||
|
"{! 01/1939-12/1940 = 6B | 01/1941-12/1942 = 7B | 01/1943-09/1944 = 8B | 10/1944- = 7B | ??? !}", |
||||||
|
"3R", |
||||||
|
"Plentiful Ammo included" |
||||||
|
], |
||||||
|
"oba_access": "≤ 1", |
||||||
|
"hob_drm": "-1 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Deploy (1TC) & Recombine without Leader", |
||||||
|
"Self-Rally OK [EXC: Conscript]", |
||||||
|
"Cowering NA [EXC: Conscript]", |
||||||
|
"Ski-trained (don Skis = one MF)", |
||||||
|
"Leader Creation NA", |
||||||
|
"Captured Use penalties NA for Russian MG <br> [EXC: LMG in 1939; .50-cal]", |
||||||
|
{ "caption": "Elite & 1st Line", "notes": [ |
||||||
|
"Always Stealthy", |
||||||
|
"Use FT/DC as Elite", |
||||||
|
"{? 07/1944- | Inherent PF | No Inherent PF | Inherent PF<sup>7/44+</sup> ?}" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"swedish": { |
||||||
|
"th_color": [ "Red", "[EXC: MG]" ], |
||||||
|
"oba": [ "6B", "3R"], |
||||||
|
"hob_drm": "0 DRM", |
||||||
|
"notes": [ |
||||||
|
"Extreme Winter effects NA", |
||||||
|
{ "caption": "1<sup>st</sup> Line", "notes": [ |
||||||
|
"Battle Hardening → Fanatic" |
||||||
|
] }, |
||||||
|
{ "caption": "Allied Troops", "notes": [ |
||||||
|
"Captured Use penalties NA" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"axis-minor": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "6B", "3R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+3 DRM", |
||||||
|
"grenades": "Smoke", |
||||||
|
"notes": [ |
||||||
|
"Escape NA", |
||||||
|
{ "caption": "1st Line & Conscript", "notes": [ |
||||||
|
"1 PAATC", |
||||||
|
"Surrender on HoB Final DR ≥ 10" |
||||||
|
] }, |
||||||
|
{ "caption": "Romanian, Hungarian", "notes": [ |
||||||
|
"{! -02/1944 = No Inherent PF | 03/1944-05/1944 = | 06/1944- = Inherent PF in non-Crew MMC | Inherent PF <span class='comment'>(Romanian<sup>3/44+</sup>, Hungarian<sup>6/44+</sup>)</span> !}" |
||||||
|
] }, |
||||||
|
{ "caption": "Romanian", "notes": [ |
||||||
|
"{! 03/1944-05/1944 = Inherent PF in non-Crew MMC !}", |
||||||
|
"{? 07/1943- | Inherent ATMM in non-Crew Elite <br> and 1st Line MMC (-2 CC DRM) | No Inherent ATMM | Inherent ATMM<sup>(7/43+)</sup> ?}" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"allied-minor": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "6B", "3R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+2 DRM", |
||||||
|
"grenades": "Smoke", |
||||||
|
"notes": [ |
||||||
|
"+1 Broken Morale vs Italians", |
||||||
|
{ "caption": "1st Line & Green", "notes": [ |
||||||
|
"1 PAATC" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"japanese": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "5B", "2R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+4 DRM", |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
"SMC PTC/Pin/Break NA", |
||||||
|
"Tank-Hunter Heroes & ATMM", |
||||||
|
"Banzai Charge (always Lax)", |
||||||
|
"ATR/MMG/HMG Breakdown penalty", |
||||||
|
"LLMC → LLTC if unbroken", |
||||||
|
"Massacre OK", |
||||||
|
"-1 Interrogation DRM", |
||||||
|
"-2 Concealment drm", |
||||||
|
"Enemy +2 search drm", |
||||||
|
"Hand-to-Hand CC & Hara-Kiri", |
||||||
|
{ "caption": "Leaders", "notes": [ |
||||||
|
"Replacement NA", |
||||||
|
"Casualty MC → elimination", |
||||||
|
"Morale/Rally/Berserk as Commissar" |
||||||
|
] }, |
||||||
|
{ "caption": "Elite & 1st Line", "notes": [ |
||||||
|
"Always Stealthy" |
||||||
|
] }, |
||||||
|
{ "caption": "Conscript", "notes": [ |
||||||
|
"Always Lax" |
||||||
|
] }, |
||||||
|
{ "caption": "Always NA", "notes": [ |
||||||
|
"PAATC", |
||||||
|
"Escape", |
||||||
|
"RtPh Surrender", |
||||||
|
"Disruption", |
||||||
|
"Encircled lower Morale", |
||||||
|
"Leader Creation" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"chinese~gmd": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "5B", "2R", |
||||||
|
"6B/2R if Majority Squad Type is 5-3-7", |
||||||
|
"5B/3R if Majority Squad Type is 3-3-7 or 3-3-6" |
||||||
|
], |
||||||
|
"oba_access": "≤ 1", |
||||||
|
"hob_drm": "0 DRM", |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
"Deploy NA", |
||||||
|
"Lax at Night", |
||||||
|
"+1 Leader Creation drm", |
||||||
|
"Human Wave", |
||||||
|
"Dare-Death Squads [EXC: 5-3-7]", |
||||||
|
{ "caption": "1st Line & Conscript", "notes": [ |
||||||
|
"1 PAATC" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"chinese": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": null, |
||||||
|
"hob_drm": "+1 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Cowering NA", |
||||||
|
"Commissars", |
||||||
|
"Human Wave", |
||||||
|
"Dare-Death Squads" |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"partisan": { |
||||||
|
"th_color": [ "Red", "[EXC: ATR/MG]" ], |
||||||
|
"notes": [ |
||||||
|
"Stealthy when Good Order", |
||||||
|
"Never Elite/Inexperienced", |
||||||
|
"ELR 5", |
||||||
|
"Leadership NA for non-Partisan units", |
||||||
|
"Massacre OK", |
||||||
|
"RtPh Surrender NA", |
||||||
|
"Disrupt NA" |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"kfw-rok": { |
||||||
|
"_comment_": "Errata (ASLJ 13 p48): KMC: '9/50+ black' s.b. '8/50+ black", |
||||||
|
"th_color": "{! -07/1950 = Red | 08/1950-04/1951 = Red (ROK) ; Black (KMC) | 05/1951- = Black | ??? !}", |
||||||
|
"oba": [ "{! 06/1950- = 10B | ??? !}", "3R", |
||||||
|
"{? 09/1950- | Plentiful Ammo included | Plentiful Ammo included (KMC) | Plentiful Ammo included (ROK: 9/50+) ?}", |
||||||
|
"{! 06/1950-08/1950 = ROK: 6B/3R !}" |
||||||
|
], |
||||||
|
"oba_access": "≤ 1 (ROK) ; 2 (KMC)", |
||||||
|
"hob_drm": "+3/+4 DRM", |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
{ "caption": "Republic of Korea (ROK)", "notes": [ |
||||||
|
"{! 06/1946-04/1951 = Early KW ROK rules !}", |
||||||
|
"1st Line MMC: <ul> <li> Battle-Harden to Fanatic </ul>", |
||||||
|
"2nd Line & Conscript MMC: <ul> <li> Always Lax <li> Deploy NA", |
||||||
|
"{? -10/1950 | Human Bullets | | Human Bullets (pre-11/50) ?}" |
||||||
|
] }, |
||||||
|
{ "caption": "Korean Marine Corps (KMC)", "notes": [ |
||||||
|
"{! 04/1949-07/1950 = Japanese-Armed KMC | 08/1950- = U.S.-Armed KMC !}", |
||||||
|
"{? -01/1951 | SW B#/X#/ROF penalty | | SW B#/X#/ROF penalty (pre-2/51) ?}" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"kfw-bcfk": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "8B", "2R" ], "oba_access": "≤ 2", |
||||||
|
"hob_drm": "-1 DRM", |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
{ "caption": "2nd Line MMC", "notes": [ |
||||||
|
"ELR Replacement → Disrupt" |
||||||
|
] }, |
||||||
|
{ "caption": "Canadian", "notes": [ |
||||||
|
"{? 01/1952- | Squads have Assault Fire | | Squads have Assault Fire<sup>1/52+</sup> ?}" |
||||||
|
] }, |
||||||
|
{ "caption": "Royal Marines", "notes": [ |
||||||
|
"Commandos", |
||||||
|
"No Non-Qualified Use penalty for RCL", |
||||||
|
"No Captured Use penalty for Communist SW", |
||||||
|
"Self-Deploy (1TC) & Self-Recombine OK" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"kfw-ounc": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "9B", "3R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": [ "0 DRM", "+3 for Turkish" ], |
||||||
|
"grenades": "SMOKE", |
||||||
|
"notes": [ |
||||||
|
{ "caption": "2nd Line MMC", "notes": [ |
||||||
|
"ELR Replacement → Disrupt [EXC: Turkish]" |
||||||
|
] }, |
||||||
|
{ "caption": "Ethiopian, French, Turkish", "notes": [ |
||||||
|
"Bayonet Charge NTC NA for leaders" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"kfw-kpa": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "5B", "2R" ], "oba_access": "≤ 1", |
||||||
|
"hob_drm": "+2 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Suicide Heroes", |
||||||
|
"Starshell restrictions", |
||||||
|
{ "caption": "As Russian", "notes": [ |
||||||
|
"Elite Personnel always Stealthy", |
||||||
|
"Elite Squads may Deploy", |
||||||
|
"Commissars", |
||||||
|
"Massacre OK", |
||||||
|
"Human Wave by SSR only" |
||||||
|
] }, |
||||||
|
{ "caption": "Assault Engineers", "notes": [ |
||||||
|
"WP grenades" |
||||||
|
] }, |
||||||
|
{ "caption": "Communist Partisans", "notes": [ |
||||||
|
"Neither Elite nor Conscript/Green", |
||||||
|
"Always Stealthy", |
||||||
|
"Massacre OK", |
||||||
|
"Disrupt & RtPh Surrender NA" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"kfw-cpva": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ |
||||||
|
"{? 04/1951- | 7B | | 7B<sup>4/51+</sup> ?}", |
||||||
|
"{! 04/1951-09/1952 = 3R | 10/1952- = 2R | ??? !}" |
||||||
|
], |
||||||
|
"oba_access": "≤ 1", |
||||||
|
"hob_drm": "+1 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Always Stealthy", |
||||||
|
"Starshell restrictions", |
||||||
|
"Armored Assault NA", |
||||||
|
"Riders NA", |
||||||
|
"{! 10/1950-03/1951 = Early KW CPVA rules !}", |
||||||
|
"Leaders & Political Officers increase Morale <br> as if Commissar", |
||||||
|
"SW B#/X#/ROF penalty", |
||||||
|
"Restricted Fire", |
||||||
|
"Infantry Platoon Movement", |
||||||
|
"Hand-to-Hand CC (-1 DRM)", |
||||||
|
"HS Infantry Overrun", |
||||||
|
"Bugles", |
||||||
|
"Entrench -1 DRM", |
||||||
|
"PAATC NTC NA", |
||||||
|
"Infantry Overrun NTC NA", |
||||||
|
"Conceal if +2 Hindrance", |
||||||
|
"Concealment -1 drm", |
||||||
|
"Civilian Interrogation is always in effect", |
||||||
|
{ "caption": "Assault Engineers", "notes": [ |
||||||
|
"WP grenades" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"burmese": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": null, |
||||||
|
"hob_drm": "+2 DRM", |
||||||
|
"grenades": null, |
||||||
|
"notes": [ |
||||||
|
"Dare-Death Squads (as if Chinese)", |
||||||
|
"Deploy NA [EXC: A20.5 & A21.22]; Recombine OK", |
||||||
|
{ "caption": "Elite and 1st Line MMC", "notes": [ |
||||||
|
"Always Stealthy" |
||||||
|
] }, |
||||||
|
{ "caption": "Leaders", "notes": [ |
||||||
|
"Morale/Berserk/Rally as Commissar" |
||||||
|
] } |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"indonesian": { |
||||||
|
"th_color": "Red", |
||||||
|
"oba": [ "5B", "3R" ], |
||||||
|
"hob_drm": "+3 DRM", |
||||||
|
"grenades": "Smoke", |
||||||
|
"notes": [ |
||||||
|
"Tank-Hunter/DC Heroes (as if 1945 Japanese)", |
||||||
|
"Hand-to-Hand Combat", |
||||||
|
"Massacre OK", |
||||||
|
"HoB DR ≥ 12 → Berserk", |
||||||
|
"Deploy NA [EXC: A20.5 & A21.22]; Recombine OK" |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"thai": { |
||||||
|
"th_color": "Black", |
||||||
|
"oba": [ "7B", "3R" ], |
||||||
|
"hob_drm": "0 DRM", |
||||||
|
"grenades": "Smoke" |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
.ma-note { margin: 2px 0 3px 0 ; text-align: justify ; } |
||||||
|
.ma-note .key { font-weight: bold ; } |
||||||
|
.ma-note p { margin-top: 2px ; } |
||||||
|
.ma-note table { margin-left: 10px ; } |
||||||
|
.ma-note li { margin-bottom: 2px ; } |
||||||
|
.ma-note .example { font-size: 95% ; font-style: italic ; color: #000080 ; } |
||||||
|
.ma-note p.errata { margin-top: 0 ; font-size: 95% ; font-style: italic ; color: #704040 ; } |
||||||
|
.ma-note span.errata { font-style: italic ; color: #704040 ; } |
||||||
|
.ma-note table { margin-left: 10px ; margin-top: -5px ; } |
||||||
|
.ma-note table th { padding: 2px 10px 2px 5px ; text-align: left ; background: #f0f0f0 ; } |
||||||
|
.ma-note table td { padding: 0 10px 0 5px ; } |
||||||
|
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; } |
||||||
|
.disabled { color: #808080 ; } |
||||||
|
.disabled .exc { color: #808080 ; } |
||||||
|
.slashed { text-decoration: line-through ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
<table style=" |
||||||
|
{%if OB_MA_NOTES_WIDTH%} width: {{OB_MA_NOTES_WIDTH}} ; {%endif%} |
||||||
|
"> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
"> |
||||||
|
{{INCLUDE:player_flag_large}}{{PLAYER_NAME}} {{VO_TYPE}} Notes |
||||||
|
|
||||||
|
{%if OB_MA_NOTES%} |
||||||
|
<tr> <td style="padding:0 5px;"> |
||||||
|
{%for ma_note in OB_MA_NOTES%} |
||||||
|
{%if not ma_note[0]%} <div class="disabled"> {%endif%} |
||||||
|
<div class="ma-note"> {{ma_note[1]}} </div> |
||||||
|
{%if not ma_note[0]%} </div> {%endif%} |
||||||
|
{%endfor%} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
{%if OB_EXTRA_MA_NOTES%} |
||||||
|
<tr> <td style="padding:0 5px;"> |
||||||
|
{%if OB_EXTRA_MA_NOTES_CAPTION%} <div class="extra-notes-caption"> {{OB_EXTRA_MA_NOTES_CAPTION}} </div> {%endif%} |
||||||
|
{%for ma_note in OB_EXTRA_MA_NOTES%} |
||||||
|
{%if not ma_note[0]%} <div class="disabled"> {%endif%} |
||||||
|
<div class="ma-note"> {{ma_note[1]}} </div> |
||||||
|
{%if not ma_note[0]%} </div> {%endif%} |
||||||
|
{%endfor%} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |
@ -1,43 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
td { margin: 0 ; padding: 0 ; } |
|
||||||
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; } |
|
||||||
sup { font-size: 75% ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table style=" |
|
||||||
{%if OB_ORDNANCE_WIDTH%} width: {{OB_ORDNANCE_WIDTH}} ; {%endif%} |
|
||||||
"> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{PLAYER_NAME}} Ordnance |
|
||||||
|
|
||||||
{%for ord in OB_ORDNANCE%} |
|
||||||
<tr style="border-bottom:1px dotted #e0e0e0;"> |
|
||||||
<td valign="top" style="padding:2px 5px 5px;"> |
|
||||||
<b> {{ord.name}} </b> |
|
||||||
{%if ord.image%} <br> <img src="{{ord.image}}"> {%endif%} |
|
||||||
<div class="note"> |
|
||||||
{%if ord.notes%} |
|
||||||
{{ord.note_number}}, {{ord.notes | join(", ")}} |
|
||||||
{%else%} |
|
||||||
{{ord.note_number}} |
|
||||||
{%endif%} |
|
||||||
</div> |
|
||||||
<td valign="top" style="padding:2px 5px;"> |
|
||||||
{%for cap in ord.capabilities%} <div> {{cap}} </div> {%endfor%} |
|
||||||
{%endfor%} |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -1,43 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
.ma-note .key { font-weight: bold ; } |
|
||||||
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; } |
|
||||||
ul { margin: 0 0 0 15px ; padding: 0 ; } |
|
||||||
sup { font-size: 75% ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table style=" |
|
||||||
{%if OB_ORDNANCE_MA_NOTES_WIDTH%} width: {{OB_ORDNANCE_MA_NOTES_WIDTH}} ; {%endif%} |
|
||||||
"> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{PLAYER_NAME}} Ordnance Notes |
|
||||||
|
|
||||||
{%if OB_ORDNANCE_MA_NOTES%} |
|
||||||
<tr> <td style="padding: 0 5px;"> |
|
||||||
{%for ma_note in OB_ORDNANCE_MA_NOTES%} |
|
||||||
<div class="ma-note"> {{ma_note}} </div> |
|
||||||
{%endfor%} |
|
||||||
{%endif%} |
|
||||||
|
|
||||||
{%if OB_ORDNANCE_EXTRA_MA_NOTES%} |
|
||||||
<tr> <td style="padding: 0 5px;"> |
|
||||||
{%if OB_ORDNANCE_EXTRA_MA_NOTES_CAPTION%} <div class="extra-notes-caption"> {{OB_ORDNANCE_EXTRA_MA_NOTES_CAPTION}} </div> {%endif%} |
|
||||||
{%for ma_note in OB_ORDNANCE_EXTRA_MA_NOTES%} |
|
||||||
<div class="ma-note"> {{ma_note}} </div> |
|
||||||
{%endfor%} |
|
||||||
{%endif%} |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -1,23 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{ORDNANCE_NAME}} |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td> <img src="{{ORDNANCE_NOTE_URL}}"> |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -1,23 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{VEHICLE_NAME}} |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td> <img src="{{VEHICLE_NOTE_URL}}"> |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -1,43 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
td { margin: 0 ; padding: 0 ; } |
|
||||||
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; } |
|
||||||
sup { font-size: 75% ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table style=" |
|
||||||
{%if OB_VEHICLES_WIDTH%} width: {{OB_VEHICLES_WIDTH}} ; {%endif%} |
|
||||||
"> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{PLAYER_NAME}} Vehicles |
|
||||||
|
|
||||||
{%for veh in OB_VEHICLES%} |
|
||||||
<tr style="border-bottom:1px dotted #e0e0e0;"> |
|
||||||
<td valign="top" style="padding:2px 5px 5px;"> |
|
||||||
<b> {{veh.name}} </b> |
|
||||||
{%if veh.image%} <br> <img src="{{veh.image}}"> {%endif%} |
|
||||||
<div class="note"> |
|
||||||
{%if veh.notes%} |
|
||||||
{{veh.note_number}}, {{veh.notes | join(", ")}} |
|
||||||
{%else%} |
|
||||||
{{veh.note_number}} |
|
||||||
{%endif%} |
|
||||||
</div> |
|
||||||
<td valign="top" style="padding:2px 5px;"> |
|
||||||
{%for cap in veh.capabilities%} <div> {{cap}} </div> {%endfor%} |
|
||||||
{%endfor%} |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -1,43 +0,0 @@ |
|||||||
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
|
||||||
|
|
||||||
<head> |
|
||||||
<meta charset="utf-8"> |
|
||||||
<style> |
|
||||||
.ma-note .key { font-weight: bold ; } |
|
||||||
.extra-notes-caption { border: 1px solid #e0e0e0 ; background: #fcfcfc ; font-weight: bold ; padding: 2px 5px ; } |
|
||||||
ul { margin: 0 0 0 15px ; padding: 0 ; } |
|
||||||
sup { font-size: 75% ; } |
|
||||||
</style> |
|
||||||
</head> |
|
||||||
|
|
||||||
<table style=" |
|
||||||
{%if OB_VEHICLES_MA_NOTES_WIDTH%} width: {{OB_VEHICLES_MA_NOTES_WIDTH}} ; {%endif%} |
|
||||||
"> |
|
||||||
|
|
||||||
<tr> |
|
||||||
<td colspan="2" style=" |
|
||||||
background: {{OB_COLOR}} ; |
|
||||||
border-bottom: 1px solid {{OB_COLOR_2}} ; |
|
||||||
padding: 2px 5px ; |
|
||||||
font-weight: bold ; |
|
||||||
"> |
|
||||||
{%if PLAYER_FLAG%}<img src="{{PLAYER_FLAG}}"> {%endif%}{{PLAYER_NAME}} Vehicle Notes |
|
||||||
|
|
||||||
{%if OB_VEHICLES_MA_NOTES%} |
|
||||||
<tr> <td style="padding: 0 5px;"> |
|
||||||
{%for ma_note in OB_VEHICLES_MA_NOTES%} |
|
||||||
<div class="ma-note"> {{ma_note}} </div> |
|
||||||
{%endfor%} |
|
||||||
{%endif%} |
|
||||||
|
|
||||||
{%if OB_VEHICLES_EXTRA_MA_NOTES%} |
|
||||||
<tr> <td style="padding: 0 5px;"> |
|
||||||
{%if OB_VEHICLES_EXTRA_MA_NOTES_CAPTION%} <div class="extra-notes-caption"> {{OB_VEHICLES_EXTRA_MA_NOTES_CAPTION}} </div> {%endif%} |
|
||||||
{%for ma_note in OB_VEHICLES_EXTRA_MA_NOTES%} |
|
||||||
<div class="ma-note"> {{ma_note}} </div> |
|
||||||
{%endfor%} |
|
||||||
{%endif%} |
|
||||||
|
|
||||||
</table> |
|
||||||
|
|
||||||
</html> |
|
@ -0,0 +1,4 @@ |
|||||||
|
{# Some versions of Java require <img> tags to have the width and height specified!?! #} |
||||||
|
{%if vo.image%} <img src="{{vo.image}}" |
||||||
|
{%if vo.small_piece%} width="48" height="48" {%else%} width="60" height="60" {%endif%} |
||||||
|
> {%endif%} |
@ -0,0 +1,85 @@ |
|||||||
|
<html> <!-- vasl-templates:id {{SNIPPET_ID}} --> |
||||||
|
|
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<style> |
||||||
|
{{CSS:common}} |
||||||
|
.note { font-size: 90% ; font-style: italic ; color: #808080 ; white-space: nowrap ; } |
||||||
|
.capability { white-space: nowrap ; } |
||||||
|
.capability .brewup { color: #a04010 ; } |
||||||
|
.comment { font-size: 96% ; font-style: italic ; color: #404040 ; white-space: nowrap ; } |
||||||
|
.comment .split-mg-red { color: #a04010 ; } |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
|
||||||
|
{# NOTE: We set a narrow width to stop lots of notes making us very wide. #} |
||||||
|
<table style=" |
||||||
|
{%if OB_VO_WIDTH%} width: {{OB_VO_WIDTH}} ; {%else%} width: 1px ; {%endif%} |
||||||
|
"> |
||||||
|
|
||||||
|
<tr> |
||||||
|
<td colspan="2" style=" |
||||||
|
background: {{OB_COLOR}} ; |
||||||
|
border-bottom: 1px solid {{OB_COLOR_2}} ; |
||||||
|
padding: 2px 5px ; |
||||||
|
font-size: 105% ; font-weight: bold ; |
||||||
|
white-space: nowrap ; |
||||||
|
"> |
||||||
|
{# CSS "white-space:nowrap" doesn't always work in VASSAL, we need to use <nobr> and here :-/ #} |
||||||
|
<nobr>{{INCLUDE:player_flag_large}}{{PLAYER_NAME|nbsp}} {{VO_TYPES}}</nobr> |
||||||
|
|
||||||
|
{%for vo in OB_VO%} |
||||||
|
|
||||||
|
{% if vo.index == 0 %} |
||||||
|
<tr> |
||||||
|
{% set PADDING_TOP = 2 %} |
||||||
|
{%else%} |
||||||
|
<tr style="border-top:1px dotted #e0e0e0;"> |
||||||
|
{% set PADDING_TOP = 5 %} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
{% if vo.name_len <= MAX_VO_NAME_LEN %} |
||||||
|
{# NOTE: If the vehicle/ordnance name is short, put the capabilities to the right of it. #} |
||||||
|
<td valign="top" style="padding:{{PADDING_TOP}} 5px 2px 5px;"> |
||||||
|
{{INCLUDE:ob_vo.name}} <br> |
||||||
|
{{INCLUDE:ob_vo.image}} |
||||||
|
{% set MAX_CAPABILITIES = 4 %} |
||||||
|
{%else%} |
||||||
|
{# NOTE: If the vehicle/ordnance name is long, put it on its own line, and the capabilities underneath. #} |
||||||
|
<td colspan="2" valign="top" style="padding:{{PADDING_TOP}} 5px 0 5px;"> |
||||||
|
{{INCLUDE:ob_vo.name}} |
||||||
|
<tr> |
||||||
|
<td valign="top" style="padding:0 5px 2px 5px;"> |
||||||
|
{{INCLUDE:ob_vo.image}} |
||||||
|
{% set MAX_CAPABILITIES = 3 %} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
{% if vo.small_piece %} |
||||||
|
{% set MAX_CAPABILITIES = MAX_CAPABILITIES - 1 %} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
{% if vo.capabilities_len > MAX_CAPABILITIES or !vo.image %} |
||||||
|
{# NOTE: If there are a lot of capabilities, tuck the note number & notes under the image. #} |
||||||
|
{# But if there is no image, we always do this, and squeeze them in to the left of the capabilities. #} |
||||||
|
<div class="note" style="margin-top:5px;"> |
||||||
|
{{INCLUDE:ob_vo.notes}} |
||||||
|
</div> |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
<td valign="top" style="padding:5px 5px 2px 5px;"> |
||||||
|
{%for cap in vo.capabilities%} <div class="capability"> {{cap}} </div> {%endfor%} |
||||||
|
{%for cmnt in vo.comments%} <div class="comment"> {{cmnt}} </div> {%endfor%} |
||||||
|
|
||||||
|
{% if vo.capabilities_len <= MAX_CAPABILITIES and vo.image %} |
||||||
|
{# NOTE: If there are only a few capabilities, let the note number & notes spread full-width. #} |
||||||
|
{# But if there is no image, we never do this (see above). #} |
||||||
|
<tr> |
||||||
|
<td class="note" valign="top" colspan="2" style="padding:2px 5px;"> |
||||||
|
{{INCLUDE:ob_vo.notes}} |
||||||
|
{%endif%} |
||||||
|
|
||||||
|
{%endfor%} |
||||||
|
|
||||||
|
</table> |
||||||
|
|
||||||
|
</html> |