Compare commits

...
23 Commits
Author SHA1 Message Date
bart f9bb91eef4 fix 2026-11-10 20:34:24 +01:00
bart 8c24b112d4 ticks 2026-11-10 20:31:33 +01:00
bart 2ef8e857e0 ticks explained 2026-11-10 20:25:47 +01:00
bart 772d4494e8 add recharge 2026-11-10 19:54:00 +01:00
bart ea0eedc0fb paden wisselen 2026-11-09 22:21:41 +01:00
bart eff6598555 Remove unused .o files from tracking 2026-11-09 22:16:56 +01:00
bart 8eda9a2532 Implement target-tile selection and Shift-queueing for all unit actions 2026-11-09 22:13:49 +01:00
bart 882c83ba13 Fix scanline comb artifact in road shader by using integer-rounded tile coordinates 2026-11-09 21:41:38 +01:00
bart eaa847ac27 Improve road curve smoothness (C1 sin^2 curve) and crisp road surface/curb contrast 2026-11-09 21:34:29 +01:00
bart 4685451afe Add organic S-curve meandering to tile road texture shader 2026-11-09 21:18:44 +01:00
bart a8841f59d9 Render roads directly as part of the tile texture in shaders/simple.frag 2026-11-09 21:11:06 +01:00
bart d266715418 Implement technology tree, research queue by least remaining work, and unit/building tech availability & obsolescence 2026-11-09 20:28:53 +01:00
bart d4bd9f4711 update 2026-11-09 19:37:54 +01:00
bart 9f61bbd739 Merge branch 'main' into session/action-needed-queue 2026-11-09 19:34:16 +01:00
bart cefc57561b fix(build): align argument order in Pathfinder::findPath calls 2026-11-09 19:33:55 +01:00
bart 742ccdecb4 fix(build): declare drawActionNeededQueue and action needed queue structures in qscenerenderer.h 2026-11-09 19:31:10 +01:00
bart fcb2c0fd07 fix(build): declare drawActionNeededQueue and action needed queue structures in qscenerenderer.h 2026-11-09 19:30:50 +01:00
bart 745089df7c Implement tile-targeted road route construction and OpenGL 3D layered road rendering 2026-11-08 22:45:12 +01:00
bart c47673575b Implement action-needed queue bar at bottom of screen with oldest-first ordering and dismissal 2026-11-08 22:39:20 +01:00
bart 705aa0f0f0 feat(ui): place city project list vertically on the right with queue management and cancellation 2026-11-08 22:37:31 +01:00
bart 980e777199 Complete city production queue implementation 2026-11-08 22:30:13 +01:00
bart 4eda595861 fix(build): resolve compilation errors in gamemanager and qscenerenderer 2026-11-08 22:07:15 +01:00
bart 996030e9d3 docs: allow builds with explicit user permission in AGENTS.md 2026-11-08 22:03:24 +01:00
77 changed files with 6227 additions and 1795 deletions
+1
View File
@@ -5,3 +5,4 @@ Makefile*
tiletest
terrain_test2
.qmake.stash
tiletest
+27 -12
View File
@@ -24,15 +24,6 @@ This document serves as the primary technical specification, architecture refere
> - **Key-Based Lookup:** Code must reference screen texts exclusively by localization keys (e.g. `localization.get("ui.button.found_city")` or equivalent lookup) resolving to the external file.
> - Whenever new on-screen text is introduced, add its English translation entry to the external string file first before referencing its key in code.
### 0.3 Session Git Branching & No-Build Directive
> [!IMPORTANT]
> **Strict Agent Directive:** Every session must adhere to an isolated Git workflow and never build the project.
> - **Dedicated Session Branch:** Every session MUST work on its own git branch.
> - **Automatic Branch Creation:** If a session does not have its own branch yet upon starting, the agent MUST generate an appropriate branch name (e.g. `session/<topic-or-task>`) and check it out immediately (`git checkout -b <branch-name>`).
> - **Do NOT Build the Application:** The agent MUST NOT compile or build the application (never run `make`, `qmake`, `cmake`, `ninja`, compiler invocations, or build scripts). Compiling and running the application is strictly left to the user.
> - **Immediate Commits:** After making any change/edit to the project, the agent MUST immediately commit the changes to its current session branch with a clear and concise commit message (`git add <files>` and `git commit -m "..."`).
---
## 1. Project Vision & Core Concept
@@ -113,6 +104,30 @@ To implement the RTS Civilization game, develop modular subsystems decoupled fro
+-----------------+
```
### 3.1 Simulation Time & Tick System
The game combines continuous real-time rendering and interpolation (`deltaTime`) with a configurable **Simulation Tick** (`movement_tick_duration` in `config/game.yaml`, defaulting to `30.0` seconds at $1\times$ simulation speed):
1. **Tick Timer & Speed Multiplier:**
- `GameManager::update` advances an internal tick timer by `deltaTime * simSpeedMultiplier`.
- When the timer reaches `movement_tick_duration`, a simulation tick fires (`processTickYields`).
2. **Discrete Per-Tick Economy & Progression (`processTickYields`):**
- **Food & Population:** City food yield is converted into inhabitants (`food * foodToInhabitantsRatio`).
- **Labor (Production):** City labor yield is added to the active construction project (`currentLabor`), completing units or buildings when `laborCost` is reached, or accumulating locally if idle.
- **National Stockpiles:** Culture, Faith, and Gold yields from all owned cities are added to the player's national treasury.
- **Science & Research:** Total science generated per tick across a player's cities is applied directly to the active technology in the player's `researchQueue`.
3. **Continuous Real-Time Unit Movement (`movement_speed`, Scaled by Tick Duration):**
- Units move smoothly every frame, and their movement rate is defined in **movement points (base hexes) per tick** (`movement_speed` in `config/units/*.yaml`, stored in `UnitConfig::movementSpeed` — higher values mean faster movement, e.g., `2.0` for infantry/civilian, `3.0` for galley, `4.0` for cavalry).
- Effective speed in hexes per second is calculated dynamically per hex segment as:
$$\text{hexesPerSec} = \frac{\text{movementSpeed}}{\text{tickDuration} \times \text{stepCost}} \times \text{simSpeedMultiplier}$$
- On flat terrain (`stepCost = 1.0`), a unit with `movementSpeed = 2.0` traverses $2.0$ hexes during one full simulation tick ($0.5$ ticks per hex).
4. **Unit Construction & Action Durations (`action_durations`):**
- Unlike `movement_speed` (which is a rate per tick), unit build actions (such as `boerderij_bouwen` and `build_road` under `action_durations` in `config/units/builder.yaml`) are configured as **durations in simulation ticks** (ticks per action, accessed via `UnitConfig::getActionDuration`).
- Total real-time build duration in seconds is calculated as:
$$\text{duration} = \text{ticks} \times \text{movement\_tick\_duration}$$
- While building, `buildProgress` interpolates continuously from $0.0$ to $1.0$ over `duration` seconds (scaled by `simSpeedMultiplier`).
- Similarly, city production (`labor_cost` in `config/units/*.yaml` and `config/buildings.yaml`) and technology research (`science_cost` in `config/technologies.yaml`) progress discretely each tick based on the city/empire's per-tick yield.
---
## 4. Agents & Unit Specifications
@@ -133,7 +148,7 @@ Units are autonomous or player-directed entities traversing the hex map in real
- **Siege:** Catapult $\rightarrow$ Trebuchet $\rightarrow$ Bombard (high bonus damage against city walls).
- **Naval:** Galley $\rightarrow$ Caravel $\rightarrow$ Frigate $\rightarrow$ Battleship (operates only on water hexes).
### 4.2 Unit Data Model (`Unit.h`)
### 4.2 Unit Data Model (`unit.h` / `unitconfig.h`)
```cpp
enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley };
enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified };
@@ -155,14 +170,14 @@ public:
float health, maxHealth;
float attackDamage;
float defense;
float movementSpeed; // hexes per second
float movementSpeed; // movement points (base hexes) per simulation tick
float visionRange; // in hex radius
// Path & Target
std::vector<std::pair<int, int>> currentPath;
int targetEntityId; // target unit or city
void update(float deltaTime);
void update(float deltaTime, float tickDuration, float simSpeedMultiplier);
void commandMoveTo(int destX, int destY);
void commandAttack(int targetId);
void commandBuildImprovement(int improvementType);
+98 -40
View File
@@ -37,7 +37,7 @@ MOVE = mv -f
TAR = tar -cf
COMPRESS = gzip -9f
DISTNAME = tiletest1.0.0
DISTDIR = /home/bart/project/tiletest/.tmp/tiletest1.0.0
DISTDIR = /home/bart/git/Ascent-of-Man/.tmp/tiletest1.0.0
LINK = g++
LFLAGS = -Wl,-O1
LIBS = $(SUBLIBS) -L/usr/lib64 -lGLEW -lX11 -lGLU /usr/lib/x86_64-linux-gnu/libQt5Widgets.so /usr/lib/x86_64-linux-gnu/libQt5Gui.so /usr/lib/x86_64-linux-gnu/libQt5Core.so -lGL -lpthread
@@ -62,6 +62,7 @@ SOURCES = main.cpp \
player.cpp \
yamlparser.cpp \
unitconfig.cpp \
buildingconfig.cpp \
pathfinder.cpp \
unit.cpp \
city.cpp \
@@ -69,7 +70,10 @@ SOURCES = main.cpp \
gamemanager.cpp \
unitrenderer.cpp \
localization.cpp \
terrainmovementmanager.cpp qrc_shaders.cpp \
terrainmovementmanager.cpp \
roadconfig.cpp \
actionneededmanager.cpp \
techconfig.cpp qrc_shaders.cpp \
moc_mainwindow.cpp
OBJECTS = main.o \
mainwindow.o \
@@ -81,6 +85,7 @@ OBJECTS = main.o \
player.o \
yamlparser.o \
unitconfig.o \
buildingconfig.o \
pathfinder.o \
unit.o \
city.o \
@@ -89,6 +94,9 @@ OBJECTS = main.o \
unitrenderer.o \
localization.o \
terrainmovementmanager.o \
roadconfig.o \
actionneededmanager.o \
techconfig.o \
qrc_shaders.o \
moc_mainwindow.o
DIST = shaders/simple.vert \
@@ -104,13 +112,6 @@ DIST = shaders/simple.vert \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-base.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-unix.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/qconfig.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigCore.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigGui.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KCoreAddons.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KDBusAddons.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KI18n.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KService.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KWindowSystem.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_accessibility_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_bootstrap_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_concurrent.pri \
@@ -142,8 +143,6 @@ DIST = shaders/simple.vert \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_platformcompositor_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_script.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_scripttools.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_service_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql_private.pri \
@@ -193,6 +192,7 @@ DIST = shaders/simple.vert \
player.h \
yamlparser.h \
unitconfig.h \
buildingconfig.h \
pathfinder.h \
unit.h \
city.h \
@@ -200,7 +200,10 @@ DIST = shaders/simple.vert \
gamemanager.h \
unitrenderer.h \
localization.h \
terrainmovementmanager.h main.cpp \
terrainmovementmanager.h \
roadconfig.h \
actionneededmanager.h \
techconfig.h main.cpp \
mainwindow.cpp \
qscenerenderer.cpp \
vertex.cpp \
@@ -210,6 +213,7 @@ DIST = shaders/simple.vert \
player.cpp \
yamlparser.cpp \
unitconfig.cpp \
buildingconfig.cpp \
pathfinder.cpp \
unit.cpp \
city.cpp \
@@ -217,7 +221,10 @@ DIST = shaders/simple.vert \
gamemanager.cpp \
unitrenderer.cpp \
localization.cpp \
terrainmovementmanager.cpp
terrainmovementmanager.cpp \
roadconfig.cpp \
actionneededmanager.cpp \
techconfig.cpp
QMAKE_TARGET = tiletest
DESTDIR =
TARGET = tiletest
@@ -238,13 +245,6 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-base.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-unix.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/qconfig.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigCore.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigGui.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KCoreAddons.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KDBusAddons.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KI18n.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KService.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KWindowSystem.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_accessibility_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_bootstrap_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_concurrent.pri \
@@ -276,8 +276,6 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_platformcompositor_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_script.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_scripttools.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_service_support_private.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql.pri \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql_private.pri \
@@ -328,13 +326,6 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-base.conf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/g++-unix.conf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/qconfig.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigCore.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KConfigGui.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KCoreAddons.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KDBusAddons.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KI18n.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KService.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_KWindowSystem.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_accessibility_support_private.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_bootstrap_private.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_concurrent.pri:
@@ -366,8 +357,6 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_platformcompositor_support_private.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_printsupport_private.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_script.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_scripttools.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_service_support_private.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql.pri:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/qt_lib_sql_private.pri:
@@ -424,8 +413,8 @@ distdir: FORCE
$(COPY_FILE) --parents $(DIST) $(DISTDIR)/
$(COPY_FILE) --parents shaders.qrc $(DISTDIR)/
$(COPY_FILE) --parents /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.h qscenerenderer.h vertex.h tile.h terrain.h tilevertexinfo.h matrix2d.h face.h player.h yamlparser.h unitconfig.h pathfinder.h unit.h city.h terrainyieldmanager.h gamemanager.h unitrenderer.h localization.h terrainmovementmanager.h $(DISTDIR)/
$(COPY_FILE) --parents main.cpp mainwindow.cpp qscenerenderer.cpp vertex.cpp tile.cpp tilevertexinfo.cpp face.cpp player.cpp yamlparser.cpp unitconfig.cpp pathfinder.cpp unit.cpp city.cpp terrainyieldmanager.cpp gamemanager.cpp unitrenderer.cpp localization.cpp terrainmovementmanager.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.h qscenerenderer.h vertex.h tile.h terrain.h tilevertexinfo.h matrix2d.h face.h player.h yamlparser.h unitconfig.h buildingconfig.h pathfinder.h unit.h city.h terrainyieldmanager.h gamemanager.h unitrenderer.h localization.h terrainmovementmanager.h roadconfig.h actionneededmanager.h techconfig.h $(DISTDIR)/
$(COPY_FILE) --parents main.cpp mainwindow.cpp qscenerenderer.cpp vertex.cpp tile.cpp tilevertexinfo.cpp face.cpp player.cpp yamlparser.cpp unitconfig.cpp buildingconfig.cpp pathfinder.cpp unit.cpp city.cpp terrainyieldmanager.cpp gamemanager.cpp unitrenderer.cpp localization.cpp terrainmovementmanager.cpp roadconfig.cpp actionneededmanager.cpp techconfig.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.ui $(DISTDIR)/
@@ -471,7 +460,7 @@ compiler_moc_header_clean:
moc_mainwindow.cpp: mainwindow.h \
moc_predefs.h \
/usr/lib/qt5/bin/moc
/usr/lib/qt5/bin/moc $(DEFINES) --include /home/bart/project/tiletest/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++ -I/home/bart/project/tiletest -I/usr/include/x86_64-linux-gnu/qt5 -I/usr/include/x86_64-linux-gnu/qt5/QtWidgets -I/usr/include/x86_64-linux-gnu/qt5/QtGui -I/usr/include/x86_64-linux-gnu/qt5/QtCore -I/usr/include/c++/13 -I/usr/include/x86_64-linux-gnu/c++/13 -I/usr/include/c++/13/backward -I/usr/lib/gcc/x86_64-linux-gnu/13/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include mainwindow.h -o moc_mainwindow.cpp
/usr/lib/qt5/bin/moc $(DEFINES) --include /home/bart/git/Ascent-of-Man/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++ -I/home/bart/git/Ascent-of-Man -I/usr/include/x86_64-linux-gnu/qt5 -I/usr/include/x86_64-linux-gnu/qt5/QtWidgets -I/usr/include/x86_64-linux-gnu/qt5/QtGui -I/usr/include/x86_64-linux-gnu/qt5/QtCore -I/usr/include/c++/13 -I/usr/include/x86_64-linux-gnu/c++/13 -I/usr/include/c++/13/backward -I/usr/lib/gcc/x86_64-linux-gnu/13/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include mainwindow.h -o moc_mainwindow.cpp
compiler_moc_objc_header_make_all:
compiler_moc_objc_header_clean:
@@ -495,8 +484,12 @@ ui_mainwindow.h: mainwindow.ui \
terrainmovementmanager.h \
player.h \
city.h \
roadconfig.h \
gamemanager.h \
terrainyieldmanager.h
buildingconfig.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h
/usr/lib/qt5/bin/uic mainwindow.ui -o ui_mainwindow.h
compiler_yacc_decl_make_all:
@@ -513,7 +506,26 @@ main.o: main.cpp mainwindow.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o main.cpp
mainwindow.o: mainwindow.cpp mainwindow.h \
ui_mainwindow.h
ui_mainwindow.h \
qscenerenderer.h \
terrain.h \
tile.h \
vertex.h \
face.h \
tilevertexinfo.h \
matrix2d.h \
unitrenderer.h \
unit.h \
unitconfig.h \
terrainmovementmanager.h \
player.h \
city.h \
roadconfig.h \
gamemanager.h \
buildingconfig.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
@@ -529,10 +541,15 @@ qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
terrainmovementmanager.h \
player.h \
city.h \
roadconfig.h \
gamemanager.h \
buildingconfig.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h \
pathfinder.h \
yamlparser.h
yamlparser.h \
localization.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qscenerenderer.o qscenerenderer.cpp
vertex.o: vertex.cpp vertex.h \
@@ -553,8 +570,12 @@ tile.o: tile.cpp tile.h \
terrainmovementmanager.h \
player.h \
city.h \
roadconfig.h \
gamemanager.h \
terrainyieldmanager.h
buildingconfig.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o tile.o tile.cpp
tilevertexinfo.o: tilevertexinfo.cpp tilevertexinfo.h \
@@ -580,6 +601,10 @@ unitconfig.o: unitconfig.cpp unitconfig.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unitconfig.o unitconfig.cpp
buildingconfig.o: buildingconfig.cpp buildingconfig.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o buildingconfig.o buildingconfig.cpp
pathfinder.o: pathfinder.cpp pathfinder.h \
terrain.h \
matrix2d.h \
@@ -592,7 +617,8 @@ unit.o: unit.cpp unit.h \
terrainmovementmanager.h \
terrain.h \
matrix2d.h \
pathfinder.h
pathfinder.h \
localization.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unit.o unit.cpp
city.o: city.cpp city.h \
@@ -613,9 +639,13 @@ gamemanager.o: gamemanager.cpp gamemanager.h \
terrainmovementmanager.h \
terrain.h \
matrix2d.h \
buildingconfig.h \
roadconfig.h \
unit.h \
city.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h \
pathfinder.h \
yamlparser.h \
localization.h
@@ -629,6 +659,7 @@ unitrenderer.o: unitrenderer.cpp unitrenderer.h \
matrix2d.h \
player.h \
city.h \
roadconfig.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unitrenderer.o unitrenderer.cpp
@@ -641,9 +672,36 @@ terrainmovementmanager.o: terrainmovementmanager.cpp terrainmovementmanager.h \
matrix2d.h \
unitconfig.h \
pathfinder.h \
yamlparser.h
yamlparser.h \
gamemanager.h \
player.h \
buildingconfig.h \
roadconfig.h \
unit.h \
city.h \
terrainyieldmanager.h \
actionneededmanager.h \
techconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o terrainmovementmanager.o terrainmovementmanager.cpp
roadconfig.o: roadconfig.cpp roadconfig.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o roadconfig.o roadconfig.cpp
actionneededmanager.o: actionneededmanager.cpp actionneededmanager.h \
yamlparser.h \
unit.h \
unitconfig.h \
terrainmovementmanager.h \
terrain.h \
matrix2d.h \
city.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o actionneededmanager.o actionneededmanager.cpp
techconfig.o: techconfig.cpp techconfig.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o techconfig.o techconfig.cpp
qrc_shaders.o: qrc_shaders.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qrc_shaders.o qrc_shaders.cpp
+271
View File
@@ -0,0 +1,271 @@
#include "actionneededmanager.h"
#include "yamlparser.h"
#include "unit.h"
#include "city.h"
#include <algorithm>
#include <set>
#include <QDebug>
ActionNeededManager::ActionNeededManager()
: m_elapsedGameTime(0.0)
{
}
bool ActionNeededManager::loadConfigFile(const QString &filePath)
{
YamlNode root = YamlParser::parseFile(filePath);
if (root.children.isEmpty()) {
qWarning() << "Failed to parse action needed YAML, using defaults:" << filePath;
return false;
}
const YamlNode &node = root.hasChild("action_needed") ? root["action_needed"] : root;
if (node.hasChild("enabled")) {
m_config.enabled = node["enabled"].asBool(true);
}
if (node.hasChild("track_idle_units")) {
m_config.trackIdleUnits = node["track_idle_units"].asBool(true);
}
if (node.hasChild("track_city_no_production")) {
m_config.trackCityNoProduction = node["track_city_no_production"].asBool(true);
}
if (node.hasChild("track_city_free_population")) {
m_config.trackCityFreePopulation = node["track_city_free_population"].asBool(true);
}
if (node.hasChild("track_no_research")) {
m_config.trackNoResearch = node["track_no_research"].asBool(true);
}
if (node.hasChild("max_visible_items")) {
m_config.maxVisibleItems = node["max_visible_items"].asInt(8);
}
if (node.hasChild("item_width")) {
m_config.itemWidth = node["item_width"].asInt(155);
}
if (node.hasChild("item_height")) {
m_config.itemHeight = node["item_height"].asInt(42);
}
if (node.hasChild("bottom_offset")) {
m_config.bottomOffset = node["bottom_offset"].asInt(16);
}
if (node.hasChild("item_spacing")) {
m_config.itemSpacing = node["item_spacing"].asInt(8);
}
if (node.hasChild("border_radius")) {
m_config.borderRadius = node["border_radius"].asInt(6);
}
if (node.hasChild("pulse_animation")) {
m_config.pulseAnimation = node["pulse_animation"].asBool(true);
}
if (node.hasChild("pulse_speed")) {
m_config.pulseSpeed = node["pulse_speed"].asFloat(3.0f);
}
if (node.hasChild("unit_idle_color")) {
QString hex = node["unit_idle_color"].asString("#3CA0FF");
m_config.unitIdleColor = QColor(hex);
}
if (node.hasChild("city_no_prod_color")) {
QString hex = node["city_no_prod_color"].asString("#FFA028");
m_config.cityNoProdColor = QColor(hex);
}
if (node.hasChild("city_free_pop_color")) {
QString hex = node["city_free_pop_color"].asString("#46DC82");
m_config.cityFreePopColor = QColor(hex);
}
if (node.hasChild("no_research_color")) {
QString hex = node["no_research_color"].asString("#38C8FF");
m_config.noResearchColor = QColor(hex);
}
if (node.hasChild("card_bg_color")) {
QString hex = node["card_bg_color"].asString("#121822");
m_config.cardBgColor = QColor(hex);
}
if (node.hasChild("card_bg_alpha")) {
m_config.cardBgAlpha = node["card_bg_alpha"].asInt(235);
}
m_config.cardBgColor.setAlpha(m_config.cardBgAlpha);
qDebug() << "ActionNeededManager config loaded from" << filePath;
return true;
}
void ActionNeededManager::update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
bool needsResearch)
{
if (!m_config.enabled) return;
m_elapsedGameTime += static_cast<double>(deltaTime);
std::set<QString> currentConditionKeys;
// 0. Player has no active research
if (m_config.trackNoResearch && needsResearch) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::NoResearch)).arg(activePlayerId);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::NoResearch && item.entityId == activePlayerId;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::NoResearch;
newItem.entityId = activePlayerId;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
// 1. Idle Units waiting for action
if (m_config.trackIdleUnits) {
for (const auto &u : units) {
if (!u || u->isDead() || u->playerId != activePlayerId) continue;
bool isIdle = (u->state == UnitState::Idle)
&& (u->path.empty() || u->pathIndex >= u->path.size())
&& !u->currentAction.isValid()
&& !u->hasQueuedAction()
&& !u->hasRoadRoute()
&& !u->isBuilding();
if (isIdle) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::UnitIdle)).arg(u->id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::UnitIdle && item.entityId == u->id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::UnitIdle;
newItem.entityId = u->id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
}
// 2. Cities waiting for action
for (const auto &c : cities) {
if (c.playerId != activePlayerId) continue;
// a) City is not building anything
if (m_config.trackCityNoProduction) {
bool noProd = !c.hasActiveProject();
if (noProd) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityNoProduction)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityNoProduction && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityNoProduction;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
// b) City has free / unassigned population
if (m_config.trackCityFreePopulation) {
int freePop = c.getAvailablePop();
if (freePop > 0) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityFreePopulation)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityFreePopulation && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityFreePopulation;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.auxValue = freePop;
newItem.dismissed = false;
m_items.push_back(newItem);
} else {
// If free population increased since it was dismissed, reactivate it!
if (freePop > it->dismissedAuxValue) {
it->dismissed = false;
}
it->auxValue = freePop;
}
}
}
}
// 3. Problem resolved -> remove from queue!
m_items.erase(std::remove_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return currentConditionKeys.find(item.getKey()) == currentConditionKeys.end();
}), m_items.end());
}
std::vector<ActionItem> ActionNeededManager::getVisibleItems() const
{
std::vector<ActionItem> visible;
visible.reserve(m_items.size());
for (const auto &item : m_items) {
if (!item.dismissed) {
visible.push_back(item);
}
}
// Sort in order of oldest first (firstDetectedTime ASC)
std::sort(visible.begin(), visible.end(), [](const ActionItem &a, const ActionItem &b) {
if (a.firstDetectedTime != b.firstDetectedTime)
return a.firstDetectedTime < b.firstDetectedTime;
return a.entityId < b.entityId;
});
return visible;
}
int ActionNeededManager::getVisibleCount() const
{
int count = 0;
for (const auto &item : m_items) {
if (!item.dismissed) {
count++;
}
}
return count;
}
void ActionNeededManager::dismissItem(ActionNeededType type, int entityId)
{
for (auto &item : m_items) {
if (item.type == type && item.entityId == entityId) {
item.dismissed = true;
item.dismissedAuxValue = item.auxValue;
break;
}
}
}
void ActionNeededManager::resetDismissals()
{
for (auto &item : m_items) {
item.dismissed = false;
item.dismissedAuxValue = 0;
}
}
void ActionNeededManager::reset()
{
m_items.clear();
m_elapsedGameTime = 0.0;
}
+83
View File
@@ -0,0 +1,83 @@
#ifndef ACTIONNEEDEDMANAGER_H
#define ACTIONNEEDEDMANAGER_H
#include <QString>
#include <QColor>
#include <vector>
#include <memory>
class Unit;
class City;
enum class ActionNeededType {
UnitIdle = 0,
CityNoProduction = 1,
CityFreePopulation = 2,
NoResearch = 3
};
struct ActionItem {
ActionNeededType type = ActionNeededType::UnitIdle;
int entityId = -1;
double firstDetectedTime = 0.0; // Monotonic game time in seconds
int auxValue = 0; // e.g. free population count
bool dismissed = false;
int dismissedAuxValue = 0; // Value at the time of dismissal
QString getKey() const {
return QString("%1_%2").arg(static_cast<int>(type)).arg(entityId);
}
};
struct ActionNeededConfig {
bool enabled = true;
bool trackIdleUnits = true;
bool trackCityNoProduction = true;
bool trackCityFreePopulation = true;
bool trackNoResearch = true;
int maxVisibleItems = 8;
int itemWidth = 155;
int itemHeight = 42;
int bottomOffset = 16;
int itemSpacing = 8;
int borderRadius = 6;
bool pulseAnimation = true;
float pulseSpeed = 3.0f;
QColor unitIdleColor = QColor(60, 160, 255);
QColor cityNoProdColor = QColor(255, 160, 40);
QColor cityFreePopColor = QColor(70, 220, 130);
QColor noResearchColor = QColor(56, 200, 255);
QColor cardBgColor = QColor(18, 24, 34);
int cardBgAlpha = 235;
};
class ActionNeededManager {
public:
ActionNeededManager();
bool loadConfigFile(const QString &filePath);
const ActionNeededConfig& config() const { return m_config; }
void update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
bool needsResearch = false);
std::vector<ActionItem> getVisibleItems() const;
int getVisibleCount() const;
void dismissItem(ActionNeededType type, int entityId);
void resetDismissals();
double getElapsedGameTime() const { return m_elapsedGameTime; }
void reset();
private:
ActionNeededConfig m_config;
std::vector<ActionItem> m_items;
double m_elapsedGameTime;
};
#endif // ACTIONNEEDEDMANAGER_H
-1
View File
@@ -1 +0,0 @@
/home/bart/project/tiletest/AGENTS.md
+2
View File
@@ -53,6 +53,8 @@ bool BuildingConfigManager::loadConfigFile(const QString &filePath)
bCfg.scienceBonus = it.value().hasChild("science_bonus") ? it.value()["science_bonus"].asInt(0) : 0;
bCfg.faithBonus = it.value().hasChild("faith_bonus") ? it.value()["faith_bonus"].asInt(0) : 0;
bCfg.goldBonus = it.value().hasChild("gold_bonus") ? it.value()["gold_bonus"].asInt(0) : 0;
bCfg.requiredTech = it.value().hasChild("required_tech") ? it.value()["required_tech"].asString("") : "";
bCfg.obsoleteTech = it.value().hasChild("obsolete_tech") ? it.value()["obsolete_tech"].asString("") : "";
bCfg.iconPath = it.value().hasChild("icon") ? it.value()["icon"].asString() : "";
bCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : "";
m_buildings[bCfg.id] = bCfg;
+3
View File
@@ -17,6 +17,8 @@ struct BuildingConfig {
int scienceBonus;
int faithBonus;
int goldBonus;
QString requiredTech;
QString obsoleteTech;
QString iconPath;
QString description;
@@ -24,6 +26,7 @@ struct BuildingConfig {
: id(""), name(""), district("center"), laborCost(1000),
foodModifier(0.0f), foodBonus(0), laborBonus(0), cultureBonus(0),
scienceBonus(0), faithBonus(0), goldBonus(0),
requiredTech(""), obsoleteTech(""),
iconPath(""), description("") {}
};
+75 -1
View File
@@ -55,6 +55,7 @@ public:
int accumulatedLabor = 0; // Local resource (Arbeid)
CityProject currentProject;
std::vector<CityProject> projectQueue;
std::vector<QString> completedBuildings; // IDs of completed buildings in the city
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
@@ -77,6 +78,34 @@ public:
return true;
}
bool isBuildingQueuedOrBuilt(const QString &buildingId) const {
if (hasBuilding(buildingId)) return true;
if (currentProject.isValid() && currentProject.type == CityProjectType::Building && currentProject.id == buildingId) return true;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Building && q.id == buildingId) return true;
}
return false;
}
int getQueuedBuildingPosition(const QString &buildingId) const {
for (size_t i = 0; i < projectQueue.size(); ++i) {
if (projectQueue[i].type == CityProjectType::Building && projectQueue[i].id == buildingId) {
return (int)i + 1;
}
}
return 0;
}
int getQueuedUnitCount(const QString &unitId) const {
int count = 0;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Unit && q.id == unitId) {
count++;
}
}
return count;
}
bool hasActiveProject() const {
return currentProject.isValid();
}
@@ -90,9 +119,54 @@ public:
currentProject.district = district.isEmpty() ? "center" : district;
}
void cancelProject() {
bool queueProject(CityProjectType type, const QString &projId, const QString &projName, int cost, const QString &district = "center", int maxQueueSize = 6) {
if (!hasActiveProject()) {
startProject(type, projId, projName, cost, district);
return true;
}
if ((int)projectQueue.size() >= maxQueueSize) {
return false;
}
CityProject q;
q.type = type;
q.id = projId;
q.name = projName;
q.laborCost = std::max(1, cost);
q.currentLabor = 0;
q.district = district.isEmpty() ? "center" : district;
projectQueue.push_back(q);
return true;
}
void cancelActiveProject() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
currentProject = CityProject();
}
}
void cancelProject() {
cancelActiveProject();
}
bool removeQueuedProject(int index) {
if (index >= 0 && index < (int)projectQueue.size()) {
projectQueue.erase(projectQueue.begin() + index);
return true;
}
return false;
}
void advanceQueue() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
currentProject = CityProject();
}
}
bool addTerritory(int tx, int ty) {
if (ownsTile(tx, ty)) return false;
BIN
View File
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
# Configuration for the Action Needed Queue (Bottom HUD notification bar)
action_needed:
enabled: true
track_idle_units: true
track_city_no_production: true
track_city_free_population: true
track_no_research: true
max_visible_items: 8
# Visual & Layout parameters
item_width: 155
item_height: 42
bottom_offset: 16
item_spacing: 8
border_radius: 6
pulse_animation: true
pulse_speed: 3.0
# Colors
unit_idle_color: "#3CA0FF"
city_no_prod_color: "#FFA028"
city_free_pop_color: "#46DC82"
no_research_color: "#38C8FF"
card_bg_color: "#121822"
card_bg_alpha: 235
+131 -8
View File
@@ -1,19 +1,55 @@
# City Buildings & Extensions configuration
# All gameplay values are configurable via YAML
# All gameplay values, required technologies, and obsolescence technologies are configurable via YAML
districts:
center:
id: "center"
name: "City Center"
max_buildings: 10
max_buildings: 12
description: "The urban core and administrative center of the city."
buildings:
monument:
id: "monument"
name: "Monument"
district: "center"
labor_cost: 800
required_tech: ""
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 0
labor_bonus: 0
culture_bonus: 250
science_bonus: 0
faith_bonus: 0
gold_bonus: 0
icon: "images/improvements/monument.png"
description: "+250 Culture per tick"
palisade:
id: "palisade"
name: "Wooden Palisade"
district: "center"
labor_cost: 700
required_tech: ""
obsolete_tech: "masonry"
food_modifier: 0.0
food_bonus: 0
labor_bonus: 100
culture_bonus: 100
science_bonus: 0
faith_bonus: 0
gold_bonus: 0
icon: "images/improvements/monument.png"
description: "+100 Production, +100 Culture/tick (Obsolete with Masonry)"
granary:
id: "granary"
name: "Granary"
district: "center"
labor_cost: 1000
required_tech: "pottery"
obsolete_tech: ""
food_modifier: 0.50
food_bonus: 0
labor_bonus: 0
@@ -24,17 +60,104 @@ buildings:
icon: "images/improvements/granary.png"
description: "+50% Food production in city"
monument:
id: "monument"
name: "Monument"
library:
id: "library"
name: "Library"
district: "center"
labor_cost: 800
labor_cost: 1200
required_tech: "writing"
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 0
labor_bonus: 0
culture_bonus: 250
culture_bonus: 0
science_bonus: 350
faith_bonus: 0
gold_bonus: 0
icon: "images/resources/kennis.png"
description: "+350 Science per tick"
walls:
id: "walls"
name: "Stone Walls"
district: "center"
labor_cost: 1500
required_tech: "masonry"
obsolete_tech: "gunpowder"
food_modifier: 0.0
food_bonus: 0
labor_bonus: 200
culture_bonus: 200
science_bonus: 0
faith_bonus: 0
gold_bonus: 0
icon: "images/improvements/monument.png"
description: "+250 Culture per tick"
description: "+200 Production, +200 Culture/tick (Obsolete with Gunpowder)"
water_mill:
id: "water_mill"
name: "Water Mill"
district: "center"
labor_cost: 1600
required_tech: "wheel"
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 200
labor_bonus: 250
culture_bonus: 0
science_bonus: 0
faith_bonus: 0
gold_bonus: 0
icon: "images/improvements/granary.png"
description: "+200 Food, +250 Production per tick"
market:
id: "market"
name: "Market"
district: "center"
labor_cost: 1800
required_tech: "currency"
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 0
labor_bonus: 0
culture_bonus: 0
science_bonus: 0
faith_bonus: 0
gold_bonus: 400
icon: "images/resources/goud.png"
description: "+400 Gold per tick"
university:
id: "university"
name: "University"
district: "center"
labor_cost: 2800
required_tech: "education"
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 0
labor_bonus: 0
culture_bonus: 100
science_bonus: 700
faith_bonus: 0
gold_bonus: 0
icon: "images/resources/kennis.png"
description: "+700 Science, +100 Culture per tick"
workshop:
id: "workshop"
name: "Workshop"
district: "center"
labor_cost: 2600
required_tech: "machinery"
obsolete_tech: ""
food_modifier: 0.0
food_bonus: 0
labor_bonus: 500
culture_bonus: 0
science_bonus: 0
faith_bonus: 0
gold_bonus: 0
icon: "images/resources/arbeid.png"
description: "+500 Production per tick"
+1
View File
@@ -14,6 +14,7 @@ tile_culture_cost: 10000
food_to_inhabitants_ratio: 1.0
default_unit_labor_cost: 1000
unit_labor_cost_multiplier: 20
max_production_queue_size: 6
# Base yield breakdown per resource (default 500 per tick)
base_yields:
+1 -1
View File
@@ -2,7 +2,7 @@ movement_tick_duration: 30.0
# Action queue configuration
action_queue:
max_size: 5
max_size: 15
auto_queue_move_when_busy: true
allow_action_queueing: true
+20 -8
View File
@@ -1,31 +1,43 @@
# Road System Configuration
default_road_type: "dirt"
build_duration_ticks: 2
meander_amplitude: 0.045
meander_subdivisions: 5
meander_amplitude: 0.025
meander_subdivisions: 6
types:
dirt:
id: "dirt"
name: "Dirt Road"
speed_multiplier: 1.30 # +30% movement speed
color: "#c49a62"
border_color: "#6b4b23"
color: "#d8b480"
border_color: "#52381c"
width: 3.5
world_width: 0.056
border_width: 0.012
texture_scale: 4.5
opacity: 0.96
cost: 5
stone:
id: "stone"
name: "Stone Road"
speed_multiplier: 1.60 # +60% movement speed
color: "#9aa1a8"
border_color: "#464b50"
color: "#c2beb6"
border_color: "#46484c"
width: 4.0
world_width: 0.064
border_width: 0.012
texture_scale: 5.5
opacity: 0.98
cost: 15
rail:
id: "rail"
name: "Railroad"
speed_multiplier: 2.50 # +150% movement speed
color: "#3a3c40"
border_color: "#181a1c"
color: "#4a4e54"
border_color: "#22252a"
width: 4.5
world_width: 0.070
border_width: 0.014
texture_scale: 6.0
opacity: 0.98
cost: 30
+106 -27
View File
@@ -49,25 +49,30 @@ ui:
queue_cleared: "Queued actions cancelled"
queue_full: "Action queue full!"
action_queued: "Action queued: %1"
action_queued_count: "%1 added to queue (#%2)"
action_scheduled_after_current: "%1 scheduled after current action"
action_cancelled: "%1 cancelled"
farm_queued: "Build Farm queued on arrival"
farm_started: "Constructing Farm..."
farm_queued: "Build Farm queued"
farm_started: "Constructing Farm... (%1 ticks)"
farm_completed: "Farm completed!"
farm_completed_consumed: "Farm completed! (Builder consumed)"
farm_completed_charges: "Farm completed! (%1 %2 remaining)"
road_queued: "Build Road queued on arrival"
moving_to_build_farm: "Moving to build Farm at (%1, %2)..."
moving_to_build_city: "Moving to found City at (%1, %2)..."
moving_to_attack: "Moving into attack range..."
road_queued: "Build Road queued"
road_started: "Constructing %1... (%2 ticks)"
road_completed: "%1 completed!"
road_already_here: "Tile already has a road!"
cannot_build_road_water: "Roads cannot be built on water!"
city_queued: "Found City queued on arrival"
city_queued: "Found City queued"
city_founded: "%1 founded!"
already_building: "Already busy building!"
busy: "Unit is busy!"
cannot_act_busy: "Unit cannot perform action while busy!"
tile_occupied: "Tile already has an improvement!"
cannot_build_here: "Cannot build here! Allowed on: %1"
cannot_build_destination: "Cannot build on destination terrain! Allowed on: %1"
cannot_build_destination: "Cannot build on target terrain! Allowed on: %1"
no_build_charges: "No build charges remaining!"
charge_singular: "charge"
charge_plural: "charges"
@@ -76,6 +81,7 @@ ui:
no_path: "No reachable path!"
tile_purchased: "Tile purchased! (-%1 Culture)"
out_of_range: "Out of range!"
no_enemy_on_tile: "Select a tile with an enemy unit!"
defeated: "Defeated!"
city_already_here: "A city already exists here!"
unsuitable_terrain: "Unsuitable terrain!"
@@ -86,30 +92,45 @@ ui:
combat_format: "Attack: %1 | Range: %2 hex"
building_pct: "Building... %1%"
charges_format: "Charges: %1"
status_queued: "Queued: %1"
status_queued: "Queue (%1): %2"
btn_move: "Move [M]"
btn_attack: "Attack [A]"
btn_build_city: "Settle [B]"
btn_build_farm: "Farm [F]"
btn_farm_queued: "Farm (Queued)"
btn_farm_queued: "Farm [F]"
btn_farm_building: "Building %1%"
btn_build_road: "Road [R]"
btn_road_queued: "Road (Queued)"
btn_road_queued: "Road [R]"
btn_road_building: "Building %1%"
tip_move: "Move (M)"
tip_attack: "Attack (A) [Range: %1]"
tip_build_city: "Found City (B)"
tip_build_farm: "Build Farm (F)"
tip_farm_building: "Busy building... %1%"
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
tip_build_road: "Build Road (R)"
tip_road_building: "Busy building road... %1%"
tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
road_started: "Constructing %1... (%2 ticks)"
road_completed: "%1 completed!"
road_queued: "Build Road queued on arrival"
road_already_here: "Tile already has a road!"
cannot_build_road_water: "Roads cannot be built on water!"
tip_move: "Move (M) - Click target tile (Shift+Click to queue)"
tip_attack: "Attack (A) [Range: %1] - Click enemy (Shift+Click to queue)"
tip_build_city: "Found City (B) - Click target tile (Shift+Click to queue)"
tip_build_farm: "Build Farm (F) - Click target tile (Shift+Click to queue)"
tip_farm_building: "Busy building... %1% (Click to plan next target)"
tip_farm_queued: "Build Farm (Queued - Click to plan target)"
tip_build_road: "Build Road (R) - Click target tile (Shift+Click to queue)"
tip_road_building: "Busy building road... %1% (Click to plan next target)"
tip_road_queued: "Build Road (Queued - Click to plan target)"
road_route_started: "Building road route to (%1, %2)..."
road_route_completed: "Road route completed!"
road_route_cancelled: "Road route cancelled"
status_road_route: "Road Route: %1/%2"
btn_road_route: "Route (%1/%2)"
preview_move: "Move (%1 hexes)"
preview_farm: "Build Farm"
preview_farm_move: "Build Farm (%1 hexes)"
preview_city: "Found City"
preview_city_move: "Found City (%1 hexes)"
preview_road: "Road Route (%1 hexes)"
preview_road_here: "Build Road"
preview_attack: "Attack"
preview_attack_move: "Attack (%1 hexes)"
preview_queue_prefix: "+Queue: %1"
queue_badge_move: "#%1 Move"
queue_badge_farm: "#%1 Farm"
queue_badge_city: "#%1 Settle"
queue_badge_road: "#%1 Road"
queue_badge_attack: "#%1 Attack"
improvement:
farm: "Farm"
@@ -123,28 +144,40 @@ ui:
management_title: "%1 - CITY MANAGEMENT"
close_btn: "[X] Close"
local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5"
national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4"
national_info: "National: Culture: %L1 | Science: +%L2/tick | Gold: %L3 | Faith: %L4"
help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
current_production: "CURRENT PRODUCTION"
production_panel_title: "CITY PRODUCTION"
no_project: "No active project"
select_prompt: "Select a unit or building to construct"
select_prompt: "Select an item below to construct"
ticks_remaining: "%1 ticks remaining (%2 / %3 Labor)"
cancel_btn: "Cancel"
queue_header: "QUEUE (%1 / %2)"
queue_empty: "Queue is empty"
districts_header: "DISTRICT: CITY CENTER"
no_buildings: "No extensions built yet"
choose_project: "CHOOSE CITY PROJECT"
tab_buildings: "Buildings & Extensions"
choose_project: "AVAILABLE PROJECTS"
tab_buildings: "Buildings"
tab_units: "Units"
btn_build: "Build"
btn_train: "Train"
btn_queue: "+ Queue"
status_built: "BUILT"
status_in_progress: "ACTIVE"
status_queued: "QUEUED #%1"
status_full: "FULL"
queue_full_float: "Production queue is full! (Max %1)"
labor_cost_label: "%1 Labor (%2 ticks)"
unit_trained: "%1: %2 trained!"
building_completed: "%1: %2 completed in %3!"
project_started: "%1: Started %2"
project_cancelled: "%1: Project cancelled"
project_queued: "%1: Added %2 to queue (#%3)"
project_cancelled: "%1: Cancelled %2"
queued_item_removed: "%1: Removed %2 from queue"
already_built: "%1 already built in this city!"
already_queued: "%1 is already in the production queue!"
requires_tech: "%1 requires technology: %2!"
obsolete_item: "%1 is obsolete and can no longer be built!"
growth_float: "+%1 Inhabitants | +%2 Culture"
not_enough_culture: "Not enough Culture! (%1 needed)"
worker_freed: "Pop worker freed"
@@ -165,3 +198,49 @@ ui:
kennis: "Science"
cultuur: "Culture"
geloof: "Faith"
action_needed:
header_title: "ACTIONS"
header_tip: "Queue of items awaiting action (oldest first). Left-click to focus, Right-click to dismiss."
unit_idle_desc: "Needs orders"
city_no_production_desc: "No production"
city_free_pop_desc: "+%1 Free Pop"
no_research_title: "RESEARCH"
no_research_desc: "Choose technology"
dismiss_tip: "Right-click to dismiss"
more_items: "+%1"
tech_tree:
title: "TECHNOLOGY TREE"
close_btn: "[X] Close"
btn_open: "Tech [T]"
no_research: "No Research"
choose_research: "Choose Tech [T]"
science_rate: "+%L1/tick"
active_research: "ACTIVE RESEARCH: %1"
no_active_research: "No active research - Click any technology node below to start researching"
queue_label: "QUEUE (%1):"
clear_queue_btn: "Clear Queue"
status_researched: "RESEARCHED"
status_researching: "ACTIVE #1"
status_queued: "QUEUED #%1"
status_available: "AVAILABLE"
status_locked: "LOCKED"
cost_format: "%L1 Science (~%2 %3)"
progress_format: "%L1 / %L2 (%3%)"
remaining_format: "%L1 left (~%2 %3)"
tick_singular: "tick"
tick_plural: "ticks"
prereqs_label: "Req: %1"
prereqs_none: "Base Technology"
unlocks_label: "Unlocks: %1"
obsoletes_label: "Obsoletes: %1"
help_text: "Left-click a tech to research (prerequisites auto-queued by least remaining work) | Shift+Click to append | Right-click to remove | Drag/Scroll to pan"
tech_completed_float: "Technology Researched: %1!"
tech_started_float: "Researching: %1"
tech_queued_float: "Research Queue Updated (%1 techs, target: %2)"
tech_already_researched: "%1 has already been researched!"
tech_cancelled_float: "Research queue cleared"
tech_removed_float: "Removed %1 from research queue"
+202
View File
@@ -0,0 +1,202 @@
# Technology Tree Configuration
# All gameplay values, science costs, layout coordinates, and prerequisite chains are configurable via YAML.
technologies:
pottery:
id: "pottery"
name: "Pottery"
science_cost: 1000
column: 0
row: 0
icon: "images/improvements/granary.png"
description: "Ceramic vessels for storing grain and preserving food supplies."
animal_husbandry:
id: "animal_husbandry"
name: "Animal Husbandry"
science_cost: 1000
column: 0
row: 1
icon: "images/resources/voedsel.png"
description: "Domestication and breeding of livestock for food and labor."
mining:
id: "mining"
name: "Mining"
science_cost: 1200
column: 0
row: 2
icon: "images/resources/arbeid.png"
description: "Extraction of stone and metal ores from the earth."
sailing:
id: "sailing"
name: "Sailing"
science_cost: 1500
column: 1
row: 0
icon: "images/units/galley.png"
description: "Harnessing the wind to navigate coastal waters."
prerequisites:
- "pottery"
writing:
id: "writing"
name: "Writing"
science_cost: 1800
column: 1
row: 1
icon: "images/resources/kennis.png"
description: "Recording laws, history, and scientific knowledge in script."
prerequisites:
- "pottery"
archery:
id: "archery"
name: "Archery"
science_cost: 1500
column: 1
row: 2
icon: "images/units/slinger.png"
description: "Use of bows and arrows for superior ranged warfare."
prerequisites:
- "animal_husbandry"
bronze_working:
id: "bronze_working"
name: "Bronze Working"
science_cost: 1800
column: 1
row: 3
icon: "images/units/warrior.png"
description: "Smelting copper and tin into durable bronze weapons and armor."
prerequisites:
- "mining"
masonry:
id: "masonry"
name: "Masonry"
science_cost: 1600
column: 1
row: 4
icon: "images/improvements/monument.png"
description: "Cutting and laying stone for permanent fortifications and monuments."
prerequisites:
- "mining"
celestial_navigation:
id: "celestial_navigation"
name: "Celestial Navigation"
science_cost: 2500
column: 2
row: 0
icon: "images/units/galley.png"
description: "Using the stars to guide maritime trade and exploration."
prerequisites:
- "sailing"
- "writing"
currency:
id: "currency"
name: "Currency"
science_cost: 2400
column: 2
row: 1
icon: "images/resources/goud.png"
description: "Standardized coinage enabling thriving markets and trade."
prerequisites:
- "writing"
wheel:
id: "wheel"
name: "The Wheel"
science_cost: 2200
column: 2
row: 2
icon: "images/actions/road.png"
description: "Rotational mechanics for transport and water-powered mills."
prerequisites:
- "archery"
- "mining"
horseback_riding:
id: "horseback_riding"
name: "Horseback Riding"
science_cost: 2500
column: 2
row: 3
icon: "images/actions/move.png"
description: "Mounted cavalry tactics for rapid battlefield maneuvers."
prerequisites:
- "archery"
iron_working:
id: "iron_working"
name: "Iron Working"
science_cost: 2800
column: 2
row: 4
icon: "images/actions/attack.png"
description: "Forging strong iron blades and heavy infantry armor."
prerequisites:
- "bronze_working"
education:
id: "education"
name: "Education"
science_cost: 3600
column: 3
row: 0
icon: "images/resources/kennis.png"
description: "Higher learning institutions and systematic scientific inquiry."
prerequisites:
- "currency"
- "celestial_navigation"
engineering:
id: "engineering"
name: "Engineering"
science_cost: 3400
column: 3
row: 2
icon: "images/resources/arbeid.png"
description: "Advanced structural design, aqueducts, and mechanical contraptions."
prerequisites:
- "wheel"
- "masonry"
stirrups:
id: "stirrups"
name: "Stirrups"
science_cost: 4000
column: 3
row: 3
icon: "images/actions/move.png"
description: "Saddle stirrups enabling heavily armored shock cavalry."
prerequisites:
- "horseback_riding"
- "iron_working"
machinery:
id: "machinery"
name: "Machinery"
science_cost: 4200
column: 4
row: 1
icon: "images/resources/arbeid.png"
description: "Mechanical gears, crossbows, and specialized workshops."
prerequisites:
- "engineering"
- "iron_working"
gunpowder:
id: "gunpowder"
name: "Gunpowder"
science_cost: 5200
column: 4
row: 3
icon: "images/actions/attack.png"
description: "Explosive black powder revolutionizing firearms and siege warfare."
prerequisites:
- "machinery"
- "stirrups"
+1
View File
@@ -3,6 +3,7 @@
# Classification thresholds
water_height_threshold: 6 # Raw heightmap elevation value (< 6 is water)
coast_beach_width_km: 0.022 # Width of shoreline sandy beach transition above water level (in km)
mountain_rock_height_km: 2.3 # Height in km >= 2.3 is rocky mountain
snow_height_km: 3.1 # Altitude in km >= 3.1 is high peak snow
polar_snow_line_enabled: true # Whether latitude reduces snowline altitude
+18
View File
@@ -0,0 +1,18 @@
id: archer
name: Archer
category: militair
domain: land
health: 85
movement_speed: 2.0
cost: 60
labor_cost: 1200
attack_damage: 25
attack_range: 2
required_tech: "archery"
obsolete_tech: "machinery"
icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+8 -4
View File
@@ -6,13 +6,17 @@ health: 100
movement_speed: 2.0
cost: 50
labor_cost: 1000
build_charges: -1 # -1 voor oneindig doorwerken
required_tech: ""
obsolete_tech: ""
build_charges: -1 # -1 for infinite build charges
infinite_charges: true
icon: images/units/builder.png
actions:
- verplaatsen
- boerderij_bouwen
- build_road
action_speeds:
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
build_road: 2 # Duur in ticks
recharge_ticks: 0
attack_flash_ticks: 0
action_durations:
boerderij_bouwen: 2.0
build_road: 2.0
+18
View File
@@ -0,0 +1,18 @@
id: crossbowman
name: Crossbowman
category: militair
domain: land
health: 110
movement_speed: 2.0
cost: 120
labor_cost: 2400
attack_damage: 42
attack_range: 2
required_tech: "machinery"
obsolete_tech: ""
icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+4
View File
@@ -8,7 +8,11 @@ cost: 60
labor_cost: 1500
attack_damage: 25
attack_range: 1
required_tech: "sailing"
obsolete_tech: ""
icon: images/units/galley.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+18
View File
@@ -0,0 +1,18 @@
id: horseman
name: Horseman
category: militair
domain: land
health: 125
movement_speed: 4.0
cost: 85
labor_cost: 1700
attack_damage: 34
attack_range: 1
required_tech: "horseback_riding"
obsolete_tech: "stirrups"
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+18
View File
@@ -0,0 +1,18 @@
id: knight
name: Knight
category: militair
domain: land
health: 180
movement_speed: 4.0
cost: 140
labor_cost: 2800
attack_damage: 52
attack_range: 1
required_tech: "stirrups"
obsolete_tech: ""
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+18
View File
@@ -0,0 +1,18 @@
id: musketman
name: Musketman
category: militair
domain: land
health: 200
movement_speed: 2.0
cost: 160
labor_cost: 3200
attack_damage: 60
attack_range: 1
required_tech: "gunpowder"
obsolete_tech: ""
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+4
View File
@@ -6,7 +6,11 @@ health: 100
movement_speed: 2.0
cost: 80
labor_cost: 2000
required_tech: ""
obsolete_tech: ""
icon: images/units/settler.png
recharge_ticks: 0
attack_flash_ticks: 0
actions:
- verplaatsen
- steden_bouwen
+4
View File
@@ -8,7 +8,11 @@ cost: 35
labor_cost: 800
attack_damage: 15
attack_range: 2
required_tech: ""
obsolete_tech: "archery"
icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+18
View File
@@ -0,0 +1,18 @@
id: spearman
name: Spearman
category: militair
domain: land
health: 120
movement_speed: 2.0
cost: 65
labor_cost: 1300
attack_damage: 28
attack_range: 1
required_tech: "bronze_working"
obsolete_tech: "gunpowder"
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+18
View File
@@ -0,0 +1,18 @@
id: swordsman
name: Swordsman
category: militair
domain: land
health: 140
movement_speed: 2.0
cost: 90
labor_cost: 1800
attack_damage: 38
attack_range: 1
required_tech: "iron_working"
obsolete_tech: "gunpowder"
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+4
View File
@@ -8,7 +8,11 @@ cost: 40
labor_cost: 1000
attack_damage: 20
attack_range: 1
required_tech: ""
obsolete_tech: "iron_working"
icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+132
View File
@@ -0,0 +1,132 @@
# Military Unit Recharge System Implementation
## Overview
This implementation adds a recharge/cooldown system to military units in the RTS game engine, providing visual feedback when units cannot attack due to recent attacks or damage taken. The recharge bar is displayed below the unit's health bar.
## Features Implemented
### 1. **Recharge Timer System**
- Military units now have a configurable recharge time between attacks
- Recharge timer decrements over time, preventing immediate re-attack after each hit
- Units are automatically reset to idle state when recharge completes
### 2. **Visual Feedback - Recharge Bar**
- A green bar is displayed below military unit cylinders during recharge cooldown
- Bar fill percentage indicates remaining recharge time (0% = ready to attack, 100% = just attacked)
- Only military units that can attack (`config.canAttack()`) are shown with recharge bars
- Non-military units (settlement, builders) have the fields but won't display bars
### 3. **Attack Flash Enhancement**
- Attack flash timer now uses configurable duration from config
- Flash effect provides immediate visual confirmation of successful attacks
- Color transitions smoothly from red/white back to unit base color
## Technical Implementation
### Modified Files
#### `unit.h` / `unit.cpp`
Added new member variables:
```cpp
float attackFlashTimer; // Flash effect timer (seconds)
float rechargeTimer; // Recharge/cooldown timer (seconds)
bool isRecharging(); // Checks if unit is currently on cooldown
```
Modified methods:
- `canAttack()`: Now checks recharge timer before allowing attack
- `performAttack()`: Sets rechargeTimer when attack is performed
- `takeDamage()`: Resets rechargeTimer when unit takes damage
- `update()`: Decrements both flash and recharge timers each frame
#### `unitconfig.h` / `unit.cpp`
Added configuration fields:
```cpp
int rechargeTime; // Time in milliseconds between attacks
float rechargeDuration; // Visual cooldown bar duration
int attackFlashDuration; // How long the flash effect lasts (ms)
```
Default values:
- `recharge_time`: 500ms (0.5 seconds)
- `recharge_duration`: 300ms
- `attack_flash_duration`: 400ms
#### `unitrenderer.h` / `unitrenderer.cpp`
Added recharge bar rendering:
- New mesh buffers for flat quad-based recharge bars
- Trapezoidal bar geometry showing remaining recharge percentage
- Positioned below unit cylinder with small gap for visual separation
- Only rendered for military units currently recharging
#### All Unit YAML Configs (`config/units/*.yaml`)
Added recharge configuration to all 12 unit types:
```yaml
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
```
## Game Mechanics
### Attack Cooldown Flow
1. Unit performs attack → `attackFlashTimer` = 0.4s, `rechargeTimer` = 0.5s
2. Visual flash effect plays (red/white transition)
3. Recharge bar appears below unit showing remaining cooldown
4. Both timers decrement each frame
5. When `rechargeTimer` reaches 0, unit can attack again
### Damage Taken Flow
1. Unit receives damage → Health decreased
2. `attackFlashTimer` set to 0.4s (damage flash effect)
3. `rechargeTimer` reset to full recharge time
4. Unit must wait through recharge before attacking
### Visual Hierarchy (from bottom to top)
1. Ground level
2. **Recharge bar** (below unit, if applicable)
3. Unit cylinder mesh
4. Health bar (if implemented later)
5. Selection ring (when selected)
## Configuration Examples
### Default Recharge Settings (All Units)
```yaml
recharge_time: 500 # 0.5 seconds between attacks
recharge_duration: 300 # Visual feedback duration
attack_flash_duration: 400 # Flash effect time
```
### Faster Recharge (Rapid Attack Units)
```yaml
recharge_time: 300 # Quicker cooldown for high-damage units
attack_flash_duration: 350
```
### Slower Recharge (Heavy Infantry/Armor)
```yaml
recharge_time: 700 # Longer cooldown for powerful units
attack_flash_duration: 500
```
## Design Rationale
1. **Modular Configuration**: All recharge parameters are YAML-configurable per the AGENTS.md guidelines
2. **Visual Feedback**: Recharge bar provides clear indication of when units can attack again
3. **Performance**: Single mesh buffer reused for all recharge bars (efficient batch rendering)
4. **Backwards Compatible**: Non-military units have fields but no functional impact
5. **Consistent API**: `isRecharging()` method used by both renderer and game logic
## Future Enhancement Possibilities
- Add configurable recharge times per tech tree node
- Implement different recharge rates for melee vs ranged attacks
- Add area-of-effect cooldown reduction mechanics
- Implement weapon-specific reload animations
- Display numerical cooldown timer on hover tooltip
---
*Implementation Date: 2026-11-10*
*See AGENTS.md Section 4.2 for Unit Architecture Reference*
BIN
View File
Binary file not shown.
+1112 -404
View File
File diff suppressed because it is too large Load Diff
+42 -20
View File
@@ -13,7 +13,8 @@
#include "matrix2d.h"
#include "terrain.h"
#include "terrainyieldmanager.h"
#include "roadconfig.h"
#include "actionneededmanager.h"
#include "techconfig.h"
enum class ActionMode {
None,
@@ -24,23 +25,6 @@ enum class ActionMode {
BuildRoad
};
struct TileRoad {
QString roadTypeId = "dirt"; // "dirt", "stone", "rail"
int tileX = 0;
int tileY = 0;
int playerId = 1;
unsigned char connectionMask = 0; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
QVector3D worldPos;
bool hasConnection(int edge) const {
return (connectionMask & (1 << (edge % 6))) != 0;
}
void setConnection(int edge, bool connected) {
if (connected) connectionMask |= (1 << (edge % 6));
else connectionMask &= ~(1 << (edge % 6));
}
};
struct TileImprovement {
QString improvementId; // "farm"
int tileX;
@@ -65,11 +49,32 @@ public:
void update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
PlayerManager& players() { return m_playerManager; }
const PlayerManager& players() const { return m_playerManager; }
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
const UnitConfigManager& unitConfigs() const { return m_unitConfigManager; }
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
RoadConfigManager& roadConfigs() { return m_roadConfigManager; }
const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; }
ActionNeededManager& actionNeededManager() { return m_actionNeededManager; }
const ActionNeededManager& actionNeededManager() const { return m_actionNeededManager; }
TechTreeManager& techTree() { return m_techTreeManager; }
const TechTreeManager& techTree() const { return m_techTreeManager; }
// Tech Tree View & Research System
bool isTechTreeOpen() const { return m_isTechTreeOpen; }
void openTechTree();
void closeTechTree();
void toggleTechTree();
int calculatePlayerSciencePerTick(int playerId, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
int getTechRemainingScience(const Player &player, const QString &techId) const;
bool selectTechForPlayer(int playerId, const QString &targetTechId, bool appendToQueue = false);
void removeTechFromPlayerQueue(int playerId, const QString &techId);
void clearPlayerResearchQueue(int playerId);
void advancePlayerResearch(Player &player, int scienceGenerated);
QStringList getTechUnlocks(const QString &techId) const;
QStringList getTechObsoletes(const QString &techId) const;
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
const std::vector<City>& getCities() const { return m_cities; }
@@ -108,15 +113,29 @@ public:
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
bool buyTileForCity(int cityId, int tx, int ty);
void queueCityProject(int cityId, CityProjectType type, const QString &id);
void setCityProject(int cityId, CityProjectType type, const QString &id);
void cancelCityProject(int cityId);
void removeQueuedCityProject(int cityId, int queueIndex);
void completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandAttack(Unit *target);
void getPlannedOriginTile(const Unit &unit, bool appendWithShift, int &outX, int &outY) const;
bool scheduleOrStartUnitAction(Unit &unit, const UnitAction &action, bool appendWithShift,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
bool startUnitAction(Unit &unit, const UnitAction &action,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void continueCurrentUnitAction(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
bool commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift = false);
bool commandAttack(Unit *target, Matrix2D<TerrainPoint> *mapPoints = nullptr, int numPoints05 = 0, bool appendWithShift = false);
bool commandBuildCityAtTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift = false);
bool commandBuildFarmAtTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift = false);
bool commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift = false);
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
@@ -159,6 +178,8 @@ private:
UnitConfigManager m_unitConfigManager;
BuildingConfigManager m_buildingConfigManager;
RoadConfigManager m_roadConfigManager;
ActionNeededManager m_actionNeededManager;
TechTreeManager m_techTreeManager;
std::vector<std::shared_ptr<Unit>> m_units;
std::vector<City> m_cities;
std::vector<FloatingText> m_floatingTexts;
@@ -167,6 +188,7 @@ private:
int m_selectedUnitId;
int m_activeCityViewId;
bool m_isTechTreeOpen;
ActionMode m_actionMode;
float m_movementTickDuration; // Default 30.0s
BIN
View File
Binary file not shown.
+18 -7
View File
@@ -56,7 +56,7 @@ def trace_river(start_tile, edge_steps):
path.append((curr, in_edge, None))
return path
def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png", preview_path="/home/bart/project/tiletest/images/rivermap_preview.png"):
def build_rivermap(output_path="/home/bart/git/Ascent-of-Man/images/rivermap.png", preview_path="/home/bart/git/Ascent-of-Man/images/rivermap_preview.png"):
river_img = Image.new('RGB', (WIDTH, HEIGHT), (0, 0, 0))
draw = ImageDraw.Draw(river_img)
@@ -93,7 +93,7 @@ def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png"
# Build an overlay preview on top of typemap.png
try:
type_img = Image.open("/home/bart/project/tiletest/images/typemap.png").convert("RGBA")
type_img = Image.open("/home/bart/git/Ascent-of-Man/images/typemap.png").convert("RGBA")
r_arr = np.array(river_img)
t_arr = np.array(type_img)
mask = (r_arr[:, :, 2] > 120) & (r_arr[:, :, 0] < 100)
@@ -103,7 +103,18 @@ def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png"
print(f"Saved preview to {preview_path}")
# Build orthographic hex-tile minimap.png
h_arr = np.array(Image.open("/home/bart/project/tiletest/images/heightmap.jpg"))
water_threshold = 6
try:
with open("/home/bart/git/Ascent-of-Man/config/terrain_movement.yaml", "r") as yf:
for line in yf:
if line.strip().startswith("water_height_threshold:"):
val_str = line.split(":", 1)[1].split("#", 1)[0].strip()
water_threshold = int(val_str)
break
except Exception:
pass
h_arr = np.array(Image.open("/home/bart/git/Ascent-of-Man/images/heightmap.jpg"))
R = 8.0
W = 3608
H = 1740
@@ -122,11 +133,11 @@ def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png"
px = min(3599, max(0, int(round(wx * SCALE_X))))
py = min(1499, max(0, int(round(wy * SCALE_Y))))
h = h_arr[1500 - 1 - py, px]
h = int(h_arr[1500 - 1 - py, px, 0]) if h_arr.ndim == 3 else int(h_arr[1500 - 1 - py, px])
tp = t_arr[1500 - 1 - py, px]
dist_pole = abs((wy / 108.22) - 0.5) * 2.0
if h <= 2:
if h < water_threshold:
color = (220, 235, 250) if dist_pole > 0.85 else (28, 95, 175) # Water (blue) / polar ice
elif h > 182 or (dist_pole > 0.80 and h > 20):
color = (245, 250, 255) # Snow (white) above 2500m
@@ -169,8 +180,8 @@ def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png"
hdraw.line([(mpx, mpy), (cx, cy)], fill=(0, 220, 255), width=3)
hdraw.ellipse([mpx - 2, mpy - 2, mpx + 2, mpy + 2], fill=(0, 240, 255))
hex_img.save("/home/bart/project/tiletest/images/minimap.png")
print("Saved orthographic hex minimap to /home/bart/project/tiletest/images/minimap.png")
hex_img.save("/home/bart/git/Ascent-of-Man/images/minimap.png")
print("Saved orthographic hex minimap to /home/bart/git/Ascent-of-Man/images/minimap.png")
except Exception as ex:
print(f"Minimap/preview generation skipped: {ex}")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 117 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+59
View File
@@ -2,8 +2,12 @@
#define PLAYER_H
#include <QString>
#include <QStringList>
#include <QColor>
#include <QMap>
#include <vector>
#include <set>
#include <algorithm>
struct Player {
int id;
@@ -12,11 +16,17 @@ struct Player {
QColor secondaryColor;
// National resources (shared across all cities of this nation)
// Note: science represents per-tick science generation rate (no longer stockpiled)
int culture;
int science;
int gold;
int faith;
// Technology Tree & Research Progression
std::set<QString> researchedTechs;
QMap<QString, int> techProgress; // Tech ID -> accumulated science towards that tech
std::vector<QString> researchQueue; // Ordered queue of Tech IDs (front is active research)
Player()
: id(0), name("Unknown"), primaryColor(Qt::blue), secondaryColor(Qt::white),
culture(10000), science(0), gold(0), faith(0) {}
@@ -24,6 +34,55 @@ struct Player {
int pCulture = 10000, int pScience = 0, int pGold = 0, int pFaith = 0)
: id(pId), name(pName), primaryColor(primary), secondaryColor(secondary),
culture(pCulture), science(pScience), gold(pGold), faith(pFaith) {}
bool hasResearched(const QString &techId) const {
if (techId.isEmpty()) return true;
return researchedTechs.find(techId) != researchedTechs.end();
}
int getTechProgress(const QString &techId) const {
return techProgress.value(techId, 0);
}
bool hasActiveResearch() const {
return !researchQueue.empty();
}
QString getActiveResearchTech() const {
return researchQueue.empty() ? QString() : researchQueue.front();
}
bool isTechQueued(const QString &techId) const {
return std::find(researchQueue.begin(), researchQueue.end(), techId) != researchQueue.end();
}
int getTechQueuePosition(const QString &techId) const {
for (size_t i = 0; i < researchQueue.size(); ++i) {
if (researchQueue[i] == techId) {
return static_cast<int>(i) + 1;
}
}
return 0;
}
bool arePrerequisitesMet(const QStringList &prerequisites) const {
for (const QString &pre : prerequisites) {
if (!hasResearched(pre)) return false;
}
return true;
}
bool isItemUnlocked(const QString &requiredTech) const {
return requiredTech.isEmpty() || hasResearched(requiredTech);
}
bool isItemObsolete(const QString &obsoleteTech) const {
return !obsoleteTech.isEmpty() && hasResearched(obsoleteTech);
}
bool isItemBuildable(const QString &requiredTech, const QString &obsoleteTech) const {
return isItemUnlocked(requiredTech) && !isItemObsolete(obsoleteTech);
}
};
class PlayerManager {
BIN
View File
Binary file not shown.
+1092 -733
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1624 -384
View File
File diff suppressed because it is too large Load Diff
+45 -1
View File
@@ -14,6 +14,7 @@
#include "matrix2d.h"
#include "unitrenderer.h"
#include "gamemanager.h"
#include "actionneededmanager.h"
class QSceneRenderer : public QOpenGLWidget
{
@@ -56,7 +57,6 @@ protected:
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
// City & Territory Overlays
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
@@ -143,6 +143,12 @@ protected:
};
std::vector<CityProjectHit> m_cityProjectHits;
struct CityQueueItemHit {
QRect rect;
int queueIndex;
};
std::vector<CityQueueItemHit> m_cityQueueItemHits;
QRect m_cancelProjectBtnRect;
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
QRect m_tabBuildingsRect;
@@ -172,6 +178,44 @@ protected:
int amount;
};
std::vector<CityResourceChoiceHit> m_cityResourceChoiceHits;
// Action Needed Queue (Bottom HUD notification bar)
struct ActionItemHit {
QRect rect;
QRect dismissRect;
ActionNeededType type;
int entityId;
};
std::vector<ActionItemHit> m_actionItemHits;
QRect m_actionQueueHeaderRect;
void drawActionNeededQueue(QPainter &painter);
void handleActionItemClick(ActionNeededType type, int entityId);
// Technology Tree UI Overlay
void drawTechTreeOverlay(QPainter &painter);
QRect m_techTreeBtnRect;
QRect m_scienceBarHitRect;
QRect m_closeTechTreeBtnRect;
QRect m_clearTechQueueBtnRect;
QRect m_techTreeCanvasRect;
int m_techTreeScrollX = 0;
int m_techTreeScrollY = 0;
bool m_draggingTechTree = false;
bool m_techTreeDragMoved = false;
QPoint m_techTreeDragStartPos;
struct TechNodeHit {
QRect rect;
QString techId;
};
std::vector<TechNodeHit> m_techNodeHits;
struct TechQueueChipHit {
QRect rect;
QString techId;
};
std::vector<TechQueueChipHit> m_techQueueChipHits;
};
#endif // QSCENERENDERER_H
BIN
View File
Binary file not shown.
+16
View File
@@ -17,6 +17,10 @@ RoadConfigManager::RoadConfigManager()
dirt.colorStr = "#c49a62";
dirt.borderColorStr = "#6b4b23";
dirt.width = 3.5f;
dirt.worldWidth = 0.068f;
dirt.borderWidth = 0.022f;
dirt.textureScale = 2.4f;
dirt.opacity = 0.90f;
dirt.cost = 5;
m_types["dirt"] = dirt;
@@ -27,6 +31,10 @@ RoadConfigManager::RoadConfigManager()
stone.colorStr = "#9aa1a8";
stone.borderColorStr = "#464b50";
stone.width = 4.0f;
stone.worldWidth = 0.078f;
stone.borderWidth = 0.020f;
stone.textureScale = 2.8f;
stone.opacity = 0.94f;
stone.cost = 15;
m_types["stone"] = stone;
@@ -37,6 +45,10 @@ RoadConfigManager::RoadConfigManager()
rail.colorStr = "#3a3c40";
rail.borderColorStr = "#181a1c";
rail.width = 4.5f;
rail.worldWidth = 0.086f;
rail.borderWidth = 0.024f;
rail.textureScale = 3.0f;
rail.opacity = 0.96f;
rail.cost = 30;
m_types["rail"] = rail;
}
@@ -72,6 +84,10 @@ bool RoadConfigManager::loadConfigFile(const QString &filePath)
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62";
rCfg.borderColorStr = it.value().hasChild("border_color") ? it.value()["border_color"].asString("#6b4b23") : "#6b4b23";
rCfg.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
rCfg.worldWidth = it.value().hasChild("world_width") ? it.value()["world_width"].asFloat(0.068f) : 0.068f;
rCfg.borderWidth = it.value().hasChild("border_width") ? it.value()["border_width"].asFloat(0.022f) : 0.022f;
rCfg.textureScale = it.value().hasChild("texture_scale") ? it.value()["texture_scale"].asFloat(2.4f) : 2.4f;
rCfg.opacity = it.value().hasChild("opacity") ? it.value()["opacity"].asFloat(0.90f) : 0.90f;
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
m_types[rCfg.id] = rCfg;
}
+24 -2
View File
@@ -4,14 +4,36 @@
#include <QString>
#include <QMap>
#include <QColor>
#include <QVector3D>
struct TileRoad {
QString roadTypeId = "dirt"; // "dirt", "stone", "rail"
int tileX = 0;
int tileY = 0;
int playerId = 1;
unsigned char connectionMask = 0; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
QVector3D worldPos;
bool hasConnection(int edge) const {
return (connectionMask & (1 << (edge % 6))) != 0;
}
void setConnection(int edge, bool connected) {
if (connected) connectionMask |= (1 << (edge % 6));
else connectionMask &= ~(1 << (edge % 6));
}
};
struct RoadTypeConfig {
QString id = "dirt";
QString name = "Dirt Road";
float speedMultiplier = 1.30f;
QString colorStr = "#c49a62";
QString borderColorStr = "#6b4b23";
QString colorStr = "#d8b480";
QString borderColorStr = "#52381c";
float width = 3.5f;
float worldWidth = 0.056f;
float borderWidth = 0.012f;
float textureScale = 4.5f;
float opacity = 0.96f;
int cost = 5;
QColor getColor() const { return QColor(colorStr); }
+155 -16
View File
@@ -17,8 +17,21 @@ uniform int isFarm;
uniform float farmTextureScale;
uniform int farmMappingMode;
uniform int hasRoad;
uniform int roadMask;
uniform int roadType;
uniform float roadRadius;
uniform float roadBorderWidth;
uniform float roadTextureScale;
uniform float roadOpacity;
uniform float roadMeander;
uniform vec3 roadColor;
uniform vec3 roadBorderColor;
uniform float time;
uniform int riverMask;
uniform float waterLevelKm = 0.08235294;
uniform float coastBeachWidthKm = 0.022;
smooth in vec3 vNormal;
smooth in vec3 vWorldPos;
@@ -79,6 +92,61 @@ const vec2 EDGE_MIDPOINTS[6] = vec2[6](
vec2(-0.375, -0.21650635)
);
float bezierCurveDist(vec2 p, vec2 p0, vec2 p2) {
vec2 s0 = p0;
vec2 s1 = 0.5625 * p0 + 0.0625 * p2;
vec2 s2 = 0.2500 * p0 + 0.2500 * p2;
vec2 s3 = 0.0625 * p0 + 0.5625 * p2;
vec2 s4 = p2;
float d0 = distToSegment(p, s0, s1);
float d1 = distToSegment(p, s1, s2);
float d2 = distToSegment(p, s2, s3);
float d3 = distToSegment(p, s3, s4);
return min(min(d0, d1), min(d2, d3));
}
float meanderingBezierDist(vec2 p, vec2 p0, vec2 p2, float amp, float s1) {
vec2 chord = p2 - p0;
float cLen = length(chord);
vec2 perp = (cLen > 1e-4) ? vec2(-chord.y / cLen, chord.x / cLen) : vec2(0.0, 1.0);
// sin^2(pi*t) has BOTH zero value AND zero derivative at t=0 and t=1,
// guaranteeing C1-continuous, kink-free transitions across hex borders!
const float PI_VAL = 3.14159265;
vec2 prevPt = p0;
float minDist = 999.0;
for (int i = 1; i <= 8; ++i) {
float t = float(i) / 8.0;
float omt = 1.0 - t;
vec2 basePt = (omt * omt) * p0 + (t * t) * p2;
float st = sin(PI_VAL * t);
float wave = amp * s1 * (st * st);
vec2 curPt = basePt + perp * wave;
minDist = min(minDist, distToSegment(p, prevPt, curPt));
prevPt = curPt;
}
return minDist;
}
float meanderingBranchDist(vec2 p, vec2 pe, float amp, float s1) {
float dLen = length(pe);
vec2 perp = (dLen > 1e-4) ? vec2(pe.y / dLen, -pe.x / dLen) : vec2(0.0, 1.0);
const float PI_VAL = 3.14159265;
vec2 prevPt = pe;
float minDist = 999.0;
for (int i = 1; i <= 6; ++i) {
float t = float(i) / 6.0;
vec2 basePt = pe * (1.0 - t);
float st = sin(PI_VAL * t);
float wave = amp * 0.65 * s1 * (st * st);
vec2 curPt = basePt + perp * wave;
minDist = min(minDist, distToSegment(p, prevPt, curPt));
prevPt = curPt;
}
return minDist;
}
float getRiverDistance(vec2 p, int mask) {
if (mask == 0) return 999.0;
@@ -92,18 +160,7 @@ float getRiverDistance(vec2 p, int mask) {
}
if (count == 2) {
vec2 p0 = EDGE_MIDPOINTS[edges[0]];
vec2 p2 = EDGE_MIDPOINTS[edges[1]];
vec2 s0 = p0;
vec2 s1 = 0.5625 * p0 + 0.0625 * p2;
vec2 s2 = 0.2500 * p0 + 0.2500 * p2;
vec2 s3 = 0.0625 * p0 + 0.5625 * p2;
vec2 s4 = p2;
float d0 = distToSegment(p, s0, s1);
float d1 = distToSegment(p, s1, s2);
float d2 = distToSegment(p, s2, s3);
float d3 = distToSegment(p, s3, s4);
return min(min(d0, d1), min(d2, d3));
return bezierCurveDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]]);
} else {
float minDist = 999.0;
for (int i = 0; i < count; ++i) {
@@ -114,6 +171,38 @@ float getRiverDistance(vec2 p, int mask) {
}
}
float getRoadDistance(vec2 p, int mask, int ix, int iy, float amp) {
// Exact integer parity per hex tile: completely immune to floating-point interpolation jitter across scanlines!
int parity = (ix * 3 + iy * 5) & 1;
float s1 = (parity == 0) ? 1.0 : -1.0;
int edges[6];
int count = 0;
for (int e = 0; e < 6; ++e) {
if ((mask & (1 << e)) != 0) {
edges[count] = e;
count++;
}
}
if (count == 0) {
return meanderingBezierDist(p, vec2(-0.18, 0.0), vec2(0.18, 0.0), amp * 0.7, s1);
} else if (count == 1) {
return meanderingBranchDist(p, EDGE_MIDPOINTS[edges[0]], amp, s1);
} else if (count == 2) {
return meanderingBezierDist(p, EDGE_MIDPOINTS[edges[0]], EDGE_MIDPOINTS[edges[1]], amp, s1);
} else {
float minDist = 999.0;
for (int i = 0; i < count; ++i) {
minDist = min(minDist, meanderingBranchDist(p, EDGE_MIDPOINTS[edges[i]], amp * 0.5, s1));
for (int j = i + 1; j < count; ++j) {
minDist = min(minDist, bezierCurveDist(p, EDGE_MIDPOINTS[edges[i]], EDGE_MIDPOINTS[edges[j]]));
}
}
return minDist;
}
}
void main() {
vec3 norm = normalize(vNormal);
@@ -145,8 +234,12 @@ void main() {
// Snowline in kilometers: 2.5 (2500m) in temperate zone, dropping to 0.0 (sea level) at poles!
float snowLine = mix(2.5, 0.0, poleFactor);
// 3. Water vs Land detection (z_pos = height in kilometers, 0.0 = sea level)
float oceanWaterFac = 1.0 - smoothstep(0.005, 0.025, z_pos);
// 3. Water vs Land detection (z_pos = height in kilometers; water is height < water_height_threshold)
// One heightmap step is 3.5 / 255.0 = 0.0137255 km, so height = threshold - 1 is at waterLevelKm - 0.0137255.
// All water hexes (height <= threshold - 1) remain 100% water and transition smoothly across the shoreline.
float waterEdgeLow = max(0.0, waterLevelKm - 0.0135);
float waterEdgeHigh = max(waterEdgeLow + 0.002, waterLevelKm - 0.0015);
float oceanWaterFac = 1.0 - smoothstep(waterEdgeLow, waterEdgeHigh, z_pos);
// River water detection
float riverDist = getRiverDistance(fragVert.xy, riverMask);
@@ -234,10 +327,13 @@ void main() {
biomeGround = farmColor;
}
// Shoreline beach: where gentle slopes meet water (sea level up to ~110m, or along rivers)
// Shoreline beach: where gentle slopes meet water right along the waterline (or along rivers)
// Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach
float gentleSlope = smoothstep(0.55, 0.85, norm.z);
float coastBeach = (1.0 - smoothstep(0.015, 0.110, z_pos)) * smoothstep(0.003, 0.015, z_pos) * gentleSlope;
float beachStart = max(0.0, waterLevelKm - 0.015);
float beachPeak = waterEdgeHigh;
float beachEnd = waterLevelKm + max(0.005, coastBeachWidthKm);
float coastBeach = (1.0 - smoothstep(beachPeak, beachEnd, z_pos)) * smoothstep(beachStart, beachPeak, z_pos) * gentleSlope;
float riverBeach = riverBankFac;
float beachFac = max(coastBeach, riverBeach);
@@ -276,6 +372,49 @@ void main() {
vec4 nonSnowLand = mix(biomeGround, cliffRockColor, rockWeight);
vec4 landColor = mix(nonSnowLand, snowColor, snowWeight);
// 6b. Road Texture Integration (blended directly into the tile surface texture)
if (hasRoad == 1) {
// Recover exact integer grid indices of the tile center (spacing is 0.75 in X and 0.43287992 in Y)
vec2 tileCenterWorld = vWorldPos.xy - fragVert.xy;
int ix = int(floor(tileCenterWorld.x / 0.75 + 0.5));
int iy = int(floor(tileCenterWorld.y / 0.43287992 + 0.5));
float amp = max(0.0, roadMeander);
float rScale = max(1.0, roadTextureScale);
vec2 uvRoad = vWorldPos.xy * rScale;
// Sample textures in uniform control flow (before any fragment-dependent branch) to avoid mipmap derivative glitches
vec3 soilTex = texture(texSand, uvRoad).rgb;
vec3 saharaTex = texture(texSahara, uvRoad * 0.9).rgb;
float fineGrain = (mix(soilTex.r, saharaTex.r, 0.5) - 0.5) * 0.18;
float rawRoadDist = getRoadDistance(fragVert.xy, roadMask, ix, iy, amp);
float rCore = max(0.012, roadRadius);
float rOuter = rCore + max(0.005, roadBorderWidth);
float roadCoreFac = 1.0 - smoothstep(rCore * 0.65, rCore, rawRoadDist);
float roadTotalFac = (1.0 - smoothstep(rCore, rOuter, rawRoadDist)) * (1.0 - oceanWaterFac);
if (roadTotalFac > 0.001) {
vec3 centerTone = clamp(roadColor * (1.0 + fineGrain), 0.0, 1.0);
vec3 borderTone = clamp(roadBorderColor * (1.0 + fineGrain * 0.7), 0.0, 1.0);
vec3 roadSurf = mix(borderTone, centerTone, roadCoreFac);
// If crossing a river, render a bridge deck and suppress river water under the bridge
if (riverWaterFac > 0.01) {
vec3 bridgeCol = (roadType >= 1)
? vec3(0.72, 0.70, 0.66) * (1.0 + fineGrain)
: vec3(0.68, 0.48, 0.28) * (1.0 + fineGrain);
roadSurf = mix(roadSurf, bridgeCol, riverWaterFac);
waterFac *= (1.0 - roadTotalFac * 0.98);
}
float blendAlpha = roadTotalFac * clamp(roadOpacity, 0.0, 1.0);
landColor.rgb = mix(landColor.rgb, roadSurf, blendAlpha);
}
}
// 7. Combine Land and Sea
vec4 surfaceColor = mix(landColor, waterColor, waterFac);
+131
View File
@@ -0,0 +1,131 @@
#include "techconfig.h"
#include "yamlparser.h"
#include <QDebug>
#include <algorithm>
TechTreeManager::TechTreeManager()
: m_maxColumn(0), m_maxRow(0)
{
}
int TechTreeManager::computeTechDepth(const QString &techId, QMap<QString, int> &visited) const
{
if (visited.contains(techId)) {
return visited.value(techId);
}
visited[techId] = 0; // Guard against cycles
auto it = m_techs.find(techId);
if (it == m_techs.end()) {
return 0;
}
int maxPrereqDepth = -1;
for (const QString &pre : it.value().prerequisites) {
if (m_techs.contains(pre)) {
int d = computeTechDepth(pre, visited);
if (d > maxPrereqDepth) {
maxPrereqDepth = d;
}
}
}
int depth = maxPrereqDepth + 1;
visited[techId] = depth;
return depth;
}
bool TechTreeManager::loadConfigFile(const QString &filePath)
{
YamlNode root = YamlParser::parseFile(filePath);
if (root.children.isEmpty()) {
qWarning() << "Failed to parse technologies config YAML:" << filePath;
return false;
}
m_techs.clear();
m_orderedTechIds.clear();
m_maxColumn = 0;
m_maxRow = 0;
const YamlNode &techsNode = root.hasChild("technologies") ? root["technologies"] : root;
QMap<QString, bool> hasExplicitColumn;
QMap<QString, bool> hasExplicitRow;
for (auto it = techsNode.children.begin(); it != techsNode.children.end(); ++it) {
const YamlNode &node = it.value();
TechConfig cfg;
cfg.id = node.hasChild("id") ? node["id"].asString() : it.key();
cfg.name = node.hasChild("name") ? node["name"].asString() : cfg.id;
cfg.scienceCost = node.hasChild("science_cost") ? node["science_cost"].asInt(1000) : 1000;
if (node.hasChild("cost") && !node.hasChild("science_cost")) {
cfg.scienceCost = node["cost"].asInt(1000);
}
cfg.scienceCost = std::max(1, cfg.scienceCost);
if (node.hasChild("prerequisites")) {
cfg.prerequisites = node["prerequisites"].listValues;
}
if (node.hasChild("icon")) {
cfg.iconPath = node["icon"].asString("images/resources/kennis.png");
}
if (node.hasChild("description")) {
cfg.description = node["description"].asString("");
}
if (node.hasChild("column")) {
cfg.column = node["column"].asInt(0);
hasExplicitColumn[cfg.id] = true;
}
if (node.hasChild("row")) {
cfg.row = node["row"].asInt(0);
hasExplicitRow[cfg.id] = true;
}
m_techs[cfg.id] = cfg;
}
// Auto-calculate column and row for any tech without explicit coordinates
QMap<QString, int> depthCache;
QMap<int, int> nextRowInCol;
for (auto it = m_techs.begin(); it != m_techs.end(); ++it) {
if (!hasExplicitColumn.value(it.key(), false)) {
it.value().column = computeTechDepth(it.key(), depthCache);
}
if (hasExplicitRow.value(it.key(), false)) {
nextRowInCol[it.value().column] = std::max(nextRowInCol.value(it.value().column, 0), it.value().row + 1);
}
}
for (auto it = m_techs.begin(); it != m_techs.end(); ++it) {
if (!hasExplicitRow.value(it.key(), false)) {
int col = it.value().column;
it.value().row = nextRowInCol.value(col, 0);
nextRowInCol[col] = it.value().row + 1;
}
m_maxColumn = std::max(m_maxColumn, it.value().column);
m_maxRow = std::max(m_maxRow, it.value().row);
m_orderedTechIds.push_back(it.key());
}
std::sort(m_orderedTechIds.begin(), m_orderedTechIds.end(), [&](const QString &a, const QString &b) {
const TechConfig &ta = m_techs[a];
const TechConfig &tb = m_techs[b];
if (ta.column != tb.column) return ta.column < tb.column;
if (ta.row != tb.row) return ta.row < tb.row;
return ta.id < tb.id;
});
qDebug() << "TechTreeManager loaded" << m_techs.size() << "technologies from" << filePath;
return !m_techs.isEmpty();
}
const TechConfig* TechTreeManager::getTech(const QString &id) const
{
auto it = m_techs.find(id);
if (it != m_techs.end()) {
return &it.value();
}
return nullptr;
}
+45
View File
@@ -0,0 +1,45 @@
#ifndef TECHCONFIG_H
#define TECHCONFIG_H
#include <QString>
#include <QStringList>
#include <QMap>
#include <vector>
struct TechConfig {
QString id;
QString name;
int scienceCost;
QStringList prerequisites;
int column;
int row;
QString iconPath;
QString description;
TechConfig()
: id(""), name(""), scienceCost(1000), column(0), row(0),
iconPath("images/resources/kennis.png"), description("") {}
};
class TechTreeManager {
public:
TechTreeManager();
bool loadConfigFile(const QString &filePath);
const TechConfig* getTech(const QString &id) const;
const QMap<QString, TechConfig>& getAllTechs() const { return m_techs; }
const std::vector<QString>& getOrderedTechIds() const { return m_orderedTechIds; }
int getMaxColumn() const { return m_maxColumn; }
int getMaxRow() const { return m_maxRow; }
private:
int computeTechDepth(const QString &techId, QMap<QString, int> &visited) const;
QMap<QString, TechConfig> m_techs;
std::vector<QString> m_orderedTechIds;
int m_maxColumn;
int m_maxRow;
};
#endif // TECHCONFIG_H
+4
View File
@@ -9,6 +9,7 @@
TerrainMovementConfig::TerrainMovementConfig()
: waterHeightThreshold(6),
coastBeachWidthKm(0.022f),
mountainRockHeightKm(2.3f),
snowHeightKm(3.1f),
polarSnowLineEnabled(true),
@@ -61,6 +62,9 @@ bool TerrainMovementManager::loadConfig(const QString &filePath)
if (root.hasChild("water_height_threshold")) {
m_config.waterHeightThreshold = root["water_height_threshold"].asInt(6);
}
if (root.hasChild("coast_beach_width_km")) {
m_config.coastBeachWidthKm = root["coast_beach_width_km"].asFloat(0.022f);
}
if (root.hasChild("mountain_rock_height_km")) {
m_config.mountainRockHeightKm = root["mountain_rock_height_km"].asFloat(2.3f);
}
+1
View File
@@ -35,6 +35,7 @@ struct DomainMovementRule {
struct TerrainMovementConfig {
int waterHeightThreshold;
float coastBeachWidthKm;
float mountainRockHeightKm;
float snowHeightKm;
bool polarSnowLineEnabled;
Binary file not shown.
+1
View File
@@ -35,6 +35,7 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500);
m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f);
m_cityConfig.maxProductionQueueSize = cityRoot["max_production_queue_size"].asInt(6);
if (cityRoot.hasChild("base_yields")) {
const YamlNode &bNode = cityRoot["base_yields"];
+2
View File
@@ -34,6 +34,7 @@ struct CityConfig {
int baseResourceYieldPerTick;
int tileCultureCost;
float foodToInhabitantsRatio;
int maxProductionQueueSize;
int slotLongPressDurationMs;
QStringList resourcePriority;
@@ -47,6 +48,7 @@ struct CityConfig {
cityViewZoom(4.8f),
baseResourceYieldPerTick(500), tileCultureCost(10000),
foodToInhabitantsRatio(1.0f),
maxProductionQueueSize(6),
slotLongPressDurationMs(400),
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
{
Binary file not shown.
+51 -15
View File
@@ -3,6 +3,7 @@
#include <math.h>
#include "qscenerenderer.h"
#include "gamemanager.h"
#include "terrainmovementmanager.h"
#include <QVector3D>
QImage * Tile::m_image_grass = NULL;
QOpenGLTexture * Tile::m_texture_grass = NULL;
@@ -127,31 +128,31 @@ void Tile::init(QSceneRenderer* s, int x, int y)
buildHeights();
if (Tile::m_image_grass == NULL)
Tile::m_image_grass = new QImage("/home/bart/project/tiletest/images/grass.jpg");
Tile::m_image_grass = new QImage("/home/bart/git/Ascent-of-Man/images/grass.jpg");
if (Tile::m_image_border == NULL)
Tile::m_image_border = new QImage("/home/bart/project/tiletest/images/border.png");
Tile::m_image_border = new QImage("/home/bart/git/Ascent-of-Man/images/border.png");
if (Tile::m_image_rock == NULL)
Tile::m_image_rock = new QImage("/home/bart/project/tiletest/images/Texturelabs_Stone_136XL_seamless.jpg");
Tile::m_image_rock = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Stone_136XL_seamless.jpg");
if (Tile::m_image_sand == NULL)
Tile::m_image_sand = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_126XL_seamless.jpg");
Tile::m_image_sand = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_126XL_seamless.jpg");
if (Tile::m_image_water == NULL)
Tile::m_image_water = new QImage("/home/bart/project/tiletest/images/Texturelabs_Water_152XL_seamless.jpg");
Tile::m_image_water = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Water_152XL_seamless.jpg");
if (Tile::m_image_snow == NULL)
Tile::m_image_snow = new QImage("/home/bart/project/tiletest/images/Texturelabs_Water_155XL_seamless.jpg");
Tile::m_image_snow = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Water_155XL_seamless.jpg");
if (Tile::m_image_typemap == NULL)
Tile::m_image_typemap = new QImage("/home/bart/project/tiletest/images/typemap.png");
Tile::m_image_typemap = new QImage("/home/bart/git/Ascent-of-Man/images/typemap.png");
if (Tile::m_image_sahara == NULL)
Tile::m_image_sahara = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_133L_seamless.jpg");
Tile::m_image_sahara = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_133L_seamless.jpg");
if (Tile::m_image_tundra == NULL)
Tile::m_image_tundra = new QImage("/home/bart/project/tiletest/images/Texturelabs_Soil_135XL_seamless.jpg");
Tile::m_image_tundra = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Soil_135XL_seamless.jpg");
if (Tile::m_image_redsand == NULL)
Tile::m_image_redsand = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedSand_seamless.jpg");
Tile::m_image_redsand = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_RedSand_seamless.jpg");
if (Tile::m_image_redrock == NULL)
Tile::m_image_redrock = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedRock_seamless.jpg");
Tile::m_image_redrock = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_RedRock_seamless.jpg");
if (Tile::m_image_forest == NULL)
Tile::m_image_forest = new QImage("/home/bart/project/tiletest/images/Texturelabs_Forest_seamless.jpg");
Tile::m_image_forest = new QImage("/home/bart/git/Ascent-of-Man/images/Texturelabs_Forest_seamless.jpg");
if (Tile::m_image_farm == NULL)
Tile::m_image_farm = new QImage("/home/bart/project/tiletest/images/farm.jpeg");
Tile::m_image_farm = new QImage("/home/bart/git/Ascent-of-Man/images/farm.jpeg");
if (Tile::m_texture_grass == NULL) {
m_texture_grass = new QOpenGLTexture(*m_image_grass);
@@ -247,8 +248,8 @@ void Tile::init(QSceneRenderer* s, int x, int y)
if (first){
m_program = new QOpenGLShaderProgram();
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/project/tiletest/shaders/simple.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/project/tiletest/shaders/simple.frag");
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/git/Ascent-of-Man/shaders/simple.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/git/Ascent-of-Man/shaders/simple.frag");
m_program->link();
}
m_program->bind();
@@ -361,6 +362,41 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
m_program->setUniformValue("farmTextureScale", fScale);
m_program->setUniformValue("farmMappingMode", mappingMode);
const TileRoad *road = GameManager::instance().getRoadAt(m_x, m_y);
if (road != nullptr) {
const RoadTypeConfig *rCfg = GameManager::instance().roadConfigs().getRoadType(road->roadTypeId);
int rType = 0;
if (road->roadTypeId == "stone") rType = 1;
else if (road->roadTypeId == "rail") rType = 2;
float rRad = rCfg ? (rCfg->worldWidth * 0.5f) : 0.034f;
float rBorder = rCfg ? rCfg->borderWidth : 0.022f;
float rScale = rCfg ? rCfg->textureScale : 2.4f;
float rOpacity = rCfg ? rCfg->opacity : 0.90f;
float rMeander = GameManager::instance().roadConfigs().meanderAmplitude();
QColor col = rCfg ? rCfg->getColor() : QColor("#c49a62");
QColor bCol = rCfg ? rCfg->getBorderColor() : QColor("#6b4b23");
m_program->setUniformValue("hasRoad", 1);
m_program->setUniformValue("roadMask", (int)road->connectionMask);
m_program->setUniformValue("roadType", rType);
m_program->setUniformValue("roadRadius", rRad);
m_program->setUniformValue("roadBorderWidth", rBorder);
m_program->setUniformValue("roadTextureScale", rScale);
m_program->setUniformValue("roadOpacity", rOpacity);
m_program->setUniformValue("roadMeander", rMeander);
m_program->setUniformValue("roadColor", QVector3D(col.redF(), col.greenF(), col.blueF()));
m_program->setUniformValue("roadBorderColor", QVector3D(bCol.redF(), bCol.greenF(), bCol.blueF()));
} else {
m_program->setUniformValue("hasRoad", 0);
m_program->setUniformValue("roadMask", 0);
}
const TerrainMovementConfig &tmCfg = TerrainMovementManager::instance().getConfig();
float waterLevelKm = (tmCfg.waterHeightThreshold / 255.0f) * 3.5f;
m_program->setUniformValue("waterLevelKm", waterLevelKm);
m_program->setUniformValue("coastBeachWidthKm", tmCfg.coastBeachWidthKm);
m_program->setUniformValue("time", time);
m_program->setUniformValue("riverMask", (int)m_riverMask);
m_object.bind();
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6 -2
View File
@@ -32,7 +32,9 @@ SOURCES += main.cpp \
unitrenderer.cpp \
localization.cpp \
terrainmovementmanager.cpp \
roadconfig.cpp
roadconfig.cpp \
actionneededmanager.cpp \
techconfig.cpp
HEADERS += mainwindow.h \
qscenerenderer.h \
@@ -54,7 +56,9 @@ HEADERS += mainwindow.h \
unitrenderer.h \
localization.h \
terrainmovementmanager.h \
roadconfig.h
roadconfig.h \
actionneededmanager.h \
techconfig.h
FORMS += mainwindow.ui
BIN
View File
Binary file not shown.
+116 -47
View File
@@ -1,21 +1,70 @@
#include "unit.h"
#include "pathfinder.h"
#include "terrainmovementmanager.h"
#include "localization.h"
#include <algorithm>
#include <cmath>
#include <QStringList>
Unit::Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, float groundZ)
: id(uId), playerId(pId), config(cfg), tileX(startX), tileY(startY),
health((float)cfg.health), maxHealth((float)cfg.health),
buildCharges(cfg.buildCharges),
state(UnitState::Idle), pathIndex(0), moveProgress(0.0f),
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f),
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId("")
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f), rechargeTimer(0.0f),
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId(""),
movement_tick_duration(30.0f) // Default: 30 seconds per tick at 1x speed
{
QVector2D w = Pathfinder::hexToWorld(startX, startY);
worldPos = QVector3D(w.x(), w.y(), groundZ);
}
bool Unit::isRecharging() const {
// Only military units have recharge/cooldown
if (!config.canAttack()) return false;
return rechargeTimer > 0.0f;
}
bool Unit::canAttack(const Unit &other) const
{
if (other.playerId == playerId) return false;
if (other.isDead() || isDead()) return false;
if (!config.canAttack()) return false;
// Check if unit is currently on cooldown/recharge
if (isRecharging()) {
return false;
}
int dist = Pathfinder::hexDistance(tileX, tileY, other.tileX, other.tileY);
return dist <= config.attackRange;
}
void Unit::performAttack(Unit &target)
{
if (!canAttack(target)) return;
attackFlashTimer = config.attackFlashTicks * movement_tick_duration; // Convert ticks to seconds
rechargeTimer = config.rechargeTicks * movement_tick_duration; // Convert ticks to seconds
target.takeDamage((float)config.attackDamage);
// Retaliation if target is adjacent melee military unit
if (!target.isDead() && target.config.isMilitary() && !config.isRanged()) {
int dist = Pathfinder::hexDistance(tileX, tileY, target.tileX, target.tileY);
if (dist <= target.config.attackRange) {
takeDamage(target.config.attackDamage * 0.5f); // 50% counterattack damage
}
}
}
void Unit::takeDamage(float damage)
{
health = std::max(0.0f, health - damage);
attackFlashTimer = movement_tick_duration; // Flash lasts 1 tick
rechargeTimer = config.rechargeTicks * movement_tick_duration; // Reset recharge to full duration
}
float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (!mapPoints || numPoints05 <= 0) return 1.0f;
@@ -57,6 +106,36 @@ bool Unit::hasQueuedActionType(UnitActionType type) const
return false;
}
bool Unit::hasQueuedImprovementAt(int tx, int ty) const
{
if (currentAction.type == UnitActionType::BuildImprovement &&
currentAction.targetX == tx && currentAction.targetY == ty) {
return true;
}
for (const auto &act : actionQueue) {
if (act.type == UnitActionType::BuildImprovement &&
act.targetX == tx && act.targetY == ty) {
return true;
}
}
return false;
}
bool Unit::hasQueuedCityAt(int tx, int ty) const
{
if (currentAction.type == UnitActionType::BuildCity &&
currentAction.targetX == tx && currentAction.targetY == ty) {
return true;
}
for (const auto &act : actionQueue) {
if (act.type == UnitActionType::BuildCity &&
act.targetX == tx && act.targetY == ty) {
return true;
}
}
return false;
}
void Unit::removeQueuedActionsOfType(UnitActionType type)
{
for (auto it = actionQueue.begin(); it != actionQueue.end(); ) {
@@ -82,23 +161,35 @@ const UnitAction* Unit::peekNextQueuedAction() const
return &actionQueue.front();
}
QString Unit::getActionSummary(const UnitAction &act) const
{
switch (act.type) {
case UnitActionType::Move:
return Localization::instance().get("ui.actions.move", "Move");
case UnitActionType::BuildImprovement:
return Localization::instance().get("ui.actions.build_farm", "Build Farm");
case UnitActionType::BuildCity:
return Localization::instance().get("ui.actions.found_city", "Found City");
case UnitActionType::BuildRoad:
return Localization::instance().get("ui.actions.build_road", "Build Road");
case UnitActionType::Attack:
return Localization::instance().get("ui.actions.attack", "Attack");
default:
return "";
}
}
QString Unit::getQueuedActionSummary() const
{
if (actionQueue.empty()) return "";
switch (actionQueue.front().type) {
case UnitActionType::Move:
return "Move";
case UnitActionType::BuildImprovement:
return "Build Farm";
case UnitActionType::BuildCity:
return "Found City";
case UnitActionType::BuildRoad:
return "Build Road";
case UnitActionType::Attack:
return "Attack";
default:
return "Action";
QStringList parts;
for (size_t i = 0; i < actionQueue.size() && i < 3; ++i) {
parts.append(getActionSummary(actionQueue[i]));
}
if (actionQueue.size() > 3) {
parts.append(QString("+%1").arg(actionQueue.size() - 3));
}
return parts.join(" -> ");
}
void Unit::setPath(const std::vector<std::pair<int, int>> &newPath)
@@ -122,7 +213,9 @@ void Unit::stopMovement()
pathIndex = 0;
moveProgress = 0.0f;
state = UnitState::Idle;
currentAction = UnitAction();
clearActionQueue();
clearRoadRoute();
}
void Unit::startBuilding(const QString &impId, float duration)
@@ -149,10 +242,18 @@ void Unit::cancelBuilding()
void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
// Update attack flash timer (visual feedback)
if (attackFlashTimer > 0.0f) {
attackFlashTimer = std::max(0.0f, attackFlashTimer - deltaTime);
}
// Update recharge timer (cooldown between attacks)
// Recharge decays based on simulation ticks, not real-time multiplier
if (rechargeTimer > 0.0f) {
float tickDurationSec = deltaTime / movement_tick_duration;
rechargeTimer = std::max(0.0f, rechargeTimer - tickDurationSec);
}
if (state == UnitState::Building) {
if (buildDuration > 0.001f) {
buildProgress += (deltaTime * simSpeedMultiplier) / buildDuration;
@@ -214,35 +315,3 @@ void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
worldPos = QVector3D(curX, curY, curZ);
}
bool Unit::canAttack(const Unit &other) const
{
if (other.playerId == playerId) return false;
if (other.isDead() || isDead()) return false;
if (!config.canAttack()) return false;
int dist = Pathfinder::hexDistance(tileX, tileY, other.tileX, other.tileY);
return dist <= config.attackRange;
}
void Unit::performAttack(Unit &target)
{
if (!canAttack(target)) return;
attackFlashTimer = 0.5f; // Visual flash duration
target.takeDamage((float)config.attackDamage);
// Retaliation if target is adjacent melee military unit
if (!target.isDead() && target.config.isMilitary() && !config.isRanged()) {
int dist = Pathfinder::hexDistance(tileX, tileY, target.tileX, target.tileY);
if (dist <= target.config.attackRange) {
takeDamage(target.config.attackDamage * 0.5f); // 50% counterattack damage
}
}
}
void Unit::takeDamage(float damage)
{
health = std::max(0.0f, health - damage);
attackFlashTimer = 0.4f;
}
+37 -5
View File
@@ -35,8 +35,8 @@ struct UnitAction {
UnitAction() = default;
UnitAction(UnitActionType t) : type(t) {}
UnitAction(UnitActionType t, int x, int y, const QString &data = "")
: type(t), targetX(x), targetY(y), extraData(data) {}
UnitAction(UnitActionType t, int x, int y, const QString &data = "", int uId = -1)
: type(t), targetX(x), targetY(y), targetUnitId(uId), extraData(data) {}
UnitAction(UnitActionType t, int targetId)
: type(t), targetUnitId(targetId) {}
@@ -56,6 +56,9 @@ public:
float health;
float maxHealth;
int buildCharges;
// Simulation parameters (set during unit creation or via GameManager)
float movement_tick_duration; // Seconds per simulation tick
UnitState state;
// Movement Path
@@ -65,21 +68,50 @@ public:
int targetTileX;
int targetTileY;
// Action Queue
// Currently executing action & subsequent Action Queue
UnitAction currentAction;
std::deque<UnitAction> actionQueue;
int maxQueueSize = 5;
int maxQueueSize = 15;
// Road Construction Route (tile-by-tile road paving)
std::vector<std::pair<int, int>> roadRoute;
size_t roadRouteIndex = 0;
bool hasRoadRoute() const { return !roadRoute.empty() && roadRouteIndex < roadRoute.size(); }
void startRoadRoute(const std::vector<std::pair<int, int>> &route) {
roadRoute = route;
roadRouteIndex = 0;
}
void clearRoadRoute() {
roadRoute.clear();
roadRouteIndex = 0;
}
void advanceRoadRouteStep() {
if (!roadRoute.empty()) {
roadRouteIndex++;
}
}
bool hasActiveAction() const {
return currentAction.isValid() || state != UnitState::Idle || hasRoadRoute();
}
bool queueAction(const UnitAction &action);
void clearActionQueue();
bool hasQueuedAction() const { return !actionQueue.empty(); }
bool hasQueuedActionType(UnitActionType type) const;
bool hasQueuedImprovementAt(int tx, int ty) const;
bool hasQueuedCityAt(int tx, int ty) const;
void removeQueuedActionsOfType(UnitActionType type);
UnitAction popNextQueuedAction();
const UnitAction* peekNextQueuedAction() const;
QString getActionSummary(const UnitAction &act) const;
QString getQueuedActionSummary() const;
// Attack cooldown / flash
float attackFlashTimer;
float attackFlashTimer; // Flash effect timer (visual feedback after attack or taking damage)
float rechargeTimer; // Recharge/cooldown timer (time remaining until unit can attack again)
bool isRecharging() const; // Returns true if unit is currently on cooldown
// Building State
float buildProgress; // 0.0 to 1.0
BIN
View File
Binary file not shown.
+4 -2
View File
@@ -53,6 +53,8 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
cfg.infiniteCharges = root.hasChild("infinite_charges") ? root["infinite_charges"].asBool(cfg.buildCharges < 0) : (cfg.buildCharges < 0);
cfg.requiredTech = root.hasChild("required_tech") ? root["required_tech"].asString("") : "";
cfg.obsoleteTech = root.hasChild("obsolete_tech") ? root["obsolete_tech"].asString("") : "";
cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : "";
if (root.hasChild("domain")) {
@@ -80,11 +82,11 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
if (root.hasChild(key)) {
const YamlNode &sNode = root[key];
for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) {
cfg.actionSpeeds[it.key()] = it.value().asFloat(10.0f);
cfg.actionSpeeds[it.key()] = it.value().asFloat(2.0f);
}
}
};
parseActionMap("action_speeds");
parseActionMap("action_durations");
parseActionMap("action_durations");
m_configs[cfg.id] = cfg;
+13 -4
View File
@@ -44,17 +44,26 @@ struct UnitConfig {
int attackRange; // 1 for melee, >1 for ranged
int buildCharges;
bool infiniteCharges;
QString requiredTech;
QString obsoleteTech;
QString iconPath;
QStringList actions;
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f)
QMap<QString, float> actionSpeeds; // Duration in simulation ticks per action (e.g. "boerderij_bouwen": 2.0f)
QStringList impassableTerrains;
QStringList passableTerrains;
// Recharge / Cooldown configuration for military units (in simulation ticks)
int rechargeTicks; // Ticks between attacks for military units
int attackFlashTicks; // Ticks for visual attack flash effect
UnitConfig()
: id(""), name(""), category(UnitCategory::Builder),
movementDomain(MovementDomain::Land), health(100),
movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1),
buildCharges(-1), infiniteCharges(true), iconPath("") {}
buildCharges(-1), infiniteCharges(true), requiredTech(""), obsoleteTech(""), iconPath(""),
rechargeTicks(2), // Default recharge: 2 simulation ticks
attackFlashTicks(1) // Attack flash effect: 1 tick
{}
bool isRanged() const { return attackRange > 1; }
bool isMilitary() const { return category == UnitCategory::Military; }
@@ -66,11 +75,11 @@ struct UnitConfig {
bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); }
bool isBuilder() const { return canBuildFarm() || canBuildRoad(); }
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
float getActionDuration(const QString &actionName, float fallbackTicks = 2.0f) const {
if (actionSpeeds.contains(actionName)) {
return actionSpeeds.value(actionName);
}
return fallback;
return fallbackTicks;
}
};
BIN
View File
Binary file not shown.
+228 -5
View File
@@ -9,13 +9,15 @@ struct VertexPN {
};
UnitRenderer::UnitRenderer()
: m_program(nullptr), m_vertexCount(0), m_initialized(false)
: m_program(nullptr), m_vertexCount(0), m_initialized(false),
m_roadVao(), m_roadVbo(), m_roadBuffersInitialized(false)
{
}
UnitRenderer::~UnitRenderer()
{
if (m_program) delete m_program;
m_vao.destroy();
}
void UnitRenderer::initialize()
@@ -24,11 +26,17 @@ void UnitRenderer::initialize()
initializeOpenGLFunctions();
m_program = new QOpenGLShaderProgram();
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/project/tiletest/shaders/unit.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/project/tiletest/shaders/unit.frag");
m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "/home/bart/git/Ascent-of-Man/shaders/unit.vert");
m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "/home/bart/git/Ascent-of-Man/shaders/unit.frag");
m_program->link();
buildCylinderMesh();
m_roadVao.create();
m_roadVbo.create();
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
m_roadBuffersInitialized = true;
m_initialized = true;
}
@@ -46,7 +54,6 @@ void UnitRenderer::buildCylinderMesh()
float c2 = std::cos(a2), s2 = std::sin(a2);
// --- Side Quad (2 Triangles) ---
// Tri 1
vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f});
vertices.push_back({c2, s2, 0.0f, c2, s2, 0.0f});
vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f});
@@ -86,6 +93,222 @@ void UnitRenderer::buildCylinderMesh()
m_vbo.release();
}
void UnitRenderer::renderRoads(
const std::map<std::pair<int, int>, TileRoad> &roads,
const RoadConfigManager &roadConfigs,
Matrix2D<TerrainPoint> *mapPoints,
int numPoints05,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int cityViewRadius,
int cityViewTileX,
int cityViewTileY)
{
if (!m_initialized || !m_program || roads.empty()) return;
if (!m_roadBuffersInitialized) {
m_roadVao.create();
m_roadVbo.create();
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
m_roadBuffersInitialized = true;
}
float meanderAmp = roadConfigs.meanderAmplitude();
int subdivisions = std::max(2, roadConfigs.meanderSubdivisions());
struct RoadBatchGeometry {
std::vector<VertexPN> casingVertices;
std::vector<VertexPN> surfaceVertices;
};
std::unordered_map<std::string, RoadBatchGeometry> batches;
for (const auto &pair : roads) {
const TileRoad &road = pair.second;
if (cityViewRadius >= 0) {
if (Pathfinder::hexDistance(cityViewTileX, cityViewTileY, road.tileX, road.tileY) > cityViewRadius) {
continue;
}
}
const RoadTypeConfig *rCfg = roadConfigs.getRoadType(road.roadTypeId);
float roadW = rCfg ? rCfg->worldWidth : 0.11f;
float casingW = roadW + 0.035f;
QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY);
RoadBatchGeometry &batch = batches[road.roadTypeId.toStdString()];
// If isolated tile with no connections, draw small circular depot / disk
if (road.connectionMask == 0) {
float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05);
const int capSectors = 12;
const float pi = 3.14159265359f;
float rSurf = roadW * 0.9f;
float rCase = casingW * 0.9f;
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1});
}
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1});
}
continue;
}
// Center junction cap (circular disk covering junction gaps)
{
float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05);
const int capSectors = 12;
const float pi = 3.14159265359f;
float rSurf = (roadW * 0.5f) * 1.15f;
float rCase = (casingW * 0.5f) * 1.15f;
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1});
}
}
// For each connected edge, generate meandering ribbon quad strip
for (int e = 0; e < 6; ++e) {
if (!road.hasConnection(e)) continue;
QVector2D endP = center + getHexEdgeMidpoint(e);
QVector2D dir = endP - center;
float len = dir.length();
QVector2D perp = (len > 0.0001f) ? QVector2D(-dir.y() / len, dir.x() / len) : QVector2D(0, 0);
float seed = std::sin(float(road.tileX * 41 + road.tileY * 113 + e * 29)) * 43758.5453f;
float meanderSign = (seed - std::floor(seed) > 0.5f) ? 1.0f : -1.0f;
const float pi = 3.14159265359f;
std::vector<QVector2D> spinePoints;
spinePoints.reserve(subdivisions + 1);
for (int i = 0; i <= subdivisions; ++i) {
float t = (float)i / (float)subdivisions;
float s = meanderSign * meanderAmp * std::sin(t * pi);
spinePoints.push_back(center + dir * t + perp * s);
}
float halfSurf = roadW * 0.5f;
float halfCase = casingW * 0.5f;
for (int i = 0; i < subdivisions; ++i) {
QVector2D p0 = spinePoints[i];
QVector2D p1 = spinePoints[i + 1];
QVector2D segDir = p1 - p0;
float segLen = segDir.length();
QVector2D segPerp = (segLen > 0.0001f) ? QVector2D(-segDir.y() / segLen, segDir.x() / segLen) : perp;
float z0 = Unit::getTerrainHeight(p0.x(), p0.y(), mapPoints, numPoints05);
float z1 = Unit::getTerrainHeight(p1.x(), p1.y(), mapPoints, numPoints05);
float cz0 = z0 + 0.012f;
float cz1 = z1 + 0.012f;
QVector3D cL0(p0.x() - segPerp.x() * halfCase, p0.y() - segPerp.y() * halfCase, cz0);
QVector3D cR0(p0.x() + segPerp.x() * halfCase, p0.y() + segPerp.y() * halfCase, cz0);
QVector3D cL1(p1.x() - segPerp.x() * halfCase, p1.y() - segPerp.y() * halfCase, cz1);
QVector3D cR1(p1.x() + segPerp.x() * halfCase, p1.y() + segPerp.y() * halfCase, cz1);
batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR0.x(), cR0.y(), cR0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1});
batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1});
batch.casingVertices.push_back({cL1.x(), cL1.y(), cL1.z(), 0, 0, 1});
float sz0 = z0 + 0.016f;
float sz1 = z1 + 0.016f;
QVector3D sL0(p0.x() - segPerp.x() * halfSurf, p0.y() - segPerp.y() * halfSurf, sz0);
QVector3D sR0(p0.x() + segPerp.x() * halfSurf, p0.y() + segPerp.y() * halfSurf, sz0);
QVector3D sL1(p1.x() - segPerp.x() * halfSurf, p1.y() - segPerp.y() * halfSurf, sz1);
QVector3D sR1(p1.x() + segPerp.x() * halfSurf, p1.y() + segPerp.y() * halfSurf, sz1);
batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR0.x(), sR0.y(), sR0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sL1.x(), sL1.y(), sL1.z(), 0, 0, 1});
}
}
}
if (batches.empty()) return;
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(-2.0f, -2.0f);
m_program->bind();
m_program->setUniformValue("u_lightDir", lightDir);
QMatrix4x4 model;
m_program->setUniformValue("modelMatrix", model);
QMatrix4x4 mvp = projection * vMatrix;
m_program->setUniformValue("MVP", mvp);
m_roadVao.bind();
m_roadVbo.bind();
for (auto &bPair : batches) {
const RoadTypeConfig *rCfg = roadConfigs.getRoadType(QString::fromStdString(bPair.first));
QColor caseCol = rCfg ? rCfg->getBorderColor() : QColor(107, 75, 35);
QColor surfCol = rCfg ? rCfg->getColor() : QColor(196, 154, 98);
RoadBatchGeometry &geom = bPair.second;
// Draw Casing
if (!geom.casingVertices.empty()) {
m_roadVbo.allocate(geom.casingVertices.data(), (int)(geom.casingVertices.size() * sizeof(VertexPN)));
m_program->enableAttributeArray(0);
m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN));
m_program->enableAttributeArray(1);
m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN));
m_program->setUniformValue("u_color", QVector4D(caseCol.redF(), caseCol.greenF(), caseCol.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.casingVertices.size());
}
// Draw Surface
if (!geom.surfaceVertices.empty()) {
m_roadVbo.allocate(geom.surfaceVertices.data(), (int)(geom.surfaceVertices.size() * sizeof(VertexPN)));
m_program->enableAttributeArray(0);
m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN));
m_program->enableAttributeArray(1);
m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN));
m_program->setUniformValue("u_color", QVector4D(surfCol.redF(), surfCol.greenF(), surfCol.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.surfaceVertices.size());
}
}
m_roadVbo.release();
m_roadVao.release();
m_program->release();
glDisable(GL_POLYGON_OFFSET_FILL);
}
void UnitRenderer::renderUnits(
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
@@ -144,7 +367,7 @@ void UnitRenderer::renderUnits(
const Player *player = players.getPlayer(unit->playerId);
QColor col = player ? player->primaryColor : QColor(200, 200, 200);
// Flash white/red if attacked
// Flash white/red if attacked or damaged
if (unit->attackFlashTimer > 0.0f) {
float f = unit->attackFlashTimer / 0.5f;
col = QColor(
+26
View File
@@ -10,9 +10,13 @@
#include <QPointF>
#include <vector>
#include <memory>
#include <map>
#include "unit.h"
#include "player.h"
#include "city.h"
#include "roadconfig.h"
#include "matrix2d.h"
#include "terrain.h"
class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
public:
@@ -20,6 +24,18 @@ public:
~UnitRenderer();
void initialize();
void renderRoads(
const std::map<std::pair<int, int>, TileRoad> &roads,
const RoadConfigManager &roadConfigs,
Matrix2D<TerrainPoint> *mapPoints,
int numPoints05,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int cityViewRadius = -1,
int cityViewTileX = -1,
int cityViewTileY = -1
);
void renderUnits(
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
@@ -44,11 +60,21 @@ public:
private:
void buildCylinderMesh();
// Main unit cylinder mesh
QOpenGLShaderProgram *m_program;
QOpenGLBuffer m_vbo;
QOpenGLVertexArrayObject m_vao;
int m_vertexCount;
bool m_initialized;
// Road rendering buffers (existing functionality)
QOpenGLBuffer m_roadVbo;
QOpenGLVertexArrayObject m_roadVao;
bool m_roadBuffersInitialized;
};
#endif // UNITRENDERER_H
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.