Merge branch 'main' into give-territory

This commit is contained in:
Théodore Léon
2025-06-07 13:31:54 +02:00
committed by GitHub
170 changed files with 4343 additions and 2968 deletions
+2
View File
@@ -3,6 +3,8 @@
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
- [ ] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
+1 -1
View File
@@ -37,7 +37,7 @@ on:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}
cancel-in-progress: false
jobs:
+25 -3
View File
@@ -1,8 +1,13 @@
name: 🧼 PR Description
name: 🧼 PR
on:
pull_request:
types: [opened, edited, synchronize]
types:
- demilestoned
- edited
- milestoned
- opened
- synchronize
permissions: {}
@@ -24,9 +29,11 @@ jobs:
errors.push('❌ Missing or short `## Description:` section.');
}
// Check all three boxes are checked
// Check all five boxes are checked
const requiredBoxes = [
/- \[x\] I have added screenshots for all UI updates/i,
/- \[x\] I process any text displayed to the user through translateText\(\) and I\'ve added it to the en\.json file/i,
/- \[x\] I have added relevant tests to the test directory/i,
/- \[x\] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced/i,
/- \[x\] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors/i
];
@@ -43,3 +50,18 @@ jobs:
} else {
console.log('✅ PR description and checklist look good.');
}
has-milestone:
name: Has Milestone
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
// Get the pull request data
const milestone = context.payload.pull_request.milestone;
if (!milestone) {
core.setFailed('❌ Pull request must have a milestone assigned before merging.');
return;
}
console.log(`✅ Milestone found: ${milestone.title}`);
+1
View File
@@ -2,6 +2,7 @@ build/
node_modules/
out/
static/
coverage/
TODO.txt
resources/images/.DS_Store
resources/.DS_Store
+3
View File
@@ -59,5 +59,8 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY startup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/startup.sh
RUN mkdir -p /tmp/.cloudflared && chmod 777 /tmp/.cloudflared
ENV CF_CONFIG_DIR=/tmp/.cloudflared
# Use the startup script as the entrypoint
ENTRYPOINT ["/usr/local/bin/startup.sh"]
+6
View File
@@ -97,10 +97,16 @@ npm run start:server-dev
```
- **Lint and fix code**:
```bash
npm run lint:fix
```
- **Testing**
```bash
npm test
```
## 🏗️ Project Structure
- `/src/client` - Frontend game client
+7 -3
View File
@@ -171,8 +171,12 @@ if [ $? -ne 0 ]; then
exit 1
fi
# Generate a random filename for the environment file to prevent conflicts
# when multiple deployments are happening at the same time.
ENV_FILE="${REMOTE_UPDATE_PATH}/${SUBDOMAIN}-${RANDOM}.env"
ssh -i $SSH_KEY $REMOTE_USER@$SERVER_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && \
cat > $REMOTE_UPDATE_PATH/.env << 'EOL'
cat > $ENV_FILE << 'EOL'
GAME_ENV=$ENV
ENV=$ENV
HOST=$HOST
@@ -192,8 +196,8 @@ OTEL_ENDPOINT=$OTEL_ENDPOINT
BASIC_AUTH_USER=$BASIC_AUTH_USER
BASIC_AUTH_PASS=$BASIC_AUTH_PASS
EOL
chmod 600 $REMOTE_UPDATE_PATH/.env && \
$REMOTE_UPDATE_SCRIPT"
chmod 600 $ENV_FILE && \
$REMOTE_UPDATE_SCRIPT $ENV_FILE"
if [ $? -ne 0 ]; then
echo "❌ Failed to execute update script on server."
+10
View File
@@ -17,4 +17,14 @@ export default {
},
transformIgnorePatterns: ["node_modules/(?!(node:)/)"],
preset: "ts-jest/presets/default-esm",
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
coverageThreshold: {
global: {
branches: 0,
functions: 0,
lines: 0,
statements: 0,
},
},
coverageReporters: ["text", "lcov", "html"],
};
+8
View File
@@ -84,6 +84,7 @@
"@types/d3": "^7.4.3",
"@types/jest": "^29.5.12",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/sinon": "^17.0.3",
@@ -8263,6 +8264,13 @@
"@types/sizzle": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+2
View File
@@ -10,6 +10,7 @@
"dev": "cross-env GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"tunnel": "npm run build-prod && npm run start:server",
"test": "jest",
"test:coverage": "jest --coverage",
"format": "prettier --ignore-unknown --write .",
"lint": "eslint",
"lint:fix": "eslint --fix",
@@ -31,6 +32,7 @@
"@types/d3": "^7.4.3",
"@types/jest": "^29.5.12",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/sinon": "^17.0.3",
-999
View File
@@ -1,999 +0,0 @@
ownerdomain=openfront.io
managerdomain=adinplay.com
#V 01.04.2025 VH
#V
#----------------------------------------------------------------------------#
# . #
# .o8 #
# oooo ooo .ooooo. ooo. .oo. .oooo. .o888oo oooo oooo .oooo.o #
# `88. .8' d88' `88b `888P"Y88b `P )88b 888 `888 `888 d88( "8 #
# `88..8' 888ooo888 888 888 .oP"888 888 888 888 `"Y88b. #
# `888' 888 . 888 888 d8( 888 888 . 888 888 o. )88b #
# `8' `Y8bod8P' o888o o888o `Y888""8o "888" `V88V"V8P' 8""888P' #
# #
# The leading advertising solution for gaming and entertainment #
# #
# To become a publisher or advertise please contact info@venatus.com #
# #
#----------------------------------------------------------------------------#
adagio.io, 1090, DIRECT
rubiconproject.com, 19116, RESELLER, 0bfd66d529a55807
pubmatic.com, 159110, RESELLER, 5d62403b186f2ace
lijit.com, 367236, RESELLER, fafdf38b16bf6b2b
improvedigital.com, 1790, RESELLER
triplelift.com, 13482, RESELLER, 6c33edb13117fd86
rubiconproject.com, 12186, RESELLER, 0bfd66d529a55807
video.unrulymedia.com, 5672421953199218469, RESELLER
amxrtb.com, 105199358, DIRECT
amxrtb.com, 105199778, DIRECT
sharethrough.com, a6a34444, RESELLER, d53b998a7bd4ecd2
appnexus.com, 12290, RESELLER
pubmatic.com, 158355, RESELLER, 5d62403b186f2ace
rubiconproject.com, 23844, RESELLER, 0bfd66d529a55807
openx.com, 559680764, RESELLER, 6a698e2ec38604c6
adform.com, 2767, RESELLER
adyoulike.com, c1314a52de718f3c214c00173d2994f9, DIRECT
pubmatic.com, 160925, RESELLER, 5d62403b186f2ace
aps.amazon.com,70247b00-ff8f-4016-b3ab-8344daf96e09,DIRECT
aniview.com, 5f2063121d82c82557194737, RESELLER, 78b21b97965ec3f8
aniview.com, 643f8e74688b10f72307cc24, DIRECT, 78b21b97965ec3f8
google.com, pub-6346866704322274, RESELLER, f08c47fec0942fa0
pubmatic.com, 160993, RESELLER, 5d62403b186f2ace
rubiconproject.com, 13918, RESELLER, 0bfd66d529a55807
google.com, pub-5717092533913515, RESELLER, f08c47fec0942fa0
gannett.com, 22652678936, RESELLER
richaudience.com, 1ru8dKmJJV, RESELLER
sharethrough.com, zLsEa05k, RESELLER, d53b998a7bd4ecd2
aps.amazon.com, 1ad7261b-91ea-4b6f-b9e9-b83522205b75, RESELLER
pubmatic.com, 161335, RESELLER, 5d62403b186f2ace
openx.com, 556532676, RESELLER, 6a698e2ec38604c6
mediago.io, 045ac24b888bcf59a09731e7f0f2084f, RESELLER
blockthrough.com, 5643766199222272, DIRECT
criteo.com, B-062405, DIRECT, 9fac4a4a87c2a44f
themediagrid.com, CVQXOH, DIRECT, 35d5010d7789b49d
freewheel.tv, 211121, DIRECT
freewheel.tv, 211129-524565, DIRECT
freewheel.tv, 211129-169843, DIRECT
google.com, pub-5781531207509232, DIRECT, f08c47fec0942fa0
google.com, pub-5781531207509232, RESELLER, f08c47fec0942fa0
google.com, pub-2553634189837243, RESELLER, f08c47fec0942fa0
gumgum.com, 13385, RESELLER, ffdef49475d318a9
gumgum.com, 14302, RESELLER, ffdef49475d318a9
rubiconproject.com, 23434, RESELLER, 0bfd66d529a55807
pubmatic.com, 157897, RESELLER, 5d62403b186f2ace
indexexchange.com, 183921, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 193067, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 194127, DIRECT, 50b1c356f2c5c8fc
indexexchange.com, 205972, RESELLER, 50b1c356f2c5c8fc
Blis.com,33,RESELLER,61453ae19a4b73f4
conversantmedia.com,40881,RESELLER,03113cd04947736d
insticator.com,843c9a44-60ea-4342-8ad4-68f894283b3e,DIRECT,b3511ffcafb23a32
sharethrough.com,Q9IzHdvp,DIRECT,d53b998a7bd4ecd2
rubiconproject.com,17062,RESELLER,0bfd66d529a55807
risecodes.com,6124caed9c7adb0001c028d8,DIRECT
pubmatic.com,95054,DIRECT,5d62403b186f2ace
openx.com,558230700,RESELLER,6a698e2ec38604c6
video.unrulymedia.com,136898039,RESELLER
lijit.com,257618,RESELLER,fafdf38b16bf6b2b
minutemedia.com,01garg96c88b,RESELLER
appnexus.com,3695,RESELLER,f5ab79cb980f11d1
kargo.com, 8688, DIRECT
kueez.com,e5b6208bc94ed2d5788e1e4c1cf5452e, DIRECT
rubiconproject.com, 16920, RESELLER, 0bfd66d529a55807
openx.com, 557564833, RESELLER, 6a698e2ec38604c6
lijit.com, 407406, RESELLER, fafdf38b16bf6b2b #SOVRN
appnexus.com, 8826,RESELLER, f5ab79cb980f11d1
Media.net,8CU4JTRF9, RESELLER
rubiconproject.com, 13762, RESELLER, 0bfd66d529a55807
media.net, 8CU8ARTF8, DIRECT
Media.net, 8CU198XI2, DIRECT
themediagrid.com, LTW57M, DIRECT, 35d5010d7789b49d
ogury.com, 086233d2-e8a8-44fc-907b-f0752e1c85de, DIRECT
appnexus.com, 11470, RESELLER
openx.com, 542378302, RESELLER, 6a698e2ec38604c6
openx.com, 540134228, RESELLER, 6a698e2ec38604c6
openx.com, 537144009, RESELLER, 6a698e2ec38604c6
openx.com, 560557013, RESELLER, 6a698e2ec38604c6
optidigital.com,p230,DIRECT
pubmatic.com,158939,RESELLER,5d62403b186f2ace
rubiconproject.com,20336,RESELLER,0bfd66d529a55807
smartadserver.com,3379,RESELLER,060d053dcf45cbf3
triplelift.com,8183,RESELLER,6c33edb13117fd86
the-ozone-project.com, ozoneven0005, DIRECT
openx.com, 540731760, RESELLER, 6a698e2ec38604c6
pubmatic.com, 160557, RESELLER, 5d62403b186f2ace
themediagrid.com, WF71T3, DIRECT, 35d5010d7789b49d
Yahoo.com, 60170, DIRECT, e1a5b5b6e3255540
pubmatic.com, 159234, RESELLER, 5d62403b186f2ace
pubmatic.com, 160552, RESELLER, 5d62403b186f2ace
pubmatic.com, 159401, RESELLER, 5d62403b186f2ace
pubmatic.com, 165533, RESELLER, 5d62403b186f2ace
richaudience.com, 1XvIoD5o0S, DIRECT
pubmatic.com, 81564, DIRECT, 5d62403b186f2ace
pubmatic.com, 156538, DIRECT, 5d62403b186f2ace
appnexus.com, 8233, DIRECT
rubiconproject.com, 13510, DIRECT
risecodes.com, 5fa94677b2db6a00015b22a9, DIRECT
pubmatic.com, 160295, RESELLER, 5d62403b186f2ace
xandr.com, 14082, RESELLER
rubiconproject.com, 23876, RESELLER, 0bfd66d529a55807
sharethrough.com, 5926d422, RESELLER, d53b998a7bd4ecd2
yieldmo.com, 2754490424016969782, RESELLER
media.net, 8CUQ6928Q, RESELLER
onetag.com, 69f48c2160c8113, RESELLER
amxrtb.com, 105199691, RESELLER
openx.com, 537140488, RESELLER, 6a698e2ec38604c6
video.unrulymedia.com, 335119963, RESELLER
seedtag.com, 5aa6c80640c9e209009721e0, DIRECT
xandr.com, 4009, DIRECT, f5ab79cb980f11d1
rubiconproject.com, 17280, DIRECT, 0bfd66d529a55807
smartadserver.com, 3050, DIRECT
lijit.com, 397546, DIRECT, fafdf38b16bf6b2b
sharethrough.com, 31c129df, DIRECT, d53b998a7bd4ecd2
sharethrough.com, awx1H4AI, RESELLER, d53b998a7bd4ecd2
smaato.com, 1100055690, DIRECT, 07bcf65f187117b4
smaato.com, 1100049216, DIRECT, 07bcf65f187117b5
rubiconproject.com, 24600, RESELLER, 0bfd66d529a55807
pubmatic.com, 156177, RESELLER, 5d62403b186f2ace
smartadserver.com, 3490, DIRECT
smartadserver.com, 4016, DIRECT
smartadserver.com, 4074, DIRECT
sovrn.com, 237754, DIRECT, fafdf38b16bf6b2b
lijit.com, 237754, DIRECT, fafdf38b16bf6b2b
lijit.com, 506352, DIRECT, fafdf38b16bf6b2b
teads.tv, 23348, DIRECT, 15a9c44f6d26cbe1
triplelift.com, 6059, RESELLER, 6c33edb13117fd86
video.unrulymedia.com, 985572675, DIRECT
video.unrulymedia.com, 985572675, RESELLER
sharethrough.com, 6qlnf8SY, RESELLER, d53b998a7bd4ecd2
appnexus.com, 12986, RESELLER, f5ab79cb980f11d1
improvedigital.com, 1069, RESELLER
pubmatic.com, 158056, RESELLER
Weborama.nl, 10714, DIRECT
adwmg.com, 101261, DIRECT, c9688a22012618e7
google.com, pub-8622186303703569, DIRECT, f08c47fec0942fa0
freewheel.tv, 1604590, DIRECT
freewheel.tv, 1604595, DIRECT
pubmatic.com, 156512, DIRECT
indexexchange.com, 183753, DIRECT
wunderkind.co, 6438, DIRECT
wunderkind.co, 6449, DIRECT
criteo.com, B-068503, DIRECT
appnexus.com, 806, DIRECT, f5ab79cb980f11d1
appnexus.com,1908,RESELLER,f5ab79cb980f11d1
adinplay.com, FTB, DIRECT
venatus.com, 67f90df66f43edab7e84d165, DIRECT
##################################
# AdinPlay.com ads.txt - 2025-04-16
##################################
adinplay.com, OFI, DIRECT
#Google
google.com, pub-3282547114800347, RESELLER, f08c47fec0942fa0
#Appnexus
appnexus.com, 8631, RESELLER, f5ab79cb980f11d1
#Index
indexexchange.com, 186547, RESELLER, 50b1c356f2c5c8fc
indexexchange.com, 187218, RESELLER, 50b1c356f2c5c8fc
indexexchange.com, 177754, RESELLER, 50b1c356f2c5c8fc
indexexchange.com, 196862, RESELLER, 50b1c356f2c5c8fc
indexexchange.com, 207014, RESELLER, 50b1c356f2c5c8fc
#Pulsepoint
contextweb.com, 561767, RESELLER, 89ff185a4c4e857c
#Pubmatic
pubmatic.com, 156975, RESELLER, 5d62403b186f2ace
pubmatic.com, 156857, RESELLER, 5d62403b186f2ace
pubmatic.com, 162231, RESELLER, 5d62403b186f2ace
#OpenX
openx.com, 540164985, RESELLER, 6a698e2ec38604c6
openx.com, 540010967, RESELLER, 6a698e2ec38604c6
openx.com, 540182293, RESELLER, 6a698e2ec38604c6
openx.com, 556894440, RESELLER, 6a698e2ec38604c6
#Sovrn
sovrn.com, 268781, RESELLER, fafdf38b16bf6b2b
lijit.com, 268781, RESELLER, fafdf38b16bf6b2b
lijit.com, 268781-eb, DIRECT, fafdf38b16bf6b2b
appnexus.com, 1360, RESELLER, f5ab79cb980f11d1
openx.com, 538959099, RESELLER, 6a698e2ec38604c6
openx.com, 539924617, RESELLER, 6a698e2ec38604c6
pubmatic.com, 137711, RESELLER, 5d62403b186f2ace
pubmatic.com, 156212, RESELLER, 5d62403b186f2ace
rubiconproject.com, 17960, RESELLER, 0bfd66d529a55807
sovrn.com, 264160, RESELLER, fafdf38b16bf6b2b
lijit.com, 264160, RESELLER, fafdf38b16bf6b2b
lijit.com, 264160-eb, DIRECT, fafdf38b16bf6b2b
smartadserver.com, 4125, RESELLER
sharethrough.com,7144eb80,RESELLER
#Oath
coxmt.com, 2000067907202, RESELLER
pubmatic.com, 156377, RESELLER, 5d62403b186f2ace #banner
pubmatic.com, 156078, RESELLER, 5d62403b186f2ace #banner
pubmatic.com, 155967, RESELLER, 5d62403b186f2ace #banner
openx.com, 537143344, RESELLER, 6a698e2ec38604c6
indexexchange.com, 175407, RESELLER, 50b1c356f2c5c8fc
#Rhythmone
rhythmone.com, 1432377581,DIRECT, a670c89d4a324e47
rhythmone.com, 665259327, DIRECT, a670c89d4a324e47
rhythmone.com, 2451244104, RESELLER, a670c89d4a324e47
video.unrulymedia.com, 2451244104, RESELLER
video.unrulymedia.com, 1432377581, DIRECT
#Gumgum
aolcloud.net,9904,RESELLER
appnexus.com,1001,DIRECT,f5ab79cb980f11d1
appnexus.com,2758,RESELLER,f5ab79cb980f11d1
appnexus.com,3135,DIRECT,f5ab79cb980f11d1
bidtellect.com,1407,RESELLER,1c34aa2d85d45e93
contextweb.com,558355,RESELLER,89ff185a4c4e857c
openx.com,537120563,DIRECT,6a698e2ec38604c6
openx.com,537149485,RESELLER,6a698e2ec38604c6
google.com,pub-9557089510405422,DIRECT,f08c47fec0942fa0
google.com,pub-3848273848634341,RESELLER,f08c47fec0942fa0
google.com, pub-7861278482560604, RESELLER, f08c47fec0942fa0
rhythmone.com,78519861,RESELLER, a670c89d4a324e47
outbrain.com,01a755b08c8c22b15d46a8b753ab6955d4,RESELLER
appnexus.com,7597,RESELLER,f5ab79cb980f11d1
openx.com,540003333,RESELLER,6a698e2ec38604c6
33across.com,0013300001r0t9mAAA,RESELLER
#Amazon
aps.amazon.com,53b902f9-cf9c-4605-aec3-2c8ce65042b8,DIRECT
gumgum.com,13543,DIRECT,ffdef49475d318a9
appnexus.com,8631,DIRECT,f5ab79cb980f11d1
indexexchange.com,196862,DIRECT,50b1c356f2c5c8fc
pubmatic.com,160006,RESELLER,5d62403b186f2ace
pubmatic.com,160096,RESELLER,5d62403b186f2ace
rubiconproject.com,18020,RESELLER,0bfd66d529a55807
pubmatic.com,162231,DIRECT,5d62403b186f2ace
appnexus.com,1908,RESELLER,f5ab79cb980f11d1
smaato.com,1100044650,RESELLER,07bcf65f187117b4
ad-generation.jp,12474,RESELLER,7f4ea9029ac04e53
districtm.io,100962,RESELLER,3fd707be9c4527c3
yieldmo.com,2719019867620450718,RESELLER
appnexus.com,3663,RESELLER,f5ab79cb980f11d1
rhythmone.com,1654642120,RESELLER,a670c89d4a324e47
yahoo.com,55029,RESELLER,e1a5b5b6e3255540
gumgum.com,14141,RESELLER,ffdef49475d318a9
admanmedia.com,726,RESELLER
emxdgt.com,2009,RESELLER,1e1d41537f7cad7f
appnexus.com,1356,RESELLER,f5ab79cb980f11d1
contextweb.com,562541,RESELLER,89ff185a4c4e857c
themediagrid.com,JTQKMP,RESELLER,35d5010d7789b49d
sovrn.com,375328,RESELLER,fafdf38b16bf6b2b
lijit.com,375328,RESELLER,fafdf38b16bf6b2b
beachfront.com,14804,RESELLER,e2541279e8e2ca4d
improvedigital.com,2050,RESELLER
mintegral.com,10043,RESELLER,0aeed750c80d6423
sonobi.com,7f5fa520f8,RESELLER,d1a215d9eb5aee9e
openx.com,556894440,DIRECT,6a698e2ec38604c6
onetag.com,7683ebe7bee7969,DIRECT
media.net,8CUZ1MK22,RESELLER
sharethrough.com,buaxQzOE,DIRECT,d53b998a7bd4ecd2
smartadserver.com,4571,DIRECT,060d053dcf45cbf3
mediago.io,045ac24b888bcf59a09731e7f0f2084f,RESELLER
adyoulike.com,7463c359225e043c111036d7a29affa5,RESELLER
minutemedia.com,01gya4708ddm,RESELLER
visiblemeasures.com,1052,RESELLER
undertone.com,4205,RESELLER,d954590d0cb265b9
admedia.com,AM1601,RESELLER,ae6c32151e71f19d
triplelift.com,8472,DIRECT,6c33edb13117fd86
kargo.com,8824,RESELLER
start.io,123111883,RESELLER
connectad.io,455,RESELLER,85ac85a30c93b3e5
# 33Across
rubiconproject.com, 16414, RESELLER, 0bfd66d529a55807 #33Across #hb #tag
rubiconproject.com, 21642, RESELLER, 0bfd66d529a55807 #33Across #hb #tag #viewable
rubiconproject.com, 21434, RESELLER, 0bfd66d529a55807 #33Across #tag #ebda
rubiconproject.com, 21720, RESELLER, 0bfd66d529a55807 #33Across EU #hb #tag
pubmatic.com, 156423, RESELLER, 5d62403b186f2ace #33Across #hb #tag
pubmatic.com, 158136, RESELLER, 5d62403b186f2ace #33Across EU #hb #tag
pubmatic.com, 158569, RESELLER, 5d62403b186f2ace #33Across #tag #ebda
appnexus.com, 10239, RESELLER, f5ab79cb980f11d1 #33Across #hb #tag #viewable
appnexus.com, 1001, RESELLER, f5ab79cb980f11d1 #33Across #tag
appnexus.com, 3135, RESELLER, f5ab79cb980f11d1 #33Across #tag
openx.com, 537120563, RESELLER, 6a698e2ec38604c6 #33Across #hb #tag
openx.com, 539392223, RESELLER, 6a698e2ec38604c6 #33Across #tag #ebda
openx.com, 540995201, RESELLER, 6a698e2ec38604c6 #33Across #hb #tag #viewable
adtech.com, 12094, RESELLER #33Across #hb #tag
adtech.com, 9993, RESELLER #33Across #tag
aol.com, 47594, RESELLER, e1a5b5b6e3255540 #33Across #hb #tag #viewable
yahoo.com, 55188, DIRECT, e1a5b5b6e3255540 #33Across #tag #ebda
advangelists.com, 8d3bba7425e7c98c50f52ca1b52d3735, RESELLER, 60d26397ec060f98 #33Across #hb #tag
sonobi.com, a416546bb7, RESELLER, d1a215d9eb5aee9e #33Across #tag #ebda
indexexchange.com, 190966, RESELLER, 50b1c356f2c5c8fc #33Across #tag #ebda
indexexchange.com, 183635, RESELLER, 50b1c356f2c5c8fc #33Across #hb #tag #viewable
google.com, pub-9557089510405422, RESELLER, f08c47fec0942fa0 #33Across #tag
#Rubiconproject
rubiconproject.com, 15636, RESELLER, 0bfd66d529a55807
#LockerDome
lockerdome.com, 11908041977355520, DIRECT
#Yield Nexus
yieldnexus.com, 1, DIRECT
ssp.ynxs.io, 185, DIRECT
appnexus.com, 10617, RESELLER, f5ab79cb980f11d1
appnexus.com, 9393, RESELLER, f5ab79cb980f11d1
advertising.com, 25034, RESELLER
sonobi.com, 783272317b, RESELLER, d1a215d9eb5aee9e
indexexchange.com, 186684,RESELLER, 50b1c356f2c5c8fc
#CPM
appnexus.com, 9624, RESELLER, f5ab79cb980f11d1
adtech.com, 11506, RESELLER
yahoo.com, 56896, RESELLER
pubmatic.com, 156078, RESELLER, 5d62403b186f2ace
advertising.com, 25218, RESELLER #video, US
beachfront.com, 9065, RESELLER
contextweb.com, 559969, RESELLER, 89ff185a4c4e857c
indexexchange.com, 189455, RESELLER, 50b1c356f2c5c8fc
advertising.com, 28320, RESELLER
richaudience.com, NtMZGaQQTT, RESELLER
adform.com, 1942, RESELLER
adform.com, 1941, RESELLER
adtech.com, 4687, RESELLER
aerserv.com, 2750, RESELLER, 2ce496b9f80eb9fa
aol.com, 27093, RESELLER
aol.com, 46658, RESELLER
aolcloud.net, 4687, RESELLER
appnexus.com, 2928, RESELLER, f5ab79cb980f11d1
contextweb.com, 560520, RESELLER, 89ff185a4c4e857c
google.com, pub-9115524111147081, RESELLER, f08c47fec0942fa0
google.com, pub-4673227357197067, RESELLER, f08c47fec0942fa0
indexexchange.com, 179394, RESELLER, 50b1c356f2c5c8fc
lijit.com, 249425, RESELLER, fafdf38b16bf6b2b
cpmstar.com, 49818, RESELLER
mobfox.com, 74240, RESELLER
mobfox.com, 45499, RESELLER
openx.com, 539625136, RESELLER, 6a698e2ec38604c6
smaato.com, 1100037086, RESELLER
smaato.com, 1100000579, RESELLER
sovrn.com, 249425, RESELLER, fafdf38b16bf6b2b
openx.com, 541079309, RESELLER, 6a698e2ec38604c6
openx.com, 541166421, RESELLER, 6a698e2ec38604c6
contextweb.com, 562263, RESELLER, 89ff185a4c4e857c
districtm.io, 102015, RESELLER, 3fd707be9c4527c3
lkqd.net, 304, RESELLER, 59c49fa9598a0117
lkqd.com, 304, RESELLER, 59c49fa9598a0117
advertising.com, 2694, RESELLER
google.com, pub-5781531207509232, RESELLER, f08c47fec0942fa0
appnexus.com, 806, RESELLER, f5ab79cb980f11d1
freewheel.tv, 211121, RESELLER
freewheel.tv, 211129, RESELLER
indexexchange.com, 183921, RESELLER, 50b1c356f2c5c8fc
openx.com, 540134228, RESELLER, 6a698e2ec38604c6
openx.com, 540634629, RESELLER, 6a698e2ec38604c6
pubmatic.com, 156715, RESELLER, 5d62403b186f2ace
rubiconproject.com, 13762, RESELLER, 0bfd66d529a55807
smartadserver.com, 3490, RESELLER
springserve.com, 550, RESELLER, a24eb641fc82e93d
beachfront.com, 4969, RESELLER, e2541279e8e2ca4d
advertising.com, 26282, RESELLER
pubmatic.com, 157310, RESELLER, 5d62403b186f2ace
rhythmone.com, 2968119028, RESELLER, a670c89d4a324e47
contextweb.com, 561910, RESELLER, 89ff185a4c4e857c
openx.com, 540226160, RESELLER, 6a698e2ec38604c6
openx.com, 540255318, RESELLER, 6a698e2ec38604c6
ssp.ynxs.io, 185, RESELLER
tremorhub.com, hpwve, RESELLER, 1a4e959a1b50034a
telaria.com, hpwve, RESELLER, 1a4e959a1b50034a
video.unrulymedia.com, UNRX-PUB-29dad46b-9bec-43c7-b950-c59d09cc8c71, RESELLER
video.unrulymedia.com, 985572675, RESELLER
rhythmone.com, 2864567592, RESELLER, a670c89d4a324e47
vidoomy.com, 51019, RESELLER
aol.com, 22762, RESELLER
freewheel.tv, 872257, RESELLER
openx.com, 540804929, RESELLER, 6a698e2ec38604c6
emxdgt.com, 1495, RESELLER, 1e1d41537f7cad7f
#Rubicon
rubiconproject.com, 23042, RESELLER, 0bfd66d529a55807
rubiconproject.com, 23044, RESELLER, 0bfd66d529a55807
#AMX
amxrtb.com, 105199469, RESELLER
appnexus.com, 12290, RESELLER, f5ab79cb980f11d1
appnexus.com, 11786, RESELLER, f5ab79cb980f11d1
indexexchange.com, 191503, RESELLER, 50b1c356f2c5c8fc
lijit.com, 260380, RESELLER, fafdf38b16bf6b2b
sovrn.com, 260380, RESELLER, fafdf38b16bf6b2b
pubmatic.com, 158355, RESELLER, 5d62403b186f2ace
appnexus.com, 9393, RESELLER, f5ab79cb980f11d1 #Video #Display
appnexus.com, 11924, RESELLER, f5ab79cb980f11d1
#Kueez
kueez.com, fe46d13305ce1b89f18a84c52275b7fe, DIRECT
appnexus.com, 8826, RESELLER
rubiconproject.com, 16920, RESELLER
openx.com, 557564833, RESELLER
lijit.com, 407406, RESELLER
media.net, 8cu4jtrf9, RESELLER
pubmatic.com, 162110, RESELLER
sharethrough.com, n98xdzel, RESELLER
33across.com, 0010b00002odu4haax, RESELLER
yieldmo.com, 3133660606033240149, RESELLER
onetag.com, 6e053d779444c00, RESELLER
video.unrulymedia.com, 3486482593, RESELLER
sonobi.com, 4c4fba1717, RESELLER
smartadserver.com, 4288, RESELLER
zetaglobal.com, 108, RESELLER
improvedigital.com, 2106, RESELLER
loopme.com, 11576, RESELLER
themediagrid.com, uot45z, RESELLER
#Aniview
aniview.com, 606c5af8b82e996ca965f498, RESELLER, 78b21b97965ec3f8
advertising.com, 23089, RESELLER
appnexus.com, 12637, RESELLER, f5ab79cb980f11d1
appnexus.com, 9382, RESELLER, f5ab79cb980f11d1
synacor.com, 82171, RESELLER, e108f11b2cdf7d5b
pubmatic.com, 156344, RESELLER, 5d62403b186f2ace
rubiconproject.com, 13344, RESELLER, 0bfd66d529a55807
indexexchange.com, 191740, RESELLER, 50b1c356f2c5c8fc
conversantmedia.com, 100195, DIRECT, 03113cd04947736d
appnexus.com, 4052, RESELLER, f5ab79cb980f11d1
contextweb.com, 561998, RESELLER, 89ff185a4c4e857c
pubmatic.com, 158100, RESELLER, 5d62403b186f2ace
yahoo.com, 55771, RESELLER, e1a5b5b6e3255540
onetag.com, 57e618150c70d90, DIRECT
google.com, pub-3769010358500643, RESELLER, f08c47fec0942fa0
video.unrulymedia.com, 3350674472, DIRECT
rhythmone.com, 3350674472, DIRECT, a670c89d4a324e47
google.com, pub-4586415728471297, RESELLER, f08c47fec0942fa0
google.com, pub-3565385483761681, DIRECT, f08c47fec0942fa0
google.com, pub-5717092533913515, RESELLER, f08c47fec0942fa0
smartadserver.com, 2786, DIRECT
improvedigital.com, 1147, DIRECT
google.com, pub-2930805104418204, RESELLER, f08c47fec0942fa0
google.com, pub-4903453974745530, RESELLER, f08c47fec0942fa0
richaudience.com, 1ru8dKmJJV, DIRECT
advertising.com, 7574, RESELLER
appnexus.com, 8233, RESELLER, f5ab79cb980f11d1
pubmatic.com, 81564, RESELLER, 5d62403b186f2ace
pubmatic.com, 156538, RESELLER, 5d62403b186f2ace
rubiconproject.com, 13510, RESELLER, 0bfd66d529a55807
smartadserver.com, 2640, RESELLER
smartadserver.com, 2441, RESELLER
yahoo.com, 57857, RESELLER, e1a5b5b6e3255540
undertone.com, 4077, DIRECT
appnexus.com, 2234, RESELLER, f5ab79cb980f11d1
rubiconproject.com, 22412, RESELLER, 0bfd66d529a55807
advertising.com, 28650, RESELLER
pubmatic.com, 160318, RESELLER, 5d62403b186f2ace
pubmatic.com, 160319, RESELLER, 5d62403b186f2ace
appnexus.com, 10112, RESELLER, f5ab79cb980f11d1
google.com, pub-0679975395820445, RESELLER, f08c47fec0942fa0
google.com, pub-9936969251765866, RESELLER, f08c47fec0942fa0
#Fluct
adingo.jp, 25262, RESELLER
pubmatic.com, 156313, RESELLER, 5d62403b186f2ace
appnexus.com, 7044, RESELLER, f5ab79cb980f11d1
pubmatic.com, 158060, RESELLER, 5d62403b186f2ace
#Conversant
conversantmedia.com, 100106, RESELLER, 03113cd04947736d
lijit.com, 411121, RESELLER, fafdf38b16bf6b2b #SOVRN
admanmedia.com, 2050, RESELLER
Appnerve.com, 187287, RESELLER
rubiconproject.com, 23644, RESELLER, 0bfd66d529a55807
#OneTag
onetag.com, 7683ebe7bee7969, RESELLER
onetag.com, 7683ebe7bee7969-OB, RESELLER
appnexus.com, 13099, RESELLER, f5ab79cb980f11d1
yahoo.com, 58905, RESELLER, e1a5b5b6e3255540
rubiconproject.com, 11006, RESELLER, 0bfd66d529a55807
smartadserver.com, 4111, RESELLER
#Media.net
media.net, 8CUEHU9Y5, RESELLER
openx.com, 537100188, RESELLER, 6a698e2ec38604c6
pubmatic.com, 159463, RESELLER, 5d62403b186f2ace
emxdgt.com, 1759, RESELLER, 1e1d41537f7cad7f
google.com, pub-7439041255533808, RESELLER, f08c47fec0942fa0
rubiconproject.com, 19396, RESELLER, 0bfd66d529a55807
onetag.com, 5d49f482552c9b6, RESELLER
sonobi.com, 83729e979b, RESELLER
33across.com, 0010b00002cGp2AAAS, RESELLER, bbea06d9c4d2853c
rhythmone.com, 3611299104, RESELLER, a670c89d4a324e47
districtm.io, 100600, RESELLER
lemmatechnologies.com, 399, RESELLER, 7829010c5bebd1fb #LEMMA
e-planning.net,ec771b05828a67fa,RESELLER,c1ba615865ed87b2
google.com, pub-9685734445476814, RESELLER, f08c47fec0942fa0
#EMX Digital
emxdgt.com, 2345, RESELLER, 1e1d41537f7cad7f
#The MediaGrid
themediagrid.com, B8ZEVT, RESELLER, 35d5010d7789b49d
themediagrid.com, 3W8S2K, RESELLER, 35d5010d7789b49d
#triplelift
triplelift.com, 12900, RESELLER, 6c33edb13117fd86
triplelift.com, 12900-EB, DIRECT, 6c33edb13117fd86
triplelift.com, 13897, DIRECT, 6c33edb13117fd86
#Sharethrough
sharethrough.com, buaxQzOE, RESELLER, d53b998a7bd4ecd2
sharethrough.com, jvyAFD6e, DIRECT, d53b998a7bd4ecd2
pubmatic.com, 156557, RESELLER, 5d62403b186f2ace
rubiconproject.com, 18694, RESELLER, 0bfd66d529a55807
openx.com, 540274407, RESELLER, 6a698e2ec38604c6
33across.com, 0013300001kQj2HAAS, RESELLER, bbea06d9c4d2853c
smaato.com, 1100047713, RESELLER, 07bcf65f187117b4
yahoo.com, 59531, RESELLER, e1a5b5b6e3255540
smartadserver.com, 4342, RESELLER
smartadserver.com, 4012, RESELLER
#V 15.01.2024 PH
#------------------------------------------------------------------------------------------------------
adagio.io, 1090, DIRECT # Adagio_0_6
rubiconproject.com, 19116, RESELLER, 0bfd66d529a55807 # Adagio_0_6
pubmatic.com, 159110, RESELLER, 5d62403b186f2ace # Adagio_0_6
improvedigital.com, 1790, RESELLER # Adagio_0_6
indexexchange.com, 194558, RESELLER # Adagio_0_6
richaudience.com, 1BTOoaD22a, DIRECT # Adagio_0_6
33across.com, 0015a00002oUk4aAAC, DIRECT, bbea06d9c4d2853c # Adagio_0_6
appnexus.com, 10239, RESELLER, f5ab79cb980f11d1 # Adagio_0_6
rubiconproject.com, 16414, RESELLER, 0bfd66d529a55807 # Adagio_0_6
lijit.com, 367236, RESELLER, fafdf38b16bf6b2b # Adagio_0_6
e-planning.net, 83c06e81531537f4, RESELLER, c1ba615865ed87b2 # Adagio_0_6
amxrtb.com, 105199358, DIRECT # AdaptMX_1_6&7
indexexchange.com, 191503, RESELLER # AdaptMX_1_6&7
appnexus.com, 11786, RESELLER # AdaptMX_1_6&7
appnexus.com, 12290, RESELLER # AdaptMX_1_6&7
pubmatic.com, 158355, RESELLER, 5d62403b186f2ace # AdaptMX_1_6&7
advertising.com, 28305, RESELLER # AdaptMX_1_6&7
rubiconproject.com, 23844, RESELLER, 0bfd66d529a55807 # AdaptMX_1_6&7
openx.com, 559680764, RESELLER, 6a698e2ec38604c6 # AdaptMX_1_6&7
adform.com, 2767, RESELLER # Adform_0_6&7
adyoulike.com, c1314a52de718f3c214c00173d2994f9, DIRECT # AdYouLike_0_6
pubmatic.com, 160925, RESELLER, 5d62403b186f2ace # AdYouLike_0_6
rubiconproject.com, 20736, RESELLER, 0bfd66d529a55807 # AdYouLike_0_6
appnexus.com, 7664, RESELLER # AdYouLike_0_6
aps.amazon.com,70247b00-ff8f-4016-b3ab-8344daf96e09,DIRECT # Amazon_3_6&7
ad-generation.jp,12474,RESELLER # Amazon_3_6&7
aniview.com, 5f2063121d82c82557194737, RESELLER, 78b21b97965ec3f8 # Aniview
aniview.com, 643f8e74688b10f72307cc24, DIRECT, 78b21b97965ec3f8 # Aniview
google.com, pub-6346866704322274, RESELLER, f08c47fec0942fa0 # Aniview
pubmatic.com, 160993, RESELLER, 5d62403b186f2ace # Aniview
rubiconproject.com, 13918, RESELLER, 0bfd66d529a55807 # Aniview
google.com, pub-5717092533913515, RESELLER, f08c47fec0942fa0 # Aniview
gannett.com, 22652678936, RESELLER # Aniview
richaudience.com, 1ru8dKmJJV, RESELLER # Aniview
appnexus.com, 12637, RESELLER, f5ab79cb980f11d1 # Aniview
google.com, pub-3565385483761681, RESELLER, f08c47fec0942fa0 # Aniview
sharethrough.com, zLsEa05k, RESELLER, d53b998a7bd4ecd2 # Aniview
aps.amazon.com, 1ad7261b-91ea-4b6f-b9e9-b83522205b75, RESELLER # Aniview
pubmatic.com, 161335, RESELLER, 5d62403b186f2ace # Aniview
google.com, pub-7734005103835923, RESELLER, f08c47fec0942fa0 # Aniview
openx.com, 559611024, RESELLER, 6a698e2ec38604c6 # Aniview
yieldlab.net, 495507, DIRECT # Aniview
blockthrough.com, 5643766199222272, DIRECT # Blockthrough
appnexus.com, 6979, RESELLER # Blockthrough
indexexchange.com, 194341, RESELLER, 50b1c356f2c5c8fc # Blockthrough
pubmatic.com, 160377, RESELLER, 5d62403b186f2ace # Blockthrough
rubiconproject.com, 23718, RESELLER, 0bfd66d529a55807 # Blockthrough
onetag.com, 75804861b76a852, DIRECT # Blockthrough
amxrtb.com, 105199664, DIRECT # Blockthrough
criteo.com, B-062405, DIRECT, 9fac4a4a87c2a44f # Criteo_0_6&7
themediagrid.com, CVQXOH, DIRECT, 35d5010d7789b49d # Criteo_0_6&7
cpmstar.com, 53615, DIRECT # CPMSTAR
rhythmone.com,1838093862,DIRECT,a670c89d4a324e47 # CPMSTAR
video.unrulymedia.com, 1838093862, DIRECT # CPMSTAR
pubmatic.com, 160251, DIRECT, 5d62403b186f2ace # CPMSTAR
pubmatic.com, 161595, DIRECT, 5d62403b186f2ace # CPMSTAR
rubiconproject.com, 23330, DIRECT, 0bfd66d529a55807 # CPMSTAR
conversantmedia.com, 41150, DIRECT, 03113cd04947736d # Epsilon
adingo.jp, 24379, DIRECT # Fluct_1_6&7
freewheel.tv, 211121, DIRECT # Freewheel_0_7
freewheel.tv, 211129, RESELLER # Freewheel_0_7
google.com, pub-5781531207509232, RESELLER, f08c47fec0942fa0 # Google_AdX_6&7
google.com, pub-2553634189837243, RESELLER, f08c47fec0942fa0 # Google_AdX_6&7
gumgum.com, 13385, RESELLER, ffdef49475d318a9 # GumGum_JP_0_9_6
gumgum.com, 14302, RESELLER, ffdef49475d318a9 # GumGum_JP_0_9_6
improvedigital.com, 1012, DIRECT # Improve_0_6&7
improvedigital.com, 1640, RESELLER # Improve_1_6
improvedigital.com, 2114, RESELLER # Improve_kids_1_6&7
indexexchange.com, 183921, DIRECT, 50b1c356f2c5c8fc # Index Exchange_0_6&7
indexexchange.com, 188416, DIRECT, 50b1c356f2c5c8fc # Index Exchange_1_6&7
indexexchange.com, 193067, DIRECT, 50b1c356f2c5c8fc # Index Exchange_2_6&7
indexexchange.com, 194127, DIRECT, 50b1c356f2c5c8fc # Index Exchange_7&4_6&7
indexexchange.com, 205972, RESELLER, 50b1c356f2c5c8fc # Index Exchange_Oz
indexexchange.com, 206870, RESELLER, 50b1c356f2c5c8fc # Index_EasyConnect
iion.io, 10133, DIRECT # iion
kargo.com, 8688, DIRECT # Kargo_0_6
rubiconproject.com, 17902, RESELLER, 0bfd66d529a55807 # Magnite_1_6&7
rubiconproject.com, 13762, RESELLER, 0bfd66d529a55807 # Magnite_0&2_6&7
telaria.com,hpwve,RESELLER,1a4e959a1b50034a # Magnite_Streaming
tremorhub.com,hpwve,RESELLER,1a4e959a1b50034a # Magnite_Streaming
media.net, 8CU8ARTF8, DIRECT # Media.net
Media.net, 8CU5786QK, DIRECT # Media.net
themediagrid.com, LTW57M, DIRECT, 35d5010d7789b49d # MediaGrid_2_6&7
minutemedia.com, 01gerz6y43ck, RESELLER # MinuteMedia_0_6
pubmatic.com, 161683, RESELLER, 5d62403b186f2ace # MinuteMedia_0_6
appnexus.com, 8381, RESELLER # MinuteMedia_0_6
triplelift.com, 6030, RESELLER, 6c33edb13117fd86 # MinuteMedia_0_6
33across.com, 0013300001jlr99AAA, RESELLER, bbea06d9c4d2853c # MinuteMedia_0_6
nobid.io, 22629800915, DIRECT # Nobid_0_6
sonobi.com, 7ad1b9f952, RESELLER, d1a215d9eb5aee9e # Nobid_0_6
xandr.com, 12701, RESELLER, f5ab79cb980f11d1 # Nobid_0_6
lijit.com, 273657, DIRECT, fafdf38b16bf6b2b # Nobid_0_6
onetag.com, 694e68b73971b58, DIRECT # Nobid_0_6
yahoo.com, 57872, RESELLER # Nobid_0_6
sharethrough.com, UvcAx8IL, DIRECT, d53b998a7bd4ecd2 # Nobid_0_6
ogury.com, 086233d2-e8a8-44fc-907b-f0752e1c85de, DIRECT # Ogury_0_6
appnexus.com, 11470, RESELLER # Ogury_0_6
openx.com, 537144009, RESELLER, 6a698e2ec38604c6 # OpenX_0_6
openx.com, 540134228, RESELLER, 6a698e2ec38604c6 # OpenX_0_7
openx.com, 540368327, RESELLER, 6a698e2ec38604c6 # OpenX_1_6&7
openx.com, 542378302, RESELLER, 6a698e2ec38604c6 # OpenX_2_6&7
the-ozone-project.com, ozoneven0005, DIRECT # Ozone_0_6
appnexus.com, 9979, RESELLER # Ozone_0_6
openx.com, 540731760, RESELLER, 6a698e2ec38604c6 # Ozone_0_6
adform.com, 2657, RESELLER, 9f5210a2f0999e32 # Ozone_0_6
pubmatic.com, 160557, RESELLER, 5d62403b186f2ace # Ozone_0_6
themediagrid.com, WF71T3, DIRECT, 35d5010d7789b49d # Ozone_0_6
pgamssp.com, 634dc90283fff00f005151f2, DIRECT # PGAM_0_7
freewheel.tv, 1489202, RESELLER # PGAM_0_7
freewheel.tv, 1488706, RESELLER # PGAM_0_7
video.unrulymedia.com, 5921144960123684292, RESELLER # PGAM_0_7
appnexus.com, 9291, RESELLER # PGAM_0_7
pubmatic.com, 162623, RESELLER, 5d62403b186f2ace # PGAM_0_7
primis.tech, 31136, DIRECT, b6b21d256ef43532 # Primis
pubmatic.com, 156595, RESELLER, 5d62403b186f2ace # Primis
google.com, pub-1320774679920841, RESELLER, f08c47fec0942fa0 # Primis
openx.com, 540258065, RESELLER, 6a698e2ec38604c6 # Primis
rubiconproject.com, 20130, RESELLER, 0bfd66d529a55807 # Primis
freewheel.tv, 19133, RESELLER, 74e8e47458f74754 # Primis
smartadserver.com, 3436, RESELLER, 060d053dcf45cbf3 # Primis
indexexchange.com, 191923, RESELLER, 50b1c356f2c5c8fc # Primis
adform.com, 2078, RESELLER # Primis
Media.net, 8CU695QH7, RESELLER # Primis
video.unrulymedia.com, 2338962694, RESELLER # Primis
sharethrough.com, flUyJowI, RESELLER, d53b998a7bd4ecd2 # Primis
triplelift.com, 8210, RESELLER, 6c33edb13117fd86 # Primis
yahoo.com, 59260, RESELLER # Primis
pubmatic.com, 159234, RESELLER, 5d62403b186f2ace # PubMatic_0_6&7
pubmatic.com, 158940, RESELLER, 5d62403b186f2ace # PubMatic_1_6&7
pubmatic.com, 160552, RESELLER, 5d62403b186f2ace # PubMatic_4_7
pubmatic.com, 159401, RESELLER, 5d62403b186f2ace # PubMatic_2_6&7
pubmatic.com, 163598, RESELLER, 5d62403b186f2ace # Pubmatic_OW
richaudience.com, 1XvIoD5o0S, DIRECT # Rich Audience_0_6&7
risecodes.com, 5fa94677b2db6a00015b22a9, DIRECT # Rise
pubmatic.com, 160295, RESELLER, 5d62403b186f2ace # Rise
xandr.com, 14082, RESELLER # Rise
rubiconproject.com, 23876, RESELLER, 0bfd66d529a55807 # Rise
media.net, 8CUQ6928Q, RESELLER # Rise_Temp
sharethrough.com, 5926d422, RESELLER, d53b998a7bd4ecd2 # Rise_Temp
sharethrough.com, 31c129df, DIRECT, d53b998a7bd4ecd2 # Sharethrough_0_6&7
sharethrough.com, Ip2TfKpa, DIRECT, d53b998a7bd4ecd2 # Sharethrough_1_6&7
smartadserver.com, 2161, RESELLER # Showheroes_7_8
appnexus.com, 8833, RESELLER, f5ab79cb980f11d1 # Showheroes_7_8
smartadserver.com, 3668, RESELLER # Showheroes_7_8
freewheel.tv, 1003361, DIRECT # Showheroes_7_8
pubmatic.com, 156695, DIRECT, 5d62403b186f2ace # Showheroes_7_8
showheroes.com, 6829, RESELLER # Showheroes_7_8
smartadserver.com, 3490, DIRECT # Smart AdServer_0&1&2_6&7
smartadserver.com, 3490-OB, DIRECT, 060d053dcf45cbf3 # Smart AdServer_0&1&2_6&7
smartadserver.com, 4016, DIRECT # Smart AdServer_0&1&2_6&7
smartadserver.com, 4074, DIRECT # Smart AdServer_0&1&2_6&7
smaato.com, 1100055690, DIRECT, 07bcf65f187117b4 # Smaato
smaato.com, 1100004890, DIRECT, 07bcf65f187117b4 # Smaato
sonobi.com, 116da9d98c, DIRECT, d1a215d9eb5aee9e # Sonobi_0_6&7
sonobi.com, e017850301, DIRECT, d1a215d9eb5aee9e # Sonobi_4_7
sovrn.com, 237754, DIRECT, fafdf38b16bf6b2b # Sovrn_0&1&2_6&7
lijit.com, 237754, DIRECT, fafdf38b16bf6b2b # Sovrn_0&1&2_6&7
lijit.com, 237754-eb, DIRECT, fafdf38b16bf6b2b # Sovrn_1_6&7
taboola.com,1422403,DIRECT,c228e6794e811952 # Taboola_6_8
triplelift.com, 6059, DIRECT, 6c33edb13117fd86 # Triplelift_0&2_6&7
triplelift.com, 6059-EB, DIRECT, 6c33edb13117fd86 # Triplelift_0&2_6&7
video.unrulymedia.com, 985572675, DIRECT # Unruly_0&2_7
rhythmone.com, 2864567592, DIRECT, a670c89d4a324e47 # Unruly_0&2_7
xandr.com, 13799, RESELLER # Unruly
sharethrough.com, 6qlnf8SY, RESELLER, d53b998a7bd4ecd2 # Unruly
vidazoo.com, 655c85dc63ceeb606a0f365f, DIRECT, b6ada874b4d7d0b2 # Vidazoo
pubmatic.com, 159988, RESELLER, 5d62403b186f2ace # Vidazoo
rubiconproject.com, 17130, RESELLER, 0bfd66d529a55807 # Vidazoo
pubmatic.com, 156512, DIRECT # Wunderkind
indexexchange.com, 183753, DIRECT # Wunderkind
wunderkind.co, 6438, DIRECT # Wunderkind
wunderkind.co, 6449, DIRECT # Wunderkind
criteo.com, B-068503, DIRECT # Wunderkind
appnexus.com, 806, DIRECT, f5ab79cb980f11d1 # Xandr_0&2_6&7
appnexus.com,1908,RESELLER,f5ab79cb980f11d1 # Xandr_0&2_6&7
#Equativ
smartadserver.com, 4571, RESELLER, 060d053dcf45cbf3
smartadserver.com, 4571-OB, RESELLER, 060d053dcf45cbf3
smartadserver.com, 4016, RESELLER, 060d053dcf45cbf3 #Global
smartadserver.com, 4012, RESELLER, 060d053dcf45cbf3 #EUR
smartadserver.com, 4071, RESELLER, 060d053dcf45cbf3 #USD
smartadserver.com, 4073, RESELLER, 060d053dcf45cbf3 #BRL
smartadserver.com, 4074, RESELLER, 060d053dcf45cbf3 #MXN
smartadserver.com, 4247, RESELLER, 060d053dcf45cbf3 #CAD
smartadserver.com, 4228, RESELLER, 060d053dcf45cbf3 #USD_CTV
pubmatic.com, 156439, RESELLER, 5d62403b186f2ace
pubmatic.com, 154037, RESELLER, 5d62403b186f2ace
rubiconproject.com, 16114, RESELLER, 0bfd66d529a55807
openx.com, 537149888, RESELLER, 6a698e2ec38604c6
appnexus.com, 3703, RESELLER, f5ab79cb980f11d1
loopme.com, 5679, RESELLER, 6c8d5f95897a5a3b
xad.com, 958, RESELLER, 81cbf0a75a5e0e9a
video.unrulymedia.com, 2564526802, RESELLER
smaato.com, 1100044045, RESELLER, 07bcf65f187117b4
pubnative.net, 1006576, RESELLER, d641df8625486a7b
verve.com, 15503, RESELLER, 0c8f5958fc2d6270
adyoulike.com, b4bf4fdd9b0b915f746f6747ff432bde, RESELLER, 4ad745ead2958bf7
axonix.com, 57264, RESELLER
admanmedia.com, 43, RESELLER
sharethrough.com, OAW69Fon, RESELLER, d53b998a7bd4ecd2
contextweb.com, 560288, RESELLER, 89ff185a4c4e857c
#nobid
nobid.io, 22931676975, DIRECT
xandr.com, 11429, RESELLER, f5ab79cb980f11d1
sharethrough.com, aRE1degH, RESELLER, d53b998a7bd4ecd2
sonobi.com, 7ad1b9f952, RESELLER, d1a215d9eb5aee9e
sharethrough.com, UvcAx8IL, RESELLER, d53b998a7bd4ecd2
amxrtb.com, 105199579, RESELLER
yahoo.com,49648,RESELLER
rubiconproject.com, 24434, RESELLER, 0bfd66d529a55807
minutemedia.com, 01gerz67grgj, RESELLER
pubmatic.com, 161683, RESELLER, 5d62403b186f2ace
appnexus.com, 8381, RESELLER, f5ab79cb980f11d1
triplelift.com, 6030, RESELLER, 6c33edb13117fd86
sonobi.com, 37fbaf262c, RESELLER, d1a215d9eb5aee9e
openx.com, 540780517, RESELLER, 6a698e2ec38604c6
rubiconproject.com, 17598, RESELLER, 0bfd66d529a55807
indexexchange.com, 196326, RESELLER, 50b1c356f2c5c8fc
yahoo.com, 59407, RESELLER, e1a5b5b6e3255540
sharethrough.com, xz7QjFBY, RESELLER, d53b998a7bd4ecd2
inmobi.com,8f261ace12c3486ba2e0d2011cd97976,RESELLER,83e75a7ae333ca9d
risecodes.com, 63ea59eef828de0001cf1773, RESELLER
inmobi.com, 9e311c7a68e94888aac7fbb4272381e2, RESELLER, 83e75a7ae333ca9d
video.unrulymedia.com, 1352466146, RESELLER
yahoo.com, 59261, RESELLER, e1a5b5b6e3255540
gumgum.com, 13926, RESELLER, ffdef49475d318a9
onetag.com, 694e68b73971b58, RESELLER
lijit.com, 273657, RESELLER, fafdf38b16bf6b2b
sovrn.com, 273657, RESELLER, fafdf38b16bf6b2b
mediafuse.com, 389, RESELLER
appnexus.com, 9538, RESELLER, f5ab79cb980f11d1
yahoo.com, 57872, RESELLER
video.unrulymedia.com, 2997140015, RESELLER
indexexchange.com, 182257, RESELLER, 50b1c356f2c5c8fc
152media.info,152M374,RESELLER
appnexus.com, 3153, RESELLER, f5ab79cb980f11d1
#media.net_serverside_displayvideo
media.net, 8CUV34PJ4, DIRECT
sharethrough.com, koRtppYA, RESELLER, d53b998a7bd4ecd2
video.unrulymedia.com, 699546687, RESELLER
lijit.com, 264726, RESELLER, fafdf38b16bf6b2b
onetag.com, 765b4e6bb9c8438, RESELLER
amxrtb.com, 105199663, RESELLER
yieldmo.com, 2954622693783052507, RESELLER
loopme.com, 11556, RESELLER, 6c8d5f95897a5a3b
Contextweb.com, 562963, RESELLER, 89ff185a4c4e857c
zeta.com, 591, RESELLER
disqus.com, 591, RESELLER
admanmedia.com, 953, RESELLER
smartadserver.com, 4106, RESELLER, 060d053dcf45cbf3
imds.tv, 82302, RESELLER, ae6c32151e71f19d
improvedigital.com, 2073, RESELLER
betweendigital.com, 44808, RESELLER
adyoulike.com, 53264963677efeda057eef7db2cb305f, RESELLER
freewheel.tv,1577878,RESELLER
freewheel.tv,1577888,RESELLER
dxkulture.com, 9533, DIRECT, 259726033fc4df0c
dxkulture.com, 0098, DIRECT, 259726033fc4df0c
adswizz.com,dxkulture,DIRECT
adswizz.com,651,DIRECT
pubmatic.com,164751,RESELLER,5d62403b186f2ace
rubiconproject.com,26094,DIRECT,0bfd66d529a55807
zetaglobal.net,790,DIRECT
ssp.disqus.com,790,DIRECT
video.unrulymedia.com,946176315,RESELLER
video.unrulymedia.com, 347774562, RESELLER
rubiconproject.com, 15268, RESELLER, 0bfd66d529a55807
pubmatic.com, 159277, RESELLER
#AdaptMX
amxrtb.com, 105199723, DIRECT
appnexus.com, 12290, RESELLER
pubmatic.com, 161527, RESELLER
rubiconproject.com, 23844, RESELLER
# Adagio
adagio.io, 1361, RESELLER
# Adagio - Magnite
rubiconproject.com, 19116, RESELLER, 0bfd66d529a55807
# Adagio - Pubmatic
pubmatic.com, 159110, RESELLER, 5d62403b186f2ace
# Adagio - Improve Digital
improvedigital.com, 1790, RESELLER
# Adagio - Onetag
onetag.com, 6b859b96c564fbe, RESELLER
appnexus.com, 13099, RESELLER
pubmatic.com, 161593, RESELLER, 5d62403b186f2ace
# Adagio - Index Exchange
indexexchange.com, 194558, RESELLER
# Adagio - 33Across
33across.com, 0015a00002oUk4aAAC, RESELLER, bbea06d9c4d2853c
yahoo.com, 57289, RESELLER, e1a5b5b6e3255540
appnexus.com, 10239, RESELLER, f5ab79cb980f11d1
rubiconproject.com, 16414, RESELLER, 0bfd66d529a55807
pubmatic.com, 156423, RESELLER, 5d62403b186f2ace
rubiconproject.com, 21642, RESELLER, 0bfd66d529a55807
conversantmedia.com, 100141, RESELLER
indexexchange.com, 191973, RESELLER, 50b1c356f2c5c8fc
triplelift.com, 12503, RESELLER, 6c33edb13117fd86
insticator.com, 4ec3ed85-2830-4174-9f7f-f545620598b9, RESELLER
sharethrough.com, Q9IzHdvp, RESELLER, d53b998a7bd4ecd2
admanmedia.com, 2216, RESELLER
connectad.io, 456, RESELLER, 85ac85a30c93b3e5
# Adagio - Equativ
smartadserver.com, 3554, RESELLER
# Adagio - Sovrn
lijit.com, 367236, RESELLER, fafdf38b16bf6b2b
# Adagio - Freewheel
freewheel.tv, 1568036, RESELLER
freewheel.tv, 1568041, RESELLER
# Adagio - OpenX
openx.com, 558899373, RESELLER, 6a698e2ec38604c6
# Adagio - Triplelift
triplelift.com, 13482, RESELLER, 6c33edb13117fd86
# Adagio - E-Planning
e-planning.net, 83c06e81531537f4, RESELLER, c1ba615865ed87b2
pubmatic.com, 156631, RESELLER, 5d62403b186f2ace
openx.com, 541031350, RESELLER, 6a698e2ec38604c6
rubiconproject.com, 12186, RESELLER, 0bfd66d529a55807
# Adagio - Nexxen
video.unrulymedia.com, 5672421953199218469, RESELLER
#Freewheel
freewheel.tv, 1598995, RESELLER
freewheel.tv, 1599004, RESELLER
#Pgam
pgamssp.com, 64661fa49d522e327b0a8b84, DIRECT
freewheel.tv, 1489202, RESELLER
freewheel.tv, 1488706, RESELLER
rubiconproject.com, 24852, RESELLER, 0bfd66d529a55807
pubmatic.com, 162623, RESELLER, 5d62403b186f2ace
video.unrulymedia.com, 5921144960123684292, RESELLER
appnexus.com, 9291, RESELLER, f5ab79cb980f11d1
#Sonobi
sonobi.com, 3ee2ca3952, RESELLER, d1a215d9eb5aee9e
#Rich Audience
richaudience.com, kWVs0vbyki, RESELLER
appnexus.com, 2928, DIRECT, f5ab79cb980f11d1
smartadserver.com, 1999, RESELLER, 060d053dcf45cbf3
#Ozone
the-ozone-project.com, OZONEAIP0001, DIRECT
appnexus.com, 9979, RESELLER, f5ab79cb980f11d1
openx.com, 540731760, RESELLER, 6a698e2ec38604c6
adform.com, 2657, RESELLER, 9f5210a2f0999e32
pubmatic.com, 160557, RESELLER, 5d62403b186f2ace
indexexchange.com, 206233, RESELLER, 50b1c356f2c5c8fc
themediagrid.com, 1J3ZI6, DIRECT, 35d5010d7789b49d
themediagrid.com, WF71T3, DIRECT, 35d5010d7789b49d
# OptiDigital
optidigital.com,p345,RESELLER
pubmatic.com,158939,RESELLER,5d62403b186f2ace
rubiconproject.com,20336,RESELLER,0bfd66d529a55807
smartadserver.com,3379,RESELLER,060d053dcf45cbf3
criteo.com,B-060926,RESELLER,9fac4a4a87c2a44f
themediagrid.com,3ETIX5,RESELLER,35d5010d7789b49d
triplelift.com,8183,RESELLER,6c33edb13117fd86
appnexus.com,12190,RESELLER,f5ab79cb980f11d1
onetag.com,806eabb849d0326,RESELLER
rtbhouse.com,mSu1piUSmB9TF4AQDGk4,RESELLER
33across.com,001Pg00000HMy0YIAT,RESELLER,bbea06d9c4d2853c
e-planning.net,a76893b96338e7e9,RESELLER,c1ba615865ed87b2
appnexus.com,15941,RESELLER,f5ab79cb980f11d1
video.unrulymedia.com,731539260,RESELLER
#Rise
risecodes.com, 643813aab7212c00011c3f28, DIRECT
pubmatic.com, 160295, RESELLER, 5d62403b186f2ace
xandr.com, 14082, RESELLER
rubiconproject.com, 23876, RESELLER, 0bfd66d529a55807
sharethrough.com, 5926d422, RESELLER, d53b998a7bd4ecd2
media.net, 8CUQ6928Q, RESELLER
sonobi.com, 4a289cdd79, RESELLER, d1a215d9eb5aee9e
video.unrulymedia.com, 335119963, RESELLER
contextweb.com,562615,RESELLER,89ff185a4c4e857c
onetag.com, 69f48c2160c8113, RESELLER
33across.com, 0010b00002Xbn7QAAR, RESELLER, bbea06d9c4d2853c
yieldmo.com, 2754490424016969782, RESELLER
openx.com, 537140488, RESELLER, 6a698e2ec38604c6
lijit.com, 405318, RESELLER, fafdf38b16bf6b2b
themediagrid.com, 4DQHAP, RESELLER, 35d5010d7789b49d
loopme.com, 11362, RESELLER, 6c8d5f95897a5a3b
amxrtb.com, 105199691, RESELLER
smartadserver.com, 4284, RESELLER
adform.com, 3119, RESELLER, 9f5210a2f0999e32
smaato.com, 1100057444, RESELLER, 07bcf65f187117b4
adyoulike.com, 78afbc34fac571736717317117dfa247, RESELLER
#Block
blockthrough.com, 5130683165442048, DIRECT
pubmatic.com, 160377, RESELLER, 5d62403b186f2ace
rubiconproject.com, 23718, RESELLER, 0bfd66d529a55807
appnexus.com, 6979, RESELLER
lijit.com, 251666, RESELLER, fafdf38b16bf6b2b
lijit.com, 251666-eb, RESELLER, fafdf38b16bf6b2b
video.unrulymedia.com, 2444764291, RESELLER
contextweb.com, 558511, RESELLER
krushmedia.com, AJxF6R572a9M6CaTvK, RESELLER
criteo.com, 8990, RESELLER
smartadserver.com, 4485, RESELLER, 060d053dcf45cbf3
smartadserver.com, 4485-OB, RESELLER, 060d053dcf45cbf3
Contextweb.com, 562926, RESELLER, 89ff185a4c4e857c
# VT Amazon TAM
aps.amazon.com,70247b00-ff8f-4016-b3ab-8344daf96e09,DIRECT
indexexchange.com, 193067, DIRECT, 50b1c356f2c5c8fc
triplelift.com, 6059, DIRECT, 6c33edb13117fd86
sharethrough.com, 31c129df, DIRECT, d53b998a7bd4ecd2
appnexus.com, 806, DIRECT, f5ab79cb980f11d1
risecodes.com, 5fa94677b2db6a00015b22a9, DIRECT
minutemedia.com, 01gerz6y43ck, RESELLER
themediagrid.com, LTW57M, DIRECT, 35d5010d7789b49d
vidazoo.com, 655c85dc63ceeb606a0f365f, DIRECT, b6ada874b4d7d0b2
smartadserver.com, 3490, DIRECT
##################################
# AdinPlay.com ads.txt - 2025-04-16
##################################
venatus.com, OFI, DIRECT
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 75.294 75.294" xml:space="preserve">
<g>
<path d="M66.097,12.089h-56.9C4.126,12.089,0,16.215,0,21.286v32.722c0,5.071,4.126,9.197,9.197,9.197h56.9
c5.071,0,9.197-4.126,9.197-9.197V21.287C75.295,16.215,71.169,12.089,66.097,12.089z M61.603,18.089L37.647,33.523L13.691,18.089
H61.603z M66.097,57.206h-56.9C7.434,57.206,6,55.771,6,54.009V21.457l29.796,19.16c0.04,0.025,0.083,0.042,0.124,0.065
c0.043,0.024,0.087,0.047,0.131,0.069c0.231,0.119,0.469,0.215,0.712,0.278c0.025,0.007,0.05,0.01,0.075,0.016
c0.267,0.063,0.537,0.102,0.807,0.102c0.001,0,0.002,0,0.002,0c0.002,0,0.003,0,0.004,0c0.27,0,0.54-0.038,0.807-0.102
c0.025-0.006,0.05-0.009,0.075-0.016c0.243-0.063,0.48-0.159,0.712-0.278c0.044-0.022,0.088-0.045,0.131-0.069
c0.041-0.023,0.084-0.04,0.124-0.065l29.796-19.16v32.551C69.295,55.771,67.86,57.206,66.097,57.206z"/>
</g>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 75.294 75.294" xml:space="preserve">
<g>
<path d="M66.097,12.089h-56.9C4.126,12.089,0,16.215,0,21.286v32.722c0,5.071,4.126,9.197,9.197,9.197h56.9
c5.071,0,9.197-4.126,9.197-9.197V21.287C75.295,16.215,71.169,12.089,66.097,12.089z M61.603,18.089L37.647,33.523L13.691,18.089
H61.603z M66.097,57.206h-56.9C7.434,57.206,6,55.771,6,54.009V21.457l29.796,19.16c0.04,0.025,0.083,0.042,0.124,0.065
c0.043,0.024,0.087,0.047,0.131,0.069c0.231,0.119,0.469,0.215,0.712,0.278c0.025,0.007,0.05,0.01,0.075,0.016
c0.267,0.063,0.537,0.102,0.807,0.102c0.001,0,0.002,0,0.002,0c0.002,0,0.003,0,0.004,0c0.27,0,0.54-0.038,0.807-0.102
c0.025-0.006,0.05-0.009,0.075-0.016c0.243-0.063,0.48-0.159,0.712-0.278c0.044-0.022,0.088-0.045,0.131-0.069
c0.041-0.023,0.084-0.04,0.124-0.065l29.796-19.16v32.551C69.295,55.771,67.86,57.206,66.097,57.206z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 75.294 75.294" xml:space="preserve">
<g>
<path d="M66.097,12.089h-56.9C4.126,12.089,0,16.215,0,21.286v32.722c0,5.071,4.126,9.197,9.197,9.197h56.9
c5.071,0,9.197-4.126,9.197-9.197V21.287C75.295,16.215,71.169,12.089,66.097,12.089z M61.603,18.089L37.647,33.523L13.691,18.089
H61.603z M66.097,57.206h-56.9C7.434,57.206,6,55.771,6,54.009V21.457l29.796,19.16c0.04,0.025,0.083,0.042,0.124,0.065
c0.043,0.024,0.087,0.047,0.131,0.069c0.231,0.119,0.469,0.215,0.712,0.278c0.025,0.007,0.05,0.01,0.075,0.016
c0.267,0.063,0.537,0.102,0.807,0.102c0.001,0,0.002,0,0.002,0c0.002,0,0.003,0,0.004,0c0.27,0,0.54-0.038,0.807-0.102
c0.025-0.006,0.05-0.009,0.075-0.016c0.243-0.063,0.48-0.159,0.712-0.278c0.044-0.022,0.088-0.045,0.131-0.069
c0.041-0.023,0.084-0.04,0.124-0.065l29.796-19.16v32.551C69.295,55.771,67.86,57.206,66.097,57.206z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg fill="#fff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 26.676 26.676" xml:space="preserve">
<g>
<path d="M26.105,21.891c-0.229,0-0.439-0.131-0.529-0.346l0,0c-0.066-0.156-1.716-3.857-7.885-4.59
c-1.285-0.156-2.824-0.236-4.693-0.25v4.613c0,0.213-0.115,0.406-0.304,0.508c-0.188,0.098-0.413,0.084-0.588-0.033L0.254,13.815
C0.094,13.708,0,13.528,0,13.339c0-0.191,0.094-0.365,0.254-0.477l11.857-7.979c0.175-0.121,0.398-0.129,0.588-0.029
c0.19,0.102,0.303,0.295,0.303,0.502v4.293c2.578,0.336,13.674,2.33,13.674,11.674c0,0.271-0.191,0.508-0.459,0.562
C26.18,21.891,26.141,21.891,26.105,21.891z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 764 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#ffffff" stroke="none">
<path d="M2285 4675 c-640 -84 -1211 -457 -1551 -1015 -278 -456 -372 -1020
-258 -1550 86 -400 284 -760 579 -1055 351 -352 787 -560 1284 -615 171 -19
442 -8 615 24 423 80 802 282 1111 591 407 407 625 931 625 1505 0 574 -218
1098 -625 1505 -348 349 -786 561 -1270 615 -122 13 -387 11 -510 -5z m574
-436 c375 -64 738 -269 989 -560 504 -583 557 -1420 129 -2067 -33 -50 -63
-92 -67 -92 -3 0 -68 63 -145 140 -77 77 -144 140 -150 140 -13 0 -295 -282
-295 -295 0 -6 63 -73 140 -150 77 -77 140 -142 140 -145 0 -12 -195 -134
-289 -181 -399 -198 -882 -229 -1302 -83 -595 206 -1020 703 -1130 1320 -30
166 -30 422 -1 585 48 265 145 487 324 744 4 6 68 -50 152 -134 l145 -145 22
20 c57 49 279 272 279 280 0 5 -64 73 -142 152 -83 83 -139 146 -133 150 297
207 556 308 878 342 113 12 320 2 456 -21z"/>
<path d="M2350 3715 l0 -126 -45 -19 c-128 -55 -251 -173 -315 -303 -143 -291
-60 -569 230 -764 25 -17 132 -76 238 -131 195 -102 284 -159 302 -192 28 -52
1 -148 -57 -202 -58 -55 -77 -58 -388 -58 l-285 0 0 -215 0 -215 160 0 160 0
0 -105 0 -105 210 0 210 0 0 125 0 126 45 19 c232 100 398 365 382 610 -12
179 -112 332 -299 458 -25 18 -132 76 -237 131 -193 100 -283 158 -301 191
-28 52 -1 148 57 202 58 55 77 58 388 58 l285 0 0 215 0 215 -160 0 -160 0 0
105 0 105 -210 0 -210 0 0 -125z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

