diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 847ce0fa9..32ff5ec73 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy to Remote +name: 🚀 Deploy on: workflow_dispatch: @@ -9,14 +9,32 @@ on: default: "staging" type: choice options: - - production + - prod - staging + target_host: + description: "Deployment Host" + required: true + default: "staging" + type: choice + options: + - eu + - us + - staging + target_subdomain: + description: "Deployment Subdomain" + required: false + default: "" + type: string + push: + branches: + - main jobs: deploy: - name: Deploy to ${{ inputs.target_environment }} + # Use different logic based on event type + name: Deploy to ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }} runs-on: ubuntu-latest - environment: ${{ inputs.target_environment }} + environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }} steps: - uses: actions/checkout@v4 @@ -29,10 +47,31 @@ jobs: mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts - cat >.env <> ~/.ssh/known_hosts + + # Determine environment based on trigger type + TARGET_ENV="${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || 'staging' }}" + TARGET_HOST="${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}" + TARGET_SUBDOMAIN="${{ github.event_name == 'workflow_dispatch' && inputs.target_subdomain || 'main' }}" + + cat >.env.$TARGET_ENV < cloudflared.deb \ + && dpkg -i cloudflared.deb \ + && rm cloudflared.deb # Set the working directory in the container WORKDIR /usr/src/app @@ -33,8 +49,9 @@ RUN rm -f /etc/nginx/sites-enabled/default RUN mkdir -p /var/log/supervisor COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf -# Expose only the Nginx port -EXPOSE 80 443 +# Copy and make executable the startup script +COPY startup.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/startup.sh -# Start Supervisor to manage both Node.js and Nginx -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file +# Use the startup script as the entrypoint +ENTRYPOINT ["/usr/local/bin/startup.sh"] \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index cb7bcc741..630afc288 100755 --- a/deploy.sh +++ b/deploy.sh @@ -7,6 +7,27 @@ set -e # Exit immediately if a command exits with a non-zero status +# Check command line arguments +if [ $# -lt 2 ] || [ $# -gt 3 ]; then + echo "Error: Please specify environment and host, with optional subdomain" + echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]" + exit 1 +fi + +# Validate first argument (environment) +if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then + echo "Error: First argument must be either 'prod' or 'staging'" + echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]" + exit 1 +fi + +# Validate second argument (host) +if [ "$2" != "eu" ] && [ "$2" != "us" ] && [ "$2" != "staging" ]; then + echo "Error: Second argument must be either 'eu', 'us', or 'staging'" + echo "Usage: $0 [prod|staging] [eu|us|staging] [subdomain]" + exit 1 +fi + # Function to print section headers print_header() { echo "======================================================" @@ -14,59 +35,59 @@ print_header() { echo "======================================================" } -# Load environment variables +ENV=$1 +HOST=$2 +SUBDOMAIN=$3 # Optional third argument for custom subdomain + +# Set subdomain - use the custom subdomain if provided, otherwise use REGION +if [ -n "$SUBDOMAIN" ]; then + echo "Using custom subdomain: $SUBDOMAIN" +else + SUBDOMAIN=$HOST + echo "Using host as subdomain: $SUBDOMAIN" +fi + +# Load common environment variables first if [ -f .env ]; then - echo "Loading configuration from .env file..." + echo "Loading common configuration from .env file..." export $(grep -v '^#' .env | xargs) fi -# Check command line argument -if [ $# -ne 1 ] || ([ "$1" != "staging" ] && [ "$1" != "eu" ] && [ "$1" != "us" ]); then - echo "Error: Please specify environment (staging, eu, or us)" - echo "Usage: $0 [staging|eu|us]" +# Load environment-specific variables +if [ -f .env.$ENV ]; then + echo "Loading $ENV-specific configuration from .env.$ENV file..." + export $(grep -v '^#' .env.$ENV | xargs) +else + echo "Error: Environment file .env.$ENV not found" exit 1 fi -REGION=$1 -VERSION_TAG="latest" -DOCKER_REPO="" -ENV="" -SSH_KEY="" - -# Set environment-specific variables -if [ "$REGION" == "staging" ]; then - print_header "DEPLOYING TO STAGING ENVIRONMENT" +if [ "$HOST" == "staging" ]; then + print_header "DEPLOYING TO STAGING HOST" SERVER_HOST=$SERVER_HOST_STAGING - DOCKER_REPO=$DOCKER_REPO_STAGING - ENV="staging" - SSH_KEY=$SSH_KEY_STAGING -elif [ "$REGION" == "us" ]; then - print_header "DEPLOYING TO US ENVIRONMENT" +elif [ "$HOST" == "us" ]; then + print_header "DEPLOYING TO US HOST" SERVER_HOST=$SERVER_HOST_US - DOCKER_REPO=$DOCKER_REPO_PROD # Uses prod Docker repo for alt environment - SSH_KEY=$SSH_KEY_PROD - ENV="prod" else - print_header "DEPLOYING TO EU ENVIRONMENT" + print_header "DEPLOYING TO EU HOST" SERVER_HOST=$SERVER_HOST_EU - DOCKER_REPO=$DOCKER_REPO_PROD - SSH_KEY=$SSH_KEY_PROD - ENV="prod" fi # Check required environment variables if [ -z "$SERVER_HOST" ]; then - echo "Error: SERVER_HOST_${REGION^^} not defined in .env file or environment" + echo "Error: ${HOST} not defined in .env file or environment" exit 1 fi # Configuration -DOCKER_USERNAME=${DOCKER_USERNAME} # Docker Hub username UPDATE_SCRIPT="./update.sh" # Path to your update script REMOTE_USER="openfront" REMOTE_UPDATE_PATH="/home/$REMOTE_USER" REMOTE_UPDATE_SCRIPT="$REMOTE_UPDATE_PATH/update-openfront.sh" # Where to place the script on server +IMAGE_NAME="${DOCKER_USERNAME}/${DOCKER_REPO}" +DOCKER_IMAGE="${IMAGE_NAME}:${VERSION_TAG}" + # Check if update script exists if [ ! -f "$UPDATE_SCRIPT" ]; then echo "Error: Update script $UPDATE_SCRIPT not found!" @@ -75,7 +96,9 @@ fi # Step 1: Build and upload Docker image to Docker Hub print_header "STEP 1: Building and uploading Docker image to Docker Hub" -echo "Region: ${REGION}" +echo "Environment: ${ENV}" +echo "Host: ${HOST}" +echo "Subdomain: ${SUBDOMAIN}" echo "Using version tag: $VERSION_TAG" echo "Docker repository: $DOCKER_REPO" @@ -107,25 +130,32 @@ chmod +x $UPDATE_SCRIPT # Copy the update script to the server scp -i $SSH_KEY $UPDATE_SCRIPT $REMOTE_USER@$SERVER_HOST:$REMOTE_UPDATE_SCRIPT -# Copy environment variables if needed -if [ -f .env ]; then - scp -i $SSH_KEY .env $REMOTE_USER@$SERVER_HOST:$REMOTE_UPDATE_PATH/.env - # Secure the .env file - ssh -i $SSH_KEY $REMOTE_USER@$SERVER_HOST "chmod 600 $REMOTE_UPDATE_PATH/.env" -fi - if [ $? -ne 0 ]; then echo "❌ Failed to copy update script to server. Stopping deployment." exit 1 fi -echo "✅ Update script successfully copied to server." - -# Step 3: Execute the update script on the server -print_header "STEP 3: Executing update script on server" - -# Make the script executable on the remote server and execute it with the environment parameter -ssh -i $SSH_KEY $REMOTE_USER@$SERVER_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT $REGION $DOCKER_USERNAME $DOCKER_REPO" +ssh -i $SSH_KEY $REMOTE_USER@$SERVER_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && \ +cat > $REMOTE_UPDATE_PATH/.env << 'EOL' +GAME_ENV=$ENV +ENV=$ENV +HOST=$HOST +SUBDOMAIN=$SUBDOMAIN +DOCKER_IMAGE=$DOCKER_IMAGE +DOCKER_TOKEN=$DOCKER_TOKEN +ADMIN_TOKEN=$ADMIN_TOKEN +CF_ACCOUNT_ID=$CF_ACCOUNT_ID +R2_ACCESS_KEY=$R2_ACCESS_KEY +R2_SECRET_KEY=$R2_SECRET_KEY +R2_BUCKET=$R2_BUCKET +CF_API_TOKEN=$CF_API_TOKEN +DOMAIN=$DOMAIN +SUBDOMAIN=$SUBDOMAIN +MON_USERNAME=$MON_USERNAME +MON_PASSWORD=$MON_PASSWORD +EOL +chmod 600 $REMOTE_UPDATE_PATH/.env && \ +$REMOTE_UPDATE_SCRIPT" if [ $? -ne 0 ]; then echo "❌ Failed to execute update script on server." @@ -133,6 +163,6 @@ if [ $? -ne 0 ]; then fi print_header "DEPLOYMENT COMPLETED SUCCESSFULLY" -echo "✅ New version deployed to ${REGION} environment!" -echo "🌐 Check your ${REGION} server to verify the deployment." +echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!" +echo "🌐 Check your server to verify the deployment." echo "=======================================================" \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 8b14cd1f5..b204b29c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,6 +13,7 @@ const gitignorePath = path.resolve(__dirname, ".gitignore"); /** @type {import('eslint').Linter.Config[]} */ export default [ includeIgnoreFile(gitignorePath), + { ignores: ["src/server/gatekeeper/**"] }, { files: ["**/*.{js,mjs,cjs,ts}"] }, { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, diff --git a/example.env b/example.env index 1be8f5410..14430d50d 100644 --- a/example.env +++ b/example.env @@ -1,20 +1,32 @@ -# Server Configuration -SERVER_HOST_STAGING=xxx.xxx.xx.xxx -SERVER_HOST_EU=xxx.xxx.xxx.xxx -SERVER_HOST_US=x.xxx.xxx.xxx -SSH_KEY_STAGING=~/.ssh/your-staging-key -SSH_KEY_PROD=~/.ssh/your-prod-key +# SSH Configuration +SSH_KEY=~/.ssh/your-ssh-key # Docker Configuration DOCKER_USERNAME=username -DOCKER_REPO_PROD=your-prod-repo -DOCKER_REPO_STAGING=your-staging-repo -DOCKER_TOKEN=your_docker_token +DOCKER_REPO=your-repo-name +DOCKER_TOKEN=your_docker_token_here # Admin credentials -ADMIN_TOKEN=your_admin_token +ADMIN_TOKEN=your_admin_token_here + +# Cloudflare Configuration +CF_ACCOUNT_ID=your_cloudflare_account_id +CF_API_TOKEN=your_cloudflare_api_token +DOMAIN=your-domain.com + +# R2 Configuration R2_ACCESS_KEY=your_r2_access_key R2_SECRET_KEY=your_r2_secret_key -R2_ACCOUNT_ID=your_r2_account_id -R2_PROD_BUCKET=your-prod-bucket -R2_STAGING_BUCKET=your-staging-bucket \ No newline at end of file +R2_BUCKET=your-bucket-name + +# Server Hosts +SERVER_HOST_STAGING=123.456.78.90 +SERVER_HOST_EU=123.456.78.91 +SERVER_HOST_US=123.456.78.92 + +# Monitoring Credentials +MON_USERNAME=monitor_username +MON_PASSWORD=monitor_password + +# Version +VERSION_TAG="latest" \ No newline at end of file diff --git a/metric-exporter.sh b/metric-exporter.sh new file mode 100755 index 000000000..e3ac7370e --- /dev/null +++ b/metric-exporter.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# Metric Collector for Prometheus Pushgateway +# This script collects metrics from Node Exporter and application sources +# and pushes them to a Prometheus Pushgateway with custom labels. + +# Configuration +NODE_EXPORTER_URL="http://localhost:9100/metrics" +APP_METRICS_URL="http://localhost:9090/metrics" +PUSHGATEWAY_BASE_URL="https://mon.openfront.io/pushgateway/metrics" +AUTH=$MON_USERNAME:$MON_PASSWORD +INTERVAL=15 # seconds + +# Function to fetch metrics from Node Exporter +fetch_node_exporter_metrics() { + curl -s --connect-timeout 5 --max-time 10 "$NODE_EXPORTER_URL" || + echo "# Error fetching Node Exporter metrics" +} + +# Function to fetch metrics from your application +fetch_app_metrics() { + curl -s --connect-timeout 5 --max-time 10 "$APP_METRICS_URL" || + echo "# Error fetching application metrics" +} + +# Function to push metrics to Pushgateway +push_metrics() { + local metrics=$1 + local job_name=$2 + + echo "Pushing $job_name metrics to Pushgateway..." + + # Create a temporary file for the metrics + TEMP_FILE=$(mktemp) + echo "$metrics" > "$TEMP_FILE" + + # Push to Pushgateway with instance label + curl -s -u "$AUTH" --data-binary @"$TEMP_FILE" \ + "$PUSHGATEWAY_BASE_URL/job/$job_name/instance/$HOST" + + # Check if push was successful + if [ $? -eq 0 ]; then + echo "$job_name metrics pushed successfully" + else + echo "Error pushing $job_name metrics" + fi + + # Remove temporary file + rm "$TEMP_FILE" +} + +# Function to add labels to metrics +add_labels() { + local metrics=$1 + + # First, handle metrics with existing labels + metrics=$(echo "$metrics" | sed -E 's/(\{[^}]*)\}/\1,env="'$ENV'",host="'$HOST'",subdomain="'$SUBDOMAIN'"}/g') + + # Then, handle metrics with no existing labels + metrics=$(echo "$metrics" | sed -E 's/^([a-zA-Z0-9_:]+)[ \t]+([0-9.e+-]+)$/\1{env="'$ENV'",host="'$HOST'",subdomain="'$SUBDOMAIN'"} \2/g') + + echo "$metrics" +} + +# Main function to collect and push metrics +collect_and_push_metrics() { + echo "Starting metrics collection cycle at $(date)" + + # Get metrics from both sources + NODE_METRICS=$(fetch_node_exporter_metrics) + APP_METRICS=$(fetch_app_metrics) + + # Clean up metrics (remove headers etc.) + NODE_METRICS=$(echo "$NODE_METRICS" | grep -v "^Fetching") + APP_METRICS=$(echo "$APP_METRICS" | grep -v "^Fetching") + + # Add labels to metrics + NODE_METRICS=$(add_labels "$NODE_METRICS") + APP_METRICS=$(add_labels "$APP_METRICS") + + # Push to Pushgateway separately + push_metrics "$NODE_METRICS" "node_exporter" + push_metrics "$APP_METRICS" "app_metrics" + + echo "Metrics collection cycle completed at $(date)" +} + +# Main execution +echo "===== Starting metrics collector =====" +echo "Environment: $ENV, HOST: $HOST, Subdomain: $SUBDOMAIN" +echo "Collecting and pushing metrics every $INTERVAL seconds" +echo "Node Exporter URL: $NODE_EXPORTER_URL" +echo "App Metrics URL: $APP_METRICS_URL" +echo "Pushgateway URL: $PUSHGATEWAY_BASE_URL" + +# Wait for app to be ready. +sleep 30 + +# Then set up interval loop +while true; do + sleep $INTERVAL + collect_and_push_metrics +done \ No newline at end of file diff --git a/resources/lang/en.json b/resources/lang/en.json index c2b7d0425..aa090c7b0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -123,6 +123,11 @@ "knownworld": "Known World", "faroeislands": "Faroe Islands" }, + "map_categories": { + "continental": "Continental", + "regional": "Regional", + "fantasy": "Other" + }, "private_lobby": { "title": "Join Private Lobby", "enter_id": "Enter Lobby ID", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 25862517d..eba0c6e25 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -353,7 +353,13 @@ export class ClientGameRunner { } } this.myPlayer.actions(tile).then((actions) => { - console.log(`got actions: ${JSON.stringify(actions)}`); + const bu = actions.buildableUnits.find( + (bu) => bu.type == UnitType.TransportShip, + ); + if (bu == null) { + console.warn(`no transport ship buildable units`); + return; + } if (actions.canAttack) { this.eventBus.emit( new SendAttackIntentEvent( @@ -362,8 +368,8 @@ export class ClientGameRunner { ), ); } else if ( - actions.canBoat !== false && - this.shouldBoat(tile, actions.canBoat) && + bu.canBuild !== false && + this.shouldBoat(tile, bu.canBuild) && this.gameView.isLand(tile) ) { this.eventBus.emit( diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 5dbe32865..a9d605eb3 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -4,7 +4,12 @@ import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { consolex } from "../core/Consolex"; -import { Difficulty, GameMapType, GameMode } from "../core/game/Game"; +import { + Difficulty, + GameMapType, + GameMode, + mapCategories, +} from "../core/game/Game"; import { GameConfig, GameInfo } from "../core/Schemas"; import { generateID } from "../core/Util"; import "./components/baseComponents/Modal"; @@ -74,23 +79,40 @@ export class HostLobbyModal extends LitElement {
${translateText("map.map")}
-
- ${Object.entries(GameMapType) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` -
this.handleMapSelection(value)}> - +
+ + ${Object.entries(mapCategories).map( + ([categoryKey, maps]) => html` +
+

+ ${translateText(`map_categories.${categoryKey}`)} +

+
+ ${maps.map((mapValue) => { + const mapKey = Object.keys(GameMapType).find( + (key) => GameMapType[key] === mapValue, + ); + return html` +
this.handleMapSelection(mapValue)} + > + +
+ `; + })}
- `, - )} +
+ `, + )}
-
${translateText("map.random")}
+
+ ${translateText("map.random")} +
diff --git a/src/client/Main.ts b/src/client/Main.ts index 567a6673b..0d00af662 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -21,6 +21,8 @@ import { LangSelector } from "./LangSelector"; import { LanguageModal } from "./LanguageModal"; import "./PublicLobby"; import { PublicLobby } from "./PublicLobby"; +import "./RandomNameButton"; +import { RandomNameButton } from "./RandomNameButton"; import { SinglePlayerModal } from "./SinglePlayerModal"; import { UserSettingModal } from "./UserSettingModal"; import "./UsernameInput"; @@ -46,6 +48,7 @@ class Client { private usernameInput: UsernameInput | null = null; private flagInput: FlagInput | null = null; private darkModeButton: DarkModeButton | null = null; + private randomNameButton: RandomNameButton | null = null; private joinModal: JoinPrivateLobbyModal; private publicLobby: PublicLobby; @@ -80,6 +83,13 @@ class Client { consolex.warn("Dark mode button element not found"); } + this.randomNameButton = document.querySelector( + "random-name-button", + ) as RandomNameButton; + if (!this.randomNameButton) { + consolex.warn("Random name button element not found"); + } + this.usernameInput = document.querySelector( "username-input", ) as UsernameInput; diff --git a/src/client/RandomNameButton.ts b/src/client/RandomNameButton.ts new file mode 100644 index 000000000..4617c784f --- /dev/null +++ b/src/client/RandomNameButton.ts @@ -0,0 +1,30 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { UserSettings } from "../core/game/UserSettings"; + +@customElement("random-name-button") +export class RandomNameButton extends LitElement { + private userSettings: UserSettings = new UserSettings(); + @state() private randomName: boolean = this.userSettings.anonymousNames(); + + createRenderRoot() { + return this; + } + + toggleRandomName() { + this.userSettings.toggleRandomName(); + this.randomName = this.userSettings.anonymousNames(); + } + + render() { + return html` + + `; + } +} diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index d7dd675ff..6a8438f6f 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -3,7 +3,13 @@ import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { consolex } from "../core/Consolex"; -import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; +import { + Difficulty, + GameMapType, + GameMode, + GameType, + mapCategories, +} from "../core/game/Game"; import { generateID } from "../core/Util"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; @@ -39,27 +45,40 @@ export class SinglePlayerModal extends LitElement {
${translateText("map.map")}
-
- ${Object.entries(GameMapType) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` -
+ + ${Object.entries(mapCategories).map( + ([categoryKey, maps]) => html` +
+

- + ${translateText(`map_categories.${categoryKey}`)} +

+
+ ${maps.map((mapValue) => { + const mapKey = Object.keys(GameMapType).find( + (key) => GameMapType[key] === mapValue, + ); + return html` +
this.handleMapSelection(mapValue)} + > + +
+ `; + })}
- `, - )} +
+ `, + )}
bu.type == UnitType.TransportShip) + ?.canBuild + ) { this.activateMenuElement(Slot.Boat, "#3f6ab1", boatIcon, () => { - this.eventBus.emit( - new SendBoatAttackIntentEvent( - this.g.owner(tile).id(), - this.clickedCell, - this.uiState.attackRatio * myPlayer.troops(), - ), - ); + // BestTransportShipSpawn is an expensive operation, so + // we calculate it here and send the spawn tile to other clients. + myPlayer.bestTransportShipSpawn(tile).then((spawn) => { + if (spawn == false) { + return; + } + + this.eventBus.emit( + new SendBoatAttackIntentEvent( + this.g.owner(tile).id(), + this.clickedCell, + this.uiState.attackRatio * myPlayer.troops(), + new Cell(this.g.x(spawn), this.g.y(spawn)), + ), + ); + }); }); } if (actions.canAttack) { diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index a939c4bc5..552470879 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -3,11 +3,7 @@ import { EventBus } from "../../../core/EventBus"; import { ClientID } from "../../../core/Schemas"; import { Theme } from "../../../core/configuration/Config"; import { UnitType } from "../../../core/game/Game"; -import { - euclDistFN, - manhattanDistFN, - TileRef, -} from "../../../core/game/GameMap"; +import { TileRef } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { @@ -264,22 +260,10 @@ export class UnitLayer implements Layer { } private handleWarShipEvent(unit: UnitView) { - const rel = this.relationship(unit); - - // Clear previous area - for (const t of this.game.bfs( - unit.lastTile(), - euclDistFN(unit.lastTile(), 6, false), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); - } - - if (unit.isActive()) { - if (unit.warshipTargetId()) { - this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 })); - } else { - this.drawSprite(unit); - } + if (unit.warshipTargetId()) { + this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 })); + } else { + this.drawSprite(unit); } } @@ -317,46 +301,11 @@ export class UnitLayer implements Layer { // interception missle from SAM private handleMissileEvent(unit: UnitView) { - const rel = this.relationship(unit); - const range = 2; - - for (const t of this.game.bfs( - unit.lastTile(), - euclDistFN(unit.lastTile(), range, false), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); - } - - if (unit.isActive()) { - this.drawSprite(unit); - } + this.drawSprite(unit); } private handleNuke(unit: UnitView) { - let range = 0; - - switch (unit.type()) { - case UnitType.AtomBomb: - range = 4; - break; - case UnitType.HydrogenBomb: - range = 6; - break; - case UnitType.MIRV: - range = 9; - break; - } - - for (const t of this.game.bfs( - unit.lastTile(), - euclDistFN(unit.lastTile(), range, false), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); - } - - if (unit.isActive()) { - this.drawSprite(unit); - } + this.drawSprite(unit); } private handleMIRVWarhead(unit: UnitView) { @@ -377,17 +326,7 @@ export class UnitLayer implements Layer { } private handleTradeShipEvent(unit: UnitView) { - // Clear previous area - for (const t of this.game.bfs( - unit.lastTile(), - euclDistFN(unit.lastTile(), 3, false), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); - } - - if (unit.isActive()) { - this.drawSprite(unit); - } + this.drawSprite(unit); } private handleBoatEvent(unit: UnitView) { @@ -399,29 +338,21 @@ export class UnitLayer implements Layer { const trail = this.boatToTrail.get(unit); trail.push(unit.lastTile()); - // Clear previous area - for (const t of this.game.bfs( - unit.lastTile(), - manhattanDistFN(unit.lastTile(), 4), - )) { - this.clearCell(this.game.x(t), this.game.y(t)); + // Paint trail + for (const t of trail.slice(-1)) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner()), + 150, + this.transportShipTrailContext, + ); } - if (unit.isActive()) { - // Paint trail - for (const t of trail.slice(-1)) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner()), - 150, - this.transportShipTrailContext, - ); - } + this.drawSprite(unit); - this.drawSprite(unit); - } else { + if (!unit.isActive()) { for (const t of trail) { this.clearCell( this.game.x(t), @@ -488,6 +419,8 @@ export class UnitLayer implements Layer { drawSprite(unit: UnitView, customTerritoryColor?: Colord) { const x = this.game.x(unit.tile()); const y = this.game.y(unit.tile()); + const lastX = this.game.x(unit.lastTile()); + const lastY = this.game.y(unit.lastTile()); let alternateViewColor = null; @@ -513,12 +446,23 @@ export class UnitLayer implements Layer { alternateViewColor, ); - this.context.drawImage( - sprite, - Math.round(x - sprite.width / 2), - Math.round(y - sprite.height / 2), - sprite.width, - sprite.width, + const clearsize = sprite.width + 1; + + this.context.clearRect( + lastX - clearsize / 2, + lastY - clearsize / 2, + clearsize, + clearsize, ); + + if (unit.isActive()) { + this.context.drawImage( + sprite, + Math.round(x - sprite.width / 2), + Math.round(y - sprite.height / 2), + sprite.width, + sprite.width, + ); + } } } diff --git a/src/client/index.html b/src/client/index.html index f0b8810f3..ba6a0f4a4 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -203,32 +203,22 @@ /> -
v21.2
+
v22.0
+ + + -
-
+
+
- +
@@ -331,6 +321,9 @@ > Wiki + + Join the Discord! +