Adding units

This commit is contained in:
hz
2026-05-18 15:30:46 -05:00
parent 6366ca46c6
commit 37a46a7b36
10 changed files with 529 additions and 3 deletions
@@ -2,6 +2,7 @@ extends Control
const SESSION_PATH := "user://auth_session.json" const SESSION_PATH := "user://auth_session.json"
const LOCATIONS_BASE_URL := "https://tloc.ranaze.com" const LOCATIONS_BASE_URL := "https://tloc.ranaze.com"
const RESOURCE_REFRESH_SECONDS := 15.0
const HexGridView := preload("res://scenes/game/hex_grid_view.gd") const HexGridView := preload("res://scenes/game/hex_grid_view.gd")
var coordinate_label: Label var coordinate_label: Label
@@ -10,10 +11,23 @@ var grid_view
var http_request: HTTPRequest var http_request: HTTPRequest
var visible_request: HTTPRequest var visible_request: HTTPRequest
var resources_request: HTTPRequest var resources_request: HTTPRequest
var units_request: HTTPRequest
var train_unit_request: HTTPRequest
var resources_timer: Timer
var session: Dictionary = {} var session: Dictionary = {}
var escape_overlay: Control var escape_overlay: Control
var escape_panel: PanelContainer var escape_panel: PanelContainer
var gold_label: Label var gold_label: Label
var tick_bar: ProgressBar
var tick_label: Label
var gold_tick_anchor_unix := 0.0
var gold_tick_seconds := 60.0
var train_button: Button
var unit_label: Label
var training_label: Label
var training_bar: ProgressBar
var training_started_unix := 0.0
var training_completes_unix := 0.0
func _ready() -> void: func _ready() -> void:
@@ -22,6 +36,11 @@ func _ready() -> void:
_assign_home_location() _assign_home_location()
func _process(_delta: float) -> void:
_update_tick_bar()
_update_training_bar()
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"): if event.is_action_pressed("ui_cancel"):
_toggle_escape_menu() _toggle_escape_menu()
@@ -91,6 +110,27 @@ func _build_ui() -> void:
gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32)) gold_label.add_theme_color_override("font_color", Color(0.95, 0.78, 0.32))
top_content.add_child(gold_label) top_content.add_child(gold_label)
var tick_box := VBoxContainer.new()
tick_box.custom_minimum_size = Vector2(130, 0)
tick_box.add_theme_constant_override("separation", 4)
top_content.add_child(tick_box)
tick_label = Label.new()
tick_label.text = "Next: --"
tick_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
tick_label.add_theme_font_size_override("font_size", 12)
tick_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78))
tick_box.add_child(tick_label)
tick_bar = ProgressBar.new()
tick_bar.custom_minimum_size = Vector2(130, 8)
tick_bar.max_value = 1.0
tick_bar.value = 0.0
tick_bar.show_percentage = false
tick_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26)))
tick_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.82, 0.65, 0.2), Color(0.9, 0.78, 0.36)))
tick_box.add_child(tick_bar)
var user_label := Label.new() var user_label := Label.new()
user_label.text = _session_name() user_label.text = _session_name()
user_label.add_theme_font_size_override("font_size", 15) user_label.add_theme_font_size_override("font_size", 15)
@@ -123,11 +163,48 @@ func _build_ui() -> void:
bottom_margin.add_theme_constant_override("margin_bottom", 8) bottom_margin.add_theme_constant_override("margin_bottom", 8)
bottom_bar.add_child(bottom_margin) bottom_bar.add_child(bottom_margin)
var bottom_content := HBoxContainer.new()
bottom_content.add_theme_constant_override("separation", 16)
bottom_margin.add_child(bottom_content)
coordinate_label = Label.new() coordinate_label = Label.new()
coordinate_label.text = "Drag to pan. Mouse wheel zooms. Select a tile to inspect territory." coordinate_label.text = "Drag to pan. Mouse wheel zooms. Select a tile to inspect territory."
coordinate_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
coordinate_label.add_theme_font_size_override("font_size", 14) coordinate_label.add_theme_font_size_override("font_size", 14)
coordinate_label.add_theme_color_override("font_color", Color(0.72, 0.8, 0.78)) coordinate_label.add_theme_color_override("font_color", Color(0.72, 0.8, 0.78))
bottom_margin.add_child(coordinate_label) bottom_content.add_child(coordinate_label)
unit_label = Label.new()
unit_label.text = "Scouts: --"
unit_label.custom_minimum_size = Vector2(88, 0)
unit_label.add_theme_font_size_override("font_size", 14)
unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78))
bottom_content.add_child(unit_label)
var training_box := VBoxContainer.new()
training_box.custom_minimum_size = Vector2(150, 0)
training_box.add_theme_constant_override("separation", 4)
bottom_content.add_child(training_box)
training_label = Label.new()
training_label.text = "Training: idle"
training_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
training_label.add_theme_font_size_override("font_size", 12)
training_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78))
training_box.add_child(training_label)
training_bar = ProgressBar.new()
training_bar.custom_minimum_size = Vector2(150, 8)
training_bar.max_value = 1.0
training_bar.value = 0.0
training_bar.show_percentage = false
training_bar.add_theme_stylebox_override("background", _progress_style(Color(0.035, 0.055, 0.055), Color(0.18, 0.28, 0.26)))
training_bar.add_theme_stylebox_override("fill", _progress_style(Color(0.28, 0.62, 0.72), Color(0.43, 0.82, 0.92)))
training_box.add_child(training_bar)
train_button = _small_button("Train Scout 50g")
train_button.pressed.connect(_train_scout)
bottom_content.add_child(train_button)
http_request = HTTPRequest.new() http_request = HTTPRequest.new()
http_request.request_completed.connect(_on_home_request_completed) http_request.request_completed.connect(_on_home_request_completed)
@@ -141,6 +218,20 @@ func _build_ui() -> void:
resources_request.request_completed.connect(_on_resources_request_completed) resources_request.request_completed.connect(_on_resources_request_completed)
add_child(resources_request) add_child(resources_request)
units_request = HTTPRequest.new()
units_request.request_completed.connect(_on_units_request_completed)
add_child(units_request)
train_unit_request = HTTPRequest.new()
train_unit_request.request_completed.connect(_on_train_unit_request_completed)
add_child(train_unit_request)
resources_timer = Timer.new()
resources_timer.wait_time = RESOURCE_REFRESH_SECONDS
resources_timer.autostart = false
resources_timer.timeout.connect(_refresh_player_state)
add_child(resources_timer)
_build_escape_menu() _build_escape_menu()
@@ -259,7 +350,13 @@ func _on_visible_request_completed(result: int, response_code: int, _headers: Pa
var visible_locations: Array = parsed var visible_locations: Array = parsed
grid_view.set_visible_locations(visible_locations, username) grid_view.set_visible_locations(visible_locations, username)
coordinate_label.text = "Visible territory loaded. Sight extends 3 hexes beyond controlled land." coordinate_label.text = "Visible territory loaded. Sight extends 3 hexes beyond controlled land."
_refresh_player_state()
resources_timer.start()
func _refresh_player_state() -> void:
_load_resources() _load_resources()
_load_units()
func _load_resources() -> void: func _load_resources() -> void:
@@ -268,6 +365,9 @@ func _load_resources() -> void:
gold_label.text = "Gold: --" gold_label.text = "Gold: --"
return return
if resources_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
var error: int = resources_request.request( var error: int = resources_request.request(
LOCATIONS_BASE_URL + "/api/resources/me", LOCATIONS_BASE_URL + "/api/resources/me",
PackedStringArray([ PackedStringArray([
@@ -294,6 +394,170 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
var gold_per_minute: int = int(resources.get("goldPerMinute", 0)) var gold_per_minute: int = int(resources.get("goldPerMinute", 0))
gold_label.text = "Gold: %d (+%d/min)" % [gold, gold_per_minute] gold_label.text = "Gold: %d (+%d/min)" % [gold, gold_per_minute]
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text)
if gold_tick_anchor_unix <= 0.0:
gold_tick_anchor_unix = Time.get_unix_time_from_system()
_update_tick_bar()
func _load_units() -> void:
var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty():
unit_label.text = "Scouts: --"
return
if units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
var error: int = units_request.request(
LOCATIONS_BASE_URL + "/api/units/me",
PackedStringArray([
"Authorization: Bearer %s" % access_token
]),
HTTPClient.METHOD_GET
)
if error != OK:
unit_label.text = "Scouts: ?"
func _on_units_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
unit_label.text = "Scouts: ?"
return
var parsed = JSON.parse_string(body.get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY:
unit_label.text = "Scouts: ?"
return
var units: Dictionary = parsed
unit_label.text = "Scouts: %d" % int(units.get("completedScouts", 0))
var training: Array = []
var raw_training = units.get("training", [])
if typeof(raw_training) == TYPE_ARRAY:
training = raw_training
if training.is_empty():
training_started_unix = 0.0
training_completes_unix = 0.0
_update_training_bar()
return
if typeof(training[0]) != TYPE_DICTIONARY:
return
var active_training: Dictionary = training[0]
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
_update_training_bar()
func _train_scout() -> void:
var access_token: String = str(session.get("accessToken", ""))
if access_token.is_empty():
coordinate_label.text = "No auth token available. Scout training was not started."
return
if train_unit_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return
train_button.disabled = true
coordinate_label.text = "Starting scout training..."
var error: int = train_unit_request.request(
LOCATIONS_BASE_URL + "/api/units/train",
PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % access_token
]),
HTTPClient.METHOD_POST,
JSON.stringify({ "unitType": "Scout" })
)
if error != OK:
train_button.disabled = false
coordinate_label.text = "Could not contact the unit training service."
func _on_train_unit_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
train_button.disabled = false
var text: String = body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS:
coordinate_label.text = "Scout training request failed."
return
if response_code < 200 or response_code >= 300:
coordinate_label.text = "Scout training failed: %s" % text
_refresh_player_state()
return
var parsed = JSON.parse_string(text)
if typeof(parsed) == TYPE_DICTIONARY:
var training: Dictionary = parsed
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
training_completes_unix = _parse_utc_timestamp(str(training.get("completesAtUtc", "")))
coordinate_label.text = "Scout training started. Completion takes 1 minute."
_refresh_player_state()
func _update_tick_bar() -> void:
if not tick_bar or not tick_label:
return
if gold_tick_anchor_unix <= 0.0:
tick_bar.value = 0.0
tick_label.text = "Next: --"
return
var elapsed: float = max(0.0, Time.get_unix_time_from_system() - gold_tick_anchor_unix)
var tick_progress: float = fposmod(elapsed, gold_tick_seconds) / gold_tick_seconds
var seconds_remaining: int = int(ceil(gold_tick_seconds - fposmod(elapsed, gold_tick_seconds)))
if seconds_remaining >= int(gold_tick_seconds):
seconds_remaining = int(gold_tick_seconds)
tick_bar.value = tick_progress
tick_label.text = "Next: %ds" % seconds_remaining
func _update_training_bar() -> void:
if not training_bar or not training_label:
return
if training_started_unix <= 0.0 or training_completes_unix <= 0.0:
training_bar.value = 0.0
training_label.text = "Training: idle"
return
var now: float = Time.get_unix_time_from_system()
var duration: float = max(1.0, training_completes_unix - training_started_unix)
var elapsed: float = clamp(now - training_started_unix, 0.0, duration)
var remaining: int = int(max(0, int(ceil(training_completes_unix - now))))
training_bar.value = elapsed / duration
if remaining <= 0:
training_label.text = "Training: ready"
else:
training_label.text = "Training: %ds" % remaining
func _parse_utc_timestamp(timestamp: String) -> float:
if timestamp.is_empty():
return 0.0
var normalized := timestamp.strip_edges()
var timezone_index := normalized.find("+")
if timezone_index > -1:
normalized = normalized.substr(0, timezone_index)
if normalized.ends_with("Z"):
normalized = normalized.substr(0, normalized.length() - 1)
var fractional_index := normalized.find(".")
if fractional_index > -1:
normalized = normalized.substr(0, fractional_index)
normalized = normalized.replace("T", " ")
return Time.get_unix_time_from_datetime_string(normalized)
func _build_escape_menu() -> void: func _build_escape_menu() -> void:
escape_overlay = Control.new() escape_overlay = Control.new()
@@ -393,6 +657,13 @@ func _menu_button(text_value: String) -> Button:
return button return button
func _small_button(text_value: String) -> Button:
var button := _menu_button(text_value)
button.custom_minimum_size = Vector2(128, 32)
button.add_theme_font_size_override("font_size", 13)
return button
func _find_first_button(node: Node) -> Button: func _find_first_button(node: Node) -> Button:
if node is Button: if node is Button:
return node return node
@@ -421,6 +692,15 @@ func _button_style(color: Color) -> StyleBoxFlat:
return style return style
func _progress_style(color: Color, border_color: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = border_color
style.set_border_width_all(1)
style.set_corner_radius_all(3)
return style
func _legend_item(color: Color, label_text: String) -> HBoxContainer: func _legend_item(color: Color, label_text: String) -> HBoxContainer:
var item := HBoxContainer.new() var item := HBoxContainer.new()
item.add_theme_constant_override("separation", 5) item.add_theme_constant_override("separation", 5)
@@ -0,0 +1,58 @@
using LocationsApi.Models;
using LocationsApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace LocationsApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UnitsController : ControllerBase
{
private readonly UnitTrainingService _units;
public UnitsController(UnitTrainingService units)
{
_units = units;
}
[Authorize(Roles = "USER,SUPER")]
[HttpGet("me")]
public async Task<ActionResult<PlayerUnitsResponse>> GetMine()
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
return Ok(await _units.GetMineAsync(username));
}
[Authorize(Roles = "USER,SUPER")]
[HttpPost("train")]
public async Task<ActionResult<UnitTrainingResponse>> Train([FromBody] TrainUnitRequest request)
{
var username = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrWhiteSpace(username))
{
return Unauthorized();
}
try
{
var training = await _units.TrainAsync(username, request.UnitType);
if (training is null)
{
return BadRequest(new { error = "Not enough gold." });
}
return Ok(UnitTrainingResponse.FromTraining(training));
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
@@ -0,0 +1,36 @@
namespace LocationsApi.Models;
public class TrainUnitRequest
{
public string UnitType { get; set; } = "Scout";
}
public class UnitTrainingResponse
{
public string Id { get; set; } = "";
public string Username { get; set; } = "";
public string UnitType { get; set; } = "";
public int GoldCost { get; set; }
public string Status { get; set; } = "";
public DateTime StartedAtUtc { get; set; }
public DateTime CompletesAtUtc { get; set; }
public DateTime? CompletedAtUtc { get; set; }
public static UnitTrainingResponse FromTraining(UnitTraining training) => new()
{
Id = training.Id,
Username = training.Username,
UnitType = training.UnitType,
GoldCost = training.GoldCost,
Status = training.Status,
StartedAtUtc = training.StartedAtUtc,
CompletesAtUtc = training.CompletesAtUtc,
CompletedAtUtc = training.CompletedAtUtc
};
}
public class PlayerUnitsResponse
{
public int CompletedScouts { get; set; }
public List<UnitTrainingResponse> Training { get; set; } = [];
}
@@ -0,0 +1,21 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace LocationsApi.Models;
public class UnitTraining
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = "";
public string Username { get; set; } = "";
public string UnitType { get; set; } = "Scout";
public int GoldCost { get; set; } = 50;
public string Status { get; set; } = "Training";
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime CompletesAtUtc { get; set; } = DateTime.UtcNow.AddMinutes(1);
public DateTime? CompletedAtUtc { get; set; }
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
}
+1
View File
@@ -9,6 +9,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddSingleton<LocationService>(); builder.Services.AddSingleton<LocationService>();
builder.Services.AddSingleton<ResourceService>(); builder.Services.AddSingleton<ResourceService>();
builder.Services.AddSingleton<UnitTrainingService>();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => builder.Services.AddSwaggerGen(c =>
{ {
+8
View File
@@ -14,6 +14,8 @@ Tracks hex locations for the territory map.
- `GET /api/locations/visible?depth=3` Override visibility depth for a request. - `GET /api/locations/visible?depth=3` Override visibility depth for a request.
- `PUT /api/locations` Upsert location data (SUPER only). - `PUT /api/locations` Upsert location data (SUPER only).
- `GET /api/resources/me` Get the authenticated player's resources. - `GET /api/resources/me` Get the authenticated player's resources.
- `GET /api/units/me` Get completed units and active training for the authenticated player.
- `POST /api/units/train` Spend gold and start training a unit.
`claim` and `put` require the same JWT issued by AuthApi. `claim` and `put` require the same JWT issued by AuthApi.
@@ -26,3 +28,9 @@ locations are unowned `Open` records until claimed by a player.
Player resource state is stored server-side in the `PlayerResources` collection. Player resource state is stored server-side in the `PlayerResources` collection.
New players start with 100 gold. A player's headquarters generates 10 gold per New players start with 100 gold. A player's headquarters generates 10 gold per
minute. Generation is applied lazily when the resource record is read. minute. Generation is applied lazily when the resource record is read.
## Units
Unit training is stored server-side in the `UnitTraining` collection. The first
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
Finished training is completed lazily when unit state is read.
@@ -51,8 +51,31 @@ public class ResourceService
public async Task<PlayerResources> GetAsync(string username) public async Task<PlayerResources> GetAsync(string username)
{ {
var resources = await EnsureAsync(username); return await EnsureAsync(username);
return await ApplyGoldGenerationAsync(resources); }
public async Task<bool> TrySpendGoldAsync(string username, int amount)
{
if (amount <= 0)
{
return true;
}
await EnsureAsync(username);
var update = Builders<PlayerResources>.Update
.Inc(x => x.Gold, -amount)
.Set(x => x.UpdatedAtUtc, DateTime.UtcNow);
var updated = await _resources.FindOneAndUpdateAsync(
x => x.Username == username && x.Gold >= amount,
update,
new FindOneAndUpdateOptions<PlayerResources>
{
ReturnDocument = ReturnDocument.After
});
return updated is not null;
} }
private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources) private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources)
@@ -0,0 +1,93 @@
using LocationsApi.Models;
using MongoDB.Driver;
namespace LocationsApi.Services;
public class UnitTrainingService
{
private const string ScoutUnitType = "Scout";
private readonly IMongoCollection<UnitTraining> _training;
private readonly ResourceService _resources;
private readonly int _scoutGoldCost;
private readonly int _scoutTrainingSeconds;
public UnitTrainingService(IConfiguration cfg, ResourceService resources)
{
var connectionString = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
var databaseName = cfg["MongoDB:DatabaseName"] ?? "territory-autobattler";
var client = new MongoClient(connectionString);
var database = client.GetDatabase(databaseName);
_training = database.GetCollection<UnitTraining>("UnitTraining");
_resources = resources;
_scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50;
_scoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds)
? Math.Max(1, scoutTrainingSeconds)
: 60;
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
}
public async Task<PlayerUnitsResponse> GetMineAsync(string username)
{
await CompleteFinishedTrainingAsync(username);
var all = await _training.Find(x => x.Username == username).ToListAsync();
var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
var activeTraining = all
.Where(x => x.Status == "Training")
.OrderBy(x => x.CompletesAtUtc)
.Select(UnitTrainingResponse.FromTraining)
.ToList();
return new PlayerUnitsResponse
{
CompletedScouts = completedScouts,
Training = activeTraining
};
}
public async Task<UnitTraining?> TrainAsync(string username, string unitType)
{
if (!string.Equals(unitType, ScoutUnitType, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Unsupported unit type.", nameof(unitType));
}
var spent = await _resources.TrySpendGoldAsync(username, _scoutGoldCost);
if (!spent)
{
return null;
}
var now = DateTime.UtcNow;
var training = new UnitTraining
{
Username = username,
UnitType = ScoutUnitType,
GoldCost = _scoutGoldCost,
Status = "Training",
StartedAtUtc = now,
CompletesAtUtc = now.AddSeconds(_scoutTrainingSeconds),
CreatedAtUtc = now,
UpdatedAtUtc = now
};
await _training.InsertOneAsync(training);
return training;
}
private async Task CompleteFinishedTrainingAsync(string username)
{
var now = DateTime.UtcNow;
var update = Builders<UnitTraining>.Update
.Set(x => x.Status, "Completed")
.Set(x => x.CompletedAtUtc, now)
.Set(x => x.UpdatedAtUtc, now);
await _training.UpdateManyAsync(
x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now,
update);
}
}
@@ -23,5 +23,9 @@
"StartingGold": 100, "StartingGold": 100,
"HeadquartersGoldPerMinute": 10 "HeadquartersGoldPerMinute": 10
}, },
"Units": {
"ScoutGoldCost": 50,
"ScoutTrainingSeconds": 60
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }
@@ -13,3 +13,5 @@ data:
World__VisibilityDepth: "3" World__VisibilityDepth: "3"
Resources__StartingGold: "100" Resources__StartingGold: "100"
Resources__HeadquartersGoldPerMinute: "10" Resources__HeadquartersGoldPerMinute: "10"
Units__ScoutGoldCost: "50"
Units__ScoutTrainingSeconds: "60"