units
@@ -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.
|
||||||
@@ -16,7 +16,7 @@ CC = gcc
|
|||||||
CXX = g++
|
CXX = g++
|
||||||
DEFINES = -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB
|
DEFINES = -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB
|
||||||
CFLAGS = -pipe -O2 -Wall -Wextra -D_REENTRANT -fPIC $(DEFINES)
|
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++
|
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
|
QMAKE = /usr/lib/qt5/bin/qmake
|
||||||
DEL_FILE = rm -f
|
DEL_FILE = rm -f
|
||||||
@@ -58,7 +58,14 @@ SOURCES = main.cpp \
|
|||||||
vertex.cpp \
|
vertex.cpp \
|
||||||
tile.cpp \
|
tile.cpp \
|
||||||
tilevertexinfo.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
|
moc_mainwindow.cpp
|
||||||
OBJECTS = main.o \
|
OBJECTS = main.o \
|
||||||
mainwindow.o \
|
mainwindow.o \
|
||||||
@@ -67,10 +74,19 @@ OBJECTS = main.o \
|
|||||||
tile.o \
|
tile.o \
|
||||||
tilevertexinfo.o \
|
tilevertexinfo.o \
|
||||||
face.o \
|
face.o \
|
||||||
|
player.o \
|
||||||
|
yamlparser.o \
|
||||||
|
unitconfig.o \
|
||||||
|
pathfinder.o \
|
||||||
|
unit.o \
|
||||||
|
gamemanager.o \
|
||||||
|
unitrenderer.o \
|
||||||
qrc_shaders.o \
|
qrc_shaders.o \
|
||||||
moc_mainwindow.o
|
moc_mainwindow.o
|
||||||
DIST = shaders/simple.vert \
|
DIST = shaders/simple.vert \
|
||||||
shaders/simple.frag \
|
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/features/spec_pre.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/unix.conf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/unix.conf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/common/linux.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/features/qt_config.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf \
|
/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/exclusive_builds.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.prf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.prf \
|
||||||
@@ -164,13 +181,28 @@ DIST = shaders/simple.vert \
|
|||||||
terrain.h \
|
terrain.h \
|
||||||
tilevertexinfo.h \
|
tilevertexinfo.h \
|
||||||
matrix2d.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 \
|
mainwindow.cpp \
|
||||||
qscenerenderer.cpp \
|
qscenerenderer.cpp \
|
||||||
vertex.cpp \
|
vertex.cpp \
|
||||||
tile.cpp \
|
tile.cpp \
|
||||||
tilevertexinfo.cpp \
|
tilevertexinfo.cpp \
|
||||||
face.cpp
|
face.cpp \
|
||||||
|
player.cpp \
|
||||||
|
yamlparser.cpp \
|
||||||
|
unitconfig.cpp \
|
||||||
|
pathfinder.cpp \
|
||||||
|
unit.cpp \
|
||||||
|
gamemanager.cpp \
|
||||||
|
unitrenderer.cpp
|
||||||
QMAKE_TARGET = tiletest
|
QMAKE_TARGET = tiletest
|
||||||
DESTDIR =
|
DESTDIR =
|
||||||
TARGET = tiletest
|
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/features/qt_config.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf \
|
/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/exclusive_builds.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf \
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.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/features/qt_config.prf:
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf:
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++/qmake.conf:
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/spec_post.prf:
|
/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/exclusive_builds.prf:
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf:
|
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/toolchain.prf:
|
||||||
/usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/default_pre.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 $(DIST) $(DISTDIR)/
|
||||||
$(COPY_FILE) --parents shaders.qrc $(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 /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 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 $(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)/
|
$(COPY_FILE) --parents mainwindow.ui $(DISTDIR)/
|
||||||
|
|
||||||
|
|
||||||
@@ -414,7 +448,7 @@ compiler_moc_predefs_make_all: moc_predefs.h
|
|||||||
compiler_moc_predefs_clean:
|
compiler_moc_predefs_clean:
|
||||||
-$(DEL_FILE) moc_predefs.h
|
-$(DEL_FILE) moc_predefs.h
|
||||||
moc_predefs.h: /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp
|
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_make_all: moc_mainwindow.cpp
|
||||||
compiler_moc_header_clean:
|
compiler_moc_header_clean:
|
||||||
@@ -439,7 +473,13 @@ ui_mainwindow.h: mainwindow.ui \
|
|||||||
vertex.h \
|
vertex.h \
|
||||||
face.h \
|
face.h \
|
||||||
tilevertexinfo.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
|
/usr/lib/qt5/bin/uic mainwindow.ui -o ui_mainwindow.h
|
||||||
|
|
||||||
compiler_yacc_decl_make_all:
|
compiler_yacc_decl_make_all:
|
||||||
@@ -456,7 +496,20 @@ main.o: main.cpp mainwindow.h
|
|||||||
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o main.cpp
|
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o main.cpp
|
||||||
|
|
||||||
mainwindow.o: mainwindow.cpp mainwindow.h \
|
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
|
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp
|
||||||
|
|
||||||
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
|
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
|
||||||
@@ -465,7 +518,14 @@ qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
|
|||||||
vertex.h \
|
vertex.h \
|
||||||
face.h \
|
face.h \
|
||||||
tilevertexinfo.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
|
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qscenerenderer.o qscenerenderer.cpp
|
||||||
|
|
||||||
vertex.o: vertex.cpp vertex.h \
|
vertex.o: vertex.cpp vertex.h \
|
||||||
@@ -479,7 +539,13 @@ tile.o: tile.cpp tile.h \
|
|||||||
tilevertexinfo.h \
|
tilevertexinfo.h \
|
||||||
terrain.h \
|
terrain.h \
|
||||||
matrix2d.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
|
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o tile.o tile.cpp
|
||||||
|
|
||||||
tilevertexinfo.o: tilevertexinfo.cpp tilevertexinfo.h \
|
tilevertexinfo.o: tilevertexinfo.cpp tilevertexinfo.h \
|
||||||
@@ -492,6 +558,48 @@ face.o: face.cpp face.h \
|
|||||||
tilevertexinfo.h
|
tilevertexinfo.h
|
||||||
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o face.o face.cpp
|
$(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
|
qrc_shaders.o: qrc_shaders.cpp
|
||||||
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qrc_shaders.o qrc_shaders.cpp
|
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qrc_shaders.o qrc_shaders.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
movement_tick_duration: 30.0
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "face.h"
|
#include "face.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
float Face::sign (QVector2D p1, QVector2D p2, QVector2D p3)
|
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 Face::PointInFace (QVector2D pt)
|
||||||
{
|
{
|
||||||
bool b1, b2, b3;
|
Q_UNUSED(pt);
|
||||||
|
return true;
|
||||||
//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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Face::Face(Vertex *v1,TileVertexInfo *vi1, Vertex *v2,TileVertexInfo *vi2,Vertex *v3,TileVertexInfo *vi3)
|
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 va = v1 - v2;
|
||||||
QVector3D vb = v3 - v2;
|
QVector3D vb = v3 - v2;
|
||||||
m_normal = QVector3D::crossProduct(va,vb);
|
m_normal = QVector3D::crossProduct(va,vb);
|
||||||
/*float norm_len = sqrt(dot(norm, norm));
|
if (m_normal.z() < 0.0f) {
|
||||||
norm /= norm_len; */
|
m_normal = -m_normal;
|
||||||
|
}
|
||||||
|
m_normal.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector3D Face::normal()
|
QVector3D Face::normal()
|
||||||
@@ -51,6 +48,11 @@ QVector3D Face::normal()
|
|||||||
return m_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)
|
void Face::findFaces(std::vector<Face *> &vec, QVector2D s)
|
||||||
{
|
{
|
||||||
if (m_subface!=NULL) {
|
if (m_subface!=NULL) {
|
||||||
@@ -59,11 +61,10 @@ void Face::findFaces(std::vector<Face *> &vec, QVector2D s)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (
|
if (m_v1 && m_v2 && m_v3 && (
|
||||||
((m_v1->position().x() == s.x()) && (m_v1->position().y() == s.y())) ||
|
pointMatch(m_v1->position(), s) ||
|
||||||
((m_v2->position().x() == s.x()) && (m_v2->position().y() == s.y())) ||
|
pointMatch(m_v2->position(), s) ||
|
||||||
((m_v3->position().x()== s.x()) && (m_v3->position().y() == s.y()))
|
pointMatch(m_v3->position(), s))) {
|
||||||
) {
|
|
||||||
vec.push_back(this);
|
vec.push_back(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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});
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 18 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 9.8 MiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 14 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 970 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 600 B |
|
Before Width: | Height: | Size: 5.4 MiB After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 5.4 MiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -7,6 +7,9 @@ class Matrix2D {
|
|||||||
std::vector<T> data;
|
std::vector<T> data;
|
||||||
unsigned int sizeX, sizeY;
|
unsigned int sizeX, sizeY;
|
||||||
public:
|
public:
|
||||||
|
unsigned int getWidth() const { return sizeX; }
|
||||||
|
unsigned int getHeight() const { return sizeY; }
|
||||||
|
|
||||||
Matrix2D (unsigned int x, unsigned int y)
|
Matrix2D (unsigned int x, unsigned int y)
|
||||||
: sizeX (x), sizeY (y) {
|
: sizeX (x), sizeY (y) {
|
||||||
data.resize (sizeX*sizeY);
|
data.resize (sizeX*sizeY);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -7,45 +7,94 @@
|
|||||||
#include "terrain.h"
|
#include "terrain.h"
|
||||||
#include "tile.h"
|
#include "tile.h"
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QTimer>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <QMap>
|
||||||
#include "matrix2d.h"
|
#include "matrix2d.h"
|
||||||
|
#include "unitrenderer.h"
|
||||||
|
#include "gamemanager.h"
|
||||||
|
|
||||||
class QSceneRenderer : public QOpenGLWidget
|
class QSceneRenderer : public QOpenGLWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
QSceneRenderer(QWidget* parent = Q_NULLPTR) ;
|
QSceneRenderer(QWidget* parent = Q_NULLPTR);
|
||||||
inline Matrix2D<TerrainPoint> * getMapPoints() {return m_point;}
|
inline Matrix2D<TerrainPoint> * getMapPoints() { return m_point; }
|
||||||
inline int getNumPointsWidth() const {return m_numPointsWidth;}
|
inline int getNumPointsWidth() const { return m_numPointsWidth; }
|
||||||
inline int getNumPointsHeight() const {return m_numPointsHeight;}
|
inline int getNumPointsHeight() const { return m_numPointsHeight; }
|
||||||
inline int getNumPoints05() const {return m_numPoints05;}
|
inline int getNumPoints05() const { return m_numPoints05; }
|
||||||
|
|
||||||
Tile * getTile(int x, int y);
|
Tile * getTile(int x, int y);
|
||||||
protected:
|
void loadRiverMap(const QImage &riverImg);
|
||||||
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();
|
|
||||||
|
|
||||||
float m_camera_x =10.0;
|
protected:
|
||||||
float m_camera_y=0.0;
|
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 Projection;
|
||||||
QMatrix4x4 cameraTransformation;
|
QMatrix4x4 cameraTransformation;
|
||||||
QVector3D cameraPosition;
|
QVector3D cameraPosition;
|
||||||
QVector3D cameraUpDirection;
|
QVector3D cameraUpDirection;
|
||||||
|
|
||||||
|
|
||||||
Matrix2D<TerrainPoint> * m_point;
|
Matrix2D<TerrainPoint> * m_point;
|
||||||
Matrix2D<Tile> * tiles;
|
Matrix2D<Tile> * tiles;
|
||||||
QPoint lastPos;
|
QPoint lastPos;
|
||||||
float zRot=0;
|
float zRot = 0.0f;
|
||||||
float m_numPointsWidth;
|
float m_numPointsWidth;
|
||||||
float m_numPointsHeight;
|
float m_numPointsHeight;
|
||||||
int m_numPoints05;
|
int m_numPoints05;
|
||||||
float m_zoom;
|
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
|
#endif // QSCENERENDERER_H
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
#version 400
|
#version 400
|
||||||
uniform sampler2D tex; //this is the texture
|
uniform sampler2D texGrass; // Unit 0
|
||||||
uniform sampler2D border; //this is the texture
|
uniform sampler2D texBorder; // Unit 1
|
||||||
uniform sampler2D rock; //this is the texture
|
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 vNormal;
|
||||||
smooth in vec3 vWorldPos;
|
smooth in vec3 vWorldPos;
|
||||||
in vec3 fragVert;
|
in vec3 fragVert;
|
||||||
@@ -10,11 +23,8 @@ uniform vec3 cameraPosition;
|
|||||||
|
|
||||||
uniform mat4 modelMatrix;
|
uniform mat4 modelMatrix;
|
||||||
in vec2 tilePos;
|
in vec2 tilePos;
|
||||||
in vec2 fragTexCoord; //this is the texture coord
|
in vec2 fragTexCoord;
|
||||||
out vec4 finalColor; //this is the output color of the pixel
|
out vec4 finalColor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct SimpleDirectionalLight
|
struct SimpleDirectionalLight
|
||||||
{
|
{
|
||||||
@@ -23,55 +33,243 @@ struct SimpleDirectionalLight
|
|||||||
float fAmbientIntensity;
|
float fAmbientIntensity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
uniform SimpleDirectionalLight sunLight;
|
uniform SimpleDirectionalLight sunLight;
|
||||||
|
|
||||||
vec4 GetDirectionalLightColor(SimpleDirectionalLight dirLight, vec3 vNormal)
|
vec4 GetDirectionalLightColor(SimpleDirectionalLight dirLight, vec3 norm)
|
||||||
{
|
{
|
||||||
float fDiffuseIntensity = dot(vNormal, -dirLight.vDirection);
|
float fDiffuseIntensity = max(dot(norm, -dirLight.vDirection), 0.0);
|
||||||
float fMult = pow(dirLight.fAmbientIntensity+fDiffuseIntensity,1.0);//clamp(dirLight.fAmbientIntensity+fDiffuseIntensity, -1.0, 1.0);
|
float fMult = dirLight.fAmbientIntensity + fDiffuseIntensity * (1.0 - dirLight.fAmbientIntensity * 0.5);
|
||||||
// if(dirLight.iSkybox == 1)fMult = dirLight.fAmbientIntensity;
|
return vec4(dirLight.vColor * fMult, 1.0);
|
||||||
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));
|
if (fSpecularFactor > 0.0)
|
||||||
vec3 vVertexToEyeVector = normalize(vEyePos-vWorldPos);
|
|
||||||
float fSpecularFactor = dot(vVertexToEyeVector, vReflectedVector);
|
|
||||||
|
|
||||||
if (fSpecularFactor > 0)
|
|
||||||
{
|
{
|
||||||
fSpecularFactor = pow(fSpecularFactor, 1.0);
|
fSpecularFactor = pow(fSpecularFactor, shininess);
|
||||||
vResult = vec4(dLight.vColor, 1.0) * 1.0 * fSpecularFactor;
|
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() {
|
void main() {
|
||||||
|
vec3 norm = normalize(vNormal);
|
||||||
|
|
||||||
vec3 surfaceToLight = normalize(sunLight.vDirection);
|
// 1. Biome Typemap Sampling
|
||||||
vec3 surfacePos = vec3(modelMatrix * vec4(fragVert, 1));
|
// World extent: [0..225.0 km, 0..108.22 km]
|
||||||
vec3 surfaceToCamera = normalize(cameraPosition - surfacePos);
|
// Note: QImage row 0 is North (worldY = 108.22), which QOpenGLTexture maps to v = 0.0.
|
||||||
vec4 bcolor = texture(border, vec2(tilePos.x,tilePos.y*1.154734411));
|
// Invert Y so North samples row 0 and South samples row 1499.
|
||||||
float fDiffuseIntensity = max(0.0, dot(normalize(vNormal), -sunLight.vDirection));
|
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;
|
// Compute smooth biome weights from typemap colors:
|
||||||
vec4 vSpecularColor = GetSpecularColor(vWorldPos, cameraPosition, sunLight, normalize(vNormal))+ GetDirectionalLightColor(sunLight,normalize(vNormal));
|
// 1. Prairie: red dominant (r - g)
|
||||||
vec4 ground = texture(tex, fragTexCoord);
|
float prairieWeight = clamp((typePixel.r - typePixel.g - 0.20) / 0.25, 0.0, 1.0);
|
||||||
vec4 rockt = texture(rock, fragTexCoord);
|
// 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;
|
// 2. Polar Latitude Calculation (World Y in [0..108.25 km])
|
||||||
if (z_pos>0.3)
|
float normY = clamp(vWorldPos.y / 108.25, 0.0, 1.0);
|
||||||
gndfac = 0.0;
|
float distFromCenter = abs(normY - 0.5) * 2.0; // 0.0 at middle, 1.0 at North/South poles
|
||||||
else if (z_pos>0.1)
|
float poleFactor = smoothstep(0.40, 0.85, distFromCenter);
|
||||||
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);
|
|
||||||
|
|
||||||
// (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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ uniform mat4 projectionMatrix;
|
|||||||
|
|
||||||
uniform mat4 normalMatrix;
|
uniform mat4 normalMatrix;
|
||||||
uniform mat4 modelMatrix;
|
uniform mat4 modelMatrix;
|
||||||
|
uniform mat4 worldMatrix;
|
||||||
|
|
||||||
layout(location = 0) in vec3 position;
|
layout(location = 0) in vec3 position;
|
||||||
layout(location = 1) in vec2 inCoord;
|
layout(location = 1) in vec2 inCoord;
|
||||||
|
|
||||||
layout (location = 2) in float zPos;
|
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 fragTexCoord;
|
||||||
out vec2 tilePos;
|
out vec2 tilePos;
|
||||||
out vec3 fragVert;
|
out vec3 fragVert;
|
||||||
@@ -20,22 +20,24 @@ smooth out vec3 vWorldPos;
|
|||||||
uniform mat4 MVP;
|
uniform mat4 MVP;
|
||||||
smooth out vec3 vNormal;
|
smooth out vec3 vNormal;
|
||||||
out float z_pos;
|
out float z_pos;
|
||||||
|
|
||||||
void main()
|
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;
|
vNormal = inNormal.xyz;
|
||||||
fragVert = position;
|
fragVert = position;
|
||||||
|
z_pos = zPos;
|
||||||
vec4 vWorldPosLocal =modelMatrix*inPositionScaled;
|
|
||||||
vWorldPos = vWorldPosLocal.xyz;
|
|
||||||
z_pos=zPos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -20,9 +20,39 @@
|
|||||||
|
|
||||||
#define PI 3.14159265359f
|
#define PI 3.14159265359f
|
||||||
|
|
||||||
struct TerrainPoint {
|
#include <QVector2D>
|
||||||
char height;
|
|
||||||
|
|
||||||
|
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
|
#endif // TERRAIN_H
|
||||||
|
|||||||
@@ -3,12 +3,30 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "qscenerenderer.h"
|
#include "qscenerenderer.h"
|
||||||
#include <QVector3D>
|
#include <QVector3D>
|
||||||
QImage * Tile::m_image = NULL;
|
QImage * Tile::m_image_grass = NULL;
|
||||||
QOpenGLTexture * Tile::m_texture = NULL;
|
QOpenGLTexture * Tile::m_texture_grass = NULL;
|
||||||
QImage * Tile::m_image_border = NULL;
|
QImage * Tile::m_image_border = NULL;
|
||||||
QOpenGLTexture * Tile::m_texture_border = NULL;
|
QOpenGLTexture * Tile::m_texture_border = NULL;
|
||||||
QOpenGLTexture * Tile::m_texture_rock = NULL;
|
QOpenGLTexture * Tile::m_texture_rock = NULL;
|
||||||
QImage * Tile::m_image_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;
|
Vertex * Tile::m_vertices= NULL;
|
||||||
int Tile::m_numVertex;
|
int Tile::m_numVertex;
|
||||||
@@ -18,10 +36,53 @@ Vertex * Tile::m_vertices= NULL;
|
|||||||
QOpenGLShaderProgram *Tile::m_program= 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()
|
Tile::Tile()
|
||||||
: m_scene(NULL), m_tilevertices(NULL), m_x(0), m_y(0),
|
: m_scene(NULL), m_tilevertices(NULL), m_x(0), m_y(0),
|
||||||
m_startpoint_x(0.0f), m_startpoint_y(0.0f),
|
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();
|
buildHeights();
|
||||||
|
|
||||||
if (Tile::m_image == NULL)
|
if (Tile::m_image_grass == NULL)
|
||||||
Tile::m_image = new QImage("/home/bart/project/tiletest/images/grass.jpg");
|
Tile::m_image_grass = new QImage("/home/bart/project/tiletest/images/grass.jpg");
|
||||||
if (Tile::m_image_border == NULL)
|
if (Tile::m_image_border == NULL)
|
||||||
Tile::m_image_border = new QImage("/home/bart/project/tiletest/images/border.png");
|
Tile::m_image_border = new QImage("/home/bart/project/tiletest/images/border.png");
|
||||||
if (Tile::m_image_rock == NULL)
|
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) {
|
if (Tile::m_texture_grass == NULL) {
|
||||||
m_texture_rock = new QOpenGLTexture(*m_image_rock);
|
m_texture_grass = new QOpenGLTexture(*m_image_grass);
|
||||||
m_texture_rock->setWrapMode(QOpenGLTexture::Repeat);
|
m_texture_grass->setWrapMode(QOpenGLTexture::Repeat);
|
||||||
m_texture_rock->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
|
m_texture_grass->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
|
||||||
m_texture_rock->setMagnificationFilter(QOpenGLTexture::Nearest);
|
m_texture_grass->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||||
//m_texture->setFormat(QOpenGLTexture::RGB32F);33
|
m_texture_grass->generateMipMaps();
|
||||||
m_texture_rock->allocateStorage();
|
|
||||||
}
|
}
|
||||||
if (Tile::m_texture_border == NULL) {
|
if (Tile::m_texture_border == NULL) {
|
||||||
m_texture_border = new QOpenGLTexture(*m_image_border);
|
m_texture_border = new QOpenGLTexture(*m_image_border);
|
||||||
m_texture_border->setWrapMode(QOpenGLTexture::Repeat);
|
m_texture_border->setWrapMode(QOpenGLTexture::Repeat);
|
||||||
m_texture_border->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
|
m_texture_border->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
|
||||||
m_texture_border->setMagnificationFilter(QOpenGLTexture::Nearest);
|
m_texture_border->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||||
//m_texture->setFormat(QOpenGLTexture::RGB32F);33
|
m_texture_border->generateMipMaps();
|
||||||
m_texture_border->allocateStorage();
|
|
||||||
}
|
}
|
||||||
if (Tile::m_texture == NULL) {
|
if (Tile::m_texture_rock == NULL) {
|
||||||
m_texture = new QOpenGLTexture(*m_image);
|
m_texture_rock = new QOpenGLTexture(*m_image_rock);
|
||||||
m_texture->setWrapMode(QOpenGLTexture::Repeat);
|
m_texture_rock->setWrapMode(QOpenGLTexture::Repeat);
|
||||||
m_texture->setMinificationFilter(QOpenGLTexture::NearestMipMapNearest);
|
m_texture_rock->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
|
||||||
m_texture->setMagnificationFilter(QOpenGLTexture::Nearest);
|
m_texture_rock->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||||
//m_texture->setFormat(QOpenGLTexture::RGB32F);
|
m_texture_rock->generateMipMaps();
|
||||||
m_texture->allocateStorage();
|
}
|
||||||
|
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){
|
if (first){
|
||||||
@@ -132,7 +271,7 @@ void Tile::init(QSceneRenderer* s, int x, int y)
|
|||||||
m_initialized = true;
|
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;
|
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 *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);
|
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_border->bind(1);
|
||||||
m_texture_rock->bind(2);
|
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
|
// Render using our shader
|
||||||
m_program->bind();
|
m_program->bind();
|
||||||
|
|
||||||
@@ -174,9 +322,20 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
|
|||||||
//mModelToCamera.scale(0.8);
|
//mModelToCamera.scale(0.8);
|
||||||
|
|
||||||
|
|
||||||
m_program->setUniformValue("grass", 0);
|
m_program->setUniformValue("texGrass", 0);
|
||||||
m_program->setUniformValue("border", 1);
|
m_program->setUniformValue("texBorder", 1);
|
||||||
m_program->setUniformValue("rock", 2);
|
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_object.bind();
|
||||||
|
|
||||||
m_heightsbuffer.bind();
|
m_heightsbuffer.bind();
|
||||||
@@ -195,6 +354,7 @@ m_program->setUniformValue("cameraPosition", cameraPosition);
|
|||||||
//mModelToCamera =vMatrix * mMatrix.inverted().transposed();//QMatrix4x4();// vPos.translate(1.0f);
|
//mModelToCamera =vMatrix * mMatrix.inverted().transposed();//QMatrix4x4();// vPos.translate(1.0f);
|
||||||
m_program->setUniformValue("normalMatrix", mModelToCamera);
|
m_program->setUniformValue("normalMatrix", mModelToCamera);
|
||||||
m_program->setUniformValue("modelMatrix", rMatrix);
|
m_program->setUniformValue("modelMatrix", rMatrix);
|
||||||
|
m_program->setUniformValue("worldMatrix", mMatrix);
|
||||||
m_program->setUniformValue("modelViewMatrix", vMatrix * mMatrix);
|
m_program->setUniformValue("modelViewMatrix", vMatrix * mMatrix);
|
||||||
m_program->setUniformValue("MVP", p * vMatrix * mMatrix);
|
m_program->setUniformValue("MVP", p * vMatrix * mMatrix);
|
||||||
glDrawArrays(GL_TRIANGLES, 0, NUM_TRIANGLES*NUM_TRIANGLE_VERTICES);
|
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() {
|
void Tile::buildHeights() {
|
||||||
m_tilevertices = new TileVertexInfo[m_numVertex];
|
m_tilevertices = new TileVertexInfo[m_numVertex];
|
||||||
Matrix2D<TerrainPoint> * points = Tile::m_scene->getMapPoints();
|
Matrix2D<TerrainPoint> * points = Tile::m_scene->getMapPoints();
|
||||||
for (int i = 0; i< m_numVertex; i++) {
|
|
||||||
|
|
||||||
/*float x=m_vertices[i].position().x();
|
float tileWorldX = m_x * (LOD_0_SIZE * 0.75f);
|
||||||
int xpos = m_vertices[i].position().x()*(m_scene->getNumPoints05()*2);
|
float tileWorldY = m_y * (LOD_0_SIZE_Y * 2.0f) + ((m_x % 2 == 0) ? 0.0f : LOD_0_SIZE_Y);
|
||||||
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 scaleX = m_scene->getNumPoints05() * 2.0f;
|
||||||
float r = points->operator ()( (int) round( m_startpoint_x+ ( m_vertices[i].position().x())*(m_scene->getNumPoints05()*2.0f))
|
float scaleY = m_scene->getNumPoints05() / LOD_0_SIZE_Y;
|
||||||
,(int) round( m_startpoint_y+ ( m_vertices[i].position().y())*(m_scene->getNumPoints05()*2.0f))).height /255.0f;
|
|
||||||
//m_heights[i] = 1r;
|
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].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);
|
m_faces.reserve(m_numVertex/3);
|
||||||
int i =0;
|
int i =0;
|
||||||
@@ -362,27 +534,62 @@ void Tile::findFaces(std::vector<Face *> &vec, QVector2D s)
|
|||||||
void Tile::buildNormals()
|
void Tile::buildNormals()
|
||||||
{
|
{
|
||||||
if (!m_initialized) return;
|
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;
|
std::vector<Face *> vec;
|
||||||
for (int i=0; i<m_numVertex;i++) {
|
for (int i=0; i<m_numVertex;i++) {
|
||||||
vec.clear();
|
vec.clear();
|
||||||
|
|
||||||
findFaces(vec, m_vertices[i].position());
|
QVector2D vPos = m_vertices[i].position();
|
||||||
m_scene->getTile(m_x-1,m_y)->findFaces(vec,m_vertices[i].position());
|
findFaces(vec, vPos);
|
||||||
m_scene->getTile(m_x+1,m_y)->findFaces(vec,m_vertices[i].position());
|
|
||||||
|
|
||||||
QVector3D r;
|
if (vPos.length() > 0.4f) {
|
||||||
float cnt =0;
|
QVector2D worldV = myWorld + vPos;
|
||||||
for (std::vector<Face *>::iterator it = vec.begin() ; it != vec.end(); ++it) {
|
for (int k = 0; k < 6; ++k) {
|
||||||
QVector3D v = (*it)->normal();
|
int nx = nbrs[k][0];
|
||||||
cnt++;
|
int ny = nbrs[k][1];
|
||||||
r.setX(r.x() + v.x());
|
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
|
||||||
r.setY(r.y() + v.y());
|
Tile *nbr = m_scene->getTile(nx, ny);
|
||||||
r.setZ(r.z() + v.z());
|
if (nbr && nbr->isInitialized()) {
|
||||||
|
QVector2D localInNbr = worldV - nbr->getWorldPosition();
|
||||||
|
nbr->findFaces(vec, localInNbr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.setX(r.x() /cnt);
|
QVector3D r(0.0f, 0.0f, 0.0f);
|
||||||
r.setY(r.y() /cnt);
|
float cnt = 0;
|
||||||
r.setZ(r.z() /cnt);
|
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);
|
m_tilevertices[i].setNormal(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Tile
|
|||||||
public:
|
public:
|
||||||
Tile();
|
Tile();
|
||||||
void init(QSceneRenderer* s,int x, int y);
|
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 buildVertices();
|
||||||
void buildNormals();
|
void buildNormals();
|
||||||
bool isInitialized() const { return m_initialized; }
|
bool isInitialized() const { return m_initialized; }
|
||||||
@@ -24,6 +24,14 @@ public:
|
|||||||
void ensureInitialized(QSceneRenderer* s, int x, int y);
|
void ensureInitialized(QSceneRenderer* s, int x, int y);
|
||||||
void ensureNormalsBuilt();
|
void ensureNormalsBuilt();
|
||||||
void findFaces(std::vector<Face *> &vec, QVector2D s);
|
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:
|
protected:
|
||||||
void addTriangleVertices(int &cnt, QVector2D t1, QVector2D t2, QVector2D t3,int level, int hex, int l1);
|
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);
|
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;
|
float m_startpoint_y;
|
||||||
bool m_initialized;
|
bool m_initialized;
|
||||||
bool m_normalsBuilt;
|
bool m_normalsBuilt;
|
||||||
|
unsigned char m_riverMask;
|
||||||
static Vertex * m_vertices;
|
static Vertex * m_vertices;
|
||||||
static QOpenGLTexture * m_texture;
|
static QOpenGLTexture * m_texture_grass;
|
||||||
static QOpenGLTexture * m_texture_border;
|
static QOpenGLTexture * m_texture_border;
|
||||||
static QOpenGLTexture * m_texture_rock;
|
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_rock;
|
||||||
static QImage * m_image_border;
|
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
|
#endif // TILE_H
|
||||||
|
|||||||
@@ -11,14 +11,22 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
|||||||
TARGET = tiletest
|
TARGET = tiletest
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
||||||
|
CONFIG += c++17
|
||||||
|
|
||||||
SOURCES += main.cpp\
|
SOURCES += main.cpp \
|
||||||
mainwindow.cpp \
|
mainwindow.cpp \
|
||||||
qscenerenderer.cpp \
|
qscenerenderer.cpp \
|
||||||
vertex.cpp \
|
vertex.cpp \
|
||||||
tile.cpp \
|
tile.cpp \
|
||||||
tilevertexinfo.cpp \
|
tilevertexinfo.cpp \
|
||||||
face.cpp
|
face.cpp \
|
||||||
|
player.cpp \
|
||||||
|
yamlparser.cpp \
|
||||||
|
unitconfig.cpp \
|
||||||
|
pathfinder.cpp \
|
||||||
|
unit.cpp \
|
||||||
|
gamemanager.cpp \
|
||||||
|
unitrenderer.cpp
|
||||||
|
|
||||||
HEADERS += mainwindow.h \
|
HEADERS += mainwindow.h \
|
||||||
qscenerenderer.h \
|
qscenerenderer.h \
|
||||||
@@ -27,7 +35,15 @@ HEADERS += mainwindow.h \
|
|||||||
terrain.h \
|
terrain.h \
|
||||||
tilevertexinfo.h \
|
tilevertexinfo.h \
|
||||||
matrix2d.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
|
FORMS += mainwindow.ui
|
||||||
|
|
||||||
@@ -36,7 +52,9 @@ unix: PKGCONFIG += glew
|
|||||||
|
|
||||||
DISTFILES += \
|
DISTFILES += \
|
||||||
shaders/simple.vert \
|
shaders/simple.vert \
|
||||||
shaders/simple.frag
|
shaders/simple.frag \
|
||||||
|
shaders/unit.vert \
|
||||||
|
shaders/unit.frag
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
shaders.qrc
|
shaders.qrc
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||