mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
100 commits
ec712463b7
...
6b64de08dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b64de08dd | ||
|
|
75ee553fab | ||
|
|
accfd151ed | ||
|
|
41076070a6 | ||
|
|
a373c0e4ef | ||
|
|
67b52e53be | ||
|
|
0f8a9f4faf | ||
|
|
79c94c99cb | ||
|
|
192454f737 | ||
|
|
438f874292 | ||
|
|
0123a28018 | ||
|
|
e34bdc87f4 | ||
|
|
1c2dbf2eb0 | ||
|
|
df72c1aad1 | ||
|
|
40ab70b804 | ||
|
|
f90804c78f | ||
|
|
d6bdf51d83 | ||
|
|
ae936e8177 | ||
|
|
06e199b12a | ||
|
|
84567e66c2 | ||
|
|
c4e7364802 | ||
|
|
867bdf318b | ||
|
|
e64a138f92 | ||
|
|
4a7b7e7df1 | ||
|
|
fa90ddc8b6 | ||
|
|
764a420797 | ||
|
|
8a2bb10827 | ||
|
|
af9d102126 | ||
|
|
822092fcb4 | ||
|
|
6e79dc2f70 | ||
|
|
6fdcd68172 | ||
|
|
e4f4030a5e | ||
|
|
2fb6347125 | ||
|
|
7aa756f6a4 | ||
|
|
e712601602 | ||
|
|
865ca4cd55 | ||
|
|
dcb7cef308 | ||
|
|
71be79791f | ||
|
|
9ab89b01c2 | ||
|
|
dcce6666ae | ||
|
|
202bdb133a | ||
|
|
46981d2b90 | ||
|
|
49c887b7ca | ||
|
|
91a37e32f6 | ||
|
|
139b08fe6d | ||
|
|
dc6da217fd | ||
|
|
e41dd2d3a4 | ||
|
|
c7953026dd | ||
|
|
0340cc9abd | ||
|
|
aeeda05433 | ||
|
|
0da1e0c398 | ||
|
|
dc1e7179a3 | ||
|
|
b6d9d87ba9 | ||
|
|
839226d542 | ||
|
|
4125e5b645 | ||
|
|
f781181899 | ||
|
|
e2f5b20922 | ||
|
|
b3165505b6 | ||
|
|
15b9a39b00 | ||
|
|
cf7b8aab39 | ||
|
|
df06b6895e | ||
|
|
a80446eb59 | ||
|
|
4122f0bfb3 | ||
|
|
aae1bbbab2 | ||
|
|
8523fa5ab4 | ||
|
|
8b1e78ae1e | ||
|
|
09671b49a2 | ||
|
|
327e70ea82 | ||
|
|
f9de339f85 | ||
|
|
ea1026a1cc | ||
|
|
2520c45220 | ||
|
|
801d21671e | ||
|
|
931c132b1b | ||
|
|
6ed963b94c | ||
|
|
a784659158 | ||
|
|
52b40aaa4f | ||
|
|
27fbf02ff4 | ||
|
|
f1a9c503b8 | ||
|
|
f39e71cca0 | ||
|
|
6456bccebe | ||
|
|
0cc025f675 | ||
|
|
05d434830f | ||
|
|
2522c305dd | ||
|
|
7e77065c56 | ||
|
|
6dd8139b43 | ||
|
|
c2c8f5bdf2 | ||
|
|
1f7d384c29 | ||
|
|
2f27dcc97d | ||
|
|
6e6864fd7c | ||
|
|
702e3f1113 | ||
|
|
f3b71a1fbf | ||
|
|
e02a2e0b31 | ||
|
|
ab4a9d6def | ||
|
|
dd396f19b6 | ||
|
|
ca8a0a5611 | ||
|
|
663f5c5ff6 | ||
|
|
0e6e0280fc | ||
|
|
adfe6b0c36 | ||
|
|
eae57d9aab | ||
|
|
91a4b834a8 |
267 changed files with 23389 additions and 20170 deletions
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
lfscheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Fetch the base branch
|
||||
run: git fetch origin ${{ env.BASE_SHA }}
|
||||
|
|
@ -24,27 +24,14 @@ jobs:
|
|||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Add remote fork origin for LFS
|
||||
run: |
|
||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
||||
git remote add ${{ gitea.actor }} https://gitea.wildfiregames.com/${PR_REPO}.git
|
||||
- name: Workaround for authentication problem with LFS
|
||||
# https://gitea.com/gitea/act_runner/issues/164
|
||||
run: |
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${{ gitea.repository }}.git/info/lfs/objects/.extraheader ''
|
||||
|
||||
PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}"
|
||||
EXTRAHEADER="$(git config --get --local http.${{ gitea.server_url }}/.extraheader)"
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/batch.extraheader \
|
||||
'${EXTRAHEADER}'
|
||||
git config --local \
|
||||
http.${{ gitea.server_url }}/${PR_REPO}.git/info/lfs/objects/.extraheader ''
|
||||
- name: Download necessary LFS assets
|
||||
shell: sh {0}
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
cppcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# cache only works for items in workspace, so configure apt to allow for caching.
|
||||
- name: Setup apt cache locations
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
|
||||
- name: Cache apt pkg db and and deb files
|
||||
id: apt-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
apt-cache
|
||||
|
|
@ -49,11 +49,11 @@ jobs:
|
|||
copyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
|
@ -66,10 +66,10 @@ jobs:
|
|||
jenkinsfiles:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,27 @@ name: pre-commit
|
|||
on:
|
||||
- push
|
||||
- pull_request
|
||||
env:
|
||||
PRE_COMMIT_VERSION: 4.6.0
|
||||
jobs:
|
||||
build:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- id: restore-pip-cache
|
||||
uses: actions/cache/restore@v4
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
key: pip-cache-v1-${{github.workflow}}-${{env.pythonLocation}}-${{env.PRE_COMMIT_VERSION}}
|
||||
path: ~/.cache/pip
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
- uses: actions/cache/save@v4
|
||||
if: steps.restore-pip-cache.outcome == 'success'
|
||||
- run: python -m pip install pre-commit=="${{ env.PRE_COMMIT_VERSION }}"
|
||||
shell: bash
|
||||
- run: python -m pip freeze --local
|
||||
shell: bash
|
||||
- uses: actions/cache@v6
|
||||
with:
|
||||
key: pip-cache-v1-${{ github.workflow }}
|
||||
path: ~/.cache/pip
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- run: pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
shell: bash
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -32,8 +32,9 @@ build/bin/
|
|||
source/tools/spirv/rules.json
|
||||
|
||||
# Linting
|
||||
cppcheck-error.log
|
||||
compile_commands.json
|
||||
copyright-check-error.log
|
||||
cppcheck-error.log
|
||||
|
||||
# Files generated by running premake5
|
||||
build/premake/.gccmachine.tmp
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ repos:
|
|||
\.patch$
|
||||
)
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.9
|
||||
rev: v0.16.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
|
@ -56,7 +56,7 @@ repos:
|
|||
files: ^binaries/
|
||||
exclude: (^binaries/data/mods/(mod|public)/art/.*\.xml|\.dae$)
|
||||
- repo: https://github.com/scop/pre-commit-shfmt
|
||||
rev: v3.12.0-2
|
||||
rev: v3.13.1-1
|
||||
hooks:
|
||||
- id: shfmt
|
||||
args:
|
||||
|
|
@ -67,7 +67,7 @@ repos:
|
|||
hooks:
|
||||
- id: shellcheck
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.45.0
|
||||
rev: v0.47.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
language_version: 22.14.0
|
||||
|
|
@ -77,13 +77,13 @@ repos:
|
|||
^source/third_party/
|
||||
)
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.37.1
|
||||
rev: v1.38.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
args:
|
||||
- --strict
|
||||
- repo: https://github.com/eslint/eslint
|
||||
rev: v9.39.2
|
||||
rev: v10.8.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
language_version: 22.14.0
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@
|
|||
<optional>
|
||||
<attribute name="define"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="srgb"/>
|
||||
</optional>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</interleave>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
layout(set = 2, binding = LOCATION, FORMAT) uniform image2D NAME
|
||||
#if USE_DESCRIPTOR_INDEXING
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(set = 2, binding = LOCATION)
|
||||
layout(std430, set = 2, binding = LOCATION)
|
||||
#else
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(set = 1, binding = LOCATION)
|
||||
layout(std430, set = 1, binding = LOCATION)
|
||||
#endif
|
||||
#else
|
||||
// We use offset to the binding slot for OpenGL to avoid overlapping with other
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
#define STORAGE_2D(LOCATION, FORMAT, NAME) \
|
||||
layout(binding = LOCATION, FORMAT) uniform image2D NAME
|
||||
#define STORAGE_BUFFER(LOCATION) \
|
||||
layout(binding = LOCATION)
|
||||
layout(std430, binding = LOCATION)
|
||||
#endif
|
||||
|
||||
#endif // STAGE_COMPUTE
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_achaemenids.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_celt.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_celt.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,20 @@
|
|||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_germ.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,18 @@
|
|||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_iber.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="ungarrisoned" frequency="1" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_rome.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/destruction_small.xml"/>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@
|
|||
</textures>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant frequency="1" name="ungarrisoned" />
|
||||
<variant name="garrisoned">
|
||||
<props>
|
||||
<prop
|
||||
actor="props/special/common/garrison_flag_seleucids.xml"
|
||||
attachpoint="garrisoned"
|
||||
selectable="false"
|
||||
/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
<group>
|
||||
<variant name="alive" frequency="1"/>
|
||||
<variant file="structures/sele/light_damage.xml"/>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:995da95e21261ae0a2718baee69d76ae5f6823d21eaefaa708a89651f2099bb0
|
||||
size 586243
|
||||
oid sha256:ed243a229225ec51ee090588a63307cbd2efedecca06f4f1a0922a4d51ee3967
|
||||
size 586427
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8e022a3ac132a32061583743d1322fc8792cdd6e7be57857547a8ed3cefbed6
|
||||
size 525300
|
||||
oid sha256:3ce649aa943771357d7c19f48294af719c50ca14178c32be60992f17845d685d
|
||||
size 525483
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:21971f20f5567f820e0f3fffdd21015e2a5ffaee309be7b6efc38d3fc56aecd3
|
||||
size 503936
|
||||
oid sha256:d1d867c2e9f3db233847da547c466325ba9f0a9e7478175281f3538cbb694709
|
||||
size 504112
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c37b1b7f12a429cd07873d0fdb7cc8692f6578b3e5320a6be0e68420fb4688e
|
||||
size 780868
|
||||
oid sha256:cc9c95992889a4c2930a39bd20f7c86b73f7c7ddb3b36492650afae2ae424641
|
||||
size 781051
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1230a92373c4e4684f791594efb35bb7d6c091377a11bd8f0daafd13816d04ec
|
||||
size 56755
|
||||
oid sha256:6779d6ca2067d6dd5594b3d2e5ef82d08de97ad058ce7084eb0bf8ae3290f4c2
|
||||
size 56953
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30a395c3b5a50214a689abe968ad15edb60212c3ea14041a2edc07295a4462c6
|
||||
size 45317
|
||||
oid sha256:9e6aca91d66df31b5820f39025c57937ce6a1593199b8a068722fdec5b0675f6
|
||||
size 45523
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce554ad452cf75bd4dbb9da5de9830309a1dccf72f72f6a74475078af7f0e1c4
|
||||
size 42851
|
||||
oid sha256:07013561c7e46b020acbd03d4f371300c38d3e364c99a1e7bbcf7467aa8a34e6
|
||||
size 43041
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60f0614c1092395065c4ba8fe6bed64da873d38c579c9cbc400e9a9b8f30f3a8
|
||||
size 272099
|
||||
oid sha256:1416efa429cd64471d50e969c43ee723382c031c676414433f8fcd58176cfa77
|
||||
size 272274
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6e7d6264d7b3e03466bf1de58ab366a023120c6f78142aebc6570218a4803d9
|
||||
size 274339
|
||||
oid sha256:9f9e6debd5f2a9659e19e74b5826d3488167edb9a897ae1ed6e4db10742d94e8
|
||||
size 274543
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0624fc07116aa70c14527457fced3afcf7b36df1535bbf10250455e6254aeed6
|
||||
size 372821
|
||||
oid sha256:091d68b7800d9141e87ba8aaf0fa93c5bc7d260dd459774075de610ca34855e2
|
||||
size 372997
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ef199b28b9d9ef19e0817b189d9e6f1e2f9861ca211890b8d714f9fe05f5c61
|
||||
size 420601
|
||||
oid sha256:c1413de5371443f1249d87fa40a821c880c749513a0347d9128a99472bec7b33
|
||||
size 420808
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:adf219c08e65bd7a149726d325b6f2b4fd67effc3dd1bc211c3163774ac8308e
|
||||
size 201143
|
||||
oid sha256:8aba3f3dc5a909ddadd9af5e7abfe9094f55ed6c97b39e7f97b09dc1cb951f4d
|
||||
size 201315
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8c688df60c4880dc92b6898728d4cef443c86831a2a2e87e61dd44661b58240
|
||||
size 205000
|
||||
oid sha256:0c3298efebf687a07d4693d9014c51e1e870e95fb9d9024c9126b40adfec75a5
|
||||
size 205172
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d86be00b7e911108e6666422a56eb061a996e6cf0bfe489d6c99f84be752444
|
||||
size 216525
|
||||
oid sha256:b97f6dff13f4639e746e1ce79c120169bccb2e09d4507dc900075cf85e788590
|
||||
size 216697
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e258d0b3a7b5c3fe2275c2816c636b7478ee1051df31a2cbe2f516465c692f9
|
||||
size 239166
|
||||
oid sha256:8d764a2d5a712f53f08560504f6841ed00b5268fea2a283d2d5254d57ce1df76
|
||||
size 239339
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:82ff53473c25d6e17aeb97f795b543df3ec1c423e8ecb8398d0d014beac57f4e
|
||||
size 101895
|
||||
oid sha256:dccc1521e6e626e75ac2f05d2c8b2c2963ee751da08a9e819e4c28954b19f01a
|
||||
size 102099
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2377bddedc6251acbe8f03bf1820d368b21cbf88fc2783695ff31036af8ed4e3
|
||||
size 82812
|
||||
oid sha256:ae1042461639bca3d3d73fb3e723ca404c8a897ee0e2ad417af1eebb5849d23f
|
||||
size 83027
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5f1530adecb42af05b6b81719a5c4d00fa06a5eff33f50eda8312ce2e0fcd5f
|
||||
size 77866
|
||||
oid sha256:b71b8848a0a2e503adbff9c1e09c06c1e76e16537f38af5b35395a04f11b365a
|
||||
size 78066
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c393e007e24473c1332c7d0c3a8093e0b542974da00b51b853ee413e45818ade
|
||||
size 91109
|
||||
oid sha256:256b3864edc754955814d5eec113e73fded49525d2e855a1171a4e4b495ffcc4
|
||||
size 91304
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8fb0f356f48284704d81b601c7947f32e91995757e8c2fc390ecaf772fbd950
|
||||
size 84604
|
||||
oid sha256:4ef0415cce15f4d1ff4e342c23a995ee5081d21b7f84eb6c19fb89c1d63f84a7
|
||||
size 84789
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c2b0d0a461535e0802d2a07395afb4546515ef4d3ccc3eab18d55a0993bc165
|
||||
size 184407
|
||||
oid sha256:2ab540ad518de662ba9a4dbe4d0d0590b6ee0b771bc482073296c949ee33d571
|
||||
size 184610
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:828e2e63b9686a601b3641be10f26e3b099ced5b8dc8d453ab94d41270f22b39
|
||||
size 213385
|
||||
oid sha256:39a6a575b8bbb0f82ed8cf4056e0df932a59fd6e3343005625b3b76644c6879c
|
||||
size 213561
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4825c81a245ddae5637f3b2dee78f7749e27a48d2efa244bc95af075ddabcd1a
|
||||
size 144598
|
||||
oid sha256:93341f17ad185e28ce62a533f1db457aec4b69bb2f67c98e4b89e4bcf9503b6a
|
||||
size 144784
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:427f9512af696afaae0721374a86ceb0e1432602013cb3dde818bfcd2791c163
|
||||
size 212264
|
||||
oid sha256:dc27dba302816891bb8f33bc65d9d68b2b330f946256aa966e5d684714af9fe2
|
||||
size 212447
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:19ca2828e5e6b99065d3d901c1a90f1b73df065dc94b3a4b76b4e5804890c137
|
||||
size 349847
|
||||
oid sha256:270fe9095c3894fb448cb91668ad48d886088006c689f0efa148e8a78a3de043
|
||||
size 350053
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57039fff36fbba71de79b89614fea988a98bdc85b2bdc5ef88ce3a632da95a55
|
||||
size 340693
|
||||
oid sha256:977b2ffb5dc9cf0364d125f0b3dd17e1fb0cd482b5c96fbf7c61b6e6a030db86
|
||||
size 340906
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:221eeca4ee2e46023d346e80c7a3b4b3852699e0c193e31fa58e0619e858f7a2
|
||||
size 272252
|
||||
oid sha256:f177172ef923455050064af60377f2e1b171ff4ec72fa9aa5fdc5c4c28000550
|
||||
size 272435
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:afc01d8e7e7efc988b2f78b58cfb6b48b5453b8786609e9ce5db38bc617a7984
|
||||
size 279942
|
||||
oid sha256:acdaaf09848dc5cb7cf207c7bb535865aa4199a5a50609ca3b632ce5d88d95a2
|
||||
size 280139
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3d1a87bfd33e5c55c8bc28eac1985f17fe5d4c51d853b4984f85051ce96a62d8
|
||||
size 2598
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5ca1fbd89c75157abb88e88d047273c93a61ff085c1eb5311a36ce07b4133590
|
||||
size 2611
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e961312d0b9cc321af9737fef9042e5c775d22bfd006ffa3697478df0425c0e7
|
||||
size 1305
|
||||
|
|
@ -330,3 +330,20 @@ Math.euclidDistance3D = function(x1, y1, z1, x2, y2, z2)
|
|||
{
|
||||
return Math.sqrt(Math.euclidDistance3DSquared(x1, y1, z1, x2, y2, z2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamps a value between a minimum and maximum range.
|
||||
*
|
||||
* @param {number} value - The value to clamp
|
||||
* @param {number} min - Minimum allowed value
|
||||
* @param {number} max - Maximum allowed value
|
||||
* @returns {number} The clamped value (min <= result <= max)
|
||||
*/
|
||||
Math.clamp = function(value, min, max)
|
||||
{
|
||||
if (value < min)
|
||||
return min;
|
||||
if (value > max)
|
||||
return max;
|
||||
return value;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -442,3 +442,17 @@ Vector3D.div = function(v, f)
|
|||
{
|
||||
return new Vector3D(v.x / f, v.y / f, v.z / f);
|
||||
};
|
||||
|
||||
/**
|
||||
* Linear interpolation between this vector and another.
|
||||
* @param {Vector2D} v - The target vector.
|
||||
* @param {number} t - Interpolation factor (0 = this, 1 = v).
|
||||
* @returns {Vector2D} A new vector at the interpolated position.
|
||||
*/
|
||||
Vector2D.prototype.lerp = function(v, t)
|
||||
{
|
||||
return new Vector2D(
|
||||
this.x + (v.x - this.x) * t,
|
||||
this.y + (v.y - this.y) * t
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<object name="gameStateNotifications"
|
||||
type="text"
|
||||
ghost="true"
|
||||
z="199"
|
||||
z="200"
|
||||
size="100%-110 40 100%-110 40"
|
||||
font="mono-stroke-10"
|
||||
textcolor="255 219 77"
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<object name="dataCounter"
|
||||
type="text"
|
||||
ghost="true"
|
||||
z="199"
|
||||
z="200"
|
||||
size="100%-100 40 100%-5 54"
|
||||
font="mono-10"
|
||||
textcolor="white"
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<object name="glbWaterMark"
|
||||
hidden="true"
|
||||
hotkey="screenshot.watermark"
|
||||
z="200"
|
||||
z="300"
|
||||
>
|
||||
<action on="Press">
|
||||
this.hidden = !this.hidden;
|
||||
|
|
|
|||
|
|
@ -127,17 +127,4 @@
|
|||
text_align="left"
|
||||
/>
|
||||
|
||||
<style name="TutorialPanel"
|
||||
buffer_zone="8"
|
||||
font="sans-bold-14"
|
||||
scrollbar="true"
|
||||
scrollbar_style="ModernScrollBar"
|
||||
scroll_bottom="true"
|
||||
textcolor="white"
|
||||
text_align="left"
|
||||
text_valign="top"
|
||||
sprite="ModernDarkBox"
|
||||
sprite_overlay="ModernDarkBoxGoldBorder"
|
||||
/>
|
||||
|
||||
</styles>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"List": [
|
||||
{ "nick": "Alexandermb", "name": "Daniel Morgado" },
|
||||
{ "nick": "Athos" },
|
||||
{ "nick": "Atrik" },
|
||||
{ "nick": "Basshunter", "name": "Sebastián Gómez" },
|
||||
{ "nick": "BigTiger", "name": "Nathanael P. Moore" },
|
||||
{ "nick": "Brightgalrs", "name": "Robert D. Schultz" },
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
{ "nick": "Enrique", "name": "Enrique Keykens Espolio" },
|
||||
{ "nick": "Gen.Kenobi", "name": "Daniel Schubert" },
|
||||
{ "nick": "Historicity", "name": "Shane" },
|
||||
{ "nick": "Langbart" },
|
||||
{ "nick": "Leyto", "name": "Valentin Levalet" },
|
||||
{ "nick": "LordGood", "name": "Johnathan B. Good" },
|
||||
{ "nick": "m7600" },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
class Menu
|
||||
{
|
||||
constructor(pauseControl, playerViewControl, chat, closePageCallback)
|
||||
constructor(pauseControl, playerViewControl, chat, closeSession)
|
||||
{
|
||||
this.menuButton = Engine.GetGUIObjectByName("menuButton");
|
||||
this.menuButton.onPress = this.toggle.bind(this);
|
||||
|
|
@ -26,7 +26,7 @@ class Menu
|
|||
this.buttons = handlerNames.map((handlerName, i) =>
|
||||
{
|
||||
const handler = new MenuButtons.prototype[handlerName](menuButtons[i], pauseControl, playerViewControl, chat);
|
||||
this.initButton(handler, menuButtons[i], i, closePageCallback);
|
||||
this.initButton(handler, menuButtons[i], i, closeSession);
|
||||
return handler;
|
||||
});
|
||||
|
||||
|
|
@ -62,12 +62,12 @@ class Menu
|
|||
this.startAnimation();
|
||||
}
|
||||
|
||||
initButton(handler, button, i, closePageCallback)
|
||||
initButton(handler, button, i, closeSession)
|
||||
{
|
||||
button.onPress = () =>
|
||||
{
|
||||
this.close();
|
||||
handler.onPress(closePageCallback);
|
||||
handler.onPress(closeSession);
|
||||
};
|
||||
button.size.top = this.buttonHeight * (i + 1) + this.margin;
|
||||
button.size.bottom = this.buttonHeight * (i + 2);
|
||||
|
|
|
|||
|
|
@ -283,13 +283,13 @@ MenuButtons.prototype.Exit = class
|
|||
this.pauseControl = pauseControl;
|
||||
}
|
||||
|
||||
onPress(closePageCallback)
|
||||
onPress(closeSession)
|
||||
{
|
||||
for (const name in QuitConfirmationMenu.prototype)
|
||||
{
|
||||
const quitConfirmation = new QuitConfirmationMenu.prototype[name]();
|
||||
if (quitConfirmation.enabled())
|
||||
quitConfirmation.display(closePageCallback);
|
||||
quitConfirmation.display(closeSession);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
type="text"
|
||||
hidden="true"
|
||||
ghost="true"
|
||||
z="199"
|
||||
z="160"
|
||||
size="100%-300 60 100%-110 80"
|
||||
font="mono-10"
|
||||
textcolor="white"
|
||||
|
|
|
|||
|
|
@ -4,15 +4,12 @@
|
|||
*/
|
||||
class NetworkStatusOverlay
|
||||
{
|
||||
constructor(closePageCallback)
|
||||
constructor(closeSession)
|
||||
{
|
||||
this.netStatus = Engine.GetGUIObjectByName("netStatus");
|
||||
this.loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText");
|
||||
|
||||
Engine.GetGUIObjectByName("disconnectedExitButton").onPress = () =>
|
||||
{
|
||||
closePageCallback({ [Engine.openRequest]: endGame(true) });
|
||||
};
|
||||
Engine.GetGUIObjectByName("disconnectedExitButton").onPress = () => { closeSession(true); };
|
||||
|
||||
registerNetworkStatusChangeHandler(this.onNetStatusMessage.bind(this));
|
||||
registerClientsLoadingHandler(this.onClientsLoadingMessage.bind(this));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
class SessionMessageBox
|
||||
{
|
||||
async display(closePageCallback)
|
||||
async display(closeSession)
|
||||
{
|
||||
closeOpenDialogs();
|
||||
g_PauseControl.implicitPause();
|
||||
|
|
@ -19,12 +19,9 @@ class SessionMessageBox
|
|||
"buttonCaptions": this.Buttons ? this.Buttons.map(button => button.caption) : undefined,
|
||||
});
|
||||
|
||||
this.closeSession = closeSession;
|
||||
if (this.Buttons && this.Buttons[buttonId].onPress)
|
||||
{
|
||||
const ret = this.Buttons[buttonId].onPress.call(this);
|
||||
if (ret !== undefined)
|
||||
closePageCallback({ [Engine.openRequest]: ret });
|
||||
}
|
||||
this.Buttons[buttonId].onPress.call(this);
|
||||
|
||||
if (this.ResumeOnClose)
|
||||
resumeGame();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</object>
|
||||
|
||||
<object hotkey="session.gui.tutorial.toggle">
|
||||
<action on="Press">toggleTutorial();</action>
|
||||
<action on="Press">g_TutorialManager?.toggleVisibility();</action>
|
||||
</object>
|
||||
|
||||
<object hotkey="silhouettes">
|
||||
|
|
|
|||
|
|
@ -507,6 +507,49 @@ var unitFilters = {
|
|||
}
|
||||
};
|
||||
|
||||
// Choose, inside a list of entities, which ones are targetable foes for boxtargeting.
|
||||
// We also filter out entities that aren't visible, or low priority like livestocks
|
||||
function getTargetableEntities(ents)
|
||||
{
|
||||
const player = g_ViewedPlayer;
|
||||
const simState = GetSimState();
|
||||
const isEnemy = simState.players[player].isEnemy;
|
||||
|
||||
// Filter valid target candidates
|
||||
const candidates = ents.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return entState &&
|
||||
unitFilters.isUnit(entity) &&
|
||||
entState.visibility != "hidden";
|
||||
});
|
||||
|
||||
// Enemy players
|
||||
const enemyPlayers = candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return isEnemy[entState.player] && entState.player !== 0 && !hasClass(entState, "Domestic");
|
||||
});
|
||||
if (enemyPlayers.length > 0)
|
||||
return enemyPlayers;
|
||||
|
||||
// Gaia
|
||||
const gaiaUnits = candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return entState.player === 0;
|
||||
});
|
||||
if (gaiaUnits.length > 0)
|
||||
return gaiaUnits;
|
||||
|
||||
// Domestic animals
|
||||
return candidates.filter(entity =>
|
||||
{
|
||||
const entState = GetEntityState(entity);
|
||||
return hasClass(entState, "Domestic");
|
||||
});
|
||||
}
|
||||
|
||||
// Choose, inside a list of entities, which ones will be selected.
|
||||
// We may use several entity filters, until one returns at least one element.
|
||||
function getPreferredEntities(ents)
|
||||
|
|
@ -572,16 +615,19 @@ function handleInputBeforeGui(ev, hoveredObject)
|
|||
case INPUT_BANDBOXING:
|
||||
{
|
||||
const bandbox = Engine.GetGUIObjectByName("bandbox");
|
||||
const isAttackMode = Engine.HotkeyIsPressed("session.attack") && !g_IsObserver;
|
||||
switch (ev.type)
|
||||
{
|
||||
case "mousemotion":
|
||||
{
|
||||
const rect = updateBandbox(bandbox, ev, false);
|
||||
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer);
|
||||
const preferredEntities = getPreferredEntities(ents);
|
||||
g_Selection.setHighlightList(preferredEntities);
|
||||
const player = isAttackMode ? -1 : g_ViewedPlayer;
|
||||
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], player);
|
||||
const highlightEntities = isAttackMode ? getTargetableEntities(ents) : getPreferredEntities(ents);
|
||||
|
||||
g_Selection.setHighlightList(highlightEntities);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -589,6 +635,30 @@ function handleInputBeforeGui(ev, hoveredObject)
|
|||
if (ev.button == SDL_BUTTON_LEFT)
|
||||
{
|
||||
const rect = updateBandbox(bandbox, ev, true);
|
||||
if (isAttackMode)
|
||||
{
|
||||
// Box attack behavior
|
||||
const ents = Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], -1);
|
||||
|
||||
// Filter to only enemy units
|
||||
const enemyUnits = getTargetableEntities(ents);
|
||||
|
||||
// Get currently selected friendly units
|
||||
const selectedUnits = g_Selection.toList();
|
||||
|
||||
if (selectedUnits.length && enemyUnits.length)
|
||||
{
|
||||
// Sort entities by position
|
||||
const sortedCombattants = sortEntitiesForEngagement(selectedUnits, enemyUnits);
|
||||
// Distribute attack orders
|
||||
distributeAttackOrders(sortedCombattants.attackers, sortedCombattants.targets);
|
||||
}
|
||||
g_Selection.setHighlightList([]);
|
||||
inputState = INPUT_NORMAL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const ents = getPreferredEntities(Engine.PickPlayerEntitiesInRect(rect[0], rect[1], rect[2], rect[3], g_ViewedPlayer));
|
||||
g_Selection.setHighlightList([]);
|
||||
|
||||
|
|
@ -1371,6 +1441,7 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
for (let i = 1; i < inputLine.length; ++i)
|
||||
lengthOfLine += inputLine[i].distanceTo(inputLine[i - 1]);
|
||||
|
||||
// Filter units that can move
|
||||
const selection = g_Selection.filter(ent => !!GetEntityState(ent).unitAI).sort((a, b) => a - b);
|
||||
|
||||
// Checking the line for a minimum length to save performance.
|
||||
|
|
@ -1380,39 +1451,43 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
return !!action && doAction(action, ev);
|
||||
}
|
||||
|
||||
// Even distribution of the units on the line.
|
||||
let p0 = inputLine[0];
|
||||
let entityDistribution = [p0];
|
||||
const distanceBetweenEnts = lengthOfLine / (selection.length - 1);
|
||||
let freeDist = -distanceBetweenEnts;
|
||||
// Sort units by their projection onto the line direction (parallel sorting)
|
||||
const sortedSelection = sortEntitiesAlongLine(selection, inputLine[0], inputLine[inputLine.length - 1], true);
|
||||
|
||||
for (let i = 1; i < inputLine.length; ++i)
|
||||
const selectionCount = sortedSelection.length;
|
||||
const targetPositions = [];
|
||||
|
||||
// Distribute units evenly along the polyline
|
||||
for (let i = 0; i < selectionCount; ++i)
|
||||
{
|
||||
const p1 = inputLine[i];
|
||||
freeDist += inputLine[i - 1].distanceTo(p1);
|
||||
const t = i / (selectionCount - 1);
|
||||
const targetDistance = t * lengthOfLine;
|
||||
|
||||
while (freeDist >= 0)
|
||||
let accumulated = 0;
|
||||
let targetPoint = inputLine[0];
|
||||
for (let j = 1; j < inputLine.length; ++j)
|
||||
{
|
||||
p0 = Vector2D.sub(p0, p1).normalize().mult(freeDist).add(p1);
|
||||
entityDistribution.push(p0);
|
||||
freeDist -= distanceBetweenEnts;
|
||||
const segStart = inputLine[j - 1];
|
||||
const segEnd = inputLine[j];
|
||||
const segLength = segEnd.distanceTo(segStart);
|
||||
|
||||
if (accumulated + segLength >= targetDistance)
|
||||
{
|
||||
// Interpolate within this segment
|
||||
const remaining = targetDistance - accumulated;
|
||||
const frac = remaining / segLength;
|
||||
targetPoint = segStart.lerp(segEnd, frac);
|
||||
break;
|
||||
}
|
||||
accumulated += segLength;
|
||||
}
|
||||
targetPositions.push(targetPoint);
|
||||
}
|
||||
|
||||
// Rounding errors can lead to missing or too many points.
|
||||
entityDistribution = entityDistribution.slice(0, selection.length);
|
||||
entityDistribution = entityDistribution.concat(new Array(selection.length - entityDistribution.length).fill(inputLine[inputLine.length - 1]));
|
||||
|
||||
if (Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[0]) +
|
||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[selection.length - 1]) >
|
||||
Vector2D.from3D(GetEntityState(selection[0]).position).distanceTo(entityDistribution[selection.length - 1]) +
|
||||
Vector2D.from3D(GetEntityState(selection[selection.length - 1]).position).distanceTo(entityDistribution[0]))
|
||||
entityDistribution.reverse();
|
||||
|
||||
Engine.PostNetworkCommand({
|
||||
"type": isAttackMovePressed() ? "attack-walk-custom" : "walk-custom",
|
||||
"entities": selection,
|
||||
"targetPositions": entityDistribution.map(pos => pos.toFixed(2)),
|
||||
"entities": sortedSelection,
|
||||
"targetPositions": targetPositions.map(pos => pos.toFixed(2)),
|
||||
"targetClasses": Engine.HotkeyIsPressed("session.attackmoveUnit") ? { "attack": ["Unit"] } : { "attack": ["Unit", "Structure"] },
|
||||
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||
"pushFront": Engine.HotkeyIsPressed("session.pushorderfront"),
|
||||
|
|
@ -1420,9 +1495,10 @@ function positionUnitsFreehandSelectionMouseUp(ev)
|
|||
});
|
||||
|
||||
// Add target markers with a minimum distance of 5 to each other.
|
||||
const entitiesBetweenMarker = Math.ceil(5 / distanceBetweenEnts);
|
||||
for (let i = 0; i < entityDistribution.length; i += entitiesBetweenMarker)
|
||||
DrawTargetMarker({ "x": entityDistribution[i].x, "z": entityDistribution[i].y });
|
||||
const stepDistance = lengthOfLine / (selectionCount - 1);
|
||||
const entitiesBetweenMarker = Math.max(1, Math.ceil(5 / stepDistance));
|
||||
for (let i = 0; i < targetPositions.length; i += entitiesBetweenMarker)
|
||||
DrawTargetMarker({ "x": targetPositions[i].x, "z": targetPositions[i].y });
|
||||
|
||||
Engine.GuiInterfaceCall("PlaySound", {
|
||||
"name": "order_walk",
|
||||
|
|
@ -1929,3 +2005,108 @@ function clearSelection()
|
|||
g_Selection.reset();
|
||||
preSelectedAction = ACTION_NONE;
|
||||
}
|
||||
|
||||
function distributeAttackOrders(attackers, targets)
|
||||
{
|
||||
if (targets.length < 1)
|
||||
return;
|
||||
|
||||
// Play attack sounds from a sample of units in selection
|
||||
const soundCount = Math.min(3, attackers.length);
|
||||
for (let i = 0; i < soundCount; i++)
|
||||
{
|
||||
const t = soundCount === 1 ? 0 : i / (soundCount - 1);
|
||||
const attackerIndex = Math.floor(t * (attackers.length - 1));
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
Engine.GuiInterfaceCall("PlaySound", {
|
||||
"name": "order_attack",
|
||||
"entity": attackers[attackerIndex]
|
||||
});
|
||||
}, i * 180);
|
||||
}
|
||||
|
||||
Engine.PostNetworkCommand({
|
||||
"type": "attack-group",
|
||||
"entities": attackers,
|
||||
"targets": targets,
|
||||
"queued": Engine.HotkeyIsPressed("session.queue"),
|
||||
"allowCapture": false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts attackers and targets for left-to-right engagement across the battle line.
|
||||
*
|
||||
* Entities are sorted by their perpendicular distance to the line connecting the
|
||||
* two army centers, creating natural pairings across the battle front.
|
||||
*
|
||||
* @param {number[]} attackers - Array of attacking entity IDs.
|
||||
* @param {number[]} targets - Array of target entity IDs.
|
||||
* @returns {Object} { attackers: number[], targets: number[] } - Both arrays sorted
|
||||
* for cross-line engagement (targets automatically reversed).
|
||||
*/
|
||||
function sortEntitiesForEngagement(attackers, targets)
|
||||
{
|
||||
const getPosition = (id) => GetEntityState(id).position;
|
||||
|
||||
const computeAveragePosition = (entities) =>
|
||||
{
|
||||
if (entities.length === 0) return new Vector2D(0, 0);
|
||||
const vectors = entities.map(id => Vector2D.from3D(getPosition(id)));
|
||||
return Vector2D.average(vectors);
|
||||
};
|
||||
|
||||
// Calculate the axis of engagement - the line between the groups' average positions
|
||||
const avgAttackers = computeAveragePosition(attackers);
|
||||
const avgTargets = computeAveragePosition(targets);
|
||||
|
||||
// Sort each group by perpendicular distance to the engagement line
|
||||
const sortedAttackers = sortEntitiesAlongLine(attackers, avgAttackers, avgTargets);
|
||||
const sortedTargets = sortEntitiesAlongLine(targets, avgAttackers, avgTargets);
|
||||
|
||||
// Reverse targets so the leftmost attacker pairs with leftmost target
|
||||
return {
|
||||
"attackers": sortedAttackers,
|
||||
"targets": sortedTargets.reverse()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts entity IDs by their projection onto a line's direction or perpendicular axis.
|
||||
*
|
||||
* @param {number[]} entities - Array of entity IDs.
|
||||
* @param {Vector2D} lineStart - Start point of the reference line.
|
||||
* @param {Vector2D} lineEnd - End point of the reference line.
|
||||
* @param {boolean} sortByDirection - If true, sort by parallel projection (along line);
|
||||
* If false, sort by perpendicular distance (default).
|
||||
* @returns {number[]} Sorted entity IDs.
|
||||
*/
|
||||
function sortEntitiesAlongLine(entities, lineStart, lineEnd, sortByDirection = false)
|
||||
{
|
||||
const dir = lineEnd.sub(lineStart);
|
||||
const length = dir.length();
|
||||
if (length === 0)
|
||||
return entities.slice();
|
||||
|
||||
// Choose basis vector: either parallel or perpendicular to the line
|
||||
let basis;
|
||||
if (sortByDirection)
|
||||
basis = dir.mult(1 / length); // Unit direction vector
|
||||
else
|
||||
basis = new Vector2D(-dir.y, dir.x).normalize(); // Perpendicular
|
||||
|
||||
const withCoord = entities.map(id =>
|
||||
{
|
||||
const pos = GetEntityState(id).position;
|
||||
const point2D = Vector2D.from3D(pos);
|
||||
const rel = point2D.sub(lineStart);
|
||||
const coord = rel.dot(basis); // Project onto basis vector
|
||||
return { id, coord };
|
||||
});
|
||||
|
||||
// Sort by that coordinate
|
||||
withCoord.sort((a, b) => a.coord - b.coord);
|
||||
return withCoord.map(item => item.id);
|
||||
}
|
||||
|
|
@ -18,12 +18,12 @@ QuitConfirmation.prototype.Buttons =
|
|||
{
|
||||
// Translation: Shown in the Dialog that shows up when the game finishes
|
||||
"caption": translate("Quit and View Summary"),
|
||||
"onPress": () => endGame(true)
|
||||
"onPress": function() { this.closeSession(true); }
|
||||
},
|
||||
{
|
||||
// Translation: Shown in the Dialog that shows up when the game finishes
|
||||
"caption": translate("Quit"),
|
||||
"onPress": () => endGame(false)
|
||||
"onPress": function() { this.closeSession(false); }
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
class QuitConfirmationDefeat extends QuitConfirmation
|
||||
{
|
||||
constructor(closePageCallback)
|
||||
constructor(closeSession)
|
||||
{
|
||||
super();
|
||||
|
||||
|
|
@ -11,10 +11,10 @@ class QuitConfirmationDefeat extends QuitConfirmation
|
|||
return;
|
||||
|
||||
this.confirmHandler = undefined;
|
||||
registerPlayersFinishedHandler(this.onPlayersFinished.bind(this, closePageCallback));
|
||||
registerPlayersFinishedHandler(this.onPlayersFinished.bind(this, closeSession));
|
||||
}
|
||||
|
||||
onPlayersFinished(closePageCallback, players, won)
|
||||
onPlayersFinished(closeSession, players, won)
|
||||
{
|
||||
if (players.indexOf(Engine.GetPlayerID()) == -1)
|
||||
return;
|
||||
|
|
@ -22,11 +22,11 @@ class QuitConfirmationDefeat extends QuitConfirmation
|
|||
// Defer simulation result until
|
||||
// 1. the loading screen finished for all networked clients (g_IsNetworkedActive)
|
||||
// 2. all messages modifying g_Players victory state were processed (next turn)
|
||||
this.confirmHandler = this.confirmExit.bind(this, won, closePageCallback);
|
||||
this.confirmHandler = this.confirmExit.bind(this, won, closeSession);
|
||||
registerSimulationUpdateHandler(this.confirmHandler);
|
||||
}
|
||||
|
||||
confirmExit(won, closePageCallback)
|
||||
confirmExit(won, closeSession)
|
||||
{
|
||||
if (g_IsNetworked && !g_IsNetworkedActive)
|
||||
return;
|
||||
|
|
@ -47,7 +47,7 @@ class QuitConfirmationDefeat extends QuitConfirmation
|
|||
|
||||
this.Buttons = askExit ? super.Buttons : undefined;
|
||||
|
||||
this.display(closePageCallback);
|
||||
this.display(closeSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ ReturnQuestion.prototype.Caption = translate("Do you want to resign or will you
|
|||
ReturnQuestion.prototype.Buttons = [
|
||||
{
|
||||
"caption": translate("I will return"),
|
||||
"onPress": () => endGame(false)
|
||||
"onPress": function() { this.closeSession(false); }
|
||||
},
|
||||
{
|
||||
"caption": translate("I resign"),
|
||||
|
|
@ -74,9 +74,9 @@ QuitConfirmationMenu.prototype.MultiplayerClient.prototype.Buttons =
|
|||
},
|
||||
{
|
||||
"caption": translate("Yes"),
|
||||
"onPress": closePageCallback =>
|
||||
"onPress": closeSession =>
|
||||
{
|
||||
(new ReturnQuestion()).display(closePageCallback);
|
||||
(new ReturnQuestion()).display(closeSession);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
*/
|
||||
class QuitConfirmationReplay extends QuitConfirmation
|
||||
{
|
||||
constructor(closePageCallback)
|
||||
constructor(closeSession)
|
||||
{
|
||||
super();
|
||||
Engine.GetGUIObjectByName("session").onReplayFinished = this.display.bind(this, closePageCallback);
|
||||
Engine.GetGUIObjectByName("session").onReplayFinished = this.display.bind(this, closeSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,11 +26,11 @@ QuitConfirmationReplay.prototype.Buttons =
|
|||
{
|
||||
// Translation: Shown in the Dialog that shows up when a replay finishes
|
||||
"caption": translate("Quit and View Summary"),
|
||||
"onPress": () => endGame(true)
|
||||
"onPress": function() { this.closeSession(true); }
|
||||
},
|
||||
{
|
||||
// Translation: Shown in the Dialog that shows up when a replay finishes
|
||||
"caption": translate("Quit"),
|
||||
"onPress": () => endGame(false)
|
||||
"onPress": function() { this.closeSession(false); }
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
/**
|
||||
* All tutorial messages received so far.
|
||||
*/
|
||||
var g_TutorialMessages = [];
|
||||
|
||||
/**
|
||||
* GUI tags applied to the most recent tutorial message.
|
||||
*/
|
||||
var g_TutorialNewMessageTags = { "color": "255 226 149" };
|
||||
|
||||
/**
|
||||
* The number of seconds we monitor to rate limit flares.
|
||||
*/
|
||||
|
|
@ -148,15 +138,14 @@ var g_NotificationsTypes =
|
|||
},
|
||||
"defeat": function(notification, player)
|
||||
{
|
||||
playersFinished(notification.allies, notification.message, false);
|
||||
handlePlayersFinished(notification.allies, notification.message, false);
|
||||
},
|
||||
"won": function(notification, player)
|
||||
{
|
||||
playersFinished(notification.allies, notification.message, true);
|
||||
handlePlayersFinished(notification.allies, notification.message, true);
|
||||
},
|
||||
"diplomacy": function(notification, player)
|
||||
{
|
||||
updatePlayerData();
|
||||
g_DiplomacyColors.onDiplomacyChange();
|
||||
|
||||
addChatMessage({
|
||||
|
|
@ -168,13 +157,12 @@ var g_NotificationsTypes =
|
|||
},
|
||||
"ceasefire-ended": function(notification, player)
|
||||
{
|
||||
updatePlayerData();
|
||||
for (const handler of g_CeasefireEndedHandlers)
|
||||
handler();
|
||||
},
|
||||
"tutorial": function(notification, player)
|
||||
{
|
||||
updateTutorial(notification);
|
||||
g_TutorialManager.handleNotification(notification);
|
||||
},
|
||||
"tribute": function(notification, player)
|
||||
{
|
||||
|
|
@ -335,7 +323,7 @@ function findGuidForPlayerID(playerID)
|
|||
/**
|
||||
* Processes all pending notifications sent from the GUIInterface simulation component.
|
||||
*/
|
||||
function handleNotifications(closePageCallback)
|
||||
function handleNotifications()
|
||||
{
|
||||
for (const notification of Engine.GuiInterfaceCall("GetNotifications"))
|
||||
{
|
||||
|
|
@ -346,7 +334,7 @@ function handleNotifications(closePageCallback)
|
|||
}
|
||||
|
||||
for (const player of notification.players)
|
||||
g_NotificationsTypes[notification.type](notification, player, closePageCallback);
|
||||
g_NotificationsTypes[notification.type](notification, player);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,57 +353,6 @@ function focusAttack(attack)
|
|||
}
|
||||
}
|
||||
|
||||
function toggleTutorial()
|
||||
{
|
||||
const tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel");
|
||||
tutorialPanel.hidden = !tutorialPanel.hidden || !Engine.GetGUIObjectByName("tutorialText").caption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tutorial panel when a new goal.
|
||||
*/
|
||||
function updateTutorial(notification, closePageCallback)
|
||||
{
|
||||
// Show the tutorial panel if not yet done
|
||||
Engine.GetGUIObjectByName("tutorialPanel").hidden = false;
|
||||
|
||||
if (notification.warning)
|
||||
{
|
||||
Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange");
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationText =
|
||||
notification.instructions.reduce((instructions, item) =>
|
||||
instructions + (typeof item === "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)),
|
||||
"");
|
||||
|
||||
Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n");
|
||||
g_TutorialMessages.push(notificationText);
|
||||
|
||||
if (notification.readyButton)
|
||||
{
|
||||
Engine.GetGUIObjectByName("tutorialReady").hidden = false;
|
||||
if (notification.leave)
|
||||
{
|
||||
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial.");
|
||||
Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit");
|
||||
|
||||
Engine.GetGUIObjectByName("tutorialReady").onPress = () =>
|
||||
{
|
||||
closePageCallback({ [Engine.openRequest]: endGame(true) });
|
||||
};
|
||||
}
|
||||
else
|
||||
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions.");
|
||||
Engine.GetGUIObjectByName("tutorialReady").hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
|
||||
* Saves the received object to mainlog.html.
|
||||
|
|
@ -476,6 +413,18 @@ function handlePlayerAssignmentsMessage(message)
|
|||
updateGUIObjects();
|
||||
}
|
||||
|
||||
function handlePlayersFinished(players, message, won)
|
||||
{
|
||||
addChatMessage({
|
||||
"type": "playerstate",
|
||||
"message": message,
|
||||
"players": players
|
||||
});
|
||||
|
||||
for (const handler of g_PlayerFinishedHandlers)
|
||||
handler(players, won);
|
||||
}
|
||||
|
||||
function onClientJoin(guid)
|
||||
{
|
||||
const playerID = g_PlayerAssignments[guid].player;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@
|
|||
<!-- Status Effects icons -->
|
||||
<object name="statusEffectsIcons" size="100%-20 4 100%-4 100%">
|
||||
<repeat count="5">
|
||||
<object type="image" size="0 0 16 16" z="200" tooltip_style="sessionToolTip"/>
|
||||
<object type="image" size="0 0 16 16" z="100" tooltip_style="sessionToolTip"/>
|
||||
</repeat>
|
||||
</object>
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ var g_ResearchProgress;
|
|||
var g_TimeNotificationOverlay;
|
||||
var g_TopPanel;
|
||||
var g_TradeDialog;
|
||||
var g_TutorialManager;
|
||||
|
||||
/**
|
||||
* Map, player and match settings set in game setup.
|
||||
|
|
@ -284,8 +285,8 @@ async function init(initData, hotloadData)
|
|||
g_DiplomacyColors = new DiplomacyColors();
|
||||
g_PlayerViewControl = new PlayerViewControl();
|
||||
g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors));
|
||||
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates);
|
||||
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl));
|
||||
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects);
|
||||
g_PauseControl = new PauseControl();
|
||||
g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay);
|
||||
g_Ambient = new Ambient();
|
||||
|
|
@ -337,26 +338,29 @@ async function init(initData, hotloadData)
|
|||
{
|
||||
if (g_IsNetworked)
|
||||
handleNetMessages().catch(reject);
|
||||
}), new Promise(closePageCallback =>
|
||||
}), new Promise(closeSession =>
|
||||
{
|
||||
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates.bind(undefined,
|
||||
closePageCallback));
|
||||
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat, closePageCallback);
|
||||
g_NetworkStatusOverlay = new NetworkStatusOverlay(closePageCallback);
|
||||
g_QuitConfirmationDefeat = new QuitConfirmationDefeat(closePageCallback);
|
||||
g_QuitConfirmationReplay = new QuitConfirmationReplay(closePageCallback);
|
||||
// TODO: use event instead
|
||||
onSimulationUpdate(closePageCallback);
|
||||
Engine.GetGUIObjectByName("session").onSimulationUpdate =
|
||||
onSimulationUpdate.bind(undefined, closePageCallback);
|
||||
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat, closeSession);
|
||||
g_NetworkStatusOverlay = new NetworkStatusOverlay(closeSession);
|
||||
g_QuitConfirmationDefeat = new QuitConfirmationDefeat(closeSession);
|
||||
g_QuitConfirmationReplay = new QuitConfirmationReplay(closeSession);
|
||||
g_TutorialManager = new TutorialManager(closeSession, hotloadData?.tutorial);
|
||||
})]);
|
||||
|
||||
// TODO: use event instead
|
||||
onSimulationUpdate();
|
||||
Engine.GetGUIObjectByName("session").onSimulationUpdate = onSimulationUpdate;
|
||||
|
||||
for (const handler of g_PlayersInitHandlers)
|
||||
handler();
|
||||
|
||||
for (const handler of g_HotkeyChangeHandlers)
|
||||
handler();
|
||||
|
||||
registerPlayersFinishedHandler(updatePlayerData);
|
||||
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updatePlayerData);
|
||||
registerCeasefireEndedHandler(updatePlayerData);
|
||||
|
||||
if (hotloadData)
|
||||
{
|
||||
g_Selection.selected = hotloadData.selection;
|
||||
|
|
@ -366,7 +370,19 @@ async function init(initData, hotloadData)
|
|||
|
||||
setTimeout(displayGamestateNotifications, 1000);
|
||||
|
||||
return promise;
|
||||
const showSummary = await promise;
|
||||
|
||||
// Depends on the simulation, so it has to be called before EndGame.
|
||||
const openRequest = getNextPageOpenRequest(showSummary);
|
||||
|
||||
Engine.EndGame();
|
||||
// After the replay file was closed in EndGame
|
||||
if (!g_IsReplay)
|
||||
Engine.AddReplayToCache(Engine.GetCurrentReplayDirectory());
|
||||
if (g_IsController && Engine.HasXmppClient())
|
||||
Engine.SendUnregisterGame();
|
||||
|
||||
return { [Engine.openRequest]: openRequest };
|
||||
}
|
||||
|
||||
function registerPlayersInitHandler(handler)
|
||||
|
|
@ -470,16 +486,23 @@ function initializeMusic()
|
|||
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
|
||||
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
|
||||
global.music.setState(global.music.states.PEACE);
|
||||
|
||||
registerPlayersFinishedHandler((players, won) =>
|
||||
{
|
||||
if (players.includes(Engine.GetPlayerID()) && !Engine.IsAtlasRunning())
|
||||
global.music.setState(won ? global.music.states.VICTORY : global.music.states.DEFEAT);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function resetTemplates(closePageCallback)
|
||||
function resetTemplates()
|
||||
{
|
||||
// Update GUI and clear player-dependent cache
|
||||
g_TemplateData = {};
|
||||
Engine.GuiInterfaceCall("ResetTemplateModified");
|
||||
|
||||
// TODO: do this more selectively
|
||||
onSimulationUpdate(closePageCallback);
|
||||
onSimulationUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -505,37 +528,6 @@ function controlsPlayer(playerID)
|
|||
playerStates[playerID].state != "defeated";
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one or more players have won or were defeated.
|
||||
*
|
||||
* @param {array} - IDs of the players who have won or were defeated.
|
||||
* @param {Object} - a plural string stating the victory reason.
|
||||
* @param {boolean} - whether these players have won or lost.
|
||||
*/
|
||||
function playersFinished(players, victoryString, won)
|
||||
{
|
||||
addChatMessage({
|
||||
"type": "playerstate",
|
||||
"message": victoryString,
|
||||
"players": players
|
||||
});
|
||||
|
||||
updatePlayerData();
|
||||
|
||||
// TODO: The other calls in this function should move too
|
||||
for (const handler of g_PlayerFinishedHandlers)
|
||||
handler(players, won);
|
||||
|
||||
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
|
||||
return;
|
||||
|
||||
global.music.setState(
|
||||
won ?
|
||||
global.music.states.VICTORY :
|
||||
global.music.states.DEFEAT
|
||||
);
|
||||
}
|
||||
|
||||
function resumeGame()
|
||||
{
|
||||
g_PauseControl.implicitResume();
|
||||
|
|
@ -550,31 +542,20 @@ function closeOpenDialogs()
|
|||
g_TradeDialog.close();
|
||||
}
|
||||
|
||||
function endGame(showSummary)
|
||||
/**
|
||||
* Put together an open request for the next GUI page to show after closing the session.
|
||||
* This could be, depending on the situation, the the summary screen, the main menu, the lobby, etc.
|
||||
*/
|
||||
function getNextPageOpenRequest(showSummary)
|
||||
{
|
||||
// Before ending the game
|
||||
const replayDirectory = Engine.GetCurrentReplayDirectory();
|
||||
const simData = Engine.GuiInterfaceCall("GetReplayMetadata");
|
||||
const playerID = Engine.GetPlayerID();
|
||||
|
||||
Engine.EndGame();
|
||||
|
||||
// After the replay file was closed in EndGame
|
||||
// Done here to keep EndGame small
|
||||
if (!g_IsReplay)
|
||||
Engine.AddReplayToCache(replayDirectory);
|
||||
|
||||
if (g_IsController && Engine.HasXmppClient())
|
||||
Engine.SendUnregisterGame();
|
||||
|
||||
const summaryData = {
|
||||
"sim": simData,
|
||||
"sim": Engine.GuiInterfaceCall("GetReplayMetadata"),
|
||||
"gui": {
|
||||
"dialog": false,
|
||||
"assignedPlayer": playerID,
|
||||
"assignedPlayer": Engine.GetPlayerID(),
|
||||
"disconnected": g_Disconnected,
|
||||
"isReplay": g_IsReplay,
|
||||
"replayDirectory": !g_HasRejoined && replayDirectory,
|
||||
"replayDirectory": !g_HasRejoined && Engine.GetCurrentReplayDirectory(),
|
||||
"replaySelectionData": g_ReplaySelectionData
|
||||
}
|
||||
};
|
||||
|
|
@ -583,9 +564,8 @@ function endGame(showSummary)
|
|||
{
|
||||
const menu = g_CampaignSession.getMenu();
|
||||
if (g_InitAttributes.campaignData.skipSummary)
|
||||
{
|
||||
return { "page": menu };
|
||||
}
|
||||
|
||||
summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run };
|
||||
summaryData.nextPage = menu;
|
||||
}
|
||||
|
|
@ -609,6 +589,7 @@ function getHotloadData()
|
|||
"selection": g_Selection.selected,
|
||||
"playerAssignments": g_PlayerAssignments,
|
||||
"player": g_Players,
|
||||
"tutorial": g_TutorialManager.getHotloadData()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -677,7 +658,7 @@ function onTick()
|
|||
Engine.GuiInterfaceCall("ClearRenamedEntities");
|
||||
}
|
||||
|
||||
function onSimulationUpdate(closePageCallback)
|
||||
function onSimulationUpdate()
|
||||
{
|
||||
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
|
||||
// g_TechnologyData data never changes, so it shouldn't be deleted.
|
||||
|
|
@ -705,7 +686,7 @@ function onSimulationUpdate(closePageCallback)
|
|||
handler();
|
||||
|
||||
// TODO: Move to handlers
|
||||
handleNotifications(closePageCallback);
|
||||
handleNotifications();
|
||||
updateGUIObjects();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
<script directory="gui/session/top_panel/"/>
|
||||
<script directory="gui/session/top_panel/IconButtons/"/>
|
||||
<script directory="gui/session/trade/"/>
|
||||
<script directory="gui/session/tutorial/"/>
|
||||
<script directory="gui/session/tutorial/panels/"/>
|
||||
|
||||
<object name="session">
|
||||
|
||||
|
|
@ -52,7 +54,7 @@
|
|||
<include file="gui/session/TimeNotificationOverlay.xml"/>
|
||||
<include file="gui/session/TopPanel.xml"/>
|
||||
<include file="gui/session/trade/TradeDialog.xml"/>
|
||||
<include file="gui/session/tutorial_panel.xml"/>
|
||||
<include file="gui/session/tutorial/Tutorial.xml"/>
|
||||
<include file="gui/session/Menu.xml"/>
|
||||
|
||||
<!-- Contains miscellanious objects s.a.: the technology research -->
|
||||
|
|
|
|||
|
|
@ -532,6 +532,49 @@
|
|||
<image backcolor="red" size="100%-1 0 100% 100%"/>
|
||||
</sprite>
|
||||
|
||||
<!-- ================================ ================================ -->
|
||||
<!-- Tutorial -->
|
||||
<!-- ================================ ================================ -->
|
||||
<sprite name="GuiExplanationPanel">
|
||||
<image texture="session/hud_panels.png"/>
|
||||
|
||||
<!-- top edge -->
|
||||
<image backcolor="41 32 11" size="0 0 100% 4"/>
|
||||
<image backcolor="164 133 57" size="2 2 100%-2 3"/>
|
||||
<image backcolor="221 180 87" size="1 1 100%-1 2"/>
|
||||
|
||||
<!-- bottom edge -->
|
||||
<image backcolor="41 32 11" size="0 100%-4 100% 100%"/>
|
||||
<image backcolor="164 133 57" size="2 100%-3 100%-2 100%-2"/>
|
||||
<image backcolor="221 180 87" size="1 100%-2 100%-1 100%-1"/>
|
||||
|
||||
<!-- left edge -->
|
||||
<image backcolor="41 32 11" size="0 4 4 100%-4"/>
|
||||
<image backcolor="164 133 57" size="2 3 3 100%-3"/>
|
||||
<image backcolor="221 180 87" size="1 2 2 100%-2"/>
|
||||
|
||||
<!-- right edge -->
|
||||
<image backcolor="41 32 11" size="100%-4 4 100% 100%-4"/>
|
||||
<image backcolor="164 133 57" size="100%-3 3 100%-2 100%-3"/>
|
||||
<image backcolor="221 180 87" size="100%-2 2 100%-1 100%-2"/>
|
||||
</sprite>
|
||||
|
||||
<sprite name="GoldenArrowDown">
|
||||
<image texture="session/golden-arrow_down.png"/>
|
||||
</sprite>
|
||||
|
||||
<sprite name="GoldenArrowUp">
|
||||
<image texture="session/golden-arrow_down.png" texture_size="0 100% 100% 0"/>
|
||||
</sprite>
|
||||
|
||||
<sprite name="GoldenArrowLeft">
|
||||
<image texture="session/golden-arrow_left.png"/>
|
||||
</sprite>
|
||||
|
||||
<sprite name="GoldernArrowRight">
|
||||
<image texture="session/golden-arrow_left.png" texture_size="100% 0 0 100%"/>
|
||||
</sprite>
|
||||
|
||||
<!-- ================================ ================================ -->
|
||||
<!-- Misc -->
|
||||
<!-- ================================ ================================ -->
|
||||
|
|
|
|||
|
|
@ -222,6 +222,57 @@
|
|||
text_valign="center"
|
||||
/>
|
||||
|
||||
<style name="TutorialInstructionText"
|
||||
buffer_zone="18"
|
||||
font="sans-bold-14"
|
||||
scrollbar="true"
|
||||
scrollbar_style="ModernScrollBar"
|
||||
textcolor="white"
|
||||
text_align="center"
|
||||
text_valign="center"
|
||||
sprite="ModernDarkBox"
|
||||
sprite_overlay="ModernDarkBoxGoldBorder"
|
||||
/>
|
||||
|
||||
<style name="TutorialInfoText"
|
||||
buffer_zone="16"
|
||||
font="sans-16"
|
||||
sprite="ModernDarkBoxGold"
|
||||
scrollbar="true"
|
||||
scrollbar_style="ModernScrollBar"
|
||||
scroll_bottom="true"
|
||||
textcolor="white"
|
||||
text_align="left"
|
||||
text_valign="top"
|
||||
/>
|
||||
|
||||
<style name="TutorialInfoTitle"
|
||||
buffer_zone="10"
|
||||
font="sans-bold-18"
|
||||
textcolor="white"
|
||||
text_align="center"
|
||||
text_valign="center"
|
||||
/>
|
||||
|
||||
<style name="TutorialExplanationTitle"
|
||||
buffer_zone="10"
|
||||
font="sans-bold-18"
|
||||
textcolor="gold"
|
||||
text_align="center"
|
||||
text_valign="top"
|
||||
/>
|
||||
|
||||
<style name="TutorialExplanationText"
|
||||
buffer_zone="10"
|
||||
font="sans-16"
|
||||
sprite="ModernFade"
|
||||
textcolor="white"
|
||||
scrollbar="true"
|
||||
scrollbar_style="ModernScrollBar"
|
||||
text_align="center"
|
||||
text_valign="top"
|
||||
/>
|
||||
|
||||
<!-- ================================ ================================ -->
|
||||
<!-- Icon Styles -->
|
||||
<!-- ================================ ================================ -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="tutorialPanels">
|
||||
<include directory="gui/session/tutorial/panels/"/>
|
||||
</object>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
// Needs to be kept in sync with the one in maps/scripts/Tutorial.js
|
||||
const TUTORIAL_STEP_TYPE = deepfreeze({
|
||||
"INSTRUCTION": 1,
|
||||
"INFO": 2,
|
||||
"GUI_EXPLANATION": 3
|
||||
});
|
||||
|
||||
/**
|
||||
* This class manages the tutorial UI, which consists of various panels, which correspond to the different step types.
|
||||
* It processes all tutorial notifications received from the simulation, translates them and passes them along to
|
||||
* according panel.
|
||||
*/
|
||||
class TutorialManager
|
||||
{
|
||||
parentObj = Engine.GetGUIObjectByName("tutorialPanels");
|
||||
|
||||
panels = new Map([
|
||||
[TUTORIAL_STEP_TYPE.INSTRUCTION, new InstructionPanel(this.continue.bind(this))],
|
||||
[TUTORIAL_STEP_TYPE.INFO, new InfoPanel(this.continue.bind(this))],
|
||||
[TUTORIAL_STEP_TYPE.GUI_EXPLANATION, new GuiExplanationPanel(this.continue.bind(this))]
|
||||
]);
|
||||
|
||||
displayedSteps = []; // All steps that have already been displayed, in the form of [stepType, panelData]
|
||||
pendingSteps = []; // All received steps that have yet to be displayed, in the form of [stepType, panelData]
|
||||
activePanel;
|
||||
currentWarning = "";
|
||||
isFinished = false;
|
||||
closeSession;
|
||||
|
||||
constructor(closeSession, hotloadData)
|
||||
{
|
||||
this.closeSession = closeSession;
|
||||
|
||||
this.parentObj.hidden = true;
|
||||
|
||||
if (hotloadData)
|
||||
{
|
||||
for (const [stepType, panelData] of hotloadData.displayedSteps)
|
||||
this.displayStep(stepType, panelData);
|
||||
|
||||
this.pendingSteps = hotloadData.pendingSteps;
|
||||
if (hotloadData.warning)
|
||||
this.displayWarning(hotloadData.warning);
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisibility()
|
||||
{
|
||||
this.parentObj.hidden = !this.displayedSteps.length || !this.parentObj.hidden;
|
||||
}
|
||||
|
||||
handleNotification(notification)
|
||||
{
|
||||
if (notification.step)
|
||||
{
|
||||
// The step might be defined in a way that requires us to split it up into several
|
||||
// before displaying them.
|
||||
|
||||
while (this.pendingSteps.length)
|
||||
this.displayNextPendingStep();
|
||||
|
||||
// To minimise duplication in the trigger script, consecutive steps that only
|
||||
// differ in the 'text' property can be combined into a single one with an array
|
||||
// 'texts'. Here we need revert this again by splitting it back into different
|
||||
// steps before passing them to the panels.
|
||||
if (Array.isArray(notification.step.panelData.texts))
|
||||
{
|
||||
const { texts, ...sharedData } = notification.step.panelData;
|
||||
this.pendingSteps = texts.map((text, i) => ([
|
||||
notification.step.type, {
|
||||
...sharedData, text,
|
||||
// Note: These could be ignored by some panels.
|
||||
"appendable": sharedData.appendable || i === 0,
|
||||
"appendToPrevious": sharedData.appendToPrevious || i > 0
|
||||
}
|
||||
]));
|
||||
this.displayNextPendingStep();
|
||||
}
|
||||
else
|
||||
this.displayStep(notification.step.type, notification.step.panelData);
|
||||
}
|
||||
|
||||
if (notification.warning)
|
||||
this.displayWarning(notification.warning);
|
||||
}
|
||||
|
||||
displayWarning(warning)
|
||||
{
|
||||
this.currentWarning = warning;
|
||||
this.activePanel.displayWarning(translate(warning));
|
||||
}
|
||||
|
||||
displayNextPendingStep()
|
||||
{
|
||||
this.displayStep(...this.pendingSteps.shift());
|
||||
}
|
||||
|
||||
displayStep(stepType, panelData)
|
||||
{
|
||||
if (this.isFinished)
|
||||
return;
|
||||
|
||||
this.parentObj.hidden = false;
|
||||
this.isFinished = panelData.isLast;
|
||||
|
||||
translateObjectKeys(panelData, ["text", "title"]);
|
||||
if (panelData.hotkeys)
|
||||
{
|
||||
let i = 0;
|
||||
panelData.text = panelData.hotkeys.length == 1 ?
|
||||
colorizeHotkey(panelData.text, panelData.hotkeys[0]) :
|
||||
sprintf(panelData.text, panelData.hotkeys.reduce((obj, hotkey) =>
|
||||
{
|
||||
obj["hotkey" + ++i] = colorizeHotkey("%(hotkey)s", hotkey);
|
||||
return obj;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
if (!this.panels.has(stepType))
|
||||
throw new Error("Failed to display tutorial step: Unkown step type: " + stepType);
|
||||
|
||||
// Explicitly don't hide the previously active panel if the new step is a GUI explanation. It's displayed
|
||||
// "on top" of everything else.
|
||||
if (stepType == TUTORIAL_STEP_TYPE.GUI_EXPLANATION)
|
||||
{
|
||||
this.panels.get(TUTORIAL_STEP_TYPE.GUI_EXPLANATION).setVisible(true);
|
||||
this.activePanel.setEnabled(false);
|
||||
}
|
||||
else
|
||||
this.panels.forEach((panel, type) => panel.setVisible(type == stepType));
|
||||
|
||||
this.activePanel = this.panels.get(stepType);
|
||||
this.activePanel.setEnabled(true);
|
||||
this.activePanel.displayStep(panelData);
|
||||
|
||||
this.displayedSteps.push([stepType, panelData]);
|
||||
this.currentWarning = "";
|
||||
}
|
||||
|
||||
continue()
|
||||
{
|
||||
if (this.isFinished)
|
||||
this.closeSession(true);
|
||||
else if (this.pendingSteps.length)
|
||||
this.displayNextPendingStep();
|
||||
else
|
||||
Engine.PostNetworkCommand({ "type": "dialog-answer", "tutorial": "continue" });
|
||||
}
|
||||
|
||||
getHotloadData()
|
||||
{
|
||||
return {
|
||||
"displayedSteps": this.displayedSteps,
|
||||
"pendingSteps": this.pendingSteps,
|
||||
"warning": this.currentWarning
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Generic superclass for tutorial panels that manages:
|
||||
* - a text object showing a step's main content
|
||||
* - a hint object giving hints or showing warnings
|
||||
* - a button to continue the tutorial or quit when it's over
|
||||
*/
|
||||
class TutorialPanel
|
||||
{
|
||||
panel;
|
||||
text;
|
||||
hint;
|
||||
button;
|
||||
|
||||
constructor(name, continueCallback)
|
||||
{
|
||||
this.panel = Engine.GetGUIObjectByName(name);
|
||||
this.text = Engine.GetGUIObjectByName(name + "Text");
|
||||
this.hint = Engine.GetGUIObjectByName(name + "Hint");
|
||||
this.button = Engine.GetGUIObjectByName(name + "Button");
|
||||
|
||||
this.button.caption = this.ButtonCaptions.Continue;
|
||||
this.button.onPress = continueCallback;
|
||||
}
|
||||
|
||||
setVisible(visible)
|
||||
{
|
||||
this.panel.hidden = !visible;
|
||||
}
|
||||
|
||||
setEnabled(enabled)
|
||||
{
|
||||
this.button.enabled = enabled;
|
||||
}
|
||||
|
||||
displayWarning(warning)
|
||||
{
|
||||
this.hint.caption = coloredText(warning, this.WarningColor);
|
||||
}
|
||||
|
||||
displayStep(step)
|
||||
{
|
||||
this.text.caption = step.text;
|
||||
this.button.hidden = !step.showContinueButton;
|
||||
|
||||
if (step.isLast)
|
||||
{
|
||||
this.hint.caption = this.HintCaptions.Quit;
|
||||
this.button.caption = this.ButtonCaptions.Quit;
|
||||
return;
|
||||
}
|
||||
|
||||
this.button.caption = step.continueButton || this.ButtonCaptions.Continue;
|
||||
this.hint.caption = step.isDone ?
|
||||
this.HintCaptions.Done :
|
||||
step.hint || (step.showContinueButton ?
|
||||
this.HintCaptions.Continue :
|
||||
this.HintCaptions.Instruction);
|
||||
}
|
||||
}
|
||||
|
||||
TutorialPanel.prototype.WarningColor = "orange";
|
||||
|
||||
TutorialPanel.prototype.ButtonCaptions = {
|
||||
"Quit": translateWithContext("button caption", "Quit"),
|
||||
"Continue": translateWithContext("button caption", "Continue")
|
||||
};
|
||||
|
||||
TutorialPanel.prototype.HintCaptions = {
|
||||
"Continue": translate("Click to continue."),
|
||||
"Instruction": translate("Follow the instructions."),
|
||||
"Done": translate("You have already done this."),
|
||||
"Quit": translate("Click to quit this tutorial.")
|
||||
};
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
/**
|
||||
* This class manages a tutorial panel meant to explain GUI elements (what they show or do) to the player, like
|
||||
* "The resource counters at the top show how many resources you have ...".
|
||||
* The explanations consist of a title and text. It also highlights the target GUI objects by fading out everything else
|
||||
* with black and uses an arrow to point to them directly.
|
||||
* If the target object is hidden the panel is still shown (pointing to nothing), but a warning displayed.
|
||||
*/
|
||||
class GuiExplanationPanel extends TutorialPanel
|
||||
{
|
||||
panel = Engine.GetGUIObjectByName("guiExplanationPanel");
|
||||
backgroundFade = Engine.GetGUIObjectByName("guiExplanationPanelFade");
|
||||
arrow = Engine.GetGUIObjectByName("guiExplanationPanelArrow");
|
||||
|
||||
currentStep;
|
||||
targetObject;
|
||||
wasTargetHidden = false;
|
||||
|
||||
constructor(continueCallback)
|
||||
{
|
||||
super("guiExplanationPanel", () =>
|
||||
{
|
||||
this.resetTargetObjectZ();
|
||||
this.targetObject = null;
|
||||
continueCallback();
|
||||
});
|
||||
|
||||
// Place the panel in front of all other session objects.
|
||||
this.panel.z = this.OverlayZValue + 10;
|
||||
// Place the black overlay behind the panel.
|
||||
this.backgroundFade.z = this.OverlayZValue - 10;
|
||||
|
||||
// We need to set the arrow's Z value explicitly since it has 'absolute' set to true, which makes it not inherit
|
||||
// the parent's value by default.
|
||||
this.arrow.z = this.OverlayZValue;
|
||||
|
||||
// Initialize the size to a default, with all relative values set to 0.
|
||||
// This is necessary for the custom sizing later.
|
||||
this.panel.size = { "right": this.PanelWidth, "bottom": this.MaxPanelHeight };
|
||||
this.arrow.size = { "right": this.ArrowScale * 2, "bottom": this.ArrowScale };
|
||||
|
||||
this.panel.onWindowResized = this.updatePanelSize.bind(this);
|
||||
this.panel.onTick = this.updateContent.bind(this);
|
||||
}
|
||||
|
||||
setVisible(visible)
|
||||
{
|
||||
super.setVisible(visible);
|
||||
if (!visible)
|
||||
this.resetTargetObjectZ();
|
||||
}
|
||||
|
||||
resetTargetObjectZ()
|
||||
{
|
||||
if (this.targetObject)
|
||||
this.targetObject.z = this.targetOriginalZ;
|
||||
}
|
||||
|
||||
displayStep(step)
|
||||
{
|
||||
super.displayStep(step);
|
||||
|
||||
this.resetTargetObjectZ();
|
||||
|
||||
if (!["left", "top", "right", "bottom"].includes(step.side))
|
||||
throw new Error("GuiExplanationPanel: Invalid or no side specified:" + step.side + ". It must be 'left', 'top', 'right', or 'bottom'.");
|
||||
|
||||
this.targetObject = Engine.TryGetGUIObjectByName(step.targetObject);
|
||||
if (!this.targetObject)
|
||||
throw new Error("GuiExplanationPanel: Non-existing target GUI object specified: '" + step.targetObject + "'.");
|
||||
|
||||
this.currentStep = step;
|
||||
|
||||
// This results in the target's grandchildren taking higher Z values than this panel (since they get their parent's value + 10).
|
||||
// But that's ok, since they should never overlap anyway.
|
||||
this.targetOriginalZ = this.targetObject.z;
|
||||
this.targetObject.z = this.OverlayZValue;
|
||||
|
||||
this.arrow.sprite = this.ArrowSprites[step.side];
|
||||
|
||||
this.wasTargetHidden = undefined;
|
||||
this.updateContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the visibility of the target object and update the text and background fade accordingly, if necessary.
|
||||
*/
|
||||
updateContent()
|
||||
{
|
||||
if (!this.targetObject)
|
||||
return;
|
||||
|
||||
const displayTextAndTitle = (title, text) =>
|
||||
{
|
||||
this.text.caption = (title ? setStringTags(title, this.TitleTags) + setStringTags("\n\n", { "font": "sans-3" }) : "") + text;
|
||||
this.updatePanelSize();
|
||||
};
|
||||
|
||||
if (this.targetObject.hidden && !this.wasTargetHidden)
|
||||
{
|
||||
this.backgroundFade.hidden = true;
|
||||
displayTextAndTitle(this.currentStep.title, this.HiddenWarning);
|
||||
this.wasTargetHidden = true;
|
||||
}
|
||||
else if (!this.targetObject.hidden && (this.wasTargetHidden == undefined || this.wasTargetHidden))
|
||||
{
|
||||
this.backgroundFade.hidden = false;
|
||||
displayTextAndTitle(this.currentStep.title, this.currentStep.text);
|
||||
this.wasTargetHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place the panel next to the target object on the desired side relative to it.
|
||||
*/
|
||||
updatePanelSize()
|
||||
{
|
||||
if (!this.targetObject)
|
||||
return;
|
||||
|
||||
const panelHeight = Math.min(this.MaxPanelHeight, this.text.size.top + this.text.getTextSize().height - this.text.size.bottom);
|
||||
const targetComputedSize = this.targetObject.getComputedSize();
|
||||
const targetHorizontalCenter = (targetComputedSize.left + targetComputedSize.right) / 2;
|
||||
const targetVerticalCenter = (targetComputedSize.top + targetComputedSize.bottom) / 2;
|
||||
|
||||
// Don't perfectly center the panel on the target object, instead shift it to one side a bit.
|
||||
// That looks better.
|
||||
const panelPlacementRatio = 0.3;
|
||||
|
||||
if (this.currentStep.side == "top" || this.currentStep.side == "bottom")
|
||||
{
|
||||
this.panel.size.left = targetHorizontalCenter - panelPlacementRatio * this.PanelWidth;
|
||||
this.panel.size.right = targetHorizontalCenter + (1 - panelPlacementRatio) * this.PanelWidth;
|
||||
this.arrow.size.left = targetHorizontalCenter - this.ArrowScale;
|
||||
this.arrow.size.right = targetHorizontalCenter + this.ArrowScale;
|
||||
|
||||
if (this.currentStep.side == "top")
|
||||
{
|
||||
this.panel.size.bottom = targetComputedSize.top - this.PanelDistanceFromTarget;
|
||||
this.panel.size.top = this.panel.size.bottom - panelHeight;
|
||||
this.arrow.size.bottom = targetComputedSize.top - this.ArrowDistanceFromTarget;
|
||||
this.arrow.size.top = this.arrow.size.bottom - this.ArrowScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.panel.size.top = targetComputedSize.bottom + this.PanelDistanceFromTarget;
|
||||
this.panel.size.bottom = this.panel.size.top + panelHeight;
|
||||
this.arrow.size.top = targetComputedSize.bottom + this.ArrowDistanceFromTarget;
|
||||
this.arrow.size.bottom = this.arrow.size.top + this.ArrowScale;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.panel.size.top = targetVerticalCenter - panelPlacementRatio * panelHeight;
|
||||
this.panel.size.bottom = targetVerticalCenter + (1 - panelPlacementRatio) * panelHeight;
|
||||
this.arrow.size.top = targetVerticalCenter - this.ArrowScale;
|
||||
this.arrow.size.bottom = targetVerticalCenter + this.ArrowScale;
|
||||
|
||||
if (this.currentStep.side == "left")
|
||||
{
|
||||
this.panel.size.right = targetComputedSize.left - this.PanelDistanceFromTarget;
|
||||
this.panel.size.left = this.panel.size.right - this.PanelWidth;
|
||||
this.arrow.size.right = targetComputedSize.left - this.ArrowDistanceFromTarget;
|
||||
this.arrow.size.left = this.arrow.size.right - this.ArrowScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.panel.size.left = targetComputedSize.right + this.PanelDistanceFromTarget;
|
||||
this.panel.size.right = this.panel.size.left + this.PanelWidth;
|
||||
this.arrow.size.left = targetComputedSize.right + this.ArrowDistanceFromTarget;
|
||||
this.arrow.size.right = this.arrow.size.left + this.ArrowScale;
|
||||
}
|
||||
}
|
||||
|
||||
let root = this.panel;
|
||||
while (root.parent) root = root.parent;
|
||||
const screenSize = root.getComputedSize();
|
||||
|
||||
// Perform some clamping to ensure the panel is always fully visible on the screen.
|
||||
if (this.panel.size.left < 0)
|
||||
{
|
||||
this.panel.size.right -= this.panel.size.left;
|
||||
this.panel.size.left = 0;
|
||||
}
|
||||
else if (this.panel.size.right > screenSize.right)
|
||||
{
|
||||
this.panel.size.left -= this.panel.size.right - screenSize.right;
|
||||
this.panel.size.right = screenSize.right;
|
||||
}
|
||||
if (this.panel.size.top < 0)
|
||||
{
|
||||
this.panel.size.bottom -= this.panel.size.top;
|
||||
this.panel.size.top = 0;
|
||||
}
|
||||
else if (this.panel.size.bottom > screenSize.bottom)
|
||||
{
|
||||
this.panel.size.top -= this.panel.size.bottom - screenSize.bottom;
|
||||
this.panel.size.bottom = screenSize.bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning shown whilever the target objects is hidden.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.HiddenWarning = coloredText(
|
||||
translate("It is currently hidden. Follow the previous instructions for it to be shown again."),
|
||||
"orange"
|
||||
);
|
||||
|
||||
/**
|
||||
* GUI tags applied to the shown title.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.TitleTags = { "color": "gold", "font": "sans-bold-18" };
|
||||
|
||||
/**
|
||||
* Which sprite to use depending on the side that the description panel is placed on. The sprites only differ in rotation.
|
||||
* E.g. if the panel is placed to the left of the target object, the arrow has to point to the right.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.ArrowSprites = {
|
||||
"left": "GoldenArrowRight",
|
||||
"top": "GoldenArrowDown",
|
||||
"right": "GoldenArrowLeft",
|
||||
"bottom": "GoldenArrowUp"
|
||||
};
|
||||
|
||||
GuiExplanationPanel.prototype.PanelWidth = 600;
|
||||
GuiExplanationPanel.prototype.MaxPanelHeight = 200;
|
||||
|
||||
/**
|
||||
* Margin between the target object and the panel itself. The arrow has to fit inside that gap.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.PanelDistanceFromTarget = 40;
|
||||
|
||||
/**
|
||||
* Margin between the target object and the arrow.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.ArrowDistanceFromTarget = 5;
|
||||
|
||||
/**
|
||||
* Greater than the z value of all other session GUI objects (except the watermark).
|
||||
*/
|
||||
GuiExplanationPanel.prototype.OverlayZValue = 250;
|
||||
|
||||
/**
|
||||
* The arrow is always twice as wide as it is long.
|
||||
* Therefore, for vertical arrows (pointing up or down) this defines the height and half the width,
|
||||
* while for horizontal arrows (pointing left or right) this defines the width and half the height.
|
||||
*/
|
||||
GuiExplanationPanel.prototype.ArrowScale = 32;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="guiExplanationPanel" type="image" sprite="GuiExplanationPanel">
|
||||
<object name="guiExplanationPanelFade" type="image" sprite="color:0 0 0 125" ghost="true" absolute="true"/>
|
||||
<object name="guiExplanationPanelText" type="text" size="18 18 100%-18 100%-54" style="TutorialExplanationText"/>
|
||||
<object name="guiExplanationPanelButton" type="button" style="ModernButtonRed" size="100%-170 100%-42 100%-30 100%-14"/>
|
||||
<object name="guiExplanationPanelHint" type="text" style="ModernLeftLabelText" size="25 100%-48 100%-190 100%-8"/>
|
||||
<object name="guiExplanationPanelArrow" type="image" absolute="true"/>
|
||||
</object>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* This class manages a tutorial panel meant to explain game concepts or mechanics to the player, like selecting units
|
||||
* or bartering, for example.
|
||||
* These explanations consist of a title and paragraphs of texts. New steps to display don't have to replace the
|
||||
* preceding ones; with the 'appendable' flag they can add a paragraph to the bottom of an existing explanation instead
|
||||
* making a brand new one.
|
||||
*/
|
||||
class InfoPanel extends TutorialPanel
|
||||
{
|
||||
title = Engine.GetGUIObjectByName("infoPanelTitle");
|
||||
|
||||
paragraphs = [];
|
||||
appendable = true;
|
||||
|
||||
constructor(continueCallback)
|
||||
{
|
||||
super("infoPanel", continueCallback);
|
||||
}
|
||||
|
||||
displayStep(step)
|
||||
{
|
||||
super.displayStep(step);
|
||||
|
||||
if (step.appendToPrevious && !this.appendable)
|
||||
{
|
||||
error("InfoPanel: Can't append step to previous because previous is marked unappendable.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!step.appendToPrevious)
|
||||
{
|
||||
this.paragraphs = [];
|
||||
if (step.title)
|
||||
this.title.caption = step.title;
|
||||
else
|
||||
this.title.hidden = true;
|
||||
}
|
||||
|
||||
// Appendable being false means that no paragraph will be appended to this (first one) in the future.
|
||||
// In that case, we want to portray it as a single piece of info rather than a list of them.
|
||||
this.appendable = step.appendToPrevious || step.appendable;
|
||||
if (this.appendable)
|
||||
{
|
||||
this.text.text_align = "left";
|
||||
const newParagraph = sprintf(this.BulletPointFormat, { "text": step.text });
|
||||
this.text.caption = this.paragraphs.length ?
|
||||
this.paragraphs.concat(coloredText(newParagraph, this.NewParagraphColor)).join("\n") :
|
||||
newParagraph;
|
||||
this.paragraphs.push(newParagraph);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.text.text_align = "center";
|
||||
this.text.caption = step.text;
|
||||
}
|
||||
|
||||
this.text.size.top = this.title.hidden || !this.title.caption ? 27 : 54;
|
||||
const panelHeight = Math.min(this.MaxPanelHeight, this.text.size.top + this.text.getTextSize().height - this.text.size.bottom);
|
||||
this.panel.size.bottom = this.panel.getComputedSize().top + panelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
InfoPanel.prototype.BulletPointFormat = translateWithContext("bullet point format", " • %(text)s");
|
||||
InfoPanel.prototype.NewParagraphColor = "gold";
|
||||
|
||||
InfoPanel.prototype.MaxPanelHeight = 235;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="infoPanel" type="image" sprite="ModernDialog" size="50%-365 90 50%+365 345">
|
||||
<object type="text" size="50%-96 -16 50%+96 10" style="TitleText">
|
||||
<translatableAttribute id="caption">Information</translatableAttribute>
|
||||
</object>
|
||||
<object name="infoPanelTitle" type="text" size="25 17 100%-25 52" style="TutorialInfoTitle"/>
|
||||
<object name="infoPanelText" type="text" size="25 52 100%-25 100%-64" style="TutorialInfoText"/>
|
||||
<object name="infoPanelHint" type="text" style="ModernLeftLabelText" size="30 100%-54 100%-200 100%-14"/>
|
||||
<object name="infoPanelButton" type="button" size="100%-180 100%-48 100%-30 100%-20" style="ModernButtonRed"/>
|
||||
</object>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* This class manages a tutorial panel meant to display basic instructions of simple tasks for the player to fulfill,
|
||||
* consisting of just a text, like "Order one of your units to build a house."
|
||||
*/
|
||||
class InstructionPanel extends TutorialPanel
|
||||
{
|
||||
instructions = [];
|
||||
|
||||
constructor(continueCallback)
|
||||
{
|
||||
super("instructionPanel", continueCallback);
|
||||
}
|
||||
|
||||
displayStep(step)
|
||||
{
|
||||
super.displayStep(step);
|
||||
|
||||
const panelHeight = Math.min(this.MaxPanelHeight, this.text.size.top + this.text.getTextSize().height - this.text.size.bottom);
|
||||
this.panel.size.bottom = this.panel.getComputedSize().top + panelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
InstructionPanel.prototype.MaxPanelHeight = 185;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="instructionPanel" type="image" sprite="color:0 0 0 125" size="50%-310 80 50%+310 210">
|
||||
<object name="instructionPanelText" type="text" style="TutorialInstructionText" size="10 10 100%-10 100%-52"/>
|
||||
<object name="instructionPanelHint" type="text" style="ModernLeftLabelText" size="25 100%-46 100%-190 100%-6"/>
|
||||
<object name="instructionPanelButton" type="button" style="ModernButtonRed" size="100%-170 100%-40 100%-30 100%-12"/>
|
||||
</object>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object name="tutorialPanel" type="image" sprite="ModernFade" size="50%-380 38 50%+380 292" hidden="true">
|
||||
|
||||
<object size="10 10 100%-20 100%-48">
|
||||
<object name="tutorialText" type="text" style="TutorialPanel"/>
|
||||
</object>
|
||||
|
||||
<object name="tutorialWarning" type="text" style="ModernLeftLabelText" size="10 100%-42 100%-20 100%-12"/>
|
||||
|
||||
<object name="tutorialReady" type="button" style="ModernButtonRed" size="100%-160 100%-42 100%-20 100%-12">
|
||||
<translatableAttribute id="caption">Ready</translatableAttribute>
|
||||
<action on="Press">Engine.PostNetworkCommand({"type": "dialog-answer", "tutorial": "ready"});</action>
|
||||
</object>
|
||||
</object>
|
||||
|
|
@ -208,3 +208,13 @@
|
|||
“This is a woman's resolve; as for men, they may live and be slaves.” \n— Boudicca, rallying against Roman occupation (Tacitus's Annals, Book 14, Chapter 35)
|
||||
“There are two things, knowledge and opinion; one makes its possessor know, the other to be ignorant.” \n— (Hippocratic treatise known as “The Law”, part of the Hippocratic Corpus)
|
||||
“Let them hate me, so long as they fear me.” \n— Caligula (Suetonius, The Twelve Caesars, Book 30)
|
||||
“There is no instance of a nation benefiting from prolonged warfare.” \n— Sun Tzu (“The Art of War”, Chapter 2)
|
||||
“To subdue the enemy without fighting is the acme of skill.” \n— Sun Tzu (“The Art of War”, Chapter 3)
|
||||
“One defends when his strength is inadequate; he attacks when it is abundant.” \n— Sun Tzu (“The Art of War”, Chapter 4)
|
||||
“When torrential water tosses boulders, it is because of its momentum. When the strike of a hawk breaks the body of its prey, it is because of timing.” \n— Sun Tzu (“The Art of War”, Chapter 5)
|
||||
“Treat your men as you would your own beloved sons. And they will follow you into the deepest valley.” \n— Sun Tzu (“The Art of War”, Chapter 10)
|
||||
“Woe to the defeated!” \n— Brennus disputing the weight of gold ransom while sacking Rome c. 387 BC (Livy, “Ab Urbe Condita”, Book 5, Chapter 48)
|
||||
“\[Brutus] found the women fighting and perishing in company with the men with such bravery that they uttered no cry even in the midst of slaughter.” \n— Brutus in Lusitania during the battles with Viriathus (Appian of Alexandria, “The Spanish Wars”, 15)
|
||||
“What I desire for my own children, I desire for all men.” \n— Ashoka the Great (“The Kalinga Rock Edicts”)
|
||||
“If you Romans choose to lord it over the world, does it follow that the world is to accept slavery ?” \n— Caratacus pleading for pardon from Roman Emperor Claudius in Rome (Tacitus, “Annals”, 12.37)
|
||||
“And where am I to get so many soldiers ?” \n— Septimius Severus, frustrated with the challenge of controlling his own men and the harsh realities of frontier warfare against the northern tribes in Britannia. (Cassius Dio, “Roman History”, Vol IX, Book LXXVI, Section 11)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
function RandomMapLogger()
|
||||
{
|
||||
this.lastTime = undefined;
|
||||
this.taskStarted = false;
|
||||
this.startTime = Engine.GetMicroseconds ? Engine.GetMicroseconds() : 0;
|
||||
this.prefix = ""; // seems noisy
|
||||
|
||||
|
|
@ -21,27 +21,18 @@ RandomMapLogger.prototype.printDirectly = function(string)
|
|||
|
||||
RandomMapLogger.prototype.print = function(string)
|
||||
{
|
||||
this.printDuration();
|
||||
this.printDirectly(this.prefix + string + "...");
|
||||
this.lastTime = Engine.GetMicroseconds();
|
||||
};
|
||||
if (this.taskStarted)
|
||||
Engine.ProfileStop();
|
||||
|
||||
RandomMapLogger.prototype.printDuration = function()
|
||||
{
|
||||
if (!this.lastTime)
|
||||
return;
|
||||
|
||||
this.printDurationDirectly("", this.lastTime);
|
||||
this.lastTime = Engine.GetMicroseconds();
|
||||
const text = this.prefix + string;
|
||||
this.printDirectly(text + "...\n");
|
||||
Engine.ProfileStart(text);
|
||||
this.taskStarted = true;
|
||||
};
|
||||
|
||||
RandomMapLogger.prototype.close = function()
|
||||
{
|
||||
this.printDuration();
|
||||
this.printDurationDirectly(this.prefix + "Total map generation time:", this.startTime);
|
||||
};
|
||||
|
||||
RandomMapLogger.prototype.printDurationDirectly = function(text, startTime)
|
||||
{
|
||||
this.printDirectly(text + " " + ((Engine.GetMicroseconds() - startTime) / 1000000).toFixed(6) + "s.\n");
|
||||
if (this.taskStarted)
|
||||
Engine.ProfileStop();
|
||||
this.printDirectly(this.prefix + "Map generation finished.\n");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -370,8 +370,7 @@ Trigger.prototype.RemoveSpread = function()
|
|||
|
||||
Trigger.prototype.EndGame = function()
|
||||
{
|
||||
Engine.QueryInterface(3, IID_Player).SetState("defeated", "trigger");
|
||||
Engine.QueryInterface(4, IID_Player).SetState("won", "trigger");
|
||||
TriggerHelper.SetPlayerWon(3, "trigger", "trigger");
|
||||
};
|
||||
|
||||
var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
|
||||
|
|
|
|||
|
|
@ -433,8 +433,7 @@ Trigger.prototype.RunExperiment = function(data)
|
|||
|
||||
Trigger.prototype.EndGame = function()
|
||||
{
|
||||
Engine.QueryInterface(4, IID_Player).SetState("defeated", "trigger");
|
||||
Engine.QueryInterface(3, IID_Player).SetState("won", "trigger");
|
||||
TriggerHelper.SetPlayerWon(3, "trigger", "trigger");
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ Trigger.prototype.StartCaptureTheRelicCountdown = function(winningPlayers)
|
|||
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
|
||||
{
|
||||
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||
if (cmpPlayer.GetState() == "won")
|
||||
if (cmpPlayer.HasWon())
|
||||
return;
|
||||
|
||||
if (winningPlayers.indexOf(playerID) == -1)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Trigger.prototype.ConquestOwnershipChanged = function(msg)
|
|||
{
|
||||
const cmpPlayer = QueryPlayerIDInterface(msg.from);
|
||||
if (cmpPlayer)
|
||||
cmpPlayer.SetState("defeated", query.defeatReason);
|
||||
cmpPlayer.Defeat(query.defeatReason);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ TriggerHelper.DefeatPlayer = function(playerID, defeatReason)
|
|||
{
|
||||
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||
if (cmpPlayer)
|
||||
cmpPlayer.SetState("defeated", defeatReason);
|
||||
cmpPlayer.Defeat(defeatReason);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -603,4 +603,147 @@ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, c
|
|||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes an entity permanently visible to all players by:
|
||||
* - Setting alwaysVisible flag
|
||||
* - Deactivating fogging (no mirages)
|
||||
* - Exploring the surrounding area
|
||||
* - Forcing a visibility update
|
||||
*
|
||||
* @param {number} ent - Entity ID to make permanently visible
|
||||
*/
|
||||
TriggerHelper.MakeEntityPermanentlyVisible = function(ent)
|
||||
{
|
||||
const cmpVisibility = Engine.QueryInterface(ent, IID_Visibility);
|
||||
if (cmpVisibility)
|
||||
{
|
||||
cmpVisibility.alwaysVisible = true;
|
||||
cmpVisibility.SetActivated(true);
|
||||
}
|
||||
|
||||
const cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
|
||||
if (cmpFogging)
|
||||
cmpFogging.PermanentlyReveal("all");
|
||||
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
const cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
if (cmpRangeManager && cmpPosition && cmpPosition.IsInWorld())
|
||||
{
|
||||
const pos = cmpPosition.GetPosition2D();
|
||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||
|
||||
const exploreRadius = TriggerHelper.CalculateExplorationRadius(ent);
|
||||
|
||||
for (let i = 1; i < numPlayers; ++i)
|
||||
cmpRangeManager.ExploreCircle(i, pos.x, pos.y, exploreRadius);
|
||||
|
||||
cmpRangeManager.RequestVisibilityUpdate(ent);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a reasonable exploration radius from an entity's footprint.
|
||||
* Calculates the radius as the entity's full extent to reveal the surrounding area.
|
||||
* This is equivalent to (extent/2) * 2, giving a 2x margin around the entity.
|
||||
*/
|
||||
TriggerHelper.CalculateExplorationRadius = function(ent)
|
||||
{
|
||||
const cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
|
||||
if (!cmpFootprint)
|
||||
return 50;
|
||||
|
||||
const shapeInfo = cmpFootprint.GetShape();
|
||||
|
||||
let halfExtent; // The radius of the entity (half its size)
|
||||
if (shapeInfo && shapeInfo.type === "circle")
|
||||
halfExtent = shapeInfo.radius || shapeInfo.size0;
|
||||
else if (shapeInfo && shapeInfo.type === "square")
|
||||
halfExtent = Math.max(shapeInfo.width || shapeInfo.size0, shapeInfo.depth || shapeInfo.size1) / 2;
|
||||
else
|
||||
return 50;
|
||||
|
||||
// Use 2x the entity's radius to reveal surrounding area
|
||||
// This gives a comfortable margin around the entity
|
||||
const radius = halfExtent * 2;
|
||||
return radius;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a notification to specific players.
|
||||
*
|
||||
* @param {string} message - The message to display (use markForTranslation)
|
||||
* @param {number[]} players - Array of player IDs to send the notification to
|
||||
* @param {number} duration - Duration in milliseconds
|
||||
* @param {Object} parameters - Translation parameters (optional)
|
||||
*/
|
||||
TriggerHelper.SendNotification = function(message, players, duration = 30000, parameters = {}, translateParameters = [])
|
||||
{
|
||||
const cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||
if (!cmpGuiInterface)
|
||||
return;
|
||||
|
||||
// Ensure all parameter values are primitive types
|
||||
const primitiveParameters = {};
|
||||
for (const key in parameters)
|
||||
{
|
||||
const value = parameters[key];
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
||||
primitiveParameters[key] = value;
|
||||
else
|
||||
primitiveParameters[key] = String(value);
|
||||
}
|
||||
|
||||
cmpGuiInterface.AddTimeNotification(
|
||||
{
|
||||
"message": message,
|
||||
"players": players,
|
||||
"parameters": primitiveParameters,
|
||||
"translateMessage": true,
|
||||
"translateParameters": translateParameters
|
||||
},
|
||||
duration
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a notification to a specific player and a different notification to all other active players.
|
||||
*
|
||||
* @param {number} player - The player ID who gets the 'owner' message
|
||||
* @param {string} ownerMessage - Message for the specified player (use markForTranslation)
|
||||
* @param {string} othersMessage - Message for all other active players (use markForTranslation)
|
||||
* @param {number} duration - Duration in milliseconds
|
||||
* @param {Object} parameters - Translation parameters shared by both messages (optional)
|
||||
* @param {Object} ownerParameters - Additional parameters for the owner message (optional)
|
||||
* @param {Object} othersParameters - Additional parameters for the others message (optional)
|
||||
*/
|
||||
TriggerHelper.SendDualNotification = function(player, ownerMessage, othersMessage, duration = 30000, parameters = {}, ownerParameters = {}, othersParameters = {})
|
||||
{
|
||||
const numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
|
||||
|
||||
const others = [-1]; // Include observers (player id -1)
|
||||
for (let playerID = 1; playerID < numPlayers; ++playerID)
|
||||
{
|
||||
if (playerID == player)
|
||||
continue;
|
||||
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||
if (cmpPlayer && cmpPlayer.GetState() == "defeated")
|
||||
continue;
|
||||
others.push(playerID);
|
||||
}
|
||||
|
||||
const mergedOwnerParams = Object.assign({}, parameters, ownerParameters);
|
||||
const mergedOthersParams = Object.assign({}, parameters, othersParameters);
|
||||
|
||||
// Auto-detect translatable parameters (those starting with "_" or named "player")
|
||||
const translateParameters = Object.keys(mergedOthersParams).filter(key => key.startsWith("_") || key == "player");
|
||||
|
||||
TriggerHelper.SendNotification(ownerMessage, [player], duration, mergedOwnerParams, translateParameters);
|
||||
TriggerHelper.SendNotification(othersMessage, others, duration, mergedOthersParams, translateParameters);
|
||||
};
|
||||
|
||||
TriggerHelper.SendWonderNotifications = function(owner, othersMessage, ownerMessage)
|
||||
{
|
||||
TriggerHelper.SendDualNotification(owner, ownerMessage, othersMessage, 30000, { "_player_": owner });
|
||||
};
|
||||
|
||||
Engine.RegisterGlobal("TriggerHelper", TriggerHelper);
|
||||
|
|
|
|||
|
|
@ -1,97 +1,101 @@
|
|||
// Needs to be kept in sync with the one in gui/session/tutorial/Tutorial.js
|
||||
var TUTORIAL_STEP_TYPE = deepfreeze({
|
||||
"INSTRUCTION": 1,
|
||||
"INFO": 2,
|
||||
"GUI_EXPLANATION": 3
|
||||
});
|
||||
|
||||
Engine.RegisterGlobal("TUTORIAL_STEP_TYPE", TUTORIAL_STEP_TYPE);
|
||||
|
||||
Trigger.prototype.InitTutorial = function(data)
|
||||
{
|
||||
this.index = 0;
|
||||
this.fullText = "";
|
||||
this.stepIndex = -1;
|
||||
this.tutorialEvents = [];
|
||||
|
||||
// Register needed triggers
|
||||
this.RegisterTrigger("OnDeserialized", "OnDeserializedTrigger", { "enabled": true });
|
||||
this.RegisterTrigger("OnPlayerCommand", "OnPlayerCommandTrigger", { "enabled": false });
|
||||
this.tutorialEvents.push("OnPlayerCommand");
|
||||
this.RegisterTrigger("OnDeserialized", "DeserializedAction", { "enabled": true });
|
||||
this.RegisterTrigger("OnPlayerCommand", "BasePlayerCommandAction", { "enabled": true });
|
||||
|
||||
for (const goal of this.tutorialGoals)
|
||||
for (const step of this.tutorialSteps)
|
||||
{
|
||||
for (const key in goal)
|
||||
for (const key in step)
|
||||
{
|
||||
if (typeof goal[key] !== "function" || this.tutorialEvents.indexOf(key) != -1)
|
||||
if (typeof step[key] !== "function" || this.tutorialEvents.indexOf(key) != -1)
|
||||
continue;
|
||||
if (key == "Init")
|
||||
continue;
|
||||
if (key == "IsDone")
|
||||
continue;
|
||||
const action = key + "Trigger";
|
||||
const action = key.substring(2) + "Action";
|
||||
this.RegisterTrigger(key, action, { "enabled": false });
|
||||
this.tutorialEvents.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
};
|
||||
|
||||
Trigger.prototype.NextGoal = function(deserializing = false)
|
||||
Trigger.prototype.NextStep = function(deserializing = false)
|
||||
{
|
||||
if (this.index > this.tutorialGoals.length)
|
||||
if (++this.stepIndex >= this.tutorialSteps.length)
|
||||
{
|
||||
this.CompleteTutorial();
|
||||
return;
|
||||
const goal = this.tutorialGoals[this.index];
|
||||
let needDelay = true;
|
||||
let readyButton = false;
|
||||
}
|
||||
const step = this.tutorialSteps[this.stepIndex];
|
||||
|
||||
Trigger.prototype.Init = goal.Init || null;
|
||||
Trigger.prototype.Init = step.Init || null;
|
||||
if (!deserializing && this.Init)
|
||||
this.Init();
|
||||
|
||||
Trigger.prototype.IsDone = goal.IsDone || (() => false);
|
||||
const goalAlreadyDone = this.IsDone();
|
||||
|
||||
for (const event of this.tutorialEvents)
|
||||
{
|
||||
const action = event + "Trigger";
|
||||
if (goal[event])
|
||||
const action = event.substring(2) + "Action";
|
||||
if (step[event])
|
||||
{
|
||||
Trigger.prototype[action] = goal[event];
|
||||
Trigger.prototype[action] =
|
||||
event == "OnPlayerCommand" ?
|
||||
(msg) =>
|
||||
{
|
||||
// Don't forward tutorial continue commands, since the step trigger actions aren't supposed to handle them,
|
||||
// plus we might have already loaded in the next step.
|
||||
if (msg.cmd.type != "dialog-answer" || msg.cmd.tutorial != "continue")
|
||||
step.OnPlayerCommand.call(this, msg);
|
||||
} :
|
||||
step[event];
|
||||
|
||||
this.EnableTrigger(event, action);
|
||||
if (!goalAlreadyDone)
|
||||
needDelay = false;
|
||||
}
|
||||
else
|
||||
this.DisableTrigger(event, action);
|
||||
}
|
||||
|
||||
// Goals without actions to be performed by the player must have
|
||||
// - either the property delay (a value > 0 to wait for a given time, and -1 to display the Ready button)
|
||||
// - or no trigger functions (needDelay will be set automatically to true and the Ready button displayed)
|
||||
if (goal.delay || needDelay)
|
||||
{
|
||||
if (goal.delay && goal.delay > 0)
|
||||
this.DoAfterDelay(+goal.delay, "NextGoal", {});
|
||||
else
|
||||
{
|
||||
this.EnableTrigger("OnPlayerCommand", "OnPlayerCommandTrigger");
|
||||
Trigger.prototype.OnPlayerCommandTrigger = function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "dialog-answer" && msg.cmd.tutorial && msg.cmd.tutorial == "ready")
|
||||
this.NextGoal();
|
||||
};
|
||||
readyButton = true;
|
||||
}
|
||||
}
|
||||
Trigger.prototype.IsDone = step.IsDone || (() => false);
|
||||
const isDone = this.IsDone();
|
||||
const showContinueButton = isDone || (step.panelData.showContinueButton === undefined ?
|
||||
this.tutorialEvents.every(event => !step[event]) :
|
||||
step.panelData.showContinueButton
|
||||
);
|
||||
|
||||
this.GoalMessage(goal.instructions, readyButton, ++this.index == this.tutorialGoals.length);
|
||||
this.DisplayStep(step, showContinueButton, isDone);
|
||||
};
|
||||
|
||||
Trigger.prototype.GoalMessage = function(instructions, readyButton=false, leave=false)
|
||||
Trigger.prototype.DisplayStep = function(step, showContinueButton = false, isDone = false)
|
||||
{
|
||||
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||
cmpGUIInterface.PushNotification({
|
||||
"type": "tutorial",
|
||||
"players": [1],
|
||||
"instructions": typeof instructions === "string" ? [instructions] : instructions,
|
||||
"readyButton": readyButton,
|
||||
"leave": leave
|
||||
"step": {
|
||||
"type": step.type,
|
||||
"panelData": {
|
||||
...step.panelData, showContinueButton, isDone
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Trigger.prototype.WarningMessage = function(warning)
|
||||
Trigger.prototype.DisplayWarning = function(warning)
|
||||
{
|
||||
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
|
||||
cmpGUIInterface.PushNotification({
|
||||
|
|
@ -101,13 +105,44 @@ Trigger.prototype.WarningMessage = function(warning)
|
|||
});
|
||||
};
|
||||
|
||||
Trigger.prototype.OnDeserializedTrigger = function()
|
||||
Trigger.prototype.CompleteTutorial = function(message = markForTranslation("You have successfully completed the tutorial."))
|
||||
{
|
||||
this.index = Math.max(0, this.index - 1);
|
||||
|
||||
// Display messages from already processed goals
|
||||
for (let i = 0; i < this.index; ++i)
|
||||
this.GoalMessage(this.tutorialGoals[i].instructions, false, false);
|
||||
|
||||
this.NextGoal(true);
|
||||
// End the tutorial first, before marking the player won, since otherwise the triggers would be invoked
|
||||
// once more when doing so.
|
||||
this.EndTutorial(message);
|
||||
TriggerHelper.SetPlayerWon(this.playerID, () => markForTranslation("%(player)s has completed the tutorial."), () => "", message);
|
||||
};
|
||||
|
||||
Trigger.prototype.FailTutorial = function(message = markForTranslation("You have failed to complete the tutorial."))
|
||||
{
|
||||
// End the tutorial first, before marking the player defeated, since otherwise the triggers would be invoked
|
||||
// once more when doing so.
|
||||
this.EndTutorial(message);
|
||||
TriggerHelper.DefeatPlayer(this.playerID, markForTranslation("%(player)s has failed the tutorial."), message);
|
||||
};
|
||||
|
||||
Trigger.prototype.EndTutorial = function(message)
|
||||
{
|
||||
this.DisableAllTriggers();
|
||||
this.DisplayStep({
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": { "isLast": true, "text": message }
|
||||
}, true, false);
|
||||
};
|
||||
|
||||
Trigger.prototype.BasePlayerCommandAction = function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "dialog-answer" && msg.cmd.tutorial == "continue")
|
||||
this.NextStep();
|
||||
};
|
||||
|
||||
Trigger.prototype.DeserializedAction = function()
|
||||
{
|
||||
this.stepIndex = Math.max(0, this.stepIndex - 1);
|
||||
|
||||
// Display messages from already processed steps
|
||||
for (let i = 0; i <= this.stepIndex; ++i)
|
||||
this.DisplayStep(this.tutorialSteps[i], false, false);
|
||||
|
||||
this.NextStep(true);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,16 +1,42 @@
|
|||
Trigger.prototype.WonderVictoryEntityRenamed = function(data)
|
||||
{
|
||||
if (this.wonderVictoryMessages[data.entity] && Engine.QueryInterface(data.newentity, IID_Wonder))
|
||||
{
|
||||
// When an entity is renamed, we first create a new entity,
|
||||
// which in case it is a wonder will receive a timer.
|
||||
// However on a rename we want to use the timer from the old entity,
|
||||
// so we need to remove the timer of the new entity.
|
||||
this.WonderVictoryDeleteTimer(data.newentity);
|
||||
if (!Engine.QueryInterface(data.newentity, IID_Wonder))
|
||||
return;
|
||||
|
||||
// Check if wonder victory is enabled
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
// Handle timer transfer (only if old entity had the timer)
|
||||
if (this.wonderVictoryMessages[data.entity])
|
||||
{
|
||||
this.WonderVictoryDeleteTimer(data.newentity);
|
||||
this.wonderVictoryMessages[data.newentity] = this.wonderVictoryMessages[data.entity];
|
||||
delete this.wonderVictoryMessages[data.entity];
|
||||
}
|
||||
|
||||
// Make the completed wonder permanently visible
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(data.newentity);
|
||||
|
||||
// Send "wonder completed" notifications (only for non-Gaia wonders)
|
||||
const cmpOwnership = Engine.QueryInterface(data.newentity, IID_Ownership);
|
||||
if (cmpOwnership)
|
||||
{
|
||||
const owner = cmpOwnership.GetOwner();
|
||||
if (owner > 0)
|
||||
{
|
||||
TriggerHelper.SendWonderNotifications(
|
||||
owner,
|
||||
markForTranslation("%(_player_)s has built a Wonder!"),
|
||||
markForTranslation("You have built a Wonder!")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
||||
|
|
@ -18,6 +44,15 @@ Trigger.prototype.WonderVictoryOwnershipChanged = function(data)
|
|||
if (!Engine.QueryInterface(data.entity, IID_Wonder))
|
||||
return;
|
||||
|
||||
// Check if wonder victory is enabled
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
this.WonderVictoryDeleteTimer(data.entity);
|
||||
|
||||
if (data.to > 0)
|
||||
|
|
@ -59,7 +94,7 @@ Trigger.prototype.WonderVictoryStartTimer = function(ent, player)
|
|||
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
|
||||
{
|
||||
const cmpPlayer = QueryPlayerIDInterface(playerID);
|
||||
if (cmpPlayer.GetState() == "won")
|
||||
if (cmpPlayer.HasWon())
|
||||
return;
|
||||
if (allies.indexOf(playerID) == -1 && playerID != player)
|
||||
others.push(playerID);
|
||||
|
|
@ -146,11 +181,69 @@ Trigger.prototype.WonderVictorySetWinner = function(playerID)
|
|||
n));
|
||||
};
|
||||
|
||||
Trigger.prototype.WonderStartNotification = function(data)
|
||||
{
|
||||
// Check if wonder victory is enabled in game settings
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
// Don't show the wonder foundation if it's not a wonder victory game
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||
if (!cmpTemplateManager)
|
||||
return;
|
||||
|
||||
const template = cmpTemplateManager.GetTemplate(data.template);
|
||||
|
||||
if (!template || !("Wonder" in template))
|
||||
return;
|
||||
|
||||
const owner = TriggerHelper.GetOwner(data.foundation);
|
||||
if (owner <= 0)
|
||||
return;
|
||||
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(data.foundation);
|
||||
|
||||
TriggerHelper.SendWonderNotifications(
|
||||
owner,
|
||||
markForTranslation("%(_player_)s has started building a Wonder."),
|
||||
markForTranslation("You have started building a Wonder.")
|
||||
);
|
||||
};
|
||||
|
||||
Trigger.prototype.RevealGaiaWonders = function()
|
||||
{
|
||||
Engine.ProfileStart("RevealGaiaWonders");
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
if (!cmpEndGameManager)
|
||||
return;
|
||||
|
||||
const victoryConditions = cmpEndGameManager.GetVictoryConditions();
|
||||
if (!victoryConditions || !victoryConditions.includes("wonder"))
|
||||
return;
|
||||
|
||||
// Find and reveal all Gaia wonders
|
||||
const gaiaWonders = Engine.GetEntitiesWithInterface(IID_Wonder).filter(
|
||||
ent => TriggerHelper.GetOwner(ent) === 0
|
||||
);
|
||||
|
||||
for (const ent of gaiaWonders)
|
||||
TriggerHelper.MakeEntityPermanentlyVisible(ent);
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
{
|
||||
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
|
||||
cmpTrigger.RegisterTrigger("OnEntityRenamed", "WonderVictoryEntityRenamed", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "WonderVictoryOwnershipChanged", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "WonderVictoryDiplomacyChanged", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnPlayerWon", "WonderVictoryPlayerWon", { "enabled": true });
|
||||
cmpTrigger.RegisterTrigger("OnConstructionStarted", "WonderStartNotification", { "enabled": true });
|
||||
cmpTrigger.DoAfterDelay(0, "RevealGaiaWonders", {});
|
||||
cmpTrigger.wonderVictoryMessages = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,46 @@
|
|||
Trigger.prototype.tutorialGoals = [
|
||||
Trigger.prototype.tutorialSteps = [
|
||||
{
|
||||
"instructions": markForTranslation("Welcome to the 0\xa0A.D. tutorial."),
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Left-click on a Civilian and then right-click on a berry bush to make that Civilian gather food. Civilians gather vegetables faster than other units."),
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit")
|
||||
this.NextGoal();
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Welcome to the 0\xa0A.D. tutorial."),
|
||||
"hint": markForTranslation("Click to start the tutorial."),
|
||||
"continueButton": markForTranslation("Start")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Select the Citizen Soldier, right-click on a tree near the Civic Center to begin gathering wood. Citizen Soldiers gather wood faster than Civilians."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Left-click on a Civilian and then right-click on a berry bush to make that Civilian gather food. Civilians gather vegetables faster than other units.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree")
|
||||
this.NextGoal();
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit")
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
{
|
||||
"text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."),
|
||||
"hotkey": "session.batchtrain"
|
||||
}
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select the Citizen Soldier, right-click on a tree near the Civic Center to begin gathering wood. Citizen Soldiers gather wood faster than Civilians.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree")
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."),
|
||||
"hotkeys": ["session.batchtrain"]
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.trainingDone = false;
|
||||
},
|
||||
"OnTrainingQueued": function(msg)
|
||||
{
|
||||
if (msg.unitTemplate != "units/spart/infantry_spearman_b" || +msg.count == 1)
|
||||
|
|
@ -36,35 +50,58 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") :
|
||||
markForTranslation("Click on the Hoplite icon.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Select the two idle Civilians and build a House nearby by selecting the House icon. Place the House by left-clicking on a piece of land."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select the two idle Civilians and build a House nearby by selecting the House icon. Place the House by left-clicking on a piece of land.")
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.trainingDone = true;
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a Storehouse beside some nearby trees. They will begin to gather wood when it's constructed."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Wait for the training of the Hoplites to finish.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return this.trainingDone;
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select the newly trained Hoplites and assign them to build a Storehouse beside some nearby trees. They will begin to gather wood when it's constructed.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Storehouse"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
{
|
||||
"text": markForTranslation("Train a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."),
|
||||
"hotkey": "session.batchtrain"
|
||||
}
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Train a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."),
|
||||
"hotkeys": ["session.batchtrain"]
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.trainingDone = false;
|
||||
|
|
@ -78,18 +115,21 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") :
|
||||
markForTranslation("Click on the Skirmisher icon.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Build a Farmstead in an open space beside the Civic Center using any idle builders."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Build a Farmstead in an open space beside the Civic Center using any idle builders.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Farmstead"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
|
|
@ -97,7 +137,11 @@ Trigger.prototype.tutorialGoals = [
|
|||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Let's wait for the Farmstead to be built."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Let's wait for them to finish building the Farmstead, so they can use it as a dropsite."),
|
||||
"hint": markForTranslation("Wait for the Farmstead to be built.")
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.trainingDone = true;
|
||||
|
|
@ -105,11 +149,14 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnStructureBuilt": function(msg)
|
||||
{
|
||||
if (TriggerHelper.EntityMatchesClassList(msg.building, "Farmstead"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Once the Farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a Field beside the Farmstead."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Now its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a Field beside the Farmstead.")
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.farmStarted = false;
|
||||
|
|
@ -123,25 +170,31 @@ Trigger.prototype.tutorialGoals = [
|
|||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Field"))
|
||||
this.farmStarted = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.trainingDone = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("The Field's builders will now automatically begin gathering food from the Field. Using the newly created group of skirmishers, get them to build another House nearby."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The Field's builders will now automatically begin gathering food from the Field. Using the newly created group of skirmishers, get them to build another House nearby.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather wood."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather wood.")
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.rallyPointSet = false;
|
||||
|
|
@ -160,37 +213,55 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") :
|
||||
markForTranslation("Click on the Hoplite icon.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.trainingStarted = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data ||
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree")
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree")
|
||||
{
|
||||
this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a wood icon."));
|
||||
this.DisplayWarning(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a wood icon."));
|
||||
return;
|
||||
}
|
||||
this.rallyPointSet = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra House using any available builder units. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra House using any available builder units.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Barracks"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the II icon (you have to wait for the barracks to be built first). This will allow Town Phase buildings to be constructed."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The Barracks will be your fifth Village Phase structure, allowing you to advance to the Town Phase."),
|
||||
"hint": markForTranslation("Wait for the Barracks to be built.")
|
||||
},
|
||||
"OnStructureBuilt": function(msg)
|
||||
{
|
||||
if (TriggerHelper.EntityMatchesClassList(msg.building, "Barracks"))
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the II icon (you have to wait for the barracks to be built first). This will allow Town Phase buildings to be constructed.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_generic");
|
||||
|
|
@ -198,11 +269,15 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnResearchQueued": function(msg)
|
||||
{
|
||||
if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("While waiting for the phasing up, you may reassign your idle workers to gathering the resources you are short of."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("While waiting for the phasing up, you may reassign your idle workers to gathering the resources you are short of."),
|
||||
"hint": markForTranslation("Wait until you reach the Town Phase.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
const cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
|
|
@ -213,19 +288,25 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnResearchFinished": function(msg)
|
||||
{
|
||||
if (msg.tech == "phase_town_generic")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Outpost"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Start training a batch of Civilians in the Civic Center and set its rally point to the Field (right click on it)."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Start training a batch of Civilians in the Civic Center and set its rally point to the Field (right click on it).")
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.rallyPointSet = false;
|
||||
|
|
@ -244,60 +325,75 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") :
|
||||
markForTranslation("Click on the Civilian icon.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.trainingStarted = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data ||
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain")
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain")
|
||||
return;
|
||||
this.rallyPointSet = true;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Tower near your Outpost."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Tower near your Outpost.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Tower"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Build a Forge and research the Infantry Training technology (sword icon) to improve infantry hack attack."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Build a Forge and research the Infantry Training technology (sword icon) to improve infantry hack attack.")
|
||||
},
|
||||
"OnResearchQueued": function(msg)
|
||||
{
|
||||
if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Forge"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("The enemy is coming. Train more soldiers to fight off the enemies."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The enemy is coming. Train more soldiers to fight off the enemies.")
|
||||
},
|
||||
"OnResearchFinished": function(msg)
|
||||
{
|
||||
this.LaunchAttack();
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Try to repel the attack."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Try to repel the attack.")
|
||||
},
|
||||
"OnOwnershipChanged": function(msg)
|
||||
{
|
||||
if (msg.to != INVALID_PLAYER)
|
||||
return;
|
||||
if (this.IsAttackRepelled())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources.")
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.marketStarted = false;
|
||||
|
|
@ -314,11 +410,14 @@ Trigger.prototype.tutorialGoals = [
|
|||
this.marketStarted = this.marketStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Market");
|
||||
this.templeStarted = this.templeStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Temple");
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "phase_city_generic");
|
||||
|
|
@ -326,11 +425,15 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnResearchQueued": function(msg)
|
||||
{
|
||||
if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("While waiting for the phase change, you may train more soldiers at the Barracks."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("While waiting for the phase change, you may train more soldiers at the Barracks."),
|
||||
"hint": markForTranslation("Wait until you reach the City Phase")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "phase_city_generic");
|
||||
|
|
@ -338,11 +441,14 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnResearchFinished": function(msg)
|
||||
{
|
||||
if (msg.tech == "phase_city_generic")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("Now that you are in City Phase, build the Arsenal nearby and then use it to construct 2 Battering Rams."),
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Now that you are in City Phase, build the Arsenal nearby and then use it to construct 2 Battering Rams.")
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.ramCount = 0;
|
||||
|
|
@ -358,29 +464,23 @@ Trigger.prototype.tutorialGoals = [
|
|||
if (this.IsDone())
|
||||
{
|
||||
this.RemoveChampions();
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once the enemy's base has been spotted, send your Siege Engines and all remaining soldiers to destroy it.\n"),
|
||||
markForTranslation("Civilians should continue to gather resources.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once the enemy's base has been spotted, send your Siege Engines and all remaining soldiers to destroy it.\n") +
|
||||
markForTranslation("Civilians should continue to gather resources.")
|
||||
},
|
||||
"OnOwnershipChanged": function(msg)
|
||||
{
|
||||
if (msg.from != this.enemyID)
|
||||
return;
|
||||
if (TriggerHelper.EntityMatchesClassList(msg.entity, "CivilCentre"))
|
||||
this.NextGoal();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": markForTranslation("The enemy has been defeated. These tutorial tasks are now completed."),
|
||||
"Init": function()
|
||||
{
|
||||
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
|
||||
cmpEndGameManager.MarkPlayerAndAlliesAsWon(1, () => "Tutorial completed", () => "");
|
||||
this.CompleteTutorial(markForTranslation("You have defeated the enemy by destroying or capturing the Civic Center. These tutorial tasks are now completed."));
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,111 +1,156 @@
|
|||
Trigger.prototype.tutorialGoals = [
|
||||
Trigger.prototype.tutorialSteps = [
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("This tutorial will teach the basics of developing your economy. Typically, you will start with a Civic Center and a couple units in Village Phase and ultimately, your goal will be to develop and expand your empire, often by evolving to Town Phase and City Phase afterward.\n"),
|
||||
{
|
||||
"text": markForTranslation("\nBefore starting, you can toggle between fullscreen and windowed mode using %(hotkey)s."),
|
||||
"hotkey": "togglefullscreen"
|
||||
},
|
||||
markForTranslation("You can change the level of zoom using the mouse wheel and the camera view using any of your keyboard's arrow keys.\n"),
|
||||
markForTranslation("Adjust the game window to your preferences.\n"),
|
||||
{
|
||||
"text": markForTranslation("\nYou may also toggle between showing and hiding this tutorial panel at any moment using %(hotkey)s.\n"),
|
||||
"hotkey": "session.gui.tutorial.toggle"
|
||||
}
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("This tutorial will teach the basics of developing your economy. Typically, you will start with a Civic Center and a couple units in Village Phase and ultimately, your goal will be to develop and expand your empire, often by evolving to Town Phase and City Phase afterward."),
|
||||
"hint": markForTranslation("Click to start."),
|
||||
"continueButton": markForTranslation("Start")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("To start off, select your building, the Civic Center, by clicking on it. A selection ring in the color of your player will be displayed after clicking.")
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"appendable": "true",
|
||||
"title": markForTranslation("Basic Controls"),
|
||||
"texts": [
|
||||
markForTranslation("You can toggle between fullscreen and windowed mode using %(hotkey1)s."),
|
||||
markForTranslation("You can change the level of zoom using the mouse wheel and the camera view using any of your keyboard's arrow keys."),
|
||||
markForTranslation("Adjust the game window to your preferences."),
|
||||
markForTranslation("You may also toggle between showing and hiding this tutorial panel at any moment using %(hotkey2)s.")
|
||||
],
|
||||
"hotkeys": ["togglefullscreen", "session.gui.tutorial.toggle"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Now that the Civic Center is selected, you will notice that a production panel will appear on the lower right of your screen detailing the actions that the buildings supports. For the production panel, available actions are not masked in any color, while an icon masked in gray indicates that the action has not been unlocked and a red mask indicates that you do not have sufficient resources to perform that action. Additionally, you can hover the cursor over any icon to show a tooltip with more details.\n"),
|
||||
markForTranslation("The top row of buttons contains portraits of units that may be trained at the building while the bottom one or two rows will have researchable technologies. Hover the cursor over the II icon. The tooltip will tell us that advancing to Town Phase requires both more constructed structures as well as more food and wood resources.")
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("To start off, select your building, the Civic Center, by clicking on it. A selection ring in the color of your player will be displayed after clicking."),
|
||||
"hint": markForTranslation("Click to continue when you have selected the Civic Center.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("You have two main types of starting units: Civilians and Citizen Soldiers. Civilians are purely economic units; they have low health and little to no attack. Citizen Soldiers are workers by default, but in times of need, can utilize a weapon to fight. You have two categories of Citizen Soldiers: Infantry and Cavalry. Civilians and Infantry Citizen Soldiers can gather any land resources while Cavalry Citizen Soldiers can only gather meat from animals.\n")
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.GUI_EXPLANATION,
|
||||
"panelData": {
|
||||
"targetObject": "unitCommands",
|
||||
"side": "top",
|
||||
"title": markForTranslation("Production Panel"),
|
||||
"texts": [
|
||||
markForTranslation("Now that the Civic Center is selected, you will notice that a production panel will appear on the lower right of your screen detailing the actions that the buildings supports. For the production panel, available actions are not masked in any color, while an icon masked in gray indicates that the action has not been unlocked and a red mask indicates that you do not have sufficient resources to perform that action. Additionally, you can hover the cursor over any icon to show a tooltip with more details."),
|
||||
markForTranslation("The top row of buttons contains portraits of units that may be trained at the building while the bottom one or two rows will have researchable technologies. Hover the cursor over the II icon. The tooltip will tell us that advancing to Town Phase requires both more constructed structures as well as more food and wood resources.")
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("As a general rule of thumb, left-clicking represents selection while right-clicking with an entity selected represents an order (gather, build, fight, etc.).\n")
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"title": markForTranslation("Types of Units"),
|
||||
"text": markForTranslation("You have two main types of starting units: Civilians and Citizen Soldiers. Civilians are purely economic units; they have low health and little to no attack. Citizen Soldiers are workers by default, but in times of need, can utilize a weapon to fight. You have two categories of Citizen Soldiers: Infantry and Cavalry. Civilians and Infantry Citizen Soldiers can gather any land resources while Cavalry Citizen Soldiers can only gather meat from animals.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("At this point, food and wood are the most important resources for developing your economy, so let's start with gathering food. Civilians gather vegetables faster than other units.\n"),
|
||||
markForTranslation("There are primarily three ways to select units:\n"),
|
||||
markForTranslation("1) Hold the left mouse button and drag a selection rectangle that encloses the units you want to select.\n"),
|
||||
{
|
||||
"text": markForTranslation("2) Click on one of them and then add additional units to your selection by holding %(hotkey)s and clicking each additional unit (or also via the above selection rectangle).\n"),
|
||||
"hotkey": "selection.add"
|
||||
},
|
||||
{
|
||||
"text": markForTranslation("3) Double-click on a unit. This will select every unit of the same type as the specified unit in your visible window. %(hotkey)s+double-click will select all units of the same type on the entire map.\n"),
|
||||
"hotkey": "selection.offscreen"
|
||||
},
|
||||
markForTranslation("You can click on an empty space on the map to reset the selection. Try each of these methods before tasking all of your Civilians to gather the berries to the southeast of your Civic Center by right-clicking on the berries when you have all the Civilians selected.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("As a general rule of thumb, left-clicking represents selection while right-clicking with an entity selected represents an order (gather, build, fight, etc.).")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"title": markForTranslation("Making Selections"),
|
||||
"texts": [
|
||||
markForTranslation("There are primarily three ways to select units:"),
|
||||
markForTranslation("1) Hold the left mouse button and drag a selection rectangle that encloses the units you want to select."),
|
||||
markForTranslation("2) Click on one of them and then add additional units to your selection by holding %(hotkey1)s and clicking each additional unit (or also via the above selection rectangle)."),
|
||||
markForTranslation("3) Double-click on a unit. This will select every unit of the same type as the specified unit in your visible window. %(hotkey2)s+double-click will select all units of the same type on the entire map."),
|
||||
markForTranslation("You can click on an empty space on the map to reset the selection.")
|
||||
],
|
||||
"hotkeys": ["selection.add", "selection.offscreen"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("At this point, food and wood are the most important resources for developing your economy.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Let's start with gathering food. Civilians gather vegetables faster than other units. Select them and order them to gather the berries to the southeast of your Civic Center by right-clicking on the berries.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Now, let's gather some wood with your Infantry Citizen Soldiers. Select your Infantry Citizen Soldiers and order them to gather wood by right-clicking on the nearest tree.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Now, let's gather some wood with your Infantry Citizen Soldiers. Select your Infantry Citizen Soldiers and order them to gather wood by right-clicking on the nearest tree.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Cavalry Citizen Soldiers are good for hunting. Select your Cavalry and order him to slaughter the chickens around your Civic Center in similar fashion.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Cavalry Citizen Soldiers are good for hunting. Select your Cavalry and order him to slaughter the chickens around your Civic Center in similar fashion.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("All your units are now gathering resources. We should train more units!\n"),
|
||||
markForTranslation("First, let's set a rally point. Setting a rally point on a building that can train units will automatically designate a task to the new unit upon completion of training. We want to send the newly trained units to gather wood on the group of trees to the south of the Civic Center. To do so, select the Civic Center by clicking on it and then right-click on one of the trees.\n"),
|
||||
markForTranslation("Rally points are indicated by a small flag at the end of the blue line.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("All your units are now gathering resources. We should train more units!")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"appendable": true,
|
||||
"title": markForTranslation("Rally Points"),
|
||||
"text": markForTranslation("First, let's set a rally point. Setting a rally point on a building that can train units will automatically designate a task to the new unit upon completion of training. We want to send the newly trained units to gather wood on the group of trees to the south of the Civic Center. To do so, select the Civic Center by clicking on it and then right-click on one of the trees.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data ||
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree")
|
||||
!msg.cmd.data.command || msg.cmd.data.command != "gather" ||
|
||||
!msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree")
|
||||
{
|
||||
this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over a tree and right-click when you see the cursor change into a wood icon."));
|
||||
this.DisplayWarning(markForTranslation("Select the Civic Center, then hover the cursor over a tree and right-click when you see the cursor change into a wood icon."));
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Now that the rally point is set, we can produce additional units and they will do their assigned task automatically.\n"),
|
||||
{
|
||||
"text": markForTranslation("Citizen Soldiers gather wood faster than Civilians. Select the Civic Center and, while holding %(hotkey)s, click on the second unit icon, the Hoplites (holding %(hotkey)s trains a batch of five units). You can also train units individually by simply clicking, but training 5 units together takes less time than training 5 units individually."),
|
||||
"hotkey": "session.batchtrain"
|
||||
}
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"appendToPrevious": true,
|
||||
"texts": [
|
||||
markForTranslation("Rally points are indicated by a small flag at the end of the blue line."),
|
||||
markForTranslation("Now that the rally point is set, we can produce additional units and they will do their assigned task automatically.")
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("Citizen Soldiers gather wood faster than Civilians. Select the Civic Center and, while holding %(hotkey)s, click on the second unit icon, the Hoplites (holding %(hotkey)s trains a batch of five units). You can also train units individually by simply clicking, but training 5 units together takes less time than training 5 units individually."),
|
||||
"hotkeys": ["session.batchtrain"]
|
||||
},
|
||||
"OnTrainingQueued": function(msg)
|
||||
{
|
||||
if (msg.unitTemplate != "units/athen/infantry_spearman_b" || +msg.count == 1)
|
||||
|
|
@ -116,71 +161,110 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to hold the hotkey while clicking to train several units.") :
|
||||
markForTranslation("The second icon represents the Hoplites.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Let's wait for the units to be trained.\n"),
|
||||
markForTranslation("While waiting, direct your attention to the panel at the top of your screen. On the upper left, you will see your current resource supply (food, wood, stone, and metal). As each worker brings resources back to the Civic Center (or another dropsite), you will see the amount of the corresponding resource increase.\n"),
|
||||
markForTranslation("This is a very important concept to keep in mind: gathered resources have to be brought back to a dropsite to be accounted, and you should always try to minimize the distance between resource and nearest dropsite to improve your gathering efficiency.")
|
||||
],
|
||||
|
||||
"type": TUTORIAL_STEP_TYPE.GUI_EXPLANATION,
|
||||
"panelData": {
|
||||
"targetObject": "resourceCounts",
|
||||
"side": "bottom",
|
||||
"title": markForTranslation("Resource Supply"),
|
||||
"text": markForTranslation("Direct your attention to the panel at the top of your screen. On the upper left, you will see your current resource supply (food, wood, stone, and metal). As each worker brings resources back to the Civic Center (or another dropsite), you will see the amount of the corresponding resource increase."),
|
||||
"showContinueButton": true
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.NextGoal();
|
||||
this.trainingFinished = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("The newly trained units automatically go to the trees and start gathering wood.\n"),
|
||||
markForTranslation("But as they have to bring it back to the Civic Center to deposit it, their gathering efficiency suffers from the distance. To fix that, we can build a Storehouse, a dropsite for wood, stone, and metal, close to the trees. To do so, select your five newly trained Citizen Soldiers and look for the construction panel on the bottom right, click on the Storehouse icon, move the mouse as close as possible to the trees you want to gather and click on a valid place to build the dropsite.\n"),
|
||||
markForTranslation("Invalid (obstructed) positions will show the building preview overlay in red.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("This is a very important concept to keep in mind: gathered resources have to be brought back to a dropsite to be accounted, and you should always try to minimize the distance between resource and nearest dropsite to improve your gathering efficiency."),
|
||||
"showContinueButton": true
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.trainingFinished = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Let's wait until the batch of Hoplites has finished training."),
|
||||
"hint": markForTranslation("Wait for the training to finish.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return this.trainingFinished;
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"title": markForTranslation("Storehouse"),
|
||||
"texts": [
|
||||
markForTranslation("The newly trained units automatically go to the trees and start gathering wood."),
|
||||
markForTranslation("But as they have to bring it back to the Civic Center to deposit it, their gathering efficiency suffers from the distance. To fix that, we can build a Storehouse, a dropsite for wood, stone, and metal, close to the trees.")
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("To do so, select your five newly trained Citizen Soldiers and look for the construction panel on the bottom right, click on the Storehouse icon, move the mouse as close as possible to the trees you want to gather and click on a valid place to build the dropsite. Invalid (obstructed) positions will show the building preview overlay in red.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/storehouse")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("The selected Citizens will automatically start constructing the building once you place the foundation.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The selected Citizens will automatically start constructing the building once you place the foundation."),
|
||||
"hint": markForTranslation("Wait for the storehouse to be built.")
|
||||
},
|
||||
"OnStructureBuilt": function(msg)
|
||||
{
|
||||
const cmpResourceDropsite = Engine.QueryInterface(msg.building, IID_ResourceDropsite);
|
||||
if (cmpResourceDropsite && cmpResourceDropsite.AcceptsType("wood"))
|
||||
this.NextGoal();
|
||||
},
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("When construction finishes, the builders default to gathering wood automatically.\n"),
|
||||
markForTranslation("In the meantime, we seem to have enough workers gathering wood. We should remove the current rally point of the Civic Center away from gathering wood. For that purpose, right-click on the Civic Center when it is selected (and the flag icon indicating the rally point is crossed out).")
|
||||
],
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "unset-rallypoint")
|
||||
this.NextGoal();
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.trainingDone = true;
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
{
|
||||
"text": markForTranslation("Let's train some Civilians to gather more food. Select the Civic Center, hold %(hotkey)s and click on the Civilian icon to train five Civilians."),
|
||||
"hotkey": "session.batchtrain"
|
||||
}
|
||||
],
|
||||
"Init": function()
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("When construction finishes, the builders default to gathering wood automatically.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("In the meantime, we seem to have enough workers gathering wood. We should remove the current rally point of the Civic Center away from gathering wood. For that purpose, right-click on the Civic Center when it is selected (and the flag icon indicating the rally point is crossed out).")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
this.trainingDone = false;
|
||||
if (msg.cmd.type == "unset-rallypoint")
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Let's train some Civilians to gather more food. Select the Civic Center, hold %(hotkey)s and click on the Civilian icon to train five Civilians."),
|
||||
"hotkeys": ["session.batchtrain"]
|
||||
},
|
||||
"OnTrainingQueued": function(msg)
|
||||
{
|
||||
|
|
@ -192,43 +276,46 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count == 1 ?
|
||||
markForTranslation("Do not forget to hold the hotkey and click to train several units.") :
|
||||
markForTranslation("The first icon represents the Civilians.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("The units should be ready soon.\n"),
|
||||
markForTranslation("In the meantime, direct your attention to your population count on the top panel. It is the fifth item from the left, after the resources. It would be prudent to keep an eye on it. It indicates your current population (including those being trained) and the current population limit, which is determined by your built structures.")
|
||||
],
|
||||
"IsDone": function(msg)
|
||||
{
|
||||
return this.trainingDone;
|
||||
"type": TUTORIAL_STEP_TYPE.GUI_EXPLANATION,
|
||||
"panelData": {
|
||||
"targetObject": "resourceCounts",
|
||||
"side": "bottom",
|
||||
"text": markForTranslation("The units should be ready soon.\nIn the meantime, direct your attention to your population count on the top panel. It is the fifth item from the left, after the resources. It would be prudent to keep an eye on it. It indicates your current population (including those being trained) and the current population limit, which is determined by your built structures."),
|
||||
"hint": markForTranslation("Wait for the training of the Civilians to finish.")
|
||||
},
|
||||
"OnTrainingFinished": function(msg)
|
||||
{
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("As you have nearly reached the population limit, you must increase it by building some new structures if you want to train more units. The most cost effective structure to increase your population limit is the House.\n"),
|
||||
markForTranslation("Now that the units are ready, let's see how to build several Houses in a row.")
|
||||
]
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"title": markForTranslation("Population Limit"),
|
||||
"text": markForTranslation("As you have nearly reached the population limit, you must increase it by building some new structures if you want to train more units. The most cost effective structure to increase your population limit is the House.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
{
|
||||
"text": markForTranslation("Select two of your newly-trained Civilians and order them to build these Houses in the empty space to the east of the Civic Center. Click on the House icon in the bottom right panel. While holding %(hotkey)s click on positions on the map where you want to build Houses. The Civilians will then start to build the houses in order. (When you give a command while holding %(hotkey)s, you put the command in a queue; units automatically switch to the next command in their queue when they finish their current command). Press Escape to get rid of the House cursor so you don't spam Houses all over the map.\n"),
|
||||
"hotkey": "selection.queue"
|
||||
},
|
||||
{
|
||||
"text": markForTranslation("Reminder: to select only two Civilians, click on the first one and then hold %(hotkey)s and click on the second one."),
|
||||
"hotkey": "selection.add"
|
||||
}
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Now that the units are ready, let's see how to build several Houses in a row.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("Select two of your newly-trained Civilians and order them to build these Houses in the empty space to the east of the Civic Center. Click on the House icon in the bottom right panel. While holding %(hotkey1)s click on positions on the map where you want to build Houses. The Civilians will then start to build the houses in order. (When you give a command while holding %(hotkey1)s, you put the command in a queue; units automatically switch to the next command in their queue when they finish their current command). Press Escape to get rid of the House cursor so you don't spam Houses all over the map.\n") +
|
||||
markForTranslation("Reminder: to select only two Civilians, click on the first one and then hold %(hotkey2)s and click on the second one."),
|
||||
"hotkeys": ["session.queue", "selection.add"]
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.houseGoal = new Set();
|
||||
|
|
@ -244,45 +331,28 @@ Trigger.prototype.tutorialGoals = [
|
|||
{
|
||||
this.houseGoal.delete(+msg.entity);
|
||||
const cmpFoundation = Engine.QueryInterface(+msg.entity, IID_Foundation);
|
||||
if (cmpFoundation && cmpFoundation.GetBuildProgress() < 1) // Destroyed before built
|
||||
if (cmpFoundation && cmpFoundation.GetBuildProgress() < 1)
|
||||
--this.houseCount;
|
||||
}
|
||||
else if (msg.from == INVALID_PLAYER && msg.to == this.playerID &&
|
||||
Engine.QueryInterface(+msg.entity, IID_Foundation) &&
|
||||
TriggerHelper.EntityMatchesClassList(+msg.entity, "House"))
|
||||
Engine.QueryInterface(+msg.entity, IID_Foundation) &&
|
||||
TriggerHelper.EntityMatchesClassList(+msg.entity, "House"))
|
||||
{
|
||||
this.houseGoal.add(+msg.entity);
|
||||
++this.houseCount;
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("You may notice that berries are a finite supply of food. We will need a more lasting food source. Fields produce an unlimited food resource, but are slower to gather than forageable fruits.\n"),
|
||||
markForTranslation("But to minimize the distance between a farm and its corresponding food dropsite, we will first build a Farmstead.")
|
||||
],
|
||||
"delay": -1,
|
||||
"OnOwnershipChanged": function(msg)
|
||||
{
|
||||
if (this.houseGoal.has(+msg.entity))
|
||||
this.houseGoal.delete(+msg.entity);
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Select the three remaining (idle) Civilians and order them to build a Farmstead in the center of the large open area to the west of the Civic Center.\n"),
|
||||
markForTranslation("We will need a decent chunk of space around the Farmstead to build Fields. In addition, we can see goats on the west side to further improve our food gathering efficiency should we ever decide to hunt them.\n"),
|
||||
{
|
||||
"text": markForTranslation("If you try to select the three idle Civilians by clicking and dragging a selection rectangle over them, you might accidentally select additional units. To avoid that, hold %(hotkey)s while selecting so that only idle units are selected. If you accidentally select a cavalry unit, hold Ctrl and click on the cavalry unit icon of the selection panel at the bottom of the screen to remove the cavalry unit from the current selection."),
|
||||
"hotkey": "selection.idleonly"
|
||||
}
|
||||
],
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/farmstead")
|
||||
this.NextGoal();
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"texts": [
|
||||
markForTranslation("You may notice that berries are a finite supply of food. We will need a more lasting food source. Fields produce an unlimited food resource, but are slower to gather than forageable fruits."),
|
||||
markForTranslation("But to minimize the distance between a farm and its corresponding food dropsite, we will first build a Farmstead.")
|
||||
],
|
||||
"showContinueButton": true
|
||||
},
|
||||
"OnOwnershipChanged": function(msg)
|
||||
{
|
||||
|
|
@ -291,10 +361,33 @@ Trigger.prototype.tutorialGoals = [
|
|||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("When the Farmstead construction is finished, its builders will automatically look for food, and in this case, they will go after the nearby goats.\n"),
|
||||
markForTranslation("But your House builders will only look for something else to build and, if nothing found, become idle. Let's wait for them to build the Houses.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("Select the three remaining (idle) Civilians and order them to build a Farmstead in the center of the large open area to the west of the Civic Center.\n") +
|
||||
markForTranslation("We will need a decent chunk of space around the Farmstead to build Fields. In addition, we can see goats on the west side to further improve our food gathering efficiency should we ever decide to hunt them.\n") +
|
||||
markForTranslation("If you try to select the three idle Civilians by clicking and dragging a selection rectangle over them, you might accidentally select additional units. To avoid that, hold %(hotkey)s while selecting so that only idle units are selected. If you accidentally select a cavalry unit, hold Ctrl and click on the cavalry unit icon of the selection panel at the bottom of the screen to remove the cavalry unit from the current selection."),
|
||||
"hotkeys": ["selection.idleonly"]
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/farmstead")
|
||||
this.NextStep();
|
||||
},
|
||||
"OnOwnershipChanged": function(msg)
|
||||
{
|
||||
if (this.houseGoal.has(+msg.entity))
|
||||
this.houseGoal.delete(+msg.entity);
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("When the Farmstead construction is finished, its builders will automatically look for food, and in this case, they will go after the nearby goats.\n") +
|
||||
markForTranslation("But your House builders will only look for something else to build and, if nothing found, become idle."),
|
||||
"hint": markForTranslation("Wait until the houses are built.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return !this.houseGoal.size;
|
||||
|
|
@ -304,51 +397,60 @@ Trigger.prototype.tutorialGoals = [
|
|||
if (this.houseGoal.has(+msg.entity))
|
||||
this.houseGoal.delete(+msg.entity);
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("When both Houses are built, select your two Civilians and order them to build a Field as close as possible to the Farmstead, which is a dropsite for all types of food.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Select your two Civilians and order them to build a Field as close as possible to the Farmstead, which is a dropsite for all types of food.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/field")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("When the Field is ready, the builders will automatically start gathering it.\n"),
|
||||
markForTranslation("The cavalry unit should have slaughtered all chickens by now. Select it and explore the area to the south of the Civic Center: there is a lake with some camels around. Move your cavalry by right-clicking on the point you want to go, and when you see a herd of camels, right-click on one of them to start hunting for food.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"text": markForTranslation("When the Field is ready, the builders will automatically start gathering it.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("The cavalry unit should have slaughtered all chickens by now. Select it and explore the area to the south of the Civic Center: there is a lake with some camels around. Move your cavalry by right-clicking on the point you want to go, and when you see a herd of camels, right-click on one of them to start hunting for food.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "gather" && msg.cmd.target &&
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat")
|
||||
this.NextGoal();
|
||||
TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat")
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Up to five Workers can gather from a Field. To add additional Workers, select the Civic Center and set a rally point on a Field by right-clicking on it. If the Field is not yet finished, new Workers sent by a rally point will help building it, and when built, they will gather food.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Up to five Workers can gather from a Field. To add additional Workers, select the Civic Center and set a rally point on a Field by right-clicking on it. If the Field is not yet finished, new Workers sent by a rally point will help building it, and when built, they will gather food.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command ||
|
||||
(msg.cmd.data.command != "build" || !msg.cmd.data.target || !TriggerHelper.EntityMatchesClassList(msg.cmd.data.target, "Field")) &&
|
||||
(msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain"))
|
||||
(msg.cmd.data.command != "build" || !msg.cmd.data.target || !TriggerHelper.EntityMatchesClassList(msg.cmd.data.target, "Field")) &&
|
||||
(msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain"))
|
||||
{
|
||||
this.WarningMessage(markForTranslation("Select the Civic Center and right-click on the Field."));
|
||||
this.DisplayWarning(markForTranslation("Select the Civic Center and right-click on the Field."));
|
||||
return;
|
||||
}
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Now click three times on the Civilian icon in the bottom right panel to train three additional farmers.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("Now click three times on the Civilian icon in the bottom right panel to train three additional farmers.")
|
||||
},
|
||||
"Init": function(msg)
|
||||
{
|
||||
this.femaleCount = 0;
|
||||
|
|
@ -363,57 +465,76 @@ Trigger.prototype.tutorialGoals = [
|
|||
const txt = +msg.count != 1 ?
|
||||
markForTranslation("Click without holding a hotkey to train a single unit.") :
|
||||
markForTranslation("Click on the Civilian icon.");
|
||||
this.WarningMessage(txt);
|
||||
this.DisplayWarning(txt);
|
||||
return;
|
||||
}
|
||||
if (++this.femaleCount == 3)
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("You can increase the gather rates of your workers by researching new technologies available in some buildings.\n"),
|
||||
markForTranslation("The farming rate, for example, can be improved with a researchable technology in the Farmstead. Select the Farmstead and look at its production panel on the bottom right. You will see several researchable technologies. Hover the cursor over them to see their costs and effects and click on the one you want to research.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"appendable": true,
|
||||
"title": markForTranslation("Technologies"),
|
||||
"text": markForTranslation("You can increase the gather rates of your workers by researching new technologies available in some buildings.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INFO,
|
||||
"panelData": {
|
||||
"appendToPrevious": true,
|
||||
"text": markForTranslation("The farming rate, for example, can be improved with a researchable technology in the Farmstead. Select the Farmstead and look at its production panel on the bottom right. You will see several researchable technologies. Hover the cursor over them to see their costs and effects and click on the one you want to research.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "gather_wicker_baskets") ||
|
||||
TriggerHelper.HasDealtWithTech(this.playerID, "gather_farming_plows");
|
||||
TriggerHelper.HasDealtWithTech(this.playerID, "gather_farming_plows");
|
||||
},
|
||||
"OnResearchQueued": function(msg)
|
||||
{
|
||||
if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Farmstead"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("We should start preparing to phase up into Town Phase, which will unlock many more units and buildings. Select the Civic Center and hover the cursor over the Town Phase icon to see what is still needed.\n"),
|
||||
markForTranslation("We now have enough resources, but one structure is missing. Although this is an economic tutorial, it is nonetheless useful to be prepared for defense in case of attack, so let's build Barracks.\n"),
|
||||
markForTranslation("Select four of your soldiers and ask them to build a Barracks: as before, start selecting the soldiers, click on the Barracks icon in the production panel and then lay down a foundation not far from your Civic Center where you want to build.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("We should start preparing to phase up into Town Phase, which will unlock many more units and buildings. Select the Civic Center and hover the cursor over the Town Phase icon to see what is still needed.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("We now have enough resources, but one structure is missing. Although this is an economic tutorial, it is nonetheless useful to be prepared for defense in case of attack, so let's build a Barracks.\n") +
|
||||
markForTranslation("Select four of your soldiers and ask them to build a Barracks: as before, start selecting the soldiers, click on the Barracks icon in the production panel and then lay down a foundation not far from your Civic Center where you want to build.")
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/barracks")
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("Let's wait for the Barracks to be built. As this construction is lengthy, you can add two soldiers to build it faster. To do so, select your Civic Center and set up a rally point on the Barracks foundation by right-clicking on it (you should see a hammer icon). Then produce two more builders by clicking on the Hoplite icon twice.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text": markForTranslation("As the construction of a Barracks is lengthy, you can add two soldiers to build it faster. To do so, select your Civic Center and set up a rally point on the Barracks foundation by right-clicking on it (you should see a hammer icon). Then produce two more builders by clicking on the Hoplite icon twice."),
|
||||
"hint": markForTranslation("Wait for the Barracks to be built.")
|
||||
},
|
||||
"OnStructureBuilt": function(msg)
|
||||
{
|
||||
if (TriggerHelper.EntityMatchesClassList(msg.building, "Barracks"))
|
||||
this.NextGoal();
|
||||
},
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("You should now be able to research Town Phase. Select the Civic Center and click on the technology icon.\n"),
|
||||
markForTranslation("If you still miss some resources (icon with red overlay), wait for them to be gathered by your workers.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("You should now be able to research Town Phase. Select the Civic Center and click on the technology icon.\n") +
|
||||
markForTranslation("If you still miss some resources (icon with red overlay), wait for them to be gathered by your workers.")
|
||||
},
|
||||
"IsDone": function()
|
||||
{
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen");
|
||||
|
|
@ -421,23 +542,25 @@ Trigger.prototype.tutorialGoals = [
|
|||
"OnResearchQueued": function(msg)
|
||||
{
|
||||
if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre"))
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("In later phases, you need usually stone and metal to build bigger structures and train better soldiers. Hence, while waiting for the research to be done, you will send half of your idle Citizen Soldiers (who have finished building the Barracks) to gather stone and the other half to gather metal.\n"),
|
||||
markForTranslation("To do so, we could select three Citizen Soldiers and right-click on the stone quarry on the west of the Civic Center (the cursor changes when hovering the stone quarry while your soldiers are selected). However, these soldiers were gathering wood, so they may still carry some wood which would be lost when starting to gather another resource.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("In later phases, you need usually stone and metal to build bigger structures and train better soldiers. Hence, while waiting for the research to be done, you will send half of your idle Citizen Soldiers (who have finished building the Barracks) to gather stone and the other half to gather metal.") +
|
||||
markForTranslation("To do so, we could select three Citizen Soldiers and right-click on the stone quarry on the west of the Civic Center (the cursor changes when hovering the stone quarry while your soldiers are selected). However, these soldiers were gathering wood, so they may still carry some wood which would be lost when starting to gather another resource.")
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
{
|
||||
"text": markForTranslation("Thus, we should order them to deposit their wood in the Civic Center along the way. To do so, we will hold %(hotkey)s while clicking to queue orders: select your soldiers, hold %(hotkey)s and right-click on the Civic Center to deposit their wood and then hold %(hotkey)s and right-click on the stone quarry to gather it.\n"),
|
||||
"hotkey": "selection.queue"
|
||||
},
|
||||
markForTranslation("Perform a similar order queue with the remaining soldiers and the metal mine in the west.")
|
||||
],
|
||||
"type": TUTORIAL_STEP_TYPE.INSTRUCTION,
|
||||
"panelData": {
|
||||
"text":
|
||||
markForTranslation("Thus, we should order them to deposit their wood in the Civic Center along the way. To do so, we will hold %(hotkey)s while clicking to queue orders: select your soldiers, hold %(hotkey)s and right-click on the Civic Center to deposit their wood and then hold %(hotkey)s and right-click on the stone quarry to gather it.\n") +
|
||||
markForTranslation("Perform a similar order queue with the remaining soldiers and the metal mine in the west."),
|
||||
"hotkeys": ["session.queue"]
|
||||
},
|
||||
"Init": function()
|
||||
{
|
||||
this.stone = false;
|
||||
|
|
@ -448,7 +571,6 @@ Trigger.prototype.tutorialGoals = [
|
|||
if (!this.stone || !this.metal)
|
||||
return false;
|
||||
return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen");
|
||||
|
||||
},
|
||||
"OnPlayerCommand": function(msg)
|
||||
{
|
||||
|
|
@ -460,18 +582,13 @@ Trigger.prototype.tutorialGoals = [
|
|||
this.metal = true;
|
||||
}
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.NextStep();
|
||||
},
|
||||
"OnResearchFinished": function(msg)
|
||||
{
|
||||
if (this.IsDone())
|
||||
this.NextGoal();
|
||||
this.CompleteTutorial(markForTranslation("This is the end of the walkthrough. This should give you a good idea of the basics of setting up your economy."));
|
||||
}
|
||||
},
|
||||
{
|
||||
"instructions": [
|
||||
markForTranslation("This is the end of the walkthrough. This should give you a good idea of the basics of setting up your economy.")
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,60 +1,63 @@
|
|||
globalThis.PlayerID = -1;
|
||||
|
||||
export function BaseAI(settings)
|
||||
export class BaseAI
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
constructor(settings)
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this.player = settings.player;
|
||||
this.player = settings.player;
|
||||
}
|
||||
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
Serialize()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
Deserialize(data, sharedScript)
|
||||
{
|
||||
}
|
||||
|
||||
Init(playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.accessibility = sharedAI.accessibility;
|
||||
|
||||
this.gameState = sharedAI.gameState[this.player];
|
||||
this.gameState.ai = this;
|
||||
|
||||
this.timeElapsed = sharedAI.timeElapsed;
|
||||
|
||||
this.CustomInit(this.gameState);
|
||||
}
|
||||
|
||||
/** AIs override this function */
|
||||
CustomInit()
|
||||
{
|
||||
}
|
||||
|
||||
HandleMessage(state, playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.OnUpdate(sharedAI);
|
||||
}
|
||||
|
||||
/** AIs override this function */
|
||||
OnUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
chat(message)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "aichat", "message": message });
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
BaseAI.prototype.Serialize = function()
|
||||
{
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
BaseAI.prototype.Deserialize = function(data, sharedScript)
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.Init = function(playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.accessibility = sharedAI.accessibility;
|
||||
|
||||
this.gameState = sharedAI.gameState[this.player];
|
||||
this.gameState.ai = this;
|
||||
|
||||
this.timeElapsed = sharedAI.timeElapsed;
|
||||
|
||||
this.CustomInit(this.gameState);
|
||||
};
|
||||
|
||||
/** AIs override this function */
|
||||
BaseAI.prototype.CustomInit = function()
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.HandleMessage = function(state, playerID, sharedAI)
|
||||
{
|
||||
PlayerID = playerID;
|
||||
this.territoryMap = sharedAI.territoryMap;
|
||||
this.OnUpdate(sharedAI);
|
||||
};
|
||||
|
||||
/** AIs override this function */
|
||||
BaseAI.prototype.OnUpdate = function()
|
||||
{
|
||||
};
|
||||
|
||||
BaseAI.prototype.chat = function(message)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "aichat", "message": message });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* Provides a nicer syntax for defining classes,
|
||||
* with support for OO-style inheritance.
|
||||
*/
|
||||
export function Class(data)
|
||||
{
|
||||
let ctor;
|
||||
if (data._init)
|
||||
ctor = data._init;
|
||||
else
|
||||
ctor = function() { };
|
||||
|
||||
if (data._super)
|
||||
ctor.prototype = { "__proto__": data._super.prototype };
|
||||
|
||||
for (const key in data)
|
||||
ctor.prototype[key] = data[key];
|
||||
|
||||
return ctor;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,368 +1,371 @@
|
|||
import { SquareVectorDistance } from "simulation/ai/common-api/utils.js";
|
||||
|
||||
export function EntityCollection(sharedAI, entities = new Map(), filters = [])
|
||||
export class EntityCollection
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
this._entities = entities;
|
||||
this._filters = filters;
|
||||
this.dynamicProp = [];
|
||||
for (const filter of this._filters)
|
||||
if (filter.dynamicProperties.length)
|
||||
this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties);
|
||||
|
||||
Object.defineProperty(this, "length", { "get": () => this._entities.size });
|
||||
this.frozen = false;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.Serialize = function()
|
||||
{
|
||||
const filters = [];
|
||||
for (const f of this._filters)
|
||||
filters.push(uneval(f));
|
||||
return {
|
||||
"ents": this.toIdArray(),
|
||||
"frozen": this.frozen,
|
||||
"filters": filters
|
||||
};
|
||||
};
|
||||
|
||||
EntityCollection.prototype.Deserialize = function(data, sharedAI)
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
for (const id of data.ents)
|
||||
this._entities.set(id, sharedAI._entities.get(id));
|
||||
|
||||
for (const f of data.filters)
|
||||
this._filters.push(eval(f));
|
||||
|
||||
if (data.frozen)
|
||||
this.freeze();
|
||||
else
|
||||
this.defreeze();
|
||||
};
|
||||
|
||||
/**
|
||||
* If an entitycollection is frozen, it will never automatically add a unit.
|
||||
* But can remove one.
|
||||
* this makes it easy to create entity collection that will auto-remove dead units
|
||||
* but never add new ones.
|
||||
*/
|
||||
EntityCollection.prototype.freeze = function()
|
||||
{
|
||||
this.frozen = true;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.defreeze = function()
|
||||
{
|
||||
this.frozen = false;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toIdArray = function()
|
||||
{
|
||||
return Array.from(this._entities.keys());
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toEntityArray = function()
|
||||
{
|
||||
return Array.from(this._entities.values());
|
||||
};
|
||||
|
||||
EntityCollection.prototype.values = function()
|
||||
{
|
||||
return this._entities.values();
|
||||
};
|
||||
|
||||
EntityCollection.prototype.toString = function()
|
||||
{
|
||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
||||
};
|
||||
|
||||
EntityCollection.prototype.filter = function(filter, thisp)
|
||||
{
|
||||
if (typeof filter === "function")
|
||||
filter = { "func": filter, "dynamicProperties": [] };
|
||||
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
if (filter.func.call(thisp, ent, id, this))
|
||||
ret.set(id, ent);
|
||||
|
||||
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the (at most) n entities nearest to targetPos.
|
||||
*/
|
||||
EntityCollection.prototype.filterNearest = function(targetPos, n)
|
||||
{
|
||||
// Compute the distance of each entity
|
||||
const data = []; // [ [id, ent, distance], ... ]
|
||||
for (const [id, ent] of this._entities)
|
||||
if (ent.position())
|
||||
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
|
||||
|
||||
// Sort by increasing distance
|
||||
data.sort((a, b) => a[2] - b[2]);
|
||||
|
||||
if (n === undefined)
|
||||
n = data.length;
|
||||
else
|
||||
n = Math.min(n, data.length);
|
||||
|
||||
// Extract the first n
|
||||
const ret = new Map();
|
||||
for (let i = 0; i < n; ++i)
|
||||
ret.set(data[i][0], data[i][1]);
|
||||
|
||||
return new EntityCollection(this._ai, ret);
|
||||
};
|
||||
|
||||
EntityCollection.prototype.filter_raw = function(callback, thisp)
|
||||
{
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
constructor(sharedAI, entities = new Map(), filters = [])
|
||||
{
|
||||
const val = ent._entity;
|
||||
if (callback.call(thisp, val, id, this))
|
||||
ret.set(id, ent);
|
||||
this._ai = sharedAI;
|
||||
this._entities = entities;
|
||||
this._filters = filters;
|
||||
this.dynamicProp = [];
|
||||
for (const filter of this._filters)
|
||||
if (filter.dynamicProperties.length)
|
||||
this.dynamicProp = this.dynamicProp.concat(filter.dynamicProperties);
|
||||
|
||||
Object.defineProperty(this, "length", { "get": () => this._entities.size });
|
||||
this.frozen = false;
|
||||
}
|
||||
return new EntityCollection(this._ai, ret);
|
||||
};
|
||||
|
||||
EntityCollection.prototype.forEach = function(callback)
|
||||
{
|
||||
for (const ent of this._entities.values())
|
||||
callback(ent);
|
||||
return this;
|
||||
};
|
||||
Serialize()
|
||||
{
|
||||
const filters = [];
|
||||
for (const f of this._filters)
|
||||
filters.push(uneval(f));
|
||||
return {
|
||||
"ents": this.toIdArray(),
|
||||
"frozen": this.frozen,
|
||||
"filters": filters
|
||||
};
|
||||
}
|
||||
|
||||
EntityCollection.prototype.hasEntities = function()
|
||||
{
|
||||
return this._entities.size !== 0;
|
||||
};
|
||||
Deserialize(data, sharedAI)
|
||||
{
|
||||
this._ai = sharedAI;
|
||||
for (const id of data.ents)
|
||||
this._entities.set(id, sharedAI._entities.get(id));
|
||||
|
||||
EntityCollection.prototype.move = function(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
for (const f of data.filters)
|
||||
this._filters.push(eval(f));
|
||||
|
||||
EntityCollection.prototype.moveToRange = function(x, z, min, max, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk-to-range",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
if (data.frozen)
|
||||
this.freeze();
|
||||
else
|
||||
this.defreeze();
|
||||
}
|
||||
|
||||
EntityCollection.prototype.attackMove = function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack-walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"targetClasses": targetClasses,
|
||||
"allowCapture": allowCapture,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
/**
|
||||
* If an entitycollection is frozen, it will never automatically add a unit.
|
||||
* But can remove one.
|
||||
* this makes it easy to create entity collection that will auto-remove dead units
|
||||
* but never add new ones.
|
||||
*/
|
||||
freeze()
|
||||
{
|
||||
this.frozen = true;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.moveIndiv = function(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
for (const id of this._entities.keys())
|
||||
defreeze()
|
||||
{
|
||||
this.frozen = false;
|
||||
}
|
||||
|
||||
toIdArray()
|
||||
{
|
||||
return Array.from(this._entities.keys());
|
||||
}
|
||||
|
||||
toEntityArray()
|
||||
{
|
||||
return Array.from(this._entities.values());
|
||||
}
|
||||
|
||||
values()
|
||||
{
|
||||
return this._entities.values();
|
||||
}
|
||||
|
||||
toString()
|
||||
{
|
||||
return "[EntityCollection " + this.toEntityArray().join(" ") + "]";
|
||||
}
|
||||
|
||||
filter(filter, thisp)
|
||||
{
|
||||
if (typeof filter === "function")
|
||||
filter = { "func": filter, "dynamicProperties": [] };
|
||||
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
if (filter.func.call(thisp, ent, id, this))
|
||||
ret.set(id, ent);
|
||||
|
||||
return new EntityCollection(this._ai, ret, this._filters.concat([filter]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the (at most) n entities nearest to targetPos.
|
||||
*/
|
||||
filterNearest(targetPos, n)
|
||||
{
|
||||
// Compute the distance of each entity
|
||||
const data = []; // [ [id, ent, distance], ... ]
|
||||
for (const [id, ent] of this._entities)
|
||||
if (ent.position())
|
||||
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
|
||||
|
||||
// Sort by increasing distance
|
||||
data.sort((a, b) => a[2] - b[2]);
|
||||
|
||||
if (n === undefined)
|
||||
n = data.length;
|
||||
else
|
||||
n = Math.min(n, data.length);
|
||||
|
||||
// Extract the first n
|
||||
const ret = new Map();
|
||||
for (let i = 0; i < n; ++i)
|
||||
ret.set(data[i][0], data[i][1]);
|
||||
|
||||
return new EntityCollection(this._ai, ret);
|
||||
}
|
||||
|
||||
filter_raw(callback, thisp)
|
||||
{
|
||||
const ret = new Map();
|
||||
for (const [id, ent] of this._entities)
|
||||
{
|
||||
const val = ent._entity;
|
||||
if (callback.call(thisp, val, id, this))
|
||||
ret.set(id, ent);
|
||||
}
|
||||
return new EntityCollection(this._ai, ret);
|
||||
}
|
||||
|
||||
forEach(callback)
|
||||
{
|
||||
for (const ent of this._entities.values())
|
||||
callback(ent);
|
||||
return this;
|
||||
}
|
||||
|
||||
hasEntities()
|
||||
{
|
||||
return this._entities.size !== 0;
|
||||
}
|
||||
|
||||
move(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": [id],
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.garrison = function(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "garrison",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.occupyTurret = function(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "occupy-turret",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.destroy = function()
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });
|
||||
return this;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.attack = function(unitId, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack",
|
||||
"entities": this.toIdArray(),
|
||||
"target": unitId,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/** violent, aggressive, defensive, passive, standground */
|
||||
EntityCollection.prototype.setStance = function(stance)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "stance",
|
||||
"entities": this.toIdArray(),
|
||||
"name": stance
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/** Returns the average position of all units */
|
||||
EntityCollection.prototype.getCentrePosition = function()
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let count = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
count++;
|
||||
return this;
|
||||
}
|
||||
|
||||
return count ? [sumPos[0]/count, sumPos[1]/count] : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the average position from the sample first units.
|
||||
* This might be faster for huge collections, but there's
|
||||
* always a risk that it'll be unprecise.
|
||||
*/
|
||||
EntityCollection.prototype.getApproximatePosition = function(sample)
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let i = 0;
|
||||
for (const ent of this._entities.values())
|
||||
moveToRange(x, z, min, max, queued = false, pushFront = false)
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
i++;
|
||||
if (i === sample)
|
||||
break;
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk-to-range",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
return i ? [sumPos[0]/i, sumPos[1]/i] : undefined;
|
||||
};
|
||||
|
||||
EntityCollection.prototype.hasEntId = function(id)
|
||||
{
|
||||
return this._entities.has(id);
|
||||
};
|
||||
|
||||
/** Removes an entity from the collection, returns true if the entity was a member, false otherwise */
|
||||
EntityCollection.prototype.removeEnt = function(ent)
|
||||
{
|
||||
if (!this._entities.has(ent.id()))
|
||||
return false;
|
||||
this._entities.delete(ent.id());
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Adds an entity to the collection, returns true if the entity was not member, false otherwise */
|
||||
EntityCollection.prototype.addEnt = function(ent)
|
||||
{
|
||||
if (this._entities.has(ent.id()))
|
||||
return false;
|
||||
this._entities.set(ent.id(), ent);
|
||||
const temp = this.toEntityArray();
|
||||
temp.sort((a, b) => a.id() - b.id());
|
||||
this._entities.clear();
|
||||
for (const e of temp)
|
||||
this._entities.set(e.id(), e);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the entity against the filters, and adds or removes it appropriately, returns true if the
|
||||
* entity collection was modified.
|
||||
* Force can add a unit despite a freezing.
|
||||
* If an entitycollection is frozen, it will never automatically add a unit.
|
||||
* But can remove one.
|
||||
*/
|
||||
EntityCollection.prototype.updateEnt = function(ent, force)
|
||||
{
|
||||
let passesFilters = true;
|
||||
for (const filter of this._filters)
|
||||
passesFilters = passesFilters && filter.func(ent);
|
||||
|
||||
if (passesFilters)
|
||||
attackMove(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false)
|
||||
{
|
||||
if (!force && this.frozen)
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack-walk",
|
||||
"entities": this.toIdArray(),
|
||||
"x": x,
|
||||
"z": z,
|
||||
"targetClasses": targetClasses,
|
||||
"allowCapture": allowCapture,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
moveIndiv(x, z, queued = false, pushFront = false)
|
||||
{
|
||||
for (const id of this._entities.keys())
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "walk",
|
||||
"entities": [id],
|
||||
"x": x,
|
||||
"z": z,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
garrison(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "garrison",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
occupyTurret(target, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "occupy-turret",
|
||||
"entities": this.toIdArray(),
|
||||
"target": target.id(),
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy()
|
||||
{
|
||||
Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": this.toIdArray() });
|
||||
return this;
|
||||
}
|
||||
|
||||
attack(unitId, queued = false, pushFront = false)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "attack",
|
||||
"entities": this.toIdArray(),
|
||||
"target": unitId,
|
||||
"queued": queued,
|
||||
"pushFront": pushFront
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** violent, aggressive, defensive, passive, standground */
|
||||
setStance(stance)
|
||||
{
|
||||
Engine.PostCommand(PlayerID, {
|
||||
"type": "stance",
|
||||
"entities": this.toIdArray(),
|
||||
"name": stance
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns the average position of all units */
|
||||
getCentrePosition()
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let count = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
count++;
|
||||
}
|
||||
|
||||
return count ? [sumPos[0]/count, sumPos[1]/count] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the average position from the sample first units.
|
||||
* This might be faster for huge collections, but there's
|
||||
* always a risk that it'll be unprecise.
|
||||
*/
|
||||
getApproximatePosition(sample)
|
||||
{
|
||||
const sumPos = [0, 0];
|
||||
let i = 0;
|
||||
for (const ent of this._entities.values())
|
||||
{
|
||||
if (!ent.position())
|
||||
continue;
|
||||
sumPos[0] += ent.position()[0];
|
||||
sumPos[1] += ent.position()[1];
|
||||
i++;
|
||||
if (i === sample)
|
||||
break;
|
||||
}
|
||||
|
||||
return i ? [sumPos[0]/i, sumPos[1]/i] : undefined;
|
||||
}
|
||||
|
||||
hasEntId(id)
|
||||
{
|
||||
return this._entities.has(id);
|
||||
}
|
||||
|
||||
/** Removes an entity from the collection, returns true if the entity was a member, false otherwise */
|
||||
removeEnt(ent)
|
||||
{
|
||||
if (!this._entities.has(ent.id()))
|
||||
return false;
|
||||
return this.addEnt(ent);
|
||||
this._entities.delete(ent.id());
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.removeEnt(ent);
|
||||
};
|
||||
/** Adds an entity to the collection, returns true if the entity was not member, false otherwise */
|
||||
addEnt(ent)
|
||||
{
|
||||
if (this._entities.has(ent.id()))
|
||||
return false;
|
||||
this._entities.set(ent.id(), ent);
|
||||
const temp = this.toEntityArray();
|
||||
temp.sort((a, b) => a.id() - b.id());
|
||||
this._entities.clear();
|
||||
for (const e of temp)
|
||||
this._entities.set(e.id(), e);
|
||||
return true;
|
||||
}
|
||||
|
||||
EntityCollection.prototype.registerUpdates = function()
|
||||
{
|
||||
this._ai.registerUpdatingEntityCollection(this);
|
||||
};
|
||||
/**
|
||||
* Checks the entity against the filters, and adds or removes it appropriately, returns true if the
|
||||
* entity collection was modified.
|
||||
* Force can add a unit despite a freezing.
|
||||
* If an entitycollection is frozen, it will never automatically add a unit.
|
||||
* But can remove one.
|
||||
*/
|
||||
updateEnt(ent, force)
|
||||
{
|
||||
let passesFilters = true;
|
||||
for (const filter of this._filters)
|
||||
passesFilters = passesFilters && filter.func(ent);
|
||||
|
||||
EntityCollection.prototype.unregister = function()
|
||||
{
|
||||
this._ai.removeUpdatingEntityCollection(this);
|
||||
};
|
||||
if (passesFilters)
|
||||
{
|
||||
if (!force && this.frozen)
|
||||
return false;
|
||||
return this.addEnt(ent);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.dynamicProperties = function()
|
||||
{
|
||||
return this.dynamicProp;
|
||||
};
|
||||
return this.removeEnt(ent);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.setUID = function(id)
|
||||
{
|
||||
this._UID = id;
|
||||
};
|
||||
registerUpdates()
|
||||
{
|
||||
this._ai.registerUpdatingEntityCollection(this);
|
||||
}
|
||||
|
||||
EntityCollection.prototype.getUID = function()
|
||||
{
|
||||
return this._UID;
|
||||
};
|
||||
unregister()
|
||||
{
|
||||
this._ai.removeUpdatingEntityCollection(this);
|
||||
}
|
||||
|
||||
dynamicProperties()
|
||||
{
|
||||
return this.dynamicProp;
|
||||
}
|
||||
|
||||
setUID(id)
|
||||
{
|
||||
this._UID = id;
|
||||
}
|
||||
|
||||
getUID()
|
||||
{
|
||||
return this._UID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,211 +2,213 @@
|
|||
* The map module.
|
||||
* Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values.
|
||||
*/
|
||||
export function InfoMap(sharedScript, type, originalMap, actualCopy)
|
||||
export class InfoMap
|
||||
{
|
||||
// get the correct dimensions according to the map type
|
||||
const map = type == "territory" || type == "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap;
|
||||
this.width = map.width;
|
||||
this.height = map.height;
|
||||
this.cellSize = map.cellSize;
|
||||
this.length = this.width * this.height;
|
||||
|
||||
this.maxVal = 255;
|
||||
|
||||
// sanity check
|
||||
if (originalMap && originalMap.length != this.length)
|
||||
warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length);
|
||||
|
||||
if (originalMap && actualCopy)
|
||||
constructor(sharedScript, type, originalMap, actualCopy)
|
||||
{
|
||||
// get the correct dimensions according to the map type
|
||||
const map = type == "territory" || type == "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap;
|
||||
this.width = map.width;
|
||||
this.height = map.height;
|
||||
this.cellSize = map.cellSize;
|
||||
this.length = this.width * this.height;
|
||||
|
||||
this.maxVal = 255;
|
||||
|
||||
// sanity check
|
||||
if (originalMap && originalMap.length != this.length)
|
||||
warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length);
|
||||
|
||||
if (originalMap && actualCopy)
|
||||
{
|
||||
this.map = new Uint8Array(this.length);
|
||||
for (let i = 0; i < this.length; ++i)
|
||||
this.map[i] = originalMap[i];
|
||||
}
|
||||
else if (originalMap)
|
||||
this.map = originalMap;
|
||||
else
|
||||
this.map = new Uint8Array(this.length);
|
||||
}
|
||||
|
||||
setMaxVal(val)
|
||||
{
|
||||
this.maxVal = val;
|
||||
}
|
||||
|
||||
gamePosToMapPos(p)
|
||||
{
|
||||
return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)];
|
||||
}
|
||||
|
||||
point(p)
|
||||
{
|
||||
const q = this.gamePosToMapPos(p);
|
||||
q[0] = q[0] >= this.width ? this.width-1 : q[0] < 0 ? 0 : q[0];
|
||||
q[1] = q[1] >= this.width ? this.width-1 : q[1] < 0 ? 0 : q[1];
|
||||
return this.map[q[0] + this.width * q[1]];
|
||||
}
|
||||
|
||||
runLoop(x0, x1, y0, y1, cx, cy, maxDist2, func)
|
||||
{
|
||||
for (let y = y0; y < y1; ++y)
|
||||
{
|
||||
const dy2 = (y - cy) * (y - cy);
|
||||
const yw = y * this.width;
|
||||
for (let x = x0; x < x1; ++x)
|
||||
{
|
||||
const dx = x - cx;
|
||||
const r2 = dx * dx + dy2;
|
||||
if (r2 >= maxDist2)
|
||||
continue;
|
||||
const w = x + yw;
|
||||
this.set(w, func(w, r2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addInfluence(cx, cy, maxDist, strength, type = "linear")
|
||||
{
|
||||
strength = strength ? strength : maxDist;
|
||||
|
||||
const x0 = Math.floor(Math.max(0, cx - maxDist));
|
||||
const y0 = Math.floor(Math.max(0, cy - maxDist));
|
||||
const x1 = Math.floor(Math.min(this.width-1, cx + maxDist));
|
||||
const y1 = Math.floor(Math.min(this.height-1, cy + maxDist));
|
||||
const maxDist2 = maxDist * maxDist;
|
||||
|
||||
if (type == "linear")
|
||||
{
|
||||
const str = strength / maxDist;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist - Math.sqrt(r2)));
|
||||
}
|
||||
else if (type == "quadratic")
|
||||
{
|
||||
const str = strength / maxDist2;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist2 - r2));
|
||||
}
|
||||
else
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + strength);
|
||||
}
|
||||
|
||||
multiplyInfluence(cx, cy, maxDist, strength, type = "constant")
|
||||
{
|
||||
strength = strength ? +strength : +maxDist;
|
||||
|
||||
const x0 = Math.max(0, cx - maxDist);
|
||||
const y0 = Math.max(0, cy - maxDist);
|
||||
const x1 = Math.min(this.width, cx + maxDist);
|
||||
const y1 = Math.min(this.height, cy + maxDist);
|
||||
const maxDist2 = maxDist * maxDist;
|
||||
|
||||
if (type == "linear")
|
||||
{
|
||||
const str = strength / maxDist;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist - Math.sqrt(r2)) * this.map[w]);
|
||||
}
|
||||
else if (type == "quadratic")
|
||||
{
|
||||
const str = strength / maxDist2;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist2 - r2) * this.map[w]);
|
||||
}
|
||||
else
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] * strength);
|
||||
}
|
||||
|
||||
/** add to current map by the parameter map pixelwise */
|
||||
add(map)
|
||||
{
|
||||
this.map = new Uint8Array(this.length);
|
||||
for (let i = 0; i < this.length; ++i)
|
||||
this.map[i] = originalMap[i];
|
||||
this.set(i, this.map[i] + map.map[i]);
|
||||
}
|
||||
else if (originalMap)
|
||||
this.map = originalMap;
|
||||
else
|
||||
this.map = new Uint8Array(this.length);
|
||||
}
|
||||
|
||||
InfoMap.prototype.setMaxVal = function(val)
|
||||
{
|
||||
this.maxVal = val;
|
||||
};
|
||||
|
||||
InfoMap.prototype.gamePosToMapPos = function(p)
|
||||
{
|
||||
return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)];
|
||||
};
|
||||
|
||||
InfoMap.prototype.point = function(p)
|
||||
{
|
||||
const q = this.gamePosToMapPos(p);
|
||||
q[0] = q[0] >= this.width ? this.width-1 : q[0] < 0 ? 0 : q[0];
|
||||
q[1] = q[1] >= this.width ? this.width-1 : q[1] < 0 ? 0 : q[1];
|
||||
return this.map[q[0] + this.width * q[1]];
|
||||
};
|
||||
|
||||
InfoMap.prototype.runLoop = function(x0, x1, y0, y1, cx, cy, maxDist2, func)
|
||||
{
|
||||
for (let y = y0; y < y1; ++y)
|
||||
/** Set the value taking overflow into account */
|
||||
set(i, value)
|
||||
{
|
||||
const dy2 = (y - cy) * (y - cy);
|
||||
const yw = y * this.width;
|
||||
for (let x = x0; x < x1; ++x)
|
||||
this.map[i] = value < 0 ? 0 : value > this.maxVal ? this.maxVal : value;
|
||||
}
|
||||
|
||||
/** Find the best non-obstructed tile */
|
||||
findBestTile(radius, obstruction)
|
||||
{
|
||||
let bestIdx;
|
||||
let bestVal = 0;
|
||||
for (let j = 0; j < this.length; ++j)
|
||||
{
|
||||
const dx = x - cx;
|
||||
const r2 = dx * dx + dy2;
|
||||
if (r2 >= maxDist2)
|
||||
if (this.map[j] <= bestVal)
|
||||
continue;
|
||||
const w = x + yw;
|
||||
this.set(w, func(w, r2));
|
||||
const i = this.getNonObstructedTile(j, radius, obstruction);
|
||||
if (i < 0)
|
||||
continue;
|
||||
bestVal = this.map[j];
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
InfoMap.prototype.addInfluence = function(cx, cy, maxDist, strength, type = "linear")
|
||||
{
|
||||
strength = strength ? strength : maxDist;
|
||||
|
||||
const x0 = Math.floor(Math.max(0, cx - maxDist));
|
||||
const y0 = Math.floor(Math.max(0, cy - maxDist));
|
||||
const x1 = Math.floor(Math.min(this.width-1, cx + maxDist));
|
||||
const y1 = Math.floor(Math.min(this.height-1, cy + maxDist));
|
||||
const maxDist2 = maxDist * maxDist;
|
||||
|
||||
if (type == "linear")
|
||||
{
|
||||
const str = strength / maxDist;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist - Math.sqrt(r2)));
|
||||
}
|
||||
else if (type == "quadratic")
|
||||
{
|
||||
const str = strength / maxDist2;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist2 - r2));
|
||||
}
|
||||
else
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + strength);
|
||||
|
||||
};
|
||||
|
||||
InfoMap.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type = "constant")
|
||||
{
|
||||
strength = strength ? +strength : +maxDist;
|
||||
|
||||
const x0 = Math.max(0, cx - maxDist);
|
||||
const y0 = Math.max(0, cy - maxDist);
|
||||
const x1 = Math.min(this.width, cx + maxDist);
|
||||
const y1 = Math.min(this.height, cy + maxDist);
|
||||
const maxDist2 = maxDist * maxDist;
|
||||
|
||||
if (type == "linear")
|
||||
{
|
||||
const str = strength / maxDist;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist - Math.sqrt(r2)) * this.map[w]);
|
||||
}
|
||||
else if (type == "quadratic")
|
||||
{
|
||||
const str = strength / maxDist2;
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist2 - r2) * this.map[w]);
|
||||
}
|
||||
else
|
||||
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] * strength);
|
||||
};
|
||||
|
||||
/** add to current map by the parameter map pixelwise */
|
||||
InfoMap.prototype.add = function(map)
|
||||
{
|
||||
for (let i = 0; i < this.length; ++i)
|
||||
this.set(i, this.map[i] + map.map[i]);
|
||||
};
|
||||
|
||||
/** Set the value taking overflow into account */
|
||||
InfoMap.prototype.set = function(i, value)
|
||||
{
|
||||
this.map[i] = value < 0 ? 0 : value > this.maxVal ? this.maxVal : value;
|
||||
};
|
||||
|
||||
/** Find the best non-obstructed tile */
|
||||
InfoMap.prototype.findBestTile = function(radius, obstruction)
|
||||
{
|
||||
let bestIdx;
|
||||
let bestVal = 0;
|
||||
for (let j = 0; j < this.length; ++j)
|
||||
{
|
||||
if (this.map[j] <= bestVal)
|
||||
continue;
|
||||
const i = this.getNonObstructedTile(j, radius, obstruction);
|
||||
if (i < 0)
|
||||
continue;
|
||||
bestVal = this.map[j];
|
||||
bestIdx = i;
|
||||
return { "idx": bestIdx, "val": bestVal };
|
||||
}
|
||||
|
||||
return { "idx": bestIdx, "val": bestVal };
|
||||
};
|
||||
|
||||
/** return any non obstructed (small) tile inside the (big) tile i from obstruction map */
|
||||
InfoMap.prototype.getNonObstructedTile = function(i, radius, obstruction)
|
||||
{
|
||||
const ratio = this.cellSize / obstruction.cellSize;
|
||||
const ix = (i % this.width) * ratio;
|
||||
const iy = Math.floor(i / this.width) * ratio;
|
||||
const w = obstruction.width;
|
||||
const r2 = radius * radius;
|
||||
let lastPoint;
|
||||
for (let kx = ix; kx < ix + ratio; ++kx)
|
||||
/** return any non obstructed (small) tile inside the (big) tile i from obstruction map */
|
||||
getNonObstructedTile(i, radius, obstruction)
|
||||
{
|
||||
if (kx < radius || kx >= w - radius)
|
||||
continue;
|
||||
for (let ky = iy; ky < iy + ratio; ++ky)
|
||||
{
|
||||
if (ky < radius || ky >= w - radius)
|
||||
continue;
|
||||
if (lastPoint && (kx - lastPoint.x)*(kx - lastPoint.x) + (ky - lastPoint.y)*(ky - lastPoint.y) < r2)
|
||||
continue;
|
||||
lastPoint = obstruction.isObstructedTile(kx, ky, radius);
|
||||
if (!lastPoint)
|
||||
return kx + ky*w;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/** return true if the area centered on tile kx-ky and with radius is obstructed */
|
||||
InfoMap.prototype.isObstructedTile = function(kx, ky, radius)
|
||||
{
|
||||
const w = this.width;
|
||||
if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] == 0)
|
||||
return { "x": kx, "y": ky };
|
||||
if (!this.pattern || this.pattern[0] != radius)
|
||||
{
|
||||
this.pattern = [radius];
|
||||
const ratio = this.cellSize / obstruction.cellSize;
|
||||
const ix = (i % this.width) * ratio;
|
||||
const iy = Math.floor(i / this.width) * ratio;
|
||||
const w = obstruction.width;
|
||||
const r2 = radius * radius;
|
||||
for (let i = 1; i <= radius; ++i)
|
||||
this.pattern.push(Math.floor(Math.sqrt(r2 - (i-0.5)*(i-0.5)) + 0.5));
|
||||
}
|
||||
for (let dy = 0; dy <= radius; ++dy)
|
||||
{
|
||||
const dxmax = this.pattern[dy];
|
||||
const xp = kx + (ky + dy)*w;
|
||||
const xm = kx + (ky - dy)*w;
|
||||
for (let dx = 0; dx <= dxmax; ++dx)
|
||||
let lastPoint;
|
||||
for (let kx = ix; kx < ix + ratio; ++kx)
|
||||
{
|
||||
if (this.map[xp + dx] == 0)
|
||||
return { "x": kx + dx, "y": ky + dy };
|
||||
if (this.map[xm + dx] == 0)
|
||||
return { "x": kx + dx, "y": ky - dy };
|
||||
if (this.map[xp - dx] == 0)
|
||||
return { "x": kx - dx, "y": ky + dy };
|
||||
if (this.map[xm - dx] == 0)
|
||||
return { "x": kx - dx, "y": ky - dy };
|
||||
if (kx < radius || kx >= w - radius)
|
||||
continue;
|
||||
for (let ky = iy; ky < iy + ratio; ++ky)
|
||||
{
|
||||
if (ky < radius || ky >= w - radius)
|
||||
continue;
|
||||
if (lastPoint && (kx - lastPoint.x)*(kx - lastPoint.x) + (ky - lastPoint.y)*(ky - lastPoint.y) < r2)
|
||||
continue;
|
||||
lastPoint = obstruction.isObstructedTile(kx, ky, radius);
|
||||
if (!lastPoint)
|
||||
return kx + ky*w;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
InfoMap.prototype.dumpIm = function(name = "default.png", threshold = this.maxVal)
|
||||
{
|
||||
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
|
||||
};
|
||||
/** return true if the area centered on tile kx-ky and with radius is obstructed */
|
||||
isObstructedTile(kx, ky, radius)
|
||||
{
|
||||
const w = this.width;
|
||||
if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] == 0)
|
||||
return { "x": kx, "y": ky };
|
||||
if (!this.pattern || this.pattern[0] != radius)
|
||||
{
|
||||
this.pattern = [radius];
|
||||
const r2 = radius * radius;
|
||||
for (let i = 1; i <= radius; ++i)
|
||||
this.pattern.push(Math.floor(Math.sqrt(r2 - (i-0.5)*(i-0.5)) + 0.5));
|
||||
}
|
||||
for (let dy = 0; dy <= radius; ++dy)
|
||||
{
|
||||
const dxmax = this.pattern[dy];
|
||||
const xp = kx + (ky + dy)*w;
|
||||
const xm = kx + (ky - dy)*w;
|
||||
for (let dx = 0; dx <= dxmax; ++dx)
|
||||
{
|
||||
if (this.map[xp + dx] == 0)
|
||||
return { "x": kx + dx, "y": ky + dy };
|
||||
if (this.map[xm + dx] == 0)
|
||||
return { "x": kx + dx, "y": ky - dy };
|
||||
if (this.map[xp - dx] == 0)
|
||||
return { "x": kx - dx, "y": ky + dy };
|
||||
if (this.map[xm - dx] == 0)
|
||||
return { "x": kx - dx, "y": ky - dy };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dumpIm(name = "default.png", threshold = this.maxVal)
|
||||
{
|
||||
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,63 @@
|
|||
Resources = new Resources();
|
||||
|
||||
export function ResourcesManager(amounts = {}, population = 0)
|
||||
export class ResourcesManager
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = amounts[key] || 0;
|
||||
constructor(amounts = {}, population = 0)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = amounts[key] || 0;
|
||||
|
||||
this.population = population > 0 ? population : 0;
|
||||
this.population = population > 0 ? population : 0;
|
||||
}
|
||||
|
||||
reset()
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = 0;
|
||||
this.population = 0;
|
||||
}
|
||||
|
||||
canAfford(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
if (this[key] < that[key])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
add(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] += that[key];
|
||||
this.population += that.population;
|
||||
}
|
||||
|
||||
subtract(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] -= that[key];
|
||||
this.population += that.population;
|
||||
}
|
||||
|
||||
multiply(n)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] *= n;
|
||||
this.population *= n;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
const amounts = {};
|
||||
for (const key of Resources.GetCodes())
|
||||
amounts[key] = this[key];
|
||||
return { "amounts": amounts, "population": this.population };
|
||||
}
|
||||
|
||||
Deserialize(data)
|
||||
{
|
||||
for (const key in data.amounts)
|
||||
this[key] = data.amounts[key];
|
||||
this.population = data.population;
|
||||
}
|
||||
}
|
||||
|
||||
ResourcesManager.prototype.reset = function()
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] = 0;
|
||||
this.population = 0;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.canAfford = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
if (this[key] < that[key])
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.add = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] += that[key];
|
||||
this.population += that.population;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.subtract = function(that)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] -= that[key];
|
||||
this.population += that.population;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.multiply = function(n)
|
||||
{
|
||||
for (const key of Resources.GetCodes())
|
||||
this[key] *= n;
|
||||
this.population *= n;
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.Serialize = function()
|
||||
{
|
||||
const amounts = {};
|
||||
for (const key of Resources.GetCodes())
|
||||
amounts[key] = this[key];
|
||||
return { "amounts": amounts, "population": this.population };
|
||||
};
|
||||
|
||||
ResourcesManager.prototype.Deserialize = function(data)
|
||||
{
|
||||
for (const key in data.amounts)
|
||||
this[key] = data.amounts[key];
|
||||
this.population = data.population;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,462 +5,451 @@ import { InfoMap } from "simulation/ai/common-api/map-module.js";
|
|||
import { Accessibility, TerrainAnalysis } from "simulation/ai/common-api/terrain-analysis.js";
|
||||
|
||||
/** Shared script handling templates and basic terrain analysis */
|
||||
export function SharedScript(settings)
|
||||
export class SharedScript
|
||||
{
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
|
||||
this._templates = settings.templates;
|
||||
|
||||
this._entityMetadata = {};
|
||||
for (const player of this._players)
|
||||
this._entityMetadata[player] = {};
|
||||
|
||||
// array of entity collections
|
||||
this._entityCollections = new Map();
|
||||
this._entitiesModifications = new Map(); // entities modifications
|
||||
this._templatesModifications = {}; // template modifications
|
||||
// each name is a reference to the actual one.
|
||||
this._entityCollectionsName = new Map();
|
||||
this._entityCollectionsByDynProp = {};
|
||||
this._entityCollectionsUID = 0;
|
||||
}
|
||||
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
SharedScript.prototype.Serialize = function()
|
||||
{
|
||||
return {
|
||||
"players": this._players,
|
||||
"templatesModifications": this._templatesModifications,
|
||||
"entitiesModifications": this._entitiesModifications,
|
||||
"metadata": this._entityMetadata
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
SharedScript.prototype.Deserialize = function(data)
|
||||
{
|
||||
this._players = data.players;
|
||||
this._templatesModifications = data.templatesModifications;
|
||||
this._entitiesModifications = data.entitiesModifications;
|
||||
this._entityMetadata = data.metadata;
|
||||
|
||||
this.isDeserialized = true;
|
||||
};
|
||||
|
||||
SharedScript.prototype.GetTemplate = function(name)
|
||||
{
|
||||
if (this._templates[name] === undefined)
|
||||
this._templates[name] = Engine.GetTemplate(name) || null;
|
||||
|
||||
return this._templates[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the shared component.
|
||||
* We need to know the initial state of the game for this, as we will use it.
|
||||
* This is called right at the end of the map generation.
|
||||
*/
|
||||
SharedScript.prototype.init = function(state, deserialization)
|
||||
{
|
||||
if (!deserialization)
|
||||
this._entitiesModifications = new Map();
|
||||
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.circularMap = state.circularMap;
|
||||
this.mapSize = state.mapSize;
|
||||
this.victoryConditions = new Set(state.victoryConditions);
|
||||
this.alliedVictory = state.alliedVictory;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
if (this.mapSize % this.passabilityMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
if (this.mapSize % this.territoryMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
/*
|
||||
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
|
||||
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
|
||||
for (let i = 0; i < this.passabilityMap.data.length; ++i)
|
||||
constructor(settings)
|
||||
{
|
||||
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
|
||||
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
|
||||
}
|
||||
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
*/
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
this._entities = new Map();
|
||||
if (state.entities)
|
||||
for (const id in state.entities)
|
||||
this._entities.set(+id, new Entity(this, state.entities[id]));
|
||||
// entity collection updated on create/destroy event.
|
||||
this.entities = new EntityCollection(this, this._entities);
|
||||
this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players)
|
||||
this._templates = settings.templates;
|
||||
|
||||
// create the terrain analyzer
|
||||
this.terrainAnalyzer = new TerrainAnalysis();
|
||||
this.terrainAnalyzer.init(this, state);
|
||||
this.accessibility = new Accessibility();
|
||||
this.accessibility.init(state, this.terrainAnalyzer);
|
||||
this._entityMetadata = {};
|
||||
for (const player of this._players)
|
||||
this._entityMetadata[player] = {};
|
||||
|
||||
// Resource types: ignore = not used for resource maps
|
||||
// abundant = abundant resource with small amount each
|
||||
// sparse = sparse resource, but huge amount each
|
||||
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
|
||||
// They are updated by checking for create and destroy events for all resources
|
||||
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
|
||||
this.influenceRadius = { "abundant": 36, "sparse": 48 };
|
||||
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
|
||||
|
||||
this.resources = []; // Contains entityIds of all resources in the maps.
|
||||
this.resourceMaps = {}; // Contains maps showing the density of resources
|
||||
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
|
||||
this.createResourceMaps();
|
||||
|
||||
this.gameState = {};
|
||||
for (const player of this._players)
|
||||
{
|
||||
this.gameState[player] = new GameState();
|
||||
this.gameState[player].init(this, state, player);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* General update of the shared script, before each AI's update
|
||||
* applies entity deltas, and each gamestate.
|
||||
*/
|
||||
SharedScript.prototype.onUpdate = function(state)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
this.init(state, true);
|
||||
this.isDeserialized = false;
|
||||
// array of entity collections
|
||||
this._entityCollections = new Map();
|
||||
this._entitiesModifications = new Map(); // entities modifications
|
||||
this._templatesModifications = {}; // template modifications
|
||||
// each name is a reference to the actual one.
|
||||
this._entityCollectionsName = new Map();
|
||||
this._entityCollectionsByDynProp = {};
|
||||
this._entityCollectionsUID = 0;
|
||||
}
|
||||
|
||||
// deals with updating based on create and destroy messages.
|
||||
this.ApplyEntitiesDelta(state);
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
Engine.ProfileStart("onUpdate");
|
||||
|
||||
// those are dynamic and need to be reset as the "state" object moves in memory.
|
||||
this.events = state.events;
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.barterPrices = state.barterPrices;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
for (const i in this.gameState)
|
||||
this.gameState[i].update(this);
|
||||
|
||||
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
|
||||
this.updateResourceMaps(this.events);
|
||||
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
SharedScript.prototype.ApplyEntitiesDelta = function(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyEntitiesDelta");
|
||||
|
||||
const foundationFinished = {};
|
||||
|
||||
// by order of updating:
|
||||
// we "Destroy" last because we want to be able to switch Metadata first.
|
||||
|
||||
for (const evt of state.events.Create)
|
||||
/** Return a simple object (using no classes etc) that will be serialized into saved games */
|
||||
Serialize()
|
||||
{
|
||||
if (!state.entities[evt.entity])
|
||||
continue; // Sometimes there are things like foundations which get destroyed too fast
|
||||
|
||||
const entity = new Entity(this, state.entities[evt.entity]);
|
||||
this._entities.set(evt.entity, entity);
|
||||
this.entities.addEnt(entity);
|
||||
|
||||
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.updateEnt(entity);
|
||||
return {
|
||||
"players": this._players,
|
||||
"templatesModifications": this._templatesModifications,
|
||||
"entitiesModifications": this._entitiesModifications,
|
||||
"metadata": this._entityMetadata
|
||||
};
|
||||
}
|
||||
|
||||
for (const evt of state.events.EntityRenamed)
|
||||
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
|
||||
/**
|
||||
* Called after the constructor when loading a saved game, with 'data' being
|
||||
* whatever Serialize() returned
|
||||
*/
|
||||
Deserialize(data)
|
||||
{
|
||||
this._players = data.players;
|
||||
this._templatesModifications = data.templatesModifications;
|
||||
this._entitiesModifications = data.entitiesModifications;
|
||||
this._entityMetadata = data.metadata;
|
||||
|
||||
this.isDeserialized = true;
|
||||
}
|
||||
|
||||
GetTemplate(name)
|
||||
{
|
||||
if (this._templates[name] === undefined)
|
||||
this._templates[name] = Engine.GetTemplate(name) || null;
|
||||
|
||||
return this._templates[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the shared component.
|
||||
* We need to know the initial state of the game for this, as we will use it.
|
||||
* This is called right at the end of the map generation.
|
||||
*/
|
||||
init(state, deserialization)
|
||||
{
|
||||
if (!deserialization)
|
||||
this._entitiesModifications = new Map();
|
||||
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.circularMap = state.circularMap;
|
||||
this.mapSize = state.mapSize;
|
||||
this.victoryConditions = new Set(state.victoryConditions);
|
||||
this.alliedVictory = state.alliedVictory;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
if (this.mapSize % this.passabilityMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width);
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
if (this.mapSize % this.territoryMap.width !== 0)
|
||||
error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width);
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
/*
|
||||
let landPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let waterPassMap = new Uint8Array(this.passabilityMap.data.length);
|
||||
let obstructionMaskLand = this.passabilityClasses["default-terrain-only"];
|
||||
let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"];
|
||||
for (let i = 0; i < this.passabilityMap.data.length; ++i)
|
||||
{
|
||||
landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
|
||||
waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
|
||||
}
|
||||
Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255);
|
||||
*/
|
||||
|
||||
this._entities = new Map();
|
||||
if (state.entities)
|
||||
for (const id in state.entities)
|
||||
this._entities.set(+id, new Entity(this, state.entities[id]));
|
||||
// entity collection updated on create/destroy event.
|
||||
this.entities = new EntityCollection(this, this._entities);
|
||||
|
||||
// create the terrain analyzer
|
||||
this.terrainAnalyzer = new TerrainAnalysis(this, state);
|
||||
this.accessibility = new Accessibility(state, this.terrainAnalyzer);
|
||||
|
||||
// Resource types: ignore = not used for resource maps
|
||||
// abundant = abundant resource with small amount each
|
||||
// sparse = sparse resource, but huge amount each
|
||||
// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
|
||||
// They are updated by checking for create and destroy events for all resources
|
||||
this.normalizationFactor = { "abundant": 50, "sparse": 90 };
|
||||
this.influenceRadius = { "abundant": 36, "sparse": 48 };
|
||||
this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 };
|
||||
|
||||
this.resources = []; // Contains entityIds of all resources in the maps.
|
||||
this.resourceMaps = {}; // Contains maps showing the density of resources
|
||||
this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
|
||||
this.createResourceMaps();
|
||||
|
||||
this.gameState = {};
|
||||
for (const player of this._players)
|
||||
{
|
||||
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
|
||||
this._entityMetadata[player][evt.entity] = {};
|
||||
this.gameState[player] = new GameState();
|
||||
this.gameState[player].init(this, state, player);
|
||||
}
|
||||
}
|
||||
|
||||
for (const evt of state.events.TrainingFinished)
|
||||
{ // Apply metadata stored in training queues
|
||||
for (const entId of evt.entities)
|
||||
if (this._entities.has(entId))
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.ConstructionFinished)
|
||||
/**
|
||||
* General update of the shared script, before each AI's update
|
||||
* applies entity deltas, and each gamestate.
|
||||
*/
|
||||
onUpdate(state)
|
||||
{
|
||||
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
|
||||
if (evt.entity != evt.newentity)
|
||||
foundationFinished[evt.entity] = true;
|
||||
}
|
||||
|
||||
for (const evt of state.events.AIMetadata)
|
||||
{
|
||||
if (!this._entities.has(evt.id))
|
||||
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
|
||||
// Apply metadata (here for buildings for example)
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.Destroy)
|
||||
{
|
||||
if (foundationFinished[evt.entity])
|
||||
evt.SuccessfulFoundation = true;
|
||||
|
||||
// The entity was destroyed but its data may still be useful, so
|
||||
// remember the AI's metadata concerning it
|
||||
evt.metadata = {};
|
||||
for (const player of this._players)
|
||||
evt.metadata[player] = this._entityMetadata[player][evt.entity];
|
||||
|
||||
const entity = this._entities.get(evt.entity);
|
||||
if (entity)
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
this.init(state, true);
|
||||
this.isDeserialized = false;
|
||||
}
|
||||
|
||||
// deals with updating based on create and destroy messages.
|
||||
this.ApplyEntitiesDelta(state);
|
||||
this.ApplyTemplatesDelta(state);
|
||||
|
||||
Engine.ProfileStart("onUpdate");
|
||||
|
||||
// those are dynamic and need to be reset as the "state" object moves in memory.
|
||||
this.events = state.events;
|
||||
this.passabilityClasses = state.passabilityClasses;
|
||||
this.playersData = state.players;
|
||||
this.timeElapsed = state.timeElapsed;
|
||||
this.barterPrices = state.barterPrices;
|
||||
this.ceasefireActive = state.ceasefireActive;
|
||||
this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000;
|
||||
|
||||
this.passabilityMap = state.passabilityMap;
|
||||
this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width;
|
||||
this.territoryMap = state.territoryMap;
|
||||
this.territoryMap.cellSize = this.mapSize / this.territoryMap.width;
|
||||
|
||||
for (const i in this.gameState)
|
||||
this.gameState[i].update(this);
|
||||
|
||||
// TODO: merge this with "ApplyEntitiesDelta" since after all they do the same.
|
||||
this.updateResourceMaps(this.events);
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
ApplyEntitiesDelta(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyEntitiesDelta");
|
||||
|
||||
const foundationFinished = {};
|
||||
|
||||
// by order of updating:
|
||||
// we "Destroy" last because we want to be able to switch Metadata first.
|
||||
|
||||
for (const evt of state.events.Create)
|
||||
{
|
||||
if (!state.entities[evt.entity])
|
||||
continue; // Sometimes there are things like foundations which get destroyed too fast
|
||||
|
||||
const entity = new Entity(this, state.entities[evt.entity]);
|
||||
this._entities.set(evt.entity, entity);
|
||||
this.entities.addEnt(entity);
|
||||
|
||||
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.removeEnt(entity);
|
||||
this.entities.removeEnt(entity);
|
||||
entCol.updateEnt(entity);
|
||||
}
|
||||
|
||||
this._entities.delete(evt.entity);
|
||||
this._entitiesModifications.delete(evt.entity);
|
||||
for (const player of this._players)
|
||||
delete this._entityMetadata[player][evt.entity];
|
||||
}
|
||||
|
||||
for (const id in state.entities)
|
||||
{
|
||||
const changes = state.entities[id];
|
||||
const entity = this._entities.get(+id);
|
||||
for (const prop in changes)
|
||||
{
|
||||
entity._entity[prop] = changes[prop];
|
||||
this.updateEntityCollections(prop, entity);
|
||||
for (const evt of state.events.EntityRenamed)
|
||||
{ // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly
|
||||
for (const player of this._players)
|
||||
{
|
||||
this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity];
|
||||
this._entityMetadata[player][evt.entity] = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply per-entity aura-related changes.
|
||||
// this supersedes tech-related changes.
|
||||
for (const id in state.changedEntityTemplateInfo)
|
||||
{
|
||||
if (!this._entities.has(+id))
|
||||
continue; // dead, presumably.
|
||||
const changes = state.changedEntityTemplateInfo[id];
|
||||
if (!this._entitiesModifications.has(+id))
|
||||
this._entitiesModifications.set(+id, new Map());
|
||||
const modif = this._entitiesModifications.get(+id);
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
for (const evt of state.events.TrainingFinished)
|
||||
{ // Apply metadata stored in training queues
|
||||
for (const entId of evt.entities)
|
||||
if (this._entities.has(entId))
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
SharedScript.prototype.ApplyTemplatesDelta = function(state)
|
||||
{
|
||||
Engine.ProfileStart("Shared ApplyTemplatesDelta");
|
||||
|
||||
for (const player in state.changedTemplateInfo)
|
||||
{
|
||||
const playerDiff = state.changedTemplateInfo[player];
|
||||
for (const template in playerDiff)
|
||||
for (const evt of state.events.ConstructionFinished)
|
||||
{
|
||||
const changes = playerDiff[template];
|
||||
if (!this._templatesModifications[template])
|
||||
this._templatesModifications[template] = {};
|
||||
if (!this._templatesModifications[template][player])
|
||||
this._templatesModifications[template][player] = new Map();
|
||||
const modif = this._templatesModifications[template][player];
|
||||
// metada are already moved by EntityRenamed when needed (i.e. construction, not repair)
|
||||
if (evt.entity != evt.newentity)
|
||||
foundationFinished[evt.entity] = true;
|
||||
}
|
||||
|
||||
for (const evt of state.events.AIMetadata)
|
||||
{
|
||||
if (!this._entities.has(evt.id))
|
||||
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
|
||||
// Apply metadata (here for buildings for example)
|
||||
for (const key in evt.metadata)
|
||||
this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]);
|
||||
}
|
||||
|
||||
for (const evt of state.events.Destroy)
|
||||
{
|
||||
if (foundationFinished[evt.entity])
|
||||
evt.SuccessfulFoundation = true;
|
||||
|
||||
// The entity was destroyed but its data may still be useful, so
|
||||
// remember the AI's metadata concerning it
|
||||
evt.metadata = {};
|
||||
for (const player of this._players)
|
||||
evt.metadata[player] = this._entityMetadata[player][evt.entity];
|
||||
|
||||
const entity = this._entities.get(evt.entity);
|
||||
if (entity)
|
||||
{
|
||||
for (const entCol of this._entityCollections.values())
|
||||
entCol.removeEnt(entity);
|
||||
this.entities.removeEnt(entity);
|
||||
}
|
||||
|
||||
this._entities.delete(evt.entity);
|
||||
this._entitiesModifications.delete(evt.entity);
|
||||
for (const player of this._players)
|
||||
delete this._entityMetadata[player][evt.entity];
|
||||
}
|
||||
|
||||
for (const id in state.entities)
|
||||
{
|
||||
const changes = state.entities[id];
|
||||
const entity = this._entities.get(+id);
|
||||
for (const prop in changes)
|
||||
{
|
||||
entity._entity[prop] = changes[prop];
|
||||
this.updateEntityCollections(prop, entity);
|
||||
}
|
||||
}
|
||||
|
||||
// apply per-entity aura-related changes.
|
||||
// this supersedes tech-related changes.
|
||||
for (const id in state.changedEntityTemplateInfo)
|
||||
{
|
||||
if (!this._entities.has(+id))
|
||||
continue; // dead, presumably.
|
||||
const changes = state.changedEntityTemplateInfo[id];
|
||||
if (!this._entitiesModifications.has(+id))
|
||||
this._entitiesModifications.set(+id, new Map());
|
||||
const modif = this._entitiesModifications.get(+id);
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
this._templatesModifications =
|
||||
Object.fromEntries(Object.entries(this._templatesModifications).sort());
|
||||
Engine.ProfileStop();
|
||||
};
|
||||
|
||||
SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection)
|
||||
{
|
||||
entCollection.setUID(this._entityCollectionsUID);
|
||||
this._entityCollections.set(this._entityCollectionsUID, entCollection);
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
ApplyTemplatesDelta(state)
|
||||
{
|
||||
if (!this._entityCollectionsByDynProp[prop])
|
||||
this._entityCollectionsByDynProp[prop] = new Map();
|
||||
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
|
||||
Engine.ProfileStart("Shared ApplyTemplatesDelta");
|
||||
|
||||
for (const player in state.changedTemplateInfo)
|
||||
{
|
||||
const playerDiff = state.changedTemplateInfo[player];
|
||||
for (const template in playerDiff)
|
||||
{
|
||||
const changes = playerDiff[template];
|
||||
if (!this._templatesModifications[template])
|
||||
this._templatesModifications[template] = {};
|
||||
if (!this._templatesModifications[template][player])
|
||||
this._templatesModifications[template][player] = new Map();
|
||||
const modif = this._templatesModifications[template][player];
|
||||
for (const change of changes)
|
||||
modif.set(change.variable, change.value);
|
||||
}
|
||||
}
|
||||
this._templatesModifications =
|
||||
Object.fromEntries(Object.entries(this._templatesModifications).sort());
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
this._entityCollectionsUID++;
|
||||
};
|
||||
|
||||
SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
|
||||
{
|
||||
const uid = entCollection.getUID();
|
||||
|
||||
if (this._entityCollections.has(uid))
|
||||
this._entityCollections.delete(uid);
|
||||
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
if (this._entityCollectionsByDynProp[prop].has(uid))
|
||||
this._entityCollectionsByDynProp[prop].delete(uid);
|
||||
};
|
||||
|
||||
SharedScript.prototype.updateEntityCollections = function(property, ent)
|
||||
{
|
||||
if (this._entityCollectionsByDynProp[property] === undefined)
|
||||
return;
|
||||
|
||||
for (const entCol of this._entityCollectionsByDynProp[property].values())
|
||||
entCol.updateEnt(ent);
|
||||
};
|
||||
|
||||
SharedScript.prototype.setMetadata = function(player, ent, key, value)
|
||||
{
|
||||
let metadata = this._entityMetadata[player][ent.id()];
|
||||
if (!metadata)
|
||||
registerUpdatingEntityCollection(entCollection)
|
||||
{
|
||||
this._entityMetadata[player][ent.id()] = {};
|
||||
metadata = this._entityMetadata[player][ent.id()];
|
||||
entCollection.setUID(this._entityCollectionsUID);
|
||||
this._entityCollections.set(this._entityCollectionsUID, entCollection);
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
{
|
||||
if (!this._entityCollectionsByDynProp[prop])
|
||||
this._entityCollectionsByDynProp[prop] = new Map();
|
||||
this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection);
|
||||
}
|
||||
this._entityCollectionsUID++;
|
||||
}
|
||||
metadata[key] = value;
|
||||
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
};
|
||||
removeUpdatingEntityCollection(entCollection)
|
||||
{
|
||||
const uid = entCollection.getUID();
|
||||
|
||||
SharedScript.prototype.getMetadata = function(player, ent, key)
|
||||
{
|
||||
return this._entityMetadata[player][ent.id()]?.[key];
|
||||
};
|
||||
if (this._entityCollections.has(uid))
|
||||
this._entityCollections.delete(uid);
|
||||
|
||||
SharedScript.prototype.deleteMetadata = function(player, ent, key)
|
||||
{
|
||||
const metadata = this._entityMetadata[player][ent.id()];
|
||||
for (const prop of entCollection.dynamicProperties())
|
||||
if (this._entityCollectionsByDynProp[prop].has(uid))
|
||||
this._entityCollectionsByDynProp[prop].delete(uid);
|
||||
}
|
||||
|
||||
if (!metadata || !(key in metadata))
|
||||
updateEntityCollections(property, ent)
|
||||
{
|
||||
if (this._entityCollectionsByDynProp[property] === undefined)
|
||||
return;
|
||||
|
||||
for (const entCol of this._entityCollectionsByDynProp[property].values())
|
||||
entCol.updateEnt(ent);
|
||||
}
|
||||
|
||||
setMetadata(player, ent, key, value)
|
||||
{
|
||||
let metadata = this._entityMetadata[player][ent.id()];
|
||||
if (!metadata)
|
||||
{
|
||||
this._entityMetadata[player][ent.id()] = {};
|
||||
metadata = this._entityMetadata[player][ent.id()];
|
||||
}
|
||||
metadata[key] = value;
|
||||
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
}
|
||||
|
||||
getMetadata(player, ent, key)
|
||||
{
|
||||
return this._entityMetadata[player][ent.id()]?.[key];
|
||||
}
|
||||
|
||||
deleteMetadata(player, ent, key)
|
||||
{
|
||||
const metadata = this._entityMetadata[player][ent.id()];
|
||||
|
||||
if (!metadata || !(key in metadata))
|
||||
return true;
|
||||
metadata[key] = undefined;
|
||||
delete metadata[key];
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
return true;
|
||||
metadata[key] = undefined;
|
||||
delete metadata[key];
|
||||
this.updateEntityCollections('metadata', ent);
|
||||
this.updateEntityCollections('metadata.' + key, ent);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function copyPrototype(descendant, parent)
|
||||
{
|
||||
const sConstructor = parent.toString();
|
||||
const aMatch = sConstructor.match(/\s*function (.*)\(/);
|
||||
/** creates a map of resource density */
|
||||
createResourceMaps()
|
||||
{
|
||||
for (const resource of Resources.GetCodes())
|
||||
{
|
||||
if (this.resourceMaps[resource] ||
|
||||
!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
|
||||
continue;
|
||||
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
|
||||
// But at that point the precision is not really important anyway. And it saves memory.
|
||||
this.resourceMaps[resource] = new InfoMap(this, "resource");
|
||||
this.ccResourceMaps[resource] = new InfoMap(this, "resource");
|
||||
}
|
||||
for (const ent of this._entities.values())
|
||||
this.addEntityToResourceMap(ent);
|
||||
}
|
||||
|
||||
if (aMatch != null)
|
||||
descendant.prototype[aMatch[1]] = parent;
|
||||
/**
|
||||
* @param {Object} events - The events from a turn.
|
||||
*/
|
||||
updateResourceMaps(events)
|
||||
{
|
||||
if (events.Destroy.some(e => this.resources.includes(e.entity)))
|
||||
{
|
||||
this.resources = [];
|
||||
this.resourceMaps = {};
|
||||
this.createResourceMaps();
|
||||
}
|
||||
|
||||
for (const p in parent.prototype)
|
||||
descendant.prototype[p] = parent.prototype[p];
|
||||
for (const e of events.Create)
|
||||
if (e.entity && this._entities.has(e.entity))
|
||||
this.addEntityToResourceMap(this._entities.get(e.entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to add to the resource map.
|
||||
*/
|
||||
addEntityToResourceMap(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, 1);
|
||||
this.resources.push(entity.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to remove from the resource map.
|
||||
*/
|
||||
removeEntityFromResourceMap(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {entity} ent - The entity to add to the resource map.
|
||||
*/
|
||||
changeEntityInResourceMapHelper(ent, multiplication = 1)
|
||||
{
|
||||
if (!ent)
|
||||
return;
|
||||
const entPos = ent.position();
|
||||
if (!entPos)
|
||||
return;
|
||||
const resource = ent.resourceSupplyType()?.generic;
|
||||
if (!resource || !this.resourceMaps[resource])
|
||||
return;
|
||||
const cellSize = this.resourceMaps[resource].cellSize;
|
||||
const x = Math.floor(entPos[0] / cellSize);
|
||||
const y = Math.floor(entPos[1] / cellSize);
|
||||
const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
|
||||
const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp];
|
||||
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2, "constant");
|
||||
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2);
|
||||
this.ccResourceMaps[resource].addInfluence(x, y, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
|
||||
}
|
||||
}
|
||||
|
||||
/** creates a map of resource density */
|
||||
SharedScript.prototype.createResourceMaps = function()
|
||||
{
|
||||
for (const resource of Resources.GetCodes())
|
||||
{
|
||||
if (this.resourceMaps[resource] ||
|
||||
!(Resources.GetResource(resource).aiAnalysisInfluenceGroup in this.normalizationFactor))
|
||||
continue;
|
||||
// We're creating them 8-bit. Things could go above 255 if there are really tons of resources
|
||||
// But at that point the precision is not really important anyway. And it saves memory.
|
||||
this.resourceMaps[resource] = new InfoMap(this, "resource");
|
||||
this.ccResourceMaps[resource] = new InfoMap(this, "resource");
|
||||
}
|
||||
for (const ent of this._entities.values())
|
||||
this.addEntityToResourceMap(ent);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} events - The events from a turn.
|
||||
*/
|
||||
SharedScript.prototype.updateResourceMaps = function(events)
|
||||
{
|
||||
if (events.Destroy.some(e => this.resources.includes(e.entity)))
|
||||
{
|
||||
this.resources = [];
|
||||
this.resourceMaps = {};
|
||||
this.createResourceMaps();
|
||||
}
|
||||
|
||||
for (const e of events.Create)
|
||||
if (e.entity && this._entities.has(e.entity))
|
||||
this.addEntityToResourceMap(this._entities.get(e.entity));
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to add to the resource map.
|
||||
*/
|
||||
SharedScript.prototype.addEntityToResourceMap = function(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, 1);
|
||||
this.resources.push(entity.id());
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {entity} entity - The entity to remove from the resource map.
|
||||
*/
|
||||
SharedScript.prototype.removeEntityFromResourceMap = function(entity)
|
||||
{
|
||||
this.changeEntityInResourceMapHelper(entity, -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {entity} ent - The entity to add to the resource map.
|
||||
*/
|
||||
SharedScript.prototype.changeEntityInResourceMapHelper = function(ent, multiplication = 1)
|
||||
{
|
||||
if (!ent)
|
||||
return;
|
||||
const entPos = ent.position();
|
||||
if (!entPos)
|
||||
return;
|
||||
const resource = ent.resourceSupplyType()?.generic;
|
||||
if (!resource || !this.resourceMaps[resource])
|
||||
return;
|
||||
const cellSize = this.resourceMaps[resource].cellSize;
|
||||
const x = Math.floor(entPos[0] / cellSize);
|
||||
const y = Math.floor(entPos[1] / cellSize);
|
||||
const grp = Resources.GetResource(resource).aiAnalysisInfluenceGroup;
|
||||
const strength = multiplication * ent.resourceSupplyMax() / this.normalizationFactor[grp];
|
||||
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2, "constant");
|
||||
this.resourceMaps[resource].addInfluence(x, y, this.influenceRadius[grp] / cellSize, strength / 2);
|
||||
this.ccResourceMaps[resource].addInfluence(x, y, this.ccInfluenceRadius[grp] / cellSize, strength, "constant");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,124 +1,127 @@
|
|||
LoadModificationTemplates();
|
||||
|
||||
/** Wrapper around a technology template */
|
||||
export function Technology(templateName)
|
||||
export class Technology
|
||||
{
|
||||
this._templateName = templateName;
|
||||
const template = TechnologyTemplates.Get(templateName);
|
||||
|
||||
// check if this is one of two paired technologies.
|
||||
if (template.partOfPair)
|
||||
constructor(templateName)
|
||||
{
|
||||
const parentTech = TechnologyTemplates.Get(template.partOfPair);
|
||||
this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0];
|
||||
this._templateName = templateName;
|
||||
const template = TechnologyTemplates.Get(templateName);
|
||||
|
||||
// check if this is one of two paired technologies.
|
||||
if (template.partOfPair)
|
||||
{
|
||||
const parentTech = TechnologyTemplates.Get(template.partOfPair);
|
||||
this._pairedWith = parentTech.pair[0] == templateName ? parentTech.pair[1] : parentTech.pair[0];
|
||||
}
|
||||
|
||||
// check if it only defines a pair:
|
||||
this._definesPair = !!template.pair;
|
||||
this._template = template;
|
||||
}
|
||||
|
||||
// check if it only defines a pair:
|
||||
this._definesPair = !!template.pair;
|
||||
this._template = template;
|
||||
/** returns generic, or specific if civ provided. */
|
||||
name(civ)
|
||||
{
|
||||
if (civ === undefined)
|
||||
return this._template.genericName;
|
||||
|
||||
if (this._template.specificName === undefined || this._template.specificName[civ] === undefined)
|
||||
return undefined;
|
||||
return this._template.specificName[civ];
|
||||
}
|
||||
|
||||
pairDef()
|
||||
{
|
||||
return this._definesPair;
|
||||
}
|
||||
|
||||
/** in case this defines a pair only, returns the two paired technologies. */
|
||||
getPairedTechs()
|
||||
{
|
||||
if (!this._definesPair)
|
||||
return undefined;
|
||||
|
||||
return this._template.pair.map(name => new Technology(name));
|
||||
}
|
||||
|
||||
pair()
|
||||
{
|
||||
return this._template.partOfPair;
|
||||
}
|
||||
|
||||
pairedWith()
|
||||
{
|
||||
if (!this._template.partOfPair)
|
||||
return undefined;
|
||||
return this._pairedWith;
|
||||
}
|
||||
|
||||
cost(researcher)
|
||||
{
|
||||
if (!this._template.cost)
|
||||
return undefined;
|
||||
const cost = {};
|
||||
for (const type in this._template.cost)
|
||||
{
|
||||
cost[type] = +this._template.cost[type];
|
||||
if (researcher)
|
||||
cost[type] *= researcher.techCostMultiplier(type);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
costSum(researcher)
|
||||
{
|
||||
const cost = this.cost(researcher);
|
||||
if (!cost)
|
||||
return 0;
|
||||
let ret = 0;
|
||||
for (const type in cost)
|
||||
ret += cost[type];
|
||||
return ret;
|
||||
}
|
||||
|
||||
researchTime()
|
||||
{
|
||||
return this._template.researchTime || 0;
|
||||
}
|
||||
|
||||
requirements(civ)
|
||||
{
|
||||
return DeriveTechnologyRequirements(this._template, civ);
|
||||
}
|
||||
|
||||
autoResearch()
|
||||
{
|
||||
if (!this._template.autoResearch)
|
||||
return undefined;
|
||||
return this._template.autoResearch;
|
||||
}
|
||||
|
||||
supersedes()
|
||||
{
|
||||
if (!this._template.supersedes)
|
||||
return undefined;
|
||||
return this._template.supersedes;
|
||||
}
|
||||
|
||||
modifications()
|
||||
{
|
||||
if (!this._template.modifications)
|
||||
return undefined;
|
||||
return this._template.modifications;
|
||||
}
|
||||
|
||||
affects()
|
||||
{
|
||||
if (!this._template.affects)
|
||||
return undefined;
|
||||
return this._template.affects;
|
||||
}
|
||||
|
||||
isAffected(classes)
|
||||
{
|
||||
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
|
||||
}
|
||||
}
|
||||
|
||||
/** returns generic, or specific if civ provided. */
|
||||
Technology.prototype.name = function(civ)
|
||||
{
|
||||
if (civ === undefined)
|
||||
return this._template.genericName;
|
||||
|
||||
if (this._template.specificName === undefined || this._template.specificName[civ] === undefined)
|
||||
return undefined;
|
||||
return this._template.specificName[civ];
|
||||
};
|
||||
|
||||
Technology.prototype.pairDef = function()
|
||||
{
|
||||
return this._definesPair;
|
||||
};
|
||||
|
||||
/** in case this defines a pair only, returns the two paired technologies. */
|
||||
Technology.prototype.getPairedTechs = function()
|
||||
{
|
||||
if (!this._definesPair)
|
||||
return undefined;
|
||||
|
||||
return this._template.pair.map(name => new Technology(name));
|
||||
};
|
||||
|
||||
Technology.prototype.pair = function()
|
||||
{
|
||||
return this._template.partOfPair;
|
||||
};
|
||||
|
||||
Technology.prototype.pairedWith = function()
|
||||
{
|
||||
if (!this._template.partOfPair)
|
||||
return undefined;
|
||||
return this._pairedWith;
|
||||
};
|
||||
|
||||
Technology.prototype.cost = function(researcher)
|
||||
{
|
||||
if (!this._template.cost)
|
||||
return undefined;
|
||||
const cost = {};
|
||||
for (const type in this._template.cost)
|
||||
{
|
||||
cost[type] = +this._template.cost[type];
|
||||
if (researcher)
|
||||
cost[type] *= researcher.techCostMultiplier(type);
|
||||
}
|
||||
return cost;
|
||||
};
|
||||
|
||||
Technology.prototype.costSum = function(researcher)
|
||||
{
|
||||
const cost = this.cost(researcher);
|
||||
if (!cost)
|
||||
return 0;
|
||||
let ret = 0;
|
||||
for (const type in cost)
|
||||
ret += cost[type];
|
||||
return ret;
|
||||
};
|
||||
|
||||
Technology.prototype.researchTime = function()
|
||||
{
|
||||
return this._template.researchTime || 0;
|
||||
};
|
||||
|
||||
Technology.prototype.requirements = function(civ)
|
||||
{
|
||||
return DeriveTechnologyRequirements(this._template, civ);
|
||||
};
|
||||
|
||||
Technology.prototype.autoResearch = function()
|
||||
{
|
||||
if (!this._template.autoResearch)
|
||||
return undefined;
|
||||
return this._template.autoResearch;
|
||||
};
|
||||
|
||||
Technology.prototype.supersedes = function()
|
||||
{
|
||||
if (!this._template.supersedes)
|
||||
return undefined;
|
||||
return this._template.supersedes;
|
||||
};
|
||||
|
||||
Technology.prototype.modifications = function()
|
||||
{
|
||||
if (!this._template.modifications)
|
||||
return undefined;
|
||||
return this._template.modifications;
|
||||
};
|
||||
|
||||
Technology.prototype.affects = function()
|
||||
{
|
||||
if (!this._template.affects)
|
||||
return undefined;
|
||||
return this._template.affects;
|
||||
};
|
||||
|
||||
Technology.prototype.isAffected = function(classes)
|
||||
{
|
||||
return this._template.affects && this._template.affects.some(affect => MatchesClassList(classes, affect));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { InfoMap } from "simulation/ai/common-api/map-module.js";
|
||||
import { copyPrototype } from "simulation/ai/common-api/shared.js";
|
||||
import * as terrainStates from "simulation/ai/common-api/terrain-states.js";
|
||||
|
||||
/**
|
||||
* TerrainAnalysis, inheriting from the Map Component.
|
||||
|
|
@ -11,51 +11,41 @@ import { copyPrototype } from "simulation/ai/common-api/shared.js";
|
|||
* This is intended for use with 8 bit maps for reduced memory usage.
|
||||
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
|
||||
*/
|
||||
export function TerrainAnalysis()
|
||||
export class TerrainAnalysis extends InfoMap
|
||||
{
|
||||
}
|
||||
|
||||
copyPrototype(TerrainAnalysis, InfoMap);
|
||||
|
||||
TerrainAnalysis.prototype.IMPASSABLE = 0;
|
||||
/**
|
||||
* non-passable by land units
|
||||
*/
|
||||
TerrainAnalysis.prototype.DEEP_WATER = 200;
|
||||
/**
|
||||
* passable by land units and water units
|
||||
*/
|
||||
TerrainAnalysis.prototype.SHALLOW_WATER = 201;
|
||||
/**
|
||||
* passable by land units
|
||||
*/
|
||||
TerrainAnalysis.prototype.LAND = 255;
|
||||
|
||||
TerrainAnalysis.prototype.init = function(sharedScript, rawState)
|
||||
{
|
||||
const passabilityMap = rawState.passabilityMap;
|
||||
this.width = passabilityMap.width;
|
||||
this.height = passabilityMap.height;
|
||||
this.cellSize = passabilityMap.cellSize;
|
||||
|
||||
const obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
|
||||
const obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
|
||||
|
||||
const obstructionTiles = new Uint8Array(passabilityMap.data.length);
|
||||
|
||||
|
||||
for (let i = 0; i < passabilityMap.data.length; ++i)
|
||||
constructor(sharedScript, rawState)
|
||||
{
|
||||
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ? this.IMPASSABLE : this.LAND;
|
||||
const passabilityMap = rawState.passabilityMap;
|
||||
|
||||
if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === this.IMPASSABLE)
|
||||
obstructionTiles[i] = this.DEEP_WATER;
|
||||
else if (!(passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === this.LAND)
|
||||
obstructionTiles[i] = this.SHALLOW_WATER;
|
||||
const obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
|
||||
const obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
|
||||
|
||||
const obstructionTiles = new Uint8Array(passabilityMap.data.length);
|
||||
|
||||
|
||||
for (let i = 0; i < passabilityMap.data.length; ++i)
|
||||
{
|
||||
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ?
|
||||
terrainStates.IMPASSABLE : terrainStates.LAND;
|
||||
|
||||
if (!(passabilityMap.data[i] & obstructionMaskWater) &&
|
||||
obstructionTiles[i] === terrainStates.IMPASSABLE)
|
||||
{
|
||||
obstructionTiles[i] = terrainStates.DEEP_WATER;
|
||||
}
|
||||
else if (!(passabilityMap.data[i] & obstructionMaskWater) &&
|
||||
obstructionTiles[i] === terrainStates.LAND)
|
||||
{
|
||||
obstructionTiles[i] = terrainStates.SHALLOW_WATER;
|
||||
}
|
||||
}
|
||||
|
||||
super(rawState, "passability", obstructionTiles);
|
||||
this.width = passabilityMap.width;
|
||||
this.height = passabilityMap.height;
|
||||
this.cellSize = passabilityMap.cellSize;
|
||||
}
|
||||
|
||||
this.InfoMap(rawState, "passability", obstructionTiles);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessibility inherits from TerrainAnalysis
|
||||
|
|
@ -65,321 +55,338 @@ TerrainAnalysis.prototype.init = function(sharedScript, rawState)
|
|||
* for optimizations it's called after the TerrainAnalyser has finished initializing his map
|
||||
* so this can use the land regions already.
|
||||
*/
|
||||
export function Accessibility()
|
||||
export class Accessibility extends InfoMap
|
||||
{
|
||||
}
|
||||
|
||||
copyPrototype(Accessibility, TerrainAnalysis);
|
||||
|
||||
Accessibility.prototype.init = function(rawState, terrainAnalyser)
|
||||
{
|
||||
this.InfoMap(rawState, "passability", terrainAnalyser.map);
|
||||
this.landPassMap = new Uint16Array(terrainAnalyser.length);
|
||||
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
|
||||
|
||||
this.maxRegions = 65535;
|
||||
this.regionSize = [];
|
||||
this.regionType = []; // "inaccessible", "land" or "water";
|
||||
// ID of the region associated with an array of region IDs.
|
||||
this.regionLinks = [];
|
||||
|
||||
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
|
||||
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
|
||||
// So start at 2.
|
||||
this.regionID = 2;
|
||||
|
||||
for (let i = 0; i < this.landPassMap.length; ++i)
|
||||
constructor(rawState, terrainAnalyser)
|
||||
{
|
||||
if (this.map[i] !== this.IMPASSABLE)
|
||||
{ // any non-painted, non-inacessible area.
|
||||
if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
|
||||
this.regionType[this.regionID++] = "land";
|
||||
if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
|
||||
this.regionType[this.regionID++] = "water";
|
||||
}
|
||||
else if (this.landPassMap[i] == 0)
|
||||
{ // any non-painted, inacessible area.
|
||||
this.floodFill(i, 1, false);
|
||||
this.floodFill(i, 1, true);
|
||||
}
|
||||
}
|
||||
super(rawState, "passability", terrainAnalyser.map);
|
||||
this.landPassMap = new Uint16Array(terrainAnalyser.length);
|
||||
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
|
||||
this.maxRegions = 65535;
|
||||
this.regionSize = [];
|
||||
this.regionType = []; // "inaccessible", "land" or "water";
|
||||
// ID of the region associated with an array of region IDs.
|
||||
this.regionLinks = [];
|
||||
|
||||
// calculating region links. Regions only touching diagonaly are not linked.
|
||||
// since we're checking all of them, we'll check from the top left to the bottom right
|
||||
const w = this.width;
|
||||
for (let x = 0; x < this.width-1; ++x)
|
||||
{
|
||||
for (let y = 0; y < this.height-1; ++y)
|
||||
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
|
||||
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
|
||||
// So start at 2.
|
||||
this.regionID = 2;
|
||||
|
||||
for (let i = 0; i < this.landPassMap.length; ++i)
|
||||
{
|
||||
// checking right.
|
||||
const thisLID = this.landPassMap[x+y*w];
|
||||
const thisNID = this.navalPassMap[x+y*w];
|
||||
const rightLID = this.landPassMap[x+1+y*w];
|
||||
const rightNID = this.navalPassMap[x+1+y*w];
|
||||
const bottomLID = this.landPassMap[x+y*w+w];
|
||||
const bottomNID = this.navalPassMap[x+y*w+w];
|
||||
if (thisLID > 1)
|
||||
{
|
||||
if (rightNID > 1)
|
||||
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
|
||||
this.regionLinks[thisLID].push(rightNID);
|
||||
if (bottomNID > 1)
|
||||
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
|
||||
this.regionLinks[thisLID].push(bottomNID);
|
||||
if (this.map[i] !== terrainStates.IMPASSABLE)
|
||||
{ // any non-painted, non-inacessible area.
|
||||
if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
|
||||
this.regionType[this.regionID++] = "land";
|
||||
if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
|
||||
this.regionType[this.regionID++] = "water";
|
||||
}
|
||||
if (thisNID > 1)
|
||||
else if (this.landPassMap[i] == 0)
|
||||
{ // any non-painted, inacessible area.
|
||||
this.floodFill(i, 1, false);
|
||||
this.floodFill(i, 1, true);
|
||||
}
|
||||
}
|
||||
|
||||
// calculating region links. Regions only touching diagonaly are not linked.
|
||||
// since we're checking all of them, we'll check from the top left to the bottom right
|
||||
const w = this.width;
|
||||
for (let x = 0; x < this.width-1; ++x)
|
||||
{
|
||||
for (let y = 0; y < this.height-1; ++y)
|
||||
{
|
||||
if (rightLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
|
||||
this.regionLinks[thisNID].push(rightLID);
|
||||
if (bottomLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
|
||||
this.regionLinks[thisNID].push(bottomLID);
|
||||
// checking right.
|
||||
const thisLID = this.landPassMap[x+y*w];
|
||||
const thisNID = this.navalPassMap[x+y*w];
|
||||
const rightLID = this.landPassMap[x+1+y*w];
|
||||
const rightNID = this.navalPassMap[x+1+y*w];
|
||||
const bottomLID = this.landPassMap[x+y*w+w];
|
||||
const bottomNID = this.navalPassMap[x+y*w+w];
|
||||
if (thisLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
|
||||
this.regionLinks[thisNID].push(thisLID);
|
||||
{
|
||||
if (rightNID > 1)
|
||||
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
|
||||
this.regionLinks[thisLID].push(rightNID);
|
||||
if (bottomNID > 1)
|
||||
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
|
||||
this.regionLinks[thisLID].push(bottomNID);
|
||||
}
|
||||
if (thisNID > 1)
|
||||
{
|
||||
if (rightLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
|
||||
this.regionLinks[thisNID].push(rightLID);
|
||||
if (bottomLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
|
||||
this.regionLinks[thisNID].push(bottomLID);
|
||||
if (thisLID > 1)
|
||||
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
|
||||
this.regionLinks[thisNID].push(thisLID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
|
||||
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
|
||||
}
|
||||
|
||||
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
|
||||
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
|
||||
};
|
||||
|
||||
Accessibility.prototype.getAccessValue = function(position, onWater)
|
||||
{
|
||||
const gamePos = this.gamePosToMapPos(position);
|
||||
if (onWater)
|
||||
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
|
||||
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
|
||||
if (ret === 1)
|
||||
getAccessValue(position, onWater)
|
||||
{
|
||||
// quick spiral search.
|
||||
const indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
|
||||
for (const i of indx)
|
||||
const gamePos = this.gamePosToMapPos(position);
|
||||
if (onWater)
|
||||
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
|
||||
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
|
||||
if (ret === 1)
|
||||
{
|
||||
const id0 = gamePos[0] + i[0];
|
||||
const id1 = gamePos[1] + i[1];
|
||||
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
|
||||
continue;
|
||||
ret = this.landPassMap[id0 + this.width*id1];
|
||||
if (ret !== 1)
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
Accessibility.prototype.getTrajectTo = function(start, end)
|
||||
{
|
||||
const pstart = this.gamePosToMapPos(start);
|
||||
const istart = pstart[0] + pstart[1]*this.width;
|
||||
const pend = this.gamePosToMapPos(end);
|
||||
const iend = pend[0] + pend[1]*this.width;
|
||||
|
||||
let onLand = true;
|
||||
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
|
||||
onLand = false;
|
||||
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
|
||||
return false;
|
||||
|
||||
let endRegion = this.landPassMap[iend];
|
||||
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
|
||||
endRegion = this.navalPassMap[iend];
|
||||
else if (endRegion <= 1)
|
||||
return false;
|
||||
|
||||
const startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
|
||||
return this.getTrajectToIndex(startRegion, endRegion);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
|
||||
* this can tell you what sea zone you need to have a dock on, for example.
|
||||
* assumes a land unit unless start point is over deep water.
|
||||
*/
|
||||
Accessibility.prototype.getTrajectToIndex = function(istart, iend)
|
||||
{
|
||||
if (istart === iend)
|
||||
return [istart];
|
||||
|
||||
const trajects = new Set();
|
||||
const explored = new Set();
|
||||
trajects.add([istart]);
|
||||
explored.add(istart);
|
||||
while (trajects.size)
|
||||
{
|
||||
for (const traj of trajects)
|
||||
{
|
||||
const ilast = traj[traj.length-1];
|
||||
for (const inew of this.regionLinks[ilast])
|
||||
// quick spiral search.
|
||||
const indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
|
||||
for (const i of indx)
|
||||
{
|
||||
if (inew === iend)
|
||||
return traj.concat(iend);
|
||||
if (explored.has(inew))
|
||||
const id0 = gamePos[0] + i[0];
|
||||
const id1 = gamePos[1] + i[1];
|
||||
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
|
||||
continue;
|
||||
trajects.add(traj.concat(inew));
|
||||
explored.add(inew);
|
||||
ret = this.landPassMap[id0 + this.width*id1];
|
||||
if (ret !== 1)
|
||||
return ret;
|
||||
}
|
||||
trajects.delete(traj);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
Accessibility.prototype.getRegionSize = function(position, onWater)
|
||||
{
|
||||
const pos = this.gamePosToMapPos(position);
|
||||
const index = pos[0] + pos[1]*this.width;
|
||||
const ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
|
||||
if (this.regionSize[ID] === undefined)
|
||||
return 0;
|
||||
return this.regionSize[ID];
|
||||
};
|
||||
|
||||
Accessibility.prototype.getRegionSizei = function(index, onWater)
|
||||
{
|
||||
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
|
||||
return 0;
|
||||
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
|
||||
return this.regionSize[this.navalPassMap[index]];
|
||||
return this.regionSize[this.landPassMap[index]];
|
||||
};
|
||||
|
||||
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
|
||||
Accessibility.prototype.floodFill = function(startIndex, value, onWater)
|
||||
{
|
||||
if (value > this.maxRegions)
|
||||
{
|
||||
error("AI accessibility map: too many regions.");
|
||||
this.landPassMap[startIndex] = 1;
|
||||
this.navalPassMap[startIndex] = 1;
|
||||
return false;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
|
||||
return false; // already painted.
|
||||
|
||||
let floodFor = "land";
|
||||
if (this.map[startIndex] === this.IMPASSABLE)
|
||||
getTrajectTo(start, end)
|
||||
{
|
||||
this.landPassMap[startIndex] = 1;
|
||||
this.navalPassMap[startIndex] = 1;
|
||||
return false;
|
||||
const pstart = this.gamePosToMapPos(start);
|
||||
const istart = pstart[0] + pstart[1]*this.width;
|
||||
const pend = this.gamePosToMapPos(end);
|
||||
const iend = pend[0] + pend[1]*this.width;
|
||||
|
||||
let onLand = true;
|
||||
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
|
||||
onLand = false;
|
||||
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
|
||||
return false;
|
||||
|
||||
let endRegion = this.landPassMap[iend];
|
||||
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
|
||||
endRegion = this.navalPassMap[iend];
|
||||
else if (endRegion <= 1)
|
||||
return false;
|
||||
|
||||
const startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
|
||||
return this.getTrajectToIndex(startRegion, endRegion);
|
||||
}
|
||||
|
||||
if (onWater === true)
|
||||
/**
|
||||
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
|
||||
* this can tell you what sea zone you need to have a dock on, for example.
|
||||
* assumes a land unit unless start point is over deep water.
|
||||
*/
|
||||
getTrajectToIndex(istart, iend)
|
||||
{
|
||||
if (this.map[startIndex] !== this.DEEP_WATER && this.map[startIndex] !== this.SHALLOW_WATER)
|
||||
if (istart === iend)
|
||||
return [istart];
|
||||
|
||||
const trajects = new Set();
|
||||
const explored = new Set();
|
||||
trajects.add([istart]);
|
||||
explored.add(istart);
|
||||
while (trajects.size)
|
||||
{
|
||||
this.navalPassMap[startIndex] = 1; // impassable for naval
|
||||
return false; // do nothing
|
||||
for (const traj of trajects)
|
||||
{
|
||||
const ilast = traj[traj.length-1];
|
||||
for (const inew of this.regionLinks[ilast])
|
||||
{
|
||||
if (inew === iend)
|
||||
return traj.concat(iend);
|
||||
if (explored.has(inew))
|
||||
continue;
|
||||
trajects.add(traj.concat(inew));
|
||||
explored.add(inew);
|
||||
}
|
||||
trajects.delete(traj);
|
||||
}
|
||||
}
|
||||
floodFor = "water";
|
||||
}
|
||||
else if (this.map[startIndex] === this.DEEP_WATER)
|
||||
{
|
||||
this.landPassMap[startIndex] = 1; // impassable for land
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// here we'll be able to start.
|
||||
for (let i = this.regionSize.length; i <= value; ++i)
|
||||
getRegionSize(position, onWater)
|
||||
{
|
||||
this.regionLinks.push([]);
|
||||
this.regionSize.push(0);
|
||||
this.regionType.push("inaccessible");
|
||||
const pos = this.gamePosToMapPos(position);
|
||||
const index = pos[0] + pos[1]*this.width;
|
||||
const ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
|
||||
if (this.regionSize[ID] === undefined)
|
||||
return 0;
|
||||
return this.regionSize[ID];
|
||||
}
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
|
||||
// Get x and y from index
|
||||
const IndexArray = [startIndex];
|
||||
let newIndex;
|
||||
while (IndexArray.length)
|
||||
getRegionSizei(index, onWater)
|
||||
{
|
||||
newIndex = IndexArray.pop();
|
||||
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
|
||||
return 0;
|
||||
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
|
||||
return this.regionSize[this.navalPassMap[index]];
|
||||
return this.regionSize[this.landPassMap[index]];
|
||||
}
|
||||
|
||||
let y = 0;
|
||||
// vertical iteration
|
||||
while (true)
|
||||
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
|
||||
floodFill(startIndex, value, onWater)
|
||||
{
|
||||
if (value > this.maxRegions)
|
||||
{
|
||||
--y;
|
||||
const index = newIndex + w*y;
|
||||
if (index < 0)
|
||||
break;
|
||||
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== this.IMPASSABLE && this.map[index] !== this.DEEP_WATER)
|
||||
continue;
|
||||
if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === this.DEEP_WATER || this.map[index] === this.SHALLOW_WATER))
|
||||
continue;
|
||||
break;
|
||||
} // should actually break
|
||||
++y;
|
||||
let reachLeft = false;
|
||||
let reachRight = false;
|
||||
let index;
|
||||
do
|
||||
error("AI accessibility map: too many regions.");
|
||||
this.landPassMap[startIndex] = 1;
|
||||
this.navalPassMap[startIndex] = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
|
||||
return false; // already painted.
|
||||
|
||||
let floodFor = "land";
|
||||
if (this.map[startIndex] === terrainStates.IMPASSABLE)
|
||||
{
|
||||
index = newIndex + w*y;
|
||||
this.landPassMap[startIndex] = 1;
|
||||
this.navalPassMap[startIndex] = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (floodFor === "land" && this.landPassMap[index] === 0 && this.map[index] !== this.IMPASSABLE && this.map[index] !== this.DEEP_WATER)
|
||||
if (onWater === true)
|
||||
{
|
||||
if (this.map[startIndex] !== terrainStates.DEEP_WATER &&
|
||||
this.map[startIndex] !== terrainStates.SHALLOW_WATER)
|
||||
{
|
||||
this.landPassMap[index] = value;
|
||||
this.regionSize[value]++;
|
||||
this.navalPassMap[startIndex] = 1; // impassable for naval
|
||||
return false; // do nothing
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index] === 0 && (this.map[index] === this.DEEP_WATER || this.map[index] === this.SHALLOW_WATER))
|
||||
floodFor = "water";
|
||||
}
|
||||
else if (this.map[startIndex] === terrainStates.DEEP_WATER)
|
||||
{
|
||||
this.landPassMap[startIndex] = 1; // impassable for land
|
||||
return false;
|
||||
}
|
||||
|
||||
// here we'll be able to start.
|
||||
for (let i = this.regionSize.length; i <= value; ++i)
|
||||
{
|
||||
this.regionLinks.push([]);
|
||||
this.regionSize.push(0);
|
||||
this.regionType.push("inaccessible");
|
||||
}
|
||||
const w = this.width;
|
||||
const h = this.height;
|
||||
|
||||
// Get x and y from index
|
||||
const IndexArray = [startIndex];
|
||||
let newIndex;
|
||||
while (IndexArray.length)
|
||||
{
|
||||
newIndex = IndexArray.pop();
|
||||
|
||||
let y = 0;
|
||||
// vertical iteration
|
||||
while (true)
|
||||
{
|
||||
this.navalPassMap[index] = value;
|
||||
this.regionSize[value]++;
|
||||
}
|
||||
else
|
||||
--y;
|
||||
const index = newIndex + w*y;
|
||||
if (index < 0)
|
||||
break;
|
||||
if (floodFor === "land" && this.landPassMap[index] === 0 &&
|
||||
this.map[index] !== terrainStates.IMPASSABLE &&
|
||||
this.map[index] !== terrainStates.DEEP_WATER)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (floodFor === "water" && this.navalPassMap[index] === 0 &&
|
||||
(this.map[index] === terrainStates.DEEP_WATER ||
|
||||
this.map[index] === terrainStates.SHALLOW_WATER))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
if (index%w > 0)
|
||||
{
|
||||
if (floodFor === "land" && this.landPassMap[index -1] === 0 && this.map[index -1] !== this.IMPASSABLE && this.map[index -1] !== this.DEEP_WATER)
|
||||
{
|
||||
if (!reachLeft)
|
||||
{
|
||||
IndexArray.push(index -1);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 && (this.map[index -1] === this.DEEP_WATER || this.map[index -1] === this.SHALLOW_WATER))
|
||||
{
|
||||
if (!reachLeft)
|
||||
{
|
||||
IndexArray.push(index -1);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (reachLeft)
|
||||
reachLeft = false;
|
||||
}
|
||||
|
||||
if (index%w < w - 1)
|
||||
{
|
||||
if (floodFor === "land" && this.landPassMap[index +1] === 0 && this.map[index +1] !== this.IMPASSABLE && this.map[index +1] !== this.DEEP_WATER)
|
||||
{
|
||||
if (!reachRight)
|
||||
{
|
||||
IndexArray.push(index +1);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 && (this.map[index +1] === this.DEEP_WATER || this.map[index +1] === this.SHALLOW_WATER))
|
||||
{
|
||||
if (!reachRight)
|
||||
{
|
||||
IndexArray.push(index +1);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (reachRight)
|
||||
reachRight = false;
|
||||
}
|
||||
} // should actually break
|
||||
++y;
|
||||
} while (index/w < h-1); // should actually break
|
||||
let reachLeft = false;
|
||||
let reachRight = false;
|
||||
let index;
|
||||
do
|
||||
{
|
||||
index = newIndex + w*y;
|
||||
|
||||
if (floodFor === "land" && this.landPassMap[index] === 0 &&
|
||||
this.map[index] !== terrainStates.IMPASSABLE &&
|
||||
this.map[index] !== terrainStates.DEEP_WATER)
|
||||
{
|
||||
this.landPassMap[index] = value;
|
||||
this.regionSize[value]++;
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index] === 0 &&
|
||||
(this.map[index] === terrainStates.DEEP_WATER ||
|
||||
this.map[index] === terrainStates.SHALLOW_WATER))
|
||||
{
|
||||
this.navalPassMap[index] = value;
|
||||
this.regionSize[value]++;
|
||||
}
|
||||
else
|
||||
break;
|
||||
|
||||
if (index%w > 0)
|
||||
{
|
||||
if (floodFor === "land" && this.landPassMap[index -1] === 0 &&
|
||||
this.map[index -1] !== terrainStates.IMPASSABLE &&
|
||||
this.map[index -1] !== terrainStates.DEEP_WATER)
|
||||
{
|
||||
if (!reachLeft)
|
||||
{
|
||||
IndexArray.push(index -1);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 &&
|
||||
(this.map[index -1] === terrainStates.DEEP_WATER ||
|
||||
this.map[index -1] === terrainStates.SHALLOW_WATER))
|
||||
{
|
||||
if (!reachLeft)
|
||||
{
|
||||
IndexArray.push(index -1);
|
||||
reachLeft = true;
|
||||
}
|
||||
}
|
||||
else if (reachLeft)
|
||||
reachLeft = false;
|
||||
}
|
||||
|
||||
if (index%w < w - 1)
|
||||
{
|
||||
if (floodFor === "land" && this.landPassMap[index +1] === 0 &&
|
||||
this.map[index +1] !== terrainStates.IMPASSABLE &&
|
||||
this.map[index +1] !== terrainStates.DEEP_WATER)
|
||||
{
|
||||
if (!reachRight)
|
||||
{
|
||||
IndexArray.push(index +1);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 &&
|
||||
(this.map[index +1] === terrainStates.DEEP_WATER ||
|
||||
this.map[index +1] === terrainStates.SHALLOW_WATER))
|
||||
{
|
||||
if (!reachRight)
|
||||
{
|
||||
IndexArray.push(index +1);
|
||||
reachRight = true;
|
||||
}
|
||||
}
|
||||
else if (reachRight)
|
||||
reachRight = false;
|
||||
}
|
||||
++y;
|
||||
} while (index/w < h-1); // should actually break
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-disable prefer-const -- Mods should be able to change them */
|
||||
|
||||
export let IMPASSABLE = 0;
|
||||
/**
|
||||
* non-passable by land units
|
||||
*/
|
||||
export let DEEP_WATER = 200;
|
||||
/**
|
||||
* passable by land units and water units
|
||||
*/
|
||||
export let SHALLOW_WATER = 201;
|
||||
/**
|
||||
* passable by land units
|
||||
*/
|
||||
export let LAND = 255;
|
||||
|
||||
/* eslint-enable prefer-const */
|
||||
|
|
@ -6,171 +6,173 @@ import { Queue } from "simulation/ai/petra/queue.js";
|
|||
import { QueueManager } from "simulation/ai/petra/queueManager.js";
|
||||
import { gameAnalysis } from "simulation/ai/petra/startingStrategy.js";
|
||||
|
||||
export function PetraBot(settings)
|
||||
export class PetraBot extends BaseAI
|
||||
{
|
||||
BaseAI.call(this, settings);
|
||||
|
||||
// played turn, because Petra doesn't play every turn.
|
||||
this.turn = 0;
|
||||
this.playedTurn = 0;
|
||||
this.elapsedTime = 0;
|
||||
|
||||
this.uniqueIDs = {
|
||||
"armies": 1, // starts at 1 to allow easier tests on armies ID existence
|
||||
"bases": 1, // base manager ID starts at one because "0" means "no base" on the map
|
||||
"plans": 0, // training/building/research plans
|
||||
"transports": 1 // transport plans start at 1 because 0 might be used as none
|
||||
};
|
||||
|
||||
this.Config = new Config(settings.difficulty, settings.behavior);
|
||||
|
||||
this.savedEvents = {};
|
||||
}
|
||||
|
||||
PetraBot.prototype = Object.create(BaseAI.prototype);
|
||||
|
||||
PetraBot.prototype.CustomInit = function(gameState)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
constructor(settings)
|
||||
{
|
||||
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
|
||||
this.canPlay = this.data.canPlay;
|
||||
this.turn = this.data.turn;
|
||||
this.playedTurn = this.data.playedTurn;
|
||||
this.elapsedTime = this.data.elapsedTime;
|
||||
this.savedEvents = this.data.savedEvents;
|
||||
super(settings);
|
||||
|
||||
// played turn, because Petra doesn't play every turn.
|
||||
this.turn = 0;
|
||||
this.playedTurn = 0;
|
||||
this.elapsedTime = 0;
|
||||
|
||||
this.uniqueIDs = {
|
||||
"armies": 1, // starts at 1 to allow easier tests on armies ID existence
|
||||
"bases": 1, // base manager ID starts at one because "0" means "no base" on the map
|
||||
"plans": 0, // training/building/research plans
|
||||
"transports": 1 // transport plans start at 1 because 0 might be used as none
|
||||
};
|
||||
|
||||
this.Config = new Config(settings.difficulty, settings.behavior);
|
||||
|
||||
this.savedEvents = {};
|
||||
}
|
||||
|
||||
CustomInit(gameState)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
{
|
||||
// WARNING: the deserializations should not modify the metadatas infos inside their init functions
|
||||
this.canPlay = this.data.canPlay;
|
||||
this.turn = this.data.turn;
|
||||
this.playedTurn = this.data.playedTurn;
|
||||
this.elapsedTime = this.data.elapsedTime;
|
||||
this.savedEvents = this.data.savedEvents;
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
for (const i in this.savedEvents[key])
|
||||
{
|
||||
const evt = this.savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
{
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
this.savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.Config.Deserialize(this.data.config);
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, {});
|
||||
this.queueManager.Deserialize(gameState, this.data.queueManager);
|
||||
this.queues = this.queueManager.queues;
|
||||
|
||||
this.HQ = new Headquarters(this.Config, true);
|
||||
this.HQ.init(gameState, this.queues);
|
||||
this.HQ.Deserialize(gameState, this.data.HQ);
|
||||
|
||||
this.uniqueIDs = this.data.uniqueIDs;
|
||||
this.isDeserialized = false;
|
||||
this.data = undefined;
|
||||
|
||||
// initialisation needed after the completion of the deserialization
|
||||
this.HQ.postinit(gameState);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Config.setConfig(gameState);
|
||||
|
||||
// this.queues can only be modified by the queue manager or things will go awry.
|
||||
this.queues = {};
|
||||
for (const i in this.Config.priorities)
|
||||
this.queues[i] = new Queue();
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, this.queues);
|
||||
|
||||
this.HQ = new Headquarters(this.Config, false);
|
||||
|
||||
this.HQ.init(gameState, this.queues);
|
||||
|
||||
// Try to analyze our starting position and set a strategy.
|
||||
this.canPlay = gameAnalysis(this.HQ, gameState);
|
||||
}
|
||||
}
|
||||
|
||||
OnUpdate(sharedScript)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
this.Init(PlayerID, sharedScript);
|
||||
|
||||
if (this.gameFinished || this.gameState.playerData.state == "defeated")
|
||||
return;
|
||||
|
||||
for (const i in sharedScript.events)
|
||||
{
|
||||
if (i == "AIMetadata") // not used inside petra
|
||||
continue;
|
||||
if (this.savedEvents[i] !== undefined)
|
||||
this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]);
|
||||
else
|
||||
this.savedEvents[i] = sharedScript.events[i];
|
||||
}
|
||||
|
||||
// Run the update every n turns, offset depending on player ID to balance the load
|
||||
this.elapsedTime = this.gameState.getTimeElapsed() / 1000;
|
||||
if (!this.playedTurn || (this.turn + this.player) % 8 == 5)
|
||||
{
|
||||
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
|
||||
|
||||
this.playedTurn++;
|
||||
|
||||
if (!this.canPlay)
|
||||
{
|
||||
Engine.ProfileStop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.HQ.update(this.gameState, this.queues, this.savedEvents);
|
||||
|
||||
this.queueManager.update(this.gameState);
|
||||
|
||||
for (const i in this.savedEvents)
|
||||
this.savedEvents[i] = [];
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
}
|
||||
|
||||
Serialize()
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
return this.data;
|
||||
|
||||
const savedEvents = {};
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
for (const i in this.savedEvents[key])
|
||||
savedEvents[key] = this.savedEvents[key].slice();
|
||||
for (const i in savedEvents[key])
|
||||
{
|
||||
const evt = this.savedEvents[key][i];
|
||||
if (!savedEvents[key][i])
|
||||
continue;
|
||||
const evt = savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
{
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
this.savedEvents[key][i] = evtmod;
|
||||
}
|
||||
savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
|
||||
this.Config.Deserialize(this.data.config);
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, {});
|
||||
this.queueManager.Deserialize(gameState, this.data.queueManager);
|
||||
this.queues = this.queueManager.queues;
|
||||
|
||||
this.HQ = new Headquarters(this.Config, true);
|
||||
this.HQ.init(gameState, this.queues);
|
||||
this.HQ.Deserialize(gameState, this.data.HQ);
|
||||
|
||||
this.uniqueIDs = this.data.uniqueIDs;
|
||||
this.isDeserialized = false;
|
||||
this.data = undefined;
|
||||
|
||||
// initialisation needed after the completion of the deserialization
|
||||
this.HQ.postinit(gameState);
|
||||
return {
|
||||
"canPlay": this.canPlay,
|
||||
"uniqueIDs": this.uniqueIDs,
|
||||
"turn": this.turn,
|
||||
"playedTurn": this.playedTurn,
|
||||
"elapsedTime": this.elapsedTime,
|
||||
"savedEvents": savedEvents,
|
||||
"config": this.Config.Serialize(),
|
||||
"queueManager": this.queueManager.Serialize(),
|
||||
"HQ": this.HQ.Serialize()
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
Deserialize(data, sharedScript)
|
||||
{
|
||||
this.Config.setConfig(gameState);
|
||||
|
||||
// this.queues can only be modified by the queue manager or things will go awry.
|
||||
this.queues = {};
|
||||
for (const i in this.Config.priorities)
|
||||
this.queues[i] = new Queue();
|
||||
|
||||
this.queueManager = new QueueManager(this.Config, this.queues);
|
||||
|
||||
this.HQ = new Headquarters(this.Config, false);
|
||||
|
||||
this.HQ.init(gameState, this.queues);
|
||||
|
||||
// Try to analyze our starting position and set a strategy.
|
||||
this.canPlay = gameAnalysis(this.HQ, gameState);
|
||||
this.isDeserialized = true;
|
||||
this.data = data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
PetraBot.prototype.OnUpdate = function(sharedScript)
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
this.Init(PlayerID, sharedScript);
|
||||
|
||||
if (this.gameFinished || this.gameState.playerData.state == "defeated")
|
||||
return;
|
||||
|
||||
for (const i in sharedScript.events)
|
||||
{
|
||||
if (i == "AIMetadata") // not used inside petra
|
||||
continue;
|
||||
if (this.savedEvents[i] !== undefined)
|
||||
this.savedEvents[i] = this.savedEvents[i].concat(sharedScript.events[i]);
|
||||
else
|
||||
this.savedEvents[i] = sharedScript.events[i];
|
||||
}
|
||||
|
||||
// Run the update every n turns, offset depending on player ID to balance the load
|
||||
this.elapsedTime = this.gameState.getTimeElapsed() / 1000;
|
||||
if (!this.playedTurn || (this.turn + this.player) % 8 == 5)
|
||||
{
|
||||
Engine.ProfileStart("PetraBot bot (player " + this.player +")");
|
||||
|
||||
this.playedTurn++;
|
||||
|
||||
if (!this.canPlay)
|
||||
{
|
||||
Engine.ProfileStop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.HQ.update(this.gameState, this.queues, this.savedEvents);
|
||||
|
||||
this.queueManager.update(this.gameState);
|
||||
|
||||
for (const i in this.savedEvents)
|
||||
this.savedEvents[i] = [];
|
||||
|
||||
Engine.ProfileStop();
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
};
|
||||
|
||||
PetraBot.prototype.Serialize = function()
|
||||
{
|
||||
if (this.isDeserialized)
|
||||
return this.data;
|
||||
|
||||
const savedEvents = {};
|
||||
for (const key in this.savedEvents)
|
||||
{
|
||||
savedEvents[key] = this.savedEvents[key].slice();
|
||||
for (const i in savedEvents[key])
|
||||
{
|
||||
if (!savedEvents[key][i])
|
||||
continue;
|
||||
const evt = savedEvents[key][i];
|
||||
const evtmod = {};
|
||||
for (const keyevt in evt)
|
||||
evtmod[keyevt] = evt[keyevt];
|
||||
savedEvents[key][i] = evtmod;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"canPlay": this.canPlay,
|
||||
"uniqueIDs": this.uniqueIDs,
|
||||
"turn": this.turn,
|
||||
"playedTurn": this.playedTurn,
|
||||
"elapsedTime": this.elapsedTime,
|
||||
"savedEvents": savedEvents,
|
||||
"config": this.Config.Serialize(),
|
||||
"queueManager": this.queueManager.Serialize(),
|
||||
"HQ": this.HQ.Serialize()
|
||||
};
|
||||
};
|
||||
|
||||
PetraBot.prototype.Deserialize = function(data, sharedScript)
|
||||
{
|
||||
this.isDeserialized = true;
|
||||
this.data = data;
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue