def __init__(self, negative, positive, *axis, smooth=0.1): """ An input axis taking values between -1 and 1. Callbacks are disconnected with -= :param negative: keycode or list of keycodes :param positive: keycode or list of keycodes :param axis: any number of JoyAxis :param smooth: Duration (s) to smooth values """ if isinstance(negative, int): negative = [negative] if isinstance(positive, int): positive = [positive] self._negative = {KeyPress(n): False for n in negative} self._positive = {KeyPress(p): False for p in positive} self._axis = set(axis) self._callbacks = Signal() self._smooth = smooth self.non_zero_time = 0 self.zero_time = 0 # Hold the number of keys pressed self._int_value = 0 # Hold the smoothed number of keys pressed self._value = 0 # Hold the total value of axis, # separately because of different tracking methods self._axis_value = 0
def __init__(self, initial_state): """ The main beginning of our application. Initializes pygame and the initial state. """ # pygame.mixer.pre_init(44100, 16, 2, 4096) pygame.init() self.size = ivec2(1920, 1080) / 2 """Display size""" self.cache = {} """Resources with filenames as keys""" pygame.display.set_caption("Butterfly Destroyers") self.screen = pygame.display.set_mode(self.size) self.on_event = Signal() self.quit = False self.clock = pygame.time.Clock() self.inputs = Inputs() self.time = 0 self.dirty = True self.data = {} # data persisting between modes # self.keys = [False] * self.MAX_KEYS self._state = None self.last_state = None self.next_state = initial_state self.process_state_change()
class State: def __init__(self, app, state=None, script=None, **kwargs): self.app = app self.state = state # parent state script = kwargs.get("script") self.scripts = Signal(lambda fn: Script(self.app, self, fn)) self.script = None if isinstance(script, str): # load script from string 'scripts/' folder self.script = script self.scripts += self.script if callable(self): # use __call__ as script self.script = self self.scripts += self def update(self, dt): if self.scripts: self.scripts.each(lambda x, dt: x.update(dt), dt) self.scripts.slots = list( filter(lambda x: not x.get().done(), self.scripts.slots)) def render(self): pass def pend(self): pass
def __init__(self, *keys): """ A boolean input. :param keys: any number of keycodes or ButtonInputs """ self._keys: Set[ButtonInput] = { KeyPress(key) if isinstance(key, int) else key for key in keys } self._pressed = {} self.just_released = False self.just_pressed = False self.just_double_pressed = False self._always = Signal() self._on_press = Signal() self._on_release = Signal() self._on_double_press = Signal() self._repeat = Signal() # _repeat[callback] = [delay, trigger_count] self.last_press = float("-inf") """Time since last release of the button""" self.press_time = 0 self.dt = 0 # time since last frame """
def __init__(self, app, ctx, script, use_input=True, script_args=None): self.app = app self.ctx = ctx self.when = When() self.slots = [] self.paused = False self.dt = 0 self.fn = script self.resume_condition = None self.script_args = script_args self.scripts = Signal() # extra scripts attached to this one # these are accumulated between yields # this is different from get_pressed() self.keys = set() self.keys_down = set() self.keys_up = set() self.use_input = use_input if use_input: self.event_slot = self.app.on_event.connect(self.event) else: self.event_slot = None # Is True while the script is not yielding # Meaning if a script calls something, .inside is True during that call # Useful for checking assert for script-only functions self.inside = False self.script = script # (this calls script property)
def test_scene(): scene = Signal() ent = Entity(None, scene) slot = scene.connect(ent) ent.slot = weakref.ref(slot) assert len(scene) == 1 ent.remove() assert len(scene) == 0
def __init__(self, app, state=None, script=None, **kwargs): self.app = app self.state = state # parent state script = kwargs.get("script") self.scripts = Signal(lambda fn: Script(self.app, self, fn)) self.script = None if isinstance(script, str): # load script from string 'scripts/' folder self.script = script self.scripts += self.script if callable(self): # use __call__ as script self.script = self self.scripts += self
def test_signal(): s = Signal() hello = s.connect(lambda: print("hello ", end=""), weak=False) s.connect(lambda: print("world"), weak=False) assert len(s) == 2 s() # 'hello world' assert s.disconnect(hello) s() # 'world' assert len(s) == 1 s.clear() assert len(s) == 0
def test_signal_queue(): # queued connection s = Signal() s.blocked += 1 a = s.connect(lambda: print("queued"), weak=False) assert len(s.queued) == 1 s() # nothing s.blocked -= 1 for slot in s.queued: slot() s.queued = [] s() # "queued" # queued disconnection s.blocked += 1 a.disconnect() assert len(s) == 1 # still attached assert len(s.queued) == 1 s.blocked -= 1 for q in s.queued: q() s.queued = [] assert len(s) == 0
def test_signal_weak(): s = Signal() w = s.connect(lambda: print("test")) del w assert len(s) == 0 s() assert len(s) == 0 s = Signal() w = s.connect(lambda: print("test")) del s # slot outlives signal? assert w.sig() is None # it works del w
def test_scene_blocked(): scene = Signal() ent = Entity(None, scene) scene.blocked += 1 slot = scene.connect(ent) scene.blocked -= 1 ent.slot = weakref.ref(slot) assert len(scene) == 0 scene.refresh() assert len(scene) == 1 scene.blocked += 1 ent.remove() scene.blocked -= 1 assert len(scene) == 1 scene.refresh() assert len(scene) == 0
class Entity: """ A basic component of the game scene. An Entity represents something that will be draw on the screen. """ def __init__(self, app, scene, filename=None, **kwargs): # print(type(self)) self.app: "App" = app self.scene = scene self.slot = None # weakref self.slots = SlotList() self.scripts = Signal( lambda fn: Script(self.app, self, fn, use_input=False)) self.life = kwargs.pop("life", None) # particle life (length of time to exist) self.on_move = Signal() self.on_update = Signal() self.on_remove = Signal() # self.dirty = True self._surface = None self.removed = False self.parent = kwargs.pop("parent", None) self.sounds = {} self.particle = kwargs.pop("particle", None) self.visible = True self._script_func = False script = kwargs.pop("script", None) self.script = None # main script self._position = kwargs.pop("position", vec3(0)) self.velocity = kwargs.pop("velocity", vec3(0)) self.acceleration = kwargs.pop("acceleration", vec3(0)) # solid means its collision-checked against other things # has_collision means the entity has a collision() callback self.has_collision = hasattr(self, "collision") self.solid = self.has_collision # if self.has_collision: # print(self, 'has collision') # if self.solid: # print(self, 'is solid') self.filename = filename if filename: self._surface = self.app.load_img(filename, kwargs.pop("scale", 1)) self.collision_size = self.size = estimate_3d_size( self._surface.get_size()) else: self.collision_size = self.size = vec3(0) self.render_size = vec3(0) """Should hold the size in pixel at which the entity was last rendered""" if hasattr(self, "event"): self.slots += app.add_event_listener(self) if isinstance(script, str): # load script from string 'scripts/' folder self.script = script self.scripts += self.script if callable(self): # use __call__ as script self.script = self self.scripts += self ai = kwargs.pop("ai", None) self.ai: "AI" = ai(self) if ai else None if kwargs: raise ValueError( "kwrgs for Entity have not all been consumed. Left:", kwargs) def clear_scripts(self): self.scripts = Signal( lambda fn: Script(self.app, self, fn, use_input=False)) # def add_script(self, fn): # """ # :param fn: add script `fn` (cls, func, or filename) # """ # self.scripts += script # return script def __str__(self): return f"{self.__class__.__name__}(pos: {self.position}, id: {id(self)})" # def once(self, duration, func) # """ # A weakref version of scene.when.once. # Used for safely triggering temp one-time events w/o holding the slot. # """ # return self.scene.when.once( # duration, # lambda wself=weakref.ref(self): func(wself), # weak=False # ) @property def position(self): return self._position @position.setter def position(self, v): """ Sets position of our entity, which controls where it appears in our scene. :param v: 3 coordinates (list, tuple, vec3) """ if len(v) == 2: print("Warning: Setting Entity position with a 2d vector.") print("Vector:", v) print("Entity:", self) raise ValueError if v is None: v = vec3(0) if v.x != v.x: raise ValueError self._position = vec3(*v) self.on_move() @property def velocity(self): return self._velocity @velocity.setter def velocity(self, value): assert value == value self._velocity = value def remove(self): if not self.removed: # for slot in self.slots: # slot.disconnect() self.slots = [] self.on_remove() if self.slot: # weird bug (?): # fail (1 pos but 2 given): # self.scene.disconnect(self.slot): # fail: missing require pos 'slot' # self.scene.disconnect() s = self.slot() if s: s.disconnect() self.removed = True # def disconnect(self): # self.remove() # NOTE: Implementing the below method automatically registers event listener # So it's commented out. It still works as before. # def event(self, event): # """ # Handle the event if needed. # :returns: True if the event was handled # """ # return False def play_sound(self, filename, callback=None, *args): """ Play sound with filename. Triggers callback when sound is done Forwards *args to channel.play() """ if filename in self.sounds: self.sounds[filename][1].stop() del self.sounds[filename] filename = path.join(SOUNDS_DIR, filename) sound = self.app.load(filename, lambda: pygame.mixer.Sound(filename)) if not sound: return None, None, None channel = pygame.mixer.find_channel() if not channel: return None, None, None channel.set_volume(SOUND_VOLUME) if callback: slot = self.scene.when.once(self.sounds[0].get_length(), callback) self.slots.add(slot) else: slot = None self.sounds[filename] = (sound, channel, slot) channel.play(sound, *args) return sound, channel, slot def update(self, dt): # if len(self.slots) > 10: # print(len(self.slots)) if self.ai: self.ai.update(self, dt) if self.acceleration != vec3(0): self.velocity += self.acceleration * dt if self.velocity != vec3(0): self.position += self.velocity * dt if self.life is not None: self.life -= dt if self.life <= 0: self.remove() return if self.scripts: self.scripts.each(lambda x, dt: x.update(dt), dt) self.scripts.slots = list( filter(lambda x: not x.get().done(), self.scripts.slots)) if self.slots: self.slots._slots = list( filter(lambda slot: not slot.once or not slot.count, self.slots._slots)) self.on_update(dt) def render(self, camera, surf=None, pos=None, scale=True, fade=True, cull=False, big=False): """ Tries to renders surface `surf` from camera perspective If `surf` is not provided, render self._surface (loaded from filename) """ if not self.visible: return if not pos: pos = self.position pp = self.scene.player.position if self.scene.player else vec4(0) if cull: if pos.x < pp.x - 1000 or pos.x > pp.x + 1000: self.remove() return surf: SurfaceType = surf or self._surface if not surf: self.render_size = None return half_diag = vec3(-surf.get_width(), surf.get_height(), 0) / 2 world_half_diag = camera.rel_to_world(half_diag) - camera.position pos_tl = camera.world_to_screen(pos + world_half_diag) pos_bl = camera.world_to_screen(pos - world_half_diag) if None in (pos_tl, pos_bl): # behind the camera self.scene.remove(self) return size = ivec2(pos_bl.xy - pos_tl.xy) self.render_size = size if not scale or 400 > size.x > 0 or big: if scale: # print(ivec2(size)) surf = pygame.transform.scale(surf, ivec2(size)) # don't fade close sprites far = abs(pos.z - pp.z) > 1000 if fade and far: max_fade_dist = camera.screen_dist * FULL_FOG_DISTANCE alpha = surf_fader(max_fade_dist, camera.distance(pos)) # If fade is integer make it bright faster alpha = clamp(int(alpha * fade), 0, 255) if surf.get_flags() & pygame.SRCALPHA: surf.fill((255, 255, 255, alpha), None, pygame.BLEND_RGBA_MULT) else: surf.set_alpha(alpha) surf.set_colorkey(0) # if not far: # if not 'Rain' in str(self) and not 'Rock' in str(self): # print('skipped fade', self) self.app.screen.blit(surf, ivec2(pos_tl))
def clear_scripts(self): self.scripts = Signal( lambda fn: Script(self.app, self, fn, use_input=False))
class App: STATES = { "intro": Intro, "game": Game, "menu": Menu, "intermission": Intermission, "credits": Credits, } # MAX_KEYS = 512 def __init__(self, initial_state): """ The main beginning of our application. Initializes pygame and the initial state. """ # pygame.mixer.pre_init(44100, 16, 2, 4096) pygame.init() self.size = ivec2(1920, 1080) / 2 """Display size""" self.cache = {} """Resources with filenames as keys""" pygame.display.set_caption("Butterfly Destroyers") self.screen = pygame.display.set_mode(self.size) self.on_event = Signal() self.quit = False self.clock = pygame.time.Clock() self.inputs = Inputs() self.time = 0 self.dirty = True self.data = {} # data persisting between modes # self.keys = [False] * self.MAX_KEYS self._state = None self.last_state = None self.next_state = initial_state self.process_state_change() def load(self, filename, resource_func): """ Attempt to load a resource from the cache, otherwise, loads it :param resource_func: a function that loads the resource if its not already available in the cache """ if filename not in self.cache: r = self.cache[filename] = resource_func() return r return self.cache[filename] def load_img(self, filename, scale=1, flipped=False): """ Load the image at the given path in a pygame surface. The file name is the name of the file without the full path. Files are looked for in the SPRITES_DIR Results are cached. Scale is an optional integer to scale the image by a given factor. """ def load_fn(): img = pygame.image.load(os.path.join(SPRITES_DIR, filename)) if scale != 1: w, h = img.get_size() img = pygame.transform.scale(img, ivec2(vec2(w, h) * scale)) if flipped: img = pygame.transform.flip(img, True, False) return img return self.load((filename, scale, flipped), load_fn) # def pend(self): # self.dirty = True def run(self): """ Main game loop. Runs until the `quit` flag is set Runs update(dt) and render() of the current game state (default: Game) """ last_t = time.time_ns() accum = 0 self.fps = 0 frames = 0 dt = 0 self.inputs.event([]) while (not self.quit) and self.state: cur_t = time.time_ns() dt += (cur_t - last_t) / (1000 * 1000 * 1000) # if dt < 0.001: # time.sleep(1 / 300) # continue # accumulate dt for skipped frames last_t = cur_t accum += dt frames += 1 if accum > 1: self.fps = frames frames = 0 accum -= 1 events = pygame.event.get() for event in events: if event.type == pygame.QUIT: return 0 self.on_event(event) self.inputs.event(events) if self.state is None: break if DEBUG: print("FRAME, dt =", dt, "FPS,", self.fps) self.inputs.update(dt) if self.update(dt) is False: break if self.render() is False: break dt = 0 # reset to accumulate def add_event_listener(self, obj): slot = self.on_event.connect(obj.event) obj.slots.append(slot) return slot def update(self, dt): """ Called every frame to update our game logic :param dt: time since last frame in seconds :return: returns False to quit gameloop """ if not self.state: return False if self.next_state: self.process_state_change() self.state.update(dt) def render(self): """ Called every frame to render our game state and update pygame display :return: returns False to quit gameloop """ # if not self.dirty: # return # self.dirty = False if self.state is None: return False self.state.render() pygame.display.update() @property def state(self): return self._state @state.setter def state(self, s): """ Schedule state change on next frame """ self.next_state = s def process_state_change(self): """ Process pending state changes """ lvl = None try: lvl = int(self.next_state) pass except ValueError: pass if lvl: stats = self.data["stats"] = self.data.get("stats", Stats()) stats.level = lvl self.next_state = "game" if self.next_state: self._state = self.STATES[self.next_state.lower()](self) self.next_state = None
def test_signal_once(): s = Signal() w = s.once(lambda: print("test")) assert len(s.slots) == 1 s()
class Axis: def __init__(self, negative, positive, *axis, smooth=0.1): """ An input axis taking values between -1 and 1. Callbacks are disconnected with -= :param negative: keycode or list of keycodes :param positive: keycode or list of keycodes :param axis: any number of JoyAxis :param smooth: Duration (s) to smooth values """ if isinstance(negative, int): negative = [negative] if isinstance(positive, int): positive = [positive] self._negative = {KeyPress(n): False for n in negative} self._positive = {KeyPress(p): False for p in positive} self._axis = set(axis) self._callbacks = Signal() self._smooth = smooth self.non_zero_time = 0 self.zero_time = 0 # Hold the number of keys pressed self._int_value = 0 # Hold the smoothed number of keys pressed self._value = 0 # Hold the total value of axis, # separately because of different tracking methods self._axis_value = 0 def __str__(self): return f"Axis({self.value})" @property def value(self): return clamp(self._value + self._axis_value, -1, 1) def always_call(self, callback): return self._callbacks.connect(callback) def __isub__(self, callback): return self._callbacks.disconnect(callback) def update(self, dt): """Trigger all callbacks and updates times""" if self._int_value != 0: # Nonzero check is okay as JoyAxis already count the threshold self.non_zero_time += dt self.zero_time = 0 else: self.non_zero_time = 0 self.zero_time += dt if self._smooth <= 0: self._value = self._int_value else: dv = dt / self._smooth if self._int_value > 0: self._value += dv elif self._int_value < 0: self._value -= dv else: if self._value > 0: self._value -= dv else: self._value += dv if abs(self._value) <= dv: # To have hard zeros self._value = 0 self._value = clamp(self._value, -1, 1) self._callbacks(self) def event(self, events): axis_value = 0 any_axis = False for event in events: for pos in self._positive: if pos.match(event): self._positive[pos] = pos.pressed(event) for neg in self._negative: if neg.match(event): self._negative[neg] = neg.pressed(event) for axis in self._axis: if axis.match(event): # We take the most extreme value val = axis.value(event) if abs(val) > abs(axis_value): axis_value = val any_axis = True self._int_value = sum(self._positive.values()) - sum(self._negative.values()) if any_axis: self._axis_value = axis_value
class Button: def __init__(self, *keys): """ A boolean input. :param keys: any number of keycodes or ButtonInputs """ self._keys: Set[ButtonInput] = { KeyPress(key) if isinstance(key, int) else key for key in keys } self._pressed = {} self.just_released = False self.just_pressed = False self.just_double_pressed = False self._always = Signal() self._on_press = Signal() self._on_release = Signal() self._on_double_press = Signal() self._repeat = Signal() # _repeat[callback] = [delay, trigger_count] self.last_press = float("-inf") """Time since last release of the button""" self.press_time = 0 self.dt = 0 # time since last frame """ Time the button has been pressed. If it isn't pressed, it is the duration of the last press. """ def update(self, dt): """Trigger all callbacks and updates times""" self.last_press += dt if self.pressed: self.press_time += dt self.dt = dt self._always(self) if self.just_pressed: self._on_press(self) if self.just_double_pressed: self._on_double_press(self) if self.just_released: self._on_release(self) if self.pressed: self._repeat.blocked += 1 for wref in self._repeat.slots: c = wref() if not c: continue if c.delay * c.repetitions <= self.press_time: # It isn;t possible to set it directly, I don't know why c.repetitions += 1 c(self) self._repeat.blocked -= 1 self._repeat.refresh() def event(self, events): self.just_pressed = False self.just_double_pressed = False self.just_released = False old_pressed = self.pressed for event in events: for key in self._keys: if key.match(event): self._pressed[key] = key.pressed(event) if not old_pressed: if self.pressed: self.press_time = 0 self.just_pressed = True if self.double_pressed: self.just_double_pressed = True else: if not self.pressed: # All keys were just released self.last_press = 0 self.just_released = True for wref in self._repeat.slots: c = wref() if not c: continue c.repetitions = 0 @property def pressed(self): """Whether the button is actually pressed.""" return sum(self._pressed.values(), 0) > 0 @property def double_pressed(self): """Whether the button was just double pressed""" return self.pressed and self.last_press < 0.1 def always_call(self, callback): return self._always.connect(callback) def on_press(self, callback): return self._on_press.connect(callback) def on_release(self, callback): return self._on_release.connect(callback) def on_double_press(self, callback): return self._on_double_press.connect(callback) def on_press_repeated(self, callback, delay): """ Call `callback` when the button is pressed and every `delay` seconds while it is pressed. Note: the same function cannot be a repeat callback for two different things. """ slot = self._repeat.connect(callback) slot.delay = delay slot.repetitions = 0 return slot def disconnect(self, callback): """Remove a callback from all types if present.""" if callback in self._always: self._always.disconnect(callback) if callback in self._on_press: self._on_press.disconnect(callback) if callback in self._on_release: self._on_release.disconnect(callback) if callback in self._on_double_press: self._on_double_press.disconnect(callback) if callback in self._repeat: self._on_double_press.disconnect(callback)
def __init__(self, app, scene, filename=None, **kwargs): # print(type(self)) self.app: "App" = app self.scene = scene self.slot = None # weakref self.slots = SlotList() self.scripts = Signal( lambda fn: Script(self.app, self, fn, use_input=False)) self.life = kwargs.pop("life", None) # particle life (length of time to exist) self.on_move = Signal() self.on_update = Signal() self.on_remove = Signal() # self.dirty = True self._surface = None self.removed = False self.parent = kwargs.pop("parent", None) self.sounds = {} self.particle = kwargs.pop("particle", None) self.visible = True self._script_func = False script = kwargs.pop("script", None) self.script = None # main script self._position = kwargs.pop("position", vec3(0)) self.velocity = kwargs.pop("velocity", vec3(0)) self.acceleration = kwargs.pop("acceleration", vec3(0)) # solid means its collision-checked against other things # has_collision means the entity has a collision() callback self.has_collision = hasattr(self, "collision") self.solid = self.has_collision # if self.has_collision: # print(self, 'has collision') # if self.solid: # print(self, 'is solid') self.filename = filename if filename: self._surface = self.app.load_img(filename, kwargs.pop("scale", 1)) self.collision_size = self.size = estimate_3d_size( self._surface.get_size()) else: self.collision_size = self.size = vec3(0) self.render_size = vec3(0) """Should hold the size in pixel at which the entity was last rendered""" if hasattr(self, "event"): self.slots += app.add_event_listener(self) if isinstance(script, str): # load script from string 'scripts/' folder self.script = script self.scripts += self.script if callable(self): # use __call__ as script self.script = self self.scripts += self ai = kwargs.pop("ai", None) self.ai: "AI" = ai(self) if ai else None if kwargs: raise ValueError( "kwrgs for Entity have not all been consumed. Left:", kwargs)
class Script: def __init__(self, app, ctx, script, use_input=True, script_args=None): self.app = app self.ctx = ctx self.when = When() self.slots = [] self.paused = False self.dt = 0 self.fn = script self.resume_condition = None self.script_args = script_args self.scripts = Signal() # extra scripts attached to this one # these are accumulated between yields # this is different from get_pressed() self.keys = set() self.keys_down = set() self.keys_up = set() self.use_input = use_input if use_input: self.event_slot = self.app.on_event.connect(self.event) else: self.event_slot = None # Is True while the script is not yielding # Meaning if a script calls something, .inside is True during that call # Useful for checking assert for script-only functions self.inside = False self.script = script # (this calls script property) def push(self, fn): print(fn) if self.script_args: script = Script(self.app, self.ctx, fn, self.use_input, *self.script_args) else: script = Script(self.app, self.ctx, fn, self.use_input) self.scripts += script def pause(self): self.paused = True def resume(self): self.paused = False def event(self, ev): if ev.type == pygame.KEYDOWN: self.keys_down.add(ev.key) self.keys.add(ev.key) elif ev.type == pygame.KEYUP: self.keys_up.add(ev.key) try: self.keys.remove(ev.key) except KeyError: pass def running(self): return self._script is not None def done(self): return self._script is None def key(self, k): # if we're in a script: return keys since last script yield # assert self.script.inside assert self.inside # please only use this in scripts assert self.event_slot # input needs to be enabled (default) if isinstance(k, str): return ord(k) in self.keys return k in self.keys def key_down(self, k): # if we're in a script: return keys since last script yield # assert self.script.inside assert self.inside # please only use this in scripts assert self.event_slot # input needs to be enabled (default) if isinstance(k, str): return ord(k) in self.keys_down return k in self.keys_down def key_up(self, k): # if we're in a script: return keys since last script yield # assert self.script.inside assert self.inside # please only use this in scripts assert self.event_slot # input needs to be enabled (default) if isinstance(k, str): return ord(k) in self.keys_up return k in self.keys_up # This makes scripting cleaner than checking script.keys directly # We need these so scripts can do "keys = script.keys" # and then call keys(), since it changes # def keys(self): # # return key downs since last script yield # assert self.inside # please only use this in scripts # assert self.event_slot # input needs to be enabled (default) # return self._keys # def keys_up(self): # # return key ups since last script yield # assert self.inside # please only use this in scripts # assert self.event_slot # input needs to be enabled (default) # return self._key_up @property def script(self): return self._script @script.setter def script(self, script=None): # print("Script:", script, self.script_args) self.slots = [] self.paused = False if isinstance(script, str): lib = importlib.import_module("game.scripts." + script) run = False if not hasattr(lib, "run"): # no run method? look for cls for name, cls in lib.__dict__.items(): if name.startswith("Level"): try: int(name[len("Level") :]) except ValueError: continue # not a level number if self.script_args: self._script = iter(cls(*self.script_args, self)) else: self._script = iter(cls(self)) break else: self.inside = True if self.script_args: self._script = run(*self.script_args, self) else: self._script = run(self) self.inside = False # self.locals = {} # exec(open(path.join(SCRIPTS_DIR, script + ".py")).read(), globals(), self.locals) # if "run" not in self.locals: # assert False # self.inside = True # self._script = self.locals["run"](self.app, self.ctx, self) elif isinstance(script, type): # So we can pass a Level class if self.script_args: self._script = iter(script(*self.script_args, self)) else: self._script = iter(script(self)) elif callable(script): # function if self.script_args: self._script = script(*self.script_args, self) else: self._script = script(self) elif script is None: self._script = None else: raise TypeError def sleep(self, t): return self.when.once(t, self.resume) def update(self, dt): # accumulate dt between yields self.dt += dt self.when.update(dt) if self.resume_condition: if self.resume_condition(): self.resume() ran_script = False # continue running script (until yield or end) if self._script and not self.paused: try: self.inside = True slot = next(self._script) ran_script = True self.inside = False if isinstance(slot, Slot): self.slots.append(slot) self.pause() elif slot: # func? self.resume_condition = slot if not self.resume_condition(): self.pause() else: pass except StopIteration: # print("Script Finished") # traceback.print_exc() self._script = None # except Exception: # traceback.print_exc() # self._script = None # extra scripts if self.scripts: print("scripts") self.scripts.each(lambda x, dt: x.update(dt), dt) self.scripts.slots = list( filter(lambda x: not x.get().done(), self.scripts.slots) ) self.inside = False if ran_script: # clear accumulated keys self.key_down = set() self.key_up = set() self.dt = 0 return ran_script
def __init__(self, app, state, script=None, script_args=None): super().__init__() self.max_particles = 32 self.app = app self.state = state self.when = When() self.slotlist = SlotList() self._sky_color = None self.ground = None self._ground_color = None self._script = None self.scripts = Signal(lambda fn: Script(self.app, self, fn)) self.lightning_slot = None self.lightning_density = 0 self.player = None self.rock_slot = None self.rain_slot = None self.has_clouds = False self.lowest_fps = 1000 self.on_render = Signal() # self.script_paused = False # self.script_slots = [] # self.stars_visible = 0 # star_density = 80 # self.star_pos = [ # (randint(0, 200), randint(0, 200)) for i in range(star_density) # ] # color change delays when using opt funcs self.delay_t = 0 self.delay = 0.5 self.time = 0 self.sky_color = None # self.ground_color = GREEN self.dt = 0 self.sounds = {} # self.script_fn = script # self.event_slot = self.app.on_event.connect(self.even) # self.script_resume_condition = None # The below wrapper is just to keep the interface the same with signal # on_collision.connect -> on_collision_connect # class CollisionSignal: # pass # self.on_collision = CollisionSignal() # self.on_collision.connect = self.on_collision_connect # self.on_collision.once = self.on_collision_once # self.on_collision.enter = self.on_collision_enter # self.on_collision.leave = self.on_collision_leave self._music = None if script: self.script = script # trigger setter self.when.every(1, self.stabilize, weak=False)
class Scene(Signal): def __init__(self, app, state, script=None, script_args=None): super().__init__() self.max_particles = 32 self.app = app self.state = state self.when = When() self.slotlist = SlotList() self._sky_color = None self.ground = None self._ground_color = None self._script = None self.scripts = Signal(lambda fn: Script(self.app, self, fn)) self.lightning_slot = None self.lightning_density = 0 self.player = None self.rock_slot = None self.rain_slot = None self.has_clouds = False self.lowest_fps = 1000 self.on_render = Signal() # self.script_paused = False # self.script_slots = [] # self.stars_visible = 0 # star_density = 80 # self.star_pos = [ # (randint(0, 200), randint(0, 200)) for i in range(star_density) # ] # color change delays when using opt funcs self.delay_t = 0 self.delay = 0.5 self.time = 0 self.sky_color = None # self.ground_color = GREEN self.dt = 0 self.sounds = {} # self.script_fn = script # self.event_slot = self.app.on_event.connect(self.even) # self.script_resume_condition = None # The below wrapper is just to keep the interface the same with signal # on_collision.connect -> on_collision_connect # class CollisionSignal: # pass # self.on_collision = CollisionSignal() # self.on_collision.connect = self.on_collision_connect # self.on_collision.once = self.on_collision_once # self.on_collision.enter = self.on_collision_enter # self.on_collision.leave = self.on_collision_leave self._music = None if script: self.script = script # trigger setter self.when.every(1, self.stabilize, weak=False) def iter_entities(self, *types): for slot in self.slots: ent = slot.get() if ent and isinstance(ent, types): yield ent def cloudy(self): if self.has_clouds: return pv = self.player.velocity if self.player else vec3(0) if hasattr(self.app.state, "player"): velz = pv.z else: velz = 0 for i in range(30): x = randint(-3000, 3000) y = randint(0, 300) z = randint(-4000, -1300) pos = vec3(x, y, z) self.add(Cloud(self.app, self, pos, velz)) self.has_clouds = True def lightning_strike(self): oldsky = vec4(self.sky_color) if self.sky_color else None self.sky_color = "white" self.when.once(0.1, lambda oldsky=oldsky: self.set_sky_color(oldsky), weak=False) self.play_sound("lightning.wav") def lightning_script(self, script): yield while True: if self.lowest_fps <= 25: break yield script.sleep(1) yield script.sleep((1 / self.lightning_density) * random.random()) self.lightning_strike() def lightning(self, density=0.5): if density < EPSILON: self.lightning_slot = None return if self.lowest_fps <= 25: return self.lightning_density = density self.lightning_slot = self.scripts.connect(self.lightning_script) def add_rock(self): if self.lowest_fps <= 25: return velz = self.player.velocity.z if self.player else vec3(0) x = randint(-500, 500) y = GROUND_HEIGHT - 15 z = -4000 ppos = self.player.position if self.player else vec3(0) pos = vec3(ppos.x, 0, ppos.z) + vec3(x, y, z) self.add(Rock(self.app, self, pos, velz)) def add_rain_drop(self): velz = self.player.velocity.z if self.player else 0 x = randint(-400, 400) y = randint(0, 300) z = randint(-3000, -2000) ppos = self.player.position if self.player else vec3(0) pos = vec3(ppos.x, 0, ppos.z) + vec3(x, y, z) self.add(Rain(self.app, self, pos, velz, particle=True)) def rain(self, density=25): if density: self.rain_slot = self.when.every(1 / density, self.add_rain_drop) else: self.rain_slot = None def rocks(self, density=10): if density: self.rock_slot = self.when.every(1 / density, self.add_rock) else: self.rock_slot = None def stars(self): if hasattr(self.app.state, "player"): velz = self.player.velocity.z else: velz = 0 for i in range(50): x = randint(-500, 500) y = -200 + (random.random()**0.5 * 800) z = -3000 pos = vec3(x, y, z) self.add(Star(self.app, self, pos, velz)) def draw_sky(self): width, height = self.app.size / 8 self.sky = pygame.Surface((width, height)) sky_color = self.sky_color or ncolor(pygame.Color("blue")) sky_color = [255 * s for s in sky_color] # for y in range(height): # interp = (1 - y / height) * 2 # for x in range(width): # rand = rand_RGB() # color = rgb_mix(sky_color, rand, 0.02) # c = (sk * 0.98 + rand * 0.02) / i**1.1 # if interp == 0: # color = (255, 255, 255) # else: # color = [min(int(c / interp ** 1.1), 255) for c in color] # self.sky.set_at((x, y), color) # Draw gradient for y in range(height): interp = (1 - y / height) * 2 base = [min(int(c / interp**1.1), 255) for c in sky_color] pygame.draw.line(self.sky, base, (0, y), (width, y)) noise = noise_surf_dense_bottom(self.sky.get_size(), random.randrange(5)) self.sky.blit( noise, (0, 0), None, ) self.sky = pygame.transform.scale(self.sky, self.app.size) # if self.stars_visible: # self.draw_stars(self.sky, self.star_pos) self.sky = pygame.transform.scale(self.sky, self.app.size) # def draw_stars(self, surface, star_positions): # size = 1 # for pos in star_positions: # star = pygame.Surface((size, size)) # star.fill((255, 255, 255)) # star.set_alpha(175) # surface.blit(star, pos) def remove_sound(self, filename): if filename in self.sounds: self.sounds[filename][0].stop() del self.sounds[filename] return True return False def ensure_sound(self, filename, callback=None, *args): """ Ensure a sound is playing. If it isn't, play it. """ if filename in self.sounds: return None, None, None return self.play_sound(filename, callback, *args) def play_sound(self, filename, callback=None, *args): """ Plays the sound with the given filename (relative to SOUNDS_DIR). Returns sound, channel, and callback slot. """ if filename in self.sounds: self.sounds[filename][0].stop() del self.sounds[filename] filename = path.join(SOUNDS_DIR, filename) sound = self.app.load(filename, lambda: pygame.mixer.Sound(filename)) if not sound: return None, None, None channel = pygame.mixer.find_channel() if not channel: return None, None, None channel.set_volume(SOUND_VOLUME) if callback: self.when.once(self.sounds[0].get_length(), callback, weak=False) else: slot = None self.sounds[filename] = (sound, channel, slot) channel.play(sound, *args) self.when.once(sound.get_length(), lambda: self.remove_sound(sound), weak=False) return sound, channel, slot @property def script(self): return self._script @script.setter def script(self, fn): self._script = (Script( self.app, self, fn, use_input=True, script_args=(self.app, self)) if fn else None) @property def music(self): return self._music @property def ground_color(self): return self._ground_color @ground_color.setter def ground_color(self, color): if color is None: return self._ground_color = ncolor(color) if not self.ground: self.ground = self.add(Ground(self.app, self, GROUND_HEIGHT)) self.ground.color = color if "ROCK" in self.app.cache: del self.app.cache["ROCK"] # rocks need to reload their color @music.setter def music(self, filename): self._music = filename if self._music: pygame.mixer.music.load(path.join(MUSIC_DIR, filename)) pygame.mixer.music.play(-1) def on_collision_connect(self, A, B, func, once=True): """ during collision (touching) """ pass def on_collision_once(self, A, B, func, once=True): """ trigger only once """ pass def on_collision_enter(self, A, B, func, once=True): """ trigger upon enter collision """ pass def on_collision_leave(self, A, B, func, once=True): """ trigger upon leave collision """ pass # @property # def script(self): # return self.script_fn # @script.setter # def script(self, script): # print("Script:",script) # if isinstance(script, str): # local = {} # exec(open(path.join(SCRIPTS_DIR, script + ".py")).read(), globals(), local) # self._script = local["script"](self.app, self) # elif isinstance(script, type): # # So we can pass a Level class # self._script = iter(script(self.app, self)) # elif script is None: # self._script = None # else: # raise TypeError # def sleep(self, t): # return self.when.once(t, self.resume) def add(self, entity): slot = self.connect(entity, weak=False) entity.slot = weakref.ref(slot) # self.slotlist += slot return entity @property def sky_color(self): return self._sky_color @sky_color.setter def sky_color(self, c): self._sky_color = ncolor(c) if c else None self.draw_sky() # reset ground gradient (depend on sky color) self.ground_color = self._ground_color # for scripts to call when.fade(1, set_sky_color) def set_sky_color(self, c): self.sky_color = ncolor(c) if c else None def set_sky_color_opt(self, c): """ Optimized for fades. """ if self.delay_t > EPSILON: return False # print("delay") self.delay_t = self.delay self._sky_color = ncolor(c) if c else None if self._sky_color: self.draw_sky() # reset ground gradient (depend on sky color) self.set_ground_color_opt(self.ground_color) return True def set_ground_color(self, c): self.ground_color = ncolor(c) if c else None def set_ground_color_opt(self, c): """ Optimized for fades. """ if not self.ground: self.ground = self.add(Ground(self.app, self, GROUND_HEIGHT)) if c: self.ground.fade_opt(ncolor(c)) else: self.ground = None def remove(self, entity): # self.slotlist -= entity super().disconnect(entity) # def resume(self): # self.script_paused = False def invalid_size(self, size): """Checks component for 0 or NaNs""" return any(c != c or abs(c) < EPSILON for c in size) def update_collisions(self, dt): # cause all scene operations to be queueed self.blocked += 1 for slot in self.slots: a = slot.get() # only check if a is solid if not a or not a.solid: continue if self.invalid_size(a.collision_size): continue # for each slot, loop through each slot for slot2 in self.slots: b = slot2.get() # only check if b is solid if not b or not b.solid: continue if slot is not slot2: if not a.has_collision and not b.has_collision: continue if self.invalid_size(b.collision_size): continue a_min = a.position - a.collision_size / 2 a_max = a.position + a.collision_size / 2 b_min = b.position - b.collision_size / 2 b_max = b.position + b.collision_size / 2 col = not (b_min.x > a_max.x or b_max.x < a_min.x or b_min.y > a_max.y or b_max.y < a_min.y or b_min.z > a_max.z or b_max.z < a_min.z) if col: if a.has_collision: a.collision(b, dt) if b.has_collision: b.collision(a, dt) self.blocked -= 1 # run pending slot queue if self.blocked == 0: self.refresh() def stabilize(self): """" Stablize FPS by setting max partilces Called every second (see when.once(1, self.stabilize in init) """ if self.app.fps > 1: self.lowest_fps = min(self.app.fps, self.lowest_fps) if self.app.fps < 45: self.max_particles = max(self.max_particles / 2, 4) if self.lowest_fps >= 60: if self.app.fps > 120: self.max_particles = min(self.max_particles * 2, 64) def filter_script(self, slot): if isinstance(slot, weakref.ref): wref = slot slot = wref() if not slot: return False if slot.get().done(): return False return True def update(self, dt): self.time += dt # print(self.time) self.delay_t = max(0, self.delay_t - dt) # do time-based events self.when.update(dt) self.update_collisions(dt) self.refresh() # main level script if self._script: self._script.update(dt) # extra scripts if self.scripts: self.scripts.each(lambda x, dt: x.update(dt), dt) self.scripts.slots = list( filter(self.filter_script, self.scripts.slots)) # self.sort(lambda a, b: a.z < b.z) # self.slots = sorted(self.slots, key=z_compare) self.slots.sort(key=z_compare) self.slots = list(filter(lambda x: not x.get().removed, self.slots)) # call update(dt) on each entity self.each(lambda x, dt: x.update(dt), dt) # update particles (remove if too many) self.blocked += 1 particle_count = 0 for i, slot in enumerate(reversed(self.slots)): e = slot.get() if e.particle: particle_count += 1 if particle_count >= self.max_particles: slot.disconnect() self.blocked -= 1 self.clean() def render(self, camera): # call render(camera) on all scene entities if self.sky_color is not None: self.app.screen.blit(self.sky, (0, 0)) # call render on each entity self.each(lambda x: x.render(camera)) self.on_render(camera)