From 9477c0282ffba2826565f34a9631dae384e3e5e4 Mon Sep 17 00:00:00 2001 From: Jason Jiang Date: Mon, 3 Aug 2026 00:09:14 -0400 Subject: [PATCH] feat: render only ground the player can currently see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terrain arrived unfiltered, so the client drew the entire world and merely desaturated what lay outside vision. The server now sends only the ground a player can actually see, and sends the whole visible set again whenever it moves. Vision is ephemeral: it lasts while a city or army is watching and no longer. Applying an update therefore clears the planes before writing the new set — without that, ground would stay lit after the army that revealed it walked away, and the client would slowly accumulate a map it is not entitled to. Fog is now a single source of truth. The client used to recompute Chebyshev distance from owned cities to decide what was fogged, which no longer agrees with the server: armies grant vision too, and the client does not track their radius. Both the shroud and the vision-edge glow now read the server's set instead. Also surfaces terrain and any special resource on the selected tile, and replaces "Beyond visibility range" with wording that matches the mechanic, since a tile is dark because nothing of yours is watching it rather than because it is far away. Co-Authored-By: Claude Opus 5 (1M context) --- proto/cityio/service/v1/map.proto | 39 +++++---- proto/cityio/service/v1/user.proto | 5 ++ src/lib/game/world/index.ts | 49 +++++++++++ src/lib/gen/cityio/service/v1/map_pb.ts | 91 +++++++++++++------- src/lib/gen/cityio/service/v1/user_pb.ts | 13 ++- src/routes/game/+layout.svelte | 19 ++++- src/routes/game/+page.svelte | 104 ++++++++++++++--------- 7 files changed, 228 insertions(+), 92 deletions(-) diff --git a/proto/cityio/service/v1/map.proto b/proto/cityio/service/v1/map.proto index 6429a7b..1d62b9b 100644 --- a/proto/cityio/service/v1/map.proto +++ b/proto/cityio/service/v1/map.proto @@ -30,27 +30,36 @@ message GetTileResponse { message GetTerrainRequest {} -// GetTerrainResponse carries the whole map in one call. +// VisibleTerrain is the ground a player can see right now. // -// Each plane is packed one byte per tile in row-major order, so the value for -// (x, y) is at index y * width + x. At the current map size that is about 5 KB -// per plane — small enough that chunking or viewport queries would cost more -// than they save, and it lets the client cache the world for the session. +// Vision is ephemeral: it comes from cities, from armies while they stand +// somewhere, and from structures held against an enemy. Tiles therefore leave +// this set as often as they enter it, so it is always sent whole rather than as +// a reveal-only delta — replacing the set outright is the only way client and +// server cannot disagree about what is still lit. // -// Terrain is generated by the server from `seed` and does not change, so this -// response is stable for the lifetime of a world. +// Tiles are addressed by row-major index (y * width + x). Each plane holds one +// byte per entry of `indices`, in the same order. +message VisibleTerrain { + repeated int32 indices = 1; + bytes terrain = 2; // cityio.entity.v1.TerrainType + bytes relief = 3; // cityio.entity.v1.ReliefType + bytes feature = 4; // cityio.entity.v1.FeatureType + bytes special = 5; // cityio.entity.v1.SpecialType + // rivers holds a 6-bit mask per tile: bit i means a river continues toward + // neighbour i. Both tiles either side of a step carry the reciprocal bit, so + // each renders its own half and rivers occupy no tile of their own. + bytes rivers = 6; +} + +// GetTerrainResponse bootstraps a client with the map's dimensions and whatever +// ground it can currently see. Everything else is unknown, and stays unknown +// until something of the player's is watching it. message GetTerrainResponse { int32 width = 1; int32 height = 2; int64 seed = 3; - bytes terrain = 4; // cityio.entity.v1.TerrainType per tile - bytes relief = 5; // cityio.entity.v1.ReliefType per tile - bytes feature = 6; // cityio.entity.v1.FeatureType per tile - bytes special = 7; // cityio.entity.v1.SpecialType per tile - // rivers holds a 6-bit mask per tile: bit i means a river continues toward - // neighbour i. Both tiles either side of a step carry the reciprocal bit, so - // each renders its own half and rivers occupy no tile of their own. - bytes rivers = 8; + VisibleTerrain visible = 4; } // MapService serves world snapshots read from the persistence layer. diff --git a/proto/cityio/service/v1/user.proto b/proto/cityio/service/v1/user.proto index bedb30d..be609de 100644 --- a/proto/cityio/service/v1/user.proto +++ b/proto/cityio/service/v1/user.proto @@ -5,6 +5,7 @@ package cityio.service.v1; import "cityio/entity/v1/common.proto"; import "cityio/entity/v1/user.proto"; import "cityio/entity/v1/bag.proto"; +import "cityio/service/v1/map.proto"; message RegisterRequest { string email = 1; @@ -42,6 +43,10 @@ message StreamStateRequest {} // StreamStateResponse wraps an EntityBag pushed to a client whenever state changes. message StreamStateResponse { cityio.entity.v1.EntityBag entities = 1; + // The player's currently visible ground, sent whole whenever it changes and + // omitted when it has not. Absent on most ticks, since vision only moves when + // a city, army or watchtower does. + optional VisibleTerrain visible_terrain = 2; } // UserService manages player accounts and the per-user resource stream. diff --git a/src/lib/game/world/index.ts b/src/lib/game/world/index.ts index b187edd..2cb8115 100644 --- a/src/lib/game/world/index.ts +++ b/src/lib/game/world/index.ts @@ -18,6 +18,55 @@ export interface WorldMap { special: Uint8Array; /** 6-bit mask per tile: bit i means a river continues toward neighbor i. */ rivers: Uint8Array; + /** + * 1 where the player can see the ground right now. Vision is ephemeral — it + * lasts only while a city, army or held structure is watching — so tiles + * leave this set as readily as they enter it, and the planes above are only + * meaningful where it is set. + */ + visible: Uint8Array; +} + +export function emptyWorld(w: number, h: number, seed: bigint): WorldMap { + const n = w * h; + return { + w, + h, + seed, + terrain: new Uint8Array(n), + relief: new Uint8Array(n), + feature: new Uint8Array(n), + special: new Uint8Array(n), + rivers: new Uint8Array(n), + visible: new Uint8Array(n) + }; +} + +/** + * Replace what the player can see with the set the server just sent. + * + * The whole set arrives every time rather than a reveal-only delta, because + * losing sight of ground is as common as gaining it. Clearing first is what + * makes a tile actually go dark when the army watching it walks away. + */ +export function applyVisibleTerrain(world: WorldMap, v: { indices: number[]; terrain: Uint8Array; relief: Uint8Array; feature: Uint8Array; special: Uint8Array; rivers: Uint8Array }): void { + world.terrain.fill(0); + world.relief.fill(0); + world.feature.fill(0); + world.special.fill(0); + world.rivers.fill(0); + world.visible.fill(0); + + for (let k = 0; k < v.indices.length; k++) { + const i = v.indices[k]; + if (i < 0 || i >= world.visible.length) continue; + world.terrain[i] = v.terrain[k]; + world.relief[i] = v.relief[k]; + world.feature[i] = v.feature[k]; + world.special[i] = v.special[k]; + world.rivers[i] = v.rivers[k]; + world.visible[i] = 1; + } } export const isWater = (t: TerrainType) => t >= TerrainType.DEEP_OCEAN && t <= TerrainType.LAKE; diff --git a/src/lib/gen/cityio/service/v1/map_pb.ts b/src/lib/gen/cityio/service/v1/map_pb.ts index 8822def..80b2bd4 100644 --- a/src/lib/gen/cityio/service/v1/map_pb.ts +++ b/src/lib/gen/cityio/service/v1/map_pb.ts @@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file cityio/service/v1/map.proto. */ export const file_cityio_service_v1_map: GenFile = /*@__PURE__*/ - fileDesc("ChtjaXR5aW8vc2VydmljZS92MS9tYXAucHJvdG8SEWNpdHlpby5zZXJ2aWNlLnYxIg8KDUdldE1hcFJlcXVlc3QinwEKDkdldE1hcFJlc3BvbnNlEioKCGNpdHlfaWRzGAEgAygLMhguY2l0eWlvLmVudGl0eS52MS5DaXR5SWQSMgoMYnVpbGRpbmdfaWRzGAIgAygLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkEi0KCGVudGl0aWVzGAMgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcioAEKBFRpbGUSCQoBeBgBIAEoBRIJCgF5GAIgASgFEi4KB2NpdHlfaWQYAyABKAsyGC5jaXR5aW8uZW50aXR5LnYxLkNpdHlJZEgAiAEBEjYKC2J1aWxkaW5nX2lkGAQgASgLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkSAGIAQFCCgoIX2NpdHlfaWRCDgoMX2J1aWxkaW5nX2lkIj8KDkdldFRpbGVSZXF1ZXN0Ei0KBmNvb3JkcxgBIAEoCzIdLmNpdHlpby5lbnRpdHkudjEuQ29vcmRpbmF0ZXMiOAoPR2V0VGlsZVJlc3BvbnNlEiUKBHRpbGUYASABKAsyFy5jaXR5aW8uc2VydmljZS52MS5UaWxlIhMKEUdldFRlcnJhaW5SZXF1ZXN0IpQBChJHZXRUZXJyYWluUmVzcG9uc2USDQoFd2lkdGgYASABKAUSDgoGaGVpZ2h0GAIgASgFEgwKBHNlZWQYAyABKAMSDwoHdGVycmFpbhgEIAEoDBIOCgZyZWxpZWYYBSABKAwSDwoHZmVhdHVyZRgGIAEoDBIPCgdzcGVjaWFsGAcgASgMEg4KBnJpdmVycxgIIAEoDDKIAgoKTWFwU2VydmljZRJNCgZHZXRNYXASIC5jaXR5aW8uc2VydmljZS52MS5HZXRNYXBSZXF1ZXN0GiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0TWFwUmVzcG9uc2USUAoHR2V0VGlsZRIhLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRpbGVSZXF1ZXN0GiIuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGlsZVJlc3BvbnNlElkKCkdldFRlcnJhaW4SJC5jaXR5aW8uc2VydmljZS52MS5HZXRUZXJyYWluUmVxdWVzdBolLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRlcnJhaW5SZXNwb25zZWIGcHJvdG8z", [file_cityio_entity_v1_common, file_cityio_entity_v1_bag]); + fileDesc("ChtjaXR5aW8vc2VydmljZS92MS9tYXAucHJvdG8SEWNpdHlpby5zZXJ2aWNlLnYxIg8KDUdldE1hcFJlcXVlc3QinwEKDkdldE1hcFJlc3BvbnNlEioKCGNpdHlfaWRzGAEgAygLMhguY2l0eWlvLmVudGl0eS52MS5DaXR5SWQSMgoMYnVpbGRpbmdfaWRzGAIgAygLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkEi0KCGVudGl0aWVzGAMgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcioAEKBFRpbGUSCQoBeBgBIAEoBRIJCgF5GAIgASgFEi4KB2NpdHlfaWQYAyABKAsyGC5jaXR5aW8uZW50aXR5LnYxLkNpdHlJZEgAiAEBEjYKC2J1aWxkaW5nX2lkGAQgASgLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkSAGIAQFCCgoIX2NpdHlfaWRCDgoMX2J1aWxkaW5nX2lkIj8KDkdldFRpbGVSZXF1ZXN0Ei0KBmNvb3JkcxgBIAEoCzIdLmNpdHlpby5lbnRpdHkudjEuQ29vcmRpbmF0ZXMiOAoPR2V0VGlsZVJlc3BvbnNlEiUKBHRpbGUYASABKAsyFy5jaXR5aW8uc2VydmljZS52MS5UaWxlIhMKEUdldFRlcnJhaW5SZXF1ZXN0InQKDlZpc2libGVUZXJyYWluEg8KB2luZGljZXMYASADKAUSDwoHdGVycmFpbhgCIAEoDBIOCgZyZWxpZWYYAyABKAwSDwoHZmVhdHVyZRgEIAEoDBIPCgdzcGVjaWFsGAUgASgMEg4KBnJpdmVycxgGIAEoDCJ1ChJHZXRUZXJyYWluUmVzcG9uc2USDQoFd2lkdGgYASABKAUSDgoGaGVpZ2h0GAIgASgFEgwKBHNlZWQYAyABKAMSMgoHdmlzaWJsZRgEIAEoCzIhLmNpdHlpby5zZXJ2aWNlLnYxLlZpc2libGVUZXJyYWluMogCCgpNYXBTZXJ2aWNlEk0KBkdldE1hcBIgLmNpdHlpby5zZXJ2aWNlLnYxLkdldE1hcFJlcXVlc3QaIS5jaXR5aW8uc2VydmljZS52MS5HZXRNYXBSZXNwb25zZRJQCgdHZXRUaWxlEiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGlsZVJlcXVlc3QaIi5jaXR5aW8uc2VydmljZS52MS5HZXRUaWxlUmVzcG9uc2USWQoKR2V0VGVycmFpbhIkLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRlcnJhaW5SZXF1ZXN0GiUuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGVycmFpblJlc3BvbnNlYgZwcm90bzM", [file_cityio_entity_v1_common, file_cityio_entity_v1_bag]); /** * @generated from message cityio.service.v1.GetMapRequest @@ -138,59 +138,50 @@ export const GetTerrainRequestSchema: GenMessage = /*@__PURE_ messageDesc(file_cityio_service_v1_map, 5); /** - * GetTerrainResponse carries the whole map in one call. + * VisibleTerrain is the ground a player can see right now. * - * Each plane is packed one byte per tile in row-major order, so the value for - * (x, y) is at index y * width + x. At the current map size that is about 5 KB - * per plane — small enough that chunking or viewport queries would cost more - * than they save, and it lets the client cache the world for the session. + * Vision is ephemeral: it comes from cities, from armies while they stand + * somewhere, and from structures held against an enemy. Tiles therefore leave + * this set as often as they enter it, so it is always sent whole rather than as + * a reveal-only delta — replacing the set outright is the only way client and + * server cannot disagree about what is still lit. * - * Terrain is generated by the server from `seed` and does not change, so this - * response is stable for the lifetime of a world. + * Tiles are addressed by row-major index (y * width + x). Each plane holds one + * byte per entry of `indices`, in the same order. * - * @generated from message cityio.service.v1.GetTerrainResponse + * @generated from message cityio.service.v1.VisibleTerrain */ -export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> & { - /** - * @generated from field: int32 width = 1; - */ - width: number; - - /** - * @generated from field: int32 height = 2; - */ - height: number; - +export type VisibleTerrain = Message<"cityio.service.v1.VisibleTerrain"> & { /** - * @generated from field: int64 seed = 3; + * @generated from field: repeated int32 indices = 1; */ - seed: bigint; + indices: number[]; /** - * cityio.entity.v1.TerrainType per tile + * cityio.entity.v1.TerrainType * - * @generated from field: bytes terrain = 4; + * @generated from field: bytes terrain = 2; */ terrain: Uint8Array; /** - * cityio.entity.v1.ReliefType per tile + * cityio.entity.v1.ReliefType * - * @generated from field: bytes relief = 5; + * @generated from field: bytes relief = 3; */ relief: Uint8Array; /** - * cityio.entity.v1.FeatureType per tile + * cityio.entity.v1.FeatureType * - * @generated from field: bytes feature = 6; + * @generated from field: bytes feature = 4; */ feature: Uint8Array; /** - * cityio.entity.v1.SpecialType per tile + * cityio.entity.v1.SpecialType * - * @generated from field: bytes special = 7; + * @generated from field: bytes special = 5; */ special: Uint8Array; @@ -199,17 +190,53 @@ export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> * neighbour i. Both tiles either side of a step carry the reciprocal bit, so * each renders its own half and rivers occupy no tile of their own. * - * @generated from field: bytes rivers = 8; + * @generated from field: bytes rivers = 6; */ rivers: Uint8Array; }; +/** + * Describes the message cityio.service.v1.VisibleTerrain. + * Use `create(VisibleTerrainSchema)` to create a new message. + */ +export const VisibleTerrainSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cityio_service_v1_map, 6); + +/** + * GetTerrainResponse bootstraps a client with the map's dimensions and whatever + * ground it can currently see. Everything else is unknown, and stays unknown + * until something of the player's is watching it. + * + * @generated from message cityio.service.v1.GetTerrainResponse + */ +export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> & { + /** + * @generated from field: int32 width = 1; + */ + width: number; + + /** + * @generated from field: int32 height = 2; + */ + height: number; + + /** + * @generated from field: int64 seed = 3; + */ + seed: bigint; + + /** + * @generated from field: cityio.service.v1.VisibleTerrain visible = 4; + */ + visible?: VisibleTerrain | undefined; +}; + /** * Describes the message cityio.service.v1.GetTerrainResponse. * Use `create(GetTerrainResponseSchema)` to create a new message. */ export const GetTerrainResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cityio_service_v1_map, 6); + messageDesc(file_cityio_service_v1_map, 7); /** * MapService serves world snapshots read from the persistence layer. diff --git a/src/lib/gen/cityio/service/v1/user_pb.ts b/src/lib/gen/cityio/service/v1/user_pb.ts index 41877a9..45c03cd 100644 --- a/src/lib/gen/cityio/service/v1/user_pb.ts +++ b/src/lib/gen/cityio/service/v1/user_pb.ts @@ -10,13 +10,15 @@ import type { User } from "../../entity/v1/user_pb"; import { file_cityio_entity_v1_user } from "../../entity/v1/user_pb"; import type { EntityBag } from "../../entity/v1/bag_pb"; import { file_cityio_entity_v1_bag } from "../../entity/v1/bag_pb"; +import type { VisibleTerrain } from "./map_pb"; +import { file_cityio_service_v1_map } from "./map_pb"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file cityio/service/v1/user.proto. */ export const file_cityio_service_v1_user: GenFile = /*@__PURE__*/ - fileDesc("ChxjaXR5aW8vc2VydmljZS92MS91c2VyLnByb3RvEhFjaXR5aW8uc2VydmljZS52MSJECg9SZWdpc3RlclJlcXVlc3QSDQoFZW1haWwYASABKAkSEAoIdXNlcm5hbWUYAiABKAkSEAoIcGFzc3dvcmQYAyABKAkiTAoQUmVnaXN0ZXJSZXNwb25zZRIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQSDQoFdG9rZW4YAiABKAkiNAoMTG9naW5SZXF1ZXN0EhIKCmlkZW50aWZpZXIYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiRAoNTG9naW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCRIkCgR1c2VyGAIgASgLMhYuY2l0eWlvLmVudGl0eS52MS5Vc2VyIjsKDkdldFVzZXJSZXF1ZXN0EikKB3VzZXJfaWQYASABKAsyGC5jaXR5aW8uZW50aXR5LnYxLlVzZXJJZCI3Cg9HZXRVc2VyUmVzcG9uc2USJAoEdXNlchgBIAEoCzIWLmNpdHlpby5lbnRpdHkudjEuVXNlciI+ChFEZWxldGVVc2VyUmVxdWVzdBIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQiFAoSRGVsZXRlVXNlclJlc3BvbnNlIhQKElN0cmVhbVN0YXRlUmVxdWVzdCJEChNTdHJlYW1TdGF0ZVJlc3BvbnNlEi0KCGVudGl0aWVzGAEgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcyuwMKC1VzZXJTZXJ2aWNlElMKCFJlZ2lzdGVyEiIuY2l0eWlvLnNlcnZpY2UudjEuUmVnaXN0ZXJSZXF1ZXN0GiMuY2l0eWlvLnNlcnZpY2UudjEuUmVnaXN0ZXJSZXNwb25zZRJKCgVMb2dpbhIfLmNpdHlpby5zZXJ2aWNlLnYxLkxvZ2luUmVxdWVzdBogLmNpdHlpby5zZXJ2aWNlLnYxLkxvZ2luUmVzcG9uc2USUAoHR2V0VXNlchIhLmNpdHlpby5zZXJ2aWNlLnYxLkdldFVzZXJSZXF1ZXN0GiIuY2l0eWlvLnNlcnZpY2UudjEuR2V0VXNlclJlc3BvbnNlElkKCkRlbGV0ZVVzZXISJC5jaXR5aW8uc2VydmljZS52MS5EZWxldGVVc2VyUmVxdWVzdBolLmNpdHlpby5zZXJ2aWNlLnYxLkRlbGV0ZVVzZXJSZXNwb25zZRJeCgtTdHJlYW1TdGF0ZRIlLmNpdHlpby5zZXJ2aWNlLnYxLlN0cmVhbVN0YXRlUmVxdWVzdBomLmNpdHlpby5zZXJ2aWNlLnYxLlN0cmVhbVN0YXRlUmVzcG9uc2UwAWIGcHJvdG8z", [file_cityio_entity_v1_common, file_cityio_entity_v1_user, file_cityio_entity_v1_bag]); + fileDesc("ChxjaXR5aW8vc2VydmljZS92MS91c2VyLnByb3RvEhFjaXR5aW8uc2VydmljZS52MSJECg9SZWdpc3RlclJlcXVlc3QSDQoFZW1haWwYASABKAkSEAoIdXNlcm5hbWUYAiABKAkSEAoIcGFzc3dvcmQYAyABKAkiTAoQUmVnaXN0ZXJSZXNwb25zZRIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQSDQoFdG9rZW4YAiABKAkiNAoMTG9naW5SZXF1ZXN0EhIKCmlkZW50aWZpZXIYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiRAoNTG9naW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCRIkCgR1c2VyGAIgASgLMhYuY2l0eWlvLmVudGl0eS52MS5Vc2VyIjsKDkdldFVzZXJSZXF1ZXN0EikKB3VzZXJfaWQYASABKAsyGC5jaXR5aW8uZW50aXR5LnYxLlVzZXJJZCI3Cg9HZXRVc2VyUmVzcG9uc2USJAoEdXNlchgBIAEoCzIWLmNpdHlpby5lbnRpdHkudjEuVXNlciI+ChFEZWxldGVVc2VyUmVxdWVzdBIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQiFAoSRGVsZXRlVXNlclJlc3BvbnNlIhQKElN0cmVhbVN0YXRlUmVxdWVzdCKZAQoTU3RyZWFtU3RhdGVSZXNwb25zZRItCghlbnRpdGllcxgBIAEoCzIbLmNpdHlpby5lbnRpdHkudjEuRW50aXR5QmFnEj8KD3Zpc2libGVfdGVycmFpbhgCIAEoCzIhLmNpdHlpby5zZXJ2aWNlLnYxLlZpc2libGVUZXJyYWluSACIAQFCEgoQX3Zpc2libGVfdGVycmFpbjK7AwoLVXNlclNlcnZpY2USUwoIUmVnaXN0ZXISIi5jaXR5aW8uc2VydmljZS52MS5SZWdpc3RlclJlcXVlc3QaIy5jaXR5aW8uc2VydmljZS52MS5SZWdpc3RlclJlc3BvbnNlEkoKBUxvZ2luEh8uY2l0eWlvLnNlcnZpY2UudjEuTG9naW5SZXF1ZXN0GiAuY2l0eWlvLnNlcnZpY2UudjEuTG9naW5SZXNwb25zZRJQCgdHZXRVc2VyEiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0VXNlclJlcXVlc3QaIi5jaXR5aW8uc2VydmljZS52MS5HZXRVc2VyUmVzcG9uc2USWQoKRGVsZXRlVXNlchIkLmNpdHlpby5zZXJ2aWNlLnYxLkRlbGV0ZVVzZXJSZXF1ZXN0GiUuY2l0eWlvLnNlcnZpY2UudjEuRGVsZXRlVXNlclJlc3BvbnNlEl4KC1N0cmVhbVN0YXRlEiUuY2l0eWlvLnNlcnZpY2UudjEuU3RyZWFtU3RhdGVSZXF1ZXN0GiYuY2l0eWlvLnNlcnZpY2UudjEuU3RyZWFtU3RhdGVSZXNwb25zZTABYgZwcm90bzM", [file_cityio_entity_v1_common, file_cityio_entity_v1_user, file_cityio_entity_v1_bag, file_cityio_service_v1_map]); /** * @generated from message cityio.service.v1.RegisterRequest @@ -198,6 +200,15 @@ export type StreamStateResponse = Message<"cityio.service.v1.StreamStateResponse * @generated from field: cityio.entity.v1.EntityBag entities = 1; */ entities?: EntityBag | undefined; + + /** + * The player's currently visible ground, sent whole whenever it changes and + * omitted when it has not. Absent on most ticks, since vision only moves when + * a city, army or watchtower does. + * + * @generated from field: optional cityio.service.v1.VisibleTerrain visible_terrain = 2; + */ + visibleTerrain?: VisibleTerrain | undefined; }; /** diff --git a/src/routes/game/+layout.svelte b/src/routes/game/+layout.svelte index f5b1c93..5851865 100644 --- a/src/routes/game/+layout.svelte +++ b/src/routes/game/+layout.svelte @@ -1,6 +1,7 @@