adding mines

This commit is contained in:
hz
2026-05-18 21:37:48 -05:00
parent 973eda5b44
commit 34d406e993
12 changed files with 294 additions and 45 deletions
@@ -36,12 +36,17 @@ var training_completion_refresh_requested := false
var building_started_unix := 0.0 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 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 selected_q := 0 var selected_q := 0
var selected_r := 0 var selected_r := 0
var selected_can_claim := false 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_is_home := false
var completed_scout_count := 0
func _ready() -> void: func _ready() -> void:
@@ -239,6 +244,7 @@ func _build_ui() -> void:
building_box.add_child(building_bar) building_box.add_child(building_bar)
train_button = _small_button("Train Scout 50g") train_button = _small_button("Train Scout 50g")
train_button.visible = false
train_button.pressed.connect(_train_scout) train_button.pressed.connect(_train_scout)
bottom_content.add_child(train_button) bottom_content.add_child(train_button)
@@ -252,6 +258,11 @@ func _build_ui() -> void:
build_log_camp_button.pressed.connect(_build_log_camp) build_log_camp_button.pressed.connect(_build_log_camp)
bottom_content.add_child(build_log_camp_button) bottom_content.add_child(build_log_camp_button)
build_mine_button = _small_button("Build Mine")
build_mine_button.disabled = true
build_mine_button.pressed.connect(_build_mine)
bottom_content.add_child(build_mine_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)
add_child(http_request) add_child(http_request)
@@ -298,15 +309,63 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
selected_q = q selected_q = q
selected_r = r selected_r = r
selected_can_claim = terrain.contains("Unclaimed") selected_can_claim = terrain.contains("Unclaimed")
selected_can_build_log_camp = terrain.contains("Forest") and terrain.contains("Your territory") and not terrain.contains("Log Camp") selected_is_home = _selected_tile_is_home()
selected_can_build_log_camp = _can_build_log_camp_on_selected_tile()
selected_can_build_mine = _can_build_mine_on_selected_tile()
if train_button:
train_button.visible = selected_is_home
if claim_button: if claim_button:
claim_button.disabled = not selected_can_claim claim_button.disabled = not _can_claim_selected_tile()
if build_log_camp_button: if build_log_camp_button:
build_log_camp_button.disabled = not selected_can_build_log_camp build_log_camp_button.disabled = not selected_can_build_log_camp
if build_mine_button:
build_mine_button.disabled = not selected_can_build_mine
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]
func _can_build_log_camp_on_selected_tile() -> bool:
return _can_build_on_selected_tile("Forest")
func _can_build_mine_on_selected_tile() -> bool:
return _can_build_on_selected_tile("Ore")
func _can_build_on_selected_tile(required_terrain: String) -> 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 terrain: String = str(location.get("terrain", ""))
var building_type: String = str(location.get("buildingType", ""))
return owner_username == username and terrain == required_terrain and building_type.is_empty()
func _can_claim_selected_tile() -> bool:
return selected_can_claim and completed_scout_count > 0
func _selected_tile_is_home() -> 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 kind: String = str(location.get("kind", ""))
return owner_username == username and kind == "Home"
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"))
@@ -457,7 +516,9 @@ 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))
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))
gold_label.text = "Gold: %d (+%d/min) | Wood: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute] var ore: int = int(resources.get("ore", 0))
var ore_per_minute: int = int(resources.get("orePerMinute", 0))
gold_label.text = "Gold: %d (+%d/min) | Wood: %d (+%d/min) | Ore: %d (+%d/min)" % [gold, gold_per_minute, wood, wood_per_minute, ore, ore_per_minute]
var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", "")) var anchor_text: String = str(resources.get("lastGoldGeneratedAtUtc", ""))
gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text) gold_tick_anchor_unix = _parse_utc_timestamp(anchor_text)
@@ -502,7 +563,10 @@ func _on_units_request_completed(result: int, response_code: int, _headers: Pack
if typeof(raw_training) == TYPE_ARRAY: if typeof(raw_training) == TYPE_ARRAY:
training = raw_training training = raw_training
unit_label.text = "Scouts: %d | Queue: %d" % [int(units.get("completedScouts", 0)), training.size()] completed_scout_count = int(units.get("completedScouts", 0))
unit_label.text = "Scouts: %d | Queue: %d" % [completed_scout_count, training.size()]
if claim_button:
claim_button.disabled = not _can_claim_selected_tile()
if training.is_empty(): if training.is_empty():
training_started_unix = 0.0 training_started_unix = 0.0
@@ -573,6 +637,9 @@ func _claim_selected_tile() -> void:
if not selected_can_claim: if not selected_can_claim:
coordinate_label.text = "Select a visible unclaimed tile to send a scout." coordinate_label.text = "Select a visible unclaimed tile to send a scout."
return return
if completed_scout_count <= 0:
coordinate_label.text = "Train a scout before claiming territory."
return
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():
@@ -623,16 +690,32 @@ func _build_log_camp() -> void:
coordinate_label.text = "Select controlled Forest territory without a building." coordinate_label.text = "Select controlled Forest territory without a building."
return return
_request_building("LogCamp")
func _build_mine() -> void:
if not selected_can_build_mine:
coordinate_label.text = "Select controlled Ore territory without a building."
return
_request_building("Mine")
func _request_building(building_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. Log Camp was not built." coordinate_label.text = "No auth token available. %s was not built." % building_type
return return
if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED: if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
return return
build_log_camp_button.disabled = true if building_type == "Mine":
coordinate_label.text = "Building Log Camp at q:%d r:%d..." % [selected_q, selected_r] build_mine_button.disabled = true
else:
build_log_camp_button.disabled = true
coordinate_label.text = "Building %s at q:%d r:%d..." % [building_type, selected_q, selected_r]
var error: int = build_request.request( var error: int = build_request.request(
LOCATIONS_BASE_URL + "/api/locations/build", LOCATIONS_BASE_URL + "/api/locations/build",
PackedStringArray([ PackedStringArray([
@@ -640,36 +723,42 @@ func _build_log_camp() -> void:
"Authorization: Bearer %s" % access_token "Authorization: Bearer %s" % access_token
]), ]),
HTTPClient.METHOD_POST, HTTPClient.METHOD_POST,
JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": "LogCamp" }) JSON.stringify({ "q": selected_q, "r": selected_r, "buildingType": building_type })
) )
if error != OK: if error != OK:
build_log_camp_button.disabled = false build_log_camp_button.disabled = not selected_can_build_log_camp
build_mine_button.disabled = not selected_can_build_mine
coordinate_label.text = "Could not contact the locations service." coordinate_label.text = "Could not contact the locations service."
func _on_build_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: func _on_build_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
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 = "Log Camp build request failed." coordinate_label.text = "Build request failed."
build_log_camp_button.disabled = not selected_can_build_log_camp build_log_camp_button.disabled = not selected_can_build_log_camp
build_mine_button.disabled = not selected_can_build_mine
return return
if response_code < 200 or response_code >= 300: if response_code < 200 or response_code >= 300:
coordinate_label.text = "Log Camp build failed: %s" % text coordinate_label.text = "Build failed: %s" % text
build_log_camp_button.disabled = not selected_can_build_log_camp build_log_camp_button.disabled = not selected_can_build_log_camp
build_mine_button.disabled = not selected_can_build_mine
_refresh_player_state() _refresh_player_state()
return return
selected_can_build_log_camp = false selected_can_build_log_camp = false
selected_can_build_mine = false
build_log_camp_button.disabled = true build_log_camp_button.disabled = true
build_mine_button.disabled = true
var parsed = JSON.parse_string(text) var parsed = JSON.parse_string(text)
if typeof(parsed) == TYPE_DICTIONARY: if typeof(parsed) == TYPE_DICTIONARY:
var location: Dictionary = parsed var location: Dictionary = parsed
active_building_label = _building_display_name(str(location.get("buildingType", "Building")))
building_started_unix = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", ""))) building_started_unix = _parse_utc_timestamp(str(location.get("buildingStartedAtUtc", "")))
building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", ""))) building_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", "")))
building_completion_refresh_requested = false building_completion_refresh_requested = false
_update_building_timer() _update_building_timer()
coordinate_label.text = "Log Camp construction started. Completion takes 1 minute." coordinate_label.text = "%s construction started." % active_building_label
_load_visible_locations() _load_visible_locations()
_refresh_player_state() _refresh_player_state()
@@ -733,9 +822,9 @@ func _update_building_timer() -> void:
building_bar.value = elapsed / duration building_bar.value = elapsed / duration
if remaining > 0: if remaining > 0:
building_label.text = "Log Camp: %ds" % remaining building_label.text = "%s: %ds" % [active_building_label, remaining]
else: else:
building_label.text = "Log Camp: ready" building_label.text = "%s: ready" % active_building_label
if remaining <= 0 and not building_completion_refresh_requested: if remaining <= 0 and not building_completion_refresh_requested:
building_completion_refresh_requested = true building_completion_refresh_requested = true
@@ -746,6 +835,7 @@ func _update_building_timer() -> void:
func _sync_active_building_timer(locations: Array, username: String) -> void: func _sync_active_building_timer(locations: Array, username: String) -> void:
var best_started := 0.0 var best_started := 0.0
var best_completes := 0.0 var best_completes := 0.0
var best_label := "Build"
for item in locations: for item in locations:
if typeof(item) != TYPE_DICTIONARY: if typeof(item) != TYPE_DICTIONARY:
@@ -754,7 +844,8 @@ func _sync_active_building_timer(locations: Array, username: String) -> void:
var location: Dictionary = item var location: Dictionary = item
if str(location.get("ownerUsername", "")) != username: if str(location.get("ownerUsername", "")) != username:
continue continue
if str(location.get("buildingType", "")) != "LogCamp": var building_type: String = str(location.get("buildingType", ""))
if building_type.is_empty():
continue continue
if str(location.get("buildingStatus", "")) != "Building": if str(location.get("buildingStatus", "")) != "Building":
continue continue
@@ -766,13 +857,25 @@ func _sync_active_building_timer(locations: Array, username: String) -> void:
if best_completes <= 0.0 or completes < best_completes: if best_completes <= 0.0 or completes < best_completes:
best_started = started best_started = started
best_completes = completes best_completes = completes
best_label = _building_display_name(building_type)
building_started_unix = best_started building_started_unix = best_started
building_completes_unix = best_completes building_completes_unix = best_completes
active_building_label = best_label
building_completion_refresh_requested = false building_completion_refresh_requested = false
_update_building_timer() _update_building_timer()
func _building_display_name(building_type: String) -> String:
match building_type:
"LogCamp":
return "Log Camp"
"Mine":
return "Mine"
_:
return "Build"
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
@@ -163,9 +163,13 @@ func _draw_tile(tile: Dictionary) -> void:
var visible_location: Dictionary = visible_tiles.get(key, {}) var visible_location: Dictionary = visible_tiles.get(key, {})
var building_type: String = str(visible_location.get("buildingType", "")) var building_type: String = str(visible_location.get("buildingType", ""))
var building_status: String = str(visible_location.get("buildingStatus", "")) var building_status: String = str(visible_location.get("buildingStatus", ""))
if building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()): if coord == home_tile:
_draw_town_hall_marker(center, height)
elif building_type == "LogCamp" and (building_status == "Built" or building_status.is_empty()):
_draw_log_camp_marker(center, height) _draw_log_camp_marker(center, height)
elif building_type == "LogCamp": elif building_type == "Mine" and (building_status == "Built" or building_status.is_empty()):
_draw_mine_marker(center, height)
elif not building_type.is_empty():
_draw_building_marker(center, height) _draw_building_marker(center, height)
@@ -301,6 +305,61 @@ func _draw_log_camp_marker(center: Vector2, height: float) -> void:
draw_line(marker_center + Vector2(-width * 0.38, marker_height * 0.86), marker_center + Vector2(width * 0.38, marker_height * 0.86), log_color.darkened(0.12), 3.0 * zoom) draw_line(marker_center + Vector2(-width * 0.38, marker_height * 0.86), marker_center + Vector2(width * 0.38, marker_height * 0.86), log_color.darkened(0.12), 3.0 * zoom)
func _draw_mine_marker(center: Vector2, height: float) -> void:
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 2.0)
var width: float = 20.0 * zoom
var marker_height: float = 13.0 * zoom
var rock_color := Color(0.28, 0.31, 0.34)
var entry_color := Color(0.08, 0.09, 0.1)
var beam_color := Color(0.62, 0.46, 0.26)
var ore_color := Color(0.72, 0.82, 0.9)
var mound := PackedVector2Array([
marker_center + Vector2(-width * 0.56, marker_height * 0.48),
marker_center + Vector2(-width * 0.32, -marker_height * 0.22),
marker_center + Vector2(0, -marker_height * 0.52),
marker_center + Vector2(width * 0.38, -marker_height * 0.18),
marker_center + Vector2(width * 0.58, marker_height * 0.48)
])
draw_colored_polygon(mound, rock_color)
draw_rect(Rect2(marker_center + Vector2(-width * 0.18, marker_height * 0.02), Vector2(width * 0.36, marker_height * 0.48)), entry_color, true)
draw_line(marker_center + Vector2(-width * 0.28, marker_height * 0.02), marker_center + Vector2(width * 0.28, marker_height * 0.02), beam_color, 2.0 * zoom)
draw_line(marker_center + Vector2(-width * 0.18, marker_height * 0.02), marker_center + Vector2(-width * 0.18, marker_height * 0.5), beam_color, 2.0 * zoom)
draw_line(marker_center + Vector2(width * 0.18, marker_height * 0.02), marker_center + Vector2(width * 0.18, marker_height * 0.5), beam_color, 2.0 * zoom)
draw_circle(marker_center + Vector2(width * 0.34, -marker_height * 0.1), 2.0 * zoom, ore_color)
func _draw_town_hall_marker(center: Vector2, height: float) -> void:
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 3.0)
var width: float = 24.0 * zoom
var marker_height: float = 18.0 * zoom
var wall_color := Color(0.76, 0.58, 0.32)
var roof_color := Color(0.42, 0.19, 0.12)
var trim_color := Color(0.98, 0.78, 0.3)
var shadow_color := Color(0.12, 0.08, 0.04)
var body_rect := Rect2(marker_center + Vector2(-width * 0.38, -marker_height * 0.02), Vector2(width * 0.76, marker_height * 0.62))
var roof := PackedVector2Array([
marker_center + Vector2(-width * 0.54, -marker_height * 0.02),
marker_center + Vector2(0, -marker_height * 0.72),
marker_center + Vector2(width * 0.54, -marker_height * 0.02)
])
var tower_rect := Rect2(marker_center + Vector2(-width * 0.13, -marker_height * 0.45), Vector2(width * 0.26, marker_height * 0.36))
var tower_roof := PackedVector2Array([
marker_center + Vector2(-width * 0.20, -marker_height * 0.45),
marker_center + Vector2(0, -marker_height * 0.84),
marker_center + Vector2(width * 0.20, -marker_height * 0.45)
])
draw_rect(Rect2(body_rect.position + Vector2(1, 1) * zoom, body_rect.size), shadow_color, true)
draw_colored_polygon(roof, roof_color)
draw_rect(body_rect, wall_color, true)
draw_rect(tower_rect, wall_color.lightened(0.08), true)
draw_colored_polygon(tower_roof, roof_color.darkened(0.05))
draw_line(body_rect.position + Vector2(0, body_rect.size.y), body_rect.position + body_rect.size, trim_color, 2.0 * zoom)
draw_rect(Rect2(marker_center + Vector2(-width * 0.08, marker_height * 0.28), Vector2(width * 0.16, marker_height * 0.32)), shadow_color.lightened(0.2), true)
func _draw_building_marker(center: Vector2, height: float) -> void: func _draw_building_marker(center: Vector2, height: float) -> void:
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 1.0) var marker_center: Vector2 = center + Vector2(0, -height * zoom - 1.0)
var width: float = 20.0 * zoom var width: float = 20.0 * zoom
@@ -368,6 +427,13 @@ func set_visible_locations(locations: Array, username: String) -> void:
queue_redraw() queue_redraw()
func get_visible_location(q: int, r: int) -> Dictionary:
var key: String = _tile_key(q, r)
if visible_tiles.has(key):
return visible_tiles[key]
return {}
func _tile_key(q: int, r: int) -> String: func _tile_key(q: int, r: int) -> String:
return "%d,%d" % [q, r] return "%d,%d" % [q, r]
@@ -420,6 +486,8 @@ func _tile_display_label(key: String, data: Dictionary) -> String:
var building_label: String = "" var building_label: String = ""
if building_type == "LogCamp": if building_type == "LogCamp":
building_label = " | Log Camp" if building_status == "Built" or building_status.is_empty() else " | Log Camp building" building_label = " | Log Camp" if building_status == "Built" or building_status.is_empty() else " | Log Camp building"
elif building_type == "Mine":
building_label = " | Mine" if building_status == "Built" or building_status.is_empty() else " | Mine building"
if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))): if home_tile == Vector2i(int(data.get("q", 0)), int(data.get("r", 0))):
return "%s | Your home%s" % [terrain, building_label] return "%s | Your home%s" % [terrain, building_label]
+1 -1
View File
@@ -52,7 +52,7 @@ public class LocationResponse
Label = location.Label, Label = location.Label,
Level = location.Level, Level = location.Level,
BuildingType = location.BuildingType, BuildingType = location.BuildingType,
BuildingStatus = location.BuildingType == "LogCamp" && string.IsNullOrWhiteSpace(location.BuildingStatus) BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine") && string.IsNullOrWhiteSpace(location.BuildingStatus)
? "Built" ? "Built"
: location.BuildingStatus, : location.BuildingStatus,
BuildingStartedAtUtc = location.BuildingStartedAtUtc, BuildingStartedAtUtc = location.BuildingStartedAtUtc,
@@ -14,8 +14,11 @@ public class PlayerResources
public int GoldPerMinute { get; set; } = 10; public int GoldPerMinute { get; set; } = 10;
public int Wood { get; set; } public int Wood { get; set; }
public int WoodPerMinute { get; set; } public int WoodPerMinute { get; set; }
public int Ore { get; set; }
public int OrePerMinute { get; set; }
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 CreatedAtUtc { get; set; } = DateTime.UtcNow; public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow; public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
} }
@@ -7,8 +7,11 @@ public class PlayerResourcesResponse
public int GoldPerMinute { get; set; } public int GoldPerMinute { get; set; }
public int Wood { get; set; } public int Wood { get; set; }
public int WoodPerMinute { get; set; } public int WoodPerMinute { get; set; }
public int Ore { get; set; }
public int OrePerMinute { 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 UpdatedAtUtc { get; set; } public DateTime UpdatedAtUtc { get; set; }
public static PlayerResourcesResponse FromResources(PlayerResources resources) => new() public static PlayerResourcesResponse FromResources(PlayerResources resources) => new()
@@ -18,8 +21,11 @@ public class PlayerResourcesResponse
GoldPerMinute = resources.GoldPerMinute, GoldPerMinute = resources.GoldPerMinute,
Wood = resources.Wood, Wood = resources.Wood,
WoodPerMinute = resources.WoodPerMinute, WoodPerMinute = resources.WoodPerMinute,
Ore = resources.Ore,
OrePerMinute = resources.OrePerMinute,
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc, LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc, LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc,
UpdatedAtUtc = resources.UpdatedAtUtc UpdatedAtUtc = resources.UpdatedAtUtc
}; };
} }
+10 -8
View File
@@ -28,26 +28,28 @@ 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. Generation is applied lazily when the resource minute. Log Camps generate wood and Mines generate ore. Generation is applied
record is read. lazily when the resource record is read.
## Units ## Units
Unit training is stored server-side in the `UnitTraining` collection. The first 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. unit type is `Scout`, which costs 50 gold and takes 60 seconds to train.
Finished training is completed lazily when unit state is read. Claiming an Finished training is completed lazily when unit state is read. Claiming an
unclaimed visible location consumes one completed Scout. unclaimed visible location consumes one completed Scout by deleting that Scout's
training record.
## Buildings ## Buildings
Buildings are stored on location records. The first building type is `LogCamp`, Buildings are stored on location records. `LogCamp` can be built on controlled
which can be built on controlled Forest territory, takes 60 seconds to build, Forest territory, takes 60 seconds to build, and adds 5 wood per minute after it
and adds 5 wood per minute to the owning player's resource generation after it finishes. `Mine` can be built on controlled Ore territory, takes 60 seconds to
finishes. Each location has one building slot; a building under construction build, and adds 5 ore per minute after it finishes. Each location has one
occupies that slot. building slot; a building under construction occupies that slot.
Timer settings are stored in the MongoDB `GameSettings` collection and are Timer settings are stored in the MongoDB `GameSettings` collection and are
seeded on first read: seeded on first read:
- `ScoutTrainingSeconds` - `ScoutTrainingSeconds`
- `LogCampBuildSeconds` - `LogCampBuildSeconds`
- `MineBuildSeconds`
@@ -7,6 +7,7 @@ public class GameSettingsService
{ {
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds"; public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
public const string LogCampBuildSeconds = "LogCampBuildSeconds"; public const string LogCampBuildSeconds = "LogCampBuildSeconds";
public const string MineBuildSeconds = "MineBuildSeconds";
private readonly IMongoCollection<GameSetting> _settings; private readonly IMongoCollection<GameSetting> _settings;
@@ -12,7 +12,9 @@ public class LocationService
private readonly int _homeSearchRadius; private readonly int _homeSearchRadius;
private readonly int _visibilityDepth; private readonly int _visibilityDepth;
private readonly int _logCampWoodPerMinute; private readonly int _logCampWoodPerMinute;
private readonly int _mineOrePerMinute;
private readonly int _defaultLogCampBuildSeconds; private readonly int _defaultLogCampBuildSeconds;
private readonly int _defaultMineBuildSeconds;
public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings) public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
{ {
@@ -29,9 +31,15 @@ public class LocationService
_logCampWoodPerMinute = int.TryParse(cfg["Buildings:LogCampWoodPerMinute"], out var logCampWoodPerMinute) _logCampWoodPerMinute = int.TryParse(cfg["Buildings:LogCampWoodPerMinute"], out var logCampWoodPerMinute)
? Math.Max(1, logCampWoodPerMinute) ? Math.Max(1, logCampWoodPerMinute)
: 5; : 5;
_mineOrePerMinute = int.TryParse(cfg["Buildings:MineOrePerMinute"], out var mineOrePerMinute)
? Math.Max(1, mineOrePerMinute)
: 5;
_defaultLogCampBuildSeconds = int.TryParse(cfg["Buildings:LogCampBuildSeconds"], out var logCampBuildSeconds) _defaultLogCampBuildSeconds = int.TryParse(cfg["Buildings:LogCampBuildSeconds"], out var logCampBuildSeconds)
? Math.Max(1, logCampBuildSeconds) ? Math.Max(1, logCampBuildSeconds)
: 60; : 60;
_defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds)
? Math.Max(1, mineBuildSeconds)
: 60;
var coordinateIndex = Builders<Location>.IndexKeys var coordinateIndex = Builders<Location>.IndexKeys
.Ascending(x => x.Q) .Ascending(x => x.Q)
@@ -123,7 +131,8 @@ public class LocationService
public async Task<Location?> BuildAsync(BuildLocationRequest request, string username) public async Task<Location?> BuildAsync(BuildLocationRequest request, string username)
{ {
if (!string.Equals(request.BuildingType, "LogCamp", StringComparison.OrdinalIgnoreCase)) var buildingType = NormalizeBuildingType(request.BuildingType);
if (buildingType is null)
{ {
throw new ArgumentException("Unsupported building type.", nameof(request.BuildingType)); throw new ArgumentException("Unsupported building type.", nameof(request.BuildingType));
} }
@@ -140,17 +149,24 @@ public class LocationService
throw new InvalidOperationException("This location already has a building or construction in progress."); throw new InvalidOperationException("This location already has a building or construction in progress.");
} }
if (!string.Equals(location.Terrain, "Forest", StringComparison.OrdinalIgnoreCase)) var requiredTerrain = buildingType == "LogCamp" ? "Forest" : "Ore";
if (!string.Equals(location.Terrain, requiredTerrain, StringComparison.OrdinalIgnoreCase))
{ {
throw new InvalidOperationException("Log Camps can only be built on Forest territory."); throw new InvalidOperationException($"{buildingType} can only be built on {requiredTerrain} territory.");
} }
await CompleteFinishedBuildingsAsync(username); await CompleteFinishedBuildingsAsync(username);
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var buildSeconds = await _settings.GetIntAsync(GameSettingsService.LogCampBuildSeconds, _defaultLogCampBuildSeconds); var settingKey = buildingType == "LogCamp"
? GameSettingsService.LogCampBuildSeconds
: GameSettingsService.MineBuildSeconds;
var defaultBuildSeconds = buildingType == "LogCamp"
? _defaultLogCampBuildSeconds
: _defaultMineBuildSeconds;
var buildSeconds = await _settings.GetIntAsync(settingKey, defaultBuildSeconds);
var update = Builders<Location>.Update var update = Builders<Location>.Update
.Set(x => x.BuildingType, "LogCamp") .Set(x => x.BuildingType, buildingType)
.Set(x => x.BuildingStatus, "Building") .Set(x => x.BuildingStatus, "Building")
.Set(x => x.BuildingStartedAtUtc, now) .Set(x => x.BuildingStartedAtUtc, now)
.Set(x => x.BuildingCompletesAtUtc, now.AddSeconds(buildSeconds)) .Set(x => x.BuildingCompletesAtUtc, now.AddSeconds(buildSeconds))
@@ -177,7 +193,7 @@ public class LocationService
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var readyBuildings = await _locations.Find(x => var readyBuildings = await _locations.Find(x =>
x.OwnerUsername == username && x.OwnerUsername == username &&
x.BuildingType == "LogCamp" && (x.BuildingType == "LogCamp" || x.BuildingType == "Mine") &&
x.BuildingStatus == "Building" && x.BuildingStatus == "Building" &&
x.BuildingCompletesAtUtc <= now) x.BuildingCompletesAtUtc <= now)
.ToListAsync(); .ToListAsync();
@@ -199,7 +215,14 @@ public class LocationService
if (updated is not null) if (updated is not null)
{ {
await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute); if (updated.BuildingType == "LogCamp")
{
await _resources.AddWoodGenerationAsync(username, _logCampWoodPerMinute);
}
else if (updated.BuildingType == "Mine")
{
await _resources.AddOreGenerationAsync(username, _mineOrePerMinute);
}
} }
} }
} }
@@ -551,4 +574,15 @@ public class LocationService
_ => "Field" _ => "Field"
}; };
} }
private static string? NormalizeBuildingType(string buildingType)
{
var trimmed = buildingType.Trim();
return trimmed.ToLowerInvariant() switch
{
"logcamp" or "log camp" => "LogCamp",
"mine" => "Mine",
_ => null
};
}
} }
@@ -33,9 +33,11 @@ public class ResourceService
.SetOnInsert(x => x.Username, username) .SetOnInsert(x => x.Username, username)
.SetOnInsert(x => x.Gold, _startingGold) .SetOnInsert(x => x.Gold, _startingGold)
.SetOnInsert(x => x.Wood, 0) .SetOnInsert(x => x.Wood, 0)
.SetOnInsert(x => x.Ore, 0)
.SetOnInsert(x => x.CreatedAtUtc, now) .SetOnInsert(x => x.CreatedAtUtc, now)
.SetOnInsert(x => x.LastGoldGeneratedAtUtc, now) .SetOnInsert(x => x.LastGoldGeneratedAtUtc, now)
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now) .SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute) .Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
.Set(x => x.UpdatedAtUtc, now); .Set(x => x.UpdatedAtUtc, now);
@@ -102,6 +104,28 @@ public class ResourceService
await _resources.UpdateOneAsync(x => x.Username == username, update); await _resources.UpdateOneAsync(x => x.Username == username, update);
} }
public async Task AddOreGenerationAsync(string username, int orePerMinute)
{
if (orePerMinute <= 0)
{
return;
}
var resources = await EnsureAsync(username);
var now = DateTime.UtcNow;
var update = Builders<PlayerResources>.Update
.Inc(x => x.OrePerMinute, orePerMinute)
.Set(x => x.UpdatedAtUtc, now);
if (resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000)
{
update = update.Set(x => x.LastOreGeneratedAtUtc, now);
}
await _resources.UpdateOneAsync(x => x.Username == username, update);
}
private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources) private async Task<PlayerResources> ApplyGoldGenerationAsync(PlayerResources resources)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@@ -111,8 +135,11 @@ public class ResourceService
var woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000 var woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000
? 0 ? 0
: (int)Math.Floor((now - resources.LastWoodGeneratedAtUtc).TotalMinutes); : (int)Math.Floor((now - resources.LastWoodGeneratedAtUtc).TotalMinutes);
var oreElapsedMinutes = resources.OrePerMinute <= 0 || resources.LastOreGeneratedAtUtc.Year < 2000
? 0
: (int)Math.Floor((now - resources.LastOreGeneratedAtUtc).TotalMinutes);
if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0) if (goldElapsedMinutes <= 0 && woodElapsedMinutes <= 0 && oreElapsedMinutes <= 0)
{ {
return resources; return resources;
} }
@@ -135,6 +162,13 @@ public class ResourceService
.Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes)); .Set(x => x.LastWoodGeneratedAtUtc, resources.LastWoodGeneratedAtUtc.AddMinutes(woodElapsedMinutes));
} }
if (oreElapsedMinutes > 0)
{
update = update
.Inc(x => x.Ore, oreElapsedMinutes * resources.OrePerMinute)
.Set(x => x.LastOreGeneratedAtUtc, resources.LastOreGeneratedAtUtc.AddMinutes(oreElapsedMinutes));
}
return await _resources.FindOneAndUpdateAsync( return await _resources.FindOneAndUpdateAsync(
x => x.Username == resources.Username, x => x.Username == resources.Username,
update, update,
@@ -34,6 +34,7 @@ public class UnitTrainingService
public async Task<PlayerUnitsResponse> GetMineAsync(string username) public async Task<PlayerUnitsResponse> GetMineAsync(string username)
{ {
await CompleteFinishedTrainingAsync(username); await CompleteFinishedTrainingAsync(username);
await _training.DeleteManyAsync(x => x.Username == username && x.Status == "Consumed");
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");
@@ -85,18 +86,11 @@ public class UnitTrainingService
{ {
await CompleteFinishedTrainingAsync(username); await CompleteFinishedTrainingAsync(username);
var now = DateTime.UtcNow; var consumed = await _training.FindOneAndDeleteAsync(
var update = Builders<UnitTraining>.Update
.Set(x => x.Status, "Consumed")
.Set(x => x.UpdatedAtUtc, now);
var consumed = await _training.FindOneAndUpdateAsync(
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed", x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
update, new FindOneAndDeleteOptions<UnitTraining>
new FindOneAndUpdateOptions<UnitTraining>
{ {
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc), Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc)
ReturnDocument = ReturnDocument.After
}); });
return consumed is not null; return consumed is not null;
+3 -1
View File
@@ -29,7 +29,9 @@
}, },
"Buildings": { "Buildings": {
"LogCampWoodPerMinute": 5, "LogCampWoodPerMinute": 5,
"LogCampBuildSeconds": 60 "LogCampBuildSeconds": 60,
"MineOrePerMinute": 5,
"MineBuildSeconds": 60
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }
@@ -17,3 +17,5 @@ data:
Units__ScoutTrainingSeconds: "60" Units__ScoutTrainingSeconds: "60"
Buildings__LogCampWoodPerMinute: "5" Buildings__LogCampWoodPerMinute: "5"
Buildings__LogCampBuildSeconds: "60" Buildings__LogCampBuildSeconds: "60"
Buildings__MineOrePerMinute: "5"
Buildings__MineBuildSeconds: "60"