- new level prototype: asterism
[cake] / Cakewalk.py
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()