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_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]
+1 -1
View File
@@ -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
};
}
+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.
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;
+3 -1
View File
@@ -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"