コード例 #1
0
ファイル: inputs.py プロジェクト: PythonixCoders/PyWeek29
    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
コード例 #2
0
ファイル: app.py プロジェクト: PythonixCoders/PyWeek29
    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()
コード例 #3
0
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
コード例 #4
0
ファイル: inputs.py プロジェクト: PythonixCoders/PyWeek29
    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
        """
コード例 #5
0
ファイル: script.py プロジェクト: PythonixCoders/PyWeek29
    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)
コード例 #6
0
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
コード例 #7
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
コード例 #8
0
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
コード例 #9
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
コード例 #10
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
コード例 #11
0
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
コード例 #12
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))
コード例 #13
0
 def clear_scripts(self):
     self.scripts = Signal(
         lambda fn: Script(self.app, self, fn, use_input=False))
コード例 #14
0
ファイル: app.py プロジェクト: PythonixCoders/PyWeek29
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
コード例 #15
0
def test_signal_once():

    s = Signal()
    w = s.once(lambda: print("test"))
    assert len(s.slots) == 1
    s()
コード例 #16
0
ファイル: inputs.py プロジェクト: PythonixCoders/PyWeek29
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
コード例 #17
0
ファイル: inputs.py プロジェクト: PythonixCoders/PyWeek29
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)
コード例 #18
0
    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)
コード例 #19
0
ファイル: script.py プロジェクト: PythonixCoders/PyWeek29
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
コード例 #20
0
    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)
コード例 #21
0
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)