Merge branch 'main' into give-territory
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -2,6 +2,7 @@ build/
|
||||
node_modules/
|
||||
out/
|
||||
static/
|
||||
coverage/
|
||||
TODO.txt
|
||||
resources/images/.DS_Store
|
||||
resources/.DS_Store
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
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 |
@@ -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 |
@@ -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 |
@@ -122,7 +122,6 @@
|
||||
"random": "عشوائي",
|
||||
"iceland": "آيسلندا",
|
||||
"pangaea": "بانجيا",
|
||||
"japan": "اليابان والجيران",
|
||||
"betweentwoseas": "بين بحرين",
|
||||
"knownworld": "العالم المعروف",
|
||||
"faroeislands": "جزر فارو",
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"random": "Произволна",
|
||||
"iceland": "Исландия",
|
||||
"pangaea": "Пангея",
|
||||
"japan": "Япония и съседи",
|
||||
"betweentwoseas": "Между Две Морета",
|
||||
"knownworld": "Познат Свят",
|
||||
"faroeislands": "Фарьорски острови",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"random": "যেকোনো",
|
||||
"iceland": "আইসল্যান্ড",
|
||||
"pangaea": "পাঞ্জিয়া",
|
||||
"japan": "জাপান ও তার পার্শ্ববর্তী অঞ্চল",
|
||||
"betweentwoseas": "দুই সমুদ্রের মধ্যবর্তী অঞ্চল",
|
||||
"knownworld": "পরিচিত পৃথিবী",
|
||||
"faroeislands": "ফ্যারো দ্বীপপুঞ্জ"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
"pangaea": "Pangaea",
|
||||
"map": "Karte",
|
||||
"betweentwoseas": "Zwischen zwei Meeren",
|
||||
"japan": "Japan und Nachbarländer",
|
||||
"knownworld": "Bekannte Welt"
|
||||
},
|
||||
"private_lobby": {
|
||||
|
||||
@@ -164,5 +164,8 @@
|
||||
"Balanced": "difficulty.Balanced",
|
||||
"Intense": "difficulty.Intense",
|
||||
"Impossible": "difficulty.Impossible"
|
||||
},
|
||||
"heads_up_message": {
|
||||
"choose_spawn": "heads_up_message.choose_spawn"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"random": "Hazarda",
|
||||
"iceland": "Islando",
|
||||
"pangaea": "Pangeo",
|
||||
"japan": "Japanio kaj najbaroj",
|
||||
"betweentwoseas": "Inter du maroj",
|
||||
"knownworld": "Konata Mondo",
|
||||
"faroeislands": "Ferooj",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"random": "רנדומלי",
|
||||
"iceland": "איסלנד",
|
||||
"pangaea": "פנגיאה",
|
||||
"japan": "יפן ושכנותיה",
|
||||
"betweentwoseas": "בין שני ימים",
|
||||
"knownworld": "העולם הידוע",
|
||||
"faroeislands": "איי פארו",
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"random": "यादृच्छिक",
|
||||
"iceland": "आइसलैंड",
|
||||
"pangaea": "पांजिया",
|
||||
"japan": "जापान और सीमावर्ती देश",
|
||||
"betweentwoseas": "समुद्रों के मध्य भूमि",
|
||||
"knownworld": "ज्ञात दुनिया",
|
||||
"faroeislands": "फ़रो द्वीपसमूह"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"random": "ランダム",
|
||||
"iceland": "アイスランド",
|
||||
"pangaea": "パンゲア",
|
||||
"japan": "日本とその隣国",
|
||||
"betweentwoseas": "2つの海の間",
|
||||
"knownworld": "知られてる世界",
|
||||
"faroeislands": "フェロー諸島",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
"pangaea": "Pangeia",
|
||||
"map": "Mapa",
|
||||
"betweentwoseas": "Entre Dois Mares",
|
||||
"japan": "Japão e Vizinhos",
|
||||
"knownworld": "Mundo Conhecido"
|
||||
},
|
||||
"private_lobby": {
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
"random": "Случайно",
|
||||
"iceland": "Исландия",
|
||||
"pangaea": "Пангея",
|
||||
"japan": "Япония и соседи",
|
||||
"betweentwoseas": "Между двух морей",
|
||||
"knownworld": "Известный мир",
|
||||
"faroeislands": "Фарерские острова",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
"pangaea": "Pangea",
|
||||
"map": "Harita",
|
||||
"betweentwoseas": "İki Deniz Arası",
|
||||
"japan": "Japonya ve Komşuları",
|
||||
"knownworld": "Bilinen Dünya"
|
||||
},
|
||||
"private_lobby": {
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"coordinates": [122, 750],
|
||||
"name": "USSR",
|
||||
"strength": 2,
|
||||
"flag": ""
|
||||
"flag": "ussr"
|
||||
},
|
||||
{
|
||||
"coordinates": [1232, 735],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)!;
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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 "?";
|
||||
|
||||
@@ -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) + "...";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
);
|
||||
}
|
||||