Adding player sensing
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 47s
Deploy Promiscuity Character API / deploy (push) Successful in 59s
Deploy Promiscuity Inventory API / deploy (push) Successful in 47s
Deploy Promiscuity Locations API / deploy (push) Successful in 47s
k8s smoke test / test (push) Successful in 9s
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 47s
Deploy Promiscuity Character API / deploy (push) Successful in 59s
Deploy Promiscuity Inventory API / deploy (push) Successful in 47s
Deploy Promiscuity Locations API / deploy (push) Successful in 47s
k8s smoke test / test (push) Successful in 9s
This commit is contained in:
parent
c596b760cf
commit
bca8e3374a
@ -6,6 +6,8 @@ const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
|||||||
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
||||||
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
||||||
const CHARACTER_SLOT_COUNT := 6
|
const CHARACTER_SLOT_COUNT := 6
|
||||||
|
const VISIBLE_CHARACTERS_REFRESH_INTERVAL := 5.0
|
||||||
|
const HEARTBEAT_INTERVAL := 10.0
|
||||||
|
|
||||||
@export var tile_size := 8.0
|
@export var tile_size := 8.0
|
||||||
@export var block_height := 1.0
|
@export var block_height := 1.0
|
||||||
@ -34,8 +36,9 @@ const CHARACTER_SLOT_COUNT := 6
|
|||||||
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
||||||
|
|
||||||
var _center_coord := Vector2i.ZERO
|
var _center_coord := Vector2i.ZERO
|
||||||
var _tiles_root: Node3D
|
var _tiles_root: Node3D
|
||||||
var _tracked_node: Node3D
|
var _remote_players_root: Node3D
|
||||||
|
var _tracked_node: Node3D
|
||||||
var _tile_nodes: Dictionary = {}
|
var _tile_nodes: Dictionary = {}
|
||||||
var _camera_start_offset := Vector3(0.0, 6.0, 10.0)
|
var _camera_start_offset := Vector3(0.0, 6.0, 10.0)
|
||||||
var _border_material: StandardMaterial3D
|
var _border_material: StandardMaterial3D
|
||||||
@ -53,12 +56,20 @@ var _inventory_request_in_flight := false
|
|||||||
var _character_inventory_items: Array = []
|
var _character_inventory_items: Array = []
|
||||||
var _selected_character_item_id := ""
|
var _selected_character_item_id := ""
|
||||||
var _selected_ground_item_id := ""
|
var _selected_ground_item_id := ""
|
||||||
|
var _visible_characters_in_flight := false
|
||||||
|
var _heartbeat_in_flight := false
|
||||||
|
var _visible_character_refresh_elapsed := 0.0
|
||||||
|
var _heartbeat_elapsed := 0.0
|
||||||
|
var _remote_character_nodes: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_tiles_root = Node3D.new()
|
_tiles_root = Node3D.new()
|
||||||
_tiles_root.name = "GeneratedTiles"
|
_tiles_root.name = "GeneratedTiles"
|
||||||
add_child(_tiles_root)
|
add_child(_tiles_root)
|
||||||
|
_remote_players_root = Node3D.new()
|
||||||
|
_remote_players_root.name = "RemotePlayers"
|
||||||
|
add_child(_remote_players_root)
|
||||||
|
|
||||||
if _camera:
|
if _camera:
|
||||||
_camera_start_offset = _camera.position
|
_camera_start_offset = _camera.position
|
||||||
@ -75,6 +86,7 @@ func _ready() -> void:
|
|||||||
_block.visible = false
|
_block.visible = false
|
||||||
_deactivate_player_for_load()
|
_deactivate_player_for_load()
|
||||||
await _load_existing_locations()
|
await _load_existing_locations()
|
||||||
|
_refresh_visible_characters()
|
||||||
_ensure_selected_location_exists(_center_coord)
|
_ensure_selected_location_exists(_center_coord)
|
||||||
_rebuild_tiles(_center_coord)
|
_rebuild_tiles(_center_coord)
|
||||||
_move_player_to_coord(_center_coord)
|
_move_player_to_coord(_center_coord)
|
||||||
@ -84,6 +96,14 @@ func _ready() -> void:
|
|||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if not _locations_loaded:
|
if not _locations_loaded:
|
||||||
return
|
return
|
||||||
|
_visible_character_refresh_elapsed += _delta
|
||||||
|
_heartbeat_elapsed += _delta
|
||||||
|
if _visible_character_refresh_elapsed >= VISIBLE_CHARACTERS_REFRESH_INTERVAL:
|
||||||
|
_visible_character_refresh_elapsed = 0.0
|
||||||
|
_refresh_visible_characters()
|
||||||
|
if _heartbeat_elapsed >= HEARTBEAT_INTERVAL:
|
||||||
|
_heartbeat_elapsed = 0.0
|
||||||
|
_send_presence_heartbeat()
|
||||||
if _inventory_menu.visible:
|
if _inventory_menu.visible:
|
||||||
return
|
return
|
||||||
var target_world_pos := _get_stream_position()
|
var target_world_pos := _get_stream_position()
|
||||||
@ -899,7 +919,7 @@ func _selected_location_name(coord: Vector2i) -> String:
|
|||||||
return "Location %d,%d" % [coord.x, coord.y]
|
return "Location %d,%d" % [coord.x, coord.y]
|
||||||
|
|
||||||
|
|
||||||
func _load_existing_locations() -> void:
|
func _load_existing_locations() -> void:
|
||||||
_locations_refresh_in_flight = true
|
_locations_refresh_in_flight = true
|
||||||
_locations_loaded = false
|
_locations_loaded = false
|
||||||
_known_locations.clear()
|
_known_locations.clear()
|
||||||
@ -974,9 +994,10 @@ func _load_existing_locations() -> void:
|
|||||||
_locations_loaded = true
|
_locations_loaded = true
|
||||||
_locations_refresh_in_flight = false
|
_locations_refresh_in_flight = false
|
||||||
_rebuild_tiles(_center_coord)
|
_rebuild_tiles(_center_coord)
|
||||||
if _queued_locations_refresh:
|
_refresh_visible_characters()
|
||||||
_queued_locations_refresh = false
|
if _queued_locations_refresh:
|
||||||
_queue_locations_refresh()
|
_queued_locations_refresh = false
|
||||||
|
_queue_locations_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _queue_locations_refresh() -> void:
|
func _queue_locations_refresh() -> void:
|
||||||
@ -1110,12 +1131,13 @@ func _sync_character_coord(coord: Vector2i) -> void:
|
|||||||
_sync_character_coord_async(coord)
|
_sync_character_coord_async(coord)
|
||||||
|
|
||||||
|
|
||||||
func _sync_character_coord_async(coord: Vector2i) -> void:
|
func _sync_character_coord_async(coord: Vector2i) -> void:
|
||||||
var response := await CharacterService.update_character_coord(_character_id, coord)
|
var response := await CharacterService.update_character_coord(_character_id, coord)
|
||||||
if response.get("ok", false):
|
if response.get("ok", false):
|
||||||
_persisted_coord = coord
|
_persisted_coord = coord
|
||||||
SelectedCharacter.set_coord(coord)
|
SelectedCharacter.set_coord(coord)
|
||||||
else:
|
_refresh_visible_characters()
|
||||||
|
else:
|
||||||
push_warning("Failed to persist character coord to %s,%s: status=%s error=%s body=%s" % [
|
push_warning("Failed to persist character coord to %s,%s: status=%s error=%s body=%s" % [
|
||||||
coord.x,
|
coord.x,
|
||||||
coord.y,
|
coord.y,
|
||||||
@ -1224,6 +1246,120 @@ func _fetch_location_inventory(location_id: String) -> Array:
|
|||||||
|
|
||||||
var payload := parsed as Dictionary
|
var payload := parsed as Dictionary
|
||||||
return _parse_floor_inventory_items(payload.get("items", []))
|
return _parse_floor_inventory_items(payload.get("items", []))
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_visible_characters() -> void:
|
||||||
|
if _visible_characters_in_flight or _character_id.is_empty():
|
||||||
|
return
|
||||||
|
_refresh_visible_characters_async()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_visible_characters_async() -> void:
|
||||||
|
_visible_characters_in_flight = true
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
|
||||||
|
var err := request.request("%s/%s/visible-characters" % [CHARACTER_API_URL, _character_id], headers, HTTPClient.METHOD_GET)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
push_warning("Failed to request visible characters: %s" % err)
|
||||||
|
_visible_characters_in_flight = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
_visible_characters_in_flight = false
|
||||||
|
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
var response_body: String = result[3].get_string_from_utf8()
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
push_warning("Failed to load visible characters (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
if typeof(parsed) != TYPE_ARRAY:
|
||||||
|
return
|
||||||
|
|
||||||
|
_apply_visible_characters(parsed as Array)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_visible_characters(characters: Array) -> void:
|
||||||
|
var wanted_ids := {}
|
||||||
|
for character_variant in characters:
|
||||||
|
if typeof(character_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var character := character_variant as Dictionary
|
||||||
|
var character_id := String(character.get("id", "")).strip_edges()
|
||||||
|
if character_id.is_empty() or character_id == _character_id:
|
||||||
|
continue
|
||||||
|
wanted_ids[character_id] = true
|
||||||
|
_upsert_remote_character(character)
|
||||||
|
|
||||||
|
for character_id in _remote_character_nodes.keys():
|
||||||
|
if wanted_ids.has(character_id):
|
||||||
|
continue
|
||||||
|
var remote_node := _remote_character_nodes[character_id] as Node3D
|
||||||
|
if remote_node:
|
||||||
|
remote_node.queue_free()
|
||||||
|
_remote_character_nodes.erase(character_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _upsert_remote_character(character: Dictionary) -> void:
|
||||||
|
var character_id := String(character.get("id", "")).strip_edges()
|
||||||
|
var coord_value: Variant = character.get("coord", {})
|
||||||
|
if typeof(coord_value) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var coord_dict := coord_value as Dictionary
|
||||||
|
var coord := Vector2i(int(coord_dict.get("x", 0)), int(coord_dict.get("y", 0)))
|
||||||
|
|
||||||
|
var remote_root := _remote_character_nodes.get(character_id) as Node3D
|
||||||
|
if remote_root == null:
|
||||||
|
remote_root = Node3D.new()
|
||||||
|
remote_root.name = "RemoteCharacter_%s" % character_id
|
||||||
|
_remote_players_root.add_child(remote_root)
|
||||||
|
|
||||||
|
var visual := _player_visual.duplicate() as Node3D
|
||||||
|
visual.name = "Visual"
|
||||||
|
visual.visible = true
|
||||||
|
remote_root.add_child(visual)
|
||||||
|
|
||||||
|
var label := Label3D.new()
|
||||||
|
label.name = "NameLabel"
|
||||||
|
label.position = Vector3(0.0, 1.8, 0.0)
|
||||||
|
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||||
|
label.pixel_size = 0.01
|
||||||
|
label.outline_size = 10
|
||||||
|
remote_root.add_child(label)
|
||||||
|
|
||||||
|
_remote_character_nodes[character_id] = remote_root
|
||||||
|
|
||||||
|
remote_root.position = Vector3(coord.x * tile_size, _get_tile_surface_y(coord), coord.y * tile_size)
|
||||||
|
var name_label := remote_root.get_node_or_null("NameLabel") as Label3D
|
||||||
|
if name_label:
|
||||||
|
name_label.text = String(character.get("name", "Player")).strip_edges()
|
||||||
|
|
||||||
|
|
||||||
|
func _send_presence_heartbeat() -> void:
|
||||||
|
if _heartbeat_in_flight or _character_id.is_empty():
|
||||||
|
return
|
||||||
|
_send_presence_heartbeat_async()
|
||||||
|
|
||||||
|
|
||||||
|
func _send_presence_heartbeat_async() -> void:
|
||||||
|
_heartbeat_in_flight = true
|
||||||
|
var response := await CharacterService.heartbeat_character(_character_id)
|
||||||
|
if not response.get("ok", false):
|
||||||
|
push_warning("Failed to send presence heartbeat: status=%s error=%s body=%s" % [
|
||||||
|
response.get("status", "n/a"),
|
||||||
|
response.get("error", ""),
|
||||||
|
response.get("body", "")
|
||||||
|
])
|
||||||
|
_heartbeat_in_flight = false
|
||||||
|
|
||||||
|
|
||||||
func _get_biome_material(tile: MeshInstance3D, biome_key: String) -> Material:
|
func _get_biome_material(tile: MeshInstance3D, biome_key: String) -> Material:
|
||||||
|
|||||||
@ -24,6 +24,10 @@ func update_character_coord(character_id: String, coord: Vector2i) -> Dictionary
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return await _request(HTTPClient.METHOD_PUT, url, payload)
|
return await _request(HTTPClient.METHOD_PUT, url, payload)
|
||||||
|
|
||||||
|
func heartbeat_character(character_id: String) -> Dictionary:
|
||||||
|
var url := "%s/%s/heartbeat" % [CHARACTER_API_URL, character_id]
|
||||||
|
return await _request(HTTPClient.METHOD_POST, url, "")
|
||||||
|
|
||||||
func _request(method: int, url: String, body: String = "") -> Dictionary:
|
func _request(method: int, url: String, body: String = "") -> Dictionary:
|
||||||
var request := HTTPRequest.new()
|
var request := HTTPRequest.new()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ namespace CharacterApi.Controllers;
|
|||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class CharactersController : ControllerBase
|
public class CharactersController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan PresenceTimeout = TimeSpan.FromSeconds(45);
|
||||||
private readonly CharacterStore _characters;
|
private readonly CharacterStore _characters;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
@ -43,6 +44,7 @@ public class CharactersController : ControllerBase
|
|||||||
Name = req.Name.Trim(),
|
Name = req.Name.Trim(),
|
||||||
Coord = new Coord { X = 0, Y = 0 },
|
Coord = new Coord { X = 0, Y = 0 },
|
||||||
VisionRadius = 3,
|
VisionRadius = 3,
|
||||||
|
LastSeenUtc = DateTime.UtcNow,
|
||||||
CreatedUtc = DateTime.UtcNow
|
CreatedUtc = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,6 +91,8 @@ public class CharactersController : ControllerBase
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _characters.TouchAsync(id);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Visible locations requested for character {CharacterId} at ({X},{Y}) radius {VisionRadius} by user {UserId}",
|
"Visible locations requested for character {CharacterId} at ({X},{Y}) radius {VisionRadius} by user {UserId}",
|
||||||
character.Id,
|
character.Id,
|
||||||
@ -164,6 +168,53 @@ public class CharactersController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/visible-characters")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> VisibleCharacters(string id)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var allowAnyOwner = User.IsInRole("SUPER");
|
||||||
|
var character = await _characters.GetByIdAsync(id);
|
||||||
|
if (character is null)
|
||||||
|
return NotFound();
|
||||||
|
if (!allowAnyOwner && character.OwnerUserId != userId)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
await _characters.TouchAsync(id);
|
||||||
|
|
||||||
|
var onlineSinceUtc = DateTime.UtcNow.Subtract(PresenceTimeout);
|
||||||
|
var visibleCharacters = await _characters.GetVisibleOthersAsync(
|
||||||
|
id,
|
||||||
|
character.Coord.X,
|
||||||
|
character.Coord.Y,
|
||||||
|
character.VisionRadius > 0 ? character.VisionRadius : 3,
|
||||||
|
onlineSinceUtc);
|
||||||
|
|
||||||
|
return Ok(visibleCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/heartbeat")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> Heartbeat(string id)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var allowAnyOwner = User.IsInRole("SUPER");
|
||||||
|
var character = await _characters.GetByIdAsync(id);
|
||||||
|
if (character is null)
|
||||||
|
return NotFound();
|
||||||
|
if (!allowAnyOwner && character.OwnerUserId != userId)
|
||||||
|
return Forbid();
|
||||||
|
|
||||||
|
await _characters.TouchAsync(id);
|
||||||
|
return Ok(new { lastSeenUtc = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/coord")]
|
[HttpPut("{id}/coord")]
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
public async Task<IActionResult> UpdateCoord(string id, [FromBody] UpdateCharacterCoordRequest req)
|
public async Task<IActionResult> UpdateCoord(string id, [FromBody] UpdateCharacterCoordRequest req)
|
||||||
|
|||||||
@ -17,5 +17,7 @@ public class Character
|
|||||||
|
|
||||||
public int VisionRadius { get; set; } = 3;
|
public int VisionRadius { get; set; } = 3;
|
||||||
|
|
||||||
|
public DateTime LastSeenUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|||||||
12
microservices/CharacterApi/Models/VisibleCharacter.cs
Normal file
12
microservices/CharacterApi/Models/VisibleCharacter.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace CharacterApi.Models;
|
||||||
|
|
||||||
|
public class VisibleCharacter
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Coord Coord { get; set; } = new();
|
||||||
|
|
||||||
|
public DateTime LastSeenUtc { get; set; }
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@ public class CharacterStore
|
|||||||
|
|
||||||
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
||||||
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
||||||
|
var coordIndex = Builders<Character>.IndexKeys.Ascending("Coord.X").Ascending("Coord.Y");
|
||||||
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(coordIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
||||||
@ -30,11 +32,38 @@ public class CharacterStore
|
|||||||
public async Task<bool> UpdateCoordAsync(string id, Coord coord)
|
public async Task<bool> UpdateCoordAsync(string id, Coord coord)
|
||||||
{
|
{
|
||||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
var update = Builders<Character>.Update.Set(c => c.Coord, coord);
|
var update = Builders<Character>.Update
|
||||||
|
.Set(c => c.Coord, coord)
|
||||||
|
.Set(c => c.LastSeenUtc, DateTime.UtcNow);
|
||||||
var result = await _col.UpdateOneAsync(filter, update);
|
var result = await _col.UpdateOneAsync(filter, update);
|
||||||
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TouchAsync(string id)
|
||||||
|
{
|
||||||
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
|
var update = Builders<Character>.Update.Set(c => c.LastSeenUtc, DateTime.UtcNow);
|
||||||
|
var result = await _col.UpdateOneAsync(filter, update);
|
||||||
|
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<VisibleCharacter>> GetVisibleOthersAsync(string characterId, int x, int y, int radius, DateTime onlineSinceUtc) =>
|
||||||
|
_col.Find(c =>
|
||||||
|
c.Id != characterId &&
|
||||||
|
c.Coord.X >= x - radius &&
|
||||||
|
c.Coord.X <= x + radius &&
|
||||||
|
c.Coord.Y >= y - radius &&
|
||||||
|
c.Coord.Y <= y + radius &&
|
||||||
|
c.LastSeenUtc >= onlineSinceUtc)
|
||||||
|
.Project(c => new VisibleCharacter
|
||||||
|
{
|
||||||
|
Id = c.Id ?? string.Empty,
|
||||||
|
Name = c.Name,
|
||||||
|
Coord = c.Coord,
|
||||||
|
LastSeenUtc = c.LastSeenUtc
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||||
{
|
{
|
||||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user