3D target demo
This commit is contained in:
parent
cc027a2517
commit
7e8efe82ec
5 changed files with 661 additions and 0 deletions
|
|
@ -1121,6 +1121,47 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject*
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::py_follow_path(PyEntity3DObject* self, PyObject* args)
|
||||||
|
{
|
||||||
|
PyObject* path_list;
|
||||||
|
if (!PyArg_ParseTuple(args, "O", &path_list)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyList_Check(path_list)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "follow_path() requires a list of (x, z) tuples");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<int, int>> path;
|
||||||
|
Py_ssize_t len = PyList_Size(path_list);
|
||||||
|
for (Py_ssize_t i = 0; i < len; ++i) {
|
||||||
|
PyObject* item = PyList_GetItem(path_list, i);
|
||||||
|
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Each path element must be (x, z) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int x = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(item, 0)));
|
||||||
|
int z = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(item, 1)));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
path.emplace_back(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->followPath(path);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::py_clear_path(PyEntity3DObject* self, PyObject* args)
|
||||||
|
{
|
||||||
|
self->data->clearPath();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* Entity3D::get_is_moving(PyEntity3DObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyBool_FromLong(self->data->isMoving() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Method and GetSet tables
|
// Method and GetSet tables
|
||||||
|
|
||||||
PyMethodDef Entity3D::methods[] = {
|
PyMethodDef Entity3D::methods[] = {
|
||||||
|
|
@ -1141,6 +1182,14 @@ PyMethodDef Entity3D::methods[] = {
|
||||||
{"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS,
|
{"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS,
|
||||||
"animate(property, target, duration, easing=None, callback=None)\n\n"
|
"animate(property, target, duration, easing=None, callback=None)\n\n"
|
||||||
"Animate a property over time. (Not yet implemented)"},
|
"Animate a property over time. (Not yet implemented)"},
|
||||||
|
{"follow_path", (PyCFunction)Entity3D::py_follow_path, METH_VARARGS,
|
||||||
|
"follow_path(path)\n\n"
|
||||||
|
"Queue path positions for smooth movement.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" path: List of (x, z) tuples (as returned by path_to())"},
|
||||||
|
{"clear_path", (PyCFunction)Entity3D::py_clear_path, METH_NOARGS,
|
||||||
|
"clear_path()\n\n"
|
||||||
|
"Clear the movement queue and stop at current position."},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1185,6 +1234,8 @@ PyGetSetDef Entity3D::getsetters[] = {
|
||||||
"Animation clip to play when entity is moving.", NULL},
|
"Animation clip to play when entity is moving.", NULL},
|
||||||
{"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip,
|
{"idle_clip", (getter)Entity3D::get_idle_clip, (setter)Entity3D::set_idle_clip,
|
||||||
"Animation clip to play when entity is stationary.", NULL},
|
"Animation clip to play when entity is stationary.", NULL},
|
||||||
|
{"is_moving", (getter)Entity3D::get_is_moving, NULL,
|
||||||
|
"Whether entity is currently moving (read-only).", NULL},
|
||||||
|
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,9 @@ public:
|
||||||
static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_at(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args);
|
static PyObject* py_update_visibility(PyEntity3DObject* self, PyObject* args);
|
||||||
static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* py_follow_path(PyEntity3DObject* self, PyObject* args);
|
||||||
|
static PyObject* py_clear_path(PyEntity3DObject* self, PyObject* args);
|
||||||
|
static PyObject* get_is_moving(PyEntity3DObject* self, void* closure);
|
||||||
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,67 @@ void Viewport3D::orbitCamera(float angle, float distance, float height) {
|
||||||
camera_.setTarget(vec3(0, 0, 0));
|
camera_.setTarget(vec3(0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec3 Viewport3D::screenToWorld(float screenX, float screenY) {
|
||||||
|
// Convert screen coordinates to normalized device coordinates (-1 to 1)
|
||||||
|
// screenX/Y are relative to the viewport position
|
||||||
|
float ndcX = (2.0f * screenX / size_.x) - 1.0f;
|
||||||
|
float ndcY = 1.0f - (2.0f * screenY / size_.y); // Flip Y for OpenGL
|
||||||
|
|
||||||
|
// Get inverse matrices
|
||||||
|
mat4 proj = camera_.getProjectionMatrix();
|
||||||
|
mat4 view = camera_.getViewMatrix();
|
||||||
|
mat4 invProj = proj.inverse();
|
||||||
|
mat4 invView = view.inverse();
|
||||||
|
|
||||||
|
// Unproject near plane point to get ray direction
|
||||||
|
vec4 rayClip(ndcX, ndcY, -1.0f, 1.0f);
|
||||||
|
vec4 rayEye = invProj * rayClip;
|
||||||
|
rayEye = vec4(rayEye.x, rayEye.y, -1.0f, 0.0f); // Direction in eye space
|
||||||
|
|
||||||
|
vec4 rayWorld4 = invView * rayEye;
|
||||||
|
vec3 rayDir = vec3(rayWorld4.x, rayWorld4.y, rayWorld4.z).normalized();
|
||||||
|
vec3 rayOrigin = camera_.getPosition();
|
||||||
|
|
||||||
|
// Intersect with Y=0 plane (ground level)
|
||||||
|
// This is a simplification - for hilly terrain, you'd want ray-marching
|
||||||
|
if (std::abs(rayDir.y) > 0.0001f) {
|
||||||
|
float t = -rayOrigin.y / rayDir.y;
|
||||||
|
if (t > 0) {
|
||||||
|
return rayOrigin + rayDir * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ray parallel to ground or pointing away - return invalid position
|
||||||
|
return vec3(-1.0f, -1.0f, -1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::followEntity(std::shared_ptr<Entity3D> entity, float distance, float height, float smoothing) {
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
// Get entity's world position
|
||||||
|
vec3 entityPos = entity->getWorldPos();
|
||||||
|
|
||||||
|
// Calculate desired camera position behind and above entity
|
||||||
|
float entityRotation = radians(entity->getRotation());
|
||||||
|
float camX = entityPos.x - std::sin(entityRotation) * distance;
|
||||||
|
float camZ = entityPos.z - std::cos(entityRotation) * distance;
|
||||||
|
float camY = entityPos.y + height;
|
||||||
|
|
||||||
|
vec3 desiredPos(camX, camY, camZ);
|
||||||
|
vec3 currentPos = camera_.getPosition();
|
||||||
|
|
||||||
|
// Smooth interpolation (smoothing is 0-1, where 1 = instant)
|
||||||
|
if (smoothing >= 1.0f) {
|
||||||
|
camera_.setPosition(desiredPos);
|
||||||
|
} else {
|
||||||
|
vec3 newPos = vec3::lerp(currentPos, desiredPos, smoothing);
|
||||||
|
camera_.setPosition(newPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look at entity (slightly above ground)
|
||||||
|
camera_.setTarget(vec3(entityPos.x, entityPos.y + 0.5f, entityPos.z));
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Mesh Layer Management
|
// Mesh Layer Management
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -2114,6 +2175,59 @@ static PyObject* Viewport3D_billboard_count(PyViewport3DObject* self, PyObject*
|
||||||
return PyLong_FromLong(static_cast<long>(billboards->size()));
|
return PyLong_FromLong(static_cast<long>(billboards->size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Camera & Input Methods (Milestone 8)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_screen_to_world(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"x", "y", NULL};
|
||||||
|
|
||||||
|
float x = 0.0f, y = 0.0f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ff", const_cast<char**>(kwlist), &x, &y)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for viewport position (user passes screen coords relative to viewport)
|
||||||
|
vec3 worldPos = self->data->screenToWorld(x, y);
|
||||||
|
|
||||||
|
// Return None if no intersection (ray parallel to ground or invalid)
|
||||||
|
if (worldPos.x < 0 && worldPos.y < 0 && worldPos.z < 0) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Py_BuildValue("(fff)", worldPos.x, worldPos.y, worldPos.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_follow(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"entity", "distance", "height", "smoothing", NULL};
|
||||||
|
|
||||||
|
PyObject* entityObj = nullptr;
|
||||||
|
float distance = 10.0f;
|
||||||
|
float height = 5.0f;
|
||||||
|
float smoothing = 1.0f; // Default to instant (for single-call positioning)
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|fff", const_cast<char**>(kwlist),
|
||||||
|
&entityObj, &distance, &height, &smoothing)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an Entity3D object
|
||||||
|
if (!PyObject_IsInstance(entityObj, (PyObject*)&mcrfpydef::PyEntity3DType)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Expected an Entity3D object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyEntity3DObject* entObj = (PyEntity3DObject*)entityObj;
|
||||||
|
if (!entObj->data) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid Entity3D object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->followEntity(entObj->data, distance, height, smoothing);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace mcrf
|
} // namespace mcrf
|
||||||
|
|
||||||
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
// Methods array - outside namespace but PyObjectType still in scope via typedef
|
||||||
|
|
@ -2275,5 +2389,23 @@ PyMethodDef Viewport3D_methods[] = {
|
||||||
"Get the number of billboards.\n\n"
|
"Get the number of billboards.\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" Number of billboards in the viewport"},
|
" Number of billboards in the viewport"},
|
||||||
|
|
||||||
|
// Camera & Input methods (Milestone 8)
|
||||||
|
{"screen_to_world", (PyCFunction)mcrf::Viewport3D_screen_to_world, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"screen_to_world(x, y) -> tuple or None\n\n"
|
||||||
|
"Convert screen coordinates to world position via ray casting.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" x: Screen X coordinate relative to viewport\n"
|
||||||
|
" y: Screen Y coordinate relative to viewport\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" (x, y, z) world position tuple, or None if no intersection with ground plane"},
|
||||||
|
{"follow", (PyCFunction)mcrf::Viewport3D_follow, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"follow(entity, distance=10, height=5, smoothing=1.0)\n\n"
|
||||||
|
"Position camera to follow an entity.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" entity: Entity3D to follow\n"
|
||||||
|
" distance: Distance behind entity\n"
|
||||||
|
" height: Camera height above entity\n"
|
||||||
|
" smoothing: Interpolation factor (0-1). 1 = instant, lower = smoother"},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,19 @@ public:
|
||||||
// Camera orbit helper for demos
|
// Camera orbit helper for demos
|
||||||
void orbitCamera(float angle, float distance, float height);
|
void orbitCamera(float angle, float distance, float height);
|
||||||
|
|
||||||
|
/// Convert screen coordinates to world position via ray casting
|
||||||
|
/// @param screenX X position relative to viewport
|
||||||
|
/// @param screenY Y position relative to viewport
|
||||||
|
/// @return World position on Y=0 plane, or (-1,-1,-1) if no intersection
|
||||||
|
vec3 screenToWorld(float screenX, float screenY);
|
||||||
|
|
||||||
|
/// Position camera to follow an entity
|
||||||
|
/// @param entity Entity to follow
|
||||||
|
/// @param distance Distance behind entity
|
||||||
|
/// @param height Height above entity
|
||||||
|
/// @param smoothing Interpolation factor (0-1, where 1 = instant)
|
||||||
|
void followEntity(std::shared_ptr<Entity3D> entity, float distance, float height, float smoothing = 1.0f);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Mesh Layer Management
|
// Mesh Layer Management
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
462
tests/demo/screens/integration_demo.py
Normal file
462
tests/demo/screens/integration_demo.py
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
# integration_demo.py - Milestone 8 Integration Demo
|
||||||
|
# Showcases all 3D features: terrain, entities, pathfinding, FOV, billboards, UI, input
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
DEMO_NAME = "3D Integration Demo"
|
||||||
|
DEMO_DESCRIPTION = """Complete 3D demo with terrain, player, NPC, FOV, and UI overlay.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
Arrow keys: Move player
|
||||||
|
Click: Move to clicked position
|
||||||
|
ESC: Quit
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the main scene
|
||||||
|
scene = mcrfpy.Scene("integration_demo")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
GRID_WIDTH = 32
|
||||||
|
GRID_DEPTH = 32
|
||||||
|
CELL_SIZE = 1.0
|
||||||
|
TERRAIN_Y_SCALE = 3.0
|
||||||
|
FOV_RADIUS = 10
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 3D Viewport
|
||||||
|
# =============================================================================
|
||||||
|
viewport = mcrfpy.Viewport3D(
|
||||||
|
pos=(10, 10),
|
||||||
|
size=(700, 550),
|
||||||
|
render_resolution=(350, 275),
|
||||||
|
fov=60.0,
|
||||||
|
camera_pos=(16.0, 15.0, 25.0),
|
||||||
|
camera_target=(16.0, 0.0, 16.0),
|
||||||
|
bg_color=mcrfpy.Color(40, 60, 100)
|
||||||
|
)
|
||||||
|
viewport.enable_fog = True
|
||||||
|
viewport.fog_near = 10.0
|
||||||
|
viewport.fog_far = 40.0
|
||||||
|
viewport.fog_color = mcrfpy.Color(40, 60, 100)
|
||||||
|
scene.children.append(viewport)
|
||||||
|
|
||||||
|
# Set up navigation grid
|
||||||
|
viewport.set_grid_size(GRID_WIDTH, GRID_DEPTH)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Terrain Generation
|
||||||
|
# =============================================================================
|
||||||
|
print("Generating terrain...")
|
||||||
|
|
||||||
|
# Create heightmap with hills
|
||||||
|
hm = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
hm.mid_point_displacement(roughness=0.5)
|
||||||
|
hm.normalize(0.0, 1.0)
|
||||||
|
|
||||||
|
# Build terrain mesh
|
||||||
|
viewport.build_terrain(
|
||||||
|
layer_name="terrain",
|
||||||
|
heightmap=hm,
|
||||||
|
y_scale=TERRAIN_Y_SCALE,
|
||||||
|
cell_size=CELL_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply heightmap to navigation grid
|
||||||
|
viewport.apply_heightmap(hm, TERRAIN_Y_SCALE)
|
||||||
|
|
||||||
|
# Mark steep slopes and water as unwalkable
|
||||||
|
viewport.apply_threshold(hm, 0.0, 0.12, False) # Low areas = water (unwalkable)
|
||||||
|
viewport.set_slope_cost(0.4, 2.0)
|
||||||
|
|
||||||
|
# Create base terrain colors (green/brown based on height)
|
||||||
|
r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
|
||||||
|
# Storage for base colors (for FOV dimming)
|
||||||
|
base_colors = []
|
||||||
|
|
||||||
|
for z in range(GRID_DEPTH):
|
||||||
|
row = []
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
h = hm[x, z]
|
||||||
|
if h < 0.12: # Water
|
||||||
|
r, g, b = 0.1, 0.2, 0.4
|
||||||
|
elif h < 0.25: # Sand/beach
|
||||||
|
r, g, b = 0.6, 0.5, 0.3
|
||||||
|
elif h < 0.6: # Grass
|
||||||
|
r, g, b = 0.2 + random.random() * 0.1, 0.4 + random.random() * 0.15, 0.15
|
||||||
|
else: # Rock/mountain
|
||||||
|
r, g, b = 0.4, 0.35, 0.3
|
||||||
|
|
||||||
|
r_map[x, z] = r
|
||||||
|
g_map[x, z] = g
|
||||||
|
b_map[x, z] = b
|
||||||
|
row.append((r, g, b))
|
||||||
|
base_colors.append(row)
|
||||||
|
|
||||||
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Find walkable starting positions
|
||||||
|
# =============================================================================
|
||||||
|
def find_walkable_pos():
|
||||||
|
"""Find a random walkable position"""
|
||||||
|
for _ in range(100):
|
||||||
|
x = random.randint(2, GRID_WIDTH - 3)
|
||||||
|
z = random.randint(2, GRID_DEPTH - 3)
|
||||||
|
cell = viewport.at(x, z)
|
||||||
|
if cell.walkable:
|
||||||
|
return (x, z)
|
||||||
|
return (GRID_WIDTH // 2, GRID_DEPTH // 2)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Player Entity
|
||||||
|
# =============================================================================
|
||||||
|
player_start = find_walkable_pos()
|
||||||
|
player = mcrfpy.Entity3D(pos=player_start, scale=0.8, color=mcrfpy.Color(50, 150, 255))
|
||||||
|
viewport.entities.append(player)
|
||||||
|
print(f"Player at {player_start}")
|
||||||
|
|
||||||
|
# Track discovered cells
|
||||||
|
discovered = set()
|
||||||
|
discovered.add(player_start)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# NPC Entity with Patrol AI
|
||||||
|
# =============================================================================
|
||||||
|
npc_start = find_walkable_pos()
|
||||||
|
while abs(npc_start[0] - player_start[0]) < 5 and abs(npc_start[1] - player_start[1]) < 5:
|
||||||
|
npc_start = find_walkable_pos()
|
||||||
|
|
||||||
|
npc = mcrfpy.Entity3D(pos=npc_start, scale=0.7, color=mcrfpy.Color(255, 100, 100))
|
||||||
|
viewport.entities.append(npc)
|
||||||
|
print(f"NPC at {npc_start}")
|
||||||
|
|
||||||
|
# NPC patrol system
|
||||||
|
class NPCController:
|
||||||
|
def __init__(self, entity, waypoints):
|
||||||
|
self.entity = entity
|
||||||
|
self.waypoints = waypoints
|
||||||
|
self.current_wp = 0
|
||||||
|
self.path = []
|
||||||
|
self.path_index = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
if self.entity.is_moving:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we have a path, follow it
|
||||||
|
if self.path_index < len(self.path):
|
||||||
|
next_pos = self.path[self.path_index]
|
||||||
|
self.entity.pos = next_pos
|
||||||
|
self.path_index += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reached waypoint, go to next
|
||||||
|
self.current_wp = (self.current_wp + 1) % len(self.waypoints)
|
||||||
|
target = self.waypoints[self.current_wp]
|
||||||
|
|
||||||
|
# Compute path to next waypoint
|
||||||
|
self.path = self.entity.path_to(target[0], target[1])
|
||||||
|
self.path_index = 0
|
||||||
|
|
||||||
|
# Create patrol waypoints
|
||||||
|
npc_waypoints = []
|
||||||
|
for _ in range(4):
|
||||||
|
wp = find_walkable_pos()
|
||||||
|
npc_waypoints.append(wp)
|
||||||
|
|
||||||
|
npc_controller = NPCController(npc, npc_waypoints)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FOV Visualization
|
||||||
|
# =============================================================================
|
||||||
|
def update_fov_colors():
|
||||||
|
"""Update terrain colors based on FOV"""
|
||||||
|
# Compute FOV from player position
|
||||||
|
visible_cells = viewport.compute_fov((player.pos[0], player.pos[1]), FOV_RADIUS)
|
||||||
|
visible_set = set((c[0], c[1]) for c in visible_cells)
|
||||||
|
|
||||||
|
# Update discovered
|
||||||
|
discovered.update(visible_set)
|
||||||
|
|
||||||
|
# Update terrain colors
|
||||||
|
r_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
g_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
b_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_DEPTH))
|
||||||
|
|
||||||
|
for z in range(GRID_DEPTH):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
base_r, base_g, base_b = base_colors[z][x]
|
||||||
|
|
||||||
|
if (x, z) in visible_set:
|
||||||
|
# Fully visible
|
||||||
|
r_map[x, z] = base_r
|
||||||
|
g_map[x, z] = base_g
|
||||||
|
b_map[x, z] = base_b
|
||||||
|
elif (x, z) in discovered:
|
||||||
|
# Discovered but not visible - dim
|
||||||
|
r_map[x, z] = base_r * 0.4
|
||||||
|
g_map[x, z] = base_g * 0.4
|
||||||
|
b_map[x, z] = base_b * 0.4
|
||||||
|
else:
|
||||||
|
# Never seen - very dark
|
||||||
|
r_map[x, z] = base_r * 0.1
|
||||||
|
g_map[x, z] = base_g * 0.1
|
||||||
|
b_map[x, z] = base_b * 0.1
|
||||||
|
|
||||||
|
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||||
|
|
||||||
|
# Initial FOV update
|
||||||
|
update_fov_colors()
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI Overlay
|
||||||
|
# =============================================================================
|
||||||
|
ui_frame = mcrfpy.Frame(
|
||||||
|
pos=(720, 10),
|
||||||
|
size=(260, 200),
|
||||||
|
fill_color=mcrfpy.Color(20, 20, 30, 220),
|
||||||
|
outline_color=mcrfpy.Color(80, 80, 120),
|
||||||
|
outline=2.0
|
||||||
|
)
|
||||||
|
scene.children.append(ui_frame)
|
||||||
|
|
||||||
|
title_label = mcrfpy.Caption(text="3D Integration Demo", pos=(740, 20))
|
||||||
|
title_label.fill_color = mcrfpy.Color(255, 255, 150)
|
||||||
|
scene.children.append(title_label)
|
||||||
|
|
||||||
|
status_label = mcrfpy.Caption(text="Status: Idle", pos=(740, 50))
|
||||||
|
status_label.fill_color = mcrfpy.Color(150, 255, 150)
|
||||||
|
scene.children.append(status_label)
|
||||||
|
|
||||||
|
player_pos_label = mcrfpy.Caption(text="Player: (0, 0)", pos=(740, 75))
|
||||||
|
player_pos_label.fill_color = mcrfpy.Color(100, 200, 255)
|
||||||
|
scene.children.append(player_pos_label)
|
||||||
|
|
||||||
|
npc_pos_label = mcrfpy.Caption(text="NPC: (0, 0)", pos=(740, 100))
|
||||||
|
npc_pos_label.fill_color = mcrfpy.Color(255, 150, 150)
|
||||||
|
scene.children.append(npc_pos_label)
|
||||||
|
|
||||||
|
fps_label = mcrfpy.Caption(text="FPS: --", pos=(740, 125))
|
||||||
|
fps_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
scene.children.append(fps_label)
|
||||||
|
|
||||||
|
discovered_label = mcrfpy.Caption(text="Discovered: 0", pos=(740, 150))
|
||||||
|
discovered_label.fill_color = mcrfpy.Color(180, 180, 100)
|
||||||
|
scene.children.append(discovered_label)
|
||||||
|
|
||||||
|
# Controls info
|
||||||
|
controls_frame = mcrfpy.Frame(
|
||||||
|
pos=(720, 220),
|
||||||
|
size=(260, 120),
|
||||||
|
fill_color=mcrfpy.Color(20, 20, 30, 200),
|
||||||
|
outline_color=mcrfpy.Color(60, 60, 80),
|
||||||
|
outline=1.0
|
||||||
|
)
|
||||||
|
scene.children.append(controls_frame)
|
||||||
|
|
||||||
|
ctrl_title = mcrfpy.Caption(text="Controls:", pos=(740, 230))
|
||||||
|
ctrl_title.fill_color = mcrfpy.Color(200, 200, 100)
|
||||||
|
scene.children.append(ctrl_title)
|
||||||
|
|
||||||
|
ctrl_lines = [
|
||||||
|
"Arrow keys: Move",
|
||||||
|
"Click: Pathfind",
|
||||||
|
"F: Toggle follow cam",
|
||||||
|
"ESC: Quit"
|
||||||
|
]
|
||||||
|
for i, line in enumerate(ctrl_lines):
|
||||||
|
cap = mcrfpy.Caption(text=line, pos=(740, 255 + i * 20))
|
||||||
|
cap.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
scene.children.append(cap)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Game State
|
||||||
|
# =============================================================================
|
||||||
|
follow_camera = True
|
||||||
|
frame_count = 0
|
||||||
|
fps_update_time = 0
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Update Function
|
||||||
|
# =============================================================================
|
||||||
|
def game_update(timer, runtime):
|
||||||
|
global frame_count, fps_update_time
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Calculate FPS
|
||||||
|
frame_count += 1
|
||||||
|
if runtime - fps_update_time >= 1000: # Update FPS every second
|
||||||
|
fps = frame_count
|
||||||
|
fps_label.text = f"FPS: {fps}"
|
||||||
|
frame_count = 0
|
||||||
|
fps_update_time = runtime
|
||||||
|
|
||||||
|
# Update NPC patrol
|
||||||
|
npc_controller.update()
|
||||||
|
|
||||||
|
# Update UI labels
|
||||||
|
px, pz = player.pos
|
||||||
|
player_pos_label.text = f"Player: ({px}, {pz})"
|
||||||
|
nx, nz = npc.pos
|
||||||
|
npc_pos_label.text = f"NPC: ({nx}, {nz})"
|
||||||
|
discovered_label.text = f"Discovered: {len(discovered)}"
|
||||||
|
|
||||||
|
# Camera follow
|
||||||
|
if follow_camera:
|
||||||
|
viewport.follow(player, distance=12.0, height=8.0, smoothing=0.1)
|
||||||
|
|
||||||
|
# Update status based on player state
|
||||||
|
if player.is_moving:
|
||||||
|
status_label.text = "Status: Moving"
|
||||||
|
status_label.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
else:
|
||||||
|
status_label.text = "Status: Idle"
|
||||||
|
status_label.fill_color = mcrfpy.Color(150, 255, 150)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Update error: {e}")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Handling
|
||||||
|
# =============================================================================
|
||||||
|
def try_move_player(dx, dz):
|
||||||
|
"""Try to move player in direction"""
|
||||||
|
new_x = player.pos[0] + dx
|
||||||
|
new_z = player.pos[1] + dz
|
||||||
|
|
||||||
|
if not viewport.is_in_fov(new_x, new_z):
|
||||||
|
# Allow moving into discovered cells even if not currently visible
|
||||||
|
if (new_x, new_z) not in discovered:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if new_x < 0 or new_x >= GRID_WIDTH or new_z < 0 or new_z >= GRID_DEPTH:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cell = viewport.at(new_x, new_z)
|
||||||
|
if not cell.walkable:
|
||||||
|
return False
|
||||||
|
|
||||||
|
player.pos = (new_x, new_z)
|
||||||
|
update_fov_colors()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def on_key(key, state):
|
||||||
|
global follow_camera
|
||||||
|
|
||||||
|
if state != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if player.is_moving:
|
||||||
|
return # Don't accept input while moving
|
||||||
|
|
||||||
|
dx, dz = 0, 0
|
||||||
|
if key == mcrfpy.Key.UP:
|
||||||
|
dz = -1
|
||||||
|
elif key == mcrfpy.Key.DOWN:
|
||||||
|
dz = 1
|
||||||
|
elif key == mcrfpy.Key.LEFT:
|
||||||
|
dx = -1
|
||||||
|
elif key == mcrfpy.Key.RIGHT:
|
||||||
|
dx = 1
|
||||||
|
elif key == mcrfpy.Key.F:
|
||||||
|
follow_camera = not follow_camera
|
||||||
|
status_label.text = f"Camera: {'Follow' if follow_camera else 'Free'}"
|
||||||
|
return
|
||||||
|
elif key == mcrfpy.Key.ESCAPE:
|
||||||
|
mcrfpy.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
if dx != 0 or dz != 0:
|
||||||
|
try_move_player(dx, dz)
|
||||||
|
|
||||||
|
# Click-to-move handling
|
||||||
|
def on_click(pos, button, state):
|
||||||
|
if button != mcrfpy.MouseButton.LEFT or state != mcrfpy.InputState.PRESSED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if player.is_moving:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert click position to viewport-relative coordinates
|
||||||
|
vp_x = pos.x - viewport.x
|
||||||
|
vp_y = pos.y - viewport.y
|
||||||
|
|
||||||
|
# Check if click is within viewport
|
||||||
|
if vp_x < 0 or vp_x >= viewport.w or vp_y < 0 or vp_y >= viewport.h:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert to world position
|
||||||
|
world_pos = viewport.screen_to_world(vp_x, vp_y)
|
||||||
|
if world_pos is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Convert to grid position
|
||||||
|
grid_x = int(world_pos[0] / CELL_SIZE)
|
||||||
|
grid_z = int(world_pos[2] / CELL_SIZE)
|
||||||
|
|
||||||
|
# Validate grid position
|
||||||
|
if grid_x < 0 or grid_x >= GRID_WIDTH or grid_z < 0 or grid_z >= GRID_DEPTH:
|
||||||
|
return
|
||||||
|
|
||||||
|
cell = viewport.at(grid_x, grid_z)
|
||||||
|
if not cell.walkable:
|
||||||
|
status_label.text = "Status: Can't walk there!"
|
||||||
|
status_label.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find path
|
||||||
|
path = player.path_to(grid_x, grid_z)
|
||||||
|
if not path:
|
||||||
|
status_label.text = "Status: No path!"
|
||||||
|
status_label.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Follow path (limited to FOV_RADIUS steps)
|
||||||
|
limited_path = path[:FOV_RADIUS]
|
||||||
|
player.follow_path(limited_path)
|
||||||
|
status_label.text = f"Status: Moving ({len(limited_path)} steps)"
|
||||||
|
status_label.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
|
||||||
|
# Schedule FOV update after movement completes
|
||||||
|
fov_update_timer = None
|
||||||
|
|
||||||
|
def update_fov_after_move(*args):
|
||||||
|
# Accept any number of args since timer may pass (runtime) or (timer, runtime)
|
||||||
|
nonlocal fov_update_timer
|
||||||
|
if not player.is_moving:
|
||||||
|
update_fov_colors()
|
||||||
|
if fov_update_timer:
|
||||||
|
fov_update_timer.stop()
|
||||||
|
|
||||||
|
fov_update_timer = mcrfpy.Timer("fov_update", update_fov_after_move, 100)
|
||||||
|
|
||||||
|
scene.on_key = on_key
|
||||||
|
viewport.on_click = on_click
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Start Game
|
||||||
|
# =============================================================================
|
||||||
|
timer = mcrfpy.Timer("game_update", game_update, 16) # ~60 FPS
|
||||||
|
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("3D Integration Demo Loaded!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" Terrain: {GRID_WIDTH}x{GRID_DEPTH} cells")
|
||||||
|
print(f" Player starts at: {player_start}")
|
||||||
|
print(f" NPC patrolling {len(npc_waypoints)} waypoints")
|
||||||
|
print()
|
||||||
|
print("Controls:")
|
||||||
|
print(" Arrow keys: Move player")
|
||||||
|
print(" Click: Pathfind to location")
|
||||||
|
print(" F: Toggle camera follow")
|
||||||
|
print(" ESC: Quit")
|
||||||
|
print("=" * 60)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue