|
|
""" |
|
|
COMBAT VISUALIZER - 3D ASTEROID DEFENSE |
|
|
======================================== |
|
|
|
|
|
DOPE ASS visualization of the quine lattice defending against asteroids. |
|
|
|
|
|
Features: |
|
|
- Full 3D Panda3D rendering |
|
|
- Spinning blade-tethers with visible flat surfaces |
|
|
- Asteroids/missiles spawning from all directions |
|
|
- Particle explosions on intercepts |
|
|
- DreamerV3 brain controlling the lattice |
|
|
- Score, wave count, damage HUD |
|
|
- Camera follows the action |
|
|
|
|
|
This is what happens when a DreamerV3 quine goes to war. |
|
|
""" |
|
|
|
|
|
import numpy as np |
|
|
import sys |
|
|
import os |
|
|
from typing import Dict, List, Optional |
|
|
from dataclasses import dataclass |
|
|
import time |
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
|
|
|
|
|
|
try: |
|
|
from direct.showbase.ShowBase import ShowBase |
|
|
from panda3d.core import ( |
|
|
Vec3, Vec4, Point3, Point2, |
|
|
LineSegs, NodePath, GeomNode, |
|
|
AmbientLight, DirectionalLight, PointLight, Spotlight, |
|
|
TextNode, CardMaker, Fog, |
|
|
GeomVertexFormat, GeomVertexData, GeomVertexWriter, |
|
|
Geom, GeomTriangles, GeomLines, |
|
|
CollisionTraverser, CollisionNode, CollisionSphere, |
|
|
CollisionHandlerQueue, BitMask32, |
|
|
TransparencyAttrib, ColorBlendAttrib, |
|
|
WindowProperties |
|
|
) |
|
|
from direct.task import Task |
|
|
from direct.particles.Particles import Particles |
|
|
from direct.particles.ParticleEffect import ParticleEffect |
|
|
from direct.gui.OnscreenText import OnscreenText |
|
|
from direct.gui.DirectGui import DirectFrame |
|
|
from direct.interval.IntervalGlobal import Sequence, Parallel, LerpPosInterval |
|
|
PANDA3D_AVAILABLE = True |
|
|
except ImportError as e: |
|
|
PANDA3D_AVAILABLE = False |
|
|
print(f"Panda3D not installed: {e}") |
|
|
print("Install with: pip install panda3d") |
|
|
|
|
|
|
|
|
def create_asteroid_geom(radius: float = 1.0, irregularity: float = 0.3) -> GeomNode: |
|
|
""" |
|
|
Create an irregular asteroid geometry (icosphere with noise). |
|
|
""" |
|
|
format = GeomVertexFormat.getV3n3c4() |
|
|
vdata = GeomVertexData("asteroid", format, Geom.UHStatic) |
|
|
|
|
|
vertex = GeomVertexWriter(vdata, "vertex") |
|
|
normal = GeomVertexWriter(vdata, "normal") |
|
|
color = GeomVertexWriter(vdata, "color") |
|
|
|
|
|
|
|
|
phi = (1 + np.sqrt(5)) / 2 |
|
|
vertices = [] |
|
|
|
|
|
base_verts = [ |
|
|
(-1, phi, 0), (1, phi, 0), (-1, -phi, 0), (1, -phi, 0), |
|
|
(0, -1, phi), (0, 1, phi), (0, -1, -phi), (0, 1, -phi), |
|
|
(phi, 0, -1), (phi, 0, 1), (-phi, 0, -1), (-phi, 0, 1) |
|
|
] |
|
|
|
|
|
for v in base_verts: |
|
|
|
|
|
n = np.array(v) |
|
|
n = n / np.linalg.norm(n) |
|
|
|
|
|
|
|
|
noise = 1.0 + irregularity * (np.random.random() - 0.5) * 2 |
|
|
v_pos = n * radius * noise |
|
|
|
|
|
vertex.addData3f(v_pos[0], v_pos[1], v_pos[2]) |
|
|
normal.addData3f(n[0], n[1], n[2]) |
|
|
|
|
|
|
|
|
r = 0.4 + 0.2 * np.random.random() |
|
|
g = 0.35 + 0.15 * np.random.random() |
|
|
b = 0.3 + 0.1 * np.random.random() |
|
|
color.addData4f(r, g, b, 1.0) |
|
|
vertices.append(v_pos) |
|
|
|
|
|
|
|
|
prim = GeomTriangles(Geom.UHStatic) |
|
|
faces = [ |
|
|
(0, 11, 5), (0, 5, 1), (0, 1, 7), (0, 7, 10), (0, 10, 11), |
|
|
(1, 5, 9), (5, 11, 4), (11, 10, 2), (10, 7, 6), (7, 1, 8), |
|
|
(3, 9, 4), (3, 4, 2), (3, 2, 6), (3, 6, 8), (3, 8, 9), |
|
|
(4, 9, 5), (2, 4, 11), (6, 2, 10), (8, 6, 7), (9, 8, 1) |
|
|
] |
|
|
|
|
|
for face in faces: |
|
|
prim.addVertices(face[0], face[1], face[2]) |
|
|
|
|
|
geom = Geom(vdata) |
|
|
geom.addPrimitive(prim) |
|
|
|
|
|
node = GeomNode("asteroid") |
|
|
node.addGeom(geom) |
|
|
|
|
|
return node |
|
|
|
|
|
|
|
|
def create_blade_geom(length: float = 5.0, chord: float = 0.5, twist: float = 0.2) -> GeomNode: |
|
|
""" |
|
|
Create a blade-tether geometry with visible flat surface. |
|
|
""" |
|
|
format = GeomVertexFormat.getV3n3c4() |
|
|
vdata = GeomVertexData("blade", format, Geom.UHStatic) |
|
|
|
|
|
vertex = GeomVertexWriter(vdata, "vertex") |
|
|
normal = GeomVertexWriter(vdata, "normal") |
|
|
color = GeomVertexWriter(vdata, "color") |
|
|
|
|
|
n_segments = 8 |
|
|
|
|
|
|
|
|
vertices = [] |
|
|
for i in range(n_segments + 1): |
|
|
t = i / n_segments |
|
|
x = t * length |
|
|
|
|
|
|
|
|
twist_angle = t * twist * np.pi |
|
|
|
|
|
|
|
|
y_lead = chord / 2 * np.cos(twist_angle) |
|
|
z_lead = chord / 2 * np.sin(twist_angle) |
|
|
y_trail = -chord / 2 * np.cos(twist_angle) |
|
|
z_trail = -chord / 2 * np.sin(twist_angle) |
|
|
|
|
|
|
|
|
vertex.addData3f(x, y_lead, z_lead) |
|
|
normal.addData3f(0, np.sin(twist_angle), np.cos(twist_angle)) |
|
|
color.addData4f(0.7, 0.8, 0.9, 0.9) |
|
|
|
|
|
|
|
|
vertex.addData3f(x, y_trail, z_trail) |
|
|
normal.addData3f(0, -np.sin(twist_angle), -np.cos(twist_angle)) |
|
|
color.addData4f(0.6, 0.7, 0.8, 0.9) |
|
|
|
|
|
|
|
|
prim = GeomTriangles(Geom.UHStatic) |
|
|
for i in range(n_segments): |
|
|
base = i * 2 |
|
|
|
|
|
prim.addVertices(base, base + 1, base + 2) |
|
|
prim.addVertices(base + 1, base + 3, base + 2) |
|
|
|
|
|
geom = Geom(vdata) |
|
|
geom.addPrimitive(prim) |
|
|
|
|
|
node = GeomNode("blade") |
|
|
node.addGeom(geom) |
|
|
|
|
|
return node |
|
|
|
|
|
|
|
|
def create_explosion_particles() -> List[np.ndarray]: |
|
|
"""Generate explosion particle positions""" |
|
|
n_particles = 50 |
|
|
particles = [] |
|
|
for _ in range(n_particles): |
|
|
direction = np.random.randn(3) |
|
|
direction = direction / np.linalg.norm(direction) |
|
|
speed = np.random.uniform(5, 20) |
|
|
particles.append(direction * speed) |
|
|
return particles |
|
|
|
|
|
|
|
|
if PANDA3D_AVAILABLE: |
|
|
|
|
|
class CombatVisualizer(ShowBase): |
|
|
""" |
|
|
Full 3D combat visualization with asteroids and blade-tethers. |
|
|
|
|
|
Controls: |
|
|
WASD - Move camera |
|
|
Mouse - Look around |
|
|
SPACE - Spawn threat wave |
|
|
B - Trigger defensive burst |
|
|
R - Reset camera |
|
|
1-4 - Camera presets |
|
|
ESC - Exit |
|
|
""" |
|
|
|
|
|
def __init__(self, arena=None): |
|
|
ShowBase.__init__(self) |
|
|
|
|
|
|
|
|
props = WindowProperties() |
|
|
props.setTitle("KAPS COMBAT ARENA - ASTEROID DEFENSE") |
|
|
props.setSize(1920, 1080) |
|
|
self.win.requestProperties(props) |
|
|
|
|
|
self.arena = arena |
|
|
|
|
|
|
|
|
self.disableMouse() |
|
|
self.camera_distance = 300 |
|
|
self.camera_angle = 0 |
|
|
self.camera_pitch = 30 |
|
|
self._setup_camera() |
|
|
|
|
|
|
|
|
self._setup_lighting() |
|
|
|
|
|
|
|
|
self._setup_fog() |
|
|
|
|
|
|
|
|
self.lattice_nodes: Dict[str, NodePath] = {} |
|
|
self.blade_nodes: Dict[str, NodePath] = {} |
|
|
self.threat_nodes: Dict[str, NodePath] = {} |
|
|
self.explosion_nodes: List[NodePath] = [] |
|
|
|
|
|
|
|
|
self.hud_elements = {} |
|
|
|
|
|
|
|
|
self._create_skybox() |
|
|
self._create_lattice() |
|
|
self._create_hud() |
|
|
|
|
|
|
|
|
self._setup_input() |
|
|
|
|
|
|
|
|
self.taskMgr.add(self._update_task, "main_update") |
|
|
self.taskMgr.add(self._camera_orbit_task, "camera_orbit") |
|
|
|
|
|
|
|
|
self.last_update_time = time.time() |
|
|
self.frame_count = 0 |
|
|
|
|
|
|
|
|
self.orbit_speed = 5 |
|
|
self.auto_orbit = True |
|
|
|
|
|
print("=" * 60) |
|
|
print("COMBAT VISUALIZER INITIALIZED") |
|
|
print("=" * 60) |
|
|
print("Controls:") |
|
|
print(" SPACE - Spawn threat wave") |
|
|
print(" B - Defensive burst") |
|
|
print(" R - Reset camera") |
|
|
print(" 1-4 - Camera presets") |
|
|
print(" O - Toggle auto-orbit") |
|
|
print(" ESC - Exit") |
|
|
print("=" * 60) |
|
|
|
|
|
def _setup_camera(self): |
|
|
"""Initialize camera position""" |
|
|
rad_angle = np.radians(self.camera_angle) |
|
|
rad_pitch = np.radians(self.camera_pitch) |
|
|
|
|
|
x = self.camera_distance * np.cos(rad_pitch) * np.cos(rad_angle) |
|
|
y = self.camera_distance * np.cos(rad_pitch) * np.sin(rad_angle) |
|
|
z = self.camera_distance * np.sin(rad_pitch) |
|
|
|
|
|
self.camera.setPos(x, y, z) |
|
|
self.camera.lookAt(0, 0, 0) |
|
|
|
|
|
def _setup_lighting(self): |
|
|
"""Create dramatic lighting""" |
|
|
|
|
|
ambient = AmbientLight("ambient") |
|
|
ambient.setColor(Vec4(0.15, 0.15, 0.2, 1)) |
|
|
ambient_np = self.render.attachNewNode(ambient) |
|
|
self.render.setLight(ambient_np) |
|
|
|
|
|
|
|
|
sun = DirectionalLight("sun") |
|
|
sun.setColor(Vec4(1.0, 0.95, 0.9, 1)) |
|
|
sun_np = self.render.attachNewNode(sun) |
|
|
sun_np.setHpr(45, -60, 0) |
|
|
self.render.setLight(sun_np) |
|
|
|
|
|
|
|
|
fill = DirectionalLight("fill") |
|
|
fill.setColor(Vec4(0.3, 0.4, 0.6, 1)) |
|
|
fill_np = self.render.attachNewNode(fill) |
|
|
fill_np.setHpr(-135, -30, 0) |
|
|
self.render.setLight(fill_np) |
|
|
|
|
|
|
|
|
self.warning_light = PointLight("warning") |
|
|
self.warning_light.setColor(Vec4(0, 0, 0, 1)) |
|
|
self.warning_light.setAttenuation((1, 0, 0.0001)) |
|
|
self.warning_light_np = self.render.attachNewNode(self.warning_light) |
|
|
self.warning_light_np.setPos(0, 0, 0) |
|
|
self.render.setLight(self.warning_light_np) |
|
|
|
|
|
def _setup_fog(self): |
|
|
"""Add distance fog""" |
|
|
fog = Fog("arena_fog") |
|
|
fog.setColor(0.05, 0.05, 0.1) |
|
|
fog.setExpDensity(0.001) |
|
|
self.render.setFog(fog) |
|
|
|
|
|
def _create_skybox(self): |
|
|
"""Create sky background with ocean below""" |
|
|
|
|
|
self.setBackgroundColor(0.4, 0.6, 0.9) |
|
|
|
|
|
|
|
|
lines = LineSegs() |
|
|
lines.setThickness(1.0) |
|
|
|
|
|
for _ in range(200): |
|
|
|
|
|
theta = np.random.uniform(0, 2 * np.pi) |
|
|
phi = np.random.uniform(0.1, np.pi/2) |
|
|
r = 2000 |
|
|
|
|
|
x = r * np.cos(phi) * np.cos(theta) |
|
|
y = r * np.cos(phi) * np.sin(theta) |
|
|
z = r * np.sin(phi) |
|
|
|
|
|
|
|
|
brightness = np.random.uniform(0.1, 0.4) |
|
|
lines.setColor(brightness, brightness, brightness * 1.1, 1) |
|
|
|
|
|
lines.moveTo(Point3(x, y, z)) |
|
|
lines.drawTo(Point3(x + 0.5, y + 0.5, z + 0.5)) |
|
|
|
|
|
stars = self.render.attachNewNode(lines.create()) |
|
|
|
|
|
|
|
|
self._create_ocean() |
|
|
|
|
|
def _create_ocean(self): |
|
|
"""Create animated ocean surface with waves""" |
|
|
ocean_size = 3000 |
|
|
ocean_depth = -150 |
|
|
wave_resolution = 40 |
|
|
|
|
|
|
|
|
format = GeomVertexFormat.getV3n3c4() |
|
|
vdata = GeomVertexData("ocean", format, Geom.UHDynamic) |
|
|
|
|
|
vertex = GeomVertexWriter(vdata, "vertex") |
|
|
normal = GeomVertexWriter(vdata, "normal") |
|
|
color = GeomVertexWriter(vdata, "color") |
|
|
|
|
|
|
|
|
self.ocean_vertices = [] |
|
|
|
|
|
|
|
|
for i in range(wave_resolution + 1): |
|
|
for j in range(wave_resolution + 1): |
|
|
|
|
|
x = (i / wave_resolution - 0.5) * ocean_size |
|
|
y = (j / wave_resolution - 0.5) * ocean_size |
|
|
|
|
|
|
|
|
wave1 = np.sin(x * 0.02) * 5 |
|
|
wave2 = np.sin(y * 0.015) * 4 |
|
|
wave3 = np.sin((x + y) * 0.01) * 3 |
|
|
z = ocean_depth + wave1 + wave2 + wave3 |
|
|
|
|
|
vertex.addData3f(x, y, z) |
|
|
normal.addData3f(0, 0, 1) |
|
|
|
|
|
|
|
|
dist = np.sqrt(x*x + y*y) / ocean_size |
|
|
r = 0.02 + 0.05 * (1 - dist) |
|
|
g = 0.15 + 0.2 * (1 - dist) |
|
|
b = 0.4 + 0.35 * (1 - dist) |
|
|
alpha = 0.85 |
|
|
color.addData4f(r, g, b, alpha) |
|
|
|
|
|
self.ocean_vertices.append([x, y, z]) |
|
|
|
|
|
|
|
|
prim = GeomTriangles(Geom.UHStatic) |
|
|
for i in range(wave_resolution): |
|
|
for j in range(wave_resolution): |
|
|
|
|
|
v0 = i * (wave_resolution + 1) + j |
|
|
v1 = v0 + 1 |
|
|
v2 = v0 + (wave_resolution + 1) |
|
|
v3 = v2 + 1 |
|
|
|
|
|
|
|
|
prim.addVertices(v0, v2, v1) |
|
|
prim.addVertices(v1, v2, v3) |
|
|
|
|
|
geom = Geom(vdata) |
|
|
geom.addPrimitive(prim) |
|
|
|
|
|
ocean_node = GeomNode("ocean") |
|
|
ocean_node.addGeom(geom) |
|
|
|
|
|
self.ocean = self.render.attachNewNode(ocean_node) |
|
|
self.ocean.setTransparency(TransparencyAttrib.MAlpha) |
|
|
|
|
|
|
|
|
self._create_wave_foam() |
|
|
|
|
|
|
|
|
self.ocean_vdata = vdata |
|
|
self.wave_time = 0 |
|
|
self.wave_resolution = wave_resolution |
|
|
|
|
|
def _create_wave_foam(self): |
|
|
"""Create foam/whitecap effect on wave peaks""" |
|
|
foam_lines = LineSegs() |
|
|
foam_lines.setThickness(2.0) |
|
|
foam_lines.setColor(0.9, 0.95, 1.0, 0.6) |
|
|
|
|
|
|
|
|
for _ in range(100): |
|
|
x = np.random.uniform(-1000, 1000) |
|
|
y = np.random.uniform(-1000, 1000) |
|
|
z = -145 + np.random.uniform(0, 5) |
|
|
length = np.random.uniform(10, 40) |
|
|
angle = np.random.uniform(0, 2 * np.pi) |
|
|
|
|
|
dx = length * np.cos(angle) |
|
|
dy = length * np.sin(angle) |
|
|
|
|
|
foam_lines.moveTo(Point3(x, y, z)) |
|
|
foam_lines.drawTo(Point3(x + dx, y + dy, z + np.random.uniform(-1, 1))) |
|
|
|
|
|
self.foam = self.render.attachNewNode(foam_lines.create()) |
|
|
|
|
|
def _create_lattice(self): |
|
|
"""Create visual representation of the quine lattice""" |
|
|
if self.arena is None: |
|
|
return |
|
|
|
|
|
all_nodes = self.arena.lattice.get_all_nodes() |
|
|
|
|
|
for node in all_nodes: |
|
|
|
|
|
sphere = self.loader.loadModel("models/misc/sphere") |
|
|
sphere.reparentTo(self.render) |
|
|
|
|
|
|
|
|
if node.role.name == 'BUZZARD': |
|
|
sphere.setScale(5) |
|
|
sphere.setColor(0.2, 0.4, 0.9, 1) |
|
|
elif node.role.name == 'VERTEBRA': |
|
|
sphere.setScale(3) |
|
|
sphere.setColor(0.3, 0.8, 0.3, 1) |
|
|
elif node.role.name == 'GYRO_ARM': |
|
|
sphere.setScale(1.5) |
|
|
sphere.setColor(0.9, 0.7, 0.2, 1) |
|
|
else: |
|
|
sphere.setScale(1) |
|
|
sphere.setColor(0.6, 0.6, 0.6, 1) |
|
|
|
|
|
pos = node.position |
|
|
sphere.setPos(pos[0], pos[1], pos[2]) |
|
|
|
|
|
self.lattice_nodes[node.node_id] = sphere |
|
|
|
|
|
|
|
|
if node.parent is not None: |
|
|
blade = self._create_blade_visual(node, node.parent) |
|
|
self.blade_nodes[node.node_id] = blade |
|
|
|
|
|
def _create_blade_visual(self, child_node, parent_node) -> NodePath: |
|
|
"""Create a blade tether visual between two nodes""" |
|
|
blade_geom = create_blade_geom(length=8.0, chord=0.8, twist=0.3) |
|
|
blade = self.render.attachNewNode(blade_geom) |
|
|
blade.setTransparency(TransparencyAttrib.MAlpha) |
|
|
|
|
|
|
|
|
pos = parent_node.position |
|
|
blade.setPos(pos[0], pos[1], pos[2]) |
|
|
|
|
|
|
|
|
child_pos = child_node.position |
|
|
direction = child_pos - pos |
|
|
if np.linalg.norm(direction) > 0.01: |
|
|
direction = direction / np.linalg.norm(direction) |
|
|
|
|
|
blade.lookAt(Point3(child_pos[0], child_pos[1], child_pos[2])) |
|
|
|
|
|
return blade |
|
|
|
|
|
def _create_threat_visual(self, threat) -> NodePath: |
|
|
"""Create visual for an incoming threat""" |
|
|
|
|
|
asteroid_geom = create_asteroid_geom( |
|
|
radius=threat.radius, |
|
|
irregularity=0.4 |
|
|
) |
|
|
node = self.render.attachNewNode(asteroid_geom) |
|
|
|
|
|
|
|
|
if threat.threat_type.name == 'MISSILE': |
|
|
node.setColor(0.9, 0.2, 0.2, 1) |
|
|
elif threat.threat_type.name == 'ENEMY_DRONE': |
|
|
node.setColor(0.8, 0.4, 0.9, 1) |
|
|
else: |
|
|
node.setColor(0.6, 0.5, 0.4, 1) |
|
|
|
|
|
|
|
|
pos = threat.position |
|
|
node.setPos(pos[0], pos[1], pos[2]) |
|
|
|
|
|
return node |
|
|
|
|
|
def _create_explosion(self, position: np.ndarray, scale: float = 1.0): |
|
|
"""Create explosion effect at position""" |
|
|
|
|
|
for _ in range(10): |
|
|
lines = LineSegs() |
|
|
lines.setThickness(3.0) |
|
|
|
|
|
|
|
|
direction = np.random.randn(3) |
|
|
direction = direction / np.linalg.norm(direction) |
|
|
length = scale * np.random.uniform(5, 15) |
|
|
|
|
|
|
|
|
lines.setColor( |
|
|
np.random.uniform(0.8, 1.0), |
|
|
np.random.uniform(0.3, 0.7), |
|
|
np.random.uniform(0.0, 0.2), |
|
|
1.0 |
|
|
) |
|
|
|
|
|
start = position |
|
|
end = position + direction * length |
|
|
|
|
|
lines.moveTo(Point3(start[0], start[1], start[2])) |
|
|
lines.drawTo(Point3(end[0], end[1], end[2])) |
|
|
|
|
|
node = self.render.attachNewNode(lines.create()) |
|
|
self.explosion_nodes.append({ |
|
|
'node': node, |
|
|
'birth_time': time.time(), |
|
|
'lifetime': 0.5 |
|
|
}) |
|
|
|
|
|
def _create_hud(self): |
|
|
"""Create heads-up display""" |
|
|
|
|
|
self.hud_elements['title'] = OnscreenText( |
|
|
text="KAPS COMBAT ARENA", |
|
|
pos=(-1.3, 0.9), |
|
|
scale=0.08, |
|
|
fg=(0.2, 0.8, 1.0, 1), |
|
|
align=TextNode.ALeft, |
|
|
font=None |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['wave'] = OnscreenText( |
|
|
text="WAVE: 0", |
|
|
pos=(-1.3, 0.8), |
|
|
scale=0.06, |
|
|
fg=(1, 1, 1, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['score'] = OnscreenText( |
|
|
text="SCORE: 0", |
|
|
pos=(-1.3, 0.7), |
|
|
scale=0.06, |
|
|
fg=(0.2, 1.0, 0.2, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['threats'] = OnscreenText( |
|
|
text="THREATS: 0", |
|
|
pos=(1.0, 0.9), |
|
|
scale=0.05, |
|
|
fg=(1, 0.4, 0.4, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['damage'] = OnscreenText( |
|
|
text="DAMAGE: 0", |
|
|
pos=(1.0, 0.8), |
|
|
scale=0.05, |
|
|
fg=(1, 0.6, 0.2, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['fps'] = OnscreenText( |
|
|
text="FPS: 60", |
|
|
pos=(1.0, -0.9), |
|
|
scale=0.04, |
|
|
fg=(0.5, 0.5, 0.5, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
|
|
|
self.hud_elements['brain'] = OnscreenText( |
|
|
text="DREAMER: ACTIVE", |
|
|
pos=(-1.3, -0.9), |
|
|
scale=0.05, |
|
|
fg=(0.8, 0.4, 1.0, 1), |
|
|
align=TextNode.ALeft |
|
|
) |
|
|
|
|
|
def _update_ocean(self, dt: float): |
|
|
"""Animate ocean waves""" |
|
|
if not hasattr(self, 'ocean_vdata'): |
|
|
return |
|
|
|
|
|
self.wave_time += dt |
|
|
|
|
|
|
|
|
vertex = GeomVertexWriter(self.ocean_vdata, "vertex") |
|
|
vertex.setRow(0) |
|
|
|
|
|
res = self.wave_resolution |
|
|
ocean_size = 3000 |
|
|
ocean_depth = -150 |
|
|
|
|
|
for i in range(res + 1): |
|
|
for j in range(res + 1): |
|
|
x = (i / res - 0.5) * ocean_size |
|
|
y = (j / res - 0.5) * ocean_size |
|
|
|
|
|
|
|
|
t = self.wave_time |
|
|
wave1 = np.sin(x * 0.02 + t * 0.5) * 6 |
|
|
wave2 = np.sin(y * 0.015 - t * 0.3) * 5 |
|
|
wave3 = np.sin((x + y) * 0.01 + t * 0.7) * 4 |
|
|
wave4 = np.sin(x * 0.008 - y * 0.008 + t * 0.2) * 8 |
|
|
z = ocean_depth + wave1 + wave2 + wave3 + wave4 |
|
|
|
|
|
vertex.setData3f(x, y, z) |
|
|
|
|
|
|
|
|
if hasattr(self, 'foam'): |
|
|
self.foam.setH(self.foam.getH() + dt * 2) |
|
|
self.foam.setPos( |
|
|
np.sin(self.wave_time * 0.1) * 20, |
|
|
np.cos(self.wave_time * 0.08) * 20, |
|
|
np.sin(self.wave_time * 0.5) * 3 |
|
|
) |
|
|
|
|
|
def _update_hud(self): |
|
|
"""Update HUD with current stats""" |
|
|
if self.arena is None: |
|
|
return |
|
|
|
|
|
stats = self.arena.stats |
|
|
|
|
|
self.hud_elements['wave'].setText(f"WAVE: {stats.wave_survived}") |
|
|
self.hud_elements['score'].setText(f"SCORE: {stats.score}") |
|
|
self.hud_elements['threats'].setText(f"THREATS: {len(self.arena.spawner.threats)}") |
|
|
self.hud_elements['damage'].setText(f"DAMAGE: {stats.damage_taken:.0f}") |
|
|
|
|
|
|
|
|
current_time = time.time() |
|
|
dt = current_time - self.last_update_time |
|
|
if dt > 0: |
|
|
fps = 1.0 / dt |
|
|
self.hud_elements['fps'].setText(f"FPS: {fps:.0f}") |
|
|
|
|
|
|
|
|
if self.frame_count % 30 < 15: |
|
|
self.hud_elements['brain'].setFg((0.8, 0.4, 1.0, 1.0)) |
|
|
else: |
|
|
self.hud_elements['brain'].setFg((1.0, 0.6, 1.0, 1.0)) |
|
|
|
|
|
def _setup_input(self): |
|
|
"""Setup keyboard controls""" |
|
|
self.accept("escape", self.userExit) |
|
|
self.accept("space", self._spawn_wave) |
|
|
self.accept("b", self._trigger_burst) |
|
|
self.accept("r", self._reset_camera) |
|
|
self.accept("o", self._toggle_orbit) |
|
|
self.accept("1", lambda: self._camera_preset(1)) |
|
|
self.accept("2", lambda: self._camera_preset(2)) |
|
|
self.accept("3", lambda: self._camera_preset(3)) |
|
|
self.accept("4", lambda: self._camera_preset(4)) |
|
|
|
|
|
def _spawn_wave(self): |
|
|
"""Manually spawn a threat wave""" |
|
|
if self.arena: |
|
|
wave = self.arena.spawner.spawn_wave() |
|
|
|
|
|
|
|
|
self.warning_light.setColor(Vec4(1, 0.2, 0.1, 1)) |
|
|
self.taskMgr.doMethodLater( |
|
|
0.5, |
|
|
lambda task: self.warning_light.setColor(Vec4(0, 0, 0, 1)) or Task.done, |
|
|
"warning_flash" |
|
|
) |
|
|
|
|
|
def _trigger_burst(self): |
|
|
"""Trigger defensive burst""" |
|
|
if self.arena: |
|
|
|
|
|
for node in self.arena.lattice.get_all_nodes(): |
|
|
node.corkscrew_frequency *= 2.0 |
|
|
|
|
|
|
|
|
self.taskMgr.doMethodLater( |
|
|
1.0, |
|
|
lambda task: self._reset_burst() or Task.done, |
|
|
"burst_reset" |
|
|
) |
|
|
|
|
|
def _reset_burst(self): |
|
|
"""Reset burst mode""" |
|
|
if self.arena: |
|
|
for node in self.arena.lattice.get_all_nodes(): |
|
|
node.corkscrew_frequency = max(1.0, node.corkscrew_frequency / 2.0) |
|
|
|
|
|
def _reset_camera(self): |
|
|
"""Reset camera to default position""" |
|
|
self.camera_distance = 300 |
|
|
self.camera_angle = 0 |
|
|
self.camera_pitch = 30 |
|
|
self._setup_camera() |
|
|
|
|
|
def _toggle_orbit(self): |
|
|
"""Toggle auto camera orbit""" |
|
|
self.auto_orbit = not self.auto_orbit |
|
|
print(f"Auto-orbit: {'ON' if self.auto_orbit else 'OFF'}") |
|
|
|
|
|
def _camera_preset(self, preset: int): |
|
|
"""Switch to camera preset""" |
|
|
presets = { |
|
|
1: (300, 0, 30), |
|
|
2: (150, 90, 10), |
|
|
3: (500, 45, 60), |
|
|
4: (100, 180, 0), |
|
|
} |
|
|
if preset in presets: |
|
|
self.camera_distance, self.camera_angle, self.camera_pitch = presets[preset] |
|
|
self._setup_camera() |
|
|
|
|
|
def _camera_orbit_task(self, task): |
|
|
"""Slowly orbit camera around scene""" |
|
|
if self.auto_orbit: |
|
|
dt = globalClock.getDt() |
|
|
self.camera_angle += self.orbit_speed * dt |
|
|
if self.camera_angle > 360: |
|
|
self.camera_angle -= 360 |
|
|
self._setup_camera() |
|
|
return Task.cont |
|
|
|
|
|
def _update_task(self, task): |
|
|
"""Main update loop""" |
|
|
current_time = time.time() |
|
|
dt = current_time - self.last_update_time |
|
|
self.last_update_time = current_time |
|
|
self.frame_count += 1 |
|
|
|
|
|
if self.arena is None: |
|
|
return Task.cont |
|
|
|
|
|
|
|
|
obs = self.arena.step() |
|
|
|
|
|
|
|
|
all_nodes = self.arena.lattice.get_all_nodes() |
|
|
for node in all_nodes: |
|
|
if node.node_id in self.lattice_nodes: |
|
|
visual = self.lattice_nodes[node.node_id] |
|
|
pos = node.position |
|
|
visual.setPos(pos[0], pos[1], pos[2]) |
|
|
|
|
|
|
|
|
if node.role.name == 'GYRO_ARM': |
|
|
visual.setH(visual.getH() + node.corkscrew_frequency * 360 * dt) |
|
|
|
|
|
|
|
|
if node.node_id in self.blade_nodes and node.parent is not None: |
|
|
blade = self.blade_nodes[node.node_id] |
|
|
parent_pos = node.parent.position |
|
|
child_pos = node.position |
|
|
|
|
|
blade.setPos(parent_pos[0], parent_pos[1], parent_pos[2]) |
|
|
blade.lookAt(Point3(child_pos[0], child_pos[1], child_pos[2])) |
|
|
|
|
|
|
|
|
blade.setR(blade.getR() + node.corkscrew_frequency * 360 * dt) |
|
|
|
|
|
|
|
|
current_threats = {t.threat_id for t in self.arena.spawner.threats if t.alive} |
|
|
|
|
|
|
|
|
for threat_id in list(self.threat_nodes.keys()): |
|
|
if threat_id not in current_threats: |
|
|
|
|
|
node = self.threat_nodes[threat_id] |
|
|
pos = node.getPos() |
|
|
self._create_explosion(np.array([pos.x, pos.y, pos.z]), 2.0) |
|
|
|
|
|
|
|
|
node.removeNode() |
|
|
del self.threat_nodes[threat_id] |
|
|
|
|
|
|
|
|
for threat in self.arena.spawner.threats: |
|
|
if threat.alive and threat.threat_id not in self.threat_nodes: |
|
|
self.threat_nodes[threat.threat_id] = self._create_threat_visual(threat) |
|
|
elif threat.alive and threat.threat_id in self.threat_nodes: |
|
|
|
|
|
pos = threat.position |
|
|
self.threat_nodes[threat.threat_id].setPos(pos[0], pos[1], pos[2]) |
|
|
|
|
|
|
|
|
node = self.threat_nodes[threat.threat_id] |
|
|
node.setH(node.getH() + 30 * dt) |
|
|
node.setP(node.getP() + 20 * dt) |
|
|
|
|
|
|
|
|
new_explosions = [] |
|
|
for exp in self.explosion_nodes: |
|
|
age = current_time - exp['birth_time'] |
|
|
if age < exp['lifetime']: |
|
|
|
|
|
alpha = 1.0 - age / exp['lifetime'] |
|
|
|
|
|
scale = 1.0 + age * 3 |
|
|
exp['node'].setScale(scale) |
|
|
new_explosions.append(exp) |
|
|
else: |
|
|
exp['node'].removeNode() |
|
|
self.explosion_nodes = new_explosions |
|
|
|
|
|
|
|
|
self._update_ocean(dt) |
|
|
|
|
|
|
|
|
self._update_hud() |
|
|
|
|
|
|
|
|
closest = self.arena.spawner.get_closest_threat(self.arena.lattice.root.position) |
|
|
if closest: |
|
|
dist = np.linalg.norm(closest.position - self.arena.lattice.root.position) |
|
|
intensity = max(0, 1.0 - dist / 200) |
|
|
self.warning_light.setColor(Vec4(intensity, intensity * 0.1, 0, 1)) |
|
|
|
|
|
return Task.cont |
|
|
|
|
|
|
|
|
def run_combat_visualization(champion_path: str = None): |
|
|
"""Launch the combat visualizer with DreamerV3 brain and cascade collective! |
|
|
|
|
|
Args: |
|
|
champion_path: Path to champion capsule .py file. If None, searches default locations. |
|
|
""" |
|
|
if not PANDA3D_AVAILABLE: |
|
|
print("Panda3D not available!") |
|
|
print("Install with: pip install panda3d") |
|
|
return |
|
|
|
|
|
import sys |
|
|
import importlib.util |
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
project_root = Path(__file__).parent.parent.parent |
|
|
sys.path.insert(0, str(project_root)) |
|
|
|
|
|
|
|
|
search_paths = [ |
|
|
|
|
|
champion_path, |
|
|
|
|
|
"F:/End-Game/glassboxgames/children/key-data-repo/models/champion_gen52.py", |
|
|
"F:/End-Game/glassboxgames/children/key-data-repo/models/champion_gen42.py", |
|
|
|
|
|
str(project_root / "champion_gen42.py"), |
|
|
] |
|
|
|
|
|
|
|
|
brain = None |
|
|
for path in search_paths: |
|
|
if path is None: |
|
|
continue |
|
|
capsule_path = Path(path) |
|
|
if capsule_path.exists(): |
|
|
try: |
|
|
print(f"[BRAIN] Loading champion from: {capsule_path}") |
|
|
|
|
|
|
|
|
spec = importlib.util.spec_from_file_location("champion", str(capsule_path)) |
|
|
champion = importlib.util.module_from_spec(spec) |
|
|
sys.modules["champion"] = champion |
|
|
spec.loader.exec_module(champion) |
|
|
|
|
|
|
|
|
if hasattr(champion, 'QuineBrain'): |
|
|
brain = champion.QuineBrain() |
|
|
print(f"[BRAIN] QuineBrain loaded! action_dim={brain.action_dim}, latent_dim={brain.latent_dim}") |
|
|
if hasattr(brain, 'get_merkle_hash'): |
|
|
print(f"[BRAIN] Merkle hash: {brain.get_merkle_hash()[:32]}...") |
|
|
break |
|
|
|
|
|
|
|
|
elif hasattr(champion, 'CapsuleAgent'): |
|
|
agent = champion.CapsuleAgent(observe=False, observe_visual=False) |
|
|
brain = agent.brain |
|
|
print(f"[BRAIN] CapsuleAgent loaded! Generation: {getattr(champion, '_GENERATION', '?')}") |
|
|
print(f"[BRAIN] Quine hash: {getattr(champion, '_QUINE_HASH', 'unknown')[:32]}...") |
|
|
break |
|
|
|
|
|
except Exception as e: |
|
|
print(f"[BRAIN] Failed to load {capsule_path.name}: {e}") |
|
|
continue |
|
|
|
|
|
if brain is None: |
|
|
print("[BRAIN] No valid champion found - using fallback reactive controller") |
|
|
|
|
|
|
|
|
from src.ai.collective_intelligence import create_collective_arena |
|
|
arena, collective = create_collective_arena() |
|
|
|
|
|
|
|
|
if brain: |
|
|
for qid, cascade_brain in collective.quines.items(): |
|
|
cascade_brain.champion = brain |
|
|
cascade_brain.brain_hash = brain.get_merkle_hash() |
|
|
print(f"[COLLECTIVE] Attached QuineBrain to {len(collective.quines)} quines") |
|
|
|
|
|
|
|
|
viz = CombatVisualizer(arena) |
|
|
viz.collective = collective |
|
|
viz.brain = brain |
|
|
|
|
|
print("\n" + "=" * 60) |
|
|
print("🚀 KAPS COMBAT ARENA - DREAMERV3 POWERED") |
|
|
print("=" * 60) |
|
|
print(f" Quines: {len(collective.quines)}") |
|
|
print(f" Genesis Root: {collective.quines[list(collective.quines.keys())[0]].last_cid or 'pending'}") |
|
|
print(f" Arena Radius: {arena.arena_radius}m") |
|
|
print("=" * 60 + "\n") |
|
|
|
|
|
viz.run() |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
run_combat_visualization() |
|
|
|