feat(entities): integració del sistema d'entitats al GameScene
This commit is contained in:
1 parent
96b71b0f65
commit
7191a08618
16 files changed
+683
-15
No files matched your search
@@ -12,9 +12,11 @@ void Entity::update(float delta_time) {
|
||||
}
|
||||
|
||||
void Entity::render() {
|
||||
// Andamiatge: el render real es cablejarà quan s'integri al GameScene.
|
||||
// Les subclasses poden sobreescriure si volen lògica especial (p.ex.
|
||||
// no dibuixar-se en estat inactiu); de moment la base no fa res.
|
||||
// No-op de base. El dibuix real el fa `EntityManager::renderAll` (viu
|
||||
// a `entity_manager_loadlevel.cpp`, que sí depèn de `Surface`). Així
|
||||
// `entity.cpp` no arrossega el pipeline de render al smoke test
|
||||
// stand-alone. Les subclasses futures poden sobreescriure si volen
|
||||
// lògica pròpia (p.ex. render condicional segons estat).
|
||||
}
|
||||
|
||||
void Entity::onPlayerContact(Player* /*player*/) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -10,9 +11,10 @@
|
||||
// Forward-declaracions: mantenim aquest header lleuger. `Player` viu a
|
||||
// `source/game/entities/player.hpp` i només es referencia per referència
|
||||
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
||||
// seu header propi.
|
||||
// seu header propi. `Surface` és el backend de render paletitzat del motor.
|
||||
class Player;
|
||||
class EventBus;
|
||||
class Surface;
|
||||
|
||||
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
||||
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
||||
@@ -78,11 +80,27 @@ class Entity {
|
||||
// estàtiques de moment) el poden ignorar.
|
||||
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||
|
||||
// Assigna la caixa de render en píxels. La invoca `EntityManager` durant
|
||||
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
|
||||
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
|
||||
|
||||
// Assigna la surface per al dibuix. `shared_ptr` perquè les surfaces
|
||||
// viuen al `Resource::Cache` i es comparteixen. Si no s'assigna,
|
||||
// `render()` no fa res.
|
||||
void setSurface(std::shared_ptr<Surface> surface) { surface_ = std::move(surface); }
|
||||
|
||||
// Accessor públic (la viewable rep per tipus complet `shared_ptr`, però
|
||||
// el valor pot ser nul). L'usa `EntityManager::renderAll` per dibuixar
|
||||
// sense dependre de la dispatch virtual — el dibuix és centralitzat al
|
||||
// manager per tal de confinar la dependència cap a `Surface`.
|
||||
[[nodiscard]] auto getSurface() const -> const std::shared_ptr<Surface>& { return surface_; }
|
||||
|
||||
protected:
|
||||
std::string name_;
|
||||
TilePos tile_{};
|
||||
SDL_FRect pixel_rect_{};
|
||||
std::optional<AnimationComponent> anim_;
|
||||
std::shared_ptr<Surface> surface_{nullptr};
|
||||
bool collides_{false};
|
||||
bool interactable_{false};
|
||||
EventBus* bus_{nullptr};
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
|
||||
// NOTA: `loadFromLevel` viu a `entity_manager_loadlevel.cpp` per evitar
|
||||
// arrossegar `Resource::Cache`, `Surface` i les seues dependències (GIF,
|
||||
// àudio) a consumidors que només volen la base del manager — en
|
||||
// particular, el smoke test stand-alone.
|
||||
|
||||
EntityManager::EntityManager() {
|
||||
// Subscripció al bus: cada sarcòfag obert incrementa el comptador;
|
||||
// quan iguala el total registrat, obrim la porta. La subscripció
|
||||
@@ -16,10 +21,19 @@ EntityManager::EntityManager() {
|
||||
door_->open();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscripció al moviment del jugador: quan publica `on_tile_entered`
|
||||
// amb payload `TilePos`, fem fan-out a totes les entitats perquè els
|
||||
// sarcòfags puguin actualitzar el seu `visited_`.
|
||||
tile_sub_id_ = bus_.subscribe("on_tile_entered", [this](const Event& ev) {
|
||||
const auto* tile = std::get_if<TilePos>(&ev.payload);
|
||||
if (tile != nullptr) { notifyPlayerEnteredTile(tile->col, tile->row); }
|
||||
});
|
||||
}
|
||||
|
||||
EntityManager::~EntityManager() {
|
||||
bus_.unsubscribe(sarc_sub_id_);
|
||||
bus_.unsubscribe(tile_sub_id_);
|
||||
}
|
||||
|
||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||
@@ -53,3 +67,8 @@ void EntityManager::update(float delta_time) {
|
||||
void EntityManager::notifyPlayerEnteredTile(int col, int row) {
|
||||
for (auto& e : entities_) { e->notifyTileEntered(col, row); }
|
||||
}
|
||||
|
||||
// NOTA: `renderAll` es defineix a `entity_manager_loadlevel.cpp`. Conté
|
||||
// blits via `Surface::render`, que dragaria el pipeline complet de render
|
||||
// (GIF, Canvas, Screen, àudio...) al smoke test. Separar-lo manté el smoke
|
||||
// autocontingut.
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/event_bus.hpp"
|
||||
@@ -47,11 +48,26 @@ class EntityManager {
|
||||
// Tick de totes les entitats.
|
||||
void update(float delta_time);
|
||||
|
||||
// Dibuix de totes les entitats. Sense ordenació per z: la crida dura
|
||||
// l'ordre d'inserció. El z-order fi (interleaving amb el Player per
|
||||
// feetY) queda per a una tasca posterior.
|
||||
void renderAll();
|
||||
|
||||
// Fan-out del hook de moviment de jugador: per a cada sarcòfag,
|
||||
// cridar notifyTileEntered. Les entitats no-sarcòfag ho ignoren per
|
||||
// defecte (veure Entity::notifyTileEntered).
|
||||
void notifyPlayerEnteredTile(int col, int row);
|
||||
|
||||
// Carrega els spawns d'un YAML de nivell i els afegeix al manager:
|
||||
// 1. Llegeix el bloc `entities:` via EntityLoader.
|
||||
// 2. Per cada spawn: crea via factory, carrega la seva surface al
|
||||
// Resource::Cache, calcula pixel_rect_ segons el tipus
|
||||
// (sarc: Map::getSarcRect; altres: col/row del tilemap) i l'afegeix.
|
||||
// 3. Loguea `Entities: N loaded` per facilitar el smoke d'arrencada.
|
||||
// Requereix que Map::loadFrom ja s'hagi cridat (necessita
|
||||
// `Map::tile_size`, `tilemap_origin_*` i `getSarcRect`).
|
||||
void loadFromLevel(const std::string& level_yaml_path);
|
||||
|
||||
[[nodiscard]] auto getBus() -> EventBus& { return bus_; }
|
||||
[[nodiscard]] auto entityCount() const -> std::size_t { return entities_.size(); }
|
||||
[[nodiscard]] auto sarcophagusCount() const -> std::size_t { return total_sarcs_; }
|
||||
@@ -64,4 +80,5 @@ class EntityManager {
|
||||
std::size_t total_sarcs_{0};
|
||||
std::size_t opened_sarcs_{0};
|
||||
EventBus::SubId sarc_sub_id_{0};
|
||||
EventBus::SubId tile_sub_id_{0};
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
// Implementació d'`EntityManager::loadFromLevel`. Viu en un TU separat
|
||||
// perquè aquesta és l'única via per la qual el manager toca
|
||||
// `Resource::Cache`, `Surface` i les seues dependències (GIF, àudio). Els
|
||||
// consumidors que només volen la base del manager (p.ex. el smoke test
|
||||
// stand-alone) enllacen `entity_manager.cpp` sense arrossegar aquest fitxer.
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/concrete/door.hpp"
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
#include "game/entities/entity.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_loader.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
// Render centralitzat al manager per confinar la dependència cap a
|
||||
// `Surface`. Les entitats exposen `getSurface()` + `getPixelRect()` i el
|
||||
// manager dispatcha el blit. Si una entitat no té surface (trigger
|
||||
// invisible, encara sense sprite) simplement es salta.
|
||||
void EntityManager::renderAll() {
|
||||
for (const auto& e : entities_) {
|
||||
const auto& surface = e->getSurface();
|
||||
if (surface == nullptr) { continue; }
|
||||
const auto RECT = e->getPixelRect();
|
||||
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Llegeix `sprite.size` o `sprite.path` d'una plantilla i retorna
|
||||
// (surface_key, pixel_size). Si falta algun camp, torna valors
|
||||
// raonables (empty key → no carrega; size [16,16] per defecte).
|
||||
struct SpriteInfo {
|
||||
std::string key; // clau al Resource::Cache (basename del GIF)
|
||||
std::string full_path; // ruta absoluta per a loadSurface
|
||||
float width{16.0F};
|
||||
float height{16.0F};
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
auto readSpriteInfo(const fkyaml::node& tmpl, const std::string& type) -> SpriteInfo {
|
||||
SpriteInfo info;
|
||||
if (!tmpl.contains("sprite")) { return info; }
|
||||
const auto& s = tmpl["sprite"];
|
||||
if (s.contains("path")) {
|
||||
const auto REL = s["path"].get_value<std::string>();
|
||||
info.key = std::filesystem::path(REL).filename().string();
|
||||
info.full_path = "data/entities/" + type + "/" + REL;
|
||||
info.valid = true;
|
||||
}
|
||||
if (s.contains("size")) {
|
||||
const auto& sz = s["size"];
|
||||
if (sz.is_sequence() && sz.size() == 2) {
|
||||
info.width = static_cast<float>(sz[0].get_value<int>());
|
||||
info.height = static_cast<float>(sz[1].get_value<int>());
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// Coordenades píxel (canvas) d'una cel·la (col, row) del tilemap. El
|
||||
// grid del tilemap està 16-alineat respecte a `tilemap_origin_*`, que
|
||||
// pot ser negatiu per overscan CPC.
|
||||
auto tileToPixel(int col, int row) -> SDL_FPoint {
|
||||
return {
|
||||
.x = static_cast<float>((col * Map::tile_size) + Map::tilemap_origin_x),
|
||||
.y = static_cast<float>((row * Map::tile_size) + Map::tilemap_origin_y),
|
||||
};
|
||||
}
|
||||
|
||||
// Converteix un rect píxel (canvas) a la seua cantonada superior-esquerra
|
||||
// en coordenades de tile del tilemap. Només per a sarcòfags, que naixen
|
||||
// ancorats a una cel·la exacta de la graella.
|
||||
struct TileAnchor {
|
||||
int col;
|
||||
int row;
|
||||
};
|
||||
auto pixelToTileAnchor(const SDL_FRect& rect) -> TileAnchor {
|
||||
return {
|
||||
.col = (static_cast<int>(rect.x) - Map::tilemap_origin_x) / Map::tile_size,
|
||||
.row = (static_cast<int>(rect.y) - Map::tilemap_origin_y) / Map::tile_size,
|
||||
};
|
||||
}
|
||||
|
||||
// Crea l'entitat per factory i li adjunta la surface si n'hi ha. Retorna
|
||||
// nullptr si la plantilla no es pot carregar o si el tipus no està
|
||||
// registrat al factory — el loop principal ja imprimeix el missatge.
|
||||
auto buildEntity(const std::string& type, SpriteInfo& sprite_out) -> std::unique_ptr<Entity> {
|
||||
fkyaml::node tmpl;
|
||||
try {
|
||||
tmpl = EntityLoader::loadTemplate(type);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "EntityManager: " << e.what() << '\n';
|
||||
return nullptr;
|
||||
}
|
||||
auto entity = EntityFactory::instance().create(type, tmpl);
|
||||
if (entity == nullptr) {
|
||||
std::cerr << "EntityManager: tipus desconegut '" << type << "'\n";
|
||||
return nullptr;
|
||||
}
|
||||
sprite_out = readSpriteInfo(tmpl, type);
|
||||
if (sprite_out.valid) {
|
||||
Resource::Cache::get().loadSurface(sprite_out.key, sprite_out.full_path);
|
||||
entity->setSurface(Resource::Cache::get().getSurface(sprite_out.key));
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Col·loca i afegeix un Sarcophagus a partir del spawn. `entity` es consumeix
|
||||
// en èxit (ownership transferit al manager); en error es destrueix aquí.
|
||||
// Membre privat amagat dins del TU: `EntityManager::` qualifica, però com
|
||||
// que no hi ha declaració al header, només és visible a aquest fitxer.
|
||||
namespace {
|
||||
auto placeSarcophagus(EntityManager& manager, std::unique_ptr<Entity> entity, const fkyaml::node& spawn) -> bool {
|
||||
if (!spawn.contains("sarc_position")) {
|
||||
std::cerr << "EntityManager: sarcophagus sense sarc_position\n";
|
||||
return false;
|
||||
}
|
||||
const auto& sp = spawn["sarc_position"];
|
||||
const auto RECT = Map::getSarcRect(sp[0].get_value<int>(), sp[1].get_value<int>());
|
||||
const auto ANCHOR = pixelToTileAnchor(RECT);
|
||||
entity->setPixelRect(RECT);
|
||||
|
||||
// Downcast segur: la factory ens garanteix que "sarcophagus" crea
|
||||
// un `Sarcophagus`. Usant el retorn de `release` al ctor del nou
|
||||
// `unique_ptr` evitem el lint `bugprone-unused-return-value`.
|
||||
if (dynamic_cast<Sarcophagus*>(entity.get()) == nullptr) {
|
||||
std::cerr << "EntityManager: la factory no ha creat un Sarcophagus\n";
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<Sarcophagus> sarc(static_cast<Sarcophagus*>(entity.release()));
|
||||
sarc->place(ANCHOR.col, ANCHOR.row, {});
|
||||
manager.addSarcophagus(std::move(sarc));
|
||||
return true;
|
||||
}
|
||||
|
||||
auto placeGeneric(EntityManager& manager, std::unique_ptr<Entity> entity, const std::string& type, const SpriteInfo& sprite, const fkyaml::node& spawn) -> bool {
|
||||
if (!spawn.contains("position")) {
|
||||
std::cerr << "EntityManager: " << type << " sense position\n";
|
||||
return false;
|
||||
}
|
||||
const auto& p = spawn["position"];
|
||||
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||
entity->setPixelRect({.x = PX.x, .y = PX.y, .w = sprite.width, .h = sprite.height});
|
||||
|
||||
if (type == "door") {
|
||||
auto* door_ptr = dynamic_cast<Door*>(entity.get());
|
||||
manager.add(std::move(entity));
|
||||
if (door_ptr != nullptr) { manager.setDoor(door_ptr); }
|
||||
} else {
|
||||
manager.add(std::move(entity));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void EntityManager::loadFromLevel(const std::string& level_yaml_path) {
|
||||
const auto SPAWNS = EntityLoader::readSpawns(level_yaml_path);
|
||||
int loaded = 0;
|
||||
|
||||
for (const auto& spawn : SPAWNS) {
|
||||
if (!spawn.contains("type")) { continue; }
|
||||
const auto TYPE = spawn["type"].get_value<std::string>();
|
||||
|
||||
SpriteInfo sprite;
|
||||
auto entity = buildEntity(TYPE, sprite);
|
||||
if (entity == nullptr) { continue; }
|
||||
|
||||
const bool OK = (TYPE == "sarcophagus")
|
||||
? placeSarcophagus(*this, std::move(entity), spawn)
|
||||
: placeGeneric(*this, std::move(entity), TYPE, sprite, spawn);
|
||||
if (OK) { ++loaded; }
|
||||
}
|
||||
|
||||
std::cout << "EntityManager: " << loaded << " entitats carregades des de "
|
||||
<< level_yaml_path << '\n';
|
||||
}
|
||||
@@ -16,15 +16,23 @@ class Entity;
|
||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||
// via `Entity::setEventBus`. Aquest sistema cobreix les necessitats
|
||||
// actuals (sarcophagus_opened, level_complete, player_hit) sense portar-hi
|
||||
// senyalització més pesada.
|
||||
// actuals (sarcophagus_opened, level_complete, player_hit, on_tile_entered)
|
||||
// sense portar-hi senyalització més pesada.
|
||||
//
|
||||
// Decisió: `std::variant` per al payload — evita any/void* i facilita
|
||||
// consumir events tipats amb `std::get_if`. Si apareixen més tipus de
|
||||
// payload, s'amplia aquí sense tocar cridants existents.
|
||||
|
||||
// Tile (col, row) — payload de `on_tile_entered`. No és un `pair<int,int>`
|
||||
// per evitar alias ambigus al variant (dos ints sense nom).
|
||||
struct TilePos {
|
||||
int col{0};
|
||||
int row{0};
|
||||
};
|
||||
|
||||
struct Event {
|
||||
std::string name;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*>;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos>;
|
||||
Payload payload;
|
||||
};
|
||||
|
||||
@@ -60,6 +68,9 @@ class EventBus {
|
||||
void publish(const std::string& name, int value) {
|
||||
publish(Event{.name = name, .payload = value});
|
||||
}
|
||||
void publish(const std::string& name, TilePos tile) {
|
||||
publish(Event{.name = name, .payload = tile});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
@@ -106,6 +108,34 @@ void Player::update(float delta_time) {
|
||||
if (Map::collides(baseBox())) { setPosY(OLD_Y); }
|
||||
|
||||
clampToPlayfield();
|
||||
|
||||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||||
// així els sarcòfags no reben falsos positius mentre el Player rellisca
|
||||
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
|
||||
// al bus; l'EntityManager farà fan-out a notifyTileEntered.
|
||||
if (bus_ != nullptr) {
|
||||
const auto [COL, ROW] = getCurrentTile();
|
||||
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||||
last_tile_col_ = COL;
|
||||
last_tile_row_ = ROW;
|
||||
bus_->publish("on_tile_entered", TilePos{.col = COL, .row = ROW});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Player::getCurrentTile() const -> std::pair<int, int> {
|
||||
// El grid del tilemap és de `Map::tile_size` píxels i està 16-alineat
|
||||
// respecte a l'origen del tilemap (pot ser negatiu per overscan). El
|
||||
// centre de la caixa de col·lisió ens dóna el tile on "està" el Player.
|
||||
const auto BOX = baseBox();
|
||||
const float CX = BOX.x + (BOX.w * 0.5F);
|
||||
const float CY = BOX.y + (BOX.h * 0.5F);
|
||||
const int COL = static_cast<int>(std::floor(
|
||||
(CX - static_cast<float>(Map::tilemap_origin_x)) / static_cast<float>(Map::tile_size)));
|
||||
const int ROW = static_cast<int>(std::floor(
|
||||
(CY - static_cast<float>(Map::tilemap_origin_y)) / static_cast<float>(Map::tile_size)));
|
||||
return {COL, ROW};
|
||||
}
|
||||
|
||||
void Player::clampToPlayfield() {
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
|
||||
class EventBus;
|
||||
|
||||
class Player : public AnimatedSprite {
|
||||
public:
|
||||
// Mapeig de tecles per al moviment del jugador. Cada jugador local en pot
|
||||
@@ -47,6 +50,16 @@ class Player : public AnimatedSprite {
|
||||
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
|
||||
}
|
||||
|
||||
// Injecta el bus del nivell. El Player publica `on_tile_entered` cada
|
||||
// vegada que el centre de la seua caixa de col·lisió entra en una
|
||||
// cel·la nova del tilemap. Si el bus no s'injecta, no emet res.
|
||||
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||
|
||||
// Tile actual del jugador (centre de `baseBox()` convertit a
|
||||
// coordenades de tile del tilemap, usant Map::tile_size i
|
||||
// Map::tilemap_origin_*). Retorna (col, row).
|
||||
[[nodiscard]] auto getCurrentTile() const -> std::pair<int, int>;
|
||||
|
||||
private:
|
||||
// Rectangle de col·lisió del jugador, usat pel mapa de colisions i com
|
||||
// a referència per al Z-order.
|
||||
@@ -72,4 +85,13 @@ class Player : public AnimatedSprite {
|
||||
void (Player::*set_pos)(float));
|
||||
|
||||
Controls controls_;
|
||||
|
||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||
// Player només en guarda un punter per publicar events.
|
||||
EventBus* bus_{nullptr};
|
||||
|
||||
// Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè
|
||||
// el primer frame en què el bus estigui connectat ja emeti un event.
|
||||
int last_tile_col_{-9999};
|
||||
int last_tile_row_{-9999};
|
||||
};
|
||||
Reference in new issue
Block a user