adding mines
This commit is contained in:
@@ -36,12 +36,17 @@ var training_completion_refresh_requested := false
|
||||
var building_started_unix := 0.0
|
||||
var building_completes_unix := 0.0
|
||||
var building_completion_refresh_requested := false
|
||||
var active_building_label := "Build"
|
||||
var claim_button: Button
|
||||
var build_log_camp_button: Button
|
||||
var build_mine_button: Button
|
||||
var selected_q := 0
|
||||
var selected_r := 0
|
||||
var selected_can_claim := 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:
|
||||
@@ -239,6 +244,7 @@ func _build_ui() -> void:
|
||||
building_box.add_child(building_bar)
|
||||
|
||||
train_button = _small_button("Train Scout 50g")
|
||||
train_button.visible = false
|
||||
train_button.pressed.connect(_train_scout)
|
||||
bottom_content.add_child(train_button)
|
||||
|
||||
@@ -252,6 +258,11 @@ func _build_ui() -> void:
|
||||
build_log_camp_button.pressed.connect(_build_log_camp)
|
||||
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.request_completed.connect(_on_home_request_completed)
|
||||
add_child(http_request)
|
||||
@@ -298,15 +309,63 @@ func _on_tile_selected(q: int, r: int, terrain: String) -> void:
|
||||
selected_q = q
|
||||
selected_r = r
|
||||
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:
|
||||
claim_button.disabled = not selected_can_claim
|
||||
claim_button.disabled = not _can_claim_selected_tile()
|
||||
if build_log_camp_button:
|
||||
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:
|
||||
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:
|
||||
if not session.is_empty():
|
||||
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 wood: int = int(resources.get("wood", 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", ""))
|
||||
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:
|
||||
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():
|
||||
training_started_unix = 0.0
|
||||
@@ -573,6 +637,9 @@ func _claim_selected_tile() -> void:
|
||||
if not selected_can_claim:
|
||||
coordinate_label.text = "Select a visible unclaimed tile to send a scout."
|
||||
return
|
||||
if completed_scout_count <= 0:
|
||||
coordinate_label.text = "Train a scout before claiming territory."
|
||||
return
|
||||
|
||||
var access_token: String = str(session.get("accessToken", ""))
|
||||
if access_token.is_empty():
|
||||
@@ -623,16 +690,32 @@ func _build_log_camp() -> void:
|
||||
coordinate_label.text = "Select controlled Forest territory without a building."
|
||||
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", ""))
|
||||
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
|
||||
|
||||
if build_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
||||
return
|
||||
|
||||
build_log_camp_button.disabled = true
|
||||
coordinate_label.text = "Building Log Camp at q:%d r:%d..." % [selected_q, selected_r]
|
||||
if building_type == "Mine":
|
||||
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(
|
||||
LOCATIONS_BASE_URL + "/api/locations/build",
|
||||
PackedStringArray([
|
||||
@@ -640,36 +723,42 @@ func _build_log_camp() -> void:
|
||||
"Authorization: Bearer %s" % access_token
|
||||
]),
|
||||
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:
|
||||
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."
|
||||
|
||||
|
||||
func _on_build_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var text: String = body.get_string_from_utf8()
|
||||
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_mine_button.disabled = not selected_can_build_mine
|
||||
return
|
||||
|
||||
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_mine_button.disabled = not selected_can_build_mine
|
||||
_refresh_player_state()
|
||||
return
|
||||
|
||||
selected_can_build_log_camp = false
|
||||
selected_can_build_mine = false
|
||||
build_log_camp_button.disabled = true
|
||||
build_mine_button.disabled = true
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
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_completes_unix = _parse_utc_timestamp(str(location.get("buildingCompletesAtUtc", "")))
|
||||
building_completion_refresh_requested = false
|
||||
_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()
|
||||
_refresh_player_state()
|
||||
|
||||
@@ -733,9 +822,9 @@ func _update_building_timer() -> void:
|
||||
building_bar.value = elapsed / duration
|
||||
|
||||
if remaining > 0:
|
||||
building_label.text = "Log Camp: %ds" % remaining
|
||||
building_label.text = "%s: %ds" % [active_building_label, remaining]
|
||||
else:
|
||||
building_label.text = "Log Camp: ready"
|
||||
building_label.text = "%s: ready" % active_building_label
|
||||
|
||||
if remaining <= 0 and not building_completion_refresh_requested:
|
||||
building_completion_refresh_requested = true
|
||||
@@ -746,6 +835,7 @@ func _update_building_timer() -> void:
|
||||
func _sync_active_building_timer(locations: Array, username: String) -> void:
|
||||
var best_started := 0.0
|
||||
var best_completes := 0.0
|
||||
var best_label := "Build"
|
||||
|
||||
for item in locations:
|
||||
if typeof(item) != TYPE_DICTIONARY:
|
||||
@@ -754,7 +844,8 @@ func _sync_active_building_timer(locations: Array, username: String) -> void:
|
||||
var location: Dictionary = item
|
||||
if str(location.get("ownerUsername", "")) != username:
|
||||
continue
|
||||
if str(location.get("buildingType", "")) != "LogCamp":
|
||||
var building_type: String = str(location.get("buildingType", ""))
|
||||
if building_type.is_empty():
|
||||
continue
|
||||
if str(location.get("buildingStatus", "")) != "Building":
|
||||
continue
|
||||
@@ -766,13 +857,25 @@ func _sync_active_building_timer(locations: Array, username: String) -> void:
|
||||
if best_completes <= 0.0 or completes < best_completes:
|
||||
best_started = started
|
||||
best_completes = completes
|
||||
best_label = _building_display_name(building_type)
|
||||
|
||||
building_started_unix = best_started
|
||||
building_completes_unix = best_completes
|
||||
active_building_label = best_label
|
||||
building_completion_refresh_requested = false
|
||||
_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:
|
||||
if timestamp.is_empty():
|
||||
return 0.0
|
||||
|
||||
@@ -163,9 +163,13 @@ func _draw_tile(tile: Dictionary) -> void:
|
||||
var visible_location: Dictionary = visible_tiles.get(key, {})
|
||||
var building_type: String = str(visible_location.get("buildingType", ""))
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
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:
|
||||
var marker_center: Vector2 = center + Vector2(0, -height * zoom - 1.0)
|
||||
var width: float = 20.0 * zoom
|
||||
@@ -368,6 +427,13 @@ func set_visible_locations(locations: Array, username: String) -> void:
|
||||
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:
|
||||
return "%d,%d" % [q, r]
|
||||
|
||||
@@ -420,6 +486,8 @@ func _tile_display_label(key: String, data: Dictionary) -> String:
|
||||
var building_label: String = ""
|
||||
if building_type == "LogCamp":
|
||||
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))):
|
||||
return "%s | Your home%s" % [terrain, building_label]
|
||||
|
||||
@@ -52,7 +52,7 @@ public class LocationResponse
|
||||
Label = location.Label,
|
||||
Level = location.Level,
|
||||
BuildingType = location.BuildingType,
|
||||
BuildingStatus = location.BuildingType == "LogCamp" && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
BuildingStatus = (location.BuildingType == "LogCamp" || location.BuildingType == "Mine") && string.IsNullOrWhiteSpace(location.BuildingStatus)
|
||||
? "Built"
|
||||
: location.BuildingStatus,
|
||||
BuildingStartedAtUtc = location.BuildingStartedAtUtc,
|
||||
|
||||
@@ -14,8 +14,11 @@ public class PlayerResources
|
||||
public int GoldPerMinute { get; set; } = 10;
|
||||
public int Wood { 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 LastWoodGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastOreGeneratedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
public DateTime CreatedAtUtc { 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 Wood { get; set; }
|
||||
public int WoodPerMinute { get; set; }
|
||||
public int Ore { get; set; }
|
||||
public int OrePerMinute { get; set; }
|
||||
public DateTime LastGoldGeneratedAtUtc { get; set; }
|
||||
public DateTime LastWoodGeneratedAtUtc { get; set; }
|
||||
public DateTime LastOreGeneratedAtUtc { get; set; }
|
||||
public DateTime UpdatedAtUtc { get; set; }
|
||||
|
||||
public static PlayerResourcesResponse FromResources(PlayerResources resources) => new()
|
||||
@@ -18,8 +21,11 @@ public class PlayerResourcesResponse
|
||||
GoldPerMinute = resources.GoldPerMinute,
|
||||
Wood = resources.Wood,
|
||||
WoodPerMinute = resources.WoodPerMinute,
|
||||
Ore = resources.Ore,
|
||||
OrePerMinute = resources.OrePerMinute,
|
||||
LastGoldGeneratedAtUtc = resources.LastGoldGeneratedAtUtc,
|
||||
LastWoodGeneratedAtUtc = resources.LastWoodGeneratedAtUtc,
|
||||
LastOreGeneratedAtUtc = resources.LastOreGeneratedAtUtc,
|
||||
UpdatedAtUtc = resources.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
record is read.
|
||||
minute. Log Camps generate wood and Mines generate ore. 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. 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 are stored on location records. The first building type is `LogCamp`,
|
||||
which can be built on controlled Forest territory, takes 60 seconds to build,
|
||||
and adds 5 wood per minute to the owning player's resource generation after it
|
||||
finishes. Each location has one building slot; a building under construction
|
||||
occupies that slot.
|
||||
Buildings are stored on location records. `LogCamp` can be built on controlled
|
||||
Forest territory, takes 60 seconds to build, and adds 5 wood per minute after it
|
||||
finishes. `Mine` can be built on controlled Ore territory, takes 60 seconds to
|
||||
build, and adds 5 ore per minute after it finishes. Each location has one
|
||||
building slot; a building under construction occupies that slot.
|
||||
|
||||
Timer settings are stored in the MongoDB `GameSettings` collection and are
|
||||
seeded on first read:
|
||||
|
||||
- `ScoutTrainingSeconds`
|
||||
- `LogCampBuildSeconds`
|
||||
- `MineBuildSeconds`
|
||||
|
||||
@@ -7,6 +7,7 @@ public class GameSettingsService
|
||||
{
|
||||
public const string ScoutTrainingSeconds = "ScoutTrainingSeconds";
|
||||
public const string LogCampBuildSeconds = "LogCampBuildSeconds";
|
||||
public const string MineBuildSeconds = "MineBuildSeconds";
|
||||
|
||||
private readonly IMongoCollection<GameSetting> _settings;
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ public class LocationService
|
||||
private readonly int _homeSearchRadius;
|
||||
private readonly int _visibilityDepth;
|
||||
private readonly int _logCampWoodPerMinute;
|
||||
private readonly int _mineOrePerMinute;
|
||||
private readonly int _defaultLogCampBuildSeconds;
|
||||
private readonly int _defaultMineBuildSeconds;
|
||||
|
||||
public LocationService(IConfiguration cfg, ResourceService resources, GameSettingsService settings)
|
||||
{
|
||||
@@ -29,9 +31,15 @@ public class LocationService
|
||||
_logCampWoodPerMinute = int.TryParse(cfg["Buildings:LogCampWoodPerMinute"], out var logCampWoodPerMinute)
|
||||
? Math.Max(1, logCampWoodPerMinute)
|
||||
: 5;
|
||||
_mineOrePerMinute = int.TryParse(cfg["Buildings:MineOrePerMinute"], out var mineOrePerMinute)
|
||||
? Math.Max(1, mineOrePerMinute)
|
||||
: 5;
|
||||
_defaultLogCampBuildSeconds = int.TryParse(cfg["Buildings:LogCampBuildSeconds"], out var logCampBuildSeconds)
|
||||
? Math.Max(1, logCampBuildSeconds)
|
||||
: 60;
|
||||
_defaultMineBuildSeconds = int.TryParse(cfg["Buildings:MineBuildSeconds"], out var mineBuildSeconds)
|
||||
? Math.Max(1, mineBuildSeconds)
|
||||
: 60;
|
||||
|
||||
var coordinateIndex = Builders<Location>.IndexKeys
|
||||
.Ascending(x => x.Q)
|
||||
@@ -123,7 +131,8 @@ public class LocationService
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -140,17 +149,24 @@ public class LocationService
|
||||
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);
|
||||
|
||||
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
|
||||
.Set(x => x.BuildingType, "LogCamp")
|
||||
.Set(x => x.BuildingType, buildingType)
|
||||
.Set(x => x.BuildingStatus, "Building")
|
||||
.Set(x => x.BuildingStartedAtUtc, now)
|
||||
.Set(x => x.BuildingCompletesAtUtc, now.AddSeconds(buildSeconds))
|
||||
@@ -177,7 +193,7 @@ public class LocationService
|
||||
var now = DateTime.UtcNow;
|
||||
var readyBuildings = await _locations.Find(x =>
|
||||
x.OwnerUsername == username &&
|
||||
x.BuildingType == "LogCamp" &&
|
||||
(x.BuildingType == "LogCamp" || x.BuildingType == "Mine") &&
|
||||
x.BuildingStatus == "Building" &&
|
||||
x.BuildingCompletesAtUtc <= now)
|
||||
.ToListAsync();
|
||||
@@ -199,7 +215,14 @@ public class LocationService
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
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.Gold, _startingGold)
|
||||
.SetOnInsert(x => x.Wood, 0)
|
||||
.SetOnInsert(x => x.Ore, 0)
|
||||
.SetOnInsert(x => x.CreatedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastGoldGeneratedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastWoodGeneratedAtUtc, now)
|
||||
.SetOnInsert(x => x.LastOreGeneratedAtUtc, now)
|
||||
.Set(x => x.GoldPerMinute, _headquartersGoldPerMinute)
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
@@ -102,6 +104,28 @@ public class ResourceService
|
||||
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)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -111,8 +135,11 @@ public class ResourceService
|
||||
var woodElapsedMinutes = resources.WoodPerMinute <= 0 || resources.LastWoodGeneratedAtUtc.Year < 2000
|
||||
? 0
|
||||
: (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;
|
||||
}
|
||||
@@ -135,6 +162,13 @@ public class ResourceService
|
||||
.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(
|
||||
x => x.Username == resources.Username,
|
||||
update,
|
||||
|
||||
@@ -34,6 +34,7 @@ public class UnitTrainingService
|
||||
public async Task<PlayerUnitsResponse> GetMineAsync(string 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 completedScouts = all.Count(x => x.UnitType == ScoutUnitType && x.Status == "Completed");
|
||||
@@ -85,18 +86,11 @@ public class UnitTrainingService
|
||||
{
|
||||
await CompleteFinishedTrainingAsync(username);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var update = Builders<UnitTraining>.Update
|
||||
.Set(x => x.Status, "Consumed")
|
||||
.Set(x => x.UpdatedAtUtc, now);
|
||||
|
||||
var consumed = await _training.FindOneAndUpdateAsync(
|
||||
var consumed = await _training.FindOneAndDeleteAsync(
|
||||
x => x.Username == username && x.UnitType == ScoutUnitType && x.Status == "Completed",
|
||||
update,
|
||||
new FindOneAndUpdateOptions<UnitTraining>
|
||||
new FindOneAndDeleteOptions<UnitTraining>
|
||||
{
|
||||
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc),
|
||||
ReturnDocument = ReturnDocument.After
|
||||
Sort = Builders<UnitTraining>.Sort.Ascending(x => x.CompletedAtUtc).Ascending(x => x.CreatedAtUtc)
|
||||
});
|
||||
|
||||
return consumed is not null;
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
},
|
||||
"Buildings": {
|
||||
"LogCampWoodPerMinute": 5,
|
||||
"LogCampBuildSeconds": 60
|
||||
"LogCampBuildSeconds": 60,
|
||||
"MineOrePerMinute": 5,
|
||||
"MineBuildSeconds": 60
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -17,3 +17,5 @@ data:
|
||||
Units__ScoutTrainingSeconds: "60"
|
||||
Buildings__LogCampWoodPerMinute: "5"
|
||||
Buildings__LogCampBuildSeconds: "60"
|
||||
Buildings__MineOrePerMinute: "5"
|
||||
Buildings__MineBuildSeconds: "60"
|
||||
|
||||
Reference in New Issue
Block a user