import math import random import js import asyncio from pyodide.ffi import to_js, create_proxy from pyscript import when from typing import Any from js import Howl, Howler fps = 60 delta_frame = 1 / fps debug = False fov = 90 THREE: Any GLTFLoader: Any Ammo: Any EffectComposer: Any RenderPass: Any UnrealBloomPass: Any keys = { "w": False, "a": False, "s": False, "d": False, " ": False, "shift": False } scene: Any window: Any console: Any canvas: Any renderer: Any document: Any camera: Any collision_config: Any dispatcher: Any broadphase: Any solver: Any dynamics_world: Any axes_helper: Any HAND_LAYER = 1 hand_camera: Any # Rules inverse_controls = False kill_y = -45 gold = 0 def set_invert_controls(new_val): global inverse_controls inverse_controls = new_val def mathRandom(num=1): rand = js.Math.random setNumber = -rand() * num + rand() * num return setNumber def lerp(a, b, t): return a + (b - a) * t def lerp_color(c1, c2, t): r = c1.r + (c2.r - c1.r) * t g = c1.g + (c2.g - c1.g) * t b = c1.b + (c2.b - c1.b) * t return THREE.Color.new(r, g, b) def random_bright_color(min_val=0, max_val=1): r = random.uniform(min_val, max_val) g = random.uniform(min_val, max_val) b = random.uniform(min_val, max_val) return THREE.Color.new(r, g, b) def set_background_image(path): overlay = document.getElementById("ui_overlay") overlay.style.backgroundImage = f"url('{path}')" overlay.style.backgroundSize = "cover" overlay.style.backgroundPosition = "center" def clear_background_image(): overlay = document.getElementById("ui_overlay") overlay.style.backgroundImage = "" def set_hud_image(path): overlay = document.getElementById("hud_overlay") overlay.style.backgroundImage = f"url('{path}')" overlay.style.backgroundSize = "cover" overlay.style.backgroundPosition = "center" def clear_hud_image(): overlay = document.getElementById("hud_overlay") overlay.style.backgroundImage = "" def random_direction_in_cone(forward, angle_deg): """ Returns a THREE.Vector3 pointing in a random direction within a cone. - forward: THREE.Vector3 (normalized), the cone's central direction - angle_deg: maximum spread angle in degrees from center Returns a new THREE.Vector3. """ angle_rad = math.radians(angle_deg) # Random spherical coordinates within cone angle theta = random.uniform(0, 2 * math.pi) phi = random.uniform(0, angle_rad) x = math.sin(phi) * math.cos(theta) y = math.sin(phi) * math.sin(theta) z = math.cos(phi) # Local direction in +Z forward space direction = THREE.Vector3.new(x, y, z) # Create a quaternion that rotates Z axis to `forward` base = THREE.Vector3.new(0, 0, 1) quat = THREE.Quaternion.new() quat.setFromUnitVectors(base, forward.clone().normalize()) direction.applyQuaternion(quat) return direction.normalize() def get_direction_and_distance(a, b): """ Returns the direction (normalized THREE.Vector3) and distance (float) from point a to point b. """ direction = b.clone().sub(a) distance = direction.length() if distance > 0: direction.normalize() return direction, distance def is_pointer_locked(): return document.pointerLockElement is not None def timeout(delay, func): proxy = create_proxy(func) js.setTimeout(proxy, delay, None) def to_js_obj(kwargs): return js.Object.fromEntries(to_js(kwargs)) def load_skybox(path, ext="png"): cube_texture_loader = THREE.CubeTextureLoader.new() cube_texture_loader.setPath(path) textures = [f"right.{ext}", f"left.{ext}", f"top.{ext}", f"bottom.{ext}", f"front.{ext}", f"back.{ext}"] skybox_texture = cube_texture_loader.load(textures) scene.background = skybox_texture def load_skysphere(asset, size=500, on_load=None): texture_loader = THREE.TextureLoader.new() def on_texture_loaded(texture): geometry = THREE.SphereGeometry.new(size, 32, 32) perms = { "map": texture, "side": THREE.DoubleSide } material = THREE.MeshBasicMaterial.new(to_js_obj(perms)) skysphere = THREE.Mesh.new(geometry, material) if on_load: on_load(skysphere) scene.add(skysphere) try: proxy.destroy() except: pass proxy = create_proxy(on_texture_loaded) texture_loader.load(asset, proxy) def set_object_position(mesh, body, x, y, z): mesh.position.set(x, y, z) body.getWorldTransform().setOrigin(Ammo.btVector3.new(x, y, z)) body.setLinearVelocity(Ammo.btVector3.new(0, 0, 0)) body.activate() def create_static_mesh_collider(mesh, position): mesh.updateMatrixWorld(True) geometry = mesh.geometry vertices = geometry.attributes.position.array indices = geometry.index.array triangle_mesh = Ammo.btTriangleMesh.new() for i in range(0, len(indices), 3): i0, i1, i2 = indices[i], indices[i + 1], indices[i + 2] v0 = THREE.Vector3.new(vertices[i0 * 3], vertices[i0 * 3 + 1], vertices[i0 * 3 + 2]) v1 = THREE.Vector3.new(vertices[i1 * 3], vertices[i1 * 3 + 1], vertices[i1 * 3 + 2]) v2 = THREE.Vector3.new(vertices[i2 * 3], vertices[i2 * 3 + 1], vertices[i2 * 3 + 2]) # Apply mesh world transform v0.applyMatrix4(mesh.matrixWorld) v1.applyMatrix4(mesh.matrixWorld) v2.applyMatrix4(mesh.matrixWorld) # Flip winding if needed (backface issue) triangle_mesh.addTriangle( Ammo.btVector3.new(v2.x, v2.y, v2.z), Ammo.btVector3.new(v1.x, v1.y, v1.z), Ammo.btVector3.new(v0.x, v0.y, v0.z), True ) shape = Ammo.btBvhTriangleMeshShape.new(triangle_mesh, True) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*position)) # already baked into triangle positions motion_state = Ammo.btDefaultMotionState.new(transform) body_info = Ammo.btRigidBodyConstructionInfo.new(0, motion_state, shape) body = Ammo.btRigidBody.new(body_info) body.setFriction(1.0) return body def set_layer_recursive(obj, layer): obj.layers.set(layer) obj.traverse(lambda c: c.layers.set(layer)) def load_asset(file, position, on_load=None): def on_loaded(gltf): gltf.scene.position.set(*position) def trav(child): if hasattr(child, "isMesh"): setattr(child, "castShadow", True) # create_static_mesh_collider(child, position) if hasattr(child, "material"): if child.material.emissive: setattr(child.material, "emissiveIntensity", 1.21) if child.material.side: setattr(child.material, "side", THREE.DoubleSide) gltf.scene.traverse(trav) if on_load is not None: on_load(gltf) scene.add(gltf.scene) try: proxy.destroy() # try destroy except: pass # exception just pass proxy = create_proxy(on_loaded) loader = GLTFLoader.new() loader.load(file, proxy) def load_asset_static(file, position, on_load=None, to_scene=False): def on_loaded(gltf): gltf.scene.position.set(*position) body = [] def trav(child): if hasattr(child, "isMesh"): setattr(child, "castShadow", True) setattr(child, "receiveShadow", True) body.append(create_static_mesh_collider(child, position)) if hasattr(child, "material"): if child.material.emissive: setattr(child.material, "emissiveIntensity", 1) if child.material.side: setattr(child.material, "side", THREE.DoubleSide) gltf.scene.traverse(trav) if on_load is not None: on_load(gltf, body) if to_scene: scene.add(gltf.scene) try: proxy.destroy() # try destroy except: pass # exception just pass proxy = create_proxy(on_loaded) loader = GLTFLoader.new() loader.load(file, proxy) def add_debug_box(size, color=0xff0000): mat = { "color": color, "wireframe": True, } material = THREE.MeshBasicMaterial.new(to_js_obj(mat)) geometry = THREE.BoxGeometry.new(size[0], size[1], size[2]) mesh = THREE.Mesh.new(geometry, material) scene.add(mesh) return mesh def add_debug_sphere(radius, color=0xff0000): mat = { "color": color, "wireframe": True, } material = THREE.MeshBasicMaterial.new(to_js_obj(mat)) geometry = THREE.SphereGeometry.new(radius) mesh = THREE.Mesh.new(geometry, material) scene.add(mesh) return mesh def load_gltf(path, on_load): loader = GLTFLoader.new() loader.load(path, create_proxy(on_load), None, create_proxy(lambda e: console.error("GLTF load failed", e))) def sync_mesh_with_body(mesh, body): transform = body.getWorldTransform() origin = transform.getOrigin() rotation = transform.getRotation() mesh.position.set(origin.x(), origin.y(), origin.z()) mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w()) def create_text_sprite(text, size=48, color="white"): ctx = canvas.getContext("2d") ctx.font = f"{size}px monospace" ctx.fillStyle = color ctx.fillText(text, 0, size) texture = THREE.CanvasTexture.new(canvas) material = THREE.SpriteMaterial.new(to_js_obj(dict(map=texture))) sprite = THREE.Sprite.new(material) return sprite def check_line_hit(start, end): from_vec = Ammo.btVector3.new(start.x, start.y, start.z) to_vec = Ammo.btVector3.new(end.x, end.y, end.z) ray_cb = Ammo.ClosestRayResultCallback.new(from_vec, to_vec) dynamics_world.rayTest(from_vec, to_vec, ray_cb) if ray_cb.hasHit(): obj = ray_cb.get_m_collisionObject() hit_point = ray_cb.get_m_hitPointWorld() hit = THREE.Vector3.new(hit_point.x(), hit_point.y(), hit_point.z()) return obj, hit return None, None def formation_circle(center, spacing, count, angle_offset=0.0): radius = spacing * count / (2 * math.pi) positions = [] for i in range(count): angle = (2 * math.pi / count) * i + angle_offset x = center[0] + math.cos(angle) * radius z = center[2] + math.sin(angle) * radius y = center[1] positions.append((x, y, z)) return positions def formation_v(center, spacing, count): positions = [] half = count // 2 for i in range(count): offset = i - half x = center[0] + offset * spacing z = center[2] + abs(offset) * spacing # further out for wider V y = center[1] positions.append((x, y, z)) return positions def spawn_projectile(pos, direction, source, damage=-10, projectile="MovingProjectile", trail="Explosion", explode="ExplosionE"): p: MovingProjectileEffect = prefab_mgr.spawn(projectile, pos, direction) p.damage = damage p.source = source p.trail = trail p.explode = explode return p # Todo SoundManager class SoundManager: def __init__(self): self.sounds = {} def load(self, name, src, volume=1.0, loop=False, positional=False): options = { "src": [src], "volume": volume, "loop": loop } if positional: options["positional"] = True # WebAudio-only options["pannerAttr"] = { "panningModel": "HRTF", "distanceModel": "inverse", "refDistance": 1, "maxDistance": 100, "rolloffFactor": 1 } self.sounds[name] = Howl.new(to_js_obj(options)) def play(self, name, volume=None, pitch_variation=0.0): sound = self.sounds.get(name) if not sound: return id = sound.play() if volume is not None: sound.volume(volume, id) if pitch_variation > 0: rate = 1.0 + random.uniform(-pitch_variation, pitch_variation) sound.rate(rate, id) def play_at(self, name, position: tuple, pitch_variation=0.05): sound = self.sounds.get(name) if not sound: return id = sound.play() if pitch_variation > 0: rate = 1.0 + random.uniform(-pitch_variation, pitch_variation) sound.rate(rate, id) sound.pos(*position, id=id) def stop(self, name): sound = self.sounds.get(name) if sound: sound.stop() def stop_all(self): for v in self.sounds.values(): v.stop() def mute(self, value=True): Howler.mute(value) sound_mgr: SoundManager = SoundManager() sound_mgr.load("explosion6", "assets/sfx/explosion6.wav", positional=True, volume=0.9) sound_mgr.load("rock_crack", "assets/sfx/rock_crack.wav", positional=True, volume=0.2) sound_mgr.load("heal", "assets/sfx/heal.wav", positional=True, volume=0.2) sound_mgr.load("beepbox_1", "assets/sfx/beepbox_1.wav", volume=0.05, loop=True) sound_mgr.load("beepbox_5", "assets/sfx/beepbox_1.wav", volume=0.05, loop=True) sound_mgr.load("ui_hover_whoosh", "assets/sfx/ui_hover_whoosh.wav", volume=1) sound_mgr.load("fire_shot", "assets/sfx/fire_shot.wav", positional=True, volume=0.75) # Todo GameObject class GameObject: def __init__(self, mesh=None, body=None, time_alive=0, on_update=None, on_death=None, is_static=False): self.is_static = is_static self.mesh = mesh self.body = body self.time_alive = time_alive self.on_update = on_update self.on_death = on_death self.health = 100 self.enemy = False def apply_damage(self, damage, source): pass def is_alive(self): return self in level_mgr.active_objects or self in level_mgr.inactive_objects def update(self): if self.on_update: self.on_update(self) if 0 < self.time_alive: self.time_alive -= 1 if self.time_alive == 0: self.apply_damage(-100, None) if self.body: x, y, z = get_body_center(self.body) if y < kill_y: self.apply_damage(-100, None) def destroy(self): if self.on_death: self.on_death(self) self.on_death = None level_mgr.remove(self) # Todo PhysicsGameObject class PhysicsGameObject(GameObject): def __init__(self, mesh, body, on_update=None, on_death=None, direction=(0, 0, 0), move_speed=0, drag=0.9): super().__init__(mesh, body, 0, on_update, on_death) self.body.apply_damage = self.apply_damage self.direction = Ammo.btVector3.new(*direction) self.move_speed = move_speed self.drag = drag def update(self): super().update() if self.is_alive(): sync_mesh_with_body(self.mesh, self.body) if 0 < self.direction.length() and 0 < self.move_speed: self.direction.normalize() d = self.direction d.op_mul(self.move_speed) v = self.body.getLinearVelocity() v.op_add(d) self.body.setLinearVelocity(v) self.move_speed *= self.drag if self.move_speed < 0.05: self.move_speed = 0 def destroy(self): super().destroy() self.mesh = None self.body = None # Todo Enemy class Enemy(PhysicsGameObject): def __init__(self, mesh, body, **kwargs): super().__init__(mesh, body, **kwargs) self.target_loc = (0, 0, 0) self.is_flying = kwargs.get("is_flying", False) self.attack_range = kwargs.get("attack_range", 10) self.attack_counter = 0 self.attack_time = 50 self.damage = -10 self.height = 6 def apply_damage(self, damage, source): health = self.health health = min(max(0, health + damage), 100) self.health = health if not health: sound_mgr.play_at("rock_crack", self.mesh.position, 0.3) self.destroy() def attack(self, direction, distance): pass def update(self): super().update() if self.body: x, y, z = get_body_center(self.body) if y < -10: self.apply_damage(-100, None) if self.is_alive(): if self.move_speed: target_loc = THREE.Vector3.new(*self.target_loc) v = self.body.getLinearVelocity() v.op_mul(self.drag) self.body.setLinearVelocity(v) a = self.mesh.position direction, dist = get_direction_and_distance(a, target_loc) if self.is_flying: direction.multiplyScalar(self.move_speed * dist * dist) self.body.applyCentralForce(Ammo.btVector3.new(*direction)) else: if is_grounded(self.body, self.height): direction.setY(0) direction.multiplyScalar(self.move_speed * 100) self.body.applyCentralForce(Ammo.btVector3.new(*direction)) if player: pos = get_body_center(player.body) direction, distance = get_direction_and_distance(self.mesh.position, THREE.Vector3.new(*pos)) if distance < self.attack_range: self.attack_counter += 1 if self.attack_counter == self.attack_time: self.attack_counter = 0 self.attack(direction, distance) else: if self.attack_counter: self.attack_counter = 0 else: self.on_death = None self.destroy() super().update() class SphereEnemy(Enemy): def __init__(self, pos=(0, 0, 0), radius=5, mass=25, **kwargs): # Visual mat = { "color": 0xffcccc, "emissive": random_bright_color(0.1, 0.3), "emissiveIntensity": 1, "metalness": 1, "roughness": 0.1 } geometry = THREE.SphereGeometry.new(radius) material = THREE.MeshStandardMaterial.new(to_js_obj(mat)) mesh = THREE.Mesh.new(geometry, material) mesh.position.set(*pos) # Physics shape = Ammo.btSphereShape.new(radius) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) motion_state = Ammo.btDefaultMotionState.new(transform) local_inertia = Ammo.btVector3.new(0, 0, 0) shape.calculateLocalInertia(mass, local_inertia) body_info = Ammo.btRigidBodyConstructionInfo.new(mass, motion_state, shape, local_inertia) body = Ammo.btRigidBody.new(body_info) body.setFriction(5) super().__init__(mesh, body, drag=0.992, move_speed=30, **kwargs) self.attack_time = 1 def attack(self, direction, distance): player.apply_damage(self.damage, self) self.apply_damage(-100, None) class CapsuleEnemy(Enemy): def __init__(self, pos=(0, 0, 0), size=(2.5, 5), mass=25, **kwargs): mat = { "color": 0xff000f, "emissive": 0xff000f, "emissiveIntensity": 1, "metalness": 4, "roughness": 0.1, } geometry = THREE.CapsuleGeometry.new(size[0], size[1]) material = THREE.MeshStandardMaterial.new(to_js_obj(mat)) mesh = THREE.Mesh.new(geometry, material) mesh.position.set(*pos) # Physics shape = Ammo.btCapsuleShape.new(size[0], size[1]) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) motion_state = Ammo.btDefaultMotionState.new(transform) local_inertia = Ammo.btVector3.new(0, 0, 0) shape.calculateLocalInertia(mass, local_inertia) body_info = Ammo.btRigidBodyConstructionInfo.new(mass, motion_state, shape, local_inertia) body = Ammo.btRigidBody.new(body_info) body.setFriction(5) body.setAngularFactor(Ammo.btVector3.new(0, 0, 0)) super().__init__(mesh, body, drag=0.992, move_speed=30, **kwargs) self.attack_range = 100 def attack(self, direction, distance): params = (self.mesh.position, direction, self, self.damage, "MovingProjectileE", "ExplosionF", "ExplosionE") spawn_projectile(*params) sound_mgr.play_at("fire_shot", self.mesh.position, pitch_variation=0.3) # Todo Boss class Boss(Enemy): def __init__(self, pos=(0, 0, 0), size=15, mass=1000, color=0xff000f, friction=5, **kwargs): # Visual mat = { "color": color, "emissive": 0xff000f, "emissiveIntensity": 1.2, "opacity": 0, "roughness": 0.8, "metalness": 8 } geometry = THREE.IcosahedronGeometry.new(size) material = THREE.MeshStandardMaterial.new(to_js_obj(mat)) mesh = THREE.Mesh.new(geometry, material) mesh.position.set(*pos) # Physics shape = Ammo.btSphereShape.new(size) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) motion_state = Ammo.btDefaultMotionState.new(transform) local_inertia = Ammo.btVector3.new(0, 0, 0) shape.calculateLocalInertia(mass, local_inertia) body_info = Ammo.btRigidBodyConstructionInfo.new(mass, motion_state, shape, local_inertia) body = Ammo.btRigidBody.new(body_info) body.setFriction(friction) body.setDamping(0.1, 0.1) super().__init__(mesh, body, drag=0.95, **kwargs) self.is_flying = True self.target_loc = (0, 40, 0) self.move_speed = 500 self.attack_range = 500 self.damage = -100 def apply_damage(self, damage, source): if not isinstance(source, Boss): super().apply_damage(damage, source) def attack(self, direction, distance): params = (self.mesh.position, direction, self, self.damage, "MovingProjectileB", "ExplosionG", "ExplosionE") spawn_projectile(*params) sound_mgr.play_at("fire_shot", self.mesh.position, pitch_variation=0.25) # Todo FormationController class FormationController(GameObject): def __init__(self, units, formation_fn, center=(0, 0, 0), spacing=3, rotation_speed=0.5, height_offset=0): super().__init__() """ units: list of game objects with a `target_loc` attribute formation_fn: function(center: tuple, spacing: float, count: int) -> list of positions center: tuple (x, y, z) spacing: float controlling distance between units """ self.units: list[Boss] = units self.formation_fn = formation_fn self.center = center self.spacing = spacing self.angle_offset = 0.0 self.rotation_speed = rotation_speed self.in_formation = True self.counter = 0 self.counter_target = random.randint(100, 300) self.height_offset = height_offset def update_formation(self): positions = self.formation_fn(self.center, self.spacing, len(self.units), self.angle_offset) for unit, pos in zip(self.units, positions): x, y, z = pos unit.target_loc = (x, y + self.height_offset, z) def set_center(self, new_center): self.center = new_center self.update_formation() return self def update(self): for u in list(self.units): if not u.is_alive(): self.units.remove(u) if self.units: if self.in_formation: self.angle_offset += self.rotation_speed * delta_frame self.update_formation() else: self.counter += 1 if self.counter == self.counter_target: self.counter = 0 self.counter_target = random.randint(150, 500) for unit in self.units: x = random.randint(-200, 200) y = random.randint(20, 200) z = random.randint(-200, 200) unit.target_loc = (x, y, z) super().update() else: self.destroy() def destroy(self): super().destroy() self.units = None self.formation_fn = None self.center = None self.spacing = None # Todo EnemySpawner class EnemySpawner(GameObject): def __init__(self): super().__init__() self.wave = 0 self.counter = 0 self.wave_time = 100 self.enemy_types = [SphereEnemy, CapsuleEnemy] self.boss_types = [Boss] self.score = 0 self.score_multi = 1 self.wave_epoch = 0 self.boss_active = False def add_score(self, src, amt): if not isinstance(src, Boss): if src is not None: prefab_mgr.spawn("ExplosionE", src.mesh.position) prefab_mgr.spawn("Pickup", src.mesh.position) else: prefab_mgr.spawn("ExplosionBoss", src.mesh.position) camera_shake_mgr.shake(amount=0.3, duration=1) self.score += amt * self.score_multi def spawn_wave(self): self.wave += 1 if self.wave % 2 == 0: enemies = [] self.boss_active = True for i in range(random.randint(1, min(self.wave, 10))): pos = (random.randint(-100, 100), 10, random.randint(-100, 100)) e = random.choice(self.boss_types)(pos) if not e.on_death: e.on_death = lambda x: self.add_score(x, 5 + self.wave_epoch * 5) enemies.append(e) pos = (random.randint(-100, 100), random.randint(10, 100), random.randint(-100, 100)) fc = FormationController(enemies, formation_circle, pos, 50, 0.5, 0) fc.in_formation = False level_mgr.add(fc) level_mgr.add(*enemies) random_rule() else: self.boss_active = False for _ in range(min(self.wave, 10)): enemies = [] for i in range(random.randint(1, min(self.wave, 5))): pos = (random.randint(-100, 100), 10, random.randint(-100, 100)) e = random.choice(self.enemy_types)(pos) if not e.on_death: e.on_death = lambda x: self.add_score(x, 1) enemies.append(e) pos = (random.randint(-100, 100), 0, random.randint(-100, 100)) fc = FormationController(enemies, formation_circle, pos, 30, 0.5) level_mgr.add(fc) level_mgr.add(*enemies) def update(self): if player: super().update() update_wave_display(self.wave) update_score_display(self.score) self.counter += 1 if self.counter == self.wave_time: self.counter = 0 e = list(filter(lambda x: isinstance(x, Enemy), level_mgr.active_objects)) if not e: self.spawn_wave() else: fcs = list(filter(lambda x: isinstance(x, FormationController), level_mgr.active_objects)) for fc in fcs: fc.center = get_body_center(player.body) if self.boss_active: if random.choice([True, False]): fc.in_formation = not fc.in_formation else: self.destroy() # Todo CameraSequencer def ease_in_out(t): return t * t * (3 - 2 * t) # sequencer = CameraSequencer().sequence([ # {"type": "lookAt", "target": [0, 15, 0], "duration": 1.0}, # {"type": "move", "to": [5, 10, 10], "duration": 2.0}, # {"type": "lookAt", "target": [0, 0, 0], "duration": 1.0}, # {"type": "move", "to": [5, 10, 5], "duration": 3.0}, # {"type": "lookAt", "target": [0, 0, 0], "duration": 1.0}, # {"type": "wait", "duration": 1.0}, # {"type": "callback", "fn": lambda: sequencer.destroy()} # ]) # manager.add(sequencer) class CameraSequencer(GameObject): def __init__(self): super().__init__() self.camera = camera self.queue = [] self.current = None self.elapsed = 0.0 self.active = False self.start_quat = None self.end_quat = None def sequence(self, steps): self.queue = steps[:] self.active = True self._next() return self def _next(self): if not self.queue: self.active = False return self.current = self.queue.pop(0) self.elapsed = 0.0 # Prep actions (e.g., save start pos for interpolation) if self.current["type"] == "move": self.start_pos = self.camera.position.clone() elif self.current["type"] == "lookAt": # self.start_pos = self.camera.position.clone() target = THREE.Vector3.new(*self.current["target"]) tmp = THREE.Object3D.new() tmp.position.copy(self.camera.position) tmp.lookAt(target) self.start_quat = self.camera.quaternion.clone() self.end_quat = tmp.quaternion.clone() def update(self): if not self.active or not self.current: return self.elapsed += delta_frame duration = self.current.get("duration", 0.0) t = ease_in_out(min(1.0, self.elapsed / duration)) action = self.current["type"] if action == "move": to = THREE.Vector3.new(*self.current["to"]) self.camera.position.lerpVectors(self.start_pos, to, t) elif action == "lookAt": # self.camera.quaternion.copy(self.start_quat).slerp(self.end_quat, t) target = THREE.Vector3.new(*self.current["target"]) self.camera.lookAt(target) elif action == "wait": pass # just a timer elif action == "callback" and t == 1.0: self.current["fn"]() self.camera.updateMatrixWorld(True) # Advance when done if self.elapsed >= duration: self._next() def create_dynamic_box(pos=(0, 0, 0), size=(1, 1, 1), mass=10, color=0xffaa00, friction=5, **kwargs): # Visual mat = { "color": color, "emissiveIntensity": 0, } geometry = THREE.BoxGeometry.new(size[0], size[1], size[2]) material = THREE.MeshStandardMaterial.new(to_js_obj(mat)) mesh = THREE.Mesh.new(geometry, material) mesh.position.set(*pos) # Physics shape = Ammo.btBoxShape.new(Ammo.btVector3.new(size[0] / 2, size[1] / 2, size[2] / 2)) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) motion_state = Ammo.btDefaultMotionState.new(transform) local_inertia = Ammo.btVector3.new(0, 0, 0) shape.calculateLocalInertia(mass, local_inertia) body_info = Ammo.btRigidBodyConstructionInfo.new(mass, motion_state, shape, local_inertia) body = Ammo.btRigidBody.new(body_info) body.setFriction(friction) return PhysicsGameObject(mesh, body, **kwargs) def create_dynamic_sphere(pos=(0, 0, 0), radius=1, mass=10, color=0xffaa00, friction=5, **kwargs): # Visual mat = { "color": color, **kwargs, } geometry = THREE.SphereGeometry.new(radius) material = THREE.MeshStandardMaterial.new(to_js_obj(mat)) mesh = THREE.Mesh.new(geometry, material) mesh.position.set(*pos) # Physics shape = Ammo.btSphereShape.new(radius) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) motion_state = Ammo.btDefaultMotionState.new(transform) local_inertia = Ammo.btVector3.new(0, 0, 0) shape.calculateLocalInertia(mass, local_inertia) body_info = Ammo.btRigidBodyConstructionInfo.new(mass, motion_state, shape, local_inertia) body = Ammo.btRigidBody.new(body_info) body.setFriction(friction) return PhysicsGameObject(mesh, body) def create_emissive_box(size=(1, 1, 1), pos=(0, 0, 0), color=0x111111, emissive=0x00ffff, intensity=1.21): material = THREE.MeshStandardMaterial.new(to_js_obj({ "color": color, "emissive": emissive, "emissiveIntensity": intensity, "metalness": 0.3, "roughness": 0.4 })) mesh = THREE.Mesh.new(THREE.BoxGeometry.new(*size), material) mesh.position.set(*pos) return mesh def create_cubes(dx=2, dy=2, dz=2): i = 0 while i < 30: geometry = THREE.IcosahedronGeometry.new(5) mat = { "flatShading": True, "color": "#ff1111", "transparent": False, "opacity": 1, "wireframe": False } mat_obj = to_js_obj(mat) material = THREE.MeshStandardMaterial.new(mat_obj) cube = THREE.Mesh.new(geometry, material) cube.speedRotation = js.Math.random() * 100 cube.positionX = mathRandom() * dx cube.positionY = mathRandom() * dy cube.positionZ = mathRandom() * dz cube.castShadow = True cube.receiveShadow = True newScaleValue = mathRandom(0.3) cube.scale.set(newScaleValue, newScaleValue, newScaleValue) cube.rotation.x = mathRandom(180 * js.Math.PI / 180) cube.rotation.y = mathRandom(180 * js.Math.PI / 180) cube.rotation.z = mathRandom(180 * js.Math.PI / 180) cube.position.set(cube.positionX, cube.positionY, cube.positionZ) i += 1 def get_body_center(body): transform = body.getWorldTransform() origin = transform.getOrigin() return origin.x(), origin.y(), origin.z() def is_grounded(body, ray_length=1.1): transform = body.getWorldTransform() origin = transform.getOrigin() from_pos = Ammo.btVector3.new(origin.x(), origin.y(), origin.z()) to_pos = Ammo.btVector3.new(origin.x(), origin.y() - ray_length, origin.z()) ray_callback = Ammo.ClosestRayResultCallback.new(from_pos, to_pos) dynamics_world.rayTest(from_pos, to_pos, ray_callback) grounded = ray_callback.hasHit() # clean up Ammo.destroy(ray_callback) Ammo.destroy(from_pos) Ammo.destroy(to_pos) return grounded # Todo Player def toggle_hud(toggle): document.getElementById("hud").style.display = "flex" if toggle else "none" def update_score_display(value): document.getElementById("score").innerText = f"Score: {value}" def update_wave_display(value): document.getElementById("wave").innerText = f"Wave: {value}" def update_health_display(value): document.getElementById("health").innerText = f"HP: {value}" def update_mana_display(value): document.getElementById("mana").innerText = f"Mana: {value}" class Player(GameObject): def __init__(self, pos=(0, 100, 0)): super().__init__() # ─── Capsule Physics ───────────────── self.height = 2.5 self.radius = 2 self.mass = 100 self.move_speed = 20 self.sprint_speed = 50 self.walk_speed = 25 self.drag = 0.75 self.mouse_sensitivity = 0.002 self.health = 100 capsule = Ammo.btCapsuleShape.new(self.radius, self.height) transform = Ammo.btTransform.new() transform.setIdentity() transform.setOrigin(Ammo.btVector3.new(*pos)) inertia = Ammo.btVector3.new(0, 0, 0) capsule.calculateLocalInertia(self.mass, inertia) motion = Ammo.btDefaultMotionState.new(transform) info = Ammo.btRigidBodyConstructionInfo.new(self.mass, motion, capsule, inertia) self.body = Ammo.btRigidBody.new(info) self.body.setAngularFactor(Ammo.btVector3.new(0, 0, 0)) self.body.setDamping(0.1, 1.0) self.body.setActivationState(4) self.body.apply_damage = self.apply_damage dynamics_world.addRigidBody(self.body) # ─── Visuals ────────────────────────── self.head = THREE.Object3D.new() self.left_hand = THREE.Object3D.new() self.right_hand = THREE.Object3D.new() # Position hands relative to the head self.height_offset = self.height * 0.5 + self.radius pos = self.body.getWorldTransform().getOrigin() px, py, pz = pos.x(), pos.y(), pos.z() self.head.position.set(px, py + self.height_offset, pz) self.left_hand.position.set(-0.8, -0.75, -0.5) # left hand slightly forward self.right_hand.position.set(0.8, -0.75, -0.5) # right hand slightly forward def create_hand_debug(color): geo = THREE.SphereGeometry.new(0.3, 16, 16) mat = THREE.MeshStandardMaterial.new(to_js_obj({ "color": color, "wireframe": False, "opacity": 0.5, "transparent": True, "metalness": 0.1, "roughness": 0.75, "flatShading": False, })) return THREE.Mesh.new(geo, mat) self.left_hand.add(create_hand_debug(0xcccccc)) self.right_hand.add(create_hand_debug(0xcccccc)) set_layer_recursive(self.left_hand, HAND_LAYER) set_layer_recursive(self.right_hand, HAND_LAYER) self.head.add(self.left_hand) self.head.add(self.right_hand) self.camera = camera # use global camera self.head.add(self.camera) scene.add(self.head) self.hand_camera = hand_camera scene.add(self.hand_camera) # ─── Input & State ───────────────────── self.yaw = 0 self.pitch = 0 self.direction = Ammo.btVector3.new(0, 0, 0) self.on_ground = False self.jump_buffer = 0.0 self.coyote_timer = 0.0 self.holding_jump = False self.jump_strength = 0 self.bob_time = 0 self.bob_intensity = 0.05 self.bob_speed = 1 self.max_tilt = 0.05 self.damage = -100 toggle_hud(True) self.lh_attack = pf_default_attack self.rh_attack = pf_default_attack global camera_shake_mgr camera_shake_mgr = CameraShake() position, direction, _, _ = self.get_aim_data() self.hand_camera.position.copy(position.add(direction.multiplyScalar(-1))) self.hand_camera.quaternion.copy(self.head.quaternion) self.hand_camera.updateMatrixWorld(True) def is_alive(self): return player def apply_damage(self, damage, source): if isinstance(source, Enemy): camera_shake_mgr.shake(amount=0.3, duration=1) if isinstance(source, (Enemy, Pickup)) or source is None: health = self.health health = min(max(0, health + damage), 100) self.health = health if not health: self.destroy() toggle_hud(False) menu_mgr.gameover() def destroy(self): super().destroy() camera_shake_mgr.end() toggle_hud(False) scene.remove(self.head) scene.remove(self.left_hand) scene.remove(self.right_hand) self.head = None self.left_hand = None self.right_hand = None global player player = None for k in list(keys.keys()): keys[k] = False def jump(self): j = [self.jump_strength] def p(_): vel = self.body.getLinearVelocity() if vel.y() < 0: vel.setY(0) self.body.setLinearVelocity(vel) self.body.applyCentralImpulse(Ammo.btVector3.new(0, (70 - j[0]) * 80, 0)) j[0] -= 1 for i in range(self.jump_strength): timeout(10 * i, p) keys[" "] = False self.holding_jump = False self.jump_strength = 0 def handle_mouse_move(self, event): self.yaw -= event.movementX * self.mouse_sensitivity self.pitch -= event.movementY * self.mouse_sensitivity self.pitch = max(-1.5, min(1.5, self.pitch)) # clamp def get_aim_data(self, offset_distance=0): self.head.updateMatrixWorld(True) self.camera.updateMatrixWorld(True) direction = THREE.Vector3.new() self.camera.getWorldDirection(direction) position = THREE.Vector3.new() self.camera.getWorldPosition(position) if offset_distance != 0: offset = direction.clone().multiplyScalar(offset_distance) position.add(offset) hand_r = THREE.Vector3.new() self.right_hand.getWorldPosition(hand_r) hand_l = THREE.Vector3.new() self.left_hand.getWorldPosition(hand_l) return position, direction, hand_r, hand_l def update(self): if camera_shake_mgr: camera_shake_mgr.update() # ─── Input → Movement Direction ────── move = Ammo.btVector3.new(0, 0, 0) if not inverse_controls: if keys["w"]: move.setZ(1) if keys["s"]: move.setZ(-1) if keys["a"]: move.setX(-1) if keys["d"]: move.setX(1) else: if keys["w"]: move.setZ(-1) if keys["s"]: move.setZ(1) if keys["a"]: move.setX(1) if keys["d"]: move.setX(-1) if keys["shift"]: self.move_speed = self.sprint_speed self.bob_speed = 2 self.bob_intensity = 0.2 else: self.move_speed = self.walk_speed self.bob_speed = 1 self.bob_intensity = 0.05 if move.length() > 0: move.normalize() dx = move.x() * math.cos(self.yaw) - move.z() * math.sin(self.yaw) dz = move.x() * math.sin(self.yaw) + move.z() * math.cos(self.yaw) self.direction = Ammo.btVector3.new(dx, 0, -dz) else: self.direction = Ammo.btVector3.new(0, 0, 0) # ─── Physics Movement ────── v = self.body.getLinearVelocity() d = self.direction d.op_mul(self.move_speed) v.op_add(d) v.op_add(dynamics_world.getGravity()) v.op_mul(self.drag) self.body.setLinearVelocity(v) # ─── Update Camera Position ───── pos = self.body.getWorldTransform().getOrigin() px, py, pz = pos.x(), pos.y(), pos.z() self.head.position.set(px, py + self.height_offset, pz) moving = self.direction.length() > 0 if moving and self.on_ground: self.bob_time += delta_frame * self.bob_speed if moving else 0 bob_t = self.bob_time bob_offset = math.sin(bob_t * math.pi) * self.bob_intensity if moving else 0 tilt_angle = math.sin(bob_t * 2 * math.pi) * self.max_tilt if moving else 0 else: bob_offset = 0 tilt_angle = 0 self.bob_time = 0 # reset to prevent stutter # Camera bob and tilt if not moving: self.head.position.y = lerp(self.head.position.y, py + self.height_offset, 0.1) self.camera.rotation.z = lerp(self.camera.rotation.z, 0, 0.1) else: self.head.position.y = py + self.height_offset + bob_offset self.camera.rotation.z = tilt_angle # ─── Camera Rotation ────── self.head.rotation.set(self.pitch, self.yaw, 0, 'YXZ') self.head.updateMatrixWorld(True) self.camera.updateMatrixWorld(True) position, direction, _, _ = self.get_aim_data() self.hand_camera.position.copy(position.add(direction.multiplyScalar(-1))) self.hand_camera.quaternion.copy(self.head.quaternion) self.hand_camera.updateMatrixWorld(True) # ─── Jump Logic ────── self.on_ground = is_grounded(self.body, self.height + self.radius) if self.holding_jump: self.jump_strength = min(self.jump_strength + 1, 100) else: if self.on_ground: self.coyote_timer = 0.2 else: self.coyote_timer = max(0, self.coyote_timer - delta_frame) self.jump_buffer = max(0, self.jump_buffer - delta_frame) if self.on_ground and self.jump_buffer > 0 and self.coyote_timer > 0: self.jump() self.jump_buffer = 0 self.coyote_timer = 0 super().update() if not self.health: if self.is_alive(): self.destroy() toggle_hud(False) menu_mgr.gameover() else: update_health_display(self.health) def hand_attack(self, is_left=True): p, d, rh, lh = self.get_aim_data() if is_left: adjusted_pos = lh.clone().add(d.clone().multiplyScalar(0.05)) spawn_projectile(adjusted_pos, d, self, self.damage, *self.lh_attack) else: adjusted_pos = rh.clone().add(d.clone().multiplyScalar(0.05)) spawn_projectile(adjusted_pos, d, self, self.damage, *self.rh_attack) camera_shake_mgr.shake(amount=0.08, duration=0.5) sound_mgr.play_at("fire_shot", adjusted_pos, pitch_variation=0.3) player: Player = None # Todo DayNightCycle class DayNightCycle: def __init__(self): # Time ranges from 0.0 (midnight) to 1.0 (end of day) self.active = True self.time_of_day = 1 self.speed = 0.01 self.sky = [] load_skysphere("assets/skysphere.jpg", on_load=lambda x: self.sky.append(x)) # Lights self.sun = THREE.DirectionalLight.new(0xffffff, 0) self.sun.position.set(0, 0, 0) scene.add(self.sun) self.sun_helper = None if debug: self.sun_helper = THREE.DirectionalLightHelper.new(self.sun) scene.add(self.sun_helper) self.ambient = THREE.AmbientLight.new(0xffffff, 0) scene.add(self.ambient) # Optional: Fog scene.fog = THREE.Fog.new(0x87ceeb, 0, 800) renderer.setClearColor(0x87ceeb) # Weather attributes (to be extended later) self.weather = "clear" self.day_color = THREE.Color.new(0x87ceeb) # sky blue self.night_color = THREE.Color.new(0x000000) self.current_sky_color = self.day_color.clone() def set_layer(self, layer): set_layer_recursive(self.sun, layer) set_layer_recursive(self.ambient, layer) def update(self): # Progress time if self.active: self.time_of_day = (self.time_of_day + delta_frame * self.speed) % 1.0 angle = self.time_of_day * 2 * math.pi # Rotate sun self.sun.position.set(math.sin(angle) * 500, math.cos(angle) * 500, 0) self.sun.lookAt(0, 0, 0) # Adjust light color/intensity brightness = max(0, math.cos(angle)) self.sun.color.setHSL(0.1, 1.0, 0.5 + 0.8 * brightness) self.sun.intensity = 0.5 + 1.2 * brightness self.ambient.intensity = 0.1 + 0.6 * brightness # Lerp between night and day colors based on brightness sky_color = lerp_color(self.night_color, self.day_color, brightness) # Apply sky color renderer.setClearColor(sky_color) scene.fog.color.copy(sky_color) self.current_sky_color.copy(sky_color) # (Optional) handle weather here self.apply_weather() def apply_weather(self): if self.weather == "rain": # Future: add rain particles or fog adjustments self.ambient.intensity *= 0.7 self.sun.intensity *= 0.7 elif self.weather == "storm": self.ambient.intensity *= 0.4 self.sun.intensity *= 0.4 elif self.weather == "clear": pass # Normal behavior def set_weather(self, type): self.weather = type day_mgr: DayNightCycle = None # Todo Effect class Effect: def __init__(self, position, **kwargs): self.prefab_name = kwargs.get("prefab_name", "Default") self.group = THREE.Group.new() self.velocities = [] self.time = 0.0 self.duration = kwargs.get("duration", 1.0) # Position the whole group self.group.position.set(position.x, position.y, position.z) self.burst_count = kwargs.get("burst_count", 10) self.size = kwargs.get("size", (2, 2, 2)) self.speed = kwargs.get("speed", (4, 4, 4)) self.color = kwargs.get("color", 0xffaa00) self.emissive = kwargs.get("emissive", 0xffffff) self.intensity = kwargs.get("intensity", 1) self.metalness = kwargs.get("metalness", 0.25) self.roughness = kwargs.get("roughness", 0.25) self.flat_shading = kwargs.get("flat_shading", False) self.transparent = kwargs.get("transparent", False) self.opacity = kwargs.get("opacity", 1) self.wireframe = kwargs.get("wire_frame", False) self.size_range = kwargs.get("size_range", (1, 5)) self.recyclable = kwargs.get("recyclable", True) def get_js_mat(self): return to_js_obj({ "color": self.color, "emissive": self.emissive, "emissiveIntensity": self.intensity, "metalness": self.metalness, "roughness": self.roughness, "flatShading": self.flat_shading, "transparent": self.transparent, "opacity": self.opacity, "wireframe": self.wireframe }) def create(self): # Add visual elements (e.g., glowing spheres or planes) for i in range(self.burst_count): # simple particle burst geom = THREE.SphereGeometry.new(random.randint(*self.size_range) / 10, 8, 8) mat = THREE.MeshStandardMaterial.new(self.get_js_mat()) mesh = THREE.Mesh.new(geom, mat) mesh.position.set( (random.random() - 0.5) * self.size[0], (random.random() - 0.5) * self.size[1], (random.random() - 0.5) * self.size[2], ) self.group.add(mesh) # Optionally store velocities or random directions for motion self.velocities = [THREE.Vector3.new( (random.random() - 0.5) * self.speed[0], (random.random() - 0.5) * self.speed[1], (random.random() - 0.5) * self.speed[2], ) for _ in range(len(self.group.children))] scene.add(self.group) def update(self): self.time += delta_frame t = self.time / self.duration # Update each mesh (scatter and fade) for i, mesh in enumerate(self.group.children): mesh.position.add(self.velocities[i].clone().multiplyScalar(delta_frame)) scale = 1.0 - t mesh.scale.set(scale, scale, scale) # Auto-remove after time if self.time > self.duration: return False # signal expired return True # still active def reset(self, *args, **kwargs): self.time = 0.0 position = args[0] self.group.position.copy(position) for i, mesh in enumerate(self.group.children): mesh.scale.set(1, 1, 1) mesh.position.set( (random.random() - 0.5) * self.size[0], (random.random() - 0.5) * self.size[1], (random.random() - 0.5) * self.size[2], ) self.velocities = [THREE.Vector3.new( (random.random() - 0.5) * self.speed[0], (random.random() - 0.5) * self.speed[1], (random.random() - 0.5) * self.speed[2], ) for _ in range(len(self.group.children))] class Pickup(Effect): def __init__(self, pos, **kwargs): super().__init__(pos, recyclable=False, **kwargs) self.move_speed = 2 def update(self): super().update() if player: p = THREE.Vector3.new(*get_body_center(player.body)) dr, d = get_direction_and_distance(THREE.Vector3.new(*self.group.position), p) if d < 5: sound_mgr.play_at("heal", p, 0.08) player.apply_damage(1, self) global gold gold += 1 return False t = dr.clone().multiplyScalar(self.move_speed) self.group.position.add(t) return True else: return False # Todo LineProjectileEffect class LineProjectileEffect(Effect): def __init__(self, start_pos, end_pos, **kwargs): super().__init__(start_pos, **kwargs) self.start_pos = start_pos self.end_pos = end_pos self.line = None def get_js_mat(self): return to_js_obj({ "color": self.color, "transparent": self.transparent, "opacity": self.opacity }) def create(self): # Create a line material = THREE.LineBasicMaterial.new(self.get_js_mat()) points = THREE.BufferGeometry.new() points.setFromPoints([self.start_pos.clone(), self.end_pos.clone()]) self.line = THREE.Line.new(points, material) self.group.add(self.line) def update(self): self.time += delta_frame alpha = 1.0 - (self.time / self.duration) self.line.material.opacity = alpha self.line.material.transparent = True self.line.material.needsUpdate = True if self.time > self.duration: return False return True def reset(self, *args, **kwargs): self.start_pos = args[0] self.end_pos = args[1] self.time = 0 self.line.geometry.setFromPoints([self.start_pos.clone(), self.end_pos.clone()]) # Todo MovingProjectileEffect class MovingProjectileEffect(Effect): def __init__(self, start, direction, **kwargs): super().__init__(start, recyclable=False, **kwargs) self.start = start.clone() self.pos = start.clone() self.direction = direction.clone().normalize() self.move_speed = kwargs.get("move_speed", 250) self.max_distance = kwargs.get("max_distance", 100) self.travelled = 0 self.damage = kwargs.get("damage", 10) self.source = kwargs.get("source", None) self.mesh = None self.bt_from = None self.bt_to = None self.on_update = None self.on_death = None self.trail = kwargs.get("trail", "ExplosionC") self.explode = kwargs.get("explode", "Explosion") def get_js_mat(self): return to_js_obj({ "color": self.color, "transparent": self.transparent, "opacity": self.opacity }) def create(self): # Create a visual line segment mat = THREE.LineBasicMaterial.new(self.get_js_mat()) geom = THREE.BufferGeometry.new() geom.setFromPoints([self.pos.clone(), self.pos.clone().add(self.direction.clone().multiplyScalar(1))]) self.mesh = THREE.Line.new(geom, mat) self.group = THREE.Group.new() self.group.add(self.mesh) # Prepare Ammo.js ray vectors for reuse self.bt_from = Ammo.btVector3.new() self.bt_to = Ammo.btVector3.new() def update(self): dist = self.move_speed * delta_frame self.travelled += dist if self.travelled > self.max_distance: return False # Raycast ahead self.bt_from.setValue(self.pos.x, self.pos.y, self.pos.z) next_pos = self.pos.clone().add(self.direction.clone().multiplyScalar(dist)) self.bt_to.setValue(next_pos.x, next_pos.y, next_pos.z) callback = Ammo.ClosestRayResultCallback.new(self.bt_from, self.bt_to) dynamics_world.rayTest(self.bt_from, self.bt_to, callback) if callback.hasHit(): hit = callback.get_m_hitPointWorld() hit_point = THREE.Vector3.new(hit.x(), hit.y(), hit.z()) hit_obj = callback.get_m_collisionObject() body = Ammo.castObject(hit_obj, Ammo.btRigidBody) # Damage tagging (could be replaced with actual health system) if hasattr(body, "apply_damage") and body.apply_damage: body.apply_damage(self.damage, self.source) if self.on_death: self.on_death(self) prefab_mgr.spawn(self.explode, hit_point) sound_mgr.play_at("explosion6", hit_point, 0.25) return False if self.on_update: self.on_update(self) prefab_mgr.spawn(self.trail, next_pos) # Update projectile position and visuals self.pos.copy(next_pos) self.mesh.geometry.setFromPoints([ self.pos.clone().sub(self.direction.clone().multiplyScalar(1)), self.pos.clone() ]) return True # Todo SwarmMissiles class SwarmMissiles(Effect): def __init__(self, position, direction, **kwargs): super().__init__(position, recyclable=False, **kwargs) self.amount = 3 self.source = None self.projectile = ("SwarmMissilesSub", "SwarmMissilesT", "SwarmMissilesE") self.direction = direction self.counter = 0 def update(self): super().update() dr, dis, = get_direction_and_distance(self.group.position, THREE.Vector3.new(0, 300, 0)) if self.counter < 15: self.counter += 1 dr.add(self.direction) dr.normalize() if dis < 1: prefab_mgr.spawn("SwarmMissilesLauncherE", self.group.position) def spawn(): e = list(filter(lambda x: isinstance(x, Enemy), level_mgr.active_objects)) if e: c = [random.choice(e)] edr, edis, = get_direction_and_distance(self.group.position, c[0].mesh.position) d = random_direction_in_cone(edr, 360) counter = [0] def follow(_): if c[0].is_alive(): if counter[0] < 50: counter[0] += 1 p.move_speed = 75 + counter[0] else: p.move_speed = pmv dr1, dist1, = get_direction_and_distance(self.group.position, c[0].mesh.position) p.direction = dr1 else: r = list(filter(lambda x: isinstance(x, Enemy), level_mgr.active_objects)) if r: c[0] = random.choice(r) p = spawn_projectile(self.group.position, d, self.source, -100, *self.projectile) p.on_update = follow def on_death(x): for j in list(filter(lambda i: isinstance(i, (Enemy, Player)), level_mgr.active_objects)): dr1, dist1, = get_direction_and_distance(x.pos, THREE.Vector3.new(*get_body_center(j.body))) if dist1 < 25: j.apply_damage(-100 / self.amount, p) p.on_death = on_death pmv = p.move_speed for i in range(self.amount): spawn() return False else: self.group.position.add(THREE.Vector3.new(*dr).multiplyScalar(2)) return True # Todo Hellfire class HellfireProjectile(Effect): def __init__(self, position, _, **kwargs): super().__init__(position, **kwargs) self.position = position self.size = 25 self.direction = THREE.Vector3.new(0, -1, 0) self.source = None self.counter = 0 self.projectile = ("HfProjectileSub", "HfProjectileSubTrail", "HfProjectileSubExp") self.damage = -30 def create(self): self.size = 25 def update(self): super().update() self.counter += 1 if self.counter == 10: self.counter = 0 if 0 < self.size: self.size -= 1 direction = random_direction_in_cone(self.direction, 30) p = spawn_projectile(self.position, direction, self.source, self.damage, *self.projectile) def on_death(x): for j in list(filter(lambda i: isinstance(i, (Enemy, Player)), level_mgr.active_objects)): dr1, dist1, = get_direction_and_distance(x.pos, THREE.Vector3.new(*get_body_center(j.body))) if dist1 < 50: j.apply_damage(-10, p) p.on_death = on_death return True else: return False return True def reset(self, *args, **kwargs): self.size = 25 self.source = None # Todo PrefabManager class PrefabManager: def __init__(self): self.prefab_factories = {} # name -> factory function self.pool = {} # name -> list of reusable objects self.active = [] # currently alive instances def register(self, name, factory): self.prefab_factories[name] = factory self.pool[name] = [] def spawn(self, name, *args, **kwargs): if name not in self.prefab_factories: return None if self.pool[name]: obj = self.pool[name].pop() obj.reset(*args, **kwargs) scene.add(obj.group) else: obj = self.prefab_factories[name](*args, **kwargs) obj.create() self.active.append(obj) return obj def update(self): still_active = [] for obj in self.active: if obj.update(): still_active.append(obj) else: self.recycle(obj) self.active = still_active def recycle(self, obj): if obj.recyclable: self.pool[obj.prefab_name].append(obj) scene.remove(obj.group) def clear(self): for obj in self.active: scene.remove(obj.group) self.active.clear() prefab_mgr: PrefabManager = PrefabManager() def prefab_register_effect(name, effect_type, **kwargs): params = {**kwargs, "prefab_name": name} prefab_mgr.register(name, lambda pos: effect_type(pos, **params)) def prefab_register_line(name, line_effect, **kwargs): params = {**kwargs, "prefab_name": name} prefab_mgr.register(name, lambda start, end: line_effect(start, end, **params)) def prefab_register_moving(name, line_effect, **kwargs): params = {**kwargs, "prefab_name": name} prefab_mgr.register(name, lambda *args: line_effect(*args, **params)) prefab_register_effect("Default", Effect, color=0xffffff, emissive=0xffffff, intensity=1.5, duration=1) prefab_register_effect("Explosion", Effect, color=0xff0000, emissive=0xffcccc, intensity=1.8, duration=0.3, transparent=True, size=(2, 2, 2), speed=(50, 50, 50), burst_count=3, size_range=(30, 50)) prefab_register_effect("ExplosionB", Effect, color=0x00ffff, emissive=0xffffff, intensity=0.65, duration=0.2, transparent=True, size=(1, 1, 1), speed=(100, 100, 100)) prefab_register_effect("ExplosionC", Effect, color=0x00ffff, emissive=0xffffff, intensity=1, duration=0.2, transparent=True, size=(1, 1, 1), speed=(15, 15, 15), burst_count=1, size_range=(5, 15)) prefab_register_effect("ExplosionD", Effect, color=0xffffff, emissive=0x00ffff, intensity=1, duration=0.2, transparent=True, size=(1, 1, 1), speed=(100, 150, 100), burst_count=5, size_range=(30, 50)) prefab_register_effect("ExplosionE", Effect, color=0xff0000, emissive=0xffcccc, intensity=1.8, duration=0.2, transparent=True, size=(3, 3, 3), speed=(100, 300, 100), burst_count=10, size_range=(15, 50)) prefab_register_effect("ExplosionF", Effect, color=0xff0000, emissive=0xffcccc, intensity=1.8, duration=0.2, transparent=True, size=(1, 1, 1), speed=(15, 15, 15), burst_count=2, size_range=(5, 15)) prefab_register_effect("ExplosionG", Effect, color=0xff0000, emissive=0xffcccc, intensity=2, duration=0.2, transparent=True, size=(1, 1, 1), speed=(100, 150, 100), burst_count=1, size_range=(30, 50)) prefab_register_effect("ExplosionBoss", Effect, color=0xff00ff, emissive=0xffccff, intensity=2, duration=0.5, transparent=True, size=(5, 5, 5), speed=(500, 500, 500), burst_count=15, size_range=(35, 75)) prefab_register_effect("ExplosionFloor", Effect, color=0xffcc00, emissive=0xffccff, intensity=2, duration=0.5, transparent=True, size=(15, 15, 15), speed=(200, 1500, 200), burst_count=15, size_range=(25, 75)) prefab_register_effect("Pickup", Pickup, color=0x00ff00, emissive=0xccffcc, intensity=1.5, duration=10, transparent=True, size=(1, 1, 1), speed=(0, 0, 0), burst_count=1, size_range=(10, 15)) prefab_register_line("LineProjectile", LineProjectileEffect, color=0xff0000, duration=0.1) prefab_register_moving("MovingProjectile", MovingProjectileEffect, max_distance=225, damage=-100) prefab_register_moving("MovingProjectileE", MovingProjectileEffect, max_distance=225, color=0xff0000, emissive=0xffcccc, intensity=1.8) prefab_register_moving("MovingProjectileB", MovingProjectileEffect, max_distance=500, color=0xff00ff, emissive=0xffff00, intensity=1.8) prefab_register_moving("HfProjectile", HellfireProjectile, size_range=(1, 10)) prefab_register_moving("HfProjectileSub", MovingProjectileEffect, max_distance=1500, color=0xff00cc, emissive=0xffcccc, intensity=3, duration=5, move_speed=500, burst_count=1) prefab_register_effect("HfProjectileSubTrail", Effect, color=0xff0000, emissive=0xffcc00, intensity=2, duration=0.5, transparent=True, size=(3, 3, 3), speed=(100, 300, 100), burst_count=1, size_range=(35, 50)) prefab_register_effect("HfProjectileSubExp", Effect, color=0xffcc00, emissive=0xff0000, intensity=4, duration=0.2, transparent=True, size=(2, 2, 2), speed=(300, 800, 300), burst_count=10, size_range=(250, 350)) prefab_register_moving("DefaultAttack", MovingProjectileEffect, max_distance=400) prefab_register_effect("DefaultAttackT", Effect, color=0x00ffff, emissive=0xffffff, intensity=1, duration=0.2, transparent=True, size=(1, 1, 1), speed=(15, 15, 15), burst_count=1, size_range=(5, 15)) prefab_register_effect("DefaultAttackE", Effect, color=0xff0000, emissive=0xffcccc, intensity=1.8, duration=0.3, transparent=True, size=(3, 3, 3), speed=(100, 100, 100), burst_count=3, size_range=(30, 50)) prefab_register_moving("SwarmMissiles", SwarmMissiles, size_range=(1, 5)) prefab_register_effect("SwarmMissilesLauncherE", Effect, color=0xffffff, emissive=0xffcccc, intensity=1, duration=0.75, transparent=True, size=(2, 2, 2), speed=(500, 500, 500), burst_count=10, size_range=(250, 350)) prefab_register_moving("SwarmMissilesSub", MovingProjectileEffect, max_distance=1000, move_speed=500, duration=10) prefab_register_effect("SwarmMissilesT", Effect, color=0x00ffff, emissive=0xccffff, intensity=1.5, duration=0.2, transparent=True, size=(1, 1, 1), speed=(15, 15, 15), burst_count=1, size_range=(10, 25)) prefab_register_effect("SwarmMissilesE", Effect, color=0xff0000, emissive=0xffcccc, intensity=1.8, duration=0.3, transparent=True, size=(3, 3, 3), speed=(500, 800, 500), burst_count=5, size_range=(30, 50)) pf_default_attack = ("DefaultAttack", "DefaultAttackT", "DefaultAttackE") pf_swarm_missiles = ("SwarmMissiles", "DefaultAttackT", "DefaultAttackE") pf_swarm_missiles_sub = ("SwarmMissilesSub", "SwarmMissilesT", "SwarmMissilesE") def prefab_spawn_body(name, body, *args, **kwargs): transform = body.getWorldTransform() origin = transform.getOrigin() x, y, z = origin.x(), origin.y(), origin.z() prefab_mgr.spawn(name, THREE.Vector3.new((x, y, z)), *args, **kwargs) def prefab_spawn_mesh(name, mesh, *args, **kwargs): prefab_mgr.spawn(name, mesh.position.copy(), *args, **kwargs) # # Todo Levels # def hellfire_event(): prefab_mgr.spawn("HfProjectile", THREE.Vector3.new(0, 600, 0), None) def set_random_key(): if random.choice([True, False]): keys[random.choice(list(keys.keys()))] = random.choice([True, False]) def no_floor(): if level_mgr.inactive_objects: counter = [ 0, list(random.choices(level_mgr.inactive_objects, k=int(len(level_mgr.inactive_objects) / 5))) ] def update(_): counter[0] += 1 if counter[0] == 1000: counter[0] = 0 for o in counter[1]: if o.is_alive(): level_mgr.remove(o) counter[1] = None g.destroy() else: for obj in counter[1]: x, y, z = get_body_center(obj.body) obj.body.getWorldTransform().setOrigin(Ammo.btVector3.new(x, y - 0.1, z)) sync_mesh_with_body(obj.mesh, obj.body) pass g = GameObject(on_update=update) level_mgr.add(g) def up_floor(): if level_mgr.inactive_objects: counter = [ 0, list(random.choices(level_mgr.inactive_objects, k=int(len(level_mgr.inactive_objects) / 5))) ] def update(_): counter[0] += 1 if counter[0] == 500: counter[0] = 0 for o in counter[1]: if o.is_alive(): if o.mesh: prefab_mgr.spawn("ExplosionFloor", o.mesh.position) level_mgr.remove(o) counter[1] = None g.destroy() else: for obj in counter[1]: x, y, z = get_body_center(obj.body) obj.body.getWorldTransform().setOrigin(Ammo.btVector3.new(x, y + 0.1, z)) sync_mesh_with_body(obj.mesh, obj.body) pass g = GameObject(on_update=update) level_mgr.add(g) def day_change(): day_mgr.day_color = random_bright_color(min_val=0.5, max_val=1) def day_end(): day_mgr.day_color = THREE.Color.new(0x87ceeb) def night_change(): day_mgr.night_color = random_bright_color(0, 0.5) def night_end(): day_mgr.night_color = THREE.Color.new(0x000000) def day_speed_up(): day_mgr.speed = 0.5 def day_speed_end(): day_mgr.speed = 0.01 def give_lh_swarm(): if player: player.lh_attack = pf_swarm_missiles def remove_lh_swarm(): if player: player.lh_attack = pf_default_attack def give_rh_swarm(): if player: player.rh_attack = pf_swarm_missiles def remove_rh_swarm(): if player: player.rh_attack = pf_default_attack def more_sprint(): if player: player.sprint_speed = 75 def less_sprint(): if player: player.sprint_speed = 25 def more_walk(): if player: player.walk_speed = 50 def less_walk(): if player: player.walk_speed = 15 def reset_walk(): if player: player.walk_speed = 25 def reset_sprint(): if player: player.sprint_speed = 50 def more_bobble(): if player: player.max_tilt = 0.5 player.bob_intensity = 0.0001 def reset_bobble(): if player: player.max_tilt = 0.05 player.bob_intensity = 0.05 rules = { "invert_controls": [False, lambda: set_invert_controls(True), lambda: set_invert_controls(False), None], "random_keys": [False, None, None, set_random_key], "less_walk": [False, less_walk, reset_walk, None], "more_walk": [False, more_walk, reset_walk, None], "hellfire": [False, hellfire_event, None, None], "no_floor": [False, no_floor, None, None], "up_floor": [False, up_floor, None, None], "day_change": [False, day_change, day_end, None], "night_change": [False, night_change, night_end, None], "day_changer": [False, day_speed_up, day_speed_end, None], "lh_swarm": [False, give_lh_swarm, remove_lh_swarm, None], "rh_swarm": [False, give_rh_swarm, remove_rh_swarm, None], "up_sprint": [False, more_sprint, reset_sprint, None], "down_sprint": [False, less_sprint, reset_sprint, None], "more_bobble": [False, more_bobble, reset_bobble, None], } # rules: active, start, end, update def start_rules(*names): for name in names: if name in rules: rules[name][0] = True if rules[name][1]: rules[name][1]() def end_rules(*names): for name in names: if name in rules: rules[name][0] = False if rules[name][2]: rules[name][2]() def end_all_rules(): end_rules(*list(rules.keys())) def update_rules(): for v in list(rules.values()): if v[0] and v[3]: v[3]() def random_rule(): c1 = random.choice(list(rules.keys())) c2 = random.choice(list(rules.keys())) end_rules(c1) start_rules(c2) def arena(manager): manager: LevelManager sound_mgr.stop("beepbox_5") sound_mgr.play("beepbox_5", pitch_variation=0.1) day_mgr.time_of_day = 1 block_size = (50, 50) for i in range(-5, 5): for j in range(-5, 5): load_asset_static("assets/blocks/block_gray.glb", (block_size[0] * i, -25, block_size[1] * j), lambda x, y: manager.add(GameObject(x.scene, y[0], is_static=True))) global player player = Player((0, 10, 100)) manager.add(EnemySpawner()) def grassland(manager): manager: LevelManager sound_mgr.stop("beepbox_5") sound_mgr.play("beepbox_5", pitch_variation=0.1) day_mgr.time_of_day = 1 block_size = (50, 50) for i in range(-5, 5): for j in range(-5, 5): load_asset_static("assets/blocks/block_green.glb", (block_size[0] * i, -25, block_size[1] * j), lambda x, y: manager.add(GameObject(x.scene, y[0], is_static=True))) global player player = Player((0, 10, 100)) manager.add(EnemySpawner()) def alien(manager): manager: LevelManager sound_mgr.stop("beepbox_5") sound_mgr.play("beepbox_5", pitch_variation=0.1) day_mgr.time_of_day = 1 block_size = (50, 50) for i in range(-5, 5): for j in range(-5, 5): load_asset_static("assets/blocks/block_purple.glb", (block_size[0] * i, -25, block_size[1] * j), lambda x, y: manager.add(GameObject(x.scene, y[0], is_static=True))) global player player = Player((0, 10, 100)) manager.add(EnemySpawner()) def ocean(manager): manager: LevelManager sound_mgr.stop("beepbox_5") sound_mgr.play("beepbox_5", pitch_variation=0.1) day_mgr.time_of_day = 1 block_size = (50, 50) for i in range(-5, 5): for j in range(-5, 5): load_asset_static("assets/blocks/block_blue.glb", (block_size[0] * i, -25, block_size[1] * j), lambda x, y: manager.add(GameObject(x.scene, y[0], is_static=True))) global player player = Player((0, 10, 100)) manager.add(EnemySpawner()) # Todo LevelManager class LevelManager: def __init__(self): self.current_level = None self.active_objects = [] self.inactive_objects = [] def load(self, level_func): self.unload() self.current_level = level_func level_func(self) def unload(self): camera_shake_mgr.end() end_all_rules() global player if player: player.destroy() player = None for obj in list(self.active_objects): self.remove(obj) for obj in list(self.inactive_objects): self.remove(obj) self.current_level = None self.active_objects.clear() self.inactive_objects.clear() prefab_mgr.clear() def update(self): update_rules() if player: player.update() for o in self.active_objects: o.update() prefab_mgr.update() def add(self, *objs): obj: GameObject for obj in objs: if obj.mesh: scene.add(obj.mesh) if obj.body: dynamics_world.addRigidBody(obj.body) if not obj.is_static: self.active_objects.append(obj) else: self.inactive_objects.append(obj) def remove(self, *objs): obj: GameObject for obj in objs: if obj.mesh: scene.remove(obj.mesh) if obj.body: dynamics_world.removeRigidBody(obj.body) Ammo.destroy(obj.body) Ammo.destroy(obj.body.getMotionState()) if obj in self.active_objects: self.active_objects.remove(obj) if obj in self.inactive_objects: self.inactive_objects.remove(obj) level_mgr: LevelManager = LevelManager() # Todo CameraShake class CameraShake: def __init__(self): self.camera = camera self.amount = 0 self.duration = 0 self.elapsed = 0 self.original_pos = camera.position.clone() self.active = False def shake(self, amount=0.5, duration=0.5): self.amount = amount self.duration = duration self.elapsed = 0 self.original_pos = self.camera.position.clone() self.active = True def update(self): if not self.active: return self.elapsed += delta_frame if self.elapsed >= self.duration: self.camera.position.copy(self.original_pos) self.active = False return decay = 1 - (self.elapsed / self.duration) offset_x = (random.random() - 0.5) * self.amount * decay offset_y = (random.random() - 0.5) * self.amount * decay offset_z = (random.random() - 0.5) * self.amount * decay self.camera.position.set( self.original_pos.x + offset_x, self.original_pos.y + offset_y, self.original_pos.z + offset_z ) def end(self): self.camera.position.copy(self.original_pos) self.active = False camera_shake_mgr: CameraShake = None # # Menu Manager # def start_game(level_name): sound_mgr.stop_all() if not is_pointer_locked(): canvas.requestPointerLock() t = { "arena": arena, "alien": alien, "ocean": ocean, "grassland": grassland } level_mgr.load(t[level_name]) return level_name # Todo MenuManager class MenuManager: def __init__(self): self.menu_div = document.getElementById("menu") self.main_screen = document.getElementById("main-screen") self.lobby_screen = document.getElementById("lobby-screen") self.start_button = document.getElementById("start-btn") self.back_button = document.getElementById("back-btn") self.level_buttons = document.querySelectorAll(".level-btn") for lb in self.level_buttons: lb.addEventListener("mouseover", create_proxy(lambda _: sound_mgr.play("ui_hover_whoosh"))) self.start_button.addEventListener("mouseover", create_proxy(lambda _: sound_mgr.play("ui_hover_whoosh"))) self.pause_screen = document.getElementById("pause-screen") self.gameover_screen = document.getElementById("gameover-screen") self.resume_btn = document.getElementById("resume-btn") self.quit_btn = document.getElementById("quit-btn") self.retry_btn = document.getElementById("retry-btn") self.quit_gameover_btn = document.getElementById("gameover-quit-btn") self.resume_btn.addEventListener("click", create_proxy(self.resume_game)) self.quit_btn.addEventListener("click", create_proxy(self.lobby)) self.retry_btn.addEventListener("click", create_proxy(self.retry_game)) self.quit_gameover_btn.addEventListener("click", create_proxy(self.lobby)) self.visible = True self.state = "main" self.last_level = None self.start_button.addEventListener("click", create_proxy(self.lobby)) self.back_button.addEventListener("click", create_proxy(self.to_main)) for btn in self.level_buttons: btn.addEventListener("click", create_proxy(self.start_level)) def on_window_resize(event): resize_renderer() window.addEventListener("resize", create_proxy(on_window_resize)) def show(self): self.menu_div.style.display = "flex" self.set_screen(self.state) self.visible = True def hide(self): self.menu_div.style.display = "none" self.visible = False def set_screen(self, state): self.state = state for el in [self.main_screen, self.lobby_screen, self.pause_screen, self.gameover_screen]: el.style.display = "none" if state == "main": self.main_screen.style.display = "flex" elif state == "lobby": self.lobby_screen.style.display = "flex" elif state == "pause": self.pause_screen.style.display = "flex" elif state == "gameover": self.gameover_screen.style.display = "flex" def to_main(self, _=None): set_background_image("assets/images/title_screen.png") sound_mgr.stop_all() self.set_screen("main") sound_mgr.stop_all() def lobby(self, _=None): set_background_image("assets/images/title_screen.png") level_mgr.unload() sound_mgr.stop_all() self.set_screen("lobby") js.document.exitPointerLock() def pause(self): set_background_image("") js.document.exitPointerLock() self.show() self.set_screen("pause") def gameover(self): set_background_image("") sound_mgr.stop_all() js.document.exitPointerLock() self.show() self.set_screen("gameover") def resume_game(self, _=None): set_background_image("") self.hide() if not is_pointer_locked(): canvas.requestPointerLock() def retry_game(self, _=None): self.hide() start_game(self.last_level) # track this in start_game() def start_level(self, event): if hasattr(event.target.dataset, "level"): level_name = event.target.dataset.level self.hide() self.last_level = start_game(level_name) menu_mgr: MenuManager = None @when("keydown", "body") def on_keydown(event): key = event.key.lower() if not menu_mgr.visible: if key in ["escape", "1", "p"]: menu_mgr.pause() elif key == "shift": k = not keys[key] keys[key] = k elif player: keys[key] = True if " " == key: player.holding_jump = True @when("keyup", "body") def on_keyup(event): key = event.key.lower() if not menu_mgr.visible: if key == "shift": pass elif player: keys[key] = False if " " == key: player.jump_buffer = 0.1 player.holding_jump = False @when("click", selector="#three-canvas") def request_pointer_lock(event): if not menu_mgr.visible and document.pointerLockElement != canvas: canvas.requestPointerLock() if player: if event.button == 0: player.hand_attack(True) if event.button == 2: player.hand_attack(False) @when("mousemove", selector="#three-canvas") def on_mouse_move(event): try: if document.pointerLockElement == canvas: player.handle_mouse_move(event) except: pass # # Main # def resize_renderer(): width = window.innerWidth height = window.innerHeight renderer.setSize(width, height) camera.aspect = width / height hand_camera.aspect = width / height camera.updateProjectionMatrix() hand_camera.updateProjectionMatrix() async def wait_for(name): while not hasattr(window, name): await asyncio.sleep(0.1) window.console.log(f"Loaded: {name}") async def main(): global document, window, console document = js.document window = js.window console = js.window.console await wait_for("THREE") await wait_for("GLTFLoader") await wait_for("Ammo") await wait_for("EffectComposer") await wait_for("RenderPass") await wait_for("UnrealBloomPass") # imports global THREE, GLTFLoader, Ammo, EffectComposer, RenderPass, UnrealBloomPass THREE = window.THREE GLTFLoader = window.GLTFLoader Ammo = window.Ammo EffectComposer = window.EffectComposer RenderPass = window.RenderPass UnrealBloomPass = window.UnrealBloomPass # Physics config global collision_config, dispatcher, broadphase, solver, dynamics_world collision_config = Ammo.btDefaultCollisionConfiguration.new() dispatcher = Ammo.btCollisionDispatcher.new(collision_config) broadphase = Ammo.btDbvtBroadphase.new() solver = Ammo.btSequentialImpulseConstraintSolver.new() dynamics_world = Ammo.btDiscreteDynamicsWorld.new(dispatcher, broadphase, solver, collision_config) dynamics_world.setGravity(Ammo.btVector3.new(0, -9.81, 0)) window.dynamics_world = dynamics_world # Scene global scene scene = THREE.Scene.new() # Add AxesHelper if debug: global axes_helper axes_helper = THREE.AxesHelper.new(5) scene.add(axes_helper) width = window.innerWidth height = window.innerHeight # Camera global camera, hand_camera aspect = width / height camera = THREE.PerspectiveCamera.new(fov, aspect, 1, 1000) hand_camera = THREE.PerspectiveCamera.new(70, aspect, 0.01, 10) hand_camera.layers.enable(HAND_LAYER) # WebGL global canvas, renderer canvas = document.getElementById("three-canvas") renderer = THREE.WebGLRenderer.new(js.Object.fromEntries(to_js({"canvas": canvas, "antialias": True}))) renderer.setSize(width, height) renderer.shadowMap.enabled = False renderer.shadowMap.type = THREE.PCFSoftShadowMap renderer.shadowMap.needsUpdate = True renderer.toneMapping = THREE.ACESFilmicToneMapping renderer.setClearColor(0x87ceeb) renderer.autoClear = False document.body.appendChild(renderer.domElement) # Other globals global day_mgr, menu_mgr, camera_shake_mgr day_mgr = DayNightCycle() menu_mgr = MenuManager() menu_mgr.show() camera_shake_mgr = CameraShake() set_background_image("assets/images/title_screen.png") # Postprocessing camera.layers.set(0) hand_camera.layers.set(HAND_LAYER) composer = EffectComposer.new(renderer) composer.addPass(RenderPass.new(scene, camera)) # Add bloom bloom_pass = UnrealBloomPass.new( THREE.Vector2.new(width, height), 1, # strength 0.5, # radius 1 # threshold ) composer.addPass(bloom_pass) while True: if not menu_mgr.visible: dynamics_world.stepSimulation(delta_frame, 10) # Step simulation day_mgr.update() level_mgr.update() day_mgr.set_layer(0) composer.render() renderer.clearDepth() day_mgr.set_layer(1) hand_camera.layers.enable(HAND_LAYER) renderer.render(scene, hand_camera) await asyncio.sleep(delta_frame) asyncio.ensure_future(main())