- new level prototype: asterism
authorFrank DeMarco <if.self.end@gmail.com>
Thu, 5 Mar 2020 08:23:00 +0000 (03:23 -0500)
committerFrank DeMarco <if.self.end@gmail.com>
Thu, 5 Mar 2020 08:23:00 +0000 (03:23 -0500)
- dialog code added but commented out for now

12 files changed:
Cakewalk.py
config
lib/pgfw
resource/BPmono-LICENSE.txt [new file with mode: 0755]
resource/TeX_Gyre_Scholar-LICENSE.txt [new file with mode: 0644]
resource/TeX_Gyre_Scholar-bolditalic.ttf [new file with mode: 0644]
resource/background/1-far-stars.png [new file with mode: 0644]
resource/background/2-close-stars.png [new file with mode: 0644]
resource/background/3-planets.png [new file with mode: 0644]
resource/cursor/1-open.png [new file with mode: 0644]
resource/cursor/2-closed.png [new file with mode: 0644]
resource/dialog_box.png [new file with mode: 0644]

index f578348..2b0abc5 100644 (file)
@@ -1,5 +1,7 @@
+# -*- coding: utf-8 -*-
+
 from random import randint, choice
-import math, os
+import math, os, random
 
 import pygame
 from pygame.locals import *
@@ -15,6 +17,19 @@ from lib.pgfw.pgfw.extension import *
 
 LEVEL_POINTS = (
 
+    # (
+    #     (106.000000, 451.000000),
+    #     (265.000000, 374.000000),
+    #     (885.000000, -416.000000),
+    #     (739.000000, 357.000000),
+    #     (-364.000000, -36.333333),
+    #     (96.000000, 38.333333),
+    #     (725.000000, 113.000000),
+    #     (891.000000, 133.666667),
+    #     (1208.000000, 255.333333),
+    #     (-594.000000, 444.000000),
+    # ),
+
     (
         (53.000000, 413.000000),
         (150.000000, 400.000000),
@@ -210,6 +225,19 @@ LEVEL_POINTS = (
     ),
 
     (
+        (106.000000, 451.000000),
+        (265.000000, 374.000000),
+        (885.000000, -416.000000),
+        (739.000000, 357.000000),
+        (-338.000000, -36.333333),
+        (96.000000, 38.333333),
+        (725.000000, 113.000000),
+        (891.000000, 133.666667),
+        (1208.000000, 255.333333),
+        (-594.000000, 444.000000),
+    ),
+
+    (
         (67.000000, 312.000000),
         (191.000000, 302.000000),
         (311.000000, 208.000000),
@@ -320,6 +348,7 @@ TITLE_POINTS = (
     Vector(805.000000, 272.000000),
 )
 
+
 class Cakewalk(Game, Animation):
 
     def __init__(self):
@@ -328,6 +357,7 @@ class Cakewalk(Game, Animation):
         Animation.__init__(self, self)
         self.subscribe(self.respond, MOUSEBUTTONDOWN)
         self.subscribe(self.respond, MOUSEBUTTONUP)
+        self.subscribe(self.respond, MOUSEMOTION)
         self.subscribe(self.respond, KEYDOWN)
         self.subscribe(self.respond, KEYUP)
         self.subscribe(self.respond, JOYBUTTONDOWN)
@@ -338,52 +368,38 @@ class Cakewalk(Game, Animation):
         self.end_active = False
         self.curve_color = pygame.Color(255, 128, 128)
         self.curve_color_offset = 0
-        # self.control_points = [
-        #     Vector(100, 100), Vector(150, 400), Vector(450, 400), Vector(500, 150)
-        # ]
         self.levels = []
         for ii, group in enumerate(LEVEL_POINTS):
             names = ["close circuit", "fishy", "heraldic", "climb",
-                     "writing on the wall", "peek", "radar", "wood", "farewell"]
+                     "writing on the wall", "peek", "radar", "wood", "asterism",
+                     "farewell"]
             self.levels.append(Level(self, group, names[ii]))
         self.current_level_index = 0
-        # self.control_points = []
         dsr = self.get_display_surface().get_rect()
-        base = pygame.image.load(self.get_resource("background.png")).convert()
-        width = dsr.w + base.get_width() - dsr.w % base.get_width()
-        height = dsr.h + base.get_height() - dsr.h % base.get_height()
-        self.background = Sprite(self)
-        frame = pygame.Surface((width, height))
-        fill_tile(frame, base)
-        self.background = []
-        self.background_size = Vector(self)
-        for x in range(0, width + base.get_width(), base.get_width()):
-            for y in range(0, height + base.get_height(), base.get_height()):
-                tile = Sprite(self)
-                tile.add_frame(base)
-                tile.location.topleft = x, y
-                self.background.append(tile)
-        self.background_size.x = x + base.get_width()
-        self.background_size.y = y + base.get_height()
-        # for ii in range(10):
-        #     self.control_points.append(Vector(randint(0, dsr.w), randint(0, dsr.h)))
-        # field = pygame.Rect(-50, -50, dsr.w + 100, dsr.h + 100)
-        # while len(self.control_points) < 7:
-        #     point = Vector(randint(field.left, field.right), randint(field.top, field.bottom))
-        #     valid = True
-        #     for other in self.control_points:
-        #         if get_distance(point, other) < 200:
-        #             valid = False
-        #             break
-        #     if valid:
-        #         self.control_points.append(point)
-        pygame.mouse.set_visible(False)
-        pygame.event.set_grab(False)
+        self.background_layers = []
+        self.background_color_conversion = {(79, 79, 79, 255): (255, 255, 255, 255)}
+        for ii, name in enumerate(("1-far-stars.png", "2-close-stars.png", "3-planets.png")):
+            tile = pygame.image.load(self.get_resource(os.path.join("background", name))).convert()
+            pixels = pygame.PixelArray(tile)
+            for row in pixels:
+                for pixel in row:
+                    color = tuple(tile.unmap_rgb(pixel))
+                    if color != (255, 255, 255, 255) and color != (79, 79, 79, 255):
+                        self.background_color_conversion[color] = get_inverted_color(color)
+            pixels.close()
+            del pixels
+            width = dsr.w + tile.get_width() * 2 - dsr.w % tile.get_width()
+            height = dsr.h + tile.get_height() * 2 - dsr.h % tile.get_height()
+            layer = Sprite(self)
+            surface = pygame.Surface((width, height))
+            if ii != 0:
+                surface.set_colorkey((255, 255, 255))
+            fill_tile(surface, tile)
+            layer.add_frame(surface)
+            self.background_layers.append(layer)
+        self.background_tile_size = Vector(*tile.get_size())
         self.mouse = Vector(*self.get_display_surface().get_rect().center)
         self.character = Sprite(self)
-        # frame = pygame.Surface((20, 20), SRCALPHA)
-        # frame.fill(pygame.Color("purple"))
-        # self.character.add_frame(frame)
         self.character.load_from_path(self.get_resource("cake-frames"), True)
         self.character.add_frameset([0], name="standing")
         self.character.add_frameset(range(1, 9), name="walking", switch=True, framerate=200)
@@ -392,11 +408,12 @@ class Cakewalk(Game, Animation):
         self.character_accelerating = False
         self.editing = False
         self.bezier_resolution = 60
+        self.previous_curve_hue = 0
         self.set_curve()
         self.set_enemies()
         self.enemy_types = Slicer, Fish, Projector
         self.enemy_type_index = 0
-        print("currently selected enemy type: %s" % self.enemy_types[self.enemy_type_index])
+        self.set_edit_mode_menu()
         self.title_music = pygame.mixer.Sound(self.get_resource("azu menu music.ogg"))
         self.level_music = pygame.mixer.Sound(self.get_resource("azu main theme.ogg"))
         self.title_music.play(-1, 0, 3000)
@@ -435,11 +452,65 @@ class Cakewalk(Game, Animation):
         blank.fill((255, 255, 255, 255))
         for frame in get_blinds_frames(blank, .04, 5):
             self.wipe.add_frame(frame)
-        self.register(self.explode, self.blow_up)
+        self.dialog_box = Sprite(self)
+        self.dialog_box.load_from_path(self.get_resource("dialog_box.png"), True)
+        self.dialog_box.location.midbottom = dsr.centerx, 449
+        self.register(self.explode, self.continue_dialog, self.blow_up, self.reveal_dialog_text,
+                      self.cancel_grab)
         self.get_configuration().type_declarations.add("bool", "effects", "crumbs")
+        for point in TITLE_POINTS:
+            point.marker = Marker(self, point)
+        self.cursor = Cursor(self)
         self.reset()
         pygame.event.clear()
 
+    def cancel_grab(self):
+        # print(pygame.mouse.get_pos())
+        pygame.event.set_grab(False)
+        # print(pygame.mouse.get_pos())
+
+    def set_edit_mode_menu(self):
+        enemy_type = self.enemy_types[self.enemy_type_index]
+        if enemy_type == Slicer:
+            self.edit_mode_selected_enemy = Slicer(self, 0)
+        elif enemy_type == Fish:
+            self.edit_mode_selected_enemy = Fish(self, 0)
+        elif enemy_type == Projector:
+            self.edit_mode_selected_enemy = Projector(self, self.mouse.copy())
+        font = pygame.font.Font(self.get_resource("BPmono.ttf"), 12)
+        text = {
+            "CTRL+T": "active enemy",
+            "SHIFT+CLICK": "add points",
+            "SHIFT+ALT+CLICK": "remove points",
+            "CTRL+CLICK": "add enemy",
+            "CTRL+ALT+CLICK": "remove enemy",
+            "CTRL+P": "print",
+            "CTRL+LEFT/RIGHT": "level",
+            "CTRL+E": "toggle edit",
+            "CTRL+UP/DOWN": "enemy speed",
+            "CTRL+SHIFT+UP/DOWN": "enemy distance"
+            }
+        self.edit_menu = Sprite(self)
+        frame = pygame.Surface((self.get_display_surface().get_width(), 24))
+        self.edit_menu.add_frame(frame)
+        frame.fill(pygame.Color("black"))
+        x = self.edit_mode_selected_enemy.location.w
+        y = 0
+        ii = 0
+        for combo, action in text.items():
+            fg = (pygame.Color("black"), pygame.Color("white"))[ii % 2]
+            bg = (pygame.Color("white"), pygame.Color("black"))[ii % 2]
+            combo_surface = render_box(font, combo, True, fg, bg, padding=(3, 0))
+            action_surface = render_box(font, action, True, fg, bg, padding=(3, 0))
+            if x + combo_surface.get_width() + action_surface.get_width() > frame.get_width():
+                x = 0
+                y += 12
+            for surface in combo_surface, action_surface:
+                frame.blit(surface, (x, y))
+                x += surface.get_width()
+            ii += 1
+        frame.set_alpha(128)
+
     def reset(self):
         self.title_active = True
         self.end_active = False
@@ -454,8 +525,10 @@ class Cakewalk(Game, Animation):
             self.title_music.stop()
             self.title_music.play(-1)
         self.set_level(0)
+        self.cancel_grab()
         self.character_speed = 1
         self.time_elapsed = 0
+        self.is_dialog = False
         path = "resource/high-scores"
         self.high_scores = []
         if os.path.exists(path):
@@ -483,7 +556,7 @@ class Cakewalk(Game, Animation):
     def get_control_points(self):
         if self.title_active:
             return TITLE_POINTS
-        elif self.end_active:
+        elif self.end_active or self.is_dialog:
             return []
         else:
             return self.get_current_level().control_points
@@ -501,20 +574,34 @@ class Cakewalk(Game, Animation):
         return self.levels[self.current_level_index]
 
     def respond(self, event):
-        if event.type == MOUSEBUTTONDOWN and event.button == 1 and self.editing:
-            found = False
-            for p in self.get_control_points():
-                translated = self.translate_point_to_screen(p)
-                if abs(translated.x - self.mouse.x) < 10 and abs(translated.y - self.mouse.y) < 10:
-                    self.selected = p
-                    found = True
-            if not found and pygame.key.get_mods() & KMOD_SHIFT:
-                points = get_points_on_line(self.get_control_points()[-1], self.mouse, 4)
-                self.get_control_points().extend(points[1:])
+        if event.type == MOUSEBUTTONDOWN and event.button == 1 and (self.editing or self.title_active):
+            found = self.find_grabbable_control_point()
+            if found:
+                self.selected = found
+                self.cursor.set_frameset("closed")
+                if self.editing:
+                    self.selected.marker.blink()
+                else:
+                    self.selected.marker.grab()
+            if not found and pygame.key.get_mods() & KMOD_SHIFT and self.editing:
+                if pygame.key.get_mods() & KMOD_ALT:
+                    for _ in range(3):
+                        if len(self.get_control_points()) > 4:
+                            self.get_control_points().pop()
+                else:
+                    points = get_points_on_line(self.get_control_points()[-1], self.mouse, 4)
+                    for point in points:
+                        point.marker = Marker(self, point)
+                        point.marker.grab()
+                    self.get_control_points().extend(points[1:])
                 self.set_curve()
                 self.reset_enemies()
-            if not found and pygame.key.get_mods() & KMOD_CTRL:
+            if not found and pygame.key.get_mods() & KMOD_CTRL and self.editing:
                 if pygame.key.get_mods() & KMOD_ALT:
+                    closest = self.get_closest_enemy()
+                    if closest is not None:
+                        self.enemies.remove(closest)
+                else:
                     min_distance = None
                     for ii, point in enumerate(self.curve):
                         translated = self.translate_point_to_screen(point)
@@ -531,10 +618,6 @@ class Cakewalk(Game, Animation):
                     elif enemy_type == Projector:
                         self.enemies.append(Projector(self, self.mouse.copy()))
                     self.reset_enemies()
-                else:
-                    closest = self.get_closest_enemy()
-                    if closest is not None:
-                        self.enemies.remove(closest)
         elif event.type == JOYBUTTONDOWN:
             self.handle_action_button_press()
         elif event.type == JOYBUTTONUP:
@@ -542,13 +625,23 @@ class Cakewalk(Game, Animation):
         elif event.type == KEYDOWN:
             if event.key == K_e and pygame.key.get_mods() & KMOD_CTRL:
                 self.editing = not self.editing
+                self.set_curve()
+                for point in self.get_control_points():
+                    if self.editing:
+                        point.marker.grab()
+                    else:
+                        point.marker.hide()
+                # if self.editing:
+                #     pygame.event.set_grab(True)
+                # else:
+                #     pygame.event.set_grab(False)
             elif event.key == K_SPACE and not self.is_playing(self.blow_up):
                 self.handle_action_button_press()
             elif event.key == K_p and pygame.key.get_mods() & KMOD_CTRL and self.editing:
                 for point in self.get_control_points():
                     print(point)
                 for enemy in self.enemies:
-                    if isinstance(enemy, Projectile) and isinstance(enemy, Fire):
+                    if not isinstance(enemy, Projectile) and not isinstance(enemy, Fire):
                         print(enemy)
             elif event.key == K_RIGHT and pygame.key.get_mods() & KMOD_CTRL:
                 self.increment_level(1)
@@ -558,7 +651,7 @@ class Cakewalk(Game, Animation):
                 closest = self.get_closest_enemy()
                 if closest is not None:
                     mod = 1 if event.key == K_UP else -1
-                    if pygame.key.get_mods() & KMOD_ALT:
+                    if pygame.key.get_mods() & KMOD_SHIFT:
                         if isinstance(closest, Slicer):
                             closest.increase_stray(2.5 * mod)
                         elif isinstance(closest, Fish):
@@ -575,14 +668,46 @@ class Cakewalk(Game, Animation):
                     self.reset_enemies()
             elif event.key == K_t and pygame.key.get_mods() & KMOD_CTRL and self.editing:
                 self.enemy_type_index = (self.enemy_type_index + 1) % len(self.enemy_types)
-                print("currently selected enemy type: %s" % self.enemy_types[self.enemy_type_index])
+                self.set_edit_mode_menu()
         elif event.type == KEYUP and event.key == K_SPACE and not self.is_playing(self.blow_up):
             self.handle_action_button_release()
         elif event.type == MOUSEBUTTONUP and event.button == 1:
-            self.selected = None
+            if self.selected is not None:
+                if self.editing:
+                    self.selected.marker.grab()
+                else:
+                    self.selected.marker.hide()
+                self.selected = None
+                if not self.editing:
+                    self.set_curve(True)
+        elif event.type == MOUSEMOTION:
+            self.halt(self.cancel_grab)
+            mouse_motion = Vector(*event.rel)
+            if abs(mouse_motion.x) + abs(mouse_motion.y) < 25:
+                dsr = self.get_display_surface().get_rect()
+                self.mouse += mouse_motion
+                if self.mouse.x > dsr.w:
+                    self.mouse.x -= dsr.w
+                elif self.mouse.x < 0:
+                    self.mouse.x += dsr.w
+                if self.mouse.y > dsr.h:
+                    self.mouse.y -= dsr.h
+                elif self.mouse.y < 0:
+                    self.mouse.y += dsr.h
+                if self.selected is not None:
+                    self.selected += mouse_motion
+            pygame.event.set_grab(True)
+            self.play(self.cancel_grab, delay=800, play_once=True)
         elif self.get_delegate().compare(event, "reset-game"):
             self.reset()
 
+    def find_grabbable_control_point(self):
+        for p in self.get_control_points():
+            translated = self.translate_point_to_screen(p)
+            if get_distance(translated, self.mouse) < 10:
+                return p
+        return None
+
     def handle_action_button_press(self):
         if self.title_active:
             self.title_active = False
@@ -593,6 +718,16 @@ class Cakewalk(Game, Animation):
         elif self.end_active:
             self.end_active = False
             self.reset()
+        elif self.is_dialog:
+            if self.is_playing(self.reveal_dialog_text):
+                self.reveal_all_text()
+            elif self.dialog_line_index == len(self.dialog) - 1:
+                self.play(self.blow_up, increment=1)
+            else:
+                self.dialog_line_index += 1
+                self.dialog_text = self.dialog[self.dialog_line_index]
+                self.dialog_text_index = 0
+                self.play(self.reveal_dialog_text)
         else:
             self.character_accelerating = True
 
@@ -603,7 +738,7 @@ class Cakewalk(Game, Animation):
         min_distance = None
         closest = None
         for enemy in self.enemies:
-            if isinstance(enemy, Projectile) and isinstance(enemy, Fire):
+            if not isinstance(enemy, Projectile) and not isinstance(enemy, Fire):
                 center = self.translate_point_to_screen(enemy.center)
                 distance = get_distance(center, self.mouse)
                 if min_distance is None or distance < min_distance:
@@ -623,23 +758,33 @@ class Cakewalk(Game, Animation):
         if not self.end_active:
             self.set_goal(start)
 
-    def set_curve(self):
+    def set_curve(self, use_previous_color=False):
         self.curve = []
         points = self.get_control_points()
         for ii in range(0, len(points) - 3, 3):
             self.curve.extend(compute_bezier_points(points[ii:ii + 4], self.bezier_resolution))
         self.curve_plate = Sprite(self)
         light_colors = []
-        start = randint(0, 300)
+        if self.editing or self.selected is not None or use_previous_color:
+            start = self.previous_curve_hue
+        else:
+            start = randint(0, 359)
+        self.previous_curve_hue = start
         for hue in range(start, start + 60, 4):
             light_colors.append(get_hsla_color(hue, 100, 50, 100))
         groups = self.translate_points_to_screen(self.curve)
-        for offset in range(30):
+        if self.editing or self.selected is not None:
+            frame_count = 1
+        else:
+            frame_count = 30
+        for offset in range(frame_count):
             frame = pygame.Surface(self.get_display_surface().get_size(), SRCALPHA)
             self.curve_plate.add_frame(frame)
             for group in groups:
                 if group:
                     # pygame.draw.lines(frame, self.curve_color, False, group, 7)
+                    # for ii in range(2, len(group) - 1, 2):
+                    #     pygame.draw.aaline(frame, (255, 255, 255), group[ii], group[ii - 2], 0)
                     for ii, point in enumerate(group[:-1]):
                         # angle = get_angle(point, group[ii + 1]) + math.pi * .5
                         # left_start = [int(round(n)) for n in get_endpoint(point, angle - math.pi * .5, 3, False)]
@@ -653,6 +798,7 @@ class Cakewalk(Game, Animation):
                         # right_start = [int(round(n)) for n in get_endpoint(point, angle + math.pi * .5, 3, False)]
                         # right_end = [int(round(n)) for n in get_endpoint(group[ii + 1], angle + math.pi * .5, 3, False)]
                         # pygame.draw.line(frame, color, right_start, right_end, 2)
+                        pygame.draw.aaline(frame, color, point, group[ii + 1], 1)
                         pygame.draw.line(frame, color, point, group[ii + 1], 1)
                         # right = get_endpoint(point, angle + math.pi * .75, 5, False)
                         # pygame.draw.line(ds, [self.curve_color, pygame.Color("white")][(ii + self.curve_color_offset) % 2],
@@ -748,6 +894,11 @@ class Cakewalk(Game, Animation):
                     self.time_result.location.midtop = dsr.centerx, 287
                     self.level_music.fadeout(2000)
                     self.title_music.play(-1, 0, 500)
+                # if increment and self.current_level_index == 0 and not self.is_dialog and not self.title_active:
+                #     self.start_dialog()
+                # else:
+                    # self.is_dialog = False
+                    # self.halt(self.continue_dialog)
                 self.increment_level(increment)
         else:
             intermediate = pygame.Surface(self.get_display_surface().get_size(), SRCALPHA)
@@ -761,37 +912,132 @@ class Cakewalk(Game, Animation):
                 self.wipe.halt()
                 self.halt(self.blow_up)
 
+    def start_dialog(self):
+        self.return_character_to_beginning()
+        self.is_dialog = True
+        self.character_pos.place(290, 250)
+        self.dialog_enemy = Fish(self, 0)
+        for ii in range(len(self.dialog_enemy.frames)):
+            for _ in range(4):
+                self.dialog_enemy.frames[ii] = pygame.transform.scale2x(
+                    self.dialog_enemy.frames[ii])
+        self.dialog_enemy.location.center = 430, 90
+        self.dialog = (
+            # "heh heh heh",
+            # "I need these roads for my transactions"
+            # "I am a sovereign merchant so I am above the law",
+            # "take the long way around or go through the sun lol"
+            "what are you transporting by the way",
+            "oh living bodies",
+            "heh heh heh",
+            "I actually get a good price on skeletons rofl"
+        )
+        self.dialog_text = self.dialog[0]
+        self.dialog_line_index = 0
+        self.dialog_text_index = 0
+        self.dialog_enemy.mirror()
+        self.set_curve()
+        if self.character.mirrored:
+            self.character.mirror()
+        self.play(self.continue_dialog)
+        self.play(self.reveal_dialog_text)
+
+    def continue_dialog(self):
+        self.dialog_enemy.update(act=False)
+        frame = self.dialog_box.get_current_frame().copy()
+        subsurface = self.get_display_surface().subsurface(self.dialog_box.location)
+        frame.blit(subsurface, (0, 0), None, BLEND_RGBA_MIN)
+        frame_pixels = pygame.PixelArray(frame)
+        for color, replacement in self.background_color_conversion.items():
+            frame_pixels.replace(color, replacement)
+        frame_pixels.close()
+        del frame_pixels
+        # if not self.is_playing(self.blow_up):
+        #     dialog_box_pixels = pygame.PixelArray(frame)
+        #     bg_pixels = pygame.PixelArray(
+        #         self.get_display_surface().subsurface(self.dialog_box.location))
+        #     for x in range(len(dialog_box_pixels)):
+        #         for y in range(len(dialog_box_pixels[0])):
+        #             if bg_pixels[x][y] != 0x707070:
+        #                 bg_hsva = frame.unmap_rgb(bg_pixels[x][y]).hsva
+        #                 dialog_box_hsva = frame.unmap_rgb(dialog_box_pixels[x][y]).hsva
+        #                 color = get_hsva_color(
+        #                     (bg_hsva[0] + 180) % 360, bg_hsva[1], dialog_box_hsva[2], dialog_box_hsva[3])
+        #                 dialog_box_pixels[x][y] = color
+        #     del dialog_box_pixels
+        #     del bg_pixels
+        self.get_display_surface().blit(frame, self.dialog_box.location)
+        font = pygame.font.Font(self.get_resource("BPmono.ttf"), 39)
+        text = self.dialog_text[:self.dialog_text_index + 1]
+        offset = 1
+        for color in pygame.Color(203, 107, 19), pygame.Color(255, 104, 104):
+            layer = Sprite(self)
+            surface = get_wrapped_text_surface(font, text, frame.get_width() - 50,
+                                               True, color)
+            layer.add_frame(surface)
+            layer.location.center = self.dialog_box.location.center
+            layer.move(offset, offset)
+            layer.update()
+            offset -= 1
+
+    def reveal_dialog_text(self):
+        self.dialog_text_index += 1
+        if self.dialog_text_index >= len(self.dialog_text):
+            self.reveal_all_text()
+
+    def reveal_all_text(self):
+        self.dialog_text_index = len(self.dialog_text) - 1
+        self.halt(self.reveal_dialog_text)
+
     def update(self):
         ds = self.get_display_surface()
         dsr = ds.get_rect()
-        for tile in self.background:
-            tile.move(-2, -2)
-            if tile.location.right <= 0:
-                tile.move(self.background_size.x, 0)
-            if tile.location.bottom <= 0:
-                tile.move(0, self.background_size.y)
-            tile.update()
-        if self.editing:
-            mouse_motion = Vector(*pygame.mouse.get_rel())
-            self.mouse += mouse_motion
-            if self.mouse.x > dsr.w:
-                self.mouse.x -= dsr.w
-            elif self.mouse.x < 0:
-                self.mouse.x += dsr.w
-            if self.mouse.y > dsr.h:
-                self.mouse.y -= dsr.h
-            elif self.mouse.y < 0:
-                self.mouse.y += dsr.h
+        # for tile in self.background:
+        #     tile.move(-2, -2)
+        #     if tile.location.right <= 0:
+        #         tile.move(self.background_size.x, 0)
+        #     if tile.location.bottom <= 0:
+        #         tile.move(0, self.background_size.y)
+        #     tile.update()
+        for ii, layer in enumerate(self.background_layers):
+            layer.move(*[-[.1, .5, 2.5][ii]] * 2)
+            if layer.location.right <= dsr.right:
+                layer.move(self.background_tile_size.x, 0)
+            if layer.location.bottom <= dsr.bottom:
+                layer.move(0, self.background_tile_size.y)
+            layer.update()
+        if self.editing or self.title_active:
+            # mouse_motion = Vector(*pygame.mouse.get_rel())
+            # self.mouse += mouse_motion
+            # if self.mouse.x > dsr.w:
+            #     self.mouse.x -= dsr.w
+            # elif self.mouse.x < 0:
+            #     self.mouse.x += dsr.w
+            # if self.mouse.y > dsr.h:
+            #     self.mouse.y -= dsr.h
+            # elif self.mouse.y < 0:
+            #     self.mouse.y += dsr.h
             if self.selected is not None:
-                self.selected += mouse_motion
-                pygame.draw.circle(ds, pygame.Color("green"), self.translate_point_to_screen(self.selected), 10)
+                self.selected += mouse_motion
+                pygame.draw.circle(ds, pygame.Color("green"), self.translate_point_to_screen(self.selected), 10)
                 self.set_curve()
                 self.reset_enemies()
             for p in self.get_control_points():
-                pygame.draw.circle(ds, pygame.Color("blue"), self.translate_point_to_screen(p), 4)
-            for group in self.translate_points_to_screen(self.get_control_points()):
-                pygame.draw.lines(ds, pygame.Color(200, 200, 200), False, group)
-        if not self.title_active and not self.is_playing(self.blow_up):
+                # pygame.draw.circle(ds, pygame.Color("blue"), self.translate_point_to_screen(p), 4)
+                if random.random() < .001 and not self.editing:
+                    p.marker.shine()
+                p.marker.update()
+            if self.editing:
+                for group in self.translate_points_to_screen(self.get_control_points()):
+                    pygame.draw.lines(ds, pygame.Color(200, 200, 200), False, group)
+                if self.mouse.y < dsr.centery:
+                    self.edit_menu.location.bottom = dsr.bottom
+                else:
+                    self.edit_menu.location.top = dsr.top
+                self.edit_mode_selected_enemy.location.topleft = self.edit_menu.location.topleft
+                self.edit_menu.update()
+                self.edit_mode_selected_enemy.update(act=False)
+        if not self.title_active and not self.is_playing(self.blow_up) and not self.is_dialog:
             for enemy in self.enemies:
                 if self.character.collide_mask(enemy) and self.next_point_index > 0:
                     self.play(self.explode, play_once=True, center=self.character.location.center)
@@ -817,8 +1063,14 @@ class Cakewalk(Game, Animation):
         if self.get_configuration("effects", "crumbs"):
             for pile in self.crumbs:
                 pile.update()
-        if self.editing:
-            pygame.draw.circle(ds, pygame.Color("yellow"), self.mouse, 3)
+        if self.editing or self.title_active:
+            self.cursor.location.center = self.mouse
+            if not self.selected:
+                if self.find_grabbable_control_point():
+                    self.cursor.set_frameset("flashing")
+                else:
+                    self.cursor.set_frameset("open")
+            self.cursor.update()
         if not self.title_active and not self.is_playing(self.blow_up):
             if self.character_accelerating:
                 self.character_speed += .5 + abs(self.character_speed) * .125
@@ -831,13 +1083,13 @@ class Cakewalk(Game, Animation):
         else:
             self.character_speed = 1
         distance_remaining = abs(self.character_speed)
-        while distance_remaining and not self.is_playing(self.blow_up) and not self.end_active:
+        while distance_remaining and not self.is_playing(self.blow_up) and not self.end_active and not self.is_dialog:
             if self.character_speed < 0 and self.next_point_index == 0:
                 self.character_speed = 0
                 break
             elif self.character_speed > 0 and self.next_point_index > len(self.curve) - 1:
                 self.character_speed = 0
-                if self.title_active or self.is_final_level():
+                if self.title_active or self.is_final_level() or self.editing:
                     increment = 0
                 else:
                     increment = 1
@@ -874,10 +1126,11 @@ class Cakewalk(Game, Animation):
                 framerate = 200
             self.character.set_framerate(framerate)
         self.character.location.midbottom = self.translate_point_to_screen(self.character_pos)
-        if not self.end_active:
+        if not self.end_active and not self.is_dialog:
             self.goal.update()
-        if not self.title_active:
-            self.time_elapsed += self.time_filter.get_last_frame_duration()
+        if not self.title_active and not self.is_dialog:
+            if not self.is_playing(self.blow_up):
+                self.time_elapsed += self.time_filter.get_last_frame_duration()
             outgoing = []
             for enemy in self.enemies:
                 if self.editing:
@@ -885,15 +1138,15 @@ class Cakewalk(Game, Animation):
                                        list(map(int, self.translate_point_to_screen(enemy.center))), 3)
                 if self.is_playing(self.blow_up) and (
                         isinstance(enemy, Projectile) or isinstance(enemy, Projector)):
-                    move = False
+                    act = False
                 else:
-                    move = True
-                enemy.update(move=move)
+                    act = True
+                enemy.update(act=act)
                 if isinstance(enemy, Projectile) and enemy.marked_to_delete:
                     outgoing.append(enemy)
             for enemy in outgoing:
                 self.enemies.remove(enemy)
-        else:
+        elif self.title_active:
             self.controls_diagram.update()
             for score in self.high_scores:
                 score.update()
@@ -936,22 +1189,95 @@ class Cakewalk(Game, Animation):
         return copy
 
 
-class Slicer(Sprite):
+class Marker(Sprite):
+
+    def __init__(self, parent, point):
+        Sprite.__init__(self, parent)
+        for start in mirrored(range(2, 16, 2)):
+            frame = pygame.Surface((33, 33), SRCALPHA)
+            for radius, alpha in get_percent_way(range(16, 2, -2)):
+                if radius <= start:
+                    pygame.draw.circle(frame, (255, 255, 255, alpha * 100), (16, 16), radius)
+            self.add_frame(frame)
+        self.add_frameset([len(self.frames) // 2], name="grabbed")
+        self.hide()
+        self.point = point
+        self.shining = False
+
+    def shine(self):
+        if not self.shining:
+            self.unhide()
+            self.set_frameset(0)
+            self.get_current_frameset().reset()
+            self.shining = True
+
+    def grab(self):
+        self.shining = False
+        self.set_frameset("grabbed")
+        self.unhide()
+
+    def blink(self):
+        self.shining = False
+        self.set_frameset(0)
+        self.unhide()
+
+    def update(self):
+        self.location.center = self.get_game().translate_point_to_screen(self.point)
+        Sprite.update(self)
+        if self.shining and self.get_current_frameset().current_index == 0:
+            self.hide()
+
+
+class Cursor(RainbowSprite):
+
+    def __init__(self, parent):
+        RainbowSprite.__init__(self, parent, hue_shift=60)
+        self.load_from_path(self.get_resource("cursor"), True)
+        self.add_frameset([0], name="open")
+        self.add_frameset([1], name="closed")
+        self.set_frames(get_color_swapped_surface(self.frames[0], (255, 255, 255), (255, 180, 180)))
+        self.add_frameset(range(2, len(self.frames)), name="flashing")
+        self.set_frameset("open")
+
+
+class Enemy(Sprite):
+
+    def __init__(self, parent, framerate=None):
+        Sprite.__init__(self, parent, framerate)
+        self.shadow_frame = None
+
+    def reset(self):
+        pass
+
+    def act(self):
+        pass
+
+    def update(self, act=True):
+        if act:
+            self.act()
+        if self.shadow_frame is not None:
+            mask = pygame.Surface(self.shadow_frame.get_size(), SRCALPHA)
+            mask.fill((0, 0, 0))
+            mask.blit(self.shadow_frame, (0, 0), None, BLEND_RGBA_MIN)
+            shadow = pygame.Surface(mask.get_size())
+            shadow.fill((255, 0, 0))
+            shadow.set_colorkey((255, 0, 0))
+            shadow.blit(mask, (0, 0))
+            shadow.set_alpha(80)
+            self.get_display_surface().blit(shadow, self.location.move(3, 3))
+        Sprite.update(self)
+        self.shadow_frame = self.get_current_frame()
+
+
+class Slicer(Enemy):
 
     def __init__(self, parent, offset, speed=5, stray=60):
-        Sprite.__init__(self, parent, 500)
+        Enemy.__init__(self, parent, 500)
         self.offset = offset
         self.speed = speed
         self.stray = stray
-        # frame = pygame.Surface((12, 12), SRCALPHA)
-        # color = pygame.Color("green")
-        # pygame.draw.line(frame, color, (0, 0), frame.get_size())
-        # pygame.draw.line(frame, color, (0, frame.get_height()), (frame.get_width(), 0))
-        # self.add_frame(frame)
         self.load_from_path(self.get_resource("slicer"), True)
-        ii = self.get_game().get_curve_index_from_offset(self.offset)
-        self.center = self.get_game().curve[ii]
-        angle = get_angle(self.get_game().curve[ii - 1], self.get_game().curve[ii + 1])
+        angle = self.get_angle()
         self.end = get_endpoint(self.center, angle, self.stray, False)
         self.start = get_endpoint(self.center, angle + math.pi, self.stray, False)
         self.reset()
@@ -969,40 +1295,42 @@ class Slicer(Sprite):
     def __repr__(self):
         return "<Slicer %.5f %.5f %.5f>" % (self.offset, self.speed, self.stray)
 
-    def update(self, move=True):
-        if move:
-            ii = self.get_game().get_curve_index_from_offset(self.offset)
-            self.center = self.get_game().curve[ii]
-            angle = get_angle(self.get_game().curve[ii - 1], self.get_game().curve[ii + 1])
-            self.end = get_endpoint(self.center, angle, self.stray, False)
-            self.start = get_endpoint(self.center, angle + math.pi, self.stray, False)
-            speed = self.speed
-            if self.toward_end:
-                distance = get_distance(self.pos, self.end)
-                if distance < speed:
-                    self.pos = self.end.copy()
-                    speed -= distance
-                    self.toward_end = False
-            else:
-                distance = get_distance(self.pos, self.start)
-                if distance < speed:
-                    self.pos = self.start.copy()
-                    speed -= distance
-                    self.toward_end = True
-            if self.toward_end:
-                step = get_step(self.pos, self.end, speed)
-                self.pos.move(*step)
-            else:
-                step = get_step(self.pos, self.start, speed)
-                self.pos.move(*step)
-            self.location.center = self.get_game().translate_point_to_screen(self.pos)
-        Sprite.update(self)
+    def get_angle(self):
+        ii = self.get_game().get_curve_index_from_offset(self.offset)
+        curve = self.get_game().curve
+        self.center = curve[ii]
+        return get_angle(curve[max(0, ii - 1)], curve[min(len(curve) - 1, ii + 1)], True) - math.pi / 2
+
+    def act(self):
+        angle = self.get_angle()
+        self.end = get_endpoint(self.center, angle, self.stray, False)
+        self.start = get_endpoint(self.center, angle + math.pi, self.stray, False)
+        speed = self.speed
+        if self.toward_end:
+            distance = get_distance(self.pos, self.end)
+            if distance < speed:
+                self.pos = self.end.copy()
+                speed -= distance
+                self.toward_end = False
+        else:
+            distance = get_distance(self.pos, self.start)
+            if distance < speed:
+                self.pos = self.start.copy()
+                speed -= distance
+                self.toward_end = True
+        if self.toward_end:
+            step = get_step(self.pos, self.end, speed)
+            self.pos.move(*step)
+        else:
+            step = get_step(self.pos, self.start, speed)
+            self.pos.move(*step)
+        self.location.center = self.get_game().translate_point_to_screen(self.pos)
 
 
-class Fish(Sprite):
+class Fish(Enemy):
 
     def __init__(self, parent, offset, speed=math.pi / 32.0, radius=30):
-        Sprite.__init__(self, parent, 300)
+        Enemy.__init__(self, parent, 300)
         self.offset = offset
         self.speed = speed
         self.radius = radius
@@ -1023,20 +1351,18 @@ class Fish(Sprite):
     def increase_radius(self, increase):
         self.radius += increase
 
-    def update(self, move=True):
-        if move:
-            ii = self.get_game().get_curve_index_from_offset(self.offset)
-            self.center = self.get_game().curve[ii]
-            self.angle += self.speed
-            self.location.center = self.get_game().translate_point_to_screen(
-                get_point_on_circle(self.center, self.radius, self.angle, False))
-        Sprite.update(self)
+    def act(self):
+        ii = self.get_game().get_curve_index_from_offset(self.offset)
+        self.center = self.get_game().curve[ii]
+        self.angle += self.speed
+        self.location.center = self.get_game().translate_point_to_screen(
+            get_point_on_circle(self.center, self.radius, self.angle, False))
 
 
-class Projector(Sprite):
+class Projector(Enemy):
 
     def __init__(self, parent, pos, speed=5.0, frequency=3000):
-        Sprite.__init__(self, parent)
+        Enemy.__init__(self, parent)
         self.pos = pos
         self.speed = speed
         self.frequency = frequency
@@ -1068,20 +1394,18 @@ class Projector(Sprite):
     def increase_frequency(self, increase):
         self.frequency += increase
 
-    def update(self, move=True):
-        if move:
-            self.location.center = self.get_game().translate_point_to_screen(self.pos)
-            self.register(self.charge, interval=self.frequency)
-        Sprite.update(self)
+    def act(self):
+        self.location.center = self.get_game().translate_point_to_screen(self.pos)
+        self.register(self.charge, interval=self.frequency)
 
 
-class Projectile(Sprite):
+class Projectile(Enemy):
 
     def __init__(self, parent):
-        Sprite.__init__(self, parent)
+        Enemy.__init__(self, parent)
         self.pos = parent.pos.copy()
         self.speed = parent.speed
-        self.angle = get_angle(self.pos, self.get_game().character.location.center) + math.pi / 2
+        self.angle = get_angle(self.pos, self.get_game().character.location.center, True) + math.pi / 2 - math.pi / 2
         self.center = self.pos
         self.load_from_path(self.get_resource("projectile"), True)
         self.marked_to_delete = False
@@ -1089,9 +1413,9 @@ class Projectile(Sprite):
     def reset(self):
         self.get_game().enemies.remove(self)
 
-    def update(self, move=True):
+    def update(self, act=True):
         dsr = self.get_display_surface().get_rect()
-        if move:
+        if act:
             self.pos.move(*get_delta(self.angle, self.speed, False))
             self.location.center = self.get_game().translate_point_to_screen(self.pos)
             self.center = self.pos
@@ -1099,13 +1423,13 @@ class Projectile(Sprite):
            self.pos.x < 0 or self.pos.x > dsr.right:
             self.marked_to_delete = True
         else:
-            Sprite.update(self)
+            Enemy.update(self, act)
 
 
-class Fire(Sprite):
+class Fire(Enemy):
 
     def __init__(self, parent, pos, angle, speed, mirroring=False):
-        Sprite.__init__(self, parent, 300)
+        Enemy.__init__(self, parent, 300)
         self.pos = pos
         self.angle = angle
         self.speed = speed
@@ -1116,17 +1440,12 @@ class Fire(Sprite):
         if self.mirroring:
             self.play(self.mirror)
 
-    def reset(self):
-        pass
-
     def mirror(self):
         self.angle += math.pi
 
-    def update(self, move=True):
-        if move:
-            self.pos.move(*get_delta(self.angle, self.speed, False))
-            self.location.center = self.get_game().translate_point_to_screen(self.pos)
-        Sprite.update(self)
+    def act(self):
+        self.pos.move(*get_delta(self.angle, self.speed, False))
+        self.location.center = self.get_game().translate_point_to_screen(self.pos)
 
 
 class Level(GameChild):
@@ -1134,6 +1453,8 @@ class Level(GameChild):
     def __init__(self, parent, points, name):
         GameChild.__init__(self, parent)
         self.control_points = [Vector(*point) for point in points]
+        for point in self.control_points:
+            point.marker = Marker(self, point)
         self.name = name
 
     def set_enemies(self):
@@ -1202,7 +1523,6 @@ class Level(GameChild):
                 Slicer(self, 0.66667, 7.75000, 57.50000),
                 Projector(self, Vector(400.000000, 56.000000), 11.25000, 2250),
                 Slicer(self, 0.03333, 5.00000, 60.00000),
-                Slicer(self, 0.03333, 5.00000, 60.00000),
                 Slicer(self, 0.05000, 5.00000, 60.00000),
                 Slicer(self, 0.06667, 5.00000, 60.00000),
                 Slicer(self, 0.08333, 5.00000, 60.00000),
@@ -1285,6 +1605,40 @@ class Level(GameChild):
                     x += margin.x
                 shift = not shift
                 y += margin.y
+        elif self.name == "asterism":
+            self.get_game().enemies = [
+                Fish(self, 0.99444, 0.073631, 30.00000),
+                Fish(self, 0.99444, 0.098175, 30.00000),
+                Fish(self, 0.35556, 0.196350, 22.00000),
+                Fish(self, 0.32778, -0.233165, 40.00000),
+                Fish(self, 0.29444, 0.110447, 72.00000),
+                Fish(self, 0.37778, -0.220893, 50.00000),
+                Fish(self, 0.16111, 0.098175, 18.00000),
+                Slicer(self, 0.61667, 5.00000, 60.00000),
+                Slicer(self, 0.14444, 5.00000, 60.00000),
+                Slicer(self, 0.17778, 5.00000, 60.00000),
+                Slicer(self, 0.63889, 5.00000, 60.00000),
+                Fish(self, 0.94444, 0.134990, 30.00000),
+                Fish(self, 0.93889, 0.159534, 26.00000),
+                Fish(self, 0.95000, 0.147262, 26.00000),
+                Slicer(self, 0.43889, 5.50000, 102.50000),
+                Slicer(self, 0.42778, 5.50000, 102.50000),
+                Slicer(self, 0.41667, 5.50000, 102.50000),
+                Slicer(self, 0.42222, 5.50000, 102.50000),
+                Slicer(self, 0.43333, 5.50000, 102.50000),
+                Slicer(self, 0.41111, 5.50000, 102.50000),
+                Slicer(self, 0.40556, 5.50000, 102.50000),
+                Slicer(self, 0.44444, 5.50000, 102.50000),
+                Slicer(self, 0.45556, 5.50000, 102.50000),
+                Slicer(self, 0.45000, 5.50000, 102.50000),
+                Slicer(self, 0.46667, 5.50000, 102.50000),
+                Slicer(self, 0.97778, 4.25000, 37.50000),
+                Slicer(self, 0.98333, 4.25000, 37.50000),
+                Slicer(self, 0.97222, 4.25000, 37.50000),
+                Fish(self, 0.98333, 0.098175, 30.00000),
+                Fish(self, 0.97778, 0.098175, 30.00000),
+                Fish(self, 0.97222, 0.098175, 30.00000),
+            ]
 
 if __name__ == '__main__':
     LD45().run()
diff --git a/config b/config
index ed38c02..b78f752 100644 (file)
--- a/config
+++ b/config
@@ -10,6 +10,7 @@ windows-icon-path = resource/icon/cake.ico
 [display]
 dimensions = 864, 486
 caption = cakewalk
+show-framerate = no
 
 [keys]
 quit = K_ESCAPE
index 1910fa5..ed14f31 160000 (submodule)
--- a/lib/pgfw
+++ b/lib/pgfw
@@ -1 +1 @@
-Subproject commit 1910fa567a7806e7db28c5b62cd2c1a939e4b912
+Subproject commit ed14f31b63e48275ace049d556bfdfe6b2c6c01f
diff --git a/resource/BPmono-LICENSE.txt b/resource/BPmono-LICENSE.txt
new file mode 100755 (executable)
index 0000000..b28f1e5
--- /dev/null
@@ -0,0 +1,18 @@
+Creative Commons Attribution-No Derivative Works 3.0 Unported
+(http://creativecommons.org/licenses/by-nd/3.0/)
+
+You are free:
+
+to Share — to copy, distribute and transmit the work
+
+Under the following conditions:
+
+Attribution. You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
+
+No Derivative Works. You may not alter, transform, or build upon this work.
+
+For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.
+
+Any of the above conditions can be waived if you get permission from the copyright holder.
+
+Nothing in this license impairs or restricts the author's moral rights.
\ No newline at end of file
diff --git a/resource/TeX_Gyre_Scholar-LICENSE.txt b/resource/TeX_Gyre_Scholar-LICENSE.txt
new file mode 100644 (file)
index 0000000..ad80c74
--- /dev/null
@@ -0,0 +1,29 @@
+This is a preliminary version (2006-09-30), barring acceptance from
+the LaTeX Project Team and other feedback, of the GUST Font License.
+(GUST is the Polish TeX Users Group, http://www.gust.org.pl)
+
+For the most recent version of this license see
+http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
+or
+http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
+
+This work may be distributed and/or modified under the conditions
+of the LaTeX Project Public License, either version 1.3c of this
+license or (at your option) any later version.
+
+Please also observe the following clause:
+1) it is requested, but not legally required, that derived works be
+   distributed only after changing the names of the fonts comprising this
+   work and given in an accompanying "manifest", and that the
+   files comprising the Work, as listed in the manifest, also be given
+   new names. Any exceptions to this request are also given in the
+   manifest.
+   
+   We recommend the manifest be given in a separate file named
+   MANIFEST-<fontid>.txt, where <fontid> is some unique identification
+   of the font family. If a separate "readme" file accompanies the Work, 
+   we recommend a name of the form README-<fontid>.txt.
+
+The latest version of the LaTeX Project Public License is in
+http://www.latex-project.org/lppl.txt and version 1.3c or later
+is part of all distributions of LaTeX version 2006/05/20 or later.
\ No newline at end of file
diff --git a/resource/TeX_Gyre_Scholar-bolditalic.ttf b/resource/TeX_Gyre_Scholar-bolditalic.ttf
new file mode 100644 (file)
index 0000000..8a9db40
Binary files /dev/null and b/resource/TeX_Gyre_Scholar-bolditalic.ttf differ
diff --git a/resource/background/1-far-stars.png b/resource/background/1-far-stars.png
new file mode 100644 (file)
index 0000000..d4fc043
Binary files /dev/null and b/resource/background/1-far-stars.png differ
diff --git a/resource/background/2-close-stars.png b/resource/background/2-close-stars.png
new file mode 100644 (file)
index 0000000..71a78b8
Binary files /dev/null and b/resource/background/2-close-stars.png differ
diff --git a/resource/background/3-planets.png b/resource/background/3-planets.png
new file mode 100644 (file)
index 0000000..c695925
Binary files /dev/null and b/resource/background/3-planets.png differ
diff --git a/resource/cursor/1-open.png b/resource/cursor/1-open.png
new file mode 100644 (file)
index 0000000..87bd8f2
Binary files /dev/null and b/resource/cursor/1-open.png differ
diff --git a/resource/cursor/2-closed.png b/resource/cursor/2-closed.png
new file mode 100644 (file)
index 0000000..7058b5f
Binary files /dev/null and b/resource/cursor/2-closed.png differ
diff --git a/resource/dialog_box.png b/resource/dialog_box.png
new file mode 100644 (file)
index 0000000..48393b4
Binary files /dev/null and b/resource/dialog_box.png differ