-1
View File
@@ -122,7 +122,6 @@
"random": "عشوائي",
"iceland": "آيسلندا",
"pangaea": "بانجيا",
"japan": "اليابان والجيران",
"betweentwoseas": "بين بحرين",
"knownworld": "العالم المعروف",
"faroeislands": "جزر فارو",
-1
View File
@@ -135,7 +135,6 @@
"random": "Произволна",
"iceland": "Исландия",
"pangaea": "Пангея",
"japan": "Япония и съседи",
"betweentwoseas": "Между Две Морета",
"knownworld": "Познат Свят",
"faroeislands": "Фарьорски острови",
-1
View File
@@ -118,7 +118,6 @@
"random": "যেকোনো",
"iceland": "আইসল্যান্ড",
"pangaea": "পাঞ্জিয়া",
"japan": "জাপান ও তার পার্শ্ববর্তী অঞ্চল",
"betweentwoseas": "দুই সমুদ্রের মধ্যবর্তী অঞ্চল",
"knownworld": "পরিচিত পৃথিবী",
"faroeislands": "ফ্যারো দ্বীপপুঞ্জ"
-1
View File
@@ -135,7 +135,6 @@
"random": "Náhodná",
"iceland": "Island",
"pangaea": "Pangea",
"japan": "Japonsko a okolí",
"betweentwoseas": "Mezi dvěma moři",
"knownworld": "Známý svět",
"faroeislands": "Faerské ostrovy",
-1
View File
@@ -112,7 +112,6 @@
"pangaea": "Pangaea",
"map": "Karte",
"betweentwoseas": "Zwischen zwei Meeren",
"japan": "Japan und Nachbarländer",
"knownworld": "Bekannte Welt"
},
"private_lobby": {
+3
View File
@@ -164,5 +164,8 @@
"Balanced": "difficulty.Balanced",
"Intense": "difficulty.Intense",
"Impossible": "difficulty.Impossible"
},
"heads_up_message": {
"choose_spawn": "heads_up_message.choose_spawn"
}
}
+24 -3
View File
@@ -5,6 +5,9 @@
"svg": "uk_us_flag",
"lang_code": "en"
},
"common": {
"close": "Close"
},
"main": {
"title": "OpenFront (ALPHA)",
"join_discord": "Join the Discord!",
@@ -17,8 +20,12 @@
"single_player": "Single Player",
"instructions": "Instructions",
"how_to_play": "How to Play",
"advertise": "Advertise",
"wiki": "Wiki"
},
"news": {
"title": "Version 23 released!"
},
"help_modal": {
"hotkeys": "Hotkeys",
"table_key": "Key",
@@ -137,7 +144,7 @@
"random": "Random",
"iceland": "Iceland",
"pangaea": "Pangaea",
"japan": "Japan and Neighbors",
"eastasia": "East Asia",
"betweentwoseas": "Between Two Seas",
"faroeislands": "Faroe Islands",
"deglaciatedantarctica": "Deglaciated Antarctica",
@@ -245,6 +252,14 @@
"view_options": "View Options",
"toggle_view": "Toggle View",
"toggle_view_desc": "Alternate view (terrain/countries)",
"attack_ratio_controls": "Attack Ratio Controls",
"attack_ratio_up": "Increase Attack Ratio",
"attack_ratio_up_desc": "Increase attack ratio by 10%",
"attack_ratio_down": "Decrease Attack Ratio",
"attack_ratio_down_desc": "Decrease attack ratio by 10%",
"attack_keybinds": "Attack Keybinds",
"boat_attack": "Boat Attack",
"boat_attack_desc": "Send a boat attack to the tile under your cursor.",
"zoom_controls": "Zoom Controls",
"zoom_out": "Zoom Out",
"zoom_out_desc": "Zoom out the map",
@@ -358,7 +373,8 @@
"you_won": "You Won!",
"other_won": "{player} has won!",
"exit": "Exit Game",
"keep": "Keep Playing"
"keep": "Keep Playing",
"wishlist": "Wishlist on Steam!"
},
"leaderboard": {
"title": "Leaderboard",
@@ -411,7 +427,9 @@
"start_trade": "Start trading",
"stop_trade": "Stop trading",
"yes": "Yes",
"no": "No"
"no": "No",
"none": "None",
"alliances": "Alliances"
},
"error_modal": {
"crashed": "Game crashed!",
@@ -420,5 +438,8 @@
"copied": "Copied!",
"failed_copy": "Failed to copy",
"desync_notice": "You are desynced from other players. What you see might differ from other players."
},
"heads_up_message": {
"choose_spawn": "Choose a starting location"
}
}
-1
View File
@@ -135,7 +135,6 @@
"random": "Hazarda",
"iceland": "Islando",
"pangaea": "Pangeo",
"japan": "Japanio kaj najbaroj",
"betweentwoseas": "Inter du maroj",
"knownworld": "Konata Mondo",
"faroeislands": "Ferooj",
-1
View File
@@ -135,7 +135,6 @@
"random": "Aleatorio",
"iceland": "Islandia",
"pangaea": "Pangea",
"japan": "Japón y alrededores",
"betweentwoseas": "Entre dos mares",
"knownworld": "El Mundo Conocido",
"faroeislands": "Islas Feroe",
-1
View File
@@ -135,7 +135,6 @@
"random": "Aléatoire",
"iceland": "Islande",
"pangaea": "Pangée",
"japan": "Japon et pays voisins",
"betweentwoseas": "Entre deux mers",
"knownworld": "Monde connu",
"faroeislands": "Îles Féroé",
-1
View File
@@ -135,7 +135,6 @@
"random": "רנדומלי",
"iceland": "איסלנד",
"pangaea": "פנגיאה",
"japan": "יפן ושכנותיה",
"betweentwoseas": "בין שני ימים",
"knownworld": "העולם הידוע",
"faroeislands": "איי פארו",
-1
View File
@@ -118,7 +118,6 @@
"random": "यादृच्छिक",
"iceland": "आइसलैंड",
"pangaea": "पांजिया",
"japan": "जापान और सीमावर्ती देश",
"betweentwoseas": "समुद्रों के मध्य भूमि",
"knownworld": "ज्ञात दुनिया",
"faroeislands": "फ़रो द्वीपसमूह"
-1
View File
@@ -135,7 +135,6 @@
"random": "Casuale",
"iceland": "Islanda",
"pangaea": "Pangea",
"japan": "Giappone e paesi confinanti",
"betweentwoseas": "Tra I Due Mari",
"knownworld": "Mondo Conosciuto",
"faroeislands": "Isole Faroe",
-1
View File
@@ -135,7 +135,6 @@
"random": "ランダム",
"iceland": "アイスランド",
"pangaea": "パンゲア",
"japan": "日本とその隣国",
"betweentwoseas": "2つの海の間",
"knownworld": "知られてる世界",
"faroeislands": "フェロー諸島",
-1
View File
@@ -135,7 +135,6 @@
"random": "Willekeurig",
"iceland": "IJsland",
"pangaea": "Pangea",
"japan": "Japan en buren",
"betweentwoseas": "Tussen twee zeeën",
"knownworld": "Bekende Wereld",
"faroeislands": "Faeröer eilanden",
-1
View File
@@ -122,7 +122,6 @@
"random": "Losowe",
"iceland": "Islandia",
"pangaea": "Pangea",
"japan": "Japonia i sąsiedzi",
"betweentwoseas": "Między dwoma morzami",
"knownworld": "Znany Świat",
"faroeislands": "Wyspy Owcze",
-1
View File
@@ -112,7 +112,6 @@
"pangaea": "Pangeia",
"map": "Mapa",
"betweentwoseas": "Entre Dois Mares",
"japan": "Japão e Vizinhos",
"knownworld": "Mundo Conhecido"
},
"private_lobby": {
-1
View File
@@ -135,7 +135,6 @@
"random": "Случайно",
"iceland": "Исландия",
"pangaea": "Пангея",
"japan": "Япония и соседи",
"betweentwoseas": "Между двух морей",
"knownworld": "Известный мир",
"faroeislands": "Фарерские острова",
-1
View File
@@ -118,7 +118,6 @@
"random": "Nasumična",
"iceland": "Island",
"pangaea": "Pangea",
"japan": "Japan i susjedi",
"betweentwoseas": "Između dva mora",
"knownworld": "Poznati svijet",
"faroeislands": "Farska ostrva",
-1
View File
@@ -122,7 +122,6 @@
"random": "ma nasa",
"iceland": "ma Isilan",
"pangaea": "ma Pansija",
"japan": "ma Nijon en ma poka",
"betweentwoseas": "insa pi telo tu",
"knownworld": "ma ale",
"faroeislands": "ma telo Paja",
-1
View File
@@ -112,7 +112,6 @@
"pangaea": "Pangea",
"map": "Harita",
"betweentwoseas": "İki Deniz Arası",
"japan": "Japonya ve Komşuları",
"knownworld": "Bilinen Dünya"
},
"private_lobby": {
-1
View File
@@ -135,7 +135,6 @@
"random": "Випадково",
"iceland": "Ісландія",
"pangaea": "Пангея",
"japan": "Японія та сусіди",
"betweentwoseas": "Поміж двох морів",
"knownworld": "Відомий світ",
"faroeislands": "Фарерські острови",
@@ -1,10 +1,10 @@
{
"name": "Japan",
"name": "East Asia",
"width": 1562,
"height": 1646,
"nations": [
{
"coordinates": [1151, 709],
"coordinates": [1150, 660],
"name": "Hokkaido",
"strength": 1,
"flag": "jp"
@@ -31,7 +31,7 @@
"coordinates": [1162, 154],
"name": "Sakhalin",
"strength": 2,
"flag": ""
"flag": "Sakhalin"
},
{
"coordinates": [571, 1116],
@@ -40,7 +40,7 @@
"flag": "jp"
},
{
"coordinates": [8612, 1183],
"coordinates": [595, 1190],
"name": "Shikoku",
"strength": 2,
"flag": "jp"

Before

Width:  |  Height:  |  Size: 9.8 MiB

After

Width:  |  Height:  |  Size: 9.8 MiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+6 -6
View File
@@ -187,7 +187,7 @@
"coordinates": [1254, 899],
"name": "Slovak Republic",
"strength": 3,
"flag": "SK"
"flag": "sk"
},
{
"coordinates": [1002, 1061],
@@ -244,7 +244,7 @@
"flag": "gb-sct"
},
{
"coordinates": [2239, 3215],
"coordinates": [2300, 510],
"name": "USSR",
"strength": 3,
"flag": "ussr"
@@ -259,7 +259,7 @@
"coordinates": [1522, 48],
"name": "Polar Bears",
"strength": 2,
"flag": "polar_bear"
"flag": "polar_bears"
},
{
"coordinates": [821, 628],
@@ -280,7 +280,7 @@
"flag": "eg"
},
{
"coordinates": [1188, 1612],
"coordinates": [1115, 1650],
"name": "State of Libya",
"strength": 1,
"flag": "ly"
@@ -289,13 +289,13 @@
"coordinates": [1919, 1608],
"name": "Hashemite Kingdom of Jordan",
"strength": 1,
"flag": "hu"
"flag": "jo"
},
{
"coordinates": [1898, 1535],
"name": "Lebanese Republic",
"strength": 1,
"flag": "hu"
"flag": "lb"
}
]
}
+8 -8
View File
@@ -19,7 +19,7 @@
"coordinates": [1334, 537],
"name": "Duchy of Aquitaine",
"strength": 2,
"flag": "aquitane"
"flag": "aquitaine"
},
{
"coordinates": [2115, 684],
@@ -31,7 +31,7 @@
"coordinates": [1207, 763],
"name": "The Basque",
"strength": 3,
"flag": ""
"flag": "es-pv"
},
{
"coordinates": [1281, 1142],
@@ -49,7 +49,7 @@
"coordinates": [561, 764],
"name": "Kingdom of Galicia",
"strength": 2,
"flag": "galicia"
"flag": "es-ga"
},
{
"coordinates": [1004, 1436],
@@ -115,13 +115,13 @@
"coordinates": [1755, 1130],
"name": "The Old Ones",
"strength": 3,
"flag": "nuragic"
"flag": "neuragic_empire"
},
{
"coordinates": [2097, 1670],
"name": "Jesuit Monks",
"name": "Tamazgha",
"strength": 2,
"flag": ""
"flag": "Amazigh flag"
},
{
"coordinates": [979, 1013],
@@ -151,7 +151,7 @@
"coordinates": [1017, 180],
"name": "Kingdom of Brittany",
"strength": 2,
"flag": "britanny"
"flag": "brittany"
},
{
"coordinates": [2072, 567],
@@ -175,7 +175,7 @@
"coordinates": [1475, 1657],
"name": "French Foreign Legion",
"strength": 3,
"flag": "french_foreign_legion"
"flag": "French foreign legion"
},
{
"coordinates": [1685, 417],
+1 -1
View File
@@ -13,7 +13,7 @@
"coordinates": [122, 750],
"name": "USSR",
"strength": 2,
"flag": ""
"flag": "ussr"
},
{
"coordinates": [1232, 735],
+10 -10
View File
@@ -4,37 +4,37 @@
"height": 1448,
"nations": [
{
"coordinates": [1693, 1045],
"coordinates": [1625, 1040],
"name": "Florida",
"strength": 3,
"flag": "Florida"
},
{
"coordinates": [1001, 427],
"coordinates": [1010, 435],
"name": "Canada",
"strength": 2,
"flag": "ca"
},
{
"coordinates": [1364, 1179],
"coordinates": [1250, 1130],
"name": "Mexico",
"strength": 2,
"flag": "mx"
},
{
"coordinates": [1556, 1295],
"coordinates": [1460, 1275],
"name": "Guatemala",
"strength": 1,
"flag": "gt"
},
{
"coordinates": [1612, 1289],
"coordinates": [1530, 1290],
"name": "Honduras",
"strength": 1,
"flag": "hn"
},
{
"coordinates": [1642, 1348],
"coordinates": [1570, 1350],
"name": "Nicaragua",
"strength": 1,
"flag": "ni"
@@ -58,7 +58,7 @@
"flag": "ve"
},
{
"coordinates": [1775, 1183],
"coordinates": [1725, 1180],
"name": "Cuba",
"strength": 1,
"flag": "cu"
@@ -94,7 +94,7 @@
"flag": "Georgia_US"
},
{
"coordinates": [420, 1209],
"coordinates": [250, 1200],
"name": "Hawaii",
"strength": 1,
"flag": "Hawaii"
@@ -283,10 +283,10 @@
"coordinates": [1189, 240],
"name": "Polar Bears",
"strength": 3,
"flag": "polar_bear"
"flag": "polar_bears"
},
{
"coordinates": [1480, 343],
"coordinates": [1480, 350],
"name": "Frost Giants",
"strength": 3,
"flag": "frost_giant"
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "Americas",
"width": 1746,
"height": 2380,
"height": 2378,
"nations": [
{
"coordinates": [438, 58],
@@ -94,7 +94,7 @@
"flag": "gf"
},
{
"coordinates": [801, 242],
"coordinates": [800, 410],
"name": "Guyana",
"strength": 1,
"flag": "gy"
@@ -133,7 +133,7 @@
"coordinates": [1270, 1035],
"name": "The Biggest Snakes",
"strength": 3,
"flag": ""
"flag": "Aztec Empire"
},
{
"coordinates": [894, 693],
+13 -1
View File
@@ -59,7 +59,7 @@
</head>
<body>
<h1>Privacy Policy</h1>
<p class="updated-date"><strong>Last Updated: April 29, 2025</strong></p>
<p class="updated-date"><strong>Last Updated: May 29, 2025</strong></p>
<p>
This Privacy Policy describes Our policies and procedures on the
@@ -598,6 +598,18 @@
<li>By email: openfrontio@gmail.com</li>
</ul>
<h2>Advertising</h2>
<p>
All or partial advertising on this Website or App is managed by Playwire
LLC. If Playwire publisher advertising services are used, Playwire LLC may
collect and use certain aggregated and anonymized data for advertising
purposes. To learn more about the types of data collected, how data is
used and your choices as a user, please visit
<a href="https://www.playwire.com/privacy-policy"
>https://www.playwire.com/privacy-policy</a
>.
</p>
<div class="footer">
<p>
By using our Service, you acknowledge that you have read and understood
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+92 -56
View File
@@ -1,5 +1,4 @@
import { translateText } from "../client/Utils";
import { consolex, initRemoteSender } from "../core/Consolex";
import { EventBus } from "../core/EventBus";
import {
ClientID,
@@ -13,7 +12,7 @@ import {
import { createGameRecord } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader";
import { Cell, UnitType } from "../core/game/Game";
import { Cell, PlayerActions, UnitType } from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import {
ErrorUpdate,
@@ -26,7 +25,12 @@ import { GameView, PlayerView } from "../core/game/GameView";
import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader";
import { UserSettings } from "../core/game/UserSettings";
import { WorkerClient } from "../core/worker/WorkerClient";
import { InputHandler, MouseMoveEvent, MouseUpEvent } from "./InputHandler";
import {
DoBoatAttackEvent,
InputHandler,
MouseMoveEvent,
MouseUpEvent,
} from "./InputHandler";
import { endGame, startGame, startTime } from "./LocalPersistantStats";
import { getPersistentID } from "./Main";
import {
@@ -58,10 +62,9 @@ export function joinLobby(
onJoin: () => void,
): () => void {
const eventBus = new EventBus();
initRemoteSender(eventBus);
consolex.log(
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
console.log(
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
);
const userSettings: UserSettings = new UserSettings();
@@ -70,21 +73,21 @@ export function joinLobby(
const transport = new Transport(lobbyConfig, eventBus);
const onconnect = () => {
consolex.log(`Joined game lobby ${lobbyConfig.gameID}`);
console.log(`Joined game lobby ${lobbyConfig.gameID}`);
transport.joinGame(0);
};
let terrainLoad: Promise<TerrainMapData> | null = null;
const onmessage = (message: ServerMessage) => {
if (message.type === "prestart") {
consolex.log(`lobby: game prestarting: ${JSON.stringify(message)}`);
console.log(`lobby: game prestarting: ${JSON.stringify(message)}`);
terrainLoad = loadTerrainMap(message.gameMap);
onPrestart();
}
if (message.type === "start") {
// Trigger prestart for singleplayer games
onPrestart();
consolex.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`);
console.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`);
onJoin();
// For multiplayer games, GameStartInfo is not known until game starts.
lobbyConfig.gameStartInfo = message.gameStartInfo;
@@ -99,7 +102,7 @@ export function joinLobby(
};
transport.connect(onconnect, onmessage);
return () => {
consolex.log("leaving game");
console.log("leaving game");
transport.leaveGame();
};
}
@@ -139,17 +142,12 @@ export async function createClientGame(
lobbyConfig.gameStartInfo.gameID,
);
consolex.log("going to init path finder");
consolex.log("inited path finder");
console.log("going to init path finder");
console.log("inited path finder");
const canvas = createCanvas();
const gameRenderer = createRenderer(
canvas,
gameView,
eventBus,
lobbyConfig.clientID,
);
const gameRenderer = createRenderer(canvas, gameView, eventBus);
consolex.log(
console.log(
`creating private game got difficulty: ${lobbyConfig.gameStartInfo.config.difficulty}`,
);
@@ -226,7 +224,7 @@ export class ClientGameRunner {
}
public start() {
consolex.log("starting client game");
console.log("starting client game");
this.isActive = true;
this.lastMessageTime = Date.now();
@@ -238,6 +236,7 @@ export class ClientGameRunner {
}, 20000);
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e));
this.eventBus.on(MouseMoveEvent, (e) => this.onMouseMove(e));
this.eventBus.on(DoBoatAttackEvent, (e) => this.doBoatAttackUnderCursor());
this.renderer.initialize();
this.input.initialize();
@@ -275,14 +274,14 @@ export class ClientGameRunner {
requestAnimationFrame(keepWorkerAlive);
const onconnect = () => {
consolex.log("Connected to game server!");
console.log("Connected to game server!");
this.transport.joinGame(this.turnsSeen);
};
const onmessage = (message: ServerMessage) => {
this.lastMessageTime = Date.now();
if (message.type === "start") {
this.hasJoined = true;
consolex.log("starting game!");
console.log("starting game!");
for (const turn of message.turns) {
if (turn.turnNumber < this.turnsSeen) {
continue;
@@ -317,7 +316,7 @@ export class ClientGameRunner {
return;
}
if (this.turnsSeen !== message.turn.turnNumber) {
consolex.error(
console.error(
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
);
} else {
@@ -350,7 +349,7 @@ export class ClientGameRunner {
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
return;
}
consolex.log(`clicked cell ${cell}`);
console.log(`clicked cell ${cell}`);
const tile = this.gameView.ref(cell.x, cell.y);
if (
this.gameView.isLand(tile) &&
@@ -370,13 +369,6 @@ export class ClientGameRunner {
}
this.myPlayer.actions(tile).then((actions) => {
if (this.myPlayer === null) return;
const bu = actions.buildableUnits.find(
(bu) => bu.type === UnitType.TransportShip,
);
if (bu === undefined) {
console.warn(`no transport ship buildable units`);
return;
}
if (actions.canAttack) {
this.eventBus.emit(
new SendAttackIntentEvent(
@@ -384,31 +376,8 @@ export class ClientGameRunner {
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
),
);
} else if (
bu.canBuild !== false &&
this.shouldBoat(tile, bu.canBuild) &&
this.gameView.isLand(tile)
) {
this.myPlayer
.bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y))
.then((spawn: number | false) => {
if (this.myPlayer === null) throw new Error("not initialized");
let spawnCell: Cell | null = null;
if (spawn !== false) {
spawnCell = new Cell(
this.gameView.x(spawn),
this.gameView.y(spawn),
);
}
this.eventBus.emit(
new SendBoatAttackIntentEvent(
this.gameView.owner(tile).id(),
cell,
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
spawnCell,
),
);
});
} else if (this.canBoatAttack(actions, tile)) {
this.sendBoatAttackIntent(tile, cell);
}
const owner = this.gameView.owner(tile);
@@ -420,6 +389,73 @@ export class ClientGameRunner {
});
}
private doBoatAttackUnderCursor(): void {
if (!this.isActive || !this.lastMousePosition) {
return;
}
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
this.lastMousePosition.x,
this.lastMousePosition.y,
);
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
return;
}
const tile = this.gameView.ref(cell.x, cell.y);
if (this.gameView.inSpawnPhase()) {
return;
}
if (this.myPlayer === null) {
const myPlayer = this.gameView.playerByClientID(this.lobby.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
this.myPlayer.actions(tile).then((actions) => {
if (!actions.canAttack && this.canBoatAttack(actions, tile)) {
this.sendBoatAttackIntent(tile, cell);
}
});
}
private canBoatAttack(actions: PlayerActions, tile: TileRef): boolean {
const bu = actions.buildableUnits.find(
(bu) => bu.type === UnitType.TransportShip,
);
if (bu === undefined) {
console.warn(`no transport ship buildable units`);
return false;
}
return (
bu.canBuild !== false &&
this.shouldBoat(tile, bu.canBuild) &&
this.gameView.isLand(tile)
);
}
private sendBoatAttackIntent(tile: TileRef, cell: Cell) {
if (!this.myPlayer) return;
this.myPlayer
.bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y))
.then((spawn: number | false) => {
if (this.myPlayer === null) throw new Error("not initialized");
let spawnCell: Cell | null = null;
if (spawn !== false) {
spawnCell = new Cell(this.gameView.x(spawn), this.gameView.y(spawn));
}
this.eventBus.emit(
new SendBoatAttackIntentEvent(
this.gameView.owner(tile).id(),
cell,
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
spawnCell,
),
);
});
}
private shouldBoat(tile: TileRef, src: TileRef) {
// TODO: Global enable flag
// TODO: Global limit autoboat to nearby shore flag
+5 -5
View File
@@ -473,7 +473,7 @@ export class HelpModal extends LitElement {
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
>
<div
class="text-gray-300 h-8 md:h-10 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
>
${translateText("help_modal.icon_crown")}
</div>
@@ -489,7 +489,7 @@ export class HelpModal extends LitElement {
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
>
<div
class="text-gray-300 h-8 md:h-10 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
>
${translateText("help_modal.icon_traitor")}
</div>
@@ -505,7 +505,7 @@ export class HelpModal extends LitElement {
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
>
<div
class="text-gray-300 h-8 md:h-10 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
>
${translateText("help_modal.icon_ally")}
</div>
@@ -523,7 +523,7 @@ export class HelpModal extends LitElement {
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
>
<div
class="text-gray-300 h-8 md:h-10 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
>
${translateText("help_modal.icon_embargo")}
</div>
@@ -539,7 +539,7 @@ export class HelpModal extends LitElement {
class="flex flex-col items-center w-full md:w-1/3 mb-2 md:mb-0"
>
<div
class="text-gray-300 h-8 md:h-10 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
class="text-gray-300 flex flex-col justify-start min-h-[3rem] w-full px-2 mb-1"
>
${translateText("help_modal.icon_request")}
</div>
+19 -59
View File
@@ -3,7 +3,6 @@ import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { consolex } from "../core/Consolex";
import {
Difficulty,
Duos,
@@ -19,6 +18,7 @@ import "./components/Difficulties";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import { JoinLobbyEvent } from "./Main";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@@ -314,59 +314,10 @@ export class HostLobbyModal extends LitElement {
<div
style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"
>
${[
[UnitType.City, "unit_type.city"],
[UnitType.DefensePost, "unit_type.defense_post"],
[UnitType.Port, "unit_type.port"],
[UnitType.Warship, "unit_type.warship"],
[UnitType.MissileSilo, "unit_type.missile_silo"],
[UnitType.SAMLauncher, "unit_type.sam_launcher"],
[UnitType.AtomBomb, "unit_type.atom_bomb"],
[UnitType.HydrogenBomb, "unit_type.hydrogen_bomb"],
[UnitType.MIRV, "unit_type.mirv"],
].map(
([unitType, translationKey]: [UnitType, string]) => html`
<label
class="option-card ${this.disabledUnits.includes(
unitType,
)
? ""
: "selected"}"
style="width: 140px;"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement)
.checked;
const parsedUnitType =
UnitType[unitType as keyof typeof UnitType];
if (parsedUnitType) {
if (checked) {
this.disabledUnits = [
...this.disabledUnits,
parsedUnitType,
];
} else {
this.disabledUnits = this.disabledUnits.filter(
(u) => u !== parsedUnitType,
);
}
this.putGameConfig();
}
}}
.checked=${this.disabledUnits.includes(unitType)}
/>
<div
class="option-card-title"
style="text-align: center;"
>
${translateText(translationKey)}
</div>
</label>
`,
)}
${renderUnitTypeOptions({
disabledUnits: this.disabledUnits,
toggleUnit: this.toggleUnit.bind(this),
})}
</div>
</div>
</div>
@@ -505,7 +456,7 @@ export class HostLobbyModal extends LitElement {
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
consolex.log(`updating disable npcs to ${this.disableNPCs}`);
console.log(`updating disable npcs to ${this.disableNPCs}`);
this.putGameConfig();
}
@@ -545,6 +496,15 @@ export class HostLobbyModal extends LitElement {
return response;
}
private toggleUnit(unit: UnitType, checked: boolean): void {
console.log(`Toggling unit type: ${unit} to ${checked}`);
this.disabledUnits = checked
? [...this.disabledUnits, unit]
: this.disabledUnits.filter((u) => u !== unit);
this.putGameConfig();
}
private getRandomMap(): GameMapType {
const maps = Object.values(GameMapType);
const randIdx = Math.floor(Math.random() * maps.length);
@@ -557,7 +517,7 @@ export class HostLobbyModal extends LitElement {
}
await this.putGameConfig();
consolex.log(
console.log(
`Starting private game with map: ${GameMapType[this.selectedMap]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
this.close();
@@ -585,7 +545,7 @@ export class HostLobbyModal extends LitElement {
this.copySuccess = false;
}, 2000);
} catch (err) {
consolex.error(`Failed to copy text: ${err}`);
console.error(`Failed to copy text: ${err}`);
}
}
@@ -625,11 +585,11 @@ async function createLobby(): Promise<GameInfo> {
}
const data = await response.json();
consolex.log("Success:", data);
console.log("Success:", data);
return data as GameInfo;
} catch (error) {
consolex.error("Error creating lobby:", error);
console.error("Error creating lobby:", error);
throw error; // Re-throw the error so the caller can handle it
}
}
+14 -4
View File
@@ -76,6 +76,8 @@ export class ShowEmojiMenuEvent implements GameEvent {
) {}
}
export class DoBoatAttackEvent implements GameEvent {}
export class AttackRatioEvent implements GameEvent {
constructor(public readonly attackRatio: number) {}
}
@@ -122,6 +124,9 @@ export class InputHandler {
moveRight: "KeyD",
zoomOut: "KeyQ",
zoomIn: "KeyE",
attackRatioDown: "Digit1",
attackRatioUp: "Digit2",
boatAttack: "KeyB",
...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"),
};
this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e));
@@ -218,8 +223,8 @@ export class InputHandler {
"ArrowRight",
"Minus",
"Equal",
"Digit1",
"Digit2",
keybinds.attackRatioDown,
keybinds.attackRatioUp,
keybinds.centerCamera,
"ControlLeft",
"ControlRight",
@@ -240,12 +245,17 @@ export class InputHandler {
this.eventBus.emit(new RefreshGraphicsEvent());
}
if (e.code === "Digit1") {
if (e.code === keybinds.boatAttack) {
e.preventDefault();
this.eventBus.emit(new DoBoatAttackEvent());
}
if (e.code === keybinds.attackRatioDown) {
e.preventDefault();
this.eventBus.emit(new AttackRatioEvent(-10));
}
if (e.code === "Digit2") {
if (e.code === keybinds.attackRatioUp) {
e.preventDefault();
this.eventBus.emit(new AttackRatioEvent(10));
}
+5 -6
View File
@@ -1,7 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { consolex } from "../core/Consolex";
import { GameInfo, GameRecord } from "../core/Schemas";
import { generateID } from "../core/Util";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
@@ -145,13 +144,13 @@ export class JoinPrivateLobbyModal extends LitElement {
this.lobbyIdInput.value = lobbyId;
} catch (err) {
consolex.error("Failed to read clipboard contents: ", err);
console.error("Failed to read clipboard contents: ", err);
}
}
private async joinLobby(): Promise<void> {
const lobbyId = this.lobbyIdInput.value;
consolex.log(`Joining lobby with ID: ${lobbyId}`);
console.log(`Joining lobby with ID: ${lobbyId}`);
this.message = `${translateText("private_lobby.checking")}`;
try {
@@ -165,7 +164,7 @@ export class JoinPrivateLobbyModal extends LitElement {
this.message = `${translateText("private_lobby.not_found")}`;
} catch (error) {
consolex.error("Error checking lobby existence:", error);
console.error("Error checking lobby existence:", error);
this.message = `${translateText("private_lobby.error")}`;
}
}
@@ -218,7 +217,7 @@ export class JoinPrivateLobbyModal extends LitElement {
archiveData.success === false &&
archiveData.error === "Version mismatch"
) {
consolex.warn(
console.warn(
`Git commit hash mismatch for game ${lobbyId}`,
archiveData.details,
);
@@ -266,7 +265,7 @@ export class JoinPrivateLobbyModal extends LitElement {
this.players = data.clients?.map((p) => p.username) ?? [];
})
.catch((error) => {
consolex.error("Error polling players:", error);
console.error("Error polling players:", error);
});
}
}
+11 -3
View File
@@ -78,10 +78,18 @@ export class LangSelector extends LitElement {
});
}
private getClosestSupportedLang(lang: string): string {
if (!lang) return "en";
if (lang in this.languageMap) return lang;
const base = lang.split("-")[0];
if (base in this.languageMap) return base;
return "en";
}
private async initializeLanguage() {
const locale = new Intl.Locale(navigator.language);
const defaultLang = locale.language;
const userLang = localStorage.getItem("lang") || defaultLang;
const browserLocale = navigator.language;
const savedLang = localStorage.getItem("lang");
const userLang = this.getClosestSupportedLang(savedLang || browserLocale);
this.defaultTranslations = await this.loadLanguage("en");
this.translations = await this.loadLanguage(userLang);
+1 -2
View File
@@ -1,4 +1,3 @@
import { consolex } from "../core/Consolex";
import { GameConfig, GameID, GameRecord } from "../core/Schemas";
import { replacer } from "../core/Util";
@@ -51,7 +50,7 @@ export function endGame(gameRecord: GameRecord) {
const gameStat = stats[gameRecord.info.gameID];
if (!gameStat) {
consolex.log("LocalPersistantStats: game not found");
console.log("LocalPersistantStats: game not found");
return;
}
+8 -6
View File
@@ -1,4 +1,3 @@
import { consolex } from "../core/Consolex";
import {
AllPlayersStats,
ClientMessage,
@@ -30,7 +29,7 @@ export class LocalServer {
private allPlayersStats: AllPlayersStats = {};
private turnsExecuted = 0;
private lastTurnCompletedTime = 0;
private turnStartTime = 0;
private turnCheckInterval: NodeJS.Timeout;
@@ -47,9 +46,10 @@ export class LocalServer {
if (
this.isReplay ||
Date.now() >
this.lastTurnCompletedTime +
this.lobbyConfig.serverConfig.turnIntervalMs()
this.turnStartTime + this.lobbyConfig.serverConfig.turnIntervalMs()
) {
this.turnStartTime = Date.now();
// End turn on the server means the client will start processing the turn.
this.endTurn();
}
}
@@ -140,11 +140,13 @@ export class LocalServer {
}
}
// This is so the client can tell us when it finished processing the turn.
public turnComplete() {
this.turnsExecuted++;
this.lastTurnCompletedTime = Date.now();
}
// endTurn in this context means the server has collected all the intents
// and will send the turn to the client.
private endTurn() {
if (this.paused) {
return;
@@ -169,7 +171,7 @@ export class LocalServer {
}
public endGame(saveFullGame: boolean = false) {
consolex.log("local server ending game");
console.log("local server ending game");
clearInterval(this.turnCheckInterval);
if (this.isReplay) {
return;
+17 -25
View File
@@ -1,6 +1,5 @@
import page from "page";
import favicon from "../../resources/images/Favicon.svg";
import { consolex } from "../core/Consolex";
import { GameRecord, GameStartInfo } from "../core/Schemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
@@ -62,20 +61,20 @@ class Client {
initialize(): void {
const newsModal = document.querySelector("news-modal") as NewsModal;
if (!newsModal) {
consolex.warn("News modal element not found");
console.warn("News modal element not found");
} else {
consolex.log("News modal element found");
console.log("News modal element found");
}
newsModal instanceof NewsModal;
const newsButton = document.querySelector("news-button") as NewsButton;
if (!newsButton) {
consolex.warn("News button element not found");
console.warn("News button element not found");
} else {
consolex.log("News button element found");
console.log("News button element found");
}
// Comment out to show news button.
newsButton.hidden = true;
// newsButton.hidden = true;
const langSelector = document.querySelector(
"lang-selector",
@@ -84,22 +83,22 @@ class Client {
"lang-selector",
) as LanguageModal;
if (!langSelector) {
consolex.warn("Lang selector element not found");
console.warn("Lang selector element not found");
}
if (!LanguageModal) {
consolex.warn("Language modal element not found");
console.warn("Language modal element not found");
}
this.flagInput = document.querySelector("flag-input") as FlagInput;
if (!this.flagInput) {
consolex.warn("Flag input element not found");
console.warn("Flag input element not found");
}
this.darkModeButton = document.querySelector(
"dark-mode-button",
) as DarkModeButton;
if (!this.darkModeButton) {
consolex.warn("Dark mode button element not found");
console.warn("Dark mode button element not found");
}
const loginDiscordButton = document.getElementById(
@@ -113,7 +112,7 @@ class Client {
"username-input",
) as UsernameInput;
if (!this.usernameInput) {
consolex.warn("Username input element not found");
console.warn("Username input element not found");
}
this.publicLobby = document.querySelector("public-lobby") as PublicLobby;
@@ -122,7 +121,7 @@ class Client {
) as NodeListOf<GoogleAdElement>;
window.addEventListener("beforeunload", () => {
consolex.log("Browser is closing");
console.log("Browser is closing");
if (this.gameStop !== null) {
this.gameStop();
}
@@ -188,8 +187,8 @@ class Client {
logoutDiscordButton.hidden = true;
return;
}
// TODO: Update the page for logged in user
loginDiscordButton.translationKey = "main.logged_in";
loginDiscordButton.hidden = true;
const { user, player } = userMeResponse;
});
}
@@ -240,21 +239,14 @@ class Client {
page("/join/:lobbyId", (ctx) => {
if (ctx.init && sessionStorage.getItem("inLobby")) {
// On page reload, go back home
page.redirect("/");
page("/");
return;
}
const lobbyId = ctx.params.lobbyId;
if (lobbyId?.endsWith("#")) {
// When the cookies button is pressed, '#' is added to the url
// causing the page to attempt to rejoin the lobby during game play.
console.error("Invalid lobby ID provided");
return;
}
this.joinModal.open(lobbyId);
consolex.log(`joining lobby ${lobbyId}`);
console.log(`joining lobby ${lobbyId}`);
});
page();
@@ -274,9 +266,9 @@ class Client {
private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail as JoinLobbyEvent;
consolex.log(`joining lobby ${lobby.gameID}`);
console.log(`joining lobby ${lobby.gameID}`);
if (this.gameStop !== null) {
consolex.log("joining lobby, stopping existing game");
console.log("joining lobby, stopping existing game");
this.gameStop();
}
const config = await getServerConfigFromClient();
@@ -348,7 +340,7 @@ class Client {
if (this.gameStop === null) {
return;
}
consolex.log("leaving lobby, cancelling game");
console.log("leaving lobby, cancelling game");
this.gameStop();
this.gameStop = null;
this.publicLobby.leaveLobby();
+33 -6
View File
@@ -12,6 +12,10 @@ export class NewsModal extends LitElement {
};
static styles = css`
:host {
display: block;
}
.news-container {
max-height: 60vh;
overflow-y: auto;
@@ -24,10 +28,20 @@ export class NewsModal extends LitElement {
.news-content {
color: #ddd;
line-height: 1.5;
background: rgba(255, 255, 255, 0.05);
background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
padding: 1rem;
}
.news-content a {
color: #4a9eff !important;
text-decoration: underline !important;
transition: color 0.2s ease;
}
.news-content a:hover {
color: #6fb3ff !important;
}
`;
render() {
@@ -36,7 +50,24 @@ export class NewsModal extends LitElement {
<div class="options-layout">
<div class="options-section">
<div class="news-container">
<div class="news-content">INSERT NEWS HERE</div>
<div class="news-content">
<h3>Main things to note:</h3>
<br />
<ul>
<li>Workers reproduce faster than troops.</li>
<li>Defense = troops divided how much land you have.</li>
<li>Attacking troops count toward your population limit.</li>
</ul>
<br />
<br />
See full changelog
<a
href="https://discord.com/channels/1284581928254701718/1286745902320713780"
target="_blank"
style="color: #4a9eff; font-weight: bold;"
>here</a
>.
</div>
</div>
</div>
</div>
@@ -58,8 +89,4 @@ export class NewsModal extends LitElement {
private close() {
this.modalEl?.close();
}
createRenderRoot() {
return this; // light DOM
}
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { consolex } from "../core/Consolex";
import { GameMode } from "../core/game/Game";
import { GameID, GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
@@ -51,7 +50,7 @@ export class PublicLobby extends LitElement {
}
});
} catch (error) {
consolex.error("Error fetching lobbies:", error);
console.error("Error fetching lobbies:", error);
}
}
@@ -63,7 +62,7 @@ export class PublicLobby extends LitElement {
const data = await response.json();
return data.lobbies;
} catch (error) {
consolex.error("Error fetching lobbies:", error);
console.error("Error fetching lobbies:", error);
throw error;
}
}
+16 -47
View File
@@ -2,7 +2,6 @@ import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import randomMap from "../../resources/images/RandomMap.webp";
import { translateText } from "../client/Utils";
import { consolex } from "../core/Consolex";
import {
Difficulty,
Duos,
@@ -21,6 +20,7 @@ import "./components/Maps";
import { FlagInput } from "./FlagInput";
import { JoinLobbyEvent } from "./Main";
import { UsernameInput } from "./UsernameInput";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
@customElement("single-player-modal")
export class SinglePlayerModal extends LitElement {
@@ -39,7 +39,7 @@ export class SinglePlayerModal extends LitElement {
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: number | typeof Duos = 2;
@state() private disabledUnits: string[] = [];
@state() private disabledUnits: UnitType[] = [];
render() {
return html`
@@ -284,48 +284,10 @@ export class SinglePlayerModal extends LitElement {
<div
style="display: flex; flex-wrap: wrap; justify-content: center; gap: 12px;"
>
${[
[UnitType.City, "unit_type.city"],
[UnitType.DefensePost, "unit_type.defense_post"],
[UnitType.Port, "unit_type.port"],
[UnitType.Warship, "unit_type.warship"],
[UnitType.MissileSilo, "unit_type.missile_silo"],
[UnitType.SAMLauncher, "unit_type.sam_launcher"],
[UnitType.AtomBomb, "unit_type.atom_bomb"],
[UnitType.HydrogenBomb, "unit_type.hydrogen_bomb"],
[UnitType.MIRV, "unit_type.mirv"],
].map(
([unitType, translationKey]) => html`
<label
class="option-card ${this.disabledUnits.includes(unitType)
? ""
: "selected"}"
style="width: 140px;"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
if (checked) {
this.disabledUnits = [
...this.disabledUnits,
unitType,
];
} else {
this.disabledUnits = this.disabledUnits.filter(
(u) => u !== unitType,
);
}
}}
.checked=${this.disabledUnits.includes(unitType)}
/>
<div class="option-card-title" style="text-align: center;">
${translateText(translationKey)}
</div>
</label>
`,
)}
${renderUnitTypeOptions({
disabledUnits: this.disabledUnits,
toggleUnit: this.toggleUnit.bind(this),
})}
</div>
</div>
</div>
@@ -403,13 +365,20 @@ export class SinglePlayerModal extends LitElement {
return maps[randIdx] as GameMapType;
}
private toggleUnit(unit: UnitType, checked: boolean): void {
console.log(`Toggling unit type: ${unit} to ${checked}`);
this.disabledUnits = checked
? [...this.disabledUnits, unit]
: this.disabledUnits.filter((u) => u !== unit);
}
private startGame() {
// If random map is selected, choose a random map now
if (this.useRandomMap) {
this.selectedMap = this.getRandomMap();
}
consolex.log(
console.log(
`Starting single player game with map: ${GameMapType[this.selectedMap]}${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
const clientID = generateID();
@@ -419,12 +388,12 @@ export class SinglePlayerModal extends LitElement {
"username-input",
) as UsernameInput;
if (!usernameInput) {
consolex.warn("Username input element not found");
console.warn("Username input element not found");
}
const flagInput = document.querySelector("flag-input") as FlagInput;
if (!flagInput) {
consolex.warn("Flag input element not found");
console.warn("Flag input element not found");
}
this.dispatchEvent(
new CustomEvent("join-lobby", {
+5 -25
View File
@@ -1,9 +1,9 @@
import { SendLogEvent } from "../core/Consolex";
import { EventBus, GameEvent } from "../core/EventBus";
import {
AllPlayers,
Cell,
GameType,
Gold,
PlayerID,
PlayerType,
Tick,
@@ -15,7 +15,6 @@ import {
ClientHashMessage,
ClientIntentMessage,
ClientJoinMessage,
ClientLogMessage,
ClientPingMessage,
ClientSendWinnerMessage,
Intent,
@@ -94,15 +93,13 @@ export class SendEmojiIntentEvent implements GameEvent {
export class SendDonateGoldIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly gold: number | null,
public readonly gold: Gold | null,
) {}
}
export class SendDonateTroopsIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly troops: number | null,
) {}
@@ -110,7 +107,6 @@ export class SendDonateTroopsIntentEvent implements GameEvent {
export class SendQuickChatEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly recipient: PlayerView,
public readonly quickChatKey: string,
public readonly variables: { [key: string]: string },
@@ -119,17 +115,13 @@ export class SendQuickChatEvent implements GameEvent {
export class SendEmbargoIntentEvent implements GameEvent {
constructor(
public readonly sender: PlayerView,
public readonly target: PlayerView,
public readonly action: "start" | "stop",
) {}
}
export class CancelAttackIntentEvent implements GameEvent {
constructor(
public readonly playerID: PlayerID,
public readonly attackID: string,
) {}
constructor(public readonly attackID: string) {}
}
export class CancelBoatIntentEvent implements GameEvent {
@@ -217,7 +209,6 @@ export class Transport {
);
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e));
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e));
this.eventBus.on(SendHashEvent, (e) => this.onSendHashEvent(e));
@@ -342,16 +333,6 @@ export class Transport {
}
}
private onSendLogEvent(event: SendLogEvent) {
this.sendMsg(
JSON.stringify({
type: "log",
log: event.log,
severity: event.severity,
} satisfies ClientLogMessage),
);
}
joinGame(numTurns: number) {
this.sendMsg(
JSON.stringify({
@@ -548,8 +529,7 @@ export class Transport {
}
private onSendHashEvent(event: SendHashEvent) {
if (this.socket === null) return;
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) {
this.sendMsg(
JSON.stringify({
type: "hash",
@@ -560,7 +540,7 @@ export class Transport {
} else {
console.log(
"WebSocket is not open. Current state:",
this.socket.readyState,
this.socket!.readyState,
);
console.log("attempting reconnect");
}
+35
View File
@@ -345,6 +345,41 @@ export class UserSettingModal extends LitElement {
@change=${this.handleKeybindChange}
></setting-keybind>
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
${translateText("user_setting.attack_ratio_controls")}
</div>
<setting-keybind
action="attackRatioDown"
label=${translateText("user_setting.attack_ratio_down")}
description=${translateText("user_setting.attack_ratio_down_desc")}
defaultKey="Digit1"
.value=${this.keybinds["attackRatioDown"] ?? ""}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="attackRatioUp"
label=${translateText("user_setting.attack_ratio_up")}
description=${translateText("user_setting.attack_ratio_up_desc")}
defaultKey="Digit2"
.value=${this.keybinds["attackRatioUp"] ?? ""}
@change=${this.handleKeybindChange}
></setting-keybind>
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
${translateText("user_setting.attack_keybinds")}
</div>
<setting-keybind
action="boatAttack"
label=${translateText("user_setting.boat_attack")}
description=${translateText("user_setting.boat_attack_desc")}
defaultKey="KeyB"
.value=${this.keybinds["boatAttack"] ?? ""}
@change=${this.handleKeybindChange}
></setting-keybind>
<div class="text-center text-white text-base font-semibold mt-5 mb-2">
${translateText("user_setting.zoom_controls")}
</div>
+1 -1
View File
@@ -47,7 +47,7 @@ export class UsernameInput extends LitElement {
/>
${this.validationError
? html`<div
class="absolute w-full mt-2 px-3 py-1 text-lg border rounded bg-white text-red-600 border-red-600 dark:bg-gray-700 dark:text-red-300 dark:border-red-300"
class="absolute z-10 w-full mt-2 px-3 py-1 text-lg border rounded bg-white text-red-600 border-red-600 dark:bg-gray-700 dark:text-red-300 dark:border-red-300"
>
${this.validationError}
</div>`
+2 -1
View File
@@ -4,7 +4,8 @@ export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
}
export function renderNumber(num: number): string {
export function renderNumber(num: number | bigint): string {
num = Number(num);
num = Math.max(num, 0);
if (num >= 10_000_000) {
+1 -1
View File
@@ -22,7 +22,7 @@ export const MapDescription: Record<keyof typeof GameMapType, string> = {
GatewayToTheAtlantic: "Gateway to the Atlantic",
Australia: "Australia",
Iceland: "Iceland",
Japan: "Japan",
EastAsia: "East Asia",
BetweenTwoSeas: "Between Two Seas",
FaroeIslands: "Faroe Islands",
DeglaciatedAntarctica: "Deglaciated Antarctica",
@@ -54,7 +54,7 @@ export class OModal extends LitElement {
color: #fff;
padding: 1.4rem;
max-height: 60dvh;
overflow-y: scroll;
overflow-y: auto;
backdrop-filter: blur(8px);
}
`;
+5 -5
View File
@@ -473,7 +473,7 @@
"name": "Comoros"
},
{
"code": "communist flag",
"code": "Communist flag",
"name": "Communist Flag"
},
{
@@ -740,7 +740,7 @@
"name": "Franks"
},
{
"code": "french foreign legion",
"code": "French foreign legion",
"name": "French Foreign Legion"
},
{
@@ -1188,7 +1188,7 @@
"name": "Luxembourg"
},
{
"code": "lydia",
"code": "Lydia",
"continent": "Asia",
"name": "Lydia"
},
@@ -1238,7 +1238,7 @@
"name": "Malta"
},
{
"code": "Māori Flag",
"code": "Māori flag",
"continent": "Oceania",
"name": "Māori Flag"
},
@@ -1466,7 +1466,7 @@
"name": "Normandy"
},
{
"code": "North Karelia",
"code": "North karelia",
"continent": "Europe",
"name": "North Karelia"
},
+26 -29
View File
@@ -1,6 +1,4 @@
import { consolex } from "../../core/Consolex";
import { EventBus } from "../../core/EventBus";
import { ClientID } from "../../core/Schemas";
import { GameView } from "../../core/game/GameView";
import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
@@ -13,15 +11,16 @@ import { ControlPanel } from "./layers/ControlPanel";
import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FxLayer } from "./layers/FxLayer";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
import { OptionsMenu } from "./layers/OptionsMenu";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { PlayerTeamLabel } from "./layers/PlayerTeamLabel";
import { RadialMenu } from "./layers/RadialMenu";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
@@ -37,7 +36,6 @@ export function createRenderer(
canvas: HTMLCanvasElement,
game: GameView,
eventBus: EventBus,
clientID: ClientID,
): GameRenderer {
const transformHandler = new TransformHandler(game, eventBus, canvas);
@@ -52,7 +50,7 @@ export function createRenderer(
// TODO maybe append this to dcoument instead of querying for them?
const emojiTable = document.querySelector("emoji-table") as EmojiTable;
if (!emojiTable || !(emojiTable instanceof EmojiTable)) {
consolex.error("EmojiTable element not found in the DOM");
console.error("EmojiTable element not found in the DOM");
}
emojiTable.eventBus = eventBus;
emojiTable.transformHandler = transformHandler;
@@ -61,32 +59,29 @@ export function createRenderer(
const buildMenu = document.querySelector("build-menu") as BuildMenu;
if (!buildMenu || !(buildMenu instanceof BuildMenu)) {
consolex.error("BuildMenu element not found in the DOM");
console.error("BuildMenu element not found in the DOM");
}
buildMenu.game = game;
buildMenu.eventBus = eventBus;
const leaderboard = document.querySelector("leader-board") as Leaderboard;
if (!emojiTable || !(leaderboard instanceof Leaderboard)) {
consolex.error("EmojiTable element not found in the DOM");
console.error("EmojiTable element not found in the DOM");
}
leaderboard.clientID = clientID;
leaderboard.eventBus = eventBus;
leaderboard.game = game;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!emojiTable || !(teamStats instanceof TeamStats)) {
consolex.error("EmojiTable element not found in the DOM");
console.error("EmojiTable element not found in the DOM");
}
teamStats.clientID = clientID;
teamStats.eventBus = eventBus;
teamStats.game = game;
const controlPanel = document.querySelector("control-panel") as ControlPanel;
if (!(controlPanel instanceof ControlPanel)) {
consolex.error("ControlPanel element not found in the DOM");
console.error("ControlPanel element not found in the DOM");
}
controlPanel.clientID = clientID;
controlPanel.eventBus = eventBus;
controlPanel.uiState = uiState;
controlPanel.game = game;
@@ -95,28 +90,25 @@ export function createRenderer(
"events-display",
) as EventsDisplay;
if (!(eventsDisplay instanceof EventsDisplay)) {
consolex.error("events display not found");
console.error("events display not found");
}
eventsDisplay.eventBus = eventBus;
eventsDisplay.game = game;
eventsDisplay.clientID = clientID;
const chatDisplay = document.querySelector("chat-display") as ChatDisplay;
if (!(chatDisplay instanceof ChatDisplay)) {
consolex.error("chat display not found");
console.error("chat display not found");
}
chatDisplay.eventBus = eventBus;
chatDisplay.game = game;
chatDisplay.clientID = clientID;
const playerInfo = document.querySelector(
"player-info-overlay",
) as PlayerInfoOverlay;
if (!(playerInfo instanceof PlayerInfoOverlay)) {
consolex.error("player info overlay not found");
console.error("player info overlay not found");
}
playerInfo.eventBus = eventBus;
playerInfo.clientID = clientID;
playerInfo.transform = transformHandler;
playerInfo.game = game;
@@ -172,6 +164,14 @@ export function createRenderer(
}
playerTeamLabel.game = game;
const headsUpMessage = document.querySelector(
"heads-up-message",
) as HeadsUpMessage;
if (!(headsUpMessage instanceof HeadsUpMessage)) {
console.error("heads-up message not found");
}
headsUpMessage.game = game;
const unitInfoModal = document.querySelector(
"unit-info-modal",
) as UnitInfoModal;
@@ -190,20 +190,19 @@ export function createRenderer(
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus),
new TerritoryLayer(game, eventBus, transformHandler),
structureLayer,
new UnitLayer(game, eventBus, clientID, transformHandler),
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new UILayer(game, eventBus, clientID, transformHandler),
new NameLayer(game, transformHandler, clientID),
new UILayer(game, eventBus, transformHandler),
new NameLayer(game, transformHandler),
eventsDisplay,
chatDisplay,
buildMenu,
new RadialMenu(
new MainRadialMenu(
eventBus,
game,
transformHandler,
clientID,
emojiTable as EmojiTable,
buildMenu,
uiState,
@@ -220,6 +219,7 @@ export function createRenderer(
topBar,
playerPanel,
playerTeamLabel,
headsUpMessage,
unitInfoModal,
multiTabModal,
];
@@ -265,11 +265,8 @@ export class GameRenderer {
window.addEventListener("resize", () => this.resizeCanvas());
this.resizeCanvas();
this.transformHandler = new TransformHandler(
this.game,
this.eventBus,
this.canvas,
);
//show whole map on startup
this.transformHandler.centerAll(0.9);
requestAnimationFrame(() => this.renderGame());
}
+4 -3
View File
@@ -125,10 +125,11 @@ export const getColoredSprite = (
customBorderColor?: Colord,
): HTMLCanvasElement => {
const owner = unit.owner();
const territoryColor = customTerritoryColor ?? theme.territoryColor(owner);
const borderColor = customBorderColor ?? theme.borderColor(owner);
const territoryColor: Colord =
customTerritoryColor ?? theme.territoryColor(owner);
const borderColor: Colord = customBorderColor ?? theme.borderColor(owner);
const spawnHighlightColor = theme.spawnHighlightColor();
const key = `${unit.type()}-${owner.id()}`;
const key = `${unit.type()}-${owner.id()}-${territoryColor.toRgbString()}-${borderColor.toRgbString()}`;
if (coloredSpriteCache.has(key)) {
return coloredSpriteCache.get(key)!;
+27
View File
@@ -257,4 +257,31 @@ export class TransformHandler {
}
this.target = null;
}
override(x: number = 0, y: number = 0, s: number = 1) {
//hardset view position
this.clearTarget();
this.offsetX = x;
this.offsetY = y;
this.scale = s;
this.changed = true;
}
centerAll(fit: number = 1) {
//position entire map centered on the screen
const vpWidth = this.boundingRect().width;
const vpHeight = this.boundingRect().height;
const mapWidth = this.game.width();
const mapHeight = this.game.height();
const scHor = (vpWidth / mapWidth) * fit;
const scVer = (vpHeight / mapHeight) * fit;
const tScale = Math.min(scHor, scVer);
const oHor = (mapWidth - vpWidth) / 2 / tScale;
const oVer = (mapHeight - vpHeight) / 2 / tScale;
this.override(oHor, oVer, tScale);
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
import { Theme } from "../../../core/configuration/Config";
import { consolex } from "../../../core/Consolex";
import { PlayerView } from "../../../core/game/GameView";
import { AnimatedSprite } from "../AnimatedSprite";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
@@ -62,7 +61,7 @@ export class SpriteFx implements Fx {
theme,
);
if (!this.animatedSprite) {
consolex.error("Could not load animated sprite", fxType);
console.error("Could not load animated sprite", fxType);
} else {
this.duration = duration ?? this.animatedSprite.lifeTime() ?? 1000;
}
+10 -8
View File
@@ -12,14 +12,14 @@ import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.s
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions, UnitType } from "../../../core/game/Game";
import { Cell, Gold, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView } from "../../../core/game/GameView";
import { BuildUnitIntentEvent } from "../../Transport";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
interface BuildItemDisplay {
export interface BuildItemDisplay {
unitType: UnitType;
icon: string;
description?: string;
@@ -27,7 +27,7 @@ interface BuildItemDisplay {
countable?: boolean;
}
const buildTable: BuildItemDisplay[][] = [
export const buildTable: BuildItemDisplay[][] = [
[
{
unitType: UnitType.AtomBomb,
@@ -96,12 +96,14 @@ const buildTable: BuildItemDisplay[][] = [
],
];
export const flattenedBuildTable = buildTable.flat();
@customElement("build-menu")
export class BuildMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
private clickedTile: TileRef;
private playerActions: PlayerActions | null;
public playerActions: PlayerActions | null;
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
tick() {
@@ -302,7 +304,7 @@ export class BuildMenu extends LitElement implements Layer {
@state()
private _hidden = true;
private canBuild(item: BuildItemDisplay): boolean {
public canBuild(item: BuildItemDisplay): boolean {
if (this.game?.myPlayer() === null || this.playerActions === null) {
return false;
}
@@ -314,16 +316,16 @@ export class BuildMenu extends LitElement implements Layer {
return unit[0].canBuild !== false;
}
private cost(item: BuildItemDisplay): number {
public cost(item: BuildItemDisplay): Gold {
for (const bu of this.playerActions?.buildableUnits ?? []) {
if (bu.type === item.unitType) {
return bu.cost;
}
}
return 0;
return 0n;
}
private count(item: BuildItemDisplay): string {
public count(item: BuildItemDisplay): string {
const player = this.game?.myPlayer();
if (!player) {
return "?";
+2 -4
View File
@@ -9,7 +9,6 @@ import {
GameUpdateType,
} from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { onlyImages } from "../../../core/Util";
import { Layer } from "./Layer";
@@ -24,7 +23,6 @@ interface ChatEvent {
export class ChatDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
public clientID: ClientID;
private active: boolean = false;
@@ -61,7 +59,7 @@ export class ChatDisplay extends LitElement implements Layer {
onDisplayMessageEvent(event: DisplayMessageUpdate) {
if (event.messageType !== MessageType.CHAT) return;
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (
event.playerID !== null &&
(!myPlayer || myPlayer.smallID() !== event.playerID)
@@ -90,7 +88,7 @@ export class ChatDisplay extends LitElement implements Layer {
if (messages) {
for (const msg of messages) {
if (msg.messageType === MessageType.CHAT) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (
msg.playerID !== null &&
(!myPlayer || myPlayer.smallID() !== msg.playerID)
@@ -0,0 +1,102 @@
import { EventBus } from "../../../core/EventBus";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { SendQuickChatEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { ChatModal, QuickChatPhrase, quickChatPhrases } from "./ChatModal";
import { COLORS, MenuElement } from "./RadialMenuElements";
export class ChatIntegration {
private ctModal: ChatModal;
constructor(
private game: GameView,
private eventBus: EventBus,
) {
this.ctModal = document.querySelector("chat-modal") as ChatModal;
if (!this.ctModal) {
throw new Error(
"Chat modal element not found. Ensure chat-modal element exists in DOM before initializing ChatIntegration",
);
}
}
setupChatModal(sender: PlayerView, recipient: PlayerView) {
this.ctModal.setSender(sender);
this.ctModal.setRecipient(recipient);
}
createQuickChatMenu(recipient: PlayerView): MenuElement[] {
if (!this.ctModal) {
throw new Error("Chat modal not set");
}
const myPlayer = this.game.myPlayer();
if (!myPlayer) {
throw new Error("Current player not found");
}
return this.ctModal.categories.map((category) => {
const categoryTranslation = translateText(`chat.cat.${category.id}`);
const categoryColor =
COLORS.chat[category.id as keyof typeof COLORS.chat] ||
COLORS.chat.default;
const phrases = quickChatPhrases[category.id] || [];
const phraseItems: MenuElement[] = phrases.map(
(phrase: QuickChatPhrase) => {
const phraseText = translateText(`chat.${category.id}.${phrase.key}`);
return {
id: `phrase-${category.id}-${phrase.key}`,
name: phraseText,
disabled: false,
text: this.shortenText(phraseText),
fontSize: "10px",
color: categoryColor,
tooltipItems: [
{
text: phraseText,
className: "description",
},
],
action: () => {
if (phrase.requiresPlayer) {
this.ctModal.openWithSelection(
category.id,
phrase.key,
myPlayer,
recipient,
);
} else {
this.eventBus.emit(
new SendQuickChatEvent(
recipient,
`${category.id}.${phrase.key}`,
{},
),
);
}
},
};
},
);
return {
id: `chat-category-${category.id}`,
name: categoryTranslation,
disabled: false,
text: categoryTranslation,
color: categoryColor,
_action: () => {}, // Empty action placeholder for RadialMenu
subMenu: () => phraseItems,
};
});
}
shortenText(text: string, maxLength = 15): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + "...";
}
}
+35 -16
View File
@@ -9,14 +9,14 @@ import { EventBus } from "../../../core/EventBus";
import { SendQuickChatEvent } from "../../Transport";
import { translateText } from "../../Utils";
type QuickChatPhrase = {
export type QuickChatPhrase = {
key: string;
requiresPlayer: boolean;
};
type QuickChatPhrases = Record<string, QuickChatPhrase[]>;
export type QuickChatPhrases = Record<string, QuickChatPhrase[]>;
const quickChatPhrases: QuickChatPhrases = quickChatData;
export const quickChatPhrases: QuickChatPhrases = quickChatData;
@customElement("chat-modal")
export class ChatModal extends LitElement {
@@ -57,7 +57,7 @@ export class ChatModal extends LitElement {
misc: [{ text: "Let's go!", requiresPlayer: false }],
};
private categories = [
public categories = [
{ id: "help" },
{ id: "attack" },
{ id: "defend" },
@@ -71,17 +71,6 @@ export class ChatModal extends LitElement {
}
render() {
const sortedPlayers = [...this.players].sort((a, b) => a.localeCompare(b));
const filteredPlayers = sortedPlayers.filter((player) =>
player.toLowerCase().includes(this.playerSearchQuery),
);
const otherPlayers = sortedPlayers.filter(
(player) => !player.toLowerCase().includes(this.playerSearchQuery),
);
const displayPlayers = [...filteredPlayers, ...otherPlayers];
return html`
<o-modal title="${translateText("chat.title")}">
<div class="chat-columns">
@@ -236,7 +225,6 @@ export class ChatModal extends LitElement {
this.eventBus.emit(
new SendQuickChatEvent(
this.sender,
this.recipient,
this.selectedQuickChatKey,
variables,
@@ -307,4 +295,35 @@ export class ChatModal extends LitElement {
public setSender(value: PlayerView) {
this.sender = value;
}
public openWithSelection(
categoryId: string,
phraseKey: string,
sender?: PlayerView,
recipient?: PlayerView,
) {
if (sender && recipient) {
const alivePlayerNames = this.g
.players()
.filter((p) => p.isAlive() && !(p.data.playerType === PlayerType.Bot))
.map((p) => p.data.name);
this.players = alivePlayerNames;
this.recipient = recipient;
this.sender = sender;
}
this.selectCategory(categoryId);
const phrase = this.getPhrasesForCategory(categoryId).find(
(p) => p.key === phraseKey,
);
if (phrase) {
this.selectPhrase(phrase);
}
this.requestUpdate();
this.modalEl?.open();
}
}
+4 -5
View File
@@ -2,8 +2,8 @@ import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Gold } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
import { SendSetTargetTroopRatioEvent } from "../../Transport";
import { renderNumber, renderTroops } from "../../Utils";
@@ -13,7 +13,6 @@ import { Layer } from "./Layer";
@customElement("control-panel")
export class ControlPanel extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
public uiState: UIState;
@@ -48,10 +47,10 @@ export class ControlPanel extends LitElement implements Layer {
private _manpower: number = 0;
@state()
private _gold: number;
private _gold: Gold;
@state()
private _goldPerSecond: number;
private _goldPerSecond: Gold;
private _lastPopulationIncreaseRate: number;
@@ -126,7 +125,7 @@ export class ControlPanel extends LitElement implements Layer {
this._troops = player.troops();
this._workers = player.workers();
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10n;
this.currentTroopRatio = player.troops() / player.population();
this.requestUpdate();
+12 -14
View File
@@ -23,7 +23,6 @@ import {
TargetPlayerUpdate,
UnitIncomingUpdate,
} from "../../../core/game/GameUpdates";
import { ClientID } from "../../../core/Schemas";
import {
CancelAttackIntentEvent,
CancelBoatIntentEvent,
@@ -66,7 +65,6 @@ interface Event {
export class EventsDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
public clientID: ClientID;
private active: boolean = false;
private events: Event[] = [];
@@ -184,7 +182,7 @@ export class EventsDisplay extends LitElement implements Layer {
renderLayer(): void {}
onDisplayMessageEvent(event: DisplayMessageUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (
event.playerID !== null &&
(!myPlayer || myPlayer.smallID() !== event.playerID)
@@ -202,7 +200,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onDisplayChatEvent(event: DisplayChatMessageUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (
event.playerID === null ||
!myPlayer ||
@@ -230,7 +228,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceRequestEvent(update: AllianceRequestUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer || update.recipientID !== myPlayer.smallID()) {
return;
}
@@ -282,7 +280,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceRequestReplyEvent(update: AllianceRequestReplyUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer || update.request.requestorID !== myPlayer.smallID()) {
return;
}
@@ -303,7 +301,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onBrokeAllianceEvent(update: BrokeAllianceUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const betrayed = this.game.playerBySmallID(update.betrayedID) as PlayerView;
@@ -341,7 +339,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onAllianceExpiredEvent(update: AllianceExpiredUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const otherID =
@@ -365,7 +363,7 @@ export class EventsDisplay extends LitElement implements Layer {
onTargetPlayerEvent(event: TargetPlayerUpdate) {
const other = this.game.playerBySmallID(event.playerID) as PlayerView;
const myPlayer = this.game.playerByClientID(this.clientID) as PlayerView;
const myPlayer = this.game.myPlayer() as PlayerView;
if (!myPlayer || !myPlayer.isFriendly(other)) return;
const target = this.game.playerBySmallID(event.targetID) as PlayerView;
@@ -380,13 +378,13 @@ export class EventsDisplay extends LitElement implements Layer {
}
emitCancelAttackIntent(id: string) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelAttackIntentEvent(myPlayer.id(), id));
this.eventBus.emit(new CancelAttackIntentEvent(id));
}
emitBoatCancelIntent(id: number) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
this.eventBus.emit(new CancelBoatIntentEvent(id));
}
@@ -406,7 +404,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onEmojiMessageEvent(update: EmojiUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const recipient =
@@ -441,7 +439,7 @@ export class EventsDisplay extends LitElement implements Layer {
}
onUnitIncomingEvent(event: UnitIncomingUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
const myPlayer = this.game.myPlayer();
if (!myPlayer || myPlayer.smallID() !== event.playerID) {
return;
@@ -0,0 +1,47 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { GameView } from "../../../core/game/GameView";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
@customElement("heads-up-message")
export class HeadsUpMessage extends LitElement implements Layer {
public game: GameView;
@state()
private isVisible = false;
createRenderRoot() {
return this;
}
init() {
this.isVisible = true;
this.requestUpdate();
}
tick() {
if (!this.game.inSpawnPhase()) {
this.isVisible = false;
this.requestUpdate();
}
}
render() {
if (!this.isVisible) {
return html``;
}
return html`
<div
class="flex items-center
w-full justify-evenly h-8 lg:h-10 top-0 lg:top-4 left-0 lg:left-4
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
@contextmenu=${(e) => e.preventDefault()}
>
${translateText("heads_up_message.choose_spawn")}
</div>
`;
}
}
+62 -14
View File
@@ -4,7 +4,6 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
@@ -36,7 +35,6 @@ export class GoToUnitEvent implements GameEvent {
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
public clientID: ClientID | null = null;
public eventBus: EventBus | null = null;
players: Entry[] = [];
@@ -46,6 +44,12 @@ export class Leaderboard extends LitElement implements Layer {
private _shownOnInit = false;
private showTopFive = true;
@state()
private _sortKey: "tiles" | "gold" | "troops" = "tiles";
@state()
private _sortOrder: "asc" | "desc" = "desc";
init() {}
tick() {
@@ -64,18 +68,39 @@ export class Leaderboard extends LitElement implements Layer {
}
}
private setSort(key: "tiles" | "gold" | "troops") {
if (this._sortKey === key) {
this._sortOrder = this._sortOrder === "asc" ? "desc" : "asc";
} else {
this._sortKey = key;
this._sortOrder = "desc";
}
this.updateLeaderboard();
}
private updateLeaderboard() {
if (this.game === null) throw new Error("Not initialized");
if (this.clientID === null) {
return;
}
const myPlayer =
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
null;
const myPlayer = this.game.myPlayer();
const sorted = this.game
.playerViews()
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
let sorted = this.game.playerViews();
const compare = (a: number, b: number) =>
this._sortOrder === "asc" ? a - b : b - a;
switch (this._sortKey) {
case "gold":
sorted = sorted.sort((a, b) =>
compare(Number(a.gold()), Number(b.gold())),
);
break;
case "troops":
sorted = sorted.sort((a, b) => compare(a.troops(), b.troops()));
break;
default:
sorted = sorted.sort((a, b) =>
compare(a.numTilesOwned(), b.numTilesOwned()),
);
}
const numTilesWithoutFallout =
this.game.numLandTiles() - this.game.numTilesWithFallout();
@@ -181,6 +206,8 @@ export class Leaderboard extends LitElement implements Layer {
th {
background-color: rgb(31 41 55 / 0.5);
color: white;
cursor: pointer;
user-select: none;
}
.myPlayer {
font-weight: bold;
@@ -282,9 +309,30 @@ export class Leaderboard extends LitElement implements Layer {
<tr>
<th>${translateText("leaderboard.rank")}</th>
<th>${translateText("leaderboard.player")}</th>
<th>${translateText("leaderboard.owned")}</th>
<th>${translateText("leaderboard.gold")}</th>
<th>${translateText("leaderboard.troops")}</th>
<th @click=${() => this.setSort("tiles")}>
${translateText("leaderboard.owned")}
${this._sortKey === "tiles"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</th>
<th @click=${() => this.setSort("gold")}>
${translateText("leaderboard.gold")}
${this._sortKey === "gold"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</th>
<th @click=${() => this.setSort("troops")}>
${translateText("leaderboard.troops")}
${this._sortKey === "troops"
? this._sortOrder === "asc"
? "⬆️"
: "⬇️"
: ""}
</th>
</tr>
</thead>
<tbody>
@@ -0,0 +1,285 @@
import { LitElement } from "lit";
import { customElement } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import { UIState } from "../UIState";
import { BuildMenu } from "./BuildMenu";
import { ChatIntegration } from "./ChatIntegration";
import { EmojiTable } from "./EmojiTable";
import { Layer } from "./Layer";
import { MenuEventManager } from "./MenuEventManager";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerInfoOverlay } from "./PlayerInfoOverlay";
import { PlayerPanel } from "./PlayerPanel";
import { RadialMenu, RadialMenuConfig } from "./RadialMenu";
import {
COLORS,
MenuElementParams,
Slot,
createRadialMenuItems,
getRootMenuItems,
updateCenterButton,
} from "./RadialMenuElements";
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
import buildIcon from "../../../../resources/images/BuildIconWhite.svg";
import infoIcon from "../../../../resources/images/InfoIcon.svg";
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
@customElement("main-radial-menu")
export class MainRadialMenu extends LitElement implements Layer {
private radialMenu: RadialMenu;
private lastTickRefresh: number = 0;
private tickRefreshInterval: number = 500;
private needsRefresh: boolean = false;
private playerActionHandler: PlayerActionHandler;
private menuEventManager: MenuEventManager;
private chatIntegration: ChatIntegration;
constructor(
private eventBus: EventBus,
private game: GameView,
private transformHandler: TransformHandler,
private emojiTable: EmojiTable,
private buildMenu: BuildMenu,
private uiState: UIState,
private playerInfoOverlay: PlayerInfoOverlay,
private playerPanel: PlayerPanel,
) {
super();
const menuConfig: RadialMenuConfig = {
centerButtonIcon: swordIcon,
tooltipStyle: `
.radial-tooltip .cost {
margin-top: 4px;
color: ${COLORS.tooltip.cost};
}
.radial-tooltip .count {
color: ${COLORS.tooltip.count};
}
`,
};
this.radialMenu = new RadialMenu(menuConfig);
this.playerActionHandler = new PlayerActionHandler(
this.eventBus,
this.uiState,
);
this.menuEventManager = new MenuEventManager(
this.eventBus,
this.game,
this.transformHandler,
this.radialMenu,
this.buildMenu,
this.emojiTable,
this.playerInfoOverlay,
this.playerPanel,
);
this.chatIntegration = new ChatIntegration(this.game, this.eventBus);
this.radialMenu.setRootMenuItems(getRootMenuItems());
}
init() {
this.radialMenu.init();
this.menuEventManager.setContextMenuCallback((myPlayer, tile, actions) => {
this.handlePlayerActions(myPlayer, actions, tile);
});
this.menuEventManager.init();
}
private async handlePlayerActions(
myPlayer: PlayerView,
actions: PlayerActions,
tile: TileRef,
) {
this.buildMenu.playerActions = actions;
const tileOwner = this.game.owner(tile);
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
if (myPlayer && recipient) {
this.chatIntegration.setupChatModal(myPlayer, recipient);
}
const params: MenuElementParams = {
myPlayer,
selected: recipient,
tileOwner,
tile,
playerActions: actions,
game: this.game,
buildMenu: this.buildMenu,
emojiTable: this.emojiTable,
playerActionHandler: this.playerActionHandler,
playerPanel: this.playerPanel,
chatIntegration: this.chatIntegration,
closeMenu: () => this.menuEventManager.closeMenu(),
};
const menuItems = createRadialMenuItems(params);
this.radialMenu.setRootMenuItems(menuItems);
updateCenterButton(params, (enabled, action) => {
this.radialMenu.enableCenterButton(enabled, action);
});
}
async tick() {
const clickedCell = this.menuEventManager.getClickedCell();
if (!this.radialMenu.isMenuVisible() || clickedCell === null) return;
const currentTime = new Date().getTime();
if (
currentTime - this.lastTickRefresh < this.tickRefreshInterval &&
!this.needsRefresh
) {
return;
}
const myPlayer = this.game.myPlayer();
if (myPlayer === null || !myPlayer.isAlive()) return;
const tile = this.game.ref(clickedCell.x, clickedCell.y);
const isSpawnPhase = this.game.inSpawnPhase();
const wasInSpawnPhase = this.menuEventManager.getWasInSpawnPhase();
if (wasInSpawnPhase !== isSpawnPhase) {
if (wasInSpawnPhase && !isSpawnPhase) {
this.needsRefresh = true;
this.menuEventManager.setWasInSpawnPhase(isSpawnPhase);
const actions = await this.playerActionHandler.getPlayerActions(
myPlayer,
tile,
);
this.updateMenuState(myPlayer, actions, tile);
this.radialMenu.refreshMenu();
return;
}
this.menuEventManager.closeMenu();
return;
}
// Check if tile ownership has changed
const originalTileOwner = this.menuEventManager.getOriginalTileOwner();
if (originalTileOwner && originalTileOwner.isPlayer()) {
if (this.game.owner(tile) !== originalTileOwner) {
this.menuEventManager.closeMenu();
return;
}
} else if (originalTileOwner) {
if (
this.game.owner(tile).isPlayer() ||
this.game.owner(tile) === myPlayer
) {
this.menuEventManager.closeMenu();
return;
}
}
this.lastTickRefresh = currentTime;
this.needsRefresh = false;
const actions = await this.playerActionHandler.getPlayerActions(
myPlayer,
tile,
);
this.updateMenuState(myPlayer, actions, tile);
}
private updateMenuState(
myPlayer: PlayerView,
actions: PlayerActions,
tile: TileRef,
) {
if (!this.radialMenu.isMenuVisible()) return;
const tileOwner = this.game.owner(tile);
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
const params: MenuElementParams = {
myPlayer,
selected: recipient,
tileOwner,
tile,
playerActions: actions,
game: this.game,
buildMenu: this.buildMenu,
emojiTable: this.emojiTable,
playerActionHandler: this.playerActionHandler,
playerPanel: this.playerPanel,
chatIntegration: this.chatIntegration,
closeMenu: () => this.menuEventManager.closeMenu(),
};
if (this.radialMenu.getCurrentLevel() === 0) {
updateCenterButton(params, (enabled, action) => {
this.radialMenu.enableCenterButton(enabled, action);
});
}
const canBuildTransport = actions.buildableUnits.find(
(bu) => bu.type === UnitType.TransportShip,
)?.canBuild;
this.radialMenu.updateMenuItem(
Slot.Build,
!this.game.inSpawnPhase(),
COLORS.build,
buildIcon,
);
if (actions?.interaction?.canSendAllianceRequest) {
this.radialMenu.updateMenuItem(Slot.Ally, true, COLORS.ally, undefined);
} else if (actions?.interaction?.canBreakAlliance) {
this.radialMenu.updateMenuItem(
Slot.Ally,
true,
COLORS.breakAlly,
undefined,
);
} else {
this.radialMenu.updateMenuItem(Slot.Ally, false, undefined, undefined);
}
this.radialMenu.updateMenuItem(
Slot.Boat,
!!canBuildTransport,
COLORS.boat,
boatIcon,
);
this.radialMenu.updateMenuItem(
Slot.Info,
this.game.hasOwner(tile),
COLORS.info,
infoIcon,
);
}
renderLayer(context: CanvasRenderingContext2D) {
this.radialMenu.renderLayer(context);
}
shouldTransform(): boolean {
return this.radialMenu.shouldTransform();
}
redraw() {
// No redraw implementation needed
}
}
@@ -0,0 +1,185 @@
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions, TerraNullius } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import {
CloseViewEvent,
ContextMenuEvent,
MouseUpEvent,
ShowBuildMenuEvent,
} from "../../InputHandler";
import { SendSpawnIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { BuildMenu } from "./BuildMenu";
import { EmojiTable } from "./EmojiTable";
import { PlayerInfoOverlay } from "./PlayerInfoOverlay";
import { PlayerPanel } from "./PlayerPanel";
import { RadialMenu } from "./RadialMenu";
export type ContextMenuCallback = (
myPlayer: PlayerView,
tile: TileRef,
actions: PlayerActions,
) => void;
export class MenuEventManager {
private clickedCell: Cell | null = null;
private lastClosed: number = 0;
private originalTileOwner: PlayerView | TerraNullius | null = null;
private wasInSpawnPhase: boolean = false;
private onContextMenuCallback: ContextMenuCallback | null = null;
constructor(
private eventBus: EventBus,
private game: GameView,
private transformHandler: TransformHandler,
private radialMenu: RadialMenu,
private buildMenu: BuildMenu,
private emojiTable: EmojiTable,
private playerInfoOverlay: PlayerInfoOverlay,
private playerPanel: PlayerPanel,
) {}
init() {
this.eventBus.on(ContextMenuEvent, (e) => this.onContextMenu(e));
this.eventBus.on(MouseUpEvent, (e) => this.onPointerUp(e));
this.eventBus.on(CloseViewEvent, () => this.closeMenu());
this.eventBus.on(ShowBuildMenuEvent, (e) => this.onShowBuildMenu(e));
}
setContextMenuCallback(callback: ContextMenuCallback) {
this.onContextMenuCallback = callback;
}
onContextMenu(event: ContextMenuEvent): Cell | null {
if (this.lastClosed + 200 > new Date().getTime()) return null;
this.closeMenu();
if (this.radialMenu.isMenuVisible()) {
this.radialMenu.hideRadialMenu();
return null;
} else {
this.radialMenu.showRadialMenu(event.x, event.y);
}
this.radialMenu.disableAllButtons();
this.clickedCell = this.transformHandler.screenToWorldCoordinates(
event.x,
event.y,
);
if (
!this.clickedCell ||
!this.game.isValidCoord(this.clickedCell.x, this.clickedCell.y)
) {
return null;
}
const tile = this.game.ref(this.clickedCell.x, this.clickedCell.y);
this.originalTileOwner = this.game.owner(tile);
this.wasInSpawnPhase = this.game.inSpawnPhase();
const myPlayer = this.game.myPlayer();
if (myPlayer === null) {
throw new Error("my player not found");
}
if (myPlayer && !myPlayer.isAlive() && !this.game.inSpawnPhase()) {
this.radialMenu.hideRadialMenu();
return null;
}
if (this.game.inSpawnPhase()) {
if (this.game.isLand(tile) && !this.game.hasOwner(tile)) {
this.radialMenu.enableCenterButton(true, () => {
if (this.clickedCell === null) return;
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
this.radialMenu.hideRadialMenu();
});
return this.clickedCell;
}
}
myPlayer.actions(tile).then((actions) => {
if (this.onContextMenuCallback) {
this.onContextMenuCallback(myPlayer, tile, actions);
}
});
return this.clickedCell;
}
getClickedCell(): Cell | null {
return this.clickedCell;
}
getOriginalTileOwner(): PlayerView | TerraNullius | null {
return this.originalTileOwner;
}
getWasInSpawnPhase(): boolean {
return this.wasInSpawnPhase;
}
setWasInSpawnPhase(value: boolean) {
this.wasInSpawnPhase = value;
}
onPointerUp(event: MouseUpEvent) {
this.playerInfoOverlay.hide();
this.hideEverything();
}
onShowBuildMenu(e: ShowBuildMenuEvent): TileRef | null {
const clickedCell = this.transformHandler.screenToWorldCoordinates(
e.x,
e.y,
);
if (clickedCell === null) {
return null;
}
if (!this.game.isValidCoord(clickedCell.x, clickedCell.y)) {
return null;
}
const tile = this.game.ref(clickedCell.x, clickedCell.y);
const p = this.game.myPlayer();
if (p === null) {
return null;
}
this.buildMenu.showMenu(tile);
return tile;
}
closeMenu() {
if (this.radialMenu.isMenuVisible()) {
this.radialMenu.hideRadialMenu();
}
if (this.buildMenu.isVisible) {
this.buildMenu.hideMenu();
}
if (this.emojiTable.isVisible) {
this.emojiTable.hideTable();
}
if (this.playerPanel.isVisible) {
this.playerPanel.hide();
}
}
hideEverything() {
if (this.radialMenu.isMenuVisible()) {
this.radialMenu.hideRadialMenu();
this.lastClosed = new Date().getTime();
}
this.emojiTable.hideTable();
this.buildMenu.hideMenu();
}
enableCenterButton(enabled: boolean, action: () => void) {
this.radialMenu.enableCenterButton(enabled, action);
}
}
+83 -48
View File
@@ -1,18 +1,20 @@
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
import allianceRequestIcon from "../../../../resources/images/AllianceRequestIcon.svg";
import allianceRequestBlackIcon from "../../../../resources/images/AllianceRequestBlackIcon.svg";
import allianceRequestWhiteIcon from "../../../../resources/images/AllianceRequestWhiteIcon.svg";
import crownIcon from "../../../../resources/images/CrownIcon.svg";
import disconnectedIcon from "../../../../resources/images/DisconnectedIcon.svg";
import embargoIcon from "../../../../resources/images/EmbargoIcon.svg";
import embargoBlackIcon from "../../../../resources/images/EmbargoBlackIcon.svg";
import embargoWhiteIcon from "../../../../resources/images/EmbargoWhiteIcon.svg";
import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
import targetIcon from "../../../../resources/images/TargetIcon.svg";
import traitorIcon from "../../../../resources/images/TraitorIcon.svg";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { ClientID } from "../../../core/Schemas";
import { Theme } from "../../../core/configuration/Config";
import { AllPlayers, Cell, nukeTypes, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { createCanvas, renderNumber, renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
@@ -40,23 +42,24 @@ export class NameLayer implements Layer {
private seenPlayers: Set<PlayerView> = new Set();
private traitorIconImage: HTMLImageElement;
private disconnectedIconImage: HTMLImageElement;
private allianceRequestIconImage: HTMLImageElement;
private allianceRequestBlackIconImage: HTMLImageElement;
private allianceRequestWhiteIconImage: HTMLImageElement;
private allianceIconImage: HTMLImageElement;
private targetIconImage: HTMLImageElement;
private crownIconImage: HTMLImageElement;
private embargoIconImage: HTMLImageElement;
private embargoBlackIconImage: HTMLImageElement;
private embargoWhiteIconImage: HTMLImageElement;
private nukeWhiteIconImage: HTMLImageElement;
private nukeRedIconImage: HTMLImageElement;
private shieldIconImage: HTMLImageElement;
private container: HTMLDivElement;
private myPlayer: PlayerView | null = null;
private firstPlace: PlayerView | null = null;
private theme: Theme = this.game.config().theme();
private userSettings: UserSettings = new UserSettings();
constructor(
private game: GameView,
private transformHandler: TransformHandler,
private clientID: ClientID,
) {
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
@@ -64,14 +67,18 @@ export class NameLayer implements Layer {
this.disconnectedIconImage.src = disconnectedIcon;
this.allianceIconImage = new Image();
this.allianceIconImage.src = allianceIcon;
this.allianceRequestIconImage = new Image();
this.allianceRequestIconImage.src = allianceRequestIcon;
this.allianceRequestBlackIconImage = new Image();
this.allianceRequestBlackIconImage.src = allianceRequestBlackIcon;
this.allianceRequestWhiteIconImage = new Image();
this.allianceRequestWhiteIconImage.src = allianceRequestWhiteIcon;
this.crownIconImage = new Image();
this.crownIconImage.src = crownIcon;
this.targetIconImage = new Image();
this.targetIconImage.src = targetIcon;
this.embargoIconImage = new Image();
this.embargoIconImage.src = embargoIcon;
this.embargoBlackIconImage = new Image();
this.embargoBlackIconImage.src = embargoBlackIcon;
this.embargoWhiteIconImage = new Image();
this.embargoWhiteIconImage.src = embargoWhiteIcon;
this.nukeWhiteIconImage = new Image();
this.nukeWhiteIconImage.src = nukeWhiteIcon;
this.nukeRedIconImage = new Image();
@@ -218,18 +225,32 @@ export class NameLayer implements Layer {
troopsDiv.style.marginTop = "-5%";
element.appendChild(troopsDiv);
const shieldDiv = document.createElement("div");
shieldDiv.classList.add("player-shield");
shieldDiv.style.zIndex = "3";
shieldDiv.style.marginTop = "-5%";
shieldDiv.style.display = "flex";
shieldDiv.style.alignItems = "center";
shieldDiv.style.gap = "0px";
shieldDiv.innerHTML = `
<img src="${this.shieldIconImage.src}" style="width: 16px; height: 16px;" />
<span style="color: black; font-size: 10px; margin-top: -2px;">0</span>
`;
element.appendChild(shieldDiv);
// TODO: Remove the shield icon.
/* eslint-disable no-constant-condition */
if (false) {
const shieldDiv = document.createElement("div");
shieldDiv.classList.add("player-shield");
shieldDiv.style.zIndex = "3";
shieldDiv.style.marginTop = "-5%";
shieldDiv.style.display = "flex";
shieldDiv.style.alignItems = "center";
shieldDiv.style.gap = "0px";
const shieldImg = document.createElement("img");
shieldImg.src = this.shieldIconImage.src;
shieldImg.style.width = "16px";
shieldImg.style.height = "16px";
const shieldSpan = document.createElement("span");
shieldSpan.textContent = "0";
shieldSpan.style.color = "black";
shieldSpan.style.fontSize = "10px";
shieldSpan.style.marginTop = "-2px";
shieldDiv.appendChild(shieldImg);
shieldDiv.appendChild(shieldSpan);
element.appendChild(shieldDiv);
}
/* eslint-enable no-constant-condition */
// Start off invisible so it doesn't flash at 0,0
element.style.display = "none";
@@ -298,11 +319,10 @@ export class NameLayer implements Layer {
const density = renderNumber(
render.player.troops() / render.player.numTilesOwned(),
);
const shieldDiv = render.element.querySelector(
".player-shield",
) as HTMLDivElement;
const shieldImg = shieldDiv.querySelector("img");
const shieldNumber = shieldDiv.querySelector("span");
const shieldDiv: HTMLDivElement | null =
render.element.querySelector(".player-shield");
const shieldImg = shieldDiv?.querySelector("img");
const shieldNumber = shieldDiv?.querySelector("span");
if (shieldImg) {
shieldImg.style.width = `${render.fontSize * 0.8}px`;
shieldImg.style.height = `${render.fontSize * 0.8}px`;
@@ -318,7 +338,8 @@ export class NameLayer implements Layer {
".player-icons",
) as HTMLDivElement;
const iconSize = Math.min(render.fontSize * 1.5, 48);
const myPlayer = this.getPlayer();
const myPlayer = this.game.myPlayer();
const isDarkMode = this.userSettings.darkMode();
// Crown icon
const existingCrown = iconsDiv.querySelector('[data-icon="crown"]');
@@ -388,13 +409,27 @@ export class NameLayer implements Layer {
}
// Alliance request icon
const data = '[data-icon="alliance-request"]';
const existingRequestAlliance = iconsDiv.querySelector(data);
let existingRequestAlliance = iconsDiv.querySelector(
'[data-icon="alliance-request"]',
);
const isThemeAllianceRequestIcon =
existingRequestAlliance?.getAttribute("dark-mode") ===
isDarkMode.toString();
const AllianceRequestIconImageSrc = isDarkMode
? this.allianceRequestWhiteIconImage.src
: this.allianceRequestBlackIconImage.src;
if (myPlayer !== null && render.player.isRequestingAllianceWith(myPlayer)) {
// Create new icon to match theme
if (existingRequestAlliance && !isThemeAllianceRequestIcon) {
existingRequestAlliance.remove();
existingRequestAlliance = null;
}
if (!existingRequestAlliance) {
iconsDiv.appendChild(
this.createIconElement(
this.allianceRequestIconImage.src,
AllianceRequestIconImageSrc,
iconSize,
"alliance-request",
),
@@ -449,19 +484,28 @@ export class NameLayer implements Layer {
existingEmoji.remove();
}
const existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
// Embargo icon
let existingEmbargo = iconsDiv.querySelector('[data-icon="embargo"]');
const hasEmbargo =
myPlayer &&
(render.player.hasEmbargoAgainst(myPlayer) ||
myPlayer.hasEmbargoAgainst(render.player));
const isThemeEmbargoIcon =
existingEmbargo?.getAttribute("dark-mode") === isDarkMode.toString();
const embargoIconImageSrc = isDarkMode
? this.embargoWhiteIconImage.src
: this.embargoBlackIconImage.src;
if (myPlayer && hasEmbargo) {
// Create new icon to match theme
if (existingEmbargo && !isThemeEmbargoIcon) {
existingEmbargo.remove();
existingEmbargo = null;
}
if (!existingEmbargo) {
iconsDiv.appendChild(
this.createIconElement(
this.embargoIconImage.src,
iconSize,
"embargo",
),
this.createIconElement(embargoIconImageSrc, iconSize, "embargo"),
);
}
} else if (existingEmbargo) {
@@ -535,6 +579,7 @@ export class NameLayer implements Layer {
icon.style.width = `${size}px`;
icon.style.height = `${size}px`;
icon.setAttribute("data-icon", id);
icon.setAttribute("dark-mode", this.userSettings.darkMode().toString());
if (center) {
icon.style.position = "absolute";
icon.style.top = "50%";
@@ -542,14 +587,4 @@ export class NameLayer implements Layer {
}
return icon;
}
private getPlayer(): PlayerView | null {
if (this.myPlayer !== null) {
return this.myPlayer;
}
this.myPlayer =
this.game.playerViews().find((p) => p.clientID() === this.clientID) ??
null;
return this.myPlayer;
}
}
@@ -0,0 +1,109 @@
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { PlayerView } from "../../../core/game/GameView";
import {
BuildUnitIntentEvent,
SendAllianceRequestIntentEvent,
SendAttackIntentEvent,
SendBoatAttackIntentEvent,
SendBreakAllianceIntentEvent,
SendDonateGoldIntentEvent,
SendDonateTroopsIntentEvent,
SendEmbargoIntentEvent,
SendEmojiIntentEvent,
SendQuickChatEvent,
SendSpawnIntentEvent,
SendTargetPlayerIntentEvent,
} from "../../Transport";
import { UIState } from "../UIState";
export class PlayerActionHandler {
constructor(
private eventBus: EventBus,
private uiState: UIState,
) {}
async getPlayerActions(
player: PlayerView,
tile: TileRef,
): Promise<PlayerActions> {
return await player.actions(tile);
}
handleAttack(player: PlayerView, targetId: string | null) {
this.eventBus.emit(
new SendAttackIntentEvent(
targetId,
this.uiState.attackRatio * player.troops(),
),
);
}
handleBoatAttack(
player: PlayerView,
targetId: string,
targetCell: Cell,
spawnTile: Cell | null,
) {
this.eventBus.emit(
new SendBoatAttackIntentEvent(
targetId,
targetCell,
this.uiState.attackRatio * player.troops(),
spawnTile,
),
);
}
async findBestTransportShipSpawn(
player: PlayerView,
tile: TileRef,
): Promise<TileRef | false> {
return await player.bestTransportShipSpawn(tile);
}
handleBuildUnit(unitType: UnitType, cellX: number, cellY: number) {
this.eventBus.emit(
new BuildUnitIntentEvent(unitType, new Cell(cellX, cellY)),
);
}
handleSpawn(spawnCell: Cell) {
this.eventBus.emit(new SendSpawnIntentEvent(spawnCell));
}
handleAllianceRequest(player: PlayerView, recipient: PlayerView) {
this.eventBus.emit(new SendAllianceRequestIntentEvent(player, recipient));
}
handleBreakAlliance(player: PlayerView, recipient: PlayerView) {
this.eventBus.emit(new SendBreakAllianceIntentEvent(player, recipient));
}
handleTargetPlayer(targetId: string | null) {
if (!targetId) return;
this.eventBus.emit(new SendTargetPlayerIntentEvent(targetId));
}
handleDonateGold(recipient: PlayerView) {
this.eventBus.emit(new SendDonateGoldIntentEvent(recipient, null));
}
handleDonateTroops(recipient: PlayerView) {
this.eventBus.emit(new SendDonateTroopsIntentEvent(recipient, null));
}
handleEmbargo(recipient: PlayerView, action: "start" | "stop") {
this.eventBus.emit(new SendEmbargoIntentEvent(recipient, action));
}
handleEmoji(targetPlayer: PlayerView | "AllPlayers", emojiIndex: number) {
this.eventBus.emit(new SendEmojiIntentEvent(targetPlayer, emojiIndex));
}
handleQuickChat(recipient: PlayerView, chatKey: string, params: any = {}) {
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, params));
}
}
@@ -11,7 +11,6 @@ import {
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { MouseMoveEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { TransformHandler } from "../TransformHandler";
@@ -42,9 +41,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
@property({ type: Object })
public game!: GameView;
@property({ type: String })
public clientID!: ClientID;
@property({ type: Object })
public eventBus!: EventBus;
@@ -137,13 +133,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
this.requestUpdate();
}
private myPlayer(): PlayerView | null {
if (!this.game) {
return null;
}
return this.game.playerByClientID(this.clientID);
}
private getRelationClass(relation: Relation): string {
switch (relation) {
case Relation.Hostile:
@@ -175,7 +164,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const myPlayer = this.game.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
let relationHtml: TemplateResult | null = null;
const attackingTroops = player
@@ -212,7 +201,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<div class="p-2">
<div
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex ${isFriendly
class="text-bold text-sm lg:text-lg font-bold mb-1 inline-flex break-all ${isFriendly
? "text-green-500"
: "text-white"}"
>
@@ -275,8 +264,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderUnitInfo(unit: UnitView) {
const isAlly =
(unit.owner() === this.myPlayer() ||
this.myPlayer()?.isFriendly(unit.owner())) ??
(unit.owner() === this.game.myPlayer() ||
this.game.myPlayer()?.isFriendly(unit.owner())) ??
false;
return html`
+23 -5
View File
@@ -40,7 +40,7 @@ export class PlayerPanel extends LitElement implements Layer {
private tile: TileRef | null = null;
@state()
private isVisible: boolean = false;
public isVisible: boolean = false;
@state()
private allianceExpiryText: string | null = null;
@@ -90,7 +90,6 @@ export class PlayerPanel extends LitElement implements Layer {
e.stopPropagation();
this.eventBus.emit(
new SendDonateTroopsIntentEvent(
myPlayer,
other,
myPlayer.troops() * this.uiState.attackRatio,
),
@@ -104,7 +103,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendDonateGoldIntentEvent(myPlayer, other, null));
this.eventBus.emit(new SendDonateGoldIntentEvent(other, null));
this.hide();
}
@@ -114,7 +113,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "start"));
this.eventBus.emit(new SendEmbargoIntentEvent(other, "start"));
this.hide();
}
@@ -124,7 +123,7 @@ export class PlayerPanel extends LitElement implements Layer {
other: PlayerView,
) {
e.stopPropagation();
this.eventBus.emit(new SendEmbargoIntentEvent(myPlayer, other, "stop"));
this.eventBus.emit(new SendEmbargoIntentEvent(other, "stop"));
this.hide();
}
@@ -330,6 +329,25 @@ export class PlayerPanel extends LitElement implements Layer {
</div>
</div>
<!-- Alliances -->
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">
${translateText("player_panel.alliances")}
(${other.allies().length})
</div>
<div
class="bg-opacity-50 bg-gray-700 rounded p-2 text-white max-w-72 max-h-20 overflow-y-auto"
translate="no"
>
${other.allies().length > 0
? other
.allies()
.map((p) => p.name())
.join(", ")
: translateText("player_panel.none")}
</div>
</div>
${this.allianceExpiryText !== null
? html`
<div class="flex flex-col gap-1">
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,471 @@
import {
AllPlayers,
Cell,
PlayerActions,
TerraNullius,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { flattenedEmojiTable } from "../../../core/Util";
import { renderNumber, translateText } from "../../Utils";
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
import { ChatIntegration } from "./ChatIntegration";
import { EmojiTable } from "./EmojiTable";
import { PlayerActionHandler } from "./PlayerActionHandler";
import { PlayerPanel } from "./PlayerPanel";
import { TooltipItem } from "./RadialMenu";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
import buildIcon from "../../../../resources/images/BuildIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import infoIcon from "../../../../resources/images/InfoIcon.svg";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
export interface MenuElementParams {
myPlayer: PlayerView;
selected: PlayerView | null;
tileOwner: PlayerView | TerraNullius;
tile: TileRef;
playerActions: PlayerActions;
game: GameView;
buildMenu: BuildMenu;
emojiTable: EmojiTable;
playerActionHandler: PlayerActionHandler;
playerPanel: PlayerPanel;
chatIntegration: ChatIntegration;
closeMenu: () => void;
}
export interface MenuElement {
id: string;
name: string;
disabled: boolean;
displayed?: boolean;
color?: string;
icon?: string;
text?: string;
fontSize?: string;
tooltipItems?: TooltipItem[];
action?: (params: MenuElementParams) => void; // For leaf items that perform actions
subMenu?: (params: MenuElementParams) => MenuElement[]; // For non-leaf items that open submenus
// Runtime properties used by RadialMenu (not to be set by menu element creators)
children?: MenuElement[];
_action?: () => void;
}
export const COLORS = {
build: "#ebe250",
building: "#2c2c2c",
boat: "#3f6ab1",
ally: "#53ac75",
breakAlly: "#c74848",
info: "#64748B",
target: "#ff0000",
infoDetails: "#7f8c8d",
infoEmoji: "#f1c40f",
trade: "#008080",
embargo: "#6600cc",
tooltip: {
cost: "#ffd700",
count: "#aaa",
},
chat: {
default: "#66c",
help: "#4caf50",
attack: "#f44336",
defend: "#2196f3",
greet: "#ff9800",
misc: "#9c27b0",
warnings: "#e3c532",
},
};
export enum Slot {
Info = "info",
Boat = "boat",
Build = "build",
Ally = "ally",
Back = "back",
}
/**
* Convert a MenuElement tree to a version usable by the RadialMenu
* by resolving subMenu functions and setting up actions
*/
export function prepareMenuElementsForRadialMenu(
elements: MenuElement[],
params: MenuElementParams,
): MenuElement[] {
return elements.map((element) => {
const prepared: MenuElement = { ...element };
// If the element has a subMenu function, execute it to get the children
if (element.subMenu) {
prepared.children = prepareMenuElementsForRadialMenu(
element.subMenu(params),
params,
);
// We don't need the subMenu function anymore
prepared.subMenu = undefined;
}
// Set up the action function to call the element's action with params
if (element.action) {
prepared._action = () => element.action!(params);
} else {
prepared._action = () => {};
}
return prepared;
});
}
export const buildMenuElement: MenuElement = {
id: Slot.Build,
name: "build",
disabled: false,
icon: buildIcon,
color: COLORS.build,
subMenu: (params: MenuElementParams) => {
const buildElements: MenuElement[] = flattenedBuildTable.map(
(item: BuildItemDisplay) => ({
id: `build_${item.unitType}`,
name: item.key
? item.key.replace("unit_type.", "")
: item.unitType.toString(),
disabled: !params.buildMenu.canBuild(item),
color: params.buildMenu.canBuild(item) ? COLORS.building : undefined,
icon: item.icon,
tooltipItems: [
{ text: translateText(item.key || ""), className: "title" },
{
text: translateText(item.description || ""),
className: "description",
},
{
text: `${renderNumber(params.buildMenu.cost(item))} ${translateText("player_panel.gold")}`,
className: "cost",
},
item.countable
? { text: `${params.buildMenu.count(item)}x`, className: "count" }
: null,
].filter((item): item is TooltipItem => item !== null),
action: (params: MenuElementParams) => {
params.playerActionHandler.handleBuildUnit(
item.unitType,
params.game.x(params.tile),
params.game.y(params.tile),
);
params.closeMenu();
},
}),
);
buildElements.push({
id: "build_menu",
name: "build",
disabled: false,
color: COLORS.build,
icon: buildIcon,
action: (params: MenuElementParams) => {
params.buildMenu.showMenu(params.tile);
},
});
return buildElements;
},
};
export const boatMenuElement: MenuElement = {
id: Slot.Boat,
name: "boat",
disabled: false,
icon: boatIcon,
color: COLORS.boat,
action: async (params: MenuElementParams) => {
if (!params.selected) return;
const spawn = await params.playerActionHandler.findBestTransportShipSpawn(
params.myPlayer,
params.tile,
);
let spawnTile: Cell | null = null;
if (spawn !== false) {
spawnTile = new Cell(params.game.x(spawn), params.game.y(spawn));
}
params.playerActionHandler.handleBoatAttack(
params.myPlayer,
params.selected.id(),
new Cell(params.game.x(params.tile), params.game.y(params.tile)),
spawnTile,
);
params.closeMenu();
},
};
export const infoMenuElement: MenuElement = {
id: Slot.Info,
name: "info",
disabled: false,
icon: infoIcon,
color: COLORS.info,
subMenu: (params: MenuElementParams) => {
if (!params.selected) return [];
return [
{
id: "info_chat",
name: "chat",
disabled: false,
color: COLORS.chat.default,
icon: chatIcon,
subMenu: (params: MenuElementParams) =>
params.chatIntegration
.createQuickChatMenu(params.selected!)
.map((item) => ({
...item,
action: item.action
? (_params: MenuElementParams) => item.action!(params)
: undefined,
})),
},
{
id: "ally_target",
name: "target",
disabled: false,
color: COLORS.target,
icon: targetIcon,
action: (params: MenuElementParams) => {
params.playerActionHandler.handleTargetPlayer(params.selected!.id());
params.closeMenu();
},
},
{
id: "ally_trade",
name: "trade",
disabled: !!params.playerActions?.interaction?.canEmbargo,
displayed: !params.playerActions?.interaction?.canEmbargo,
color: COLORS.trade,
text: translateText("player_panel.start_trade"),
action: (params: MenuElementParams) => {
params.playerActionHandler.handleEmbargo(params.selected!, "start");
params.closeMenu();
},
},
{
id: "ally_embargo",
name: "embargo",
disabled: !params.playerActions?.interaction?.canEmbargo,
displayed: !!params.playerActions?.interaction?.canEmbargo,
color: COLORS.embargo,
text: translateText("player_panel.stop_trade"),
action: (params: MenuElementParams) => {
params.playerActionHandler.handleEmbargo(params.selected!, "stop");
params.closeMenu();
},
},
{
id: "ally_request",
name: "request",
disabled: !params.playerActions?.interaction?.canSendAllianceRequest,
displayed: !params.playerActions?.interaction?.canBreakAlliance,
color: COLORS.ally,
icon: allianceIcon,
action: (params: MenuElementParams) => {
params.playerActionHandler.handleAllianceRequest(
params.myPlayer,
params.selected!,
);
params.closeMenu();
},
},
{
id: "ally_break",
name: "break",
disabled: !params.playerActions?.interaction?.canBreakAlliance,
displayed: !!params.playerActions?.interaction?.canBreakAlliance,
color: COLORS.breakAlly,
icon: traitorIcon,
action: (params: MenuElementParams) => {
params.playerActionHandler.handleBreakAlliance(
params.myPlayer,
params.selected!,
);
params.closeMenu();
},
},
{
id: "ally_donate_gold",
name: "donate gold",
disabled: !params.playerActions?.interaction?.canDonate,
color: COLORS.ally,
icon: donateGoldIcon,
action: (params: MenuElementParams) => {
params.playerActionHandler.handleDonateGold(params.selected!);
params.closeMenu();
},
},
{
id: "ally_donate_troops",
name: "donate troops",
disabled: !params.playerActions?.interaction?.canDonate,
color: COLORS.ally,
icon: donateTroopIcon,
action: (params: MenuElementParams) => {
params.playerActionHandler.handleDonateTroops(params.selected!);
params.closeMenu();
},
},
{
id: "info_player",
name: "player",
disabled: false,
color: COLORS.info,
icon: infoIcon,
action: (params: MenuElementParams) => {
params.playerPanel.show(params.playerActions, params.tile);
},
},
{
id: "info_emoji",
name: "emoji",
disabled: false,
color: COLORS.infoEmoji,
icon: emojiIcon,
subMenu: () => {
const emojiElements: MenuElement[] = [];
const emojiCount = 15;
for (let i = 0; i < emojiCount; i++) {
emojiElements.push({
id: `emoji_${i}`,
name: flattenedEmojiTable[i],
text: flattenedEmojiTable[i],
disabled: false,
fontSize: "25px",
action: (params: MenuElementParams) => {
const targetPlayer =
params.selected === params.game.myPlayer()
? AllPlayers
: params.selected;
params.playerActionHandler.handleEmoji(targetPlayer!, i);
params.closeMenu();
},
});
}
emojiElements.push({
id: "emoji_more",
name: "more",
disabled: false,
color: COLORS.infoEmoji,
icon: emojiIcon,
action: (params: MenuElementParams) => {
params.emojiTable.showTable((emoji) => {
const targetPlayer =
params.selected === params.game.myPlayer()
? AllPlayers
: params.selected;
params.playerActionHandler.handleEmoji(
targetPlayer!,
flattenedEmojiTable.indexOf(emoji),
);
params.emojiTable.hideTable();
});
},
});
return emojiElements;
},
},
].filter((item) => item.displayed !== false);
},
};
export function createMenuItems(params: MenuElementParams): MenuElement[] {
const canBuildTransport = params.playerActions.buildableUnits.find(
(bu) => bu.type === UnitType.TransportShip,
)?.canBuild;
return [
{
...boatMenuElement,
disabled: !canBuildTransport || !params.selected,
},
{
...buildMenuElement,
disabled: params.game.inSpawnPhase(),
},
{
...infoMenuElement,
disabled: !params.game.hasOwner(params.tile),
},
];
}
export function createRadialMenuItems(
params: MenuElementParams,
): MenuElement[] {
const elements = createMenuItems(params);
return prepareMenuElementsForRadialMenu(elements, params);
}
export function getRootMenuItems(): MenuElement[] {
return [
{
id: Slot.Boat,
name: "boat",
disabled: true,
_action: () => {},
icon: boatIcon,
},
{
id: Slot.Build,
name: "build",
disabled: true,
_action: () => {},
icon: buildIcon,
},
{
id: Slot.Info,
name: "info",
disabled: true,
_action: () => {},
icon: infoIcon,
},
];
}
export function updateCenterButton(
params: MenuElementParams,
enableCenterButton: (enabled: boolean, action?: (() => void) | null) => void,
) {
if (params.playerActions.canAttack) {
enableCenterButton(true, () => {
if (params.tileOwner !== params.myPlayer) {
params.playerActionHandler.handleAttack(
params.myPlayer,
params.tileOwner.id(),
);
}
params.closeMenu();
});
} else {
enableCenterButton(false);
}
}
+1 -3
View File
@@ -3,7 +3,6 @@ import { customElement, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { GameMode } from "../../../core/game/Game";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { renderNumber } from "../../Utils";
import { Layer } from "./Layer";
@@ -18,7 +17,6 @@ interface TeamEntry {
@customElement("team-stats")
export class TeamStats extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
teams: TeamEntry[] = [];
@@ -60,7 +58,7 @@ export class TeamStats extends LitElement implements Layer {
this.teams = Object.entries(grouped)
.map(([teamStr, teamPlayers]) => {
let totalGold = 0;
let totalGold = 0n;
let totalTroops = 0;
let totalScoreSort = 0;
+50 -48
View File
@@ -8,6 +8,7 @@ import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { PseudoRandom } from "../../../core/PseudoRandom";
import { AlternateViewEvent, DragEvent } from "../../InputHandler";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
export class TerritoryLayer implements Layer {
@@ -32,7 +33,7 @@ export class TerritoryLayer implements Layer {
private lastDragTime = 0;
private nodrawDragDuration = 200;
private refreshRate = 10;
private refreshRate = 10; //refresh every 10ms
private lastRefresh = 0;
private lastFocusedPlayer: PlayerView | null = null;
@@ -40,6 +41,7 @@ export class TerritoryLayer implements Layer {
constructor(
private game: GameView,
private eventBus: EventBus,
private transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
}
@@ -48,11 +50,10 @@ export class TerritoryLayer implements Layer {
return true;
}
paintPlayerBorder(player: PlayerView) {
player.borderTiles().then((playerBorderTiles) => {
playerBorderTiles.borderTiles.forEach((tile: TileRef) => {
this.paintTerritory(tile, true); // Immediately paint the tile instead of enqueueing
});
async paintPlayerBorder(player: PlayerView) {
const tiles = await player.borderTiles();
tiles.borderTiles.forEach((tile: TileRef) => {
this.paintTerritory(tile, true); // Immediately paint the tile instead of enqueueing
});
}
@@ -128,11 +129,7 @@ export class TerritoryLayer implements Layer {
euclDistFN(centerTile, 9, true),
)) {
if (!this.game.hasOwner(tile)) {
this.paintHighlightCell(
new Cell(this.game.x(tile), this.game.y(tile)),
color,
255,
);
this.paintHighlightTile(tile, color, 255);
}
}
}
@@ -155,16 +152,16 @@ export class TerritoryLayer implements Layer {
const context = this.canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.imageData = this.context.getImageData(
0,
0,
this.game.width(),
this.game.height(),
this.canvas.width,
this.canvas.height,
);
this.initImageData();
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
this.context.putImageData(this.imageData, 0, 0);
// Add a second canvas for highlights
@@ -199,7 +196,19 @@ export class TerritoryLayer implements Layer {
) {
this.lastRefresh = now;
this.renderTerritory();
this.context.putImageData(this.imageData, 0, 0);
const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect();
const vx0 = Math.max(0, topLeft.x);
const vy0 = Math.max(0, topLeft.y);
const vx1 = Math.min(this.game.width() - 1, bottomRight.x);
const vy1 = Math.min(this.game.height() - 1, bottomRight.y);
const w = vx1 - vx0 + 1;
const h = vy1 - vy0 + 1;
if (w > 0 && h > 0) {
this.context.putImageData(this.imageData, 0, 0, vx0, vy0, w, h);
}
}
if (this.alternativeView) {
return;
@@ -231,7 +240,13 @@ export class TerritoryLayer implements Layer {
while (numToRender > 0) {
numToRender--;
const tile = this.tileToRenderQueue.pop().tile;
const entry = this.tileToRenderQueue.pop();
if (!entry) {
break;
}
const tile = entry.tile;
this.paintTerritory(tile);
for (const neighbor of this.game.neighbors(tile)) {
this.paintTerritory(neighbor, true);
@@ -245,15 +260,10 @@ export class TerritoryLayer implements Layer {
}
if (!this.game.hasOwner(tile)) {
if (this.game.hasFallout(tile)) {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.falloutColor(),
150,
);
this.paintTile(tile, this.theme.falloutColor(), 150);
return;
}
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
this.clearTile(tile);
return;
}
const owner = this.game.owner(tile) as PlayerView;
@@ -273,40 +283,28 @@ export class TerritoryLayer implements Layer {
const lightTile =
(x % 2 === 0 && y % 2 === 0) || (y % 2 === 1 && x % 2 === 1);
const borderColor = lightTile ? borderColors.light : borderColors.dark;
this.paintCell(x, y, borderColor, 255);
this.paintTile(tile, borderColor, 255);
} else {
const useBorderColor = playerIsFocused
? this.theme.focusedBorderColor()
: this.theme.borderColor(owner);
this.paintCell(
this.game.x(tile),
this.game.y(tile),
useBorderColor,
255,
);
this.paintTile(tile, useBorderColor, 255);
}
} else {
this.paintCell(
this.game.x(tile),
this.game.y(tile),
this.theme.territoryColor(owner),
150,
);
this.paintTile(tile, this.theme.territoryColor(owner), 150);
}
}
paintCell(x: number, y: number, color: Colord, alpha: number) {
const index = y * this.game.width() + x;
const offset = index * 4;
paintTile(tile: TileRef, color: Colord, alpha: number) {
const offset = tile * 4;
this.imageData.data[offset] = color.rgba.r;
this.imageData.data[offset + 1] = color.rgba.g;
this.imageData.data[offset + 2] = color.rgba.b;
this.imageData.data[offset + 3] = alpha;
}
clearCell(cell: Cell) {
const index = cell.y * this.game.width() + cell.x;
const offset = index * 4;
clearTile(tile: TileRef) {
const offset = tile * 4;
this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent)
}
@@ -324,13 +322,17 @@ export class TerritoryLayer implements Layer {
});
}
paintHighlightCell(cell: Cell, color: Colord, alpha: number) {
this.clearCell(cell);
paintHighlightTile(tile: TileRef, color: Colord, alpha: number) {
this.clearTile(tile);
const x = this.game.x(tile);
const y = this.game.y(tile);
this.highlightContext.fillStyle = color.alpha(alpha / 255).toRgbString();
this.highlightContext.fillRect(cell.x, cell.y, 1, 1);
this.highlightContext.fillRect(x, y, 1, 1);
}
clearHighlightCell(cell: Cell) {
this.highlightContext.clearRect(cell.x, cell.y, 1, 1);
clearHighlightTile(tile: TileRef) {
const x = this.game.x(tile);
const y = this.game.y(tile);
this.highlightContext.clearRect(x, y, 1, 1);
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ export class TopBar extends LitElement implements Layer {
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
const maxPop = this.game.config().maxPopulation(myPlayer);
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10n;
return html`
<div
-2
View File
@@ -1,6 +1,5 @@
import { Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { ClientID } from "../../../core/Schemas";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { GameView, UnitView } from "../../../core/game/GameView";
@@ -35,7 +34,6 @@ export class UILayer implements Layer {
constructor(
private game: GameView,
private eventBus: EventBus,
private clientID: ClientID,
private transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
+15 -24
View File
@@ -1,11 +1,9 @@
import { colord, Colord } from "colord";
import { EventBus } from "../../../core/EventBus";
import { ClientID } from "../../../core/Schemas";
import { Theme } from "../../../core/configuration/Config";
import { UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { GameView, UnitView } from "../../../core/game/GameView";
import { BezenhamLine } from "../../../core/utilities/Line";
import {
AlternateViewEvent,
@@ -16,6 +14,7 @@ import { MoveWarshipIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import {
getColoredSprite,
isSpriteReady,
@@ -40,8 +39,6 @@ export class UnitLayer implements Layer {
private alternateView = false;
private myPlayer: PlayerView | null = null;
private oldShellTile = new Map<UnitView, TileRef>();
private transformHandler: TransformHandler;
@@ -55,7 +52,6 @@ export class UnitLayer implements Layer {
constructor(
private game: GameView,
private eventBus: EventBus,
private clientID: ClientID,
transformHandler: TransformHandler,
) {
this.theme = game.config().theme();
@@ -67,11 +63,11 @@ export class UnitLayer implements Layer {
}
tick() {
if (this.myPlayer === null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
const unitIds = this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => unit.id);
this.updateUnitsSprites();
this.updateUnitsSprites(unitIds ?? []);
}
init() {
@@ -95,18 +91,13 @@ export class UnitLayer implements Layer {
}
const clickRef = this.game.ref(cell.x, cell.y);
// Make sure we have the current player
if (this.myPlayer === null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
// Only select warships owned by the player
return this.game
.units(UnitType.Warship)
.filter(
(unit) =>
unit.isActive() &&
unit.owner() === this.myPlayer && // Only allow selecting own warships
unit.owner() === this.game.myPlayer() && // Only allow selecting own warships
this.game.manhattanDist(unit.tile(), clickRef) <=
this.WARSHIP_SELECTION_RADIUS,
)
@@ -202,7 +193,7 @@ export class UnitLayer implements Layer {
this.transportShipTrailCanvas.width = this.game.width();
this.transportShipTrailCanvas.height = this.game.height();
this.updateUnitsSprites();
this.updateUnitsSprites(this.game.units().map((unit) => unit.id()));
this.unitToTrail.forEach((trail, unit) => {
for (const t of trail) {
@@ -218,10 +209,9 @@ export class UnitLayer implements Layer {
});
}
private updateUnitsSprites() {
const unitsToUpdate = this.game
.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.map((unit) => this.game.unit(unit.id))
private updateUnitsSprites(unitIds: number[]) {
const unitsToUpdate = unitIds
?.map((id) => this.game.unit(id))
.filter((unit) => unit !== undefined);
if (unitsToUpdate) {
@@ -254,13 +244,14 @@ export class UnitLayer implements Layer {
}
private relationship(unit: UnitView): Relationship {
if (this.myPlayer === null) {
const myPlayer = this.game.myPlayer();
if (myPlayer === null) {
return Relationship.Enemy;
}
if (this.myPlayer === unit.owner()) {
if (myPlayer === unit.owner()) {
return Relationship.Self;
}
if (this.myPlayer.isFriendly(unit.owner())) {
if (myPlayer.isFriendly(unit.owner())) {
return Relationship.Ally;
}
return Relationship.Enemy;
+18 -1
View File
@@ -148,7 +148,24 @@ export class WinModal extends LitElement implements Layer {
}
innerHtml() {
return html``;
return html`<p>
<a
href="https://store.steampowered.com/app/3560670"
target="_blank"
rel="noopener noreferrer"
style="
color: #4a9eff;
text-decoration: underline;
font-weight: 500;
transition: color 0.2s ease;
font-size: 24px;
"
onmouseover="this.style.color='#6db3ff'"
onmouseout="this.style.color='#4a9eff'"
>
${translateText("win_modal.wishlist")}
</a>
</p>`;
}
show() {
+23 -1
View File
@@ -99,6 +99,11 @@
display: none;
}
}
/* display:none if child has class parent-hidden since we can't use shadow DOM in Lit due to Tailwind */
.component-hideable:has(> .parent-hidden) {
display: none;
}
</style>
<!-- Immediate execution to prevent FOUC -->
@@ -224,7 +229,9 @@
<div class="container__row">
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
<username-input class="relative w-full"></username-input>
<news-button class="w-[20%] md:w-[15%]"></news-button>
<news-button
class="w-[20%] md:w-[15%] component-hideable"
></news-button>
</div>
<div></div>
<div>
@@ -299,6 +306,11 @@
<options-menu></options-menu>
<player-info-overlay></player-info-overlay>
</div>
<div
class="fixed bottom-[30px] sm:bottom-auto sm:top-[20px] z-50 mx-auto max-w-max inset-x-0 items-center"
>
<heads-up-message></heads-up-message>
</div>
<div class="fixed left-[10px] top-[10px] z-50 w-36 lg:w-48 items-center">
<player-team-label></player-team-label>
</div>
@@ -368,6 +380,16 @@
<a href="/terms-of-service.html" class="t-link" target="_blank">
Terms of Service
</a>
<p style="text-align: center">
<a
href="https://www.playwire.com/contact-direct-sales"
data-i18n="main.advertise"
class="t-link"
target="_blank"
rel="noopener"
>Advertise</a
>
</p>
</div>
</div>
</footer>
+8 -16
View File
@@ -1,4 +1,5 @@
import { decodeJwt } from "jose";
import { z } from "zod/v4";
import {
RefreshResponseSchema,
TokenPayload,
@@ -49,7 +50,7 @@ export async function logOut(allSessions: boolean = false) {
__isLoggedIn = false;
const response = await fetch(
getApiBase() + allSessions ? "/revoke" : "/logout",
getApiBase() + (allSessions ? "/revoke" : "/logout"),
{
method: "POST",
headers: {
@@ -138,12 +139,9 @@ function _isLoggedIn(): IsLoggedInResponse {
const result = TokenPayloadSchema.safeParse(payload);
if (!result.success) {
const error = z.prettifyError(result.error);
// Invalid response
console.error(
"Invalid payload",
// JSON.stringify(payload),
JSON.stringify(result.error),
);
console.error("Invalid payload", error);
return false;
}
@@ -171,11 +169,8 @@ export async function postRefresh(): Promise<boolean> {
const body = await response.json();
const result = RefreshResponseSchema.safeParse(body);
if (!result.success) {
console.error(
"Invalid response",
JSON.stringify(body),
JSON.stringify(result.error),
);
const error = z.prettifyError(result.error);
console.error("Invalid response", error);
return false;
}
localStorage.setItem("token", result.data.token);
@@ -201,11 +196,8 @@ export async function getUserMe(): Promise<UserMeResponse | false> {
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
console.error(
"Invalid response",
JSON.stringify(body),
JSON.stringify(result.error),
);
const error = z.prettifyError(result.error);
console.error("Invalid response", error);
return false;
}
return result.data;
+3 -3
View File
@@ -6,6 +6,7 @@ import betweenTwoSeas from "../../../resources/maps/BetweenTwoSeasThumb.webp";
import blackSea from "../../../resources/maps/BlackSeaThumb.webp";
import britannia from "../../../resources/maps/BritanniaThumb.webp";
import deglaciatedAntarctica from "../../../resources/maps/DeglaciatedAntarcticaThumb.webp";
import eastasia from "../../../resources/maps/EastAsiaThumb.webp";
import europeClassic from "../../../resources/maps/EuropeClassicThumb.webp";
import europe from "../../../resources/maps/EuropeThumb.webp";
import falklandislands from "../../../resources/maps/FalklandIslandsThumb.webp";
@@ -13,7 +14,6 @@ import faroeislands from "../../../resources/maps/FaroeIslandsThumb.webp";
import gatewayToTheAtlantic from "../../../resources/maps/GatewayToTheAtlanticThumb.webp";
import halkidiki from "../../../resources/maps/HalkidikiThumb.webp";
import iceland from "../../../resources/maps/IcelandThumb.webp";
import japan from "../../../resources/maps/JapanThumb.webp";
import mars from "../../../resources/maps/MarsThumb.webp";
import mena from "../../../resources/maps/MenaThumb.webp";
import northAmerica from "../../../resources/maps/NorthAmericaThumb.webp";
@@ -61,8 +61,8 @@ export function getMapsImage(map: GameMapType): string {
return australia;
case GameMapType.Iceland:
return iceland;
case GameMapType.Japan:
return japan;
case GameMapType.EastAsia:
return eastasia;
case GameMapType.BetweenTwoSeas:
return betweenTwoSeas;
case GameMapType.FaroeIslands:
@@ -0,0 +1,48 @@
// renderUnitTypeOptions.ts
import { html, TemplateResult } from "lit";
import { UnitType } from "../../core/game/Game";
import { translateText } from "../Utils";
export interface UnitTypeRenderContext {
disabledUnits: UnitType[];
toggleUnit: (unit: UnitType, checked: boolean) => void;
}
const unitOptions: { type: UnitType; translationKey: string }[] = [
{ type: UnitType.City, translationKey: "unit_type.city" },
{ type: UnitType.DefensePost, translationKey: "unit_type.defense_post" },
{ type: UnitType.Port, translationKey: "unit_type.port" },
{ type: UnitType.Warship, translationKey: "unit_type.warship" },
{ type: UnitType.MissileSilo, translationKey: "unit_type.missile_silo" },
{ type: UnitType.SAMLauncher, translationKey: "unit_type.sam_launcher" },
{ type: UnitType.AtomBomb, translationKey: "unit_type.atom_bomb" },
{ type: UnitType.HydrogenBomb, translationKey: "unit_type.hydrogen_bomb" },
{ type: UnitType.MIRV, translationKey: "unit_type.mirv" },
];
export function renderUnitTypeOptions({
disabledUnits,
toggleUnit,
}: UnitTypeRenderContext): TemplateResult[] {
return unitOptions.map(
({ type, translationKey }) => html`
<label
class="option-card ${disabledUnits.includes(type) ? "" : "selected"}"
style="width: 140px;"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
.checked=${disabledUnits.includes(type)}
@change=${(e: Event) => {
const checked = (e.target as HTMLInputElement).checked;
toggleUnit(type, checked);
}}
/>
<div class="option-card-title" style="text-align: center;">
${translateText(translationKey)}
</div>
</label>
`,
);
}

Some files were not shown because too many files have changed in this diff Show More