This commit is contained in:
bart
2026-11-08 15:42:54 +01:00
parent dfd497d728
commit 5af4581182
80 changed files with 4494 additions and 437 deletions
+235
View File
@@ -0,0 +1,235 @@
# AGENTS.md — RTS Civilization Game Engine Architecture & Roadmap
This document serves as the primary technical specification, architecture reference, and development guide for AI agents and developers implementing an **RTS Civilization** game (Real-Time Strategy / 4X hybrid) on top of this hexagonal 3D terrain engine.
---
## 1. Project Vision & Core Concept
**Goal:** Transform this 3D hexagonal terrain engine into a seamless **Real-Time Strategy Civilization** game.
### Gameplay Formula:
- **Civilization Depth (4X):** Hex-based empire building, city founding, tile improvements (farms, mines, lumber mills), territory border expansion, technology tree, and resource economy (Food, Production, Gold, Science, Culture).
- **RTS Dynamics:** Continuous real-time simulation (with pause/speed controls) rather than strict turn-waiting. Units move, scout, build, and fight in real time with tactical unit control, responsive pathfinding, and dynamic skirmishes.
- **Visual Realism:** Modern OpenGL 4.0 rendering with realistic heightmaps, animated procedural water, smooth multi-biome blending (grassland, forest, desert, tundra, red rock canyon, snow-capped mountains), and hex-edge river networks.
---
## 2. Existing Codebase Architecture
The project is built on **C++**, **Qt (Widgets & OpenGL)**, and **OpenGL 4.0 Core Profile**.
### Key Source Files & Responsibilities:
- `terrain.h`:
- Map dimensions: `MAP_WIDTH = 300`, `MAP_HEIGHT = 125` (total 37,500 hexagons).
- Hex geometry constants: `LOD_0_SIZE = 1.0f`, `LOD_0_SIZE_Y = 0.4328799f`.
- Staggered column neighbor resolution: `getHexNeighbor(x, y, edge, &nx, &ny)`.
- Edge midpoints: `getHexEdgeMidpoint(edge)` for river routing.
- Data structure `TerrainPoint`: height, biome type, river flags.
- `tile.h` & `tile.cpp`:
- Hexagon subdivision into triangles (LOD system: 6 base triangles subdivided $\times 4 \times 4 = 96$ triangles per hex).
- Per-vertex elevation sampling from `heightmap.jpg` (scaled to $3.5\text{ km}$ peak height) with riverbed depression carving.
- Smooth neighbor-aware normal generation across hex seams.
- Triplanar texture binding: grass, rock cliff, sand beach, animated water, snow, sahara, tundra, red sand, red rock canyon, and woodland forest.
- `qscenerenderer.h` & `qscenerenderer.cpp`:
- `QOpenGLWidget` subclass managing camera view, rotation, and mouse interaction.
- Interactive orthographic hex-tile minimap with camera viewport outline, drag-to-pan, and zoom controls.
- `shaders/simple.vert` & `shaders/simple.frag`:
- GLSL 400 shaders.
- Biome typemap sampling and altitude/latitude-based snowline ($2.5\text{ km}$ temperate, down to sea-level at polar caps).
- Procedural dynamic dual-wave ocean and riverbed water rendering.
- `generate_rivers.py`:
- Python map preprocessor for strict hex-edge river connectivity and orthographic minimap generation.
### Coordinate Systems:
- **Grid Coordinates:** `(tx, ty)` with $0 \le tx < 300$ and $0 \le ty < 125$.
- **World Coordinates:**
$$\text{worldX} = tx \times 0.75$$
$$\text{worldY} = ty \times (2.0 \times \text{LOD\_0\_SIZE\_Y}) + (tx \% 2 == 0 \; ? \; 0.0 : \text{LOD\_0\_SIZE\_Y})$$
- **Elevation (Z):** $0.0$ at sea level; land rises up to $\approx 3.5\text{ km}$.
---
## 3. Game Systems Architecture
To implement the RTS Civilization game, develop modular subsystems decoupled from the raw rendering loop:
```
+-------------------+
| GameManager |
+---------+---------+
|
+-----------------+--------+--------+-----------------+
| | | |
+----+----+ +-----+-----+ +-----+-----+ +-----+-----+
| Faction | | Unit | | City | | Economy & |
| Manager | | Manager | | Manager | | Tech Tree |
+----+----+ +-----+-----+ +-----+-----+ +-----+-----+
| | | |
| +-------+-------+ | |
| | Pathfinding | | |
| | (Hex A*) | | |
| +---------------+ | |
+-----------------+-----------------+ |
| |
+--------+--------+ |
| Fog of War / | |
| Visibility |<-------------------------+
+--------+--------+
|
+--------+--------+
| QSceneRenderer | (OpenGL Viewport + UI Overlays)
+-----------------+
```
---
## 4. Agents & Unit Specifications
Units are autonomous or player-directed entities traversing the hex map in real time.
### 4.1 Unit Types
1. **Civilian Units:**
- **Settler:** Consumed to found a new City Center on valid land tiles (cannot settle on water or mountains).
- **Worker / Builder:** Constructs tile improvements (Farms, Mines, Lumber Mills, Quarries, Roads) and repairs pillaged tiles. Can be automated via worker AI.
- **Trader / Caravan:** Establishes land or sea trade routes between cities, providing continuous Gold, Food, and Production.
2. **Military Units (RTS Combat):**
- **Melee:** Scout $\rightarrow$ Warrior $\rightarrow$ Swordsman $\rightarrow$ Musketman $\rightarrow$ Infantry.
- **Ranged:** Archer $\rightarrow$ Crossbowman $\rightarrow$ Field Cannon $\rightarrow$ Artillery.
- **Cavalry:** Horseman $\rightarrow$ Knight $\rightarrow$ Cavalry $\rightarrow$ Tank (high movement speed, flanking bonuses).
- **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`)
```cpp
enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley };
enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified };
class Unit {
public:
int id;
int factionId;
UnitType type;
UnitState state;
// Grid Position & Smooth Real-time World Position
int tileX, tileY;
QVector3D worldPosition;
QVector3D targetPosition;
float moveProgress; // 0.0 to 1.0 between hexes
// Attributes
float health, maxHealth;
float attackDamage;
float defense;
float movementSpeed; // hexes per second
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 commandMoveTo(int destX, int destY);
void commandAttack(int targetId);
void commandBuildImprovement(int improvementType);
};
```
### 4.3 Real-time Hex Pathfinding (`Pathfinder.h`)
- Implement **A\*** over the hexagonal grid with cost functions:
- **Flatland (Grassland/Plains):** Base cost $1.0$.
- **Forest / Jungle:** Cost $2.0$.
- **Hills:** Cost $2.0$.
- **River Crossing:** Penalty $+1.5$ (unless road/bridge is present).
- **Mountain / Deep Water:** Impassable for land units.
- **Friendly vs Enemy Hexes:** ZOC (Zone of Control) penalties near hostile military units.
---
## 5. Cities, Territory & Improvements
### 5.1 City Architecture
- **City Founding:** Consumes a Settler. Claims the center hex and immediate ring of 6 adjacent hexes.
- **Borders:** Expanding territory based on accumulated Culture.
- **Hex Yields:**
- **Food:** Drives city population growth ($Pop = 1, 2, 3 \dots$).
- **Production (Hammers):** Contributes to the active construction queue (units, buildings, wonders).
- **Gold:** Empire treasury for unit upkeep, tile purchasing, and rush production.
- **Science:** Advances active technology research in real time.
- **Culture:** Expands borders and advances civics/policies.
### 5.2 Tile Improvements
- **Farm:** Built on Grassland/Plains/Floodplains ($+2\text{ Food}$, river adjacency bonus).
- **Mine:** Built on Hills ($+2\text{ Production}$).
- **Lumber Mill:** Built on Forest ($+1\text{ Production}$, preserves forest defense).
- **Road:** Built along hex edges/centers to reduce movement cost to $0.33$.
---
## 6. AI Agent Behavior (Civilization Empire AI)
AI opponents must operate through a hierarchical decision architecture:
1. **Strategic Commander (Empire Level):**
- Assesses empire needs: expansion, military defense, economic development, or technology rush.
- Queues city construction and manages empire-wide resource allocation.
2. **Operational Commander (Theater Level):**
- Coordinates multi-unit formations (e.g. Settler with Warrior escort; army squads containing front-line melee and rear-line archers).
- Manages tactical skirmishes, city sieges, and territorial defense.
3. **Tactical Agent (Unit Level):**
- Implements Utility AI or Behavior Trees:
- Worker AI: evaluates unworked tiles in city radius and constructs optimal improvements.
- Scout AI: moves toward unrevealed fog of war while avoiding hostile barbarians.
- Military AI: attacks vulnerable targets, retreats when health drops below $30\%$, uses terrain defense bonuses.
---
## 7. Fog of War & Vision System
1. **Three Visibility States:**
- **Unexplored (Black):** Never seen. No terrain, no entities.
- **Explored / Fog of War (Grayscale/Dimmed):** Previously discovered terrain and static cities visible; enemy units hidden.
- **Visible (Full Color):** Currently within line-of-sight of any friendly unit or city.
2. **GPU Texture Mask:**
- Maintain a $300 \times 125$ texture updated on the CPU / compute shader representing visibility $(0 = \text{black}, 128 = \text{fog}, 255 = \text{visible})$.
- Sample this mask in `simple.frag` to dim or mask terrain and hide out-of-vision units.
---
## 8. Implementation Roadmap
### Phase 1: Core Simulation Loop & Pathfinding
- [ ] Create `GameSimulation` class with fixed-tick update (e.g., 20 ticks/sec).
- [ ] Implement hex A* pathfinding (`Pathfinder.h/.cpp`) using `getHexNeighbor`.
- [ ] Add basic `Unit` class with real-time waypoint interpolation across hex centers.
- [ ] Implement mouse raycasting / hex picking from screen click to `(tx, ty)`.
### Phase 2: Unit Rendering & RTS Controls
- [ ] Render 3D unit meshes / billboards on tile surfaces at correct elevation $Z$.
- [ ] Implement standard RTS controls: Left-click select, box-select, right-click move/attack.
- [ ] Add unit health bars, selection rings, and minimap unit dots.
### Phase 3: Cities & Tile Improvements
- [ ] Implement `City` class, city founding with Settler, and territory border rendering.
- [ ] Add Worker unit improvement actions (Farm, Mine, Road).
- [ ] Implement resource yield accumulation (Food, Production, Gold, Science).
### Phase 4: Combat, Technology & Opponent AI
- [ ] Implement real-time combat calculation, ranged attack trajectories, and damage resolution.
- [ ] Add Technology Tree interface and research progression.
- [ ] Implement AI Factions and Barbarian encampments.
---
## 9. Coding Guidelines for Agents & Developers
- **Language Standard:** C++17 with Qt 5/6 framework.
- **Rendering Thread Safety:** Game logic and pathfinding must run synchronously or safely decoupled from `paintGL()`. Never call OpenGL functions outside the OpenGL context thread.
- **Memory Management:** Use RAII and smart pointers (`std::unique_ptr`, `std::shared_ptr`) for entities. Avoid raw manual array allocations in game loop updates.
- **Performance:**
- With 37,500 hexes, avoid iterating all hexes every frame. Use spatial partitioning or chunking for unit queries.
- Batch unit rendering using instanced OpenGL draw calls (`glDrawArraysInstanced` or `glDrawElementsInstanced`).
- **Math Consistency:** Always use `terrain.h` helper functions (`getHexNeighbor`, `getHexEdgeMidpoint`, `LOD_0_SIZE_Y`) to maintain consistent geometry and alignment with the terrain mesh.
+119 -11
View File
@@ -16,7 +16,7 @@ CC = gcc
CXX = g++
DEFINES = -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB
CFLAGS = -pipe -O2 -Wall -Wextra -D_REENTRANT -fPIC $(DEFINES)
CXXFLAGS = -pipe -O2 -Wall -Wextra -D_REENTRANT -fPIC $(DEFINES)
CXXFLAGS = -pipe -O2 -std=gnu++1z -Wall -Wextra -D_REENTRANT -fPIC $(DEFINES)
INCPATH = -I. -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. -I. -I/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++
QMAKE = /usr/lib/qt5/bin/qmake
DEL_FILE = rm -f
@@ -58,7 +58,14 @@ SOURCES = main.cpp \
vertex.cpp \
tile.cpp \
tilevertexinfo.cpp \
face.cpp qrc_shaders.cpp \
face.cpp \
player.cpp \
yamlparser.cpp \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
gamemanager.cpp \
unitrenderer.cpp qrc_shaders.cpp \
moc_mainwindow.cpp
OBJECTS = main.o \
mainwindow.o \
@@ -67,10 +74,19 @@ OBJECTS = main.o \
tile.o \
tilevertexinfo.o \
face.o \
player.o \
yamlparser.o \
unitconfig.o \
pathfinder.o \
unit.o \
gamemanager.o \
unitrenderer.o \
qrc_shaders.o \
moc_mainwindow.o
DIST = shaders/simple.vert \
shaders/simple.frag \
shaders/unit.vert \
shaders/unit.frag \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_pre.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/unix.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/linux.conf \
@@ -137,6 +153,7 @@ DIST = shaders/simple.vert \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/qt_config.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf \
.qmake.stash \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/exclusive_builds.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.prf \
@@ -164,13 +181,28 @@ DIST = shaders/simple.vert \
terrain.h \
tilevertexinfo.h \
matrix2d.h \
face.h main.cpp \
face.h \
player.h \
yamlparser.h \
unitconfig.h \
pathfinder.h \
unit.h \
city.h \
gamemanager.h \
unitrenderer.h main.cpp \
mainwindow.cpp \
qscenerenderer.cpp \
vertex.cpp \
tile.cpp \
tilevertexinfo.cpp \
face.cpp
face.cpp \
player.cpp \
yamlparser.cpp \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
gamemanager.cpp \
unitrenderer.cpp
QMAKE_TARGET = tiletest
DESTDIR =
TARGET = tiletest
@@ -248,6 +280,7 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/qt_config.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf \
.qmake.stash \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/exclusive_builds.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.prf \
@@ -337,6 +370,7 @@ Makefile: tiletest.pro /usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.con
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/qt_config.prf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf:
.qmake.stash:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/exclusive_builds.prf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf:
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.prf:
@@ -375,8 +409,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 $(DISTDIR)/
$(COPY_FILE) --parents main.cpp mainwindow.cpp qscenerenderer.cpp vertex.cpp tile.cpp tilevertexinfo.cpp face.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 gamemanager.h unitrenderer.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 gamemanager.cpp unitrenderer.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.ui $(DISTDIR)/
@@ -414,7 +448,7 @@ compiler_moc_predefs_make_all: moc_predefs.h
compiler_moc_predefs_clean:
-$(DEL_FILE) moc_predefs.h
moc_predefs.h: /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp
g++ -pipe -O2 -Wall -Wextra -dM -E -o moc_predefs.h /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp
g++ -pipe -O2 -std=gnu++1z -Wall -Wextra -dM -E -o moc_predefs.h /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp
compiler_moc_header_make_all: moc_mainwindow.cpp
compiler_moc_header_clean:
@@ -439,7 +473,13 @@ ui_mainwindow.h: mainwindow.ui \
vertex.h \
face.h \
tilevertexinfo.h \
matrix2d.h
matrix2d.h \
unitrenderer.h \
unit.h \
unitconfig.h \
player.h \
city.h \
gamemanager.h
/usr/lib/qt5/bin/uic mainwindow.ui -o ui_mainwindow.h
compiler_yacc_decl_make_all:
@@ -456,7 +496,20 @@ 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 \
player.h \
city.h \
gamemanager.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
@@ -465,7 +518,14 @@ qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
vertex.h \
face.h \
tilevertexinfo.h \
matrix2d.h
matrix2d.h \
unitrenderer.h \
unit.h \
unitconfig.h \
player.h \
city.h \
gamemanager.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qscenerenderer.o qscenerenderer.cpp
vertex.o: vertex.cpp vertex.h \
@@ -479,7 +539,13 @@ tile.o: tile.cpp tile.h \
tilevertexinfo.h \
terrain.h \
matrix2d.h \
qscenerenderer.h
qscenerenderer.h \
unitrenderer.h \
unit.h \
unitconfig.h \
player.h \
city.h \
gamemanager.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o tile.o tile.cpp
tilevertexinfo.o: tilevertexinfo.cpp tilevertexinfo.h \
@@ -492,6 +558,48 @@ face.o: face.cpp face.h \
tilevertexinfo.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o face.o face.cpp
player.o: player.cpp player.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o player.o player.cpp
yamlparser.o: yamlparser.cpp yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o yamlparser.o yamlparser.cpp
unitconfig.o: unitconfig.cpp unitconfig.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unitconfig.o unitconfig.cpp
pathfinder.o: pathfinder.cpp pathfinder.h \
terrain.h \
matrix2d.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o pathfinder.o pathfinder.cpp
unit.o: unit.cpp unit.h \
unitconfig.h \
terrain.h \
matrix2d.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unit.o unit.cpp
gamemanager.o: gamemanager.cpp gamemanager.h \
player.h \
unitconfig.h \
unit.h \
terrain.h \
matrix2d.h \
city.h \
pathfinder.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o gamemanager.o gamemanager.cpp
unitrenderer.o: unitrenderer.cpp unitrenderer.h \
unit.h \
unitconfig.h \
terrain.h \
matrix2d.h \
player.h \
city.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unitrenderer.o unitrenderer.cpp
qrc_shaders.o: qrc_shaders.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qrc_shaders.o qrc_shaders.cpp
Symlink
+1
View File
@@ -0,0 +1 @@
/home/bart/project/tiletest/AGENTS.md
Symlink
+1
View File
@@ -0,0 +1 @@
/home/bart/project/tiletest/AGENTS.md
+20
View File
@@ -0,0 +1,20 @@
#ifndef CITY_H
#define CITY_H
#include <QString>
#include <QVector3D>
struct City {
int id;
int playerId;
QString name;
int tileX;
int tileY;
QVector3D worldPos;
City() : id(0), playerId(0), name(""), tileX(0), tileY(0), worldPos(0, 0, 0) {}
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos)
: id(cId), playerId(pId), name(cName), tileX(tx), tileY(ty), worldPos(pos) {}
};
#endif // CITY_H
+1
View File
@@ -0,0 +1 @@
movement_tick_duration: 30.0
+10
View File
@@ -0,0 +1,10 @@
id: settler
name: Settler
category: bouwen
health: 100
movement_speed: 2.0
cost: 80
icon: images/units/settler.png
actions:
- verplaatsen
- steden_bouwen
+12
View File
@@ -0,0 +1,12 @@
id: slinger
name: Slinger
category: militair
health: 70
movement_speed: 2.0
cost: 35
attack_damage: 15
attack_range: 2
icon: images/units/slinger.png
actions:
- verplaatsen
- aanvallen
+12
View File
@@ -0,0 +1,12 @@
id: warrior
name: Warrior
category: militair
health: 100
movement_speed: 2.0
cost: 40
attack_damage: 20
attack_range: 1
icon: images/units/warrior.png
actions:
- verplaatsen
- aanvallen
+16 -15
View File
@@ -1,5 +1,5 @@
#include "face.h"
#include <cmath>
float Face::sign (QVector2D p1, QVector2D p2, QVector2D p3)
{
@@ -8,13 +8,8 @@ float Face::sign (QVector2D p1, QVector2D p2, QVector2D p3)
bool Face::PointInFace (QVector2D pt)
{
bool b1, b2, b3;
//b1 = sign(pt, v1, v2) < 0.0f;
//b2 = sign(pt, v2, v3) < 0.0f;
// b3 = sign(pt, v3, v1) < 0.0f;
return ((b1 == b2) && (b2 == b3));
Q_UNUSED(pt);
return true;
}
Face::Face(Vertex *v1,TileVertexInfo *vi1, Vertex *v2,TileVertexInfo *vi2,Vertex *v3,TileVertexInfo *vi3)
@@ -42,8 +37,10 @@ void Face::recalcNormal()
QVector3D va = v1 - v2;
QVector3D vb = v3 - v2;
m_normal = QVector3D::crossProduct(va,vb);
/*float norm_len = sqrt(dot(norm, norm));
norm /= norm_len; */
if (m_normal.z() < 0.0f) {
m_normal = -m_normal;
}
m_normal.normalize();
}
QVector3D Face::normal()
@@ -51,6 +48,11 @@ QVector3D Face::normal()
return m_normal;
}
static inline bool pointMatch(const QVector2D& p1, const QVector2D& p2)
{
return (std::abs(p1.x() - p2.x()) < 1e-4f) && (std::abs(p1.y() - p2.y()) < 1e-4f);
}
void Face::findFaces(std::vector<Face *> &vec, QVector2D s)
{
if (m_subface!=NULL) {
@@ -59,11 +61,10 @@ void Face::findFaces(std::vector<Face *> &vec, QVector2D s)
}
}
else {
if (
((m_v1->position().x() == s.x()) && (m_v1->position().y() == s.y())) ||
((m_v2->position().x() == s.x()) && (m_v2->position().y() == s.y())) ||
((m_v3->position().x()== s.x()) && (m_v3->position().y() == s.y()))
) {
if (m_v1 && m_v2 && m_v3 && (
pointMatch(m_v1->position(), s) ||
pointMatch(m_v2->position(), s) ||
pointMatch(m_v3->position(), s))) {
vec.push_back(this);
}
}
BIN
View File
Binary file not shown.
+223
View File
@@ -0,0 +1,223 @@
#include "gamemanager.h"
#include "pathfinder.h"
#include "yamlparser.h"
#include <QDebug>
#include <cmath>
GameManager& GameManager::instance()
{
static GameManager s_instance;
return s_instance;
}
GameManager::GameManager()
: m_selectedUnitId(-1), m_actionMode(ActionMode::None),
m_movementTickDuration(30.0f), m_tickTimer(0.0f),
m_simSpeedMultiplier(1.0f), m_nextUnitId(0), m_nextCityId(0),
m_initialized(false)
{
}
void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (m_initialized) return;
// Load game settings
QString gameConfigPath = baseDir + "/config/game.yaml";
YamlNode gameRoot = YamlParser::parseFile(gameConfigPath);
if (gameRoot.hasChild("movement_tick_duration")) {
m_movementTickDuration = gameRoot["movement_tick_duration"].asFloat(30.0f);
}
// Load unit YAML configurations
QString unitsConfigDir = baseDir + "/config/units";
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
// Spawn initial demo units near starting camera position (Cam: ~28, 16 km -> hex ~37, 18)
// Player 1 (Blue & Gold)
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05);
spawnUnit("warrior", 1, 37, 18, mapPoints, numPoints05);
spawnUnit("slinger", 1, 37, 19, mapPoints, numPoints05);
// Player 2 (Red & White) - Opponent units
spawnUnit("warrior", 2, 40, 18, mapPoints, numPoints05);
spawnUnit("slinger", 2, 41, 19, mapPoints, numPoints05);
m_initialized = true;
qDebug() << "GameManager initialized with" << m_units.size() << "units.";
}
Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, int tileY,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
const UnitConfig *cfg = m_unitConfigManager.getConfig(configId);
if (!cfg) {
qWarning() << "Unit config not found:" << configId;
return nullptr;
}
QVector2D w = Pathfinder::hexToWorld(tileX, tileY);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
int id = getNextUnitId();
auto unit = std::make_shared<Unit>(id, playerId, *cfg, tileX, tileY, gz);
m_units.push_back(unit);
return unit.get();
}
void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
// Update movement tick timer
m_tickTimer += deltaTime * m_simSpeedMultiplier;
if (m_tickTimer >= m_movementTickDuration) {
m_tickTimer = std::fmod(m_tickTimer, m_movementTickDuration);
}
// Update units
for (auto &u : m_units) {
if (!u->isDead()) {
u->update(deltaTime, m_movementTickDuration, m_simSpeedMultiplier, mapPoints, numPoints05);
}
}
// Clean up dead units
for (auto it = m_units.begin(); it != m_units.end(); ) {
if ((*it)->isDead()) {
if ((*it)->id == m_selectedUnitId) {
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
it = m_units.erase(it);
} else {
++it;
}
}
// Update floating texts
for (auto it = m_floatingTexts.begin(); it != m_floatingTexts.end(); ) {
it->life -= deltaTime;
it->worldPos.setZ(it->worldPos.z() + deltaTime * 0.4f); // Float upward
if (it->life <= 0.0f) {
it = m_floatingTexts.erase(it);
} else {
++it;
}
}
}
Unit* GameManager::getSelectedUnit()
{
return getUnitById(m_selectedUnitId);
}
const Unit* GameManager::getSelectedUnit() const
{
for (const auto &u : m_units) {
if (u->id == m_selectedUnitId) return u.get();
}
return nullptr;
}
void GameManager::selectUnit(int id)
{
m_selectedUnitId = id;
m_actionMode = ActionMode::None;
}
void GameManager::deselectUnit()
{
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
Unit* GameManager::getUnitAt(int tx, int ty)
{
for (auto &u : m_units) {
if (!u->isDead() && u->tileX == tx && u->tileY == ty) {
return u.get();
}
}
return nullptr;
}
Unit* GameManager::getUnitById(int id)
{
for (auto &u : m_units) {
if (u->id == id && !u->isDead()) return u.get();
}
return nullptr;
}
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected) return;
if (targetX == selected->tileX && targetY == selected->tileY) {
return;
}
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05
);
if (path.size() > 1) {
selected->setPath(path);
addFloatingText("Verplaatsen", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
}
}
void GameManager::commandAttack(Unit *target)
{
Unit *selected = getSelectedUnit();
if (!selected || !target) return;
if (!selected->canAttack(*target)) {
addFloatingText("Buiten bereik!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
float damage = (float)selected->config.attackDamage;
selected->performAttack(*target);
QString dmgText = QString("-%1 HP").arg((int)damage);
addFloatingText(dmgText, target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50));
if (target->isDead()) {
addFloatingText("Verslagen!", target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
}
m_actionMode = ActionMode::None;
}
void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected || !selected->config.canBuildCity()) return;
// Check if tile already has a city
for (const auto &c : m_cities) {
if (c.tileX == selected->tileX && c.tileY == selected->tileY) {
addFloatingText("Al een stad hier!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
}
int cId = getNextCityId();
QString cityName = QString("Stad %1").arg(cId);
QVector2D w = Pathfinder::hexToWorld(selected->tileX, selected->tileY);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
m_cities.emplace_back(cId, selected->playerId, cityName, selected->tileX, selected->tileY, QVector3D(w.x(), w.y(), gz));
addFloatingText(QString("%1 gesticht!").arg(cityName), QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
// Consuming settler
selected->takeDamage(99999.0f);
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
void GameManager::addFloatingText(const QString &text, const QVector3D &worldPos, const QColor &color, float duration)
{
m_floatingTexts.push_back({text, worldPos, duration, duration, color});
}
+95
View File
@@ -0,0 +1,95 @@
#ifndef GAMEMANAGER_H
#define GAMEMANAGER_H
#include <vector>
#include <memory>
#include <QString>
#include "player.h"
#include "unitconfig.h"
#include "unit.h"
#include "city.h"
#include "matrix2d.h"
#include "terrain.h"
enum class ActionMode {
None,
Move,
Attack,
BuildCity
};
struct FloatingText {
QString text;
QVector3D worldPos;
float life; // Remaining seconds
float maxLife;
QColor color;
};
class GameManager {
public:
static GameManager& instance();
void init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
PlayerManager& players() { return m_playerManager; }
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
const std::vector<City>& getCities() const { return m_cities; }
const std::vector<FloatingText>& getFloatingTexts() const { return m_floatingTexts; }
Unit* getSelectedUnit();
const Unit* getSelectedUnit() const;
void selectUnit(int id);
void deselectUnit();
ActionMode currentActionMode() const { return m_actionMode; }
void setActionMode(ActionMode mode) { m_actionMode = mode; }
Unit* getUnitAt(int tx, int ty);
Unit* getUnitById(int id);
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandAttack(Unit *target);
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
float getTickDuration() const { return m_movementTickDuration; }
void setTickDuration(float d) { m_movementTickDuration = std::max(1.0f, d); }
float getTickTimer() const { return m_tickTimer; }
float getSimSpeed() const { return m_simSpeedMultiplier; }
void setSimSpeed(float s) { m_simSpeedMultiplier = std::max(0.2f, std::min(10.0f, s)); }
void addFloatingText(const QString &text, const QVector3D &worldPos, const QColor &color, float duration = 1.8f);
int getNextUnitId() { return ++m_nextUnitId; }
int getNextCityId() { return ++m_nextCityId; }
Unit* spawnUnit(const QString &configId, int playerId, int tileX, int tileY,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
private:
GameManager();
~GameManager() = default;
PlayerManager m_playerManager;
UnitConfigManager m_unitConfigManager;
std::vector<std::shared_ptr<Unit>> m_units;
std::vector<City> m_cities;
std::vector<FloatingText> m_floatingTexts;
int m_selectedUnitId;
ActionMode m_actionMode;
float m_movementTickDuration; // Default 30.0s
float m_tickTimer;
float m_simSpeedMultiplier; // 1.0x, 2.0x, etc.
int m_nextUnitId;
int m_nextCityId;
bool m_initialized;
};
#endif // GAMEMANAGER_H
BIN
View File
Binary file not shown.
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
River generator and map builder for tiletest.
Encodes rivers onto images/rivermap.png such that rivers STRICTLY pass through
the exact midpoints of the 6 hexagon edges, ensuring 100% seamless connections.
"""
from PIL import Image, ImageDraw
import numpy as np
WIDTH = 3600
HEIGHT = 1500
SCALE_X = 8.0
SCALE_Y = 9.24045
LOD_0_SIZE_Y = 0.4328799197461722
# 6 edge midpoints in local tile space (radius R = 0.5)
MIDPOINTS = [
(-0.375, 0.21650635), # Edge 0 (Top-Left)
( 0.0, 0.43301270), # Edge 1 (Top)
( 0.375, 0.21650635), # Edge 2 (Top-Right)
( 0.375, -0.21650635), # Edge 3 (Bottom-Right)
( 0.0, -0.43301270), # Edge 4 (Bottom)
(-0.375, -0.21650635) # Edge 5 (Bottom-Left)
]
def tile_world_pos(tx, ty):
wx = tx * 0.75
wy = ty * (LOD_0_SIZE_Y * 2.0) + (0.0 if tx % 2 == 0 else LOD_0_SIZE_Y)
return wx, wy
def world_to_pixel(wx, wy):
px = int(round(wx * SCALE_X))
py = int(round(wy * SCALE_Y))
return px, HEIGHT - 1 - py
def get_hex_neighbor(x, y, edge):
even = (x % 2 == 0)
if edge == 0: return (x - 1, y if even else y + 1)
if edge == 1: return (x, y + 1)
if edge == 2: return (x + 1, y if even else y + 1)
if edge == 3: return (x + 1, y - 1 if even else y)
if edge == 4: return (x, y - 1)
if edge == 5: return (x - 1, y - 1 if even else y)
return (x, y)
def trace_river(start_tile, edge_steps):
"""Traces a river from a start tile following a sequence of exit edges."""
path = []
curr = start_tile
in_edge = None
for out_edge in edge_steps:
path.append((curr, in_edge, out_edge))
curr = get_hex_neighbor(curr[0], curr[1], out_edge)
in_edge = (out_edge + 3) % 6
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"):
river_img = Image.new('RGB', (WIDTH, HEIGHT), (0, 0, 0))
draw = ImageDraw.Draw(river_img)
# 1. Main River: starts high on mountain (36, 31) in snow, flows down to South Ocean (32, 14)
main_river_steps = [5, 5, 5, 5] + [4] * 15
main_river = trace_river((36, 31), main_river_steps)
# 2. Mountain Tributary: starts on ridge (36, 28) and merges with main river at (32, 26)
trib_river_steps = [5, 5, 5, 5]
tributary = trace_river((36, 28), trib_river_steps)
# 3. Canyon River in Prairie (Red Sand & Red Rock)
prairie_steps = [4, 4, 5, 4, 4, 3, 4, 4, 4]
prairie_river = trace_river((110, 32), prairie_steps)
rivers = [main_river, tributary, prairie_river]
river_color = (0, 180, 255)
for r in rivers:
for (tx, ty), e_in, e_out in r:
wx, wy = tile_world_pos(tx, ty)
cx, cy = world_to_pixel(wx, wy)
edges = [e for e in (e_in, e_out) if e is not None]
for e in edges:
mx, my = MIDPOINTS[e]
mpx, mpy = world_to_pixel(wx + mx, wy + my)
# Line between midpoint and tile center
draw.line([(mpx, mpy), (cx, cy)], fill=river_color, width=2)
# Small marker at midpoint for detection
draw.rectangle([mpx - 1, mpy - 1, mpx + 1, mpy + 1], fill=river_color)
river_img.save(output_path)
print(f"Saved rivermap to {output_path}")
# Build an overlay preview on top of typemap.png
try:
type_img = Image.open("/home/bart/project/tiletest/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)
t_arr[mask] = [0, 220, 255, 255]
preview = Image.fromarray(t_arr)
preview.save(preview_path)
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"))
R = 8.0
W = 3608
H = 1740
hex_img = Image.new('RGB', (W, H), (15, 20, 30))
hdraw = ImageDraw.Draw(hex_img)
angles = [np.pi * 60.0 * i / 180.0 for i in range(6)]
corner_dx = [-R * np.cos(a) for a in angles]
corner_dy = [ R * np.sin(a) for a in angles]
for ty in range(125):
for tx in range(300):
wx = tx * 0.75
wy = ty * (LOD_0_SIZE_Y * 2.0) + (0.0 if tx % 2 == 0 else LOD_0_SIZE_Y)
cx = (wx + 0.5) * 16.0
cy = H - 1 - (wy + LOD_0_SIZE_Y) * 16.0
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]
tp = t_arr[1500 - 1 - py, px]
dist_pole = abs((wy / 108.22) - 0.5) * 2.0
if h <= 2:
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
elif h > 73:
# Mountain rock (between 1000m and 2500m)
r, g, b = int(tp[0]), int(tp[1]), int(tp[2])
if r > 150 and g < 100:
color = (175, 75, 55) # Red rock mountain in prairie
else:
color = (125, 130, 135) # Gray alpine mountain rock
else:
# Biome ground below 1000m
r, g, b = int(tp[0]), int(tp[1]), int(tp[2])
sat = max(r, g, b) - min(r, g, b)
if r > 150 and g < 100:
color = (195, 75, 45) # Prairie / Red Rock
elif r > 150 and g > 150 and b < 100:
color = (225, 185, 65) # Sahara yellow
elif b > 50 and r < 50:
color = (35, 115, 50) # Forest green
elif sat < 40 and g < 140:
color = (110, 125, 95) # Tundra slate
else:
color = (85, 175, 55) # Grassland green
poly = [(cx + corner_dx[i], cy + corner_dy[i]) for i in range(6)]
hdraw.polygon(poly, fill=color, outline=(0, 0, 0, 45))
# Draw rivers on hex map
for r in rivers:
for (tx, ty), e_in, e_out in r:
wx, wy = tile_world_pos(tx, ty)
cx = (wx + 0.5) * 16.0
cy = H - 1 - (wy + LOD_0_SIZE_Y) * 16.0
edges = [e for e in (e_in, e_out) if e is not None]
for e in edges:
mx, my = MIDPOINTS[e]
mpx = (wx + mx + 0.5) * 16.0
mpy = H - 1 - (wy + my + LOD_0_SIZE_Y) * 16.0
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")
except Exception as ex:
print(f"Minimap/preview generation skipped: {ex}")
if __name__ == "__main__":
build_rivermap()
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
View File
Binary file not shown.
+3
View File
@@ -7,6 +7,9 @@ class Matrix2D {
std::vector<T> data;
unsigned int sizeX, sizeY;
public:
unsigned int getWidth() const { return sizeX; }
unsigned int getHeight() const { return sizeY; }
Matrix2D (unsigned int x, unsigned int y)
: sizeX (x), sizeY (y) {
data.resize (sizeX*sizeY);
+162
View File
@@ -0,0 +1,162 @@
#include "pathfinder.h"
#include <cmath>
#include <queue>
#include <unordered_map>
#include <algorithm>
QVector2D Pathfinder::hexToWorld(int x, int y)
{
float wx = x * (LOD_0_SIZE * 0.75f);
float wy = y * (LOD_0_SIZE_Y * 2.0f) + ((x % 2 == 0) ? 0.0f : LOD_0_SIZE_Y);
return QVector2D(wx, wy);
}
void Pathfinder::worldToHex(float wx, float wy, int &outX, int &outY)
{
float stepX = LOD_0_SIZE * 0.75f;
float stepY = LOD_0_SIZE_Y * 2.0f;
int approxX = (int) std::round(wx / stepX);
int approxY = (int) std::round(wy / stepY);
float minDistSq = 1e9f;
int bestX = 0;
int bestY = 0;
for (int dx = -2; dx <= 2; ++dx) {
for (int dy = -2; dy <= 2; ++dy) {
int cx = approxX + dx;
int cy = approxY + dy;
if (cx >= 0 && cx < MAP_WIDTH && cy >= 0 && cy < MAP_HEIGHT) {
QVector2D center = hexToWorld(cx, cy);
float distSq = (wx - center.x()) * (wx - center.x()) + (wy - center.y()) * (wy - center.y());
if (distSq < minDistSq) {
minDistSq = distSq;
bestX = cx;
bestY = cy;
}
}
}
}
outX = bestX;
outY = bestY;
}
int Pathfinder::hexDistance(int x1, int y1, int x2, int y2)
{
int q1 = x1;
int r1 = y1 - (x1 - (x1 & 1)) / 2;
int s1 = -q1 - r1;
int q2 = x2;
int r2 = y2 - (x2 - (x2 & 1)) / 2;
int s2 = -q2 - r2;
return (std::abs(q1 - q2) + std::abs(r1 - r2) + std::abs(s1 - s2)) / 2;
}
namespace {
struct Node {
int x, y;
float g, f;
bool operator>(const Node &other) const {
return f > other.f;
}
};
inline int packCoord(int x, int y) {
return (y << 16) | (x & 0xFFFF);
}
}
std::vector<std::pair<int, int>> Pathfinder::findPath(
int startX, int startY,
int goalX, int goalY,
Matrix2D<TerrainPoint> *mapPoints,
int numPoints05)
{
std::vector<std::pair<int, int>> path;
if (startX == goalX && startY == goalY) {
path.emplace_back(startX, startY);
return path;
}
if (goalX < 0 || goalX >= MAP_WIDTH || goalY < 0 || goalY >= MAP_HEIGHT) {
return path;
}
std::priority_queue<Node, std::vector<Node>, std::greater<Node>> openSet;
std::unordered_map<int, float> gScore;
std::unordered_map<int, int> cameFrom;
int startPacked = packCoord(startX, startY);
gScore[startPacked] = 0.0f;
openSet.push({startX, startY, 0.0f, (float)hexDistance(startX, startY, goalX, goalY)});
int maxIterations = 3000;
int iterations = 0;
bool found = false;
while (!openSet.empty() && iterations++ < maxIterations) {
Node current = openSet.top();
openSet.pop();
if (current.x == goalX && current.y == goalY) {
found = true;
break;
}
int currPacked = packCoord(current.x, current.y);
if (current.g > gScore[currPacked]) continue;
for (int edge = 0; edge < 6; ++edge) {
int nx, ny;
getHexNeighbor(current.x, current.y, edge, nx, ny);
if (nx < 0 || nx >= MAP_WIDTH || ny < 0 || ny >= MAP_HEIGHT) continue;
float stepCost = 1.0f;
// Elevation check if terrain points are available
if (mapPoints && numPoints05 > 0) {
float scaleX = numPoints05 * 2.0f;
float scaleY = numPoints05 / LOD_0_SIZE_Y;
QVector2D nCenter = hexToWorld(nx, ny);
int mapX = (int) std::round(nCenter.x() * scaleX);
int mapY = (int) std::round(nCenter.y() * scaleY);
if (mapX >= 0 && mapX < (int)mapPoints->getWidth() && mapY >= 0 && mapY < (int)mapPoints->getHeight()) {
unsigned char h = mapPoints->operator()(mapX, mapY).height;
// Slightly penalize water tiles or high mountains
if (h < 5) stepCost += 4.0f;
else if (h > 220) stepCost += 2.0f;
}
}
float tentativeG = current.g + stepCost;
int nPacked = packCoord(nx, ny);
if (!gScore.count(nPacked) || tentativeG < gScore[nPacked]) {
gScore[nPacked] = tentativeG;
cameFrom[nPacked] = currPacked;
float h = (float) hexDistance(nx, ny, goalX, goalY);
openSet.push({nx, ny, tentativeG, tentativeG + h});
}
}
}
if (found) {
int curr = packCoord(goalX, goalY);
while (curr != startPacked) {
int cx = curr & 0xFFFF;
int cy = curr >> 16;
path.emplace_back(cx, cy);
curr = cameFrom[curr];
}
path.emplace_back(startX, startY);
std::reverse(path.begin(), path.end());
}
return path;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef PATHFINDER_H
#define PATHFINDER_H
#include <vector>
#include <utility>
#include <QVector2D>
#include "terrain.h"
#include "matrix2d.h"
class Pathfinder {
public:
static QVector2D hexToWorld(int x, int y);
static void worldToHex(float wx, float wy, int &outX, int &outY);
static int hexDistance(int x1, int y1, int x2, int y2);
static std::vector<std::pair<int, int>> findPath(
int startX, int startY,
int goalX, int goalY,
Matrix2D<TerrainPoint> *mapPoints = nullptr,
int numPoints05 = 0
);
};
#endif // PATHFINDER_H
BIN
View File
Binary file not shown.
+53
View File
@@ -0,0 +1,53 @@
#include "player.h"
PlayerManager::PlayerManager()
{
// Register default players with distinct, unique color combinations
// Player 1: Royal Blue primary, Radiant Gold secondary
addPlayer(1, "Speler 1", QColor(30, 136, 229), QColor(255, 213, 79));
// Player 2: Crimson Red primary, Crisp White secondary
addPlayer(2, "Speler 2", QColor(229, 57, 53), QColor(255, 255, 255));
// Player 3: Emerald Green primary, Amber Yellow secondary
addPlayer(3, "Speler 3", QColor(67, 160, 71), QColor(255, 179, 0));
// Player 4: Imperial Purple primary, Cyan secondary
addPlayer(4, "Speler 4", QColor(142, 36, 170), QColor(0, 229, 255));
}
bool PlayerManager::isCombinationUnique(const QColor &primary, const QColor &secondary, int excludeId) const
{
for (const auto &p : m_players) {
if (p.id == excludeId) continue;
if (p.primaryColor == primary && p.secondaryColor == secondary) {
return false;
}
}
return true;
}
bool PlayerManager::addPlayer(int id, const QString &name, const QColor &primary, const QColor &secondary)
{
if (!isCombinationUnique(primary, secondary)) {
return false;
}
m_players.emplace_back(id, name, primary, secondary);
return true;
}
const Player* PlayerManager::getPlayer(int id) const
{
for (const auto &p : m_players) {
if (p.id == id) return &p;
}
return nullptr;
}
Player* PlayerManager::getPlayer(int id)
{
for (auto &p : m_players) {
if (p.id == id) return &p;
}
return nullptr;
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef PLAYER_H
#define PLAYER_H
#include <QString>
#include <QColor>
#include <vector>
struct Player {
int id;
QString name;
QColor primaryColor;
QColor secondaryColor;
Player() : id(0), name("Unknown"), primaryColor(Qt::blue), secondaryColor(Qt::white) {}
Player(int pId, const QString &pName, const QColor &primary, const QColor &secondary)
: id(pId), name(pName), primaryColor(primary), secondaryColor(secondary) {}
};
class PlayerManager {
public:
PlayerManager();
bool addPlayer(int id, const QString &name, const QColor &primary, const QColor &secondary);
const Player* getPlayer(int id) const;
Player* getPlayer(int id);
const std::vector<Player>& getPlayers() const { return m_players; }
bool isCombinationUnique(const QColor &primary, const QColor &secondary, int excludeId = -1) const;
private:
std::vector<Player> m_players;
};
#endif // PLAYER_H
BIN
View File
Binary file not shown.
+783 -207
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1138 -62
View File
File diff suppressed because it is too large Load Diff
+67 -18
View File
@@ -7,45 +7,94 @@
#include "terrain.h"
#include "tile.h"
#include <QMouseEvent>
#include <QElapsedTimer>
#include <QTimer>
#include <vector>
#include <QMap>
#include "matrix2d.h"
#include "unitrenderer.h"
#include "gamemanager.h"
class QSceneRenderer : public QOpenGLWidget
{
public:
QSceneRenderer(QWidget* parent = Q_NULLPTR) ;
inline Matrix2D<TerrainPoint> * getMapPoints() {return m_point;}
inline int getNumPointsWidth() const {return m_numPointsWidth;}
inline int getNumPointsHeight() const {return m_numPointsHeight;}
inline int getNumPoints05() const {return m_numPoints05;}
QSceneRenderer(QWidget* parent = Q_NULLPTR);
inline Matrix2D<TerrainPoint> * getMapPoints() { return m_point; }
inline int getNumPointsWidth() const { return m_numPointsWidth; }
inline int getNumPointsHeight() const { return m_numPointsHeight; }
inline int getNumPoints05() const { return m_numPoints05; }
Tile * getTile(int x, int y);
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void keyPressEvent(QKeyEvent * e);
void wheelEvent(QWheelEvent * e);
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void loadRiverMap(const QImage &riverImg);
float m_camera_x =10.0;
float m_camera_y=0.0;
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent * e) override;
void wheelEvent(QWheelEvent * e) override;
void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
// Minimap helpers
QRect getMinimapRect() const;
QRect getMinimapCardRect() const;
QRect getZoomOutButtonRect() const;
QRect getZoomResetButtonRect() const;
QRect getZoomInButtonRect() const;
QRect getMinimapSourceRect() const;
QPointF worldToMinimap(float wx, float wy, const QRect &rect) const;
QVector2D minimapToWorld(const QPoint &pos, const QRect &rect) const;
void drawMinimap(QPainter &painter);
// Units HUD and 2D overlays
void drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawUnitCard(QPainter &painter, Unit *unit);
void drawTickBar(QPainter &painter);
void drawPathAndRange(QPainter &painter, Unit *unit, const QMatrix4x4 &vMatrix);
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
QImage getTintedIcon(const QString &path, const QColor &color);
float m_camera_x = 10.0f;
float m_camera_y = 0.0f;
QMatrix4x4 Projection;
QMatrix4x4 cameraTransformation;
QVector3D cameraPosition;
QVector3D cameraUpDirection;
Matrix2D<TerrainPoint> * m_point;
Matrix2D<Tile> * tiles;
QPoint lastPos;
float zRot=0;
float zRot = 0.0f;
float m_numPointsWidth;
float m_numPointsHeight;
int m_numPoints05;
float m_zoom;
QElapsedTimer m_elapsedTimer;
float m_lastFrameTime;
QImage m_image_minimap;
bool m_draggingMinimap;
float m_minimapZoom;
// Units & UI state
UnitRenderer m_unitRenderer;
QMap<QString, QImage> m_iconCache;
QMap<QString, QImage> m_tintedIconCache;
QPoint m_mouseCurrentPos;
struct ActionBubble {
QRect rect;
QString actionId;
QString tooltip;
};
std::vector<ActionBubble> m_activeActionBubbles;
QRect m_speed1xRect;
QRect m_speed2xRect;
QRect m_speed5xRect;
QRect m_unitCardRect;
};
#endif // QSCENERENDERER_H
BIN
View File
Binary file not shown.
+239 -41
View File
@@ -1,7 +1,20 @@
#version 400
uniform sampler2D tex; //this is the texture
uniform sampler2D border; //this is the texture
uniform sampler2D rock; //this is the texture
uniform sampler2D texGrass; // Unit 0
uniform sampler2D texBorder; // Unit 1
uniform sampler2D texRock; // Unit 2 (Stone cliffs)
uniform sampler2D texSand; // Unit 3 (Soil beach)
uniform sampler2D texWater; // Unit 4 (Dynamic ocean)
uniform sampler2D texSnow; // Unit 5 (Texturelabs_Water_155XL snow/ice)
uniform sampler2D texTypeMap; // Unit 6 (Biome type map)
uniform sampler2D texSahara; // Unit 7 (Texturelabs_Soil_133L desert sand)
uniform sampler2D texTundra; // Unit 8 (Texturelabs_Soil_135XL tundra soil)
uniform sampler2D texRedSand; // Unit 9 (Texturelabs_RedSand prairie sand)
uniform sampler2D texRedRock; // Unit 10 (Texturelabs_RedRock canyon cliff)
uniform sampler2D texForest; // Unit 11 (Texturelabs_Forest_seamless woodland floor)
uniform float time;
uniform int riverMask;
smooth in vec3 vNormal;
smooth in vec3 vWorldPos;
in vec3 fragVert;
@@ -10,11 +23,8 @@ uniform vec3 cameraPosition;
uniform mat4 modelMatrix;
in vec2 tilePos;
in vec2 fragTexCoord; //this is the texture coord
out vec4 finalColor; //this is the output color of the pixel
in vec2 fragTexCoord;
out vec4 finalColor;
struct SimpleDirectionalLight
{
@@ -23,55 +33,243 @@ struct SimpleDirectionalLight
float fAmbientIntensity;
};
uniform SimpleDirectionalLight sunLight;
vec4 GetDirectionalLightColor(SimpleDirectionalLight dirLight, vec3 vNormal)
vec4 GetDirectionalLightColor(SimpleDirectionalLight dirLight, vec3 norm)
{
float fDiffuseIntensity = dot(vNormal, -dirLight.vDirection);
float fMult = pow(dirLight.fAmbientIntensity+fDiffuseIntensity,1.0);//clamp(dirLight.fAmbientIntensity+fDiffuseIntensity, -1.0, 1.0);
// if(dirLight.iSkybox == 1)fMult = dirLight.fAmbientIntensity;
return vec4(dirLight.vColor*fMult, 1.0);
float fDiffuseIntensity = max(dot(norm, -dirLight.vDirection), 0.0);
float fMult = dirLight.fAmbientIntensity + fDiffuseIntensity * (1.0 - dirLight.fAmbientIntensity * 0.5);
return vec4(dirLight.vColor * fMult, 1.0);
}
vec4 GetSpecularColor(vec3 vWorldPos, vec3 vEyePos, SimpleDirectionalLight dLight, vec3 vNormal)
vec4 GetSpecularColor(vec3 worldPos, vec3 eyePos, SimpleDirectionalLight dLight, vec3 norm, float shininess, float intensity)
{
vec4 vResult = vec4(0.0, 0.0, 0.0, 0.0);
vec3 vReflectedVector = normalize(reflect(dLight.vDirection, norm));
vec3 vVertexToEyeVector = normalize(eyePos - worldPos);
float fSpecularFactor = max(dot(vVertexToEyeVector, vReflectedVector), 0.0);
vec3 vReflectedVector = normalize(reflect(dLight.vDirection, vNormal));
vec3 vVertexToEyeVector = normalize(vEyePos-vWorldPos);
float fSpecularFactor = dot(vVertexToEyeVector, vReflectedVector);
if (fSpecularFactor > 0)
if (fSpecularFactor > 0.0)
{
fSpecularFactor = pow(fSpecularFactor, 1.0);
vResult = vec4(dLight.vColor, 1.0) * 1.0 * fSpecularFactor;
fSpecularFactor = pow(fSpecularFactor, shininess);
return vec4(dLight.vColor, 1.0) * intensity * fSpecularFactor;
}
return vec4(0.0);
}
float distToSegment(vec2 p, vec2 a, vec2 b) {
vec2 ba = b - a;
vec2 pa = p - a;
float l2 = dot(ba, ba);
if (l2 <= 1e-6) return length(pa);
float h = clamp(dot(pa, ba) / l2, 0.0, 1.0);
return length(pa - ba * h);
}
const vec2 EDGE_MIDPOINTS[6] = vec2[6](
vec2(-0.375, 0.21650635),
vec2( 0.0, 0.43301270),
vec2( 0.375, 0.21650635),
vec2( 0.375, -0.21650635),
vec2( 0.0, -0.43301270),
vec2(-0.375, -0.21650635)
);
float getRiverDistance(vec2 p, int mask) {
if (mask == 0) return 999.0;
int edges[6];
int count = 0;
for (int e = 0; e < 6; ++e) {
if ((mask & (1 << e)) != 0) {
edges[count] = e;
count++;
}
}
return vResult;
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));
} else {
float minDist = 999.0;
for (int i = 0; i < count; ++i) {
float d = distToSegment(p, EDGE_MIDPOINTS[edges[i]], vec2(0.0));
minDist = min(minDist, d);
}
return minDist;
}
}
void main() {
vec3 norm = normalize(vNormal);
vec3 surfaceToLight = normalize(sunLight.vDirection);
vec3 surfacePos = vec3(modelMatrix * vec4(fragVert, 1));
vec3 surfaceToCamera = normalize(cameraPosition - surfacePos);
vec4 bcolor = texture(border, vec2(tilePos.x,tilePos.y*1.154734411));
float fDiffuseIntensity = max(0.0, dot(normalize(vNormal), -sunLight.vDirection));
// 1. Biome Typemap Sampling
// World extent: [0..225.0 km, 0..108.22 km]
// Note: QImage row 0 is North (worldY = 108.22), which QOpenGLTexture maps to v = 0.0.
// Invert Y so North samples row 0 and South samples row 1499.
vec2 uvType = vec2(clamp(vWorldPos.x / 225.0, 0.0, 1.0), 1.0 - clamp(vWorldPos.y / 108.22, 0.0, 1.0));
vec4 typePixel = texture(texTypeMap, uvType);
float addval = bcolor.a;
vec4 vSpecularColor = GetSpecularColor(vWorldPos, cameraPosition, sunLight, normalize(vNormal))+ GetDirectionalLightColor(sunLight,normalize(vNormal));
vec4 ground = texture(tex, fragTexCoord);
vec4 rockt = texture(rock, fragTexCoord);
// Compute smooth biome weights from typemap colors:
// 1. Prairie: red dominant (r - g)
float prairieWeight = clamp((typePixel.r - typePixel.g - 0.20) / 0.25, 0.0, 1.0);
// 2. Sahara: yellow (r & g high, b low)
float saharaWeight = clamp((min(typePixel.r, typePixel.g) - 0.45) / 0.25, 0.0, 1.0) * (1.0 - prairieWeight);
// 3. Forest: elevated blue with low red (b > 0.15, r < 0.25)
float forestWeight = clamp((typePixel.b - 0.15) / 0.20, 0.0, 1.0) * clamp((0.25 - typePixel.r) / 0.15, 0.0, 1.0) * (1.0 - prairieWeight) * (1.0 - saharaWeight);
// 4. Tundra: muted slate/olive (low saturation, moderate/low green)
float sat = max(typePixel.r, max(typePixel.g, typePixel.b)) - min(typePixel.r, min(typePixel.g, typePixel.b));
float tundraWeight = clamp((0.25 - sat) / 0.15, 0.0, 1.0) * clamp((0.55 - typePixel.g) / 0.15, 0.0, 1.0) * (1.0 - prairieWeight) * (1.0 - saharaWeight) * (1.0 - forestWeight);
// 5. Grassland: green dominant open plains
float grasslandWeight = clamp(1.0 - (prairieWeight + saharaWeight + forestWeight + tundraWeight), 0.0, 1.0);
float gndfac = 1.0;
if (z_pos>0.3)
gndfac = 0.0;
else if (z_pos>0.1)
gndfac=1.0 - 1.0 / 0.2 * (z_pos-0.1) ;
finalColor = (ground *gndfac+rockt *(1.0-gndfac) +bcolor*addval) * vSpecularColor; //texture(tex, fragTexCoord) * pow(max(0.0, dot(surfaceToCamera, reflect(-surfaceToLight, normal))), 0.5);
// 2. Polar Latitude Calculation (World Y in [0..108.25 km])
float normY = clamp(vWorldPos.y / 108.25, 0.0, 1.0);
float distFromCenter = abs(normY - 0.5) * 2.0; // 0.0 at middle, 1.0 at North/South poles
float poleFactor = smoothstep(0.40, 0.85, distFromCenter);
// (texture(tex, fragTexCoord)+bcolor*addval)*vec4(sunLight.vColor*(sunLight.fAmbientIntensity+fDiffuseIntensity), 1.0);
// 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);
// River water detection
float riverDist = getRiverDistance(fragVert.xy, riverMask);
float riverWaterRadius = 0.024;
float riverBankRadius = 0.052;
float riverWaterFac = 1.0 - smoothstep(riverWaterRadius * 0.7, riverWaterRadius, riverDist);
float riverBankFac = (1.0 - smoothstep(riverWaterRadius, riverBankRadius, riverDist)) * (1.0 - riverWaterFac);
// Combined water factor (ocean or river)
float waterFac = max(oceanWaterFac, riverWaterFac);
// 4. Dynamic animated ocean water
vec2 uvW1 = vWorldPos.xy * 0.30 + vec2(time * 0.035, time * 0.020);
vec2 uvW2 = vWorldPos.xy * 0.38 + vec2(-time * 0.025, time * 0.030);
vec2 waveDistort1 = vec2(sin(uvW1.y * 14.0 + time * 2.2), cos(uvW1.x * 14.0 + time * 1.8)) * 0.025;
vec2 waveDistort2 = vec2(cos(uvW2.y * 18.0 - time * 1.5), sin(uvW2.x * 18.0 - time * 2.5)) * 0.020;
vec4 wSample1 = texture(texWater, uvW1 + waveDistort1);
vec4 wSample2 = texture(texWater, uvW2 + waveDistort2);
vec4 waterColor = mix(wSample1, wSample2, 0.5);
waterColor.rgb = mix(waterColor.rgb, vec3(0.05, 0.30, 0.50), 0.30);
vec3 waterNorm = normalize(vec3(
(waveDistort1.x + waveDistort2.x) * 2.5,
(waveDistort1.y + waveDistort2.y) * 2.5,
1.0
));
// 5. Triplanar mapping coordinates for Land, Cliffs & Snow
vec3 blendWeights = pow(abs(norm), vec3(4.0));
blendWeights /= (blendWeights.x + blendWeights.y + blendWeights.z);
vec2 uvX = vWorldPos.yz * 0.35;
vec2 uvY = vWorldPos.xz * 0.35;
vec2 uvZ = vWorldPos.xy * 0.35;
// Rock textures:
// Gray rock (Texturelabs_Stone_136XL_seamless)
vec4 rockGray = texture(texRock, uvX) * blendWeights.x
+ texture(texRock, uvY) * blendWeights.y
+ texture(texRock, uvZ) * blendWeights.z;
// Red rock canyon cliff (Texturelabs_RedRock_seamless)
vec4 rockRed = texture(texRedRock, uvX) * blendWeights.x
+ texture(texRedRock, uvY) * blendWeights.y
+ texture(texRedRock, uvZ) * blendWeights.z;
// Cliff rock: red sandstone in Prairie canyon, gray alpine elsewhere
vec4 cliffRockColor = mix(rockGray, rockRed, prairieWeight);
// Snow / Ice texture (Texturelabs_Water_155XL_seamless)
vec4 snowColor = texture(texSnow, uvX) * blendWeights.x
+ texture(texSnow, uvY) * blendWeights.y
+ texture(texSnow, uvZ) * blendWeights.z;
// Ground textures:
vec4 grassColor = texture(texGrass, uvZ);
vec4 forestColor = texture(texForest, uvZ);
vec4 beachColor = texture(texSand, uvZ);
vec4 saharaColor = texture(texSahara, uvZ);
vec4 tundraColor = texture(texTundra, uvZ);
vec4 redSandColor = texture(texRedSand, uvZ);
// Composite ground texture across all biomes:
vec4 biomeGround = grassColor * grasslandWeight
+ forestColor * forestWeight
+ saharaColor * saharaWeight
+ tundraColor * tundraWeight
+ redSandColor * prairieWeight;
// Shoreline beach: where gentle slopes meet water (sea level up to ~110m, 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 riverBeach = riverBankFac;
float beachFac = max(coastBeach, riverBeach);
// Suppress beach only at the extreme polar ice sheet edges
float polarIceCover = smoothstep(0.82, 0.96, distFromCenter);
beachFac *= (1.0 - polarIceCover);
// Apply sandy beach across all land types gently meeting water
biomeGround = mix(biomeGround, beachColor, beachFac * 0.90);
// Polar pack ice on cold polar waters
float polarSeaIce = smoothstep(0.72, 0.94, distFromCenter);
waterColor = mix(waterColor, snowColor, polarSeaIce * 0.95);
// 6. Slope & Elevation Blending Rules
// norm.z: 1.0 = flat horizontal ground, < 0.55 = steep slope / vertical cliff
float flatness = clamp((norm.z - 0.55) / 0.25, 0.0, 1.0);
// Mountain rock elevation factor: begins at 1000m (1.0 km), ramps up towards 2200m (2.2 km)
float rockLine = mix(1.0, 0.0, poleFactor);
float mountainAltitude = smoothstep(rockLine, min(snowLine, rockLine + 1.2), z_pos);
// Steep slopes are always rock; flat ground becomes rock as altitude rises above 1000m
float rockWeight = max(1.0 - flatness, mountainAltitude);
// Snow rule: starts above 2500m in temperate zone, dropping to sea level at poles
float snowHeight = smoothstep(snowLine - 0.20, snowLine + 0.30, z_pos);
float snowSlope = clamp((norm.z - 0.28) / 0.25, 0.0, 1.0);
float snowWeight = snowHeight * snowSlope;
// At polar caps, ice covers flat ground at sea level
float polarLandIce = smoothstep(0.72, 0.92, distFromCenter);
snowWeight = max(snowWeight, polarLandIce * snowSlope);
// Composite ground: biome ground -> mountain rock above 1000m & on cliffs -> snow above 2500m
vec4 nonSnowLand = mix(biomeGround, cliffRockColor, rockWeight);
vec4 landColor = mix(nonSnowLand, snowColor, snowWeight);
// 7. Combine Land and Sea
vec4 surfaceColor = mix(landColor, waterColor, waterFac);
// 8. Hexagon grid border outline
vec4 bcolor = texture(texBorder, vec2(tilePos.x, tilePos.y * 1.154734411));
float borderAlpha = bcolor.a * (1.0 - waterFac * 0.7);
// 9. Lighting & Specular glint
vec3 activeNorm = mix(norm, waterNorm, waterFac);
vec4 lightDiffuse = GetDirectionalLightColor(sunLight, activeNorm);
// Specular: crisp sparkle on snow and water
float specShininess = mix(mix(16.0, 32.0, snowWeight), 48.0, waterFac);
float specIntensity = mix(mix(0.12, 0.55, snowWeight), 0.85, waterFac);
vec4 lightSpecular = GetSpecularColor(vWorldPos, cameraPosition, sunLight, activeNorm, specShininess, specIntensity);
finalColor = (surfaceColor + bcolor * borderAlpha) * lightDiffuse + lightSpecular;
}
+16 -14
View File
@@ -5,14 +5,14 @@ uniform mat4 projectionMatrix;
uniform mat4 normalMatrix;
uniform mat4 modelMatrix;
uniform mat4 worldMatrix;
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 inCoord;
layout (location = 2) in float zPos;
layout (location =3) in vec3 inNormal;
layout (location = 3) in vec3 inNormal;
//in float height;
out vec2 fragTexCoord;
out vec2 tilePos;
out vec3 fragVert;
@@ -20,22 +20,24 @@ smooth out vec3 vWorldPos;
uniform mat4 MVP;
smooth out vec3 vNormal;
out float z_pos;
void main()
{
gl_Position = MVP * vec4(position.x, position.y, zPos, 1.0);
// World position: anchored to terrain ground
vec4 wPos = worldMatrix * vec4(position.x, position.y, zPos, 1.0);
vWorldPos = wPos.xyz;
// Texture coordinates based on world coordinates (stays fixed when camera moves)
fragTexCoord = vec2(wPos.x * 0.5, wPos.y * 0.5);
// Tile-local position for the hexagon border outline
vec4 tpos = modelMatrix * vec4(position.x, position.y, 0.0, 1.0);
tilePos = vec2(tpos.x, tpos.y);
vec4 inPositionScaled = vec4(position, 1.0);
gl_Position = MVP *vec4(position.x,position.y,zPos, 1.0);
vec4 texp= modelViewMatrix *vec4(position.x,position.y,0, 1.0);
fragTexCoord = vec2(texp.x/2,texp.y/2);
vec4 tpos = modelMatrix*vec4(position.x,position.y,0, 1.0);
tilePos = vec2(tpos.x,tpos.y);
//vColor = vec4(color, 1.0);
vec4 vRes = vec4(inNormal, 0.0);
vNormal = inNormal.xyz;
fragVert = position;
vec4 vWorldPosLocal =modelMatrix*inPositionScaled;
vWorldPos = vWorldPosLocal.xyz;
z_pos=zPos;
z_pos = zPos;
}
+18
View File
@@ -0,0 +1,18 @@
#version 400
in vec3 vNormal;
in vec3 vWorldPos;
uniform vec4 u_color;
uniform vec3 u_lightDir;
out vec4 fragColor;
void main()
{
vec3 n = normalize(vNormal);
vec3 l = normalize(u_lightDir);
float diff = max(dot(n, l), 0.0);
float ambient = 0.55;
float light = clamp(ambient + diff * 0.45, 0.0, 1.0);
fragColor = vec4(u_color.rgb * light, u_color.a);
}
+17
View File
@@ -0,0 +1,17 @@
#version 400
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
uniform mat4 MVP;
uniform mat4 modelMatrix;
out vec3 vNormal;
out vec3 vWorldPos;
void main()
{
vec4 wPos = modelMatrix * vec4(inPosition, 1.0);
vWorldPos = wPos.xyz;
vNormal = mat3(modelMatrix) * inNormal;
gl_Position = MVP * vec4(inPosition, 1.0);
}
+32 -2
View File
@@ -20,9 +20,39 @@
#define PI 3.14159265359f
struct TerrainPoint {
char height;
#include <QVector2D>
struct TerrainPoint {
unsigned char height;
unsigned char type;
unsigned char river;
};
// Returns the 6 edge midpoints in local tile coordinates (radius = 0.5)
inline QVector2D getHexEdgeMidpoint(int edge) {
static const QVector2D midpoints[6] = {
QVector2D(-0.375f, 0.21650635f), // Edge 0 (Top-Left)
QVector2D( 0.0f, 0.43301270f), // Edge 1 (Top)
QVector2D( 0.375f, 0.21650635f), // Edge 2 (Top-Right)
QVector2D( 0.375f, -0.21650635f), // Edge 3 (Bottom-Right)
QVector2D( 0.0f, -0.43301270f), // Edge 4 (Bottom)
QVector2D(-0.375f, -0.21650635f) // Edge 5 (Bottom-Left)
};
return midpoints[(edge % 6 + 6) % 6];
}
// Returns the coordinates of the neighbor tile sharing the given edge
inline void getHexNeighbor(int x, int y, int edge, int &nx, int &ny) {
bool even = (x % 2 == 0);
switch ((edge % 6 + 6) % 6) {
case 0: nx = x - 1; ny = even ? y : y + 1; break;
case 1: nx = x; ny = y + 1; break;
case 2: nx = x + 1; ny = even ? y : y + 1; break;
case 3: nx = x + 1; ny = even ? y - 1 : y; break;
case 4: nx = x; ny = y - 1; break;
case 5: nx = x - 1; ny = even ? y - 1 : y; break;
default: nx = x; ny = y; break;
}
}
#endif // TERRAIN_H
+260 -53
View File
@@ -3,12 +3,30 @@
#include <math.h>
#include "qscenerenderer.h"
#include <QVector3D>
QImage * Tile::m_image = NULL;
QOpenGLTexture * Tile::m_texture = NULL;
QImage * Tile::m_image_grass = NULL;
QOpenGLTexture * Tile::m_texture_grass = NULL;
QImage * Tile::m_image_border = NULL;
QOpenGLTexture * Tile::m_texture_border = NULL;
QOpenGLTexture * Tile::m_texture_rock = NULL;
QImage * Tile::m_image_rock = NULL;
QOpenGLTexture * Tile::m_texture_sand = NULL;
QImage * Tile::m_image_sand = NULL;
QOpenGLTexture * Tile::m_texture_water = NULL;
QImage * Tile::m_image_water = NULL;
QOpenGLTexture * Tile::m_texture_snow = NULL;
QImage * Tile::m_image_snow = NULL;
QOpenGLTexture * Tile::m_texture_typemap = NULL;
QImage * Tile::m_image_typemap = NULL;
QOpenGLTexture * Tile::m_texture_sahara = NULL;
QImage * Tile::m_image_sahara = NULL;
QOpenGLTexture * Tile::m_texture_tundra = NULL;
QImage * Tile::m_image_tundra = NULL;
QOpenGLTexture * Tile::m_texture_redsand = NULL;
QImage * Tile::m_image_redsand = NULL;
QOpenGLTexture * Tile::m_texture_redrock = NULL;
QImage * Tile::m_image_redrock = NULL;
QOpenGLTexture * Tile::m_texture_forest = NULL;
QImage * Tile::m_image_forest = NULL;
Vertex * Tile::m_vertices= NULL;
int Tile::m_numVertex;
@@ -18,10 +36,53 @@ Vertex * Tile::m_vertices= NULL;
QOpenGLShaderProgram *Tile::m_program= NULL;
static float distToSegment(QVector2D p, QVector2D a, QVector2D b) {
QVector2D ba = b - a;
QVector2D pa = p - a;
float l2 = QVector2D::dotProduct(ba, ba);
if (l2 <= 1e-6f) return pa.length();
float h = std::max(0.0f, std::min(1.0f, QVector2D::dotProduct(pa, ba) / l2));
return (pa - ba * h).length();
}
float Tile::getRiverDistance(QVector2D p, unsigned char mask) {
if (mask == 0) return 999.0f;
int edges[6];
int count = 0;
for (int e = 0; e < 6; ++e) {
if (mask & (1 << e)) {
edges[count++] = e;
}
}
if (count == 2) {
QVector2D p0 = getHexEdgeMidpoint(edges[0]);
QVector2D p2 = getHexEdgeMidpoint(edges[1]);
QVector2D s0 = p0;
QVector2D s1 = 0.5625f * p0 + 0.0625f * p2;
QVector2D s2 = 0.2500f * p0 + 0.2500f * p2;
QVector2D s3 = 0.0625f * p0 + 0.5625f * p2;
QVector2D 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 std::min(std::min(d0, d1), std::min(d2, d3));
} else {
float minDist = 999.0f;
for (int i = 0; i < count; ++i) {
float d = distToSegment(p, getHexEdgeMidpoint(edges[i]), QVector2D(0.0f, 0.0f));
minDist = std::min(minDist, d);
}
return minDist;
}
}
Tile::Tile()
: m_scene(NULL), m_tilevertices(NULL), m_x(0), m_y(0),
m_startpoint_x(0.0f), m_startpoint_y(0.0f),
m_initialized(false), m_normalsBuilt(false)
m_initialized(false), m_normalsBuilt(false), m_riverMask(0)
{
}
@@ -62,36 +123,114 @@ void Tile::init(QSceneRenderer* s, int x, int y)
buildHeights();
if (Tile::m_image == NULL)
Tile::m_image = new QImage("/home/bart/project/tiletest/images/grass.jpg");
if (Tile::m_image_grass == NULL)
Tile::m_image_grass = new QImage("/home/bart/project/tiletest/images/grass.jpg");
if (Tile::m_image_border == NULL)
Tile::m_image_border = new QImage("/home/bart/project/tiletest/images/border.png");
if (Tile::m_image_rock == NULL)
Tile::m_image_rock = new QImage("/home/bart/project/tiletest/images/rock.jpg");
Tile::m_image_rock = new QImage("/home/bart/project/tiletest/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");
if (Tile::m_image_water == NULL)
Tile::m_image_water = new QImage("/home/bart/project/tiletest/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");
if (Tile::m_image_typemap == NULL)
Tile::m_image_typemap = new QImage("/home/bart/project/tiletest/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");
if (Tile::m_image_tundra == NULL)
Tile::m_image_tundra = new QImage("/home/bart/project/tiletest/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");
if (Tile::m_image_redrock == NULL)
Tile::m_image_redrock = new QImage("/home/bart/project/tiletest/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");
if (Tile::m_texture_rock == NULL) {
m_texture_rock = new QOpenGLTexture(*m_image_rock);
m_texture_rock->setWrapMode(QOpenGLTexture::Repeat);
m_texture_rock->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
m_texture_rock->setMagnificationFilter(QOpenGLTexture::Nearest);
//m_texture->setFormat(QOpenGLTexture::RGB32F);33
m_texture_rock->allocateStorage();
if (Tile::m_texture_grass == NULL) {
m_texture_grass = new QOpenGLTexture(*m_image_grass);
m_texture_grass->setWrapMode(QOpenGLTexture::Repeat);
m_texture_grass->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_grass->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_grass->generateMipMaps();
}
if (Tile::m_texture_border == NULL) {
m_texture_border = new QOpenGLTexture(*m_image_border);
m_texture_border->setWrapMode(QOpenGLTexture::Repeat);
m_texture_border->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
m_texture_border->setMagnificationFilter(QOpenGLTexture::Nearest);
//m_texture->setFormat(QOpenGLTexture::RGB32F);33
m_texture_border->allocateStorage();
m_texture_border->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_border->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_border->generateMipMaps();
}
if (Tile::m_texture == NULL) {
m_texture = new QOpenGLTexture(*m_image);
m_texture->setWrapMode(QOpenGLTexture::Repeat);
m_texture->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
m_texture->setMagnificationFilter(QOpenGLTexture::Nearest);
//m_texture->setFormat(QOpenGLTexture::RGB32F);
m_texture->allocateStorage();
if (Tile::m_texture_rock == NULL) {
m_texture_rock = new QOpenGLTexture(*m_image_rock);
m_texture_rock->setWrapMode(QOpenGLTexture::Repeat);
m_texture_rock->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_rock->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_rock->generateMipMaps();
}
if (Tile::m_texture_sand == NULL) {
m_texture_sand = new QOpenGLTexture(*m_image_sand);
m_texture_sand->setWrapMode(QOpenGLTexture::Repeat);
m_texture_sand->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_sand->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_sand->generateMipMaps();
}
if (Tile::m_texture_water == NULL) {
m_texture_water = new QOpenGLTexture(*m_image_water);
m_texture_water->setWrapMode(QOpenGLTexture::Repeat);
m_texture_water->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_water->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_water->generateMipMaps();
}
if (Tile::m_texture_snow == NULL) {
m_texture_snow = new QOpenGLTexture(*m_image_snow);
m_texture_snow->setWrapMode(QOpenGLTexture::Repeat);
m_texture_snow->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_snow->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_snow->generateMipMaps();
}
if (Tile::m_texture_typemap == NULL) {
m_texture_typemap = new QOpenGLTexture(*m_image_typemap);
m_texture_typemap->setWrapMode(QOpenGLTexture::ClampToEdge);
m_texture_typemap->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_typemap->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_typemap->generateMipMaps();
}
if (Tile::m_texture_sahara == NULL) {
m_texture_sahara = new QOpenGLTexture(*m_image_sahara);
m_texture_sahara->setWrapMode(QOpenGLTexture::Repeat);
m_texture_sahara->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_sahara->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_sahara->generateMipMaps();
}
if (Tile::m_texture_tundra == NULL) {
m_texture_tundra = new QOpenGLTexture(*m_image_tundra);
m_texture_tundra->setWrapMode(QOpenGLTexture::Repeat);
m_texture_tundra->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_tundra->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_tundra->generateMipMaps();
}
if (Tile::m_texture_redsand == NULL) {
m_texture_redsand = new QOpenGLTexture(*m_image_redsand);
m_texture_redsand->setWrapMode(QOpenGLTexture::Repeat);
m_texture_redsand->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_redsand->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_redsand->generateMipMaps();
}
if (Tile::m_texture_redrock == NULL) {
m_texture_redrock = new QOpenGLTexture(*m_image_redrock);
m_texture_redrock->setWrapMode(QOpenGLTexture::Repeat);
m_texture_redrock->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_redrock->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_redrock->generateMipMaps();
}
if (Tile::m_texture_forest == NULL) {
m_texture_forest = new QOpenGLTexture(*m_image_forest);
m_texture_forest->setWrapMode(QOpenGLTexture::Repeat);
m_texture_forest->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_forest->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_forest->generateMipMaps();
}
if (first){
@@ -132,7 +271,7 @@ void Tile::init(QSceneRenderer* s, int x, int y)
m_initialized = true;
}
void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDirection,QMatrix4x4& vMatrix, float zRot)
void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDirection,QMatrix4x4& vMatrix, float time)
{
if (!m_initialized) return;
@@ -151,9 +290,18 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
//mMatrix.translate(m_x*LOD_0_SIZE *1.5+ (m_y%2==1 ?( LOD_0_SIZE*1.5f)/2.0f:0),m_y*LOD_0_SIZE/2.0f,1.0f);
mMatrix.translate(m_x*(LOD_0_SIZE*0.75) ,(m_y*(LOD_0_SIZE_Y*2)+(m_x%2==0?0:LOD_0_SIZE_Y)) ,1.0f);
m_texture->bind(0);
m_texture_grass->bind(0);
m_texture_border->bind(1);
m_texture_rock->bind(2);
m_texture_sand->bind(3);
m_texture_water->bind(4);
m_texture_snow->bind(5);
m_texture_typemap->bind(6);
m_texture_sahara->bind(7);
m_texture_tundra->bind(8);
m_texture_redsand->bind(9);
m_texture_redrock->bind(10);
m_texture_forest->bind(11);
// Render using our shader
m_program->bind();
@@ -174,9 +322,20 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
//mModelToCamera.scale(0.8);
m_program->setUniformValue("grass", 0);
m_program->setUniformValue("border", 1);
m_program->setUniformValue("rock", 2);
m_program->setUniformValue("texGrass", 0);
m_program->setUniformValue("texBorder", 1);
m_program->setUniformValue("texRock", 2);
m_program->setUniformValue("texSand", 3);
m_program->setUniformValue("texWater", 4);
m_program->setUniformValue("texSnow", 5);
m_program->setUniformValue("texTypeMap", 6);
m_program->setUniformValue("texSahara", 7);
m_program->setUniformValue("texTundra", 8);
m_program->setUniformValue("texRedSand", 9);
m_program->setUniformValue("texRedRock", 10);
m_program->setUniformValue("texForest", 11);
m_program->setUniformValue("time", time);
m_program->setUniformValue("riverMask", (int)m_riverMask);
m_object.bind();
m_heightsbuffer.bind();
@@ -195,6 +354,7 @@ m_program->setUniformValue("cameraPosition", cameraPosition);
//mModelToCamera =vMatrix * mMatrix.inverted().transposed();//QMatrix4x4();// vPos.translate(1.0f);
m_program->setUniformValue("normalMatrix", mModelToCamera);
m_program->setUniformValue("modelMatrix", rMatrix);
m_program->setUniformValue("worldMatrix", mMatrix);
m_program->setUniformValue("modelViewMatrix", vMatrix * mMatrix);
m_program->setUniformValue("MVP", p * vMatrix * mMatrix);
glDrawArrays(GL_TRIANGLES, 0, NUM_TRIANGLES*NUM_TRIANGLE_VERTICES);
@@ -317,18 +477,30 @@ void Tile::drawSubTriangleVertices(int &cnt, QVector2D t1, QVector2D t2, QVector
void Tile::buildHeights() {
m_tilevertices = new TileVertexInfo[m_numVertex];
Matrix2D<TerrainPoint> * points = Tile::m_scene->getMapPoints();
for (int i = 0; i< m_numVertex; i++) {
/*float x=m_vertices[i].position().x();
int xpos = m_vertices[i].position().x()*(m_scene->getNumPoints05()*2);
int mappos=(int) round( m_startpoint_x+ ( m_vertices[i].position().x())*(m_scene->getNumPoints05()*2));
int res = (int) round( m_startpoint_x+ ( m_vertices[i].position().x())*(m_scene->getNumPoints05()*2));*/
float r = points->operator ()( (int) round( m_startpoint_x+ ( m_vertices[i].position().x())*(m_scene->getNumPoints05()*2.0f))
,(int) round( m_startpoint_y+ ( m_vertices[i].position().y())*(m_scene->getNumPoints05()*2.0f))).height /255.0f;
//m_heights[i] = 1r;
float tileWorldX = m_x * (LOD_0_SIZE * 0.75f);
float tileWorldY = m_y * (LOD_0_SIZE_Y * 2.0f) + ((m_x % 2 == 0) ? 0.0f : LOD_0_SIZE_Y);
float scaleX = m_scene->getNumPoints05() * 2.0f;
float scaleY = m_scene->getNumPoints05() / LOD_0_SIZE_Y;
for (int i = 0; i < m_numVertex; i++) {
float worldX = tileWorldX + m_vertices[i].position().x();
float worldY = tileWorldY + m_vertices[i].position().y();
int mapX = (int) round(worldX * scaleX);
int mapY = (int) round(worldY * scaleY);
float r = (points->operator()(mapX, mapY).height / 255.0f) * 3.5f;
if (m_riverMask != 0) {
float rDist = getRiverDistance(m_vertices[i].position(), m_riverMask);
float riverBankRadius = 0.052f;
if (rDist < riverBankRadius) {
float carve = 0.018f * (1.0f - (rDist / riverBankRadius));
r = std::max(0.0f, r - carve);
}
}
m_tilevertices[i].setHeight(r);
//m_tilevertices[i].setNormal(QVector3D(rand()%20/10.0f-1.0,rand()%20/10.0f-1.0,rand()%20/10.0f-1.0));
}
m_faces.reserve(m_numVertex/3);
int i =0;
@@ -362,27 +534,62 @@ void Tile::findFaces(std::vector<Face *> &vec, QVector2D s)
void Tile::buildNormals()
{
if (!m_initialized) return;
QVector2D myWorld = getWorldPosition();
int nbrs[6][2];
if (m_x % 2 == 0) {
nbrs[0][0] = m_x; nbrs[0][1] = m_y - 1;
nbrs[1][0] = m_x; nbrs[1][1] = m_y + 1;
nbrs[2][0] = m_x - 1; nbrs[2][1] = m_y - 1;
nbrs[3][0] = m_x - 1; nbrs[3][1] = m_y;
nbrs[4][0] = m_x + 1; nbrs[4][1] = m_y - 1;
nbrs[5][0] = m_x + 1; nbrs[5][1] = m_y;
} else {
nbrs[0][0] = m_x; nbrs[0][1] = m_y - 1;
nbrs[1][0] = m_x; nbrs[1][1] = m_y + 1;
nbrs[2][0] = m_x - 1; nbrs[2][1] = m_y;
nbrs[3][0] = m_x - 1; nbrs[3][1] = m_y + 1;
nbrs[4][0] = m_x + 1; nbrs[4][1] = m_y;
nbrs[5][0] = m_x + 1; nbrs[5][1] = m_y + 1;
}
std::vector<Face *> vec;
for (int i=0; i<m_numVertex;i++) {
vec.clear();
findFaces(vec, m_vertices[i].position());
m_scene->getTile(m_x-1,m_y)->findFaces(vec,m_vertices[i].position());
m_scene->getTile(m_x+1,m_y)->findFaces(vec,m_vertices[i].position());
QVector2D vPos = m_vertices[i].position();
findFaces(vec, vPos);
QVector3D r;
float cnt =0;
for (std::vector<Face *>::iterator it = vec.begin() ; it != vec.end(); ++it) {
QVector3D v = (*it)->normal();
cnt++;
r.setX(r.x() + v.x());
r.setY(r.y() + v.y());
r.setZ(r.z() + v.z());
if (vPos.length() > 0.4f) {
QVector2D worldV = myWorld + vPos;
for (int k = 0; k < 6; ++k) {
int nx = nbrs[k][0];
int ny = nbrs[k][1];
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
Tile *nbr = m_scene->getTile(nx, ny);
if (nbr && nbr->isInitialized()) {
QVector2D localInNbr = worldV - nbr->getWorldPosition();
nbr->findFaces(vec, localInNbr);
}
}
}
}
r.setX(r.x() /cnt);
r.setY(r.y() /cnt);
r.setZ(r.z() /cnt);
QVector3D r(0.0f, 0.0f, 0.0f);
float cnt = 0;
for (std::vector<Face *>::iterator it = vec.begin() ; it != vec.end(); ++it) {
QVector3D v = (*it)->normal();
if (!v.isNull()) {
cnt++;
r += v;
}
}
if (cnt > 0.0f && !r.isNull()) {
r.normalize();
} else {
r = QVector3D(0.0f, 0.0f, 1.0f);
}
m_tilevertices[i].setNormal(r);
}
}
+30 -3
View File
@@ -16,7 +16,7 @@ class Tile
public:
Tile();
void init(QSceneRenderer* s,int x, int y);
void paint(QMatrix4x4 p,QVector3D& cameraPosition,QVector3D& cameraUpDirection,QMatrix4x4& vMatrix, float zRot);
void paint(QMatrix4x4 p,QVector3D& cameraPosition,QVector3D& cameraUpDirection,QMatrix4x4& vMatrix, float time);
void buildVertices();
void buildNormals();
bool isInitialized() const { return m_initialized; }
@@ -24,6 +24,14 @@ public:
void ensureInitialized(QSceneRenderer* s, int x, int y);
void ensureNormalsBuilt();
void findFaces(std::vector<Face *> &vec, QVector2D s);
QVector2D getWorldPosition() const {
return QVector2D(m_x * (LOD_0_SIZE * 0.75f), m_y * (LOD_0_SIZE_Y * 2.0f) + (m_x % 2 == 0 ? 0.0f : LOD_0_SIZE_Y));
}
void addRiverEdge(int edge) { m_riverMask |= (1 << ((edge % 6 + 6) % 6)); }
void setRiverMask(unsigned char mask) { m_riverMask = mask; }
unsigned char getRiverMask() const { return m_riverMask; }
bool hasRiver() const { return m_riverMask != 0; }
static float getRiverDistance(QVector2D p, unsigned char mask);
protected:
void addTriangleVertices(int &cnt, QVector2D t1, QVector2D t2, QVector2D t3,int level, int hex, int l1);
void drawSubTriangleVertices(int &cnt, QVector2D t1, QVector2D t2, QVector2D t3, int hex, int l1, int l2);
@@ -47,13 +55,32 @@ protected:
float m_startpoint_y;
bool m_initialized;
bool m_normalsBuilt;
unsigned char m_riverMask;
static Vertex * m_vertices;
static QOpenGLTexture * m_texture;
static QOpenGLTexture * m_texture_grass;
static QOpenGLTexture * m_texture_border;
static QOpenGLTexture * m_texture_rock;
static QImage * m_image;
static QOpenGLTexture * m_texture_sand;
static QOpenGLTexture * m_texture_water;
static QOpenGLTexture * m_texture_snow;
static QOpenGLTexture * m_texture_typemap;
static QOpenGLTexture * m_texture_sahara;
static QOpenGLTexture * m_texture_tundra;
static QOpenGLTexture * m_texture_redsand;
static QOpenGLTexture * m_texture_redrock;
static QOpenGLTexture * m_texture_forest;
static QImage * m_image_grass;
static QImage * m_image_rock;
static QImage * m_image_border;
static QImage * m_image_sand;
static QImage * m_image_water;
static QImage * m_image_snow;
static QImage * m_image_typemap;
static QImage * m_image_sahara;
static QImage * m_image_tundra;
static QImage * m_image_redsand;
static QImage * m_image_redrock;
static QImage * m_image_forest;
};
#endif // TILE_H
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+22 -4
View File
@@ -11,14 +11,22 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = tiletest
TEMPLATE = app
CONFIG += c++17
SOURCES += main.cpp\
SOURCES += main.cpp \
mainwindow.cpp \
qscenerenderer.cpp \
vertex.cpp \
tile.cpp \
tilevertexinfo.cpp \
face.cpp
face.cpp \
player.cpp \
yamlparser.cpp \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
gamemanager.cpp \
unitrenderer.cpp
HEADERS += mainwindow.h \
qscenerenderer.h \
@@ -27,7 +35,15 @@ HEADERS += mainwindow.h \
terrain.h \
tilevertexinfo.h \
matrix2d.h \
face.h
face.h \
player.h \
yamlparser.h \
unitconfig.h \
pathfinder.h \
unit.h \
city.h \
gamemanager.h \
unitrenderer.h
FORMS += mainwindow.ui
@@ -36,7 +52,9 @@ unix: PKGCONFIG += glew
DISTFILES += \
shaders/simple.vert \
shaders/simple.frag
shaders/simple.frag \
shaders/unit.vert \
shaders/unit.frag
RESOURCES += \
shaders.qrc
+137
View File
@@ -0,0 +1,137 @@
#include "unit.h"
#include "pathfinder.h"
#include <algorithm>
#include <cmath>
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),
state(UnitState::Idle), pathIndex(0), moveProgress(0.0f),
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f)
{
QVector2D w = Pathfinder::hexToWorld(startX, startY);
worldPos = QVector3D(w.x(), w.y(), groundZ);
}
float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (!mapPoints || numPoints05 <= 0) return 1.0f;
float scaleX = numPoints05 * 2.0f;
float scaleY = numPoints05 / LOD_0_SIZE_Y;
int mapX = (int) std::round(worldX * scaleX);
int mapY = (int) std::round(worldY * scaleY);
if (mapX < 0) mapX = 0;
if (mapX >= (int)mapPoints->getWidth()) mapX = (int)mapPoints->getWidth() - 1;
if (mapY < 0) mapY = 0;
if (mapY >= (int)mapPoints->getHeight()) mapY = (int)mapPoints->getHeight() - 1;
unsigned char h = mapPoints->operator()(mapX, mapY).height;
return (h / 255.0f) * 3.5f + 1.0f;
}
void Unit::setPath(const std::vector<std::pair<int, int>> &newPath)
{
path = newPath;
pathIndex = 0;
moveProgress = 0.0f;
if (path.size() > 1) {
state = UnitState::Moving;
targetTileX = path.back().first;
targetTileY = path.back().second;
} else {
state = UnitState::Idle;
}
}
void Unit::stopMovement()
{
path.clear();
pathIndex = 0;
moveProgress = 0.0f;
state = UnitState::Idle;
}
void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (attackFlashTimer > 0.0f) {
attackFlashTimer = std::max(0.0f, attackFlashTimer - deltaTime);
}
if (state != UnitState::Moving || path.size() <= 1) {
return;
}
// Speed in hexes per second: movementSpeed points per tickDuration seconds
float effectiveTickDuration = std::max(0.1f, tickDuration);
float hexesPerSec = (config.movementSpeed / effectiveTickDuration) * simSpeedMultiplier;
moveProgress += deltaTime * hexesPerSec;
while (moveProgress >= 1.0f && pathIndex + 1 < path.size()) {
moveProgress -= 1.0f;
pathIndex++;
tileX = path[pathIndex].first;
tileY = path[pathIndex].second;
}
if (pathIndex + 1 >= path.size()) {
// Reached destination
tileX = path.back().first;
tileY = path.back().second;
QVector2D dest = Pathfinder::hexToWorld(tileX, tileY);
float gz = getTerrainHeight(dest.x(), dest.y(), mapPoints, numPoints05);
worldPos = QVector3D(dest.x(), dest.y(), gz);
state = UnitState::Idle;
path.clear();
moveProgress = 0.0f;
return;
}
// Smoothly interpolate between current waypoint and next waypoint
QVector2D from = Pathfinder::hexToWorld(path[pathIndex].first, path[pathIndex].second);
QVector2D to = Pathfinder::hexToWorld(path[pathIndex + 1].first, path[pathIndex + 1].second);
float t = std::max(0.0f, std::min(1.0f, moveProgress));
float curX = from.x() + t * (to.x() - from.x());
float curY = from.y() + t * (to.y() - from.y());
float curZ = getTerrainHeight(curX, curY, mapPoints, numPoints05);
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;
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef UNIT_H
#define UNIT_H
#include <QString>
#include <QVector3D>
#include <vector>
#include <utility>
#include "unitconfig.h"
#include "terrain.h"
#include "matrix2d.h"
enum class UnitState {
Idle,
Moving,
Attacking
};
class Unit {
public:
Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, float groundZ);
int id;
int playerId;
UnitConfig config;
int tileX;
int tileY;
QVector3D worldPos;
float health;
float maxHealth;
UnitState state;
// Movement Path
std::vector<std::pair<int, int>> path;
size_t pathIndex;
float moveProgress; // 0.0 to 1.0 between hexes
int targetTileX;
int targetTileY;
// Attack cooldown / flash
float attackFlashTimer;
void update(float deltaTime, float tickDuration, float simSpeedMultiplier,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void setPath(const std::vector<std::pair<int, int>> &newPath);
void stopMovement();
bool canAttack(const Unit &other) const;
void performAttack(Unit &target);
void takeDamage(float damage);
bool isDead() const { return health <= 0.001f; }
static float getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
};
#endif // UNIT_H
BIN
View File
Binary file not shown.
+68
View File
@@ -0,0 +1,68 @@
#include "unitconfig.h"
#include "yamlparser.h"
#include <QDir>
#include <QFileInfo>
#include <QDebug>
UnitConfigManager::UnitConfigManager()
{
}
bool UnitConfigManager::loadFromDirectory(const QString &dirPath)
{
QDir dir(dirPath);
if (!dir.exists()) {
qWarning() << "Unit config directory does not exist:" << dirPath;
return false;
}
QStringList filters;
filters << "*.yaml" << "*.yml";
QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &fileInfo : fileList) {
loadConfigFile(fileInfo.absoluteFilePath());
}
return !m_configs.isEmpty();
}
bool UnitConfigManager::loadConfigFile(const QString &filePath)
{
YamlNode root = YamlParser::parseFile(filePath);
if (root.children.isEmpty()) {
qWarning() << "Failed to parse unit config YAML:" << filePath;
return false;
}
UnitConfig cfg;
cfg.id = root.hasChild("id") ? root["id"].asString() : QFileInfo(filePath).baseName();
cfg.name = root.hasChild("name") ? root["name"].asString() : cfg.id;
cfg.category = stringToUnitCategory(root.hasChild("category") ? root["category"].asString() : "bouwen");
cfg.health = root.hasChild("health") ? root["health"].asInt(100) : 100;
cfg.movementSpeed = root.hasChild("movement_speed") ? root["movement_speed"].asFloat(2.0f) : 2.0f;
cfg.cost = root.hasChild("cost") ? root["cost"].asInt(20) : 20;
cfg.attackDamage = root.hasChild("attack_damage") ? root["attack_damage"].asInt(0) : 0;
cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : "";
if (root.hasChild("actions")) {
cfg.actions = root["actions"].listValues;
}
// Always ensure verplaatsen action is present
if (!cfg.actions.contains("verplaatsen")) {
cfg.actions.prepend("verplaatsen");
}
m_configs[cfg.id] = cfg;
return true;
}
const UnitConfig* UnitConfigManager::getConfig(const QString &id) const
{
auto it = m_configs.find(id);
if (it != m_configs.end()) {
return &it.value();
}
return nullptr;
}
+68
View File
@@ -0,0 +1,68 @@
#ifndef UNITCONFIG_H
#define UNITCONFIG_H
#include <QString>
#include <QStringList>
#include <QMap>
#include <vector>
enum class UnitCategory {
Military, // militair
Reconnaissance, // verkenning
Builder // bouwen
};
inline QString unitCategoryToString(UnitCategory cat) {
switch (cat) {
case UnitCategory::Military: return "Militair";
case UnitCategory::Reconnaissance: return "Verkenning";
case UnitCategory::Builder: return "Bouwen";
}
return "Onbekend";
}
inline UnitCategory stringToUnitCategory(const QString &str) {
QString s = str.trimmed().toLower();
if (s == "militair" || s == "military") return UnitCategory::Military;
if (s == "verkenning" || s == "recon" || s == "scout") return UnitCategory::Reconnaissance;
if (s == "bouwen" || s == "builder" || s == "civilian") return UnitCategory::Builder;
return UnitCategory::Builder;
}
struct UnitConfig {
QString id;
QString name;
UnitCategory category;
int health;
float movementSpeed;
int cost;
int attackDamage;
int attackRange; // 1 for melee, >1 for ranged
QString iconPath;
QStringList actions;
UnitConfig()
: id(""), name(""), category(UnitCategory::Builder), health(100),
movementSpeed(2.0f), cost(10), attackDamage(0), attackRange(1),
iconPath("") {}
bool isRanged() const { return attackRange > 1; }
bool isMilitary() const { return category == UnitCategory::Military; }
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
bool canBuildCity() const { return actions.contains("steden_bouwen") || actions.contains("build_city"); }
};
class UnitConfigManager {
public:
UnitConfigManager();
bool loadFromDirectory(const QString &dirPath);
bool loadConfigFile(const QString &filePath);
const UnitConfig* getConfig(const QString &id) const;
const QMap<QString, UnitConfig>& getAllConfigs() const { return m_configs; }
private:
QMap<QString, UnitConfig> m_configs;
};
#endif // UNITCONFIG_H
BIN
View File
Binary file not shown.
+191
View File
@@ -0,0 +1,191 @@
#include "unitrenderer.h"
#include <cmath>
#include <vector>
struct VertexPN {
float x, y, z;
float nx, ny, nz;
};
UnitRenderer::UnitRenderer()
: m_program(nullptr), m_vertexCount(0), m_initialized(false)
{
}
UnitRenderer::~UnitRenderer()
{
if (m_program) delete m_program;
}
void UnitRenderer::initialize()
{
if (m_initialized) return;
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->link();
buildCylinderMesh();
m_initialized = true;
}
void UnitRenderer::buildCylinderMesh()
{
std::vector<VertexPN> vertices;
const int sectors = 24;
const float pi = 3.14159265359f;
for (int i = 0; i < sectors; ++i) {
float a1 = 2.0f * pi * i / sectors;
float a2 = 2.0f * pi * (i + 1) / sectors;
float c1 = std::cos(a1), s1 = std::sin(a1);
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});
// Tri 2
vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f});
vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f});
vertices.push_back({c1, s1, 1.0f, c1, s1, 0.0f});
// --- Top Cap ---
vertices.push_back({0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f});
vertices.push_back({c1, s1, 1.0f, 0.0f, 0.0f, 1.0f});
vertices.push_back({c2, s2, 1.0f, 0.0f, 0.0f, 1.0f});
// --- Bottom Cap ---
vertices.push_back({0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f});
vertices.push_back({c2, s2, 0.0f, 0.0f, 0.0f, -1.0f});
vertices.push_back({c1, s1, 0.0f, 0.0f, 0.0f, -1.0f});
}
m_vertexCount = (int)vertices.size();
m_vao.create();
m_vao.bind();
m_vbo.create();
m_vbo.bind();
m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
m_vbo.allocate(vertices.data(), (int)(vertices.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_vao.release();
m_vbo.release();
}
void UnitRenderer::renderUnits(
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
const PlayerManager &players,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int selectedUnitId)
{
if (!m_initialized || !m_program) return;
glEnable(GL_DEPTH_TEST);
m_program->bind();
m_program->setUniformValue("u_lightDir", lightDir);
m_vao.bind();
// 1. Render Cities (as stylized hexagonal / cylindrical city bastions)
for (const auto &c : cities) {
const Player *p = players.getPlayer(c.playerId);
QColor col = p ? p->primaryColor : QColor(100, 100, 100);
QMatrix4x4 model;
model.translate(c.worldPos.x(), c.worldPos.y(), c.worldPos.z());
model.scale(0.30f, 0.30f, 0.45f);
QMatrix4x4 mvp = projection * vMatrix * model;
m_program->setUniformValue("modelMatrix", model);
m_program->setUniformValue("MVP", mvp);
m_program->setUniformValue("u_color", QVector4D(col.redF(), col.greenF(), col.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
}
// 2. Render Units (Cylinders in player primary color)
const float unitRadius = 0.18f;
const float unitHeight = 0.36f;
for (const auto &unit : units) {
if (unit->isDead()) continue;
const Player *player = players.getPlayer(unit->playerId);
QColor col = player ? player->primaryColor : QColor(200, 200, 200);
// Flash white/red if attacked
if (unit->attackFlashTimer > 0.0f) {
float f = unit->attackFlashTimer / 0.5f;
col = QColor(
(int)(col.red() * (1.0f - f) + 255 * f),
(int)(col.green() * (1.0f - f) + 255 * f),
(int)(col.blue() * (1.0f - f) + 255 * f)
);
}
QMatrix4x4 model;
model.translate(unit->worldPos.x(), unit->worldPos.y(), unit->worldPos.z());
model.scale(unitRadius, unitRadius, unitHeight);
QMatrix4x4 mvp = projection * vMatrix * model;
m_program->setUniformValue("modelMatrix", model);
m_program->setUniformValue("MVP", mvp);
m_program->setUniformValue("u_color", QVector4D(col.redF(), col.greenF(), col.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
// Ground selection base disk if selected
if (unit->id == selectedUnitId) {
QMatrix4x4 selModel;
selModel.translate(unit->worldPos.x(), unit->worldPos.y(), unit->worldPos.z());
selModel.scale(unitRadius * 1.6f, unitRadius * 1.6f, 0.04f);
QMatrix4x4 selMvp = projection * vMatrix * selModel;
m_program->setUniformValue("modelMatrix", selModel);
m_program->setUniformValue("MVP", selMvp);
m_program->setUniformValue("u_color", QVector4D(1.0f, 0.9f, 0.2f, 0.9f));
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
}
}
m_vao.release();
m_program->release();
}
QPointF UnitRenderer::projectToScreen(
const QVector3D &worldPoint,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
int viewportW, int viewportH,
bool &inFront)
{
QMatrix4x4 vp = projection * vMatrix;
QVector4D clip = vp.map(QVector4D(worldPoint, 1.0f));
inFront = (clip.w() > 0.01f);
if (!inFront) {
return QPointF(-9999, -9999);
}
QVector3D ndc = clip.toVector3DAffine();
float sx = (ndc.x() + 1.0f) * 0.5f * viewportW;
float sy = (1.0f - (ndc.y() + 1.0f) * 0.5f) * viewportH;
return QPointF(sx, sy);
}
+51
View File
@@ -0,0 +1,51 @@
#ifndef UNITRENDERER_H
#define UNITRENDERER_H
#include <QOpenGLFunctions_4_0_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QMatrix4x4>
#include <QVector3D>
#include <QPointF>
#include <vector>
#include <memory>
#include "unit.h"
#include "player.h"
#include "city.h"
class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
public:
UnitRenderer();
~UnitRenderer();
void initialize();
void renderUnits(
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities,
const PlayerManager &players,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int selectedUnitId
);
static QPointF projectToScreen(
const QVector3D &worldPoint,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
int viewportW, int viewportH,
bool &inFront
);
private:
void buildCylinderMesh();
QOpenGLShaderProgram *m_program;
QOpenGLBuffer m_vbo;
QOpenGLVertexArrayObject m_vao;
int m_vertexCount;
bool m_initialized;
};
#endif // UNITRENDERER_H
BIN
View File
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
#include "yamlparser.h"
#include <QFile>
#include <QTextStream>
YamlNode YamlParser::parseFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return YamlNode();
}
QTextStream in(&file);
return parseString(in.readAll());
}
YamlNode YamlParser::parseString(const QString &content)
{
YamlNode root;
QStringList lines = content.split('\n');
QString currentKey = "";
for (int i = 0; i < lines.size(); ++i) {
QString line = lines[i];
// Strip comments
int commentIdx = line.indexOf('#');
if (commentIdx != -1) {
line = line.left(commentIdx);
}
QString trimmed = line.trimmed();
if (trimmed.isEmpty()) continue;
// Check for list item: "- something"
if (trimmed.startsWith("- ")) {
QString itemVal = trimmed.mid(2).trimmed();
if (!currentKey.isEmpty()) {
root.children[currentKey].listValues.append(itemVal);
}
continue;
}
// Check for key: value
int colonIdx = trimmed.indexOf(':');
if (colonIdx != -1) {
QString key = trimmed.left(colonIdx).trimmed();
QString val = trimmed.mid(colonIdx + 1).trimmed();
// Strip enclosing quotes if any
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
val = val.mid(1, val.length() - 2);
}
currentKey = key;
YamlNode node;
node.value = val;
root.children[key] = node;
}
}
return root;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef YAMLPARSER_H
#define YAMLPARSER_H
#include <QString>
#include <QMap>
#include <QStringList>
#include <QVector>
class YamlNode {
public:
QString value;
QStringList listValues;
QMap<QString, YamlNode> children;
bool hasChild(const QString &key) const { return children.contains(key); }
YamlNode operator[](const QString &key) const { return children.value(key); }
YamlNode& operator[](const QString &key) { return children[key]; }
QString asString(const QString &def = "") const { return value.isEmpty() ? def : value; }
int asInt(int def = 0) const { bool ok; int v = value.toInt(&ok); return ok ? v : def; }
float asFloat(float def = 0.0f) const { bool ok; float v = value.toFloat(&ok); return ok ? v : def; }
bool asBool(bool def = false) const {
QString s = value.trimmed().toLower();
if (s == "true" || s == "yes" || s == "1") return true;
if (s == "false" || s == "no" || s == "0") return false;
return def;
}
};
class YamlParser {
public:
static YamlNode parseFile(const QString &filePath);
static YamlNode parseString(const QString &content);
};
#endif // YAMLPARSER_H
BIN
View File
Binary file not shown.