adding resource limits and archers
This commit is contained in:
@@ -31,6 +31,7 @@ var resource_text := "Gold: --\nWood: --\nOre: --"
|
|||||||
var gold_tick_anchor_unix := 0.0
|
var gold_tick_anchor_unix := 0.0
|
||||||
var gold_tick_seconds := 60.0
|
var gold_tick_seconds := 60.0
|
||||||
var train_button: Button
|
var train_button: Button
|
||||||
|
var train_archer_button: Button
|
||||||
var unit_label: Label
|
var unit_label: Label
|
||||||
var training_box: VBoxContainer
|
var training_box: VBoxContainer
|
||||||
var training_label: Label
|
var training_label: Label
|
||||||
@@ -47,6 +48,7 @@ var building_started_unix := 0.0
|
|||||||
var building_completes_unix := 0.0
|
var building_completes_unix := 0.0
|
||||||
var building_completion_refresh_requested := false
|
var building_completion_refresh_requested := false
|
||||||
var active_building_label := "Build"
|
var active_building_label := "Build"
|
||||||
|
var active_training_label := "Training"
|
||||||
var claim_button: Button
|
var claim_button: Button
|
||||||
var build_log_camp_button: Button
|
var build_log_camp_button: Button
|
||||||
var build_mine_button: Button
|
var build_mine_button: Button
|
||||||
@@ -57,8 +59,10 @@ var selected_can_claim := false
|
|||||||
var selected_can_build_log_camp := false
|
var selected_can_build_log_camp := false
|
||||||
var selected_can_build_mine := false
|
var selected_can_build_mine := false
|
||||||
var selected_can_build_barracks := false
|
var selected_can_build_barracks := false
|
||||||
|
var selected_can_train_archer := false
|
||||||
var selected_is_home := false
|
var selected_is_home := false
|
||||||
var completed_scout_count := 0
|
var completed_scout_count := 0
|
||||||
|
var completed_archer_count := 0
|
||||||
var refresh_prompt_active := false
|
var refresh_prompt_active := false
|
||||||
var refresh_request_active := false
|
var refresh_request_active := false
|
||||||
|
|
||||||
@@ -202,6 +206,11 @@ func _build_ui() -> void:
|
|||||||
train_button.pressed.connect(_train_scout)
|
train_button.pressed.connect(_train_scout)
|
||||||
action_buttons.add_child(train_button)
|
action_buttons.add_child(train_button)
|
||||||
|
|
||||||
|
train_archer_button = _small_button("Archer 100g 100w")
|
||||||
|
train_archer_button.visible = false
|
||||||
|
train_archer_button.pressed.connect(_train_archer)
|
||||||
|
action_buttons.add_child(train_archer_button)
|
||||||
|
|
||||||
claim_button = _small_button("Claim 1 Scout")
|
claim_button = _small_button("Claim 1 Scout")
|
||||||
claim_button.visible = false
|
claim_button.visible = false
|
||||||
claim_button.pressed.connect(_claim_selected_tile)
|
claim_button.pressed.connect(_claim_selected_tile)
|
||||||
@@ -223,8 +232,8 @@ func _build_ui() -> void:
|
|||||||
action_buttons.add_child(build_barracks_button)
|
action_buttons.add_child(build_barracks_button)
|
||||||
|
|
||||||
unit_label = Label.new()
|
unit_label = Label.new()
|
||||||
unit_label.text = "Scouts: -- | Queue: --"
|
unit_label.text = "Scouts: -- | Archers: -- | Queue: --"
|
||||||
unit_label.custom_minimum_size = Vector2(126, 0)
|
unit_label.custom_minimum_size = Vector2(180, 0)
|
||||||
unit_label.add_theme_font_size_override("font_size", 14)
|
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))
|
unit_label.add_theme_color_override("font_color", Color(0.72, 0.82, 0.78))
|
||||||
status_row.add_child(unit_label)
|
status_row.add_child(unit_label)
|
||||||
@@ -408,6 +417,7 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
|||||||
selected_can_build_log_camp = _can_build_log_camp_on_selected_tile()
|
selected_can_build_log_camp = _can_build_log_camp_on_selected_tile()
|
||||||
selected_can_build_mine = _can_build_mine_on_selected_tile()
|
selected_can_build_mine = _can_build_mine_on_selected_tile()
|
||||||
selected_can_build_barracks = _can_build_barracks_on_selected_tile()
|
selected_can_build_barracks = _can_build_barracks_on_selected_tile()
|
||||||
|
selected_can_train_archer = _selected_tile_has_built_barracks()
|
||||||
_update_action_buttons()
|
_update_action_buttons()
|
||||||
if coordinate_label:
|
if coordinate_label:
|
||||||
coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain]
|
coordinate_label.text = "Selected q:%d r:%d | %s territory" % [q, r, terrain]
|
||||||
@@ -449,6 +459,9 @@ func _update_action_buttons() -> void:
|
|||||||
if train_button:
|
if train_button:
|
||||||
train_button.visible = selected_is_home
|
train_button.visible = selected_is_home
|
||||||
train_button.disabled = false
|
train_button.disabled = false
|
||||||
|
if train_archer_button:
|
||||||
|
train_archer_button.visible = selected_can_train_archer
|
||||||
|
train_archer_button.disabled = false
|
||||||
if claim_button:
|
if claim_button:
|
||||||
claim_button.visible = _can_claim_selected_tile()
|
claim_button.visible = _can_claim_selected_tile()
|
||||||
claim_button.disabled = false
|
claim_button.disabled = false
|
||||||
@@ -477,6 +490,21 @@ func _selected_tile_is_home() -> bool:
|
|||||||
return owner_username == username and kind == "Home"
|
return owner_username == username and kind == "Home"
|
||||||
|
|
||||||
|
|
||||||
|
func _selected_tile_has_built_barracks() -> bool:
|
||||||
|
if not grid_view:
|
||||||
|
return false
|
||||||
|
|
||||||
|
var location: Dictionary = grid_view.get_visible_location(selected_q, selected_r)
|
||||||
|
if location.is_empty():
|
||||||
|
return false
|
||||||
|
|
||||||
|
var username: String = str(session.get("username", ""))
|
||||||
|
var owner_username: String = str(location.get("ownerUsername", ""))
|
||||||
|
var building_type: String = _nullable_string(location.get("buildingType", ""))
|
||||||
|
var building_status: String = _nullable_string(location.get("buildingStatus", ""))
|
||||||
|
return owner_username == username and building_type == "Barracks" and (building_status == "Built" or building_status.is_empty())
|
||||||
|
|
||||||
|
|
||||||
func _session_name() -> String:
|
func _session_name() -> String:
|
||||||
if not session.is_empty():
|
if not session.is_empty():
|
||||||
var session_username: String = str(session.get("username", "Signed in"))
|
var session_username: String = str(session.get("username", "Signed in"))
|
||||||
@@ -720,11 +748,24 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
|
|||||||
var resources: Dictionary = parsed
|
var resources: Dictionary = parsed
|
||||||
var gold: int = int(resources.get("gold", 0))
|
var gold: int = int(resources.get("gold", 0))
|
||||||
var gold_per_minute: int = int(resources.get("goldPerMinute", 0))
|
var gold_per_minute: int = int(resources.get("goldPerMinute", 0))
|
||||||
|
var gold_capacity: int = int(resources.get("goldCapacity", 500))
|
||||||
var wood: int = int(resources.get("wood", 0))
|
var wood: int = int(resources.get("wood", 0))
|
||||||
var wood_per_minute: int = int(resources.get("woodPerMinute", 0))
|
var wood_per_minute: int = int(resources.get("woodPerMinute", 0))
|
||||||
|
var wood_capacity: int = int(resources.get("woodCapacity", 100))
|
||||||
var ore: int = int(resources.get("ore", 0))
|
var ore: int = int(resources.get("ore", 0))
|
||||||
var ore_per_minute: int = int(resources.get("orePerMinute", 0))
|
var ore_per_minute: int = int(resources.get("orePerMinute", 0))
|
||||||
resource_text = "Gold: %d (+%d/min)\nWood: %d (+%d/min)\nOre: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute, ore, ore_per_minute]
|
var ore_capacity: int = int(resources.get("oreCapacity", 100))
|
||||||
|
resource_text = "Gold: %d/%d (+%d/min)\nWood: %d/%d (+%d/min)\nOre: %d/%d (+%d/min)" % [
|
||||||
|
gold,
|
||||||
|
gold_capacity,
|
||||||
|
gold_per_minute,
|
||||||
|
wood,
|
||||||
|
wood_capacity,
|
||||||
|
wood_per_minute,
|
||||||
|
ore,
|
||||||
|
ore_capacity,
|
||||||
|
ore_per_minute
|
||||||
|
]
|
||||||
gold_label.text = resource_text
|
gold_label.text = resource_text
|
||||||
|
|
||||||
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
|
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
|
||||||
@@ -737,7 +778,7 @@ func _on_resources_request_completed(result: int, response_code: int, _headers:
|
|||||||
func _load_units() -> void:
|
func _load_units() -> void:
|
||||||
var access_token: String = str(session.get("accessToken", ""))
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
if access_token.is_empty():
|
if access_token.is_empty():
|
||||||
unit_label.text = "Scouts: --"
|
unit_label.text = "Scouts: -- | Archers: -- | Queue: --"
|
||||||
return
|
return
|
||||||
|
|
||||||
if units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
if units_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||||
@@ -756,12 +797,12 @@ func _load_units() -> void:
|
|||||||
|
|
||||||
func _on_units_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
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:
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
unit_label.text = "Scouts: ?"
|
unit_label.text = "Scouts: ? | Archers: ? | Queue: ?"
|
||||||
return
|
return
|
||||||
|
|
||||||
var parsed = JSON.parse_string(body.get_string_from_utf8())
|
var parsed = JSON.parse_string(body.get_string_from_utf8())
|
||||||
if typeof(parsed) != TYPE_DICTIONARY:
|
if typeof(parsed) != TYPE_DICTIONARY:
|
||||||
unit_label.text = "Scouts: ?"
|
unit_label.text = "Scouts: ? | Archers: ? | Queue: ?"
|
||||||
return
|
return
|
||||||
|
|
||||||
var units: Dictionary = parsed
|
var units: Dictionary = parsed
|
||||||
@@ -771,12 +812,14 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
|||||||
training = raw_training
|
training = raw_training
|
||||||
|
|
||||||
completed_scout_count = int(units.get("completedScouts", 0))
|
completed_scout_count = int(units.get("completedScouts", 0))
|
||||||
unit_label.text = "Scouts: %d | Queue: %d" % [completed_scout_count, training.size()]
|
completed_archer_count = int(units.get("completedArchers", 0))
|
||||||
|
unit_label.text = "Scouts: %d | Archers: %d | Queue: %d" % [completed_scout_count, completed_archer_count, training.size()]
|
||||||
_update_action_buttons()
|
_update_action_buttons()
|
||||||
|
|
||||||
if training.is_empty():
|
if training.is_empty():
|
||||||
training_started_unix = 0.0
|
training_started_unix = 0.0
|
||||||
training_completes_unix = 0.0
|
training_completes_unix = 0.0
|
||||||
|
active_training_label = "Training"
|
||||||
training_completion_refresh_requested = false
|
training_completion_refresh_requested = false
|
||||||
_update_training_bar()
|
_update_training_bar()
|
||||||
return
|
return
|
||||||
@@ -785,6 +828,7 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
|||||||
return
|
return
|
||||||
|
|
||||||
var active_training: Dictionary = training[0]
|
var active_training: Dictionary = training[0]
|
||||||
|
active_training_label = _unit_display_name(str(active_training.get("unitType", "Training")))
|
||||||
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
|
training_started_unix = _parse_utc_timestamp(str(active_training.get("startedAtUtc", "")))
|
||||||
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
|
training_completes_unix = _parse_utc_timestamp(str(active_training.get("completesAtUtc", "")))
|
||||||
training_completion_refresh_requested = false
|
training_completion_refresh_requested = false
|
||||||
@@ -792,16 +836,31 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
|
|||||||
|
|
||||||
|
|
||||||
func _train_scout() -> void:
|
func _train_scout() -> void:
|
||||||
|
_request_unit_training("Scout")
|
||||||
|
|
||||||
|
|
||||||
|
func _train_archer() -> void:
|
||||||
|
if not selected_can_train_archer:
|
||||||
|
coordinate_label.text = "Select a completed Barracks to train Archers."
|
||||||
|
return
|
||||||
|
|
||||||
|
_request_unit_training("Archer")
|
||||||
|
|
||||||
|
|
||||||
|
func _request_unit_training(unit_type: String) -> void:
|
||||||
var access_token: String = str(session.get("accessToken", ""))
|
var access_token: String = str(session.get("accessToken", ""))
|
||||||
if access_token.is_empty():
|
if access_token.is_empty():
|
||||||
coordinate_label.text = "No auth token available. Scout training was not started."
|
coordinate_label.text = "No auth token available. %s training was not started." % unit_type
|
||||||
return
|
return
|
||||||
|
|
||||||
if train_unit_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
if train_unit_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if unit_type == "Archer":
|
||||||
|
train_archer_button.disabled = true
|
||||||
|
else:
|
||||||
train_button.disabled = true
|
train_button.disabled = true
|
||||||
coordinate_label.text = "Starting scout training..."
|
coordinate_label.text = "Starting %s training..." % unit_type
|
||||||
var error: int = train_unit_request.request(
|
var error: int = train_unit_request.request(
|
||||||
LOCATIONS_BASE_URL + "/api/units/train",
|
LOCATIONS_BASE_URL + "/api/units/train",
|
||||||
PackedStringArray([
|
PackedStringArray([
|
||||||
@@ -809,32 +868,33 @@ func _train_scout() -> void:
|
|||||||
"Authorization: Bearer %s" % access_token
|
"Authorization: Bearer %s" % access_token
|
||||||
]),
|
]),
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
JSON.stringify({ "unitType": "Scout" })
|
JSON.stringify({ "unitType": unit_type })
|
||||||
)
|
)
|
||||||
if error != OK:
|
if error != OK:
|
||||||
train_button.disabled = false
|
_update_action_buttons()
|
||||||
coordinate_label.text = "Could not contact the unit training service."
|
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:
|
func _on_train_unit_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
train_button.disabled = false
|
_update_action_buttons()
|
||||||
var text: String = body.get_string_from_utf8()
|
var text: String = body.get_string_from_utf8()
|
||||||
if result != HTTPRequest.RESULT_SUCCESS:
|
if result != HTTPRequest.RESULT_SUCCESS:
|
||||||
coordinate_label.text = "Scout training request failed."
|
coordinate_label.text = "Unit training request failed."
|
||||||
return
|
return
|
||||||
|
|
||||||
if response_code < 200 or response_code >= 300:
|
if response_code < 200 or response_code >= 300:
|
||||||
coordinate_label.text = "Scout training failed: %s" % text
|
coordinate_label.text = "Unit training failed: %s" % text
|
||||||
_refresh_player_state()
|
_refresh_player_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
var parsed = JSON.parse_string(text)
|
var parsed = JSON.parse_string(text)
|
||||||
if typeof(parsed) == TYPE_DICTIONARY:
|
if typeof(parsed) == TYPE_DICTIONARY:
|
||||||
var training: Dictionary = parsed
|
var training: Dictionary = parsed
|
||||||
|
active_training_label = _unit_display_name(str(training.get("unitType", "Training")))
|
||||||
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
|
training_started_unix = _parse_utc_timestamp(str(training.get("startedAtUtc", "")))
|
||||||
training_completes_unix = _parse_utc_timestamp(str(training.get("completesAtUtc", "")))
|
training_completes_unix = _parse_utc_timestamp(str(training.get("completesAtUtc", "")))
|
||||||
training_completion_refresh_requested = false
|
training_completion_refresh_requested = false
|
||||||
coordinate_label.text = "Scout training started. Completion takes 1 minute."
|
coordinate_label.text = "%s training started. Completion takes 1 minute." % active_training_label
|
||||||
|
|
||||||
_refresh_player_state()
|
_refresh_player_state()
|
||||||
|
|
||||||
@@ -1018,12 +1078,12 @@ func _update_training_bar() -> void:
|
|||||||
training_bar.value = elapsed / duration
|
training_bar.value = elapsed / duration
|
||||||
|
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
training_label.text = "Training: ready"
|
training_label.text = "%s: ready" % active_training_label
|
||||||
if not training_completion_refresh_requested:
|
if not training_completion_refresh_requested:
|
||||||
training_completion_refresh_requested = true
|
training_completion_refresh_requested = true
|
||||||
_load_units()
|
_load_units()
|
||||||
else:
|
else:
|
||||||
training_label.text = "Training: %ds" % remaining
|
training_label.text = "%s: %ds" % [active_training_label, remaining]
|
||||||
_update_progress_panel_visibility()
|
_update_progress_panel_visibility()
|
||||||
|
|
||||||
|
|
||||||
@@ -1108,6 +1168,16 @@ func _building_display_name(building_type: String) -> String:
|
|||||||
return "Build"
|
return "Build"
|
||||||
|
|
||||||
|
|
||||||
|
func _unit_display_name(unit_type: String) -> String:
|
||||||
|
match unit_type:
|
||||||
|
"Scout":
|
||||||
|
return "Scout"
|
||||||
|
"Archer":
|
||||||
|
return "Archer"
|
||||||
|
_:
|
||||||
|
return "Training"
|
||||||
|
|
||||||
|
|
||||||
func _parse_utc_timestamp(timestamp: String) -> float:
|
func _parse_utc_timestamp(timestamp: String) -> float:
|
||||||
if timestamp.is_empty():
|
if timestamp.is_empty():
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class UnitsController : ControllerBase
|
|||||||
var training = await _units.TrainAsync(username, request.UnitType);
|
var training = await _units.TrainAsync(username, request.UnitType);
|
||||||
if (training is null)
|
if (training is null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { error = "Not enough gold." });
|
return BadRequest(new { error = "Not enough resources." });
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(UnitTrainingResponse.FromTraining(training));
|
return Ok(UnitTrainingResponse.FromTraining(training));
|
||||||
@@ -54,5 +54,9 @@ public class UnitsController : ControllerBase
|
|||||||
{
|
{
|
||||||
return BadRequest(new { error = ex.Message });
|
return BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ public class PlayerResources
|
|||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
public int Gold { get; set; } = 100;
|
public int Gold { get; set; } = 100;
|
||||||
public int GoldPerMinute { get; set; } = 10;
|
public int GoldPerMinute { get; set; } = 10;
|
||||||
|
public int GoldCapacity { get; set; } = 500;
|
||||||
public int Wood { get; set; }
|
public int Wood { get; set; }
|
||||||
public int WoodPerMinute { get; set; }
|
public int WoodPerMinute { get; set; }
|
||||||
|
public int WoodCapacity { get; set; } = 100;
|
||||||
public int Ore { get; set; }
|
public int Ore { get; set; }
|
||||||
public int OrePerMinute { get; set; }
|
public int OrePerMinute { get; set; }
|
||||||
|
public int OreCapacity { get; set; } = 100;
|
||||||
public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime LastGoldGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime LastOreGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime LastOreGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ public class PlayerResourcesResponse
|
|||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
public int Gold { get; set; }
|
public int Gold { get; set; }
|
||||||
public int GoldPerMinute { get; set; }
|
public int GoldPerMinute { get; set; }
|
||||||
|
public int GoldCapacity { get; set; }
|
||||||
public int Wood { get; set; }
|
public int Wood { get; set; }
|
||||||
public int WoodPerMinute { get; set; }
|
public int WoodPerMinute { get; set; }
|
||||||
|
public int WoodCapacity { get; set; }
|
||||||
public int Ore { get; set; }
|
public int Ore { get; set; }
|
||||||
public int OrePerMinute { get; set; }
|
public int OrePerMinute { get; set; }
|
||||||
|
public int OreCapacity { get; set; }
|
||||||
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
||||||
public DateTime LastWoodGeneratedAtUtc { get; set; }
|
public DateTime LastWoodGeneratedAtUtc { get; set; }
|
||||||
public DateTime LastOreGeneratedAtUtc { get; set; }
|
public DateTime LastOreGeneratedAtUtc { get; set; }
|
||||||
@@ -19,10 +22,13 @@ public class PlayerResourcesResponse
|
|||||||
Username = resources.Username,
|
Username = resources.Username,
|
||||||
Gold = resources.Gold,
|
Gold = resources.Gold,
|
||||||
GoldPerMinute = resources.GoldPerMinute,
|
GoldPerMinute = resources.GoldPerMinute,
|
||||||
|
GoldCapacity = resources.GoldCapacity,
|
||||||
Wood = resources.Wood,
|
Wood = resources.Wood,
|
||||||
WoodPerMinute = resources.WoodPerMinute,
|
WoodPerMinute = resources.WoodPerMinute,
|
||||||
|
WoodCapacity = resources.WoodCapacity,
|
||||||
Ore = resources.Ore,
|
Ore = resources.Ore,
|
||||||
OrePerMinute = resources.OrePerMinute,
|
OrePerMinute = resources.OrePerMinute,
|
||||||
|
OreCapacity = resources.OreCapacity,
|
||||||
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
||||||
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
|
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
|
||||||
LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc,
|
LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public class UnitTrainingResponse
|
|||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
public string UnitType { get; set; } = "";
|
public string UnitType { get; set; } = "";
|
||||||
public int GoldCost { get; set; }
|
public int GoldCost { get; set; }
|
||||||
|
public int WoodCost { get; set; }
|
||||||
|
public int OreCost { get; set; }
|
||||||
public string Status { get; set; } = "";
|
public string Status { get; set; } = "";
|
||||||
public DateTime StartedAtUtc { get; set; }
|
public DateTime StartedAtUtc { get; set; }
|
||||||
public DateTime CompletesAtUtc { get; set; }
|
public DateTime CompletesAtUtc { get; set; }
|
||||||
@@ -22,6 +24,8 @@ public class UnitTrainingResponse
|
|||||||
Username = training.Username,
|
Username = training.Username,
|
||||||
UnitType = training.UnitType,
|
UnitType = training.UnitType,
|
||||||
GoldCost = training.GoldCost,
|
GoldCost = training.GoldCost,
|
||||||
|
WoodCost = training.WoodCost,
|
||||||
|
OreCost = training.OreCost,
|
||||||
Status = training.Status,
|
Status = training.Status,
|
||||||
StartedAtUtc = training.StartedAtUtc,
|
StartedAtUtc = training.StartedAtUtc,
|
||||||
CompletesAtUtc = training.CompletesAtUtc,
|
CompletesAtUtc = training.CompletesAtUtc,
|
||||||
@@ -32,5 +36,6 @@ public class UnitTrainingResponse
|
|||||||
public class PlayerUnitsResponse
|
public class PlayerUnitsResponse
|
||||||
{
|
{
|
||||||
public int CompletedScouts { get; set; }
|
public int CompletedScouts { get; set; }
|
||||||
|
public int CompletedArchers { get; set; }
|
||||||
public List<UnitTrainingResponse> Training { get; set; } = [];
|
public List<UnitTrainingResponse> Training { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class UnitTraining
|
|||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
public string UnitType { get; set; } = "Scout";
|
public string UnitType { get; set; } = "Scout";
|
||||||
public int GoldCost { get; set; } = 50;
|
public int GoldCost { get; set; } = 50;
|
||||||
|
public int WoodCost { get; set; }
|
||||||
|
public int OreCost { get; set; }
|
||||||
public string Status { get; set; } = "Training";
|
public string Status { get; set; } = "Training";
|
||||||
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
|
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime CompletesAtUtc { get; set; } = DateTime.UtcNow.AddMinutes(1);
|
public DateTime CompletesAtUtc { get; set; } = DateTime.UtcNow.AddMinutes(1);
|
||||||
|
|||||||
@@ -28,16 +28,18 @@ 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. Log Camps generate wood and Mines generate ore. Generation is applied
|
minute. The starting town hall stores up to 500 gold, 100 wood, and 100 ore.
|
||||||
lazily when the resource record is read.
|
Log Camps generate wood and Mines generate ore. Generation is applied lazily
|
||||||
|
when the resource record is read and clamps at the current storage capacity.
|
||||||
|
|
||||||
## Units
|
## Units
|
||||||
|
|
||||||
Unit training is stored server-side in the `UnitTraining` collection. The first
|
Unit training is stored server-side in the `UnitTraining` collection. `Scout`
|
||||||
unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
|
costs 50 gold and takes 60 seconds to train. `Archer` requires a completed
|
||||||
Finished training is completed lazily when unit state is read. Claiming an
|
Barracks, costs 100 gold and 100 wood, and takes 60 seconds to train. Finished
|
||||||
unclaimed visible location consumes one completed Scout by deleting that Scout's
|
training is completed lazily when unit state is read. Claiming an unclaimed
|
||||||
training record.
|
visible location consumes one completed Scout by deleting that Scout's training
|
||||||
|
record.
|
||||||
|
|
||||||
## Buildings
|
## Buildings
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ Timer settings are stored in the MongoDB `GameSettings` collection and are
|
|||||||
seeded on first read:
|
seeded on first read:
|
||||||
|
|
||||||
- `ScoutTrainingSeconds`
|
- `ScoutTrainingSeconds`
|
||||||
|
- `ArcherTrainingSeconds`
|
||||||
- `LogCampBuildSeconds`
|
- `LogCampBuildSeconds`
|
||||||
- `MineBuildSeconds`
|
- `MineBuildSeconds`
|
||||||
- `BarracksBuildSeconds`
|
- `BarracksBuildSeconds`
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace LocationsApi.Services;
|
|||||||
public class GameSettingsService
|
public class GameSettingsService
|
||||||
{
|
{
|
||||||
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
|
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
|
||||||
|
public const string ArcherTrainingSeconds = "ArcherTrainingSeconds";
|
||||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||||
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
public const string BarracksBuildSeconds = "BarracksBuildSeconds";
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public class ResourceService
|
|||||||
private readonly IMongoCollection<PlayerResources> _resources;
|
private readonly IMongoCollection<PlayerResources> _resources;
|
||||||
private readonly int _startingGold;
|
private readonly int _startingGold;
|
||||||
private readonly int _headquartersGoldPerMinute;
|
private readonly int _headquartersGoldPerMinute;
|
||||||
|
private readonly int _townHallGoldCapacity;
|
||||||
|
private readonly int _townHallWoodCapacity;
|
||||||
|
private readonly int _townHallOreCapacity;
|
||||||
|
|
||||||
public ResourceService(IConfiguration cfg)
|
public ResourceService(IConfiguration cfg)
|
||||||
{
|
{
|
||||||
@@ -20,6 +23,15 @@ public class ResourceService
|
|||||||
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
_headquartersGoldPerMinute = int.TryParse(cfg["Resources:HeadquartersGoldPerMinute"], out var goldPerMinute)
|
||||||
? Math.Max(0, goldPerMinute)
|
? Math.Max(0, goldPerMinute)
|
||||||
: 10;
|
: 10;
|
||||||
|
_townHallGoldCapacity = int.TryParse(cfg["Resources:TownHallGoldCapacity"], out var goldCapacity)
|
||||||
|
? Math.Max(0, goldCapacity)
|
||||||
|
: 500;
|
||||||
|
_townHallWoodCapacity = int.TryParse(cfg["Resources:TownHallWoodCapacity"], out var woodCapacity)
|
||||||
|
? Math.Max(0, woodCapacity)
|
||||||
|
: 100;
|
||||||
|
_townHallOreCapacity = int.TryParse(cfg["Resources:TownHallOreCapacity"], out var oreCapacity)
|
||||||
|
? Math.Max(0, oreCapacity)
|
||||||
|
: 100;
|
||||||
|
|
||||||
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
_resources.Indexes.CreateOne(new CreateIndexModel<PlayerResources>(
|
||||||
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
Builders<PlayerResources>.IndexKeys.Ascending(x => x.Username),
|
||||||
@@ -31,7 +43,7 @@ public class ResourceService
|
|||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var update = Builders<PlayerResources>.Update
|
var update = Builders<PlayerResources>.Update
|
||||||
.SetOnInsert(x => x.Username, username)
|
.SetOnInsert(x => x.Username, username)
|
||||||
.SetOnInsert(x => x.Gold, _startingGold)
|
.SetOnInsert(x => x.Gold, Math.Min(_startingGold, _townHallGoldCapacity))
|
||||||
.SetOnInsert(x => x.Wood, 0)
|
.SetOnInsert(x => x.Wood, 0)
|
||||||
.SetOnInsert(x => x.Ore, 0)
|
.SetOnInsert(x => x.Ore, 0)
|
||||||
.SetOnInsert(x => x.CreatedAtUtc, now)
|
.SetOnInsert(x => x.CreatedAtUtc, now)
|
||||||
@@ -39,6 +51,9 @@ public class ResourceService
|
|||||||
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
|
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
|
||||||
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
|
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
|
||||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||||
|
.Set(x => x.GoldCapacity, _townHallGoldCapacity)
|
||||||
|
.Set(x => x.WoodCapacity, _townHallWoodCapacity)
|
||||||
|
.Set(x => x.OreCapacity, _townHallOreCapacity)
|
||||||
.Set(x => x.UpdatedAtUtc, now);
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
var resources = await _resources.FindOneAndUpdateAsync(
|
var resources = await _resources.FindOneAndUpdateAsync(
|
||||||
@@ -167,34 +182,52 @@ public class ResourceService
|
|||||||
var oreElapsedMinutes = resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000
|
var oreElapsedMinutes = resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000
|
||||||
? 0
|
? 0
|
||||||
: (int)Math.Floor((now - resources.LastOreGeneratedAtUtc).TotalMinutes);
|
: (int)Math.Floor((now - resources.LastOreGeneratedAtUtc).TotalMinutes);
|
||||||
|
var cappedGold = Math.Min(resources.Gold, _townHallGoldCapacity);
|
||||||
|
var cappedWood = Math.Min(resources.Wood, _townHallWoodCapacity);
|
||||||
|
var cappedOre = Math.Min(resources.Ore, _townHallOreCapacity);
|
||||||
|
var isOverCapacity = resources.Gold != cappedGold || resources.Wood != cappedWood || resources.Ore != cappedOre;
|
||||||
|
|
||||||
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0)
|
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0 && !isOverCapacity)
|
||||||
{
|
{
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
var update = Builders<PlayerResources>.Update
|
var update = Builders<PlayerResources>.Update
|
||||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||||
|
.Set(x => x.GoldCapacity, _townHallGoldCapacity)
|
||||||
|
.Set(x => x.WoodCapacity, _townHallWoodCapacity)
|
||||||
|
.Set(x => x.OreCapacity, _townHallOreCapacity)
|
||||||
.Set(x => x.UpdatedAtUtc, now);
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
|
if (isOverCapacity)
|
||||||
|
{
|
||||||
|
update = update
|
||||||
|
.Set(x => x.Gold, cappedGold)
|
||||||
|
.Set(x => x.Wood, cappedWood)
|
||||||
|
.Set(x => x.Ore, cappedOre);
|
||||||
|
}
|
||||||
|
|
||||||
if (goldElapsedMinutes > 0)
|
if (goldElapsedMinutes > 0)
|
||||||
{
|
{
|
||||||
|
var generatedGold = Math.Min(_townHallGoldCapacity, cappedGold + (goldElapsedMinutes * resources.GoldPerMinute));
|
||||||
update = update
|
update = update
|
||||||
.Inc(x => x.Gold, goldElapsedMinutes * resources.GoldPerMinute)
|
.Set(x => x.Gold, generatedGold)
|
||||||
.Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes));
|
.Set(x => x.LastGoldGeneratedAtUtc, resources.LastGoldGeneratedAtUtc.AddMinutes(goldElapsedMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (woodElapsedMinutes > 0)
|
if (woodElapsedMinutes > 0)
|
||||||
{
|
{
|
||||||
|
var generatedWood = Math.Min(_townHallWoodCapacity, cappedWood + (woodElapsedMinutes * resources.WoodPerMinute));
|
||||||
update = update
|
update = update
|
||||||
.Inc(x => x.Wood, woodElapsedMinutes * resources.WoodPerMinute)
|
.Set(x => x.Wood, generatedWood)
|
||||||
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
|
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oreElapsedMinutes > 0)
|
if (oreElapsedMinutes > 0)
|
||||||
{
|
{
|
||||||
|
var generatedOre = Math.Min(_townHallOreCapacity, cappedOre + (oreElapsedMinutes * resources.OrePerMinute));
|
||||||
update = update
|
update = update
|
||||||
.Inc(x => x.Ore, oreElapsedMinutes * resources.OrePerMinute)
|
.Set(x => x.Ore, generatedOre)
|
||||||
.Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes));
|
.Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ namespace LocationsApi.Services;
|
|||||||
public class UnitTrainingService
|
public class UnitTrainingService
|
||||||
{
|
{
|
||||||
private const string ScoutUnitType = "Scout";
|
private const string ScoutUnitType = "Scout";
|
||||||
|
private const string ArcherUnitType = "Archer";
|
||||||
|
|
||||||
private readonly IMongoCollection<UnitTraining> _training;
|
private readonly IMongoCollection<UnitTraining> _training;
|
||||||
|
private readonly IMongoCollection<Location> _locations;
|
||||||
private readonly ResourceService _resources;
|
private readonly ResourceService _resources;
|
||||||
private readonly GameSettingsService _settings;
|
private readonly GameSettingsService _settings;
|
||||||
private readonly int _scoutGoldCost;
|
private readonly int _scoutGoldCost;
|
||||||
|
private readonly int _archerGoldCost;
|
||||||
|
private readonly int _archerWoodCost;
|
||||||
private readonly int _defaultScoutTrainingSeconds;
|
private readonly int _defaultScoutTrainingSeconds;
|
||||||
|
private readonly int _defaultArcherTrainingSeconds;
|
||||||
|
|
||||||
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
public UnitTrainingService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||||
{
|
{
|
||||||
@@ -20,12 +25,18 @@ public class UnitTrainingService
|
|||||||
var client = new MongoClient(connectionString);
|
var client = new MongoClient(connectionString);
|
||||||
var database = client.GetDatabase(databaseName);
|
var database = client.GetDatabase(databaseName);
|
||||||
_training = database.GetCollection<UnitTraining>("UnitTraining");
|
_training = database.GetCollection<UnitTraining>("UnitTraining");
|
||||||
|
_locations = database.GetCollection<Location>("Locations");
|
||||||
_resources = resources;
|
_resources = resources;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50;
|
_scoutGoldCost = int.TryParse(cfg["Units:ScoutGoldCost"], out var scoutGoldCost) ? Math.Max(0, scoutGoldCost) : 50;
|
||||||
|
_archerGoldCost = int.TryParse(cfg["Units:ArcherGoldCost"], out var archerGoldCost) ? Math.Max(0, archerGoldCost) : 100;
|
||||||
|
_archerWoodCost = int.TryParse(cfg["Units:ArcherWoodCost"], out var archerWoodCost) ? Math.Max(0, archerWoodCost) : 100;
|
||||||
_defaultScoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds)
|
_defaultScoutTrainingSeconds = int.TryParse(cfg["Units:ScoutTrainingSeconds"], out var scoutTrainingSeconds)
|
||||||
? Math.Max(1, scoutTrainingSeconds)
|
? Math.Max(1, scoutTrainingSeconds)
|
||||||
: 60;
|
: 60;
|
||||||
|
_defaultArcherTrainingSeconds = int.TryParse(cfg["Units:ArcherTrainingSeconds"], out var archerTrainingSeconds)
|
||||||
|
? Math.Max(1, archerTrainingSeconds)
|
||||||
|
: 60;
|
||||||
|
|
||||||
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
_training.Indexes.CreateOne(new CreateIndexModel<UnitTraining>(
|
||||||
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
|
Builders<UnitTraining>.IndexKeys.Ascending(x => x.Username).Ascending(x => x.Status)));
|
||||||
@@ -38,6 +49,7 @@ public class UnitTrainingService
|
|||||||
|
|
||||||
var all = await _training.Find(x => x.Username == username).ToListAsync();
|
var all = await _training.Find(x => x.Username == username).ToListAsync();
|
||||||
var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
|
var completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
|
||||||
|
var completedArchers = all.Count(x => x.UnitType == ArcherUnitType && x.Status == "Completed");
|
||||||
var activeTraining = all
|
var activeTraining = all
|
||||||
.Where(x => x.Status == "Training")
|
.Where(x => x.Status == "Training")
|
||||||
.OrderBy(x => x.CompletesAtUtc)
|
.OrderBy(x => x.CompletesAtUtc)
|
||||||
@@ -47,33 +59,47 @@ public class UnitTrainingService
|
|||||||
return new PlayerUnitsResponse
|
return new PlayerUnitsResponse
|
||||||
{
|
{
|
||||||
CompletedScouts = completedScouts,
|
CompletedScouts = completedScouts,
|
||||||
|
CompletedArchers = completedArchers,
|
||||||
Training = activeTraining
|
Training = activeTraining
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UnitTraining?> TrainAsync(string username, string unitType)
|
public async Task<UnitTraining?> TrainAsync(string username, string unitType)
|
||||||
{
|
{
|
||||||
if (!string.Equals(unitType, ScoutUnitType, StringComparison.OrdinalIgnoreCase))
|
var normalizedUnitType = NormalizeUnitType(unitType);
|
||||||
|
if (normalizedUnitType is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Unsupported unit type.", nameof(unitType));
|
throw new ArgumentException("Unsupported unit type.", nameof(unitType));
|
||||||
}
|
}
|
||||||
|
|
||||||
var spent = await _resources.TrySpendGoldAsync(username, _scoutGoldCost);
|
if (normalizedUnitType == ArcherUnitType && !await HasBuiltBarracksAsync(username))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("A completed Barracks is required to train Archers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var goldCost = normalizedUnitType == ArcherUnitType ? _archerGoldCost : _scoutGoldCost;
|
||||||
|
var woodCost = normalizedUnitType == ArcherUnitType ? _archerWoodCost : 0;
|
||||||
|
var spent = normalizedUnitType == ArcherUnitType
|
||||||
|
? await _resources.TrySpendResourcesAsync(username, goldCost, woodCost, 0)
|
||||||
|
: await _resources.TrySpendGoldAsync(username, goldCost);
|
||||||
if (!spent)
|
if (!spent)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var scoutTrainingSeconds = await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds);
|
var trainingSeconds = normalizedUnitType == ArcherUnitType
|
||||||
|
? await _settings.GetIntAsync(GameSettingsService.ArcherTrainingSeconds, _defaultArcherTrainingSeconds)
|
||||||
|
: await _settings.GetIntAsync(GameSettingsService.ScoutTrainingSeconds, _defaultScoutTrainingSeconds);
|
||||||
var training = new UnitTraining
|
var training = new UnitTraining
|
||||||
{
|
{
|
||||||
Username = username,
|
Username = username,
|
||||||
UnitType = ScoutUnitType,
|
UnitType = normalizedUnitType,
|
||||||
GoldCost = _scoutGoldCost,
|
GoldCost = goldCost,
|
||||||
|
WoodCost = woodCost,
|
||||||
Status = "Training",
|
Status = "Training",
|
||||||
StartedAtUtc = now,
|
StartedAtUtc = now,
|
||||||
CompletesAtUtc = now.AddSeconds(scoutTrainingSeconds),
|
CompletesAtUtc = now.AddSeconds(trainingSeconds),
|
||||||
CreatedAtUtc = now,
|
CreatedAtUtc = now,
|
||||||
UpdatedAtUtc = now
|
UpdatedAtUtc = now
|
||||||
};
|
};
|
||||||
@@ -82,6 +108,32 @@ public class UnitTrainingService
|
|||||||
return training;
|
return training;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HasBuiltBarracksAsync(string username)
|
||||||
|
{
|
||||||
|
await CompleteFinishedBuildingsAsync(username);
|
||||||
|
return await _locations.Find(x =>
|
||||||
|
x.OwnerUsername == username &&
|
||||||
|
x.BuildingType == "Barracks" &&
|
||||||
|
(x.BuildingStatus == "Built" || x.BuildingStatus == null || x.BuildingStatus == ""))
|
||||||
|
.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompleteFinishedBuildingsAsync(string username)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var update = Builders<Location>.Update
|
||||||
|
.Set(x => x.BuildingStatus, "Built")
|
||||||
|
.Set(x => x.BuildingBuiltAtUtc, now)
|
||||||
|
.Set(x => x.UpdatedAtUtc, now);
|
||||||
|
|
||||||
|
await _locations.UpdateManyAsync(
|
||||||
|
x => x.OwnerUsername == username &&
|
||||||
|
x.BuildingType == "Barracks" &&
|
||||||
|
x.BuildingStatus == "Building" &&
|
||||||
|
x.BuildingCompletesAtUtc <= now,
|
||||||
|
update);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
public async Task<bool> TryConsumeCompletedScoutAsync(string username)
|
||||||
{
|
{
|
||||||
await CompleteFinishedTrainingAsync(username);
|
await CompleteFinishedTrainingAsync(username);
|
||||||
@@ -114,4 +166,15 @@ public class UnitTrainingService
|
|||||||
x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now,
|
x => x.Username == username && x.Status == "Training" && x.CompletesAtUtc <= now,
|
||||||
update);
|
update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeUnitType(string unitType)
|
||||||
|
{
|
||||||
|
var trimmed = unitType.Trim();
|
||||||
|
return trimmed.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"scout" => ScoutUnitType,
|
||||||
|
"archer" => ArcherUnitType,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,17 @@
|
|||||||
},
|
},
|
||||||
"Resources": {
|
"Resources": {
|
||||||
"StartingGold": 100,
|
"StartingGold": 100,
|
||||||
"HeadquartersGoldPerMinute": 10
|
"HeadquartersGoldPerMinute": 10,
|
||||||
|
"TownHallGoldCapacity": 500,
|
||||||
|
"TownHallWoodCapacity": 100,
|
||||||
|
"TownHallOreCapacity": 100
|
||||||
},
|
},
|
||||||
"Units": {
|
"Units": {
|
||||||
"ScoutGoldCost": 50,
|
"ScoutGoldCost": 50,
|
||||||
"ScoutTrainingSeconds": 60
|
"ScoutTrainingSeconds": 60,
|
||||||
|
"ArcherGoldCost": 100,
|
||||||
|
"ArcherWoodCost": 100,
|
||||||
|
"ArcherTrainingSeconds": 60
|
||||||
},
|
},
|
||||||
"Buildings": {
|
"Buildings": {
|
||||||
"LogCampWoodPerMinute": 5,
|
"LogCampWoodPerMinute": 5,
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ data:
|
|||||||
World__VisibilityDepth: "3"
|
World__VisibilityDepth: "3"
|
||||||
Resources__StartingGold: "100"
|
Resources__StartingGold: "100"
|
||||||
Resources__HeadquartersGoldPerMinute: "10"
|
Resources__HeadquartersGoldPerMinute: "10"
|
||||||
|
Resources__TownHallGoldCapacity: "500"
|
||||||
|
Resources__TownHallWoodCapacity: "100"
|
||||||
|
Resources__TownHallOreCapacity: "100"
|
||||||
Units__ScoutGoldCost: "50"
|
Units__ScoutGoldCost: "50"
|
||||||
Units__ScoutTrainingSeconds: "60"
|
Units__ScoutTrainingSeconds: "60"
|
||||||
|
Units__ArcherGoldCost: "100"
|
||||||
|
Units__ArcherWoodCost: "100"
|
||||||
|
Units__ArcherTrainingSeconds: "60"
|
||||||
Buildings__LogCampWoodPerMinute: "5"
|
Buildings__LogCampWoodPerMinute: "5"
|
||||||
Buildings__LogCampBuildSeconds: "60"
|
Buildings__LogCampBuildSeconds: "60"
|
||||||
Buildings__MineOrePerMinute: "5"
|
Buildings__MineOrePerMinute: "5"
|
||||||
|
|||||||
Reference in New Issue
Block a user