Merge pull request #3897 from openfrontio/TOS-and-Privacy-Policy-updates

Update privacy policy and terms of service
This commit is contained in:
iamlewis
2026-05-11 12:20:14 +01:00
committed by GitHub
62 changed files with 6064 additions and 5644 deletions
+25
View File
@@ -71,3 +71,28 @@ jobs:
cache: "npm"
- run: npm ci
- run: npx prettier --check .
gen-maps:
name: 🗺️ Generated maps up to date
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- uses: actions/setup-go@v6
with:
go-version-file: map-generator/go.mod
cache-dependency-path: map-generator/go.sum
- run: npm ci
- run: npm run gen-maps
- name: Check for diff
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::Generated maps are out of date — run 'npm run gen-maps' and commit the result."
git status --short
git --no-pager diff
exit 1
fi
-1
View File
@@ -101,7 +101,6 @@ All new features and bug fixes should include relevant tests. We use **Vitest**.
## Submitting a Pull Request
1. **Commit your changes**:
- Write clear, concise commit messages.
- Use the present tense ("Add feature" not "Added feature").
-4
View File
@@ -181,7 +181,6 @@ Feel free to ask questions in the translation Discord server!
To ensure code quality and project stability, we use a progressive contribution system:
1. **New Contributors**: Limited to UI improvements and small bug fixes only
- This helps you become familiar with the codebase
- UI changes are easier to review and less likely to break core functionality
- Small, focused PRs have a higher chance of being accepted
@@ -193,20 +192,17 @@ To ensure code quality and project stability, we use a progressive contribution
### How to Contribute Successfully
1. **Before Starting Work**:
- Open an issue describing what you want to contribute
- Wait for maintainer feedback before investing significant time
- Small improvements can proceed directly to PR stage
2. **Code Quality Requirements**:
- All code must be well-commented and follow existing style patterns
- New features should not break existing functionality
- Code should be thoroughly tested before submission
- All code changes in src/core _MUST_ be tested.
3. **Pull Request Process**:
- Keep PRs focused on a single feature or bug fix
- Include screenshots for UI changes
- Describe what testing you've performed
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,140 @@
{
"name": "Middle East",
"nations": [
{
"coordinates": [300, 65],
"name": "Ottoman Empire",
"flag": "tr"
},
{
"coordinates": [1639, 558],
"name": "Qajar Dynasty",
"flag": "Persia"
},
{
"coordinates": [1141, 797],
"name": "Emirate of Kuwait",
"flag": "Socialist_flag"
},
{
"coordinates": [1880, 1353],
"name": "Sultanate of Muscat",
"flag": "Socialist_flag"
},
{
"coordinates": [1703, 1402],
"name": "Imamate of Oman",
"flag": "White Flag"
},
{
"coordinates": [1592, 1239],
"name": "Trucial States",
"flag": "Socialist_flag"
},
{
"coordinates": [1129, 1875],
"name": "Aden Protectorate",
"flag": "gb"
},
{
"coordinates": [964, 1744],
"name": "Kingdom of Yemen",
"flag": "Kingdom of Yemen"
},
{
"coordinates": [844, 1655],
"name": "Emirate of Asir",
"flag": "Emirate of Asir"
},
{
"coordinates": [579, 1173],
"name": "Kingdom of Hejaz",
"flag": "Arabia"
},
{
"coordinates": [800, 1052],
"name": "Rashidi Emirate",
"flag": "Rashidi Emirate"
},
{
"coordinates": [1092, 1336],
"name": "Sultanate of Nejd",
"flag": "Sultanate of Nejd"
},
{
"coordinates": [1397, 1128],
"name": "Qatar",
"flag": "qa"
},
{
"coordinates": [973, 296],
"name": "Kingdom of Iraq",
"flag": "Kingdom of Iraq"
},
{
"coordinates": [554, 364],
"name": "Kingdom of Syria",
"flag": "Kingdom of Syria"
},
{
"coordinates": [423, 647],
"name": "Palestine Mandate",
"flag": "gb"
},
{
"coordinates": [100, 781],
"name": "Kingdom of Egypt",
"flag": "Kingdom of Egypt"
},
{
"coordinates": [159, 1530],
"name": "Anglo-Egyptian Sudan",
"flag": "gb"
},
{
"coordinates": [578, 1766],
"name": "Italian Eritrea",
"flag": "italy"
},
{
"coordinates": [401, 2005],
"name": "Ethiopian Empire",
"flag": "Ethiopian Empire"
},
{
"coordinates": [826, 2044],
"name": "French Somaliland",
"flag": "fr"
},
{
"coordinates": [1455, 902],
"name": "British Bushehr",
"flag": "gb"
},
{
"coordinates": [185, 375],
"name": "British Cyprus",
"flag": "gb"
},
{
"coordinates": [2127, 373],
"name": "Emirate of Afghanistan",
"flag": "Emirate of Afghanistan"
},
{
"coordinates": [2087, 925],
"name": "Baluchistan Agency",
"flag": "gb"
},
{
"coordinates": [932, 15],
"name": "Republic of Armenia",
"flag": "am"
},
{
"coordinates": [1671, 71],
"name": "Russian State",
"flag": "ru"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 956 KiB

@@ -0,0 +1,140 @@
{
"name": "Taiwan Strait",
"nations": [
{
"coordinates": [900, 32],
"name": "Fuzhou",
"flag": "cn"
},
{
"coordinates": [789, 282],
"name": "Putian",
"flag": "cn"
},
{
"coordinates": [641, 447],
"name": "Quanzhou",
"flag": "cn"
},
{
"coordinates": [430, 275],
"name": "Chinese Military Base",
"flag": "cn"
},
{
"coordinates": [499, 590],
"name": "Xiamen",
"flag": "cn"
},
{
"coordinates": [340, 550],
"name": "Zhangzhou",
"flag": "cn"
},
{
"coordinates": [206, 449],
"name": "Longyan",
"flag": "cn"
},
{
"coordinates": [107, 898],
"name": "Shantou",
"flag": "cn"
},
{
"coordinates": [7, 836],
"name": "Chaozhou",
"flag": "cn"
},
{
"coordinates": [21, 319],
"name": "Giant Pandas",
"flag": "cn"
},
{
"coordinates": [593, 607],
"name": "Kinmen Island",
"flag": "tw"
},
{
"coordinates": [975, 913],
"name": "Penghu",
"flag": "tw"
},
{
"coordinates": [1252, 1244],
"name": "Kaohsiung",
"flag": "tw"
},
{
"coordinates": [1210, 1079],
"name": "Tainan",
"flag": "tw"
},
{
"coordinates": [1491, 1152],
"name": "Taitung",
"flag": "tw"
},
{
"coordinates": [1643, 702],
"name": "Hualien",
"flag": "tw"
},
{
"coordinates": [1249, 911],
"name": "Chiayi",
"flag": "tw"
},
{
"coordinates": [1272, 697],
"name": "Taichung City",
"flag": "tw"
},
{
"coordinates": [1752, 284],
"name": "Keelung",
"flag": "tw"
},
{
"coordinates": [1655, 255],
"name": "Taipei",
"flag": "tw"
},
{
"coordinates": [1542, 290],
"name": "Taoyuan",
"flag": "tw"
},
{
"coordinates": [1464, 353],
"name": "Hsinchu",
"flag": "tw"
},
{
"coordinates": [1351, 473],
"name": "Miaoli",
"flag": "tw"
},
{
"coordinates": [1629, 1433],
"name": "Lanyu",
"flag": "tw"
},
{
"coordinates": [41, 602],
"name": "Meizhou",
"flag": "cn"
},
{
"coordinates": [619, 89],
"name": "Sanming",
"flag": "cn"
},
{
"coordinates": [225, 84],
"name": "Tea Plantations",
"flag": "cn"
}
]
}
+31 -29
View File
@@ -21,20 +21,32 @@ var maps = []struct {
Name string
IsTest bool
}{
{Name: "achiran"},
{Name: "aegean"},
{Name: "africa"},
{Name: "alps"},
{Name: "amazonriver"},
{Name: "antarctica"},
{Name: "archipelagosea"},
{Name: "arctic"},
{Name: "asia"},
{Name: "australia"},
{Name: "achiran"},
{Name: "alps"},
{Name: "baikal"},
{Name: "baikalnukewars"},
{Name: "betweentwoseas"},
{Name: "bajacalifornia"},
{Name: "beringsea"},
{Name: "beringstrait"},
{Name: "betweentwoseas"},
{Name: "blacksea"},
{Name: "bosphorusstraits"},
{Name: "britannia"},
{Name: "britanniaclassic"},
{Name: "caucasus"},
{Name: "conakry"},
{Name: "deglaciatedantarctica"},
{Name: "didier"},
{Name: "didierfrance"},
{Name: "dyslexdria"},
{Name: "eastasia"},
{Name: "europe"},
{Name: "europeclassic"},
@@ -43,59 +55,49 @@ var maps = []struct {
{Name: "fourislands"},
{Name: "gatewaytotheatlantic"},
{Name: "giantworldmap"},
{Name: "greatlakes"},
{Name: "gulfofstlawrence"},
{Name: "halkidiki"},
{Name: "hawaii"},
{Name: "iceland"},
{Name: "italia"},
{Name: "japan"},
{Name: "lemnos"},
{Name: "lisbon"},
{Name: "losangeles"},
{Name: "luna"},
{Name: "manicouagan"},
{Name: "straitofmalacca"},
{Name: "marenostrum"},
{Name: "mars"},
{Name: "mena"},
{Name: "middleeast"},
{Name: "milkyway"},
{Name: "montreal"},
{Name: "newyorkcity"},
{Name: "niledelta"},
{Name: "northamerica"},
{Name: "oceania"},
{Name: "pangaea"},
{Name: "passage"},
{Name: "pluto"},
{Name: "sanfrancisco"},
{Name: "sierpinski"},
{Name: "southamerica"},
{Name: "straitofgibraltar"},
{Name: "straitofhormuz"},
{Name: "straitofmalacca"},
{Name: "surrounded"},
{Name: "svalmel"},
{Name: "world"},
{Name: "lemnos"},
{Name: "twolakes"},
{Name: "taiwanstrait"},
{Name: "thebox"},
{Name: "tourney1"},
{Name: "tourney2"},
{Name: "tourney3"},
{Name: "tourney4"},
{Name: "thebox"},
{Name: "didier"},
{Name: "didierfrance"},
{Name: "amazonriver"},
{Name: "yenisei"},
{Name: "tradersdream"},
{Name: "hawaii"},
{Name: "niledelta"},
{Name: "arctic"},
{Name: "sanfrancisco"},
{Name: "aegean"},
{Name: "milkyway"},
{Name: "marenostrum"},
{Name: "greatlakes"},
{Name: "dyslexdria"},
{Name: "luna"},
{Name: "conakry"},
{Name: "caucasus"},
{Name: "losangeles"},
{Name: "beringsea"},
{Name: "antarctica"},
{Name: "archipelagosea"},
{Name: "bajacalifornia"},
{Name: "twolakes"},
{Name: "world"},
{Name: "yenisei"},
{Name: "big_plains", IsTest: true},
{Name: "half_land_half_ocean", IsTest: true},
{Name: "ocean_and_land", IsTest: true},
+1761 -4876
View File
File diff suppressed because it is too large Load Diff
+52 -64
View File
@@ -29,103 +29,91 @@
]
},
"devDependencies": {
"@datastructures-js/priority-queue": "^6.3.3",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@tailwindcss/vite": "^4.1.18",
"@datastructures-js/priority-queue": "^6.3.5",
"@eslint/compat": "^2.0.5",
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.2.4",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
"@types/ejs": "^3.1.5",
"@types/express": "^5.0.6",
"@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
"@types/msgpack5": "^3.4.6",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.11",
"@types/node": "^24.12.0",
"@types/pg": "^8.20.0",
"@types/seedrandom": "^3.0.8",
"@types/sinon": "^17.0.3",
"@types/systeminformation": "^3.23.1",
"@types/ws": "^8.5.11",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
"autoprefixer": "^10.4.20",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.1.5",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"benchmark": "^2.1.4",
"canvas": "^3.2.1",
"chai": "^5.1.1",
"canvas": "^3.2.3",
"concurrently": "^9.2.1",
"cross-env": "^7.0.3",
"cross-env": "^10.1.0",
"d3": "^7.9.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.1.1",
"eslint-formatter-gha": "^1.5.2",
"glob": "^13.0.0",
"globals": "^16.0.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-formatter-gha": "^2.0.1",
"glob": "^13.0.6",
"globals": "^17.6.0",
"husky": "^9.1.7",
"jsdom": "^27.4.0",
"lint-staged": "^16.1.2",
"lit": "^3.3.1",
"jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"lit": "^3.3.2",
"lit-markdown": "^1.3.2",
"mrmime": "^2.0.0",
"mrmime": "^2.0.1",
"pixi-filters": "^6.1.5",
"pixi.js": "^8.18.1",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-sh": "^0.17.4",
"protobufjs": "^7.5.5",
"sinon": "^21.0.1",
"sinon-chai": "^4.0.0",
"tailwindcss": "^4.1.18",
"prettier": "^3.8.3",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-sh": "^0.18.1",
"tailwindcss": "^4.2.4",
"tsconfig-paths": "^4.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1",
"vite": "^7.3.2",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^6.0.3",
"vitest": "^4.0.16",
"vitest-canvas-mock": "^1.1.3"
"vitest": "^4.1.5",
"vitest-canvas-mock": "^1.1.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.758.0",
"@lit-labs/virtualizer": "^2.1.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.200.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.200.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-logs": "^0.200.0",
"@opentelemetry/sdk-metrics": "^2.0.0",
"@opentelemetry/semantic-conventions": "^1.32.0",
"@opentelemetry/winston-transport": "^0.11.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.216.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.216.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.216.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-logs": "^0.216.0",
"@opentelemetry/sdk-metrics": "^2.7.1",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@opentelemetry/winston-transport": "^0.26.0",
"@types/compression": "^1.8.1",
"colord": "^2.9.3",
"colorjs.io": "^0.5.2",
"colorjs.io": "^0.6.1",
"compression": "^1.8.1",
"dompurify": "^3.4.0",
"dotenv": "^16.5.0",
"ejs": "^3.1.10",
"dompurify": "^3.4.2",
"dotenv": "^17.4.2",
"ejs": "^5.0.2",
"express": "^5.2.1",
"express-rate-limit": "^8.3.2",
"fastpriorityqueue": "^0.7.5",
"express-rate-limit": "^8.5.1",
"fastpriorityqueue": "^0.8.0",
"howler": "^2.2.4",
"intl-messageformat": "^10.7.16",
"intl-messageformat": "^11.2.3",
"ip-anonymize": "^0.1.0",
"jose": "^6.0.10",
"jose": "^6.2.3",
"js-yaml": "^4.1.1",
"limiter": "^3.0.0",
"nanoid": "^3.3.6",
"node-html-parser": "^7.0.2",
"obscenity": "^0.4.3",
"nanoid": "^5.1.11",
"node-html-parser": "^7.1.0",
"obscenity": "^0.4.6",
"seedrandom": "^3.0.5",
"ts-node": "^10.9.2",
"tsx": "^4.17.0",
"winston": "^3.17.0",
"ws": "^8.18.0",
"zod": "^4.0.5"
"tsx": "^4.21.0",
"winston": "^3.19.0",
"ws": "^8.20.0",
"zod": "^4.4.2"
},
"type": "module"
}
+40 -6
View File
@@ -665,6 +665,16 @@
"continent": "North America",
"name": "El Salvador"
},
{
"code": "Emirate of Afghanistan",
"continent": "Asia",
"name": "Emirate of Afghanistan"
},
{
"code": "Emirate of Asir",
"continent": "Asia",
"name": "Emirate of Asir"
},
{
"code": "granada",
"continent": "Europe",
@@ -721,6 +731,11 @@
"continent": "Africa",
"name": "Ethiopia"
},
{
"code": "Ethiopian Empire",
"continent": "Africa",
"name": "Ethiopian Empire"
},
{
"code": "eu",
"continent": "Europe",
@@ -1050,11 +1065,6 @@
"continent": "Europe",
"name": "Italy"
},
{
"code": "italy",
"continent": "Europe",
"name": "Kingdom of Italy"
},
{
"code": "jm",
"continent": "North America",
@@ -1130,6 +1140,11 @@
"continent": "Asia",
"name": "Kingdom of Iraq"
},
{
"code": "italy",
"continent": "Europe",
"name": "Kingdom of Italy"
},
{
"code": "Kingdom of Jerusalem",
"continent": "Asia",
@@ -1140,6 +1155,16 @@
"continent": "Asia",
"name": "Kingdom of Judah"
},
{
"code": "Kingdom of Syria",
"continent": "Asia",
"name": "Kingdom of Syria"
},
{
"code": "Kingdom of Yemen",
"continent": "Asia",
"name": "Kingdom of Yemen"
},
{
"code": "Kirghiz SSR",
"continent": "Asia",
@@ -1808,6 +1833,11 @@
"continent": "North America",
"name": "Quebec"
},
{
"code": "Rashidi Emirate",
"continent": "Asia",
"name": "Rashidi Emirate"
},
{
"code": "Republic of China",
"continent": "Asia",
@@ -2057,7 +2087,7 @@
},
{
"code": "Socialist_flag",
"name": "Socialist Flag"
"name": "Red Flag"
},
{
"code": "sb",
@@ -2463,6 +2493,10 @@
"continent": "Africa",
"name": "Western Sahara"
},
{
"code": "White Flag",
"name": "White Flag"
},
{
"code": "Wisconsin",
"continent": "North America",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
<image href="data:image/png;base64,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" x="7.500" y="32.813" width="135.000" height="84.375" />
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+7 -3
View File
@@ -418,6 +418,7 @@
"giantworldmap": "Giant World Map",
"europe": "Europe",
"mena": "MENA",
"middleeast": "Middle East",
"northamerica": "North America",
"oceania": "Oceania",
"blacksea": "Black Sea",
@@ -489,7 +490,8 @@
"beringsea": "Bering Sea",
"antarctica": "Antarctica",
"archipelagosea": "Archipelago Sea",
"bajacalifornia": "Baja California"
"bajacalifornia": "Baja California",
"taiwanstrait": "Taiwan Strait"
},
"map_categories": {
"featured": "Featured",
@@ -695,9 +697,9 @@
"left_click_label": "Left Click to Open Menu",
"left_click_desc": "When ON, left-click opens menu and sword button attacks. When OFF, left-click attacks directly.",
"left_click_menu": "Left Click Menu",
"attack_ratio_label": "⚔️ Attack Ratio",
"attack_ratio_label": "Attack Ratio",
"attack_ratio_desc": "What percentage of your troops to send in an attack (1100%)",
"territory_patterns_label": "🏳️ Territory Skins",
"territory_patterns_label": "Territory Skins",
"territory_patterns_desc": "Choose whether to display territory skin designs in game",
"coordinate_grid_label": "Coordinate Grid",
"coordinate_grid_desc": "Toggle the alphanumeric grid overlay",
@@ -757,6 +759,8 @@
"boat_attack_desc": "Send a boat attack to the tile under your cursor.",
"ground_attack": "Ground Attack",
"ground_attack_desc": "Send a ground attack to the tile under your cursor.",
"retaliate_attack": "Retaliate",
"retaliate_attack_desc": "Send a retaliation attack to blunt/negate the force of the most recent active attacker. Only available when you are being attacked.",
"ally_keybinds": "Ally Keybinds",
"request_alliance": "Request Alliance",
"request_alliance_desc": "Send an alliance request to the player whose tile is under your cursor.",
+155
View File
@@ -0,0 +1,155 @@
{
"map": {
"height": 2060,
"num_land_tiles": 3449078,
"width": 2200
},
"map16x": {
"height": 515,
"num_land_tiles": 211600,
"width": 550
},
"map4x": {
"height": 1030,
"num_land_tiles": 856603,
"width": 1100
},
"name": "Middle East",
"nations": [
{
"coordinates": [300, 65],
"flag": "tr",
"name": "Ottoman Empire"
},
{
"coordinates": [1639, 558],
"flag": "Persia",
"name": "Qajar Dynasty"
},
{
"coordinates": [1141, 797],
"flag": "Socialist_flag",
"name": "Emirate of Kuwait"
},
{
"coordinates": [1880, 1353],
"flag": "Socialist_flag",
"name": "Sultanate of Muscat"
},
{
"coordinates": [1703, 1402],
"flag": "White Flag",
"name": "Imamate of Oman"
},
{
"coordinates": [1592, 1239],
"flag": "Socialist_flag",
"name": "Trucial States"
},
{
"coordinates": [1129, 1875],
"flag": "gb",
"name": "Aden Protectorate"
},
{
"coordinates": [964, 1744],
"flag": "Kingdom of Yemen",
"name": "Kingdom of Yemen"
},
{
"coordinates": [844, 1655],
"flag": "Emirate of Asir",
"name": "Emirate of Asir"
},
{
"coordinates": [579, 1173],
"flag": "Arabia",
"name": "Kingdom of Hejaz"
},
{
"coordinates": [800, 1052],
"flag": "Rashidi Emirate",
"name": "Rashidi Emirate"
},
{
"coordinates": [1092, 1336],
"flag": "Sultanate of Nejd",
"name": "Sultanate of Nejd"
},
{
"coordinates": [1397, 1128],
"flag": "qa",
"name": "Qatar"
},
{
"coordinates": [973, 296],
"flag": "Kingdom of Iraq",
"name": "Kingdom of Iraq"
},
{
"coordinates": [554, 364],
"flag": "Kingdom of Syria",
"name": "Kingdom of Syria"
},
{
"coordinates": [423, 647],
"flag": "gb",
"name": "Palestine Mandate"
},
{
"coordinates": [100, 781],
"flag": "Kingdom of Egypt",
"name": "Kingdom of Egypt"
},
{
"coordinates": [159, 1530],
"flag": "gb",
"name": "Anglo-Egyptian Sudan"
},
{
"coordinates": [578, 1766],
"flag": "italy",
"name": "Italian Eritrea"
},
{
"coordinates": [401, 2005],
"flag": "Ethiopian Empire",
"name": "Ethiopian Empire"
},
{
"coordinates": [826, 2044],
"flag": "fr",
"name": "French Somaliland"
},
{
"coordinates": [1455, 902],
"flag": "gb",
"name": "British Bushehr"
},
{
"coordinates": [185, 375],
"flag": "gb",
"name": "British Cyprus"
},
{
"coordinates": [2127, 373],
"flag": "Emirate of Afghanistan",
"name": "Emirate of Afghanistan"
},
{
"coordinates": [2087, 925],
"flag": "gb",
"name": "Baluchistan Agency"
},
{
"coordinates": [932, 15],
"flag": "am",
"name": "Republic of Armenia"
},
{
"coordinates": [1671, 71],
"flag": "ru",
"name": "Russian State"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+155
View File
@@ -0,0 +1,155 @@
{
"map": {
"height": 1508,
"num_land_tiles": 1107518,
"width": 1800
},
"map16x": {
"height": 377,
"num_land_tiles": 67799,
"width": 450
},
"map4x": {
"height": 754,
"num_land_tiles": 274583,
"width": 900
},
"name": "Taiwan Strait",
"nations": [
{
"coordinates": [900, 32],
"flag": "cn",
"name": "Fuzhou"
},
{
"coordinates": [789, 282],
"flag": "cn",
"name": "Putian"
},
{
"coordinates": [641, 447],
"flag": "cn",
"name": "Quanzhou"
},
{
"coordinates": [430, 275],
"flag": "cn",
"name": "Chinese Military Base"
},
{
"coordinates": [499, 590],
"flag": "cn",
"name": "Xiamen"
},
{
"coordinates": [340, 550],
"flag": "cn",
"name": "Zhangzhou"
},
{
"coordinates": [206, 449],
"flag": "cn",
"name": "Longyan"
},
{
"coordinates": [107, 898],
"flag": "cn",
"name": "Shantou"
},
{
"coordinates": [7, 836],
"flag": "cn",
"name": "Chaozhou"
},
{
"coordinates": [21, 319],
"flag": "cn",
"name": "Giant Pandas"
},
{
"coordinates": [593, 607],
"flag": "tw",
"name": "Kinmen Island"
},
{
"coordinates": [975, 913],
"flag": "tw",
"name": "Penghu"
},
{
"coordinates": [1252, 1244],
"flag": "tw",
"name": "Kaohsiung"
},
{
"coordinates": [1210, 1079],
"flag": "tw",
"name": "Tainan"
},
{
"coordinates": [1491, 1152],
"flag": "tw",
"name": "Taitung"
},
{
"coordinates": [1643, 702],
"flag": "tw",
"name": "Hualien"
},
{
"coordinates": [1249, 911],
"flag": "tw",
"name": "Chiayi"
},
{
"coordinates": [1272, 697],
"flag": "tw",
"name": "Taichung City"
},
{
"coordinates": [1752, 284],
"flag": "tw",
"name": "Keelung"
},
{
"coordinates": [1655, 255],
"flag": "tw",
"name": "Taipei"
},
{
"coordinates": [1542, 290],
"flag": "tw",
"name": "Taoyuan"
},
{
"coordinates": [1464, 353],
"flag": "tw",
"name": "Hsinchu"
},
{
"coordinates": [1351, 473],
"flag": "tw",
"name": "Miaoli"
},
{
"coordinates": [1629, 1433],
"flag": "tw",
"name": "Lanyu"
},
{
"coordinates": [41, 602],
"flag": "cn",
"name": "Meizhou"
},
{
"coordinates": [619, 89],
"flag": "cn",
"name": "Sanming"
},
{
"coordinates": [225, 84],
"flag": "cn",
"name": "Tea Plantations"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because it is too large Load Diff
+274 -19
View File
@@ -26,6 +26,11 @@
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
h3 {
color: #34495e;
margin-top: 20px;
margin-bottom: 10px;
}
ul {
padding-left: 25px;
}
@@ -51,12 +56,13 @@
</head>
<body>
<h1>Terms of Service</h1>
<p class="updated-date"><strong>Last Updated: 4/29/2025</strong></p>
<p class="updated-date"><strong>Last Updated: 5/11/2026</strong></p>
<h2>1. Introduction</h2>
<p>
Welcome to OpenFront ("we," "our," "us"). These Terms of Service ("Terms")
govern your access to and use of our website at https://openfront.io and
govern your access to and use of our website at https://openfront.io, our
game clients distributed via Steam, Crazy Games, and other platforms, and
our Discord bot (collectively, the "Service"). By accessing or using our
Service, you agree to be bound by these Terms.
</p>
@@ -64,7 +70,9 @@
<h2>2. Definitions</h2>
<ul>
<li>
"Service" refers to both our website and Discord bot functionality
"Service" refers to our website, game clients (including those
distributed via Steam, Crazy Games, and other platforms), and Discord
bot functionality
</li>
<li>"Bot" refers specifically to our Discord bot application</li>
<li>"Website" refers to https://openfront.io</li>
@@ -73,9 +81,31 @@
Service
</li>
<li>"Discord" refers to Discord Inc. and its services</li>
<li>
"FFA" or "Free-for-All" refers to any game mode in which players are not
formally assigned to teams by the game and each player's stated
objective is to win individually
</li>
</ul>
<h2>3. Account Registration and Discord Integration</h2>
<h2>3. Eligibility</h2>
<p>
You must be at least 13 years old to use our Service. If you are a
resident of the European Economic Area, the United Kingdom, or any other
jurisdiction that requires a higher minimum age for the processing of
personal data, you must be at least 16 years old, unless local law
provides for a lower age of digital consent (in which case you must meet
that age).
</p>
<p>
By using the Service, you represent and warrant that you meet these age
requirements and that you have the legal capacity to enter into these
Terms. If we become aware that we have collected personal information from
a user below the applicable minimum age, we will take reasonable steps to
delete that information and terminate the account.
</p>
<h2>4. Account Registration and Discord Integration</h2>
<p>
Our Service uses Discord for authentication. By accessing or using our
Service, you authorize us to access certain Discord account information,
@@ -89,15 +119,24 @@
account.
</p>
<h2>4. User Conduct</h2>
<h2>5. User Conduct</h2>
<p>When using our Service, you agree not to:</p>
<ul>
<li>
Use the Service in any way that violates any applicable laws or
regulations
</li>
<li>Harass, abuse, or harm another person or group</li>
<li>Impersonate another user or person</li>
<li>Harass, abuse, threaten, or harm another person or group</li>
<li>
Engage in hate speech or post content that is discriminatory, racist,
sexist, homophobic, transphobic, or otherwise targets a person or group
based on a protected characteristic
</li>
<li>
Post or transmit content that is sexually explicit, obscene, violent, or
that sexualises, endangers, or otherwise harms minors
</li>
<li>Impersonate another user, OpenFront staff, or any other person</li>
<li>Use our Service for unauthorized advertising or promotion</li>
<li>
Attempt to access areas of our Service that you are not authorized to
@@ -118,7 +157,176 @@
<li>Attempt to reverse engineer any portion of our Service</li>
</ul>
<h2>5. Data Collection and Privacy</h2>
<h2>6. Fair Play and Competitive Integrity</h2>
<p>
OpenFront is a competitive multiplayer game and we take competitive
integrity seriously. In addition to the conduct rules in Section 5, you
agree not to engage in any of the following behaviours:
</p>
<h3>6.1 Offensive usernames and identifiers</h3>
<p>
You may not use any username, clan tag, flag, custom pattern, or other
identifier displayed in the Service that is hateful, harassing,
discriminatory, sexually explicit, threatening, that glorifies violence or
self-harm, that impersonates another person or entity (including OpenFront
staff, moderators, or well-known players), that promotes illegal activity,
or that is otherwise offensive. We reserve the right to force-rename
accounts, remove or revoke cosmetic items, or suspend accounts at our sole
discretion, with or without prior notice, where we consider an identifier
to fall within this prohibition.
</p>
<h3>6.2 Teaming and collusion in Free-for-All modes</h3>
<p>
In any FFA mode, you may not coordinate with other players to gain a
competitive advantage. Prohibited behaviour includes, without limitation:
</p>
<ul>
<li>
Pre-arranged alliances or non-aggression pacts arranged outside the
match
</li>
<li>Sharing accounts or "hot-seating" between multiple people</li>
<li>
Deliberately feeding resources, territory, or units to another player,
other than in dedicated team game modes where shared resources and
cooperative play between teammates are an intended function of the mode
</li>
<li>
Using external communication channels (Discord, voice chat, livestreams,
co-watching, or any other out-of-game communication) to coordinate play
with other participants in the same FFA match
</li>
</ul>
<p>
In-game diplomacy — including truces, non-aggression, and betrayal —
negotiated during the course of a single match using in-game mechanics is
a normal part of OpenFront and is not prohibited. The prohibition in this
section is directed at sustained, pre-arranged, or externally-coordinated
play that defeats the purpose of an FFA mode.
</p>
<h3>6.3 Multi-accounting</h3>
<p>
You may not create, control, or use more than one account to participate
in the same match, to manipulate matchmaking, to evade a ban or other
moderation action, or otherwise to circumvent these Terms.
</p>
<h3>6.4 Rating and ranking manipulation</h3>
<p>
You may not engage in win-trading, account boosting, intentional losses
("throwing"), queue dodging, or any other behaviour designed to manipulate
competitive rankings, leaderboards, tournament results, or matchmaking,
whether for yourself or for another player.
</p>
<h3>6.5 Cheating and unauthorised modifications</h3>
<p>
You may not use, develop, distribute, or promote any cheat, bot, macro,
automation tool, modified client, memory editor, packet manipulation tool,
or any third-party software or technique that provides an unfair gameplay
advantage or alters the intended behaviour of the game.
</p>
<h3>6.6 Exploits</h3>
<p>
You may not knowingly exploit any bug, glitch, or unintended behaviour in
the Service. Suspected bugs must be reported to us privately via our
official GitHub repository or Discord server rather than exploited or
publicly weaponised.
</p>
<h3>6.7 Disruptive play</h3>
<p>
You may not engage in sustained behaviour intended to ruin the experience
for other players, including griefing, spawn-camping in violation of mode
rules, intentional AFK or idle play, chat spam, or coordinated harassment
of specific players.
</p>
<h3>6.8 Enforcement</h3>
<p>
We may, at our sole discretion and with or without prior notice, take any
of the following actions in response to a breach of these Terms, including
the rules in this Section 6: issue a warning; temporarily suspend an
account; permanently ban an account; force-rename an account; revoke,
remove, or refuse to deliver cosmetic items; reset or remove statistics,
ratings, or leaderboard entries; or take any other corrective action we
consider appropriate.
</p>
<p>
Enforcement actions may apply across all accounts that we reasonably
believe are controlled by, or operated on behalf of, the same person, and
across all platforms on which OpenFront is distributed (including the
website, Steam, Crazy Games, our Discord server, and our Discord bot).
Severe violations — including but not limited to cheating, the use or
distribution of unauthorised modifications, repeated offences after prior
warnings, or any behaviour that endangers minors — may result in an
immediate permanent ban without prior warning.
</p>
<h3>6.9 Appeals</h3>
<p>
If you believe an enforcement action against your account was made in
error, you may contact us at support@openfront.io within 30 days of the
action. We will review appeals in good faith but reserve the right to
maintain or modify any enforcement action at our sole discretion. We do
not commit to a specific response timeframe and our decision on an appeal
is final.
</p>
<h2>7. Public Profiles, Leaderboards and Display Names</h2>
<p>
Some features of the Service are public by design. By using the Service,
you acknowledge and agree that the following information may be displayed
publicly to other users and to non-users (including on the Website,
in-game, on Steam, on Crazy Games, in our Discord server, and on
third-party sites or applications that display OpenFront data):
</p>
<ul>
<li>
<strong>Player ID.</strong> Each account is assigned a unique,
persistent identifier ("Player ID"). Your Player ID may be displayed
publicly alongside your in-game activity and may be used to look up your
match history, statistics, and leaderboard position.
</li>
<li>
<strong>Display name.</strong> A display name associated with your
Player ID may be shown publicly. This may take the form of a custom
display name that you have set, your Discord username, or one or more of
the names you have most recently used in the Service. We may retain and
display a history of recent display names in order to deter ban evasion,
name spoofing, and impersonation, and to support fair play enforcement.
</li>
<li>
<strong>In-game activity.</strong> Match results, statistics, ratings,
leaderboard positions, cosmetic items in use, clan or team affiliations,
and similar gameplay information may be displayed publicly.
</li>
</ul>
<p>
Your email address, Discord ID, IP address, payment information, and any
other identifying or contact information that you have not chosen to
display in-game will not be exposed publicly through this feature.
</p>
<p>
Display names remain subject to Section 6.1. We may force-rename, hide, or
replace display names that breach that section, and we may retain a record
of removed display names internally for moderation and enforcement
purposes.
</p>
<p>
If you do not wish to have your historical display names shown publicly
and would like to request that we display only your current name, please
contact us at support@openfront.io. We will assess such requests on a
case-by-case basis and reserve the right to decline where retention of
historical names is necessary for moderation, anti-evasion, or
anti-impersonation purposes.
</p>
<h2>8. Data Collection and Privacy</h2>
<p>
We collect and process personal information as described in our Privacy
Policy, which is incorporated by reference into these Terms. By using our
@@ -126,7 +334,7 @@
Policy, including our collection, use, and sharing of your information.
</p>
<h2>6. Content and Intellectual Property</h2>
<h2>9. Content and Intellectual Property</h2>
<p>
All content available through our Service, including but not limited to
text, graphics, logos, icons, images, audio clips, and software, is the
@@ -134,7 +342,7 @@
trademark, and other intellectual property laws.
</p>
<h2>7. User-Generated Content</h2>
<h2>10. User-Generated Content</h2>
<p>
You retain ownership of any content you submit, post, or display on or
through our Service ("User Content"). By submitting User Content, you
@@ -143,8 +351,15 @@
and publicly perform your User Content in connection with operating and
providing our Service.
</p>
<p>
You represent and warrant that you have all rights necessary to submit
your User Content and to grant the licence above, and that your User
Content does not infringe the intellectual property, privacy, or other
rights of any third party and does not breach Sections 5 or 6 of these
Terms.
</p>
<h2>8. Service Modifications and Availability</h2>
<h2>11. Service Modifications and Availability</h2>
<p>
We reserve the right to modify, suspend, or discontinue the Service (or
any part thereof) at any time, with or without notice. We will not be
@@ -158,7 +373,7 @@
errors.
</p>
<h2>9. Limitation of Liability</h2>
<h2>12. Limitation of Liability</h2>
<p>
To the maximum extent permitted by law, OpenFront and its affiliates,
officers, employees, agents, partners, and licensors will not be liable
@@ -175,13 +390,18 @@
<li>
Unauthorized access, use, or alteration of your transmissions or content
</li>
<li>
Any enforcement action taken under Section 6 of these Terms, including
any account suspension, ban, force-rename, or removal of cosmetic items,
statistics, or leaderboard entries
</li>
</ul>
<p>
In no event shall our aggregate liability for all claims relating to the
Service exceed one hundred dollars ($100).
</p>
<h2>10. Disclaimer of Warranties</h2>
<h2>13. Disclaimer of Warranties</h2>
<p>
The Service is provided on an "AS IS" and "AS AVAILABLE" basis without any
warranty of any kind, whether express or implied. We expressly disclaim
@@ -190,21 +410,28 @@
particular purpose, and non-infringement.
</p>
<h2>11. Discord's Terms and Conditions</h2>
<h2>14. Discord's Terms and Conditions</h2>
<p>
Your use of Discord's services is also governed by Discord's Terms of
Service and Privacy Policy. Our Service does not override or modify any
terms and conditions that govern your use of Discord's services.
</p>
<h2>12. Termination</h2>
<h2>15. Termination</h2>
<p>
We may terminate or suspend your access to the Service immediately,
without prior notice or liability, for any reason whatsoever, including
without limitation if you breach these Terms.
</p>
<p>
A termination or suspension under this Section 15 may, at our sole
discretion, extend to all accounts and platforms that we reasonably
believe are controlled by, or operated on behalf of, the same person, and
to associated cosmetic items, statistics, ratings, and leaderboard
entries.
</p>
<h2>13. Changes to Terms</h2>
<h2>16. Changes to Terms</h2>
<p>
We reserve the right to modify or replace these Terms at any time. If a
revision is material, we will try to provide at least 30 days' notice
@@ -216,16 +443,44 @@
effective, you agree to be bound by the revised terms.
</p>
<h2>14. Governing Law</h2>
<h2>17. Governing Law</h2>
<p>
These Terms shall be governed and construed in accordance with the laws of
California, without regard to its conflict of law provisions.
</p>
<h2>15. Contact Us</h2>
<h2>18. Severability</h2>
<p>
If any provision of these Terms is held to be invalid, illegal, or
unenforceable by a court of competent jurisdiction, that provision shall
be modified to the minimum extent necessary to make it enforceable, or, if
it cannot be so modified, shall be severed from these Terms. The remaining
provisions shall continue in full force and effect.
</p>
<h2>19. Entire Agreement</h2>
<p>
These Terms, together with our Privacy Policy and any other policies
expressly incorporated by reference, constitute the entire agreement
between you and OpenFront with respect to the Service and supersede any
prior or contemporaneous agreements, communications, or understandings,
whether written or oral, relating to the Service.
</p>
<h2>20. Contact Us</h2>
<p class="contact">
If you have any questions about these Terms, please contact us at: <br />
If you have any questions about these Terms, please contact us at:
<br />
legal@openfront.io
<br /><br />
To appeal an enforcement action, please contact:
<br />
support@openfront.io
<br /><br />
OpenFront LLC<br />
c/o Northwest Registered Agent, Inc.<br />
2108 N Street, Suite N<br />
Sacramento, CA 95816, United States
</p>
<div class="footer">
+47 -2
View File
@@ -13,7 +13,12 @@ import {
import { createPartialGameRecord, findClosestBy, replacer } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config";
import { getGameLogicConfig } from "../core/configuration/ConfigLoader";
import { BuildableUnit, Structures, UnitType } from "../core/game/Game";
import {
BuildableUnit,
PlayerType,
Structures,
UnitType,
} from "../core/game/Game";
import { TileRef } from "../core/game/GameMap";
import { GameMapLoader } from "../core/game/GameMapLoader";
import {
@@ -34,6 +39,7 @@ import {
DoBreakAllianceEvent,
DoGroundAttackEvent,
DoRequestAllianceEvent,
DoRetaliateAttackEvent,
InputHandler,
MouseMoveEvent,
MouseUpEvent,
@@ -237,7 +243,7 @@ async function createClientGame(
userSettings,
lobbyConfig.gameRecord !== undefined,
);
let gameMap: TerrainMapData | null = null;
let gameMap: TerrainMapData;
if (terrainLoad) {
gameMap = await terrainLoad;
@@ -391,6 +397,10 @@ export class ClientGameRunner {
DoGroundAttackEvent,
this.doGroundAttackUnderCursor.bind(this),
);
this.eventBus.on(
DoRetaliateAttackEvent,
this.doRetaliateAttackMostRecent.bind(this),
);
this.eventBus.on(
DoRequestAllianceEvent,
this.doRequestAllianceUnderCursor.bind(this),
@@ -783,6 +793,41 @@ export class ClientGameRunner {
});
}
private doRetaliateAttackMostRecent(): void {
if (!this.isActive || this.gameView.inSpawnPhase()) {
return;
}
if (this.myPlayer === null) {
if (!this.clientID) return;
const myPlayer = this.gameView.playerByClientID(this.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
const incomingAttacks = this.myPlayer.incomingAttacks().filter((a) => {
const t = (
this.gameView.playerBySmallID(a.attackerID) as PlayerView
).type();
return t !== PlayerType.Bot;
});
if (incomingAttacks.length === 0) return;
const mostRecentAttack = incomingAttacks[incomingAttacks.length - 1];
const attacker = this.gameView.playerBySmallID(
mostRecentAttack.attackerID,
) as PlayerView;
if (!attacker) return;
const counterTroops = Math.min(
mostRecentAttack.troops,
this.renderer.uiState.attackRatio * this.myPlayer.troops(),
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}
private doRequestAllianceUnderCursor(): void {
const tile = this.getTileUnderCursor();
if (tile === null) return;
+1 -1
View File
@@ -283,7 +283,7 @@ export class GameModeSelector extends LitElement {
? getSecondsUntilServerTimestamp(lobby.startsAt, this.serverTimeOffset)
: undefined;
let timeDisplay: string = "";
let timeDisplay: string;
let timeDisplayUppercase = false;
if (timeRemaining === undefined) {
timeDisplay = renderDuration(this.defaultLobbyTime);
+18 -4
View File
@@ -153,6 +153,8 @@ export class DoBoatAttackEvent implements GameEvent {}
export class DoGroundAttackEvent implements GameEvent {}
export class DoRetaliateAttackEvent implements GameEvent {}
export class DoRequestAllianceEvent implements GameEvent {}
export class DoBreakAllianceEvent implements GameEvent {}
@@ -496,6 +498,11 @@ export class InputHandler {
this.eventBus.emit(new DoGroundAttackEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.retaliateAttack)) {
e.preventDefault();
this.eventBus.emit(new DoRetaliateAttackEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.attackRatioDown)) {
e.preventDefault();
const increment = this.userSettings.attackRatioIncrement();
@@ -519,10 +526,12 @@ export class InputHandler {
}
// Two-phase build keybind matching: exact code match first, then digit/Numpad alias.
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);
if (matchedBuild !== null) {
e.preventDefault();
this.setGhostStructure(matchedBuild);
if (this.canUseBuildKeybinds()) {
const matchedBuild = this.resolveBuildKeybind(e.code, e.shiftKey);
if (matchedBuild !== null) {
e.preventDefault();
this.setGhostStructure(matchedBuild);
}
}
if (this.keybindMatchesEvent(e, this.keybinds.requestAlliance)) {
@@ -943,6 +952,11 @@ export class InputHandler {
return null;
}
private canUseBuildKeybinds(): boolean {
const myPlayer = this.gameView.myPlayer?.();
return !this.gameView.inSpawnPhase() && myPlayer?.isAlive() === true;
}
private getPinchDistance(): number {
const pointerEvents = Array.from(this.pointers.values());
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
+10
View File
@@ -620,6 +620,16 @@ export class UserSettingModal extends BaseModal {
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="retaliateAttack"
label=${translateText("user_setting.retaliate_attack")}
description=${translateText("user_setting.retaliate_attack_desc")}
defaultKey="Shift+KeyR"
.value=${this.getKeyValue("retaliateAttack")}
.display=${this.getKeyChar("retaliateAttack")}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="swapDirection"
label=${translateText("user_setting.swap_direction")}
+1 -1
View File
@@ -404,7 +404,7 @@ export class CosmeticContainer extends LitElement {
if (this.name) {
this._nameEl ??= document.createElement("div");
const cfg = rarityConfig[this.rarity] ?? fallback;
this._nameEl.className = `text-xs font-bold uppercase tracking-wider text-center truncate w-full`;
this._nameEl.className = `text-xs font-bold uppercase tracking-wider text-center whitespace-normal break-words w-full`;
this._nameEl.style.color = cfg.nameColor;
this._nameEl.title = this.name;
this._nameEl.textContent = this.name;
+4 -4
View File
@@ -69,17 +69,17 @@ export class MobileNavBar extends LitElement {
></div>
<div
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] pt-4 pb-4 px-5 gap-4 lg:gap-[clamp(1rem,3vh,3rem)]"
>
<!-- Logo + Menu -->
<div
class="flex flex-col text-malibu-blue mb-[clamp(1rem,2vh,2rem)] ml-[clamp(0.2rem,0.4vw,0.4vh)]"
class="flex flex-col text-malibu-blue mb-4 ml-[clamp(0.2rem,0.4vw,0.4vh)]"
>
<div class="flex flex-col items-center gap-2">
<div class="flex flex-col items-center gap-1">
<img
src=${assetUrl("images/OpenFrontLogo.svg")}
alt="OpenFront"
class="h-full w-auto"
class="w-auto h-auto max-w-[220px] max-h-[4.5rem]"
/>
<div
id="game-version"
+1 -1
View File
@@ -18,7 +18,7 @@ export function placeName(game: Game, player: Player): NameViewData {
player.largestClusterBoundingBox ??
calculateBoundingBox(game, player.borderTiles());
let scalingFactor = 1;
let scalingFactor: number;
const width = boundingBox.max.x - boundingBox.min.x;
const height = boundingBox.max.y - boundingBox.min.y;
const size = Math.min(width, height);
@@ -489,7 +489,7 @@ export class SpriteFactory {
if (stage === undefined) throw new Error("Not initialized");
const parentContainer = new PIXI.Container();
const circle = new PIXI.Graphics();
let radius = 0;
let radius: number;
switch (type) {
case UnitType.SAMLauncher:
radius = this.game.config().samRange(level ?? 1);
@@ -350,7 +350,7 @@ export class StructureIconsLayer implements Layer {
(scale <= ZOOM_THRESHOLD || !this.renderSprites);
this.levelsStage!.visible = scale > ZOOM_THRESHOLD && this.renderSprites;
if (this.renderer) {
this.renderer?.render(this.rootStage);
this.renderer.render(this.rootStage);
mainContext.drawImage(this.renderer.canvas, 0, 0);
}
}
+1 -1
View File
@@ -244,7 +244,7 @@ export class TerritoryLayer implements Layer {
minRad + (maxRad - minRad) * (0.5 + 0.5 * Math.sin(this.borderAnimTime));
const baseColor = this.theme.spawnHighlightSelfColor(); //white
let teamColor: Colord | null = null;
let teamColor: Colord;
const team: Team | null = focusedPlayer.team();
if (team !== null && Object.values(ColoredTeams).includes(team)) {
+2 -3
View File
@@ -63,10 +63,9 @@ export async function collectGraphicsDiagnostics(
/* ---------- Rendering ---------- */
let gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
let type: RendererType = "Canvas2D";
gl =
const gl =
canvas.getContext("webgl2", { antialias: true }) ??
canvas.getContext("webgl", { antialias: true });
@@ -111,7 +110,7 @@ export async function collectGraphicsDiagnostics(
/* ---------- Power ---------- */
let power: PowerInfo = {};
let power: PowerInfo;
if ("getBattery" in navigator) {
try {
+1 -1
View File
@@ -131,7 +131,7 @@ export class GameRunner {
this.currTurn++;
let updates: GameUpdates;
let tickExecutionDuration: number = 0;
let tickExecutionDuration: number;
try {
const startTime = performance.now();
+1 -1
View File
@@ -65,7 +65,7 @@ export class ColorAllocator {
this.availableColors = [...this.fallbackColors];
}
let selectedIndex = 0;
let selectedIndex: number;
if (this.assigned.size === 0 || this.assigned.size > 50) {
// Randomly pick the first color if no colors have been assigned yet.
+2 -2
View File
@@ -635,8 +635,8 @@ export class DefaultConfig implements Config {
defenderTroopLoss: number;
tilesPerTickUsed: number;
} {
let mag = 0;
let speed = 0;
let mag: number;
let speed: number;
const type = gm.terrainType(tileToConquer);
switch (type) {
case TerrainType.Plains:
+28 -24
View File
@@ -274,12 +274,11 @@ export class AttackExecution implements Execution {
this.attack.removeBorderTile(tileToConquer);
let onBorder = false;
for (const n of this.mg.neighbors(tileToConquer)) {
if (this.mg.owner(n) === this._owner) {
this.mg.forEachNeighbor(tileToConquer, (n) => {
if (!onBorder && this.mg.owner(n) === this._owner) {
onBorder = true;
break;
}
}
});
if (this.mg.owner(tileToConquer) !== this.target || !onBorder) {
continue;
}
@@ -323,22 +322,22 @@ export class AttackExecution implements Execution {
const tickNow = this.mg.ticks(); // cache tick
for (const neighbor of this.mg.neighbors(tile)) {
this.mg.forEachNeighbor(tile, (neighbor) => {
if (
this.mg.isWater(neighbor) ||
this.mg.owner(neighbor) !== this.target
) {
continue;
return;
}
this.attack.addBorderTile(neighbor);
this.attack!.addBorderTile(neighbor);
let numOwnedByMe = 0;
for (const n of this.mg.neighbors(neighbor)) {
this.mg.forEachNeighbor(neighbor, (n) => {
if (this.mg.owner(n) === this._owner) {
numOwnedByMe++;
}
}
});
let mag = 0;
let mag: number;
switch (this.mg.terrainType(neighbor)) {
case TerrainType.Plains:
mag = 1;
@@ -349,6 +348,9 @@ export class AttackExecution implements Execution {
case TerrainType.Mountain:
mag = 2;
break;
default:
mag = 0;
break;
}
const priority =
@@ -356,33 +358,35 @@ export class AttackExecution implements Execution {
tickNow;
this.toConquer.enqueue(neighbor, priority);
}
});
}
private handleDeadDefender() {
if (!(this.target.isPlayer() && this.target.numTilesOwned() < 100)) return;
const target: Player = this.target;
this.mg.conquerPlayer(this._owner, this.target);
this.mg.conquerPlayer(this._owner, target);
for (let i = 0; i < 10; i++) {
for (const tile of this.target.tiles()) {
const borders = this.mg
.neighbors(tile)
.some((t) => this.mg.owner(t) === this._owner);
for (const tile of target.tiles()) {
let borders = false;
this.mg.forEachNeighbor(tile, (t) => {
if (!borders && this.mg.owner(t) === this._owner) {
borders = true;
}
});
if (borders) {
this._owner.conquer(tile);
} else {
for (const neighbor of this.mg.neighbors(tile)) {
let captured = false;
this.mg.forEachNeighbor(tile, (neighbor) => {
if (captured) return;
const no = this.mg.owner(neighbor);
if (
no.isPlayer() &&
no !== this.target &&
!no.isFriendly(this.target)
) {
if (no.isPlayer() && no !== target && !no.isFriendly(target)) {
this.mg.player(no.id()).conquer(tile);
break;
captured = true;
}
}
});
}
}
}
+1 -1
View File
@@ -183,7 +183,7 @@ export class AiAttackBehavior {
continue;
}
let matchesCriteria = false;
let matchesCriteria: boolean;
if (highInterestOnly) {
// High-interest targeting: prioritize unowned tiles or tiles owned by bots
matchesCriteria = !owner.isPlayer() || owner.type() === PlayerType.Bot;
+4
View File
@@ -169,6 +169,8 @@ export enum GameMapType {
Antarctica = "Antarctica",
ArchipelagoSea = "ArchipelagoSea",
BajaCalifornia = "Baja California",
MiddleEast = "Middle East",
TaiwanStrait = "Taiwan Strait",
}
export type GameMapName = keyof typeof GameMapType;
@@ -230,6 +232,8 @@ export const mapCategories: Record<string, GameMapType[]> = {
GameMapType.BeringSea,
GameMapType.ArchipelagoSea,
GameMapType.BajaCalifornia,
GameMapType.MiddleEast,
GameMapType.TaiwanStrait,
],
fantasy: [
GameMapType.Pangaea,
+1
View File
@@ -19,6 +19,7 @@ export function getDefaultKeybinds(isMac: boolean): Record<string, string> {
attackRatioUp: "KeyY",
boatAttack: "KeyB",
groundAttack: "KeyG",
retaliateAttack: "Shift+KeyR",
requestAlliance: "KeyK",
breakAlliance: "KeyL",
swapDirection: "KeyU",
+1 -3
View File
@@ -11,9 +11,7 @@ export interface ParabolaOptions {
const PARABOLA_MIN_HEIGHT = 50;
export class ParabolaUniversalPathFinder
implements SteppingPathFinder<TileRef>
{
export class ParabolaUniversalPathFinder implements SteppingPathFinder<TileRef> {
private curve: DistanceBasedBezierCurve | null = null;
private lastTo: TileRef | null = null;
+1 -1
View File
@@ -50,7 +50,7 @@ async function drain(): Promise<void> {
draining = true;
drainRequested = false;
let shouldContinue = false;
let shouldContinue: boolean;
try {
const gr = await gameRunner;
if (!gr) {
+1 -2
View File
@@ -121,8 +121,7 @@ export interface AttackClusteredPositionsMessage extends BaseWorkerMessage {
attackID?: string;
}
export interface AttackClusteredPositionsResultMessage
extends BaseWorkerMessage {
export interface AttackClusteredPositionsResultMessage extends BaseWorkerMessage {
type: "attack_clustered_positions_result";
attacks: { id: string; positions: { x: number; y: number }[] }[];
}
+1 -1
View File
@@ -210,7 +210,7 @@ export async function buildPreview(
? `${mode} on ${map}${gameTypeLabel}`
: "OpenFront Game";
let description = "";
let description: string;
if (isFinished) {
const parts: string[] = [];
if (winner) {
+3 -1
View File
@@ -574,7 +574,7 @@ export class GameServer {
}
} catch (error) {
this.log.info(
`error handline websocket request in game server: ${error}`,
`error handling websocket request in game server: ${error}`,
{
clientID: client.clientID,
},
@@ -791,6 +791,8 @@ export class GameServer {
} satisfies ServerStartGameMessage),
);
} catch (error) {
// can be enabled once we can use {cause: error} in Error constructor starting with ES2022
// eslint-disable-next-line preserve-caught-error
throw new Error(
`error sending start message for game ${this.id}, ${error}`.substring(
0,
+5 -9
View File
@@ -15,11 +15,6 @@ const config = getServerConfigFromServer();
const resource = getOtelResource();
// Initialize the OpenTelemetry Logger Provider
const loggerProvider = new LoggerProvider({
resource,
});
if (config.otelEnabled()) {
console.log("OTEL enabled");
// Configure OpenTelemetry endpoint with basic auth (if provided)
@@ -31,10 +26,11 @@ if (config.otelEnabled()) {
headers,
});
// Add a log processor with the exporter
loggerProvider.addLogRecordProcessor(
new SimpleLogRecordProcessor(logExporter),
);
// Initialize the OpenTelemetry Logger Provider
const loggerProvider = new LoggerProvider({
resource,
processors: [new SimpleLogRecordProcessor(logExporter)],
});
// Set as the global logger provider
logsAPI.logs.setGlobalLoggerProvider(loggerProvider);
+36 -34
View File
@@ -30,72 +30,74 @@ const MAX_PLAYER_COUNT = 125;
// How many times each map should appear in the playlist.
// Note: The Partial should eventually be removed for better type safety.
const frequency: Partial<Record<GameMapName, number>> = {
Achiran: 5,
Aegean: 6,
Africa: 7,
Alps: 4,
AmazonRiver: 3,
Antarctica: 1,
ArchipelagoSea: 3,
Arctic: 6,
Asia: 6,
Australia: 4,
Achiran: 5,
Baikal: 5,
BajaCalifornia: 4,
BeringSea: 5,
BeringStrait: 2,
BetweenTwoSeas: 5,
BlackSea: 6,
BosphorusStraits: 3,
Britannia: 5,
Caucasus: 5,
Conakry: 3,
DeglaciatedAntarctica: 4,
Didier: 1,
DidierFrance: 1,
Dyslexdria: 8,
EastAsia: 5,
Europe: 7,
FalklandIslands: 4,
FaroeIslands: 4,
FourIslands: 4,
GatewayToTheAtlantic: 5,
GreatLakes: 6,
GulfOfStLawrence: 4,
Halkidiki: 4,
Hawaii: 4,
Iceland: 4,
Italia: 6,
Japan: 6,
Lemnos: 3,
Lisbon: 4,
LosAngeles: 8,
Luna: 6,
Manicouagan: 4,
MareNostrum: 6,
Mars: 3,
Mena: 6,
MiddleEast: 8,
MilkyWay: 8,
Montreal: 6,
NewYorkCity: 3,
NileDelta: 4,
NorthAmerica: 5,
Pangaea: 5,
Passage: 4,
Pluto: 6,
SanFrancisco: 3,
Sierpinski: 10,
SouthAmerica: 5,
StraitOfGibraltar: 5,
Svalmel: 8,
World: 20,
Lemnos: 3,
Passage: 4,
TwoLakes: 6,
StraitOfHormuz: 4,
Surrounded: 4,
DidierFrance: 1,
Didier: 1,
AmazonRiver: 3,
BosphorusStraits: 3,
BeringStrait: 2,
Sierpinski: 10,
TheBox: 3,
Yenisei: 6,
TradersDream: 4,
Hawaii: 4,
Alps: 4,
NileDelta: 4,
Arctic: 6,
SanFrancisco: 3,
Aegean: 6,
MilkyWay: 8,
MareNostrum: 6,
Dyslexdria: 8,
GreatLakes: 6,
StraitOfMalacca: 4,
Luna: 6,
Conakry: 3,
Caucasus: 5,
LosAngeles: 8,
BeringSea: 5,
Antarctica: 1,
ArchipelagoSea: 3,
BajaCalifornia: 4,
Surrounded: 4,
Svalmel: 8,
TaiwanStrait: 5,
TheBox: 3,
TradersDream: 4,
TwoLakes: 6,
World: 20,
Yenisei: 6,
};
const TEAM_WEIGHTS: { config: TeamCountConfig; weight: number }[] = [
+2
View File
@@ -219,6 +219,8 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
try {
decodePatternData(found.pattern, this.b64urlDecode);
} catch (e) {
// can be enabled once we can use {cause: error} in Error constructor starting with ES2022
// eslint-disable-next-line preserve-caught-error
throw new Error(`Invalid pattern ${name}`);
}
+16 -2
View File
@@ -9,7 +9,7 @@ import {
import { UIState } from "../src/client/graphics/UIState";
import { EventBus } from "../src/core/EventBus";
import { UnitType } from "../src/core/game/Game";
import { GameView } from "../src/core/game/GameView";
import { GameView, PlayerView } from "../src/core/game/GameView";
import { KEYBINDS_KEY, UserSettings } from "../src/core/game/UserSettings";
class MockPointerEvent {
@@ -49,7 +49,10 @@ describe("InputHandler AutoUpgrade", () => {
testSettings = new UserSettings();
testSettings.removeCached(KEYBINDS_KEY, false);
mockGameView = { inSpawnPhase: () => false } as GameView;
mockGameView = {
inSpawnPhase: () => false,
myPlayer: () => ({ isAlive: () => true }),
} as GameView;
mockCanvas = document.createElement("canvas");
mockCanvas.width = 800;
mockCanvas.height = 600;
@@ -626,6 +629,17 @@ describe("InputHandler AutoUpgrade", () => {
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MIRV);
});
test("does not set ghost structure when the player is dead", () => {
mockGameView.myPlayer = () =>
({ isAlive: () => false }) as unknown as PlayerView;
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
);
expect(inputHandler["uiState"].ghostStructure).toBeNull();
});
});
describe("Build keybind two-phase matching (exact code first, then digit/Numpad alias)", () => {
+1 -1
View File
@@ -620,5 +620,5 @@ describe("Translation System", () => {
expect(missingKeys).toEqual([]);
expect(unusedKeys).toEqual([]);
});
}, 30000);
});
+22 -33
View File
@@ -53,9 +53,8 @@ describe("ClanModal — handlers", () => {
describe("handleApprove increments selectedClan.memberCount", () => {
it("increments memberCount by 1 after successful approveClanRequest", async () => {
const { approveClanRequest, fetchClanRequests } = await import(
"../../../src/client/ClanApi"
);
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanRequests as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -90,9 +89,8 @@ describe("ClanModal — handlers", () => {
});
it("does not increment memberCount when approveClanRequest fails", async () => {
const { approveClanRequest, fetchClanRequests } = await import(
"../../../src/client/ClanApi"
);
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -125,9 +123,8 @@ describe("ClanModal — handlers", () => {
});
it("treats undefined memberCount as 0 and increments to 1", async () => {
const { approveClanRequest, fetchClanRequests } = await import(
"../../../src/client/ClanApi"
);
const { approveClanRequest, fetchClanRequests } =
await import("../../../src/client/ClanApi");
(approveClanRequest as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanRequests as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -271,9 +268,8 @@ describe("ClanModal — handlers", () => {
});
it("handleBan syncs memberCount via clan-updated event on success", async () => {
const { banClanMember, fetchClanMembers } = await import(
"../../../src/client/ClanApi"
);
const { banClanMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
(banClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
// Server returns the post-ban member total (was 5, now 4).
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
@@ -310,9 +306,8 @@ describe("ClanModal — handlers", () => {
describe("handleUnban", () => {
it("removes ban from list and decrements total on success", async () => {
const { unbanClanMember, fetchClanBans } = await import(
"../../../src/client/ClanApi"
);
const { unbanClanMember, fetchClanBans } =
await import("../../../src/client/ClanApi");
(unbanClanMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanBans as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [
@@ -377,9 +372,8 @@ describe("ClanModal — handlers", () => {
});
it("calls kickMember and syncs memberCount on success", async () => {
const { kickMember, fetchClanMembers } = await import(
"../../../src/client/ClanApi"
);
const { kickMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
(kickMember as ReturnType<typeof vi.fn>).mockResolvedValue(true);
(fetchClanMembers as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
results: [],
@@ -411,9 +405,8 @@ describe("ClanModal — handlers", () => {
});
it("does not mutate state when kickMember fails", async () => {
const { kickMember, fetchClanMembers } = await import(
"../../../src/client/ClanApi"
);
const { kickMember, fetchClanMembers } =
await import("../../../src/client/ClanApi");
(kickMember as ReturnType<typeof vi.fn>).mockResolvedValue({
error: "clan_modal.error_generic",
});
@@ -600,9 +593,8 @@ describe("ClanModal — handlers", () => {
describe("handleJoin", () => {
beforeEach(async () => {
const { fetchClanDetail, fetchClanStats } = await import(
"../../../src/client/ClanApi"
);
const { fetchClanDetail, fetchClanStats } =
await import("../../../src/client/ClanApi");
(fetchClanDetail as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
makeClan({ isOpen: true, memberCount: 5 }),
);
@@ -615,9 +607,8 @@ describe("ClanModal — handlers", () => {
});
it("switches detail view into member mode immediately after open-clan join", async () => {
const { joinClan, fetchClanMembers } = await import(
"../../../src/client/ClanApi"
);
const { joinClan, fetchClanMembers } =
await import("../../../src/client/ClanApi");
(joinClan as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
status: "joined",
});
@@ -781,9 +772,8 @@ describe("ClanModal — handlers", () => {
});
it("clears confirmAction and removes the dialog after confirming", async () => {
const { transferLeadership } = await import(
"../../../src/client/ClanApi"
);
const { transferLeadership } =
await import("../../../src/client/ClanApi");
(transferLeadership as ReturnType<typeof vi.fn>).mockResolvedValue(true);
const dialog = modal.querySelector("confirm-dialog");
@@ -802,9 +792,8 @@ describe("ClanModal — handlers", () => {
});
it("clears confirmAction when cancel is clicked, without calling the API", async () => {
const { transferLeadership } = await import(
"../../../src/client/ClanApi"
);
const { transferLeadership } =
await import("../../../src/client/ClanApi");
const dialog = modal.querySelector("confirm-dialog");
expect(dialog).toBeTruthy();
@@ -241,9 +241,8 @@ describe("ClanModal — rendering", () => {
});
it("shows 0 in the stats row of the detail view when memberCount is undefined", async () => {
const { fetchClanDetail, fetchClanStats } = await import(
"../../../src/client/ClanApi"
);
const { fetchClanDetail, fetchClanStats } =
await import("../../../src/client/ClanApi");
(fetchClanDetail as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
makeClan({ memberCount: undefined }),
);
+6 -8
View File
@@ -5,7 +5,6 @@ import path from "path";
import { fileURLToPath } from "url";
import { defineConfig, loadEnv, type Plugin } from "vite";
import { createHtmlPlugin } from "vite-plugin-html";
import tsconfigPaths from "vite-tsconfig-paths";
import {
type AssetManifest,
buildAssetUrl,
@@ -155,17 +154,13 @@ export default defineConfig(({ mode }) => {
publicDir: isProduction ? false : "resources",
resolve: {
tsconfigPaths: true,
alias: {
"protobufjs/minimal": path.resolve(
__dirname,
"node_modules/protobufjs/minimal.js",
),
resources: path.resolve(__dirname, "resources"),
},
},
plugins: [
tsconfigPaths(),
...(!isProduction
? [serveProprietaryDir(proprietaryDir, resourcesDir)]
: []),
@@ -209,8 +204,11 @@ export default defineConfig(({ mode }) => {
assetsDir: "assets", // Sub-directory for assets
rollupOptions: {
output: {
manualChunks: {
vendor: ["pixi.js", "howler", "zod", "protobufjs"],
manualChunks: (id) => {
const vendorModules = ["pixi.js", "howler", "zod"];
if (vendorModules.some((module) => id.includes(module))) {
return "vendor";
}
},
},
},