playscii/game_util_objects.py

516 lines
16 KiB
Python

import os.path
import random
from collision import (
CST_AABB,
CST_CIRCLE,
CST_TILE,
CT_GENERIC_DYNAMIC,
CT_GENERIC_STATIC,
CT_NONE,
CT_PLAYER,
)
from game_object import FACING_DIRS, GameObject
class GameObjectAttachment(GameObject):
"GameObject that doesn't think about anything, just renders"
collision_type = CT_NONE
should_save = False
selectable = False
exclude_from_class_list = True
physics_move = False
offset_x, offset_y, offset_z = 0.0, 0.0, 0.0
"Offset from parent object's origin"
fixed_z = False
"If True, Z will not be locked to GO we're attached to"
editable = GameObject.editable + ["offset_x", "offset_y", "offset_z"]
def attach_to(self, game_object):
"Attach this object to given object."
self.parent = game_object
def update(self):
# very minimal update!
if not self.art.updated_this_tick:
self.art.update()
def post_update(self):
# after parent has moved, snap to its location
self.x = self.parent.x + self.offset_x
self.y = self.parent.y + self.offset_y
if not self.fixed_z:
self.z = self.parent.z + self.offset_z
class BlobShadow(GameObjectAttachment):
"Generic blob shadow attachment class"
art_src = "blob_shadow"
alpha = 0.5
class StaticTileBG(GameObject):
"Generic static world object with tile-based collision"
collision_shape_type = CST_TILE
collision_type = CT_GENERIC_STATIC
physics_move = False
class StaticTileObject(GameObject):
collision_shape_type = CST_TILE
collision_type = CT_GENERIC_STATIC
physics_move = False
y_sort = True
class StaticBoxObject(GameObject):
"Generic static world object with AABB-based (rectangle) collision"
collision_shape_type = CST_AABB
collision_type = CT_GENERIC_STATIC
physics_move = False
class DynamicBoxObject(GameObject):
collision_shape_type = CST_AABB
collision_type = CT_GENERIC_DYNAMIC
y_sort = True
class Pickup(GameObject):
collision_shape_type = CST_CIRCLE
collision_type = CT_GENERIC_DYNAMIC
y_sort = True
attachment_classes = {"shadow": "BlobShadow"}
class Projectile(GameObject):
"Generic projectile class"
fast_move_steps = 1
collision_type = CT_GENERIC_DYNAMIC
collision_shape_type = CST_CIRCLE
move_accel_x = move_accel_y = 400.0
noncolliding_classes = ["Projectile"]
lifespan = 10.0
"Projectiles should be transient, limited max life"
should_save = False
def __init__(self, world, obj_data=None):
GameObject.__init__(self, world, obj_data)
self.fire_dir_x, self.fire_dir_y = 0, 0
def fire(self, firer, dir_x=0, dir_y=1):
self.set_loc(firer.x, firer.y, firer.z)
self.reset_last_loc()
self.fire_dir_x, self.fire_dir_y = dir_x, dir_y
def update(self):
if (self.fire_dir_x, self.fire_dir_y) != (0, 0):
self.move(self.fire_dir_x, self.fire_dir_y)
GameObject.update(self)
class Character(GameObject):
"Generic character class"
state_changes_art = True
stand_if_not_moving = True
move_state = "walk"
"Move state name - added to valid_states in init so subclasses recognized"
collision_shape_type = CST_CIRCLE
collision_type = CT_GENERIC_DYNAMIC
def __init__(self, world, obj_data=None):
if self.move_state not in self.valid_states:
self.valid_states.append(self.move_state)
GameObject.__init__(self, world, obj_data)
# assume that character should start idling, if its art animates
if self.art.frames > 0:
self.start_animating()
def update_state(self):
GameObject.update_state(self)
if self.state_changes_art and abs(self.vel_x) > 0.1 or abs(self.vel_y) > 0.1:
self.state = self.move_state
class Player(Character):
"Generic player class"
log_move = False
collision_type = CT_PLAYER
editable = Character.editable + [
"move_accel_x",
"move_accel_y",
"ground_friction",
"air_friction",
"bounciness",
"stop_velocity",
]
def pre_first_update(self):
if self.world.player is None:
self.world.player = self
if self.world.player_camera_lock:
self.world.camera.focus_object = self
else:
self.world.camera.focus_object = None
def button_pressed(self, button_index):
pass
def button_unpressed(self, button_index):
pass
class TopDownPlayer(Player):
y_sort = True
attachment_classes = {"shadow": "BlobShadow"}
facing_changes_art = True
def get_facing_dir(self):
return FACING_DIRS[self.facing]
class WorldPropertiesObject(GameObject):
"Special magic singleton object that stores and sets GameWorld properties"
art_src = "world_properties_object"
visible = deleteable = selectable = False
locked = True
physics_move = False
exclude_from_object_list = True
exclude_from_class_list = True
world_props = [
"game_title",
"gravity_x",
"gravity_y",
"gravity_z",
"hud_class_name",
"globals_object_class_name",
"camera_x",
"camera_y",
"camera_z",
"bg_color_r",
"bg_color_g",
"bg_color_b",
"bg_color_a",
"player_camera_lock",
"object_grid_snap",
"draw_hud",
"collision_enabled",
"show_collision_all",
"show_bounds_all",
"show_origin_all",
"show_all_rooms",
"room_camera_changes_enabled",
"draw_debug_objects",
]
"""
Properties we serialize on behalf of GameWorld
TODO: figure out how to make these defaults sync with those in GW?
"""
serialized = world_props
editable = []
"All visible properties are serialized, not editable"
def __init__(self, world, obj_data=None):
GameObject.__init__(self, world, obj_data)
world_class = type(world)
for v in self.serialized:
if obj_data and v in obj_data:
# if world instance has property from loaded data, use it
if hasattr(self.world, v):
setattr(self.world, v, obj_data[v])
setattr(self, v, obj_data[v])
# use world class (default) property if loaded data lacks it
elif hasattr(world_class, v):
setattr(self, v, getattr(world_class, v))
else:
# set explicitly as float, for camera & bg color
setattr(self, v, 0.0)
# special handling of bg color (a list)
self.world.bg_color = [
self.bg_color_r,
self.bg_color_g,
self.bg_color_b,
self.bg_color_a,
]
self.world.camera.set_loc(self.camera_x, self.camera_y, self.camera_z)
# TODO: figure out why collision_enabled seems to default False!
def set_object_property(self, prop_name, new_value):
setattr(self, prop_name, new_value)
# special handling for some values, eg bg color and camera
if prop_name.startswith("bg_color_"):
component = {"r": 0, "g": 1, "b": 2, "a": 3}[prop_name[-1]]
self.world.bg_color[component] = float(new_value)
elif prop_name.startswith("camera_") and len(prop_name) == len("camera_x"):
setattr(self.world.camera, prop_name[-1], new_value)
# some properties have unique set methods in GW
elif prop_name == "show_collision_all":
self.world.toggle_all_collision_viz()
elif prop_name == "show_bounds_all":
self.world.toggle_all_bounds_viz()
elif prop_name == "show_origin_all":
self.world.toggle_all_origin_viz()
elif prop_name == "player_camera_lock":
self.world.toggle_player_camera_lock()
# normal properties you can just set: set em
elif hasattr(self.world, prop_name):
setattr(self.world, prop_name, new_value)
def update_from_world(self):
self.camera_x = self.world.camera.x
self.camera_y = self.world.camera.y
self.camera_z = self.world.camera.z
class WorldGlobalsObject(GameObject):
"""
Invisible object holding global state, variables etc in GameWorld.globals.
Subclass can be specified in WorldPropertiesObject.
NOTE: this object is spawned from scratch every load, it's never serialized!
"""
should_save = False
visible = deleteable = selectable = False
locked = True
exclude_from_object_list = True
exclude_from_class_list = True
physics_move = False
serialized = []
editable = []
class LocationMarker(GameObject):
"Very simple GameObject that marks an XYZ location for eg camera points"
art_src = "loc_marker"
serialized = ["name", "x", "y", "z", "visible", "locked"]
editable = []
alpha = 0.5
physics_move = False
is_debug = True
class StaticTileTrigger(GameObject):
"""
Generic static trigger with tile-based collision.
Overlaps but doesn't collide.
"""
is_debug = True
collision_shape_type = CST_TILE
collision_type = CT_GENERIC_STATIC
noncolliding_classes = ["GameObject"]
physics_move = False
serialized = ["name", "x", "y", "z", "art_src", "visible", "locked"]
def started_overlapping(self, other):
# self.app.log('Trigger overlapped with %s' % other.name)
pass
class WarpTrigger(StaticTileTrigger):
"Trigger that warps object to a room/marker when they touch it."
is_debug = True
art_src = "trigger_default"
alpha = 0.5
destination_marker_name = None
"If set, warp to this location marker"
destination_room_name = None
"If set, make this room the world's current"
use_marker_room = True
"If True, change to destination marker's room"
warp_class_names = ["Player"]
"List of class names to warp on contact with us."
serialized = StaticTileTrigger.serialized + [
"destination_room_name",
"destination_marker_name",
"use_marker_room",
]
def __init__(self, world, obj_data=None):
StaticTileTrigger.__init__(self, world, obj_data)
self.warp_classes = [
self.world.get_class_by_name(class_name)
for class_name in self.warp_class_names
]
def started_overlapping(self, other):
if other.warped_recently():
return
# bail if object's class isn't allowed
valid_class = False
for c in self.warp_classes:
if isinstance(other, c):
valid_class = True
break
if not valid_class:
return
if self.destination_room_name:
if other is self.world.player:
# if overlapping object is player, change current room
# to destination room
self.world.change_room(self.destination_room_name)
else:
# if object is only in one room, move them to destination room
if len(other.rooms) == 1:
old_room = other.rooms.values()[0]
old_room.remove_object(other)
self.destination_room.add_object(other)
elif self.destination_marker_name:
marker = self.world.objects.get(self.destination_marker_name, None)
if not marker:
self.app.log(
"Warp destination object {} not found".format(
self.destination_marker_name
)
)
return
other.set_loc(marker.x, marker.y, marker.z)
# warp to marker's room if specified, pick a random one if multiple
if self.use_marker_room and len(marker.rooms) == 1:
room = random.choice(list(marker.rooms.values()))
# warn if both room and marker are set but they conflict
if (
self.destination_room_name
and room.name != self.destination_room_name
):
self.app.log(
"Marker {}'s room differs from destination room {}".format(
marker.name, self.destination_room_name
)
)
self.world.change_room(room.name)
other.last_warp_update = self.world.updates
class ObjectSpawner(LocationMarker):
"Simple object that spawns an object when triggered"
is_debug = True
spawn_class_name = None
spawn_obj_name = ""
spawn_random_in_bounds = False
"If True, spawn somewhere in this object's bounds, else spawn at location"
spawn_obj_data = {}
"Dict of properties to set on newly spawned object"
times_to_fire = -1
"Number of times we can fire, -1 = infinite"
trigger_on_room_enter = True
"Set False for any subclass that triggers in some other way"
destroy_on_room_exit = True
"if True, spawned object will be destroyed when player leaves its room"
serialized = LocationMarker.serialized + [
"spawn_class_name",
"spawn_obj_name",
"times_to_fire",
"destroy_on_room_exit",
]
def __init__(self, world, obj_data=None):
LocationMarker.__init__(self, world, obj_data)
self.times_fired = 0
# list of objects we've spawned
self.spawned_objects = []
def get_spawn_class_name(self):
"Return class name of object to spawn."
return self.spawn_class_name
def get_spawn_location(self):
"Return x,y location we should spawn a new object at."
if not self.spawn_random_in_bounds:
return self.x, self.y
left, top, right, bottom = self.get_edges()
x = left + random.random() * (right - left)
y = top + random.random() * (bottom - top)
return x, y
def can_spawn(self):
"Return True if spawner is allowed to spawn."
return True
def do_spawn(self):
"Spawn and returns object."
class_name = self.get_spawn_class_name()
if not class_name:
return None
x, y = self.get_spawn_location()
new_obj = self.world.spawn_object_of_class(class_name, x, y)
if self.spawn_obj_name:
self.world.rename_object(new_obj, self.spawn_obj_name)
# new object should be in same rooms as us
new_obj.rooms.update(self.rooms)
self.spawned_objects.append(new_obj)
# save a reference to us, the spawner
new_obj.spawner = self
# TODO: put new object in our room(s), apply spawn_obj_data
return new_obj
def trigger(self):
"Poke this spawner to do its thing, returns an object if spawned"
if self.times_to_fire != -1 and self.times_fired >= self.times_to_fire:
return None
if not self.can_spawn():
return None
if self.times_fired != -1:
self.times_fired += 1
return self.do_spawn()
def room_entered(self, room, old_room):
if self.trigger_on_room_enter:
self.trigger()
def room_exited(self, room, new_room):
if not self.destroy_on_room_exit:
return
for obj in self.spawned_objects:
obj.destroy()
class SoundBlaster(LocationMarker):
"Simple object that plays sound when triggered"
is_debug = True
sound_name = ""
"String name of sound to play, minus any extension"
can_play = True
"If False, won't play sound when triggered"
play_on_room_enter = True
loops = -1
"Number of times to loop, if -1 loop indefinitely"
serialized = LocationMarker.serialized + [
"sound_name",
"can_play",
"play_on_room_enter",
]
def __init__(self, world, obj_data=None):
LocationMarker.__init__(self, world, obj_data)
# find file, try common extensions
for ext in ["", ".ogg", ".wav"]:
filename = self.sound_name + ext
if self.world.sounds_dir and os.path.exists(
self.world.sounds_dir + filename
):
self.sound_filenames[self.sound_name] = filename
return
self.world.app.log(
"Couldn't find sound file {} for SoundBlaster {}".format(
self.sound_name, self.name
)
)
def room_entered(self, room, old_room):
self.play_sound(self.sound_name, self.loops)
def room_exited(self, room, new_room):
self.stop_sound(self.sound_name)