mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 05:30:45 +00:00
Merge main into nations-ai
This commit is contained in:
@@ -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 <<EOF
|
||||
SERVER_HOST_STAGING=${{ secrets.SERVER_USERNAME }}@${{ secrets.SERVER_HOST }}
|
||||
DOCKER_REPO_STAGING=${{ vars.DOCKERHUB_REPO }}
|
||||
ssh-keyscan -H ${{ secrets.SERVER_HOST_STAGING }} >> ~/.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 <<EOF
|
||||
ADMIN_TOKEN=${{ secrets.ADMIN_TOKEN }}
|
||||
CF_ACCOUNT_ID=${{ secrets.CF_ACCOUNT_ID }}
|
||||
CF_API_TOKEN=${{ secrets.CF_API_TOKEN }}
|
||||
DOCKER_REPO=${{ vars.DOCKERHUB_REPO }}
|
||||
DOCKER_USERNAME=${{ vars.DOCKERHUB_USERNAME }}
|
||||
DOMAIN=${{ vars.DOMAIN }}
|
||||
MON_PASSWORD=${{ secrets.MON_PASSWORD }}
|
||||
MON_USERNAME=${{ secrets.MON_USERNAME }}
|
||||
R2_ACCESS_KEY=${{ secrets.R2_ACCESS_KEY }}
|
||||
R2_BUCKET=${{ secrets.R2_BUCKET }}
|
||||
R2_SECRET_KEY=${{ secrets.R2_SECRET_KEY }}
|
||||
SERVER_HOST_STAGING=${{ secrets.SERVER_HOST_STAGING }}
|
||||
SERVER_HOST_US=${{ secrets.SERVER_HOST_US }}
|
||||
SERVER_HOST_EU=${{ secrets.SERVER_HOST_EU }}
|
||||
SSH_KEY=~/.ssh/id_rsa
|
||||
VERSION_TAG="latest"
|
||||
EOF
|
||||
./deploy.sh ${{ inputs.target_environment }}
|
||||
|
||||
./deploy.sh $TARGET_ENV $TARGET_HOST $TARGET_SUBDOMAIN
|
||||
echo "Deployed to $TARGET_ENV environment on $TARGET_HOST host with subdomain $TARGET_SUBDOMAIN"
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ static/
|
||||
TODO.txt
|
||||
resources/images/.DS_Store
|
||||
resources/.DS_Store
|
||||
.env
|
||||
.env*
|
||||
.DS_Store
|
||||
Regular → Executable
+8
-1
@@ -1 +1,8 @@
|
||||
npx lint-staged
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Add PATH setup to ensure npx is found
|
||||
export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
|
||||
|
||||
# Then run lint-staged if tests pass
|
||||
npx lint-staged
|
||||
+25
-8
@@ -1,12 +1,28 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
|
||||
# Install Nginx, Supervisor and Git (for Husky)
|
||||
RUN apt-get update && apt-get install -y nginx supervisor git && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# Install Nginx, Supervisor, Git, jq, curl, and Node Exporter dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
nginx \
|
||||
supervisor \
|
||||
git \
|
||||
curl \
|
||||
jq \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node Exporter
|
||||
RUN mkdir -p /opt/node_exporter && \
|
||||
wget -qO- https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz | \
|
||||
tar xvz --strip-components=1 -C /opt/node_exporter && \
|
||||
ln -s /opt/node_exporter/node_exporter /usr/local/bin/
|
||||
|
||||
# Install cloudflared
|
||||
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > 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"]
|
||||
# Use the startup script as the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/startup.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 "======================================================="
|
||||
@@ -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,
|
||||
|
||||
+25
-13
@@ -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
|
||||
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"
|
||||
Executable
+103
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
<!-- Map Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">${translateText("map.map")}</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div @click=${() => this.handleMapSelection(value)}>
|
||||
<map-display
|
||||
.mapKey=${key}
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === value}
|
||||
.translation=${translateText(
|
||||
`map.${key.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
<div class="option-cards flex-col">
|
||||
<!-- Use the imported mapCategories -->
|
||||
${Object.entries(mapCategories).map(
|
||||
([categoryKey, maps]) => html`
|
||||
<div class="w-full mb-4">
|
||||
<h3
|
||||
class="text-lg font-semibold mb-2 text-center text-gray-300"
|
||||
>
|
||||
${translateText(`map_categories.${categoryKey}`)}
|
||||
</h3>
|
||||
<div class="flex flex-row flex-wrap justify-center gap-4">
|
||||
${maps.map((mapValue) => {
|
||||
const mapKey = Object.keys(GameMapType).find(
|
||||
(key) => GameMapType[key] === mapValue,
|
||||
);
|
||||
return html`
|
||||
<div
|
||||
@click=${() => this.handleMapSelection(mapValue)}
|
||||
>
|
||||
<map-display
|
||||
.mapKey=${mapKey}
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === mapValue}
|
||||
.translation=${translateText(
|
||||
`map.${mapKey.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
<div
|
||||
class="option-card random-map ${
|
||||
this.useRandomMap ? "selected" : ""
|
||||
@@ -104,7 +126,9 @@ export class HostLobbyModal extends LitElement {
|
||||
style="width:100%; aspect-ratio: 4/2; object-fit:cover; border-radius:8px;"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-card-title">${translateText("map.random")}</div>
|
||||
<div class="option-card-title">
|
||||
${translateText("map.random")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
<button
|
||||
title="Random Name"
|
||||
class="absolute top-0 left-0 md:top-[10px] md:left-[10px] border-none bg-none cursor-pointer text-2xl"
|
||||
@click=${() => this.toggleRandomName()}
|
||||
>
|
||||
${this.randomName ? "🥷" : "🕵️"}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
<!-- Map Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">${translateText("map.map")}</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div
|
||||
@click=${function () {
|
||||
this.handleMapSelection(value);
|
||||
}}
|
||||
<div class="option-cards flex-col">
|
||||
<!-- Use the imported mapCategories -->
|
||||
${Object.entries(mapCategories).map(
|
||||
([categoryKey, maps]) => html`
|
||||
<div class="w-full mb-4">
|
||||
<h3
|
||||
class="text-lg font-semibold mb-2 text-center text-gray-300"
|
||||
>
|
||||
<map-display
|
||||
.mapKey=${key}
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === value}
|
||||
.translation=${translateText(
|
||||
`map.${key.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
${translateText(`map_categories.${categoryKey}`)}
|
||||
</h3>
|
||||
<div class="flex flex-row flex-wrap justify-center gap-4">
|
||||
${maps.map((mapValue) => {
|
||||
const mapKey = Object.keys(GameMapType).find(
|
||||
(key) => GameMapType[key] === mapValue,
|
||||
);
|
||||
return html`
|
||||
<div
|
||||
@click=${() => this.handleMapSelection(mapValue)}
|
||||
>
|
||||
<map-display
|
||||
.mapKey=${mapKey}
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === mapValue}
|
||||
.translation=${translateText(
|
||||
`map.${mapKey.toLowerCase()}`,
|
||||
)}
|
||||
></map-display>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
<div
|
||||
class="option-card random-map ${this.useRandomMap
|
||||
? "selected"
|
||||
|
||||
@@ -68,8 +68,9 @@ export class SendAttackIntentEvent implements GameEvent {
|
||||
export class SendBoatAttackIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly targetID: PlayerID,
|
||||
public readonly cell: Cell,
|
||||
public readonly dst: Cell,
|
||||
public readonly troops: number,
|
||||
public readonly src: Cell | null = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -414,8 +415,10 @@ export class Transport {
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
dstX: event.dst.x,
|
||||
dstY: event.dst.y,
|
||||
srcX: event.src?.x,
|
||||
srcY: event.src?.y,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -82,10 +82,8 @@ export const getColoredSprite = (
|
||||
const territoryColor = customTerritoryColor ?? theme.territoryColor(owner);
|
||||
const borderColor = customBorderColor ?? theme.borderColor(owner);
|
||||
const spawnHighlightColor = theme.spawnHighlightColor();
|
||||
const colorKey = customTerritoryColor
|
||||
? customTerritoryColor.toRgbString()
|
||||
: "";
|
||||
const key = owner.id() + unit.type() + colorKey;
|
||||
const colorKey = territoryColor.toRgbString() + borderColor.toRgbString();
|
||||
const key = unit.type() + colorKey;
|
||||
|
||||
if (coloredSpriteCache.has(key)) {
|
||||
return coloredSpriteCache.get(key)!;
|
||||
|
||||
@@ -301,7 +301,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
if (!unit) {
|
||||
return false;
|
||||
}
|
||||
return unit[0].canBuild;
|
||||
return unit[0].canBuild !== false;
|
||||
}
|
||||
|
||||
private cost(item: BuildItemDisplay): number {
|
||||
|
||||
@@ -195,6 +195,7 @@ export class NameLayer implements Layer {
|
||||
nameDiv.style.alignItems = "center";
|
||||
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "player-name-span";
|
||||
nameSpan.innerHTML = player.name();
|
||||
nameDiv.appendChild(nameSpan);
|
||||
element.appendChild(nameDiv);
|
||||
@@ -262,6 +263,10 @@ export class NameLayer implements Layer {
|
||||
nameDiv.style.fontSize = `${render.fontSize}px`;
|
||||
nameDiv.style.lineHeight = `${render.fontSize}px`;
|
||||
nameDiv.style.color = render.fontColor;
|
||||
const span = nameDiv.querySelector(".player-name-span");
|
||||
if (span) {
|
||||
span.innerHTML = render.player.name();
|
||||
}
|
||||
if (flagDiv) {
|
||||
flagDiv.style.height = `${render.fontSize}px`;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
private onToggleRandomNameModeButtonClick() {
|
||||
this.userSettings.toggleRandomName();
|
||||
}
|
||||
|
||||
private onToggleFocusLockedButtonClick() {
|
||||
this.userSettings.toggleFocusLocked();
|
||||
this.requestUpdate();
|
||||
@@ -196,6 +200,12 @@ export class OptionsMenu extends LitElement implements Layer {
|
||||
title: "Dark Mode",
|
||||
children: "🌙: " + (this.userSettings.darkMode() ? "On" : "Off"),
|
||||
})}
|
||||
${button({
|
||||
onClick: this.onToggleRandomNameModeButtonClick,
|
||||
title: "Random name mode",
|
||||
children:
|
||||
"🥷: " + (this.userSettings.anonymousNames() ? "On" : "Off"),
|
||||
})}
|
||||
${button({
|
||||
onClick: this.onToggleLeftClickOpensMenu,
|
||||
title: "Left click",
|
||||
|
||||
@@ -8,7 +8,12 @@ import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
|
||||
import { consolex } from "../../../core/Consolex";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Cell, PlayerActions, TerraNullius } from "../../../core/game/Game";
|
||||
import {
|
||||
Cell,
|
||||
PlayerActions,
|
||||
TerraNullius,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
@@ -374,15 +379,27 @@ export class RadialMenu implements Layer {
|
||||
);
|
||||
});
|
||||
}
|
||||
if (actions.canBoat) {
|
||||
if (
|
||||
actions.buildableUnits.find((bu) => 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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-17
@@ -203,32 +203,22 @@
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="l-header__highlightText">v21.2</div>
|
||||
<div class="l-header__highlightText">v22.0</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="bg-image"></div>
|
||||
|
||||
<random-name-button></random-name-button>
|
||||
|
||||
<dark-mode-button></dark-mode-button>
|
||||
<!-- Main container with responsive padding -->
|
||||
<main class="flex justify-center items-center flex-grow">
|
||||
<div class="container">
|
||||
<main class="flex justify-center flex-grow">
|
||||
<div class="container pt-12">
|
||||
<div class="container__row">
|
||||
<flag-input class="w-[20%] md:w-[15%]"></flag-input>
|
||||
<username-input class="w-full"></username-input>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://discord.gg/openfront"
|
||||
class="w-full bg-[#5865F2] hover:bg-[#4752C4] text-white p-3 sm:p-4 lg:p-5 font-medium text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300 flex justify-center items-center gap-5"
|
||||
>
|
||||
<img
|
||||
style="height: 50px; width: 50px"
|
||||
alt="Discord"
|
||||
src="../../resources/icons/discord.svg"
|
||||
/>
|
||||
<span data-i18n="main.join_discord"> Join the Discord! </span>
|
||||
</a>
|
||||
</div>
|
||||
<div></div>
|
||||
<div>
|
||||
<public-lobby class="w-full"></public-lobby>
|
||||
</div>
|
||||
@@ -331,6 +321,9 @@
|
||||
>
|
||||
Wiki
|
||||
</a>
|
||||
<a target="_blank" href="https://discord.gg/openfront" class="t-link">
|
||||
<span data-i18n="main.join_discord"> Join the Discord! </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="l-footer__col t-text-white">
|
||||
© 2025
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
|
||||
.l-footer__col {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
+12
-10
@@ -4,7 +4,6 @@ import { Executor } from "./execution/ExecutionManager";
|
||||
import { WinCheckExecution } from "./execution/WinCheckExecution";
|
||||
import {
|
||||
AllPlayers,
|
||||
BuildableUnit,
|
||||
Game,
|
||||
GameUpdates,
|
||||
NameViewData,
|
||||
@@ -15,9 +14,9 @@ import {
|
||||
PlayerInfo,
|
||||
PlayerProfile,
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
import { createGame } from "./game/GameImpl";
|
||||
import { TileRef } from "./game/GameMap";
|
||||
import {
|
||||
ErrorUpdate,
|
||||
GameUpdateType,
|
||||
@@ -159,15 +158,8 @@ export class GameRunner {
|
||||
const player = this.game.player(playerID);
|
||||
const tile = this.game.ref(x, y);
|
||||
const actions = {
|
||||
canBoat: player.canBoat(tile),
|
||||
canAttack: player.canAttack(tile),
|
||||
buildableUnits: Object.values(UnitType).map((u) => {
|
||||
return {
|
||||
type: u,
|
||||
canBuild: player.canBuild(u, tile) != false,
|
||||
cost: this.game.config().unitInfo(u).cost(player),
|
||||
} as BuildableUnit;
|
||||
}),
|
||||
buildableUnits: player.buildableUnits(tile),
|
||||
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
|
||||
} as PlayerActions;
|
||||
|
||||
@@ -202,4 +194,14 @@ export class GameRunner {
|
||||
borderTiles: player.borderTiles(),
|
||||
} as PlayerBorderTiles;
|
||||
}
|
||||
public bestTransportShipSpawn(
|
||||
playerID: PlayerID,
|
||||
targetTile: TileRef,
|
||||
): TileRef | false {
|
||||
const player = this.game.player(playerID);
|
||||
if (!player.isPlayer()) {
|
||||
throw new Error(`player with id ${playerID} not found`);
|
||||
}
|
||||
return player.bestTransportShipSpawn(targetTile);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -196,8 +196,10 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("boat"),
|
||||
targetID: ID.nullable(),
|
||||
troops: z.number().nullable(),
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
dstX: z.number(),
|
||||
dstY: z.number(),
|
||||
srcX: z.number(),
|
||||
srcY: z.number(),
|
||||
});
|
||||
|
||||
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
|
||||
|
||||
+23
-68
@@ -1,8 +1,8 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import twemoji from "twemoji";
|
||||
import { Cell, Game, Player, Team, Unit } from "./game/Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap";
|
||||
import { Cell, Team, Unit } from "./game/Game";
|
||||
import { GameMap, TileRef } from "./game/GameMap";
|
||||
import {
|
||||
AllPlayersStats,
|
||||
ClientID,
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
Turn,
|
||||
} from "./Schemas";
|
||||
|
||||
import {
|
||||
BOT_NAME_PREFIXES,
|
||||
BOT_NAME_SUFFIXES,
|
||||
} from "./execution/utils/BotNames";
|
||||
|
||||
export function manhattanDistWrapped(
|
||||
c1: Cell,
|
||||
c2: Cell,
|
||||
@@ -57,72 +62,6 @@ export function distSortUnit(
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: refactor to new file
|
||||
export function sourceDstOceanShore(
|
||||
gm: Game,
|
||||
src: Player,
|
||||
tile: TileRef,
|
||||
): [TileRef | null, TileRef | null] {
|
||||
const dst = gm.owner(tile);
|
||||
const srcTile = closestShoreFromPlayer(gm, src, tile);
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestShoreTN(gm, tile, 50);
|
||||
}
|
||||
return [srcTile, dstTile];
|
||||
}
|
||||
|
||||
export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
||||
const dst = gm.playerBySmallID(gm.ownerID(tile));
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestShoreTN(gm, tile, 50);
|
||||
}
|
||||
return dstTile;
|
||||
}
|
||||
|
||||
export function closestShoreFromPlayer(
|
||||
gm: GameMap,
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | null {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
gm.isShore(t),
|
||||
);
|
||||
if (shoreTiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shoreTiles.reduce((closest, current) => {
|
||||
const closestDistance = gm.manhattanDist(target, closest);
|
||||
const currentDistance = gm.manhattanDist(target, current);
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
}
|
||||
|
||||
function closestShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
||||
),
|
||||
)
|
||||
.filter((t) => gm.isShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
||||
if (tn.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return tn[0];
|
||||
}
|
||||
|
||||
export function simpleHash(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
@@ -352,3 +291,19 @@ export function withinInt(num: bigint, min: bigint, max: bigint): bigint {
|
||||
const atLeastMin = maxInt(num, min);
|
||||
return minInt(atLeastMin, max);
|
||||
}
|
||||
|
||||
export function createRandomName(
|
||||
name: string,
|
||||
playerType: string,
|
||||
): string | null {
|
||||
let randomName = null;
|
||||
if (playerType === "HUMAN") {
|
||||
const hash = simpleHash(name);
|
||||
const prefixIndex = hash % BOT_NAME_PREFIXES.length;
|
||||
const suffixIndex =
|
||||
Math.floor(hash / BOT_NAME_PREFIXES.length) % BOT_NAME_SUFFIXES.length;
|
||||
|
||||
randomName = `👤 ${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
|
||||
}
|
||||
return randomName;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export interface Config {
|
||||
maxPopulation(player: Player | PlayerView): number;
|
||||
cityPopulationIncrease(): number;
|
||||
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
|
||||
warshipShellLifetime(): number;
|
||||
shellLifetime(): number;
|
||||
boatMaxNumber(): number;
|
||||
allianceDuration(): Tick;
|
||||
allianceRequestCooldown(): Tick;
|
||||
@@ -111,18 +111,25 @@ export interface Config {
|
||||
unitInfo(type: UnitType): UnitInfo;
|
||||
tradeShipGold(dist: number): Gold;
|
||||
tradeShipSpawnRate(numberOfPorts: number): number;
|
||||
safeFromPiratesCooldownMax(): number;
|
||||
defensePostRange(): number;
|
||||
SAMCooldown(): number;
|
||||
SiloCooldown(): number;
|
||||
defensePostDefenseBonus(): number;
|
||||
falloutDefenseModifier(percentOfFallout: number): number;
|
||||
difficultyModifier(difficulty: Difficulty): number;
|
||||
warshipPatrolRange(): number;
|
||||
warshipShellAttackRate(): number;
|
||||
warshipTargettingRange(): number;
|
||||
defensePostShellAttackRate(): number;
|
||||
defensePostTargettingRange(): number;
|
||||
// 0-1
|
||||
traitorDefenseDebuff(): number;
|
||||
traitorDuration(): number;
|
||||
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
|
||||
defaultNukeSpeed(): number;
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number;
|
||||
structureMinDist(): number;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
|
||||
@@ -34,7 +34,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
return process.env.GIT_COMMIT;
|
||||
}
|
||||
r2Endpoint(): string {
|
||||
return `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
||||
return `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
|
||||
}
|
||||
r2AccessKey(): string {
|
||||
return process.env.R2_ACCESS_KEY;
|
||||
@@ -69,7 +69,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
GameMapType.Europe,
|
||||
].includes(map)
|
||||
) {
|
||||
return Math.random() < 0.2 ? 150 : 70;
|
||||
return Math.random() < 0.2 ? 100 : 50;
|
||||
}
|
||||
// Maps with ~2.5 - ~3.5 mil pixels
|
||||
if (
|
||||
@@ -80,7 +80,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
GameMapType.Asia,
|
||||
].includes(map)
|
||||
) {
|
||||
return Math.random() < 0.2 ? 100 : 50;
|
||||
return Math.random() < 0.3 ? 50 : 25;
|
||||
}
|
||||
// Maps with ~2 mil pixels
|
||||
if (
|
||||
@@ -92,7 +92,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
GameMapType.FaroeIslands,
|
||||
].includes(map)
|
||||
) {
|
||||
return Math.random() < 0.2 ? 70 : 40;
|
||||
return Math.random() < 0.3 ? 50 : 25;
|
||||
}
|
||||
// Maps smaller than ~2 mil pixels
|
||||
if (
|
||||
@@ -102,14 +102,14 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
GameMapType.Pangaea,
|
||||
].includes(map)
|
||||
) {
|
||||
return Math.random() < 0.2 ? 60 : 35;
|
||||
return Math.random() < 0.5 ? 30 : 15;
|
||||
}
|
||||
// world belongs with the ~2 mils, but these amounts never made sense so I assume the insanity is intended.
|
||||
if (map == GameMapType.World) {
|
||||
return Math.random() < 0.2 ? 150 : 60;
|
||||
return Math.random() < 0.2 ? 150 : 50;
|
||||
}
|
||||
// default return for non specified map
|
||||
return Math.random() < 0.2 ? 85 : 45;
|
||||
return Math.random() < 0.2 ? 50 : 20;
|
||||
}
|
||||
workerIndex(gameID: GameID): number {
|
||||
return simpleHash(gameID) % this.numWorkers();
|
||||
@@ -144,7 +144,7 @@ export class DefaultConfig implements Config {
|
||||
return 0.5;
|
||||
}
|
||||
traitorDuration(): number {
|
||||
return 30 * 10; // 30 seconds
|
||||
return 15 * 10; // 15 seconds
|
||||
}
|
||||
spawnImmunityDuration(): Tick {
|
||||
return 5 * 10;
|
||||
@@ -396,7 +396,7 @@ export class DefaultConfig implements Config {
|
||||
return 80;
|
||||
}
|
||||
boatMaxNumber(): number {
|
||||
return 3;
|
||||
return 9;
|
||||
}
|
||||
numSpawnPhaseTurns(): number {
|
||||
return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300;
|
||||
@@ -676,4 +676,36 @@ export class DefaultConfig implements Config {
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number {
|
||||
return (5 * humans) / Math.max(1, tilesOwned);
|
||||
}
|
||||
|
||||
structureMinDist(): number {
|
||||
return 18;
|
||||
}
|
||||
|
||||
shellLifetime(): number {
|
||||
return 50;
|
||||
}
|
||||
|
||||
warshipPatrolRange(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
warshipTargettingRange(): number {
|
||||
return 130;
|
||||
}
|
||||
|
||||
warshipShellAttackRate(): number {
|
||||
return 20;
|
||||
}
|
||||
|
||||
defensePostShellAttackRate(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
safeFromPiratesCooldownMax(): number {
|
||||
return 20;
|
||||
}
|
||||
|
||||
defensePostTargettingRange(): number {
|
||||
return 75;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
export class DefensePostExecution implements Execution {
|
||||
private player: Player;
|
||||
@@ -15,6 +16,11 @@ export class DefensePostExecution implements Execution {
|
||||
private post: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit = null;
|
||||
private lastShellAttack = 0;
|
||||
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
@@ -30,6 +36,27 @@ export class DefensePostExecution implements Execution {
|
||||
this.player = mg.player(this.ownerId);
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
const shellAttackRate = this.mg.config().defensePostShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
new ShellExecution(
|
||||
this.post.tile(),
|
||||
this.post.owner(),
|
||||
this.post,
|
||||
this.target,
|
||||
),
|
||||
);
|
||||
if (!this.target.hasHealth()) {
|
||||
// Don't send multiple shells to target that can be oneshotted
|
||||
this.alreadySentShell.add(this.target);
|
||||
this.target = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.post == null) {
|
||||
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
|
||||
@@ -48,6 +75,52 @@ export class DefensePostExecution implements Execution {
|
||||
if (this.player != this.post.owner()) {
|
||||
this.player = this.post.owner();
|
||||
}
|
||||
|
||||
if (this.target != null && !this.target.isActive()) {
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.post.tile(),
|
||||
this.mg.config().defensePostTargettingRange(),
|
||||
[UnitType.TransportShip, UnitType.Warship],
|
||||
)
|
||||
.filter(
|
||||
({ unit }) =>
|
||||
unit.owner() !== this.post.owner() &&
|
||||
!unit.owner().isFriendly(this.post.owner()) &&
|
||||
!this.alreadySentShell.has(unit),
|
||||
);
|
||||
|
||||
this.target =
|
||||
ships.sort((a, b) => {
|
||||
const { unit: unitA, distSquared: distA } = a;
|
||||
const { unit: unitB, distSquared: distB } = b;
|
||||
|
||||
// Prioritize TransportShip
|
||||
if (
|
||||
unitA.type() === UnitType.TransportShip &&
|
||||
unitB.type() !== UnitType.TransportShip
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
unitA.type() !== UnitType.TransportShip &&
|
||||
unitB.type() === UnitType.TransportShip
|
||||
)
|
||||
return 1;
|
||||
|
||||
// If both are the same type, sort by distance (lower `distSquared` means closer)
|
||||
return distA - distB;
|
||||
})[0]?.unit ?? null;
|
||||
|
||||
if (this.target == null || !this.target.isActive()) {
|
||||
this.target = null;
|
||||
return;
|
||||
} else {
|
||||
this.shoot();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
|
||||
@@ -68,8 +68,9 @@ export class Executor {
|
||||
return new TransportShipExecution(
|
||||
playerID,
|
||||
intent.targetID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
this.mg.ref(intent.dstX, intent.dstY),
|
||||
intent.troops,
|
||||
this.mg.ref(intent.srcX, intent.srcY),
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(playerID, intent.recipient);
|
||||
|
||||
@@ -394,6 +394,7 @@ export class FakeHumanExecution implements Execution {
|
||||
other.id(),
|
||||
closest.y,
|
||||
this.player.troops() / 5,
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -541,6 +542,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.mg.owner(dst).id(),
|
||||
dst,
|
||||
this.player.troops() / 5,
|
||||
null,
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -33,14 +33,15 @@ export class MissileSiloExecution implements Execution {
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.silo == null) {
|
||||
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
|
||||
const spawn = this.player.canBuild(UnitType.MissileSilo, this.tile);
|
||||
if (spawn === false) {
|
||||
consolex.warn(
|
||||
`player ${this.player} cannot build missile silo at ${this.tile}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile, {
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, spawn, {
|
||||
cooldownDuration: this.mg.config().SiloCooldown(),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { SAMMissileExecution } from "./SAMMissileExecution";
|
||||
export class SAMLauncherExecution implements Execution {
|
||||
private player: Player;
|
||||
private mg: Game;
|
||||
private sam: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
private target: Unit = null;
|
||||
@@ -32,7 +31,12 @@ export class SAMLauncherExecution implements Execution {
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
private sam: Unit | null = null,
|
||||
) {
|
||||
if (sam != null) {
|
||||
this.tile = sam.tile();
|
||||
}
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
|
||||
@@ -42,8 +42,7 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
|
||||
if (this.destroyAtTick == -1 && !this.ownerUnit.isActive()) {
|
||||
this.destroyAtTick =
|
||||
this.mg.ticks() + this.mg.config().warshipShellLifetime();
|
||||
this.destroyAtTick = this.mg.ticks() + this.mg.config().shellLifetime();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
@@ -55,7 +54,7 @@ export class ShellExecution implements Execution {
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.shell.info().damage);
|
||||
this.target.modifyHealth(-this.effectOnTarget());
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
@@ -72,6 +71,11 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private effectOnTarget(): number {
|
||||
const baseDamage: number = this.mg.config().unitInfo(UnitType.Shell).damage;
|
||||
return baseDamage;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export class TradeShipExecution implements Execution {
|
||||
}
|
||||
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn, {
|
||||
dstPort: this._dstPort,
|
||||
lastSetSafeFromPirates: ticks,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,11 +57,11 @@ export class TradeShipExecution implements Execution {
|
||||
}
|
||||
|
||||
if (this.origOwner != this.tradeShip.owner()) {
|
||||
// Store as vairable in case ship is recaptured by previous owner
|
||||
// Store as variable in case ship is recaptured by previous owner
|
||||
this.wasCaptured = true;
|
||||
}
|
||||
|
||||
// If a player captures an other player's port while trading we should delete
|
||||
// If a player captures another player's port while trading we should delete
|
||||
// the ship.
|
||||
if (this._dstPort.owner().id() == this.srcPort.owner().id()) {
|
||||
this.tradeShip.delete(false);
|
||||
@@ -107,6 +108,10 @@ export class TradeShipExecution implements Execution {
|
||||
this.tradeShip.move(this.tradeShip.tile());
|
||||
break;
|
||||
case PathFindResultType.NextTile:
|
||||
// Update safeFromPirates status
|
||||
if (this.mg.isWater(result.tile) && this.mg.isShoreline(result.tile)) {
|
||||
this.tradeShip.setSafeFromPirates();
|
||||
}
|
||||
this.tradeShip.move(result.tile);
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { targetTransportTile } from "../Util";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
export class TransportShipExecution implements Execution {
|
||||
@@ -26,10 +26,10 @@ export class TransportShipExecution implements Execution {
|
||||
private mg: Game;
|
||||
private attacker: Player;
|
||||
private target: Player | TerraNullius;
|
||||
private embarkDelay = 10;
|
||||
|
||||
// TODO make private
|
||||
public path: TileRef[];
|
||||
private src: TileRef | null;
|
||||
private dst: TileRef | null;
|
||||
|
||||
private boat: Unit;
|
||||
@@ -41,6 +41,7 @@ export class TransportShipExecution implements Execution {
|
||||
private targetID: PlayerID | null,
|
||||
private ref: TileRef,
|
||||
private troops: number | null,
|
||||
private src: TileRef | null,
|
||||
) {}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
@@ -112,14 +113,22 @@ export class TransportShipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
const src = this.attacker.canBuild(UnitType.TransportShip, this.dst);
|
||||
if (src == false) {
|
||||
|
||||
const closestTileSrc = this.attacker.canBuild(
|
||||
UnitType.TransportShip,
|
||||
this.dst,
|
||||
);
|
||||
if (closestTileSrc == false) {
|
||||
consolex.warn(`can't build transport ship`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.src = src;
|
||||
if (this.src == null) {
|
||||
// Only update the src if it's not already set
|
||||
// because we assume that the src is set to the best spawn tile
|
||||
this.src = closestTileSrc;
|
||||
}
|
||||
|
||||
this.boat = this.attacker.buildUnit(
|
||||
UnitType.TransportShip,
|
||||
@@ -136,6 +145,10 @@ export class TransportShipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (this.embarkDelay > 0) {
|
||||
this.embarkDelay--;
|
||||
return;
|
||||
}
|
||||
if (ticks - this.lastMove < this.ticksPerMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,12 +26,7 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
private patrolTile: TileRef;
|
||||
|
||||
// TODO: put in config
|
||||
private searchRange = 100;
|
||||
|
||||
private shellAttackRate = 5;
|
||||
private lastShellAttack = 0;
|
||||
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
constructor(
|
||||
@@ -72,7 +67,8 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private shoot() {
|
||||
if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) {
|
||||
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
this.mg.addExecution(
|
||||
new ShellExecution(
|
||||
@@ -137,7 +133,7 @@ export class WarshipExecution implements Execution {
|
||||
const ships = this.mg
|
||||
.nearbyUnits(
|
||||
this.warship.tile(),
|
||||
130, // Search range
|
||||
this.mg.config().warshipTargettingRange(),
|
||||
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
||||
)
|
||||
.filter(
|
||||
@@ -146,9 +142,11 @@ export class WarshipExecution implements Execution {
|
||||
unit !== this.warship &&
|
||||
!unit.owner().isFriendly(this.warship.owner()) &&
|
||||
!this.alreadySentShell.has(unit) &&
|
||||
(unit.type() !== UnitType.TradeShip || hasPort) &&
|
||||
(unit.type() !== UnitType.TradeShip ||
|
||||
unit.dstPort()?.owner() !== this._owner),
|
||||
(hasPort &&
|
||||
unit.dstPort()?.owner() !== this.warship.owner() &&
|
||||
!unit.dstPort()?.owner().isFriendly(this.warship.owner()) &&
|
||||
unit.isSafeFromPirates() !== true)),
|
||||
);
|
||||
|
||||
this.target =
|
||||
@@ -198,9 +196,10 @@ export class WarshipExecution implements Execution {
|
||||
if (
|
||||
this.target == null ||
|
||||
!this.target.isActive() ||
|
||||
this.target.owner() == this._owner
|
||||
this.target.owner() == this._owner ||
|
||||
this.target.isSafeFromPirates() == true
|
||||
) {
|
||||
// In case another destroyer captured or destroyed target
|
||||
// In case another warship captured or destroyed target, or the target escaped into safe waters
|
||||
this.target = null;
|
||||
return;
|
||||
}
|
||||
@@ -250,18 +249,29 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
randomTile(): TileRef {
|
||||
while (true) {
|
||||
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
|
||||
const maxAttemptBeforeExpand: number = warshipPatrolRange * 2;
|
||||
let attemptCount: number = 0;
|
||||
let expandCount: number = 0;
|
||||
while (expandCount < 3) {
|
||||
const x =
|
||||
this.mg.x(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
const y =
|
||||
this.mg.y(this.patrolCenterTile) +
|
||||
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
}
|
||||
const tile = this.mg.ref(x, y);
|
||||
if (!this.mg.isOcean(tile)) {
|
||||
if (!this.mg.isOcean(tile) || this.mg.isShoreline(tile)) {
|
||||
attemptCount++;
|
||||
if (attemptCount === maxAttemptBeforeExpand) {
|
||||
expandCount++;
|
||||
attemptCount = 0;
|
||||
warshipPatrolRange =
|
||||
warshipPatrolRange + Math.floor(warshipPatrolRange / 2);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return tile;
|
||||
|
||||
+31
-3
@@ -70,6 +70,30 @@ export enum GameMapType {
|
||||
FaroeIslands = "FaroeIslands",
|
||||
}
|
||||
|
||||
export const mapCategories: Record<string, GameMapType[]> = {
|
||||
continental: [
|
||||
GameMapType.World,
|
||||
GameMapType.NorthAmerica,
|
||||
GameMapType.SouthAmerica,
|
||||
GameMapType.Europe,
|
||||
GameMapType.Asia,
|
||||
GameMapType.Africa,
|
||||
GameMapType.Oceania,
|
||||
],
|
||||
regional: [
|
||||
GameMapType.BlackSea,
|
||||
GameMapType.Britannia,
|
||||
GameMapType.GatewayToTheAtlantic,
|
||||
GameMapType.BetweenTwoSeas,
|
||||
GameMapType.Iceland,
|
||||
GameMapType.Japan,
|
||||
GameMapType.Mena,
|
||||
GameMapType.Australia,
|
||||
GameMapType.FaroeIslands,
|
||||
],
|
||||
fantasy: [GameMapType.Pangaea, GameMapType.Mars, GameMapType.KnownWorld],
|
||||
};
|
||||
|
||||
export enum GameType {
|
||||
Singleplayer = "Singleplayer",
|
||||
Public = "Public",
|
||||
@@ -240,6 +264,7 @@ export class PlayerInfo {
|
||||
// Some units have info specific to them
|
||||
export interface UnitSpecificInfos {
|
||||
dstPort?: Unit; // Only for trade ships
|
||||
lastSetSafeFromPirates?: number; // Only for trade ships
|
||||
detonationDst?: TileRef; // Only for nukes
|
||||
warshipTarget?: Unit;
|
||||
cooldownDuration?: number;
|
||||
@@ -273,6 +298,8 @@ export interface Unit {
|
||||
isCooldown(): boolean;
|
||||
setDstPort(dstPort: Unit): void;
|
||||
dstPort(): Unit; // Only for trade ships
|
||||
setSafeFromPirates(): void; // Only for trade ships
|
||||
isSafeFromPirates(): boolean; // Only for trade ships
|
||||
detonationDst(): TileRef; // Only for nukes
|
||||
|
||||
setMoveTarget(cell: TileRef): void;
|
||||
@@ -347,6 +374,7 @@ export interface Player {
|
||||
// Units
|
||||
units(...types: UnitType[]): Unit[];
|
||||
unitsIncludingConstruction(type: UnitType): Unit[];
|
||||
buildableUnits(tile: TileRef): BuildableUnit[];
|
||||
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
|
||||
buildUnit(
|
||||
type: UnitType,
|
||||
@@ -416,8 +444,9 @@ export interface Player {
|
||||
// Misc
|
||||
toUpdate(): PlayerUpdate;
|
||||
playerProfile(): PlayerProfile;
|
||||
canBoat(tile: TileRef): TileRef | false;
|
||||
tradingPorts(port: Unit): Unit[];
|
||||
// WARNING: this operation is expensive.
|
||||
bestTransportShipSpawn(tile: TileRef): TileRef | false;
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
@@ -474,7 +503,6 @@ export interface Game extends GameMap {
|
||||
}
|
||||
|
||||
export interface PlayerActions {
|
||||
canBoat: TileRef | false;
|
||||
canAttack: boolean;
|
||||
buildableUnits: BuildableUnit[];
|
||||
canSendEmojiAllPlayers: boolean;
|
||||
@@ -482,7 +510,7 @@ export interface PlayerActions {
|
||||
}
|
||||
|
||||
export interface BuildableUnit {
|
||||
canBuild: boolean;
|
||||
canBuild: TileRef | false;
|
||||
type: UnitType;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from "../configuration/Config";
|
||||
import { ClientID, GameID, PlayerStats } from "../Schemas";
|
||||
import { createRandomName } from "../Util";
|
||||
import { WorkerClient } from "../worker/WorkerClient";
|
||||
import {
|
||||
Cell,
|
||||
@@ -123,11 +124,22 @@ export class UnitView {
|
||||
}
|
||||
|
||||
export class PlayerView {
|
||||
public anonymousName: string;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
public data: PlayerUpdate,
|
||||
public nameData: NameViewData,
|
||||
) {}
|
||||
) {
|
||||
if (data.clientID == game.myClientID()) {
|
||||
this.anonymousName = this.data.name;
|
||||
} else {
|
||||
this.anonymousName = createRandomName(
|
||||
this.data.name,
|
||||
this.data.playerType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async actions(tile: TileRef): Promise<PlayerActions> {
|
||||
return this.game.worker.playerInteraction(
|
||||
@@ -166,11 +178,16 @@ export class PlayerView {
|
||||
return this.data.flag;
|
||||
}
|
||||
name(): string {
|
||||
return this.data.name;
|
||||
return userSettings.anonymousNames() && this.anonymousName !== null
|
||||
? this.anonymousName
|
||||
: this.data.name;
|
||||
}
|
||||
displayName(): string {
|
||||
return this.data.displayName;
|
||||
return userSettings.anonymousNames() && this.anonymousName !== null
|
||||
? this.anonymousName
|
||||
: this.data.name;
|
||||
}
|
||||
|
||||
clientID(): ClientID {
|
||||
return this.data.clientID;
|
||||
}
|
||||
@@ -242,6 +259,10 @@ export class PlayerView {
|
||||
return this.game.worker.playerProfile(this.smallID());
|
||||
}
|
||||
|
||||
bestTransportShipSpawn(targetTile: TileRef): Promise<TileRef | false> {
|
||||
return this.game.worker.transportShipSpawn(this.id(), targetTile);
|
||||
}
|
||||
|
||||
transitiveTargets(): PlayerView[] {
|
||||
return [...this.targets(), ...this.allies().flatMap((p) => p.targets())];
|
||||
}
|
||||
|
||||
+79
-92
@@ -4,12 +4,10 @@ import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ClientID } from "../Schemas";
|
||||
import {
|
||||
assertNever,
|
||||
closestShoreFromPlayer,
|
||||
distSortUnit,
|
||||
maxInt,
|
||||
minInt,
|
||||
simpleHash,
|
||||
targetTransportTile,
|
||||
toInt,
|
||||
within,
|
||||
} from "../Util";
|
||||
@@ -20,6 +18,7 @@ import {
|
||||
AllianceRequest,
|
||||
AllPlayers,
|
||||
Attack,
|
||||
BuildableUnit,
|
||||
Cell,
|
||||
EmojiMessage,
|
||||
GameMode,
|
||||
@@ -43,6 +42,10 @@ import { GameImpl } from "./GameImpl";
|
||||
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
|
||||
import { AttackUpdate, GameUpdateType, PlayerUpdate } from "./GameUpdates";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import {
|
||||
bestShoreDeploymentSource,
|
||||
canBuildTransportShip,
|
||||
} from "./TransportShipUtils";
|
||||
import { UnitImpl } from "./UnitImpl";
|
||||
|
||||
interface Target {
|
||||
@@ -729,7 +732,22 @@ export class PlayerImpl implements Player {
|
||||
return b;
|
||||
}
|
||||
|
||||
canBuild(unitType: UnitType, targetTile: TileRef): TileRef | false {
|
||||
public buildableUnits(tile: TileRef): BuildableUnit[] {
|
||||
const validTiles = this.validStructureSpawnTiles(tile);
|
||||
return Object.values(UnitType).map((u) => {
|
||||
return {
|
||||
type: u,
|
||||
canBuild: this.canBuild(u, tile, validTiles),
|
||||
cost: this.mg.config().unitInfo(u).cost(this),
|
||||
} as BuildableUnit;
|
||||
});
|
||||
}
|
||||
|
||||
canBuild(
|
||||
unitType: UnitType,
|
||||
targetTile: TileRef,
|
||||
validTiles: TileRef[] | null = null,
|
||||
): TileRef | false {
|
||||
// prevent the building of nukes and nuke related buildings
|
||||
if (this.mg.config().disableNukes()) {
|
||||
if (
|
||||
@@ -761,14 +779,14 @@ export class PlayerImpl implements Player {
|
||||
case UnitType.MIRVWarhead:
|
||||
return targetTile;
|
||||
case UnitType.Port:
|
||||
return this.portSpawn(targetTile);
|
||||
return this.portSpawn(targetTile, validTiles);
|
||||
case UnitType.Warship:
|
||||
return this.warshipSpawn(targetTile);
|
||||
case UnitType.Shell:
|
||||
case UnitType.SAMMissile:
|
||||
return targetTile;
|
||||
case UnitType.TransportShip:
|
||||
return this.transportShipSpawn(targetTile);
|
||||
return canBuildTransportShip(this.mg, this, targetTile);
|
||||
case UnitType.TradeShip:
|
||||
return this.tradeShipSpawn(targetTile);
|
||||
case UnitType.MissileSilo:
|
||||
@@ -776,7 +794,7 @@ export class PlayerImpl implements Player {
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.City:
|
||||
case UnitType.Construction:
|
||||
return this.landBasedStructureSpawn(targetTile);
|
||||
return this.landBasedStructureSpawn(targetTile, validTiles);
|
||||
default:
|
||||
assertNever(unitType);
|
||||
}
|
||||
@@ -801,7 +819,7 @@ export class PlayerImpl implements Player {
|
||||
return spawns[0].tile();
|
||||
}
|
||||
|
||||
portSpawn(tile: TileRef): TileRef | false {
|
||||
portSpawn(tile: TileRef, validTiles: TileRef[]): TileRef | false {
|
||||
const spawns = Array.from(
|
||||
this.mg.bfs(
|
||||
tile,
|
||||
@@ -813,10 +831,15 @@ export class PlayerImpl implements Player {
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
return false;
|
||||
const validTileSet = new Set(
|
||||
validTiles ?? this.validStructureSpawnTiles(tile),
|
||||
);
|
||||
for (const t of spawns) {
|
||||
if (validTileSet.has(t)) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return spawns[0];
|
||||
return false;
|
||||
}
|
||||
|
||||
warshipSpawn(tile: TileRef): TileRef | false {
|
||||
@@ -834,22 +857,54 @@ export class PlayerImpl implements Player {
|
||||
return spawns[0].tile();
|
||||
}
|
||||
|
||||
landBasedStructureSpawn(tile: TileRef): TileRef | false {
|
||||
if (this.mg.owner(tile) != this) {
|
||||
landBasedStructureSpawn(
|
||||
tile: TileRef,
|
||||
validTiles: TileRef[] | null = null,
|
||||
): TileRef | false {
|
||||
const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
|
||||
if (tiles.length == 0) {
|
||||
return false;
|
||||
}
|
||||
return tile;
|
||||
return tiles[0];
|
||||
}
|
||||
|
||||
transportShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
if (!this.mg.isShore(targetTile)) {
|
||||
return false;
|
||||
private validStructureSpawnTiles(tile: TileRef): TileRef[] {
|
||||
if (this.mg.owner(tile) != this) {
|
||||
return [];
|
||||
}
|
||||
const spawn = closestShoreFromPlayer(this.mg, this, targetTile);
|
||||
if (spawn == null) {
|
||||
return false;
|
||||
const searchRadius = 15;
|
||||
const searchRadiusSquared = searchRadius ** 2;
|
||||
const types = Object.values(UnitType).filter((unitTypeValue) => {
|
||||
return this.mg.config().unitInfo(unitTypeValue).territoryBound;
|
||||
});
|
||||
|
||||
const nearbyUnits = this.mg
|
||||
.nearbyUnits(tile, searchRadius * 2, types)
|
||||
.map((u) => u.unit);
|
||||
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
|
||||
return (
|
||||
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
|
||||
gm.ownerID(t) == this.smallID()
|
||||
);
|
||||
});
|
||||
const validSet: Set<TileRef> = new Set(nearbyTiles);
|
||||
|
||||
const minDistSquared = this.mg.config().structureMinDist() ** 2;
|
||||
for (const t of nearbyTiles) {
|
||||
for (const unit of nearbyUnits) {
|
||||
if (this.mg.euclideanDistSquared(unit.tile(), t) < minDistSquared) {
|
||||
validSet.delete(t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return spawn;
|
||||
const valid = Array.from(validSet);
|
||||
valid.sort(
|
||||
(a, b) =>
|
||||
this.mg.euclideanDistSquared(a, tile) -
|
||||
this.mg.euclideanDistSquared(b, tile),
|
||||
);
|
||||
return valid;
|
||||
}
|
||||
|
||||
tradeShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
@@ -892,78 +947,6 @@ export class PlayerImpl implements Player {
|
||||
return rel;
|
||||
}
|
||||
|
||||
public canBoat(tile: TileRef): TileRef | false {
|
||||
if (
|
||||
this.units(UnitType.TransportShip).length >=
|
||||
this.mg.config().boatMaxNumber()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dst = targetTransportTile(this.mg, tile);
|
||||
if (dst == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const other = this.mg.owner(tile);
|
||||
if (other == this) {
|
||||
return false;
|
||||
}
|
||||
if (other.isPlayer() && this.isFriendly(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.mg.isOceanShore(dst)) {
|
||||
let myPlayerBordersOcean = false;
|
||||
for (const bt of this.borderTiles()) {
|
||||
if (this.mg.isOceanShore(bt)) {
|
||||
myPlayerBordersOcean = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let otherPlayerBordersOcean = false;
|
||||
if (!this.mg.hasOwner(tile)) {
|
||||
otherPlayerBordersOcean = true;
|
||||
} else {
|
||||
for (const bt of (other as Player).borderTiles()) {
|
||||
if (this.mg.isOceanShore(bt)) {
|
||||
otherPlayerBordersOcean = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
|
||||
return this.canBuild(UnitType.TransportShip, dst);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we are boating in a lake, so do a bfs from target until we find
|
||||
// a border tile owned by the player
|
||||
|
||||
const tiles = this.mg.bfs(
|
||||
dst,
|
||||
andFN(
|
||||
manhattanDistFN(dst, 300),
|
||||
(_, t: TileRef) => this.mg.isLake(t) || this.mg.isShore(t),
|
||||
),
|
||||
);
|
||||
|
||||
const sorted = Array.from(tiles).sort(
|
||||
(a, b) => this.mg.manhattanDist(dst, a) - this.mg.manhattanDist(dst, b),
|
||||
);
|
||||
|
||||
for (const t of sorted) {
|
||||
if (this.mg.owner(t) == this) {
|
||||
return this.canBuild(UnitType.TransportShip, dst);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
@@ -1032,6 +1015,10 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
}
|
||||
|
||||
bestTransportShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
return bestShoreDeploymentSource(this.mg, this, targetTile);
|
||||
}
|
||||
|
||||
// It's a probability list, so if an element appears twice it's because it's
|
||||
// twice more likely to be picked later.
|
||||
tradingPorts(port: Unit): Unit[] {
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { PathFindResultType } from "../pathfinding/AStar";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { Game, Player, UnitType } from "./Game";
|
||||
import { andFN, GameMap, manhattanDistFN, TileRef } from "./GameMap";
|
||||
|
||||
export function canBuildTransportShip(
|
||||
game: Game,
|
||||
player: Player,
|
||||
tile: TileRef,
|
||||
): TileRef | false {
|
||||
if (
|
||||
player.units(UnitType.TransportShip).length >= game.config().boatMaxNumber()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dst = targetTransportTile(game, tile);
|
||||
if (dst == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const other = game.owner(tile);
|
||||
if (other == player) {
|
||||
return false;
|
||||
}
|
||||
if (other.isPlayer() && player.isFriendly(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (game.isOceanShore(dst)) {
|
||||
let myPlayerBordersOcean = false;
|
||||
for (const bt of player.borderTiles()) {
|
||||
if (game.isOceanShore(bt)) {
|
||||
myPlayerBordersOcean = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let otherPlayerBordersOcean = false;
|
||||
if (!game.hasOwner(tile)) {
|
||||
otherPlayerBordersOcean = true;
|
||||
} else {
|
||||
for (const bt of (other as Player).borderTiles()) {
|
||||
if (game.isOceanShore(bt)) {
|
||||
otherPlayerBordersOcean = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (myPlayerBordersOcean && otherPlayerBordersOcean) {
|
||||
return transportShipSpawn(game, player, dst);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we are boating in a lake, so do a bfs from target until we find
|
||||
// a border tile owned by the player
|
||||
|
||||
const tiles = game.bfs(
|
||||
dst,
|
||||
andFN(
|
||||
manhattanDistFN(dst, 300),
|
||||
(_, t: TileRef) => game.isLake(t) || game.isShore(t),
|
||||
),
|
||||
);
|
||||
|
||||
const sorted = Array.from(tiles).sort(
|
||||
(a, b) => game.manhattanDist(dst, a) - game.manhattanDist(dst, b),
|
||||
);
|
||||
|
||||
for (const t of sorted) {
|
||||
if (game.owner(t) == player) {
|
||||
return transportShipSpawn(game, player, t);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function transportShipSpawn(
|
||||
game: Game,
|
||||
player: Player,
|
||||
targetTile: TileRef,
|
||||
): TileRef | false {
|
||||
if (!game.isShore(targetTile)) {
|
||||
return false;
|
||||
}
|
||||
const spawn = closestShoreFromPlayer(game, player, targetTile);
|
||||
if (spawn == null) {
|
||||
return false;
|
||||
}
|
||||
return spawn;
|
||||
}
|
||||
|
||||
export function sourceDstOceanShore(
|
||||
gm: Game,
|
||||
src: Player,
|
||||
tile: TileRef,
|
||||
): [TileRef | null, TileRef | null] {
|
||||
const dst = gm.owner(tile);
|
||||
const srcTile = closestShoreFromPlayer(gm, src, tile);
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestShoreTN(gm, tile, 50);
|
||||
}
|
||||
return [srcTile, dstTile];
|
||||
}
|
||||
|
||||
export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
||||
const dst = gm.playerBySmallID(gm.ownerID(tile));
|
||||
let dstTile: TileRef | null = null;
|
||||
if (dst.isPlayer()) {
|
||||
dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
|
||||
} else {
|
||||
dstTile = closestShoreTN(gm, tile, 50);
|
||||
}
|
||||
return dstTile;
|
||||
}
|
||||
|
||||
export function closestShoreFromPlayer(
|
||||
gm: GameMap,
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | null {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
gm.isShore(t),
|
||||
);
|
||||
if (shoreTiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return shoreTiles.reduce((closest, current) => {
|
||||
const closestDistance = gm.manhattanDist(target, closest);
|
||||
const currentDistance = gm.manhattanDist(target, current);
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the best shore tile for deployment among the player's shore tiles for the shortest route.
|
||||
* Calculates paths from 4 extremum tiles and the Manhattan-closest tile.
|
||||
*/
|
||||
export function bestShoreDeploymentSource(
|
||||
gm: Game,
|
||||
player: Player,
|
||||
target: TileRef,
|
||||
): TileRef | null {
|
||||
target = targetTransportTile(gm, target);
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
let closestManhattanDistance = Infinity;
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
let bestByManhattan: TileRef = null;
|
||||
const extremumTiles: Record<string, TileRef> = {
|
||||
minX: null,
|
||||
minY: null,
|
||||
maxX: null,
|
||||
maxY: null,
|
||||
};
|
||||
|
||||
for (const tile of player.borderTiles()) {
|
||||
if (!gm.isShore(tile)) continue;
|
||||
|
||||
const distance = gm.manhattanDist(tile, target);
|
||||
const cell = gm.cell(tile);
|
||||
|
||||
// Manhattan-closest tile
|
||||
if (distance < closestManhattanDistance) {
|
||||
closestManhattanDistance = distance;
|
||||
bestByManhattan = tile;
|
||||
}
|
||||
|
||||
// Extremum tiles
|
||||
if (cell.x < minX) {
|
||||
minX = cell.x;
|
||||
extremumTiles.minX = tile;
|
||||
} else if (cell.y < minY) {
|
||||
minY = cell.y;
|
||||
extremumTiles.minY = tile;
|
||||
} else if (cell.x > maxX) {
|
||||
maxX = cell.x;
|
||||
extremumTiles.maxX = tile;
|
||||
} else if (cell.y > maxY) {
|
||||
maxY = cell.y;
|
||||
extremumTiles.maxY = tile;
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
bestByManhattan,
|
||||
extremumTiles.minX,
|
||||
extremumTiles.minY,
|
||||
extremumTiles.maxX,
|
||||
extremumTiles.maxY,
|
||||
].filter(Boolean);
|
||||
|
||||
if (!candidates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the shortest actual path distance
|
||||
let closestShoreTile: TileRef | null = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const shoreTile of candidates) {
|
||||
const pathDistance = calculatePathDistance(gm, shoreTile, target);
|
||||
|
||||
if (pathDistance !== null && pathDistance < closestDistance) {
|
||||
closestDistance = pathDistance;
|
||||
closestShoreTile = shoreTile;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the Manhattan-closest tile if no path was found
|
||||
return closestShoreTile || bestByManhattan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the distance between two tiles using A*
|
||||
* Returns null if no path is found
|
||||
*/
|
||||
function calculatePathDistance(
|
||||
gm: Game,
|
||||
start: TileRef,
|
||||
target: TileRef,
|
||||
): number | null {
|
||||
let currentTile = start;
|
||||
let tileDistance = 0;
|
||||
const pathFinder = PathFinder.Mini(gm, 20_000, false);
|
||||
|
||||
while (true) {
|
||||
const result = pathFinder.nextTile(currentTile, target);
|
||||
|
||||
if (result.type === PathFindResultType.Completed) {
|
||||
return tileDistance;
|
||||
} else if (result.type === PathFindResultType.NextTile) {
|
||||
currentTile = result.tile;
|
||||
tileDistance++;
|
||||
} else if (
|
||||
result.type === PathFindResultType.PathNotFound ||
|
||||
result.type === PathFindResultType.Pending
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
// @ts-expect-error type is never
|
||||
throw new Error(`Unexpected pathfinding result type: ${result.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closestShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
||||
),
|
||||
)
|
||||
.filter((t) => gm.isShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
||||
if (tn.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return tn[0];
|
||||
}
|
||||
@@ -17,11 +17,11 @@ export class UnitImpl implements Unit {
|
||||
private _active = true;
|
||||
private _health: bigint;
|
||||
private _lastTile: TileRef = null;
|
||||
// Currently only warship use it
|
||||
private _target: Unit = null;
|
||||
private _moveTarget: TileRef = null;
|
||||
private _targetedBySAM = false;
|
||||
|
||||
private _safeFromPiratesCooldown: number; // Only for trade ships
|
||||
private _lastSetSafeFromPirates: number; // Only for trade ships
|
||||
private _constructionType: UnitType = undefined;
|
||||
|
||||
private _cooldownTick: Tick | null = null;
|
||||
@@ -45,6 +45,10 @@ export class UnitImpl implements Unit {
|
||||
this._detonationDst = unitsSpecificInfos.detonationDst;
|
||||
this._warshipTarget = unitsSpecificInfos.warshipTarget;
|
||||
this._cooldownDuration = unitsSpecificInfos.cooldownDuration;
|
||||
this._lastSetSafeFromPirates = unitsSpecificInfos.lastSetSafeFromPirates;
|
||||
this._safeFromPiratesCooldown = this.mg
|
||||
.config()
|
||||
.safeFromPiratesCooldownMax();
|
||||
}
|
||||
|
||||
id() {
|
||||
@@ -233,4 +237,15 @@ export class UnitImpl implements Unit {
|
||||
targetedBySAM(): boolean {
|
||||
return this._targetedBySAM;
|
||||
}
|
||||
|
||||
setSafeFromPirates(): void {
|
||||
this._lastSetSafeFromPirates = this.mg.ticks();
|
||||
}
|
||||
|
||||
isSafeFromPirates(): boolean {
|
||||
return (
|
||||
this.mg.ticks() - this._lastSetSafeFromPirates <
|
||||
this._safeFromPiratesCooldown
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export class UserSettings {
|
||||
emojis() {
|
||||
return this.get("settings.emojis", true);
|
||||
}
|
||||
anonymousNames() {
|
||||
return this.get("settings.anonymousNames", false);
|
||||
}
|
||||
|
||||
darkMode() {
|
||||
return this.get("settings.darkMode", false);
|
||||
@@ -42,6 +45,10 @@ export class UserSettings {
|
||||
this.set("settings.emojis", !this.emojis());
|
||||
}
|
||||
|
||||
toggleRandomName() {
|
||||
this.set("settings.anonymousNames", !this.anonymousNames());
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
this.set("settings.darkMode", !this.darkMode());
|
||||
if (this.darkMode()) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PlayerActionsResultMessage,
|
||||
PlayerBorderTilesResultMessage,
|
||||
PlayerProfileResultMessage,
|
||||
TransportShipSpawnResultMessage,
|
||||
WorkerMessage,
|
||||
} from "./WorkerMessages";
|
||||
|
||||
@@ -120,6 +121,25 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
case "transport_ship_spawn":
|
||||
if (!gameRunner) {
|
||||
throw new Error("Game runner not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const spawnTile = (await gameRunner).bestTransportShipSpawn(
|
||||
message.playerID,
|
||||
message.targetTile,
|
||||
);
|
||||
sendMessage({
|
||||
type: "transport_ship_spawn_result",
|
||||
id: message.id,
|
||||
result: spawnTile,
|
||||
} as TransportShipSpawnResultMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to spawn transport ship:", error);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown message :", message);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
PlayerID,
|
||||
PlayerProfile,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { ClientID, GameStartInfo, Turn } from "../Schemas";
|
||||
import { generateID } from "../Util";
|
||||
@@ -188,6 +189,36 @@ export class WorkerClient {
|
||||
});
|
||||
}
|
||||
|
||||
transportShipSpawn(
|
||||
playerID: PlayerID,
|
||||
targetTile: TileRef,
|
||||
): Promise<TileRef | false> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
reject(new Error("Worker not initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = generateID();
|
||||
|
||||
this.messageHandlers.set(messageId, (message) => {
|
||||
if (
|
||||
message.type === "transport_ship_spawn_result" &&
|
||||
message.result !== undefined
|
||||
) {
|
||||
resolve(message.result);
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "transport_ship_spawn",
|
||||
id: messageId,
|
||||
playerID: playerID,
|
||||
targetTile: targetTile,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.worker.terminate();
|
||||
this.messageHandlers.clear();
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
PlayerID,
|
||||
PlayerProfile,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { ClientID, GameStartInfo, Turn } from "../Schemas";
|
||||
|
||||
@@ -18,7 +19,9 @@ export type WorkerMessageType =
|
||||
| "player_profile"
|
||||
| "player_profile_result"
|
||||
| "player_border_tiles"
|
||||
| "player_border_tiles_result";
|
||||
| "player_border_tiles_result"
|
||||
| "transport_ship_spawn"
|
||||
| "transport_ship_spawn_result";
|
||||
|
||||
// Base interface for all messages
|
||||
interface BaseWorkerMessage {
|
||||
@@ -84,6 +87,17 @@ export interface PlayerBorderTilesResultMessage extends BaseWorkerMessage {
|
||||
result: PlayerBorderTiles;
|
||||
}
|
||||
|
||||
export interface TransportShipSpawnMessage extends BaseWorkerMessage {
|
||||
type: "transport_ship_spawn";
|
||||
playerID: PlayerID;
|
||||
targetTile: TileRef;
|
||||
}
|
||||
|
||||
export interface TransportShipSpawnResultMessage extends BaseWorkerMessage {
|
||||
type: "transport_ship_spawn_result";
|
||||
result: TileRef | false;
|
||||
}
|
||||
|
||||
// Union types for type safety
|
||||
export type MainThreadMessage =
|
||||
| HeartbeatMessage
|
||||
@@ -91,7 +105,8 @@ export type MainThreadMessage =
|
||||
| TurnMessage
|
||||
| PlayerActionsMessage
|
||||
| PlayerProfileMessage
|
||||
| PlayerBorderTilesMessage;
|
||||
| PlayerBorderTilesMessage
|
||||
| TransportShipSpawnMessage;
|
||||
|
||||
// Message send from worker
|
||||
export type WorkerMessage =
|
||||
@@ -99,4 +114,5 @@ export type WorkerMessage =
|
||||
| GameUpdateMessage
|
||||
| PlayerActionsResultMessage
|
||||
| PlayerProfileResultMessage
|
||||
| PlayerBorderTilesResultMessage;
|
||||
| PlayerBorderTilesResultMessage
|
||||
| TransportShipSpawnResultMessage;
|
||||
|
||||
@@ -81,25 +81,25 @@ export class MapPlaylist {
|
||||
// Big Maps are those larger than ~2.5 mil pixels
|
||||
case PlaylistType.BigMaps:
|
||||
return {
|
||||
Europe: 3,
|
||||
NorthAmerica: 2,
|
||||
Europe: 2,
|
||||
NorthAmerica: 1,
|
||||
Africa: 2,
|
||||
Britannia: 1,
|
||||
GatewayToTheAtlantic: 2,
|
||||
Australia: 2,
|
||||
Iceland: 2,
|
||||
SouthAmerica: 3,
|
||||
SouthAmerica: 1,
|
||||
KnownWorld: 2,
|
||||
};
|
||||
case PlaylistType.SmallMaps:
|
||||
return {
|
||||
World: 1,
|
||||
World: 4,
|
||||
Mena: 2,
|
||||
Pangaea: 1,
|
||||
Asia: 1,
|
||||
Mars: 1,
|
||||
BetweenTwoSeas: 3,
|
||||
Japan: 3,
|
||||
BetweenTwoSeas: 2,
|
||||
Japan: 2,
|
||||
BlackSea: 1,
|
||||
FaroeIslands: 2,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import promClient from "prom-client";
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameManager } from "./GameManager";
|
||||
|
||||
const config = getServerConfigFromServer();
|
||||
const region = config.region();
|
||||
|
||||
// Initialize the Prometheus registry
|
||||
const register = new promClient.Registry();
|
||||
|
||||
@@ -15,21 +11,18 @@ promClient.collectDefaultMetrics({ register });
|
||||
const activeGamesGauge = new promClient.Gauge({
|
||||
name: "openfront_active_games_count",
|
||||
help: "Number of active games on this worker",
|
||||
labelNames: ["region"],
|
||||
registers: [register],
|
||||
});
|
||||
|
||||
const connectedClientsGauge = new promClient.Gauge({
|
||||
name: "openfront_connected_clients_count",
|
||||
help: "Number of connected clients on this worker",
|
||||
labelNames: ["region"],
|
||||
registers: [register],
|
||||
});
|
||||
|
||||
const memoryUsageGauge = new promClient.Gauge({
|
||||
name: "openfront_memory_usage_bytes",
|
||||
help: "Current memory usage of the worker process in bytes",
|
||||
labelNames: ["region"],
|
||||
registers: [register],
|
||||
});
|
||||
|
||||
@@ -42,11 +35,11 @@ export const metrics = {
|
||||
|
||||
// Function to update game-related metrics
|
||||
updateGameMetrics: (gameManager: GameManager) => {
|
||||
activeGamesGauge.set({ region: region }, gameManager.activeGames());
|
||||
connectedClientsGauge.set({ region: region }, gameManager.activeClients());
|
||||
activeGamesGauge.set(gameManager.activeGames());
|
||||
connectedClientsGauge.set(gameManager.activeClients());
|
||||
|
||||
// Update memory usage metrics
|
||||
const memoryUsage = process.memoryUsage();
|
||||
memoryUsageGauge.set({ region: region }, memoryUsage.heapUsed);
|
||||
memoryUsageGauge.set(memoryUsage.heapUsed);
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
Submodule src/server/gatekeeper updated: 4d3fd72121...8324db9408
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Check if required environment variables are set
|
||||
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ -z "$SUBDOMAIN" ] || [ -z "$DOMAIN" ]; then
|
||||
echo "Error: Required environment variables not set"
|
||||
echo "Please set CF_API_TOKEN, CF_ACCOUNT_ID, SUBDOMAIN, and DOMAIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate a unique tunnel name using timestamp
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
TUNNEL_NAME="${SUBDOMAIN}-tunnel-${TIMESTAMP}"
|
||||
echo "Using unique tunnel name: ${TUNNEL_NAME}"
|
||||
|
||||
# Create a new tunnel
|
||||
echo "Creating Cloudflare tunnel for subdomain ${SUBDOMAIN}..."
|
||||
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"name\":\"${TUNNEL_NAME}\"}")
|
||||
|
||||
# Extract tunnel ID and token
|
||||
TUNNEL_ID=$(echo $TUNNEL_RESPONSE | jq -r '.result.id')
|
||||
TUNNEL_TOKEN=$(echo $TUNNEL_RESPONSE | jq -r '.result.token')
|
||||
|
||||
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" == "null" ]; then
|
||||
echo "Failed to create tunnel"
|
||||
echo $TUNNEL_RESPONSE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tunnel created with ID: ${TUNNEL_ID}"
|
||||
|
||||
# Configure the tunnel with hostname
|
||||
echo "Configuring tunnel to point to ${SUBDOMAIN}.${DOMAIN}..."
|
||||
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"config\":{\"ingress\":[{\"hostname\":\"${SUBDOMAIN}.${DOMAIN}\",\"service\":\"http://localhost:80\"},{\"service\":\"http_status:404\"}]}}"
|
||||
|
||||
# Update DNS record to point to the new tunnel
|
||||
echo "Updating DNS record to point to the new tunnel..."
|
||||
|
||||
# First check if DNS record exists
|
||||
DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
ZONE_ID=$(echo $DNS_RECORDS | jq -r '.result[0].id')
|
||||
|
||||
if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" == "null" ]; then
|
||||
echo "Could not find zone ID for domain ${DOMAIN}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for existing record
|
||||
EXISTING_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}.${DOMAIN}" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json")
|
||||
|
||||
RECORD_ID=$(echo $EXISTING_RECORDS | jq -r '.result[0].id')
|
||||
|
||||
# Create or update the DNS record
|
||||
if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" == "null" ]; then
|
||||
# Create new record
|
||||
echo "Creating new DNS record..."
|
||||
DNS_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
|
||||
else
|
||||
# Update existing record
|
||||
echo "Updating existing DNS record..."
|
||||
DNS_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
|
||||
-H "Authorization: Bearer ${CF_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
|
||||
fi
|
||||
|
||||
|
||||
# Log the tunnel information
|
||||
echo "Tunnel is set up! Site will be available at: https://${SUBDOMAIN}.${DOMAIN}"
|
||||
|
||||
|
||||
# Export the tunnel token for supervisord
|
||||
export CLOUDFLARE_TUNNEL_TOKEN=${TUNNEL_TOKEN}
|
||||
|
||||
# Start supervisord
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
+23
-1
@@ -22,4 +22,26 @@ user=node
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:cloudflared]
|
||||
command=cloudflared tunnel run --token %(ENV_CLOUDFLARE_TUNNEL_TOKEN)s
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/cloudflared.log
|
||||
stderr_logfile=/var/log/cloudflared-err.log
|
||||
|
||||
[program:node_exporter]
|
||||
command=/usr/local/bin/node_exporter
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/node_exporter.log
|
||||
stderr_logfile=/var/log/node_exporter-err.log
|
||||
|
||||
|
||||
[program:metrics_exporter]
|
||||
command=/usr/src/app/metric-exporter.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/var/log/metrics-exporter.log
|
||||
stderr_logfile=/var/log/metrics-exporter-err.log
|
||||
|
||||
@@ -19,9 +19,9 @@ let defender: Player;
|
||||
let defenderSpawn: TileRef;
|
||||
let attackerSpawn: TileRef;
|
||||
|
||||
function sendBoat(target: TileRef, troops: number) {
|
||||
function sendBoat(target: TileRef, source: TileRef, troops: number) {
|
||||
game.addExecution(
|
||||
new TransportShipExecution(defender.id(), null, target, troops),
|
||||
new TransportShipExecution(defender.id(), null, target, troops, source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("Attack", () => {
|
||||
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
sendBoat(game.ref(15, 8), 100);
|
||||
sendBoat(game.ref(15, 8), game.ref(10, 5), 100);
|
||||
|
||||
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
|
||||
+31
-56
@@ -1,4 +1,4 @@
|
||||
import { NukeExecution } from "../src/core/execution/NukeExecution";
|
||||
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
Game,
|
||||
@@ -7,32 +7,13 @@ import {
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "../src/core/game/Game";
|
||||
import { TileRef } from "../src/core/game/GameMap";
|
||||
import { setup } from "./util/Setup";
|
||||
import { constructionExecution } from "./util/utils";
|
||||
import { constructionExecution, executeTicks } from "./util/utils";
|
||||
|
||||
let game: Game;
|
||||
let attacker: Player;
|
||||
let defender: Player;
|
||||
|
||||
function attackerBuildsNuke(
|
||||
source: TileRef,
|
||||
target: TileRef,
|
||||
initialize = true,
|
||||
) {
|
||||
game.addExecution(
|
||||
new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source),
|
||||
);
|
||||
if (initialize) {
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function defenderBuildsSam(x: number, y: number) {
|
||||
constructionExecution(game, defender.id(), x, y, UnitType.SAMLauncher);
|
||||
}
|
||||
|
||||
describe("SAM", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("Plains", { infiniteGold: true, instantBuild: true });
|
||||
@@ -69,62 +50,56 @@ describe("SAM", () => {
|
||||
});
|
||||
|
||||
test("one sam should take down one nuke", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 1));
|
||||
|
||||
executeTicks(game, 3);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("sam should only get one nuke at a time", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1), false);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 1));
|
||||
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
executeTicks(game, 3);
|
||||
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("sam should cooldown as long as configured", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy();
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
expect(sam.isCooldown()).toBeFalsy();
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
executeTicks(game, 3);
|
||||
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
for (let i = 0; i < game.config().SAMCooldown() - 2; i++) {
|
||||
game.executeNextTick();
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeTruthy();
|
||||
expect(sam.isCooldown()).toBeTruthy();
|
||||
}
|
||||
|
||||
game.executeNextTick();
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy();
|
||||
executeTicks(game, 2);
|
||||
|
||||
expect(sam.isCooldown()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("two sams should not target twice same nuke", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
defenderBuildsSam(1, 2);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
const sam1 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
|
||||
const sam2 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 2));
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 2));
|
||||
|
||||
expect(defender.units(UnitType.SAMLauncher)).toHaveLength(2);
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
executeTicks(game, 3);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
const sams = defender.units(UnitType.SAMLauncher);
|
||||
// Only one sam must have shot
|
||||
expect(
|
||||
(sams[0].isCooldown() && !sams[1].isCooldown()) ||
|
||||
(sams[1].isCooldown() && !sams[0].isCooldown()),
|
||||
).toBe(true);
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
expect([sam1, sam2].filter((s) => s.isCooldown())).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,3 +28,9 @@ export function constructionExecution(
|
||||
game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
export function executeTicks(game: Game, numTicks: number): void {
|
||||
for (let i = 0; i < numTicks; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,20 @@
|
||||
# update.sh - Script to update Docker container on Hetzner server
|
||||
# Called by deploy.sh after uploading Docker image to Docker Hub
|
||||
|
||||
# Check if environment parameter is provided
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Error: Required parameters missing"
|
||||
echo "Usage: $0 <REGION> <docker_username> <docker_repo>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set parameters
|
||||
REGION=$1
|
||||
DOCKER_USERNAME=$2
|
||||
DOCKER_REPO=$3
|
||||
|
||||
# Container and image configuration
|
||||
CONTAINER_NAME="openfront-${REGION}"
|
||||
IMAGE_NAME="${DOCKER_USERNAME}/${DOCKER_REPO}"
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:latest"
|
||||
|
||||
echo "======================================================"
|
||||
echo "🔄 UPDATING SERVER: ${REGION} ENVIRONMENT"
|
||||
echo "======================================================"
|
||||
echo "Container name: ${CONTAINER_NAME}"
|
||||
echo "Docker image: ${FULL_IMAGE_NAME}"
|
||||
|
||||
# Load environment variables if .env exists
|
||||
if [ -f /home/openfront/.env ]; then
|
||||
echo "Loading environment variables from .env file..."
|
||||
export $(grep -v '^#' /home/openfront/.env | xargs)
|
||||
fi
|
||||
|
||||
echo "======================================================"
|
||||
echo "🔄 UPDATING SERVER: ${HOST} ENVIRONMENT"
|
||||
echo "======================================================"
|
||||
|
||||
|
||||
# Container and image configuration
|
||||
CONTAINER_NAME="openfront-${ENV}-${SUBDOMAIN}"
|
||||
|
||||
docker login -u $DOCKER_USERNAME -p $DOCKER_TOKEN
|
||||
|
||||
# Install Loki Docker plugin if not already installed
|
||||
@@ -46,8 +31,8 @@ else
|
||||
echo "Loki Docker plugin already installed."
|
||||
fi
|
||||
|
||||
echo "Pulling latest image from Docker Hub..."
|
||||
docker pull $FULL_IMAGE_NAME
|
||||
echo "Pulling ${DOCKER_IMAGE} from Docker Hub..."
|
||||
docker pull $DOCKER_IMAGE
|
||||
|
||||
echo "Checking for existing container..."
|
||||
# Check for running container
|
||||
@@ -86,27 +71,20 @@ if [ -n "$PORT_CHECK" ]; then
|
||||
echo "Attempting to proceed anyway..."
|
||||
fi
|
||||
|
||||
ENV="prod"
|
||||
if [ "$REGION" == "staging" ]; then
|
||||
ENV="staging"
|
||||
fi
|
||||
|
||||
echo "Starting new container for ${REGION} environment..."
|
||||
docker run -d -p 80:80 -p 127.0.0.1:9090:9090 \
|
||||
echo "Starting new container for ${HOST} environment..."
|
||||
docker run -d \
|
||||
--restart=always \
|
||||
$VOLUME_MOUNTS \
|
||||
--log-driver=loki \
|
||||
--log-opt loki-url="http://localhost:3100/loki/api/v1/push" \
|
||||
--log-opt loki-url="https://${MON_USERNAME}:${MON_PASSWORD}@mon.openfront.io/loki/loki/api/v1/push" \
|
||||
--log-opt loki-batch-size="400" \
|
||||
--log-opt loki-external-labels="job=docker,environment=${ENV},host=${REGION},region=${REGION}" \
|
||||
--env GAME_ENV=${ENV} \
|
||||
--env REGION=${REGION} \
|
||||
--log-opt loki-external-labels="job=docker,environment=${ENV},host=${HOST},subdomain=${SUBDOMAIN}" \
|
||||
--env-file /home/openfront/.env \
|
||||
--name ${CONTAINER_NAME} \
|
||||
$FULL_IMAGE_NAME
|
||||
$DOCKER_IMAGE
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Update complete! New ${REGION} container is running."
|
||||
echo "Update complete! New ${CONTAINER_NAME} container is running."
|
||||
|
||||
# Final cleanup after successful deployment
|
||||
echo "Performing final cleanup of unused Docker resources..."
|
||||
|
||||
Reference in New Issue
Block a user