Merge pull request #3897 from openfrontio/TOS-and-Privacy-Policy-updates
Update privacy policy and terms of service
@@ -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
|
||||
|
||||
@@ -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").
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -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 |
@@ -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 (1–100%)",
|
||||
"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.",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class GameRunner {
|
||||
this.currTurn++;
|
||||
|
||||
let updates: GameUpdates;
|
||||
let tickExecutionDuration: number = 0;
|
||||
let tickExecutionDuration: number;
|
||||
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }[] }[];
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }[] = [
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -620,5 +620,5 @@ describe("Translation System", () => {
|
||||
|
||||
expect(missingKeys).toEqual([]);
|
||||
expect(unusedKeys).toEqual([]);
|
||||
});
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||