├── .dockerignore
├── .env.dist
├── .github
└── workflows
│ └── docker-image.yml
├── .gitignore
├── Caddyfile
├── Dockerfile
├── LICENSE.md
├── README.md
├── __tests__
├── lib
│ └── conquerUpdater.test.js
└── mockupData
│ └── TheFingersHex
│ ├── 1.json
│ ├── 2.json
│ ├── 3.json
│ ├── 4.json
│ └── 5.json
├── app.js
├── bin
└── www
├── config.dist.yml
├── docker-compose.yml
├── frontend
├── Components
│ ├── Draft.vue
│ ├── Stats.vue
│ ├── VPCounter.vue
│ └── VPCounterStats.vue
├── Interaction
│ └── Merge.js
├── Search.js
├── admin.js
├── flags.js
├── index.js
├── main.js
├── mapControls.js
├── mapEditTools.js
├── measure.js
├── staticLayer.js
├── stats.js
├── style.scss
├── tools
│ ├── arty.js
│ ├── edit.js
│ ├── icon.js
│ ├── line.js
│ ├── merge.js
│ ├── polygon.js
│ ├── scissor.js
│ ├── select.js
│ ├── sidebar.js
│ └── sidebarArty.js
└── webSocket.js
├── jsconfig.json
├── lib
├── ACLS.js
├── ambient.d.ts
├── config.js
├── conquerUpdater.js
├── discord.js
├── draftStatus.js
├── eventLog.js
├── featureLoader.js
├── fileHandler.js
├── session.js
├── updater.js
└── warapi.js
├── package-lock.json
├── package.json
├── public
├── images
│ ├── artilleryChevron.svg
│ ├── artilleryTarget.svg
│ ├── artilleryVector.svg
│ ├── artilleryWindDir.svg
│ ├── artilleryWindPip.svg
│ ├── base
│ │ ├── EmplacementHouse.svg
│ │ ├── base.svg
│ │ ├── base_frontline.svg
│ │ ├── base_obs.svg
│ │ ├── base_obs_t2.svg
│ │ ├── base_sleep.svg
│ │ ├── flag.png
│ │ ├── flag.svg
│ │ ├── flag2.png
│ │ ├── flag2.svg
│ │ ├── friendly_planned_intel_center.svg
│ │ ├── friendly_planned_storm_cannon.svg
│ │ ├── friendly_planned_weather_station.svg
│ │ └── lock.svg
│ ├── colonial.webp
│ ├── cross.svg
│ ├── crossColonial.png
│ ├── crossWarden.png
│ ├── facility-enemy
│ │ ├── enemy_ammo_facility.svg
│ │ ├── enemy_ass_mats_1.svg
│ │ ├── enemy_ass_mats_2.svg
│ │ ├── enemy_ass_mats_3.svg
│ │ ├── enemy_ass_mats_4.svg
│ │ ├── enemy_ass_mats_5.svg
│ │ ├── enemy_base.svg
│ │ ├── enemy_base_frontline.svg
│ │ ├── enemy_base_obs.svg
│ │ ├── enemy_base_obs_t2.svg
│ │ ├── enemy_base_sleep.svg
│ │ ├── enemy_broken_components.svg
│ │ ├── enemy_cmats.svg
│ │ ├── enemy_diesel.svg
│ │ ├── enemy_drydock.svg
│ │ ├── enemy_enriched.svg
│ │ ├── enemy_facility_generic.svg
│ │ ├── enemy_fire_equipment.svg
│ │ ├── enemy_heavy.svg
│ │ ├── enemy_pcmats.svg
│ │ ├── enemy_petrol.svg
│ │ ├── enemy_planned_intel_center.svg
│ │ ├── enemy_planned_storm_cannon.svg
│ │ ├── enemy_planned_weather_station.svg
│ │ ├── enemy_scmats.svg
│ │ ├── enemy_supplies.svg
│ │ ├── enemy_tank_facility.svg
│ │ ├── enemy_train_facility.svg
│ │ ├── enemy_vehicle_facility.svg
│ │ └── enemy_water.svg
│ ├── facility-private
│ │ ├── private_ammo_facility.svg
│ │ ├── private_ass_mats_1.svg
│ │ ├── private_ass_mats_2.svg
│ │ ├── private_ass_mats_3.svg
│ │ ├── private_ass_mats_4.svg
│ │ ├── private_ass_mats_5.svg
│ │ ├── private_broken_components.svg
│ │ ├── private_cmats.svg
│ │ ├── private_diesel.svg
│ │ ├── private_drydock.svg
│ │ ├── private_enriched.svg
│ │ ├── private_facility_generic.svg
│ │ ├── private_heavy.svg
│ │ ├── private_pcmats.svg
│ │ ├── private_petrol.svg
│ │ ├── private_scmats.svg
│ │ ├── private_supplies.svg
│ │ ├── private_tank_facility.svg
│ │ ├── private_train_facility.svg
│ │ ├── private_vehicle_facility.svg
│ │ └── private_water.svg
│ ├── facility
│ │ ├── Rail_Yard.svg
│ │ ├── friendly_fire_equipment.svg
│ │ ├── generator.svg
│ │ ├── maintenance.svg
│ │ ├── port.svg
│ │ ├── public_ammo_facility.svg
│ │ ├── public_ass_mats_1.svg
│ │ ├── public_ass_mats_2.svg
│ │ ├── public_ass_mats_3.svg
│ │ ├── public_ass_mats_4.svg
│ │ ├── public_ass_mats_5.svg
│ │ ├── public_broken_components.svg
│ │ ├── public_cmats.svg
│ │ ├── public_diesel.svg
│ │ ├── public_drydock.svg
│ │ ├── public_enriched.svg
│ │ ├── public_facility_generic.svg
│ │ ├── public_heavy.svg
│ │ ├── public_pcmats.svg
│ │ ├── public_petrol.svg
│ │ ├── public_scmats.svg
│ │ ├── public_supplies.svg
│ │ ├── public_tank_facility.svg
│ │ ├── public_train_facility.svg
│ │ ├── public_vehicle_facility.svg
│ │ └── public_water.svg
│ ├── favicon.png
│ ├── favicon.svg
│ ├── field
│ │ ├── MapIconCoalFieldColor.png
│ │ ├── MapIconComponentMineColor.png
│ │ ├── MapIconComponentsColor.png
│ │ ├── MapIconFacilityMineOilRig.png
│ │ ├── MapIconOilFieldColor.png
│ │ ├── MapIconSalvageColor.png
│ │ ├── MapIconSalvageMineColor.png
│ │ ├── MapIconSulfurColor.png
│ │ ├── MapIconSulfurMineColor.png
│ │ ├── coal_field.svg
│ │ ├── comp_field.svg
│ │ ├── comp_mine.svg
│ │ ├── oil_field.svg
│ │ ├── scrap_field.svg
│ │ ├── scrap_mine.svg
│ │ ├── sulfur_field.svg
│ │ └── sulfur_mine.svg
│ ├── humanQueue.svg
│ ├── humanQueueColonial.png
│ ├── humanQueueWarden.png
│ ├── industry
│ │ ├── MapIconAmmoFactory.png
│ │ ├── MapIconCoastalGun.png
│ │ ├── MapIconConstructionYard.png
│ │ ├── MapIconFactory.png
│ │ ├── MapIconManufacturing.png
│ │ ├── MapIconMassProductionFactory.png
│ │ ├── MapIconMedical.png
│ │ ├── MapIconSeaport.png
│ │ ├── MapIconShipyard.png
│ │ ├── MapIconStorageFacility.png
│ │ ├── MapIconTechCenter.png
│ │ ├── MapIconVehicle.png
│ │ └── MapIconWorkshop.png
│ ├── information
│ │ ├── WeatherEventRain.svg
│ │ ├── WeatherEventSnow.svg
│ │ ├── caution.svg
│ │ ├── danger.svg
│ │ ├── enemy_bridge_destroyed.svg
│ │ ├── enemy_freighter_blockade.svg
│ │ ├── friendly_bridge_destroyed.svg
│ │ ├── friendly_freighter_blockade.svg
│ │ ├── information.svg
│ │ ├── minefield.svg
│ │ ├── warning.svg
│ │ └── warningIce.svg
│ ├── og.png
│ ├── og.svg
│ ├── sign
│ │ ├── Carriageway_Both_Points.svg
│ │ ├── Carriageway_Left_Point_Only.svg
│ │ ├── Carriageway_Right_Point_Only.svg
│ │ ├── Dual_Carriageway.svg
│ │ ├── End_Of_Dual_Carriageway.svg
│ │ ├── dead_end.svg
│ │ ├── dual_carriageway_ends_ahead.svg
│ │ ├── keep_left.svg
│ │ ├── keep_right.svg
│ │ ├── level_crossing.svg
│ │ ├── motorway.svg
│ │ ├── motorway_end.svg
│ │ ├── no_entry_sign.svg
│ │ ├── no_stopping.svg
│ │ ├── no_waiting.svg
│ │ └── parking.svg
│ ├── stormCannon
│ │ ├── MapIconBorderBase.png
│ │ ├── MapIconBunkerBaseTier1.png
│ │ ├── MapIconBunkerBaseTier2.png
│ │ ├── MapIconBunkerBaseTier3.png
│ │ ├── MapIconFacilityVehicleFactory1.png
│ │ ├── MapIconForwardBase1.png
│ │ ├── MapIconIntelCenter.png
│ │ ├── MapIconIntelCenterColonial.png
│ │ ├── MapIconIntelCenterWarden.png
│ │ ├── MapIconRocketGroundZero.png
│ │ ├── MapIconRocketGroundZeroColonial.png
│ │ ├── MapIconRocketGroundZeroWarden.png
│ │ ├── MapIconRocketSite.png
│ │ ├── MapIconRocketSiteColonial.png
│ │ ├── MapIconRocketSiteWarden.png
│ │ ├── MapIconRocketSiteWithRocket.png
│ │ ├── MapIconRocketSiteWithRocketColonial.png
│ │ ├── MapIconRocketSiteWithRocketWarden.png
│ │ ├── MapIconRocketTarget.png
│ │ ├── MapIconRocketTargetColonial.png
│ │ ├── MapIconRocketTargetWarden.png
│ │ ├── MapIconStormCannon.png
│ │ ├── MapIconStormCannonColonial.png
│ │ ├── MapIconStormCannonWarden.png
│ │ ├── MapIconWeatherStation.png
│ │ ├── MapIconWeatherStationColonial.png
│ │ └── MapIconWeatherStationWarden.png
│ ├── town
│ │ ├── MapIconFortKeep.png
│ │ ├── MapIconObservationTower.png
│ │ ├── MapIconRelicBase.png
│ │ ├── MapIconSafehouse.png
│ │ ├── MapIconTownBaseTier1.png
│ │ ├── MapIconTownBaseTier1Colonial.png
│ │ ├── MapIconTownBaseTier1Nuke.png
│ │ ├── MapIconTownBaseTier1Warden.png
│ │ ├── MapIconTownBaseTier2.png
│ │ ├── MapIconTownBaseTier2Colonial.png
│ │ ├── MapIconTownBaseTier2Nuke.png
│ │ ├── MapIconTownBaseTier2Warden.png
│ │ ├── MapIconTownBaseTier3.png
│ │ ├── MapIconTownBaseTier3Colonial.png
│ │ ├── MapIconTownBaseTier3Nuke.png
│ │ └── MapIconTownBaseTier3Warden.png
│ └── warden.webp
└── static.json
├── routes
└── index.js
├── syncBackups.sh
├── tools
├── deadland.json
├── defaultFeatures.js
├── eventlog.js
├── fetchFields.js
├── imageTaint.html
├── moveDefaultFeatures.js
├── regionGenerator.js
├── staticUpdater.js
└── voroni.js
├── views
├── access.html
├── admin.config.html
├── admin.eventlog.html
├── base.html
├── clock.svg
├── error.html
├── help.html
├── index.html
├── login.html
├── sidebar-arty.html
├── sidebar-edit.html
└── stats.html
├── webpack.config.js
├── webpack.dev.js
├── webpack.prod.js
└── websocket.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | backups
2 | node_modules
3 | sessions
4 | public/map
5 | data
6 | logs
7 | .git
8 | .dockerignore
9 | .gitignore
10 | .idea
11 | .env
12 | start.bash
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | PORT=3000
3 | SECRET=secret
4 | COMMIT_HASH=dev
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: docker-image
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | - 'dev'
8 | tags:
9 | - 'v*'
10 |
11 | jobs:
12 | docker:
13 | runs-on: ubuntu-latest
14 | steps:
15 | -
16 | name: Checkout
17 | uses: actions/checkout@v3
18 | -
19 | name: Docker meta
20 | id: meta
21 | uses: docker/metadata-action@v4
22 | with:
23 | images: |
24 | attribdd/foxhole-map-annotate
25 | tags: |
26 | type=ref,event=branch
27 | type=ref,event=pr
28 | type=semver,pattern={{version}}
29 | type=semver,pattern={{major}}.{{minor}}
30 | -
31 | name: Login to DockerHub
32 | if: github.event_name != 'pull_request'
33 | uses: docker/login-action@v2
34 | with:
35 | username: ${{ secrets.DOCKERHUB_USERNAME }}
36 | password: ${{ secrets.DOCKERHUB_TOKEN }}
37 | -
38 | name: Build and push
39 | uses: docker/build-push-action@v3
40 | with:
41 | context: .
42 | push: ${{ github.event_name != 'pull_request' }}
43 | tags: ${{ steps.meta.outputs.tags }}
44 | labels: ${{ steps.meta.outputs.labels }}
45 | build-args: |
46 | COMMIT_HASH=${{ github.sha }}
47 | -
48 | name: Build and push
49 | uses: docker/build-push-action@v3
50 | with:
51 | context: .
52 | target: caddy
53 | push: ${{ github.event_name != 'pull_request' }}
54 | tags: ${{ steps.meta.outputs.tags }}-caddy
55 | labels: ${{ steps.meta.outputs.labels }}
56 | build-args: |
57 | COMMIT_HASH=${{ github.sha }}
58 |
59 | deploy:
60 | needs: docker
61 | runs-on: ubuntu-latest
62 | if: github.event_name == 'push' && (github.ref_name == 'master' || github.ref_name == 'dev')
63 | steps:
64 | -
65 | name: Deploy to production
66 | uses: appleboy/ssh-action@v1.0.3
67 | with:
68 | host: ${{ secrets.PROD_HOST }}
69 | username: ${{ secrets.PROD_USER }}
70 | key: ${{ secrets.PROD_KEY }}
71 | passphrase: ${{ secrets.PASSPHRASE }}
72 | script: |
73 | docker pull attribdd/foxhole-map-annotate:${{ github.ref_name }} attribdd/foxhole-map-annotate:${{ github.ref_name }}-caddy
74 | cd /mnt/deployments/foxhole-map-annotate/
75 | docker compose up -d
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | backups
3 | public/map*
4 | sessions
5 | data
6 | logs
7 | .env
8 | node_modules
9 | public/dist
10 | hidden
11 |
12 | ## VS Code
13 | .history
14 | .vscode
--------------------------------------------------------------------------------
/Caddyfile:
--------------------------------------------------------------------------------
1 | :80 {
2 | root * /srv/
3 | file_server
4 |
5 | @static {
6 | path /dist/* /images/* /map/*
7 | }
8 |
9 | handle @static {
10 | file_server
11 | header {
12 | Cache-Control "public, max-age=7200000, immutable"
13 | }
14 | }
15 |
16 | @notStatic {
17 | not path /dist/* /images/* /map/*
18 | }
19 |
20 | # Proxy non-static requests to the 'map' container
21 | reverse_proxy @notStatic map:3000 {
22 | transport http {
23 | versions h1 h2
24 | }
25 | header_up Host {host}
26 | header_up X-Real-IP {remote_host}
27 | header_up X-Forwarded-For {remote_host}
28 | header_up X-Forwarded-Proto {scheme}
29 | }
30 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:21 AS build
2 | ENV NODE_ENV=development
3 | WORKDIR /app
4 | COPY ["package.json", "package-lock.json*", "./"]
5 | RUN npm install
6 | COPY . .
7 |
8 | RUN npm run build
9 |
10 | FROM caddy:2.10.0-alpine AS caddy
11 | COPY --from=build /app/public /srv/
12 | COPY Caddyfile /etc/caddy/Caddyfile
13 |
14 | FROM node:21-alpine
15 | ENV NODE_ENV=production
16 | WORKDIR /app
17 | COPY ["package.json", "package-lock.json*", "./"]
18 | RUN npm install --omit=dev
19 |
20 | COPY . .
21 | COPY --from=build /app/public/dist /app/public/dist
22 |
23 | ARG COMMIT_HASH
24 | ENV COMMIT_HASH=${COMMIT_HASH}
25 |
26 | CMD [ "node", "bin/www" ]
--------------------------------------------------------------------------------
/__tests__/mockupData/TheFingersHex/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "regionId": 38,
3 | "scorchedVictoryTowns": 0,
4 | "mapItems": [
5 | {
6 | "teamId": "NONE",
7 | "iconType": 20,
8 | "x": 0.8716014,
9 | "y": 0.58127743,
10 | "flags": 0,
11 | "viewDirection": 0
12 | },
13 | {
14 | "teamId": "COLONIALS",
15 | "iconType": 45,
16 | "x": 0.4513862,
17 | "y": 0.36646345,
18 | "flags": 8,
19 | "viewDirection": 0
20 | }
21 | ],
22 | "mapItemsC": [],
23 | "mapItemsW": [],
24 | "mapTextItems": [],
25 | "lastUpdated": 1701336872072,
26 | "version": 10
27 | }
--------------------------------------------------------------------------------
/__tests__/mockupData/TheFingersHex/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "regionId": 38,
3 | "scorchedVictoryTowns": 0,
4 | "mapItems": [
5 | {
6 | "teamId": "NONE",
7 | "iconType": 20,
8 | "x": 0.8716014,
9 | "y": 0.58127743,
10 | "flags": 0,
11 | "viewDirection": 0
12 | },
13 | {
14 | "teamId": "NONE",
15 | "iconType": 45,
16 | "x": 0.4513862,
17 | "y": 0.36646345,
18 | "flags": 8,
19 | "viewDirection": 0
20 | }
21 | ],
22 | "mapItemsC": [],
23 | "mapItemsW": [],
24 | "mapTextItems": [],
25 | "lastUpdated": 1701336872080,
26 | "version": 11
27 | }
--------------------------------------------------------------------------------
/__tests__/mockupData/TheFingersHex/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "regionId": 38,
3 | "scorchedVictoryTowns": 0,
4 | "mapItems": [
5 | {
6 | "teamId": "NONE",
7 | "iconType": 20,
8 | "x": 0.8716014,
9 | "y": 0.58127743,
10 | "flags": 0,
11 | "viewDirection": 0
12 | },
13 | {
14 | "teamId": "WARDENS",
15 | "iconType": 45,
16 | "x": 0.4513862,
17 | "y": 0.36646345,
18 | "flags": 8,
19 | "viewDirection": 0
20 | },
21 | {
22 | "teamId": "WARDENS",
23 | "iconType": 59,
24 | "x": 0.5513862,
25 | "y": 0.46646345,
26 | "flags": 0,
27 | "viewDirection": 0
28 | }
29 | ],
30 | "mapItemsC": [],
31 | "mapItemsW": [],
32 | "mapTextItems": [],
33 | "lastUpdated": 1701336872090,
34 | "version": 12
35 | }
--------------------------------------------------------------------------------
/__tests__/mockupData/TheFingersHex/4.json:
--------------------------------------------------------------------------------
1 | {
2 | "regionId": 38,
3 | "scorchedVictoryTowns": 0,
4 | "mapItems": [
5 | {
6 | "teamId": "NONE",
7 | "iconType": 20,
8 | "x": 0.8716014,
9 | "y": 0.58127743,
10 | "flags": 0,
11 | "viewDirection": 0
12 | },
13 | {
14 | "teamId": "NONE",
15 | "iconType": 45,
16 | "x": 0.4513862,
17 | "y": 0.36646345,
18 | "flags": 8,
19 | "viewDirection": 0
20 | },
21 | {
22 | "teamId": "WARDENS",
23 | "iconType": 59,
24 | "x": 0.5513862,
25 | "y": 0.46646345,
26 | "flags": 0,
27 | "viewDirection": 0
28 | },
29 | {
30 | "teamId": "WARDENS",
31 | "iconType": 59,
32 | "x": 0.5523862,
33 | "y": 0.46646345,
34 | "flags": 0,
35 | "viewDirection": 0
36 | }
37 | ],
38 | "mapItemsC": [],
39 | "mapItemsW": [],
40 | "mapTextItems": [],
41 | "lastUpdated": 1701336872100,
42 | "version": 13
43 | }
--------------------------------------------------------------------------------
/__tests__/mockupData/TheFingersHex/5.json:
--------------------------------------------------------------------------------
1 | {
2 | "regionId": 38,
3 | "scorchedVictoryTowns": 0,
4 | "mapItems": [
5 | {
6 | "teamId": "NONE",
7 | "iconType": 20,
8 | "x": 0.8716014,
9 | "y": 0.58127743,
10 | "flags": 0,
11 | "viewDirection": 0
12 | },
13 | {
14 | "teamId": "COLONIALS",
15 | "iconType": 45,
16 | "x": 0.4513862,
17 | "y": 0.36646345,
18 | "flags": 8,
19 | "viewDirection": 0
20 | },
21 | {
22 | "teamId": "WARDENS",
23 | "iconType": 59,
24 | "x": 0.5523862,
25 | "y": 0.46646345,
26 | "flags": 0,
27 | "viewDirection": 0
28 | },
29 | {
30 | "teamId": "COLONIALS",
31 | "iconType": 59,
32 | "x": 0.5513862,
33 | "y": 0.46646345,
34 | "flags": 0,
35 | "viewDirection": 0
36 | }
37 | ],
38 | "mapItemsC": [],
39 | "mapItemsW": [],
40 | "mapTextItems": [],
41 | "lastUpdated": 1701336872110,
42 | "version": 14
43 | }
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | import http from "node:http";
8 |
9 | import debug from "debug";
10 |
11 | import app from "../app.js";
12 |
13 | /**
14 | * Get port from environment and store in Express.
15 | */
16 |
17 | var port = normalizePort(process.env.PORT || '3000');
18 | app.set('port', port);
19 |
20 | /**
21 | * Create HTTP server.
22 | */
23 |
24 | var server = http.createServer(app);
25 |
26 | /**
27 | * Listen on provided port, on all network interfaces.
28 | */
29 |
30 | server.listen(port);
31 | server.on('error', onError);
32 | server.on('listening', onListening);
33 |
34 | /**
35 | * Websocket
36 | */
37 | import('../websocket.js')
38 | .then((module) => module.default)
39 | .then((startServer) => startServer(server))
40 |
41 | /**
42 | * Normalize a port into a number, string, or false.
43 | */
44 |
45 | function normalizePort(val) {
46 | var port = parseInt(val, 10);
47 |
48 | if (isNaN(port)) {
49 | // named pipe
50 | return val;
51 | }
52 |
53 | if (port >= 0) {
54 | // port number
55 | return port;
56 | }
57 |
58 | return false;
59 | }
60 |
61 | /**
62 | * Event listener for HTTP server "error" event.
63 | */
64 |
65 | function onError(error) {
66 | if (error.syscall !== 'listen') {
67 | throw error;
68 | }
69 |
70 | var bind = typeof port === 'string'
71 | ? 'Pipe ' + port
72 | : 'Port ' + port;
73 |
74 | // handle specific listen errors with friendly messages
75 | switch (error.code) {
76 | case 'EACCES':
77 | console.error(bind + ' requires elevated privileges');
78 | process.exit(1);
79 | break;
80 | case 'EADDRINUSE':
81 | console.error(bind + ' is already in use');
82 | process.exit(1);
83 | break;
84 | default:
85 | throw error;
86 | }
87 | }
88 |
89 | /**
90 | * Event listener for HTTP server "listening" event.
91 | */
92 |
93 | function onListening() {
94 | var addr = server.address();
95 | var bind = typeof addr === 'string'
96 | ? 'pipe ' + addr
97 | : 'port ' + addr.port;
98 | var debugInstance = debug('foxhole-map-annotate:server');
99 | debugInstance('Listening on ' + bind);
100 | }
101 |
--------------------------------------------------------------------------------
/config.dist.yml:
--------------------------------------------------------------------------------
1 | access:
2 | users: {}
3 | roles: {}
4 |
5 | shard:
6 | name: Able
7 | url: war-service-live.foxholeservices.com
8 |
9 | discord:
10 | key: ''
11 | secret: ''
12 |
13 | basic:
14 | url: http://localhost:3000
15 | title: Warden Express
16 | color: '#245682'
17 | faction: Warden
18 | links: []
19 |
20 | images:
21 | favicon: /images/favicon.svg
22 | faviconPng: /images/favicon.png
23 | opengraph: /images/og.png
24 | logo: /images/favicon.svg
25 |
26 | text:
27 | login: |
28 |
You need to be a verified Warden in WUH or
29 | WTG to see the Map.
30 |
31 | accessDenied: |
32 | You need to be a verified Warden in WUH or WTG.
33 |
34 | feedback: |
35 | If you have Feedback please contact us via WTAs Discord.
36 | If you want more access, please also checkout the Discord from WTA.
37 | Bugs can be reported at #wardenexpress-bugreports on WTAs Discord or at github
38 |
39 | contributors: |
40 | Hosting: attrib
41 |
42 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 |
3 | services:
4 |
5 | map:
6 | build: .
7 | image: "attribdd/foxhole-map-annotate:master"
8 | restart: always
9 | volumes:
10 | - ./public/map:/app/public/map
11 | - ./sessions:/app/sessions
12 | - ./data:/app/data
13 | environment:
14 | SECRET: sessionStorageSecret
15 | ports:
16 | - "3000:3000"
17 | expose:
18 | - 3000
19 |
20 | # optional, better performance for production to handle static files and remove some load from the map server
21 | caddy:
22 | build:
23 | context: .
24 | dockerfile: Dockerfile
25 | target: caddy
26 | image: "attribdd/foxhole-map-annotate:master-caddy"
27 | profiles:
28 | - caddy
29 | restart: always
30 | volumes:
31 | - ./Caddyfile:/etc/caddy/Caddyfile
32 | - ./public/map:/srv/map
33 | - ./data/caddy/data:/data
34 | - ./data/caddy/config:/config
35 | ports:
36 | - "8000:80"
37 | labels:
38 | - "traefik.enable=true"
39 | - "traefik.http.routers.map.rule=Host(`warden.express`)"
40 | - "traefik.http.routers.map.entrypoints=websecure"
41 | - "traefik.http.routers.map.tls.certresolver=le"
42 |
--------------------------------------------------------------------------------
/frontend/Components/VPCounter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 | {{ score.Colonial }}
(+{{ score.ColonialUnclaimed }})/{{ score.requiredVPs }}
5 |
6 |
7 | {{ score.Warden }}
(+{{ score.WardenUnclaimed }})/{{ score.requiredVPs }}
8 |

9 |
10 |
11 |
12 |
43 |
44 |
49 |
50 |
--------------------------------------------------------------------------------
/frontend/Components/VPCounterStats.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 | {{ score.Colonial }}
(+{{ score.ColonialUnclaimed }})/{{ score.requiredVPs }}
5 |
6 |
7 | {{ score.Warden }}
(+{{ score.WardenUnclaimed }})/{{ score.requiredVPs }}
8 |

9 |
10 |
11 |
12 |
46 |
47 |
52 |
53 |
--------------------------------------------------------------------------------
/frontend/Search.js:
--------------------------------------------------------------------------------
1 | import SearchFeature from "ol-ext/control/SearchFeature.js";
2 | import LayerSwitcher from "ol-layerswitcher";
3 | import { getCenter } from "ol/extent.js";
4 |
5 | class Search extends SearchFeature {
6 |
7 | constructor() {
8 | super({
9 | className: 'feature-search text-bg-light',
10 | property: 'notes',
11 | maxItems: 50,
12 | });
13 |
14 | this.on('select', function (e) {
15 | const map = this.getMap();
16 | if (map) {
17 | map.getView().animate({
18 | center: getCenter(e.search.getGeometry().getExtent()),
19 | resolution: e.search.get('type') === 'Region' ? 1.75 : 0.75,
20 | duration: 2000,
21 | })
22 | }
23 | });
24 | }
25 |
26 | getSearchString(f) {
27 | return this.getTitle(f);
28 | }
29 |
30 | autocomplete(search) {
31 | const result = [];
32 | search = search.replace(/^\*/, '');
33 | const rex = new RegExp(search, 'i');
34 | let max = this.get('maxItems');
35 | LayerSwitcher.forEachRecursive(this.getMap(), (layer) => {
36 | if (max <= 0) {
37 | return
38 | }
39 | const searchableLayer = layer.get('searchable') !== undefined ? layer.get('searchable') : true
40 | if (layer.getSource?.().getFeatures && searchableLayer) {
41 | // search by notes
42 | const features = layer.getSource().getFeatures()
43 | for (const feature of features) {
44 | let att = this.getSearchString(feature);
45 | if (att) {
46 | att.replaceAll("\n", ' ')
47 | }
48 | if (att && rex.test(att)) {
49 | result.push(feature);
50 | if ((--max) <= 0) {
51 | break;
52 | }
53 | continue;
54 | }
55 | // search by username
56 | const user = feature.get('user');
57 | if (user !== undefined && rex.test(user)) {
58 | result.push(feature);
59 | if ((--max) <= 0) {
60 | break;
61 | }
62 | continue;
63 | }
64 | // search by clan
65 | const clan = feature.get('clan');
66 | if (clan !== undefined && rex.test(clan)) {
67 | result.push(feature);
68 | if ((--max) <= 0) {
69 | break;
70 | }
71 | }
72 | }
73 | }
74 | });
75 | return result;
76 | }
77 |
78 | }
79 |
80 | export default Search
--------------------------------------------------------------------------------
/frontend/main.js:
--------------------------------------------------------------------------------
1 | import "./style.scss";
2 | import * as bootstrap from 'bootstrap';
3 |
4 | window.bootstrap = bootstrap
--------------------------------------------------------------------------------
/frontend/stats.js:
--------------------------------------------------------------------------------
1 | import { createApp, reactive } from "vue";
2 |
3 | import Stats from "./Components/Stats.vue";
4 | import VPCounterStats from "./Components/VPCounterStats.vue";
5 | import Socket from "./webSocket.js";
6 |
7 |
8 | const data = reactive({
9 | version: null,
10 | warStatus: '',
11 | requiredVictoryTowns: 32,
12 | conquerStatus: {},
13 | warFeatures: {}
14 | })
15 |
16 | const queue = reactive({queues: {}})
17 |
18 | const socket = new Socket('/stats')
19 | socket.on('init', (initData) => {
20 | if (data.version === null) {
21 | data.version = initData.version
22 | } else if (data.version !== initData.version) {
23 | window.location.reload()
24 | }
25 | data.requiredVictoryTowns = initData.requiredVictoryTowns
26 | data.warStatus = initData.warStatus
27 | data.conquerStatus = initData.conquerStatus
28 | data.warFeatures = initData.warFeatures
29 | queue.queues = initData.queueStatus.queues
30 | });
31 |
32 | socket.on('conquer', (conquerData) => {
33 | if (data.conquerStatus.version === conquerData.version) {
34 | return
35 | }
36 | if (!conquerData.full && conquerData.oldVersion !== data.conquerStatus.version) {
37 | socket.send('getConquerStatus', true)
38 | return
39 | }
40 | data.conquerStatus.version = conquerData.version
41 | data.conquerStatus.features = conquerData.full ? conquerData.features : {...data.conquerStatus.features, ...conquerData.features}
42 | data.conquerStatus.warNumber = conquerData.warNumber
43 | });
44 |
45 | socket.on('queue', (queues) => {
46 | queue.queues = queues.queues
47 | })
48 |
49 | createApp(Stats, {
50 | data: data,
51 | queueStatus: queue,
52 | }).mount('#map')
53 |
54 | createApp(VPCounterStats, {
55 | data: data,
56 | }).mount('#war-score')
--------------------------------------------------------------------------------
/frontend/tools/arty.js:
--------------------------------------------------------------------------------
1 | import { Control } from "ol/control.js";
2 |
3 | import { createCustomControlElement } from "../mapControls.js";
4 |
5 | class Arty {
6 |
7 | /**
8 | * @param {EditTools} tools
9 | * @param {import("ol").Map} map
10 | */
11 | constructor(tools, map) {
12 | this.map = map
13 | this.tools = tools
14 | this.controlElement = createCustomControlElement('triangle', (e, selected) => {
15 | tools.sidebarArty.bsOffcanvas.show()
16 | tools.sidebarArty.artyShow()
17 | this.controlElement.classList.remove('selected')
18 | }, {
19 | elementClass: 'arty-button',
20 | title: 'Toggle Artillery Calculator',
21 | })
22 | this.control = new Control({
23 | element: this.controlElement
24 | })
25 | }
26 | }
27 |
28 |
29 | export default Arty
30 |
31 |
--------------------------------------------------------------------------------
/frontend/tools/edit.js:
--------------------------------------------------------------------------------
1 | import { Control } from "ol/control.js";
2 | import { altKeyOnly, shiftKeyOnly, singleClick } from "ol/events/condition.js";
3 | import { Modify } from "ol/interaction.js";
4 |
5 | import { createCustomControlElement } from "../mapControls.js";
6 |
7 | class Edit {
8 |
9 | /**
10 | * @param {EditTools} tools
11 | * @param {import("ol").Map} map
12 | */
13 | constructor(tools, map) {
14 | this.map = map
15 | this.tools = tools
16 | this.controlElement = createCustomControlElement('gear', (e, selected) => {
17 | tools.changeMode(selected)
18 | }, {
19 | elementClass: 'edit-button',
20 | title: 'Toggle EditMode (e)',
21 | })
22 | this.control = new Control({
23 | element: this.controlElement
24 | })
25 | document.addEventListener('keydown', (event) => {
26 | if (event.target.nodeName.toLowerCase() === 'input' || event.target.nodeName.toLowerCase() === 'textarea') {
27 | return
28 | }
29 | if (event.key === 'e') {
30 | if (!tools.editMode) {
31 | this.controlElement.classList.add('selected')
32 | tools.changeMode(true)
33 | }
34 | }
35 | if (event.key === 'Escape') {
36 | if (tools.editMode) {
37 | this.controlElement.classList.remove('selected')
38 | tools.changeMode(false)
39 | }
40 | }
41 | })
42 | tools.on(tools.EVENT_EDIT_MODE_ENABLED, this.editModeEnabled)
43 | tools.on(tools.EVENT_EDIT_MODE_DISABLED, this.editModeDisabled)
44 |
45 | this.modify = new Modify({
46 | features: this.tools.select.getFeatures(),
47 | deleteCondition: (event) => {
48 | if (altKeyOnly(event) && singleClick(event)) {
49 | event.stopPropagation()
50 | return true
51 | }
52 | if (shiftKeyOnly(event) && singleClick(event)) {
53 | event.stopPropagation()
54 | const feature = this.tools.select.getFeatures().pop()
55 | const type = feature.get('type')
56 | this.tools.emit(type + '-deselected', feature)
57 | if (Object.keys(this.tools.iconTools).includes(type)) {
58 | this.tools.emit(this.tools.EVENT_ICON_DELETED, feature)
59 | }
60 | this.tools.select.changed()
61 | return false
62 | }
63 | return false
64 | }
65 | });
66 | }
67 |
68 | editModeEnabled = () => {
69 | this.map.addInteraction(this.modify)
70 | this.map.addInteraction(this.tools.line.snap)
71 | }
72 |
73 |
74 | editModeDisabled = () => {
75 | this.map.removeInteraction(this.modify)
76 | this.map.removeInteraction(this.tools.line.snap)
77 | }
78 | }
79 |
80 |
81 | export default Edit
--------------------------------------------------------------------------------
/frontend/tools/merge.js:
--------------------------------------------------------------------------------
1 | import MergeInteraction from "../Interaction/Merge.js";
2 |
3 | class Merge {
4 |
5 | /**
6 | * @param {EditTools} tools
7 | * @param {import("ol").Map} map
8 | */
9 | constructor(tools, map) {
10 | this.map = map
11 | this.merge = new MergeInteraction({
12 | features: tools.line.allLinesCollection
13 | })
14 |
15 | this.merge.on('mergedLine', (event) => {
16 | for (let feature of event.oldFeatures) {
17 | tools.emit(tools.EVENT_ICON_DELETED, feature)
18 | }
19 | tools.emit(tools.EVENT_ICON_ADDED, event.newFeature)
20 | tools.changeTool(false)
21 | })
22 |
23 | tools.on(tools.EVENT_EDIT_MODE_DISABLED, this.toolDeSelected)
24 | tools.on(tools.EVENT_TOOL_SELECTED, (selectedTool) => {
25 | if (selectedTool === 'merge') {
26 | this.toolSelected()
27 | } else {
28 | this.toolDeSelected()
29 | }
30 | })
31 | }
32 |
33 | toolSelected = () => {
34 | this.map.addInteraction(this.merge)
35 | }
36 |
37 | toolDeSelected = () => {
38 | this.map.removeInteraction(this.merge)
39 | }
40 |
41 | }
42 |
43 | export default Merge
--------------------------------------------------------------------------------
/frontend/tools/scissor.js:
--------------------------------------------------------------------------------
1 | import Split from "ol-ext/interaction/Split.js";
2 | import { Vector as VectorSource } from "ol/source.js";
3 |
4 | class Scissor {
5 |
6 | /**
7 | * @param {EditTools} tools
8 | * @param {import("ol").Map} map
9 | */
10 | constructor(tools, map) {
11 | this.map = map
12 | const fakeSource = new VectorSource({
13 | features: tools.line.allLinesCollection,
14 | });
15 | this.split = new Split({
16 | sources: fakeSource,
17 | })
18 |
19 | this.split.on('aftersplit', (event) => {
20 | tools.emit(tools.EVENT_ICON_DELETED, event.original)
21 | for (let feature of event.features) {
22 | feature.set('id', null)
23 | // All new features by split will be added by event with a new id
24 | fakeSource.removeFeature(feature)
25 | tools.emit(tools.EVENT_ICON_ADDED, feature)
26 | }
27 | tools.changeTool(false)
28 | })
29 |
30 | tools.on(tools.EVENT_EDIT_MODE_DISABLED, this.toolDeSelected)
31 | tools.on(tools.EVENT_TOOL_SELECTED, (selectedTool) => {
32 | if (selectedTool === 'scissor') {
33 | this.toolSelected()
34 | } else {
35 | this.toolDeSelected()
36 | }
37 | })
38 | }
39 |
40 | toolSelected = () => {
41 | this.map.addInteraction(this.split)
42 | }
43 |
44 | toolDeSelected = () => {
45 | this.map.removeInteraction(this.split)
46 | }
47 |
48 | }
49 |
50 | export default Scissor
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "checkJs": true,
4 | "target": "ESNext",
5 | "module": "NodeNext",
6 | "moduleResolution": "NodeNext",
7 | "strict": true,
8 | "noUncheckedIndexedAccess": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "skipLibCheck": true,
11 | "resolveJsonModule": true
12 | },
13 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
14 | "exclude": ["node_modules"]
15 | }
16 |
--------------------------------------------------------------------------------
/lib/ambient.d.ts:
--------------------------------------------------------------------------------
1 | import type session from "express-session";
2 | import type { GrantSession, GrantResponse } from "grant";
3 |
4 | import type { Access } from "./ACLS.js";
5 | import type { Session, SessionData } from "express-session";
6 |
7 | declare global {
8 | namespace NodeJS {
9 | interface ProcessEnv {
10 | NODE_ENV:
11 | | "test"
12 | | "development"
13 | | "production"
14 | | (string & NonNullable