Apply ruff auto-fixes and formatting
This commit is contained in:
parent
0bdd700350
commit
1e4d31121b
87 changed files with 8157 additions and 5443 deletions
|
|
@ -1,31 +1,32 @@
|
|||
|
||||
import traceback
|
||||
|
||||
from art import ART_DIR
|
||||
|
||||
|
||||
class ArtExporter:
|
||||
|
||||
"""
|
||||
Class for exporting an Art into a non-Playscii format.
|
||||
Export logic happens in run_export; exporter authors simply extend this
|
||||
class, override run_export and the class properties below.
|
||||
"""
|
||||
|
||||
format_name = 'ERROR - ArtExporter.format_name'
|
||||
|
||||
format_name = "ERROR - ArtExporter.format_name"
|
||||
"User-visible name for this format, shown in export chooser."
|
||||
format_description = "ERROR - ArtExporter.format_description"
|
||||
"String (can be triple-quoted) describing format, shown in export chooser."
|
||||
file_extension = ''
|
||||
file_extension = ""
|
||||
"Extension to give the exported file, sans dot."
|
||||
options_dialog_class = None
|
||||
"UIDialog subclass exposing export options to user."
|
||||
|
||||
|
||||
def __init__(self, app, out_filename, options={}):
|
||||
self.app = app
|
||||
self.art = self.app.ui.active_art
|
||||
# add file extension to output filename if not present
|
||||
if self.file_extension and not out_filename.endswith('.%s' % self.file_extension):
|
||||
out_filename += '.%s' % self.file_extension
|
||||
if self.file_extension and not out_filename.endswith(
|
||||
".%s" % self.file_extension
|
||||
):
|
||||
out_filename += ".%s" % self.file_extension
|
||||
# output filename in documents/art dir
|
||||
if not out_filename.startswith(self.app.documents_dir + ART_DIR):
|
||||
out_filename = self.app.documents_dir + ART_DIR + out_filename
|
||||
|
|
@ -40,15 +41,18 @@ class ArtExporter:
|
|||
if self.run_export(out_filename, options):
|
||||
self.success = True
|
||||
else:
|
||||
line = '%s failed to export %s, see console for errors' % (self.__class__.__name__, out_filename)
|
||||
line = "%s failed to export %s, see console for errors" % (
|
||||
self.__class__.__name__,
|
||||
out_filename,
|
||||
)
|
||||
self.app.log(line)
|
||||
self.app.ui.message_line.post_line(line, hold_time=10, error=True)
|
||||
except:
|
||||
for line in traceback.format_exc().split('\n'):
|
||||
for line in traceback.format_exc().split("\n"):
|
||||
self.app.log(line)
|
||||
# store last used export options for "Export last"
|
||||
self.app.last_export_options = options
|
||||
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
"""
|
||||
Contains the actual export logic. Write data based on current art,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import os
|
||||
import traceback
|
||||
|
||||
import os, traceback
|
||||
|
||||
from art import Art, ART_FILE_EXTENSION, DEFAULT_CHARSET, DEFAULT_PALETTE
|
||||
from art import ART_FILE_EXTENSION, DEFAULT_CHARSET, DEFAULT_PALETTE
|
||||
from ui_file_chooser_dialog import GenericImportChooserDialog
|
||||
|
||||
|
||||
class ArtImporter:
|
||||
|
||||
"""
|
||||
Class for creating a new Art from data in non-Playscii format.
|
||||
Import logic happens in run_import; importer authors simply extend this
|
||||
class, override run_import and the class properties below.
|
||||
"""
|
||||
|
||||
format_name = 'ERROR - ArtImporter.format_name'
|
||||
|
||||
format_name = "ERROR - ArtImporter.format_name"
|
||||
"User-visible name for this format, shown in import chooser."
|
||||
format_description = "ERROR - ArtImporter.format_description"
|
||||
"String (can be triple-quoted) describing format, shown in import chooser."
|
||||
|
|
@ -25,20 +25,27 @@ class ArtImporter:
|
|||
"""
|
||||
options_dialog_class = None
|
||||
"UIDialog subclass exposing import options to user."
|
||||
generic_error = '%s failed to import %s'
|
||||
generic_error = "%s failed to import %s"
|
||||
# if False (eg bitmap conversion), "Imported successfully" message
|
||||
# won't show on successful creation
|
||||
completes_instantly = True
|
||||
|
||||
|
||||
def __init__(self, app, in_filename, options={}):
|
||||
self.app = app
|
||||
new_filename = '%s.%s' % (os.path.splitext(in_filename)[0],
|
||||
ART_FILE_EXTENSION)
|
||||
new_filename = "%s.%s" % (os.path.splitext(in_filename)[0], ART_FILE_EXTENSION)
|
||||
self.art = self.app.new_art(new_filename)
|
||||
# use charset and palette of existing art
|
||||
charset = self.app.ui.active_art.charset if self.app.ui.active_art else self.app.load_charset(DEFAULT_CHARSET)
|
||||
charset = (
|
||||
self.app.ui.active_art.charset
|
||||
if self.app.ui.active_art
|
||||
else self.app.load_charset(DEFAULT_CHARSET)
|
||||
)
|
||||
self.art.set_charset(charset)
|
||||
palette = self.app.ui.active_art.palette if self.app.ui.active_art else self.app.load_palette(DEFAULT_PALETTE)
|
||||
palette = (
|
||||
self.app.ui.active_art.palette
|
||||
if self.app.ui.active_art
|
||||
else self.app.load_palette(DEFAULT_PALETTE)
|
||||
)
|
||||
self.art.set_palette(palette)
|
||||
self.app.set_new_art_for_edit(self.art)
|
||||
self.art.clear_frame_layer(0, 0, 1)
|
||||
|
|
@ -49,7 +56,7 @@ class ArtImporter:
|
|||
if self.run_import(in_filename, options):
|
||||
self.success = True
|
||||
except:
|
||||
for line in traceback.format_exc().split('\n'):
|
||||
for line in traceback.format_exc().split("\n"):
|
||||
self.app.log(line)
|
||||
if not self.success:
|
||||
line = self.generic_error % (self.__class__.__name__, in_filename)
|
||||
|
|
@ -66,20 +73,20 @@ class ArtImporter:
|
|||
# adjust for new art size and set it active
|
||||
self.app.ui.adjust_for_art_resize(self.art)
|
||||
self.app.ui.set_active_art(self.art)
|
||||
|
||||
|
||||
def set_art_charset(self, charset_name):
|
||||
"Convenience function for setting charset by name from run_import."
|
||||
self.art.set_charset_by_name(charset_name)
|
||||
|
||||
|
||||
def set_art_palette(self, palette_name):
|
||||
"Convenience function for setting palette by name from run_import."
|
||||
self.art.set_palette_by_name(palette_name)
|
||||
|
||||
|
||||
def resize(self, new_width, new_height):
|
||||
"Convenience function for resizing art from run_import"
|
||||
self.art.resize(new_width, new_height)
|
||||
self.app.ui.adjust_for_art_resize(self.art)
|
||||
|
||||
|
||||
def run_import(self, in_filename, options):
|
||||
"""
|
||||
Contains the actual import logic. Read input file, set Art
|
||||
|
|
|
|||
69
audio.py
69
audio.py
|
|
@ -1,31 +1,32 @@
|
|||
|
||||
import ctypes
|
||||
|
||||
from sdl2 import sdlmixer
|
||||
|
||||
|
||||
class PlayingSound:
|
||||
"represents a currently playing sound"
|
||||
|
||||
def __init__(self, filename, channel, game_object, looping=False):
|
||||
self.filename = filename
|
||||
self.channel = channel
|
||||
self.go = game_object
|
||||
self.looping = looping
|
||||
|
||||
|
||||
class AudioLord:
|
||||
|
||||
sample_rate = 44100
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
# initialize audio
|
||||
sdlmixer.Mix_Init(sdlmixer.MIX_INIT_OGG|sdlmixer.MIX_INIT_MOD)
|
||||
sdlmixer.Mix_OpenAudio(self.sample_rate, sdlmixer.MIX_DEFAULT_FORMAT,
|
||||
2, 1024)
|
||||
sdlmixer.Mix_Init(sdlmixer.MIX_INIT_OGG | sdlmixer.MIX_INIT_MOD)
|
||||
sdlmixer.Mix_OpenAudio(self.sample_rate, sdlmixer.MIX_DEFAULT_FORMAT, 2, 1024)
|
||||
self.reset()
|
||||
# sound callback
|
||||
# retain handle to C callable even though we don't use it directly
|
||||
self.sound_cb = ctypes.CFUNCTYPE(None, ctypes.c_int)(self.channel_finished)
|
||||
sdlmixer.Mix_ChannelFinished(self.sound_cb)
|
||||
|
||||
|
||||
def channel_finished(self, channel):
|
||||
# remove sound from dicts of playing channels and sounds
|
||||
old_sound = self.playing_channels.pop(channel)
|
||||
|
|
@ -33,7 +34,7 @@ class AudioLord:
|
|||
# remove empty list
|
||||
if self.playing_sounds[old_sound.filename] == []:
|
||||
self.playing_sounds.pop(old_sound.filename)
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.stop_all_music()
|
||||
self.stop_all_sounds()
|
||||
|
|
@ -44,24 +45,25 @@ class AudioLord:
|
|||
# {channel_number: PlayingSound object}
|
||||
self.playing_channels = {}
|
||||
# handle init case where self.musics doesn't exist yet
|
||||
if hasattr(self, 'musics'):
|
||||
if hasattr(self, "musics"):
|
||||
for music in self.musics.values():
|
||||
sdlmixer.Mix_FreeMusic(music)
|
||||
self.musics = {}
|
||||
if hasattr(self, 'sounds'):
|
||||
if hasattr(self, "sounds"):
|
||||
for sound in self.sounds.values():
|
||||
sdlmixer.Mix_FreeChunk(sound)
|
||||
self.sounds = {}
|
||||
|
||||
|
||||
def register_sound(self, sound_filename):
|
||||
if sound_filename in self.sounds:
|
||||
return self.sounds[sound_filename]
|
||||
new_sound = sdlmixer.Mix_LoadWAV(bytes(sound_filename, 'utf-8'))
|
||||
new_sound = sdlmixer.Mix_LoadWAV(bytes(sound_filename, "utf-8"))
|
||||
self.sounds[sound_filename] = new_sound
|
||||
return new_sound
|
||||
|
||||
def object_play_sound(self, game_object, sound_filename,
|
||||
loops=0, allow_multiple=False):
|
||||
|
||||
def object_play_sound(
|
||||
self, game_object, sound_filename, loops=0, allow_multiple=False
|
||||
):
|
||||
# TODO: volume param? sdlmixer.MIX_MAX_VOLUME if not specified
|
||||
# bail if same object isn't allowed to play same sound multiple times
|
||||
if not allow_multiple and sound_filename in self.playing_sounds:
|
||||
|
|
@ -71,74 +73,75 @@ class AudioLord:
|
|||
sound = self.register_sound(sound_filename)
|
||||
channel = sdlmixer.Mix_PlayChannel(-1, sound, loops)
|
||||
# add sound to dicts of playing sounds and channels
|
||||
new_playing_sound = PlayingSound(sound_filename, channel, game_object,
|
||||
loops == -1)
|
||||
new_playing_sound = PlayingSound(
|
||||
sound_filename, channel, game_object, loops == -1
|
||||
)
|
||||
if sound_filename in self.playing_sounds:
|
||||
self.playing_sounds[sound_filename].append(new_playing_sound)
|
||||
else:
|
||||
self.playing_sounds[sound_filename] = [new_playing_sound]
|
||||
self.playing_channels[channel] = new_playing_sound
|
||||
|
||||
|
||||
def object_stop_sound(self, game_object, sound_filename):
|
||||
if not sound_filename in self.playing_sounds:
|
||||
if sound_filename not in self.playing_sounds:
|
||||
return
|
||||
# stop all instances of this sound object might be playing
|
||||
for sound in self.playing_sounds[sound_filename]:
|
||||
if game_object is sound.go:
|
||||
sdlmixer.Mix_HaltChannel(sound.channel)
|
||||
|
||||
|
||||
def object_stop_all_sounds(self, game_object):
|
||||
sounds_to_stop = []
|
||||
for sound_filename,sounds in self.playing_sounds.items():
|
||||
for sound_filename, sounds in self.playing_sounds.items():
|
||||
for sound in sounds:
|
||||
if sound.go is game_object:
|
||||
sounds_to_stop.append(sound_filename)
|
||||
for sound_filename in sounds_to_stop:
|
||||
self.object_stop_sound(game_object, sound_filename)
|
||||
|
||||
|
||||
def stop_all_sounds(self):
|
||||
sdlmixer.Mix_HaltChannel(-1)
|
||||
|
||||
|
||||
def set_music(self, music_filename):
|
||||
if music_filename in self.musics:
|
||||
return
|
||||
new_music = sdlmixer.Mix_LoadMUS(bytes(music_filename, 'utf-8'))
|
||||
new_music = sdlmixer.Mix_LoadMUS(bytes(music_filename, "utf-8"))
|
||||
self.musics[music_filename] = new_music
|
||||
|
||||
|
||||
def start_music(self, music_filename, loops=-1):
|
||||
# TODO: fade in support etc
|
||||
music = self.musics[music_filename]
|
||||
sdlmixer.Mix_PlayMusic(music, loops)
|
||||
self.current_music = music_filename
|
||||
|
||||
|
||||
def pause_music(self):
|
||||
if self.current_music:
|
||||
sdlmixer.Mix_PauseMusic()
|
||||
|
||||
|
||||
def resume_music(self):
|
||||
if self.current_music:
|
||||
sdlmixer.Mix_ResumeMusic()
|
||||
|
||||
|
||||
def stop_music(self, music_filename):
|
||||
# TODO: fade out support
|
||||
sdlmixer.Mix_HaltMusic()
|
||||
self.current_music = None
|
||||
|
||||
|
||||
def is_music_playing(self):
|
||||
return bool(sdlmixer.Mix_PlayingMusic())
|
||||
|
||||
|
||||
def resume_music(self):
|
||||
if self.current_music:
|
||||
sdlmixer.Mix_ResumeMusic()
|
||||
|
||||
|
||||
def stop_all_music(self):
|
||||
sdlmixer.Mix_HaltMusic()
|
||||
self.current_music = None
|
||||
|
||||
|
||||
def update(self):
|
||||
if self.current_music and not self.is_music_playing():
|
||||
self.current_music = None
|
||||
|
||||
|
||||
def destroy(self):
|
||||
self.reset()
|
||||
sdlmixer.Mix_CloseAudio()
|
||||
|
|
|
|||
119
camera.py
119
camera.py
|
|
@ -1,14 +1,17 @@
|
|||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
import vector
|
||||
|
||||
|
||||
def clamp(val, lowest, highest):
|
||||
return min(highest, max(lowest, val))
|
||||
|
||||
|
||||
class Camera:
|
||||
|
||||
# good starting values
|
||||
start_x,start_y = 0,0
|
||||
start_x, start_y = 0, 0
|
||||
start_zoom = 2.5
|
||||
x_tilt, y_tilt = 0, 0
|
||||
# pan/zoom speed tuning
|
||||
|
|
@ -28,35 +31,35 @@ class Camera:
|
|||
min_velocity = 0.05
|
||||
# map extents
|
||||
# starting values only, bounds are generated according to art size
|
||||
min_x,max_x = -10, 50
|
||||
min_y,max_y = -50, 10
|
||||
min_x, max_x = -10, 50
|
||||
min_y, max_y = -50, 10
|
||||
use_bounds = True
|
||||
min_zoom,max_zoom = 1, 1000
|
||||
min_zoom, max_zoom = 1, 1000
|
||||
# matrices -> worldspace renderable vertex shader uniforms
|
||||
fov = 90
|
||||
near_z = 0.0001
|
||||
far_z = 100000
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.reset()
|
||||
self.max_pan_speed = self.base_max_pan_speed
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.x, self.y = self.start_x, self.start_y
|
||||
self.z = self.start_zoom
|
||||
# store look vectors so world/screen space conversions can refer to it
|
||||
self.look_x, self.look_y, self.look_z = None,None,None
|
||||
self.vel_x, self.vel_y, self.vel_z = 0,0,0
|
||||
self.look_x, self.look_y, self.look_z = None, None, None
|
||||
self.vel_x, self.vel_y, self.vel_z = 0, 0, 0
|
||||
self.mouse_panned, self.moved_this_frame = False, False
|
||||
# GameObject to focus on
|
||||
self.focus_object = None
|
||||
self.calc_projection_matrix()
|
||||
self.calc_view_matrix()
|
||||
|
||||
|
||||
def calc_projection_matrix(self):
|
||||
self.projection_matrix = self.get_perspective_matrix()
|
||||
|
||||
|
||||
def calc_view_matrix(self):
|
||||
eye = vector.Vec3(self.x, self.y, self.z)
|
||||
up = vector.Vec3(0, 1, 0)
|
||||
|
|
@ -65,24 +68,23 @@ class Camera:
|
|||
forward = (target - eye).normalize()
|
||||
side = forward.cross(up).normalize()
|
||||
upward = side.cross(forward)
|
||||
m = [[side.x, upward.x, -forward.x, 0],
|
||||
[side.y, upward.y, -forward.y, 0],
|
||||
[side.z, upward.z, -forward.z, 0],
|
||||
[-eye.dot(side), -eye.dot(upward), eye.dot(forward), 1]]
|
||||
m = [
|
||||
[side.x, upward.x, -forward.x, 0],
|
||||
[side.y, upward.y, -forward.y, 0],
|
||||
[side.z, upward.z, -forward.z, 0],
|
||||
[-eye.dot(side), -eye.dot(upward), eye.dot(forward), 1],
|
||||
]
|
||||
self.view_matrix = np.array(m, dtype=np.float32)
|
||||
self.look_x, self.look_y, self.look_z = side, upward, forward
|
||||
|
||||
|
||||
def get_perspective_matrix(self):
|
||||
zmul = (-2 * self.near_z * self.far_z) / (self.far_z - self.near_z)
|
||||
ymul = 1 / math.tan(self.fov * math.pi / 360)
|
||||
aspect = self.app.window_width / self.app.window_height
|
||||
xmul = ymul / aspect
|
||||
m = [[xmul, 0, 0, 0],
|
||||
[ 0, ymul, 0, 0],
|
||||
[ 0, 0, -1, -1],
|
||||
[ 0, 0, zmul, 0]]
|
||||
m = [[xmul, 0, 0, 0], [0, ymul, 0, 0], [0, 0, -1, -1], [0, 0, zmul, 0]]
|
||||
return np.array(m, dtype=np.float32)
|
||||
|
||||
|
||||
def get_ortho_matrix(self, width=None, height=None):
|
||||
width, height = width or self.app.window_width, height or self.app.window_height
|
||||
m = np.eye(4, 4, dtype=np.float32)
|
||||
|
|
@ -95,12 +97,9 @@ class Camera:
|
|||
wx = -(right + left) / (right - left)
|
||||
wy = -(top + bottom) / (top - bottom)
|
||||
wz = -(self.far_z + self.near_z) / (self.far_z - self.near_z)
|
||||
m = [[ x, 0, 0, 0],
|
||||
[ 0, y, 0, 0],
|
||||
[ 0, 0, z, 0],
|
||||
[wx, wy, wz, 0]]
|
||||
m = [[x, 0, 0, 0], [0, y, 0, 0], [0, 0, z, 0], [wx, wy, wz, 0]]
|
||||
return np.array(m, dtype=np.float32)
|
||||
|
||||
|
||||
def pan(self, dx, dy, keyboard=False):
|
||||
# modify pan speed based on zoom according to a factor
|
||||
m = (self.pan_zoom_increase_factor * self.z) / self.min_zoom
|
||||
|
|
@ -109,7 +108,7 @@ class Camera:
|
|||
# for brevity, app passes in whether user appears to be keyboard editing
|
||||
if keyboard:
|
||||
self.app.keyboard_editing = True
|
||||
|
||||
|
||||
def zoom(self, dz, keyboard=False, towards_cursor=False):
|
||||
self.vel_z += dz * self.zoom_accel
|
||||
# pan towards cursor while zooming?
|
||||
|
|
@ -119,11 +118,11 @@ class Camera:
|
|||
self.pan(dx, dy, keyboard)
|
||||
if keyboard:
|
||||
self.app.keyboard_editing = True
|
||||
|
||||
|
||||
def get_current_zoom_pct(self):
|
||||
"returns % of base (1:1) for current camera"
|
||||
return (self.get_base_zoom() / self.z) * 100
|
||||
|
||||
|
||||
def get_base_zoom(self):
|
||||
"returns camera Z needed for 1:1 pixel zoom"
|
||||
wh = self.app.window_height
|
||||
|
|
@ -132,10 +131,10 @@ class Camera:
|
|||
if ch == 8:
|
||||
ch = 16
|
||||
return wh / ch
|
||||
|
||||
|
||||
def set_to_base_zoom(self):
|
||||
self.z = self.get_base_zoom()
|
||||
|
||||
|
||||
def zoom_proportional(self, direction):
|
||||
"zooms in or out via increments of 1:1 pixel scales for active art"
|
||||
if not self.app.ui.active_art:
|
||||
|
|
@ -167,7 +166,7 @@ class Camera:
|
|||
break
|
||||
# kill all Z velocity for camera so we don't drift out of 1:1
|
||||
self.vel_z = 0
|
||||
|
||||
|
||||
def find_closest_zoom_extents(self):
|
||||
def corners_on_screen():
|
||||
art = self.app.ui.active_art
|
||||
|
|
@ -177,12 +176,12 @@ class Camera:
|
|||
x2 = x1 + art.width * art.quad_width
|
||||
y2 = y1 - art.height * art.quad_height
|
||||
right, bot = vector.world_to_screen_normalized(self.app, x2, y2, z)
|
||||
#print('(%.3f, %.3f) -> (%.3f, %.3f)' % (left, top, right, bot))
|
||||
# print('(%.3f, %.3f) -> (%.3f, %.3f)' % (left, top, right, bot))
|
||||
# add 1 tile of UI chars to top and bottom margins
|
||||
top_margin = 1 - self.app.ui.menu_bar.art.quad_height
|
||||
bot_margin = -1 + self.app.ui.status_bar.art.quad_height
|
||||
return left >= -1 and top <= top_margin and \
|
||||
right <= 1 and bot >= bot_margin
|
||||
return left >= -1 and top <= top_margin and right <= 1 and bot >= bot_margin
|
||||
|
||||
# zoom out from minimum until all corners are visible
|
||||
self.z = self.min_zoom
|
||||
# recalc view matrix each move so projection stays correct
|
||||
|
|
@ -192,16 +191,24 @@ class Camera:
|
|||
self.zoom_proportional(-1)
|
||||
self.calc_view_matrix()
|
||||
tries += 1
|
||||
|
||||
|
||||
def toggle_zoom_extents(self, override=None):
|
||||
art = self.app.ui.active_art
|
||||
if override is not None:
|
||||
art.camera_zoomed_extents = not override
|
||||
if art.camera_zoomed_extents:
|
||||
# restore cached position
|
||||
self.x, self.y, self.z = art.non_extents_camera_x, art.non_extents_camera_y, art.non_extents_camera_z
|
||||
self.x, self.y, self.z = (
|
||||
art.non_extents_camera_x,
|
||||
art.non_extents_camera_y,
|
||||
art.non_extents_camera_z,
|
||||
)
|
||||
else:
|
||||
art.non_extents_camera_x, art.non_extents_camera_y, art.non_extents_camera_z = self.x, self.y, self.z
|
||||
(
|
||||
art.non_extents_camera_x,
|
||||
art.non_extents_camera_y,
|
||||
art.non_extents_camera_z,
|
||||
) = self.x, self.y, self.z
|
||||
# center camera on art
|
||||
self.x = (art.width * art.quad_width) / 2
|
||||
self.y = -(art.height * art.quad_height) / 2
|
||||
|
|
@ -209,27 +216,27 @@ class Camera:
|
|||
# kill all camera velocity when snapping
|
||||
self.vel_x, self.vel_y, self.vel_z = 0, 0, 0
|
||||
art.camera_zoomed_extents = not art.camera_zoomed_extents
|
||||
|
||||
|
||||
def window_resized(self):
|
||||
self.calc_projection_matrix()
|
||||
|
||||
|
||||
def set_zoom(self, z):
|
||||
# TODO: set lerp target, clear if keyboard etc call zoom()
|
||||
self.z = z
|
||||
|
||||
|
||||
def set_loc(self, x, y, z):
|
||||
self.x, self.y, self.z = x, y, (z or self.z) # z optional
|
||||
|
||||
self.x, self.y, self.z = x, y, (z or self.z) # z optional
|
||||
|
||||
def set_loc_from_obj(self, game_object):
|
||||
self.set_loc(game_object.x, game_object.y, game_object.z)
|
||||
|
||||
|
||||
def set_for_art(self, art):
|
||||
# set limits
|
||||
self.max_x = art.width * art.quad_width
|
||||
self.min_y = -art.height * art.quad_height
|
||||
# use saved pan/zoom
|
||||
self.set_loc(art.camera_x, art.camera_y, art.camera_z)
|
||||
|
||||
|
||||
def mouse_pan(self, dx, dy):
|
||||
"pan view based on mouse delta"
|
||||
if dx == 0 and dy == 0:
|
||||
|
|
@ -240,12 +247,13 @@ class Camera:
|
|||
self.y += dy / self.mouse_pan_rate * m
|
||||
self.vel_x = self.vel_y = 0
|
||||
self.mouse_panned = True
|
||||
|
||||
|
||||
def update(self):
|
||||
# zoom-proportional pan scale is based on art
|
||||
if self.app.ui.active_art:
|
||||
speed_scale = clamp(self.get_current_zoom_pct(),
|
||||
self.pan_min_pct, self.pan_max_pct)
|
||||
speed_scale = clamp(
|
||||
self.get_current_zoom_pct(), self.pan_min_pct, self.pan_max_pct
|
||||
)
|
||||
self.max_pan_speed = self.base_max_pan_speed / (speed_scale / 100)
|
||||
else:
|
||||
self.max_pan_speed = self.base_max_pan_speed
|
||||
|
|
@ -256,7 +264,7 @@ class Camera:
|
|||
# track towards target
|
||||
# TODO: revisit this for better feel later
|
||||
dx, dy = self.focus_object.x - self.x, self.focus_object.y - self.y
|
||||
l = math.sqrt(dx ** 2 + dy ** 2)
|
||||
l = math.sqrt(dx**2 + dy**2)
|
||||
if l != 0 and l > 0.1:
|
||||
il = 1 / l
|
||||
dx *= il
|
||||
|
|
@ -269,7 +277,7 @@ class Camera:
|
|||
self.vel_y = clamp(self.vel_y, -self.max_pan_speed, self.max_pan_speed)
|
||||
# apply friction
|
||||
self.vel_x *= 1 - self.pan_friction
|
||||
self.vel_y *= 1 - self.pan_friction
|
||||
self.vel_y *= 1 - self.pan_friction
|
||||
if abs(self.vel_x) < self.min_velocity:
|
||||
self.vel_x = 0
|
||||
if abs(self.vel_y) < self.min_velocity:
|
||||
|
|
@ -296,8 +304,13 @@ class Camera:
|
|||
self.z = clamp(self.z, self.min_zoom, self.max_zoom)
|
||||
# set view matrix from xyz
|
||||
self.calc_view_matrix()
|
||||
self.moved_this_frame = self.mouse_panned or self.x != self.last_x or self.y != self.last_y or self.z != self.last_z
|
||||
self.moved_this_frame = (
|
||||
self.mouse_panned
|
||||
or self.x != self.last_x
|
||||
or self.y != self.last_y
|
||||
or self.z != self.last_z
|
||||
)
|
||||
self.mouse_panned = False
|
||||
|
||||
|
||||
def log_loc(self):
|
||||
self.app.log('camera x=%s, y=%s, z=%s' % (self.x, self.y, self.z))
|
||||
self.app.log("camera x=%s, y=%s, z=%s" % (self.x, self.y, self.z))
|
||||
|
|
|
|||
96
charset.py
96
charset.py
|
|
@ -1,23 +1,28 @@
|
|||
import os.path, string, time
|
||||
import os.path
|
||||
import string
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from texture import Texture
|
||||
|
||||
CHARSET_DIR = 'charsets/'
|
||||
CHARSET_FILE_EXTENSION = 'char'
|
||||
CHARSET_DIR = "charsets/"
|
||||
CHARSET_FILE_EXTENSION = "char"
|
||||
|
||||
|
||||
class CharacterSetLord:
|
||||
|
||||
# time in ms between checks for hot reload
|
||||
hot_reload_check_interval = 2 * 1000
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.last_check = 0
|
||||
|
||||
|
||||
def check_hot_reload(self):
|
||||
if self.app.get_elapsed_time() - self.last_check < self.hot_reload_check_interval:
|
||||
if (
|
||||
self.app.get_elapsed_time() - self.last_check
|
||||
< self.hot_reload_check_interval
|
||||
):
|
||||
return
|
||||
self.last_check = self.app.get_elapsed_time()
|
||||
changed = None
|
||||
|
|
@ -28,22 +33,29 @@ class CharacterSetLord:
|
|||
try:
|
||||
success = charset.load_char_data()
|
||||
if success:
|
||||
self.app.log('CharacterSetLord: success reloading %s' % charset.filename)
|
||||
self.app.log(
|
||||
"CharacterSetLord: success reloading %s" % charset.filename
|
||||
)
|
||||
else:
|
||||
self.app.log('CharacterSetLord: failed reloading %s' % charset.filename, True)
|
||||
self.app.log(
|
||||
"CharacterSetLord: failed reloading %s" % charset.filename,
|
||||
True,
|
||||
)
|
||||
except:
|
||||
self.app.log('CharacterSetLord: failed reloading %s' % charset.filename, True)
|
||||
self.app.log(
|
||||
"CharacterSetLord: failed reloading %s" % charset.filename, True
|
||||
)
|
||||
|
||||
|
||||
class CharacterSet:
|
||||
|
||||
transparent_color = (0, 0, 0)
|
||||
|
||||
|
||||
def __init__(self, app, src_filename, log):
|
||||
self.init_success = False
|
||||
self.app = app
|
||||
self.filename = self.app.find_filename_path(src_filename, CHARSET_DIR,
|
||||
CHARSET_FILE_EXTENSION)
|
||||
self.filename = self.app.find_filename_path(
|
||||
src_filename, CHARSET_DIR, CHARSET_FILE_EXTENSION
|
||||
)
|
||||
if not self.filename:
|
||||
self.app.log("Couldn't find character set data %s" % self.filename)
|
||||
return
|
||||
|
|
@ -62,19 +74,21 @@ class CharacterSet:
|
|||
self.app.log("loaded charmap '%s' from %s:" % (self.name, self.filename))
|
||||
self.report()
|
||||
self.init_success = True
|
||||
|
||||
|
||||
def load_char_data(self):
|
||||
"carries out majority of CharacterSet init, including loading image"
|
||||
char_data_src = open(self.filename, encoding='utf-8').readlines()
|
||||
char_data_src = open(self.filename, encoding="utf-8").readlines()
|
||||
# allow comments: discard any line in char data starting with //
|
||||
# (make sure this doesn't muck up legit mapping data)
|
||||
char_data = []
|
||||
for line in char_data_src:
|
||||
if not line.startswith('//'):
|
||||
if not line.startswith("//"):
|
||||
char_data.append(line)
|
||||
# first line = image file
|
||||
# hold off assigning to self.image_filename til we know it's valid
|
||||
img_filename = self.app.find_filename_path(char_data.pop(0).strip(), CHARSET_DIR, 'png')
|
||||
img_filename = self.app.find_filename_path(
|
||||
char_data.pop(0).strip(), CHARSET_DIR, "png"
|
||||
)
|
||||
if not img_filename:
|
||||
self.app.log("Couldn't find character set image %s" % self.image_filename)
|
||||
return False
|
||||
|
|
@ -82,14 +96,14 @@ class CharacterSet:
|
|||
# now that we know the image file's name, store its last modified time
|
||||
self.last_image_change = os.path.getmtime(self.image_filename)
|
||||
# second line = character set dimensions
|
||||
second_line = char_data.pop(0).strip().split(',')
|
||||
second_line = char_data.pop(0).strip().split(",")
|
||||
self.map_width, self.map_height = int(second_line[0]), int(second_line[1])
|
||||
self.char_mapping = {}
|
||||
index = 0
|
||||
for line in char_data:
|
||||
# strip newlines from mapping
|
||||
for char in line.strip('\r\n'):
|
||||
if not char in self.char_mapping:
|
||||
for char in line.strip("\r\n"):
|
||||
if char not in self.char_mapping:
|
||||
self.char_mapping[char] = index
|
||||
index += 1
|
||||
if index >= self.map_width * self.map_height:
|
||||
|
|
@ -105,12 +119,12 @@ class CharacterSet:
|
|||
if has_upper and not has_lower:
|
||||
for char in string.ascii_lowercase:
|
||||
# set may not have all letters
|
||||
if not char.upper() in self.char_mapping:
|
||||
if char.upper() not in self.char_mapping:
|
||||
continue
|
||||
self.char_mapping[char] = self.char_mapping[char.upper()]
|
||||
elif has_lower and not has_upper:
|
||||
for char in string.ascii_uppercase:
|
||||
if not char.lower() in self.char_mapping:
|
||||
if char.lower() not in self.char_mapping:
|
||||
continue
|
||||
self.char_mapping[char] = self.char_mapping[char.lower()]
|
||||
# last valid index a character can be
|
||||
|
|
@ -121,11 +135,11 @@ class CharacterSet:
|
|||
# store base filename for easy comparisons with not-yet-loaded sets
|
||||
self.base_filename = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
return True
|
||||
|
||||
|
||||
def load_image_data(self):
|
||||
# load and process image
|
||||
img = Image.open(self.image_filename)
|
||||
img = img.convert('RGBA')
|
||||
img = img.convert("RGBA")
|
||||
# flip for openGL
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
self.image_width, self.image_height = img.size
|
||||
|
|
@ -143,25 +157,35 @@ class CharacterSet:
|
|||
# flip image data back and save it for later, eg image conversion
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
self.image_data = img
|
||||
|
||||
|
||||
def set_char_dimensions(self):
|
||||
# store character dimensions and UV size
|
||||
self.char_width = int(self.image_width / self.map_width)
|
||||
self.char_height = int(self.image_height / self.map_height)
|
||||
self.u_width = self.char_width / self.image_width
|
||||
self.v_height = self.char_height / self.image_height
|
||||
|
||||
|
||||
def report(self):
|
||||
self.app.log(' source texture %s is %s x %s pixels' % (self.image_filename, self.image_width, self.image_height))
|
||||
self.app.log(' char pixel width/height is %s x %s' % (self.char_width, self.char_height))
|
||||
self.app.log(' char map width/height is %s x %s' % (self.map_width, self.map_height))
|
||||
self.app.log(' last character index: %s' % self.last_index)
|
||||
|
||||
self.app.log(
|
||||
" source texture %s is %s x %s pixels"
|
||||
% (self.image_filename, self.image_width, self.image_height)
|
||||
)
|
||||
self.app.log(
|
||||
" char pixel width/height is %s x %s" % (self.char_width, self.char_height)
|
||||
)
|
||||
self.app.log(
|
||||
" char map width/height is %s x %s" % (self.map_width, self.map_height)
|
||||
)
|
||||
self.app.log(" last character index: %s" % self.last_index)
|
||||
|
||||
def has_updated(self):
|
||||
"return True if source image file has changed since last check"
|
||||
# tolerate bad filenames in data, don't check stamps on nonexistent ones
|
||||
if not self.image_filename or not os.path.exists(self.filename) or \
|
||||
not os.path.exists(self.image_filename):
|
||||
if (
|
||||
not self.image_filename
|
||||
or not os.path.exists(self.filename)
|
||||
or not os.path.exists(self.image_filename)
|
||||
):
|
||||
return False
|
||||
data_changed = os.path.getmtime(self.filename) > self.last_data_change
|
||||
img_changed = os.path.getmtime(self.image_filename) > self.last_image_change
|
||||
|
|
@ -170,10 +194,10 @@ class CharacterSet:
|
|||
if img_changed:
|
||||
self.last_image_change = time.time()
|
||||
return data_changed or img_changed
|
||||
|
||||
|
||||
def get_char_index(self, char):
|
||||
return self.char_mapping.get(char, 0)
|
||||
|
||||
|
||||
def get_solid_pixels_in_char(self, char_index):
|
||||
"Returns # of solid pixels in character at given index"
|
||||
tile_x = int(char_index % self.map_width)
|
||||
|
|
|
|||
270
collision.py
270
collision.py
|
|
@ -1,8 +1,11 @@
|
|||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
from renderable import TileRenderable
|
||||
from renderable_line import CircleCollisionRenderable, BoxCollisionRenderable, TileBoxCollisionRenderable
|
||||
from renderable_line import (
|
||||
BoxCollisionRenderable,
|
||||
CircleCollisionRenderable,
|
||||
TileBoxCollisionRenderable,
|
||||
)
|
||||
|
||||
# collision shape types
|
||||
CST_NONE = 0
|
||||
|
|
@ -32,11 +35,11 @@ CTG_DYNAMIC = [CT_GENERIC_DYNAMIC, CT_PLAYER]
|
|||
|
||||
__pdoc__ = {}
|
||||
# named tuples for collision structs that don't merit a class
|
||||
Contact = namedtuple('Contact', ['overlap', 'timestamp'])
|
||||
__pdoc__['Contact'] = "Represents a contact between two objects."
|
||||
Contact = namedtuple("Contact", ["overlap", "timestamp"])
|
||||
__pdoc__["Contact"] = "Represents a contact between two objects."
|
||||
|
||||
ShapeOverlap = namedtuple('ShapeOverlap', ['x', 'y', 'dist', 'area', 'other'])
|
||||
__pdoc__['ShapeOverlap'] = "Represents a CollisionShape's overlap with another."
|
||||
ShapeOverlap = namedtuple("ShapeOverlap", ["x", "y", "dist", "area", "other"])
|
||||
__pdoc__["ShapeOverlap"] = "Represents a CollisionShape's overlap with another."
|
||||
|
||||
|
||||
class CollisionShape:
|
||||
|
|
@ -44,6 +47,7 @@ class CollisionShape:
|
|||
Abstract class for a shape that can overlap and collide with other shapes.
|
||||
Shapes are part of a Collideable which in turn is part of a GameObject.
|
||||
"""
|
||||
|
||||
def resolve_overlaps_with_shapes(self, shapes):
|
||||
"Resolve this shape's overlap(s) with given list of shapes."
|
||||
overlaps = []
|
||||
|
|
@ -57,11 +61,11 @@ class CollisionShape:
|
|||
return
|
||||
# resolve collisions in order of largest -> smallest overlap
|
||||
overlaps.sort(key=lambda item: item.area, reverse=True)
|
||||
for i,old_overlap in enumerate(overlaps):
|
||||
for i, old_overlap in enumerate(overlaps):
|
||||
# resolve first overlap without recalculating
|
||||
overlap = self.get_overlap(old_overlap.other) if i > 0 else overlaps[0]
|
||||
self.resolve_overlap(overlap)
|
||||
|
||||
|
||||
def resolve_overlap(self, overlap):
|
||||
"Resolve this shape's given overlap."
|
||||
other = overlap.other
|
||||
|
|
@ -97,7 +101,7 @@ class CollisionShape:
|
|||
world.try_object_method(self.go, self.go.started_colliding, [other.go])
|
||||
if b_started_a:
|
||||
world.try_object_method(other.go, other.go.started_colliding, [self.go])
|
||||
|
||||
|
||||
def get_overlapping_static_shapes(self):
|
||||
"Return a list of static shapes that overlap with this shape."
|
||||
overlapping_shapes = []
|
||||
|
|
@ -118,80 +122,118 @@ class CollisionShape:
|
|||
else:
|
||||
# skip if even bounds don't overlap
|
||||
obj_left, obj_top, obj_right, obj_bottom = obj.get_edges()
|
||||
if not boxes_overlap(shape_left, shape_top, shape_right, shape_bottom,
|
||||
obj_left, obj_top, obj_right, obj_bottom):
|
||||
if not boxes_overlap(
|
||||
shape_left,
|
||||
shape_top,
|
||||
shape_right,
|
||||
shape_bottom,
|
||||
obj_left,
|
||||
obj_top,
|
||||
obj_right,
|
||||
obj_bottom,
|
||||
):
|
||||
continue
|
||||
overlapping_shapes += obj.collision.get_shapes_overlapping_box(shape_left, shape_top, shape_right, shape_bottom)
|
||||
overlapping_shapes += obj.collision.get_shapes_overlapping_box(
|
||||
shape_left, shape_top, shape_right, shape_bottom
|
||||
)
|
||||
return overlapping_shapes
|
||||
|
||||
|
||||
class CircleCollisionShape(CollisionShape):
|
||||
"CollisionShape using a circle area."
|
||||
|
||||
def __init__(self, loc_x, loc_y, radius, game_object):
|
||||
self.x, self.y = loc_x, loc_y
|
||||
self.radius = radius
|
||||
self.go = game_object
|
||||
|
||||
|
||||
def get_box(self):
|
||||
"Return world coordinates of our bounds (left, top, right, bottom)"
|
||||
return self.x - self.radius, self.y - self.radius, self.x + self.radius, self.y + self.radius
|
||||
|
||||
return (
|
||||
self.x - self.radius,
|
||||
self.y - self.radius,
|
||||
self.x + self.radius,
|
||||
self.y + self.radius,
|
||||
)
|
||||
|
||||
def is_point_inside(self, x, y):
|
||||
"Return True if given point is inside this shape."
|
||||
return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius ** 2
|
||||
|
||||
return (self.x - x) ** 2 + (self.y - y) ** 2 <= self.radius**2
|
||||
|
||||
def overlaps_line(self, x1, y1, x2, y2):
|
||||
"Return True if this circle overlaps given line segment."
|
||||
return circle_overlaps_line(self.x, self.y, self.radius, x1, y1, x2, y2)
|
||||
|
||||
|
||||
def get_overlap(self, other):
|
||||
"Return ShapeOverlap data for this shape's overlap with given other."
|
||||
if type(other) is CircleCollisionShape:
|
||||
px, py, pdist1, pdist2 = point_circle_penetration(self.x, self.y,
|
||||
other.x, other.y,
|
||||
self.radius + other.radius)
|
||||
px, py, pdist1, pdist2 = point_circle_penetration(
|
||||
self.x, self.y, other.x, other.y, self.radius + other.radius
|
||||
)
|
||||
elif type(other) is AABBCollisionShape:
|
||||
px, py, pdist1, pdist2 = circle_box_penetration(self.x, self.y,
|
||||
other.x, other.y,
|
||||
self.radius, other.halfwidth,
|
||||
other.halfheight)
|
||||
px, py, pdist1, pdist2 = circle_box_penetration(
|
||||
self.x,
|
||||
self.y,
|
||||
other.x,
|
||||
other.y,
|
||||
self.radius,
|
||||
other.halfwidth,
|
||||
other.halfheight,
|
||||
)
|
||||
area = abs(pdist1 * pdist2) if pdist1 < 0 else 0
|
||||
return ShapeOverlap(x=px, y=py, dist=pdist1, area=area, other=other)
|
||||
|
||||
|
||||
class AABBCollisionShape(CollisionShape):
|
||||
"CollisionShape using an axis-aligned bounding box area."
|
||||
|
||||
def __init__(self, loc_x, loc_y, halfwidth, halfheight, game_object):
|
||||
self.x, self.y = loc_x, loc_y
|
||||
self.halfwidth, self.halfheight = halfwidth, halfheight
|
||||
self.go = game_object
|
||||
# for CST_TILE objects, lists of tile(s) we cover
|
||||
self.tiles = []
|
||||
|
||||
|
||||
def get_box(self):
|
||||
return self.x - self.halfwidth, self.y - self.halfheight, self.x + self.halfwidth, self.y + self.halfheight
|
||||
|
||||
return (
|
||||
self.x - self.halfwidth,
|
||||
self.y - self.halfheight,
|
||||
self.x + self.halfwidth,
|
||||
self.y + self.halfheight,
|
||||
)
|
||||
|
||||
def is_point_inside(self, x, y):
|
||||
"Return True if given point is inside this shape."
|
||||
return point_in_box(x, y, *self.get_box())
|
||||
|
||||
|
||||
def overlaps_line(self, x1, y1, x2, y2):
|
||||
"Return True if this box overlaps given line segment."
|
||||
left, top, right, bottom = self.get_box()
|
||||
return box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2)
|
||||
|
||||
|
||||
def get_overlap(self, other):
|
||||
"Return ShapeOverlap data for this shape's overlap with given other."
|
||||
if type(other) is AABBCollisionShape:
|
||||
px, py, pdist1, pdist2 = box_penetration(self.x, self.y,
|
||||
other.x, other.y,
|
||||
self.halfwidth, self.halfheight,
|
||||
other.halfwidth, other.halfheight)
|
||||
px, py, pdist1, pdist2 = box_penetration(
|
||||
self.x,
|
||||
self.y,
|
||||
other.x,
|
||||
other.y,
|
||||
self.halfwidth,
|
||||
self.halfheight,
|
||||
other.halfwidth,
|
||||
other.halfheight,
|
||||
)
|
||||
elif type(other) is CircleCollisionShape:
|
||||
px, py, pdist1, pdist2 = circle_box_penetration(other.x, other.y,
|
||||
self.x, self.y,
|
||||
other.radius, self.halfwidth,
|
||||
self.halfheight)
|
||||
px, py, pdist1, pdist2 = circle_box_penetration(
|
||||
other.x,
|
||||
other.y,
|
||||
self.x,
|
||||
self.y,
|
||||
other.radius,
|
||||
self.halfwidth,
|
||||
self.halfheight,
|
||||
)
|
||||
# reverse result if we're shape B
|
||||
px, py = -px, -py
|
||||
area = abs(pdist1 * pdist2) if pdist1 < 0 else 0
|
||||
|
|
@ -200,8 +242,10 @@ class AABBCollisionShape(CollisionShape):
|
|||
|
||||
class Collideable:
|
||||
"Collision component for GameObjects. Contains a list of shapes."
|
||||
|
||||
use_art_offset = False
|
||||
"use game object's art_off_pct values"
|
||||
|
||||
def __init__(self, obj):
|
||||
"Create new Collideable for given GameObject."
|
||||
self.go = obj
|
||||
|
|
@ -212,7 +256,7 @@ class Collideable:
|
|||
self.contacts = {}
|
||||
"Dict of contacts with other objects, by object name"
|
||||
self.create_shapes()
|
||||
|
||||
|
||||
def create_shapes(self):
|
||||
"""
|
||||
Create collision shape(s) appropriate to our game object's
|
||||
|
|
@ -231,7 +275,7 @@ class Collideable:
|
|||
# update renderables once if static
|
||||
if not self.go.is_dynamic():
|
||||
self.update_renderables()
|
||||
|
||||
|
||||
def _clear_shapes(self):
|
||||
for r in self.renderables:
|
||||
r.destroy()
|
||||
|
|
@ -240,41 +284,49 @@ class Collideable:
|
|||
self.cl._remove_shape(shape)
|
||||
self.shapes = []
|
||||
"List of CollisionShapes"
|
||||
|
||||
|
||||
def _create_circle(self):
|
||||
x = self.go.x + self.go.col_offset_x
|
||||
y = self.go.y + self.go.col_offset_y
|
||||
shape = self.cl._add_circle_shape(x, y, self.go.col_radius, self.go)
|
||||
self.shapes = [shape]
|
||||
self.renderables = [CircleCollisionRenderable(shape)]
|
||||
|
||||
|
||||
def _create_box(self):
|
||||
x = self.go.x # + self.go.col_offset_x
|
||||
y = self.go.y # + self.go.col_offset_y
|
||||
shape = self.cl._add_box_shape(x, y,
|
||||
self.go.col_width / 2,
|
||||
self.go.col_height / 2,
|
||||
self.go)
|
||||
x = self.go.x # + self.go.col_offset_x
|
||||
y = self.go.y # + self.go.col_offset_y
|
||||
shape = self.cl._add_box_shape(
|
||||
x, y, self.go.col_width / 2, self.go.col_height / 2, self.go
|
||||
)
|
||||
self.shapes = [shape]
|
||||
self.renderables = [BoxCollisionRenderable(shape)]
|
||||
|
||||
|
||||
def _create_merged_tile_boxes(self):
|
||||
"Create AABB shapes for a CST_TILE object"
|
||||
# generate fewer, larger boxes!
|
||||
frame = self.go.renderable.frame
|
||||
if not self.go.col_layer_name in self.go.art.layer_names:
|
||||
self.go.app.dev_log("%s: Couldn't find collision layer with name '%s'" % (self.go.name, self.go.col_layer_name))
|
||||
if self.go.col_layer_name not in self.go.art.layer_names:
|
||||
self.go.app.dev_log(
|
||||
"%s: Couldn't find collision layer with name '%s'"
|
||||
% (self.go.name, self.go.col_layer_name)
|
||||
)
|
||||
return
|
||||
layer = self.go.art.layer_names.index(self.go.col_layer_name)
|
||||
|
||||
# tile is available if it's not empty and not already covered by a shape
|
||||
def tile_available(tile_x, tile_y):
|
||||
return self.go.art.get_char_index_at(frame, layer, tile_x, tile_y) != 0 and not (tile_x, tile_y) in self.tile_shapes
|
||||
return (
|
||||
self.go.art.get_char_index_at(frame, layer, tile_x, tile_y) != 0
|
||||
and (tile_x, tile_y) not in self.tile_shapes
|
||||
)
|
||||
|
||||
def tile_range_available(start_x, end_x, start_y, end_y):
|
||||
for y in range(start_y, end_y + 1):
|
||||
for x in range(start_x, end_x + 1):
|
||||
if not tile_available(x, y):
|
||||
return False
|
||||
return True
|
||||
|
||||
for y in range(self.go.art.height):
|
||||
for x in range(self.go.art.width):
|
||||
if not tile_available(x, y):
|
||||
|
|
@ -286,7 +338,9 @@ class Collideable:
|
|||
end_x += 1
|
||||
# then fill top to bottom
|
||||
end_y = y
|
||||
while end_y < self.go.art.height - 1 and tile_range_available(x, end_x, y, end_y + 1):
|
||||
while end_y < self.go.art.height - 1 and tile_range_available(
|
||||
x, end_x, y, end_y + 1
|
||||
):
|
||||
end_y += 1
|
||||
# compute origin and halfsizes of box covering tile range
|
||||
wx1, wy1 = self.go.get_tile_loc(x, y, tile_center=True)
|
||||
|
|
@ -299,8 +353,7 @@ class Collideable:
|
|||
halfheight = (end_y - y) * self.go.art.quad_height
|
||||
halfheight /= 2
|
||||
halfheight += self.go.art.quad_height / 2
|
||||
shape = self.cl._add_box_shape(wx, wy, halfwidth, halfheight,
|
||||
self.go)
|
||||
shape = self.cl._add_box_shape(wx, wy, halfwidth, halfheight, self.go)
|
||||
# fill in cell(s) in our tile collision dict,
|
||||
# write list of tiles shape covers to shape.tiles
|
||||
for tile_y in range(y, end_y + 1):
|
||||
|
|
@ -312,26 +365,26 @@ class Collideable:
|
|||
r.update()
|
||||
self.shapes.append(shape)
|
||||
self.renderables.append(r)
|
||||
|
||||
|
||||
def get_shape_overlapping_point(self, x, y):
|
||||
"Return shape if it's overlapping given point, None if no overlap."
|
||||
tile_x, tile_y = self.go.get_tile_at_point(x, y)
|
||||
return self.tile_shapes.get((tile_x, tile_y), None)
|
||||
|
||||
|
||||
def get_shapes_overlapping_box(self, left, top, right, bottom):
|
||||
"Return a list of our shapes that overlap given box."
|
||||
shapes = []
|
||||
tiles = self.go.get_tiles_overlapping_box(left, top, right, bottom)
|
||||
for (x, y) in tiles:
|
||||
for x, y in tiles:
|
||||
shape = self.tile_shapes.get((x, y), None)
|
||||
if shape and not shape in shapes:
|
||||
if shape and shape not in shapes:
|
||||
shapes.append(shape)
|
||||
return shapes
|
||||
|
||||
|
||||
def update(self):
|
||||
if self.go and self.go.is_dynamic():
|
||||
self.update_transform_from_object()
|
||||
|
||||
|
||||
def update_transform_from_object(self, obj=None):
|
||||
"Snap our shapes to location of given object (if unspecified, our GO)."
|
||||
obj = obj or self.go
|
||||
|
|
@ -341,7 +394,7 @@ class Collideable:
|
|||
for shape in self.shapes:
|
||||
shape.x = obj.x + obj.col_offset_x
|
||||
shape.y = obj.y + obj.col_offset_y
|
||||
|
||||
|
||||
def set_shape_color(self, shape, new_color):
|
||||
"Set the color of a given shape's debug LineRenderable."
|
||||
try:
|
||||
|
|
@ -351,15 +404,15 @@ class Collideable:
|
|||
self.renderables[shape_index].color = new_color
|
||||
self.renderables[shape_index].build_geo()
|
||||
self.renderables[shape_index].rebind_buffers()
|
||||
|
||||
|
||||
def update_renderables(self):
|
||||
for r in self.renderables:
|
||||
r.update()
|
||||
|
||||
|
||||
def render(self):
|
||||
for r in self.renderables:
|
||||
r.render()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
for r in self.renderables:
|
||||
r.destroy()
|
||||
|
|
@ -373,26 +426,29 @@ class CollisionLord:
|
|||
Collision manager object, tracks Collideables, detects overlaps and
|
||||
resolves collisions.
|
||||
"""
|
||||
|
||||
iterations = 7
|
||||
"""
|
||||
Number of times to resolve collisions per update. Lower at own risk;
|
||||
multi-object collisions require multiple iterations to settle correctly.
|
||||
"""
|
||||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
self.ticks = 0
|
||||
# list of objects processed for collision this frame
|
||||
self.collisions_this_frame = []
|
||||
self.reset()
|
||||
|
||||
|
||||
def report(self):
|
||||
print('%s: %s dynamic shapes, %s static shapes' % (self,
|
||||
len(self.dynamic_shapes),
|
||||
len(self.static_shapes)))
|
||||
|
||||
print(
|
||||
"%s: %s dynamic shapes, %s static shapes"
|
||||
% (self, len(self.dynamic_shapes), len(self.static_shapes))
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
self.dynamic_shapes, self.static_shapes = [], []
|
||||
|
||||
|
||||
def _add_circle_shape(self, x, y, radius, game_object):
|
||||
shape = CircleCollisionShape(x, y, radius, game_object)
|
||||
if game_object.is_dynamic():
|
||||
|
|
@ -400,7 +456,7 @@ class CollisionLord:
|
|||
else:
|
||||
self.static_shapes.append(shape)
|
||||
return shape
|
||||
|
||||
|
||||
def _add_box_shape(self, x, y, halfwidth, halfheight, game_object):
|
||||
shape = AABBCollisionShape(x, y, halfwidth, halfheight, game_object)
|
||||
if game_object.is_dynamic():
|
||||
|
|
@ -408,13 +464,13 @@ class CollisionLord:
|
|||
else:
|
||||
self.static_shapes.append(shape)
|
||||
return shape
|
||||
|
||||
|
||||
def _remove_shape(self, shape):
|
||||
if shape in self.dynamic_shapes:
|
||||
self.dynamic_shapes.remove(shape)
|
||||
elif shape in self.static_shapes:
|
||||
self.static_shapes.remove(shape)
|
||||
|
||||
|
||||
def update(self):
|
||||
"Resolve overlaps between all relevant world objects."
|
||||
for i in range(self.iterations):
|
||||
|
|
@ -437,19 +493,25 @@ class CollisionLord:
|
|||
|
||||
# collision handling
|
||||
|
||||
|
||||
def point_in_box(x, y, box_left, box_top, box_right, box_bottom):
|
||||
"Return True if given point lies within box with given corners."
|
||||
return box_left <= x <= box_right and box_bottom <= y <= box_top
|
||||
|
||||
def boxes_overlap(left_a, top_a, right_a, bottom_a,
|
||||
left_b, top_b, right_b, bottom_b):
|
||||
|
||||
def boxes_overlap(left_a, top_a, right_a, bottom_a, left_b, top_b, right_b, bottom_b):
|
||||
"Return True if given boxes A and B overlap."
|
||||
for (x, y) in ((left_a, top_a), (right_a, top_a),
|
||||
(right_a, bottom_a), (left_a, bottom_a)):
|
||||
for x, y in (
|
||||
(left_a, top_a),
|
||||
(right_a, top_a),
|
||||
(right_a, bottom_a),
|
||||
(left_a, bottom_a),
|
||||
):
|
||||
if left_b <= x <= right_b and bottom_b <= y <= top_b:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
|
||||
"Return True if given lines intersect."
|
||||
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
|
||||
|
|
@ -465,6 +527,7 @@ def lines_intersect(x1, y1, x2, y2, x3, y3, x4, y4):
|
|||
ub = numer2 / denom
|
||||
return ua >= 0 and ua <= 1 and ub >= 0 and ub <= 1
|
||||
|
||||
|
||||
def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2):
|
||||
"Return point on given line that's closest to given point."
|
||||
wx, wy = point_x - x1, point_y - y1
|
||||
|
|
@ -473,7 +536,7 @@ def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2):
|
|||
if proj <= 0:
|
||||
# line point 1 is closest
|
||||
return x1, y1
|
||||
vsq = dir_x ** 2 + dir_y ** 2
|
||||
vsq = dir_x**2 + dir_y**2
|
||||
if proj >= vsq:
|
||||
# line point 2 is closest
|
||||
return x2, y2
|
||||
|
|
@ -481,25 +544,32 @@ def line_point_closest_to_point(point_x, point_y, x1, y1, x2, y2):
|
|||
# closest point is between 1 and 2
|
||||
return x1 + (proj / vsq) * dir_x, y1 + (proj / vsq) * dir_y
|
||||
|
||||
|
||||
def circle_overlaps_line(circle_x, circle_y, radius, x1, y1, x2, y2):
|
||||
"Return True if given circle overlaps given line."
|
||||
# get closest point on line to circle center
|
||||
closest_x, closest_y = line_point_closest_to_point(circle_x, circle_y,
|
||||
x1, y1, x2, y2)
|
||||
closest_x, closest_y = line_point_closest_to_point(
|
||||
circle_x, circle_y, x1, y1, x2, y2
|
||||
)
|
||||
dist_x, dist_y = closest_x - circle_x, closest_y - circle_y
|
||||
return dist_x ** 2 + dist_y ** 2 <= radius ** 2
|
||||
return dist_x**2 + dist_y**2 <= radius**2
|
||||
|
||||
|
||||
def box_overlaps_line(left, top, right, bottom, x1, y1, x2, y2):
|
||||
"Return True if given box overlaps given line."
|
||||
# TODO: determine if this is less efficient than slab method below
|
||||
if point_in_box(x1, y1, left, top, right, bottom) and \
|
||||
point_in_box(x2, y2, left, top, right, bottom):
|
||||
if point_in_box(x1, y1, left, top, right, bottom) and point_in_box(
|
||||
x2, y2, left, top, right, bottom
|
||||
):
|
||||
return True
|
||||
# check left/top/right/bottoms edges
|
||||
return lines_intersect(left, top, left, bottom, x1, y1, x2, y2) or \
|
||||
lines_intersect(left, top, right, top, x1, y1, x2, y2) or \
|
||||
lines_intersect(right, top, right, bottom, x1, y1, x2, y2) or \
|
||||
lines_intersect(left, bottom, right, bottom, x1, y1, x2, y2)
|
||||
return (
|
||||
lines_intersect(left, top, left, bottom, x1, y1, x2, y2)
|
||||
or lines_intersect(left, top, right, top, x1, y1, x2, y2)
|
||||
or lines_intersect(right, top, right, bottom, x1, y1, x2, y2)
|
||||
or lines_intersect(left, bottom, right, bottom, x1, y1, x2, y2)
|
||||
)
|
||||
|
||||
|
||||
def box_overlaps_ray(left, top, right, bottom, x1, y1, x2, y2):
|
||||
"Return True if given box overlaps given ray."
|
||||
|
|
@ -519,16 +589,18 @@ def box_overlaps_ray(left, top, right, bottom, x1, y1, x2, y2):
|
|||
tmax = min(tmax, max(ty1, ty2))
|
||||
return tmax >= tmin
|
||||
|
||||
|
||||
def point_circle_penetration(point_x, point_y, circle_x, circle_y, radius):
|
||||
"Return normalized penetration x, y, and distance for given circles."
|
||||
dx, dy = circle_x - point_x, circle_y - point_y
|
||||
pdist = math.sqrt(dx ** 2 + dy ** 2)
|
||||
pdist = math.sqrt(dx**2 + dy**2)
|
||||
# point is center of circle, arbitrarily project out in +X
|
||||
if pdist == 0:
|
||||
return 1, 0, -radius, -radius
|
||||
# TODO: calculate other axis of intersection for area?
|
||||
return dx / pdist, dy / pdist, pdist - radius, pdist - radius
|
||||
|
||||
|
||||
def box_penetration(ax, ay, bx, by, ahw, ahh, bhw, bhh):
|
||||
"Return penetration vector and magnitude for given boxes."
|
||||
left_a, right_a = ax - ahw, ax + ahw
|
||||
|
|
@ -553,22 +625,32 @@ def box_penetration(ax, ay, bx, by, ahw, ahh, bhw, bhh):
|
|||
elif dy < 0:
|
||||
return 0, -1, -py, -px
|
||||
|
||||
def circle_box_penetration(circle_x, circle_y, box_x, box_y, circle_radius,
|
||||
box_hw, box_hh):
|
||||
|
||||
def circle_box_penetration(
|
||||
circle_x, circle_y, box_x, box_y, circle_radius, box_hw, box_hh
|
||||
):
|
||||
"Return penetration vector and magnitude for given circle and box."
|
||||
box_left, box_right = box_x - box_hw, box_x + box_hw
|
||||
box_top, box_bottom = box_y + box_hh, box_y - box_hh
|
||||
# if circle center inside box, use box-on-box penetration vector + distance
|
||||
if point_in_box(circle_x, circle_y, box_left, box_top, box_right, box_bottom):
|
||||
return box_penetration(circle_x, circle_y, box_x, box_y,
|
||||
circle_radius, circle_radius, box_hw, box_hh)
|
||||
return box_penetration(
|
||||
circle_x,
|
||||
circle_y,
|
||||
box_x,
|
||||
box_y,
|
||||
circle_radius,
|
||||
circle_radius,
|
||||
box_hw,
|
||||
box_hh,
|
||||
)
|
||||
# find point on AABB edges closest to center of circle
|
||||
# clamp = min(highest, max(lowest, val))
|
||||
px = min(box_right, max(box_left, circle_x))
|
||||
py = min(box_top, max(box_bottom, circle_y))
|
||||
closest_x = circle_x - px
|
||||
closest_y = circle_y - py
|
||||
d = math.sqrt(closest_x ** 2 + closest_y ** 2)
|
||||
d = math.sqrt(closest_x**2 + closest_y**2)
|
||||
pdist = circle_radius - d
|
||||
if d == 0:
|
||||
return
|
||||
|
|
|
|||
218
cursor.py
218
cursor.py
|
|
@ -1,4 +1,6 @@
|
|||
import math, ctypes
|
||||
import ctypes
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from OpenGL import GL
|
||||
|
||||
|
|
@ -24,38 +26,29 @@ OUTSIDE_EDGE_SIZE = 0.2
|
|||
THICKNESS = 0.1
|
||||
|
||||
corner_verts = [
|
||||
0, 0, # A/0
|
||||
OUTSIDE_EDGE_SIZE, 0, # B/1
|
||||
OUTSIDE_EDGE_SIZE, -THICKNESS, # C/2
|
||||
THICKNESS, -THICKNESS, # D/3
|
||||
THICKNESS, -OUTSIDE_EDGE_SIZE, # E/4
|
||||
0, -OUTSIDE_EDGE_SIZE # F/5
|
||||
0,
|
||||
0, # A/0
|
||||
OUTSIDE_EDGE_SIZE,
|
||||
0, # B/1
|
||||
OUTSIDE_EDGE_SIZE,
|
||||
-THICKNESS, # C/2
|
||||
THICKNESS,
|
||||
-THICKNESS, # D/3
|
||||
THICKNESS,
|
||||
-OUTSIDE_EDGE_SIZE, # E/4
|
||||
0,
|
||||
-OUTSIDE_EDGE_SIZE, # F/5
|
||||
]
|
||||
|
||||
# vert indices for the above
|
||||
corner_elems = [
|
||||
0, 1, 2,
|
||||
0, 2, 3,
|
||||
0, 3, 4,
|
||||
0, 5, 4
|
||||
]
|
||||
corner_elems = [0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 5, 4]
|
||||
|
||||
# X/Y flip transforms to make all 4 corners
|
||||
# (top left, top right, bottom left, bottom right)
|
||||
corner_transforms = [
|
||||
( 1, 1),
|
||||
(-1, 1),
|
||||
( 1, -1),
|
||||
(-1, -1)
|
||||
]
|
||||
corner_transforms = [(1, 1), (-1, 1), (1, -1), (-1, -1)]
|
||||
|
||||
# offsets to translate the 4 corners by
|
||||
corner_offsets = [
|
||||
(0, 0),
|
||||
(1, 0),
|
||||
(0, -1),
|
||||
(1, -1)
|
||||
]
|
||||
corner_offsets = [(0, 0), (1, 0), (0, -1), (1, -1)]
|
||||
|
||||
BASE_COLOR = (0.8, 0.8, 0.8, 1)
|
||||
|
||||
|
|
@ -63,14 +56,14 @@ BASE_COLOR = (0.8, 0.8, 0.8, 1)
|
|||
# because a static vertex list wouldn't be able to adjust to different
|
||||
# character set aspect ratios.
|
||||
|
||||
|
||||
class Cursor:
|
||||
|
||||
vert_shader_source = 'cursor_v.glsl'
|
||||
frag_shader_source = 'cursor_f.glsl'
|
||||
vert_shader_source = "cursor_v.glsl"
|
||||
frag_shader_source = "cursor_f.glsl"
|
||||
alpha = 1
|
||||
icon_scale_factor = 4
|
||||
logg = False
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.x, self.y, self.z = 0, 0, 0
|
||||
|
|
@ -92,29 +85,40 @@ class Cursor:
|
|||
self.elem_array = np.array(corner_elems, dtype=np.uint32)
|
||||
self.vert_count = int(len(self.elem_array))
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vert_array.nbytes,
|
||||
self.vert_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.vert_array.nbytes,
|
||||
self.vert_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_array.nbytes,
|
||||
self.elem_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER,
|
||||
self.elem_array.nbytes,
|
||||
self.elem_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
# shader, attributes
|
||||
self.shader = self.app.sl.new_shader(self.vert_shader_source, self.frag_shader_source)
|
||||
self.shader = self.app.sl.new_shader(
|
||||
self.vert_shader_source, self.frag_shader_source
|
||||
)
|
||||
# vert positions
|
||||
self.pos_attrib = self.shader.get_attrib_location('vertPosition')
|
||||
self.pos_attrib = self.shader.get_attrib_location("vertPosition")
|
||||
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||
offset = ctypes.c_void_p(0)
|
||||
GL.glVertexAttribPointer(self.pos_attrib, 2,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||
GL.glVertexAttribPointer(
|
||||
self.pos_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
|
||||
)
|
||||
# uniforms
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location('projection')
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location('view')
|
||||
self.position_uniform = self.shader.get_uniform_location('objectPosition')
|
||||
self.scale_uniform = self.shader.get_uniform_location('objectScale')
|
||||
self.color_uniform = self.shader.get_uniform_location('baseColor')
|
||||
self.quad_size_uniform = self.shader.get_uniform_location('quadSize')
|
||||
self.xform_uniform = self.shader.get_uniform_location('vertTransform')
|
||||
self.offset_uniform = self.shader.get_uniform_location('vertOffset')
|
||||
self.alpha_uniform = self.shader.get_uniform_location('baseAlpha')
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location("projection")
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location("view")
|
||||
self.position_uniform = self.shader.get_uniform_location("objectPosition")
|
||||
self.scale_uniform = self.shader.get_uniform_location("objectScale")
|
||||
self.color_uniform = self.shader.get_uniform_location("baseColor")
|
||||
self.quad_size_uniform = self.shader.get_uniform_location("quadSize")
|
||||
self.xform_uniform = self.shader.get_uniform_location("vertTransform")
|
||||
self.offset_uniform = self.shader.get_uniform_location("vertOffset")
|
||||
self.alpha_uniform = self.shader.get_uniform_location("baseAlpha")
|
||||
# finish
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
|
|
@ -122,11 +126,11 @@ class Cursor:
|
|||
GL.glBindVertexArray(0)
|
||||
# init tool sprite, tool will provide texture when rendered
|
||||
self.tool_sprite = UISpriteRenderable(self.app)
|
||||
|
||||
|
||||
def clamp_to_active_art(self):
|
||||
self.x = max(0, min(self.x, self.app.ui.active_art.width - 1))
|
||||
self.y = min(0, max(self.y, -self.app.ui.active_art.height + 1))
|
||||
|
||||
|
||||
def keyboard_move(self, delta_x, delta_y):
|
||||
if not self.app.ui.active_art:
|
||||
return
|
||||
|
|
@ -136,11 +140,14 @@ class Cursor:
|
|||
self.moved = True
|
||||
self.app.keyboard_editing = True
|
||||
if self.logg:
|
||||
self.app.log('Cursor: %s,%s,%s scale %.2f,%.2f' % (self.x, self.y, self.z, self.scale_x, self.scale_y))
|
||||
|
||||
self.app.log(
|
||||
"Cursor: %s,%s,%s scale %.2f,%.2f"
|
||||
% (self.x, self.y, self.z, self.scale_x, self.scale_y)
|
||||
)
|
||||
|
||||
def set_scale(self, new_scale):
|
||||
self.scale_x = self.scale_y = new_scale
|
||||
|
||||
|
||||
def get_tile(self):
|
||||
# adjust for brush size
|
||||
size = self.app.ui.selected_tool.brush_size
|
||||
|
|
@ -149,7 +156,7 @@ class Cursor:
|
|||
return int(self.x + size_offset), int(-self.y + size_offset)
|
||||
else:
|
||||
return int(self.x), int(-self.y)
|
||||
|
||||
|
||||
def center_in_art(self):
|
||||
art = self.app.ui.active_art
|
||||
if not art:
|
||||
|
|
@ -157,20 +164,20 @@ class Cursor:
|
|||
self.x = round(art.width / 2) * art.quad_width
|
||||
self.y = round(-art.height / 2) * art.quad_height
|
||||
self.moved = True
|
||||
|
||||
|
||||
# !!TODO!! finish this, work in progress
|
||||
def get_tiles_under_drag(self):
|
||||
"""
|
||||
returns list of tuple coordinates of all tiles under cursor's current
|
||||
position AND tiles it's moved over since last update
|
||||
"""
|
||||
|
||||
|
||||
# TODO: get vector of last to current position, for each tile under
|
||||
# current brush, do line trace along grid towards last point
|
||||
|
||||
|
||||
# TODO: this works in two out of four diagonals,
|
||||
# swap current and last positions to determine delta?
|
||||
|
||||
|
||||
if self.last_x <= self.x:
|
||||
x0, y0 = self.last_x, -self.last_y
|
||||
x1, y1 = self.x, -self.y
|
||||
|
|
@ -178,10 +185,10 @@ class Cursor:
|
|||
x0, y0 = self.x, -self.y
|
||||
x1, y1 = self.last_x, -self.last_y
|
||||
tiles = vector.get_tiles_along_line(x0, y0, x1, y1)
|
||||
print('drag from %s,%s to %s,%s:' % (x0, y0, x1, y1))
|
||||
print("drag from %s,%s to %s,%s:" % (x0, y0, x1, y1))
|
||||
print(tiles)
|
||||
return tiles
|
||||
|
||||
|
||||
def get_tiles_under_brush(self):
|
||||
"""
|
||||
returns list of tuple coordinates of all tiles under the cursor @ its
|
||||
|
|
@ -194,11 +201,11 @@ class Cursor:
|
|||
for x in range(x_start, x_start + size):
|
||||
tiles.append((x, y))
|
||||
return tiles
|
||||
|
||||
|
||||
def undo_preview_edits(self):
|
||||
for edit in self.preview_edits:
|
||||
edit.undo()
|
||||
|
||||
|
||||
def update_cursor_preview(self):
|
||||
# rebuild list of cursor preview commands
|
||||
if self.app.ui.selected_tool.show_preview:
|
||||
|
|
@ -207,9 +214,12 @@ class Cursor:
|
|||
edit.apply()
|
||||
else:
|
||||
self.preview_edits = []
|
||||
|
||||
|
||||
def start_paint(self):
|
||||
if self.app.ui.console.visible or self.app.ui.popup in self.app.ui.hovered_elements:
|
||||
if (
|
||||
self.app.ui.console.visible
|
||||
or self.app.ui.popup in self.app.ui.hovered_elements
|
||||
):
|
||||
return
|
||||
if self.app.ui.selected_tool is self.app.ui.grab_tool:
|
||||
self.app.ui.grab_tool.grab()
|
||||
|
|
@ -219,11 +229,14 @@ class Cursor:
|
|||
self.current_command.add_command_tiles(self.preview_edits)
|
||||
self.preview_edits = []
|
||||
self.app.ui.active_art.set_unsaved_changes(True)
|
||||
#print(self.app.ui.active_art.command_stack)
|
||||
|
||||
# print(self.app.ui.active_art.command_stack)
|
||||
|
||||
def finish_paint(self):
|
||||
"invoked by mouse button up and undo"
|
||||
if self.app.ui.console.visible or self.app.ui.popup in self.app.ui.hovered_elements:
|
||||
if (
|
||||
self.app.ui.console.visible
|
||||
or self.app.ui.popup in self.app.ui.hovered_elements
|
||||
):
|
||||
return
|
||||
# push current command group onto undo stack
|
||||
if not self.current_command:
|
||||
|
|
@ -234,25 +247,27 @@ class Cursor:
|
|||
# tools like rotate produce a different change each time, so update again
|
||||
if self.app.ui.selected_tool.update_preview_after_paint:
|
||||
self.update_cursor_preview()
|
||||
#print(self.app.ui.active_art.command_stack)
|
||||
|
||||
# print(self.app.ui.active_art.command_stack)
|
||||
|
||||
def moved_this_frame(self):
|
||||
return self.moved or \
|
||||
int(self.last_x) != int(self.x) or \
|
||||
int(self.last_y) != int(self.y)
|
||||
|
||||
return (
|
||||
self.moved
|
||||
or int(self.last_x) != int(self.x)
|
||||
or int(self.last_y) != int(self.y)
|
||||
)
|
||||
|
||||
def reposition_from_mouse(self):
|
||||
self.x, self.y, _ = vector.screen_to_world(self.app,
|
||||
self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
|
||||
self.x, self.y, _ = vector.screen_to_world(
|
||||
self.app, self.app.mouse_x, self.app.mouse_y
|
||||
)
|
||||
|
||||
def snap_to_tile(self):
|
||||
w, h = self.app.ui.active_art.quad_width, self.app.ui.active_art.quad_height
|
||||
char_aspect = w / h
|
||||
# round result for oddly proportioned charsets
|
||||
self.x = round(math.floor(self.x / w) * w)
|
||||
self.y = round(math.ceil(self.y / h) * h * char_aspect)
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
# vector.screen_to_world result will be off because camera hasn't
|
||||
# moved yet, recalc view matrix
|
||||
|
|
@ -261,16 +276,18 @@ class Cursor:
|
|||
self.snap_to_tile()
|
||||
self.update_cursor_preview()
|
||||
self.entered_new_tile()
|
||||
|
||||
|
||||
def update(self):
|
||||
# save old positions before update
|
||||
self.last_x, self.last_y = self.x, self.y
|
||||
# pulse alpha and scale
|
||||
self.alpha = 0.75 + (math.sin(self.app.get_elapsed_time() / 100) / 2)
|
||||
#self.scale_x = 1.5 + (math.sin(self.get_elapsed_time() / 100) / 50 - 0.5)
|
||||
# self.scale_x = 1.5 + (math.sin(self.get_elapsed_time() / 100) / 50 - 0.5)
|
||||
mouse_moved = self.app.mouse_dx != 0 or self.app.mouse_dy != 0
|
||||
# update cursor from mouse if: mouse moved, camera moved w/o keyboard
|
||||
if mouse_moved or (not self.app.keyboard_editing and self.app.camera.moved_this_frame):
|
||||
if mouse_moved or (
|
||||
not self.app.keyboard_editing and self.app.camera.moved_this_frame
|
||||
):
|
||||
# don't let mouse move cursor if text tool input is happening
|
||||
if not self.app.ui.text_tool.input_active:
|
||||
self.reposition_from_mouse()
|
||||
|
|
@ -297,36 +314,50 @@ class Cursor:
|
|||
self.update_cursor_preview()
|
||||
if self.moved_this_frame():
|
||||
self.entered_new_tile()
|
||||
|
||||
|
||||
def end_update(self):
|
||||
"called at the end of App.update"
|
||||
self.moved = False
|
||||
|
||||
|
||||
def entered_new_tile(self):
|
||||
if self.current_command and self.app.ui.selected_tool.paint_while_dragging:
|
||||
# add new tile(s) to current command group
|
||||
self.current_command.add_command_tiles(self.preview_edits)
|
||||
self.app.ui.active_art.set_unsaved_changes(True)
|
||||
self.preview_edits = []
|
||||
|
||||
|
||||
def render(self):
|
||||
GL.glUseProgram(self.shader.program)
|
||||
GL.glUniformMatrix4fv(self.proj_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.projection_matrix)
|
||||
GL.glUniformMatrix4fv(self.view_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.view_matrix)
|
||||
GL.glUniformMatrix4fv(
|
||||
self.proj_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.projection_matrix
|
||||
)
|
||||
GL.glUniformMatrix4fv(
|
||||
self.view_matrix_uniform, 1, GL.GL_FALSE, self.app.camera.view_matrix
|
||||
)
|
||||
GL.glUniform3f(self.position_uniform, self.x, self.y, self.z)
|
||||
GL.glUniform3f(self.scale_uniform, self.scale_x, self.scale_y, self.scale_z)
|
||||
GL.glUniform4fv(self.color_uniform, 1, self.color)
|
||||
GL.glUniform2f(self.quad_size_uniform, self.app.ui.active_art.quad_width, self.app.ui.active_art.quad_height)
|
||||
GL.glUniform2f(
|
||||
self.quad_size_uniform,
|
||||
self.app.ui.active_art.quad_width,
|
||||
self.app.ui.active_art.quad_height,
|
||||
)
|
||||
GL.glUniform1f(self.alpha_uniform, self.alpha)
|
||||
# VAO vs non-VAO paths
|
||||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(self.vao)
|
||||
else:
|
||||
attrib = self.shader.get_attrib_location # for brevity
|
||||
attrib = self.shader.get_attrib_location # for brevity
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glVertexAttribPointer(attrib('vertPosition'), 2, GL.GL_FLOAT, GL.GL_FALSE, 0,
|
||||
ctypes.c_void_p(0))
|
||||
GL.glEnableVertexAttribArray(attrib('vertPosition'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("vertPosition"),
|
||||
2,
|
||||
GL.GL_FLOAT,
|
||||
GL.GL_FALSE,
|
||||
0,
|
||||
ctypes.c_void_p(0),
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("vertPosition"))
|
||||
# bind elem array instead of passing it to glDrawElements - latter
|
||||
# sends pyopengl a new array, which is deprecated and breaks on Mac.
|
||||
# thanks Erin Congden!
|
||||
|
|
@ -335,12 +366,13 @@ class Cursor:
|
|||
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
|
||||
# draw 4 corners
|
||||
for i in range(4):
|
||||
tx,ty = corner_transforms[i][0], corner_transforms[i][1]
|
||||
ox,oy = corner_offsets[i][0], corner_offsets[i][1]
|
||||
tx, ty = corner_transforms[i][0], corner_transforms[i][1]
|
||||
ox, oy = corner_offsets[i][0], corner_offsets[i][1]
|
||||
GL.glUniform2f(self.xform_uniform, tx, ty)
|
||||
GL.glUniform2f(self.offset_uniform, ox, oy)
|
||||
GL.glDrawElements(GL.GL_TRIANGLES, self.vert_count,
|
||||
GL.GL_UNSIGNED_INT, None)
|
||||
GL.glDrawElements(
|
||||
GL.GL_TRIANGLES, self.vert_count, GL.GL_UNSIGNED_INT, None
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
GL.glDisable(GL.GL_BLEND)
|
||||
if self.app.use_vao:
|
||||
|
|
|
|||
183
edit_command.py
183
edit_command.py
|
|
@ -1,9 +1,6 @@
|
|||
import time
|
||||
|
||||
class EditCommand:
|
||||
|
||||
"undo/redo-able representation of an art edit (eg paint, erase) operation"
|
||||
|
||||
|
||||
def __init__(self, art):
|
||||
self.art = art
|
||||
self.start_time = art.app.get_elapsed_time()
|
||||
|
|
@ -12,7 +9,7 @@ class EditCommand:
|
|||
# this prevents multiple commands operating on the same tile
|
||||
# from stomping each other
|
||||
self.tile_commands = {}
|
||||
|
||||
|
||||
def get_number_of_commands(self):
|
||||
commands = 0
|
||||
for frame in self.tile_commands.values():
|
||||
|
|
@ -21,48 +18,52 @@ class EditCommand:
|
|||
for tile in column.values():
|
||||
commands += 1
|
||||
return commands
|
||||
|
||||
|
||||
def __str__(self):
|
||||
# get unique-ish ID from memory address
|
||||
addr = self.__repr__()
|
||||
addr = addr[addr.find('0'):-1]
|
||||
s = 'EditCommand_%s: %s tiles, time %s' % (addr, self.get_number_of_commands(),
|
||||
self.finish_time)
|
||||
addr = addr[addr.find("0") : -1]
|
||||
s = "EditCommand_%s: %s tiles, time %s" % (
|
||||
addr,
|
||||
self.get_number_of_commands(),
|
||||
self.finish_time,
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
def add_command_tiles(self, new_command_tiles):
|
||||
for ct in new_command_tiles:
|
||||
# create new tables for frames/layers/columns if not present
|
||||
if not ct.frame in self.tile_commands:
|
||||
if ct.frame not in self.tile_commands:
|
||||
self.tile_commands[ct.frame] = {}
|
||||
if not ct.layer in self.tile_commands[ct.frame]:
|
||||
if ct.layer not in self.tile_commands[ct.frame]:
|
||||
self.tile_commands[ct.frame][ct.layer] = {}
|
||||
if not ct.y in self.tile_commands[ct.frame][ct.layer]:
|
||||
if ct.y not in self.tile_commands[ct.frame][ct.layer]:
|
||||
self.tile_commands[ct.frame][ct.layer][ct.y] = {}
|
||||
# preserve "before" state of any command we overwrite
|
||||
if ct.x in self.tile_commands[ct.frame][ct.layer][ct.y]:
|
||||
old_ct = self.tile_commands[ct.frame][ct.layer][ct.y][ct.x]
|
||||
ct.set_before(old_ct.b_char, old_ct.b_fg, old_ct.b_bg,
|
||||
old_ct.b_xform)
|
||||
ct.set_before(old_ct.b_char, old_ct.b_fg, old_ct.b_bg, old_ct.b_xform)
|
||||
self.tile_commands[ct.frame][ct.layer][ct.y][ct.x] = ct
|
||||
|
||||
|
||||
def undo_commands_for_tile(self, frame, layer, x, y):
|
||||
# no commands at all yet, maybe
|
||||
if len(self.tile_commands) == 0:
|
||||
return
|
||||
# tile might not have undo commands, eg text entry beyond start region
|
||||
if not y in self.tile_commands[frame][layer] or \
|
||||
not x in self.tile_commands[frame][layer][y]:
|
||||
if (
|
||||
y not in self.tile_commands[frame][layer]
|
||||
or x not in self.tile_commands[frame][layer][y]
|
||||
):
|
||||
return
|
||||
self.tile_commands[frame][layer][y][x].undo()
|
||||
|
||||
|
||||
def undo(self):
|
||||
for frame in self.tile_commands.values():
|
||||
for layer in frame.values():
|
||||
for column in layer.values():
|
||||
for tile_command in column.values():
|
||||
tile_command.undo()
|
||||
|
||||
|
||||
def apply(self):
|
||||
for frame in self.tile_commands.values():
|
||||
for layer in frame.values():
|
||||
|
|
@ -72,15 +73,14 @@ class EditCommand:
|
|||
|
||||
|
||||
class EntireArtCommand:
|
||||
|
||||
"""
|
||||
undo/redo-able representation of a whole-art operation, eg:
|
||||
resize/crop, run art script, add/remove layer, etc
|
||||
"""
|
||||
|
||||
|
||||
# art arrays to grab
|
||||
array_types = ['chars', 'fg_colors', 'bg_colors', 'uv_mods']
|
||||
|
||||
array_types = ["chars", "fg_colors", "bg_colors", "uv_mods"]
|
||||
|
||||
def __init__(self, art, origin_x=0, origin_y=0):
|
||||
self.art = art
|
||||
# remember origin of resize command
|
||||
|
|
@ -88,14 +88,14 @@ class EntireArtCommand:
|
|||
self.before_frame = art.active_frame
|
||||
self.before_layer = art.active_layer
|
||||
self.start_time = self.finish_time = art.app.get_elapsed_time()
|
||||
|
||||
|
||||
def save_tiles(self, before=True):
|
||||
# save copies of tile data lists
|
||||
prefix = 'b' if before else 'a'
|
||||
prefix = "b" if before else "a"
|
||||
for atype in self.array_types:
|
||||
# save list as eg "b_chars" for "character data before operation"
|
||||
src_data = getattr(self.art, atype)
|
||||
var_name = '%s_%s' % (prefix, atype)
|
||||
var_name = "%s_%s" % (prefix, atype)
|
||||
# deep copy each frame's data, else before == after
|
||||
new_data = []
|
||||
for frame in src_data:
|
||||
|
|
@ -105,7 +105,7 @@ class EntireArtCommand:
|
|||
self.before_size = (self.art.width, self.art.height)
|
||||
else:
|
||||
self.after_size = (self.art.width, self.art.height)
|
||||
|
||||
|
||||
def undo(self):
|
||||
# undo might remove frames/layers that were added
|
||||
self.art.set_active_frame(self.before_frame)
|
||||
|
|
@ -114,19 +114,19 @@ class EntireArtCommand:
|
|||
x, y = self.before_size
|
||||
self.art.resize(x, y, self.origin_x, self.origin_y)
|
||||
for atype in self.array_types:
|
||||
new_data = getattr(self, 'b_' + atype)
|
||||
new_data = getattr(self, "b_" + atype)
|
||||
setattr(self.art, atype, new_data[:])
|
||||
if self.before_size != self.after_size:
|
||||
# Art.resize will set geo_changed and mark all frames changed
|
||||
self.art.app.ui.adjust_for_art_resize(self.art)
|
||||
self.art.mark_all_frames_changed()
|
||||
|
||||
|
||||
def apply(self):
|
||||
if self.before_size != self.after_size:
|
||||
x, y = self.after_size
|
||||
self.art.resize(x, y, self.origin_x, self.origin_y)
|
||||
for atype in self.array_types:
|
||||
new_data = getattr(self, 'a_' + atype)
|
||||
new_data = getattr(self, "a_" + atype)
|
||||
setattr(self.art, atype, new_data[:])
|
||||
if self.before_size != self.after_size:
|
||||
self.art.app.ui.adjust_for_art_resize(self.art)
|
||||
|
|
@ -134,7 +134,6 @@ class EntireArtCommand:
|
|||
|
||||
|
||||
class EditCommandTile:
|
||||
|
||||
def __init__(self, art):
|
||||
self.art = art
|
||||
self.creation_time = self.art.app.get_elapsed_time()
|
||||
|
|
@ -144,26 +143,40 @@ class EditCommandTile:
|
|||
self.frame = self.layer = self.x = self.y = None
|
||||
self.b_char = self.b_fg = self.b_bg = self.b_xform = None
|
||||
self.a_char = self.a_fg = self.a_bg = self.a_xform = None
|
||||
|
||||
|
||||
def __str__(self):
|
||||
s = 'F%s L%s %s,%s @ %.2f: ' % (self.frame, self.layer, str(self.x).rjust(2, '0'), str(self.y).rjust(2, '0'), self.creation_time)
|
||||
s += 'c%s f%s b%s x%s -> ' % (self.b_char, self.b_fg, self.b_bg, self.b_xform)
|
||||
s += 'c%s f%s b%s x%s' % (self.a_char, self.a_fg, self.a_bg, self.a_xform)
|
||||
s = "F%s L%s %s,%s @ %.2f: " % (
|
||||
self.frame,
|
||||
self.layer,
|
||||
str(self.x).rjust(2, "0"),
|
||||
str(self.y).rjust(2, "0"),
|
||||
self.creation_time,
|
||||
)
|
||||
s += "c%s f%s b%s x%s -> " % (self.b_char, self.b_fg, self.b_bg, self.b_xform)
|
||||
s += "c%s f%s b%s x%s" % (self.a_char, self.a_fg, self.a_bg, self.a_xform)
|
||||
return s
|
||||
|
||||
|
||||
def __eq__(self, value):
|
||||
return self.frame == value.frame and self.layer == value.layer and \
|
||||
self.x == value.x and self.y == value.y and \
|
||||
self.b_char == value.b_char and self.b_fg == value.b_fg and \
|
||||
self.b_bg == value.b_bg and self.b_xform == value.b_xform and \
|
||||
self.a_char == value.a_char and self.a_fg == value.a_fg and \
|
||||
self.a_bg == value.a_bg and self.a_xform == value.a_xform
|
||||
|
||||
return (
|
||||
self.frame == value.frame
|
||||
and self.layer == value.layer
|
||||
and self.x == value.x
|
||||
and self.y == value.y
|
||||
and self.b_char == value.b_char
|
||||
and self.b_fg == value.b_fg
|
||||
and self.b_bg == value.b_bg
|
||||
and self.b_xform == value.b_xform
|
||||
and self.a_char == value.a_char
|
||||
and self.a_fg == value.a_fg
|
||||
and self.a_bg == value.a_bg
|
||||
and self.a_xform == value.a_xform
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
"returns a deep copy of this tile command"
|
||||
new_ect = EditCommandTile(self.art)
|
||||
# TODO: old or new timestamp? does it matter?
|
||||
#new_ect.creation_time = self.art.app.get_elapsed_time()
|
||||
# new_ect.creation_time = self.art.app.get_elapsed_time()
|
||||
new_ect.creation_time = self.creation_time
|
||||
# copy all properties
|
||||
new_ect.frame, new_ect.layer = self.frame, self.layer
|
||||
|
|
@ -173,22 +186,27 @@ class EditCommandTile:
|
|||
new_ect.a_char, new_ect.a_xform = self.a_char, self.a_xform
|
||||
new_ect.a_fg, new_ect.a_bg = self.a_fg, self.a_bg
|
||||
return new_ect
|
||||
|
||||
|
||||
def set_tile(self, frame, layer, x, y):
|
||||
self.frame, self.layer = frame, layer
|
||||
self.x, self.y = x, y
|
||||
|
||||
|
||||
def set_before(self, char, fg, bg, xform):
|
||||
self.b_char, self.b_xform = char, xform
|
||||
self.b_fg, self.b_bg = fg, bg
|
||||
|
||||
|
||||
def set_after(self, char, fg, bg, xform):
|
||||
self.a_char, self.a_xform = char, xform
|
||||
self.a_fg, self.a_bg = fg, bg
|
||||
|
||||
|
||||
def is_null(self):
|
||||
return self.a_char == self.b_char and self.a_fg == self.b_fg and self.a_bg == self.b_bg and self.a_xform == self.b_xform
|
||||
|
||||
return (
|
||||
self.a_char == self.b_char
|
||||
and self.a_fg == self.b_fg
|
||||
and self.a_bg == self.b_bg
|
||||
and self.a_xform == self.b_xform
|
||||
)
|
||||
|
||||
def undo(self):
|
||||
# tile's frame or layer may have been deleted
|
||||
if self.layer > self.art.layers - 1 or self.frame > self.art.frames - 1:
|
||||
|
|
@ -196,37 +214,64 @@ class EditCommandTile:
|
|||
if self.x >= self.art.width or self.y >= self.art.height:
|
||||
return
|
||||
tool = self.art.app.ui.selected_tool
|
||||
set_all = tool.affects_char and tool.affects_fg_color and tool.affects_fg_color and tool.affects_xform
|
||||
self.art.set_tile_at(self.frame, self.layer, self.x, self.y,
|
||||
self.b_char, self.b_fg, self.b_bg, self.b_xform, set_all)
|
||||
|
||||
set_all = (
|
||||
tool.affects_char
|
||||
and tool.affects_fg_color
|
||||
and tool.affects_fg_color
|
||||
and tool.affects_xform
|
||||
)
|
||||
self.art.set_tile_at(
|
||||
self.frame,
|
||||
self.layer,
|
||||
self.x,
|
||||
self.y,
|
||||
self.b_char,
|
||||
self.b_fg,
|
||||
self.b_bg,
|
||||
self.b_xform,
|
||||
set_all,
|
||||
)
|
||||
|
||||
def apply(self):
|
||||
tool = self.art.app.ui.selected_tool
|
||||
set_all = tool.affects_char and tool.affects_fg_color and tool.affects_fg_color and tool.affects_xform
|
||||
self.art.set_tile_at(self.frame, self.layer, self.x, self.y,
|
||||
self.a_char, self.a_fg, self.a_bg, self.a_xform, set_all)
|
||||
set_all = (
|
||||
tool.affects_char
|
||||
and tool.affects_fg_color
|
||||
and tool.affects_fg_color
|
||||
and tool.affects_xform
|
||||
)
|
||||
self.art.set_tile_at(
|
||||
self.frame,
|
||||
self.layer,
|
||||
self.x,
|
||||
self.y,
|
||||
self.a_char,
|
||||
self.a_fg,
|
||||
self.a_bg,
|
||||
self.a_xform,
|
||||
set_all,
|
||||
)
|
||||
|
||||
|
||||
class CommandStack:
|
||||
|
||||
def __init__(self, art):
|
||||
self.art = art
|
||||
self.undo_commands, self.redo_commands = [], []
|
||||
|
||||
|
||||
def __str__(self):
|
||||
s = 'stack for %s:\n' % self.art.filename
|
||||
s += '===\nundo:\n'
|
||||
s = "stack for %s:\n" % self.art.filename
|
||||
s += "===\nundo:\n"
|
||||
for cmd in self.undo_commands:
|
||||
s += str(cmd) + '\n'
|
||||
s += '\n===\nredo:\n'
|
||||
s += str(cmd) + "\n"
|
||||
s += "\n===\nredo:\n"
|
||||
for cmd in self.redo_commands:
|
||||
s += str(cmd) + '\n'
|
||||
s += str(cmd) + "\n"
|
||||
return s
|
||||
|
||||
|
||||
def commit_commands(self, new_commands):
|
||||
self.undo_commands += new_commands[:]
|
||||
self.clear_redo()
|
||||
|
||||
|
||||
def undo(self):
|
||||
if len(self.undo_commands) == 0:
|
||||
return
|
||||
|
|
@ -235,7 +280,7 @@ class CommandStack:
|
|||
command.undo()
|
||||
self.redo_commands.append(command)
|
||||
self.art.app.cursor.update_cursor_preview()
|
||||
|
||||
|
||||
def redo(self):
|
||||
if len(self.redo_commands) == 0:
|
||||
return
|
||||
|
|
@ -247,6 +292,6 @@ class CommandStack:
|
|||
# add to end of undo stack
|
||||
self.undo_commands.append(command)
|
||||
self.art.app.cursor.update_cursor_preview()
|
||||
|
||||
|
||||
def clear_redo(self):
|
||||
self.redo_commands = []
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
DEFAULT_FG, DEFAULT_BG = 7, 0
|
||||
WIDTH = 80
|
||||
MAX_LINES = 250
|
||||
|
||||
|
||||
class ANSImporter(ArtImporter):
|
||||
format_name = 'ANSI'
|
||||
format_name = "ANSI"
|
||||
format_description = """
|
||||
Classic scene format using ANSI standard codes.
|
||||
Assumes 80 columns, DOS character set and EGA palette.
|
||||
"""
|
||||
allowed_file_extensions = ['ans', 'txt']
|
||||
|
||||
allowed_file_extensions = ["ans", "txt"]
|
||||
|
||||
def get_sequence(self, data):
|
||||
"returns a list of ints from given data ending in a letter"
|
||||
i = 0
|
||||
|
|
@ -22,28 +22,28 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
i += 1
|
||||
seq.append(data[i])
|
||||
return seq
|
||||
|
||||
|
||||
def get_commands_from_sequence(self, seq):
|
||||
"returns command type & commands (separated by semicolon) from sequence"
|
||||
cmds = []
|
||||
new_cmd = ''
|
||||
new_cmd = ""
|
||||
for k in seq[:-1]:
|
||||
if k != 59:
|
||||
new_cmd += chr(k)
|
||||
else:
|
||||
cmds.append(new_cmd)
|
||||
new_cmd = ''
|
||||
new_cmd = ""
|
||||
# include last command
|
||||
cmds.append(new_cmd)
|
||||
return chr(seq[-1]), cmds
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
self.set_art_charset('dos')
|
||||
self.set_art_palette('ansi')
|
||||
self.set_art_charset("dos")
|
||||
self.set_art_palette("ansi")
|
||||
# resize to arbitrary height, crop once we know final line count
|
||||
self.resize(WIDTH, MAX_LINES)
|
||||
self.art.clear_frame_layer(0, 0, DEFAULT_BG + 1)
|
||||
data = open(in_filename, 'rb').read()
|
||||
data = open(in_filename, "rb").read()
|
||||
x, y = 0, 0
|
||||
# cursor save/restore codes position
|
||||
saved_x, saved_y = 0, 0
|
||||
|
|
@ -57,18 +57,19 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
if x >= WIDTH:
|
||||
x = 0
|
||||
y += 1
|
||||
if y > max_y: max_y = y
|
||||
if y > max_y:
|
||||
max_y = y
|
||||
# how much we will advance through bytes for next iteration
|
||||
increment = 1
|
||||
# command sequence
|
||||
if data[i] == 27 and data[i+1] == 91:
|
||||
if data[i] == 27 and data[i + 1] == 91:
|
||||
increment += 1
|
||||
# grab full length of sequence
|
||||
seq = self.get_sequence(data[i+2:])
|
||||
seq = self.get_sequence(data[i + 2 :])
|
||||
# split sequence into individual commands
|
||||
cmd_type, cmds = self.get_commands_from_sequence(seq)
|
||||
# display control
|
||||
if cmd_type == 'm':
|
||||
if cmd_type == "m":
|
||||
# empty command = reset
|
||||
if len(cmds) == 0:
|
||||
fg, bg = DEFAULT_FG, DEFAULT_BG
|
||||
|
|
@ -96,30 +97,33 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
# change fg color
|
||||
elif 30 <= code <= 37:
|
||||
fg = code - 30
|
||||
if fg_bright: fg += 8
|
||||
if fg_bright:
|
||||
fg += 8
|
||||
# change bg color
|
||||
elif 40 <= code <= 47:
|
||||
bg = code - 40
|
||||
if bg_bright: bg += 8
|
||||
#else: print('unhandled display code %s' % code)
|
||||
if bg_bright:
|
||||
bg += 8
|
||||
# else: print('unhandled display code %s' % code)
|
||||
# cursor up/down/forward/back
|
||||
elif cmd_type == 'A':
|
||||
elif cmd_type == "A":
|
||||
y -= int(cmds[0]) if cmds[0] else 1
|
||||
elif cmd_type == 'B':
|
||||
elif cmd_type == "B":
|
||||
y += int(cmds[0]) if cmds[0] else 1
|
||||
if y > max_y: max_y = y
|
||||
elif cmd_type == 'C':
|
||||
if y > max_y:
|
||||
max_y = y
|
||||
elif cmd_type == "C":
|
||||
x += int(cmds[0]) if cmds[0] else 1
|
||||
elif cmd_type == 'D':
|
||||
elif cmd_type == "D":
|
||||
x -= int(cmds[0]) if cmds[0] else 1
|
||||
# break
|
||||
elif ord(cmd_type) == 26:
|
||||
break
|
||||
# set line wrap (ignore for now)
|
||||
elif cmd_type == 'h':
|
||||
elif cmd_type == "h":
|
||||
pass
|
||||
# move cursor to Y,X
|
||||
elif cmd_type == 'H' or cmd_type == 'f':
|
||||
elif cmd_type == "H" or cmd_type == "f":
|
||||
if len(cmds) == 0 or len(cmds[0]) == 0:
|
||||
new_y = 0
|
||||
else:
|
||||
|
|
@ -129,9 +133,10 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
else:
|
||||
new_x = int(cmds[1]) - 1
|
||||
x, y = new_x, new_y
|
||||
if y > max_y: max_y = y
|
||||
if y > max_y:
|
||||
max_y = y
|
||||
# clear line/screen
|
||||
elif cmd_type == 'J':
|
||||
elif cmd_type == "J":
|
||||
cmd = int(cmds[0]) if cmds else 0
|
||||
# 0: clear from cursor to end of screen
|
||||
if cmd == 0:
|
||||
|
|
@ -146,24 +151,26 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
x, y = 0, 0
|
||||
self.art.clear_frame_layer(0, 0, DEFAULT_BG + 1)
|
||||
# save cursor position
|
||||
elif cmd_type == 's':
|
||||
elif cmd_type == "s":
|
||||
saved_x, saved_y = x, y
|
||||
# restore cursor position
|
||||
elif cmd_type == 'u':
|
||||
elif cmd_type == "u":
|
||||
x, y = saved_x, saved_y
|
||||
#else: print('unhandled escape code %s' % cmd_type)
|
||||
# else: print('unhandled escape code %s' % cmd_type)
|
||||
increment += len(seq)
|
||||
# CR + LF
|
||||
elif data[i] == 13 and data[i+1] == 10:
|
||||
elif data[i] == 13 and data[i + 1] == 10:
|
||||
increment += 1
|
||||
x = 0
|
||||
y += 1
|
||||
if y > max_y: max_y = y
|
||||
if y > max_y:
|
||||
max_y = y
|
||||
# LF
|
||||
elif data[i] == 10:
|
||||
x = 0
|
||||
y += 1
|
||||
if y > max_y: max_y = y
|
||||
if y > max_y:
|
||||
max_y = y
|
||||
# indent
|
||||
elif data[i] == 9:
|
||||
x += 8
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
# import as white on black for ease of edit + export
|
||||
DEFAULT_FG, DEFAULT_BG =113, 1
|
||||
DEFAULT_FG, DEFAULT_BG = 113, 1
|
||||
# most ATAs are 40 columns, but some are a couple chars longer and a few are 80!
|
||||
WIDTH, HEIGHT = 80, 40
|
||||
|
||||
|
||||
class ATAImporter(ArtImporter):
|
||||
format_name = 'ATASCII'
|
||||
format_name = "ATASCII"
|
||||
format_description = """
|
||||
ATARI 8-bit computer version of ASCII.
|
||||
Imports with ATASCII character set and Atari palette.
|
||||
"""
|
||||
allowed_file_extensions = ['ata']
|
||||
|
||||
allowed_file_extensions = ["ata"]
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
self.set_art_charset('atari')
|
||||
self.set_art_palette('atari')
|
||||
self.set_art_charset("atari")
|
||||
self.set_art_palette("atari")
|
||||
self.resize(WIDTH, HEIGHT)
|
||||
self.art.clear_frame_layer(0, 0, DEFAULT_BG)
|
||||
# iterate over the bytes
|
||||
data = open(in_filename, 'rb').read()
|
||||
data = open(in_filename, "rb").read()
|
||||
i = 0
|
||||
x, y = 0, 0
|
||||
while i < len(data):
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
|
||||
# bitmap image conversion predates the import/export system so it's a bit weird.
|
||||
# conversion happens over time, so it merely kicks off the process.
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ui_file_chooser_dialog import ImageFileChooserDialog
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_art_dialog import ImportOptionsDialog
|
||||
from image_convert import ImageConverter
|
||||
from art import DEFAULT_CHARSET, DEFAULT_HEIGHT, DEFAULT_PALETTE, DEFAULT_WIDTH
|
||||
from art_import import ArtImporter
|
||||
from image_convert import ImageConverter
|
||||
from palette import PaletteFromFile
|
||||
from art import DEFAULT_CHARSET, DEFAULT_PALETTE, DEFAULT_WIDTH, DEFAULT_HEIGHT
|
||||
from ui_art_dialog import ImportOptionsDialog
|
||||
from ui_dialog import Field, UIDialog
|
||||
from ui_file_chooser_dialog import ImageFileChooserDialog
|
||||
|
||||
# custom chooser showing image previews, shares parent w/ "palette from image"
|
||||
|
||||
|
||||
class ConvertImageChooserDialog(ImageFileChooserDialog):
|
||||
|
||||
title = 'Convert image'
|
||||
confirm_caption = 'Choose'
|
||||
|
||||
title = "Convert image"
|
||||
confirm_caption = "Choose"
|
||||
|
||||
def confirm_pressed(self):
|
||||
filename = self.field_texts[0]
|
||||
if not os.path.exists(filename) or not os.path.isfile(filename):
|
||||
|
|
@ -30,28 +30,24 @@ class ConvertImageChooserDialog(ImageFileChooserDialog):
|
|||
dialog_class = self.ui.app.importer.options_dialog_class
|
||||
# tell the dialog which image we chose, store its size
|
||||
w, h = Image.open(filename).size
|
||||
options = {
|
||||
'filename': filename,
|
||||
'image_width': w,
|
||||
'image_height': h
|
||||
}
|
||||
options = {"filename": filename, "image_width": w, "image_height": h}
|
||||
self.ui.open_dialog(dialog_class, options)
|
||||
|
||||
|
||||
# custom dialog box providing convert options
|
||||
|
||||
|
||||
class ConvertImageOptionsDialog(ImportOptionsDialog):
|
||||
|
||||
title = 'Convert bitmap image options'
|
||||
field0_label = 'Color palette:'
|
||||
field1_label = 'Current palette (%s)'
|
||||
field2_label = 'From source image; # of colors:'
|
||||
field3_label = ' '
|
||||
field5_label = 'Converted art size:'
|
||||
field6_label = 'Best fit to current size (%s)'
|
||||
field7_label = '%% of source image: (%s)'
|
||||
field8_label = ' '
|
||||
field10_label = 'Smooth (bicubic) scale source image'
|
||||
title = "Convert bitmap image options"
|
||||
field0_label = "Color palette:"
|
||||
field1_label = "Current palette (%s)"
|
||||
field2_label = "From source image; # of colors:"
|
||||
field3_label = " "
|
||||
field5_label = "Converted art size:"
|
||||
field6_label = "Best fit to current size (%s)"
|
||||
field7_label = "%% of source image: (%s)"
|
||||
field8_label = " "
|
||||
field10_label = "Smooth (bicubic) scale source image"
|
||||
radio_groups = [(1, 2), (6, 7)]
|
||||
field_width = UIDialog.default_short_field_width
|
||||
# to get the layout we want, we must specify 0 padding lines and
|
||||
|
|
@ -62,57 +58,61 @@ class ConvertImageOptionsDialog(ImportOptionsDialog):
|
|||
Field(label=field1_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field2_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field3_label, type=int, width=field_width, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field5_label, type=None, width=0, oneline=True),
|
||||
Field(label=field6_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field7_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field8_label, type=float, width=field_width, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field10_label, type=bool, width=0, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True)
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
]
|
||||
invalid_color_error = 'Palettes must be between 2 and 256 colors.'
|
||||
invalid_scale_error = 'Scale must be greater than 0.0'
|
||||
invalid_color_error = "Palettes must be between 2 and 256 colors."
|
||||
invalid_scale_error = "Scale must be greater than 0.0"
|
||||
# redraw dynamic labels
|
||||
always_redraw_labels = True
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 1:
|
||||
return UIDialog.true_field_text
|
||||
elif field_number == 3:
|
||||
# # of colors from source image
|
||||
return '64'
|
||||
return "64"
|
||||
elif field_number == 6:
|
||||
return UIDialog.true_field_text
|
||||
elif field_number == 8:
|
||||
# % of source image size
|
||||
return '50.0'
|
||||
return "50.0"
|
||||
elif field_number == 10:
|
||||
return ' '
|
||||
return ''
|
||||
|
||||
return " "
|
||||
return ""
|
||||
|
||||
def get_field_label(self, field_index):
|
||||
label = self.fields[field_index].label
|
||||
# custom label replacements to show palette, possible convert sizes
|
||||
if field_index == 1:
|
||||
label %= self.ui.active_art.palette.name if self.ui.active_art else DEFAULT_PALETTE
|
||||
label %= (
|
||||
self.ui.active_art.palette.name
|
||||
if self.ui.active_art
|
||||
else DEFAULT_PALETTE
|
||||
)
|
||||
elif field_index == 6:
|
||||
# can't assume any art is open, use defaults if needed
|
||||
w = self.ui.active_art.width if self.ui.active_art else DEFAULT_WIDTH
|
||||
h = self.ui.active_art.height if self.ui.active_art else DEFAULT_HEIGHT
|
||||
label %= '%s x %s' % (w, h)
|
||||
label %= "%s x %s" % (w, h)
|
||||
elif field_index == 7:
|
||||
# scale # might not be valid
|
||||
valid,_ = self.is_input_valid()
|
||||
valid, _ = self.is_input_valid()
|
||||
if not valid:
|
||||
return label % '???'
|
||||
label %= '%s x %s' % self.get_tile_scale()
|
||||
return label % "???"
|
||||
label %= "%s x %s" % self.get_tile_scale()
|
||||
return label
|
||||
|
||||
|
||||
def get_tile_scale(self):
|
||||
"returns scale in tiles of image dimensions"
|
||||
# filename won't be set just after dialog is created
|
||||
if not hasattr(self, 'filename'):
|
||||
if not hasattr(self, "filename"):
|
||||
return 0, 0
|
||||
scale = float(self.field_texts[8]) / 100
|
||||
# can't assume any art is open, use defaults if needed
|
||||
|
|
@ -127,65 +127,79 @@ class ConvertImageOptionsDialog(ImportOptionsDialog):
|
|||
width *= scale
|
||||
height *= scale
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
# colors: int between 2 and 256
|
||||
try: int(self.field_texts[3])
|
||||
except: return False, self.invalid_color_error
|
||||
try:
|
||||
int(self.field_texts[3])
|
||||
except:
|
||||
return False, self.invalid_color_error
|
||||
colors = int(self.field_texts[3])
|
||||
if colors < 2 or colors > 256:
|
||||
if colors < 2 or colors > 256:
|
||||
return False, self.invalid_color_error
|
||||
# % scale: >0 float
|
||||
try: float(self.field_texts[8])
|
||||
except: return False, self.invalid_scale_error
|
||||
try:
|
||||
float(self.field_texts[8])
|
||||
except:
|
||||
return False, self.invalid_scale_error
|
||||
if float(self.field_texts[8]) <= 0:
|
||||
return False, self.invalid_scale_error
|
||||
return True, None
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
self.dismiss()
|
||||
# compile options for importer
|
||||
options = {}
|
||||
# create new palette from image?
|
||||
if self.field_texts[1].strip():
|
||||
options['palette'] = self.ui.active_art.palette.name if self.ui.active_art else DEFAULT_PALETTE
|
||||
options["palette"] = (
|
||||
self.ui.active_art.palette.name
|
||||
if self.ui.active_art
|
||||
else DEFAULT_PALETTE
|
||||
)
|
||||
else:
|
||||
# create new palette
|
||||
palette_filename = os.path.basename(self.filename)
|
||||
colors = int(self.field_texts[3])
|
||||
new_pal = PaletteFromFile(self.ui.app, self.filename,
|
||||
palette_filename, colors)
|
||||
new_pal = PaletteFromFile(
|
||||
self.ui.app, self.filename, palette_filename, colors
|
||||
)
|
||||
# palette now loaded and saved to disk
|
||||
options['palette'] = new_pal.name
|
||||
options["palette"] = new_pal.name
|
||||
# rescale art?
|
||||
if self.field_texts[6].strip():
|
||||
options['art_width'] = self.ui.active_art.width if self.ui.active_art else DEFAULT_WIDTH
|
||||
options['art_height'] = self.ui.active_art.height if self.ui.active_art else DEFAULT_HEIGHT
|
||||
options["art_width"] = (
|
||||
self.ui.active_art.width if self.ui.active_art else DEFAULT_WIDTH
|
||||
)
|
||||
options["art_height"] = (
|
||||
self.ui.active_art.height if self.ui.active_art else DEFAULT_HEIGHT
|
||||
)
|
||||
else:
|
||||
# art dimensions = scale% of image dimensions, in tiles
|
||||
options['art_width'], options['art_height'] = self.get_tile_scale()
|
||||
options['bicubic_scale'] = bool(self.field_texts[10].strip())
|
||||
options["art_width"], options["art_height"] = self.get_tile_scale()
|
||||
options["bicubic_scale"] = bool(self.field_texts[10].strip())
|
||||
ImportOptionsDialog.do_import(self.ui.app, self.filename, options)
|
||||
|
||||
|
||||
class BitmapImageImporter(ArtImporter):
|
||||
format_name = 'Bitmap image'
|
||||
format_name = "Bitmap image"
|
||||
format_description = """
|
||||
Bitmap image in PNG, JPEG, or BMP format.
|
||||
"""
|
||||
file_chooser_dialog_class = ConvertImageChooserDialog
|
||||
options_dialog_class = ConvertImageOptionsDialog
|
||||
completes_instantly = False
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
# modify self.app.ui.active_art based on options
|
||||
palette = self.app.load_palette(options['palette'])
|
||||
palette = self.app.load_palette(options["palette"])
|
||||
self.art.set_palette(palette)
|
||||
width, height = options['art_width'], options['art_height']
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options["bicubic_scale"]
|
||||
# let ImageConverter do the actual heavy lifting
|
||||
ic = ImageConverter(self.app, in_filename, self.art, bicubic_scale)
|
||||
# early failures: file no longer exists, PIL fails to load and convert image
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
|
||||
import numpy as np
|
||||
|
||||
from formats.in_bitmap import (
|
||||
BitmapImageImporter,
|
||||
ConvertImageChooserDialog,
|
||||
ConvertImageOptionsDialog,
|
||||
)
|
||||
from image_convert import ImageConverter
|
||||
from ui_dialog import UIDialog, Field, SkipFieldType
|
||||
from formats.in_bitmap import BitmapImageImporter, ConvertImageChooserDialog, ConvertImageOptionsDialog
|
||||
from ui_dialog import Field, SkipFieldType, UIDialog
|
||||
|
||||
|
||||
class TwoColorConvertImageOptionsDialog(ConvertImageOptionsDialog):
|
||||
# simplified version of parent options dialog, reusing as much as possible
|
||||
title = 'Convert 2-color bitmap image options'
|
||||
title = "Convert 2-color bitmap image options"
|
||||
field5_label = ConvertImageOptionsDialog.field5_label
|
||||
field6_label = ConvertImageOptionsDialog.field6_label
|
||||
field7_label = ConvertImageOptionsDialog.field7_label
|
||||
|
|
@ -16,39 +19,38 @@ class TwoColorConvertImageOptionsDialog(ConvertImageOptionsDialog):
|
|||
field10_label = ConvertImageOptionsDialog.field10_label
|
||||
field_width = ConvertImageOptionsDialog.field_width
|
||||
fields = [
|
||||
Field(label='', type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label='', type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label='', type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label='', type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label='', type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label="", type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label="", type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label="", type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label="", type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label="", type=SkipFieldType, width=0, oneline=True),
|
||||
Field(label=field5_label, type=None, width=0, oneline=True),
|
||||
Field(label=field6_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field7_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field8_label, type=float, width=field_width, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field10_label, type=bool, width=0, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True)
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, ui, options):
|
||||
ConvertImageOptionsDialog.__init__(self, ui, options)
|
||||
self.active_field = 6
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# alternate defaults - use 1:1 scaling
|
||||
if field_number == 6:
|
||||
return ' '
|
||||
return " "
|
||||
elif field_number == 7:
|
||||
return UIDialog.true_field_text
|
||||
elif field_number == 8:
|
||||
# % of source image size - alternate default
|
||||
return '100.0'
|
||||
return "100.0"
|
||||
else:
|
||||
return ConvertImageOptionsDialog.get_initial_field_text(self, field_number)
|
||||
|
||||
|
||||
class TwoColorImageConverter(ImageConverter):
|
||||
|
||||
def get_color_combos_for_block(self, src_block):
|
||||
colors, counts = np.unique(src_block, False, False, return_counts=True)
|
||||
if len(colors) > 0:
|
||||
|
|
@ -60,7 +62,7 @@ class TwoColorImageConverter(ImageConverter):
|
|||
|
||||
|
||||
class TwoColorBitmapImageImporter(BitmapImageImporter):
|
||||
format_name = '2-color bitmap image'
|
||||
format_name = "2-color bitmap image"
|
||||
format_description = """
|
||||
Variation on bitmap image conversion that forces
|
||||
a black and white (1-bit) palette, and doesn't use
|
||||
|
|
@ -69,15 +71,15 @@ fg/bg color swaps. Suitable for plaintext export.
|
|||
file_chooser_dialog_class = ConvertImageChooserDialog
|
||||
options_dialog_class = TwoColorConvertImageOptionsDialog
|
||||
completes_instantly = False
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
# force palette to 1-bit black and white
|
||||
palette = self.app.load_palette('bw')
|
||||
palette = self.app.load_palette("bw")
|
||||
self.art.set_palette(palette)
|
||||
|
||||
width, height = options['art_width'], options['art_height']
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options["bicubic_scale"]
|
||||
ic = TwoColorImageConverter(self.app, in_filename, self.art, bicubic_scale)
|
||||
# early failures: file no longer exists, PIL fails to load and convert image
|
||||
if not ic.init_success:
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
|
||||
# "convert folder of images to animation"
|
||||
# heavy lifting still done by ImageConverter, this mainly coordinates
|
||||
# conversion of multiple frames
|
||||
|
||||
import os, time
|
||||
import os
|
||||
import time
|
||||
|
||||
import image_convert
|
||||
import formats.in_bitmap as bm
|
||||
import image_convert
|
||||
|
||||
|
||||
class ImageSequenceConverter:
|
||||
|
||||
def __init__(self, app, image_filenames, art, bicubic_scale):
|
||||
self.init_success = False
|
||||
self.app = app
|
||||
self.start_time = time.time()
|
||||
self.image_filenames = image_filenames
|
||||
# App.update_window_title uses image_filename for titlebar
|
||||
self.image_filename = ''
|
||||
self.image_filename = ""
|
||||
# common name of sequence
|
||||
self.image_name = os.path.splitext(self.image_filename)[0]
|
||||
self.art = art
|
||||
|
|
@ -24,7 +24,7 @@ class ImageSequenceConverter:
|
|||
# queue up first frame
|
||||
self.next_image(first=True)
|
||||
self.init_success = True
|
||||
|
||||
|
||||
def next_image(self, first=False):
|
||||
# pop last image off stack
|
||||
if not first:
|
||||
|
|
@ -36,10 +36,9 @@ class ImageSequenceConverter:
|
|||
# next frame
|
||||
self.art.set_active_frame(self.art.active_frame + 1)
|
||||
try:
|
||||
self.current_frame_converter = image_convert.ImageConverter(self.app,
|
||||
self.image_filenames[0],
|
||||
self.art,
|
||||
self.bicubic_scale, self)
|
||||
self.current_frame_converter = image_convert.ImageConverter(
|
||||
self.app, self.image_filenames[0], self.art, self.bicubic_scale, self
|
||||
)
|
||||
except:
|
||||
self.fail()
|
||||
return
|
||||
|
|
@ -49,11 +48,11 @@ class ImageSequenceConverter:
|
|||
self.image_filename = self.image_filenames[0]
|
||||
self.preview_sprite = self.current_frame_converter.preview_sprite
|
||||
self.app.update_window_title()
|
||||
|
||||
|
||||
def fail(self):
|
||||
self.app.log('Bad frame %s' % self.image_filenames[0], error=True)
|
||||
self.app.log("Bad frame %s" % self.image_filenames[0], error=True)
|
||||
self.finish(True)
|
||||
|
||||
|
||||
def update(self):
|
||||
# create converter for new frame if current one is done,
|
||||
# else update current one
|
||||
|
|
@ -61,53 +60,57 @@ class ImageSequenceConverter:
|
|||
self.next_image()
|
||||
else:
|
||||
self.current_frame_converter.update()
|
||||
|
||||
|
||||
def finish(self, cancelled=False):
|
||||
time_taken = time.time() - self.start_time
|
||||
(verb, error) = ('cancelled', True) if cancelled else ('finished', False)
|
||||
self.app.log('Conversion of image sequence %s %s after %.3f seconds' % (self.image_name, verb, time_taken), error)
|
||||
(verb, error) = ("cancelled", True) if cancelled else ("finished", False)
|
||||
self.app.log(
|
||||
"Conversion of image sequence %s %s after %.3f seconds"
|
||||
% (self.image_name, verb, time_taken),
|
||||
error,
|
||||
)
|
||||
self.app.converter = None
|
||||
self.app.update_window_title()
|
||||
|
||||
|
||||
class ConvertImageSequenceChooserDialog(bm.ConvertImageChooserDialog):
|
||||
title = 'Convert folder'
|
||||
confirm_caption = 'Choose First Image'
|
||||
title = "Convert folder"
|
||||
confirm_caption = "Choose First Image"
|
||||
|
||||
|
||||
class BitmapImageSequenceImporter(bm.BitmapImageImporter):
|
||||
format_name = 'Bitmap image folder'
|
||||
format_name = "Bitmap image folder"
|
||||
format_description = """
|
||||
Converts a folder of Bitmap images (PNG, JPEG, or BMP)
|
||||
into an animation. Dimensions will be based on first
|
||||
image chosen.
|
||||
"""
|
||||
file_chooser_dialog_class = ConvertImageSequenceChooserDialog
|
||||
#options_dialog_class = bm.ConvertImageOptionsDialog
|
||||
|
||||
# options_dialog_class = bm.ConvertImageOptionsDialog
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
palette = self.app.load_palette(options['palette'])
|
||||
palette = self.app.load_palette(options["palette"])
|
||||
self.art.set_palette(palette)
|
||||
width, height = options['art_width'], options['art_height']
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options["bicubic_scale"]
|
||||
# get dir listing with full pathname
|
||||
in_dir = os.path.dirname(in_filename)
|
||||
in_files = ['%s/%s' % (in_dir, f) for f in os.listdir(in_dir)]
|
||||
in_files = ["%s/%s" % (in_dir, f) for f in os.listdir(in_dir)]
|
||||
in_files.sort()
|
||||
# assume numeric sequence starts from chosen file
|
||||
in_files = in_files[in_files.index(in_filename):]
|
||||
in_files = in_files[in_files.index(in_filename) :]
|
||||
# remove files from end of list if they don't end in a number
|
||||
while not os.path.splitext(in_files[-1])[0][-1].isdecimal() and \
|
||||
len(in_files) > 0:
|
||||
while (
|
||||
not os.path.splitext(in_files[-1])[0][-1].isdecimal() and len(in_files) > 0
|
||||
):
|
||||
in_files.pop()
|
||||
# add frames to art as needed
|
||||
while self.art.frames < len(in_files):
|
||||
self.art.add_frame_to_end(log=False)
|
||||
self.art.set_active_frame(0)
|
||||
# create converter
|
||||
isc = ImageSequenceConverter(self.app, in_files, self.art,
|
||||
bicubic_scale)
|
||||
isc = ImageSequenceConverter(self.app, in_files, self.art, bicubic_scale)
|
||||
# bail on early failure
|
||||
if not isc.init_success:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,76 +1,77 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_art_dialog import ImportOptionsDialog
|
||||
from ui_dialog import Field, UIDialog
|
||||
|
||||
|
||||
class EDSCIIImportOptionsDialog(ImportOptionsDialog):
|
||||
title = 'Import EDSCII (legacy format) art'
|
||||
field0_label = 'Width override (leave 0 to guess):'
|
||||
title = "Import EDSCII (legacy format) art"
|
||||
field0_label = "Width override (leave 0 to guess):"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=field_width, oneline=False)
|
||||
]
|
||||
invalid_width_error = 'Invalid width override.'
|
||||
|
||||
fields = [Field(label=field0_label, type=int, width=field_width, oneline=False)]
|
||||
invalid_width_error = "Invalid width override."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return '0'
|
||||
return ''
|
||||
|
||||
return "0"
|
||||
return ""
|
||||
|
||||
def is_input_valid(self):
|
||||
# valid widths: any >=0 int
|
||||
try: int(self.field_texts[0])
|
||||
except: return False, self.invalid_width_error
|
||||
try:
|
||||
int(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_width_error
|
||||
if int(self.field_texts[0]) < 0:
|
||||
return False, self.invalid_width_error
|
||||
return True, None
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
width = int(self.field_texts[0])
|
||||
width = width if width > 0 else None
|
||||
options = {'width_override':width}
|
||||
options = {"width_override": width}
|
||||
self.dismiss()
|
||||
# self.filename is set in our importer's file_chooser_dialog_class
|
||||
ImportOptionsDialog.do_import(self.ui.app, self.filename, options)
|
||||
|
||||
|
||||
class EDSCIIImporter(ArtImporter):
|
||||
|
||||
format_name = 'EDSCII'
|
||||
format_name = "EDSCII"
|
||||
format_description = """
|
||||
Binary format for EDSCII, Playscii's predecessor.
|
||||
Assumes single frame, single layer document.
|
||||
Current character set and palette will be used.
|
||||
"""
|
||||
options_dialog_class = EDSCIIImportOptionsDialog
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
data = open(in_filename, 'rb').read()
|
||||
data = open(in_filename, "rb").read()
|
||||
# document width = find longest stretch before a \n
|
||||
longest_line = 0
|
||||
for line in data.splitlines():
|
||||
if len(line) > longest_line:
|
||||
longest_line = len(line)
|
||||
# user can override assumed document width, needed for a few files
|
||||
width = options.get('width_override', None) or int(longest_line / 3)
|
||||
width = options.get("width_override", None) or int(longest_line / 3)
|
||||
# derive height from width
|
||||
# 2-byte line breaks might produce non-int result, cast erases this
|
||||
height = int(len(data) / width / 3)
|
||||
self.art.resize(width, height)
|
||||
|
||||
# populate char/color arrays by scanning width-long chunks of file
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
yield l[i : i + n]
|
||||
|
||||
# 3 bytes per tile, +1 for line ending
|
||||
# BUT: files saved in windows may have 2 byte line breaks, try to detect
|
||||
lb_length = 1
|
||||
lines = chunks(data, (width * 3) + lb_length)
|
||||
for line in lines:
|
||||
if line[-2] == ord('\r') and line[-1] == ord('\n'):
|
||||
#self.app.log('EDSCIIImporter: windows-style line breaks detected')
|
||||
if line[-2] == ord("\r") and line[-1] == ord("\n"):
|
||||
# self.app.log('EDSCIIImporter: windows-style line breaks detected')
|
||||
lb_length = 2
|
||||
break
|
||||
# recreate generator after first use
|
||||
|
|
@ -81,8 +82,8 @@ Current character set and palette will be used.
|
|||
while index < len(line) - lb_length:
|
||||
char = line[index]
|
||||
# +1 to color indices; playscii color index 0 = transparent
|
||||
fg = line[index+1] + 1
|
||||
bg = line[index+2] + 1
|
||||
fg = line[index + 1] + 1
|
||||
bg = line[index + 2] + 1
|
||||
self.art.set_tile_at(0, 0, x, y, char, fg, bg)
|
||||
index += 3
|
||||
x += 1
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
|
||||
class EndDoomImporter(ArtImporter):
|
||||
format_name = 'ENDOOM'
|
||||
format_name = "ENDOOM"
|
||||
format_description = """
|
||||
ENDOOM lump file format for Doom engine games.
|
||||
80x25 DOS ASCII with EGA palette.
|
||||
Background colors can only be EGA colors 0-8.
|
||||
"""
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
"""
|
||||
from http://doomwiki.org/wiki/ENDOOM:
|
||||
|
|
@ -18,17 +18,17 @@ Background colors can only be EGA colors 0-8.
|
|||
second byte = color:
|
||||
bits 0-3 = fg color, bits 4-6 = bg color, bit 7 = blink
|
||||
"""
|
||||
self.set_art_charset('dos')
|
||||
self.set_art_palette('ega')
|
||||
self.set_art_charset("dos")
|
||||
self.set_art_palette("ega")
|
||||
self.art.resize(80, 25)
|
||||
data = open(in_filename, 'rb').read(4000)
|
||||
data = open(in_filename, "rb").read(4000)
|
||||
x, y = 0, 0
|
||||
for i,byte in enumerate(data):
|
||||
for i, byte in enumerate(data):
|
||||
if i % 2 != 0:
|
||||
continue
|
||||
color_byte = data[i+1]
|
||||
color_byte = data[i + 1]
|
||||
bits = bin(color_byte)[2:]
|
||||
bits = bits.rjust(7, '0')
|
||||
bits = bits.rjust(7, "0")
|
||||
bg_bits = bits[:3]
|
||||
fg_bits = bits[3:]
|
||||
offset = 1
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
|
||||
class TextImporter(ArtImporter):
|
||||
|
||||
format_name = 'Plain text'
|
||||
format_name = "Plain text"
|
||||
format_description = """
|
||||
ASCII art in ordinary text format.
|
||||
Assumes single frame, single layer document.
|
||||
Current character set and palette will be used.
|
||||
"""
|
||||
|
||||
|
||||
def run_import(self, in_filename, options={}):
|
||||
lines = open(in_filename).readlines()
|
||||
# determine length of longest line
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
|
||||
from art_export import ArtExporter
|
||||
|
||||
WIDTH = 80
|
||||
ENCODING = 'cp1252' # old default
|
||||
ENCODING = 'us-ascii' # DEBUG
|
||||
ENCODING = 'latin_1' # DEBUG - seems to handle >128 chars ok?
|
||||
ENCODING = "cp1252" # old default
|
||||
ENCODING = "us-ascii" # DEBUG
|
||||
ENCODING = "latin_1" # DEBUG - seems to handle >128 chars ok?
|
||||
|
||||
|
||||
class ANSExporter(ArtExporter):
|
||||
format_name = 'ANSI'
|
||||
format_name = "ANSI"
|
||||
format_description = """
|
||||
Classic scene format using ANSI standard codes.
|
||||
Assumes 80 columns, DOS character set and EGA palette.
|
||||
Exports active layer of active frame.
|
||||
"""
|
||||
file_extension = 'ans'
|
||||
|
||||
file_extension = "ans"
|
||||
|
||||
def get_display_command(self, fg, bg):
|
||||
"return a display command sequence string for given colors"
|
||||
# reset colors on every tile
|
||||
s = chr(27) + chr(91) + '0;'
|
||||
s = chr(27) + chr(91) + "0;"
|
||||
if fg >= 8:
|
||||
s += '1;'
|
||||
s += "1;"
|
||||
fg -= 8
|
||||
if bg >= 8:
|
||||
s += '5;'
|
||||
s += "5;"
|
||||
bg -= 8
|
||||
s += '%s;' % (fg + 30)
|
||||
s += '%s' % (bg + 40)
|
||||
s += 'm'
|
||||
s += "%s;" % (fg + 30)
|
||||
s += "%s" % (bg + 40)
|
||||
s += "m"
|
||||
return s
|
||||
|
||||
|
||||
def write(self, data):
|
||||
self.outfile.write(data.encode(ENCODING))
|
||||
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
# binary file; encoding into ANSI bytes happens just before write
|
||||
self.outfile = open(out_filename, 'wb')
|
||||
self.outfile = open(out_filename, "wb")
|
||||
layer = self.art.active_layer
|
||||
frame = self.art.active_frame
|
||||
for y in range(self.art.height):
|
||||
|
|
@ -57,6 +57,6 @@ Exports active layer of active frame.
|
|||
# special (top row) chars won't display in terminal anyway
|
||||
self.write(chr(0))
|
||||
# carriage return + line feed
|
||||
self.outfile.write(b'\r\n')
|
||||
self.outfile.write(b"\r\n")
|
||||
self.outfile.close()
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
|
||||
from art_export import ArtExporter
|
||||
from art import TileIter
|
||||
from art_export import ArtExporter
|
||||
|
||||
|
||||
class ANSExporter(ArtExporter):
|
||||
format_name = 'ATASCII'
|
||||
format_name = "ATASCII"
|
||||
format_description = """
|
||||
ATARI 8-bit computer version of ASCII.
|
||||
Assumes ATASCII character set and Atari palette.
|
||||
Any tile with non-black background will be considered inverted.
|
||||
"""
|
||||
file_extension = 'ata'
|
||||
|
||||
file_extension = "ata"
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
# binary file; encoding into ANSI bytes happens just before write
|
||||
self.outfile = open(out_filename, 'wb')
|
||||
self.outfile = open(out_filename, "wb")
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
# only read from layer 0 of frame 0
|
||||
if layer > 0 or frame > 0:
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
|
||||
from art_export import ArtExporter
|
||||
|
||||
WIDTH, HEIGHT = 80, 25
|
||||
|
||||
|
||||
class EndDoomExporter(ArtExporter):
|
||||
format_name = 'ENDOOM'
|
||||
format_name = "ENDOOM"
|
||||
format_description = """
|
||||
ENDOOM lump file format for Doom engine games.
|
||||
80x25 DOS ASCII with EGA palette.
|
||||
Background colors can only be EGA colors 0-8.
|
||||
"""
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
if self.art.width < WIDTH or self.art.height < HEIGHT:
|
||||
self.app.log("ENDOOM export: Art isn't big enough!")
|
||||
return False
|
||||
outfile = open(out_filename, 'wb')
|
||||
outfile = open(out_filename, "wb")
|
||||
for y in range(HEIGHT):
|
||||
for x in range(WIDTH):
|
||||
char, fg, bg, xform = self.art.get_tile_at(0, 0, x, y)
|
||||
|
|
@ -26,11 +27,11 @@ Background colors can only be EGA colors 0-8.
|
|||
bg = max(0, bg)
|
||||
char_byte = bytes([char])
|
||||
outfile.write(char_byte)
|
||||
fg_bits = bin(fg)[2:].rjust(4, '0')
|
||||
fg_bits = bin(fg)[2:].rjust(4, "0")
|
||||
# BG color can't be above 8
|
||||
bg %= 8
|
||||
bg_bits = bin(bg)[2:].rjust(3, '0')
|
||||
color_bits = '0' + bg_bits + fg_bits
|
||||
bg_bits = bin(bg)[2:].rjust(3, "0")
|
||||
color_bits = "0" + bg_bits + fg_bits
|
||||
color_byte = int(color_bits, 2)
|
||||
color_byte = bytes([color_byte])
|
||||
outfile.write(color_byte)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
|
||||
from art_export import ArtExporter
|
||||
from image_export import export_animation
|
||||
|
||||
|
||||
class GIFExporter(ArtExporter):
|
||||
format_name = 'Animated GIF image'
|
||||
format_name = "Animated GIF image"
|
||||
format_description = """
|
||||
Animated GIF of all frames in current document, with
|
||||
transparency and proper frame timings.
|
||||
"""
|
||||
file_extension = 'gif'
|
||||
file_extension = "gif"
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
# heavy lifting done by image_export module
|
||||
export_animation(self.app, self.app.ui.active_art, out_filename)
|
||||
|
|
|
|||
|
|
@ -1,67 +1,70 @@
|
|||
|
||||
from art_export import ArtExporter
|
||||
from image_export import export_still_image
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_art_dialog import ExportOptionsDialog
|
||||
from ui_dialog import Field, UIDialog
|
||||
|
||||
DEFAULT_SCALE = 4
|
||||
DEFAULT_CRT = True
|
||||
|
||||
|
||||
class PNGExportOptionsDialog(ExportOptionsDialog):
|
||||
title = 'PNG image export options'
|
||||
field0_label = 'Scale factor (%s pixels)'
|
||||
field1_label = 'CRT filter'
|
||||
title = "PNG image export options"
|
||||
field0_label = "Scale factor (%s pixels)"
|
||||
field1_label = "CRT filter"
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=6, oneline=False),
|
||||
Field(label=field1_label, type=bool, width=0, oneline=True)
|
||||
Field(label=field1_label, type=bool, width=0, oneline=True),
|
||||
]
|
||||
# redraw dynamic labels
|
||||
always_redraw_labels = True
|
||||
invalid_scale_error = 'Scale must be greater than 0'
|
||||
|
||||
invalid_scale_error = "Scale must be greater than 0"
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return str(DEFAULT_SCALE)
|
||||
elif field_number == 1:
|
||||
return [' ', UIDialog.true_field_text][DEFAULT_CRT]
|
||||
|
||||
return [" ", UIDialog.true_field_text][DEFAULT_CRT]
|
||||
|
||||
def get_field_label(self, field_index):
|
||||
label = self.fields[field_index].label
|
||||
if field_index == 0:
|
||||
valid,_ = self.is_input_valid()
|
||||
valid, _ = self.is_input_valid()
|
||||
if not valid:
|
||||
label %= '???'
|
||||
label %= "???"
|
||||
else:
|
||||
# calculate exported image size
|
||||
art = self.ui.active_art
|
||||
scale = int(self.field_texts[0])
|
||||
width = art.charset.char_width * art.width * scale
|
||||
height = art.charset.char_height * art.height * scale
|
||||
label %= '%s x %s' % (width, height)
|
||||
label %= "%s x %s" % (width, height)
|
||||
return label
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
# scale factor: >0 int
|
||||
try: int(self.field_texts[0])
|
||||
except: return False, self.invalid_scale_error
|
||||
try:
|
||||
int(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_scale_error
|
||||
if int(self.field_texts[0]) <= 0:
|
||||
return False, self.invalid_scale_error
|
||||
return True, None
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
self.dismiss()
|
||||
# compile options for exporter
|
||||
options = {
|
||||
'scale': int(self.field_texts[0]),
|
||||
'crt': bool(self.field_texts[1].strip())
|
||||
"scale": int(self.field_texts[0]),
|
||||
"crt": bool(self.field_texts[1].strip()),
|
||||
}
|
||||
ExportOptionsDialog.do_export(self.ui.app, self.filename, options)
|
||||
|
||||
|
||||
class PNGExporter(ArtExporter):
|
||||
format_name = 'PNG image'
|
||||
format_name = "PNG image"
|
||||
format_description = """
|
||||
PNG format (lossless compression) still image of current frame.
|
||||
Can be exported with or without CRT filter effect.
|
||||
|
|
@ -70,12 +73,15 @@ exported image will be 8-bit with same palette as this Art.
|
|||
Otherwise it will be 32-bit with alpha transparency.
|
||||
If CRT filter is enabled, image will always be 32-bit.
|
||||
"""
|
||||
file_extension = 'png'
|
||||
file_extension = "png"
|
||||
options_dialog_class = PNGExportOptionsDialog
|
||||
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
# heavy lifting done by image_export module
|
||||
return export_still_image(self.app, self.app.ui.active_art,
|
||||
out_filename,
|
||||
crt=options.get('crt', DEFAULT_CRT),
|
||||
scale=options.get('scale', DEFAULT_SCALE))
|
||||
return export_still_image(
|
||||
self.app,
|
||||
self.app.ui.active_art,
|
||||
out_filename,
|
||||
crt=options.get("crt", DEFAULT_CRT),
|
||||
scale=options.get("scale", DEFAULT_SCALE),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
|
||||
import os
|
||||
|
||||
from art_export import ArtExporter
|
||||
from image_export import export_still_image
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_art_dialog import ExportOptionsDialog
|
||||
from renderable import LAYER_VIS_FULL, LAYER_VIS_NONE
|
||||
from ui_art_dialog import ExportOptionsDialog
|
||||
from ui_dialog import Field, UIDialog
|
||||
|
||||
FILE_EXTENSION = 'png'
|
||||
FILE_EXTENSION = "png"
|
||||
|
||||
DEFAULT_SCALE = 1
|
||||
DEFAULT_CRT = False
|
||||
|
||||
def get_full_filename(in_filename, frame, layer_name,
|
||||
use_frame, use_layer,
|
||||
forbidden_chars):
|
||||
|
||||
def get_full_filename(
|
||||
in_filename, frame, layer_name, use_frame, use_layer, forbidden_chars
|
||||
):
|
||||
"Returns properly mutated filename for given frame/layer data"
|
||||
# strip out path and extension from filename as we mutate it
|
||||
dirname = os.path.dirname(in_filename)
|
||||
|
|
@ -22,62 +22,63 @@ def get_full_filename(in_filename, frame, layer_name,
|
|||
base_filename = os.path.splitext(base_filename)[0]
|
||||
fn = base_filename
|
||||
if use_frame:
|
||||
fn += '_%s' % (str(frame).rjust(4, '0'))
|
||||
fn += "_%s" % (str(frame).rjust(4, "0"))
|
||||
if use_layer:
|
||||
fn += '_%s' % layer_name
|
||||
fn += "_%s" % layer_name
|
||||
# strip unfriendly chars from output filename
|
||||
for forbidden_char in ['\\', '/', '*', ':']:
|
||||
fn = fn.replace(forbidden_char, '')
|
||||
for forbidden_char in ["\\", "/", "*", ":"]:
|
||||
fn = fn.replace(forbidden_char, "")
|
||||
# add path and extension for final mutated filename
|
||||
return '%s/%s.%s' % (dirname, fn, FILE_EXTENSION)
|
||||
return "%s/%s.%s" % (dirname, fn, FILE_EXTENSION)
|
||||
|
||||
|
||||
class PNGSetExportOptionsDialog(ExportOptionsDialog):
|
||||
title = 'PNG set export options'
|
||||
tile_width = 60 # extra width for filename preview
|
||||
field0_label = 'Scale factor (%s pixels)'
|
||||
field1_label = 'CRT filter'
|
||||
field2_label = 'Export frames'
|
||||
field3_label = 'Export layers'
|
||||
field4_label = 'First filename (in set of %s):'
|
||||
field5_label = ' %s'
|
||||
title = "PNG set export options"
|
||||
tile_width = 60 # extra width for filename preview
|
||||
field0_label = "Scale factor (%s pixels)"
|
||||
field1_label = "CRT filter"
|
||||
field2_label = "Export frames"
|
||||
field3_label = "Export layers"
|
||||
field4_label = "First filename (in set of %s):"
|
||||
field5_label = " %s"
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=6, oneline=False),
|
||||
Field(label=field1_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field2_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field3_label, type=bool, width=0, oneline=True),
|
||||
Field(label=field4_label, type=None, width=0, oneline=True),
|
||||
Field(label=field5_label, type=None, width=0, oneline=True)
|
||||
Field(label=field5_label, type=None, width=0, oneline=True),
|
||||
]
|
||||
# redraw dynamic labels
|
||||
always_redraw_labels = True
|
||||
invalid_scale_error = 'Scale must be greater than 0'
|
||||
|
||||
invalid_scale_error = "Scale must be greater than 0"
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
art = self.ui.active_art
|
||||
if field_number == 0:
|
||||
return str(DEFAULT_SCALE)
|
||||
elif field_number == 1:
|
||||
return [' ', UIDialog.true_field_text][DEFAULT_CRT]
|
||||
return [" ", UIDialog.true_field_text][DEFAULT_CRT]
|
||||
elif field_number == 2:
|
||||
# default false if only one frame
|
||||
return [' ', UIDialog.true_field_text][art.frames > 1]
|
||||
return [" ", UIDialog.true_field_text][art.frames > 1]
|
||||
elif field_number == 3:
|
||||
# default false if only one layer
|
||||
return [' ', UIDialog.true_field_text][art.layers > 1]
|
||||
|
||||
return [" ", UIDialog.true_field_text][art.layers > 1]
|
||||
|
||||
def get_field_label(self, field_index):
|
||||
label = self.fields[field_index].label
|
||||
if field_index == 0:
|
||||
valid,_ = self.is_input_valid()
|
||||
valid, _ = self.is_input_valid()
|
||||
if not valid:
|
||||
label %= '???'
|
||||
label %= "???"
|
||||
else:
|
||||
# calculate exported image size
|
||||
art = self.ui.active_art
|
||||
scale = int(self.field_texts[0])
|
||||
width = art.charset.char_width * art.width * scale
|
||||
height = art.charset.char_height * art.height * scale
|
||||
label %= '%s x %s' % (width, height)
|
||||
label %= "%s x %s" % (width, height)
|
||||
# show how many images exported set will be
|
||||
elif field_index == 4:
|
||||
export_frames = bool(self.field_texts[2].strip())
|
||||
|
|
@ -90,52 +91,60 @@ class PNGSetExportOptionsDialog(ExportOptionsDialog):
|
|||
elif export_layers:
|
||||
label %= str(art.layers)
|
||||
else:
|
||||
label %= '1'
|
||||
label %= "1"
|
||||
# preview frame + layer filename mutations based on current settings
|
||||
elif field_index == 5:
|
||||
export_frames = bool(self.field_texts[2].strip())
|
||||
export_layers = bool(self.field_texts[3].strip())
|
||||
art = self.ui.active_art
|
||||
fn = get_full_filename(self.filename, 0, art.layer_names[0],
|
||||
export_frames, export_layers,
|
||||
self.ui.app.forbidden_filename_chars)
|
||||
fn = get_full_filename(
|
||||
self.filename,
|
||||
0,
|
||||
art.layer_names[0],
|
||||
export_frames,
|
||||
export_layers,
|
||||
self.ui.app.forbidden_filename_chars,
|
||||
)
|
||||
fn = os.path.basename(fn)
|
||||
label %= fn
|
||||
return label
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
# scale factor: >0 int
|
||||
try: int(self.field_texts[0])
|
||||
except: return False, self.invalid_scale_error
|
||||
try:
|
||||
int(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_scale_error
|
||||
if int(self.field_texts[0]) <= 0:
|
||||
return False, self.invalid_scale_error
|
||||
return True, None
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
self.dismiss()
|
||||
# compile options for importer
|
||||
options = {
|
||||
'scale': int(self.field_texts[0]),
|
||||
'crt': bool(self.field_texts[1].strip()),
|
||||
'frames': bool(self.field_texts[2].strip()),
|
||||
'layers': bool(self.field_texts[3].strip())
|
||||
"scale": int(self.field_texts[0]),
|
||||
"crt": bool(self.field_texts[1].strip()),
|
||||
"frames": bool(self.field_texts[2].strip()),
|
||||
"layers": bool(self.field_texts[3].strip()),
|
||||
}
|
||||
ExportOptionsDialog.do_export(self.ui.app, self.filename, options)
|
||||
|
||||
|
||||
class PNGSetExporter(ArtExporter):
|
||||
format_name = 'PNG image set'
|
||||
format_name = "PNG image set"
|
||||
format_description = """
|
||||
PNG image set for each frame and/or layer.
|
||||
"""
|
||||
file_extension = FILE_EXTENSION
|
||||
options_dialog_class = PNGSetExportOptionsDialog
|
||||
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
export_frames = options['frames']
|
||||
export_layers = options['layers']
|
||||
export_frames = options["frames"]
|
||||
export_layers = options["layers"]
|
||||
art = self.app.ui.active_art
|
||||
# remember user's active frame/layer/viz settings so we
|
||||
# can set em back when done
|
||||
|
|
@ -145,7 +154,9 @@ PNG image set for each frame and/or layer.
|
|||
start_layer_viz = self.app.inactive_layer_visibility
|
||||
self.app.onion_frames_visible = False
|
||||
# if multi-player, only show active layer
|
||||
self.app.inactive_layer_visibility = LAYER_VIS_NONE if export_layers else LAYER_VIS_FULL
|
||||
self.app.inactive_layer_visibility = (
|
||||
LAYER_VIS_NONE if export_layers else LAYER_VIS_FULL
|
||||
)
|
||||
success = True
|
||||
for frame in range(art.frames):
|
||||
# if exporting layers but not frames, only export active frame
|
||||
|
|
@ -154,13 +165,21 @@ PNG image set for each frame and/or layer.
|
|||
art.set_active_frame(frame)
|
||||
for layer in range(art.layers):
|
||||
art.set_active_layer(layer)
|
||||
full_filename = get_full_filename(out_filename, frame,
|
||||
art.layer_names[layer],
|
||||
export_frames, export_layers,
|
||||
self.app.forbidden_filename_chars)
|
||||
if not export_still_image(self.app, art, full_filename,
|
||||
crt=options.get('crt', DEFAULT_CRT),
|
||||
scale=options.get('scale', DEFAULT_SCALE)):
|
||||
full_filename = get_full_filename(
|
||||
out_filename,
|
||||
frame,
|
||||
art.layer_names[layer],
|
||||
export_frames,
|
||||
export_layers,
|
||||
self.app.forbidden_filename_chars,
|
||||
)
|
||||
if not export_still_image(
|
||||
self.app,
|
||||
art,
|
||||
full_filename,
|
||||
crt=options.get("crt", DEFAULT_CRT),
|
||||
scale=options.get("scale", DEFAULT_SCALE),
|
||||
):
|
||||
success = False
|
||||
# put everything back how user left it
|
||||
art.set_active_frame(start_frame)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
from art_export import ArtExporter
|
||||
|
||||
|
||||
class TextExporter(ArtExporter):
|
||||
format_name = 'Plain text'
|
||||
format_name = "Plain text"
|
||||
format_description = """
|
||||
ASCII art in ordinary text format.
|
||||
Assumes single frame, single layer document.
|
||||
Current character set will be used; make sure it supports
|
||||
any extended characters you want translated.
|
||||
"""
|
||||
file_extension = 'txt'
|
||||
file_extension = "txt"
|
||||
|
||||
def run_export(self, out_filename, options):
|
||||
# utf-8 is safest encoding to use here, but non-default on Windows
|
||||
outfile = open(out_filename, 'w', encoding='utf-8')
|
||||
outfile = open(out_filename, "w", encoding="utf-8")
|
||||
for y in range(self.art.height):
|
||||
for x in range(self.art.width):
|
||||
char = self.art.get_char_index_at(0, 0, x, y)
|
||||
found_char = False
|
||||
for k,v in self.art.charset.char_mapping.items():
|
||||
for k, v in self.art.charset.char_mapping.items():
|
||||
if v == char:
|
||||
found_char = True
|
||||
outfile.write(k)
|
||||
break
|
||||
# if char not found, just write a blank space
|
||||
if not found_char:
|
||||
outfile.write(' ')
|
||||
outfile.write('\n')
|
||||
outfile.write(" ")
|
||||
outfile.write("\n")
|
||||
outfile.close()
|
||||
return True
|
||||
|
|
|
|||
109
framebuffer.py
109
framebuffer.py
|
|
@ -3,16 +3,18 @@ from OpenGL import GL
|
|||
|
||||
|
||||
class Framebuffer:
|
||||
|
||||
start_crt_enabled = False
|
||||
disable_crt = False
|
||||
clear_color = (0, 0, 0, 1)
|
||||
# declared as an option here in case people want to sub their own via CFG
|
||||
crt_fragment_shader_filename = 'framebuffer_f_crt.glsl'
|
||||
|
||||
crt_fragment_shader_filename = "framebuffer_f_crt.glsl"
|
||||
|
||||
def __init__(self, app, width=None, height=None):
|
||||
self.app = app
|
||||
self.width, self.height = width or self.app.window_width, height or self.app.window_height
|
||||
self.width, self.height = (
|
||||
width or self.app.window_width,
|
||||
height or self.app.window_height,
|
||||
)
|
||||
# bind vao before compiling shaders
|
||||
if self.app.use_vao:
|
||||
self.vao = GL.glGenVertexArrays(1)
|
||||
|
|
@ -20,65 +22,86 @@ class Framebuffer:
|
|||
self.vbo = GL.glGenBuffers(1)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
|
||||
fb_verts = np.array([-1, -1, 1, -1, -1, 1, 1, 1], dtype=np.float32)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, fb_verts.nbytes, fb_verts,
|
||||
GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER, fb_verts.nbytes, fb_verts, GL.GL_STATIC_DRAW
|
||||
)
|
||||
# texture, depth buffer, framebuffer
|
||||
self.texture = GL.glGenTextures(1)
|
||||
self.depth_buffer = GL.glGenRenderbuffers(1)
|
||||
self.framebuffer = GL.glGenFramebuffers(1)
|
||||
self.setup_texture_and_buffers()
|
||||
# shaders
|
||||
self.plain_shader = self.app.sl.new_shader('framebuffer_v.glsl', 'framebuffer_f.glsl')
|
||||
self.plain_shader = self.app.sl.new_shader(
|
||||
"framebuffer_v.glsl", "framebuffer_f.glsl"
|
||||
)
|
||||
if not self.disable_crt:
|
||||
self.crt_shader = self.app.sl.new_shader('framebuffer_v.glsl', self.crt_fragment_shader_filename)
|
||||
self.crt_shader = self.app.sl.new_shader(
|
||||
"framebuffer_v.glsl", self.crt_fragment_shader_filename
|
||||
)
|
||||
self.crt = self.get_crt_enabled()
|
||||
# shader uniforms and attributes
|
||||
self.plain_tex_uniform = self.plain_shader.get_uniform_location('fbo_texture')
|
||||
self.plain_attrib = self.plain_shader.get_attrib_location('v_coord')
|
||||
self.plain_tex_uniform = self.plain_shader.get_uniform_location("fbo_texture")
|
||||
self.plain_attrib = self.plain_shader.get_attrib_location("v_coord")
|
||||
GL.glEnableVertexAttribArray(self.plain_attrib)
|
||||
GL.glVertexAttribPointer(self.plain_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None)
|
||||
GL.glVertexAttribPointer(
|
||||
self.plain_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None
|
||||
)
|
||||
if not self.disable_crt:
|
||||
self.crt_tex_uniform = self.crt_shader.get_uniform_location('fbo_texture')
|
||||
self.crt_time_uniform = self.crt_shader.get_uniform_location('elapsed_time')
|
||||
self.crt_res_uniform = self.crt_shader.get_uniform_location('resolution')
|
||||
self.crt_tex_uniform = self.crt_shader.get_uniform_location("fbo_texture")
|
||||
self.crt_time_uniform = self.crt_shader.get_uniform_location("elapsed_time")
|
||||
self.crt_res_uniform = self.crt_shader.get_uniform_location("resolution")
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(0)
|
||||
|
||||
|
||||
def get_crt_enabled(self):
|
||||
return self.disable_crt or self.start_crt_enabled
|
||||
|
||||
|
||||
def setup_texture_and_buffers(self):
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D,
|
||||
GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D,
|
||||
GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D,
|
||||
GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D,
|
||||
GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE)
|
||||
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA,
|
||||
self.width, self.height, 0,
|
||||
GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, None)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE)
|
||||
GL.glTexImage2D(
|
||||
GL.GL_TEXTURE_2D,
|
||||
0,
|
||||
GL.GL_RGBA,
|
||||
self.width,
|
||||
self.height,
|
||||
0,
|
||||
GL.GL_RGBA,
|
||||
GL.GL_UNSIGNED_BYTE,
|
||||
None,
|
||||
)
|
||||
GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, self.depth_buffer)
|
||||
GL.glRenderbufferStorage(GL.GL_RENDERBUFFER, GL.GL_DEPTH_COMPONENT16,
|
||||
self.width, self.height)
|
||||
GL.glRenderbufferStorage(
|
||||
GL.GL_RENDERBUFFER, GL.GL_DEPTH_COMPONENT16, self.width, self.height
|
||||
)
|
||||
GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, 0)
|
||||
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self.framebuffer)
|
||||
GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0,
|
||||
GL.GL_TEXTURE_2D, self.texture, 0)
|
||||
GL.glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT,
|
||||
GL.GL_RENDERBUFFER, self.depth_buffer)
|
||||
GL.glFramebufferTexture2D(
|
||||
GL.GL_FRAMEBUFFER,
|
||||
GL.GL_COLOR_ATTACHMENT0,
|
||||
GL.GL_TEXTURE_2D,
|
||||
self.texture,
|
||||
0,
|
||||
)
|
||||
GL.glFramebufferRenderbuffer(
|
||||
GL.GL_FRAMEBUFFER,
|
||||
GL.GL_DEPTH_ATTACHMENT,
|
||||
GL.GL_RENDERBUFFER,
|
||||
self.depth_buffer,
|
||||
)
|
||||
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
|
||||
|
||||
|
||||
def resize(self, new_width, new_height):
|
||||
self.width, self.height = new_width, new_height
|
||||
self.setup_texture_and_buffers()
|
||||
|
||||
|
||||
def toggle_crt(self):
|
||||
self.crt = not self.crt
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.app.use_vao:
|
||||
GL.glDeleteVertexArrays(1, [self.vao])
|
||||
|
|
@ -86,7 +109,7 @@ class Framebuffer:
|
|||
GL.glDeleteRenderbuffers(1, [self.depth_buffer])
|
||||
GL.glDeleteTextures([self.texture])
|
||||
GL.glDeleteFramebuffers(1, [self.framebuffer])
|
||||
|
||||
|
||||
def render(self):
|
||||
if self.crt and not self.disable_crt:
|
||||
GL.glUseProgram(self.crt_shader.program)
|
||||
|
|
@ -104,7 +127,9 @@ class Framebuffer:
|
|||
GL.glBindVertexArray(self.vao)
|
||||
else:
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
|
||||
GL.glVertexAttribPointer(self.plain_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None)
|
||||
GL.glVertexAttribPointer(
|
||||
self.plain_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None
|
||||
)
|
||||
GL.glEnableVertexAttribArray(self.plain_attrib)
|
||||
GL.glDrawArrays(GL.GL_TRIANGLE_STRIP, 0, 4)
|
||||
if self.app.use_vao:
|
||||
|
|
@ -114,9 +139,13 @@ class Framebuffer:
|
|||
|
||||
class ExportFramebuffer(Framebuffer):
|
||||
clear_color = (0, 0, 0, 0)
|
||||
def get_crt_enabled(self): return True
|
||||
|
||||
def get_crt_enabled(self):
|
||||
return True
|
||||
|
||||
|
||||
class ExportFramebufferNoCRT(Framebuffer):
|
||||
clear_color = (0, 0, 0, 0)
|
||||
def get_crt_enabled(self): return False
|
||||
|
||||
def get_crt_enabled(self):
|
||||
return False
|
||||
|
|
|
|||
15
game_hud.py
15
game_hud.py
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
from art import Art
|
||||
from renderable import TileRenderable
|
||||
|
||||
|
||||
class GameHUDArt(Art):
|
||||
#recalc_quad_height = False
|
||||
# recalc_quad_height = False
|
||||
log_creation = False
|
||||
quad_width = 0.1
|
||||
|
||||
|
|
@ -13,33 +12,33 @@ class GameHUDRenderable(TileRenderable):
|
|||
def get_projection_matrix(self):
|
||||
# much like UIRenderable, use UI's matrices to render in screen space
|
||||
return self.app.ui.view_matrix
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.ui.view_matrix
|
||||
|
||||
|
||||
class GameHUD:
|
||||
|
||||
"stub HUD, subclass and put your own stuff here"
|
||||
|
||||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
self.arts, self.renderables = [], []
|
||||
|
||||
|
||||
def update(self):
|
||||
for art in self.arts:
|
||||
art.update()
|
||||
for r in self.renderables:
|
||||
r.update()
|
||||
|
||||
|
||||
def should_render(self):
|
||||
return True
|
||||
|
||||
|
||||
def render(self):
|
||||
if not self.should_render():
|
||||
return
|
||||
for r in self.renderables:
|
||||
r.render()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
for r in self.renderables:
|
||||
r.destroy()
|
||||
|
|
|
|||
500
game_object.py
500
game_object.py
File diff suppressed because it is too large
Load diff
99
game_room.py
99
game_room.py
|
|
@ -1,27 +1,35 @@
|
|||
|
||||
from game_object import GameObject
|
||||
|
||||
|
||||
class GameRoom:
|
||||
"""
|
||||
A collection of GameObjects within a GameWorld. Can be used to limit scope
|
||||
of object updates, collisions, etc.
|
||||
"""
|
||||
camera_marker_name = ''
|
||||
|
||||
camera_marker_name = ""
|
||||
"If set, camera will move to marker with this name when room entered"
|
||||
camera_follow_player = False
|
||||
"If True, camera will follow player while in this room"
|
||||
left_edge_warp_dest_name, right_edge_warp_dest_name = '', ''
|
||||
left_edge_warp_dest_name, right_edge_warp_dest_name = "", ""
|
||||
"If set, warp to room OR marker with this name when edge crossed"
|
||||
top_edge_warp_dest_name, bottom_edge_warp_dest_name = '', ''
|
||||
warp_edge_bounds_obj_name = ''
|
||||
"Object whose art's bounds should be used as our \"edges\" for above"
|
||||
serialized = ['name', 'camera_marker_name', 'left_edge_warp_dest_name',
|
||||
'right_edge_warp_dest_name', 'top_edge_warp_dest_name',
|
||||
'bottom_edge_warp_dest_name', 'warp_edge_bounds_obj_name',
|
||||
'camera_follow_player']
|
||||
top_edge_warp_dest_name, bottom_edge_warp_dest_name = "", ""
|
||||
warp_edge_bounds_obj_name = ""
|
||||
'Object whose art\'s bounds should be used as our "edges" for above'
|
||||
serialized = [
|
||||
"name",
|
||||
"camera_marker_name",
|
||||
"left_edge_warp_dest_name",
|
||||
"right_edge_warp_dest_name",
|
||||
"top_edge_warp_dest_name",
|
||||
"bottom_edge_warp_dest_name",
|
||||
"warp_edge_bounds_obj_name",
|
||||
"camera_follow_player",
|
||||
]
|
||||
"List of string names of members to serialize for this Room class."
|
||||
log_changes = False
|
||||
"Log changes to and from this room"
|
||||
|
||||
def __init__(self, world, name, room_data=None):
|
||||
self.world = world
|
||||
self.name = name
|
||||
|
|
@ -34,8 +42,10 @@ class GameRoom:
|
|||
# TODO: this is copy-pasted from GameObject, find a way to unify
|
||||
# TODO: GameWorld.set_data_for that takes instance, serialized list, data dict
|
||||
for v in self.serialized:
|
||||
if not v in room_data:
|
||||
self.world.app.dev_log("Serialized property '%s' not found for room %s" % (v, self.name))
|
||||
if v not in room_data:
|
||||
self.world.app.dev_log(
|
||||
"Serialized property '%s' not found for room %s" % (v, self.name)
|
||||
)
|
||||
continue
|
||||
if not hasattr(self, v):
|
||||
setattr(self, v, None)
|
||||
|
|
@ -47,18 +57,19 @@ class GameRoom:
|
|||
else:
|
||||
setattr(self, v, room_data[v])
|
||||
# find objects by name and add them
|
||||
for obj_name in room_data.get('objects', []):
|
||||
for obj_name in room_data.get("objects", []):
|
||||
self.add_object_by_name(obj_name)
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.reset_edge_warps()
|
||||
|
||||
|
||||
def reset_edge_warps(self):
|
||||
self.edge_obj = self.world.objects.get(self.warp_edge_bounds_obj_name, None)
|
||||
# no warping if we don't know our bounds
|
||||
if not self.edge_obj:
|
||||
return
|
||||
edge_dest_name_suffix = '_name'
|
||||
edge_dest_name_suffix = "_name"
|
||||
|
||||
def set_edge_dest(dest_property):
|
||||
# property name to destination name
|
||||
dest_name = getattr(self, dest_property)
|
||||
|
|
@ -66,26 +77,31 @@ class GameRoom:
|
|||
dest_room = self.world.rooms.get(dest_name, None)
|
||||
dest_obj = self.world.objects.get(dest_name, None)
|
||||
# derive member name from serialized property name
|
||||
member_name = dest_property.replace(edge_dest_name_suffix, '')
|
||||
member_name = dest_property.replace(edge_dest_name_suffix, "")
|
||||
setattr(self, member_name, dest_room or dest_obj or None)
|
||||
for pname in ['left_edge_warp_dest_name', 'right_edge_warp_dest_name',
|
||||
'top_edge_warp_dest_name', 'bottom_edge_warp_dest_name']:
|
||||
|
||||
for pname in [
|
||||
"left_edge_warp_dest_name",
|
||||
"right_edge_warp_dest_name",
|
||||
"top_edge_warp_dest_name",
|
||||
"bottom_edge_warp_dest_name",
|
||||
]:
|
||||
set_edge_dest(pname)
|
||||
|
||||
|
||||
def set_camera_marker_name(self, marker_name):
|
||||
if not marker_name in self.world.objects:
|
||||
if marker_name not in self.world.objects:
|
||||
self.world.app.log("Couldn't find camera marker with name %s" % marker_name)
|
||||
return
|
||||
self.camera_marker_name = marker_name
|
||||
if self is self.world.current_room:
|
||||
self.use_camera_marker()
|
||||
|
||||
|
||||
def use_camera_marker(self):
|
||||
if not self.camera_marker_name in self.world.objects:
|
||||
if self.camera_marker_name not in self.world.objects:
|
||||
return
|
||||
cam_mark = self.world.objects[self.camera_marker_name]
|
||||
self.world.camera.set_loc_from_obj(cam_mark)
|
||||
|
||||
|
||||
def entered(self, old_room):
|
||||
"Run when the player enters this room."
|
||||
if self.log_changes:
|
||||
|
|
@ -100,7 +116,7 @@ class GameRoom:
|
|||
# tell objects in this room player has entered so eg spawners can fire
|
||||
for obj in self.objects.values():
|
||||
obj.room_entered(self, old_room)
|
||||
|
||||
|
||||
def exited(self, new_room):
|
||||
"Run when the player exits this room."
|
||||
if self.log_changes:
|
||||
|
|
@ -108,7 +124,7 @@ class GameRoom:
|
|||
# tell objects in this room player has exited
|
||||
for obj in self.objects.values():
|
||||
obj.room_exited(self, new_room)
|
||||
|
||||
|
||||
def add_object_by_name(self, obj_name):
|
||||
"Add object with given name to this room."
|
||||
obj = self.world.objects.get(obj_name, None)
|
||||
|
|
@ -116,12 +132,12 @@ class GameRoom:
|
|||
self.world.app.log("Couldn't find object named %s" % obj_name)
|
||||
return
|
||||
self.add_object(obj)
|
||||
|
||||
|
||||
def add_object(self, obj):
|
||||
"Add object (by reference) to this room."
|
||||
self.objects[obj.name] = obj
|
||||
obj.rooms[self.name] = self
|
||||
|
||||
|
||||
def remove_object_by_name(self, obj_name):
|
||||
"Remove object with given name from this room."
|
||||
obj = self.world.objects.get(obj_name, None)
|
||||
|
|
@ -129,33 +145,42 @@ class GameRoom:
|
|||
self.world.app.log("Couldn't find object named %s" % obj_name)
|
||||
return
|
||||
self.remove_object(obj)
|
||||
|
||||
|
||||
def remove_object(self, obj):
|
||||
"Remove object (by reference) from this room."
|
||||
if obj.name in self.objects:
|
||||
self.objects.pop(obj.name)
|
||||
else:
|
||||
self.world.app.log("GameRoom %s doesn't contain GameObject %s" % (self.name, obj.name))
|
||||
self.world.app.log(
|
||||
"GameRoom %s doesn't contain GameObject %s" % (self.name, obj.name)
|
||||
)
|
||||
if self.name in obj.rooms:
|
||||
obj.rooms.pop(self.name)
|
||||
else:
|
||||
self.world.app.log("GameObject %s not found in GameRoom %s" % (obj.name, self.name))
|
||||
|
||||
self.world.app.log(
|
||||
"GameObject %s not found in GameRoom %s" % (obj.name, self.name)
|
||||
)
|
||||
|
||||
def get_dict(self):
|
||||
"Return a dict that GameWorld.save_to_file can dump to JSON"
|
||||
object_names = list(self.objects.keys())
|
||||
d = {'class_name': type(self).__name__, 'objects': object_names}
|
||||
d = {"class_name": type(self).__name__, "objects": object_names}
|
||||
# serialize whatever other vars are declared in self.serialized
|
||||
for prop_name in self.serialized:
|
||||
if hasattr(self, prop_name):
|
||||
d[prop_name] = getattr(self, prop_name)
|
||||
return d
|
||||
|
||||
|
||||
def _check_edge_warp(self, game_object):
|
||||
# bail if no bounds or edge warp destinations set
|
||||
if not self.edge_obj:
|
||||
return
|
||||
if not self.left_edge_warp_dest and not self.right_edge_warp_dest and not self.top_edge_warp_dest and not self.bottom_edge_warp_dest:
|
||||
if (
|
||||
not self.left_edge_warp_dest
|
||||
and not self.right_edge_warp_dest
|
||||
and not self.top_edge_warp_dest
|
||||
and not self.bottom_edge_warp_dest
|
||||
):
|
||||
return
|
||||
if game_object.warped_recently():
|
||||
return
|
||||
|
|
@ -181,11 +206,11 @@ class GameRoom:
|
|||
# TODO: change room or not? use_marker_room flag a la WarpTrigger?
|
||||
game_object.set_loc(warp_dest.x, warp_dest.y)
|
||||
game_object.last_warp_update = self.world.updates
|
||||
|
||||
|
||||
def update(self):
|
||||
if self is self.world.current_room:
|
||||
self._check_edge_warp(self.world.player)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.name in self.world.rooms:
|
||||
self.world.rooms.pop(self.name)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,41 @@
|
|||
import os.path
|
||||
import random
|
||||
|
||||
import os.path, random
|
||||
from collision import (
|
||||
CST_AABB,
|
||||
CST_CIRCLE,
|
||||
CST_TILE,
|
||||
CT_GENERIC_DYNAMIC,
|
||||
CT_GENERIC_STATIC,
|
||||
CT_NONE,
|
||||
CT_PLAYER,
|
||||
)
|
||||
from game_object import FACING_DIRS, GameObject
|
||||
|
||||
from game_object import GameObject, FACING_DIRS
|
||||
from collision import CST_NONE, CST_CIRCLE, CST_AABB, CST_TILE, CT_NONE, CT_GENERIC_STATIC, CT_GENERIC_DYNAMIC, CT_PLAYER, CTG_STATIC, CTG_DYNAMIC
|
||||
|
||||
class GameObjectAttachment(GameObject):
|
||||
"GameObject that doesn't think about anything, just renders"
|
||||
|
||||
collision_type = CT_NONE
|
||||
should_save = False
|
||||
selectable = False
|
||||
exclude_from_class_list = True
|
||||
physics_move = False
|
||||
offset_x, offset_y, offset_z = 0., 0., 0.
|
||||
offset_x, offset_y, offset_z = 0.0, 0.0, 0.0
|
||||
"Offset from parent object's origin"
|
||||
fixed_z = False
|
||||
"If True, Z will not be locked to GO we're attached to"
|
||||
editable = GameObject.editable + ['offset_x', 'offset_y', 'offset_z']
|
||||
|
||||
editable = GameObject.editable + ["offset_x", "offset_y", "offset_z"]
|
||||
|
||||
def attach_to(self, game_object):
|
||||
"Attach this object to given object."
|
||||
self.parent = game_object
|
||||
|
||||
|
||||
def update(self):
|
||||
# very minimal update!
|
||||
if not self.art.updated_this_tick:
|
||||
self.art.update()
|
||||
|
||||
|
||||
def post_update(self):
|
||||
# after parent has moved, snap to its location
|
||||
self.x = self.parent.x + self.offset_x
|
||||
|
|
@ -36,93 +46,112 @@ class GameObjectAttachment(GameObject):
|
|||
|
||||
class BlobShadow(GameObjectAttachment):
|
||||
"Generic blob shadow attachment class"
|
||||
art_src = 'blob_shadow'
|
||||
|
||||
art_src = "blob_shadow"
|
||||
alpha = 0.5
|
||||
|
||||
|
||||
class StaticTileBG(GameObject):
|
||||
"Generic static world object with tile-based collision"
|
||||
|
||||
collision_shape_type = CST_TILE
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
physics_move = False
|
||||
|
||||
|
||||
class StaticTileObject(GameObject):
|
||||
collision_shape_type = CST_TILE
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
physics_move = False
|
||||
y_sort = True
|
||||
|
||||
|
||||
class StaticBoxObject(GameObject):
|
||||
"Generic static world object with AABB-based (rectangle) collision"
|
||||
|
||||
collision_shape_type = CST_AABB
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
physics_move = False
|
||||
|
||||
|
||||
class DynamicBoxObject(GameObject):
|
||||
collision_shape_type = CST_AABB
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
y_sort = True
|
||||
|
||||
|
||||
class Pickup(GameObject):
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
y_sort = True
|
||||
attachment_classes = { 'shadow': 'BlobShadow' }
|
||||
attachment_classes = {"shadow": "BlobShadow"}
|
||||
|
||||
|
||||
class Projectile(GameObject):
|
||||
"Generic projectile class"
|
||||
|
||||
fast_move_steps = 1
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
collision_shape_type = CST_CIRCLE
|
||||
move_accel_x = move_accel_y = 400.
|
||||
noncolliding_classes = ['Projectile']
|
||||
lifespan = 10.
|
||||
move_accel_x = move_accel_y = 400.0
|
||||
noncolliding_classes = ["Projectile"]
|
||||
lifespan = 10.0
|
||||
"Projectiles should be transient, limited max life"
|
||||
should_save = False
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
self.fire_dir_x, self.fire_dir_y = 0, 0
|
||||
|
||||
|
||||
def fire(self, firer, dir_x=0, dir_y=1):
|
||||
self.set_loc(firer.x, firer.y, firer.z)
|
||||
self.reset_last_loc()
|
||||
self.fire_dir_x, self.fire_dir_y = dir_x, dir_y
|
||||
|
||||
|
||||
def update(self):
|
||||
if (self.fire_dir_x, self.fire_dir_y) != (0, 0):
|
||||
self.move(self.fire_dir_x, self.fire_dir_y)
|
||||
GameObject.update(self)
|
||||
|
||||
|
||||
class Character(GameObject):
|
||||
"Generic character class"
|
||||
|
||||
state_changes_art = True
|
||||
stand_if_not_moving = True
|
||||
move_state = 'walk'
|
||||
move_state = "walk"
|
||||
"Move state name - added to valid_states in init so subclasses recognized"
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
if not self.move_state in self.valid_states:
|
||||
if self.move_state not in self.valid_states:
|
||||
self.valid_states.append(self.move_state)
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
# assume that character should start idling, if its art animates
|
||||
if self.art.frames > 0:
|
||||
self.start_animating()
|
||||
|
||||
|
||||
def update_state(self):
|
||||
GameObject.update_state(self)
|
||||
if self.state_changes_art and abs(self.vel_x) > 0.1 or abs(self.vel_y) > 0.1:
|
||||
self.state = self.move_state
|
||||
|
||||
|
||||
class Player(Character):
|
||||
"Generic player class"
|
||||
|
||||
log_move = False
|
||||
collision_type = CT_PLAYER
|
||||
editable = Character.editable + ['move_accel_x', 'move_accel_y',
|
||||
'ground_friction', 'air_friction',
|
||||
'bounciness', 'stop_velocity']
|
||||
|
||||
editable = Character.editable + [
|
||||
"move_accel_x",
|
||||
"move_accel_y",
|
||||
"ground_friction",
|
||||
"air_friction",
|
||||
"bounciness",
|
||||
"stop_velocity",
|
||||
]
|
||||
|
||||
def pre_first_update(self):
|
||||
if self.world.player is None:
|
||||
self.world.player = self
|
||||
|
|
@ -130,40 +159,56 @@ class Player(Character):
|
|||
self.world.camera.focus_object = self
|
||||
else:
|
||||
self.world.camera.focus_object = None
|
||||
|
||||
|
||||
def button_pressed(self, button_index):
|
||||
pass
|
||||
|
||||
|
||||
def button_unpressed(self, button_index):
|
||||
pass
|
||||
|
||||
|
||||
class TopDownPlayer(Player):
|
||||
|
||||
y_sort = True
|
||||
attachment_classes = { 'shadow': 'BlobShadow' }
|
||||
attachment_classes = {"shadow": "BlobShadow"}
|
||||
facing_changes_art = True
|
||||
|
||||
|
||||
def get_facing_dir(self):
|
||||
return FACING_DIRS[self.facing]
|
||||
|
||||
|
||||
class WorldPropertiesObject(GameObject):
|
||||
"Special magic singleton object that stores and sets GameWorld properties"
|
||||
art_src = 'world_properties_object'
|
||||
|
||||
art_src = "world_properties_object"
|
||||
visible = deleteable = selectable = False
|
||||
locked = True
|
||||
physics_move = False
|
||||
exclude_from_object_list = True
|
||||
exclude_from_class_list = True
|
||||
world_props = ['game_title', 'gravity_x', 'gravity_y', 'gravity_z',
|
||||
'hud_class_name', 'globals_object_class_name',
|
||||
'camera_x', 'camera_y', 'camera_z',
|
||||
'bg_color_r', 'bg_color_g', 'bg_color_b', 'bg_color_a',
|
||||
'player_camera_lock', 'object_grid_snap', 'draw_hud',
|
||||
'collision_enabled', 'show_collision_all', 'show_bounds_all',
|
||||
'show_origin_all', 'show_all_rooms',
|
||||
'room_camera_changes_enabled', 'draw_debug_objects'
|
||||
world_props = [
|
||||
"game_title",
|
||||
"gravity_x",
|
||||
"gravity_y",
|
||||
"gravity_z",
|
||||
"hud_class_name",
|
||||
"globals_object_class_name",
|
||||
"camera_x",
|
||||
"camera_y",
|
||||
"camera_z",
|
||||
"bg_color_r",
|
||||
"bg_color_g",
|
||||
"bg_color_b",
|
||||
"bg_color_a",
|
||||
"player_camera_lock",
|
||||
"object_grid_snap",
|
||||
"draw_hud",
|
||||
"collision_enabled",
|
||||
"show_collision_all",
|
||||
"show_bounds_all",
|
||||
"show_origin_all",
|
||||
"show_all_rooms",
|
||||
"room_camera_changes_enabled",
|
||||
"draw_debug_objects",
|
||||
]
|
||||
"""
|
||||
Properties we serialize on behalf of GameWorld
|
||||
|
|
@ -172,6 +217,7 @@ class WorldPropertiesObject(GameObject):
|
|||
serialized = world_props
|
||||
editable = []
|
||||
"All visible properties are serialized, not editable"
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
world_class = type(world)
|
||||
|
|
@ -188,31 +234,36 @@ class WorldPropertiesObject(GameObject):
|
|||
# set explicitly as float, for camera & bg color
|
||||
setattr(self, v, 0.0)
|
||||
# special handling of bg color (a list)
|
||||
self.world.bg_color = [self.bg_color_r, self.bg_color_g, self.bg_color_b, self.bg_color_a]
|
||||
self.world.bg_color = [
|
||||
self.bg_color_r,
|
||||
self.bg_color_g,
|
||||
self.bg_color_b,
|
||||
self.bg_color_a,
|
||||
]
|
||||
self.world.camera.set_loc(self.camera_x, self.camera_y, self.camera_z)
|
||||
# TODO: figure out why collision_enabled seems to default False!
|
||||
|
||||
|
||||
def set_object_property(self, prop_name, new_value):
|
||||
setattr(self, prop_name, new_value)
|
||||
# special handling for some values, eg bg color and camera
|
||||
if prop_name.startswith('bg_color_'):
|
||||
component = {'r': 0, 'g': 1, 'b': 2, 'a': 3}[prop_name[-1]]
|
||||
if prop_name.startswith("bg_color_"):
|
||||
component = {"r": 0, "g": 1, "b": 2, "a": 3}[prop_name[-1]]
|
||||
self.world.bg_color[component] = float(new_value)
|
||||
elif prop_name.startswith('camera_') and len(prop_name) == len('camera_x'):
|
||||
elif prop_name.startswith("camera_") and len(prop_name) == len("camera_x"):
|
||||
setattr(self.world.camera, prop_name[-1], new_value)
|
||||
# some properties have unique set methods in GW
|
||||
elif prop_name == 'show_collision_all':
|
||||
elif prop_name == "show_collision_all":
|
||||
self.world.toggle_all_collision_viz()
|
||||
elif prop_name == 'show_bounds_all':
|
||||
elif prop_name == "show_bounds_all":
|
||||
self.world.toggle_all_bounds_viz()
|
||||
elif prop_name == 'show_origin_all':
|
||||
elif prop_name == "show_origin_all":
|
||||
self.world.toggle_all_origin_viz()
|
||||
elif prop_name == 'player_camera_lock':
|
||||
elif prop_name == "player_camera_lock":
|
||||
self.world.toggle_player_camera_lock()
|
||||
# normal properties you can just set: set em
|
||||
elif hasattr(self.world, prop_name):
|
||||
setattr(self.world, prop_name, new_value)
|
||||
|
||||
|
||||
def update_from_world(self):
|
||||
self.camera_x = self.world.camera.x
|
||||
self.camera_y = self.world.camera.y
|
||||
|
|
@ -225,6 +276,7 @@ class WorldGlobalsObject(GameObject):
|
|||
Subclass can be specified in WorldPropertiesObject.
|
||||
NOTE: this object is spawned from scratch every load, it's never serialized!
|
||||
"""
|
||||
|
||||
should_save = False
|
||||
visible = deleteable = selectable = False
|
||||
locked = True
|
||||
|
|
@ -237,8 +289,9 @@ class WorldGlobalsObject(GameObject):
|
|||
|
||||
class LocationMarker(GameObject):
|
||||
"Very simple GameObject that marks an XYZ location for eg camera points"
|
||||
art_src = 'loc_marker'
|
||||
serialized = ['name', 'x', 'y', 'z', 'visible', 'locked']
|
||||
|
||||
art_src = "loc_marker"
|
||||
serialized = ["name", "x", "y", "z", "visible", "locked"]
|
||||
editable = []
|
||||
alpha = 0.5
|
||||
physics_move = False
|
||||
|
|
@ -250,21 +303,24 @@ class StaticTileTrigger(GameObject):
|
|||
Generic static trigger with tile-based collision.
|
||||
Overlaps but doesn't collide.
|
||||
"""
|
||||
|
||||
is_debug = True
|
||||
collision_shape_type = CST_TILE
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
noncolliding_classes = ['GameObject']
|
||||
noncolliding_classes = ["GameObject"]
|
||||
physics_move = False
|
||||
serialized = ['name', 'x', 'y', 'z', 'art_src', 'visible', 'locked']
|
||||
|
||||
serialized = ["name", "x", "y", "z", "art_src", "visible", "locked"]
|
||||
|
||||
def started_overlapping(self, other):
|
||||
#self.app.log('Trigger overlapped with %s' % other.name)
|
||||
# self.app.log('Trigger overlapped with %s' % other.name)
|
||||
pass
|
||||
|
||||
|
||||
class WarpTrigger(StaticTileTrigger):
|
||||
"Trigger that warps object to a room/marker when they touch it."
|
||||
|
||||
is_debug = True
|
||||
art_src = 'trigger_default'
|
||||
art_src = "trigger_default"
|
||||
alpha = 0.5
|
||||
destination_marker_name = None
|
||||
"If set, warp to this location marker"
|
||||
|
|
@ -272,16 +328,21 @@ class WarpTrigger(StaticTileTrigger):
|
|||
"If set, make this room the world's current"
|
||||
use_marker_room = True
|
||||
"If True, change to destination marker's room"
|
||||
warp_class_names = ['Player']
|
||||
warp_class_names = ["Player"]
|
||||
"List of class names to warp on contact with us."
|
||||
serialized = StaticTileTrigger.serialized + ['destination_room_name',
|
||||
'destination_marker_name',
|
||||
'use_marker_room']
|
||||
|
||||
serialized = StaticTileTrigger.serialized + [
|
||||
"destination_room_name",
|
||||
"destination_marker_name",
|
||||
"use_marker_room",
|
||||
]
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
StaticTileTrigger.__init__(self, world, obj_data)
|
||||
self.warp_classes = [self.world.get_class_by_name(class_name) for class_name in self.warp_class_names]
|
||||
|
||||
self.warp_classes = [
|
||||
self.world.get_class_by_name(class_name)
|
||||
for class_name in self.warp_class_names
|
||||
]
|
||||
|
||||
def started_overlapping(self, other):
|
||||
if other.warped_recently():
|
||||
return
|
||||
|
|
@ -307,25 +368,34 @@ class WarpTrigger(StaticTileTrigger):
|
|||
elif self.destination_marker_name:
|
||||
marker = self.world.objects.get(self.destination_marker_name, None)
|
||||
if not marker:
|
||||
self.app.log('Warp destination object %s not found' % self.destination_marker_name)
|
||||
self.app.log(
|
||||
"Warp destination object %s not found"
|
||||
% self.destination_marker_name
|
||||
)
|
||||
return
|
||||
other.set_loc(marker.x, marker.y, marker.z)
|
||||
# warp to marker's room if specified, pick a random one if multiple
|
||||
if self.use_marker_room and len(marker.rooms) == 1:
|
||||
room = random.choice(list(marker.rooms.values()))
|
||||
# warn if both room and marker are set but they conflict
|
||||
if self.destination_room_name and \
|
||||
room.name != self.destination_room_name:
|
||||
self.app.log("Marker %s's room differs from destination room %s" % (marker.name, self.destination_room_name))
|
||||
if (
|
||||
self.destination_room_name
|
||||
and room.name != self.destination_room_name
|
||||
):
|
||||
self.app.log(
|
||||
"Marker %s's room differs from destination room %s"
|
||||
% (marker.name, self.destination_room_name)
|
||||
)
|
||||
self.world.change_room(room.name)
|
||||
other.last_warp_update = self.world.updates
|
||||
|
||||
|
||||
class ObjectSpawner(LocationMarker):
|
||||
"Simple object that spawns an object when triggered"
|
||||
|
||||
is_debug = True
|
||||
spawn_class_name = None
|
||||
spawn_obj_name = ''
|
||||
spawn_obj_name = ""
|
||||
spawn_random_in_bounds = False
|
||||
"If True, spawn somewhere in this object's bounds, else spawn at location"
|
||||
spawn_obj_data = {}
|
||||
|
|
@ -336,20 +406,23 @@ class ObjectSpawner(LocationMarker):
|
|||
"Set False for any subclass that triggers in some other way"
|
||||
destroy_on_room_exit = True
|
||||
"if True, spawned object will be destroyed when player leaves its room"
|
||||
serialized = LocationMarker.serialized + ['spawn_class_name', 'spawn_obj_name',
|
||||
'times_to_fire', 'destroy_on_room_exit'
|
||||
serialized = LocationMarker.serialized + [
|
||||
"spawn_class_name",
|
||||
"spawn_obj_name",
|
||||
"times_to_fire",
|
||||
"destroy_on_room_exit",
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
LocationMarker.__init__(self, world, obj_data)
|
||||
self.times_fired = 0
|
||||
# list of objects we've spawned
|
||||
self.spawned_objects = []
|
||||
|
||||
|
||||
def get_spawn_class_name(self):
|
||||
"Return class name of object to spawn."
|
||||
return self.spawn_class_name
|
||||
|
||||
|
||||
def get_spawn_location(self):
|
||||
"Return x,y location we should spawn a new object at."
|
||||
if not self.spawn_random_in_bounds:
|
||||
|
|
@ -358,11 +431,11 @@ class ObjectSpawner(LocationMarker):
|
|||
x = left + random.random() * (right - left)
|
||||
y = top + random.random() * (bottom - top)
|
||||
return x, y
|
||||
|
||||
|
||||
def can_spawn(self):
|
||||
"Return True if spawner is allowed to spawn."
|
||||
return True
|
||||
|
||||
|
||||
def do_spawn(self):
|
||||
"Spawn and returns object."
|
||||
class_name = self.get_spawn_class_name()
|
||||
|
|
@ -379,7 +452,7 @@ class ObjectSpawner(LocationMarker):
|
|||
new_obj.spawner = self
|
||||
# TODO: put new object in our room(s), apply spawn_obj_data
|
||||
return new_obj
|
||||
|
||||
|
||||
def trigger(self):
|
||||
"Poke this spawner to do its thing, returns an object if spawned"
|
||||
if self.times_to_fire != -1 and self.times_fired >= self.times_to_fire:
|
||||
|
|
@ -389,11 +462,11 @@ class ObjectSpawner(LocationMarker):
|
|||
if self.times_fired != -1:
|
||||
self.times_fired += 1
|
||||
return self.do_spawn()
|
||||
|
||||
|
||||
def room_entered(self, room, old_room):
|
||||
if self.trigger_on_room_enter:
|
||||
self.trigger()
|
||||
|
||||
|
||||
def room_exited(self, room, new_room):
|
||||
if not self.destroy_on_room_exit:
|
||||
return
|
||||
|
|
@ -403,29 +476,38 @@ class ObjectSpawner(LocationMarker):
|
|||
|
||||
class SoundBlaster(LocationMarker):
|
||||
"Simple object that plays sound when triggered"
|
||||
|
||||
is_debug = True
|
||||
sound_name = ''
|
||||
sound_name = ""
|
||||
"String name of sound to play, minus any extension"
|
||||
can_play = True
|
||||
"If False, won't play sound when triggered"
|
||||
play_on_room_enter = True
|
||||
loops = -1
|
||||
"Number of times to loop, if -1 loop indefinitely"
|
||||
serialized = LocationMarker.serialized + ['sound_name', 'can_play',
|
||||
'play_on_room_enter']
|
||||
|
||||
serialized = LocationMarker.serialized + [
|
||||
"sound_name",
|
||||
"can_play",
|
||||
"play_on_room_enter",
|
||||
]
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
LocationMarker.__init__(self, world, obj_data)
|
||||
# find file, try common extensions
|
||||
for ext in ['', '.ogg', '.wav']:
|
||||
for ext in ["", ".ogg", ".wav"]:
|
||||
filename = self.sound_name + ext
|
||||
if self.world.sounds_dir and os.path.exists(self.world.sounds_dir + filename):
|
||||
if self.world.sounds_dir and os.path.exists(
|
||||
self.world.sounds_dir + filename
|
||||
):
|
||||
self.sound_filenames[self.sound_name] = filename
|
||||
return
|
||||
self.world.app.log("Couldn't find sound file %s for SoundBlaster %s" % (self.sound_name, self.name))
|
||||
|
||||
self.world.app.log(
|
||||
"Couldn't find sound file %s for SoundBlaster %s"
|
||||
% (self.sound_name, self.name)
|
||||
)
|
||||
|
||||
def room_entered(self, room, old_room):
|
||||
self.play_sound(self.sound_name, self.loops)
|
||||
|
||||
|
||||
def room_exited(self, room, new_room):
|
||||
self.stop_sound(self.sound_name)
|
||||
|
|
|
|||
530
game_world.py
530
game_world.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from game_object import GameObject
|
||||
|
||||
# initial work: 2019-02-17 and 18
|
||||
|
|
@ -31,24 +30,45 @@ DIR_NORTH = (0, -1)
|
|||
DIR_SOUTH = (0, 1)
|
||||
DIR_EAST = (1, 0)
|
||||
DIR_WEST = (-1, 0)
|
||||
LEFT_TURN_DIRS = { DIR_NORTH: DIR_WEST, DIR_WEST: DIR_SOUTH,
|
||||
DIR_SOUTH: DIR_EAST, DIR_EAST: DIR_NORTH }
|
||||
RIGHT_TURN_DIRS = { DIR_NORTH: DIR_EAST, DIR_EAST: DIR_SOUTH,
|
||||
DIR_SOUTH: DIR_WEST, DIR_WEST: DIR_NORTH }
|
||||
DIR_NAMES = { DIR_NORTH: 'north', DIR_SOUTH: 'south',
|
||||
DIR_EAST: 'east', DIR_WEST: 'west' }
|
||||
OPPOSITE_DIRS = { DIR_NORTH: DIR_SOUTH, DIR_SOUTH: DIR_NORTH,
|
||||
DIR_EAST: DIR_WEST, DIR_WEST: DIR_EAST }
|
||||
LEFT_TURN_DIRS = {
|
||||
DIR_NORTH: DIR_WEST,
|
||||
DIR_WEST: DIR_SOUTH,
|
||||
DIR_SOUTH: DIR_EAST,
|
||||
DIR_EAST: DIR_NORTH,
|
||||
}
|
||||
RIGHT_TURN_DIRS = {
|
||||
DIR_NORTH: DIR_EAST,
|
||||
DIR_EAST: DIR_SOUTH,
|
||||
DIR_SOUTH: DIR_WEST,
|
||||
DIR_WEST: DIR_NORTH,
|
||||
}
|
||||
DIR_NAMES = {DIR_NORTH: "north", DIR_SOUTH: "south", DIR_EAST: "east", DIR_WEST: "west"}
|
||||
OPPOSITE_DIRS = {
|
||||
DIR_NORTH: DIR_SOUTH,
|
||||
DIR_SOUTH: DIR_NORTH,
|
||||
DIR_EAST: DIR_WEST,
|
||||
DIR_WEST: DIR_EAST,
|
||||
}
|
||||
|
||||
|
||||
class CompositeTester(GameObject):
|
||||
# slightly confusing terms here, our "source" will be loaded at runtime
|
||||
art_src = 'comptest_dest'
|
||||
art_src = "comptest_dest"
|
||||
use_art_instance = True
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
# load composite source art
|
||||
comp_src_art = self.app.load_art('comptest_src', False)
|
||||
self.art.composite_from(comp_src_art, 0, 0, 0, 0,
|
||||
comp_src_art.width, comp_src_art.height,
|
||||
0, 0, 3, 2)
|
||||
comp_src_art = self.app.load_art("comptest_src", False)
|
||||
self.art.composite_from(
|
||||
comp_src_art,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
comp_src_art.width,
|
||||
comp_src_art.height,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
2,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
|
||||
import vector
|
||||
|
||||
from game_object import GameObject
|
||||
from renderable_line import DebugLineRenderable
|
||||
|
||||
|
||||
# stuff for troubleshooting "get tiles intersecting line" etc
|
||||
|
||||
|
||||
|
|
@ -14,6 +11,7 @@ class DebugMarker(GameObject):
|
|||
generate_art = True
|
||||
should_save = False
|
||||
alpha = 0.5
|
||||
|
||||
def pre_first_update(self):
|
||||
# red X with yellow background
|
||||
self.art.set_tile_at(0, 0, 0, 0, 24, 3, 8)
|
||||
|
|
@ -24,34 +22,34 @@ class LineTester(GameObject):
|
|||
art_width, art_height = 40, 40
|
||||
art_off_pct_x, art_off_pct_y = 0.0, 0.0
|
||||
generate_art = True
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.mark_a = self.world.spawn_object_of_class('DebugMarker', -3, 33)
|
||||
self.mark_b = self.world.spawn_object_of_class('DebugMarker', -10, 40)
|
||||
self.mark_a = self.world.spawn_object_of_class("DebugMarker", -3, 33)
|
||||
self.mark_b = self.world.spawn_object_of_class("DebugMarker", -10, 40)
|
||||
self.z = -0.01
|
||||
self.world.grid.visible = True
|
||||
self.line = DebugLineRenderable(self.app, self.art)
|
||||
self.line.z = 0.0
|
||||
self.line.line_width = 3
|
||||
|
||||
|
||||
def update(self):
|
||||
GameObject.update(self)
|
||||
# debug line
|
||||
self.line.set_lines([(self.mark_a.x, self.mark_a.y, 0.0),
|
||||
(self.mark_b.x, self.mark_b.y, 0.0)])
|
||||
self.line.set_lines(
|
||||
[(self.mark_a.x, self.mark_a.y, 0.0), (self.mark_b.x, self.mark_b.y, 0.0)]
|
||||
)
|
||||
# paint tiles under line
|
||||
self.art.clear_frame_layer(0, 0, 7)
|
||||
line_func = vector.get_tiles_along_line
|
||||
line_func = vector.get_tiles_along_integer_line
|
||||
tiles = line_func(self.mark_a.x, self.mark_a.y,
|
||||
self.mark_b.x, self.mark_b.y)
|
||||
tiles = line_func(self.mark_a.x, self.mark_a.y, self.mark_b.x, self.mark_b.y)
|
||||
for tile in tiles:
|
||||
x, y = self.get_tile_at_point(tile[0], tile[1])
|
||||
char, fg = 1, 6
|
||||
self.art.set_tile_at(0, 0, x, y, char, fg)
|
||||
|
||||
|
||||
def render(self, layer, z_override=None):
|
||||
GameObject.render(self, layer, z_override)
|
||||
# TODO not sure why this is necessary, pre_first_update should run before first render(), right? blech
|
||||
if hasattr(self, 'line') and self.line:
|
||||
if hasattr(self, "line") and self.line:
|
||||
self.line.render()
|
||||
|
|
|
|||
|
|
@ -1,45 +1,47 @@
|
|||
|
||||
from game_object import GameObject
|
||||
from game_util_objects import Player
|
||||
|
||||
from games.crawler.scripts.crawler import DIR_NORTH, DIR_SOUTH, DIR_EAST, DIR_WEST, LEFT_TURN_DIRS, RIGHT_TURN_DIRS, DIR_NAMES, OPPOSITE_DIRS
|
||||
from games.crawler.scripts.crawler import (
|
||||
DIR_EAST,
|
||||
DIR_NAMES,
|
||||
DIR_NORTH,
|
||||
DIR_SOUTH,
|
||||
DIR_WEST,
|
||||
LEFT_TURN_DIRS,
|
||||
OPPOSITE_DIRS,
|
||||
RIGHT_TURN_DIRS,
|
||||
)
|
||||
|
||||
|
||||
class CrawlPlayer(Player):
|
||||
should_save = False # we are spawned by maze
|
||||
should_save = False # we are spawned by maze
|
||||
generate_art = True
|
||||
art_width, art_height = 1, 1
|
||||
art_charset, art_palette = 'jpetscii', 'c64_pepto'
|
||||
art_charset, art_palette = "jpetscii", "c64_pepto"
|
||||
art_off_pct_x, art_off_pct_y = 0, 0
|
||||
# bespoke grid-based movement method
|
||||
physics_move = False
|
||||
handle_key_events = True
|
||||
|
||||
|
||||
view_range_tiles = 8
|
||||
fg_color = 8 # yellow
|
||||
dir_chars = { DIR_NORTH: 147,
|
||||
DIR_SOUTH: 163,
|
||||
DIR_EAST: 181,
|
||||
DIR_WEST: 180
|
||||
}
|
||||
|
||||
fg_color = 8 # yellow
|
||||
dir_chars = {DIR_NORTH: 147, DIR_SOUTH: 163, DIR_EAST: 181, DIR_WEST: 180}
|
||||
|
||||
def pre_first_update(self):
|
||||
Player.pre_first_update(self)
|
||||
# top-down facing
|
||||
self.direction = DIR_NORTH
|
||||
self.maze.update_tile_visibilities()
|
||||
self.art.set_tile_at(0, 0, 0, 0, self.dir_chars[self.direction], self.fg_color)
|
||||
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
# turning?
|
||||
if key == 'left':
|
||||
if key == "left":
|
||||
self.direction = LEFT_TURN_DIRS[self.direction]
|
||||
elif key == 'right':
|
||||
elif key == "right":
|
||||
self.direction = RIGHT_TURN_DIRS[self.direction]
|
||||
# moving?
|
||||
elif key == 'up' or key == 'down':
|
||||
elif key == "up" or key == "down":
|
||||
x, y = self.maze.get_tile_at_point(self.x, self.y)
|
||||
if key == 'up':
|
||||
if key == "up":
|
||||
new_x = x + self.direction[0]
|
||||
new_y = y + self.direction[1]
|
||||
else:
|
||||
|
|
@ -48,7 +50,11 @@ class CrawlPlayer(Player):
|
|||
# is move valid?
|
||||
if self.maze.is_tile_solid(new_x, new_y):
|
||||
# TEMP negative feedback
|
||||
dir_name = DIR_NAMES[self.direction] if key == 'up' else DIR_NAMES[OPPOSITE_DIRS[self.direction]]
|
||||
dir_name = (
|
||||
DIR_NAMES[self.direction]
|
||||
if key == "up"
|
||||
else DIR_NAMES[OPPOSITE_DIRS[self.direction]]
|
||||
)
|
||||
self.app.log("can't go %s!" % dir_name)
|
||||
else:
|
||||
self.x, self.y = self.maze.x + new_x, self.maze.y - new_y
|
||||
|
|
|
|||
|
|
@ -1,36 +1,41 @@
|
|||
|
||||
from game_object import GameObject
|
||||
from vector import get_tiles_along_integer_line
|
||||
|
||||
from art import TileIter
|
||||
|
||||
from random import randint # DEBUG
|
||||
|
||||
from games.crawler.scripts.crawler import DIR_NORTH, DIR_SOUTH, DIR_EAST, DIR_WEST, LEFT_TURN_DIRS, RIGHT_TURN_DIRS, DIR_NAMES, OPPOSITE_DIRS
|
||||
from game_object import GameObject
|
||||
from games.crawler.scripts.crawler import (
|
||||
DIR_EAST,
|
||||
DIR_NORTH,
|
||||
DIR_SOUTH,
|
||||
DIR_WEST,
|
||||
)
|
||||
from vector import get_tiles_along_integer_line
|
||||
|
||||
|
||||
class CrawlTopDownView(GameObject):
|
||||
art_src = 'maze2'
|
||||
art_src = "maze2"
|
||||
art_off_pct_x, art_off_pct_y = 0, 0
|
||||
# we will be modifying this view at runtime so don't write on the source art
|
||||
use_art_instance = True
|
||||
# first character we find with this index will be where we spawn player
|
||||
playerstart_char_index = 147
|
||||
# undiscovered = player has never seen this tile
|
||||
undiscovered_color_index = 1 # black
|
||||
undiscovered_color_index = 1 # black
|
||||
# discovered = player has seen this tile but isn't currently looking at it
|
||||
discovered_color_index = 12 # dark grey
|
||||
|
||||
discovered_color_index = 12 # dark grey
|
||||
|
||||
def pre_first_update(self):
|
||||
# scan art for spot to spawn player
|
||||
player_x, player_y = -1, -1
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
if self.art.get_char_index_at(frame, layer, x, y) == self.playerstart_char_index:
|
||||
if (
|
||||
self.art.get_char_index_at(frame, layer, x, y)
|
||||
== self.playerstart_char_index
|
||||
):
|
||||
player_x, player_y = self.x + x, self.y - y
|
||||
# clear the tile at this spot in our art
|
||||
self.art.set_char_index_at(frame, layer, x, y, 0)
|
||||
break
|
||||
self.world.player = self.world.spawn_object_of_class('CrawlPlayer', player_x, player_y)
|
||||
self.world.player = self.world.spawn_object_of_class(
|
||||
"CrawlPlayer", player_x, player_y
|
||||
)
|
||||
# give player a ref to us
|
||||
self.world.player.maze = self
|
||||
# make a copy of original layer to color for visibility, hide original
|
||||
|
|
@ -41,16 +46,16 @@ class CrawlTopDownView(GameObject):
|
|||
continue
|
||||
# set all tiles undiscovered
|
||||
self.art.set_color_at(0, layer, x, y, self.undiscovered_color_index)
|
||||
self.art.mark_all_frames_changed() # DEBUG - this fixes the difference in result when use_art_instance=True! why?
|
||||
self.art.mark_all_frames_changed() # DEBUG - this fixes the difference in result when use_art_instance=True! why?
|
||||
# keep a list of tiles player can see
|
||||
self.player_visible_tiles = []
|
||||
|
||||
|
||||
def is_tile_solid(self, x, y):
|
||||
return self.art.get_char_index_at(0, 0, x, y) != 0
|
||||
|
||||
|
||||
# world to tile: self.get_tile_at_point(world_x, world_y)
|
||||
# tile to world: self.get_tile_loc(tile_x, tile_y)
|
||||
|
||||
|
||||
def get_visible_tiles(self, x, y, dir_x, dir_y, tile_range, see_thru_walls=False):
|
||||
"return tiles visible from given point facing given direction"
|
||||
# NOTE: all the calculations here are done in this object's art's tile
|
||||
|
|
@ -72,7 +77,7 @@ class CrawlTopDownView(GameObject):
|
|||
# scan back of frustum tile by tile left to right,
|
||||
# checking each tile hit
|
||||
scan_distance = 0
|
||||
scan_length = tile_range * 2 + 1 # TODO make sure this is correct
|
||||
scan_length = tile_range * 2 + 1 # TODO make sure this is correct
|
||||
while scan_distance < scan_length:
|
||||
scan_x = scan_start_x + (scan_dir_x * scan_distance)
|
||||
scan_y = scan_start_y + (scan_dir_y * scan_distance)
|
||||
|
|
@ -80,17 +85,21 @@ class CrawlTopDownView(GameObject):
|
|||
for tile in hit_tiles:
|
||||
tile_x, tile_y = tile[0], tile[1]
|
||||
# skip out-of-bounds tiles
|
||||
if 0 > tile_x or tile_x >= self.art.width or \
|
||||
0 > tile_y or tile_y >= self.art.height:
|
||||
if (
|
||||
tile_x < 0
|
||||
or tile_x >= self.art.width
|
||||
or tile_y < 0
|
||||
or tile_y >= self.art.height
|
||||
):
|
||||
continue
|
||||
# whether this tile is solid or not, we have seen it
|
||||
if not tile in tiles:
|
||||
if tile not in tiles:
|
||||
tiles.append((tile_x, tile_y))
|
||||
if not see_thru_walls and self.is_tile_solid(*tile):
|
||||
break
|
||||
scan_distance += 1
|
||||
return tiles
|
||||
|
||||
|
||||
def update_tile_visibilities(self):
|
||||
"""
|
||||
update our art's tile visuals based on what tiles can be, can't be,
|
||||
|
|
@ -99,25 +108,27 @@ class CrawlTopDownView(GameObject):
|
|||
previously_visible_tiles = self.player_visible_tiles[:]
|
||||
p = self.world.player
|
||||
px, py = self.get_tile_at_point(p.x, p.y)
|
||||
self.player_visible_tiles = self.get_visible_tiles(px, py,
|
||||
*p.direction,
|
||||
p.view_range_tiles,
|
||||
see_thru_walls=False)
|
||||
#print(self.player_visible_tiles)
|
||||
self.player_visible_tiles = self.get_visible_tiles(
|
||||
px, py, *p.direction, p.view_range_tiles, see_thru_walls=False
|
||||
)
|
||||
# print(self.player_visible_tiles)
|
||||
# color currently visible tiles
|
||||
for tile in self.player_visible_tiles:
|
||||
#print(tile)
|
||||
if 0 > tile[0] or tile[0] >= self.art.width or \
|
||||
0 > tile[1] or tile[1] >= self.art.height:
|
||||
# print(tile)
|
||||
if (
|
||||
tile[0] < 0
|
||||
or tile[0] >= self.art.width
|
||||
or tile[1] < 0
|
||||
or tile[1] >= self.art.height
|
||||
):
|
||||
continue
|
||||
if self.is_tile_solid(*tile):
|
||||
orig_color = self.art.get_fg_color_index_at(0, 0, *tile)
|
||||
self.art.set_color_at(0, 1, *tile, orig_color)
|
||||
else:
|
||||
#self.art.set_color_at(0, 1, *tile, randint(2, 14)) # DEBUG
|
||||
# self.art.set_color_at(0, 1, *tile, randint(2, 14)) # DEBUG
|
||||
pass
|
||||
# color "previously seen" tiles
|
||||
for tile in previously_visible_tiles:
|
||||
if not tile in self.player_visible_tiles and \
|
||||
self.is_tile_solid(*tile):
|
||||
if tile not in self.player_visible_tiles and self.is_tile_solid(*tile):
|
||||
self.art.set_color_at(0, 1, *tile, self.discovered_color_index)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import math
|
||||
from game_util_objects import DynamicBoxObject, Pickup, StaticTileObject, TopDownPlayer
|
||||
|
||||
from game_util_objects import TopDownPlayer, StaticTileBG, StaticTileObject, DynamicBoxObject, Pickup
|
||||
from collision import CST_AABB
|
||||
|
||||
class CronoPlayer(TopDownPlayer):
|
||||
art_src = 'crono'
|
||||
|
||||
art_src = "crono"
|
||||
|
||||
col_radius = 1.5
|
||||
|
||||
|
||||
# AABB testing
|
||||
#collision_shape_type = CST_AABB
|
||||
#col_offset_x, col_offset_y = 0, 1.25
|
||||
|
||||
# collision_shape_type = CST_AABB
|
||||
# col_offset_x, col_offset_y = 0, 1.25
|
||||
|
||||
col_width = 3
|
||||
col_height = 3
|
||||
art_off_pct_y = 0.9
|
||||
|
||||
|
||||
class Chest(DynamicBoxObject):
|
||||
art_src = 'chest'
|
||||
art_src = "chest"
|
||||
col_width, col_height = 6, 4
|
||||
col_offset_y = -0.5
|
||||
|
||||
|
||||
class Urn(Pickup):
|
||||
art_src = 'urn'
|
||||
art_src = "urn"
|
||||
col_radius = 2
|
||||
art_off_pct_y = 0.85
|
||||
|
||||
|
||||
class Bed(StaticTileObject):
|
||||
art_src = 'bed'
|
||||
art_src = "bed"
|
||||
art_off_pct_x, art_off_pct_y = 0.5, 1
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# PETSCII Fireplace for Playscii
|
||||
# https://jp.itch.io/petscii-fireplace
|
||||
|
||||
|
|
@ -10,11 +9,12 @@ expensive compared to many old demoscene fire tricks. But it's easy to think abo
|
|||
and tune, which was the right call for a one-day exercise :]
|
||||
"""
|
||||
|
||||
import os, webbrowser
|
||||
from random import random, randint, choice
|
||||
import os
|
||||
import webbrowser
|
||||
from random import choice, randint
|
||||
|
||||
from game_object import GameObject
|
||||
from art import TileIter
|
||||
from game_object import GameObject
|
||||
|
||||
#
|
||||
# some tuning knobs
|
||||
|
|
@ -28,31 +28,30 @@ SPAWN_MARGIN_X = 8
|
|||
# each particle's character "decays" towards 0 in random jumps
|
||||
CHAR_DECAY_RATE_MAX = 16
|
||||
# music is just an OGG file, modders feel free to provide your own in sounds/
|
||||
MUSIC_FILENAME = 'music.ogg'
|
||||
MUSIC_URL = 'http://brotherandroid.com'
|
||||
MUSIC_FILENAME = "music.ogg"
|
||||
MUSIC_URL = "http://brotherandroid.com"
|
||||
# random ranges for time in seconds til next message pops up
|
||||
MESSAGE_DELAY_MIN, MESSAGE_DELAY_MAX = 300, 600
|
||||
MESSAGES = [
|
||||
'Happy Holidays',
|
||||
'Merry Christmas',
|
||||
'Happy New Year',
|
||||
'Happy Hanukkah',
|
||||
'Happy Kwanzaa',
|
||||
'Feliz Navidad',
|
||||
'Joyeux Noel'
|
||||
"Happy Holidays",
|
||||
"Merry Christmas",
|
||||
"Happy New Year",
|
||||
"Happy Hanukkah",
|
||||
"Happy Kwanzaa",
|
||||
"Feliz Navidad",
|
||||
"Joyeux Noel",
|
||||
]
|
||||
|
||||
|
||||
class Fireplace(GameObject):
|
||||
|
||||
"The main game object, manages particles, handles input, draws the fire."
|
||||
|
||||
|
||||
generate_art = True
|
||||
art_charset = 'c64_petscii'
|
||||
art_width, art_height = 54, 30 # approximately 16x9 aspect
|
||||
art_palette = 'fireplace'
|
||||
art_charset = "c64_petscii"
|
||||
art_width, art_height = 54, 30 # approximately 16x9 aspect
|
||||
art_palette = "fireplace"
|
||||
handle_key_events = True
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.art.add_layer(z=0.01)
|
||||
self.target_particles = TARGET_PARTICLES_DEFAULT
|
||||
|
|
@ -75,12 +74,12 @@ class Fireplace(GameObject):
|
|||
self.help_screen.z = 1
|
||||
self.help_screen.set_scale(0.75, 0.75, 1)
|
||||
# start with help screen up, uncomment to hide on start
|
||||
#self.help_screen.visible = False
|
||||
# self.help_screen.visible = False
|
||||
# don't bother creating credit screen if no music present
|
||||
self.credit_screen = None
|
||||
self.music_exists = False
|
||||
if os.path.exists(self.world.sounds_dir + MUSIC_FILENAME):
|
||||
self.app.log('%s found in %s' % (MUSIC_FILENAME, self.world.sounds_dir))
|
||||
self.app.log("%s found in %s" % (MUSIC_FILENAME, self.world.sounds_dir))
|
||||
self.world.play_music(MUSIC_FILENAME)
|
||||
self.music_paused = False
|
||||
self.music_exists = True
|
||||
|
|
@ -89,9 +88,9 @@ class Fireplace(GameObject):
|
|||
self.credit_screen.z = 1.1
|
||||
self.credit_screen.set_scale(0.75, 0.75, 1)
|
||||
else:
|
||||
self.app.log('No %s found in %s' % (MUSIC_FILENAME, self.world.sounds_dir))
|
||||
self.app.log("No %s found in %s" % (MUSIC_FILENAME, self.world.sounds_dir))
|
||||
self.set_new_message_time()
|
||||
|
||||
|
||||
def update(self):
|
||||
# shift messages on layer 2 upward gradually
|
||||
if self.app.frames % 10 == 0:
|
||||
|
|
@ -145,18 +144,22 @@ class Fireplace(GameObject):
|
|||
self.art.set_tile_at(frame, layer, x, y, ch, fg - 1, bg - 1)
|
||||
# draw particles
|
||||
# (looks nicer if we don't clear between frames, actually)
|
||||
#self.art.clear_frame_layer(0, 0)
|
||||
# self.art.clear_frame_layer(0, 0)
|
||||
for p in self.particles:
|
||||
self.art.set_tile_at(0, 0, p.x, p.y, self.weighted_chars[p.char], p.fg, p.bg)
|
||||
self.art.set_tile_at(
|
||||
0, 0, p.x, p.y, self.weighted_chars[p.char], p.fg, p.bg
|
||||
)
|
||||
# spawn new particles to maintain target count
|
||||
while len(self.particles) < self.target_particles:
|
||||
p = FireParticle(self)
|
||||
self.particles.append(p)
|
||||
GameObject.update(self)
|
||||
|
||||
|
||||
def set_new_message_time(self):
|
||||
self.next_message_time = self.world.get_elapsed_time() / 1000 + randint(MESSAGE_DELAY_MIN, MESSAGE_DELAY_MAX)
|
||||
|
||||
self.next_message_time = self.world.get_elapsed_time() / 1000 + randint(
|
||||
MESSAGE_DELAY_MIN, MESSAGE_DELAY_MAX
|
||||
)
|
||||
|
||||
def post_new_message(self):
|
||||
msg_text = choice(MESSAGES)
|
||||
x = randint(0, self.art.width - len(msg_text))
|
||||
|
|
@ -164,45 +167,48 @@ class Fireplace(GameObject):
|
|||
y = randint(int(self.art.height / 2), self.art.height)
|
||||
# write to second layer
|
||||
self.art.write_string(0, 1, x, y, msg_text, randint(12, 16))
|
||||
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
# in many Playscii games all input goes through the Player object;
|
||||
# here input is handled by this object.
|
||||
if key == 'escape' and not self.world.app.can_edit:
|
||||
if key == "escape" and not self.world.app.can_edit:
|
||||
self.world.app.should_quit = True
|
||||
elif key == 'h':
|
||||
elif key == "h":
|
||||
self.help_screen.visible = not self.help_screen.visible
|
||||
if self.credit_screen:
|
||||
self.credit_screen.visible = not self.credit_screen.visible
|
||||
elif key == 'm' and self.music_exists:
|
||||
elif key == "m" and self.music_exists:
|
||||
if self.music_paused:
|
||||
self.world.resume_music()
|
||||
self.music_paused = False
|
||||
else:
|
||||
self.world.pause_music()
|
||||
self.music_paused = True
|
||||
elif key == 'c':
|
||||
elif key == "c":
|
||||
if not self.app.fb.disable_crt:
|
||||
self.app.fb.toggle_crt()
|
||||
elif key == '=' or key == '+':
|
||||
elif key == "=" or key == "+":
|
||||
self.target_particles += 10
|
||||
self.art.write_string(0, 0, 0, 0, 'Embers: %s' % self.target_particles, 15, 1)
|
||||
elif key == '-':
|
||||
self.art.write_string(
|
||||
0, 0, 0, 0, "Embers: %s" % self.target_particles, 15, 1
|
||||
)
|
||||
elif key == "-":
|
||||
if self.target_particles <= 10:
|
||||
return
|
||||
self.target_particles -= 10
|
||||
self.art.write_string(0, 0, 0, 0, 'Embers: %s' % self.target_particles, 15, 1)
|
||||
self.art.write_string(
|
||||
0, 0, 0, 0, "Embers: %s" % self.target_particles, 15, 1
|
||||
)
|
||||
|
||||
|
||||
class FireParticle:
|
||||
|
||||
"Simulated particle, spawned and ticked and rendered by a Fireplace object."
|
||||
|
||||
|
||||
def __init__(self, fp):
|
||||
# pick char and color here; Fireplace should just run sim
|
||||
self.y = fp.art.height
|
||||
# spawn at random point along bottom edge, within margin
|
||||
self.x = randint(SPAWN_MARGIN_X, fp.art.width - SPAWN_MARGIN_X)
|
||||
self.x = randint(SPAWN_MARGIN_X, fp.art.width - SPAWN_MARGIN_X)
|
||||
# char here is not character index but density, which decays;
|
||||
# fp.weighted_chars is used to look up actual index
|
||||
self.char = randint(100, fp.art.charset.last_index - 1)
|
||||
|
|
@ -214,7 +220,7 @@ class FireParticle:
|
|||
self.bg = randint(0, len(fp.art.palette.colors) - 1)
|
||||
# hang on to fireplace
|
||||
self.fp = fp
|
||||
|
||||
|
||||
def update(self):
|
||||
# no need for out-of-range checks; fireplace will cull particles that
|
||||
# reach the top of the screen
|
||||
|
|
@ -228,9 +234,9 @@ class FireParticle:
|
|||
self.bg -= randint(0, 1)
|
||||
# don't bother with range checks on colors;
|
||||
# if random embers "flare up" that's cool
|
||||
#self.fg = max(0, self.fg)
|
||||
#self.bg = max(0, self.bg)
|
||||
|
||||
# self.fg = max(0, self.fg)
|
||||
# self.bg = max(0, self.bg)
|
||||
|
||||
def merge(self, other):
|
||||
# merge (sum w/ other) colors & chars (ie when particles overlap)
|
||||
self.char += other.char
|
||||
|
|
@ -239,27 +245,26 @@ class FireParticle:
|
|||
|
||||
|
||||
class HelpScreen(GameObject):
|
||||
art_src = 'help'
|
||||
art_src = "help"
|
||||
alpha = 0.7
|
||||
|
||||
|
||||
class CreditScreen(GameObject):
|
||||
|
||||
"Separate object for the clickable area of the help screen."
|
||||
|
||||
art_src = 'credit'
|
||||
|
||||
art_src = "credit"
|
||||
alpha = 0.7
|
||||
handle_mouse_events = True
|
||||
|
||||
|
||||
def clicked(self, button, mouse_x, mouse_y):
|
||||
if self.visible:
|
||||
webbrowser.open(MUSIC_URL)
|
||||
|
||||
|
||||
def hovered(self, mouse_x, mouse_y):
|
||||
# hilight text on hover
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
self.art.set_color_at(frame, layer, x, y, 2)
|
||||
|
||||
|
||||
def unhovered(self, mouse_x, mouse_y):
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
self.art.set_color_at(frame, layer, x, y, 16)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from random import choice
|
||||
|
||||
from art import TileIter
|
||||
|
|
@ -21,14 +20,14 @@ GS_LOST = 2
|
|||
class Board(GameObject):
|
||||
generate_art = True
|
||||
art_width, art_height = BOARD_WIDTH, BOARD_HEIGHT
|
||||
art_charset = 'jpetscii'
|
||||
art_palette = 'c64_original'
|
||||
art_charset = "jpetscii"
|
||||
art_palette = "c64_original"
|
||||
handle_key_events = True
|
||||
|
||||
|
||||
def __init__(self, world, obj_data):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
self.reset()
|
||||
|
||||
|
||||
def reset(self):
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
color = choice(TILE_COLORS)
|
||||
|
|
@ -39,33 +38,33 @@ class Board(GameObject):
|
|||
self.flood_with_color(start_color)
|
||||
self.turns = STARTING_TURNS
|
||||
self.game_state = GS_PLAYING
|
||||
|
||||
|
||||
def get_adjacent_tiles(self, x, y):
|
||||
tiles = []
|
||||
if x > 0:
|
||||
tiles.append((x-1, y))
|
||||
tiles.append((x - 1, y))
|
||||
if x < BOARD_WIDTH - 1:
|
||||
tiles.append((x+1, y))
|
||||
tiles.append((x + 1, y))
|
||||
if y > 0:
|
||||
tiles.append((x, y-1))
|
||||
tiles.append((x, y - 1))
|
||||
if y < BOARD_HEIGHT - 1:
|
||||
tiles.append((x, y+1))
|
||||
tiles.append((x, y + 1))
|
||||
return tiles
|
||||
|
||||
|
||||
def flood_with_color(self, flood_color):
|
||||
# set captured tiles to new color
|
||||
for tile_x,tile_y in self.captured_tiles:
|
||||
for tile_x, tile_y in self.captured_tiles:
|
||||
self.art.set_color_at(0, 0, tile_x, tile_y, flood_color, False)
|
||||
# capture like-colored tiles adjacent to captured tiles
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
if not (x, y) in self.captured_tiles:
|
||||
if (x, y) not in self.captured_tiles:
|
||||
continue
|
||||
adjacents = self.get_adjacent_tiles(x, y)
|
||||
for adj_x,adj_y in adjacents:
|
||||
for adj_x, adj_y in adjacents:
|
||||
adj_color = self.art.get_bg_color_index_at(frame, layer, adj_x, adj_y)
|
||||
if adj_color == flood_color:
|
||||
self.captured_tiles.append((adj_x, adj_y))
|
||||
|
||||
|
||||
def color_picked(self, color):
|
||||
self.flood_with_color(TILE_COLORS[color])
|
||||
self.turns -= 1
|
||||
|
|
@ -74,14 +73,14 @@ class Board(GameObject):
|
|||
elif self.turns == 0:
|
||||
self.game_state = GS_LOST
|
||||
# TODO: reset after delay / feedback?
|
||||
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
if self.game_state != GS_PLAYING:
|
||||
self.reset()
|
||||
return
|
||||
# get list of valid keys from length of tile_colors
|
||||
valid_keys = ['%s' % str(i + 1) for i in range(len(TILE_COLORS))]
|
||||
if not key in valid_keys:
|
||||
valid_keys = ["%s" % str(i + 1) for i in range(len(TILE_COLORS))]
|
||||
if key not in valid_keys:
|
||||
return
|
||||
key = int(key) - 1
|
||||
self.color_picked(key)
|
||||
|
|
@ -90,9 +89,9 @@ class Board(GameObject):
|
|||
class ColorBar(GameObject):
|
||||
generate_art = True
|
||||
art_width, art_height = len(TILE_COLORS), 1
|
||||
art_charset = 'jpetscii'
|
||||
art_palette = 'c64_original'
|
||||
|
||||
art_charset = "jpetscii"
|
||||
art_palette = "c64_original"
|
||||
|
||||
def __init__(self, world, obj_data):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
i = 0
|
||||
|
|
@ -103,31 +102,31 @@ class ColorBar(GameObject):
|
|||
|
||||
|
||||
class TurnsBar(GameObject):
|
||||
text = 'turns: %s'
|
||||
text = "turns: %s"
|
||||
generate_art = True
|
||||
art_width, art_height = len(text) + 3, 1
|
||||
art_charset = 'jpetscii'
|
||||
art_palette = 'c64_original'
|
||||
|
||||
art_charset = "jpetscii"
|
||||
art_palette = "c64_original"
|
||||
|
||||
def __init__(self, world, obj_data):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
self.board = None
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.board = self.world.get_all_objects_of_type('Board')[0]
|
||||
|
||||
self.board = self.world.get_all_objects_of_type("Board")[0]
|
||||
|
||||
def draw_text(self):
|
||||
if not self.board:
|
||||
return
|
||||
self.art.clear_frame_layer(0, 0)
|
||||
new_text = self.text % self.board.turns
|
||||
if self.board.game_state == GS_WON:
|
||||
new_text = 'won!!'
|
||||
new_text = "won!!"
|
||||
elif self.board.game_state == GS_LOST:
|
||||
new_text = 'lost :('
|
||||
new_text = "lost :("
|
||||
color = TILE_COLORS[self.board.turns % len(TILE_COLORS)]
|
||||
self.art.write_string(0, 0, 0, 0, new_text, color, 0)
|
||||
|
||||
|
||||
def update(self):
|
||||
GameObject.update(self)
|
||||
self.draw_text()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
|
||||
from game_hud import GameHUD, GameHUDRenderable
|
||||
|
||||
class MazeHUD(GameHUD):
|
||||
|
||||
class MazeHUD(GameHUD):
|
||||
message_color = 4
|
||||
|
||||
|
||||
def __init__(self, world):
|
||||
GameHUD.__init__(self, world)
|
||||
self.msg_art = self.world.app.new_art('mazehud_msg', 42, 1,
|
||||
'jpetscii', 'c64_original')
|
||||
self.msg_art = self.world.app.new_art(
|
||||
"mazehud_msg", 42, 1, "jpetscii", "c64_original"
|
||||
)
|
||||
self.msg = GameHUDRenderable(self.world.app, self.msg_art)
|
||||
self.arts = [self.msg_art]
|
||||
self.renderables = [self.msg]
|
||||
|
|
@ -17,10 +17,10 @@ class MazeHUD(GameHUD):
|
|||
aspect = self.world.app.window_height / self.world.app.window_width
|
||||
self.msg.scale_x = 0.075 * aspect
|
||||
self.msg.scale_y = 0.05
|
||||
self.current_msg = ''
|
||||
self.current_msg = ""
|
||||
self.msg_art.clear_frame_layer(0, 0, 0, self.message_color)
|
||||
self.post_msg('Welcome to MAZE, the amazing example game!')
|
||||
|
||||
self.post_msg("Welcome to MAZE, the amazing example game!")
|
||||
|
||||
def post_msg(self, msg_text):
|
||||
self.current_msg = msg_text
|
||||
self.msg_art.clear_frame_layer(0, 0, 0, self.message_color)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
|
||||
import math, random
|
||||
import math
|
||||
import random
|
||||
|
||||
from art import TileIter
|
||||
from collision import (
|
||||
CST_CIRCLE,
|
||||
CST_TILE,
|
||||
CT_GENERIC_DYNAMIC,
|
||||
CT_GENERIC_STATIC,
|
||||
CT_NONE,
|
||||
)
|
||||
from game_object import GameObject
|
||||
from game_util_objects import Player, StaticTileBG
|
||||
from collision import CST_NONE, CST_CIRCLE, CST_AABB, CST_TILE, CT_NONE, CT_GENERIC_STATIC, CT_GENERIC_DYNAMIC, CT_PLAYER, CTG_STATIC, CTG_DYNAMIC
|
||||
|
||||
|
||||
class MazeBG(StaticTileBG):
|
||||
z = -0.1
|
||||
|
||||
|
||||
class MazeNPC(GameObject):
|
||||
art_src = 'npc'
|
||||
art_src = "npc"
|
||||
use_art_instance = True
|
||||
col_radius = 0.5
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
bark = 'Well hello there!'
|
||||
|
||||
bark = "Well hello there!"
|
||||
|
||||
def started_colliding(self, other):
|
||||
if not isinstance(other, Player):
|
||||
return
|
||||
self.world.hud.post_msg(self.bark)
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.z = 0.1
|
||||
# TODO: investigate why this random color set doesn't seem to work
|
||||
|
|
@ -30,18 +38,19 @@ class MazeNPC(GameObject):
|
|||
for art in self.arts.values():
|
||||
art.set_all_non_transparent_colors(random_color)
|
||||
|
||||
|
||||
class MazeBaker(MazeNPC):
|
||||
bark = 'Sorry, all outta bread today!'
|
||||
bark = "Sorry, all outta bread today!"
|
||||
|
||||
|
||||
class MazeCritter(MazeNPC):
|
||||
|
||||
"dynamically-spawned NPC that wobbles around"
|
||||
|
||||
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
should_save = False
|
||||
move_rate = 0.25
|
||||
bark = 'wheee!'
|
||||
|
||||
bark = "wheee!"
|
||||
|
||||
def update(self):
|
||||
# skitter around randomly
|
||||
x, y = (random.random() * 2) - 1, (random.random() * 2) - 1
|
||||
|
|
@ -52,50 +61,49 @@ class MazeCritter(MazeNPC):
|
|||
|
||||
|
||||
class MazePickup(GameObject):
|
||||
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
col_radius = 0.5
|
||||
|
||||
|
||||
hold_offset_y = 1.2
|
||||
consume_on_use = True
|
||||
sound_filenames = {'pickup': 'pickup.ogg'}
|
||||
|
||||
sound_filenames = {"pickup": "pickup.ogg"}
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
self.holder = None
|
||||
|
||||
|
||||
def started_colliding(self, other):
|
||||
if not isinstance(other, Player):
|
||||
return
|
||||
if self is other.held_object:
|
||||
return
|
||||
other.pick_up(self)
|
||||
|
||||
|
||||
def stopped_colliding(self, other):
|
||||
if not isinstance(other, Player):
|
||||
return
|
||||
if self is not other.held_object:
|
||||
self.enable_collision()
|
||||
|
||||
|
||||
def picked_up(self, new_holder):
|
||||
self.holder = new_holder
|
||||
self.world.hud.post_msg('got %s!' % self.display_name)
|
||||
self.world.hud.post_msg("got %s!" % self.display_name)
|
||||
self.disable_collision()
|
||||
self.play_sound('pickup')
|
||||
|
||||
self.play_sound("pickup")
|
||||
|
||||
def used(self, user):
|
||||
if 'used' in self.sound_filenames:
|
||||
self.play_sound('used')
|
||||
if "used" in self.sound_filenames:
|
||||
self.play_sound("used")
|
||||
if self.consume_on_use:
|
||||
self.destroy()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.holder:
|
||||
self.holder.held_object = None
|
||||
self.holder = None
|
||||
GameObject.destroy(self)
|
||||
|
||||
|
||||
def update(self):
|
||||
GameObject.update(self)
|
||||
if not self.holder:
|
||||
|
|
@ -103,33 +111,33 @@ class MazePickup(GameObject):
|
|||
# if held, shadow holder
|
||||
self.x = self.holder.x
|
||||
# bob slightly above holder's head
|
||||
bob_y = math.sin(self.world.get_elapsed_time() / 100) / 10
|
||||
bob_y = math.sin(self.world.get_elapsed_time() / 100) / 10
|
||||
self.y = self.holder.y + self.hold_offset_y + bob_y
|
||||
self.z = self.holder.z
|
||||
|
||||
|
||||
class MazeKey(MazePickup):
|
||||
art_src = 'key'
|
||||
display_name = 'a gold key'
|
||||
used_message = 'unlocked!'
|
||||
art_src = "key"
|
||||
display_name = "a gold key"
|
||||
used_message = "unlocked!"
|
||||
|
||||
|
||||
class MazeAx(MazePickup):
|
||||
art_src = 'ax'
|
||||
display_name = 'an ax'
|
||||
art_src = "ax"
|
||||
display_name = "an ax"
|
||||
consume_on_use = False
|
||||
used_message = 'chop!'
|
||||
used_message = "chop!"
|
||||
# TODO: see if there's a way to add to MazePickup's sound dict here :/
|
||||
sound_filenames = {'pickup': 'pickup.ogg',
|
||||
'used': 'break.ogg'}
|
||||
sound_filenames = {"pickup": "pickup.ogg", "used": "break.ogg"}
|
||||
|
||||
|
||||
class MazePortalKey(MazePickup):
|
||||
art_src = 'artifact'
|
||||
display_name = 'the Artifact of Zendor'
|
||||
used_message = '!!??!?!!?!?!?!!'
|
||||
art_src = "artifact"
|
||||
display_name = "the Artifact of Zendor"
|
||||
used_message = "!!??!?!!?!?!?!!"
|
||||
consume_on_use = False
|
||||
sound_filenames = {'pickup': 'artifact.ogg',
|
||||
'used': 'portal.ogg'}
|
||||
|
||||
sound_filenames = {"pickup": "artifact.ogg", "used": "portal.ogg"}
|
||||
|
||||
def update(self):
|
||||
MazePickup.update(self)
|
||||
ch, fg, bg, xform = self.art.get_tile_at(0, 0, 0, 0)
|
||||
|
|
@ -146,21 +154,21 @@ class MazePortalKey(MazePickup):
|
|||
|
||||
|
||||
class MazeLock(StaticTileBG):
|
||||
art_src = 'lock'
|
||||
art_src = "lock"
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
col_radius = 0.5
|
||||
mass = 0.0
|
||||
key_type = MazeKey
|
||||
|
||||
|
||||
def started_colliding(self, other):
|
||||
if not isinstance(other, Player):
|
||||
return
|
||||
if other.held_object and type(other.held_object) is self.key_type:
|
||||
self.unlocked(other)
|
||||
else:
|
||||
self.world.hud.post_msg('blocked - need %s!' % self.key_type.display_name)
|
||||
|
||||
self.world.hud.post_msg("blocked - need %s!" % self.key_type.display_name)
|
||||
|
||||
def unlocked(self, other):
|
||||
self.disable_collision()
|
||||
self.visible = False
|
||||
|
|
@ -168,22 +176,21 @@ class MazeLock(StaticTileBG):
|
|||
|
||||
|
||||
class MazeBlockage(MazeLock):
|
||||
art_src = 'debris'
|
||||
art_src = "debris"
|
||||
key_type = MazeAx
|
||||
|
||||
|
||||
class MazePortalGate(MazeLock):
|
||||
|
||||
art_src = 'portalgate'
|
||||
art_src = "portalgate"
|
||||
key_type = MazePortalKey
|
||||
collision_shape_type = CST_TILE
|
||||
collision_type = CT_GENERIC_STATIC
|
||||
|
||||
|
||||
def update(self):
|
||||
MazeLock.update(self)
|
||||
if self.collision_type == CT_NONE:
|
||||
if not self.art.is_script_running('evap'):
|
||||
self.art.run_script_every('evap')
|
||||
if not self.art.is_script_running("evap"):
|
||||
self.art.run_script_every("evap")
|
||||
return
|
||||
# cycle non-black colors
|
||||
BLACK = 1
|
||||
|
|
@ -207,7 +214,8 @@ class MazePortalGate(MazeLock):
|
|||
|
||||
|
||||
class MazePortal(GameObject):
|
||||
art_src = 'portal'
|
||||
art_src = "portal"
|
||||
|
||||
def update(self):
|
||||
GameObject.update(self)
|
||||
if self.app.updates % 2 != 0:
|
||||
|
|
@ -215,17 +223,17 @@ class MazePortal(GameObject):
|
|||
ramps = {11: 10, 10: 3, 3: 11}
|
||||
for frame, layer, x, y in TileIter(self.art):
|
||||
ch, fg, bg, xform = self.art.get_tile_at(frame, layer, x, y)
|
||||
fg = ramps.get(fg, None)
|
||||
fg = ramps.get(fg)
|
||||
self.art.set_tile_at(frame, layer, x, y, ch, fg, bg, xform)
|
||||
|
||||
|
||||
class MazeStandingNPC(GameObject):
|
||||
art_src = 'npc'
|
||||
art_src = "npc"
|
||||
col_radius = 0.5
|
||||
collision_shape_type = CST_CIRCLE
|
||||
collision_type = CT_GENERIC_DYNAMIC
|
||||
bark = 'Well hello there!'
|
||||
|
||||
bark = "Well hello there!"
|
||||
|
||||
def started_colliding(self, other):
|
||||
if not isinstance(other, Player):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
|
||||
import math
|
||||
|
||||
from game_util_objects import Player, BlobShadow
|
||||
from game_util_objects import BlobShadow, Player
|
||||
from games.maze.scripts.rooms import OutsideRoom
|
||||
|
||||
|
||||
class PlayerBlobShadow(BlobShadow):
|
||||
z = 0
|
||||
fixed_z = True
|
||||
scale_x = scale_y = 0.5
|
||||
offset_y = -0.5
|
||||
|
||||
def pre_first_update(self):
|
||||
BlobShadow.pre_first_update(self)
|
||||
# TODO: figure out why class default scale isn't taking?
|
||||
|
|
@ -16,24 +17,24 @@ class PlayerBlobShadow(BlobShadow):
|
|||
|
||||
|
||||
class MazePlayer(Player):
|
||||
art_src = 'player'
|
||||
move_state = 'stand'
|
||||
art_src = "player"
|
||||
move_state = "stand"
|
||||
col_radius = 0.5
|
||||
# TODO: setting this to 2 fixes tunneling, but shouldn't slow down the player!
|
||||
fast_move_steps = 2
|
||||
attachment_classes = { 'shadow': 'PlayerBlobShadow' }
|
||||
|
||||
attachment_classes = {"shadow": "PlayerBlobShadow"}
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
Player.__init__(self, world, obj_data)
|
||||
self.held_object = None
|
||||
|
||||
|
||||
def pick_up(self, pickup):
|
||||
# drop any other held item first
|
||||
if self.held_object:
|
||||
self.drop(self.held_object, pickup)
|
||||
self.held_object = pickup
|
||||
pickup.picked_up(self)
|
||||
|
||||
|
||||
def drop(self, pickup, new_pickup=None):
|
||||
# drop pickup in place of one we're swapping with, else drop at feet
|
||||
if new_pickup:
|
||||
|
|
@ -41,11 +42,11 @@ class MazePlayer(Player):
|
|||
else:
|
||||
pickup.x, pickup.y = self.x, self.y
|
||||
pickup.holder = None
|
||||
|
||||
|
||||
def use_item(self):
|
||||
self.world.hud.post_msg(self.held_object.used_message)
|
||||
self.held_object.used(self)
|
||||
|
||||
|
||||
def update(self):
|
||||
Player.update(self)
|
||||
if type(self.world.current_room) is OutsideRoom:
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
|
||||
from game_room import GameRoom
|
||||
|
||||
|
||||
class MazeRoom(GameRoom):
|
||||
|
||||
def exited(self, new_room):
|
||||
GameRoom.exited(self, new_room)
|
||||
# clear message line when exiting
|
||||
if self.world.hud:
|
||||
self.world.hud.post_msg('')
|
||||
self.world.hud.post_msg("")
|
||||
|
||||
|
||||
class OutsideRoom(MazeRoom):
|
||||
|
||||
camera_follow_player = True
|
||||
|
||||
|
||||
def entered(self, old_room):
|
||||
MazeRoom.entered(self, old_room)
|
||||
self.world.collision_enabled = False
|
||||
self.world.app.camera.y_tilt = 4
|
||||
|
||||
|
||||
def exited(self, new_room):
|
||||
MazeRoom.exited(self, new_room)
|
||||
self.world.collision_enabled = True
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import math
|
||||
import random
|
||||
|
||||
import math, random
|
||||
from game_util_objects import Character, Player, StaticTileBG, WarpTrigger
|
||||
|
||||
from game_object import GameObject
|
||||
from game_util_objects import StaticTileBG, Player, Character, WarpTrigger
|
||||
from collision import CST_AABB
|
||||
|
||||
class PlatformWorld(StaticTileBG):
|
||||
draw_col_layer = True
|
||||
|
||||
|
||||
class PlatformPlayer(Player):
|
||||
|
||||
# from http://www.piratehearts.com/blog/2010/08/30/40/:
|
||||
# JumpSpeed = sqrt(2.0f * Gravity * JumpHeight);
|
||||
|
||||
art_src = 'player'
|
||||
#collision_shape_type = CST_AABB
|
||||
|
||||
art_src = "player"
|
||||
# collision_shape_type = CST_AABB
|
||||
col_width = 2
|
||||
col_height = 3
|
||||
handle_key_events = True
|
||||
|
|
@ -25,16 +24,16 @@ class PlatformPlayer(Player):
|
|||
ground_friction = 20
|
||||
air_friction = 15
|
||||
max_jump_press_time = 0.15
|
||||
editable = Player.editable + ['max_jump_press_time']
|
||||
jump_key = 'x'
|
||||
|
||||
editable = Player.editable + ["max_jump_press_time"]
|
||||
jump_key = "x"
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
Player.__init__(self, world, obj_data)
|
||||
self.jump_time = 0
|
||||
# don't jump again until jump is released and pressed again
|
||||
self.jump_ready = True
|
||||
self.started_jump = False
|
||||
|
||||
|
||||
def started_colliding(self, other):
|
||||
Player.started_colliding(self, other)
|
||||
if isinstance(other, PlatformMonster):
|
||||
|
|
@ -42,77 +41,87 @@ class PlatformPlayer(Player):
|
|||
dx, dy = other.x - self.x, other.y - self.y
|
||||
if abs(dy) > abs(dx) and dy < -1:
|
||||
other.destroy()
|
||||
|
||||
|
||||
def is_affected_by_gravity(self):
|
||||
return True
|
||||
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
if key == self.jump_key and self.jump_ready:
|
||||
self.jump()
|
||||
self.jump_ready = False
|
||||
self.started_jump = True
|
||||
|
||||
|
||||
def handle_key_up(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
if key == self.jump_key:
|
||||
self.jump_ready = True
|
||||
|
||||
|
||||
def allow_move_y(self, dy):
|
||||
# disable regular up/down movement, jump button sets move_y directly
|
||||
return False
|
||||
|
||||
|
||||
def update_state(self):
|
||||
self.state = 'stand' if self.is_on_ground() and (self.move_x, self.move_y) == (0, 0) else 'walk'
|
||||
|
||||
self.state = (
|
||||
"stand"
|
||||
if self.is_on_ground() and (self.move_x, self.move_y) == (0, 0)
|
||||
else "walk"
|
||||
)
|
||||
|
||||
def moved_this_frame(self):
|
||||
delta = math.sqrt(abs(self.last_x - self.x) ** 2 + abs(self.last_y - self.y) ** 2 + abs(self.last_z - self.z) ** 2)
|
||||
delta = math.sqrt(
|
||||
abs(self.last_x - self.x) ** 2
|
||||
+ abs(self.last_y - self.y) ** 2
|
||||
+ abs(self.last_z - self.z) ** 2
|
||||
)
|
||||
return delta > self.stop_velocity
|
||||
|
||||
|
||||
def is_on_ground(self):
|
||||
# works for now: just check for -Y contact with first world object
|
||||
ground = self.world.get_first_object_of_type('PlatformWorld')
|
||||
ground = self.world.get_first_object_of_type("PlatformWorld")
|
||||
contact = self.collision.contacts.get(ground.name, None)
|
||||
if not contact:
|
||||
return False
|
||||
return contact.overlap.y < 0
|
||||
|
||||
|
||||
def jump(self):
|
||||
self.jump_time += self.get_time_since_last_update() / 1000
|
||||
if self.jump_time < self.max_jump_press_time:
|
||||
self.move_y += 1
|
||||
|
||||
|
||||
def update(self):
|
||||
on_ground = self.is_on_ground()
|
||||
if on_ground and self.jump_time > 0:
|
||||
self.jump_time = 0
|
||||
# poll jump key for variable length jump
|
||||
if self.world.app.il.is_key_pressed(self.jump_key) and \
|
||||
(self.started_jump or not on_ground):
|
||||
if self.world.app.il.is_key_pressed(self.jump_key) and (
|
||||
self.started_jump or not on_ground
|
||||
):
|
||||
self.jump()
|
||||
self.started_jump = False
|
||||
Player.update(self)
|
||||
# wobble as we walk a la ELC2
|
||||
if self.state == 'walk' and on_ground:
|
||||
if self.state == "walk" and on_ground:
|
||||
self.y += math.sin(self.world.app.updates) / 5
|
||||
|
||||
|
||||
class PlatformMonster(Character):
|
||||
art_src = 'monster'
|
||||
move_state = 'stand'
|
||||
art_src = "monster"
|
||||
move_state = "stand"
|
||||
animating = True
|
||||
fast_move_steps = 2
|
||||
move_accel_x = 100
|
||||
col_radius = 1
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
# pick random starting direction
|
||||
self.move_dir_x = random.choice([-1, 1])
|
||||
self.set_timer_function('hit_wall', self.check_wall_hits, 0.2)
|
||||
|
||||
self.set_timer_function("hit_wall", self.check_wall_hits, 0.2)
|
||||
|
||||
def is_affected_by_gravity(self):
|
||||
return True
|
||||
|
||||
|
||||
def allow_move_y(self, dy):
|
||||
return False
|
||||
|
||||
|
||||
def check_wall_hits(self):
|
||||
"Turn around if a wall is immediately ahead of direction we're moving."
|
||||
# check collision in direction we're moving
|
||||
|
|
@ -123,20 +132,22 @@ class PlatformMonster(Character):
|
|||
x = self.x - self.col_radius - margin
|
||||
y = self.y
|
||||
# DEBUG see trace destination
|
||||
#lines = [(self.x, self.y, 0), (x, y, 0)]
|
||||
#self.app.debug_line_renderable.set_lines(lines)
|
||||
hits, shapes = self.world.get_colliders_at_point(x, y,
|
||||
#include_object_names=[],
|
||||
include_class_names=['PlatformWorld',
|
||||
'PlatformMonster'],
|
||||
exclude_object_names=[self.name])
|
||||
# lines = [(self.x, self.y, 0), (x, y, 0)]
|
||||
# self.app.debug_line_renderable.set_lines(lines)
|
||||
hits, shapes = self.world.get_colliders_at_point(
|
||||
x,
|
||||
y,
|
||||
# include_object_names=[],
|
||||
include_class_names=["PlatformWorld", "PlatformMonster"],
|
||||
exclude_object_names=[self.name],
|
||||
)
|
||||
if len(hits) > 0:
|
||||
self.move_dir_x = -self.move_dir_x
|
||||
|
||||
|
||||
def update(self):
|
||||
self.move(self.move_dir_x, 0)
|
||||
Character.update(self)
|
||||
|
||||
|
||||
class PlatformWarpTrigger(WarpTrigger):
|
||||
warp_class_names = ['Player', 'PlatformMonster']
|
||||
warp_class_names = ["Player", "PlatformMonster"]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
|
||||
import math, random
|
||||
import math
|
||||
import random
|
||||
|
||||
from game_object import GameObject
|
||||
from game_util_objects import Player, Character, Projectile, StaticTileBG, ObjectSpawner
|
||||
from game_util_objects import Character, ObjectSpawner, Player, Projectile, StaticTileBG
|
||||
|
||||
|
||||
class ShmupPlayer(Player):
|
||||
state_changes_art = False
|
||||
move_state = 'stand'
|
||||
art_src = 'player'
|
||||
move_state = "stand"
|
||||
art_src = "player"
|
||||
handle_key_events = True
|
||||
invincible = False # DEBUG
|
||||
serialized = Player.serialized + ['invincible']
|
||||
invincible = False # DEBUG
|
||||
serialized = Player.serialized + ["invincible"]
|
||||
respawn_delay = 3
|
||||
# refire delay, else holding X chokes game
|
||||
fire_delay = 0.15
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
Player.__init__(self, world, obj_data)
|
||||
# track last death and last fire time for respawn and refire delays
|
||||
|
|
@ -22,33 +23,33 @@ class ShmupPlayer(Player):
|
|||
self.last_fire_time = 0
|
||||
# save our start position
|
||||
self.start_x, self.start_y = self.x, self.y
|
||||
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
if key == 'x' and self.state == 'dead':
|
||||
if key == "x" and self.state == "dead":
|
||||
# respawn after short delay
|
||||
time = self.world.get_elapsed_time() / 1000
|
||||
if time >= self.last_death_time + self.respawn_delay:
|
||||
self.state = 'stand'
|
||||
self.state = "stand"
|
||||
self.set_loc(self.start_x, self.start_y)
|
||||
self.visible = True
|
||||
|
||||
|
||||
def update_state(self):
|
||||
# only two states, ignore stuff parent class does for this
|
||||
pass
|
||||
|
||||
|
||||
def die(self, killer):
|
||||
if self.invincible or self.state == 'dead':
|
||||
if self.invincible or self.state == "dead":
|
||||
return
|
||||
boom = Boom(self.world)
|
||||
boom.set_loc(self.x, self.y)
|
||||
self.state = 'dead'
|
||||
self.state = "dead"
|
||||
self.last_death_time = self.world.get_elapsed_time() / 1000
|
||||
self.visible = False
|
||||
|
||||
|
||||
def update(self):
|
||||
Player.update(self)
|
||||
# poll fire key directly for continuous fire (with refire delay)
|
||||
if self.state != 'dead' and self.world.app.il.is_key_pressed('x'):
|
||||
if self.state != "dead" and self.world.app.il.is_key_pressed("x"):
|
||||
time = self.world.get_elapsed_time() / 1000
|
||||
if time >= self.last_fire_time + self.fire_delay:
|
||||
proj = ShmupPlayerProjectile(self.world)
|
||||
|
|
@ -58,38 +59,44 @@ class ShmupPlayer(Player):
|
|||
|
||||
class PlayerBlocker(StaticTileBG):
|
||||
"keeps player from advancing too far upfield"
|
||||
art_src = 'blockline_horiz'
|
||||
noncolliding_classes = ['Projectile', 'ShmupEnemy']
|
||||
|
||||
art_src = "blockline_horiz"
|
||||
noncolliding_classes = ["Projectile", "ShmupEnemy"]
|
||||
|
||||
|
||||
class EnemySpawner(ObjectSpawner):
|
||||
"sits at top of screen and spawns enemies"
|
||||
art_src = 'spawn_area'
|
||||
|
||||
art_src = "spawn_area"
|
||||
spawn_random_in_bounds = True
|
||||
trigger_on_room_enter = False
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
ObjectSpawner.__init__(self, world, obj_data)
|
||||
self.next_spawn_time = 0
|
||||
self.target_enemy_count = 1
|
||||
|
||||
|
||||
def can_spawn(self):
|
||||
player = self.world.get_first_object_of_type('ShmupPlayer')
|
||||
player = self.world.get_first_object_of_type("ShmupPlayer")
|
||||
# only spawn if player has fired, there's room, and it's time
|
||||
return player and player.state != 'dead' and \
|
||||
player.last_fire_time > 0 and \
|
||||
len(self.spawned_objects) < self.target_enemy_count and \
|
||||
self.world.get_elapsed_time() >= self.next_spawn_time
|
||||
|
||||
return (
|
||||
player
|
||||
and player.state != "dead"
|
||||
and player.last_fire_time > 0
|
||||
and len(self.spawned_objects) < self.target_enemy_count
|
||||
and self.world.get_elapsed_time() >= self.next_spawn_time
|
||||
)
|
||||
|
||||
def get_spawn_class_name(self):
|
||||
roll = random.random()
|
||||
# pick random enemy type to spawn
|
||||
if roll > 0.8:
|
||||
return 'Enemy1'
|
||||
return "Enemy1"
|
||||
elif roll > 0.6:
|
||||
return 'Enemy2'
|
||||
return "Enemy2"
|
||||
else:
|
||||
return 'Asteroid'
|
||||
|
||||
return "Asteroid"
|
||||
|
||||
def update(self):
|
||||
StaticTileBG.update(self)
|
||||
# bump up enemy counts as time goes on
|
||||
|
|
@ -106,37 +113,42 @@ class EnemySpawner(ObjectSpawner):
|
|||
next_delay = random.random() * 3
|
||||
self.next_spawn_time = self.world.get_elapsed_time() + next_delay * 1000
|
||||
|
||||
|
||||
class EnemyDeleter(StaticTileBG):
|
||||
"deletes enemies once they hit a certain point on screen"
|
||||
art_src = 'blockline_horiz'
|
||||
|
||||
art_src = "blockline_horiz"
|
||||
|
||||
def started_colliding(self, other):
|
||||
if isinstance(other, ShmupEnemy):
|
||||
other.destroy()
|
||||
|
||||
|
||||
class ShmupEnemy(Character):
|
||||
state_changes_art = False
|
||||
move_state = 'stand'
|
||||
move_state = "stand"
|
||||
should_save = False
|
||||
invincible = False # DEBUG
|
||||
serialized = Character.serialized + ['invincible']
|
||||
|
||||
invincible = False # DEBUG
|
||||
serialized = Character.serialized + ["invincible"]
|
||||
|
||||
def started_colliding(self, other):
|
||||
if isinstance(other, ShmupPlayer):
|
||||
other.die(self)
|
||||
|
||||
|
||||
def fire_proj(self):
|
||||
proj = ShmupEnemyProjectile(self.world)
|
||||
# fire downward
|
||||
proj.fire(self, 0, -1)
|
||||
|
||||
|
||||
def update(self):
|
||||
self.move(0, -1)
|
||||
Character.update(self)
|
||||
|
||||
|
||||
class Enemy1(ShmupEnemy):
|
||||
art_src = 'enemy1'
|
||||
art_src = "enemy1"
|
||||
move_accel_y = 100
|
||||
|
||||
|
||||
def update(self):
|
||||
# sine wave motion in X
|
||||
time = self.world.get_elapsed_time()
|
||||
|
|
@ -146,16 +158,17 @@ class Enemy1(ShmupEnemy):
|
|||
self.fire_proj()
|
||||
ShmupEnemy.update(self)
|
||||
|
||||
|
||||
class Enemy2(ShmupEnemy):
|
||||
art_src = 'enemy2'
|
||||
art_src = "enemy2"
|
||||
animating = True
|
||||
move_accel_y = 50
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
ShmupEnemy.pre_first_update(self)
|
||||
# pick random lateral movement goal
|
||||
self.goal_x, y = self.spawner.get_spawn_location()
|
||||
|
||||
|
||||
def update(self):
|
||||
# move to random goal X
|
||||
dx = self.goal_x - self.x
|
||||
|
|
@ -171,16 +184,20 @@ class Enemy2(ShmupEnemy):
|
|||
self.fire_proj()
|
||||
ShmupEnemy.update(self)
|
||||
|
||||
|
||||
class Asteroid(ShmupEnemy):
|
||||
"totally inert, just moves slowly down the screen"
|
||||
art_src = 'asteroid'
|
||||
|
||||
art_src = "asteroid"
|
||||
move_accel_y = 200
|
||||
|
||||
|
||||
class ShmupPlayerProjectile(Projectile):
|
||||
animating = True
|
||||
art_src = 'player_proj'
|
||||
art_src = "player_proj"
|
||||
use_art_instance = True
|
||||
noncolliding_classes = Projectile.noncolliding_classes + ['Boom', 'Player']
|
||||
noncolliding_classes = Projectile.noncolliding_classes + ["Boom", "Player"]
|
||||
|
||||
def started_colliding(self, other):
|
||||
if isinstance(other, ShmupEnemy) and not other.invincible:
|
||||
boom = Boom(self.world)
|
||||
|
|
@ -189,48 +206,52 @@ class ShmupPlayerProjectile(Projectile):
|
|||
other.destroy()
|
||||
self.destroy()
|
||||
|
||||
|
||||
class ShmupEnemyProjectile(Projectile):
|
||||
animating = True
|
||||
art_src = 'enemy_proj'
|
||||
art_src = "enemy_proj"
|
||||
use_art_instance = True
|
||||
noncolliding_classes = Projectile.noncolliding_classes + ['Boom', 'ShmupEnemy']
|
||||
noncolliding_classes = Projectile.noncolliding_classes + ["Boom", "ShmupEnemy"]
|
||||
|
||||
def started_colliding(self, other):
|
||||
if isinstance(other, ShmupPlayer) and other.state != 'dead':
|
||||
if isinstance(other, ShmupPlayer) and other.state != "dead":
|
||||
other.die(self)
|
||||
self.destroy()
|
||||
|
||||
|
||||
class Boom(GameObject):
|
||||
art_src = 'boom'
|
||||
art_src = "boom"
|
||||
animating = True
|
||||
use_art_instance = True
|
||||
should_save = False
|
||||
z = 0.5
|
||||
scale_x, scale_y = 3, 3
|
||||
lifespan = 0.5
|
||||
|
||||
def get_acceleration(self, vel_x, vel_y, vel_z):
|
||||
return 0, 0, -100
|
||||
|
||||
|
||||
class Starfield(GameObject):
|
||||
|
||||
"scrolling background with stars generated on-the-fly - no PSCI file!"
|
||||
|
||||
|
||||
generate_art = True
|
||||
art_width, art_height = 30, 41
|
||||
art_charset = 'jpetscii'
|
||||
alpha = 0.25 # NOTE: this will be overriden by saved instance because it's in the list of serialized properties
|
||||
art_charset = "jpetscii"
|
||||
alpha = 0.25 # NOTE: this will be overriden by saved instance because it's in the list of serialized properties
|
||||
# indices of star characters
|
||||
star_chars = [201]
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
self.art.clear_frame_layer(0, 0)
|
||||
|
||||
|
||||
def create_star(self):
|
||||
"create a star at a random point along the top edge"
|
||||
x = int(random.random() * self.art_width)
|
||||
char = random.choice(self.star_chars)
|
||||
color = self.art.palette.get_random_color_index()
|
||||
self.art.set_tile_at(0, 0, x, 0, char, color)
|
||||
|
||||
|
||||
def update(self):
|
||||
# maybe create a star at the top, clear bottom line, then shift-wrap
|
||||
if random.random() < 0.25:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import random
|
||||
import time
|
||||
|
||||
import time, random
|
||||
|
||||
from art import ART_DIR, UV_FLIPX, UV_FLIPY, UV_ROTATE180
|
||||
from game_object import GameObject
|
||||
from art import UV_FLIPX, UV_FLIPY, UV_ROTATE180, ART_DIR
|
||||
from renderable import TileRenderable
|
||||
|
||||
from games.wildflowers.scripts.ramps import PALETTE_RAMPS
|
||||
from games.wildflowers.scripts.petal import Petal
|
||||
from games.wildflowers.scripts.frond import Frond
|
||||
|
||||
from games.wildflowers.scripts.petal import Petal
|
||||
from games.wildflowers.scripts.ramps import PALETTE_RAMPS
|
||||
from renderable import TileRenderable
|
||||
|
||||
# TODO: random size range?
|
||||
# (should also change camera zoom, probably frond/petal counts)
|
||||
|
|
@ -16,19 +14,18 @@ FLOWER_WIDTH, FLOWER_HEIGHT = 16, 16
|
|||
|
||||
|
||||
class FlowerObject(GameObject):
|
||||
|
||||
generate_art = True
|
||||
should_save = False
|
||||
physics_move = False
|
||||
art_width, art_height = FLOWER_WIDTH, FLOWER_HEIGHT
|
||||
|
||||
|
||||
min_petals, max_petals = 0, 4
|
||||
min_fronds, max_fronds = 0, 8
|
||||
# every flower must have at least this many petals + fronds
|
||||
minimum_complexity = 4
|
||||
# app updates per grow update; 1 = grow every frame
|
||||
ticks_per_grow = 4
|
||||
|
||||
|
||||
# DEBUG: if True, add current time to date seed as a decimal,
|
||||
# to test with highly specific values
|
||||
# (note: this turns the seed from an int into a float)
|
||||
|
|
@ -36,13 +33,13 @@ class FlowerObject(GameObject):
|
|||
# DEBUG: if nonzero, use this seed for testing
|
||||
debug_seed = 0
|
||||
debug_log = False
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
# set random seed based on date, a different flower each day
|
||||
t = time.localtime()
|
||||
year, month, day = t.tm_year, t.tm_mon, t.tm_mday
|
||||
weekday = t.tm_wday # 0 = monday
|
||||
weekday = t.tm_wday # 0 = monday
|
||||
date = year * 10000 + month * 100 + day
|
||||
if self.seed_includes_time:
|
||||
date += t.tm_hour * 0.01 + t.tm_min * 0.0001 + t.tm_sec * 0.000001
|
||||
|
|
@ -54,21 +51,23 @@ class FlowerObject(GameObject):
|
|||
# pick a random dark BG color (will be quantized to palette)
|
||||
r, g, b = random.random() / 10, random.random() / 10, random.random() / 10
|
||||
# set up art with character set, size, and a random (supported) palette
|
||||
self.art.set_charset_by_name('jpetscii')
|
||||
self.art.set_charset_by_name("jpetscii")
|
||||
palette = random.choice(list(PALETTE_RAMPS.keys()))
|
||||
self.art.set_palette_by_name(palette)
|
||||
# quantize bg color and set it for art and world
|
||||
self.bg_index = self.art.palette.get_closest_color_index(int(r * 255), int(g * 255), int(b * 255))
|
||||
self.bg_index = self.art.palette.get_closest_color_index(
|
||||
int(r * 255), int(g * 255), int(b * 255)
|
||||
)
|
||||
bg_color = self.art.palette.colors[self.bg_index]
|
||||
self.world.bg_color[0] = bg_color[0] / 255.0
|
||||
self.world.bg_color[1] = bg_color[1] / 255.0
|
||||
self.world.bg_color[2] = bg_color[2] / 255.0
|
||||
self.world.bg_color[3] = 1.0 # set here or alpha is zero?
|
||||
self.world.bg_color[3] = 1.0 # set here or alpha is zero?
|
||||
self.art.resize(self.art_width, self.art_height)
|
||||
self.app.ui.adjust_for_art_resize(self) # grid etc
|
||||
self.app.ui.adjust_for_art_resize(self) # grid etc
|
||||
self.art.clear_frame_layer(0, 0, bg_color=self.bg_index)
|
||||
# petals on a layer underneath fronds?
|
||||
#self.art.add_layer(z=-0.001, name='petals')
|
||||
# self.art.add_layer(z=-0.001, name='petals')
|
||||
self.finished_growing = False
|
||||
# some flowers can be more petal-centric or frond-centric,
|
||||
# but keep a certain minimum complexity
|
||||
|
|
@ -78,29 +77,36 @@ class FlowerObject(GameObject):
|
|||
petal_count = random.randint(self.min_petals, self.max_petals)
|
||||
frond_count = random.randint(self.min_fronds, self.max_fronds)
|
||||
self.petals = []
|
||||
#petal_count = 5 # DEBUG
|
||||
# petal_count = 5 # DEBUG
|
||||
for i in range(petal_count):
|
||||
self.petals.append(Petal(self, i))
|
||||
# sort petals by radius largest to smallest,
|
||||
# so big ones don't totally stomp smaller ones
|
||||
self.petals.sort(key=lambda item: item.goal_radius, reverse=True)
|
||||
self.fronds = []
|
||||
#frond_count = 0 # DEBUG
|
||||
# frond_count = 0 # DEBUG
|
||||
for i in range(frond_count):
|
||||
self.fronds.append(Frond(self, i))
|
||||
# track # of growth updates we've had
|
||||
self.grows = 0
|
||||
# create an art document we can add frames to and later export
|
||||
self.export_filename = '%s%swildflower_%s' % (self.app.documents_dir, ART_DIR, self.seed)
|
||||
self.exportable_art = self.app.new_art(self.export_filename,
|
||||
self.art_width, self.art_height,
|
||||
self.art.charset.name,
|
||||
self.art.palette.name)
|
||||
self.export_filename = "%s%swildflower_%s" % (
|
||||
self.app.documents_dir,
|
||||
ART_DIR,
|
||||
self.seed,
|
||||
)
|
||||
self.exportable_art = self.app.new_art(
|
||||
self.export_filename,
|
||||
self.art_width,
|
||||
self.art_height,
|
||||
self.art.charset.name,
|
||||
self.art.palette.name,
|
||||
)
|
||||
# re-set art's filename to be in documents dir rather than game dir :/
|
||||
self.exportable_art.set_filename(self.export_filename)
|
||||
# image export process needs a renderable
|
||||
r = TileRenderable(self.app, self.exportable_art)
|
||||
|
||||
|
||||
def update(self):
|
||||
GameObject.update(self)
|
||||
# grow only every few ticks, so you can watch the design grow
|
||||
|
|
@ -108,10 +114,10 @@ class FlowerObject(GameObject):
|
|||
return
|
||||
if not self.finished_growing:
|
||||
self.update_growth()
|
||||
|
||||
|
||||
def update_growth(self):
|
||||
if self.debug_log:
|
||||
print('update growth:')
|
||||
print("update growth:")
|
||||
grew = False
|
||||
for p in self.petals:
|
||||
if not p.finished_growing:
|
||||
|
|
@ -136,8 +142,8 @@ class FlowerObject(GameObject):
|
|||
self.finished_growing = True
|
||||
self.exportable_art.set_active_frame(self.exportable_art.frames - 1)
|
||||
if self.debug_log:
|
||||
print('flower finished')
|
||||
|
||||
print("flower finished")
|
||||
|
||||
def paint_mirrored(self, layer, x, y, char, fg, bg=None):
|
||||
# only paint if in top left quadrant
|
||||
if x > (self.art_width / 2) - 1 or y > (self.art_height / 2) - 1:
|
||||
|
|
@ -148,13 +154,12 @@ class FlowerObject(GameObject):
|
|||
top_right = (self.art_width - 1 - x, y)
|
||||
bottom_left = (x, self.art_height - 1 - y)
|
||||
bottom_right = (self.art_width - 1 - x, self.art_height - 1 - y)
|
||||
self.art.set_tile_at(0, layer, *top_right,
|
||||
char, fg, bg, transform=UV_FLIPX)
|
||||
self.art.set_tile_at(0, layer, *bottom_left,
|
||||
char, fg, bg, transform=UV_FLIPY)
|
||||
self.art.set_tile_at(0, layer, *bottom_right,
|
||||
char, fg, bg, transform=UV_ROTATE180)
|
||||
|
||||
self.art.set_tile_at(0, layer, *top_right, char, fg, bg, transform=UV_FLIPX)
|
||||
self.art.set_tile_at(0, layer, *bottom_left, char, fg, bg, transform=UV_FLIPY)
|
||||
self.art.set_tile_at(
|
||||
0, layer, *bottom_right, char, fg, bg, transform=UV_ROTATE180
|
||||
)
|
||||
|
||||
def copy_new_frame(self):
|
||||
# add new frame to art for export
|
||||
# (art starts with 1 frame, only do this after first frame written)
|
||||
|
|
|
|||
|
|
@ -1,51 +1,57 @@
|
|||
|
||||
import random
|
||||
|
||||
from games.wildflowers.scripts.ramps import RampIterator
|
||||
|
||||
|
||||
# growth direction consts
|
||||
NONE = (0, 0)
|
||||
LEFT = (-1, 0)
|
||||
LEFT_UP = (-1, -1)
|
||||
UP = (0, -1)
|
||||
RIGHT_UP = (1, -1)
|
||||
RIGHT = (1, 0)
|
||||
NONE = (0, 0)
|
||||
LEFT = (-1, 0)
|
||||
LEFT_UP = (-1, -1)
|
||||
UP = (0, -1)
|
||||
RIGHT_UP = (1, -1)
|
||||
RIGHT = (1, 0)
|
||||
RIGHT_DOWN = (1, 1)
|
||||
DOWN = (0, 1)
|
||||
LEFT_DOWN = (-1, 1)
|
||||
DOWN = (0, 1)
|
||||
LEFT_DOWN = (-1, 1)
|
||||
DIRS = [LEFT, LEFT_UP, UP, RIGHT_UP, RIGHT, RIGHT_DOWN, DOWN, LEFT_DOWN]
|
||||
|
||||
FROND_CHARS = [
|
||||
# thick and skinny \
|
||||
151, 166,
|
||||
151,
|
||||
166,
|
||||
# thick and skinny /
|
||||
150, 167,
|
||||
150,
|
||||
167,
|
||||
# thick and skinny X
|
||||
183, 182,
|
||||
183,
|
||||
182,
|
||||
# solid inward wedges, NW NE SE SW
|
||||
148, 149, 164, 165
|
||||
148,
|
||||
149,
|
||||
164,
|
||||
165,
|
||||
]
|
||||
|
||||
|
||||
class Frond:
|
||||
|
||||
min_life, max_life = 3, 16
|
||||
random_char_chance = 0.5
|
||||
mutate_char_chance = 0.2
|
||||
# layer all fronds should paint on
|
||||
layer = 0
|
||||
debug = False
|
||||
|
||||
|
||||
def __init__(self, flower, index):
|
||||
self.flower = flower
|
||||
self.index = index
|
||||
self.finished_growing = False
|
||||
# choose growth function
|
||||
self.growth_functions = [self.grow_straight_line, self.grow_curl,
|
||||
self.grow_wander_outward]
|
||||
self.growth_functions = [
|
||||
self.grow_straight_line,
|
||||
self.grow_curl,
|
||||
self.grow_wander_outward,
|
||||
]
|
||||
self.get_grow_dir = random.choice(self.growth_functions)
|
||||
#self.get_grow_dir = self.grow_curl # DEBUG
|
||||
# self.get_grow_dir = self.grow_curl # DEBUG
|
||||
# for straight line growers, set a consistent direction
|
||||
if self.get_grow_dir == self.grow_straight_line:
|
||||
self.grow_line = random.choice(DIRS)
|
||||
|
|
@ -65,7 +71,7 @@ class Frond:
|
|||
else:
|
||||
self.char = random.randint(1, 255)
|
||||
# first grow() will paint first character
|
||||
|
||||
|
||||
def grow(self):
|
||||
"""
|
||||
grows this frond by another tile
|
||||
|
|
@ -75,16 +81,22 @@ class Frond:
|
|||
if self.life <= 0 or self.color == self.ramp.end:
|
||||
self.finished_growing = True
|
||||
if self.debug:
|
||||
print(' frond %i finished.' % self.index)
|
||||
print(" frond %i finished." % self.index)
|
||||
return painted
|
||||
if self.debug:
|
||||
print(' frond %i at (%i, %i) using %s' % (self.index, self.x, self.y, self.get_grow_dir.__name__))
|
||||
print(
|
||||
" frond %i at (%i, %i) using %s"
|
||||
% (self.index, self.x, self.y, self.get_grow_dir.__name__)
|
||||
)
|
||||
# if we're out of bounds, simply don't paint;
|
||||
# we might go back in bounds next grow
|
||||
if 0 <= self.x < self.flower.art_width - 1 and \
|
||||
0 <= self.y < self.flower.art_height - 1:
|
||||
self.flower.paint_mirrored(self.layer, self.x, self.y,
|
||||
self.char, self.color)
|
||||
if (
|
||||
0 <= self.x < self.flower.art_width - 1
|
||||
and 0 <= self.y < self.flower.art_height - 1
|
||||
):
|
||||
self.flower.paint_mirrored(
|
||||
self.layer, self.x, self.y, self.char, self.color
|
||||
)
|
||||
painted = True
|
||||
self.growth_history.append((self.x, self.y))
|
||||
self.life -= 1
|
||||
|
|
@ -104,16 +116,16 @@ class Frond:
|
|||
grow_x, grow_y = self.get_grow_dir((last_x, last_y))
|
||||
self.x, self.y = self.x + grow_x, self.y + grow_y
|
||||
return painted
|
||||
|
||||
|
||||
# paint and growth functions work in top left quadrant, then mirrored
|
||||
|
||||
|
||||
def grow_straight_line(self, last_dir):
|
||||
return self.grow_line
|
||||
|
||||
|
||||
def grow_wander_outward(self, last_dir):
|
||||
# (original prototype growth algo)
|
||||
return random.choice([LEFT_UP, LEFT, UP])
|
||||
|
||||
|
||||
def grow_curl(self, last_dir):
|
||||
if last_dir == NONE:
|
||||
return random.choice([LEFT, LEFT_UP, UP])
|
||||
|
|
|
|||
|
|
@ -1,33 +1,41 @@
|
|||
|
||||
import random, math
|
||||
import math
|
||||
import random
|
||||
|
||||
from games.wildflowers.scripts.ramps import RampIterator
|
||||
|
||||
|
||||
PETAL_CHARS = [
|
||||
# solid block
|
||||
255,
|
||||
# shaded boxes
|
||||
254, 253,
|
||||
254,
|
||||
253,
|
||||
# solid circle
|
||||
122,
|
||||
# curved corner lines, NW NE SE SW
|
||||
105, 107, 139, 137,
|
||||
105,
|
||||
107,
|
||||
139,
|
||||
137,
|
||||
# mostly-solid curved corners, NW NE SE SW
|
||||
144, 146, 178, 176,
|
||||
144,
|
||||
146,
|
||||
178,
|
||||
176,
|
||||
# solid inward wedges, NW NE SE SW
|
||||
148, 149, 164, 165
|
||||
148,
|
||||
149,
|
||||
164,
|
||||
165,
|
||||
]
|
||||
|
||||
|
||||
class Petal:
|
||||
|
||||
min_radius = 3
|
||||
mutate_char_chance = 0.2
|
||||
# layer all petals should paint on
|
||||
layer = 0
|
||||
debug = False
|
||||
|
||||
|
||||
def __init__(self, flower, index):
|
||||
self.flower = flower
|
||||
self.index = index
|
||||
|
|
@ -36,8 +44,12 @@ class Petal:
|
|||
max_radius = int(self.flower.art_width / 2)
|
||||
self.goal_radius = random.randint(self.min_radius, max_radius)
|
||||
self.radius = 0
|
||||
ring_styles = [self.get_ring_tiles_box, self.get_ring_tiles_wings,
|
||||
self.get_ring_tiles_diamond, self.get_ring_tiles_circle]
|
||||
ring_styles = [
|
||||
self.get_ring_tiles_box,
|
||||
self.get_ring_tiles_wings,
|
||||
self.get_ring_tiles_diamond,
|
||||
self.get_ring_tiles_circle,
|
||||
]
|
||||
self.get_ring_tiles = random.choice(ring_styles)
|
||||
# pick a starting point near center
|
||||
w, h = self.flower.art_width, self.flower.art_height
|
||||
|
|
@ -48,14 +60,23 @@ class Petal:
|
|||
self.color = self.ramp.color
|
||||
# random char from predefined list
|
||||
self.char = random.choice(PETAL_CHARS)
|
||||
|
||||
|
||||
def grow(self):
|
||||
# grow outward (up and left) from center in "rings"
|
||||
if self.radius >= self.goal_radius:
|
||||
self.finished_growing = True
|
||||
return
|
||||
if self.debug:
|
||||
print(' petal %i at (%i, %i) at radius %i using %s' % (self.index, self.x, self.y, self.radius, self.get_ring_tiles.__name__))
|
||||
print(
|
||||
" petal %i at (%i, %i) at radius %i using %s"
|
||||
% (
|
||||
self.index,
|
||||
self.x,
|
||||
self.y,
|
||||
self.radius,
|
||||
self.get_ring_tiles.__name__,
|
||||
)
|
||||
)
|
||||
self.paint_ring()
|
||||
# grow and change
|
||||
self.radius += 1
|
||||
|
|
@ -63,19 +84,20 @@ class Petal:
|
|||
# roll against chaos to mutate character
|
||||
if random.random() < self.chaos * self.mutate_char_chance:
|
||||
self.char = random.choice(PETAL_CHARS)
|
||||
|
||||
|
||||
def paint_ring(self):
|
||||
tiles = self.get_ring_tiles()
|
||||
for t in tiles:
|
||||
x = self.x - t[0]
|
||||
y = self.y - t[1]
|
||||
# don't paint out of bounds
|
||||
if 0 <= x < self.flower.art_width - 1 and \
|
||||
0 <= y < self.flower.art_height - 1:
|
||||
self.flower.paint_mirrored(self.layer, x, y,
|
||||
self.char, self.color)
|
||||
#print('%s, %s' % (x, y))
|
||||
|
||||
if (
|
||||
0 <= x < self.flower.art_width - 1
|
||||
and 0 <= y < self.flower.art_height - 1
|
||||
):
|
||||
self.flower.paint_mirrored(self.layer, x, y, self.char, self.color)
|
||||
# print('%s, %s' % (x, y))
|
||||
|
||||
def get_ring_tiles_box(self):
|
||||
tiles = []
|
||||
for x in range(self.radius + 1):
|
||||
|
|
@ -83,7 +105,7 @@ class Petal:
|
|||
for y in range(self.radius + 1):
|
||||
tiles.append((self.radius, y))
|
||||
return tiles
|
||||
|
||||
|
||||
def get_ring_tiles_dealieX(self):
|
||||
# not sure what to call this but it's a nice shape
|
||||
tiles = []
|
||||
|
|
@ -91,7 +113,7 @@ class Petal:
|
|||
for x in range(self.radius):
|
||||
tiles.append((x - self.radius, y - self.radius))
|
||||
return tiles
|
||||
|
||||
|
||||
def get_ring_tiles_wings(self):
|
||||
# not sure what to call this but it's a nice shape
|
||||
tiles = []
|
||||
|
|
@ -103,7 +125,6 @@ class Petal:
|
|||
tiles.append((x, y))
|
||||
return tiles
|
||||
|
||||
|
||||
def get_ring_tiles_diamond(self):
|
||||
tiles = []
|
||||
for y in range(self.radius, -1, -1):
|
||||
|
|
@ -111,7 +132,7 @@ class Petal:
|
|||
if x + y == self.radius:
|
||||
tiles.append((x, y))
|
||||
return tiles
|
||||
|
||||
|
||||
def get_ring_tiles_circle(self):
|
||||
tiles = []
|
||||
angle = 0
|
||||
|
|
@ -120,6 +141,6 @@ class Petal:
|
|||
angle += math.radians(90.0 / resolution)
|
||||
x = round(math.cos(angle) * self.radius)
|
||||
y = round(math.sin(angle) * self.radius)
|
||||
if not (x, y) in tiles:
|
||||
if (x, y) not in tiles:
|
||||
tiles.append((x, y))
|
||||
return tiles
|
||||
|
|
|
|||
|
|
@ -1,53 +1,52 @@
|
|||
|
||||
import random
|
||||
|
||||
# wildflowers palette ramp definitions
|
||||
|
||||
PALETTE_RAMPS = {
|
||||
# palette name : list of its ramps
|
||||
'dpaint': [
|
||||
"dpaint": [
|
||||
# ramp tuple: (start index, length, stride)
|
||||
# generally, lighter / more vivid to darker
|
||||
(17, 16, 1), # white to black
|
||||
(33, 16, 1), # red to black
|
||||
(49, 8, 1), # white to red
|
||||
(57, 8, 1), # light orange to dark orange
|
||||
(49, 8, 1), # white to red
|
||||
(57, 8, 1), # light orange to dark orange
|
||||
(65, 16, 1), # light yellow to ~black
|
||||
(81, 8, 1), # light green to green
|
||||
(81, 8, 1), # light green to green
|
||||
(89, 24, 1), # white to green to ~black
|
||||
(113, 16, 1), # light cyan to ~black
|
||||
(113, 16, 1), # light cyan to ~black
|
||||
(129, 8, 1), # light blue to blue
|
||||
(137, 24, 1), # white to blue to ~black
|
||||
(161, 16, 1), # light purple to ~black
|
||||
(177, 16, 1), # light magenta to ~black
|
||||
(193, 24, 1), # pale flesh to ~black
|
||||
(225, 22, 1) # ROYGBV rainbow
|
||||
(137, 24, 1), # white to blue to ~black
|
||||
(161, 16, 1), # light purple to ~black
|
||||
(177, 16, 1), # light magenta to ~black
|
||||
(193, 24, 1), # pale flesh to ~black
|
||||
(225, 22, 1), # ROYGBV rainbow
|
||||
],
|
||||
'doom': [
|
||||
"doom": [
|
||||
(17, 27, 1), # very light pink to dark red
|
||||
(44, 20, 1), # pale flesh to brown
|
||||
(69, 26, 1), # white to very dark grey
|
||||
(95, 14, 1), # bright green to ~black
|
||||
(109, 12, 1), # light tan to dark tan
|
||||
(109, 12, 1), # light tan to dark tan
|
||||
(126, 4, 1), # olive drab
|
||||
(130, 7, 1), # light gold to gold brown
|
||||
(137, 18, 1), # white to dark red
|
||||
(155, 14, 1), # white to dark blue
|
||||
(169, 11, 1), # white to orange
|
||||
(137, 18, 1), # white to dark red
|
||||
(155, 14, 1), # white to dark blue
|
||||
(169, 11, 1), # white to orange
|
||||
(180, 7, 1), # white to yellow
|
||||
(187, 4, 1), # orange to burnt orange
|
||||
(193, 7, 1), # dark blue to black
|
||||
(201, 5, 1) # light magenta to dark purple
|
||||
(201, 5, 1), # light magenta to dark purple
|
||||
],
|
||||
'quake': [
|
||||
"quake": [
|
||||
(16, 15, -1), # white to black
|
||||
(32, 16, -1), # mustard to black
|
||||
(48, 16, -1), # lavender to black
|
||||
(63, 15, -1), # olive to black
|
||||
(79, 16, -1), # red to black
|
||||
(92, 13, -1), # orange to ~black
|
||||
(108, 16, -1), # yellow to orange to ~black
|
||||
(124, 16, -1), # pale flesh to ~black
|
||||
(108, 16, -1), # yellow to orange to ~black
|
||||
(124, 16, -1), # pale flesh to ~black
|
||||
(125, 16, 1), # light purple to ~black
|
||||
(141, 13, 1), # purpleish pink to ~black
|
||||
(154, 15, 1), # light tan to ~black
|
||||
|
|
@ -57,48 +56,47 @@ PALETTE_RAMPS = {
|
|||
(233, 4, -1), # yellow to brown
|
||||
(236, 3, -1), # light blue to blue
|
||||
(240, 4, -1), # red to dark red
|
||||
(243, 3, -1) # white to yellow
|
||||
(243, 3, -1), # white to yellow
|
||||
],
|
||||
'heretic': [
|
||||
"heretic": [
|
||||
(35, 35, -1), # white to black
|
||||
(51, 16, -1), # light grey to dark grey
|
||||
(65, 14, -1), # white to dark violent-grey
|
||||
(94, 29, -1), # white to dark brown
|
||||
(110, 16, -1), # light tan to brown
|
||||
(136, 26, -1), # light yellow to dark golden brown
|
||||
(110, 16, -1), # light tan to brown
|
||||
(136, 26, -1), # light yellow to dark golden brown
|
||||
(144, 8, -1), # yellow to orange
|
||||
(160, 16, -1), # red to dark red
|
||||
(160, 16, -1), # red to dark red
|
||||
(168, 8, -1), # white to pink
|
||||
(176, 8, -1), # light magenta to dark magenta
|
||||
(184, 8, -1), # white to purple
|
||||
(208, 24, -1), # white to cyan to dark blue
|
||||
(224, 16, -1), # light green to dark green
|
||||
(240, 16, -1), # olive to dark olive
|
||||
(247, 7, -1) # red to yellow
|
||||
(208, 24, -1), # white to cyan to dark blue
|
||||
(224, 16, -1), # light green to dark green
|
||||
(240, 16, -1), # olive to dark olive
|
||||
(247, 7, -1), # red to yellow
|
||||
],
|
||||
"atari": [
|
||||
(113, 8, -16), # white to black
|
||||
(114, 8, -16), # yellow to muddy brown
|
||||
(115, 8, -16), # dull gold to brown
|
||||
(116, 8, -16), # peach to burnt orange
|
||||
(117, 8, -16), # pink to red
|
||||
(118, 8, -16), # magenta to dark magenta
|
||||
(119, 8, -16), # purple to dark purple
|
||||
(120, 8, -16), # violet to dark violet
|
||||
(121, 8, -16), # light blue to dark blue
|
||||
(122, 8, -16), # light cobalt to dark cobalt
|
||||
(123, 8, -16), # light teal to dark teal
|
||||
(124, 8, -16), # light sea green to dark sea green
|
||||
(125, 8, -16), # light green to dark green
|
||||
(126, 8, -16), # yellow green to dark yellow green
|
||||
(127, 8, -16), # pale yellow to dark olive
|
||||
(128, 8, -16), # gold to golden brown
|
||||
],
|
||||
'atari': [
|
||||
(113, 8, -16), # white to black
|
||||
(114, 8, -16), # yellow to muddy brown
|
||||
(115, 8, -16), # dull gold to brown
|
||||
(116, 8, -16), # peach to burnt orange
|
||||
(117, 8, -16), # pink to red
|
||||
(118, 8, -16), # magenta to dark magenta
|
||||
(119, 8, -16), # purple to dark purple
|
||||
(120, 8, -16), # violet to dark violet
|
||||
(121, 8, -16), # light blue to dark blue
|
||||
(122, 8, -16), # light cobalt to dark cobalt
|
||||
(123, 8, -16), # light teal to dark teal
|
||||
(124, 8, -16), # light sea green to dark sea green
|
||||
(125, 8, -16), # light green to dark green
|
||||
(126, 8, -16), # yellow green to dark yellow green
|
||||
(127, 8, -16), # pale yellow to dark olive
|
||||
(128, 8, -16) # gold to golden brown
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class RampIterator:
|
||||
|
||||
def __init__(self, flower):
|
||||
ramp_def = random.choice(PALETTE_RAMPS[flower.art.palette.name])
|
||||
self.start, self.length, self.stride = ramp_def
|
||||
|
|
@ -106,7 +104,7 @@ class RampIterator:
|
|||
# determine starting color, somewhere along ramp
|
||||
self.start_step = random.randint(0, self.length - 1)
|
||||
self.color = self.start + (self.start_step * self.stride)
|
||||
|
||||
|
||||
def go_to_next_color(self):
|
||||
self.color += self.stride
|
||||
return self.color
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
from game_util_objects import WorldGlobalsObject, GameObject
|
||||
from image_export import export_animation, export_still_image
|
||||
|
||||
from games.wildflowers.scripts.flower import FlowerObject
|
||||
from game_util_objects import GameObject, WorldGlobalsObject
|
||||
from image_export import export_still_image
|
||||
|
||||
"""
|
||||
overall approach:
|
||||
|
|
@ -22,63 +19,65 @@ character ramps based on direction changes, visual density, something else?
|
|||
|
||||
|
||||
class FlowerGlobals(WorldGlobalsObject):
|
||||
|
||||
# if True, generate a 4x4 grid instead of just one
|
||||
test_gen = False
|
||||
handle_key_events = True
|
||||
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
WorldGlobalsObject.__init__(self, world, obj_data)
|
||||
|
||||
|
||||
def pre_first_update(self):
|
||||
#self.app.can_edit = False
|
||||
# self.app.can_edit = False
|
||||
self.app.ui.set_game_edit_ui_visibility(False)
|
||||
self.app.ui.message_line.post_line('')
|
||||
self.app.ui.message_line.post_line("")
|
||||
if self.test_gen:
|
||||
for x in range(4):
|
||||
for y in range(4):
|
||||
flower = self.world.spawn_object_of_class('FlowerObject')
|
||||
flower = self.world.spawn_object_of_class("FlowerObject")
|
||||
flower.set_loc(x * flower.art.width, y * flower.art.height)
|
||||
self.world.camera.set_loc(25, 25, 35)
|
||||
else:
|
||||
flower = self.world.spawn_object_of_class('FlowerObject')
|
||||
flower = self.world.spawn_object_of_class("FlowerObject")
|
||||
self.world.camera.set_loc(0, 0, 10)
|
||||
self.flower = flower
|
||||
self.world.spawn_object_of_class('SeedDisplay')
|
||||
|
||||
self.world.spawn_object_of_class("SeedDisplay")
|
||||
|
||||
def handle_key_down(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
if key != 'e':
|
||||
if key != "e":
|
||||
return
|
||||
if not self.flower:
|
||||
return
|
||||
|
||||
|
||||
# save to .psci
|
||||
# hold on last frame
|
||||
self.flower.exportable_art.frame_delays[-1] = 6.0
|
||||
self.flower.exportable_art.save_to_file()
|
||||
# TODO: investigate why opening for edit puts art mode in a bad state
|
||||
#self.app.load_art_for_edit(self.flower.exportable_art.filename)
|
||||
|
||||
# self.app.load_art_for_edit(self.flower.exportable_art.filename)
|
||||
|
||||
# save to .gif - TODO investigate problem with frame deltas not clearing
|
||||
#export_animation(self.app, self.flower.exportable_art,
|
||||
# export_animation(self.app, self.flower.exportable_art,
|
||||
# self.flower.export_filename + '.gif',
|
||||
# bg_color=self.world.bg_color, loop=False)
|
||||
|
||||
|
||||
# export to .png - works
|
||||
export_still_image(self.app, self.flower.exportable_art,
|
||||
self.flower.export_filename + '.png',
|
||||
crt=self.app.fb.crt, scale=4,
|
||||
bg_color=self.world.bg_color)
|
||||
self.app.log('Exported %s.png' % self.flower.export_filename)
|
||||
export_still_image(
|
||||
self.app,
|
||||
self.flower.exportable_art,
|
||||
self.flower.export_filename + ".png",
|
||||
crt=self.app.fb.crt,
|
||||
scale=4,
|
||||
bg_color=self.world.bg_color,
|
||||
)
|
||||
self.app.log("Exported %s.png" % self.flower.export_filename)
|
||||
|
||||
|
||||
class SeedDisplay(GameObject):
|
||||
|
||||
generate_art = True
|
||||
art_width, art_height = 30, 1
|
||||
art_charset = 'ui'
|
||||
art_palette = 'c64_original'
|
||||
|
||||
art_charset = "ui"
|
||||
art_palette = "c64_original"
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
GameObject.__init__(self, world, obj_data)
|
||||
self.art.clear_frame_layer(0, 0)
|
||||
|
|
|
|||
38
grid.py
38
grid.py
|
|
@ -8,15 +8,15 @@ AXIS_COLOR = (0.8, 0.8, 0.8, 0.5)
|
|||
BASE_COLOR = (0.5, 0.5, 0.5, 0.25)
|
||||
EXTENTS_COLOR = (0, 0, 0, 1)
|
||||
|
||||
|
||||
class Grid(LineRenderable):
|
||||
|
||||
visible = True
|
||||
draw_axes = False
|
||||
|
||||
|
||||
def get_tile_size(self):
|
||||
"Returns (width, height) grid size in tiles."
|
||||
return 1, 1
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
"build vert, element, and color arrays"
|
||||
w, h = self.get_tile_size()
|
||||
|
|
@ -28,7 +28,7 @@ class Grid(LineRenderable):
|
|||
index = 4
|
||||
# axes - Y and X
|
||||
if self.draw_axes:
|
||||
v += [(w/2, -h), (w/2, 0), (0, -h/2), (w, -h/2)]
|
||||
v += [(w / 2, -h), (w / 2, 0), (0, -h / 2), (w, -h / 2)]
|
||||
e += [4, 5, 6, 7]
|
||||
color = AXIS_COLOR
|
||||
c += color * 4
|
||||
|
|
@ -37,69 +37,67 @@ class Grid(LineRenderable):
|
|||
color = BASE_COLOR
|
||||
for x in range(1, w):
|
||||
# skip middle line
|
||||
if not self.draw_axes or x != w/2:
|
||||
if not self.draw_axes or x != w / 2:
|
||||
v += [(x, -h), (x, 0)]
|
||||
e += [index, index+1]
|
||||
e += [index, index + 1]
|
||||
c += color * 2
|
||||
index += 2
|
||||
for y in range(1, h):
|
||||
if not self.draw_axes or y != h/2:
|
||||
if not self.draw_axes or y != h / 2:
|
||||
v += [(0, -y), (w, -y)]
|
||||
e += [index, index+1]
|
||||
e += [index, index + 1]
|
||||
c += color * 2
|
||||
index += 2
|
||||
self.vert_array = np.array(v, dtype=np.float32)
|
||||
self.elem_array = np.array(e, dtype=np.uint32)
|
||||
self.color_array = np.array(c, dtype=np.float32)
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.z = 0
|
||||
|
||||
|
||||
def reset(self):
|
||||
"macro for convenience - rescale, reposition, update renderable"
|
||||
self.build_geo()
|
||||
self.reset_loc()
|
||||
self.rebind_buffers()
|
||||
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.camera.projection_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.camera.view_matrix
|
||||
|
||||
|
||||
class ArtGrid(Grid):
|
||||
|
||||
def reset_loc(self):
|
||||
self.x, self.y = 0, 0
|
||||
self.z = self.app.ui.active_art.layers_z[self.app.ui.active_art.active_layer]
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.quad_size_ref = self.app.ui.active_art
|
||||
Grid.reset(self)
|
||||
|
||||
|
||||
def get_tile_size(self):
|
||||
return self.app.ui.active_art.width, self.app.ui.active_art.height
|
||||
|
||||
|
||||
class GameGrid(Grid):
|
||||
|
||||
draw_axes = True
|
||||
base_size = 800
|
||||
|
||||
|
||||
def get_tile_size(self):
|
||||
# TODO: dynamically adjust bounds based on furthest away objects?
|
||||
return self.base_size, self.base_size
|
||||
|
||||
|
||||
def set_base_size(self, new_size):
|
||||
self.base_size = new_size
|
||||
self.reset()
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
# center of grid at world zero
|
||||
qw, qh = self.get_quad_size()
|
||||
|
|
|
|||
108
image_convert.py
108
image_convert.py
|
|
@ -1,11 +1,12 @@
|
|||
import math
|
||||
import os.path
|
||||
import time
|
||||
|
||||
import math, os.path, time
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from PIL import Image, ImageChops, ImageStat
|
||||
|
||||
from lab_color import lab_color_diff, rgb_to_lab
|
||||
from renderable_sprite import SpriteRenderable
|
||||
from lab_color import rgb_to_lab, lab_color_diff
|
||||
|
||||
"""
|
||||
notes / future research
|
||||
|
|
@ -23,15 +24,17 @@ https://www.youtube.com/watch?v=L6CkYou6hYU
|
|||
- downsample each block bilinearly, divide each into 4x4 cells, then compare them with similarly bilinearly-downsampled char blocks
|
||||
"""
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
|
||||
tiles_per_tick = 1
|
||||
lab_color_comparison = True
|
||||
# delay in seconds before beginning to convert tiles.
|
||||
# lets eg UI catch up to BitmapImageImporter changes to Art.
|
||||
start_delay = 1.0
|
||||
|
||||
def __init__(self, app, image_filename, art, bicubic_scale=False, sequence_converter=None):
|
||||
|
||||
def __init__(
|
||||
self, app, image_filename, art, bicubic_scale=False, sequence_converter=None
|
||||
):
|
||||
self.init_success = False
|
||||
image_filename = app.find_filename_path(image_filename)
|
||||
if not image_filename or not os.path.exists(image_filename):
|
||||
|
|
@ -46,7 +49,7 @@ class ImageConverter:
|
|||
# if an ImageSequenceConverter created us, keep a handle to it
|
||||
self.sequence_converter = sequence_converter
|
||||
try:
|
||||
self.src_img = Image.open(self.image_filename).convert('RGB')
|
||||
self.src_img = Image.open(self.image_filename).convert("RGB")
|
||||
except:
|
||||
return
|
||||
# if we're part of a sequence, app doesn't need handle directly to us
|
||||
|
|
@ -70,21 +73,24 @@ class ImageConverter:
|
|||
self.src_array = np.reshape(self.src_array, (src_h, src_w))
|
||||
# convert charmap to 1-bit color for fast value swaps during
|
||||
# block comparison
|
||||
self.char_img = self.art.charset.image_data.copy().convert('RGB')
|
||||
bw_pal_img = Image.new('P', (1, 1))
|
||||
self.char_img = self.art.charset.image_data.copy().convert("RGB")
|
||||
bw_pal_img = Image.new("P", (1, 1))
|
||||
bw_pal = [0, 0, 0, 255, 255, 255]
|
||||
while len(bw_pal) < 256 * 3:
|
||||
bw_pal.append(0)
|
||||
bw_pal_img.putpalette(tuple(bw_pal))
|
||||
self.char_img = self.char_img.quantize(palette=bw_pal_img)
|
||||
self.char_array = np.fromstring(self.char_img.tobytes(), dtype=np.uint8)
|
||||
self.char_array = np.reshape(self.char_array, (self.art.charset.image_height, self.art.charset.image_width))
|
||||
self.char_array = np.reshape(
|
||||
self.char_array,
|
||||
(self.art.charset.image_height, self.art.charset.image_width),
|
||||
)
|
||||
# create, size and position image preview
|
||||
preview_img = self.src_img.copy()
|
||||
# remove transparency if source image is a GIF to avoid a PIL crash :[
|
||||
# TODO: https://github.com/python-pillow/Pillow/issues/1377
|
||||
if 'transparency' in preview_img.info:
|
||||
preview_img.info.pop('transparency')
|
||||
if "transparency" in preview_img.info:
|
||||
preview_img.info.pop("transparency")
|
||||
self.preview_sprite = SpriteRenderable(self.app, None, preview_img)
|
||||
# preview image scale takes into account character aspect
|
||||
self.preview_sprite.scale_x = w / (self.char_w / self.art.quad_width)
|
||||
|
|
@ -107,39 +113,45 @@ class ImageConverter:
|
|||
if len(self.char_blocks) > self.art.charset.last_index:
|
||||
break
|
||||
self.init_success = True
|
||||
|
||||
|
||||
def get_generated_color_diffs(self, colors):
|
||||
# build table of color diffs
|
||||
unique_colors = len(colors)
|
||||
color_diffs = np.zeros((unique_colors, unique_colors), dtype=np.float32)
|
||||
# option: L*a*b color space conversion for greater accuracy
|
||||
get_color_diff = self.get_lab_color_diff if self.lab_color_comparison else self.get_rgb_color_diff
|
||||
#get_color_diff = self.get_nonlinear_rgb_color_diff
|
||||
for i,color in enumerate(colors):
|
||||
for j,other_color in enumerate(colors):
|
||||
get_color_diff = (
|
||||
self.get_lab_color_diff
|
||||
if self.lab_color_comparison
|
||||
else self.get_rgb_color_diff
|
||||
)
|
||||
# get_color_diff = self.get_nonlinear_rgb_color_diff
|
||||
for i, color in enumerate(colors):
|
||||
for j, other_color in enumerate(colors):
|
||||
color_diffs[i][j] = get_color_diff(color, other_color)
|
||||
return color_diffs
|
||||
|
||||
|
||||
def get_rgb_color_diff(self, color1, color2):
|
||||
r = abs(color1[0] - color2[0])
|
||||
g = abs(color1[1] - color2[1])
|
||||
b = abs(color1[2] - color2[2])
|
||||
a = abs(color1[3] - color2[3])
|
||||
return abs(r + g + b + a)
|
||||
|
||||
|
||||
def get_lab_color_diff(self, color1, color2):
|
||||
l1, a1, b1 = rgb_to_lab(*color1[:3])
|
||||
l2, a2, b2 = rgb_to_lab(*color2[:3])
|
||||
return lab_color_diff(l1, a1, b1, l2, a2, b2)
|
||||
|
||||
|
||||
def get_nonlinear_rgb_color_diff(self, color1, color2):
|
||||
# from http://www.compuphase.com/cmetric.htm
|
||||
rmean = int((color1[0] + color2[0]) / 2)
|
||||
r = color1[0] - color2[0]
|
||||
g = color1[1] - color2[1]
|
||||
b = color1[2] - color2[2]
|
||||
return math.sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8))
|
||||
|
||||
return math.sqrt(
|
||||
(((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8)
|
||||
)
|
||||
|
||||
def update(self):
|
||||
if time.time() < self.start_time + self.start_delay:
|
||||
return
|
||||
|
|
@ -152,9 +164,16 @@ class ImageConverter:
|
|||
# but transparency isn't properly supported yet
|
||||
fg = self.art.palette.darkest_index if fg == 0 else fg
|
||||
bg = self.art.palette.darkest_index if bg == 0 else bg
|
||||
self.art.set_tile_at(self.art.active_frame, self.art.active_layer,
|
||||
self.x, self.y, char, fg, bg)
|
||||
#print('set block %s,%s to ch %s fg %s bg %s' % (self.x, self.y, char, fg, bg))
|
||||
self.art.set_tile_at(
|
||||
self.art.active_frame,
|
||||
self.art.active_layer,
|
||||
self.x,
|
||||
self.y,
|
||||
char,
|
||||
fg,
|
||||
bg,
|
||||
)
|
||||
# print('set block %s,%s to ch %s fg %s bg %s' % (self.x, self.y, char, fg, bg))
|
||||
self.x += 1
|
||||
if self.x >= self.art.width:
|
||||
self.x = 0
|
||||
|
|
@ -162,7 +181,7 @@ class ImageConverter:
|
|||
if self.y >= self.art.height:
|
||||
self.finish()
|
||||
break
|
||||
|
||||
|
||||
def get_color_combos_for_block(self, src_block):
|
||||
"""
|
||||
returns # of unique colors, AND
|
||||
|
|
@ -175,12 +194,12 @@ class ImageConverter:
|
|||
return colors, []
|
||||
# sort by most to least used colors
|
||||
color_counts = []
|
||||
for i,color in enumerate(colors):
|
||||
for i, color in enumerate(colors):
|
||||
color_counts += [(color, counts[i])]
|
||||
color_counts.sort(key=lambda item: item[1], reverse=True)
|
||||
combos = []
|
||||
for color1,count1 in color_counts:
|
||||
for color2,count2 in color_counts:
|
||||
for color1, count1 in color_counts:
|
||||
for color2, count2 in color_counts:
|
||||
if color1 == color2:
|
||||
continue
|
||||
# fg/bg color swap SHOULD be allowed
|
||||
|
|
@ -188,7 +207,7 @@ class ImageConverter:
|
|||
continue
|
||||
combos.append((color1, color2))
|
||||
return colors, combos
|
||||
|
||||
|
||||
def get_best_tile_for_block(self, src_block):
|
||||
"returns a (char, fg, bg) tuple for the best match of given block"
|
||||
colors, combos = self.get_color_combos_for_block(src_block)
|
||||
|
|
@ -202,14 +221,14 @@ class ImageConverter:
|
|||
best_char = 0
|
||||
best_diff = 9999999999999
|
||||
best_fg, best_bg = 0, 0
|
||||
for bg,fg in combos:
|
||||
for bg, fg in combos:
|
||||
# reset char index before each run through charset
|
||||
char_index = 0
|
||||
char_array = self.char_array.copy()
|
||||
# replace 1-bit color of char image with fg and bg colors
|
||||
char_array[char_array == 0] = bg
|
||||
char_array[char_array == 1] = fg
|
||||
for (x0, y0, x1, y1) in self.char_blocks:
|
||||
for x0, y0, x1, y1 in self.char_blocks:
|
||||
char_block = char_array[y0:y1, x0:x1]
|
||||
# using array of difference values w/ fancy numpy indexing,
|
||||
# sum() it
|
||||
|
|
@ -222,31 +241,34 @@ class ImageConverter:
|
|||
best_diff = diff
|
||||
best_char = char_index
|
||||
best_fg, best_bg = fg, bg
|
||||
#print('%s is new best char index, diff %s:' % (char_index, diff))
|
||||
# print('%s is new best char index, diff %s:' % (char_index, diff))
|
||||
char_index += 1
|
||||
# return best (least different to source block) char/fg/bg found
|
||||
#print('%s is best char index, diff %s:' % (best_char, best_diff))
|
||||
# print('%s is best char index, diff %s:' % (best_char, best_diff))
|
||||
return (best_char, best_fg, best_bg)
|
||||
|
||||
|
||||
def print_block(self, block, fg, bg):
|
||||
"prints ASCII representation of a block with . and # as white and black"
|
||||
w, h = block.shape
|
||||
s = ''
|
||||
s = ""
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if block[y][x] == fg:
|
||||
s += '#'
|
||||
s += "#"
|
||||
else:
|
||||
s += '.'
|
||||
s += '\n'
|
||||
s += "."
|
||||
s += "\n"
|
||||
print(s)
|
||||
|
||||
|
||||
def finish(self, cancelled=False):
|
||||
self.finished = True
|
||||
if not self.sequence_converter:
|
||||
time_taken = time.time() - self.start_time
|
||||
verb = 'cancelled' if cancelled else 'finished'
|
||||
self.app.log('Conversion of image %s %s after %.3f seconds' % (self.image_filename, verb, time_taken))
|
||||
verb = "cancelled" if cancelled else "finished"
|
||||
self.app.log(
|
||||
"Conversion of image %s %s after %.3f seconds"
|
||||
% (self.image_filename, verb, time_taken)
|
||||
)
|
||||
self.app.converter = None
|
||||
self.preview_sprite = None
|
||||
self.app.update_window_title()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import os
|
||||
from OpenGL import GL
|
||||
from PIL import Image, ImageChops, GifImagePlugin
|
||||
from PIL import GifImagePlugin, Image, ImageChops
|
||||
|
||||
from framebuffer import ExportFramebuffer, ExportFramebufferNoCRT
|
||||
|
||||
|
||||
def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0, 0)):
|
||||
"returns a PIL image of given frame of given art, None on failure"
|
||||
post_fb_class = ExportFramebuffer if allow_crt else ExportFramebufferNoCRT
|
||||
|
|
@ -13,8 +13,14 @@ def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0,
|
|||
w, h = int(w * scale), int(h * scale)
|
||||
# error out if over max texture size
|
||||
if w > app.max_texture_size or h > app.max_texture_size:
|
||||
app.log("ERROR: Image output size (%s x %s) exceeds your hardware's max supported texture size (%s x %s)!" % (w, h, app.max_texture_size, app.max_texture_size), error=True)
|
||||
app.log(' Please export at a smaller scale or chop up your artwork :[', error=True)
|
||||
app.log(
|
||||
"ERROR: Image output size (%s x %s) exceeds your hardware's max supported texture size (%s x %s)!"
|
||||
% (w, h, app.max_texture_size, app.max_texture_size),
|
||||
error=True,
|
||||
)
|
||||
app.log(
|
||||
" Please export at a smaller scale or chop up your artwork :[", error=True
|
||||
)
|
||||
return None
|
||||
# create CRT framebuffer
|
||||
post_fb = post_fb_class(app, w, h)
|
||||
|
|
@ -24,8 +30,12 @@ def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0,
|
|||
GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, render_buffer)
|
||||
GL.glRenderbufferStorage(GL.GL_RENDERBUFFER, GL.GL_RGBA8, w, h)
|
||||
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, export_fb)
|
||||
GL.glFramebufferRenderbuffer(GL.GL_DRAW_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0,
|
||||
GL.GL_RENDERBUFFER, render_buffer)
|
||||
GL.glFramebufferRenderbuffer(
|
||||
GL.GL_DRAW_FRAMEBUFFER,
|
||||
GL.GL_COLOR_ATTACHMENT0,
|
||||
GL.GL_RENDERBUFFER,
|
||||
render_buffer,
|
||||
)
|
||||
GL.glViewport(0, 0, w, h)
|
||||
# do render
|
||||
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, post_fb.framebuffer)
|
||||
|
|
@ -38,8 +48,9 @@ def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0,
|
|||
post_fb.render()
|
||||
GL.glReadBuffer(GL.GL_COLOR_ATTACHMENT0)
|
||||
# read pixels from it
|
||||
pixels = GL.glReadPixels(0, 0, w, h, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE,
|
||||
outputType=None)
|
||||
pixels = GL.glReadPixels(
|
||||
0, 0, w, h, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, outputType=None
|
||||
)
|
||||
# cleanup / deinit of GL stuff
|
||||
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
|
||||
GL.glViewport(0, 0, app.window_width, app.window_height)
|
||||
|
|
@ -48,10 +59,11 @@ def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0,
|
|||
post_fb.destroy()
|
||||
# GL pixel data as numpy array -> bytes for PIL image export
|
||||
pixel_bytes = pixels.flatten().tobytes()
|
||||
src_img = Image.frombytes(mode='RGBA', size=(w, h), data=pixel_bytes)
|
||||
src_img = Image.frombytes(mode="RGBA", size=(w, h), data=pixel_bytes)
|
||||
src_img = src_img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
return src_img
|
||||
|
||||
|
||||
def export_animation(app, art, out_filename, bg_color=None, loop=True):
|
||||
# get list of rendered frame images
|
||||
frames = []
|
||||
|
|
@ -60,33 +72,38 @@ def export_animation(app, art, out_filename, bg_color=None, loop=True):
|
|||
# if bg color is specified, this isn't art mode; play along
|
||||
if bg_color is not None:
|
||||
f_transp = bg_color
|
||||
art.palette.colors[0] = (round(bg_color[0] * 255),
|
||||
round(bg_color[1] * 255),
|
||||
round(bg_color[2] * 255),
|
||||
255)
|
||||
art.palette.colors[0] = (
|
||||
round(bg_color[0] * 255),
|
||||
round(bg_color[1] * 255),
|
||||
round(bg_color[2] * 255),
|
||||
255,
|
||||
)
|
||||
else:
|
||||
# GL wants floats
|
||||
f_transp = (i_transp[0]/255, i_transp[1]/255, i_transp[2]/255, 1.)
|
||||
f_transp = (i_transp[0] / 255, i_transp[1] / 255, i_transp[2] / 255, 1.0)
|
||||
for frame in range(art.frames):
|
||||
frame_img = get_frame_image(app, art, frame, allow_crt=False,
|
||||
scale=1, bg_color=f_transp)
|
||||
frame_img = get_frame_image(
|
||||
app, art, frame, allow_crt=False, scale=1, bg_color=f_transp
|
||||
)
|
||||
if bg_color is not None:
|
||||
# if bg color is specified, assume no transparency
|
||||
frame_img = art.palette.get_palettized_image(frame_img, force_no_transparency=True)
|
||||
frame_img = art.palette.get_palettized_image(
|
||||
frame_img, force_no_transparency=True
|
||||
)
|
||||
else:
|
||||
frame_img = art.palette.get_palettized_image(frame_img, i_transp[:3])
|
||||
frames.append(frame_img)
|
||||
# compile frames into animated GIF with proper frame delays
|
||||
# technique thanks to:
|
||||
# https://github.com/python-pillow/Pillow/blob/master/Scripts/gifmaker.py
|
||||
output_img = open(out_filename, 'wb')
|
||||
for i,img in enumerate(frames):
|
||||
output_img = open(out_filename, "wb")
|
||||
for i, img in enumerate(frames):
|
||||
delay = art.frame_delays[i] * 1000
|
||||
if i == 0:
|
||||
data = GifImagePlugin.getheader(img)[0]
|
||||
# PIL only wants to write GIF87a for some reason...
|
||||
# welcome to 1989 B]
|
||||
data[0] = data[0].replace(b'7', b'9')
|
||||
data[0] = data[0].replace(b"7", b"9")
|
||||
# TODO: loop doesn't work?
|
||||
if bg_color is not None:
|
||||
# if bg color is specified, assume no transparency
|
||||
|
|
@ -95,23 +112,24 @@ def export_animation(app, art, out_filename, bg_color=None, loop=True):
|
|||
else:
|
||||
data += GifImagePlugin.getdata(img, duration=delay)
|
||||
else:
|
||||
data += GifImagePlugin.getdata(img, duration=delay,
|
||||
transparency=0, loop=0)
|
||||
data += GifImagePlugin.getdata(
|
||||
img, duration=delay, transparency=0, loop=0
|
||||
)
|
||||
for b in data:
|
||||
output_img.write(b)
|
||||
continue
|
||||
delta = ImageChops.subtract_modulo(img, frames[i-1])
|
||||
delta = ImageChops.subtract_modulo(img, frames[i - 1])
|
||||
# Image.getbbox() rather unhelpfully returns None if no delta
|
||||
dw, dh = delta.size
|
||||
bbox = delta.getbbox() or (0, 0, dw, dh)
|
||||
for b in GifImagePlugin.getdata(img.crop(bbox), offset=bbox[:2],
|
||||
duration=delay, transparency=0,
|
||||
loop=0):
|
||||
for b in GifImagePlugin.getdata(
|
||||
img.crop(bbox), offset=bbox[:2], duration=delay, transparency=0, loop=0
|
||||
):
|
||||
output_img.write(b)
|
||||
output_img.write(b';')
|
||||
output_img.write(b";")
|
||||
output_img.close()
|
||||
output_format = 'Animated GIF'
|
||||
#app.log('%s exported (%s)' % (out_filename, output_format))
|
||||
output_format = "Animated GIF"
|
||||
# app.log('%s exported (%s)' % (out_filename, output_format))
|
||||
|
||||
|
||||
def export_still_image(app, art, out_filename, crt=True, scale=1, bg_color=None):
|
||||
|
|
@ -124,20 +142,20 @@ def export_still_image(app, art, out_filename, crt=True, scale=1, bg_color=None)
|
|||
src_img = get_frame_image(app, art, art.active_frame, crt, scale, bg_color)
|
||||
if not src_img:
|
||||
return False
|
||||
src_img.save(out_filename, 'PNG')
|
||||
output_format = '32-bit w/ alpha'
|
||||
src_img.save(out_filename, "PNG")
|
||||
output_format = "32-bit w/ alpha"
|
||||
else:
|
||||
# else convert to current palette.
|
||||
# as with aniGIF export, use arbitrary color for transparency
|
||||
i_transp = art.palette.get_random_non_palette_color()
|
||||
f_transp = (i_transp[0]/255, i_transp[1]/255, i_transp[2]/255, 1.)
|
||||
f_transp = (i_transp[0] / 255, i_transp[1] / 255, i_transp[2] / 255, 1.0)
|
||||
src_img = get_frame_image(app, art, art.active_frame, False, scale, f_transp)
|
||||
if not src_img:
|
||||
return False
|
||||
output_img = art.palette.get_palettized_image(src_img, i_transp[:3])
|
||||
output_img.save(out_filename, 'PNG', transparency=0)
|
||||
output_format = '8-bit palettized w/ transparency'
|
||||
#app.log('%s exported (%s)' % (out_filename, output_format))
|
||||
output_img.save(out_filename, "PNG", transparency=0)
|
||||
output_format = "8-bit palettized w/ transparency"
|
||||
# app.log('%s exported (%s)' % (out_filename, output_format))
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -153,6 +171,6 @@ def write_thumbnail(app, art_filename, thumb_filename):
|
|||
art.renderables.append(renderable)
|
||||
img = get_frame_image(app, art, 0, allow_crt=False)
|
||||
if img:
|
||||
img.save(thumb_filename, 'PNG')
|
||||
img.save(thumb_filename, "PNG")
|
||||
if renderable:
|
||||
renderable.destroy()
|
||||
|
|
|
|||
730
input_handler.py
730
input_handler.py
File diff suppressed because it is too large
Load diff
|
|
@ -5,9 +5,27 @@ import sdl2
|
|||
# MAYBE-TODO: find out if this breaks for non-US english KB layouts
|
||||
|
||||
SHIFT_MAP = {
|
||||
'1': '!', '2': '@', '3': '#', '4': '$', '5': '%', '6': '^', '7': '&', '8': '*',
|
||||
'9': '(', '0': ')', '-': '_', '=': '+', '`': '~', '[': '{', ']': '}', '\\': '|',
|
||||
';': ':', "'": '"', ',': '<', '.': '>', '/': '?'
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
"`": "~",
|
||||
"[": "{",
|
||||
"]": "}",
|
||||
"\\": "|",
|
||||
";": ":",
|
||||
"'": '"',
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
}
|
||||
|
||||
NUMLOCK_ON_MAP = {
|
||||
|
|
@ -26,7 +44,7 @@ NUMLOCK_ON_MAP = {
|
|||
sdl2.SDLK_KP_PLUS: sdl2.SDLK_PLUS,
|
||||
sdl2.SDLK_KP_MINUS: sdl2.SDLK_MINUS,
|
||||
sdl2.SDLK_KP_PERIOD: sdl2.SDLK_PERIOD,
|
||||
sdl2.SDLK_KP_ENTER: sdl2.SDLK_RETURN
|
||||
sdl2.SDLK_KP_ENTER: sdl2.SDLK_RETURN,
|
||||
}
|
||||
|
||||
NUMLOCK_OFF_MAP = {
|
||||
|
|
@ -40,5 +58,5 @@ NUMLOCK_OFF_MAP = {
|
|||
sdl2.SDLK_KP_8: sdl2.SDLK_UP,
|
||||
sdl2.SDLK_KP_9: sdl2.SDLK_PAGEUP,
|
||||
sdl2.SDLK_KP_PERIOD: sdl2.SDLK_DELETE,
|
||||
sdl2.SDLK_KP_ENTER: sdl2.SDLK_RETURN
|
||||
sdl2.SDLK_KP_ENTER: sdl2.SDLK_RETURN,
|
||||
}
|
||||
|
|
|
|||
22
lab_color.py
22
lab_color.py
|
|
@ -3,20 +3,21 @@
|
|||
|
||||
import math
|
||||
|
||||
|
||||
def rgb_to_xyz(r, g, b):
|
||||
r /= 255.0
|
||||
g /= 255.0
|
||||
b /= 255.0
|
||||
if r > 0.04045:
|
||||
r = ((r + 0.055) / 1.055)**2.4
|
||||
r = ((r + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
r /= 12.92
|
||||
if g > 0.04045:
|
||||
g = ((g + 0.055) / 1.055)**2.4
|
||||
g = ((g + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
g /= 12.92
|
||||
if b > 0.04045:
|
||||
b = ((b + 0.055) / 1.055)**2.4
|
||||
b = ((b + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
b /= 12.92
|
||||
r *= 100
|
||||
|
|
@ -28,21 +29,22 @@ def rgb_to_xyz(r, g, b):
|
|||
z = r * 0.0193 + g * 0.1192 + b * 0.9505
|
||||
return x, y, z
|
||||
|
||||
|
||||
def xyz_to_lab(x, y, z):
|
||||
# observer: 2deg, illuminant: D65
|
||||
x /= 95.047
|
||||
y /= 100.0
|
||||
z /= 108.883
|
||||
if x > 0.008856:
|
||||
x = x**(1.0/3)
|
||||
x = x ** (1.0 / 3)
|
||||
else:
|
||||
x = (7.787 * x) + (16.0 / 116)
|
||||
if y > 0.008856:
|
||||
y = y**(1.0/3)
|
||||
y = y ** (1.0 / 3)
|
||||
else:
|
||||
y = (7.787 * y) + (16.0 / 116)
|
||||
if z > 0.008856:
|
||||
z = z**(1.0/3)
|
||||
z = z ** (1.0 / 3)
|
||||
else:
|
||||
z = (7.787 * z) + (16.0 / 116)
|
||||
l = (116 * y) - 16
|
||||
|
|
@ -50,13 +52,15 @@ def xyz_to_lab(x, y, z):
|
|||
b = 200 * (y - z)
|
||||
return l, a, b
|
||||
|
||||
|
||||
def rgb_to_lab(r, g, b):
|
||||
x, y, z = rgb_to_xyz(r, g, b)
|
||||
return xyz_to_lab(x, y, z)
|
||||
|
||||
|
||||
def lab_color_diff(l1, a1, b1, l2, a2, b2):
|
||||
"quick n' dirty CIE 1976 color delta"
|
||||
dl = (l1 - l2)**2
|
||||
da = (a1 - a2)**2
|
||||
db = (b1 - b2)**2
|
||||
dl = (l1 - l2) ** 2
|
||||
da = (a1 - a2) ** 2
|
||||
db = (b1 - b2) ** 2
|
||||
return math.sqrt(dl + da + db)
|
||||
|
|
|
|||
133
palette.py
133
palette.py
|
|
@ -1,25 +1,31 @@
|
|||
import os.path, math, time
|
||||
import math
|
||||
import os.path
|
||||
import time
|
||||
from random import randint
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from lab_color import lab_color_diff, rgb_to_lab
|
||||
from texture import Texture
|
||||
from lab_color import rgb_to_lab, lab_color_diff
|
||||
|
||||
PALETTE_DIR = 'palettes/'
|
||||
PALETTE_EXTENSIONS = ['png', 'gif', 'bmp']
|
||||
PALETTE_DIR = "palettes/"
|
||||
PALETTE_EXTENSIONS = ["png", "gif", "bmp"]
|
||||
MAX_COLORS = 1024
|
||||
|
||||
|
||||
class PaletteLord:
|
||||
|
||||
# time in ms between checks for hot reload
|
||||
hot_reload_check_interval = 2 * 1000
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.last_check = 0
|
||||
|
||||
|
||||
def check_hot_reload(self):
|
||||
if self.app.get_elapsed_time() - self.last_check < self.hot_reload_check_interval:
|
||||
if (
|
||||
self.app.get_elapsed_time() - self.last_check
|
||||
< self.hot_reload_check_interval
|
||||
):
|
||||
return
|
||||
self.last_check = self.app.get_elapsed_time()
|
||||
changed = None
|
||||
|
|
@ -28,18 +34,20 @@ class PaletteLord:
|
|||
changed = palette.filename
|
||||
try:
|
||||
palette.load_image()
|
||||
self.app.log('PaletteLord: success reloading %s' % palette.filename)
|
||||
self.app.log("PaletteLord: success reloading %s" % palette.filename)
|
||||
except:
|
||||
self.app.log('PaletteLord: failed reloading %s' % palette.filename, True)
|
||||
self.app.log(
|
||||
"PaletteLord: failed reloading %s" % palette.filename, True
|
||||
)
|
||||
|
||||
|
||||
class Palette:
|
||||
|
||||
def __init__(self, app, src_filename, log):
|
||||
self.init_success = False
|
||||
self.app = app
|
||||
self.filename = self.app.find_filename_path(src_filename, PALETTE_DIR,
|
||||
PALETTE_EXTENSIONS)
|
||||
self.filename = self.app.find_filename_path(
|
||||
src_filename, PALETTE_DIR, PALETTE_EXTENSIONS
|
||||
)
|
||||
if self.filename is None:
|
||||
self.app.log("Couldn't find palette image %s" % src_filename)
|
||||
return
|
||||
|
|
@ -50,15 +58,15 @@ class Palette:
|
|||
self.base_filename = os.path.splitext(os.path.basename(self.filename))[0]
|
||||
if log and not self.app.game_mode:
|
||||
self.app.log("loaded palette '%s' from %s:" % (self.name, self.filename))
|
||||
self.app.log(' unique colors found: %s' % int(len(self.colors)-1))
|
||||
self.app.log(' darkest color index: %s' % self.darkest_index)
|
||||
self.app.log(' lightest color index: %s' % self.lightest_index)
|
||||
self.app.log(" unique colors found: %s" % int(len(self.colors) - 1))
|
||||
self.app.log(" darkest color index: %s" % self.darkest_index)
|
||||
self.app.log(" lightest color index: %s" % self.lightest_index)
|
||||
self.init_success = True
|
||||
|
||||
|
||||
def load_image(self):
|
||||
"loads palette data from the given bitmap image"
|
||||
src_img = Image.open(self.filename)
|
||||
src_img = src_img.convert('RGBA')
|
||||
src_img = src_img.convert("RGBA")
|
||||
width, height = src_img.size
|
||||
# store texture for chooser preview etc
|
||||
self.src_texture = Texture(src_img.tobytes(), width, height)
|
||||
|
|
@ -75,10 +83,10 @@ class Palette:
|
|||
if len(self.colors) >= MAX_COLORS:
|
||||
break
|
||||
color = src_img.getpixel((x, y))
|
||||
if not color in self.colors:
|
||||
if color not in self.colors:
|
||||
self.colors.append(color)
|
||||
# is this lightest/darkest unique color so far? save index
|
||||
luminosity = color[0]*0.21 + color[1]*0.72 + color[2]*0.07
|
||||
luminosity = color[0] * 0.21 + color[1] * 0.72 + color[2] * 0.07
|
||||
if luminosity < darkest:
|
||||
darkest = luminosity
|
||||
self.darkest_index = len(self.colors) - 1
|
||||
|
|
@ -86,27 +94,27 @@ class Palette:
|
|||
lightest = luminosity
|
||||
self.lightest_index = len(self.colors) - 1
|
||||
# create new 1D image with unique colors
|
||||
img = Image.new('RGBA', (MAX_COLORS, 1), (0, 0, 0, 0))
|
||||
img = Image.new("RGBA", (MAX_COLORS, 1), (0, 0, 0, 0))
|
||||
x = 0
|
||||
for color in self.colors:
|
||||
img.putpixel((x, 0), color)
|
||||
x += 1
|
||||
# debug: save out generated palette texture
|
||||
#img.save('palette.png')
|
||||
# img.save('palette.png')
|
||||
self.texture = Texture(img.tobytes(), MAX_COLORS, 1)
|
||||
|
||||
|
||||
def has_updated(self):
|
||||
"return True if source image file has changed since last check"
|
||||
changed = os.path.getmtime(self.filename) > self.last_image_change
|
||||
if changed:
|
||||
self.last_image_change = time.time()
|
||||
return changed
|
||||
|
||||
|
||||
def generate_image(self):
|
||||
width = min(16, len(self.colors) - 1)
|
||||
height = math.floor((len(self.colors) - 1) / width)
|
||||
# new PIL image, blank (0 alpha) pixels
|
||||
img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
# set each pixel from color list (minus first, transparent color)
|
||||
color_index = 1
|
||||
for y in range(height):
|
||||
|
|
@ -116,44 +124,48 @@ class Palette:
|
|||
img.putpixel((x, y), self.colors[color_index])
|
||||
color_index += 1
|
||||
return img
|
||||
|
||||
|
||||
def export_as_image(self):
|
||||
img = self.generate_image()
|
||||
block_size = 8
|
||||
# scale up
|
||||
width, height = img.size
|
||||
img = img.resize((width * block_size, height * block_size),
|
||||
resample=Image.NEAREST)
|
||||
img = img.resize(
|
||||
(width * block_size, height * block_size), resample=Image.NEAREST
|
||||
)
|
||||
# write to file
|
||||
img_filename = self.app.documents_dir + PALETTE_DIR + self.name + '.png'
|
||||
img_filename = self.app.documents_dir + PALETTE_DIR + self.name + ".png"
|
||||
img.save(img_filename)
|
||||
|
||||
|
||||
def all_colors_opaque(self):
|
||||
"returns True if we have any non-opaque (<1 alpha) colors"
|
||||
for color in self.colors[1:]:
|
||||
if color[3] < 255:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_random_non_palette_color(self):
|
||||
"returns random color not in this palette, eg for 8-bit transparency"
|
||||
|
||||
def rand_byte():
|
||||
return randint(0, 255)
|
||||
|
||||
# assume full alpha
|
||||
r, g, b, a = rand_byte(), rand_byte(), rand_byte(), 255
|
||||
while (r, g, b, a) in self.colors:
|
||||
r, g, b = rand_byte(), rand_byte(), rand_byte()
|
||||
return r, g, b, a
|
||||
|
||||
def get_palettized_image(self, src_img, transparent_color=(0, 0, 0),
|
||||
force_no_transparency=False):
|
||||
|
||||
def get_palettized_image(
|
||||
self, src_img, transparent_color=(0, 0, 0), force_no_transparency=False
|
||||
):
|
||||
"returns a copy of source image quantized to this palette"
|
||||
pal_img = Image.new('P', (1, 1))
|
||||
pal_img = Image.new("P", (1, 1))
|
||||
# source must be in RGB (no alpha) format
|
||||
out_img = src_img.convert('RGB')
|
||||
out_img = src_img.convert("RGB")
|
||||
# Image.putpalette needs a flat tuple :/
|
||||
colors = []
|
||||
for i,color in enumerate(self.colors):
|
||||
for i, color in enumerate(self.colors):
|
||||
# ignore alpha for palettized image output
|
||||
for channel in color[:-1]:
|
||||
colors.append(channel)
|
||||
|
|
@ -165,12 +177,11 @@ class Palette:
|
|||
for i in range(3):
|
||||
colors.append(0)
|
||||
# palette for PIL must be exactly 256 colors
|
||||
colors = colors[:256*3]
|
||||
colors = colors[: 256 * 3]
|
||||
pal_img.putpalette(tuple(colors))
|
||||
return out_img.quantize(palette=pal_img)
|
||||
|
||||
def are_colors_similar(self, color_index_a, palette_b, color_index_b,
|
||||
tolerance=50):
|
||||
|
||||
def are_colors_similar(self, color_index_a, palette_b, color_index_b, tolerance=50):
|
||||
"""
|
||||
returns True if color index A is similar to color index B from
|
||||
another palette.
|
||||
|
|
@ -181,35 +192,34 @@ class Palette:
|
|||
g_diff = abs(color_a[1] - color_b[1])
|
||||
b_diff = abs(color_a[2] - color_b[2])
|
||||
return (r_diff + g_diff + b_diff) <= tolerance
|
||||
|
||||
|
||||
def get_closest_color_index(self, r, g, b):
|
||||
"returns index of closest color in this palette to given color (kinda slow?)"
|
||||
closest_diff = 99999999999
|
||||
closest_diff_index = -1
|
||||
for i,color in enumerate(self.colors):
|
||||
for i, color in enumerate(self.colors):
|
||||
l1, a1, b1 = rgb_to_lab(r, g, b)
|
||||
l2, a2, b2 = rgb_to_lab(*color[:3])
|
||||
diff = lab_color_diff(l1, a1, b1, l2, a2, b2)
|
||||
if diff < closest_diff:
|
||||
closest_diff = diff
|
||||
closest_diff_index = i
|
||||
#print('%s is closest to input color %s' % (self.colors[closest_diff_index], (r, g, b)))
|
||||
# print('%s is closest to input color %s' % (self.colors[closest_diff_index], (r, g, b)))
|
||||
return closest_diff_index
|
||||
|
||||
|
||||
def get_random_color_index(self):
|
||||
# exclude transparent first index
|
||||
return randint(1, len(self.colors))
|
||||
|
||||
|
||||
class PaletteFromList(Palette):
|
||||
|
||||
"palette created from list of 3/4-tuple base-255 colors instead of image"
|
||||
|
||||
|
||||
def __init__(self, app, src_color_list, log):
|
||||
self.init_success = False
|
||||
self.app = app
|
||||
# generate a unique non-user-facing palette name
|
||||
name = 'PaletteFromList_%s' % time.time()
|
||||
name = "PaletteFromList_%s" % time.time()
|
||||
self.filename = self.name = self.base_filename = name
|
||||
colors = []
|
||||
for color in src_color_list:
|
||||
|
|
@ -222,7 +232,7 @@ class PaletteFromList(Palette):
|
|||
lightest = 0
|
||||
darkest = 255 * 3 + 1
|
||||
for color in self.colors:
|
||||
luminosity = color[0]*0.21 + color[1]*0.72 + color[2]*0.07
|
||||
luminosity = color[0] * 0.21 + color[1] * 0.72 + color[2] * 0.07
|
||||
if luminosity < darkest:
|
||||
darkest = luminosity
|
||||
self.darkest_index = len(self.colors) - 1
|
||||
|
|
@ -230,7 +240,7 @@ class PaletteFromList(Palette):
|
|||
lightest = luminosity
|
||||
self.lightest_index = len(self.colors) - 1
|
||||
# create texture
|
||||
img = Image.new('RGBA', (MAX_COLORS, 1), (0, 0, 0, 0))
|
||||
img = Image.new("RGBA", (MAX_COLORS, 1), (0, 0, 0, 0))
|
||||
x = 0
|
||||
for color in self.colors:
|
||||
img.putpixel((x, 0), color)
|
||||
|
|
@ -238,17 +248,16 @@ class PaletteFromList(Palette):
|
|||
self.texture = Texture(img.tobytes(), MAX_COLORS, 1)
|
||||
if log and not self.app.game_mode:
|
||||
self.app.log("generated new palette '%s'" % (self.name))
|
||||
self.app.log(' unique colors: %s' % int(len(self.colors)-1))
|
||||
self.app.log(' darkest color index: %s' % self.darkest_index)
|
||||
self.app.log(' lightest color index: %s' % self.lightest_index)
|
||||
|
||||
self.app.log(" unique colors: %s" % int(len(self.colors) - 1))
|
||||
self.app.log(" darkest color index: %s" % self.darkest_index)
|
||||
self.app.log(" lightest color index: %s" % self.lightest_index)
|
||||
|
||||
def has_updated(self):
|
||||
"No bitmap source for this type of palette, so no hot-reload"
|
||||
return False
|
||||
|
||||
|
||||
class PaletteFromFile(Palette):
|
||||
|
||||
def __init__(self, app, src_filename, palette_filename, colors=MAX_COLORS):
|
||||
self.init_success = False
|
||||
src_filename = app.find_filename_path(src_filename)
|
||||
|
|
@ -258,8 +267,10 @@ class PaletteFromFile(Palette):
|
|||
# dither source image, re-save it, use that as the source for a palette
|
||||
src_img = Image.open(src_filename)
|
||||
# method:
|
||||
src_img = src_img.convert('P', None, Image.FLOYDSTEINBERG, Image.ADAPTIVE, colors)
|
||||
src_img = src_img.convert('RGBA')
|
||||
src_img = src_img.convert(
|
||||
"P", None, Image.FLOYDSTEINBERG, Image.ADAPTIVE, colors
|
||||
)
|
||||
src_img = src_img.convert("RGBA")
|
||||
# write converted source image with new filename
|
||||
# snip path & extension if it has em
|
||||
palette_filename = os.path.basename(palette_filename)
|
||||
|
|
@ -267,13 +278,15 @@ class PaletteFromFile(Palette):
|
|||
# get most appropriate path for palette image
|
||||
palette_path = app.get_dirnames(PALETTE_DIR, False)[0]
|
||||
# if new filename exists, add a number to avoid overwriting
|
||||
if os.path.exists(palette_path + palette_filename + '.png'):
|
||||
if os.path.exists(palette_path + palette_filename + ".png"):
|
||||
i = 0
|
||||
while os.path.exists('%s%s%s.png' % (palette_path, palette_filename, str(i))):
|
||||
while os.path.exists(
|
||||
"%s%s%s.png" % (palette_path, palette_filename, str(i))
|
||||
):
|
||||
i += 1
|
||||
palette_filename += str(i)
|
||||
# (re-)add path and PNG extension
|
||||
palette_filename = palette_path + palette_filename + '.png'
|
||||
palette_filename = palette_path + palette_filename + ".png"
|
||||
src_img.save(palette_filename)
|
||||
# create the actual palette and export it as an image
|
||||
Palette.__init__(self, app, palette_filename, True)
|
||||
|
|
|
|||
709
playscii.py
709
playscii.py
File diff suppressed because it is too large
Load diff
334
renderable.py
334
renderable.py
|
|
@ -1,6 +1,9 @@
|
|||
import os, math, ctypes
|
||||
import ctypes
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from OpenGL import GL
|
||||
|
||||
from art import VERT_LENGTH
|
||||
from palette import MAX_COLORS
|
||||
|
||||
|
|
@ -16,22 +19,23 @@ class TileRenderable:
|
|||
rectangular OpenGL triangle-pairs. Animation frames are uploaded into our
|
||||
buffers from source Art's numpy arrays.
|
||||
"""
|
||||
vert_shader_source = 'renderable_v.glsl'
|
||||
|
||||
vert_shader_source = "renderable_v.glsl"
|
||||
"vertex shader: includes view projection matrix, XYZ camera uniforms."
|
||||
frag_shader_source = 'renderable_f.glsl'
|
||||
frag_shader_source = "renderable_f.glsl"
|
||||
"Pixel shader: handles FG/BG colors."
|
||||
log_create_destroy = False
|
||||
log_animation = False
|
||||
log_buffer_updates = False
|
||||
grain_strength = 0.
|
||||
alpha = 1.
|
||||
grain_strength = 0.0
|
||||
alpha = 1.0
|
||||
"Alpha (0 to 1) for entire Renderable."
|
||||
bg_alpha = 1.
|
||||
bg_alpha = 1.0
|
||||
"Alpha (0 to 1) *only* for tile background colors."
|
||||
default_move_rate = 1
|
||||
use_art_offset = True
|
||||
"Use game object's art_off_pct values."
|
||||
|
||||
|
||||
def __init__(self, app, art, game_object=None):
|
||||
"Create Renderable with given Art, optionally bound to given GameObject"
|
||||
self.app = app
|
||||
|
|
@ -69,68 +73,128 @@ class TileRenderable:
|
|||
if self.app.use_vao:
|
||||
self.vao = GL.glGenVertexArrays(1)
|
||||
GL.glBindVertexArray(self.vao)
|
||||
self.shader = self.app.sl.new_shader(self.vert_shader_source, self.frag_shader_source)
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location('projection')
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location('view')
|
||||
self.position_uniform = self.shader.get_uniform_location('objectPosition')
|
||||
self.scale_uniform = self.shader.get_uniform_location('objectScale')
|
||||
self.charset_width_uniform = self.shader.get_uniform_location('charMapWidth')
|
||||
self.charset_height_uniform = self.shader.get_uniform_location('charMapHeight')
|
||||
self.char_uv_width_uniform = self.shader.get_uniform_location('charUVWidth')
|
||||
self.char_uv_height_uniform = self.shader.get_uniform_location('charUVHeight')
|
||||
self.charset_tex_uniform = self.shader.get_uniform_location('charset')
|
||||
self.palette_tex_uniform = self.shader.get_uniform_location('palette')
|
||||
self.grain_tex_uniform = self.shader.get_uniform_location('grain')
|
||||
self.palette_width_uniform = self.shader.get_uniform_location('palTextureWidth')
|
||||
self.grain_strength_uniform = self.shader.get_uniform_location('grainStrength')
|
||||
self.alpha_uniform = self.shader.get_uniform_location('alpha')
|
||||
self.brightness_uniform = self.shader.get_uniform_location('brightness')
|
||||
self.bg_alpha_uniform = self.shader.get_uniform_location('bgColorAlpha')
|
||||
self.shader = self.app.sl.new_shader(
|
||||
self.vert_shader_source, self.frag_shader_source
|
||||
)
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location("projection")
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location("view")
|
||||
self.position_uniform = self.shader.get_uniform_location("objectPosition")
|
||||
self.scale_uniform = self.shader.get_uniform_location("objectScale")
|
||||
self.charset_width_uniform = self.shader.get_uniform_location("charMapWidth")
|
||||
self.charset_height_uniform = self.shader.get_uniform_location("charMapHeight")
|
||||
self.char_uv_width_uniform = self.shader.get_uniform_location("charUVWidth")
|
||||
self.char_uv_height_uniform = self.shader.get_uniform_location("charUVHeight")
|
||||
self.charset_tex_uniform = self.shader.get_uniform_location("charset")
|
||||
self.palette_tex_uniform = self.shader.get_uniform_location("palette")
|
||||
self.grain_tex_uniform = self.shader.get_uniform_location("grain")
|
||||
self.palette_width_uniform = self.shader.get_uniform_location("palTextureWidth")
|
||||
self.grain_strength_uniform = self.shader.get_uniform_location("grainStrength")
|
||||
self.alpha_uniform = self.shader.get_uniform_location("alpha")
|
||||
self.brightness_uniform = self.shader.get_uniform_location("brightness")
|
||||
self.bg_alpha_uniform = self.shader.get_uniform_location("bgColorAlpha")
|
||||
self.create_buffers()
|
||||
# finish
|
||||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(0)
|
||||
if self.log_create_destroy:
|
||||
self.app.log('created: %s' % self)
|
||||
|
||||
self.app.log("created: %s" % self)
|
||||
|
||||
def __str__(self):
|
||||
"for debug purposes, return a concise unique name"
|
||||
for i,r in enumerate(self.art.renderables):
|
||||
for i, r in enumerate(self.art.renderables):
|
||||
if r is self:
|
||||
break
|
||||
return '%s %s %s' % (self.art.get_simple_name(), self.__class__.__name__, i)
|
||||
|
||||
return "%s %s %s" % (self.art.get_simple_name(), self.__class__.__name__, i)
|
||||
|
||||
def create_buffers(self):
|
||||
# vertex positions and elements
|
||||
# determine vertex count needed for render
|
||||
self.vert_count = int(len(self.art.elem_array))
|
||||
self.vert_buffer, self.elem_buffer = GL.glGenBuffers(2)
|
||||
self.update_buffer(self.vert_buffer, self.art.vert_array,
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_STATIC_DRAW, GL.GL_FLOAT, 'vertPosition', VERT_LENGTH)
|
||||
self.update_buffer(self.elem_buffer, self.art.elem_array,
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER, GL.GL_STATIC_DRAW, GL.GL_UNSIGNED_INT, None, None)
|
||||
self.update_buffer(
|
||||
self.vert_buffer,
|
||||
self.art.vert_array,
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_STATIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
"vertPosition",
|
||||
VERT_LENGTH,
|
||||
)
|
||||
self.update_buffer(
|
||||
self.elem_buffer,
|
||||
self.art.elem_array,
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER,
|
||||
GL.GL_STATIC_DRAW,
|
||||
GL.GL_UNSIGNED_INT,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
# tile data buffers
|
||||
# use GL_DYNAMIC_DRAW given they change every time a char/color changes
|
||||
self.char_buffer, self.uv_buffer = GL.glGenBuffers(2)
|
||||
# character indices (which become vertex UVs)
|
||||
self.update_buffer(self.char_buffer, self.art.chars[self.frame],
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_DYNAMIC_DRAW, GL.GL_FLOAT, 'charIndex', 1)
|
||||
self.update_buffer(
|
||||
self.char_buffer,
|
||||
self.art.chars[self.frame],
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
"charIndex",
|
||||
1,
|
||||
)
|
||||
# UV "mods" - modify UV derived from character index
|
||||
self.update_buffer(self.uv_buffer, self.art.uv_mods[self.frame],
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_DYNAMIC_DRAW, GL.GL_FLOAT, 'uvMod', 2)
|
||||
self.update_buffer(
|
||||
self.uv_buffer,
|
||||
self.art.uv_mods[self.frame],
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
"uvMod",
|
||||
2,
|
||||
)
|
||||
self.fg_buffer, self.bg_buffer = GL.glGenBuffers(2)
|
||||
# foreground/background color indices (which become rgba colors)
|
||||
self.update_buffer(self.fg_buffer, self.art.fg_colors[self.frame],
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_DYNAMIC_DRAW, GL.GL_FLOAT, 'fgColorIndex', 1)
|
||||
self.update_buffer(self.bg_buffer, self.art.bg_colors[self.frame],
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_DYNAMIC_DRAW, GL.GL_FLOAT, 'bgColorIndex', 1)
|
||||
|
||||
self.update_buffer(
|
||||
self.fg_buffer,
|
||||
self.art.fg_colors[self.frame],
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
"fgColorIndex",
|
||||
1,
|
||||
)
|
||||
self.update_buffer(
|
||||
self.bg_buffer,
|
||||
self.art.bg_colors[self.frame],
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
"bgColorIndex",
|
||||
1,
|
||||
)
|
||||
|
||||
def update_geo_buffers(self):
|
||||
self.update_buffer(self.vert_buffer, self.art.vert_array, GL.GL_ARRAY_BUFFER, GL.GL_STATIC_DRAW, GL.GL_FLOAT, None, None)
|
||||
self.update_buffer(self.elem_buffer, self.art.elem_array, GL.GL_ELEMENT_ARRAY_BUFFER, GL.GL_STATIC_DRAW, GL.GL_UNSIGNED_INT, None, None)
|
||||
self.update_buffer(
|
||||
self.vert_buffer,
|
||||
self.art.vert_array,
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_STATIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
self.update_buffer(
|
||||
self.elem_buffer,
|
||||
self.art.elem_array,
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER,
|
||||
GL.GL_STATIC_DRAW,
|
||||
GL.GL_UNSIGNED_INT,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
# total vertex count probably changed
|
||||
self.vert_count = int(len(self.art.elem_array))
|
||||
|
||||
|
||||
def update_tile_buffers(self, update_chars, update_uvs, update_fg, update_bg):
|
||||
"Update GL data arrays for tile characters, fg/bg colors, transforms."
|
||||
updates = {}
|
||||
|
|
@ -143,32 +207,58 @@ class TileRenderable:
|
|||
if update_bg:
|
||||
updates[self.bg_buffer] = self.art.bg_colors
|
||||
for update in updates:
|
||||
self.update_buffer(update, updates[update][self.frame],
|
||||
GL.GL_ARRAY_BUFFER, GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT, None, None)
|
||||
|
||||
def update_buffer(self, buffer_index, array, target, buffer_type, data_type,
|
||||
attrib_name, attrib_size):
|
||||
self.update_buffer(
|
||||
update,
|
||||
updates[update][self.frame],
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
GL.GL_DYNAMIC_DRAW,
|
||||
GL.GL_FLOAT,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
def update_buffer(
|
||||
self,
|
||||
buffer_index,
|
||||
array,
|
||||
target,
|
||||
buffer_type,
|
||||
data_type,
|
||||
attrib_name,
|
||||
attrib_size,
|
||||
):
|
||||
if self.log_buffer_updates:
|
||||
self.app.log('update_buffer: %s, %s, %s, %s, %s, %s, %s' % (buffer_index, array, target, buffer_type, data_type, attrib_name, attrib_size))
|
||||
self.app.log(
|
||||
"update_buffer: %s, %s, %s, %s, %s, %s, %s"
|
||||
% (
|
||||
buffer_index,
|
||||
array,
|
||||
target,
|
||||
buffer_type,
|
||||
data_type,
|
||||
attrib_name,
|
||||
attrib_size,
|
||||
)
|
||||
)
|
||||
GL.glBindBuffer(target, buffer_index)
|
||||
GL.glBufferData(target, array.nbytes, array, buffer_type)
|
||||
if attrib_name:
|
||||
attrib = self.shader.get_attrib_location(attrib_name)
|
||||
GL.glEnableVertexAttribArray(attrib)
|
||||
GL.glVertexAttribPointer(attrib, attrib_size, data_type,
|
||||
GL.GL_FALSE, 0, ctypes.c_void_p(0))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib, attrib_size, data_type, GL.GL_FALSE, 0, ctypes.c_void_p(0)
|
||||
)
|
||||
# unbind each buffer before binding next
|
||||
GL.glBindBuffer(target, 0)
|
||||
|
||||
|
||||
def advance_frame(self):
|
||||
"Advance to our Art's next animation frame."
|
||||
self.set_frame(self.frame + 1)
|
||||
|
||||
|
||||
def rewind_frame(self):
|
||||
"Rewind to our Art's previous animation frame."
|
||||
self.set_frame(self.frame - 1)
|
||||
|
||||
|
||||
def set_frame(self, new_frame_index):
|
||||
"Set us to display our Art's given animation frame."
|
||||
if new_frame_index == self.frame:
|
||||
|
|
@ -177,20 +267,22 @@ class TileRenderable:
|
|||
self.frame = new_frame_index % self.art.frames
|
||||
self.update_tile_buffers(True, True, True, True)
|
||||
if self.log_animation:
|
||||
self.app.log('%s animating from frames %s to %s' % (self, old_frame, self.frame))
|
||||
|
||||
self.app.log(
|
||||
"%s animating from frames %s to %s" % (self, old_frame, self.frame)
|
||||
)
|
||||
|
||||
def start_animating(self):
|
||||
"Start animation playback."
|
||||
self.animating = True
|
||||
self.anim_timer = 0
|
||||
|
||||
|
||||
def stop_animating(self):
|
||||
"Pause animation playback on current frame (in game mode)."
|
||||
self.animating = False
|
||||
# restore to active frame if stopping
|
||||
if not self.app.game_mode:
|
||||
self.set_frame(self.art.active_frame)
|
||||
|
||||
|
||||
def set_art(self, new_art):
|
||||
"Display and bind to given Art."
|
||||
if self.art:
|
||||
|
|
@ -202,12 +294,12 @@ class TileRenderable:
|
|||
self.frame %= self.art.frames
|
||||
self.update_geo_buffers()
|
||||
self.update_tile_buffers(True, True, True, True)
|
||||
#print('%s now uses Art %s' % (self, self.art.filename))
|
||||
|
||||
# print('%s now uses Art %s' % (self, self.art.filename))
|
||||
|
||||
def reset_size(self):
|
||||
self.width = self.art.width * self.art.quad_width * abs(self.scale_x)
|
||||
self.height = self.art.height * self.art.quad_height * self.scale_y
|
||||
|
||||
|
||||
def move_to(self, x, y, z, travel_time=None):
|
||||
"""
|
||||
Start simple linear interpolation to given destination over given time.
|
||||
|
|
@ -220,20 +312,22 @@ class TileRenderable:
|
|||
dx = x - self.x
|
||||
dy = y - self.y
|
||||
dz = z - self.z
|
||||
dist = math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
|
||||
dist = math.sqrt(dx**2 + dy**2 + dz**2)
|
||||
self.move_rate = dist / frames
|
||||
else:
|
||||
self.move_rate = self.default_move_rate
|
||||
self.ui_moving = True
|
||||
self.goal_x, self.goal_y, self.goal_z = x, y, z
|
||||
if self.log_animation:
|
||||
self.app.log('%s will move to %s,%s' % (self.art.filename, self.goal_x, self.goal_y))
|
||||
|
||||
self.app.log(
|
||||
"%s will move to %s,%s" % (self.art.filename, self.goal_x, self.goal_y)
|
||||
)
|
||||
|
||||
def snap_to(self, x, y, z):
|
||||
self.x, self.y, self.z = x, y, z
|
||||
self.goal_x, self.goal_y, self.goal_z = x, y, z
|
||||
self.ui_moving = False
|
||||
|
||||
|
||||
def update_transform_from_object(self, obj):
|
||||
"Update our position & scale based on that of given game object."
|
||||
self.z = obj.z
|
||||
|
|
@ -250,14 +344,14 @@ class TileRenderable:
|
|||
if obj.flip_x:
|
||||
self.scale_x *= -1
|
||||
self.scale_z = obj.scale_z
|
||||
|
||||
|
||||
def update_loc(self):
|
||||
# TODO: probably time to bust out the ol' vector module for this stuff
|
||||
# get delta
|
||||
dx = self.goal_x - self.x
|
||||
dy = self.goal_y - self.y
|
||||
dz = self.goal_z - self.z
|
||||
dist = math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
|
||||
dist = math.sqrt(dx**2 + dy**2 + dz**2)
|
||||
# close enough?
|
||||
if dist <= self.move_rate:
|
||||
self.x = self.goal_x
|
||||
|
|
@ -273,8 +367,8 @@ class TileRenderable:
|
|||
self.x += self.move_rate * dir_x
|
||||
self.y += self.move_rate * dir_y
|
||||
self.z += self.move_rate * dir_z
|
||||
#self.app.log('%s moved to %s,%s' % (self, self.x, self.y))
|
||||
|
||||
# self.app.log('%s moved to %s,%s' % (self, self.x, self.y))
|
||||
|
||||
def update(self):
|
||||
if self.go:
|
||||
self.update_transform_from_object(self.go)
|
||||
|
|
@ -297,31 +391,41 @@ class TileRenderable:
|
|||
# TODO: if new_frame < self.frame, count anim loop?
|
||||
self.set_frame(new_frame)
|
||||
self.last_frame_time = self.app.get_elapsed_time()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.app.use_vao:
|
||||
GL.glDeleteVertexArrays(1, [self.vao])
|
||||
GL.glDeleteBuffers(6, [self.vert_buffer, self.elem_buffer, self.char_buffer, self.uv_buffer, self.fg_buffer, self.bg_buffer])
|
||||
GL.glDeleteBuffers(
|
||||
6,
|
||||
[
|
||||
self.vert_buffer,
|
||||
self.elem_buffer,
|
||||
self.char_buffer,
|
||||
self.uv_buffer,
|
||||
self.fg_buffer,
|
||||
self.bg_buffer,
|
||||
],
|
||||
)
|
||||
if self.art and self in self.art.renderables:
|
||||
self.art.renderables.remove(self)
|
||||
if self.log_create_destroy:
|
||||
self.app.log('destroyed: %s' % self)
|
||||
|
||||
self.app.log("destroyed: %s" % self)
|
||||
|
||||
def get_projection_matrix(self):
|
||||
"""
|
||||
UIRenderable overrides this so it doesn't have to override
|
||||
Renderable.render and duplicate lots of code.
|
||||
"""
|
||||
return np.eye(4, 4) if self.exporting else self.camera.projection_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return np.eye(4, 4) if self.exporting else self.camera.view_matrix
|
||||
|
||||
|
||||
def get_loc(self):
|
||||
"Returns world space location as (x, y, z) tuple."
|
||||
export_loc = (-1, 1, 0)
|
||||
return export_loc if self.exporting else (self.x, self.y, self.z)
|
||||
|
||||
|
||||
def get_scale(self):
|
||||
"Returns world space scale as (x, y, z) tuple."
|
||||
if not self.exporting:
|
||||
|
|
@ -329,7 +433,7 @@ class TileRenderable:
|
|||
x = 2 / (self.art.width * self.art.quad_width)
|
||||
y = 2 / (self.art.height * self.art.quad_height)
|
||||
return (x, y, 1)
|
||||
|
||||
|
||||
def render_frame_for_export(self, frame):
|
||||
self.exporting = True
|
||||
self.set_frame(frame)
|
||||
|
|
@ -344,7 +448,7 @@ class TileRenderable:
|
|||
self.render()
|
||||
self.art.app.inactive_layer_visibility = ilv
|
||||
self.exporting = False
|
||||
|
||||
|
||||
def render(self, layers=None, z_override=None, brightness=1.0):
|
||||
"""
|
||||
Render given list of layers at given Z depth.
|
||||
|
|
@ -374,10 +478,12 @@ class TileRenderable:
|
|||
GL.glUniform1f(self.palette_width_uniform, MAX_COLORS)
|
||||
GL.glUniform1f(self.grain_strength_uniform, self.grain_strength)
|
||||
# camera uniforms
|
||||
GL.glUniformMatrix4fv(self.proj_matrix_uniform, 1, GL.GL_FALSE,
|
||||
self.get_projection_matrix())
|
||||
GL.glUniformMatrix4fv(self.view_matrix_uniform, 1, GL.GL_FALSE,
|
||||
self.get_view_matrix())
|
||||
GL.glUniformMatrix4fv(
|
||||
self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix()
|
||||
)
|
||||
GL.glUniformMatrix4fv(
|
||||
self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix()
|
||||
)
|
||||
# TODO: determine if cost of setting all above uniforms for each
|
||||
# Renderable is significant enough to warrant opti where they're set once
|
||||
GL.glUniform1f(self.bg_alpha_uniform, self.bg_alpha)
|
||||
|
|
@ -387,29 +493,39 @@ class TileRenderable:
|
|||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(self.vao)
|
||||
else:
|
||||
attrib = self.shader.get_attrib_location # for brevity
|
||||
attrib = self.shader.get_attrib_location # for brevity
|
||||
vp = ctypes.c_void_p(0)
|
||||
# bind each buffer and set its attrib:
|
||||
# verts
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glVertexAttribPointer(attrib('vertPosition'), VERT_LENGTH, GL.GL_FLOAT, GL.GL_FALSE, 0, vp)
|
||||
GL.glEnableVertexAttribArray(attrib('vertPosition'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("vertPosition"), VERT_LENGTH, GL.GL_FLOAT, GL.GL_FALSE, 0, vp
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("vertPosition"))
|
||||
# chars
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.char_buffer)
|
||||
GL.glVertexAttribPointer(attrib('charIndex'), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp)
|
||||
GL.glEnableVertexAttribArray(attrib('charIndex'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("charIndex"), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("charIndex"))
|
||||
# uvs
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.uv_buffer)
|
||||
GL.glVertexAttribPointer(attrib('uvMod'), 2, GL.GL_FLOAT, GL.GL_FALSE, 0, vp)
|
||||
GL.glEnableVertexAttribArray(attrib('uvMod'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("uvMod"), 2, GL.GL_FLOAT, GL.GL_FALSE, 0, vp
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("uvMod"))
|
||||
# fg colors
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.fg_buffer)
|
||||
GL.glVertexAttribPointer(attrib('fgColorIndex'), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp)
|
||||
GL.glEnableVertexAttribArray(attrib('fgColorIndex'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("fgColorIndex"), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("fgColorIndex"))
|
||||
# bg colors
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.bg_buffer)
|
||||
GL.glVertexAttribPointer(attrib('bgColorIndex'), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp)
|
||||
GL.glEnableVertexAttribArray(attrib('bgColorIndex'))
|
||||
GL.glVertexAttribPointer(
|
||||
attrib("bgColorIndex"), 1, GL.GL_FLOAT, GL.GL_FALSE, 0, vp
|
||||
)
|
||||
GL.glEnableVertexAttribArray(attrib("bgColorIndex"))
|
||||
# finally, bind element buffer
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||
GL.glEnable(GL.GL_BLEND)
|
||||
|
|
@ -430,8 +546,14 @@ class TileRenderable:
|
|||
layer_start = i * layer_size
|
||||
layer_end = layer_start + layer_size
|
||||
# for active art, dim all but active layer based on UI setting
|
||||
if not self.app.game_mode and self.art is self.app.ui.active_art and i != self.art.active_layer:
|
||||
GL.glUniform1f(self.alpha_uniform, self.alpha * self.app.inactive_layer_visibility)
|
||||
if (
|
||||
not self.app.game_mode
|
||||
and self.art is self.app.ui.active_art
|
||||
and i != self.art.active_layer
|
||||
):
|
||||
GL.glUniform1f(
|
||||
self.alpha_uniform, self.alpha * self.app.inactive_layer_visibility
|
||||
)
|
||||
else:
|
||||
GL.glUniform1f(self.alpha_uniform, self.alpha)
|
||||
# use position offset instead of baked-in Z for layers - this
|
||||
|
|
@ -442,8 +564,12 @@ class TileRenderable:
|
|||
z += self.art.layers_z[i]
|
||||
z = z_override if z_override else z
|
||||
GL.glUniform3f(self.position_uniform, x, y, z)
|
||||
GL.glDrawElements(GL.GL_TRIANGLES, layer_size, GL.GL_UNSIGNED_INT,
|
||||
ctypes.c_void_p(layer_start * ctypes.sizeof(ctypes.c_uint)))
|
||||
GL.glDrawElements(
|
||||
GL.GL_TRIANGLES,
|
||||
layer_size,
|
||||
GL.GL_UNSIGNED_INT,
|
||||
ctypes.c_void_p(layer_start * ctypes.sizeof(ctypes.c_uint)),
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
GL.glDisable(GL.GL_BLEND)
|
||||
if self.app.use_vao:
|
||||
|
|
@ -452,23 +578,21 @@ class TileRenderable:
|
|||
|
||||
|
||||
class OnionTileRenderable(TileRenderable):
|
||||
|
||||
"TileRenderable subclass used for onion skin display in Art Mode animation."
|
||||
|
||||
|
||||
# never animate
|
||||
def start_animating(self):
|
||||
pass
|
||||
|
||||
|
||||
def stop_animating(self):
|
||||
pass
|
||||
|
||||
|
||||
class GameObjectRenderable(TileRenderable):
|
||||
|
||||
"""
|
||||
TileRenderable subclass used by GameObjects. Almost no custom logic for now.
|
||||
"""
|
||||
|
||||
|
||||
def get_loc(self):
|
||||
"""
|
||||
Returns world space location as (x, y, z) tuple, offset by our
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import math, time, ctypes, platform
|
||||
import ctypes
|
||||
import math
|
||||
import platform
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from OpenGL import GL
|
||||
|
||||
from renderable import TileRenderable
|
||||
|
||||
class LineRenderable():
|
||||
|
||||
|
||||
class LineRenderable:
|
||||
"Renderable comprised of GL_LINES"
|
||||
|
||||
vert_shader_source = 'lines_v.glsl'
|
||||
vert_shader_source_3d = 'lines_3d_v.glsl'
|
||||
frag_shader_source = 'lines_f.glsl'
|
||||
|
||||
vert_shader_source = "lines_v.glsl"
|
||||
vert_shader_source_3d = "lines_3d_v.glsl"
|
||||
frag_shader_source = "lines_f.glsl"
|
||||
log_create_destroy = False
|
||||
line_width = 1
|
||||
# items in vert array: 2 for XY-only renderables, 3 for ones that include Z
|
||||
|
|
@ -17,12 +22,12 @@ class LineRenderable():
|
|||
# use game object's art_off_pct values
|
||||
use_art_offset = True
|
||||
visible = True
|
||||
|
||||
|
||||
def __init__(self, app, quad_size_ref=None, game_object=None):
|
||||
self.app = app
|
||||
# we may be attached to a game object
|
||||
self.go = game_object
|
||||
self.unique_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
self.unique_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
self.quad_size_ref = quad_size_ref
|
||||
self.x, self.y, self.z = 0, 0, 0
|
||||
self.scale_x, self.scale_y = 1, 1
|
||||
|
|
@ -36,123 +41,155 @@ class LineRenderable():
|
|||
GL.glBindVertexArray(self.vao)
|
||||
if self.vert_items == 3:
|
||||
self.vert_shader_source = self.vert_shader_source_3d
|
||||
self.shader = self.app.sl.new_shader(self.vert_shader_source, self.frag_shader_source)
|
||||
self.shader = self.app.sl.new_shader(
|
||||
self.vert_shader_source, self.frag_shader_source
|
||||
)
|
||||
# uniforms
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location('projection')
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location('view')
|
||||
self.position_uniform = self.shader.get_uniform_location('objectPosition')
|
||||
self.scale_uniform = self.shader.get_uniform_location('objectScale')
|
||||
self.quad_size_uniform = self.shader.get_uniform_location('quadSize')
|
||||
self.color_uniform = self.shader.get_uniform_location('objectColor')
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location("projection")
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location("view")
|
||||
self.position_uniform = self.shader.get_uniform_location("objectPosition")
|
||||
self.scale_uniform = self.shader.get_uniform_location("objectScale")
|
||||
self.quad_size_uniform = self.shader.get_uniform_location("quadSize")
|
||||
self.color_uniform = self.shader.get_uniform_location("objectColor")
|
||||
# vert buffers
|
||||
self.vert_buffer, self.elem_buffer = GL.glGenBuffers(2)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vert_array.nbytes,
|
||||
self.vert_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.vert_array.nbytes,
|
||||
self.vert_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_array.nbytes,
|
||||
self.elem_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER,
|
||||
self.elem_array.nbytes,
|
||||
self.elem_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
self.vert_count = int(len(self.elem_array))
|
||||
self.pos_attrib = self.shader.get_attrib_location('vertPosition')
|
||||
self.pos_attrib = self.shader.get_attrib_location("vertPosition")
|
||||
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||
offset = ctypes.c_void_p(0)
|
||||
GL.glVertexAttribPointer(self.pos_attrib, self.vert_items,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||
GL.glVertexAttribPointer(
|
||||
self.pos_attrib, self.vert_items, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
# vert colors
|
||||
self.color_buffer = GL.glGenBuffers(1)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.color_array.nbytes,
|
||||
self.color_array, GL.GL_STATIC_DRAW)
|
||||
self.color_attrib = self.shader.get_attrib_location('vertColor')
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.color_array.nbytes,
|
||||
self.color_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
self.color_attrib = self.shader.get_attrib_location("vertColor")
|
||||
GL.glEnableVertexAttribArray(self.color_attrib)
|
||||
GL.glVertexAttribPointer(self.color_attrib, 4,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||
GL.glVertexAttribPointer(
|
||||
self.color_attrib, 4, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(0)
|
||||
if self.log_create_destroy:
|
||||
self.app.log('created: %s' % self)
|
||||
|
||||
self.app.log("created: %s" % self)
|
||||
|
||||
def __str__(self):
|
||||
"for debug purposes, return a unique name"
|
||||
return self.unique_name
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
"""
|
||||
create self.vert_array, self.elem_array, self.color_array
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
pass
|
||||
|
||||
|
||||
def update(self):
|
||||
if self.go:
|
||||
self.update_transform_from_object(self.go)
|
||||
|
||||
|
||||
def reset_size(self):
|
||||
self.width, self.height = self.get_size()
|
||||
|
||||
|
||||
def update_transform_from_object(self, obj):
|
||||
TileRenderable.update_transform_from_object(self, obj)
|
||||
|
||||
|
||||
def rebind_buffers(self):
|
||||
# resend verts
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vert_array.nbytes,
|
||||
self.vert_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.vert_array.nbytes,
|
||||
self.vert_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||
GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_array.nbytes,
|
||||
self.elem_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ELEMENT_ARRAY_BUFFER,
|
||||
self.elem_array.nbytes,
|
||||
self.elem_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
self.vert_count = int(len(self.elem_array))
|
||||
# resend color
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.color_array.nbytes,
|
||||
self.color_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.color_array.nbytes,
|
||||
self.color_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return np.eye(4, 4)
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return np.eye(4, 4)
|
||||
|
||||
|
||||
def get_loc(self):
|
||||
return self.x, self.y, self.z
|
||||
|
||||
|
||||
def get_size(self):
|
||||
# overriden in subclasses that need specific width/height data
|
||||
return 1, 1
|
||||
|
||||
|
||||
def get_quad_size(self):
|
||||
if self.quad_size_ref:
|
||||
return self.quad_size_ref.quad_width, self.quad_size_ref.quad_height
|
||||
else:
|
||||
return 1, 1
|
||||
|
||||
|
||||
def get_color(self):
|
||||
return (1, 1, 1, 1)
|
||||
|
||||
|
||||
def get_line_width(self):
|
||||
return self.line_width
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.app.use_vao:
|
||||
GL.glDeleteVertexArrays(1, [self.vao])
|
||||
GL.glDeleteBuffers(3, [self.vert_buffer, self.elem_buffer, self.color_buffer])
|
||||
if self.log_create_destroy:
|
||||
self.app.log('destroyed: %s' % self)
|
||||
|
||||
self.app.log("destroyed: %s" % self)
|
||||
|
||||
def render(self):
|
||||
if not self.visible:
|
||||
return
|
||||
GL.glUseProgram(self.shader.program)
|
||||
GL.glUniformMatrix4fv(self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix())
|
||||
GL.glUniformMatrix4fv(self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix())
|
||||
GL.glUniformMatrix4fv(
|
||||
self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix()
|
||||
)
|
||||
GL.glUniformMatrix4fv(
|
||||
self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix()
|
||||
)
|
||||
GL.glUniform3f(self.position_uniform, *self.get_loc())
|
||||
GL.glUniform3f(self.scale_uniform, self.scale_x, self.scale_y, self.scale_z)
|
||||
GL.glUniform2f(self.quad_size_uniform, *self.get_quad_size())
|
||||
|
|
@ -165,22 +202,28 @@ class LineRenderable():
|
|||
# attribs:
|
||||
# pos
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glVertexAttribPointer(self.pos_attrib, self.vert_items,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0))
|
||||
GL.glVertexAttribPointer(
|
||||
self.pos_attrib,
|
||||
self.vert_items,
|
||||
GL.GL_FLOAT,
|
||||
GL.GL_FALSE,
|
||||
0,
|
||||
ctypes.c_void_p(0),
|
||||
)
|
||||
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||
# color
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.color_buffer)
|
||||
GL.glVertexAttribPointer(self.color_attrib, 4,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||
GL.glVertexAttribPointer(
|
||||
self.color_attrib, 4, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
|
||||
)
|
||||
GL.glEnableVertexAttribArray(self.color_attrib)
|
||||
# bind elem array - see similar behavior in Cursor.render
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self.elem_buffer)
|
||||
GL.glEnable(GL.GL_BLEND)
|
||||
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
|
||||
if platform.system() != 'Darwin':
|
||||
if platform.system() != "Darwin":
|
||||
GL.glLineWidth(self.get_line_width())
|
||||
GL.glDrawElements(GL.GL_LINES, self.vert_count,
|
||||
GL.GL_UNSIGNED_INT, None)
|
||||
GL.glDrawElements(GL.GL_LINES, self.vert_count, GL.GL_UNSIGNED_INT, None)
|
||||
GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0)
|
||||
GL.glDisable(GL.GL_BLEND)
|
||||
if self.app.use_vao:
|
||||
|
|
@ -191,6 +234,7 @@ class LineRenderable():
|
|||
# common data/code used by various boxes
|
||||
BOX_VERTS = [(0, 0), (1, 0), (1, -1), (0, -1)]
|
||||
|
||||
|
||||
def get_box_arrays(vert_list=None, color=(1, 1, 1, 1)):
|
||||
verts = np.array(vert_list or BOX_VERTS, dtype=np.float32)
|
||||
elems = np.array([0, 1, 1, 2, 2, 3, 3, 0], dtype=np.uint32)
|
||||
|
|
@ -199,11 +243,11 @@ def get_box_arrays(vert_list=None, color=(1, 1, 1, 1)):
|
|||
|
||||
|
||||
class UIRenderableX(LineRenderable):
|
||||
|
||||
"Red X used to denote transparent color in various places"
|
||||
|
||||
color = (1, 0, 0, 1)
|
||||
line_width = 2
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
self.vert_array = np.array([(0, 0), (1, 1), (1, 0), (0, 1)], dtype=np.float32)
|
||||
self.elem_array = np.array([0, 1, 2, 3], dtype=np.uint32)
|
||||
|
|
@ -211,55 +255,56 @@ class UIRenderableX(LineRenderable):
|
|||
|
||||
|
||||
class SwatchSelectionBoxRenderable(LineRenderable):
|
||||
|
||||
"used for UI selection boxes etc"
|
||||
|
||||
|
||||
color = (0.5, 0.5, 0.5, 1)
|
||||
line_width = 2
|
||||
|
||||
|
||||
def __init__(self, app, quad_size_ref):
|
||||
LineRenderable.__init__(self, app, quad_size_ref)
|
||||
# track tile X and Y for cursor movement
|
||||
self.tile_x, self.tile_y = 0,0
|
||||
|
||||
self.tile_x, self.tile_y = 0, 0
|
||||
|
||||
def get_color(self):
|
||||
return self.color
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(None, self.color)
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
|
||||
None, self.color
|
||||
)
|
||||
|
||||
|
||||
class ToolSelectionBoxRenderable(LineRenderable):
|
||||
line_width = 2
|
||||
|
||||
|
||||
def get_color(self):
|
||||
return (1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(None)
|
||||
|
||||
|
||||
class WorldLineRenderable(LineRenderable):
|
||||
"any LineRenderable that draws in world, ie in 3D perspective"
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.camera.projection_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.camera.view_matrix
|
||||
|
||||
|
||||
class DebugLineRenderable(WorldLineRenderable):
|
||||
|
||||
"""
|
||||
renderable for drawing debug lines in the world.
|
||||
use set_lines and add_lines to replace and add to, respectively, the list
|
||||
of 3D vertex locations (and, optionally, colors).
|
||||
"""
|
||||
|
||||
|
||||
color = (0.5, 0, 0, 1)
|
||||
vert_items = 3
|
||||
line_width = 3
|
||||
|
||||
|
||||
def set_lines(self, new_verts, new_colors=None):
|
||||
"replace current debug lines with new given lines"
|
||||
self.vert_array = np.array(new_verts, dtype=np.float32)
|
||||
|
|
@ -268,20 +313,21 @@ class DebugLineRenderable(WorldLineRenderable):
|
|||
for i in range(1, len(new_verts)):
|
||||
elements += [i - 1, i]
|
||||
self.elem_array = np.array(elements, dtype=np.uint32)
|
||||
self.color_array = np.array(new_colors or self.color * len(new_verts),
|
||||
dtype=np.float32)
|
||||
self.color_array = np.array(
|
||||
new_colors or self.color * len(new_verts), dtype=np.float32
|
||||
)
|
||||
self.rebind_buffers()
|
||||
|
||||
|
||||
def set_color(self, new_color):
|
||||
"changes all debug lines to given color"
|
||||
self.color = new_color
|
||||
lines = int(len(self.vert_array) / self.vert_items)
|
||||
self.color_array = np.array(self.color * lines, dtype=np.float32)
|
||||
self.rebind_buffers()
|
||||
|
||||
|
||||
def get_quad_size(self):
|
||||
return 1, 1
|
||||
|
||||
|
||||
def add_lines(self, new_verts, new_colors=None):
|
||||
"add lines to the current ones"
|
||||
line_items = len(self.vert_array)
|
||||
|
|
@ -289,7 +335,7 @@ class DebugLineRenderable(WorldLineRenderable):
|
|||
# if new_verts is a list of tuples, unpack into flat list
|
||||
if type(new_verts[0]) is tuple:
|
||||
new_verts_unpacked = []
|
||||
for (x, y, z) in new_verts:
|
||||
for x, y, z in new_verts:
|
||||
new_verts_unpacked += [x, y, z]
|
||||
new_verts = new_verts_unpacked
|
||||
new_size = int(line_items + len(new_verts))
|
||||
|
|
@ -300,24 +346,27 @@ class DebugLineRenderable(WorldLineRenderable):
|
|||
new_elem_size = int(old_elem_size + len(new_verts) / self.vert_items)
|
||||
# TODO: "contiguous" parameter that joins new lines with previous
|
||||
self.elem_array.resize(new_elem_size)
|
||||
self.elem_array[old_elem_size:new_elem_size] = range(old_elem_size,
|
||||
new_elem_size)
|
||||
self.elem_array[old_elem_size:new_elem_size] = range(
|
||||
old_elem_size, new_elem_size
|
||||
)
|
||||
# grow color buffer
|
||||
old_color_size = len(self.color_array)
|
||||
new_color_size = int(old_color_size + len(new_verts) / self.vert_items * 4)
|
||||
self.color_array.resize(new_color_size)
|
||||
self.color_array[old_color_size:new_color_size] = new_colors or self.color * int(len(new_verts) / self.vert_items)
|
||||
self.color_array[old_color_size:new_color_size] = (
|
||||
new_colors or self.color * int(len(new_verts) / self.vert_items)
|
||||
)
|
||||
self.rebind_buffers()
|
||||
|
||||
|
||||
def reset_lines(self):
|
||||
self.build_geo()
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
# start empty
|
||||
self.vert_array = np.array([], dtype=np.float32)
|
||||
self.elem_array = np.array([], dtype=np.uint32)
|
||||
self.color_array = np.array([], dtype=np.float32)
|
||||
|
||||
|
||||
def render(self):
|
||||
# only render if we have any data
|
||||
if len(self.vert_array) == 0:
|
||||
|
|
@ -326,64 +375,73 @@ class DebugLineRenderable(WorldLineRenderable):
|
|||
|
||||
|
||||
class OriginIndicatorRenderable(WorldLineRenderable):
|
||||
|
||||
"classic 3-axis thingy showing location/rotation/scale"
|
||||
|
||||
red = (1.0, 0.1, 0.1, 1.0)
|
||||
|
||||
red = (1.0, 0.1, 0.1, 1.0)
|
||||
green = (0.1, 1.0, 0.1, 1.0)
|
||||
blue = (0.1, 0.1, 1.0, 1.0)
|
||||
blue = (0.1, 0.1, 1.0, 1.0)
|
||||
origin = (0, 0, 0)
|
||||
x_axis = (1, 0, 0)
|
||||
y_axis = (0, 1, 0)
|
||||
z_axis = (0, 0, 1)
|
||||
vert_items = 3
|
||||
vert_items = 3
|
||||
line_width = 3
|
||||
use_art_offset = False
|
||||
|
||||
|
||||
def __init__(self, app, game_object):
|
||||
LineRenderable.__init__(self, app, None, game_object)
|
||||
|
||||
|
||||
def get_quad_size(self):
|
||||
return 1, 1
|
||||
|
||||
|
||||
def get_size(self):
|
||||
return self.go.scale_x, self.go.scale_y
|
||||
|
||||
|
||||
def update_transform_from_object(self, obj):
|
||||
self.x, self.y, self.z = obj.x, obj.y, obj.z
|
||||
self.scale_x, self.scale_y = obj.scale_x, obj.scale_y
|
||||
if obj.flip_x:
|
||||
self.scale_x *= -1
|
||||
self.scale_z = obj.scale_z
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
self.vert_array = np.array([self.origin, self.x_axis,
|
||||
self.origin, self.y_axis,
|
||||
self.origin, self.z_axis],
|
||||
dtype=np.float32)
|
||||
self.vert_array = np.array(
|
||||
[
|
||||
self.origin,
|
||||
self.x_axis,
|
||||
self.origin,
|
||||
self.y_axis,
|
||||
self.origin,
|
||||
self.z_axis,
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
self.elem_array = np.array([0, 1, 2, 3, 4, 5], dtype=np.uint32)
|
||||
self.color_array = np.array([self.red, self.red, self.green, self.green,
|
||||
self.blue, self.blue], dtype=np.float32)
|
||||
self.color_array = np.array(
|
||||
[self.red, self.red, self.green, self.green, self.blue, self.blue],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
|
||||
class BoundsIndicatorRenderable(WorldLineRenderable):
|
||||
color = (1, 1, 1, 0.5)
|
||||
line_width_active = 2
|
||||
line_width_inactive = 1
|
||||
|
||||
|
||||
def __init__(self, app, game_object):
|
||||
self.art = game_object.renderable.art
|
||||
LineRenderable.__init__(self, app, None, game_object)
|
||||
|
||||
|
||||
def set_art(self, new_art):
|
||||
self.art = new_art
|
||||
self.reset_size()
|
||||
|
||||
|
||||
def get_size(self):
|
||||
art = self.go.art
|
||||
w = (art.width * art.quad_width) * self.go.scale_x
|
||||
h = (art.height * art.quad_height) * self.go.scale_y
|
||||
return w, h
|
||||
|
||||
|
||||
def get_color(self):
|
||||
# pulse if selected
|
||||
if self.go in self.app.gw.selected_objects:
|
||||
|
|
@ -391,33 +449,41 @@ class BoundsIndicatorRenderable(WorldLineRenderable):
|
|||
return (color, color, color, 1)
|
||||
else:
|
||||
return (1, 1, 1, 1)
|
||||
|
||||
|
||||
def get_line_width(self):
|
||||
return self.line_width_active if self.go in self.app.gw.selected_objects else self.line_width_inactive
|
||||
|
||||
return (
|
||||
self.line_width_active
|
||||
if self.go in self.app.gw.selected_objects
|
||||
else self.line_width_inactive
|
||||
)
|
||||
|
||||
def get_quad_size(self):
|
||||
if not self.go:
|
||||
return 1, 1
|
||||
return self.art.width * self.art.quad_width, self.art.height * self.art.quad_height
|
||||
|
||||
return (
|
||||
self.art.width * self.art.quad_width,
|
||||
self.art.height * self.art.quad_height,
|
||||
)
|
||||
|
||||
def build_geo(self):
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(None, self.color)
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
|
||||
None, self.color
|
||||
)
|
||||
|
||||
|
||||
class CollisionRenderable(WorldLineRenderable):
|
||||
|
||||
# green = dynamic, blue = static
|
||||
dynamic_color = (0, 1, 0, 1)
|
||||
static_color = (0, 0, 1, 1)
|
||||
|
||||
|
||||
def __init__(self, shape):
|
||||
self.color = self.dynamic_color if shape.go.is_dynamic() else self.static_color
|
||||
self.shape = shape
|
||||
WorldLineRenderable.__init__(self, shape.go.app, None, shape.go)
|
||||
|
||||
|
||||
def update(self):
|
||||
self.update_transform_from_object(self.shape)
|
||||
|
||||
|
||||
def update_transform_from_object(self, obj):
|
||||
self.x = obj.x
|
||||
self.y = obj.y
|
||||
|
|
@ -435,19 +501,18 @@ def get_circle_points(radius, steps=24):
|
|||
|
||||
|
||||
class CircleCollisionRenderable(CollisionRenderable):
|
||||
|
||||
line_width = 2
|
||||
segments = 24
|
||||
|
||||
|
||||
def get_quad_size(self):
|
||||
return self.shape.radius, self.shape.radius
|
||||
|
||||
|
||||
def get_size(self):
|
||||
w = h = self.shape.radius * 2
|
||||
w *= self.go.scale_x
|
||||
h *= self.go.scale_y
|
||||
return w, h
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
verts, elements, colors = [], [], []
|
||||
angle = 0
|
||||
|
|
@ -460,7 +525,7 @@ class CircleCollisionRenderable(CollisionRenderable):
|
|||
y = math.sin(angle)
|
||||
verts.append((x, y))
|
||||
last_x, last_y = x, y
|
||||
elements.append((i, i+1))
|
||||
elements.append((i, i + 1))
|
||||
i += 2
|
||||
colors.append([self.color * 2])
|
||||
self.vert_array = np.array(verts, dtype=np.float32)
|
||||
|
|
@ -469,26 +534,29 @@ class CircleCollisionRenderable(CollisionRenderable):
|
|||
|
||||
|
||||
class BoxCollisionRenderable(CollisionRenderable):
|
||||
|
||||
line_width = 2
|
||||
|
||||
|
||||
def get_quad_size(self):
|
||||
return self.shape.halfwidth * 2, self.shape.halfheight * 2
|
||||
|
||||
|
||||
def get_size(self):
|
||||
w, h = self.shape.halfwidth * 2, self.shape.halfheight * 2
|
||||
w *= self.go.scale_x
|
||||
h *= self.go.scale_y
|
||||
return w, h
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
verts = [(-0.5, 0.5), (0.5, 0.5), (0.5, -0.5), (-0.5, -0.5)]
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(verts, self.color)
|
||||
self.vert_array, self.elem_array, self.color_array = get_box_arrays(
|
||||
verts, self.color
|
||||
)
|
||||
|
||||
|
||||
class TileBoxCollisionRenderable(BoxCollisionRenderable):
|
||||
"box for each tile in a CST_TILE object"
|
||||
|
||||
line_width = 1
|
||||
|
||||
def get_loc(self):
|
||||
# draw at Z level of collision layer
|
||||
return self.x, self.y, self.go.get_layer_z(self.go.col_layer_name)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
import ctypes, time
|
||||
import numpy as np
|
||||
import ctypes
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from OpenGL import GL
|
||||
from PIL import Image
|
||||
|
||||
from texture import Texture
|
||||
|
||||
|
||||
class SpriteRenderable:
|
||||
|
||||
"basic renderable object using an image for a texture"
|
||||
|
||||
|
||||
vert_array = np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=np.float32)
|
||||
vert_shader_source = 'sprite_v.glsl'
|
||||
frag_shader_source = 'sprite_f.glsl'
|
||||
texture_filename = 'ui/icon.png'
|
||||
vert_shader_source = "sprite_v.glsl"
|
||||
frag_shader_source = "sprite_f.glsl"
|
||||
texture_filename = "ui/icon.png"
|
||||
alpha = 1
|
||||
tex_scale_x, tex_scale_y = 1, 1
|
||||
blend = True
|
||||
flip_y = True
|
||||
tex_wrap = False
|
||||
|
||||
|
||||
def __init__(self, app, texture_filename=None, image_data=None):
|
||||
self.app = app
|
||||
self.unique_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
self.unique_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
self.x, self.y, self.z = self.get_initial_position()
|
||||
self.scale_x, self.scale_y, self.scale_z = self.get_initial_scale()
|
||||
if self.app.use_vao:
|
||||
|
|
@ -33,62 +35,73 @@ class SpriteRenderable:
|
|||
self.texture_filename = texture_filename
|
||||
if not image_data:
|
||||
image_data = Image.open(self.texture_filename)
|
||||
image_data = image_data.convert('RGBA')
|
||||
image_data = image_data.convert("RGBA")
|
||||
if self.flip_y:
|
||||
image_data = image_data.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
w, h = image_data.size
|
||||
self.texture = Texture(image_data.tobytes(), w, h)
|
||||
self.shader = self.app.sl.new_shader(self.vert_shader_source, self.frag_shader_source)
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location('projection')
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location('view')
|
||||
self.position_uniform = self.shader.get_uniform_location('objectPosition')
|
||||
self.scale_uniform = self.shader.get_uniform_location('objectScale')
|
||||
self.tex_uniform = self.shader.get_uniform_location('texture0')
|
||||
self.tex_scale_uniform = self.shader.get_uniform_location('texScale')
|
||||
self.alpha_uniform = self.shader.get_uniform_location('alpha')
|
||||
self.shader = self.app.sl.new_shader(
|
||||
self.vert_shader_source, self.frag_shader_source
|
||||
)
|
||||
self.proj_matrix_uniform = self.shader.get_uniform_location("projection")
|
||||
self.view_matrix_uniform = self.shader.get_uniform_location("view")
|
||||
self.position_uniform = self.shader.get_uniform_location("objectPosition")
|
||||
self.scale_uniform = self.shader.get_uniform_location("objectScale")
|
||||
self.tex_uniform = self.shader.get_uniform_location("texture0")
|
||||
self.tex_scale_uniform = self.shader.get_uniform_location("texScale")
|
||||
self.alpha_uniform = self.shader.get_uniform_location("alpha")
|
||||
self.vert_buffer = GL.glGenBuffers(1)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glBufferData(GL.GL_ARRAY_BUFFER, self.vert_array.nbytes,
|
||||
self.vert_array, GL.GL_STATIC_DRAW)
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
self.vert_array.nbytes,
|
||||
self.vert_array,
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
self.vert_count = 4
|
||||
self.pos_attrib = self.shader.get_attrib_location('vertPosition')
|
||||
self.pos_attrib = self.shader.get_attrib_location("vertPosition")
|
||||
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||
offset = ctypes.c_void_p(0)
|
||||
GL.glVertexAttribPointer(self.pos_attrib, 2,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, offset)
|
||||
GL.glVertexAttribPointer(
|
||||
self.pos_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, offset
|
||||
)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, 0)
|
||||
if self.app.use_vao:
|
||||
GL.glBindVertexArray(0)
|
||||
self.texture.set_wrap(self.tex_wrap)
|
||||
|
||||
|
||||
def get_initial_position(self):
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
def get_initial_scale(self):
|
||||
return 1, 1, 1
|
||||
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.camera.projection_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.camera.view_matrix
|
||||
|
||||
|
||||
def get_texture_scale(self):
|
||||
return self.tex_scale_x, self.tex_scale_y
|
||||
|
||||
|
||||
def destroy(self):
|
||||
if self.app.use_vao:
|
||||
GL.glDeleteVertexArrays(1, [self.vao])
|
||||
GL.glDeleteBuffers(1, [self.vert_buffer])
|
||||
|
||||
|
||||
def render(self):
|
||||
GL.glUseProgram(self.shader.program)
|
||||
GL.glActiveTexture(GL.GL_TEXTURE0)
|
||||
GL.glUniform1i(self.tex_uniform, 0)
|
||||
GL.glUniform2f(self.tex_scale_uniform, *self.get_texture_scale())
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture.gltex)
|
||||
GL.glUniformMatrix4fv(self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix())
|
||||
GL.glUniformMatrix4fv(self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix())
|
||||
GL.glUniformMatrix4fv(
|
||||
self.proj_matrix_uniform, 1, GL.GL_FALSE, self.get_projection_matrix()
|
||||
)
|
||||
GL.glUniformMatrix4fv(
|
||||
self.view_matrix_uniform, 1, GL.GL_FALSE, self.get_view_matrix()
|
||||
)
|
||||
GL.glUniform3f(self.position_uniform, self.x, self.y, self.z)
|
||||
GL.glUniform3f(self.scale_uniform, self.scale_x, self.scale_y, self.scale_z)
|
||||
GL.glUniform1f(self.alpha_uniform, self.alpha)
|
||||
|
|
@ -96,8 +109,9 @@ class SpriteRenderable:
|
|||
GL.glBindVertexArray(self.vao)
|
||||
else:
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vert_buffer)
|
||||
GL.glVertexAttribPointer(self.pos_attrib, 2,
|
||||
GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0))
|
||||
GL.glVertexAttribPointer(
|
||||
self.pos_attrib, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, ctypes.c_void_p(0)
|
||||
)
|
||||
GL.glEnableVertexAttribArray(self.pos_attrib)
|
||||
if self.blend:
|
||||
GL.glEnable(GL.GL_BLEND)
|
||||
|
|
@ -111,10 +125,9 @@ class SpriteRenderable:
|
|||
|
||||
|
||||
class UISpriteRenderable(SpriteRenderable):
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.ui.view_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.ui.view_matrix
|
||||
|
||||
|
|
@ -122,11 +135,11 @@ class UISpriteRenderable(SpriteRenderable):
|
|||
class UIBGTextureRenderable(UISpriteRenderable):
|
||||
alpha = 0.8
|
||||
tex_wrap = True
|
||||
texture_filename = 'ui/bgnoise_alpha.png'
|
||||
texture_filename = "ui/bgnoise_alpha.png"
|
||||
tex_scale_x, tex_scale_y = 8, 8
|
||||
|
||||
|
||||
def get_initial_position(self):
|
||||
return -1, -1, 0
|
||||
|
||||
|
||||
def get_initial_scale(self):
|
||||
return 2, 2, 1
|
||||
|
|
|
|||
29
selection.py
29
selection.py
|
|
@ -1,24 +1,25 @@
|
|||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from renderable_line import LineRenderable
|
||||
|
||||
|
||||
class SelectionRenderable(LineRenderable):
|
||||
|
||||
color = (0.8, 0.8, 0.8, 1)
|
||||
line_width = 2
|
||||
x, y, z = 0, 0, 0
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
# init empty arrays; geo is rebuilt every time selection changes
|
||||
self.vert_array = np.array([], dtype=np.float32)
|
||||
self.elem_array = np.array([], dtype=np.uint32)
|
||||
self.color_array = np.array([], dtype=np.float32)
|
||||
|
||||
|
||||
def get_adjacent_tile(self, tiles, x, y, dir_x, dir_y):
|
||||
"returns True or False based on tile dict lookup relative to given tile"
|
||||
return tiles.get((x + dir_x, y + dir_y), False)
|
||||
|
||||
|
||||
def rebuild_geo(self, tiles):
|
||||
# array source lists of verts, elements, colors
|
||||
v, e, c = [], [], []
|
||||
|
|
@ -31,14 +32,16 @@ class SelectionRenderable(LineRenderable):
|
|||
below = self.get_adjacent_tile(tiles, x, y, 0, 1)
|
||||
left = self.get_adjacent_tile(tiles, x, y, -1, 0)
|
||||
right = self.get_adjacent_tile(tiles, x, y, 1, 0)
|
||||
top_left = ( x, -y)
|
||||
top_right = (x+1, -y)
|
||||
bottom_right = (x+1, -y-1)
|
||||
bottom_left = ( x, -y-1)
|
||||
top_left = (x, -y)
|
||||
top_right = (x + 1, -y)
|
||||
bottom_right = (x + 1, -y - 1)
|
||||
bottom_left = (x, -y - 1)
|
||||
|
||||
def add_line(vert_a, vert_b, verts, elems, colors, element_index):
|
||||
verts += [vert_a, vert_b]
|
||||
elems += [element_index, element_index+1]
|
||||
elems += [element_index, element_index + 1]
|
||||
colors += self.color * 2
|
||||
|
||||
# verts = corners
|
||||
if not above:
|
||||
# top edge
|
||||
|
|
@ -60,16 +63,16 @@ class SelectionRenderable(LineRenderable):
|
|||
self.vert_array = np.array(v, dtype=np.float32)
|
||||
self.elem_array = np.array(e, dtype=np.uint32)
|
||||
self.color_array = np.array(c, dtype=np.float32)
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.camera.projection_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.app.camera.view_matrix
|
||||
|
||||
|
||||
def get_color(self):
|
||||
# pulse for visibility
|
||||
a = 0.75 + (math.sin(self.app.get_elapsed_time() / 100) / 2)
|
||||
|
|
|
|||
88
shader.py
88
shader.py
|
|
@ -1,31 +1,40 @@
|
|||
import os.path, time, platform
|
||||
import os.path
|
||||
import platform
|
||||
import time
|
||||
|
||||
from OpenGL import GL
|
||||
from OpenGL.GL import shaders
|
||||
|
||||
SHADER_PATH = 'shaders/'
|
||||
SHADER_PATH = "shaders/"
|
||||
|
||||
|
||||
class ShaderLord:
|
||||
|
||||
# time in ms between checks for hot reload
|
||||
hot_reload_check_interval = 2 * 1000
|
||||
|
||||
|
||||
def __init__(self, app):
|
||||
"AWAKENS THE SHADERLORD"
|
||||
self.app = app
|
||||
self.shaders = []
|
||||
|
||||
|
||||
def new_shader(self, vert_source_file, frag_source_file):
|
||||
self.last_check = 0
|
||||
for shader in self.shaders:
|
||||
if shader.vert_source_file == vert_source_file and shader.frag_source_file == frag_source_file:
|
||||
#self.app.log('%s already uses same source' % shader)
|
||||
if (
|
||||
shader.vert_source_file == vert_source_file
|
||||
and shader.frag_source_file == frag_source_file
|
||||
):
|
||||
# self.app.log('%s already uses same source' % shader)
|
||||
return shader
|
||||
s = Shader(self, vert_source_file, frag_source_file)
|
||||
self.shaders.append(s)
|
||||
return s
|
||||
|
||||
|
||||
def check_hot_reload(self):
|
||||
if self.app.get_elapsed_time() - self.last_check < self.hot_reload_check_interval:
|
||||
if (
|
||||
self.app.get_elapsed_time() - self.last_check
|
||||
< self.hot_reload_check_interval
|
||||
):
|
||||
return
|
||||
self.last_check = self.app.get_elapsed_time()
|
||||
for shader in self.shaders:
|
||||
|
|
@ -34,14 +43,13 @@ class ShaderLord:
|
|||
shader.recompile(GL.GL_VERTEX_SHADER)
|
||||
if frag_shader_updated:
|
||||
shader.recompile(GL.GL_FRAGMENT_SHADER)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
for shader in self.shaders:
|
||||
shader.destroy()
|
||||
|
||||
|
||||
class Shader:
|
||||
|
||||
log_compile = False
|
||||
"If True, log shader compilation"
|
||||
# per-platform shader versions, declared here for easier CFG fiddling
|
||||
|
|
@ -49,7 +57,7 @@ class Shader:
|
|||
glsl_version_unix = 130
|
||||
glsl_version_macos = 150
|
||||
glsl_version_es = 100
|
||||
|
||||
|
||||
def __init__(self, shader_lord, vert_source_file, frag_source_file):
|
||||
self.sl = shader_lord
|
||||
# vertex shader
|
||||
|
|
@ -57,52 +65,62 @@ class Shader:
|
|||
self.last_vert_change = time.time()
|
||||
vert_source = self.get_shader_source(self.vert_source_file)
|
||||
if self.log_compile:
|
||||
self.sl.app.log('Compiling vertex shader %s...' % self.vert_source_file)
|
||||
self.vert_shader = self.try_compile_shader(vert_source, GL.GL_VERTEX_SHADER, self.vert_source_file)
|
||||
self.sl.app.log("Compiling vertex shader %s..." % self.vert_source_file)
|
||||
self.vert_shader = self.try_compile_shader(
|
||||
vert_source, GL.GL_VERTEX_SHADER, self.vert_source_file
|
||||
)
|
||||
if self.log_compile and self.vert_shader:
|
||||
self.sl.app.log('Compiled vertex shader %s in %.6f seconds' % (self.vert_source_file, time.time() - self.last_vert_change))
|
||||
self.sl.app.log(
|
||||
"Compiled vertex shader %s in %.6f seconds"
|
||||
% (self.vert_source_file, time.time() - self.last_vert_change)
|
||||
)
|
||||
# fragment shader
|
||||
self.frag_source_file = frag_source_file
|
||||
self.last_frag_change = time.time()
|
||||
frag_source = self.get_shader_source(self.frag_source_file)
|
||||
if self.log_compile:
|
||||
self.sl.app.log('Compiling fragment shader %s...' % self.frag_source_file)
|
||||
self.frag_shader = self.try_compile_shader(frag_source, GL.GL_FRAGMENT_SHADER, self.frag_source_file)
|
||||
self.sl.app.log("Compiling fragment shader %s..." % self.frag_source_file)
|
||||
self.frag_shader = self.try_compile_shader(
|
||||
frag_source, GL.GL_FRAGMENT_SHADER, self.frag_source_file
|
||||
)
|
||||
if self.log_compile and self.frag_shader:
|
||||
self.sl.app.log('Compiled fragment shader %s in %.6f seconds' % (self.frag_source_file, time.time() - self.last_frag_change))
|
||||
self.sl.app.log(
|
||||
"Compiled fragment shader %s in %.6f seconds"
|
||||
% (self.frag_source_file, time.time() - self.last_frag_change)
|
||||
)
|
||||
# shader program
|
||||
if self.vert_shader and self.frag_shader:
|
||||
self.program = shaders.compileProgram(self.vert_shader, self.frag_shader)
|
||||
|
||||
|
||||
def get_shader_source(self, source_file):
|
||||
src = open(SHADER_PATH + source_file, 'rb').read()
|
||||
src = open(SHADER_PATH + source_file, "rb").read()
|
||||
# prepend shader version for different platforms
|
||||
if self.sl.app.context_es:
|
||||
shader_version = self.glsl_version_es
|
||||
elif platform.system() == 'Windows':
|
||||
elif platform.system() == "Windows":
|
||||
shader_version = self.glsl_version_windows
|
||||
elif platform.system() == 'Darwin':
|
||||
elif platform.system() == "Darwin":
|
||||
shader_version = self.glsl_version_macos
|
||||
else:
|
||||
shader_version = self.glsl_version_unix
|
||||
version_string = '#version %s\n' % shader_version
|
||||
src = bytes(version_string, 'utf-8') + src
|
||||
version_string = "#version %s\n" % shader_version
|
||||
src = bytes(version_string, "utf-8") + src
|
||||
return src
|
||||
|
||||
|
||||
def try_compile_shader(self, source, shader_type, source_filename):
|
||||
"Catch and print shader compilation exceptions"
|
||||
try:
|
||||
shader = shaders.compileShader(source, shader_type)
|
||||
except Exception as e:
|
||||
self.sl.app.log('%s: ' % source_filename)
|
||||
lines = e.args[0].split('\\n')
|
||||
self.sl.app.log("%s: " % source_filename)
|
||||
lines = e.args[0].split("\\n")
|
||||
# salvage block after "shader compile failure" enclosed in b""
|
||||
pre = lines.pop(0).split('b"')
|
||||
for line in pre + lines[:-1]:
|
||||
self.sl.app.log(' ' + line)
|
||||
self.sl.app.log(" " + line)
|
||||
return
|
||||
return shader
|
||||
|
||||
|
||||
def has_updated(self):
|
||||
vert_mod_time = os.path.getmtime(SHADER_PATH + self.vert_source_file)
|
||||
frag_mod_time = os.path.getmtime(SHADER_PATH + self.frag_source_file)
|
||||
|
|
@ -115,7 +133,7 @@ class Shader:
|
|||
if frag_changed:
|
||||
self.last_frag_change = time.time()
|
||||
return vert_changed, frag_changed
|
||||
|
||||
|
||||
def recompile(self, shader_type):
|
||||
file_to_reload = self.vert_source_file
|
||||
if shader_type == GL.GL_FRAGMENT_SHADER:
|
||||
|
|
@ -124,9 +142,9 @@ class Shader:
|
|||
try:
|
||||
new_shader = shaders.compileShader(new_shader_source, shader_type)
|
||||
# TODO: use try_compile_shader instead here, make sure exception passes thru ok
|
||||
self.sl.app.log('ShaderLord: success reloading %s' % file_to_reload)
|
||||
self.sl.app.log("ShaderLord: success reloading %s" % file_to_reload)
|
||||
except:
|
||||
self.sl.app.log('ShaderLord: failed reloading %s' % file_to_reload)
|
||||
self.sl.app.log("ShaderLord: failed reloading %s" % file_to_reload)
|
||||
return
|
||||
# recompile program with new shader
|
||||
if shader_type == GL.GL_VERTEX_SHADER:
|
||||
|
|
@ -134,13 +152,13 @@ class Shader:
|
|||
else:
|
||||
self.frag_shader = new_shader
|
||||
self.program = shaders.compileProgram(self.vert_shader, self.frag_shader)
|
||||
|
||||
|
||||
def get_uniform_location(self, uniform_name):
|
||||
return GL.glGetUniformLocation(self.program, uniform_name)
|
||||
|
||||
|
||||
def get_attrib_location(self, attrib_name):
|
||||
return GL.glGetAttribLocation(self.program, attrib_name)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
GL.glDeleteProgram(self.program)
|
||||
|
||||
|
|
|
|||
27
texture.py
27
texture.py
|
|
@ -1,15 +1,15 @@
|
|||
import numpy as np
|
||||
from OpenGL import GL
|
||||
|
||||
|
||||
class Texture:
|
||||
|
||||
# TODO: move texture data init to a set method to make hot reload trivial(?)
|
||||
|
||||
|
||||
mag_filter = GL.GL_NEAREST
|
||||
min_filter = GL.GL_NEAREST
|
||||
#min_filter = GL.GL_NEAREST_MIPMAP_NEAREST
|
||||
# min_filter = GL.GL_NEAREST_MIPMAP_NEAREST
|
||||
packing = GL.GL_UNPACK_ALIGNMENT
|
||||
|
||||
|
||||
def __init__(self, data, width, height):
|
||||
self.width, self.height = width, height
|
||||
img_data = np.frombuffer(data, dtype=np.uint8)
|
||||
|
|
@ -18,23 +18,32 @@ class Texture:
|
|||
GL.glBindTexture(GL.GL_TEXTURE_2D, self.gltex)
|
||||
self.set_filter(self.mag_filter, self.min_filter, False)
|
||||
self.set_wrap(False, False)
|
||||
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0,
|
||||
GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, img_data)
|
||||
GL.glTexImage2D(
|
||||
GL.GL_TEXTURE_2D,
|
||||
0,
|
||||
GL.GL_RGBA,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
GL.GL_RGBA,
|
||||
GL.GL_UNSIGNED_BYTE,
|
||||
img_data,
|
||||
)
|
||||
if bool(GL.glGenerateMipmap):
|
||||
GL.glGenerateMipmap(GL.GL_TEXTURE_2D)
|
||||
|
||||
|
||||
def set_filter(self, new_mag_filter, new_min_filter, bind_first=True):
|
||||
if bind_first:
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, self.gltex)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, new_mag_filter)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, new_min_filter)
|
||||
|
||||
|
||||
def set_wrap(self, new_wrap, bind_first=True):
|
||||
if bind_first:
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, self.gltex)
|
||||
wrap = GL.GL_REPEAT if new_wrap else GL.GL_CLAMP_TO_EDGE
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, wrap)
|
||||
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, wrap)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
GL.glDeleteTextures([self.gltex])
|
||||
|
|
|
|||
374
ui.py
374
ui.py
|
|
@ -1,24 +1,45 @@
|
|||
import sdl2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import sdl2
|
||||
from OpenGL import GL
|
||||
from PIL import Image
|
||||
|
||||
from art import (
|
||||
UV_FLIP270,
|
||||
UV_NORMAL,
|
||||
uv_names,
|
||||
)
|
||||
from edit_command import EditCommand, EditCommandTile, EntireArtCommand
|
||||
from texture import Texture
|
||||
from ui_element import UIArt, FPSCounterUI, MessageLineUI, DebugTextUI, GameSelectionLabel, GameHoverLabel, ToolTip
|
||||
from ui_colors import UIColors
|
||||
from ui_console import ConsoleUI
|
||||
from ui_status_bar import StatusBarUI
|
||||
from ui_popup import ToolPopup
|
||||
from ui_edit_panel import EditListPanel
|
||||
from ui_element import (
|
||||
DebugTextUI,
|
||||
FPSCounterUI,
|
||||
GameHoverLabel,
|
||||
GameSelectionLabel,
|
||||
MessageLineUI,
|
||||
ToolTip,
|
||||
UIArt,
|
||||
)
|
||||
from ui_menu_bar import ArtMenuBar, GameMenuBar
|
||||
from ui_menu_pulldown import PulldownMenu
|
||||
from ui_edit_panel import EditListPanel
|
||||
from ui_object_panel import EditObjectPanel
|
||||
from ui_colors import UIColors
|
||||
from ui_tool import PencilTool, EraseTool, GrabTool, RotateTool, TextTool, SelectTool, PasteTool, FillTool
|
||||
from ui_popup import ToolPopup
|
||||
from ui_status_bar import StatusBarUI
|
||||
from ui_tool import (
|
||||
EraseTool,
|
||||
FillTool,
|
||||
GrabTool,
|
||||
PasteTool,
|
||||
PencilTool,
|
||||
RotateTool,
|
||||
SelectTool,
|
||||
TextTool,
|
||||
)
|
||||
from ui_toolbar import ArtToolBar
|
||||
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY, UV_FLIP90, UV_FLIP270, uv_names
|
||||
from edit_command import EditCommand, EditCommandTile, EntireArtCommand
|
||||
|
||||
UI_ASSET_DIR = 'ui/'
|
||||
UI_ASSET_DIR = "ui/"
|
||||
SCALE_INCREMENT = 0.25
|
||||
# spacing factor of each non-active document's scale from active document
|
||||
MDI_MARGIN = 1.1
|
||||
|
|
@ -30,40 +51,47 @@ OIS_FILL = 2
|
|||
|
||||
|
||||
class UI:
|
||||
|
||||
# user-configured UI scale factor
|
||||
scale = 1.0
|
||||
max_onion_alpha = 0.5
|
||||
charset_name = 'ui'
|
||||
palette_name = 'c64_original'
|
||||
charset_name = "ui"
|
||||
palette_name = "c64_original"
|
||||
# red color for warnings
|
||||
error_color_index = UIColors.brightred
|
||||
# low-contrast background texture that distinguishes UI from flat color
|
||||
grain_texture_path = UI_ASSET_DIR + 'bgnoise_alpha.png'
|
||||
grain_texture_path = UI_ASSET_DIR + "bgnoise_alpha.png"
|
||||
# expose to classes that don't want to import this module
|
||||
asset_dir = UI_ASSET_DIR
|
||||
visible = True
|
||||
logg = False
|
||||
popup_hold_to_show = False
|
||||
flip_affects_xforms = True
|
||||
tool_classes = [ PencilTool, EraseTool, GrabTool, RotateTool, TextTool,
|
||||
SelectTool, PasteTool, FillTool ]
|
||||
tool_selected_log = 'tool selected'
|
||||
art_selected_log = 'Now editing'
|
||||
frame_selected_log = 'Now editing frame %s (hold time %ss)'
|
||||
layer_selected_log = 'Now editing layer: %s'
|
||||
swap_color_log = 'Swapped FG/BG colors'
|
||||
affects_char_on_log = 'will affect characters'
|
||||
affects_char_off_log = 'will not affect characters'
|
||||
affects_fg_on_log = 'will affect foreground colors'
|
||||
affects_fg_off_log = 'will not affect foreground colors'
|
||||
affects_bg_on_log = 'will affect background colors'
|
||||
affects_bg_off_log = 'will not affect background colors'
|
||||
affects_xform_on_log = 'will affect character rotation/flip'
|
||||
affects_xform_off_log = 'will not affect character rotation/flip'
|
||||
xform_selected_log = 'Selected character transform:'
|
||||
show_edit_ui_log = 'Edit UI hidden, press %s to unhide.'
|
||||
|
||||
tool_classes = [
|
||||
PencilTool,
|
||||
EraseTool,
|
||||
GrabTool,
|
||||
RotateTool,
|
||||
TextTool,
|
||||
SelectTool,
|
||||
PasteTool,
|
||||
FillTool,
|
||||
]
|
||||
tool_selected_log = "tool selected"
|
||||
art_selected_log = "Now editing"
|
||||
frame_selected_log = "Now editing frame %s (hold time %ss)"
|
||||
layer_selected_log = "Now editing layer: %s"
|
||||
swap_color_log = "Swapped FG/BG colors"
|
||||
affects_char_on_log = "will affect characters"
|
||||
affects_char_off_log = "will not affect characters"
|
||||
affects_fg_on_log = "will affect foreground colors"
|
||||
affects_fg_off_log = "will not affect foreground colors"
|
||||
affects_bg_on_log = "will affect background colors"
|
||||
affects_bg_off_log = "will not affect background colors"
|
||||
affects_xform_on_log = "will affect character rotation/flip"
|
||||
affects_xform_off_log = "will not affect character rotation/flip"
|
||||
xform_selected_log = "Selected character transform:"
|
||||
show_edit_ui_log = "Edit UI hidden, press %s to unhide."
|
||||
|
||||
def __init__(self, app, active_art):
|
||||
self.app = app
|
||||
# the current art being edited
|
||||
|
|
@ -92,7 +120,7 @@ class UI:
|
|||
# create tools
|
||||
for t in self.tool_classes:
|
||||
new_tool = t(self)
|
||||
tool_name = '%s_tool' % new_tool.name
|
||||
tool_name = "%s_tool" % new_tool.name
|
||||
setattr(self, tool_name, new_tool)
|
||||
# stick in a list for popup tool tab
|
||||
self.tools.append(new_tool)
|
||||
|
|
@ -125,17 +153,27 @@ class UI:
|
|||
self.edit_object_panel = EditObjectPanel(self)
|
||||
self.game_selection_label = GameSelectionLabel(self)
|
||||
self.game_hover_label = GameHoverLabel(self)
|
||||
self.elements += [self.fps_counter, self.status_bar, self.popup,
|
||||
self.message_line, self.debug_text, self.pulldown,
|
||||
self.art_menu_bar, self.game_menu_bar, self.tooltip,
|
||||
self.art_toolbar,
|
||||
self.edit_list_panel, self.edit_object_panel,
|
||||
self.game_hover_label, self.game_selection_label]
|
||||
self.elements += [
|
||||
self.fps_counter,
|
||||
self.status_bar,
|
||||
self.popup,
|
||||
self.message_line,
|
||||
self.debug_text,
|
||||
self.pulldown,
|
||||
self.art_menu_bar,
|
||||
self.game_menu_bar,
|
||||
self.tooltip,
|
||||
self.art_toolbar,
|
||||
self.edit_list_panel,
|
||||
self.edit_object_panel,
|
||||
self.game_hover_label,
|
||||
self.game_selection_label,
|
||||
]
|
||||
# add console last so it draws last
|
||||
self.elements.append(self.console)
|
||||
# grain texture
|
||||
img = Image.open(self.grain_texture_path)
|
||||
img = img.convert('RGBA')
|
||||
img = img.convert("RGBA")
|
||||
width, height = img.size
|
||||
self.grain_texture = Texture(img.tobytes(), width, height)
|
||||
self.grain_texture.set_wrap(True)
|
||||
|
|
@ -145,7 +183,7 @@ class UI:
|
|||
# if editing is disallowed, hide game mode UI
|
||||
if not self.app.can_edit:
|
||||
self.set_game_edit_ui_visibility(False)
|
||||
|
||||
|
||||
def set_scale(self, new_scale):
|
||||
old_scale = self.scale
|
||||
self.scale = new_scale
|
||||
|
|
@ -155,8 +193,12 @@ class UI:
|
|||
aspect = float(self.app.window_width) / self.app.window_height
|
||||
inv_aspect = float(self.app.window_height) / self.app.window_width
|
||||
# MAYBE-TODO: this math is correct but hard to follow, rewrite for clarity
|
||||
width = self.app.window_width / (self.charset.char_width * self.scale * inv_aspect)
|
||||
height = self.app.window_height / (self.charset.char_height * self.scale * inv_aspect)
|
||||
width = self.app.window_width / (
|
||||
self.charset.char_width * self.scale * inv_aspect
|
||||
)
|
||||
height = self.app.window_height / (
|
||||
self.charset.char_height * self.scale * inv_aspect
|
||||
)
|
||||
# any new UI elements created should use new scale
|
||||
UIArt.quad_width = 2 / width * aspect
|
||||
UIArt.quad_height = 2 / height * aspect
|
||||
|
|
@ -165,8 +207,11 @@ class UI:
|
|||
# tell elements to refresh
|
||||
self.set_elements_scale()
|
||||
if self.scale != old_scale:
|
||||
self.message_line.post_line('UI scale is now %s (%.3f x %.3f)' % (self.scale, self.width_tiles, self.height_tiles))
|
||||
|
||||
self.message_line.post_line(
|
||||
"UI scale is now %s (%.3f x %.3f)"
|
||||
% (self.scale, self.width_tiles, self.height_tiles)
|
||||
)
|
||||
|
||||
def set_elements_scale(self):
|
||||
for e in self.elements:
|
||||
e.art.quad_width, e.art.quad_height = UIArt.quad_width, UIArt.quad_height
|
||||
|
|
@ -174,11 +219,11 @@ class UI:
|
|||
e.reset_art()
|
||||
e.reset_loc()
|
||||
e.art.geo_changed = True
|
||||
|
||||
|
||||
def window_resized(self):
|
||||
# recalc renderables' quad size (same scale, different aspect)
|
||||
self.set_scale(self.scale)
|
||||
|
||||
|
||||
def size_and_position_overlay_image(self):
|
||||
# called any time active art changes, or active art changes size
|
||||
r = self.app.overlay_renderable
|
||||
|
|
@ -197,7 +242,7 @@ class UI:
|
|||
r.scale_y = self.active_art.height * self.active_art.quad_height
|
||||
r.y = -r.scale_y
|
||||
r.z = self.active_art.layers_z[self.active_art.active_layer]
|
||||
|
||||
|
||||
def set_active_art(self, new_art):
|
||||
self.active_art = new_art
|
||||
new_charset = self.active_art.charset
|
||||
|
|
@ -231,16 +276,17 @@ class UI:
|
|||
# rescale/reposition overlay image
|
||||
self.size_and_position_overlay_image()
|
||||
# tell select tool renderables
|
||||
for r in [self.select_tool.select_renderable,
|
||||
self.select_tool.drag_renderable]:
|
||||
for r in [self.select_tool.select_renderable, self.select_tool.drag_renderable]:
|
||||
r.quad_size_ref = new_art
|
||||
r.rebuild_geo(self.select_tool.selected_tiles)
|
||||
self.app.update_window_title()
|
||||
if self.app.can_edit:
|
||||
self.message_line.post_line('%s %s' % (self.art_selected_log, self.active_art.filename))
|
||||
|
||||
self.message_line.post_line(
|
||||
"%s %s" % (self.art_selected_log, self.active_art.filename)
|
||||
)
|
||||
|
||||
def set_active_art_by_filename(self, art_filename):
|
||||
for i,art in enumerate(self.app.art_loaded_for_edit):
|
||||
for i, art in enumerate(self.app.art_loaded_for_edit):
|
||||
if art_filename == art.filename:
|
||||
break
|
||||
new_active_art = self.app.art_loaded_for_edit.pop(i)
|
||||
|
|
@ -248,7 +294,7 @@ class UI:
|
|||
new_active_renderable = self.app.edit_renderables.pop(i)
|
||||
self.app.edit_renderables.insert(0, new_active_renderable)
|
||||
self.set_active_art(new_active_art)
|
||||
|
||||
|
||||
def previous_active_art(self):
|
||||
"cycles to next art in app.art_loaded_for_edit"
|
||||
if len(self.app.art_loaded_for_edit) == 1:
|
||||
|
|
@ -258,7 +304,7 @@ class UI:
|
|||
next_active_renderable = self.app.edit_renderables.pop(-1)
|
||||
self.app.edit_renderables.insert(0, next_active_renderable)
|
||||
self.set_active_art(self.app.art_loaded_for_edit[0])
|
||||
|
||||
|
||||
def next_active_art(self):
|
||||
if len(self.app.art_loaded_for_edit) == 1:
|
||||
return
|
||||
|
|
@ -267,12 +313,12 @@ class UI:
|
|||
last_active_renderable = self.app.edit_renderables.pop(0)
|
||||
self.app.edit_renderables.append(last_active_renderable)
|
||||
self.set_active_art(self.app.art_loaded_for_edit[0])
|
||||
|
||||
|
||||
def set_selected_tool(self, new_tool):
|
||||
if self.app.game_mode:
|
||||
return
|
||||
# don't re-select same tool, except to cycle fill tool (see below)
|
||||
if new_tool == self.selected_tool and not type(new_tool) is FillTool:
|
||||
if new_tool == self.selected_tool and type(new_tool) is not FillTool:
|
||||
return
|
||||
# bail out of text entry if active
|
||||
if self.selected_tool is self.text_tool:
|
||||
|
|
@ -285,20 +331,26 @@ class UI:
|
|||
# if we're selecting the fill tool and it's already selected,
|
||||
# cycle through its 3 modes (char/fg/bg boundary)
|
||||
cycled_fill = False
|
||||
if type(self.selected_tool) is FillTool and \
|
||||
type(self.previous_tool) is FillTool:
|
||||
self.selected_tool.boundary_mode = self.selected_tool.next_boundary_modes[self.selected_tool.boundary_mode]
|
||||
if (
|
||||
type(self.selected_tool) is FillTool
|
||||
and type(self.previous_tool) is FillTool
|
||||
):
|
||||
self.selected_tool.boundary_mode = self.selected_tool.next_boundary_modes[
|
||||
self.selected_tool.boundary_mode
|
||||
]
|
||||
# TODO: do we need a message line message for this?
|
||||
#self.app.log(self.selected_tool.boundary_mode)
|
||||
# self.app.log(self.selected_tool.boundary_mode)
|
||||
cycled_fill = True
|
||||
# close menu if we selected tool from it
|
||||
if self.menu_bar.active_menu_name and not cycled_fill:
|
||||
self.menu_bar.close_active_menu()
|
||||
self.message_line.post_line('%s %s' % (self.selected_tool.get_button_caption(), self.tool_selected_log))
|
||||
|
||||
self.message_line.post_line(
|
||||
"%s %s" % (self.selected_tool.get_button_caption(), self.tool_selected_log)
|
||||
)
|
||||
|
||||
def cycle_fill_tool_mode(self):
|
||||
self.set_selected_tool(self.fill_tool)
|
||||
|
||||
|
||||
def get_longest_tool_name_length(self):
|
||||
"VERY specific function to help status bar draw its buttons"
|
||||
longest = 0
|
||||
|
|
@ -306,7 +358,7 @@ class UI:
|
|||
if len(tool.button_caption) > longest:
|
||||
longest = len(tool.button_caption)
|
||||
return longest
|
||||
|
||||
|
||||
def cycle_selected_tool(self, back=False):
|
||||
if not self.active_art:
|
||||
return
|
||||
|
|
@ -317,16 +369,17 @@ class UI:
|
|||
tool_index += 1
|
||||
tool_index %= len(self.tools)
|
||||
self.set_selected_tool(self.tools[tool_index])
|
||||
|
||||
|
||||
def set_selected_xform(self, new_xform):
|
||||
self.selected_xform = new_xform
|
||||
self.popup.set_xform(new_xform)
|
||||
self.tool_settings_changed = True
|
||||
line = '%s %s' % (self.xform_selected_log, uv_names[self.selected_xform])
|
||||
line = "%s %s" % (self.xform_selected_log, uv_names[self.selected_xform])
|
||||
self.message_line.post_line(line)
|
||||
|
||||
|
||||
def cycle_selected_xform(self, back=False):
|
||||
if self.app.game_mode: return
|
||||
if self.app.game_mode:
|
||||
return
|
||||
xform = self.selected_xform
|
||||
if back:
|
||||
xform -= 1
|
||||
|
|
@ -334,40 +387,42 @@ class UI:
|
|||
xform += 1
|
||||
xform %= UV_FLIP270 + 1
|
||||
self.set_selected_xform(xform)
|
||||
|
||||
|
||||
def reset_onion_frames(self, new_art=None):
|
||||
"set correct visibility, frame, and alpha for all onion renderables"
|
||||
new_art = new_art or self.active_art
|
||||
alpha = self.max_onion_alpha
|
||||
total_onion_frames = 0
|
||||
|
||||
def set_onion(r, new_frame, alpha):
|
||||
# scale back if fewer than MAX_ONION_FRAMES in either direction
|
||||
if total_onion_frames >= new_art.frames:
|
||||
r.visible = False
|
||||
return
|
||||
r.visible = True
|
||||
if not new_art is r.art:
|
||||
if new_art is not r.art:
|
||||
r.set_art(new_art)
|
||||
r.set_frame(new_frame)
|
||||
r.alpha = alpha
|
||||
# make BG dimmer so it's easier to see
|
||||
r.bg_alpha = alpha / 2
|
||||
|
||||
# populate "next" frames first
|
||||
for i,r in enumerate(self.app.onion_renderables_next):
|
||||
for i, r in enumerate(self.app.onion_renderables_next):
|
||||
total_onion_frames += 1
|
||||
new_frame = new_art.active_frame + i + 1
|
||||
set_onion(r, new_frame, alpha)
|
||||
alpha /= 2
|
||||
#print('next onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
|
||||
# print('next onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
|
||||
alpha = self.max_onion_alpha
|
||||
for i,r in enumerate(self.app.onion_renderables_prev):
|
||||
for i, r in enumerate(self.app.onion_renderables_prev):
|
||||
total_onion_frames += 1
|
||||
new_frame = new_art.active_frame - (i + 1)
|
||||
set_onion(r, new_frame, alpha)
|
||||
# each successive onion layer is dimmer
|
||||
alpha /= 2
|
||||
#print('previous onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
|
||||
|
||||
# print('previous onion %s set to frame %s alpha %s' % (i, new_frame, alpha))
|
||||
|
||||
def set_active_frame(self, new_frame):
|
||||
if not self.active_art.set_active_frame(new_frame):
|
||||
return
|
||||
|
|
@ -377,7 +432,7 @@ class UI:
|
|||
delay = self.active_art.frame_delays[frame]
|
||||
if self.app.can_edit:
|
||||
self.message_line.post_line(self.frame_selected_log % (frame + 1, delay))
|
||||
|
||||
|
||||
def set_active_layer(self, new_layer):
|
||||
self.active_art.set_active_layer(new_layer)
|
||||
z = self.active_art.layers_z[self.active_art.active_layer]
|
||||
|
|
@ -391,20 +446,22 @@ class UI:
|
|||
if self.app.can_edit:
|
||||
self.message_line.post_line(self.layer_selected_log % layer_name)
|
||||
self.size_and_position_overlay_image()
|
||||
|
||||
|
||||
def select_char(self, new_char_index):
|
||||
if not self.active_art:
|
||||
return
|
||||
# wrap at last valid index
|
||||
self.selected_char = new_char_index % self.active_art.charset.last_index
|
||||
# only update char tooltip if it was already up; avoid stomping others
|
||||
char_cycle_button = self.status_bar.button_map['char_cycle']
|
||||
char_toggle_button = self.status_bar.button_map['char_toggle']
|
||||
if char_cycle_button in self.status_bar.hovered_buttons or \
|
||||
char_toggle_button in self.status_bar.hovered_buttons:
|
||||
char_cycle_button = self.status_bar.button_map["char_cycle"]
|
||||
char_toggle_button = self.status_bar.button_map["char_toggle"]
|
||||
if (
|
||||
char_cycle_button in self.status_bar.hovered_buttons
|
||||
or char_toggle_button in self.status_bar.hovered_buttons
|
||||
):
|
||||
char_toggle_button.update_tooltip()
|
||||
self.tool_settings_changed = True
|
||||
|
||||
|
||||
def select_color(self, new_color_index, fg):
|
||||
"common code for select_fg/bg"
|
||||
if not self.active_art:
|
||||
|
|
@ -415,19 +472,29 @@ class UI:
|
|||
else:
|
||||
self.selected_bg_color = new_color_index
|
||||
# same don't-stomp-another-tooltip check as above
|
||||
toggle_button = self.status_bar.button_map['fg_toggle'] if fg else self.status_bar.button_map['bg_toggle']
|
||||
cycle_button = self.status_bar.button_map['fg_cycle'] if fg else self.status_bar.button_map['bg_cycle']
|
||||
if toggle_button in self.status_bar.hovered_buttons or \
|
||||
cycle_button in self.status_bar.hovered_buttons:
|
||||
toggle_button = (
|
||||
self.status_bar.button_map["fg_toggle"]
|
||||
if fg
|
||||
else self.status_bar.button_map["bg_toggle"]
|
||||
)
|
||||
cycle_button = (
|
||||
self.status_bar.button_map["fg_cycle"]
|
||||
if fg
|
||||
else self.status_bar.button_map["bg_cycle"]
|
||||
)
|
||||
if (
|
||||
toggle_button in self.status_bar.hovered_buttons
|
||||
or cycle_button in self.status_bar.hovered_buttons
|
||||
):
|
||||
toggle_button.update_tooltip()
|
||||
self.tool_settings_changed = True
|
||||
|
||||
|
||||
def select_fg(self, new_fg_index):
|
||||
self.select_color(new_fg_index, True)
|
||||
|
||||
|
||||
def select_bg(self, new_bg_index):
|
||||
self.select_color(new_bg_index, False)
|
||||
|
||||
|
||||
def swap_fg_bg_colors(self):
|
||||
if self.app.game_mode:
|
||||
return
|
||||
|
|
@ -435,11 +502,11 @@ class UI:
|
|||
self.selected_fg_color, self.selected_bg_color = bg, fg
|
||||
self.tool_settings_changed = True
|
||||
self.message_line.post_line(self.swap_color_log)
|
||||
|
||||
|
||||
def cut_selection(self):
|
||||
self.copy_selection()
|
||||
self.erase_tiles_in_selection()
|
||||
|
||||
|
||||
def erase_selection_or_art(self):
|
||||
if len(self.select_tool.selected_tiles) > 0:
|
||||
self.erase_tiles_in_selection()
|
||||
|
|
@ -447,7 +514,7 @@ class UI:
|
|||
self.select_all()
|
||||
self.erase_tiles_in_selection()
|
||||
self.select_none()
|
||||
|
||||
|
||||
def erase_tiles_in_selection(self):
|
||||
# create and commit command group to clear all tiles in selection
|
||||
frame, layer = self.active_art.active_frame, self.active_art.active_layer
|
||||
|
|
@ -455,7 +522,9 @@ class UI:
|
|||
for tile in self.select_tool.selected_tiles:
|
||||
new_tile_command = EditCommandTile(self.active_art)
|
||||
new_tile_command.set_tile(frame, layer, *tile)
|
||||
b_char, b_fg, b_bg, b_xform = self.active_art.get_tile_at(frame, layer, *tile)
|
||||
b_char, b_fg, b_bg, b_xform = self.active_art.get_tile_at(
|
||||
frame, layer, *tile
|
||||
)
|
||||
new_tile_command.set_before(b_char, b_fg, b_bg, b_xform)
|
||||
a_char = a_fg = 0
|
||||
a_xform = UV_NORMAL
|
||||
|
|
@ -466,7 +535,7 @@ class UI:
|
|||
new_command.apply()
|
||||
self.active_art.command_stack.commit_commands([new_command])
|
||||
self.active_art.set_unsaved_changes(True)
|
||||
|
||||
|
||||
def copy_selection(self):
|
||||
# convert current selection tiles (active frame+layer) into
|
||||
# EditCommandTiles for Cursor.preview_edits
|
||||
|
|
@ -499,7 +568,7 @@ class UI:
|
|||
tile_command.set_tile(frame, layer, x, y)
|
||||
self.clipboard_width = max_x - min_x
|
||||
self.clipboard_height = max_y - min_y
|
||||
|
||||
|
||||
def crop_to_selection(self, art):
|
||||
# ignore non-rectangular selection features, use top left and bottom
|
||||
# right corners
|
||||
|
|
@ -523,7 +592,7 @@ class UI:
|
|||
command = EntireArtCommand(art, min_x, min_y)
|
||||
command.save_tiles(before=True)
|
||||
art.resize(w, h, min_x, min_y)
|
||||
self.app.log('Resized %s to %s x %s' % (art.filename, w, h))
|
||||
self.app.log("Resized %s to %s x %s" % (art.filename, w, h))
|
||||
art.set_unsaved_changes(True)
|
||||
# clear selection to avoid having tiles we know are OoB selected
|
||||
self.select_tool.selected_tiles = {}
|
||||
|
|
@ -531,11 +600,11 @@ class UI:
|
|||
# commit command
|
||||
command.save_tiles(before=False)
|
||||
art.command_stack.commit_commands([command])
|
||||
|
||||
|
||||
def reset_edit_renderables(self):
|
||||
# reposition all art renderables and change their opacity
|
||||
x, y = 0, 0
|
||||
for i,r in enumerate(self.app.edit_renderables):
|
||||
for i, r in enumerate(self.app.edit_renderables):
|
||||
# always put active art at 0,0
|
||||
if r in self.active_art.renderables:
|
||||
r.alpha = 1
|
||||
|
|
@ -553,7 +622,7 @@ class UI:
|
|||
r.move_to(x * MDI_MARGIN, 0, -i, 0.2)
|
||||
x += r.art.width * r.art.quad_width
|
||||
y -= r.art.height * r.art.quad_height
|
||||
|
||||
|
||||
def adjust_for_art_resize(self, art):
|
||||
if art is not self.active_art:
|
||||
return
|
||||
|
|
@ -568,7 +637,7 @@ class UI:
|
|||
if self.app.cursor.y > art.height:
|
||||
self.app.cursor.y = art.height
|
||||
self.app.cursor.moved = True
|
||||
|
||||
|
||||
def resize_art(self, art, new_width, new_height, origin_x, origin_y, bg_fill):
|
||||
# create command for undo/redo
|
||||
command = EntireArtCommand(art, origin_x, origin_y)
|
||||
|
|
@ -580,16 +649,16 @@ class UI:
|
|||
command.save_tiles(before=False)
|
||||
art.command_stack.commit_commands([command])
|
||||
art.set_unsaved_changes(True)
|
||||
|
||||
|
||||
def select_none(self):
|
||||
self.select_tool.selected_tiles = {}
|
||||
|
||||
|
||||
def select_all(self):
|
||||
self.select_tool.selected_tiles = {}
|
||||
for y in range(self.active_art.height):
|
||||
for x in range(self.active_art.width):
|
||||
self.select_tool.selected_tiles[(x, y)] = True
|
||||
|
||||
|
||||
def invert_selection(self):
|
||||
old_selection = self.select_tool.selected_tiles.copy()
|
||||
self.select_tool.selected_tiles = {}
|
||||
|
|
@ -597,12 +666,12 @@ class UI:
|
|||
for x in range(self.active_art.width):
|
||||
if not old_selection.get((x, y), False):
|
||||
self.select_tool.selected_tiles[(x, y)] = True
|
||||
|
||||
|
||||
def get_screen_coords(self, window_x, window_y):
|
||||
x = (2 * window_x) / self.app.window_width - 1
|
||||
y = (-2 * window_y) / self.app.window_height + 1
|
||||
return x, y
|
||||
|
||||
|
||||
def update(self):
|
||||
self.select_tool.update()
|
||||
# window coordinates -> OpenGL coordinates
|
||||
|
|
@ -615,14 +684,19 @@ class UI:
|
|||
if self.console.visible:
|
||||
continue
|
||||
# only check visible elements
|
||||
if self.app.has_mouse_focus and e.is_visible() and e.can_hover and e.is_inside(mx, my):
|
||||
if (
|
||||
self.app.has_mouse_focus
|
||||
and e.is_visible()
|
||||
and e.can_hover
|
||||
and e.is_inside(mx, my)
|
||||
):
|
||||
self.hovered_elements.append(e)
|
||||
# only hover if we weren't last update
|
||||
if not e in was_hovering:
|
||||
if e not in was_hovering:
|
||||
e.hovered()
|
||||
for e in was_hovering:
|
||||
# unhover if app window loses mouse focus
|
||||
if not self.app.has_mouse_focus or not e in self.hovered_elements:
|
||||
if not self.app.has_mouse_focus or e not in self.hovered_elements:
|
||||
e.unhovered()
|
||||
# update all elements, regardless of whether they're being hovered etc
|
||||
for e in self.elements:
|
||||
|
|
@ -632,7 +706,7 @@ class UI:
|
|||
# art update: tell renderables to refresh buffers
|
||||
e.art.update()
|
||||
self.tool_settings_changed = False
|
||||
|
||||
|
||||
def clicked(self, mouse_button):
|
||||
handled = False
|
||||
# return True if any button handled the input
|
||||
|
|
@ -642,17 +716,21 @@ class UI:
|
|||
if e.clicked(mouse_button):
|
||||
handled = True
|
||||
# close pulldown if clicking outside it / the menu bar
|
||||
if self.pulldown.visible and not self.pulldown in self.hovered_elements and not self.menu_bar in self.hovered_elements:
|
||||
if (
|
||||
self.pulldown.visible
|
||||
and self.pulldown not in self.hovered_elements
|
||||
and self.menu_bar not in self.hovered_elements
|
||||
):
|
||||
self.menu_bar.close_active_menu()
|
||||
return handled
|
||||
|
||||
|
||||
def unclicked(self, mouse_button):
|
||||
handled = False
|
||||
for e in self.hovered_elements:
|
||||
if e.unclicked(mouse_button):
|
||||
handled = True
|
||||
return handled
|
||||
|
||||
|
||||
def wheel_moved(self, wheel_y):
|
||||
handled = False
|
||||
# use wheel to scroll chooser dialogs
|
||||
|
|
@ -660,17 +738,19 @@ class UI:
|
|||
# an SDL keycode from that?
|
||||
if self.active_dialog:
|
||||
keycode = sdl2.SDLK_UP if wheel_y > 0 else sdl2.SDLK_DOWN
|
||||
self.active_dialog.handle_input(keycode,
|
||||
self.app.il.shift_pressed,
|
||||
self.app.il.alt_pressed,
|
||||
self.app.il.ctrl_pressed)
|
||||
self.active_dialog.handle_input(
|
||||
keycode,
|
||||
self.app.il.shift_pressed,
|
||||
self.app.il.alt_pressed,
|
||||
self.app.il.ctrl_pressed,
|
||||
)
|
||||
handled = True
|
||||
elif len(self.hovered_elements) > 0:
|
||||
for e in self.hovered_elements:
|
||||
if e.wheel_moved(wheel_y):
|
||||
handled = True
|
||||
return handled
|
||||
|
||||
|
||||
def quick_grab(self):
|
||||
if self.app.game_mode:
|
||||
return
|
||||
|
|
@ -678,17 +758,17 @@ class UI:
|
|||
return
|
||||
self.grab_tool.grab()
|
||||
self.tool_settings_changed = True
|
||||
|
||||
|
||||
def undo(self):
|
||||
# if still painting, finish
|
||||
if self.app.cursor.current_command:
|
||||
self.app.cursor.finish_paint()
|
||||
self.active_art.command_stack.undo()
|
||||
self.active_art.set_unsaved_changes(True)
|
||||
|
||||
|
||||
def redo(self):
|
||||
self.active_art.command_stack.redo()
|
||||
|
||||
|
||||
def open_dialog(self, dialog_class, options={}):
|
||||
if self.app.game_mode and not dialog_class.game_mode_visible:
|
||||
return
|
||||
|
|
@ -696,14 +776,14 @@ class UI:
|
|||
self.active_dialog = dialog
|
||||
self.keyboard_focus_element = self.active_dialog
|
||||
# insert dialog at index 0 so it draws first instead of last
|
||||
#self.elements.insert(0, dialog)
|
||||
# self.elements.insert(0, dialog)
|
||||
self.elements.remove(self.console)
|
||||
self.elements.append(dialog)
|
||||
self.elements.append(self.console)
|
||||
|
||||
|
||||
def is_game_edit_ui_visible(self):
|
||||
return self.game_menu_bar.visible
|
||||
|
||||
|
||||
def set_game_edit_ui_visibility(self, visible, show_message=True):
|
||||
self.game_menu_bar.visible = visible
|
||||
self.edit_list_panel.visible = visible
|
||||
|
|
@ -712,24 +792,26 @@ class UI:
|
|||
# relinquish keyboard focus in play mode
|
||||
self.keyboard_focus_element = None
|
||||
if show_message and self.app.il:
|
||||
bind = self.app.il.get_command_shortcut('toggle_game_edit_ui')
|
||||
bind = self.app.il.get_command_shortcut("toggle_game_edit_ui")
|
||||
bind = bind.title()
|
||||
self.message_line.post_line(self.show_edit_ui_log % bind, 10)
|
||||
else:
|
||||
self.message_line.post_line('')
|
||||
self.message_line.post_line("")
|
||||
self.app.update_window_title()
|
||||
|
||||
|
||||
def object_selection_changed(self):
|
||||
if len(self.app.gw.selected_objects) == 0:
|
||||
self.keyboard_focus_element = None
|
||||
self.refocus_keyboard()
|
||||
|
||||
|
||||
def switch_edit_panel_focus(self, reverse=False):
|
||||
# only allow tabbing away if list panel is in allowed mode
|
||||
lp = self.edit_list_panel
|
||||
if self.keyboard_focus_element is lp and \
|
||||
lp.list_operation in lp.list_operations_allow_kb_focus and \
|
||||
self.active_dialog:
|
||||
if (
|
||||
self.keyboard_focus_element is lp
|
||||
and lp.list_operation in lp.list_operations_allow_kb_focus
|
||||
and self.active_dialog
|
||||
):
|
||||
self.keyboard_focus_element = self.active_dialog
|
||||
# prevent any other tabbing away from active dialog
|
||||
if self.active_dialog:
|
||||
|
|
@ -746,14 +828,14 @@ class UI:
|
|||
# handle shift-tab
|
||||
if reverse:
|
||||
focus_elements.reverse()
|
||||
for i,element in enumerate(focus_elements[:-1]):
|
||||
for i, element in enumerate(focus_elements[:-1]):
|
||||
if self.keyboard_focus_element is element:
|
||||
self.keyboard_focus_element = focus_elements[i+1]
|
||||
self.keyboard_focus_element = focus_elements[i + 1]
|
||||
break
|
||||
# update keyboard hover for both
|
||||
self.edit_object_panel.update_keyboard_hover()
|
||||
self.edit_list_panel.update_keyboard_hover()
|
||||
|
||||
|
||||
def refocus_keyboard(self):
|
||||
"called when an element closes, sets new keyboard_focus_element"
|
||||
if self.active_dialog:
|
||||
|
|
@ -764,27 +846,31 @@ class UI:
|
|||
self.keyboard_focus_element = self.popup
|
||||
elif self.pulldown.visible:
|
||||
self.keyboard_focus_element = self.pulldown
|
||||
elif self.edit_list_panel.is_visible() and not self.edit_object_panel.is_visible():
|
||||
elif (
|
||||
self.edit_list_panel.is_visible()
|
||||
and not self.edit_object_panel.is_visible()
|
||||
):
|
||||
self.keyboard_focus_element = self.edit_list_panel
|
||||
elif self.edit_object_panel.is_visible() and not self.edit_list_panel.is_visible():
|
||||
elif (
|
||||
self.edit_object_panel.is_visible()
|
||||
and not self.edit_list_panel.is_visible()
|
||||
):
|
||||
self.keyboard_focus_element = self.edit_object_panel
|
||||
|
||||
|
||||
def keyboard_navigate(self, move_x, move_y):
|
||||
self.keyboard_focus_element.keyboard_navigate(move_x, move_y)
|
||||
|
||||
|
||||
def toggle_game_edit_ui(self):
|
||||
# if editing is disallowed, only run this once to disable UI
|
||||
if not self.app.can_edit:
|
||||
return
|
||||
elif not self.app.game_mode:
|
||||
if not self.app.can_edit or not self.app.game_mode:
|
||||
return
|
||||
self.set_game_edit_ui_visibility(not self.game_menu_bar.visible)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
for e in self.elements:
|
||||
e.destroy()
|
||||
self.grain_texture.destroy()
|
||||
|
||||
|
||||
def render(self):
|
||||
for e in self.elements:
|
||||
if e.is_visible():
|
||||
|
|
|
|||
570
ui_art_dialog.py
570
ui_art_dialog.py
File diff suppressed because it is too large
Load diff
90
ui_button.py
90
ui_button.py
|
|
@ -1,20 +1,19 @@
|
|||
|
||||
from ui_colors import UIColors
|
||||
|
||||
TEXT_LEFT = 0
|
||||
TEXT_CENTER = 1
|
||||
TEXT_RIGHT = 2
|
||||
|
||||
BUTTON_STATES = ['normal', 'hovered', 'clicked', 'dimmed']
|
||||
BUTTON_STATES = ["normal", "hovered", "clicked", "dimmed"]
|
||||
|
||||
|
||||
class UIButton:
|
||||
|
||||
"clickable button that does something in a UIElement"
|
||||
|
||||
|
||||
# x/y/width/height given in tile scale
|
||||
x, y = 0, 0
|
||||
width, height = 1, 1
|
||||
caption = 'TEST'
|
||||
caption = "TEST"
|
||||
caption_justify = TEXT_LEFT
|
||||
# paint caption from string, or not
|
||||
should_draw_caption = True
|
||||
|
|
@ -44,65 +43,72 @@ class UIButton:
|
|||
# if true, display a tooltip when hovered, and dismiss it when unhovered.
|
||||
# contents set from get_tooltip_text and positioned by get_tooltip_location.
|
||||
tooltip_on_hover = False
|
||||
|
||||
|
||||
def __init__(self, element, starting_state=None):
|
||||
self.element = element
|
||||
self.state = starting_state or 'normal'
|
||||
|
||||
self.state = starting_state or "normal"
|
||||
|
||||
def log_event(self, event_type):
|
||||
"common code for button event logging"
|
||||
if self.element.ui.logg:
|
||||
self.element.ui.app.log("UIButton: %s's %s %s" % (self.element.__class__.__name__, self.__class__.__name__, event_type))
|
||||
|
||||
self.element.ui.app.log(
|
||||
"UIButton: %s's %s %s"
|
||||
% (self.element.__class__.__name__, self.__class__.__name__, event_type)
|
||||
)
|
||||
|
||||
def set_state(self, new_state):
|
||||
if not new_state in BUTTON_STATES:
|
||||
self.element.ui.app.log('Unrecognized state for button %s: %s' % (self.__class__.__name__, new_state))
|
||||
if new_state not in BUTTON_STATES:
|
||||
self.element.ui.app.log(
|
||||
"Unrecognized state for button %s: %s"
|
||||
% (self.__class__.__name__, new_state)
|
||||
)
|
||||
return
|
||||
self.dimmed = new_state == 'dimmed'
|
||||
self.dimmed = new_state == "dimmed"
|
||||
self.state = new_state
|
||||
self.set_state_colors()
|
||||
|
||||
|
||||
def get_state_colors(self, state):
|
||||
fg = getattr(self, '%s_fg_color' % state)
|
||||
bg = getattr(self, '%s_bg_color' % state)
|
||||
fg = getattr(self, "%s_fg_color" % state)
|
||||
bg = getattr(self, "%s_bg_color" % state)
|
||||
return fg, bg
|
||||
|
||||
|
||||
def set_state_colors(self):
|
||||
if self.never_draw:
|
||||
return
|
||||
# set colors for entire button area based on current state
|
||||
if self.dimmed and self.state == 'normal':
|
||||
self.state = 'dimmed'
|
||||
if self.dimmed and self.state == "normal":
|
||||
self.state = "dimmed"
|
||||
# just bail if we're trying to draw something out of bounds
|
||||
if self.x + self.width > self.element.art.width:
|
||||
return
|
||||
elif self.y + self.height > self.element.art.height:
|
||||
if (
|
||||
self.x + self.width > self.element.art.width
|
||||
or self.y + self.height > self.element.art.height
|
||||
):
|
||||
return
|
||||
fg, bg = self.get_state_colors(self.state)
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.element.art.set_tile_at(0, 0, self.x + x, self.y + y, None, fg, bg)
|
||||
|
||||
|
||||
def update_tooltip(self):
|
||||
tt = self.element.ui.tooltip
|
||||
tt.reset_art()
|
||||
tt.set_text(self.get_tooltip_text())
|
||||
tt.tile_x, tt.tile_y = self.get_tooltip_location()
|
||||
tt.reset_loc()
|
||||
|
||||
|
||||
def hover(self):
|
||||
self.log_event('hovered')
|
||||
self.set_state('hovered')
|
||||
self.log_event("hovered")
|
||||
self.set_state("hovered")
|
||||
if self.tooltip_on_hover:
|
||||
self.element.ui.tooltip.visible = True
|
||||
self.update_tooltip()
|
||||
|
||||
|
||||
def unhover(self):
|
||||
self.log_event('unhovered')
|
||||
self.log_event("unhovered")
|
||||
if self.dimmed:
|
||||
self.set_state('dimmed')
|
||||
self.set_state("dimmed")
|
||||
else:
|
||||
self.set_state('normal')
|
||||
self.set_state("normal")
|
||||
if self.tooltip_on_hover:
|
||||
# if two buttons are adjacent, we might be unhovering this one
|
||||
# right after hovering the other in the same frame. if so,
|
||||
|
|
@ -115,31 +121,31 @@ class UIButton:
|
|||
another_tooltip = True
|
||||
if not another_tooltip:
|
||||
self.element.ui.tooltip.visible = False
|
||||
|
||||
|
||||
def click(self):
|
||||
self.log_event('clicked')
|
||||
self.set_state('clicked')
|
||||
|
||||
self.log_event("clicked")
|
||||
self.set_state("clicked")
|
||||
|
||||
def unclick(self):
|
||||
self.log_event('unclicked')
|
||||
self.log_event("unclicked")
|
||||
if self in self.element.hovered_buttons:
|
||||
self.hover()
|
||||
else:
|
||||
self.unhover()
|
||||
|
||||
|
||||
def get_tooltip_text(self):
|
||||
"override in a subclass to define this button's tooltip text"
|
||||
return 'ERROR'
|
||||
|
||||
return "ERROR"
|
||||
|
||||
def get_tooltip_location(self):
|
||||
"override in a subclass to define this button's tooltip screen location"
|
||||
return 10, 10
|
||||
|
||||
|
||||
def draw_caption(self):
|
||||
y = self.y + self.caption_y
|
||||
text = self.caption
|
||||
# trim if too long
|
||||
text = text[:self.width]
|
||||
text = text[: self.width]
|
||||
if self.caption_justify == TEXT_CENTER:
|
||||
text = text.center(self.width)
|
||||
elif self.caption_justify == TEXT_RIGHT:
|
||||
|
|
@ -150,10 +156,10 @@ class UIButton:
|
|||
if self.clear_before_caption_draw:
|
||||
for ty in range(self.height):
|
||||
for tx in range(self.width):
|
||||
self.element.art.set_char_index_at(0, 0, self.x+tx, y+ty, 0)
|
||||
self.element.art.set_char_index_at(0, 0, self.x + tx, y + ty, 0)
|
||||
# leave FG color None; should already have been set
|
||||
self.element.art.write_string(0, 0, self.x, y, text, None)
|
||||
|
||||
|
||||
def draw(self):
|
||||
if self.never_draw:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
# coding=utf-8
|
||||
|
||||
import os
|
||||
|
||||
import sdl2
|
||||
|
||||
from art import UV_FLIPY, UV_NORMAL
|
||||
from renderable_sprite import UISpriteRenderable
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_button import UIButton
|
||||
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY
|
||||
from ui_colors import UIColors
|
||||
from ui_dialog import Field, UIDialog
|
||||
|
||||
|
||||
class ChooserItemButton(UIButton):
|
||||
|
||||
"button representing a ChooserItem"
|
||||
|
||||
|
||||
item = None
|
||||
width = 20
|
||||
big_width = 30
|
||||
clear_before_caption_draw = True
|
||||
|
||||
|
||||
def __init__(self, element):
|
||||
# more room for list items if screen is wide enough
|
||||
if element.ui.width_tiles - 20 > element.big_width:
|
||||
self.width = self.big_width
|
||||
UIButton.__init__(self, element)
|
||||
self.callback = self.pick_item
|
||||
|
||||
|
||||
def pick_item(self):
|
||||
if not self.item:
|
||||
return
|
||||
|
|
@ -33,20 +31,20 @@ class ChooserItemButton(UIButton):
|
|||
|
||||
|
||||
class ScrollArrowButton(UIButton):
|
||||
|
||||
"button that scrolls up or down in a chooser item view"
|
||||
|
||||
|
||||
arrow_char = 129
|
||||
up = True
|
||||
normal_bg_color = UIDialog.bg_color
|
||||
dimmed_fg_color = UIColors.medgrey
|
||||
dimmed_bg_color = UIDialog.bg_color
|
||||
|
||||
|
||||
def draw_caption(self):
|
||||
xform = [UV_FLIPY, UV_NORMAL][self.up]
|
||||
self.element.art.set_tile_at(0, 0, self.x, self.y + self.caption_y,
|
||||
self.arrow_char, None, None, xform)
|
||||
|
||||
self.element.art.set_tile_at(
|
||||
0, 0, self.x, self.y + self.caption_y, self.arrow_char, None, None, xform
|
||||
)
|
||||
|
||||
def callback(self):
|
||||
if self.up and self.element.scroll_index > 0:
|
||||
self.element.scroll_index -= 1
|
||||
|
|
@ -59,9 +57,8 @@ class ScrollArrowButton(UIButton):
|
|||
|
||||
|
||||
class ChooserItem:
|
||||
|
||||
label = 'Chooser item'
|
||||
|
||||
label = "Chooser item"
|
||||
|
||||
def __init__(self, index, name):
|
||||
self.index = index
|
||||
# item's unique name, eg a filename
|
||||
|
|
@ -69,43 +66,44 @@ class ChooserItem:
|
|||
self.label = self.get_label()
|
||||
# validity flag lets ChooserItem subclasses exclude themselves
|
||||
self.valid = True
|
||||
|
||||
def get_label(self): return self.name
|
||||
|
||||
def get_description_lines(self): return []
|
||||
|
||||
def get_preview_texture(self): return None
|
||||
|
||||
def load(self, app): pass
|
||||
|
||||
|
||||
def get_label(self):
|
||||
return self.name
|
||||
|
||||
def get_description_lines(self):
|
||||
return []
|
||||
|
||||
def get_preview_texture(self):
|
||||
return None
|
||||
|
||||
def load(self, app):
|
||||
pass
|
||||
|
||||
def picked(self, element):
|
||||
# set item selected and refresh preview
|
||||
element.set_selected_item_index(self.index)
|
||||
|
||||
|
||||
class ChooserDialog(UIDialog):
|
||||
|
||||
title = 'Chooser'
|
||||
confirm_caption = 'Set'
|
||||
cancel_caption = 'Close'
|
||||
message = ''
|
||||
title = "Chooser"
|
||||
confirm_caption = "Set"
|
||||
cancel_caption = "Close"
|
||||
message = ""
|
||||
# if True, chooser shows files; show filename on first line of description
|
||||
show_filenames = False
|
||||
directory_aware = False
|
||||
tile_width, tile_height = 60, 20
|
||||
# use these if screen is big enough
|
||||
big_width, big_height = 80, 30
|
||||
fields = [
|
||||
Field(label='', type=str, width=tile_width - 4, oneline=True)
|
||||
]
|
||||
fields = [Field(label="", type=str, width=tile_width - 4, oneline=True)]
|
||||
item_start_x, item_start_y = 2, 4
|
||||
no_preview_label = 'No preview available!'
|
||||
no_preview_label = "No preview available!"
|
||||
show_preview_image = True
|
||||
item_button_class = ChooserItemButton
|
||||
chooser_item_class = ChooserItem
|
||||
scrollbar_shade_char = 54
|
||||
flip_preview_y = True
|
||||
|
||||
|
||||
def __init__(self, ui, options):
|
||||
self.ui = ui
|
||||
# semikludge: track whether user has selected anything in a new dir,
|
||||
|
|
@ -113,12 +111,13 @@ class ChooserDialog(UIDialog):
|
|||
self.first_selection_made = False
|
||||
if self.ui.width_tiles - 20 > self.big_width:
|
||||
self.tile_width = self.big_width
|
||||
self.fields[0] = Field(label='', type=str,
|
||||
width=self.tile_width - 4, oneline=True)
|
||||
self.fields[0] = Field(
|
||||
label="", type=str, width=self.tile_width - 4, oneline=True
|
||||
)
|
||||
if self.ui.height_tiles - 30 > self.big_height:
|
||||
self.tile_height = self.big_height
|
||||
self.items_in_view = self.tile_height - self.item_start_y - 3
|
||||
self.field_texts = ['']
|
||||
self.field_texts = [""]
|
||||
# set active field earlier than UIDialog.init so set_initial_dir
|
||||
# can change its text
|
||||
self.active_field = 0
|
||||
|
|
@ -149,7 +148,7 @@ class ChooserDialog(UIDialog):
|
|||
self.preview_renderable.blend = False
|
||||
# offset into items list view provided by buttons starts from
|
||||
self.position_preview()
|
||||
|
||||
|
||||
def init_buttons(self):
|
||||
for i in range(self.items_in_view):
|
||||
button = self.item_button_class(self)
|
||||
|
|
@ -167,24 +166,24 @@ class ChooserDialog(UIDialog):
|
|||
self.down_arrow_button.y = self.item_start_y + self.items_in_view - 1
|
||||
self.down_arrow_button.up = False
|
||||
self.buttons += [self.up_arrow_button, self.down_arrow_button]
|
||||
|
||||
|
||||
def set_initial_dir(self):
|
||||
# for directory-aware dialogs, subclasses specify here where to start
|
||||
self.current_dir = '.'
|
||||
|
||||
self.current_dir = "."
|
||||
|
||||
def change_current_dir(self, new_dir):
|
||||
# check permissions:
|
||||
# os.access(new_dir, os.R_OK) seems to always return True,
|
||||
# so try/catch listdir instead
|
||||
try:
|
||||
l = os.listdir(new_dir)
|
||||
except PermissionError as e:
|
||||
line = 'No permission to access %s!' % os.path.abspath(new_dir)
|
||||
except PermissionError:
|
||||
line = "No permission to access %s!" % os.path.abspath(new_dir)
|
||||
self.ui.message_line.post_line(line, error=True)
|
||||
return False
|
||||
self.current_dir = new_dir
|
||||
if not self.current_dir.endswith('/'):
|
||||
self.current_dir += '/'
|
||||
if not self.current_dir.endswith("/"):
|
||||
self.current_dir += "/"
|
||||
# redo items and redraw
|
||||
self.selected_item_index = 0
|
||||
self.scroll_index = 0
|
||||
|
|
@ -192,9 +191,8 @@ class ChooserDialog(UIDialog):
|
|||
self.items = self.get_items()
|
||||
self.reset_art(False)
|
||||
return True
|
||||
|
||||
def set_selected_item_index(self, new_index, set_field_text=True,
|
||||
update_view=True):
|
||||
|
||||
def set_selected_item_index(self, new_index, set_field_text=True, update_view=True):
|
||||
"""
|
||||
set the view's selected item to specified index
|
||||
perform usually-necessary refresh functions for convenience
|
||||
|
|
@ -202,17 +200,25 @@ class ChooserDialog(UIDialog):
|
|||
move_dir = new_index - self.selected_item_index
|
||||
self.selected_item_index = new_index
|
||||
can_scroll = len(self.items) > self.items_in_view
|
||||
should_scroll = self.selected_item_index >= self.scroll_index + self.items_in_view or self.selected_item_index < self.scroll_index
|
||||
should_scroll = (
|
||||
self.selected_item_index >= self.scroll_index + self.items_in_view
|
||||
or self.selected_item_index < self.scroll_index
|
||||
)
|
||||
if not can_scroll:
|
||||
self.scroll_index = 0
|
||||
elif should_scroll:
|
||||
# keep selection in bounds
|
||||
self.selected_item_index = min(self.selected_item_index, len(self.items)-1)
|
||||
self.selected_item_index = min(
|
||||
self.selected_item_index, len(self.items) - 1
|
||||
)
|
||||
# scrolling up
|
||||
if move_dir <= 0:
|
||||
self.scroll_index = self.selected_item_index
|
||||
# scrolling down
|
||||
elif move_dir > 0 and self.selected_item_index - self.scroll_index == self.items_in_view:
|
||||
elif (
|
||||
move_dir > 0
|
||||
and self.selected_item_index - self.scroll_index == self.items_in_view
|
||||
):
|
||||
self.scroll_index = self.selected_item_index - self.items_in_view + 1
|
||||
# keep scroll in bounds
|
||||
self.scroll_index = min(self.scroll_index, self.get_max_scroll())
|
||||
|
|
@ -225,31 +231,35 @@ class ChooserDialog(UIDialog):
|
|||
self.load_selected_item()
|
||||
self.reset_art(False)
|
||||
self.position_preview()
|
||||
|
||||
|
||||
def get_max_scroll(self):
|
||||
return len(self.items) - self.items_in_view
|
||||
|
||||
|
||||
def get_selected_item(self):
|
||||
# return None if out of bounds
|
||||
return self.items[self.selected_item_index] if self.selected_item_index < len(self.items) else None
|
||||
|
||||
return (
|
||||
self.items[self.selected_item_index]
|
||||
if self.selected_item_index < len(self.items)
|
||||
else None
|
||||
)
|
||||
|
||||
def load_selected_item(self):
|
||||
item = self.get_selected_item()
|
||||
item.load(self.ui.app)
|
||||
|
||||
|
||||
def get_initial_selection(self):
|
||||
# subclasses return index of initial selection
|
||||
return 0
|
||||
|
||||
|
||||
def set_preview(self):
|
||||
item = self.get_selected_item()
|
||||
if self.show_preview_image:
|
||||
self.preview_renderable.texture = item.get_preview_texture(self.ui.app)
|
||||
|
||||
|
||||
def get_items(self):
|
||||
# subclasses generate lists of items here
|
||||
return []
|
||||
|
||||
|
||||
def position_preview(self, reset=True):
|
||||
if reset:
|
||||
self.set_preview()
|
||||
|
|
@ -261,29 +271,38 @@ class ChooserDialog(UIDialog):
|
|||
self.preview_renderable.x = self.x + x
|
||||
self.preview_renderable.scale_x = (self.tile_width - 2) * qw - x
|
||||
# determine height based on width, then y position
|
||||
img_inv_aspect = self.preview_renderable.texture.height / self.preview_renderable.texture.width
|
||||
img_inv_aspect = (
|
||||
self.preview_renderable.texture.height
|
||||
/ self.preview_renderable.texture.width
|
||||
)
|
||||
screen_aspect = self.ui.app.window_width / self.ui.app.window_height
|
||||
self.preview_renderable.scale_y = self.preview_renderable.scale_x * img_inv_aspect * screen_aspect
|
||||
self.preview_renderable.scale_y = (
|
||||
self.preview_renderable.scale_x * img_inv_aspect * screen_aspect
|
||||
)
|
||||
y = (self.description_end_y + 1) * qh
|
||||
# if preview height is above max allotted size, set height to fill size
|
||||
# and scale down width
|
||||
max_y = (self.tile_height - 3) * qh
|
||||
if self.preview_renderable.scale_y > max_y - y:
|
||||
self.preview_renderable.scale_y = max_y - y
|
||||
self.preview_renderable.scale_x = self.preview_renderable.scale_y * (1 / img_inv_aspect) * (1 / screen_aspect)
|
||||
self.preview_renderable.scale_x = (
|
||||
self.preview_renderable.scale_y
|
||||
* (1 / img_inv_aspect)
|
||||
* (1 / screen_aspect)
|
||||
)
|
||||
# flip in Y for some (palettes) but not for others (charsets)
|
||||
if self.flip_preview_y:
|
||||
self.preview_renderable.scale_y = -self.preview_renderable.scale_y
|
||||
else:
|
||||
y += self.preview_renderable.scale_y
|
||||
self.preview_renderable.y = self.y - y
|
||||
|
||||
|
||||
def get_height(self, msg_lines):
|
||||
return self.tile_height
|
||||
|
||||
|
||||
def reset_buttons(self):
|
||||
# (re)generate buttons from contents of self.items
|
||||
for i,button in enumerate(self.item_buttons):
|
||||
for i, button in enumerate(self.item_buttons):
|
||||
# ??? each button's callback loads charset/palette/whatev
|
||||
if i >= len(self.items):
|
||||
button.never_draw = True
|
||||
|
|
@ -310,23 +329,23 @@ class ChooserDialog(UIDialog):
|
|||
if not self.up_arrow_button:
|
||||
return
|
||||
# dim scroll buttons if we don't have enough items to scroll
|
||||
state, hover = 'normal', True
|
||||
state, hover = "normal", True
|
||||
if len(self.items) <= self.items_in_view:
|
||||
state = 'dimmed'
|
||||
state = "dimmed"
|
||||
hover = False
|
||||
for button in [self.up_arrow_button, self.down_arrow_button]:
|
||||
button.set_state(state)
|
||||
button.can_hover = hover
|
||||
|
||||
|
||||
def get_description_filename(self, item):
|
||||
"returns a description-appropriate filename for given item"
|
||||
# truncate from start to fit in description area if needed
|
||||
max_width = self.tile_width
|
||||
max_width -= self.item_start_x + self.item_button_width + 5
|
||||
if len(item.name) > max_width - 1:
|
||||
return '…' + item.name[-max_width:]
|
||||
return "…" + item.name[-max_width:]
|
||||
return item.name
|
||||
|
||||
|
||||
def get_selected_description_lines(self):
|
||||
item = self.get_selected_item()
|
||||
lines = []
|
||||
|
|
@ -334,7 +353,7 @@ class ChooserDialog(UIDialog):
|
|||
lines += [self.get_description_filename(item)]
|
||||
lines += item.get_description_lines() or []
|
||||
return lines
|
||||
|
||||
|
||||
def draw_selected_description(self):
|
||||
x = self.tile_width - 2
|
||||
y = self.item_start_y
|
||||
|
|
@ -343,11 +362,10 @@ class ChooserDialog(UIDialog):
|
|||
# trim line if it's too long
|
||||
max_width = self.tile_width - self.item_button_width - 7
|
||||
line = line[:max_width]
|
||||
self.art.write_string(0, 0, x, y, line, None, None,
|
||||
right_justify=True)
|
||||
self.art.write_string(0, 0, x, y, line, None, None, right_justify=True)
|
||||
y += 1
|
||||
self.description_end_y = y
|
||||
|
||||
|
||||
def reset_art(self, resize=True):
|
||||
self.reset_buttons()
|
||||
# UIDialog does: clear window, draw titlebar and confirm/cancel buttons
|
||||
|
|
@ -363,33 +381,34 @@ class ChooserDialog(UIDialog):
|
|||
if len(self.items) <= self.items_in_view:
|
||||
fg = self.up_arrow_button.dimmed_fg_color
|
||||
for y in range(self.up_arrow_button.y + 1, self.down_arrow_button.y):
|
||||
self.art.set_tile_at(0, 0, self.up_arrow_button.x, y,
|
||||
self.scrollbar_shade_char, fg)
|
||||
|
||||
self.art.set_tile_at(
|
||||
0, 0, self.up_arrow_button.x, y, self.scrollbar_shade_char, fg
|
||||
)
|
||||
|
||||
def update_drag(self, mouse_dx, mouse_dy):
|
||||
UIDialog.update_drag(self, mouse_dx, mouse_dy)
|
||||
# update thumbnail renderable's position too
|
||||
self.position_preview(False)
|
||||
|
||||
|
||||
def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
keystr = sdl2.SDL_GetKeyName(key).decode()
|
||||
# up/down keys navigate list
|
||||
new_index = self.selected_item_index
|
||||
navigated = False
|
||||
if keystr == 'Return':
|
||||
if keystr == "Return":
|
||||
# if handle_enter returns True, bail before rest of input handling -
|
||||
# make sure any changes to handle_enter are safe for this!
|
||||
if self.handle_enter(shift_pressed, alt_pressed, ctrl_pressed):
|
||||
return
|
||||
elif keystr == 'Up':
|
||||
elif keystr == "Up":
|
||||
navigated = True
|
||||
if self.selected_item_index > 0:
|
||||
new_index -= 1
|
||||
elif keystr == 'Down':
|
||||
elif keystr == "Down":
|
||||
navigated = True
|
||||
if self.selected_item_index < len(self.items) - 1:
|
||||
new_index += 1
|
||||
elif keystr == 'PageUp':
|
||||
elif keystr == "PageUp":
|
||||
navigated = True
|
||||
page_size = int(self.items_in_view / 2)
|
||||
new_index -= page_size
|
||||
|
|
@ -397,7 +416,7 @@ class ChooserDialog(UIDialog):
|
|||
# scroll follows selection jumps
|
||||
self.scroll_index -= page_size
|
||||
self.scroll_index = max(0, self.scroll_index)
|
||||
elif keystr == 'PageDown':
|
||||
elif keystr == "PageDown":
|
||||
navigated = True
|
||||
page_size = int(self.items_in_view / 2)
|
||||
new_index += page_size
|
||||
|
|
@ -405,11 +424,11 @@ class ChooserDialog(UIDialog):
|
|||
self.scroll_index += page_size
|
||||
self.scroll_index = min(self.scroll_index, self.get_max_scroll())
|
||||
# home/end: beginning/end of list, respectively
|
||||
elif keystr == 'Home':
|
||||
elif keystr == "Home":
|
||||
navigated = True
|
||||
new_index = 0
|
||||
self.scroll_index = 0
|
||||
elif keystr == 'End':
|
||||
elif keystr == "End":
|
||||
navigated = True
|
||||
new_index = len(self.items) - 1
|
||||
self.scroll_index = len(self.items) - self.items_in_view
|
||||
|
|
@ -419,33 +438,33 @@ class ChooserDialog(UIDialog):
|
|||
# if we didn't navigate, seek based on new alphanumeric input
|
||||
if not navigated:
|
||||
self.text_input_seek()
|
||||
|
||||
|
||||
def text_input_seek(self):
|
||||
field_text = self.field_texts[self.active_field]
|
||||
if field_text.strip() == '':
|
||||
if field_text.strip() == "":
|
||||
return
|
||||
# seek should be case-insensitive
|
||||
field_text = field_text.lower()
|
||||
# field text may be a full path; only care about the base
|
||||
field_text = os.path.basename(field_text)
|
||||
for i,item in enumerate(self.items):
|
||||
for i, item in enumerate(self.items):
|
||||
# match to base item name within dir
|
||||
# (if it's a dir, snip last / for match)
|
||||
item_base = item.name.lower()
|
||||
if item_base.endswith('/'):
|
||||
if item_base.endswith("/"):
|
||||
item_base = item_base[:-1]
|
||||
item_base = os.path.basename(item_base)
|
||||
item_base = os.path.splitext(item_base)[0]
|
||||
if item_base.startswith(field_text):
|
||||
self.set_selected_item_index(i, set_field_text=False)
|
||||
break
|
||||
|
||||
|
||||
def handle_enter(self, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
"handle Enter key, return False if rest of handle_input should continue"
|
||||
# if selected item is already in text field, pick it
|
||||
field_text = self.field_texts[self.active_field]
|
||||
selected_item = self.get_selected_item()
|
||||
if field_text.strip() == '':
|
||||
if field_text.strip() == "":
|
||||
self.field_texts[self.active_field] = field_text = selected_item.name
|
||||
return True
|
||||
if field_text == selected_item.name:
|
||||
|
|
@ -459,9 +478,13 @@ class ChooserDialog(UIDialog):
|
|||
self.field_texts[self.active_field] = selected_item.name
|
||||
return True
|
||||
# special case for parent dir ..
|
||||
if self.directory_aware and field_text == self.current_dir and selected_item.name == '..':
|
||||
if (
|
||||
self.directory_aware
|
||||
and field_text == self.current_dir
|
||||
and selected_item.name == ".."
|
||||
):
|
||||
self.first_selection_made = True
|
||||
return self.change_current_dir('..')
|
||||
return self.change_current_dir("..")
|
||||
if self.directory_aware and os.path.isdir(field_text):
|
||||
self.first_selection_made = True
|
||||
return self.change_current_dir(field_text)
|
||||
|
|
@ -470,13 +493,13 @@ class ChooserDialog(UIDialog):
|
|||
# if a file, change to its dir and select it
|
||||
if self.directory_aware and file_dir_name != self.current_dir:
|
||||
if self.change_current_dir(file_dir_name):
|
||||
for i,item in enumerate(self.items):
|
||||
for i, item in enumerate(self.items):
|
||||
if item.name == field_text:
|
||||
self.set_selected_item_index(i)
|
||||
item.picked(self)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def render(self):
|
||||
UIDialog.render(self)
|
||||
if self.show_preview_image and self.preview_renderable.texture:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
class UIColors:
|
||||
"color indices for UI (c64 original) palette"
|
||||
|
||||
white = 2
|
||||
lightgrey = 16
|
||||
medgrey = 13
|
||||
|
|
|
|||
338
ui_console.py
338
ui_console.py
|
|
@ -1,46 +1,45 @@
|
|||
import os
|
||||
import sdl2
|
||||
from math import ceil
|
||||
|
||||
from ui_element import UIElement
|
||||
from art import UV_FLIPY
|
||||
from key_shifts import SHIFT_MAP
|
||||
|
||||
from image_convert import ImageConverter
|
||||
from palette import PaletteFromFile
|
||||
|
||||
from image_export import export_still_image, export_animation
|
||||
|
||||
from PIL import Image
|
||||
import sdl2
|
||||
|
||||
# imports for console execution namespace - be careful!
|
||||
from OpenGL import GL
|
||||
from art import UV_FLIPY
|
||||
from image_convert import ImageConverter
|
||||
from image_export import export_animation, export_still_image
|
||||
from key_shifts import SHIFT_MAP
|
||||
from palette import PaletteFromFile
|
||||
from ui_element import UIElement
|
||||
|
||||
CONSOLE_HISTORY_FILENAME = "console_history"
|
||||
|
||||
CONSOLE_HISTORY_FILENAME = 'console_history'
|
||||
|
||||
class ConsoleCommand:
|
||||
"parent class for console commands"
|
||||
description = '[Enter a description for this command!]'
|
||||
|
||||
description = "[Enter a description for this command!]"
|
||||
|
||||
def execute(console, args):
|
||||
return 'Test command executed.'
|
||||
return "Test command executed."
|
||||
|
||||
|
||||
class QuitCommand(ConsoleCommand):
|
||||
description = 'Quit Playscii.'
|
||||
description = "Quit Playscii."
|
||||
|
||||
def execute(console, args):
|
||||
console.ui.app.should_quit = True
|
||||
|
||||
|
||||
class SaveCommand(ConsoleCommand):
|
||||
description = 'Save active art, under new filename if given.'
|
||||
description = "Save active art, under new filename if given."
|
||||
|
||||
def execute(console, args):
|
||||
# save currently active file
|
||||
art = console.ui.active_art
|
||||
# set new filename if given
|
||||
if len(args) > 0:
|
||||
old_filename = art.filename
|
||||
art.set_filename(' '.join(args))
|
||||
art.set_filename(" ".join(args))
|
||||
art.save_to_file()
|
||||
console.ui.app.load_art_for_edit(old_filename)
|
||||
console.ui.set_active_art_by_filename(art.filename)
|
||||
|
|
@ -50,71 +49,88 @@ class SaveCommand(ConsoleCommand):
|
|||
|
||||
|
||||
class OpenCommand(ConsoleCommand):
|
||||
description = 'Open art with given filename.'
|
||||
description = "Open art with given filename."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: open [art filename]'
|
||||
filename = ' '.join(args)
|
||||
return "Usage: open [art filename]"
|
||||
filename = " ".join(args)
|
||||
console.ui.app.load_art_for_edit(filename)
|
||||
|
||||
|
||||
class RevertArtCommand(ConsoleCommand):
|
||||
description = 'Revert active art to last saved version.'
|
||||
description = "Revert active art to last saved version."
|
||||
|
||||
def execute(console, args):
|
||||
console.ui.app.revert_active_art()
|
||||
|
||||
|
||||
class LoadPaletteCommand(ConsoleCommand):
|
||||
description = 'Set the given color palette as active.'
|
||||
description = "Set the given color palette as active."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: pal [palette filename]'
|
||||
filename = ' '.join(args)
|
||||
return "Usage: pal [palette filename]"
|
||||
filename = " ".join(args)
|
||||
# load AND set
|
||||
palette = console.ui.app.load_palette(filename)
|
||||
console.ui.active_art.set_palette(palette)
|
||||
console.ui.popup.set_active_palette(palette)
|
||||
|
||||
|
||||
class LoadCharSetCommand(ConsoleCommand):
|
||||
description = 'Set the given character set as active.'
|
||||
description = "Set the given character set as active."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: char [character set filename]'
|
||||
filename = ' '.join(args)
|
||||
return "Usage: char [character set filename]"
|
||||
filename = " ".join(args)
|
||||
charset = console.ui.app.load_charset(filename)
|
||||
console.ui.active_art.set_charset(charset)
|
||||
console.ui.popup.set_active_charset(charset)
|
||||
|
||||
|
||||
class ImageExportCommand(ConsoleCommand):
|
||||
description = 'Export active art as PNG image.'
|
||||
description = "Export active art as PNG image."
|
||||
|
||||
def execute(console, args):
|
||||
export_still_image(console.ui.app, console.ui.active_art)
|
||||
|
||||
|
||||
class AnimExportCommand(ConsoleCommand):
|
||||
description = 'Export active art as animated GIF image.'
|
||||
description = "Export active art as animated GIF image."
|
||||
|
||||
def execute(console, args):
|
||||
export_animation(console.ui.app, console.ui.active_art)
|
||||
|
||||
|
||||
class ConvertImageCommand(ConsoleCommand):
|
||||
description = 'Convert given bitmap image to current character set + color palette.'
|
||||
description = "Convert given bitmap image to current character set + color palette."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: conv [image filename]'
|
||||
image_filename = ' '.join(args)
|
||||
return "Usage: conv [image filename]"
|
||||
image_filename = " ".join(args)
|
||||
ImageConverter(console.ui.app, image_filename, console.ui.active_art)
|
||||
console.ui.app.update_window_title()
|
||||
|
||||
|
||||
class OverlayImageCommand(ConsoleCommand):
|
||||
description = 'Draw given bitmap image over active art document.'
|
||||
description = "Draw given bitmap image over active art document."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: img [image filename]'
|
||||
image_filename = ' '.join(args)
|
||||
return "Usage: img [image filename]"
|
||||
image_filename = " ".join(args)
|
||||
console.ui.app.set_overlay_image(image_filename)
|
||||
|
||||
|
||||
class ImportCommand(ConsoleCommand):
|
||||
description = 'Import file using an ArtImport class'
|
||||
description = "Import file using an ArtImport class"
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) < 2:
|
||||
return 'Usage: imp [ArtImporter class name] [filename]'
|
||||
return "Usage: imp [ArtImporter class name] [filename]"
|
||||
importers = console.ui.app.get_importers()
|
||||
importer_classname, filename = args[0], args[1]
|
||||
importer_class = None
|
||||
|
|
@ -127,11 +143,13 @@ class ImportCommand(ConsoleCommand):
|
|||
console.ui.app.log("Couldn't find file %s" % filename)
|
||||
importer = importer_class(console.ui.app, filename)
|
||||
|
||||
|
||||
class ExportCommand(ConsoleCommand):
|
||||
description = 'Export current art using an ArtExport class'
|
||||
description = "Export current art using an ArtExport class"
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) < 2:
|
||||
return 'Usage: exp [ArtExporter class name] [filename]'
|
||||
return "Usage: exp [ArtExporter class name] [filename]"
|
||||
exporters = console.ui.app.get_exporters()
|
||||
exporter_classname, filename = args[0], args[1]
|
||||
exporter_class = None
|
||||
|
|
@ -142,116 +160,134 @@ class ExportCommand(ConsoleCommand):
|
|||
console.ui.app.log("Couldn't find exporter class %s" % exporter_classname)
|
||||
exporter = exporter_class(console.ui.app, filename)
|
||||
|
||||
|
||||
class PaletteFromImageCommand(ConsoleCommand):
|
||||
description = 'Convert given image into a palette file.'
|
||||
description = "Convert given image into a palette file."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: getpal [image filename]'
|
||||
src_filename = ' '.join(args)
|
||||
return "Usage: getpal [image filename]"
|
||||
src_filename = " ".join(args)
|
||||
new_pal = PaletteFromFile(console.ui.app, src_filename, src_filename)
|
||||
if not new_pal.init_success:
|
||||
return
|
||||
#console.ui.app.load_palette(new_pal.filename)
|
||||
# console.ui.app.load_palette(new_pal.filename)
|
||||
console.ui.app.palettes.append(new_pal)
|
||||
console.ui.active_art.set_palette(new_pal)
|
||||
console.ui.popup.set_active_palette(new_pal)
|
||||
|
||||
|
||||
class SetGameDirCommand(ConsoleCommand):
|
||||
description = 'Load game from the given folder.'
|
||||
description = "Load game from the given folder."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: setgame [game dir name]'
|
||||
game_dir_name = ' '.join(args)
|
||||
return "Usage: setgame [game dir name]"
|
||||
game_dir_name = " ".join(args)
|
||||
console.ui.app.gw.set_game_dir(game_dir_name, True)
|
||||
|
||||
|
||||
class LoadGameStateCommand(ConsoleCommand):
|
||||
description = 'Load the given game state save file.'
|
||||
description = "Load the given game state save file."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: game [game state filename]'
|
||||
gs_name = ' '.join(args)
|
||||
return "Usage: game [game state filename]"
|
||||
gs_name = " ".join(args)
|
||||
console.ui.app.gw.load_game_state(gs_name)
|
||||
|
||||
|
||||
class SaveGameStateCommand(ConsoleCommand):
|
||||
description = 'Save the current game state as the given filename.'
|
||||
description = "Save the current game state as the given filename."
|
||||
|
||||
def execute(console, args):
|
||||
"Usage: savegame [game state filename]"
|
||||
gs_name = ' '.join(args)
|
||||
gs_name = " ".join(args)
|
||||
console.ui.app.gw.save_to_file(gs_name)
|
||||
|
||||
|
||||
class SpawnObjectCommand(ConsoleCommand):
|
||||
description = 'Spawn an object of the given class name.'
|
||||
description = "Spawn an object of the given class name."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: spawn [class name]'
|
||||
class_name = ' '.join(args)
|
||||
return "Usage: spawn [class name]"
|
||||
class_name = " ".join(args)
|
||||
console.ui.app.gw.spawn_object_of_class(class_name)
|
||||
|
||||
|
||||
class CommandListCommand(ConsoleCommand):
|
||||
description = 'Show the list of console commands.'
|
||||
description = "Show the list of console commands."
|
||||
|
||||
def execute(console, args):
|
||||
# TODO: print a command with usage if available
|
||||
console.ui.app.log('Commands:')
|
||||
console.ui.app.log("Commands:")
|
||||
# alphabetize command list
|
||||
command_list = list(commands.keys())
|
||||
command_list.sort()
|
||||
for command in command_list:
|
||||
desc = commands[command].description
|
||||
console.ui.app.log(' %s - %s' % (command, desc))
|
||||
console.ui.app.log(" %s - %s" % (command, desc))
|
||||
|
||||
|
||||
class RunArtScriptCommand(ConsoleCommand):
|
||||
description = 'Run art script with given filename on active art.'
|
||||
description = "Run art script with given filename on active art."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) == 0:
|
||||
return 'Usage: src [art script filename]'
|
||||
filename = ' '.join(args)
|
||||
return "Usage: src [art script filename]"
|
||||
filename = " ".join(args)
|
||||
console.ui.active_art.run_script(filename)
|
||||
|
||||
|
||||
class RunEveryArtScriptCommand(ConsoleCommand):
|
||||
description = 'Run art script with given filename on active art at given rate.'
|
||||
description = "Run art script with given filename on active art at given rate."
|
||||
|
||||
def execute(console, args):
|
||||
if len(args) < 2:
|
||||
return 'Usage: srcev [rate] [art script filename]'
|
||||
return "Usage: srcev [rate] [art script filename]"
|
||||
rate = float(args[0])
|
||||
filename = ' '.join(args[1:])
|
||||
filename = " ".join(args[1:])
|
||||
console.ui.active_art.run_script_every(filename, rate)
|
||||
# hide so user can immediately see what script is doing
|
||||
console.hide()
|
||||
|
||||
|
||||
class StopArtScriptsCommand(ConsoleCommand):
|
||||
description = 'Stop all actively running art scripts.'
|
||||
description = "Stop all actively running art scripts."
|
||||
|
||||
def execute(console, args):
|
||||
console.ui.active_art.stop_all_scripts()
|
||||
|
||||
|
||||
# map strings to command classes for ConsoleUI.parse
|
||||
commands = {
|
||||
'exit': QuitCommand,
|
||||
'quit': QuitCommand,
|
||||
'save': SaveCommand,
|
||||
'open': OpenCommand,
|
||||
'char': LoadCharSetCommand,
|
||||
'pal': LoadPaletteCommand,
|
||||
'imgexp': ImageExportCommand,
|
||||
'animexport': AnimExportCommand,
|
||||
'conv': ConvertImageCommand,
|
||||
'getpal': PaletteFromImageCommand,
|
||||
'setgame': SetGameDirCommand,
|
||||
'game': LoadGameStateCommand,
|
||||
'savegame': SaveGameStateCommand,
|
||||
'spawn': SpawnObjectCommand,
|
||||
'help': CommandListCommand,
|
||||
'scr': RunArtScriptCommand,
|
||||
'screv': RunEveryArtScriptCommand,
|
||||
'scrstop': StopArtScriptsCommand,
|
||||
'revert': RevertArtCommand,
|
||||
'img': OverlayImageCommand,
|
||||
'imp': ImportCommand,
|
||||
'exp': ExportCommand
|
||||
"exit": QuitCommand,
|
||||
"quit": QuitCommand,
|
||||
"save": SaveCommand,
|
||||
"open": OpenCommand,
|
||||
"char": LoadCharSetCommand,
|
||||
"pal": LoadPaletteCommand,
|
||||
"imgexp": ImageExportCommand,
|
||||
"animexport": AnimExportCommand,
|
||||
"conv": ConvertImageCommand,
|
||||
"getpal": PaletteFromImageCommand,
|
||||
"setgame": SetGameDirCommand,
|
||||
"game": LoadGameStateCommand,
|
||||
"savegame": SaveGameStateCommand,
|
||||
"spawn": SpawnObjectCommand,
|
||||
"help": CommandListCommand,
|
||||
"scr": RunArtScriptCommand,
|
||||
"screv": RunEveryArtScriptCommand,
|
||||
"scrstop": StopArtScriptsCommand,
|
||||
"revert": RevertArtCommand,
|
||||
"img": OverlayImageCommand,
|
||||
"imp": ImportCommand,
|
||||
"exp": ExportCommand,
|
||||
}
|
||||
|
||||
|
||||
class ConsoleUI(UIElement):
|
||||
|
||||
visible = False
|
||||
snap_top = True
|
||||
snap_left = True
|
||||
|
|
@ -260,18 +296,18 @@ class ConsoleUI(UIElement):
|
|||
# how long (seconds) to shift/fade into view when invoked
|
||||
show_anim_time = 0.75
|
||||
bg_alpha = 0.75
|
||||
prompt = '>'
|
||||
prompt = ">"
|
||||
# _ ish char
|
||||
bottom_line_char_index = 76
|
||||
right_margin = 3
|
||||
# transient, but must be set here b/c UIElement.init calls reset_art
|
||||
current_line = ''
|
||||
current_line = ""
|
||||
game_mode_visible = True
|
||||
all_modes_visible = True
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.bg_color_index = ui.colors.darkgrey
|
||||
self.highlight_color = 8 # yellow
|
||||
self.highlight_color = 8 # yellow
|
||||
UIElement.__init__(self, ui)
|
||||
# state stuff for console move/fade
|
||||
self.alpha = 0
|
||||
|
|
@ -283,26 +319,28 @@ class ConsoleUI(UIElement):
|
|||
self.last_lines = []
|
||||
self.history_filename = self.ui.app.config_dir + CONSOLE_HISTORY_FILENAME
|
||||
if os.path.exists(self.history_filename):
|
||||
self.history_file = open(self.history_filename, 'r')
|
||||
self.history_file = open(self.history_filename)
|
||||
try:
|
||||
self.command_history = self.history_file.readlines()
|
||||
except:
|
||||
self.command_history = []
|
||||
self.history_file = open(self.history_filename, 'a')
|
||||
self.history_file = open(self.history_filename, "a")
|
||||
else:
|
||||
self.history_file = open(self.history_filename, 'w+')
|
||||
self.history_file = open(self.history_filename, "w+")
|
||||
self.command_history = []
|
||||
self.history_index = 0
|
||||
# junk data in last user line so it changes on first update
|
||||
self.last_user_line = 'test'
|
||||
self.last_user_line = "test"
|
||||
# max line length = width of console minus prompt + _
|
||||
self.max_line_length = int(self.art.width) - self.right_margin
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
self.width = ceil(self.ui.width_tiles * self.ui.scale)
|
||||
# % of screen must take aspect into account
|
||||
inv_aspect = self.ui.app.window_height / self.ui.app.window_width
|
||||
self.height = int(self.ui.height_tiles * self.height_screen_pct * inv_aspect * self.ui.scale)
|
||||
self.height = int(
|
||||
self.ui.height_tiles * self.height_screen_pct * inv_aspect * self.ui.scale
|
||||
)
|
||||
# dim background
|
||||
self.renderable.bg_alpha = self.bg_alpha
|
||||
# must resize here, as window width will vary
|
||||
|
|
@ -311,43 +349,43 @@ class ConsoleUI(UIElement):
|
|||
self.text_color = self.ui.palette.lightest_index
|
||||
self.clear()
|
||||
# truncate current user line if it's too long for new width
|
||||
self.current_line = self.current_line[:self.max_line_length]
|
||||
#self.update_user_line()
|
||||
self.current_line = self.current_line[: self.max_line_length]
|
||||
# self.update_user_line()
|
||||
# empty log lines so they refresh from app
|
||||
self.last_user_line = 'XXtestXX'
|
||||
self.last_user_line = "XXtestXX"
|
||||
self.last_lines = []
|
||||
|
||||
|
||||
def toggle(self):
|
||||
if self.visible:
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
|
||||
|
||||
def show(self):
|
||||
self.visible = True
|
||||
self.target_alpha = 1
|
||||
self.target_y = 1
|
||||
self.ui.menu_bar.visible = False
|
||||
self.ui.pulldown.visible = False
|
||||
|
||||
|
||||
def hide(self):
|
||||
self.target_alpha = 0
|
||||
self.target_y = 2
|
||||
if self.ui.app.can_edit:
|
||||
self.ui.menu_bar.visible = True
|
||||
|
||||
|
||||
def update_loc(self):
|
||||
# TODO: this lerp is super awful, simpler way based on dt?
|
||||
# TODO: use self.show_anim_time instead of this garbage!
|
||||
speed = 0.25
|
||||
|
||||
|
||||
if self.y > self.target_y:
|
||||
self.y -= speed
|
||||
elif self.y < self.target_y:
|
||||
self.y += speed
|
||||
if abs(self.y - self.target_y) < speed:
|
||||
self.y = self.target_y
|
||||
|
||||
|
||||
if self.alpha > self.target_alpha:
|
||||
self.alpha -= speed / 2
|
||||
elif self.alpha < self.target_alpha:
|
||||
|
|
@ -356,35 +394,44 @@ class ConsoleUI(UIElement):
|
|||
self.alpha = self.target_alpha
|
||||
if self.alpha == 0:
|
||||
self.visible = False
|
||||
|
||||
|
||||
self.renderable.y = self.y
|
||||
self.renderable.alpha = self.alpha
|
||||
|
||||
|
||||
def clear(self):
|
||||
self.art.clear_frame_layer(0, 0, self.bg_color_index)
|
||||
# line -1 is always a line of ____________
|
||||
for x in range(self.width):
|
||||
self.art.set_tile_at(0, 0, x, -1, self.bottom_line_char_index, self.text_color, None, UV_FLIPY)
|
||||
|
||||
self.art.set_tile_at(
|
||||
0,
|
||||
0,
|
||||
x,
|
||||
-1,
|
||||
self.bottom_line_char_index,
|
||||
self.text_color,
|
||||
None,
|
||||
UV_FLIPY,
|
||||
)
|
||||
|
||||
def update_user_line(self):
|
||||
"draw current user input on second to last line, with >_ prompt"
|
||||
# clear entire user line first
|
||||
self.art.write_string(0, 0, 0, -2, ' ' * self.width, self.text_color)
|
||||
self.art.write_string(0, 0, 0, -2, '%s ' % self.prompt, self.text_color)
|
||||
self.art.write_string(0, 0, 0, -2, " " * self.width, self.text_color)
|
||||
self.art.write_string(0, 0, 0, -2, "%s " % self.prompt, self.text_color)
|
||||
# if first item of line is a valid command, change its color
|
||||
items = self.current_line.split()
|
||||
if len(items) > 0 and items[0] in commands:
|
||||
self.art.write_string(0, 0, 2, -2, items[0], self.highlight_color)
|
||||
offset = 2 + len(items[0]) + 1
|
||||
args = ' '.join(items[1:])
|
||||
args = " ".join(items[1:])
|
||||
self.art.write_string(0, 0, offset, -2, args, self.text_color)
|
||||
else:
|
||||
self.art.write_string(0, 0, 2, -2, self.current_line, self.text_color)
|
||||
# draw underscore for caret at end of input string
|
||||
x = len(self.prompt) + len(self.current_line) + 1
|
||||
i = self.ui.charset.get_char_index('_')
|
||||
i = self.ui.charset.get_char_index("_")
|
||||
self.art.set_char_index_at(0, 0, x, -2, i)
|
||||
|
||||
|
||||
def update_log_lines(self):
|
||||
"update art from log lines"
|
||||
log_index = -1
|
||||
|
|
@ -395,10 +442,10 @@ class ConsoleUI(UIElement):
|
|||
break
|
||||
# trim line to width of console
|
||||
if len(line) >= self.max_line_length:
|
||||
line = line[:self.max_line_length]
|
||||
line = line[: self.max_line_length]
|
||||
self.art.write_string(0, 0, 1, y, line, self.text_color)
|
||||
log_index -= 1
|
||||
|
||||
|
||||
def update(self):
|
||||
"update our Art with the current console log lines + user input"
|
||||
self.update_loc()
|
||||
|
|
@ -423,44 +470,47 @@ class ConsoleUI(UIElement):
|
|||
# update user line independently of log, it changes at a different rate
|
||||
if user_input_changed:
|
||||
self.update_user_line()
|
||||
|
||||
|
||||
def visit_command_history(self, index):
|
||||
if len(self.command_history) == 0:
|
||||
return
|
||||
self.history_index = index
|
||||
self.history_index %= len(self.command_history)
|
||||
self.current_line = self.command_history[self.history_index].strip()
|
||||
|
||||
|
||||
def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
"handles a key from Application.input"
|
||||
keystr = sdl2.SDL_GetKeyName(key).decode()
|
||||
# TODO: get console bound key from InputLord, detect that instead of
|
||||
# hard-coded backquote
|
||||
if keystr == '`':
|
||||
if keystr == "`":
|
||||
self.toggle()
|
||||
return
|
||||
elif keystr == 'Return':
|
||||
line = '%s %s' % (self.prompt, self.current_line)
|
||||
elif keystr == "Return":
|
||||
line = "%s %s" % (self.prompt, self.current_line)
|
||||
self.ui.app.log(line)
|
||||
# if command is same as last, don't repeat it
|
||||
if len(self.command_history) == 0 or (len(self.command_history) > 0 and self.current_line != self.command_history[-1]):
|
||||
if len(self.command_history) == 0 or (
|
||||
len(self.command_history) > 0
|
||||
and self.current_line != self.command_history[-1]
|
||||
):
|
||||
# don't add blank lines to history
|
||||
if self.current_line.strip():
|
||||
self.command_history.append(self.current_line)
|
||||
self.history_file.write(self.current_line + '\n')
|
||||
self.history_file.write(self.current_line + "\n")
|
||||
self.parse(self.current_line)
|
||||
self.current_line = ''
|
||||
self.current_line = ""
|
||||
self.history_index = 0
|
||||
elif keystr == 'Tab':
|
||||
elif keystr == "Tab":
|
||||
# TODO: autocomplete (commands, filenames)
|
||||
pass
|
||||
elif keystr == 'Up':
|
||||
elif keystr == "Up":
|
||||
# page back through command history
|
||||
self.visit_command_history(self.history_index - 1)
|
||||
elif keystr == 'Down':
|
||||
elif keystr == "Down":
|
||||
# page forward through command history
|
||||
self.visit_command_history(self.history_index + 1)
|
||||
elif keystr == 'Backspace' and len(self.current_line) > 0:
|
||||
elif keystr == "Backspace" and len(self.current_line) > 0:
|
||||
# alt-backspace: delete to last delimiter, eg periods
|
||||
if alt_pressed:
|
||||
# "index to delete to"
|
||||
|
|
@ -472,7 +522,7 @@ class ConsoleUI(UIElement):
|
|||
if delete_index > -1:
|
||||
self.current_line = self.current_line[:delete_index]
|
||||
else:
|
||||
self.current_line = ''
|
||||
self.current_line = ""
|
||||
# user is bailing on whatever they were typing,
|
||||
# reset position in cmd history
|
||||
self.history_index = 0
|
||||
|
|
@ -481,18 +531,18 @@ class ConsoleUI(UIElement):
|
|||
if len(self.current_line) == 0:
|
||||
# same as above: reset position in cmd history
|
||||
self.history_index = 0
|
||||
elif keystr == 'Space':
|
||||
keystr = ' '
|
||||
elif keystr == "Space":
|
||||
keystr = " "
|
||||
# ignore any other non-character keys
|
||||
if len(keystr) > 1:
|
||||
return
|
||||
if keystr.isalpha() and not shift_pressed:
|
||||
keystr = keystr.lower()
|
||||
elif not keystr.isalpha() and shift_pressed:
|
||||
keystr = SHIFT_MAP.get(keystr, '')
|
||||
keystr = SHIFT_MAP.get(keystr, "")
|
||||
if len(self.current_line) < self.max_line_length:
|
||||
self.current_line += keystr
|
||||
|
||||
|
||||
def parse(self, line):
|
||||
# is line in a list of know commands? if so, handle it.
|
||||
items = line.split()
|
||||
|
|
@ -511,28 +561,32 @@ class ConsoleUI(UIElement):
|
|||
camera = app.camera
|
||||
art = ui.active_art
|
||||
player = app.gw.player
|
||||
sel = None if len(app.gw.selected_objects) == 0 else app.gw.selected_objects[0]
|
||||
sel = (
|
||||
None
|
||||
if len(app.gw.selected_objects) == 0
|
||||
else app.gw.selected_objects[0]
|
||||
)
|
||||
world = app.gw
|
||||
hud = app.gw.hud
|
||||
# special handling of assignment statements, eg x = 3:
|
||||
# detect strings that pattern-match, send them to exec(),
|
||||
# send all other strings to eval()
|
||||
eq_index = line.find('=')
|
||||
is_assignment = eq_index != -1 and line[eq_index+1] != '='
|
||||
eq_index = line.find("=")
|
||||
is_assignment = eq_index != -1 and line[eq_index + 1] != "="
|
||||
if is_assignment:
|
||||
exec(line)
|
||||
else:
|
||||
output = str(eval(line))
|
||||
except Exception as e:
|
||||
# try to output useful error text
|
||||
output = '%s: %s' % (e.__class__.__name__, str(e))
|
||||
output = "%s: %s" % (e.__class__.__name__, str(e))
|
||||
# commands CAN return None, so only log if there's something
|
||||
if output and output != 'None':
|
||||
if output and output != "None":
|
||||
self.ui.app.log(output)
|
||||
|
||||
|
||||
def destroy(self):
|
||||
self.history_file.close()
|
||||
|
||||
|
||||
# delimiters - alt-backspace deletes to most recent one of these
|
||||
delimiters = [' ', '.', ')', ']', ',', '_']
|
||||
delimiters = [" ", ".", ")", "]", ",", "_"]
|
||||
|
|
|
|||
211
ui_dialog.py
211
ui_dialog.py
|
|
@ -1,49 +1,57 @@
|
|||
import platform
|
||||
import sdl2
|
||||
from collections import namedtuple
|
||||
|
||||
from ui_element import UIElement
|
||||
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
|
||||
from ui_colors import UIColors
|
||||
import sdl2
|
||||
|
||||
from key_shifts import SHIFT_MAP
|
||||
from ui_button import TEXT_CENTER, UIButton
|
||||
from ui_colors import UIColors
|
||||
from ui_element import UIElement
|
||||
|
||||
|
||||
Field = namedtuple('Field', ['label', # text label for field
|
||||
'type', # supported: str int float bool
|
||||
'width', # width in tiles of the field
|
||||
'oneline']) # label and field drawn on same line
|
||||
Field = namedtuple(
|
||||
"Field",
|
||||
[
|
||||
"label", # text label for field
|
||||
"type", # supported: str int float bool
|
||||
"width", # width in tiles of the field
|
||||
"oneline",
|
||||
],
|
||||
) # label and field drawn on same line
|
||||
|
||||
|
||||
# "null" field type that tells UI drawing to skip it
|
||||
class SkipFieldType: pass
|
||||
class SkipFieldType:
|
||||
pass
|
||||
|
||||
|
||||
class ConfirmButton(UIButton):
|
||||
caption = 'Confirm'
|
||||
caption = "Confirm"
|
||||
caption_justify = TEXT_CENTER
|
||||
width = len(caption) + 2
|
||||
dimmed_fg_color = UIColors.lightgrey
|
||||
dimmed_bg_color = UIColors.white
|
||||
|
||||
|
||||
class CancelButton(ConfirmButton):
|
||||
caption = 'Cancel'
|
||||
caption = "Cancel"
|
||||
width = len(caption) + 2
|
||||
|
||||
|
||||
class OtherButton(ConfirmButton):
|
||||
"button for 3rd option in some dialogs, eg Don't Save"
|
||||
caption = 'Other'
|
||||
|
||||
caption = "Other"
|
||||
width = len(caption) + 2
|
||||
visible = False
|
||||
|
||||
|
||||
class UIDialog(UIElement):
|
||||
|
||||
tile_width, tile_height = 40, 8
|
||||
# extra lines added to height beyond contents length
|
||||
extra_lines = 0
|
||||
fg_color = UIColors.black
|
||||
bg_color = UIColors.white
|
||||
title = 'Test Dialog Box'
|
||||
title = "Test Dialog Box"
|
||||
# string message not tied to a specific field
|
||||
message = None
|
||||
other_button_visible = False
|
||||
|
|
@ -72,23 +80,25 @@ class UIDialog(UIElement):
|
|||
radio_true_char_index = 127
|
||||
radio_false_char_index = 126
|
||||
# field text set for bool fields with True value
|
||||
true_field_text = 'x'
|
||||
true_field_text = "x"
|
||||
# if True, field labels will redraw with fields after handling input
|
||||
always_redraw_labels = False
|
||||
|
||||
|
||||
def __init__(self, ui, options):
|
||||
self.ui = ui
|
||||
# apply options, eg passed in from UI.open_dialog
|
||||
for k,v in options.items():
|
||||
for k, v in options.items():
|
||||
setattr(self, k, v)
|
||||
self.confirm_button = ConfirmButton(self)
|
||||
self.other_button = OtherButton(self)
|
||||
self.cancel_button = CancelButton(self)
|
||||
|
||||
# handle caption overrides
|
||||
def caption_override(button, alt_caption):
|
||||
if alt_caption and button.caption != alt_caption:
|
||||
button.caption = alt_caption
|
||||
button.width = len(alt_caption) + 2
|
||||
|
||||
caption_override(self.confirm_button, self.confirm_caption)
|
||||
caption_override(self.other_button, self.other_caption)
|
||||
caption_override(self.cancel_button, self.cancel_caption)
|
||||
|
|
@ -98,18 +108,18 @@ class UIDialog(UIElement):
|
|||
self.buttons = [self.confirm_button, self.other_button, self.cancel_button]
|
||||
# populate fields with text
|
||||
self.field_texts = []
|
||||
for i,field in enumerate(self.fields):
|
||||
for i, field in enumerate(self.fields):
|
||||
self.field_texts.append(self.get_initial_field_text(i))
|
||||
# field cursor starts on
|
||||
self.active_field = 0
|
||||
UIElement.__init__(self, ui)
|
||||
if self.ui.menu_bar and self.ui.menu_bar.active_menu_name:
|
||||
self.ui.menu_bar.close_active_menu()
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
"subclasses specify a given field's initial text here"
|
||||
return ''
|
||||
|
||||
return ""
|
||||
|
||||
def get_height(self, msg_lines):
|
||||
"determine size based on contents (subclasses can use custom logic)"
|
||||
# base height = 4, titlebar + padding + buttons + padding
|
||||
|
|
@ -125,7 +135,7 @@ class UIDialog(UIElement):
|
|||
h += self.y_spacing + 2
|
||||
h += self.extra_lines
|
||||
return h
|
||||
|
||||
|
||||
def reset_art(self, resize=True, clear_buttons=True):
|
||||
# get_message splits into >1 line if too long
|
||||
msg_lines = self.get_message() if self.message else []
|
||||
|
|
@ -138,20 +148,19 @@ class UIDialog(UIElement):
|
|||
self.y = (self.tile_height * qh) / 2
|
||||
# draw window
|
||||
self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
|
||||
s = ' ' + self.title.ljust(self.tile_width - 1)
|
||||
s = " " + self.title.ljust(self.tile_width - 1)
|
||||
# invert titlebar (if kb focus)
|
||||
fg = self.titlebar_fg_color
|
||||
bg = self.titlebar_bg_color
|
||||
if not self is self.ui.keyboard_focus_element and \
|
||||
self is self.ui.active_dialog:
|
||||
if self is not self.ui.keyboard_focus_element and self is self.ui.active_dialog:
|
||||
fg = self.fg_color
|
||||
bg = self.bg_color
|
||||
self.art.write_string(0, 0, 0, 0, s, fg, bg)
|
||||
# message
|
||||
if self.message:
|
||||
y = 2
|
||||
for i,line in enumerate(msg_lines):
|
||||
self.art.write_string(0, 0, 2, y+i, line)
|
||||
for i, line in enumerate(msg_lines):
|
||||
self.art.write_string(0, 0, 2, y + i, line)
|
||||
# field caption(s)
|
||||
self.draw_fields()
|
||||
# position buttons
|
||||
|
|
@ -167,7 +176,7 @@ class UIDialog(UIElement):
|
|||
# create field buttons so you can click em
|
||||
if clear_buttons:
|
||||
self.buttons = [self.confirm_button, self.other_button, self.cancel_button]
|
||||
for i,field in enumerate(self.fields):
|
||||
for i, field in enumerate(self.fields):
|
||||
# None-type field = just a label
|
||||
if field.type is None:
|
||||
continue
|
||||
|
|
@ -175,27 +184,30 @@ class UIDialog(UIElement):
|
|||
field_button.field_number = i
|
||||
# field settings mean button can be in a variety of places
|
||||
field_button.width = 1 if field.type is bool else field.width
|
||||
field_button.x = 2 if not field.oneline or field.type is bool else len(field.label) + 1
|
||||
field_button.x = (
|
||||
2 if not field.oneline or field.type is bool else len(field.label) + 1
|
||||
)
|
||||
field_button.y = self.get_field_y(i)
|
||||
if not field.oneline:
|
||||
field_button.y += 1
|
||||
self.buttons.append(field_button)
|
||||
# draw buttons
|
||||
UIElement.reset_art(self)
|
||||
|
||||
|
||||
def update_drag(self, mouse_dx, mouse_dy):
|
||||
win_w, win_h = self.ui.app.window_width, self.ui.app.window_height
|
||||
self.x += (mouse_dx / win_w) * 2
|
||||
self.y -= (mouse_dy / win_h) * 2
|
||||
self.renderable.x, self.renderable.y = self.x, self.y
|
||||
|
||||
|
||||
def hovered(self):
|
||||
# mouse hover on focus
|
||||
if (self.ui.app.mouse_dx or self.ui.app.mouse_dy) and \
|
||||
not self is self.ui.keyboard_focus_element:
|
||||
if (
|
||||
self.ui.app.mouse_dx or self.ui.app.mouse_dy
|
||||
) and self is not self.ui.keyboard_focus_element:
|
||||
self.ui.keyboard_focus_element = self
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def update(self):
|
||||
# redraw fields every update for cursor blink
|
||||
# (seems a waste, no real perf impact tho)
|
||||
|
|
@ -206,25 +218,25 @@ class UIDialog(UIElement):
|
|||
bottom_y = self.tile_height - 1
|
||||
# first clear any previous warnings
|
||||
self.art.clear_line(0, 0, bottom_y)
|
||||
self.confirm_button.set_state('normal')
|
||||
self.confirm_button.set_state("normal")
|
||||
# some dialogs use reason for warning + valid input
|
||||
if reason:
|
||||
fg = self.ui.error_color_index
|
||||
self.art.write_string(0, 0, 1, bottom_y, reason, fg)
|
||||
if not valid:
|
||||
self.confirm_button.set_state('dimmed')
|
||||
self.confirm_button.set_state("dimmed")
|
||||
UIElement.update(self)
|
||||
|
||||
|
||||
def get_message(self):
|
||||
# if a triple quoted string, split line breaks
|
||||
msg = self.message.rstrip().split('\n')
|
||||
msg = self.message.rstrip().split("\n")
|
||||
msg_lines = []
|
||||
for line in msg:
|
||||
if line != '':
|
||||
if line != "":
|
||||
msg_lines.append(line)
|
||||
# TODO: split over multiple lines if too long
|
||||
return msg_lines
|
||||
|
||||
|
||||
def get_field_colors(self, index):
|
||||
"return FG and BG colors for field with given index"
|
||||
fg, bg = self.inactive_field_fg_color, self.inactive_field_bg_color
|
||||
|
|
@ -232,16 +244,16 @@ class UIDialog(UIElement):
|
|||
if self is self.ui.keyboard_focus_element and index == self.active_field:
|
||||
fg, bg = self.active_field_fg_color, self.active_field_bg_color
|
||||
return fg, bg
|
||||
|
||||
|
||||
def get_field_label(self, field_index):
|
||||
"Subclasses can override to do custom label logic eg string formatting"
|
||||
return self.fields[field_index].label
|
||||
|
||||
|
||||
def draw_fields(self, with_labels=True):
|
||||
y = 2
|
||||
if self.message:
|
||||
y += len(self.get_message()) + 1
|
||||
for i,field in enumerate(self.fields):
|
||||
for i, field in enumerate(self.fields):
|
||||
if field.type is SkipFieldType:
|
||||
continue
|
||||
x = 2
|
||||
|
|
@ -256,7 +268,11 @@ class UIDialog(UIElement):
|
|||
# true/false ~ field text is 'x'
|
||||
field_true = self.field_texts[i] == self.true_field_text
|
||||
if is_radio:
|
||||
char = self.radio_true_char_index if field_true else self.radio_false_char_index
|
||||
char = (
|
||||
self.radio_true_char_index
|
||||
if field_true
|
||||
else self.radio_false_char_index
|
||||
)
|
||||
else:
|
||||
char = self.checkbox_char_index if field_true else 0
|
||||
fg, bg = self.get_field_colors(i)
|
||||
|
|
@ -275,19 +291,19 @@ class UIDialog(UIElement):
|
|||
else:
|
||||
y += 1
|
||||
# draw field contents
|
||||
if not field.type in [bool, None]:
|
||||
if field.type not in [bool, None]:
|
||||
fg, bg = self.get_field_colors(i)
|
||||
text = self.field_texts[i]
|
||||
# caret for active field (if kb focus)
|
||||
if i == self.active_field and self is self.ui.keyboard_focus_element:
|
||||
blink_on = int(self.ui.app.get_elapsed_time() / 250) % 2
|
||||
if blink_on:
|
||||
text += '_'
|
||||
text += "_"
|
||||
# pad with spaces to full width of field
|
||||
text = text.ljust(field.width)
|
||||
self.art.write_string(0, 0, x, y, text, fg, bg)
|
||||
y += self.y_spacing + 1
|
||||
|
||||
|
||||
def get_field_y(self, field_index):
|
||||
"returns a Y value for where the given field (caption) should start"
|
||||
y = 2
|
||||
|
|
@ -300,7 +316,7 @@ class UIDialog(UIElement):
|
|||
else:
|
||||
y += self.y_spacing + 2
|
||||
return y
|
||||
|
||||
|
||||
def get_toggled_bool_field(self, field_index):
|
||||
field_text = self.field_texts[field_index]
|
||||
on = field_text == self.true_field_text
|
||||
|
|
@ -312,111 +328,121 @@ class UIDialog(UIElement):
|
|||
if not on:
|
||||
for i in group:
|
||||
if i != field_index:
|
||||
self.field_texts[i] = ' '
|
||||
self.field_texts[i] = " "
|
||||
break
|
||||
# toggle checkbox
|
||||
if not radio_button:
|
||||
return ' ' if on else self.true_field_text
|
||||
return " " if on else self.true_field_text
|
||||
# only toggle radio button on; selecting others toggles it off
|
||||
elif on:
|
||||
return field_text
|
||||
else:
|
||||
return self.true_field_text
|
||||
|
||||
|
||||
def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
keystr = sdl2.SDL_GetKeyName(key).decode()
|
||||
field = None
|
||||
field_text = ''
|
||||
field_text = ""
|
||||
if self.active_field < len(self.fields):
|
||||
field = self.fields[self.active_field]
|
||||
field_text = self.field_texts[self.active_field]
|
||||
# special case: shortcut 'D' for 3rd button if no field input
|
||||
if len(self.fields) == 0 and keystr.lower() == 'd':
|
||||
if len(self.fields) == 0 and keystr.lower() == "d":
|
||||
self.other_pressed()
|
||||
return
|
||||
if keystr == '`' and not shift_pressed:
|
||||
if keystr == "`" and not shift_pressed:
|
||||
self.ui.console.toggle()
|
||||
return
|
||||
# if list panel is up don't let user tab away
|
||||
lp = self.ui.edit_list_panel
|
||||
# only allow tab to focus shift IF list panel accepts it
|
||||
if keystr == 'Tab' and lp.is_visible() and \
|
||||
lp.list_operation in lp.list_operations_allow_kb_focus:
|
||||
if (
|
||||
keystr == "Tab"
|
||||
and lp.is_visible()
|
||||
and lp.list_operation in lp.list_operations_allow_kb_focus
|
||||
):
|
||||
self.ui.keyboard_focus_element = self.ui.edit_list_panel
|
||||
return
|
||||
elif keystr == 'Return':
|
||||
elif keystr == "Return":
|
||||
self.confirm_pressed()
|
||||
elif keystr == 'Escape':
|
||||
elif keystr == "Escape":
|
||||
self.cancel_pressed()
|
||||
# cycle through fields with up/down
|
||||
elif keystr == 'Up' or (keystr == 'Tab' and shift_pressed):
|
||||
elif keystr == "Up" or (keystr == "Tab" and shift_pressed):
|
||||
if len(self.fields) > 1:
|
||||
self.active_field -= 1
|
||||
self.active_field %= len(self.fields)
|
||||
# skip over None-type fields aka dead labels
|
||||
while self.fields[self.active_field].type is None or self.fields[self.active_field].type is SkipFieldType:
|
||||
while (
|
||||
self.fields[self.active_field].type is None
|
||||
or self.fields[self.active_field].type is SkipFieldType
|
||||
):
|
||||
self.active_field -= 1
|
||||
self.active_field %= len(self.fields)
|
||||
return
|
||||
elif keystr == 'Down' or keystr == 'Tab':
|
||||
elif keystr == "Down" or keystr == "Tab":
|
||||
if len(self.fields) > 1:
|
||||
self.active_field += 1
|
||||
self.active_field %= len(self.fields)
|
||||
while self.fields[self.active_field].type is None or self.fields[self.active_field].type is SkipFieldType:
|
||||
while (
|
||||
self.fields[self.active_field].type is None
|
||||
or self.fields[self.active_field].type is SkipFieldType
|
||||
):
|
||||
self.active_field += 1
|
||||
self.active_field %= len(self.fields)
|
||||
return
|
||||
elif keystr == 'Backspace':
|
||||
if len(field_text) == 0:
|
||||
pass
|
||||
# don't let user clear a bool value
|
||||
# TODO: allow for checkboxes but not radio buttons
|
||||
elif field and field.type is bool:
|
||||
elif keystr == "Backspace":
|
||||
if len(field_text) == 0 or field and field.type is bool:
|
||||
pass
|
||||
elif alt_pressed:
|
||||
# for file dialogs, delete back to last slash
|
||||
last_slash = field_text[:-1].rfind('/')
|
||||
last_slash = field_text[:-1].rfind("/")
|
||||
# on windows, recognize backslash as well
|
||||
if platform.system() == 'Windows':
|
||||
last_backslash = field_text[:-1].rfind('\\')
|
||||
if platform.system() == "Windows":
|
||||
last_backslash = field_text[:-1].rfind("\\")
|
||||
if last_backslash != -1 and last_slash != -1:
|
||||
last_slash = min(last_backslash, last_slash)
|
||||
if last_slash == -1:
|
||||
field_text = ''
|
||||
field_text = ""
|
||||
else:
|
||||
field_text = field_text[:last_slash+1]
|
||||
field_text = field_text[: last_slash + 1]
|
||||
else:
|
||||
field_text = field_text[:-1]
|
||||
elif keystr == 'Space':
|
||||
elif keystr == "Space":
|
||||
# if field.type is bool, toggle value
|
||||
if field.type is bool:
|
||||
field_text = self.get_toggled_bool_field(self.active_field)
|
||||
else:
|
||||
field_text += ' '
|
||||
field_text += " "
|
||||
elif len(keystr) > 1:
|
||||
return
|
||||
# alphanumeric text input
|
||||
elif field and not field.type is bool:
|
||||
elif field and field.type is not bool:
|
||||
if field.type is str:
|
||||
if not shift_pressed:
|
||||
keystr = keystr.lower()
|
||||
if not keystr.isalpha() and shift_pressed:
|
||||
keystr = SHIFT_MAP.get(keystr, '')
|
||||
elif field.type is int and not keystr.isdigit() and keystr != '-':
|
||||
return
|
||||
# this doesn't guard against things like 0.00.001
|
||||
elif field.type is float and not keystr.isdigit() and keystr != '.' and keystr != '-':
|
||||
keystr = SHIFT_MAP.get(keystr, "")
|
||||
elif (
|
||||
field.type is int
|
||||
and not keystr.isdigit()
|
||||
and keystr != "-"
|
||||
or field.type is float
|
||||
and not keystr.isdigit()
|
||||
and keystr != "."
|
||||
and keystr != "-"
|
||||
):
|
||||
return
|
||||
field_text += keystr
|
||||
# apply new field text and redraw
|
||||
if field and (len(field_text) < field.width or field.type is bool):
|
||||
self.field_texts[self.active_field] = field_text
|
||||
self.draw_fields(self.always_redraw_labels)
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
"subclasses that want to filter input put logic here"
|
||||
return True, None
|
||||
|
||||
|
||||
def dismiss(self):
|
||||
# let UI forget about us
|
||||
self.ui.active_dialog = None
|
||||
|
|
@ -424,32 +450,33 @@ class UIDialog(UIElement):
|
|||
self.ui.keyboard_focus_element = None
|
||||
self.ui.refocus_keyboard()
|
||||
self.ui.elements.remove(self)
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
# subclasses do more here :]
|
||||
self.dismiss()
|
||||
|
||||
|
||||
def cancel_pressed(self):
|
||||
self.dismiss()
|
||||
|
||||
|
||||
def other_pressed(self):
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class DialogFieldButton(UIButton):
|
||||
|
||||
"invisible button that provides clickability for input fields"
|
||||
|
||||
caption = ''
|
||||
|
||||
caption = ""
|
||||
# re-set by dialog constructor
|
||||
field_number = 0
|
||||
never_draw = True
|
||||
|
||||
|
||||
def click(self):
|
||||
UIButton.click(self)
|
||||
self.element.active_field = self.field_number
|
||||
# toggle if a bool field
|
||||
if self.element.fields[self.field_number].type is bool:
|
||||
self.element.field_texts[self.field_number] = self.element.get_toggled_bool_field(self.field_number)
|
||||
self.element.field_texts[self.field_number] = (
|
||||
self.element.get_toggled_bool_field(self.field_number)
|
||||
)
|
||||
# redraw fields & labels
|
||||
self.element.draw_fields(self.element.always_redraw_labels)
|
||||
|
|
|
|||
277
ui_edit_panel.py
277
ui_edit_panel.py
|
|
@ -1,17 +1,28 @@
|
|||
import os
|
||||
|
||||
from ui_element import UIElement
|
||||
from game_world import STATE_FILE_EXTENSION, TOP_GAME_DIR
|
||||
from ui_button import UIButton
|
||||
from ui_game_dialog import LoadGameStateDialog, SaveGameStateDialog
|
||||
from ui_chooser_dialog import ScrollArrowButton
|
||||
from ui_colors import UIColors
|
||||
|
||||
from game_world import TOP_GAME_DIR, STATE_FILE_EXTENSION
|
||||
from ui_list_operations import LO_NONE, LO_SELECT_OBJECTS, LO_SET_SPAWN_CLASS, LO_LOAD_STATE, LO_SET_ROOM, LO_SET_ROOM_OBJECTS, LO_SET_OBJECT_ROOMS, LO_OPEN_GAME_DIR, LO_SET_ROOM_EDGE_WARP, LO_SET_ROOM_EDGE_OBJ, LO_SET_ROOM_CAMERA
|
||||
from ui_element import UIElement
|
||||
from ui_list_operations import (
|
||||
LO_LOAD_STATE,
|
||||
LO_NONE,
|
||||
LO_OPEN_GAME_DIR,
|
||||
LO_SELECT_OBJECTS,
|
||||
LO_SET_OBJECT_ROOMS,
|
||||
LO_SET_ROOM,
|
||||
LO_SET_ROOM_CAMERA,
|
||||
LO_SET_ROOM_EDGE_OBJ,
|
||||
LO_SET_ROOM_EDGE_WARP,
|
||||
LO_SET_ROOM_OBJECTS,
|
||||
LO_SET_SPAWN_CLASS,
|
||||
)
|
||||
|
||||
|
||||
class GamePanel(UIElement):
|
||||
"base class of game edit UI panels"
|
||||
|
||||
tile_y = 5
|
||||
game_mode_visible = True
|
||||
fg_color = UIColors.black
|
||||
|
|
@ -22,7 +33,7 @@ class GamePanel(UIElement):
|
|||
support_keyboard_navigation = True
|
||||
support_scrolling = True
|
||||
keyboard_nav_offset = -2
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.world = self.ui.app.gw
|
||||
|
|
@ -30,60 +41,76 @@ class GamePanel(UIElement):
|
|||
self.buttons = []
|
||||
self.create_buttons()
|
||||
self.keyboard_nav_index = 0
|
||||
|
||||
def create_buttons(self): pass
|
||||
|
||||
def create_buttons(self):
|
||||
pass
|
||||
|
||||
# label and main item draw functions - overridden in subclasses
|
||||
def get_label(self): pass
|
||||
def refresh_items(self): pass
|
||||
|
||||
def get_label(self):
|
||||
pass
|
||||
|
||||
def refresh_items(self):
|
||||
pass
|
||||
|
||||
# reset all buttons to default state
|
||||
def clear_buttons(self, button_list=None):
|
||||
buttons = button_list or self.buttons
|
||||
for button in buttons:
|
||||
self.reset_button(button)
|
||||
|
||||
|
||||
def reset_button(self, button):
|
||||
button.normal_fg_color = UIButton.normal_fg_color
|
||||
button.normal_bg_color = UIButton.normal_bg_color
|
||||
button.hovered_fg_color = UIButton.hovered_fg_color
|
||||
button.hovered_bg_color = UIButton.hovered_bg_color
|
||||
button.can_hover = True
|
||||
|
||||
|
||||
def highlight_button(self, button):
|
||||
button.normal_fg_color = UIButton.clicked_fg_color
|
||||
button.normal_bg_color = UIButton.clicked_bg_color
|
||||
button.hovered_fg_color = UIButton.clicked_fg_color
|
||||
button.hovered_bg_color = UIButton.clicked_bg_color
|
||||
button.can_hover = True
|
||||
|
||||
|
||||
def draw_titlebar(self):
|
||||
# only shade titlebar if panel has keyboard focus
|
||||
fg = self.titlebar_fg if self is self.ui.keyboard_focus_element else self.fg_color
|
||||
bg = self.titlebar_bg if self is self.ui.keyboard_focus_element else self.bg_color
|
||||
fg = (
|
||||
self.titlebar_fg
|
||||
if self is self.ui.keyboard_focus_element
|
||||
else self.fg_color
|
||||
)
|
||||
bg = (
|
||||
self.titlebar_bg
|
||||
if self is self.ui.keyboard_focus_element
|
||||
else self.bg_color
|
||||
)
|
||||
self.art.clear_line(0, 0, 0, fg, bg)
|
||||
label = self.get_label()
|
||||
if len(label) > self.tile_width:
|
||||
label = label[:self.tile_width]
|
||||
label = label[: self.tile_width]
|
||||
if self.text_left:
|
||||
self.art.write_string(0, 0, 0, 0, label)
|
||||
else:
|
||||
self.art.write_string(0, 0, -1, 0, label, None, None, True)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
|
||||
self.draw_titlebar()
|
||||
self.refresh_items()
|
||||
UIElement.reset_art(self)
|
||||
|
||||
|
||||
def clicked(self, mouse_button):
|
||||
# always handle input, even if we didn't hit a button
|
||||
UIElement.clicked(self, mouse_button)
|
||||
return True
|
||||
|
||||
|
||||
def hovered(self):
|
||||
# mouse hover on focus
|
||||
if self.ui.app.mouse_dx or self.ui.app.mouse_dy and \
|
||||
not self is self.ui.keyboard_focus_element:
|
||||
if (
|
||||
self.ui.app.mouse_dx
|
||||
or self.ui.app.mouse_dy
|
||||
and self is not self.ui.keyboard_focus_element
|
||||
):
|
||||
self.ui.keyboard_focus_element = self
|
||||
if self.ui.active_dialog:
|
||||
self.ui.active_dialog.reset_art()
|
||||
|
|
@ -93,16 +120,20 @@ class ListButton(UIButton):
|
|||
width = 28
|
||||
clear_before_caption_draw = True
|
||||
|
||||
|
||||
class ListScrollArrowButton(ScrollArrowButton):
|
||||
x = ListButton.width
|
||||
normal_bg_color = UIButton.normal_bg_color
|
||||
|
||||
|
||||
class ListScrollUpArrowButton(ListScrollArrowButton):
|
||||
y = 1
|
||||
|
||||
|
||||
class ListScrollDownArrowButton(ListScrollArrowButton):
|
||||
up = False
|
||||
|
||||
|
||||
class EditListPanel(GamePanel):
|
||||
tile_width = ListButton.width + 1
|
||||
tile_y = 5
|
||||
|
|
@ -110,35 +141,38 @@ class EditListPanel(GamePanel):
|
|||
# height will change based on how many items in list
|
||||
tile_height = 30
|
||||
snap_left = True
|
||||
spawn_msg = 'Click anywhere in the world view to spawn a %s'
|
||||
spawn_msg = "Click anywhere in the world view to spawn a %s"
|
||||
# transient state
|
||||
titlebar = 'List titlebar'
|
||||
titlebar = "List titlebar"
|
||||
items = []
|
||||
# text helping user know how to bail
|
||||
cancel_tip = 'ESC cancels'
|
||||
cancel_tip = "ESC cancels"
|
||||
list_operation_labels = {
|
||||
LO_NONE: 'Stuff:',
|
||||
LO_SELECT_OBJECTS: 'Select objects:',
|
||||
LO_SET_SPAWN_CLASS: 'Class to spawn:',
|
||||
LO_LOAD_STATE: 'State to load:',
|
||||
LO_SET_ROOM: 'Change room:',
|
||||
LO_NONE: "Stuff:",
|
||||
LO_SELECT_OBJECTS: "Select objects:",
|
||||
LO_SET_SPAWN_CLASS: "Class to spawn:",
|
||||
LO_LOAD_STATE: "State to load:",
|
||||
LO_SET_ROOM: "Change room:",
|
||||
LO_SET_ROOM_OBJECTS: "Set objects for %s:",
|
||||
LO_SET_OBJECT_ROOMS: "Set rooms for %s:",
|
||||
LO_OPEN_GAME_DIR: 'Open game:',
|
||||
LO_SET_ROOM_EDGE_WARP: 'Set edge warp room/object:',
|
||||
LO_SET_ROOM_EDGE_OBJ: 'Set edge bounds object:',
|
||||
LO_SET_ROOM_CAMERA: 'Set room camera marker:'
|
||||
LO_OPEN_GAME_DIR: "Open game:",
|
||||
LO_SET_ROOM_EDGE_WARP: "Set edge warp room/object:",
|
||||
LO_SET_ROOM_EDGE_OBJ: "Set edge bounds object:",
|
||||
LO_SET_ROOM_CAMERA: "Set room camera marker:",
|
||||
}
|
||||
list_operations_allow_kb_focus = [
|
||||
LO_SET_ROOM_EDGE_WARP,
|
||||
LO_SET_ROOM_EDGE_OBJ,
|
||||
LO_SET_ROOM_CAMERA
|
||||
LO_SET_ROOM_CAMERA,
|
||||
]
|
||||
|
||||
|
||||
class ListItem:
|
||||
def __init__(self, name, obj): self.name, self.obj = name, obj
|
||||
def __str__(self): return self.name
|
||||
|
||||
def __init__(self, name, obj):
|
||||
self.name, self.obj = name, obj
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __init__(self, ui):
|
||||
# topmost index of items to show in view
|
||||
self.list_scroll_index = 0
|
||||
|
|
@ -149,41 +183,45 @@ class EditListPanel(GamePanel):
|
|||
for list_op in self.list_operation_labels:
|
||||
self.scroll_indices[list_op] = 0
|
||||
# map list operations to list builder functions
|
||||
self.list_functions = {LO_NONE: self.list_none,
|
||||
LO_SELECT_OBJECTS: self.list_objects,
|
||||
LO_SET_SPAWN_CLASS: self.list_classes,
|
||||
LO_LOAD_STATE: self.list_states,
|
||||
LO_SET_ROOM: self.list_rooms,
|
||||
LO_SET_ROOM_OBJECTS: self.list_objects,
|
||||
LO_SET_OBJECT_ROOMS: self.list_rooms,
|
||||
LO_OPEN_GAME_DIR: self.list_games,
|
||||
LO_SET_ROOM_EDGE_WARP: self.list_rooms_and_objects,
|
||||
LO_SET_ROOM_EDGE_OBJ: self.list_objects,
|
||||
LO_SET_ROOM_CAMERA: self.list_objects
|
||||
self.list_functions = {
|
||||
LO_NONE: self.list_none,
|
||||
LO_SELECT_OBJECTS: self.list_objects,
|
||||
LO_SET_SPAWN_CLASS: self.list_classes,
|
||||
LO_LOAD_STATE: self.list_states,
|
||||
LO_SET_ROOM: self.list_rooms,
|
||||
LO_SET_ROOM_OBJECTS: self.list_objects,
|
||||
LO_SET_OBJECT_ROOMS: self.list_rooms,
|
||||
LO_OPEN_GAME_DIR: self.list_games,
|
||||
LO_SET_ROOM_EDGE_WARP: self.list_rooms_and_objects,
|
||||
LO_SET_ROOM_EDGE_OBJ: self.list_objects,
|
||||
LO_SET_ROOM_CAMERA: self.list_objects,
|
||||
}
|
||||
# map list operations to "item clicked" functions
|
||||
self.click_functions = {LO_SELECT_OBJECTS: self.select_object,
|
||||
LO_SET_SPAWN_CLASS: self.set_spawn_class,
|
||||
LO_LOAD_STATE: self.load_state,
|
||||
LO_SET_ROOM: self.set_room,
|
||||
LO_SET_ROOM_OBJECTS: self.set_room_object,
|
||||
LO_SET_OBJECT_ROOMS: self.set_object_room,
|
||||
LO_OPEN_GAME_DIR: self.open_game_dir,
|
||||
LO_SET_ROOM_EDGE_WARP: self.set_room_edge_warp,
|
||||
LO_SET_ROOM_EDGE_OBJ: self.set_room_bounds_obj,
|
||||
LO_SET_ROOM_CAMERA: self.set_room_camera
|
||||
self.click_functions = {
|
||||
LO_SELECT_OBJECTS: self.select_object,
|
||||
LO_SET_SPAWN_CLASS: self.set_spawn_class,
|
||||
LO_LOAD_STATE: self.load_state,
|
||||
LO_SET_ROOM: self.set_room,
|
||||
LO_SET_ROOM_OBJECTS: self.set_room_object,
|
||||
LO_SET_OBJECT_ROOMS: self.set_object_room,
|
||||
LO_OPEN_GAME_DIR: self.open_game_dir,
|
||||
LO_SET_ROOM_EDGE_WARP: self.set_room_edge_warp,
|
||||
LO_SET_ROOM_EDGE_OBJ: self.set_room_bounds_obj,
|
||||
LO_SET_ROOM_CAMERA: self.set_room_camera,
|
||||
}
|
||||
# separate lists for item buttons vs other controls
|
||||
self.list_buttons = []
|
||||
# set when game resets
|
||||
self.should_reset_list = False
|
||||
GamePanel.__init__(self, ui)
|
||||
|
||||
|
||||
def create_buttons(self):
|
||||
def list_callback(item=None):
|
||||
if not item: return
|
||||
if not item:
|
||||
return
|
||||
self.clicked_item(item)
|
||||
for y in range(self.tile_height-1):
|
||||
|
||||
for y in range(self.tile_height - 1):
|
||||
button = ListButton(self)
|
||||
button.y = y + 1
|
||||
button.callback = list_callback
|
||||
|
|
@ -198,32 +236,33 @@ class EditListPanel(GamePanel):
|
|||
# TODO: adjust height according to screen tile height
|
||||
self.down_button.y = self.tile_height - 1
|
||||
self.buttons.append(self.down_button)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
GamePanel.reset_art(self)
|
||||
x = self.tile_width - 1
|
||||
for y in range(1, self.tile_height):
|
||||
self.art.set_tile_at(0, 0, x, y, self.scrollbar_shade_char,
|
||||
UIColors.medgrey)
|
||||
|
||||
self.art.set_tile_at(
|
||||
0, 0, x, y, self.scrollbar_shade_char, UIColors.medgrey
|
||||
)
|
||||
|
||||
def cancel(self):
|
||||
self.set_list_operation(LO_NONE)
|
||||
self.world.classname_to_spawn = None
|
||||
|
||||
|
||||
def scroll_list_up(self):
|
||||
if self.list_scroll_index > 0:
|
||||
self.list_scroll_index -= 1
|
||||
|
||||
|
||||
def scroll_list_down(self):
|
||||
max_scroll = len(self.items) - self.tile_height
|
||||
#max_scroll = len(self.element.items) - self.element.items_in_view
|
||||
# max_scroll = len(self.element.items) - self.element.items_in_view
|
||||
if self.list_scroll_index <= max_scroll:
|
||||
self.list_scroll_index += 1
|
||||
|
||||
|
||||
def clicked_item(self, item):
|
||||
# do thing appropriate to current list operation
|
||||
self.click_functions[self.list_operation](item)
|
||||
|
||||
|
||||
def wheel_moved(self, wheel_y):
|
||||
if wheel_y > 0:
|
||||
self.scroll_list_up()
|
||||
|
|
@ -231,7 +270,7 @@ class EditListPanel(GamePanel):
|
|||
if wheel_y < 0:
|
||||
self.scroll_list_down()
|
||||
return True
|
||||
|
||||
|
||||
def set_list_operation(self, new_op):
|
||||
"changes list type and sets new items"
|
||||
if new_op == LO_LOAD_STATE and not self.world.game_dir:
|
||||
|
|
@ -255,11 +294,14 @@ class EditListPanel(GamePanel):
|
|||
self.list_scroll_index = self.scroll_indices[self.list_operation]
|
||||
# keep in bounds if list size changed since last view
|
||||
self.list_scroll_index = min(self.list_scroll_index, len(self.items))
|
||||
|
||||
|
||||
def get_label(self):
|
||||
label = '%s (%s)' % (self.list_operation_labels[self.list_operation], self.cancel_tip)
|
||||
label = "%s (%s)" % (
|
||||
self.list_operation_labels[self.list_operation],
|
||||
self.cancel_tip,
|
||||
)
|
||||
# some labels contain variables
|
||||
if '%s' in label:
|
||||
if "%s" in label:
|
||||
if self.list_operation == LO_SET_ROOM_OBJECTS:
|
||||
if self.world.current_room:
|
||||
label %= self.world.current_room.name
|
||||
|
|
@ -267,7 +309,7 @@ class EditListPanel(GamePanel):
|
|||
if len(self.world.selected_objects) == 1:
|
||||
label %= self.world.selected_objects[0].name
|
||||
return label
|
||||
|
||||
|
||||
def should_highlight(self, item):
|
||||
if self.list_operation == LO_SELECT_OBJECTS:
|
||||
return item.obj in self.world.selected_objects
|
||||
|
|
@ -280,25 +322,30 @@ class EditListPanel(GamePanel):
|
|||
elif self.list_operation == LO_SET_ROOM:
|
||||
return self.world.current_room and item.name == self.world.current_room.name
|
||||
elif self.list_operation == LO_SET_ROOM_OBJECTS:
|
||||
return self.world.current_room and item.name in self.world.current_room.objects
|
||||
return (
|
||||
self.world.current_room and item.name in self.world.current_room.objects
|
||||
)
|
||||
elif self.list_operation == LO_SET_OBJECT_ROOMS:
|
||||
return len(self.world.selected_objects) == 1 and item.name in self.world.selected_objects[0].rooms
|
||||
return (
|
||||
len(self.world.selected_objects) == 1
|
||||
and item.name in self.world.selected_objects[0].rooms
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def game_reset(self):
|
||||
self.should_reset_list = True
|
||||
|
||||
|
||||
def items_changed(self):
|
||||
"called by anything that changes the items list, eg object add/delete"
|
||||
self.items = self.list_functions[self.list_operation]()
|
||||
# change selected item index if it's OOB
|
||||
if self.keyboard_nav_index >= len(self.items):
|
||||
self.keyboard_nav_index = len(self.items) - 1
|
||||
|
||||
|
||||
def refresh_items(self):
|
||||
for i,b in enumerate(self.list_buttons):
|
||||
for i, b in enumerate(self.list_buttons):
|
||||
if i >= len(self.items):
|
||||
b.caption = ''
|
||||
b.caption = ""
|
||||
b.cb_arg = None
|
||||
self.reset_button(b)
|
||||
b.can_hover = False
|
||||
|
|
@ -306,7 +353,7 @@ class EditListPanel(GamePanel):
|
|||
index = self.list_scroll_index + i
|
||||
item = self.items[index]
|
||||
b.cb_arg = item
|
||||
b.caption = item.name[:self.tile_width - 1]
|
||||
b.caption = item.name[: self.tile_width - 1]
|
||||
b.can_hover = True
|
||||
# change button appearance if this item should remain
|
||||
# highlighted/selected
|
||||
|
|
@ -315,7 +362,7 @@ class EditListPanel(GamePanel):
|
|||
else:
|
||||
self.reset_button(b)
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def post_keyboard_navigate(self):
|
||||
# check for scrolling
|
||||
if len(self.items) <= len(self.list_buttons):
|
||||
|
|
@ -336,7 +383,7 @@ class EditListPanel(GamePanel):
|
|||
elif self.keyboard_nav_index < 0:
|
||||
self.scroll_list_up()
|
||||
self.keyboard_nav_index += 1
|
||||
|
||||
|
||||
def update(self):
|
||||
if self.should_reset_list:
|
||||
self.set_list_operation(self.list_operation)
|
||||
|
|
@ -346,18 +393,18 @@ class EditListPanel(GamePanel):
|
|||
self.refresh_items()
|
||||
GamePanel.update(self)
|
||||
self.renderable.alpha = 1 if self is self.ui.keyboard_focus_element else 0.5
|
||||
|
||||
|
||||
def is_visible(self):
|
||||
return GamePanel.is_visible(self) and self.list_operation != LO_NONE
|
||||
|
||||
|
||||
#
|
||||
# list functions
|
||||
#
|
||||
def list_classes(self):
|
||||
items = []
|
||||
base_class = self.world.modules['game_object'].GameObject
|
||||
base_class = self.world.modules["game_object"].GameObject
|
||||
# get list of available classes from GameWorld
|
||||
for classname,classdef in self.world._get_all_loaded_classes().items():
|
||||
for classname, classdef in self.world._get_all_loaded_classes().items():
|
||||
# ignore non-GO classes, eg GameRoom, GameHUD
|
||||
if not issubclass(classdef, base_class):
|
||||
continue
|
||||
|
|
@ -368,7 +415,7 @@ class EditListPanel(GamePanel):
|
|||
# sort classes alphabetically
|
||||
items.sort(key=lambda i: i.name)
|
||||
return items
|
||||
|
||||
|
||||
def list_objects(self):
|
||||
items = []
|
||||
# include just-spawned objects too
|
||||
|
|
@ -377,24 +424,27 @@ class EditListPanel(GamePanel):
|
|||
for obj in all_objects.values():
|
||||
if obj.exclude_from_object_list:
|
||||
continue
|
||||
if self.world.list_only_current_room_objects and not self.world.current_room.name in obj.rooms:
|
||||
if (
|
||||
self.world.list_only_current_room_objects
|
||||
and self.world.current_room.name not in obj.rooms
|
||||
):
|
||||
continue
|
||||
li = self.ListItem(obj.name, obj)
|
||||
items.append(li)
|
||||
# sort object names alphabetically
|
||||
items.sort(key=lambda i: i.name)
|
||||
return items
|
||||
|
||||
|
||||
def list_states(self):
|
||||
items = []
|
||||
# list state files in current game dir
|
||||
for filename in os.listdir(self.world.game_dir):
|
||||
if filename.endswith('.' + STATE_FILE_EXTENSION):
|
||||
if filename.endswith("." + STATE_FILE_EXTENSION):
|
||||
li = self.ListItem(filename[:-3], None)
|
||||
items.append(li)
|
||||
items.sort(key=lambda i: i.name)
|
||||
return items
|
||||
|
||||
|
||||
def list_rooms(self):
|
||||
items = []
|
||||
for room in self.world.rooms.values():
|
||||
|
|
@ -402,7 +452,7 @@ class EditListPanel(GamePanel):
|
|||
items.append(li)
|
||||
items.sort(key=lambda i: i.name)
|
||||
return items
|
||||
|
||||
|
||||
def list_games(self):
|
||||
def get_dirs(dirname):
|
||||
dirs = []
|
||||
|
|
@ -410,6 +460,7 @@ class EditListPanel(GamePanel):
|
|||
if os.path.isdir(dirname + filename):
|
||||
dirs.append(filename)
|
||||
return dirs
|
||||
|
||||
# get list of both app dir games and user dir games
|
||||
docs_game_dir = self.ui.app.documents_dir + TOP_GAME_DIR
|
||||
items = []
|
||||
|
|
@ -419,18 +470,18 @@ class EditListPanel(GamePanel):
|
|||
li = self.ListItem(game, None)
|
||||
items.append(li)
|
||||
return items
|
||||
|
||||
|
||||
def list_rooms_and_objects(self):
|
||||
items = self.list_rooms()
|
||||
# prefix room names with "ROOM:"
|
||||
for i,item in enumerate(items):
|
||||
item.name = 'ROOM: %s' % item.name
|
||||
for i, item in enumerate(items):
|
||||
item.name = "ROOM: %s" % item.name
|
||||
items += self.list_objects()
|
||||
return items
|
||||
|
||||
|
||||
def list_none(self):
|
||||
return []
|
||||
|
||||
|
||||
#
|
||||
# "clicked list item" functions
|
||||
#
|
||||
|
|
@ -443,25 +494,27 @@ class EditListPanel(GamePanel):
|
|||
else:
|
||||
self.world.deselect_all()
|
||||
self.world.select_object(item.obj, force=True)
|
||||
|
||||
|
||||
def set_spawn_class(self, item):
|
||||
# set this class to be the one spawned when GameWorld is clicked
|
||||
self.world.classname_to_spawn = item.name
|
||||
self.ui.message_line.post_line(self.spawn_msg % self.world.classname_to_spawn, 5)
|
||||
|
||||
self.ui.message_line.post_line(
|
||||
self.spawn_msg % self.world.classname_to_spawn, 5
|
||||
)
|
||||
|
||||
def load_state(self, item):
|
||||
self.world.load_game_state(item.name)
|
||||
|
||||
|
||||
def set_room(self, item):
|
||||
self.world.change_room(item.name)
|
||||
|
||||
|
||||
def set_room_object(self, item):
|
||||
# add/remove object from current room
|
||||
if item.name in self.world.current_room.objects:
|
||||
self.world.current_room.remove_object_by_name(item.name)
|
||||
else:
|
||||
self.world.current_room.add_object_by_name(item.name)
|
||||
|
||||
|
||||
def set_object_room(self, item):
|
||||
# UI can only show a single object's rooms, do nothing if many selected
|
||||
if len(self.world.selected_objects) != 1:
|
||||
|
|
@ -473,20 +526,20 @@ class EditListPanel(GamePanel):
|
|||
room.remove_object(obj)
|
||||
else:
|
||||
room.add_object(obj)
|
||||
|
||||
|
||||
def open_game_dir(self, item):
|
||||
self.world.set_game_dir(item.name, True)
|
||||
|
||||
|
||||
def set_room_edge_warp(self, item):
|
||||
dialog = self.ui.active_dialog
|
||||
dialog.field_texts[dialog.active_field] = item.obj.name
|
||||
self.ui.keyboard_focus_element = dialog
|
||||
|
||||
|
||||
def set_room_bounds_obj(self, item):
|
||||
dialog = self.ui.active_dialog
|
||||
dialog.field_texts[dialog.active_field] = item.obj.name
|
||||
self.ui.keyboard_focus_element = dialog
|
||||
|
||||
|
||||
def set_room_camera(self, item):
|
||||
dialog = self.ui.active_dialog
|
||||
dialog.field_texts[dialog.active_field] = item.obj.name
|
||||
|
|
|
|||
188
ui_element.py
188
ui_element.py
|
|
@ -1,15 +1,12 @@
|
|||
import time
|
||||
import numpy as np
|
||||
from math import ceil
|
||||
|
||||
import vector
|
||||
from art import Art
|
||||
from renderable import TileRenderable
|
||||
from renderable_line import LineRenderable
|
||||
from ui_button import UIButton
|
||||
|
||||
|
||||
class UIElement:
|
||||
|
||||
# size, in tiles
|
||||
tile_width, tile_height = 1, 1
|
||||
snap_top, snap_bottom, snap_left, snap_right = False, False, False, False
|
||||
|
|
@ -35,13 +32,20 @@ class UIElement:
|
|||
game_mode_visible = False
|
||||
all_modes_visible = False
|
||||
keyboard_nav_offset = 0
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.hovered_buttons = []
|
||||
# generate a unique name
|
||||
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
self.art = UIArt(art_name, self.ui.app, self.ui.charset, self.ui.palette, self.tile_width, self.tile_height)
|
||||
art_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
self.art = UIArt(
|
||||
art_name,
|
||||
self.ui.app,
|
||||
self.ui.charset,
|
||||
self.ui.palette,
|
||||
self.tile_width,
|
||||
self.tile_height,
|
||||
)
|
||||
self.renderable = UIRenderable(self.ui.app, self.art)
|
||||
self.renderable.ui = self.ui
|
||||
# some elements add their own renderables before calling this
|
||||
|
|
@ -53,13 +57,13 @@ class UIElement:
|
|||
self.reset_loc()
|
||||
if self.support_keyboard_navigation:
|
||||
self.keyboard_nav_index = 0
|
||||
|
||||
|
||||
def is_inside(self, x, y):
|
||||
"returns True if given point is inside this element's bounds"
|
||||
w = self.tile_width * self.art.quad_width
|
||||
h = self.tile_height * self.art.quad_height
|
||||
return self.x <= x <= self.x+w and self.y >= y >= self.y-h
|
||||
|
||||
return self.x <= x <= self.x + w and self.y >= y >= self.y - h
|
||||
|
||||
def is_inside_button(self, x, y, button):
|
||||
"returns True if given point is inside the given button's bounds"
|
||||
aqw, aqh = self.art.quad_width, self.art.quad_height
|
||||
|
|
@ -69,30 +73,30 @@ class UIElement:
|
|||
bxmin, bymin = self.x + bx, self.y - by
|
||||
bxmax, bymax = bxmin + bw, bymin - bh
|
||||
return bxmin <= x <= bxmax and bymin >= y >= bymax
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
"""
|
||||
runs on init and resize, restores state.
|
||||
"""
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def draw_buttons(self):
|
||||
for button in self.buttons:
|
||||
if button.visible:
|
||||
button.draw()
|
||||
|
||||
|
||||
def hovered(self):
|
||||
self.log_event('hovered')
|
||||
|
||||
self.log_event("hovered")
|
||||
|
||||
def unhovered(self):
|
||||
self.log_event('unhovered')
|
||||
|
||||
self.log_event("unhovered")
|
||||
|
||||
def wheel_moved(self, wheel_y):
|
||||
handled = False
|
||||
return handled
|
||||
|
||||
|
||||
def clicked(self, mouse_button):
|
||||
self.log_event('clicked', mouse_button)
|
||||
self.log_event("clicked", mouse_button)
|
||||
# return if a button did something
|
||||
handled = False
|
||||
# tell any hovered buttons they've been clicked
|
||||
|
|
@ -116,9 +120,9 @@ class UIElement:
|
|||
if self.always_consume_input:
|
||||
return True
|
||||
return handled
|
||||
|
||||
|
||||
def unclicked(self, mouse_button):
|
||||
self.log_event('unclicked', mouse_button)
|
||||
self.log_event("unclicked", mouse_button)
|
||||
handled = False
|
||||
for b in self.hovered_buttons:
|
||||
b.unclick()
|
||||
|
|
@ -126,21 +130,27 @@ class UIElement:
|
|||
if self.always_consume_input:
|
||||
return True
|
||||
return handled
|
||||
|
||||
|
||||
def log_event(self, event_type, mouse_button=None):
|
||||
mouse_button = mouse_button or '[n/a]'
|
||||
mouse_button = mouse_button or "[n/a]"
|
||||
if self.ui.logg:
|
||||
self.ui.app.log('UIElement: %s %s with mouse button %s' % (self.__class__.__name__, event_type, mouse_button))
|
||||
|
||||
self.ui.app.log(
|
||||
"UIElement: %s %s with mouse button %s"
|
||||
% (self.__class__.__name__, event_type, mouse_button)
|
||||
)
|
||||
|
||||
def is_visible(self):
|
||||
if self.all_modes_visible:
|
||||
return self.visible
|
||||
elif not self.ui.app.game_mode and self.game_mode_visible:
|
||||
return False
|
||||
elif self.ui.app.game_mode and not self.game_mode_visible:
|
||||
elif (
|
||||
not self.ui.app.game_mode
|
||||
and self.game_mode_visible
|
||||
or self.ui.app.game_mode
|
||||
and not self.game_mode_visible
|
||||
):
|
||||
return False
|
||||
return self.visible
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
if self.snap_top:
|
||||
self.y = 1
|
||||
|
|
@ -155,7 +165,7 @@ class UIElement:
|
|||
elif self.tile_x:
|
||||
self.x = -1 + (self.tile_x * self.art.quad_width)
|
||||
self.renderable.x, self.renderable.y = self.x, self.y
|
||||
|
||||
|
||||
def keyboard_navigate(self, move_x, move_y):
|
||||
if not self.support_keyboard_navigation:
|
||||
return
|
||||
|
|
@ -174,7 +184,10 @@ class UIElement:
|
|||
self.keyboard_nav_index %= len(self.buttons) + self.keyboard_nav_offset
|
||||
tries = 0
|
||||
# recognize two different kinds of inactive items: empty caption and dim state
|
||||
while tries < len(self.buttons) and (self.buttons[self.keyboard_nav_index].caption == '' or self.buttons[self.keyboard_nav_index].state == 'dimmed'):
|
||||
while tries < len(self.buttons) and (
|
||||
self.buttons[self.keyboard_nav_index].caption == ""
|
||||
or self.buttons[self.keyboard_nav_index].state == "dimmed"
|
||||
):
|
||||
# move_y might be zero, give it a direction to avoid infinite loop
|
||||
# if menu item 0 is dimmed
|
||||
self.keyboard_nav_index += move_y or 1
|
||||
|
|
@ -184,23 +197,23 @@ class UIElement:
|
|||
return
|
||||
self.post_keyboard_navigate()
|
||||
self.update_keyboard_hover()
|
||||
|
||||
|
||||
def update_keyboard_hover(self):
|
||||
if not self.support_keyboard_navigation:
|
||||
return
|
||||
for i,button in enumerate(self.buttons):
|
||||
for i, button in enumerate(self.buttons):
|
||||
# don't higlhight if this panel doesn't have focus
|
||||
if self.keyboard_nav_index == i and self is self.ui.keyboard_focus_element:
|
||||
button.set_state('hovered')
|
||||
elif button.state != 'dimmed':
|
||||
button.set_state('normal')
|
||||
|
||||
button.set_state("hovered")
|
||||
elif button.state != "dimmed":
|
||||
button.set_state("normal")
|
||||
|
||||
def keyboard_select_item(self):
|
||||
if not self.support_keyboard_navigation:
|
||||
return
|
||||
button = self.buttons[self.keyboard_nav_index]
|
||||
# don't allow selecting dimmed buttons
|
||||
if button.state == 'dimmed':
|
||||
if button.state == "dimmed":
|
||||
return
|
||||
# check for None; cb_arg could be 0
|
||||
if button.cb_arg is not None:
|
||||
|
|
@ -208,11 +221,11 @@ class UIElement:
|
|||
else:
|
||||
button.callback()
|
||||
return button
|
||||
|
||||
|
||||
def post_keyboard_navigate(self):
|
||||
# subclasses can put stuff here to check scrolling etc
|
||||
pass
|
||||
|
||||
|
||||
def update(self):
|
||||
"runs every frame, checks button states"
|
||||
# this is very similar to UI.update, implying an alternative structure
|
||||
|
|
@ -225,16 +238,20 @@ class UIElement:
|
|||
for b in self.buttons:
|
||||
# element.clicked might have been set it non-hoverable, acknowledge
|
||||
# its hoveredness here so it can unhover correctly
|
||||
if b.visible and (b.can_hover or b.state == 'clicked') and self.is_inside_button(mx, my, b):
|
||||
if (
|
||||
b.visible
|
||||
and (b.can_hover or b.state == "clicked")
|
||||
and self.is_inside_button(mx, my, b)
|
||||
):
|
||||
self.hovered_buttons.append(b)
|
||||
if not b in was_hovering:
|
||||
if b not in was_hovering:
|
||||
b.hover()
|
||||
for b in was_hovering:
|
||||
if not b in self.hovered_buttons:
|
||||
if b not in self.hovered_buttons:
|
||||
b.unhover()
|
||||
# tiles might have just changed
|
||||
self.art.update()
|
||||
|
||||
|
||||
def render(self):
|
||||
# ("is visible" check happens in UI.render, calls our is_visible)
|
||||
# render drop shadow first
|
||||
|
|
@ -246,7 +263,7 @@ class UIElement:
|
|||
self.renderable.render(brightness=0.1)
|
||||
self.renderable.x, self.renderable.y = orig_x, orig_y
|
||||
self.renderable.render()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
for r in self.renderables:
|
||||
r.destroy()
|
||||
|
|
@ -258,27 +275,25 @@ class UIArt(Art):
|
|||
|
||||
|
||||
class UIRenderable(TileRenderable):
|
||||
|
||||
grain_strength = 0.2
|
||||
|
||||
|
||||
def get_projection_matrix(self):
|
||||
# don't use projection matrix, ie identity[0][0]=aspect;
|
||||
# rather do all aspect correction in UI.set_scale when determining quad size
|
||||
return self.ui.view_matrix
|
||||
|
||||
|
||||
def get_view_matrix(self):
|
||||
return self.ui.view_matrix
|
||||
|
||||
|
||||
class FPSCounterUI(UIElement):
|
||||
|
||||
tile_y = 1
|
||||
tile_width, tile_height = 12, 2
|
||||
snap_right = True
|
||||
game_mode_visible = True
|
||||
all_modes_visible = True
|
||||
visible = False
|
||||
|
||||
|
||||
def update(self):
|
||||
bg = 0
|
||||
self.art.clear_frame_layer(0, 0, bg)
|
||||
|
|
@ -288,13 +303,13 @@ class FPSCounterUI(UIElement):
|
|||
color = self.ui.colors.yellow
|
||||
if self.ui.app.fps < 10:
|
||||
color = self.ui.colors.red
|
||||
text = '%.1f fps' % self.ui.app.fps
|
||||
text = "%.1f fps" % self.ui.app.fps
|
||||
x = self.tile_width - 1
|
||||
self.art.write_string(0, 0, x, 0, text, color, None, True)
|
||||
# display last tick time; frame_time includes delay, is useless
|
||||
text = '%.1f ms ' % self.ui.app.frame_time
|
||||
text = "%.1f ms " % self.ui.app.frame_time
|
||||
self.art.write_string(0, 0, x, 1, text, color, None, True)
|
||||
|
||||
|
||||
def render(self):
|
||||
# always show FPS if low
|
||||
if self.visible or self.ui.app.fps < 30:
|
||||
|
|
@ -302,9 +317,8 @@ class FPSCounterUI(UIElement):
|
|||
|
||||
|
||||
class MessageLineUI(UIElement):
|
||||
|
||||
"when console outputs something new, show last line here before fading out"
|
||||
|
||||
|
||||
tile_y = 2
|
||||
snap_left = True
|
||||
# just info, don't bother with hover, click etc
|
||||
|
|
@ -314,21 +328,21 @@ class MessageLineUI(UIElement):
|
|||
game_mode_visible = True
|
||||
all_modes_visible = True
|
||||
drop_shadow = True
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
UIElement.__init__(self, ui)
|
||||
# line we're currently displaying (even after fading out)
|
||||
self.line = ''
|
||||
self.line = ""
|
||||
self.last_post = self.ui.app.get_elapsed_time()
|
||||
self.hold_time = self.default_hold_time
|
||||
self.alpha = 1
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
self.tile_width = ceil(self.ui.width_tiles)
|
||||
self.art.resize(self.tile_width, self.tile_height)
|
||||
self.art.clear_frame_layer(0, 0, 0, self.ui.colors.white)
|
||||
UIElement.reset_loc(self)
|
||||
|
||||
|
||||
def post_line(self, new_line, hold_time=None, error=False):
|
||||
"write a line to this element (ie so as not to spam console log)"
|
||||
self.hold_time = hold_time or self.default_hold_time
|
||||
|
|
@ -336,12 +350,12 @@ class MessageLineUI(UIElement):
|
|||
color = self.ui.error_color_index if error else self.ui.colors.white
|
||||
start_x = 1
|
||||
# trim to screen width
|
||||
self.line = str(new_line)[:self.tile_width-start_x-1]
|
||||
self.line = str(new_line)[: self.tile_width - start_x - 1]
|
||||
self.art.clear_frame_layer(0, 0, 0, color)
|
||||
self.art.write_string(0, 0, start_x, 0, self.line)
|
||||
self.alpha = 1
|
||||
self.last_post = self.ui.app.get_elapsed_time()
|
||||
|
||||
|
||||
def update(self):
|
||||
if self.ui.app.get_elapsed_time() > self.last_post + (self.hold_time * 1000):
|
||||
if self.alpha >= self.fade_rate:
|
||||
|
|
@ -349,70 +363,71 @@ class MessageLineUI(UIElement):
|
|||
if self.alpha <= self.fade_rate:
|
||||
self.alpha = 0
|
||||
self.renderable.alpha = self.alpha
|
||||
|
||||
|
||||
def render(self):
|
||||
# TODO: draw if popup is visible but not obscuring message line?
|
||||
if not self.ui.popup in self.ui.hovered_elements and not self.ui.console.visible:
|
||||
if (
|
||||
self.ui.popup not in self.ui.hovered_elements
|
||||
and not self.ui.console.visible
|
||||
):
|
||||
UIElement.render(self)
|
||||
|
||||
|
||||
class DebugTextUI(UIElement):
|
||||
|
||||
"simple UI element for posting debug text"
|
||||
|
||||
|
||||
tile_x, tile_y = 1, 4
|
||||
tile_height = 20
|
||||
clear_lines_after_render = True
|
||||
game_mode_visible = True
|
||||
visible = False
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
UIElement.__init__(self, ui)
|
||||
self.lines = []
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
self.tile_width = ceil(self.ui.width_tiles)
|
||||
self.art.resize(self.tile_width, self.tile_height)
|
||||
self.art.clear_frame_layer(0, 0, 0, self.ui.colors.white)
|
||||
UIElement.reset_loc(self)
|
||||
|
||||
|
||||
def post_lines(self, lines):
|
||||
if type(lines) is list:
|
||||
self.lines += lines
|
||||
else:
|
||||
self.lines += [lines]
|
||||
|
||||
|
||||
def update(self):
|
||||
self.art.clear_frame_layer(0, 0, 0, self.ui.colors.white)
|
||||
for y,line in enumerate(self.lines):
|
||||
for y, line in enumerate(self.lines):
|
||||
self.art.write_string(0, 0, 0, y, line)
|
||||
|
||||
|
||||
def render(self):
|
||||
UIElement.render(self)
|
||||
if self.clear_lines_after_render:
|
||||
self.lines = []
|
||||
#self.art.clear_frame_layer(0, 0, 0, self.ui.colors.white)
|
||||
# self.art.clear_frame_layer(0, 0, 0, self.ui.colors.white)
|
||||
|
||||
|
||||
class ToolTip(UIElement):
|
||||
|
||||
"popup text label that is invoked and controlled by a UIButton hover"
|
||||
|
||||
|
||||
visible = False
|
||||
tile_width, tile_height = 30, 1
|
||||
tile_x, tile_y = 10, 5
|
||||
|
||||
|
||||
def set_text(self, text):
|
||||
self.art.write_string(0, 0, 0, 0, text,
|
||||
self.ui.colors.black, self.ui.colors.white)
|
||||
self.art.write_string(
|
||||
0, 0, 0, 0, text, self.ui.colors.black, self.ui.colors.white
|
||||
)
|
||||
# clear tiles past end of text
|
||||
for x in range(len(text), self.tile_width):
|
||||
self.art.set_color_at(0, 0, x, 0, 0, 0)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
UIElement.reset_art(self)
|
||||
self.art.clear_frame_layer(0, 0,
|
||||
self.ui.colors.white, self.ui.colors.black)
|
||||
self.art.clear_frame_layer(0, 0, self.ui.colors.white, self.ui.colors.black)
|
||||
|
||||
|
||||
class GameLabel(UIElement):
|
||||
|
|
@ -423,9 +438,8 @@ class GameLabel(UIElement):
|
|||
|
||||
|
||||
class GameSelectionLabel(GameLabel):
|
||||
|
||||
multi_select_label = '[%s selected]'
|
||||
|
||||
multi_select_label = "[%s selected]"
|
||||
|
||||
def update(self):
|
||||
self.visible = False
|
||||
if self.ui.pulldown.visible or not self.ui.is_game_edit_ui_visible():
|
||||
|
|
@ -435,7 +449,7 @@ class GameSelectionLabel(GameLabel):
|
|||
self.visible = True
|
||||
if len(self.ui.app.gw.selected_objects) == 1:
|
||||
obj = self.ui.app.gw.selected_objects[0]
|
||||
text = obj.name[:self.tile_width-1]
|
||||
text = obj.name[: self.tile_width - 1]
|
||||
x, y, z = obj.x, obj.y, obj.z
|
||||
else:
|
||||
# draw "[N selected]" at avg of selected object locations
|
||||
|
|
@ -453,10 +467,10 @@ class GameSelectionLabel(GameLabel):
|
|||
self.x, self.y = vector.world_to_screen_normalized(self.ui.app, x, y, z)
|
||||
self.reset_loc()
|
||||
|
||||
|
||||
class GameHoverLabel(GameLabel):
|
||||
|
||||
alpha = 0.75
|
||||
|
||||
|
||||
def update(self):
|
||||
self.visible = False
|
||||
if self.ui.pulldown.visible or not self.ui.is_game_edit_ui_visible():
|
||||
|
|
@ -465,7 +479,7 @@ class GameHoverLabel(GameLabel):
|
|||
return
|
||||
self.visible = True
|
||||
obj = self.ui.app.gw.hovered_focus_object
|
||||
text = obj.name[:self.tile_width-1]
|
||||
text = obj.name[: self.tile_width - 1]
|
||||
x, y, z = obj.x, obj.y, obj.z
|
||||
self.art.clear_line(0, 0, 0, self.ui.colors.white, -1)
|
||||
self.art.write_string(0, 0, 0, 0, text)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,33 @@
|
|||
|
||||
import os, time, json
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from texture import Texture
|
||||
from ui_chooser_dialog import ChooserDialog, ChooserItem, ChooserItemButton
|
||||
from ui_console import OpenCommand, LoadCharSetCommand, LoadPaletteCommand
|
||||
from ui_art_dialog import PaletteFromFileDialog, ImportOptionsDialog
|
||||
from art import ART_DIR, ART_FILE_EXTENSION, THUMBNAIL_CACHE_DIR, SCRIPT_FILE_EXTENSION, ART_SCRIPT_DIR
|
||||
from palette import Palette, PALETTE_DIR, PALETTE_EXTENSIONS
|
||||
from charset import CharacterSet, CHARSET_DIR, CHARSET_FILE_EXTENSION
|
||||
from art import (
|
||||
ART_DIR,
|
||||
ART_FILE_EXTENSION,
|
||||
ART_SCRIPT_DIR,
|
||||
SCRIPT_FILE_EXTENSION,
|
||||
THUMBNAIL_CACHE_DIR,
|
||||
)
|
||||
from charset import CHARSET_DIR, CHARSET_FILE_EXTENSION
|
||||
from image_export import write_thumbnail
|
||||
from palette import PALETTE_DIR, PALETTE_EXTENSIONS
|
||||
from texture import Texture
|
||||
from ui_art_dialog import ImportOptionsDialog, PaletteFromFileDialog
|
||||
from ui_chooser_dialog import ChooserDialog, ChooserItem
|
||||
from ui_console import OpenCommand
|
||||
|
||||
|
||||
class BaseFileChooserItem(ChooserItem):
|
||||
|
||||
hide_file_extension = False
|
||||
|
||||
|
||||
def get_short_dir_name(self):
|
||||
# name should end in / but don't assume
|
||||
dir_name = self.name[:-1] if self.name.endswith('/') else self.name
|
||||
return os.path.basename(dir_name) + '/'
|
||||
|
||||
dir_name = self.name[:-1] if self.name.endswith("/") else self.name
|
||||
return os.path.basename(dir_name) + "/"
|
||||
|
||||
def get_label(self):
|
||||
if os.path.isdir(self.name):
|
||||
return self.get_short_dir_name()
|
||||
|
|
@ -31,15 +37,15 @@ class BaseFileChooserItem(ChooserItem):
|
|||
return os.path.splitext(label)[0]
|
||||
else:
|
||||
return label
|
||||
|
||||
|
||||
def get_description_lines(self):
|
||||
if os.path.isdir(self.name):
|
||||
if self.name == '..':
|
||||
return ['[parent folder]']
|
||||
if self.name == "..":
|
||||
return ["[parent folder]"]
|
||||
# TODO: # of items in dir?
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
def picked(self, element):
|
||||
# if this is different from the last clicked item, pick it
|
||||
if element.selected_item_index != self.index:
|
||||
|
|
@ -52,8 +58,8 @@ class BaseFileChooserItem(ChooserItem):
|
|||
if not element.first_selection_made:
|
||||
element.first_selection_made = True
|
||||
return
|
||||
if self.name == '..' and self.name != '/':
|
||||
new_dir = os.path.abspath(os.path.abspath(element.current_dir) + '/..')
|
||||
if self.name == ".." and self.name != "/":
|
||||
new_dir = os.path.abspath(os.path.abspath(element.current_dir) + "/..")
|
||||
element.change_current_dir(new_dir)
|
||||
elif os.path.isdir(self.name):
|
||||
new_dir = element.current_dir + self.get_short_dir_name()
|
||||
|
|
@ -62,39 +68,40 @@ class BaseFileChooserItem(ChooserItem):
|
|||
element.confirm_pressed()
|
||||
element.first_selection_made = False
|
||||
|
||||
|
||||
class BaseFileChooserDialog(ChooserDialog):
|
||||
|
||||
"base class for choosers whose items correspond with files"
|
||||
|
||||
chooser_item_class = BaseFileChooserItem
|
||||
show_filenames = True
|
||||
file_extensions = []
|
||||
|
||||
|
||||
def set_initial_dir(self):
|
||||
self.current_dir = self.ui.app.documents_dir
|
||||
self.field_texts[self.active_field] = self.current_dir
|
||||
|
||||
|
||||
def get_filenames(self):
|
||||
"subclasses override: get list of desired filenames"
|
||||
return self.get_sorted_dir_list()
|
||||
|
||||
|
||||
def get_sorted_dir_list(self):
|
||||
"common code for getting sorted directory + file lists"
|
||||
# list parent, then dirs, then filenames with extension(s)
|
||||
parent = [] if self.current_dir == '/' else ['..']
|
||||
parent = [] if self.current_dir == "/" else [".."]
|
||||
if not os.path.exists(self.current_dir):
|
||||
return parent
|
||||
dirs, files = [], []
|
||||
for filename in os.listdir(self.current_dir):
|
||||
# skip unix-hidden files
|
||||
if filename.startswith('.'):
|
||||
if filename.startswith("."):
|
||||
continue
|
||||
full_filename = self.current_dir + filename
|
||||
# if no extensions specified, take any file
|
||||
if len(self.file_extensions) == 0:
|
||||
self.file_extensions = ['']
|
||||
self.file_extensions = [""]
|
||||
for ext in self.file_extensions:
|
||||
if os.path.isdir(full_filename):
|
||||
dirs += [full_filename + '/']
|
||||
dirs += [full_filename + "/"]
|
||||
break
|
||||
elif filename.lower().endswith(ext.lower()):
|
||||
files += [full_filename]
|
||||
|
|
@ -102,7 +109,7 @@ class BaseFileChooserDialog(ChooserDialog):
|
|||
dirs.sort(key=lambda x: x.lower())
|
||||
files.sort(key=lambda x: x.lower())
|
||||
return parent + dirs + files
|
||||
|
||||
|
||||
def get_items(self):
|
||||
"populate and return items from list of files, loading as needed"
|
||||
items = []
|
||||
|
|
@ -123,12 +130,12 @@ class BaseFileChooserDialog(ChooserDialog):
|
|||
# art chooser
|
||||
#
|
||||
|
||||
|
||||
class ArtChooserItem(BaseFileChooserItem):
|
||||
|
||||
# set in load()
|
||||
art_width = None
|
||||
hide_file_extension = True
|
||||
|
||||
|
||||
def get_description_lines(self):
|
||||
lines = BaseFileChooserItem.get_description_lines(self)
|
||||
if lines is not None:
|
||||
|
|
@ -136,31 +143,33 @@ class ArtChooserItem(BaseFileChooserItem):
|
|||
if not self.art_width:
|
||||
return []
|
||||
mod_time = time.gmtime(self.art_mod_time)
|
||||
mod_time = time.strftime('%Y-%m-%d %H:%M:%S', mod_time)
|
||||
lines = ['last change: %s' % mod_time]
|
||||
line = '%s x %s, ' % (self.art_width, self.art_height)
|
||||
line += '%s frame' % self.art_frames
|
||||
mod_time = time.strftime("%Y-%m-%d %H:%M:%S", mod_time)
|
||||
lines = ["last change: %s" % mod_time]
|
||||
line = "%s x %s, " % (self.art_width, self.art_height)
|
||||
line += "%s frame" % self.art_frames
|
||||
# pluralize properly
|
||||
line += 's' if self.art_frames > 1 else ''
|
||||
line += ', %s layer' % self.art_layers
|
||||
line += 's' if self.art_layers > 1 else ''
|
||||
line += "s" if self.art_frames > 1 else ""
|
||||
line += ", %s layer" % self.art_layers
|
||||
line += "s" if self.art_layers > 1 else ""
|
||||
lines += [line]
|
||||
lines += ['char: %s, pal: %s' % (self.art_charset, self.art_palette)]
|
||||
lines += ["char: %s, pal: %s" % (self.art_charset, self.art_palette)]
|
||||
return lines
|
||||
|
||||
|
||||
def get_preview_texture(self, app):
|
||||
if os.path.isdir(self.name):
|
||||
return
|
||||
thumbnail_filename = app.cache_dir + THUMBNAIL_CACHE_DIR + self.art_hash + '.png'
|
||||
thumbnail_filename = (
|
||||
app.cache_dir + THUMBNAIL_CACHE_DIR + self.art_hash + ".png"
|
||||
)
|
||||
# create thumbnail if it doesn't exist
|
||||
if not os.path.exists(thumbnail_filename):
|
||||
write_thumbnail(app, self.name, thumbnail_filename)
|
||||
# read thumbnail
|
||||
img = Image.open(thumbnail_filename)
|
||||
img = img.convert('RGBA')
|
||||
img = img.convert("RGBA")
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
return Texture(img.tobytes(), *img.size)
|
||||
|
||||
|
||||
def load(self, app):
|
||||
if os.path.isdir(self.name):
|
||||
return
|
||||
|
|
@ -172,33 +181,36 @@ class ArtChooserItem(BaseFileChooserItem):
|
|||
self.art_hash = app.get_file_hash(self.name)
|
||||
# rather than load the entire art, just get some high level stats
|
||||
d = json.load(open(self.name))
|
||||
self.art_width, self.art_height = d['width'], d['height']
|
||||
self.art_frames = len(d['frames'])
|
||||
self.art_layers = len(d['frames'][0]['layers'])
|
||||
self.art_charset = d['charset']
|
||||
self.art_palette = d['palette']
|
||||
self.art_width, self.art_height = d["width"], d["height"]
|
||||
self.art_frames = len(d["frames"])
|
||||
self.art_layers = len(d["frames"][0]["layers"])
|
||||
self.art_charset = d["charset"]
|
||||
self.art_palette = d["palette"]
|
||||
|
||||
|
||||
class ArtChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Open art'
|
||||
confirm_caption = 'Open'
|
||||
cancel_caption = 'Cancel'
|
||||
title = "Open art"
|
||||
confirm_caption = "Open"
|
||||
cancel_caption = "Cancel"
|
||||
chooser_item_class = ArtChooserItem
|
||||
flip_preview_y = False
|
||||
directory_aware = True
|
||||
file_extensions = [ART_FILE_EXTENSION]
|
||||
|
||||
|
||||
def set_initial_dir(self):
|
||||
# TODO: IF no art in Documents dir yet, start in app/art/ for examples?
|
||||
# get last opened dir, else start in docs/game art dir
|
||||
if self.ui.app.last_art_dir:
|
||||
self.current_dir = self.ui.app.last_art_dir
|
||||
else:
|
||||
self.current_dir = self.ui.app.gw.game_dir if self.ui.app.gw.game_dir else self.ui.app.documents_dir
|
||||
self.current_dir = (
|
||||
self.ui.app.gw.game_dir
|
||||
if self.ui.app.gw.game_dir
|
||||
else self.ui.app.documents_dir
|
||||
)
|
||||
self.current_dir += ART_DIR
|
||||
self.field_texts[self.active_field] = self.current_dir
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
if not os.path.exists(self.field_texts[0]):
|
||||
return
|
||||
|
|
@ -211,27 +223,26 @@ class ArtChooserDialog(BaseFileChooserDialog):
|
|||
# generic file chooser for importers
|
||||
#
|
||||
class GenericImportChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Import %s'
|
||||
confirm_caption = 'Import'
|
||||
cancel_caption = 'Cancel'
|
||||
# allowed extensions set by invoking
|
||||
title = "Import %s"
|
||||
confirm_caption = "Import"
|
||||
cancel_caption = "Cancel"
|
||||
# allowed extensions set by invoking
|
||||
file_extensions = []
|
||||
show_preview_image = False
|
||||
directory_aware = True
|
||||
|
||||
|
||||
def __init__(self, ui, options):
|
||||
self.title %= ui.app.importer.format_name
|
||||
self.file_extensions = ui.app.importer.allowed_file_extensions
|
||||
BaseFileChooserDialog.__init__(self, ui, options)
|
||||
|
||||
|
||||
def set_initial_dir(self):
|
||||
if self.ui.app.last_import_dir:
|
||||
self.current_dir = self.ui.app.last_import_dir
|
||||
else:
|
||||
self.current_dir = self.ui.app.documents_dir
|
||||
self.field_texts[self.active_field] = self.current_dir
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
filename = self.field_texts[0]
|
||||
if not os.path.exists(filename):
|
||||
|
|
@ -240,15 +251,13 @@ class GenericImportChooserDialog(BaseFileChooserDialog):
|
|||
self.dismiss()
|
||||
# importer might offer a dialog for options
|
||||
if self.ui.app.importer.options_dialog_class:
|
||||
options = {'filename': filename}
|
||||
self.ui.open_dialog(self.ui.app.importer.options_dialog_class,
|
||||
options)
|
||||
options = {"filename": filename}
|
||||
self.ui.open_dialog(self.ui.app.importer.options_dialog_class, options)
|
||||
else:
|
||||
ImportOptionsDialog.do_import(self.ui.app, filename, {})
|
||||
|
||||
|
||||
class ImageChooserItem(BaseFileChooserItem):
|
||||
|
||||
def get_preview_texture(self, app):
|
||||
if os.path.isdir(self.name):
|
||||
return
|
||||
|
|
@ -258,26 +267,26 @@ class ImageChooserItem(BaseFileChooserItem):
|
|||
except:
|
||||
return
|
||||
try:
|
||||
img = img.convert('RGBA')
|
||||
img = img.convert("RGBA")
|
||||
except:
|
||||
# (probably) PIL bug: some images just crash! return None
|
||||
return
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
return Texture(img.tobytes(), *img.size)
|
||||
|
||||
|
||||
class ImageFileChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
cancel_caption = 'Cancel'
|
||||
cancel_caption = "Cancel"
|
||||
chooser_item_class = ImageChooserItem
|
||||
flip_preview_y = False
|
||||
directory_aware = True
|
||||
file_extensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif']
|
||||
file_extensions = ["png", "jpg", "jpeg", "bmp", "gif"]
|
||||
|
||||
|
||||
class PaletteFromImageChooserDialog(ImageFileChooserDialog):
|
||||
|
||||
title = 'Palette from image'
|
||||
confirm_caption = 'Choose'
|
||||
|
||||
title = "Palette from image"
|
||||
confirm_caption = "Choose"
|
||||
|
||||
def confirm_pressed(self):
|
||||
if not os.path.exists(self.field_texts[0]):
|
||||
return
|
||||
|
|
@ -291,31 +300,31 @@ class PaletteFromImageChooserDialog(ImageFileChooserDialog):
|
|||
palette_filename = os.path.splitext(palette_filename)[0]
|
||||
self.ui.active_dialog.field_texts[1] = palette_filename
|
||||
|
||||
|
||||
#
|
||||
# palette chooser
|
||||
#
|
||||
|
||||
|
||||
class PaletteChooserItem(BaseFileChooserItem):
|
||||
|
||||
def get_label(self):
|
||||
return os.path.splitext(self.name)[0]
|
||||
|
||||
|
||||
def get_description_lines(self):
|
||||
colors = len(self.palette.colors)
|
||||
return ['Unique colors: %s' % str(colors - 1)]
|
||||
|
||||
return ["Unique colors: %s" % str(colors - 1)]
|
||||
|
||||
def get_preview_texture(self, app):
|
||||
return self.palette.src_texture
|
||||
|
||||
|
||||
def load(self, app):
|
||||
self.palette = app.load_palette(self.name)
|
||||
|
||||
|
||||
class PaletteChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Choose palette'
|
||||
title = "Choose palette"
|
||||
chooser_item_class = PaletteChooserItem
|
||||
|
||||
|
||||
def get_initial_selection(self):
|
||||
if not self.ui.active_art:
|
||||
return 0
|
||||
|
|
@ -324,9 +333,9 @@ class PaletteChooserDialog(BaseFileChooserDialog):
|
|||
# eg filename minus extension
|
||||
if item.label == self.ui.active_art.palette.name:
|
||||
return item.index
|
||||
#print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
||||
# print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
||||
return 0
|
||||
|
||||
|
||||
def get_filenames(self):
|
||||
filenames = []
|
||||
# search all files in dirs with appropriate extensions
|
||||
|
|
@ -337,54 +346,54 @@ class PaletteChooserDialog(BaseFileChooserDialog):
|
|||
filenames.append(filename)
|
||||
filenames.sort(key=lambda x: x.lower())
|
||||
return filenames
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
item = self.get_selected_item()
|
||||
self.ui.active_art.set_palette(item.palette, log=True)
|
||||
self.ui.popup.set_active_palette(item.palette)
|
||||
|
||||
|
||||
#
|
||||
# charset chooser
|
||||
#
|
||||
|
||||
|
||||
class CharsetChooserItem(BaseFileChooserItem):
|
||||
|
||||
def get_label(self):
|
||||
return os.path.splitext(self.name)[0]
|
||||
|
||||
|
||||
def get_description_lines(self):
|
||||
# first comment in file = description
|
||||
lines = []
|
||||
for line in open(self.charset.filename, encoding='utf-8').readlines():
|
||||
for line in open(self.charset.filename, encoding="utf-8").readlines():
|
||||
line = line.strip()
|
||||
if line.startswith('//'):
|
||||
if line.startswith("//"):
|
||||
lines.append(line[2:])
|
||||
break
|
||||
lines.append('Characters: %s' % str(self.charset.last_index))
|
||||
lines.append("Characters: %s" % str(self.charset.last_index))
|
||||
return lines
|
||||
|
||||
|
||||
def get_preview_texture(self, app):
|
||||
return self.charset.texture
|
||||
|
||||
|
||||
def load(self, app):
|
||||
self.charset = app.load_charset(self.name)
|
||||
|
||||
|
||||
class CharSetChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Choose character set'
|
||||
title = "Choose character set"
|
||||
flip_preview_y = False
|
||||
chooser_item_class = CharsetChooserItem
|
||||
|
||||
|
||||
def get_initial_selection(self):
|
||||
if not self.ui.active_art:
|
||||
return 0
|
||||
for item in self.items:
|
||||
if item.label == self.ui.active_art.charset.name:
|
||||
return item.index
|
||||
#print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
||||
# print("couldn't find initial selection for %s, returning 0" % self.__class__.__name__)
|
||||
return 0
|
||||
|
||||
|
||||
def get_filenames(self):
|
||||
filenames = []
|
||||
# search all files in dirs with appropriate extensions
|
||||
|
|
@ -394,7 +403,7 @@ class CharSetChooserDialog(BaseFileChooserDialog):
|
|||
filenames.append(filename)
|
||||
filenames.sort(key=lambda x: x.lower())
|
||||
return filenames
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
item = self.get_selected_item()
|
||||
self.ui.active_art.set_charset(item.charset, log=True)
|
||||
|
|
@ -405,11 +414,10 @@ class CharSetChooserDialog(BaseFileChooserDialog):
|
|||
|
||||
|
||||
class ArtScriptChooserItem(BaseFileChooserItem):
|
||||
|
||||
def get_label(self):
|
||||
label = os.path.splitext(self.name)[0]
|
||||
return os.path.basename(label)
|
||||
|
||||
|
||||
def get_description_lines(self):
|
||||
lines = []
|
||||
# read every comment line until a non-comment line is encountered
|
||||
|
|
@ -417,25 +425,24 @@ class ArtScriptChooserItem(BaseFileChooserItem):
|
|||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if not line.startswith('#'):
|
||||
if not line.startswith("#"):
|
||||
break
|
||||
# snip #
|
||||
line = line[line.index('#')+1:]
|
||||
line = line[line.index("#") + 1 :]
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def load(self, app):
|
||||
self.script = open(self.name)
|
||||
|
||||
|
||||
class RunArtScriptDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Run Artscript'
|
||||
title = "Run Artscript"
|
||||
tile_width, big_width = 70, 90
|
||||
tile_height, big_height = 15, 25
|
||||
chooser_item_class = ArtScriptChooserItem
|
||||
show_preview_image = False
|
||||
|
||||
|
||||
def get_filenames(self):
|
||||
filenames = []
|
||||
# search all files in dirs with appropriate extensions
|
||||
|
|
@ -445,7 +452,7 @@ class RunArtScriptDialog(BaseFileChooserDialog):
|
|||
filenames.append(dirname + filename)
|
||||
filenames.sort(key=lambda x: x.lower())
|
||||
return filenames
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
item = self.get_selected_item()
|
||||
self.ui.app.last_art_script = item.name
|
||||
|
|
@ -454,10 +461,9 @@ class RunArtScriptDialog(BaseFileChooserDialog):
|
|||
|
||||
|
||||
class OverlayImageFileChooserDialog(ImageFileChooserDialog):
|
||||
|
||||
title = 'Choose overlay image'
|
||||
confirm_caption = 'Choose'
|
||||
|
||||
title = "Choose overlay image"
|
||||
confirm_caption = "Choose"
|
||||
|
||||
def confirm_pressed(self):
|
||||
filename = self.field_texts[0]
|
||||
self.ui.app.set_overlay_image(filename)
|
||||
|
|
|
|||
|
|
@ -1,144 +1,146 @@
|
|||
|
||||
from ui_dialog import UIDialog, Field
|
||||
|
||||
from ui_console import SetGameDirCommand, LoadGameStateCommand, SaveGameStateCommand
|
||||
from ui_list_operations import LO_NONE, LO_SELECT_OBJECTS, LO_SET_SPAWN_CLASS, LO_LOAD_STATE, LO_SET_ROOM, LO_SET_ROOM_OBJECTS, LO_SET_OBJECT_ROOMS, LO_OPEN_GAME_DIR, LO_SET_ROOM_EDGE_WARP
|
||||
from ui_console import LoadGameStateCommand, SaveGameStateCommand
|
||||
from ui_dialog import Field, UIDialog
|
||||
from ui_list_operations import (
|
||||
LO_NONE,
|
||||
)
|
||||
|
||||
|
||||
class NewGameDirDialog(UIDialog):
|
||||
title = 'New game'
|
||||
field0_label = 'Name of new game folder:'
|
||||
field1_label = 'Name of new game:'
|
||||
title = "New game"
|
||||
field0_label = "Name of new game folder:"
|
||||
field1_label = "Name of new game:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False),
|
||||
Field(label=field1_label, type=str, width=field_width, oneline=False)
|
||||
Field(label=field1_label, type=str, width=field_width, oneline=False),
|
||||
]
|
||||
confirm_caption = 'Create'
|
||||
confirm_caption = "Create"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
# TODO: only allow names that don't already exist
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# provide a reasonable non-blank name
|
||||
if field_number == 0:
|
||||
return 'newgame'
|
||||
return "newgame"
|
||||
elif field_number == 1:
|
||||
return type(self.ui.app.gw).game_title
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
if self.ui.app.gw.create_new_game(self.field_texts[0], self.field_texts[1]):
|
||||
self.ui.app.enter_game_mode()
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class LoadGameStateDialog(UIDialog):
|
||||
|
||||
title = 'Open game state'
|
||||
field_label = 'Game state file to open:'
|
||||
title = "Open game state"
|
||||
field_label = "Game state file to open:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Open'
|
||||
fields = [Field(label=field_label, type=str, width=field_width, oneline=False)]
|
||||
confirm_caption = "Open"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
# TODO: only allow valid game state file in current game directory
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
LoadGameStateCommand.execute(self.ui.console, [self.field_texts[0]])
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class SaveGameStateDialog(UIDialog):
|
||||
|
||||
title = 'Save game state'
|
||||
field_label = 'New filename for game state:'
|
||||
title = "Save game state"
|
||||
field_label = "New filename for game state:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Save'
|
||||
fields = [Field(label=field_label, type=str, width=field_width, oneline=False)]
|
||||
confirm_caption = "Save"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
SaveGameStateCommand.execute(self.ui.console, [self.field_texts[0]])
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class AddRoomDialog(UIDialog):
|
||||
title = 'Add new room'
|
||||
field0_label = 'Name for new room:'
|
||||
field1_label = 'Class of new room:'
|
||||
title = "Add new room"
|
||||
field0_label = "Name for new room:"
|
||||
field1_label = "Class of new room:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False),
|
||||
Field(label=field1_label, type=str, width=field_width, oneline=False)
|
||||
Field(label=field1_label, type=str, width=field_width, oneline=False),
|
||||
]
|
||||
confirm_caption = 'Add'
|
||||
confirm_caption = "Add"
|
||||
game_mode_visible = True
|
||||
invalid_room_name_error = 'Invalid room name.'
|
||||
|
||||
invalid_room_name_error = "Invalid room name."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# provide a reasonable non-blank name
|
||||
if field_number == 0:
|
||||
return 'Room ' + str(len(self.ui.app.gw.rooms) + 1)
|
||||
return "Room " + str(len(self.ui.app.gw.rooms) + 1)
|
||||
elif field_number == 1:
|
||||
return 'GameRoom'
|
||||
|
||||
return "GameRoom"
|
||||
|
||||
def is_input_valid(self):
|
||||
return self.field_texts[0] != '', self.invalid_room_name_error
|
||||
|
||||
return self.field_texts[0] != "", self.invalid_room_name_error
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
self.ui.app.gw.add_room(self.field_texts[0], self.field_texts[1])
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class SetRoomCamDialog(UIDialog):
|
||||
title = 'Set room camera marker'
|
||||
title = "Set room camera marker"
|
||||
tile_width = 48
|
||||
field0_label = 'Name of location marker object for this room:'
|
||||
field0_label = "Name of location marker object for this room:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
fields = [Field(label=field0_label, type=str, width=field_width, oneline=False)]
|
||||
confirm_caption = "Set"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
def dismiss(self):
|
||||
self.ui.edit_list_panel.set_list_operation(LO_NONE)
|
||||
UIDialog.dismiss(self)
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
self.ui.app.gw.current_room.set_camera_marker_name(self.field_texts[0])
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class SetRoomEdgeWarpsDialog(UIDialog):
|
||||
title = 'Set room edge warps'
|
||||
title = "Set room edge warps"
|
||||
tile_width = 48
|
||||
fields = 4
|
||||
field0_label = 'Name of room/object to warp at LEFT edge:'
|
||||
field1_label = 'Name of room/object to warp at RIGHT edge:'
|
||||
field2_label = 'Name of room/object to warp at TOP edge:'
|
||||
field3_label = 'Name of room/object to warp at BOTTOM edge:'
|
||||
field0_label = "Name of room/object to warp at LEFT edge:"
|
||||
field1_label = "Name of room/object to warp at RIGHT edge:"
|
||||
field2_label = "Name of room/object to warp at TOP edge:"
|
||||
field3_label = "Name of room/object to warp at BOTTOM edge:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False),
|
||||
Field(label=field1_label, type=str, width=field_width, oneline=False),
|
||||
Field(label=field2_label, type=str, width=field_width, oneline=False),
|
||||
Field(label=field3_label, type=str, width=field_width, oneline=False)
|
||||
Field(label=field3_label, type=str, width=field_width, oneline=False),
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
confirm_caption = "Set"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
room = self.ui.app.gw.current_room
|
||||
names = {0: room.left_edge_warp_dest_name, 1: room.right_edge_warp_dest_name,
|
||||
2: room.top_edge_warp_dest_name, 3: room.bottom_edge_warp_dest_name}
|
||||
names = {
|
||||
0: room.left_edge_warp_dest_name,
|
||||
1: room.right_edge_warp_dest_name,
|
||||
2: room.top_edge_warp_dest_name,
|
||||
3: room.bottom_edge_warp_dest_name,
|
||||
}
|
||||
return names[field_number]
|
||||
|
||||
|
||||
def dismiss(self):
|
||||
self.ui.edit_list_panel.set_list_operation(LO_NONE)
|
||||
UIDialog.dismiss(self)
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
room = self.ui.app.gw.current_room
|
||||
room.left_edge_warp_dest_name = self.field_texts[0]
|
||||
|
|
@ -148,51 +150,50 @@ class SetRoomEdgeWarpsDialog(UIDialog):
|
|||
room.reset_edge_warps()
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class SetRoomBoundsObjDialog(UIDialog):
|
||||
title = 'Set room edge object'
|
||||
field0_label = 'Name of object to use for room bounds:'
|
||||
title = "Set room edge object"
|
||||
field0_label = "Name of object to use for room bounds:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
fields = [Field(label=field0_label, type=str, width=field_width, oneline=False)]
|
||||
confirm_caption = "Set"
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return self.ui.app.gw.current_room.warp_edge_bounds_obj_name
|
||||
|
||||
|
||||
def dismiss(self):
|
||||
self.ui.edit_list_panel.set_list_operation(LO_NONE)
|
||||
UIDialog.dismiss(self)
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
room = self.ui.app.gw.current_room
|
||||
room.warp_edge_bounds_obj_name = self.field_texts[0]
|
||||
room.reset_edge_warps()
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class RenameRoomDialog(UIDialog):
|
||||
title = 'Rename room'
|
||||
field0_label = 'New name for current room:'
|
||||
title = "Rename room"
|
||||
field0_label = "New name for current room:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Rename'
|
||||
fields = [Field(label=field0_label, type=str, width=field_width, oneline=False)]
|
||||
confirm_caption = "Rename"
|
||||
game_mode_visible = True
|
||||
invalid_room_name_error = 'Invalid room name.'
|
||||
|
||||
invalid_room_name_error = "Invalid room name."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return self.ui.app.gw.current_room.name
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
return self.field_texts[0] != '', self.invalid_room_name_error
|
||||
|
||||
return self.field_texts[0] != "", self.invalid_room_name_error
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
world = self.ui.app.gw
|
||||
world.rename_room(world.current_room, self.field_texts[0])
|
||||
self.dismiss()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
# coding=utf-8
|
||||
from ui_menu_pulldown_item import (
|
||||
FileQuitItem,
|
||||
PulldownMenuData,
|
||||
PulldownMenuItem,
|
||||
SeparatorItem,
|
||||
ViewSetZoomItem,
|
||||
ViewToggleCameraTiltItem,
|
||||
ViewToggleCRTItem,
|
||||
ViewToggleGridItem,
|
||||
)
|
||||
|
||||
from ui_menu_pulldown_item import PulldownMenuItem, SeparatorItem, PulldownMenuData, FileQuitItem, ViewToggleCRTItem, ViewToggleCameraTiltItem, ViewSetZoomItem, ViewToggleGridItem
|
||||
|
||||
class GameModePulldownMenuItem(PulldownMenuItem):
|
||||
# unless overridden, game mode items not allowed in art mode
|
||||
|
|
@ -11,300 +19,408 @@ class GameModePulldownMenuItem(PulldownMenuItem):
|
|||
# game menu
|
||||
#
|
||||
class HideEditUIItem(GameModePulldownMenuItem):
|
||||
label = 'Hide edit UI'
|
||||
command = 'toggle_game_edit_ui'
|
||||
label = "Hide edit UI"
|
||||
command = "toggle_game_edit_ui"
|
||||
close_on_select = True
|
||||
always_active = True
|
||||
|
||||
|
||||
class NewGameDirItem(GameModePulldownMenuItem):
|
||||
label = 'New game…'
|
||||
command = 'new_game_dir'
|
||||
label = "New game…"
|
||||
command = "new_game_dir"
|
||||
always_active = True
|
||||
|
||||
|
||||
class SetGameDirItem(GameModePulldownMenuItem):
|
||||
label = 'Open game…'
|
||||
command = 'set_game_dir'
|
||||
label = "Open game…"
|
||||
command = "set_game_dir"
|
||||
close_on_select = True
|
||||
always_active = True
|
||||
|
||||
|
||||
class PauseGameItem(GameModePulldownMenuItem):
|
||||
label = 'blah'
|
||||
command = 'toggle_anim_playback'
|
||||
label = "blah"
|
||||
command = "toggle_anim_playback"
|
||||
always_active = True
|
||||
|
||||
def get_label(app):
|
||||
return ['Pause game', 'Unpause game'][app.gw.paused]
|
||||
return ["Pause game", "Unpause game"][app.gw.paused]
|
||||
|
||||
|
||||
class OpenConsoleItem(GameModePulldownMenuItem):
|
||||
label = 'Open dev console'
|
||||
command = 'toggle_console'
|
||||
label = "Open dev console"
|
||||
command = "toggle_console"
|
||||
close_on_select = True
|
||||
always_active = True
|
||||
art_mode_allowed = True
|
||||
|
||||
|
||||
#
|
||||
# state menu
|
||||
#
|
||||
class ResetStateItem(GameModePulldownMenuItem):
|
||||
label = 'Reset to last state'
|
||||
command = 'reset_game'
|
||||
label = "Reset to last state"
|
||||
command = "reset_game"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class LoadStateItem(GameModePulldownMenuItem):
|
||||
label = 'Load state…'
|
||||
command = 'load_game_state'
|
||||
label = "Load state…"
|
||||
command = "load_game_state"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class SaveStateItem(GameModePulldownMenuItem):
|
||||
label = 'Save current state'
|
||||
command = 'save_current'
|
||||
label = "Save current state"
|
||||
command = "save_current"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class SaveNewStateItem(GameModePulldownMenuItem):
|
||||
label = 'Save new state…'
|
||||
command = 'save_game_state'
|
||||
label = "Save new state…"
|
||||
command = "save_game_state"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
#
|
||||
# view menu
|
||||
#
|
||||
class ObjectsToCameraItem(GameModePulldownMenuItem):
|
||||
label = 'Move selected object(s) to camera'
|
||||
command = 'objects_to_camera'
|
||||
label = "Move selected object(s) to camera"
|
||||
command = "objects_to_camera"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
class CameraToObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Move camera to selected object'
|
||||
command = 'camera_to_objects'
|
||||
label = "Move camera to selected object"
|
||||
command = "camera_to_objects"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) != 1
|
||||
|
||||
|
||||
class ToggleDebugObjectsItem(GameModePulldownMenuItem):
|
||||
label = ' Draw debug objects'
|
||||
command = 'toggle_debug_objects'
|
||||
label = " Draw debug objects"
|
||||
command = "toggle_debug_objects"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.properties and ui.app.gw.properties.draw_debug_objects
|
||||
|
||||
|
||||
class ToggleOriginVizItem(GameModePulldownMenuItem):
|
||||
label = ' Show all object origins'
|
||||
command = 'toggle_all_origin_viz'
|
||||
label = " Show all object origins"
|
||||
command = "toggle_all_origin_viz"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.show_origin_all
|
||||
|
||||
|
||||
class ToggleBoundsVizItem(GameModePulldownMenuItem):
|
||||
label = ' Show all object bounds'
|
||||
command = 'toggle_all_bounds_viz'
|
||||
label = " Show all object bounds"
|
||||
command = "toggle_all_bounds_viz"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.show_bounds_all
|
||||
|
||||
|
||||
class ToggleCollisionVizItem(GameModePulldownMenuItem):
|
||||
label = ' Show all object collision'
|
||||
command = 'toggle_all_collision_viz'
|
||||
label = " Show all object collision"
|
||||
command = "toggle_all_collision_viz"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.show_collision_all
|
||||
|
||||
|
||||
#
|
||||
# world menu
|
||||
#
|
||||
class EditWorldPropertiesItem(GameModePulldownMenuItem):
|
||||
label = 'Edit world properties…'
|
||||
command = 'edit_world_properties'
|
||||
label = "Edit world properties…"
|
||||
command = "edit_world_properties"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
#
|
||||
# room menu
|
||||
#
|
||||
|
||||
|
||||
class ChangeRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Change current room…'
|
||||
command = 'change_current_room'
|
||||
label = "Change current room…"
|
||||
command = "change_current_room"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.rooms) == 0
|
||||
|
||||
|
||||
class AddRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Add room…'
|
||||
command = 'add_room'
|
||||
label = "Add room…"
|
||||
command = "add_room"
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class SetRoomObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Add/remove objects from room…'
|
||||
command = 'set_room_objects'
|
||||
label = "Add/remove objects from room…"
|
||||
command = "set_room_objects"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class RemoveRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Remove this room'
|
||||
command = 'remove_current_room'
|
||||
label = "Remove this room"
|
||||
command = "remove_current_room"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class RenameRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Rename this room…'
|
||||
command = 'rename_current_room'
|
||||
label = "Rename this room…"
|
||||
command = "rename_current_room"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class ToggleAllRoomsVizItem(GameModePulldownMenuItem):
|
||||
label = 'blah'
|
||||
command = 'toggle_all_rooms_visible'
|
||||
label = "blah"
|
||||
command = "toggle_all_rooms_visible"
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.rooms) == 0
|
||||
|
||||
def get_label(app):
|
||||
return ['Show all rooms', 'Show only current room'][app.gw.show_all_rooms]
|
||||
return ["Show all rooms", "Show only current room"][app.gw.show_all_rooms]
|
||||
|
||||
|
||||
class ToggleListOnlyRoomObjectItem(GameModePulldownMenuItem):
|
||||
label = ' List only objects in this room'
|
||||
command = 'toggle_list_only_room_objects'
|
||||
label = " List only objects in this room"
|
||||
command = "toggle_list_only_room_objects"
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.rooms) == 0
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.list_only_current_room_objects
|
||||
|
||||
|
||||
class ToggleRoomCamerasItem(GameModePulldownMenuItem):
|
||||
label = ' Camera changes with room'
|
||||
command = 'toggle_room_camera_changes'
|
||||
label = " Camera changes with room"
|
||||
command = "toggle_room_camera_changes"
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.rooms) == 0
|
||||
|
||||
def should_mark(ui):
|
||||
return ui.app.gw.room_camera_changes_enabled
|
||||
|
||||
|
||||
class SetRoomCameraItem(GameModePulldownMenuItem):
|
||||
label = "Set this room's camera marker…"
|
||||
command = 'set_room_camera_marker'
|
||||
command = "set_room_camera_marker"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class SetRoomEdgeDestinationsItem(GameModePulldownMenuItem):
|
||||
label = "Set this room's edge warps…"
|
||||
command = 'set_room_edge_warps'
|
||||
command = "set_room_edge_warps"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class SetRoomBoundsObject(GameModePulldownMenuItem):
|
||||
label = "Set this room's edge object…"
|
||||
command = 'set_room_bounds_obj'
|
||||
command = "set_room_bounds_obj"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None
|
||||
|
||||
|
||||
class AddSelectedToCurrentRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Add selected objects to this room'
|
||||
command = 'add_selected_to_room'
|
||||
label = "Add selected objects to this room"
|
||||
command = "add_selected_to_room"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None or len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
class RemoveSelectedFromCurrentRoomItem(GameModePulldownMenuItem):
|
||||
label = 'Remove selected objects from this room'
|
||||
command = 'remove_selected_from_room'
|
||||
label = "Remove selected objects from this room"
|
||||
command = "remove_selected_from_room"
|
||||
|
||||
def should_dim(app):
|
||||
return app.gw.current_room is None or len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
#
|
||||
# object menu
|
||||
#
|
||||
|
||||
|
||||
class SpawnObjectItem(GameModePulldownMenuItem):
|
||||
label = 'Spawn object…'
|
||||
command = 'choose_spawn_object_class'
|
||||
label = "Spawn object…"
|
||||
command = "choose_spawn_object_class"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class DuplicateObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Duplicate selected objects'
|
||||
command = 'duplicate_selected_objects'
|
||||
label = "Duplicate selected objects"
|
||||
command = "duplicate_selected_objects"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
class SelectObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Select objects…'
|
||||
command = 'select_objects'
|
||||
label = "Select objects…"
|
||||
command = "select_objects"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return not app.gw.game_dir
|
||||
|
||||
|
||||
class EditArtForObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Edit art for selected…'
|
||||
command = 'edit_art_for_selected_objects'
|
||||
label = "Edit art for selected…"
|
||||
command = "edit_art_for_selected_objects"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
class SetObjectRoomsItem(GameModePulldownMenuItem):
|
||||
label = 'Add/remove this object from rooms…'
|
||||
command = 'set_object_rooms'
|
||||
label = "Add/remove this object from rooms…"
|
||||
command = "set_object_rooms"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) != 1
|
||||
|
||||
|
||||
class DeleteSelectedObjectsItem(GameModePulldownMenuItem):
|
||||
label = 'Delete selected object(s)'
|
||||
command = 'erase_selection_or_art'
|
||||
label = "Delete selected object(s)"
|
||||
command = "erase_selection_or_art"
|
||||
close_on_select = True
|
||||
|
||||
def should_dim(app):
|
||||
return len(app.gw.selected_objects) == 0
|
||||
|
||||
|
||||
class GameMenuData(PulldownMenuData):
|
||||
items = [HideEditUIItem, OpenConsoleItem, SeparatorItem,
|
||||
NewGameDirItem, SetGameDirItem, PauseGameItem, SeparatorItem,
|
||||
FileQuitItem]
|
||||
items = [
|
||||
HideEditUIItem,
|
||||
OpenConsoleItem,
|
||||
SeparatorItem,
|
||||
NewGameDirItem,
|
||||
SetGameDirItem,
|
||||
PauseGameItem,
|
||||
SeparatorItem,
|
||||
FileQuitItem,
|
||||
]
|
||||
|
||||
|
||||
class GameStateMenuData(PulldownMenuData):
|
||||
items = [ResetStateItem, LoadStateItem, SaveStateItem, SaveNewStateItem]
|
||||
|
||||
|
||||
class GameViewMenuData(PulldownMenuData):
|
||||
items = [ViewToggleCRTItem, ViewToggleGridItem, SeparatorItem,
|
||||
ViewSetZoomItem, ViewToggleCameraTiltItem, SeparatorItem,
|
||||
ObjectsToCameraItem, CameraToObjectsItem, ToggleDebugObjectsItem,
|
||||
ToggleOriginVizItem, ToggleBoundsVizItem, ToggleCollisionVizItem]
|
||||
|
||||
items = [
|
||||
ViewToggleCRTItem,
|
||||
ViewToggleGridItem,
|
||||
SeparatorItem,
|
||||
ViewSetZoomItem,
|
||||
ViewToggleCameraTiltItem,
|
||||
SeparatorItem,
|
||||
ObjectsToCameraItem,
|
||||
CameraToObjectsItem,
|
||||
ToggleDebugObjectsItem,
|
||||
ToggleOriginVizItem,
|
||||
ToggleBoundsVizItem,
|
||||
ToggleCollisionVizItem,
|
||||
]
|
||||
|
||||
def should_mark_item(item, ui):
|
||||
if hasattr(item, 'should_mark'):
|
||||
if hasattr(item, "should_mark"):
|
||||
return item.should_mark(ui)
|
||||
return False
|
||||
|
||||
|
||||
class GameWorldMenuData(PulldownMenuData):
|
||||
items = [EditWorldPropertiesItem]
|
||||
|
||||
|
||||
class GameRoomMenuData(PulldownMenuData):
|
||||
items = [ChangeRoomItem, AddRoomItem, RemoveRoomItem, RenameRoomItem,
|
||||
ToggleAllRoomsVizItem, ToggleListOnlyRoomObjectItem, ToggleRoomCamerasItem, SeparatorItem,
|
||||
AddSelectedToCurrentRoomItem, RemoveSelectedFromCurrentRoomItem,
|
||||
SetRoomObjectsItem, SeparatorItem,
|
||||
SetRoomCameraItem, SetRoomEdgeDestinationsItem, SetRoomBoundsObject,
|
||||
SeparatorItem
|
||||
items = [
|
||||
ChangeRoomItem,
|
||||
AddRoomItem,
|
||||
RemoveRoomItem,
|
||||
RenameRoomItem,
|
||||
ToggleAllRoomsVizItem,
|
||||
ToggleListOnlyRoomObjectItem,
|
||||
ToggleRoomCamerasItem,
|
||||
SeparatorItem,
|
||||
AddSelectedToCurrentRoomItem,
|
||||
RemoveSelectedFromCurrentRoomItem,
|
||||
SetRoomObjectsItem,
|
||||
SeparatorItem,
|
||||
SetRoomCameraItem,
|
||||
SetRoomEdgeDestinationsItem,
|
||||
SetRoomBoundsObject,
|
||||
SeparatorItem,
|
||||
]
|
||||
|
||||
def should_mark_item(item, ui):
|
||||
"show checkmark for current room"
|
||||
if not ui.app.gw.current_room:
|
||||
return False
|
||||
if hasattr(item, 'should_mark'):
|
||||
if hasattr(item, "should_mark"):
|
||||
return item.should_mark(ui)
|
||||
return ui.app.gw.current_room.name == item.cb_arg
|
||||
|
||||
|
||||
def get_items(app):
|
||||
items = []
|
||||
if len(app.gw.rooms) == 0:
|
||||
|
|
@ -320,18 +436,21 @@ class GameRoomMenuData(PulldownMenuData):
|
|||
if len(item.label) + 1 > longest_line:
|
||||
longest_line = len(item.label) + 1
|
||||
# cap at max allowed line length
|
||||
for room_name,room in app.gw.rooms.items():
|
||||
class TempMenuItemClass(GameModePulldownMenuItem): pass
|
||||
for room_name, room in app.gw.rooms.items():
|
||||
|
||||
class TempMenuItemClass(GameModePulldownMenuItem):
|
||||
pass
|
||||
|
||||
item = TempMenuItemClass
|
||||
# leave spaces for mark
|
||||
item.label = ' %s' % room_name
|
||||
item.label = " %s" % room_name
|
||||
# pad, put Z depth on far right
|
||||
item.label = item.label.ljust(longest_line)
|
||||
# trim to keep below a max length
|
||||
item.label = item.label[:longest_line]
|
||||
# tell PulldownMenu's button creation process not to auto-pad
|
||||
item.no_pad = True
|
||||
item.command = 'change_current_room_to'
|
||||
item.command = "change_current_room_to"
|
||||
item.cb_arg = room_name
|
||||
items.append(item)
|
||||
# sort room list alphabetically so it's stable, if arbitrary
|
||||
|
|
@ -340,6 +459,12 @@ class GameRoomMenuData(PulldownMenuData):
|
|||
|
||||
|
||||
class GameObjectMenuData(PulldownMenuData):
|
||||
items = [SpawnObjectItem, DuplicateObjectsItem, SeparatorItem,
|
||||
SelectObjectsItem, EditArtForObjectsItem, SetObjectRoomsItem,
|
||||
DeleteSelectedObjectsItem]
|
||||
items = [
|
||||
SpawnObjectItem,
|
||||
DuplicateObjectsItem,
|
||||
SeparatorItem,
|
||||
SelectObjectsItem,
|
||||
EditArtForObjectsItem,
|
||||
SetObjectRoomsItem,
|
||||
DeleteSelectedObjectsItem,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,65 +1,65 @@
|
|||
import sdl2
|
||||
|
||||
from ui_element import UIElement
|
||||
from ui_dialog import UIDialog
|
||||
from ui_element import UIElement
|
||||
|
||||
|
||||
class PagedInfoDialog(UIDialog):
|
||||
|
||||
"dialog that presents multiple pages of info w/ buttons to navigate next/last page"
|
||||
|
||||
title = 'Info'
|
||||
|
||||
title = "Info"
|
||||
# message = list of page strings, each can be triple-quoted / contain line breaks
|
||||
message = ['']
|
||||
message = [""]
|
||||
tile_width = 54
|
||||
confirm_caption = '>>'
|
||||
other_caption = '<<'
|
||||
cancel_caption = 'Done'
|
||||
confirm_caption = ">>"
|
||||
other_caption = "<<"
|
||||
cancel_caption = "Done"
|
||||
other_button_visible = True
|
||||
extra_lines = 1
|
||||
|
||||
|
||||
def __init__(self, ui, options):
|
||||
self.page = 0
|
||||
UIDialog.__init__(self, ui, options)
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def update(self):
|
||||
# disable prev/next buttons if we're at either end of the page list
|
||||
if self.page == 0:
|
||||
self.other_button.can_hover = False
|
||||
self.other_button.set_state('dimmed')
|
||||
self.other_button.set_state("dimmed")
|
||||
elif self.page == len(self.message) - 1:
|
||||
self.confirm_button.can_hover = False
|
||||
self.confirm_button.set_state('dimmed')
|
||||
self.confirm_button.set_state("dimmed")
|
||||
else:
|
||||
for button in [self.confirm_button, self.other_button]:
|
||||
button.can_hover = True
|
||||
button.dimmed = False
|
||||
if button.state != 'normal':
|
||||
button.set_state('normal')
|
||||
if button.state != "normal":
|
||||
button.set_state("normal")
|
||||
UIElement.update(self)
|
||||
|
||||
|
||||
def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
keystr = sdl2.SDL_GetKeyName(key).decode()
|
||||
if keystr == 'Left':
|
||||
if keystr == "Left":
|
||||
self.other_pressed()
|
||||
elif keystr == 'Right':
|
||||
elif keystr == "Right":
|
||||
self.confirm_pressed()
|
||||
elif keystr == 'Escape':
|
||||
elif keystr == "Escape":
|
||||
self.cancel_pressed()
|
||||
|
||||
|
||||
def get_message(self):
|
||||
return self.message[self.page].rstrip().split('\n')
|
||||
|
||||
return self.message[self.page].rstrip().split("\n")
|
||||
|
||||
def confirm_pressed(self):
|
||||
# confirm repurposed to "next page"
|
||||
if self.page < len(self.message) - 1:
|
||||
self.page += 1
|
||||
# redraw, tell reset_art not to resize
|
||||
self.reset_art(False)
|
||||
|
||||
|
||||
def cancel_pressed(self):
|
||||
self.dismiss()
|
||||
|
||||
|
||||
def other_pressed(self):
|
||||
# other repurposed to "previous page"
|
||||
if self.page > 0:
|
||||
|
|
@ -68,8 +68,8 @@ class PagedInfoDialog(UIDialog):
|
|||
|
||||
|
||||
about_message = [
|
||||
# max line width 50 characters!
|
||||
"""
|
||||
# max line width 50 characters!
|
||||
"""
|
||||
by JP LeBreton (c) 2014-2022 |
|
||||
|
||||
Playscii was made with the support of many nice
|
||||
|
|
@ -89,7 +89,7 @@ James Noble, David Pittman, Richard Porczak,
|
|||
Dan Sanderson, Shannon Strucci, Pablo López Soriano,
|
||||
Jack Turner, Chris Welch, Andrew Yoder
|
||||
""",
|
||||
"""
|
||||
"""
|
||||
Programming Contributions:
|
||||
|
||||
Mattias Gustavsson, Rohit Nirmal, Sean Gubelman,
|
||||
|
|
@ -108,7 +108,7 @@ Anna Anthropy, Andi McClure, Bret Victor,
|
|||
Tim Sweeney (ZZT), Craig Hickman (Kid Pix),
|
||||
Bill Atkinson (HyperCard)
|
||||
""",
|
||||
"""
|
||||
"""
|
||||
Love, Encouragement, Moral Support:
|
||||
|
||||
L Stiger
|
||||
|
|
@ -120,14 +120,16 @@ Aubrey Hesselgren
|
|||
Zak McClendon
|
||||
Claire Hosking
|
||||
#tool-design
|
||||
"""
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
class AboutDialog(PagedInfoDialog):
|
||||
title = 'Playscii'
|
||||
title = "Playscii"
|
||||
message = about_message
|
||||
game_mode_visible = True
|
||||
all_modes_visible = True
|
||||
|
||||
def __init__(self, ui, options):
|
||||
self.title += ' %s' % ui.app.version
|
||||
self.title += " %s" % ui.app.version
|
||||
PagedInfoDialog.__init__(self, ui, options)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# list operations - tells ListPanel what to do when clicked
|
||||
|
||||
LO_NONE = 0
|
||||
|
|
|
|||
203
ui_menu_bar.py
203
ui_menu_bar.py
|
|
@ -1,15 +1,33 @@
|
|||
from math import ceil
|
||||
|
||||
from ui_element import UIElement
|
||||
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
|
||||
from ui_menu_pulldown_item import FileMenuData, EditMenuData, ToolMenuData, ViewMenuData, ArtMenuData, FrameMenuData, LayerMenuData, CharColorMenuData, HelpMenuData
|
||||
from ui_game_menu_pulldown_item import GameMenuData, GameStateMenuData, GameViewMenuData, GameWorldMenuData, GameRoomMenuData, GameObjectMenuData
|
||||
from ui_info_dialog import AboutDialog
|
||||
from ui_colors import UIColors
|
||||
from renderable_sprite import UISpriteRenderable
|
||||
from ui_button import TEXT_CENTER, UIButton
|
||||
from ui_colors import UIColors
|
||||
from ui_element import UIElement
|
||||
from ui_game_menu_pulldown_item import (
|
||||
GameMenuData,
|
||||
GameObjectMenuData,
|
||||
GameRoomMenuData,
|
||||
GameStateMenuData,
|
||||
GameViewMenuData,
|
||||
GameWorldMenuData,
|
||||
)
|
||||
from ui_info_dialog import AboutDialog
|
||||
from ui_menu_pulldown_item import (
|
||||
ArtMenuData,
|
||||
CharColorMenuData,
|
||||
EditMenuData,
|
||||
FileMenuData,
|
||||
FrameMenuData,
|
||||
HelpMenuData,
|
||||
LayerMenuData,
|
||||
ToolMenuData,
|
||||
ViewMenuData,
|
||||
)
|
||||
|
||||
|
||||
class MenuButton(UIButton):
|
||||
caption = 'Base Class Menu Button'
|
||||
caption = "Base Class Menu Button"
|
||||
caption_justify = TEXT_CENTER
|
||||
# menu data is just a class w/ little more than a list of items, partly
|
||||
# so we don't have to list all the items here in a different module
|
||||
|
|
@ -18,11 +36,11 @@ class MenuButton(UIButton):
|
|||
normal_bg_color = UIColors.white
|
||||
hovered_bg_color = UIColors.lightgrey
|
||||
dimmed_bg_color = UIColors.lightgrey
|
||||
|
||||
|
||||
def __init__(self, element):
|
||||
UIButton.__init__(self, element)
|
||||
self.callback = self.open_pulldown
|
||||
|
||||
|
||||
def open_pulldown(self):
|
||||
# don't open menus if a dialog is up
|
||||
if self.element.ui.active_dialog:
|
||||
|
|
@ -44,118 +62,137 @@ class MenuButton(UIButton):
|
|||
|
||||
# playscii logo button = normal UIButton, opens About screen directly
|
||||
class PlaysciiMenuButton(UIButton):
|
||||
name = 'playscii'
|
||||
caption = ' '
|
||||
name = "playscii"
|
||||
caption = " "
|
||||
caption_justify = TEXT_CENTER
|
||||
width = len(caption) + 2
|
||||
normal_bg_color = MenuButton.normal_bg_color
|
||||
hovered_bg_color = MenuButton.hovered_bg_color
|
||||
dimmed_bg_color = MenuButton.dimmed_bg_color
|
||||
|
||||
|
||||
#
|
||||
# art mode menu buttons
|
||||
#
|
||||
|
||||
|
||||
class FileMenuButton(MenuButton):
|
||||
name = 'file'
|
||||
caption = 'File'
|
||||
name = "file"
|
||||
caption = "File"
|
||||
menu_data = FileMenuData
|
||||
|
||||
|
||||
class EditMenuButton(MenuButton):
|
||||
name = 'edit'
|
||||
caption = 'Edit'
|
||||
name = "edit"
|
||||
caption = "Edit"
|
||||
menu_data = EditMenuData
|
||||
|
||||
|
||||
class ToolMenuButton(MenuButton):
|
||||
name = 'tool'
|
||||
caption = 'Tool'
|
||||
name = "tool"
|
||||
caption = "Tool"
|
||||
menu_data = ToolMenuData
|
||||
|
||||
|
||||
class ViewMenuButton(MenuButton):
|
||||
name = 'view'
|
||||
caption = 'View'
|
||||
name = "view"
|
||||
caption = "View"
|
||||
menu_data = ViewMenuData
|
||||
|
||||
|
||||
class ArtMenuButton(MenuButton):
|
||||
name = 'art'
|
||||
caption = 'Art'
|
||||
name = "art"
|
||||
caption = "Art"
|
||||
menu_data = ArtMenuData
|
||||
|
||||
|
||||
class FrameMenuButton(MenuButton):
|
||||
name = 'frame'
|
||||
caption = 'Frame'
|
||||
name = "frame"
|
||||
caption = "Frame"
|
||||
menu_data = FrameMenuData
|
||||
|
||||
|
||||
class LayerMenuButton(MenuButton):
|
||||
name = 'layer'
|
||||
caption = 'Layer'
|
||||
name = "layer"
|
||||
caption = "Layer"
|
||||
menu_data = LayerMenuData
|
||||
|
||||
|
||||
class CharColorMenuButton(MenuButton):
|
||||
name = 'char_color'
|
||||
caption = 'Char/Color'
|
||||
name = "char_color"
|
||||
caption = "Char/Color"
|
||||
menu_data = CharColorMenuData
|
||||
|
||||
|
||||
# (appears in both art and game mode menus)
|
||||
class HelpMenuButton(MenuButton):
|
||||
name = 'help'
|
||||
caption = 'Help'
|
||||
name = "help"
|
||||
caption = "Help"
|
||||
menu_data = HelpMenuData
|
||||
|
||||
|
||||
#
|
||||
# game mode menu buttons
|
||||
#
|
||||
|
||||
|
||||
class GameMenuButton(MenuButton):
|
||||
name = 'game'
|
||||
caption = 'Game'
|
||||
name = "game"
|
||||
caption = "Game"
|
||||
menu_data = GameMenuData
|
||||
|
||||
|
||||
class StateMenuButton(MenuButton):
|
||||
name = 'state'
|
||||
caption = 'State'
|
||||
name = "state"
|
||||
caption = "State"
|
||||
menu_data = GameStateMenuData
|
||||
|
||||
|
||||
class GameViewMenuButton(MenuButton):
|
||||
name = 'view'
|
||||
caption = 'View'
|
||||
name = "view"
|
||||
caption = "View"
|
||||
menu_data = GameViewMenuData
|
||||
|
||||
|
||||
class WorldMenuButton(MenuButton):
|
||||
name = 'world'
|
||||
caption = 'World'
|
||||
name = "world"
|
||||
caption = "World"
|
||||
menu_data = GameWorldMenuData
|
||||
|
||||
|
||||
class RoomMenuButton(MenuButton):
|
||||
name = 'room'
|
||||
caption = 'Room'
|
||||
name = "room"
|
||||
caption = "Room"
|
||||
menu_data = GameRoomMenuData
|
||||
|
||||
|
||||
class ObjectMenuButton(MenuButton):
|
||||
name = 'object'
|
||||
caption = 'Object'
|
||||
name = "object"
|
||||
caption = "Object"
|
||||
menu_data = GameObjectMenuData
|
||||
|
||||
|
||||
class ModeMenuButton(UIButton):
|
||||
caption_justify = TEXT_CENTER
|
||||
normal_bg_color = UIColors.black
|
||||
normal_fg_color = UIColors.white
|
||||
#hovered_bg_color = UIColors.lightgrey
|
||||
#dimmed_bg_color = UIColors.lightgrey
|
||||
# hovered_bg_color = UIColors.lightgrey
|
||||
# dimmed_bg_color = UIColors.lightgrey
|
||||
|
||||
|
||||
class ArtModeMenuButton(ModeMenuButton):
|
||||
caption = 'Game Mode'
|
||||
caption = "Game Mode"
|
||||
width = len(caption) + 2
|
||||
|
||||
|
||||
class GameModeMenuButton(ModeMenuButton):
|
||||
caption = 'Art Mode'
|
||||
caption = "Art Mode"
|
||||
width = len(caption) + 2
|
||||
|
||||
|
||||
class MenuBar(UIElement):
|
||||
|
||||
"main menu bar element, has lots of buttons which control the pulldown"
|
||||
|
||||
|
||||
snap_top = True
|
||||
snap_left = True
|
||||
always_consume_input = True
|
||||
|
|
@ -165,7 +202,7 @@ class MenuBar(UIElement):
|
|||
mode_button_class = None
|
||||
# empty tiles between each button
|
||||
button_padding = 1
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
# bitmap icon for about menu button
|
||||
self.playscii_sprite = UISpriteRenderable(ui.app)
|
||||
|
|
@ -180,7 +217,7 @@ class MenuBar(UIElement):
|
|||
button.width = len(button.caption) + 2
|
||||
button.x = x
|
||||
x += button.width + self.button_padding
|
||||
setattr(self, '%s_button' % button.name, button)
|
||||
setattr(self, "%s_button" % button.name, button)
|
||||
# NOTE: callback already defined in MenuButton class,
|
||||
# menu data for pulldown with set in MenuButton subclass
|
||||
button.pulldown = self.ui.pulldown
|
||||
|
|
@ -197,22 +234,24 @@ class MenuBar(UIElement):
|
|||
if not self.mode_button_class:
|
||||
return
|
||||
self.mode_button = self.mode_button_class(self)
|
||||
self.mode_button.x = int(self.ui.width_tiles * self.ui.scale) - self.mode_button.width
|
||||
self.mode_button.x = (
|
||||
int(self.ui.width_tiles * self.ui.scale) - self.mode_button.width
|
||||
)
|
||||
self.mode_button.callback = self.toggle_game_mode
|
||||
self.buttons.append(self.mode_button)
|
||||
|
||||
|
||||
def reset_icon(self):
|
||||
inv_aspect = self.ui.app.window_height / self.ui.app.window_width
|
||||
self.playscii_sprite.scale_x = self.art.quad_height * inv_aspect
|
||||
self.playscii_sprite.scale_y = self.art.quad_height
|
||||
self.playscii_sprite.x = -1 + self.art.quad_width
|
||||
self.playscii_sprite.y = 1 - self.art.quad_height
|
||||
|
||||
|
||||
def open_about(self):
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
self.ui.open_dialog(AboutDialog)
|
||||
|
||||
|
||||
def toggle_game_mode(self):
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
|
|
@ -221,18 +260,18 @@ class MenuBar(UIElement):
|
|||
else:
|
||||
self.ui.app.exit_game_mode()
|
||||
self.ui.app.update_window_title()
|
||||
|
||||
|
||||
def close_active_menu(self):
|
||||
# un-dim active menu button
|
||||
for button in self.menu_buttons:
|
||||
if button.name == self.active_menu_name:
|
||||
button.dimmed = False
|
||||
button.set_state('normal')
|
||||
button.set_state("normal")
|
||||
self.active_menu_name = None
|
||||
self.ui.pulldown.visible = False
|
||||
self.ui.keyboard_focus_element = None
|
||||
self.ui.refocus_keyboard()
|
||||
|
||||
|
||||
def refresh_active_menu(self):
|
||||
if not self.ui.pulldown.visible:
|
||||
return
|
||||
|
|
@ -240,36 +279,36 @@ class MenuBar(UIElement):
|
|||
if button.name == self.active_menu_name:
|
||||
# don't reset keyboard nav index
|
||||
self.ui.pulldown.open_at(button, False)
|
||||
|
||||
|
||||
def open_menu_by_name(self, menu_name):
|
||||
if not self.ui.app.can_edit:
|
||||
return
|
||||
for button in self.menu_buttons:
|
||||
if button.name == menu_name:
|
||||
button.callback()
|
||||
|
||||
|
||||
def open_menu_by_index(self, index):
|
||||
if index > len(self.menu_buttons) - 1:
|
||||
return
|
||||
# don't navigate to the about menu
|
||||
# (does this mean it's not accessible via kb-only? probably, that's fine)
|
||||
if self.menu_buttons[index].name == 'playscii':
|
||||
if self.menu_buttons[index].name == "playscii":
|
||||
return
|
||||
self.menu_buttons[index].callback()
|
||||
|
||||
|
||||
def get_menu_index(self, menu_name):
|
||||
for i,button in enumerate(self.menu_buttons):
|
||||
for i, button in enumerate(self.menu_buttons):
|
||||
if button.name == self.active_menu_name:
|
||||
return i
|
||||
|
||||
|
||||
def next_menu(self):
|
||||
i = self.get_menu_index(self.active_menu_name)
|
||||
self.open_menu_by_index(i + 1)
|
||||
|
||||
|
||||
def previous_menu(self):
|
||||
i = self.get_menu_index(self.active_menu_name)
|
||||
self.open_menu_by_index(i - 1)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
self.tile_width = ceil(self.ui.width_tiles * self.ui.scale)
|
||||
# must resize here, as window width will vary
|
||||
|
|
@ -280,28 +319,46 @@ class MenuBar(UIElement):
|
|||
self.art.clear_frame_layer(0, 0, bg, fg)
|
||||
# reposition right-justified mode switch button
|
||||
if self.mode_button:
|
||||
self.mode_button.x = int(self.ui.width_tiles * self.ui.scale) - self.mode_button.width
|
||||
self.mode_button.x = (
|
||||
int(self.ui.width_tiles * self.ui.scale) - self.mode_button.width
|
||||
)
|
||||
# draw buttons, etc
|
||||
UIElement.reset_art(self)
|
||||
self.reset_icon()
|
||||
|
||||
|
||||
def render(self):
|
||||
UIElement.render(self)
|
||||
self.playscii_sprite.render()
|
||||
|
||||
|
||||
def destroy(self):
|
||||
UIElement.destroy(self)
|
||||
self.playscii_sprite.destroy()
|
||||
|
||||
|
||||
class ArtMenuBar(MenuBar):
|
||||
button_classes = [FileMenuButton, EditMenuButton, ToolMenuButton,
|
||||
ViewMenuButton, ArtMenuButton, FrameMenuButton,
|
||||
LayerMenuButton, CharColorMenuButton, HelpMenuButton]
|
||||
button_classes = [
|
||||
FileMenuButton,
|
||||
EditMenuButton,
|
||||
ToolMenuButton,
|
||||
ViewMenuButton,
|
||||
ArtMenuButton,
|
||||
FrameMenuButton,
|
||||
LayerMenuButton,
|
||||
CharColorMenuButton,
|
||||
HelpMenuButton,
|
||||
]
|
||||
mode_button_class = GameModeMenuButton
|
||||
|
||||
|
||||
class GameMenuBar(MenuBar):
|
||||
button_classes = [GameMenuButton, StateMenuButton, GameViewMenuButton,
|
||||
WorldMenuButton, RoomMenuButton, ObjectMenuButton,
|
||||
HelpMenuButton]
|
||||
button_classes = [
|
||||
GameMenuButton,
|
||||
StateMenuButton,
|
||||
GameViewMenuButton,
|
||||
WorldMenuButton,
|
||||
RoomMenuButton,
|
||||
ObjectMenuButton,
|
||||
HelpMenuButton,
|
||||
]
|
||||
game_mode_visible = True
|
||||
mode_button_class = ArtModeMenuButton
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
|
||||
from ui_element import UIElement
|
||||
from art import UV_FLIPX, UV_FLIPY, UV_ROTATE180
|
||||
from ui_button import UIButton
|
||||
from ui_colors import UIColors
|
||||
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY
|
||||
from ui_menu_pulldown_item import PulldownMenuItem, PulldownMenuData, SeparatorItem
|
||||
from ui_element import UIElement
|
||||
from ui_menu_pulldown_item import PulldownMenuData, PulldownMenuItem, SeparatorItem
|
||||
|
||||
|
||||
class MenuItemButton(UIButton):
|
||||
dimmed_fg_color = UIColors.medgrey
|
||||
dimmed_bg_color = UIColors.lightgrey
|
||||
|
||||
|
||||
def hover(self):
|
||||
UIButton.hover(self)
|
||||
# keyboard nav if hovering with mouse
|
||||
for i,button in enumerate(self.element.buttons):
|
||||
for i, button in enumerate(self.element.buttons):
|
||||
if button is self:
|
||||
self.element.keyboard_nav_index = i
|
||||
self.element.update_keyboard_hover()
|
||||
break
|
||||
|
||||
|
||||
def click(self):
|
||||
UIButton.click(self)
|
||||
if self.item.close_on_select:
|
||||
|
|
@ -26,9 +25,8 @@ class MenuItemButton(UIButton):
|
|||
|
||||
|
||||
class PulldownMenu(UIElement):
|
||||
|
||||
"element that's moved and resized based on currently active pulldown"
|
||||
|
||||
|
||||
label_shortcut_padding = 5
|
||||
visible = False
|
||||
always_consume_input = True
|
||||
|
|
@ -41,7 +39,7 @@ class PulldownMenu(UIElement):
|
|||
all_modes_visible = True
|
||||
support_keyboard_navigation = True
|
||||
keyboard_nav_left_right = True
|
||||
|
||||
|
||||
def open_at(self, menu_button, reset_keyboard_nav_index=True):
|
||||
# set X and Y based on calling menu button's location
|
||||
self.tile_x = menu_button.x
|
||||
|
|
@ -57,7 +55,7 @@ class PulldownMenu(UIElement):
|
|||
if menu_button.menu_data.get_items is not PulldownMenuData.get_items:
|
||||
items += menu_button.menu_data.get_items(self.ui.app)
|
||||
for item in items:
|
||||
shortcut,command = self.get_shortcut(item)
|
||||
shortcut, command = self.get_shortcut(item)
|
||||
shortcuts.append(shortcut)
|
||||
callbacks.append(command)
|
||||
# get label, static or dynamic
|
||||
|
|
@ -81,11 +79,18 @@ class PulldownMenu(UIElement):
|
|||
self.draw_border(menu_button)
|
||||
# create as many buttons as needed, set their sizes, captions, callbacks
|
||||
self.buttons = []
|
||||
for i,item in enumerate(items):
|
||||
for i, item in enumerate(items):
|
||||
# skip button creation for separators, just draw a line
|
||||
if item is SeparatorItem:
|
||||
for x in range(1, self.tile_width - 1):
|
||||
self.art.set_tile_at(0, 0, x, i+1, self.border_horizontal_line_char, self.border_color)
|
||||
self.art.set_tile_at(
|
||||
0,
|
||||
0,
|
||||
x,
|
||||
i + 1,
|
||||
self.border_horizontal_line_char,
|
||||
self.border_color,
|
||||
)
|
||||
continue
|
||||
button = MenuItemButton(self)
|
||||
# give button a handle to its item
|
||||
|
|
@ -99,30 +104,33 @@ class PulldownMenu(UIElement):
|
|||
button.caption = full_label
|
||||
button.width = len(full_label)
|
||||
button.x = 1
|
||||
button.y = i+1
|
||||
button.y = i + 1
|
||||
button.callback = callbacks[i]
|
||||
if item.cb_arg is not None:
|
||||
button.cb_arg = item.cb_arg
|
||||
# dim items that aren't applicable to current app state
|
||||
if not item.always_active and item.should_dim(self.ui.app):
|
||||
button.set_state('dimmed')
|
||||
button.set_state("dimmed")
|
||||
button.can_hover = False
|
||||
self.buttons.append(button)
|
||||
# set our X and Y, draw buttons, etc
|
||||
self.reset_loc()
|
||||
self.reset_art()
|
||||
# if this menu has special logic for marking items, use it
|
||||
if not menu_button.menu_data.should_mark_item is PulldownMenuData.should_mark_item:
|
||||
for i,item in enumerate(items):
|
||||
if (
|
||||
menu_button.menu_data.should_mark_item
|
||||
is not PulldownMenuData.should_mark_item
|
||||
):
|
||||
for i, item in enumerate(items):
|
||||
if menu_button.menu_data.should_mark_item(item, self.ui):
|
||||
self.art.set_char_index_at(0, 0, 1, i+1, self.mark_char)
|
||||
self.art.set_char_index_at(0, 0, 1, i + 1, self.mark_char)
|
||||
# reset keyboard nav state for popups
|
||||
if reset_keyboard_nav_index:
|
||||
self.keyboard_nav_index = 0
|
||||
self.keyboard_navigate(0, 0)
|
||||
self.visible = True
|
||||
self.ui.keyboard_focus_element = self
|
||||
|
||||
|
||||
def draw_border(self, menu_button):
|
||||
"draws a fancy lil frame around the pulldown's edge"
|
||||
fg = self.border_color
|
||||
|
|
@ -130,12 +138,12 @@ class PulldownMenu(UIElement):
|
|||
# top/bottom edges
|
||||
for x in range(self.tile_width):
|
||||
self.art.set_tile_at(0, 0, x, 0, char, fg)
|
||||
self.art.set_tile_at(0, 0, x, self.tile_height-1, char, fg)
|
||||
self.art.set_tile_at(0, 0, x, self.tile_height - 1, char, fg)
|
||||
# left/right edges
|
||||
char = self.border_vertical_line_char
|
||||
for y in range(self.tile_height):
|
||||
self.art.set_tile_at(0, 0, 0, y, char, fg)
|
||||
self.art.set_tile_at(0, 0, self.tile_width-1, y, char, fg)
|
||||
self.art.set_tile_at(0, 0, self.tile_width - 1, y, char, fg)
|
||||
# corners: bottom left, bottom right, top right
|
||||
char = self.border_corner_char
|
||||
x, y = 0, self.tile_height - 1
|
||||
|
|
@ -151,32 +159,33 @@ class PulldownMenu(UIElement):
|
|||
for x in range(1, len(menu_button.caption) + 2):
|
||||
self.art.set_tile_at(0, 0, x, 0, 0)
|
||||
self.art.set_tile_at(0, 0, x, 0, char, fg, None, UV_FLIPY)
|
||||
|
||||
|
||||
def get_shortcut(self, menu_item):
|
||||
# get InputLord's binding from given menu item's command name,
|
||||
# return concise string for bind and the actual function it runs.
|
||||
def null():
|
||||
pass
|
||||
|
||||
# special handling of SeparatorMenuItem, no command or label
|
||||
if menu_item is SeparatorItem:
|
||||
return '', null
|
||||
return "", null
|
||||
binds = self.ui.app.il.edit_binds
|
||||
for bind_tuple in binds:
|
||||
command_functions = binds[bind_tuple]
|
||||
for f in command_functions:
|
||||
if f.__name__ == 'BIND_%s' % menu_item.command:
|
||||
shortcut = ''
|
||||
if f.__name__ == "BIND_%s" % menu_item.command:
|
||||
shortcut = ""
|
||||
# shift, alt, ctrl
|
||||
if bind_tuple[1]:
|
||||
shortcut += 'Shift-'
|
||||
shortcut += "Shift-"
|
||||
if bind_tuple[2]:
|
||||
shortcut += 'Alt-'
|
||||
shortcut += "Alt-"
|
||||
if bind_tuple[3]:
|
||||
# TODO: cmd vs ctrl for mac vs non
|
||||
shortcut += 'C-'
|
||||
shortcut += "C-"
|
||||
# bind strings that start with _ will be disregarded
|
||||
if not (bind_tuple[0].startswith('_') and len(bind_tuple[0]) > 1):
|
||||
if not (bind_tuple[0].startswith("_") and len(bind_tuple[0]) > 1):
|
||||
shortcut += bind_tuple[0]
|
||||
return shortcut, f
|
||||
self.ui.app.log('Shortcut/command not found: %s' % menu_item.command)
|
||||
return '', null
|
||||
self.ui.app.log("Shortcut/command not found: %s" % menu_item.command)
|
||||
return "", null
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,40 +1,44 @@
|
|||
import os
|
||||
|
||||
from ui_button import UIButton, TEXT_RIGHT
|
||||
from ui_edit_panel import GamePanel
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_button import TEXT_RIGHT, UIButton
|
||||
from ui_colors import UIColors
|
||||
from ui_dialog import Field, UIDialog
|
||||
from ui_edit_panel import GamePanel
|
||||
|
||||
|
||||
class ResetObjectButton(UIButton):
|
||||
caption = 'Reset object properties'
|
||||
caption = "Reset object properties"
|
||||
caption_justify = TEXT_RIGHT
|
||||
|
||||
def selected(button):
|
||||
world = button.element.world
|
||||
world.reset_object_in_place(world.selected_objects[0])
|
||||
|
||||
|
||||
class EditObjectPropertyDialog(UIDialog):
|
||||
|
||||
"dialog invoked by panel property click, modified at runtime as needed"
|
||||
base_title = 'Set %s'
|
||||
field0_base_label = 'New %s for %s:'
|
||||
|
||||
base_title = "Set %s"
|
||||
field0_base_label = "New %s for %s:"
|
||||
field_width = UIDialog.default_field_width
|
||||
fields = [
|
||||
Field(label=field0_base_label, type=str, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
confirm_caption = "Set"
|
||||
center_in_window = False
|
||||
game_mode_visible = True
|
||||
|
||||
|
||||
def is_input_valid(self):
|
||||
try: self.fields[0].type(self.field_texts[0])
|
||||
except: return False, ''
|
||||
try:
|
||||
self.fields[0].type(self.field_texts[0])
|
||||
except:
|
||||
return False, ""
|
||||
return True, None
|
||||
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
# set property for selected object(s)
|
||||
new_value = self.fields[0].type(self.field_texts[0])
|
||||
for obj in self.ui.app.gw.selected_objects:
|
||||
|
|
@ -49,17 +53,18 @@ class EditObjectPropertyButton(UIButton):
|
|||
|
||||
|
||||
class PropertyItem:
|
||||
multi_value_text = '[various]'
|
||||
|
||||
multi_value_text = "[various]"
|
||||
|
||||
def __init__(self, prop_name):
|
||||
self.prop_name = prop_name
|
||||
# property value & type filled in after creation
|
||||
self.prop_value = None
|
||||
self.prop_type = None
|
||||
|
||||
def set_value(self, value):
|
||||
# convert value to a button-friendly string
|
||||
if type(value) is float:
|
||||
valstr = '%.3f' % value
|
||||
valstr = "%.3f" % value
|
||||
# non-fixed decimal version may be shorter, if so use it
|
||||
if len(str(value)) < len(valstr):
|
||||
valstr = str(value)
|
||||
|
|
@ -80,33 +85,36 @@ class PropertyItem:
|
|||
|
||||
|
||||
class EditObjectPanel(GamePanel):
|
||||
|
||||
"panel showing info for selected game object"
|
||||
|
||||
tile_width = 36
|
||||
tile_height = 36
|
||||
snap_right = True
|
||||
text_left = False
|
||||
base_button_classes = [ResetObjectButton]
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.base_buttons = []
|
||||
self.property_buttons = []
|
||||
GamePanel.__init__(self, ui)
|
||||
|
||||
|
||||
def create_buttons(self):
|
||||
# buttons for persistent unique commands, eg reset object
|
||||
for i,button_class in enumerate(self.base_button_classes):
|
||||
for i, button_class in enumerate(self.base_button_classes):
|
||||
button = button_class(self)
|
||||
button.caption += ' '
|
||||
button.caption += " "
|
||||
button.width = self.tile_width
|
||||
button.y = i + 1
|
||||
button.callback = button.selected
|
||||
if button.clear_before_caption_draw:
|
||||
button.refresh_caption()
|
||||
self.base_buttons.append(button)
|
||||
|
||||
def callback(item=None):
|
||||
if not item: return
|
||||
if not item:
|
||||
return
|
||||
self.clicked_item(item)
|
||||
|
||||
for y in range(self.tile_height - len(self.base_buttons) - 1):
|
||||
button = EditObjectPropertyButton(self)
|
||||
button.y = y + len(self.base_buttons) + 1
|
||||
|
|
@ -117,7 +125,7 @@ class EditObjectPanel(GamePanel):
|
|||
button.width = 10
|
||||
self.property_buttons.append(button)
|
||||
self.buttons = self.base_buttons[:] + self.property_buttons[:]
|
||||
|
||||
|
||||
def clicked_item(self, item):
|
||||
# if property is a bool just toggle/set it, no need for a dialog
|
||||
if item.prop_type is bool:
|
||||
|
|
@ -130,19 +138,28 @@ class EditObjectPanel(GamePanel):
|
|||
obj.set_object_property(item.prop_name, not val)
|
||||
return
|
||||
# set dialog values appropriate to property being edited
|
||||
EditObjectPropertyDialog.title = EditObjectPropertyDialog.base_title % item.prop_name
|
||||
|
||||
EditObjectPropertyDialog.title = (
|
||||
EditObjectPropertyDialog.base_title % item.prop_name
|
||||
)
|
||||
|
||||
# can't set named tuple values directly, build a new one and set it
|
||||
old_field = EditObjectPropertyDialog.fields[0]
|
||||
new_label = EditObjectPropertyDialog.field0_base_label % (item.prop_type.__name__, item.prop_name)
|
||||
new_label = EditObjectPropertyDialog.field0_base_label % (
|
||||
item.prop_type.__name__,
|
||||
item.prop_name,
|
||||
)
|
||||
new_type = item.prop_type
|
||||
# if None, assume string
|
||||
if item.prop_type is type(None):
|
||||
new_type = str
|
||||
new_field = Field(label=new_label, type=new_type, width=old_field.width,
|
||||
oneline=old_field.oneline)
|
||||
new_field = Field(
|
||||
label=new_label,
|
||||
type=new_type,
|
||||
width=old_field.width,
|
||||
oneline=old_field.oneline,
|
||||
)
|
||||
EditObjectPropertyDialog.fields[0] = new_field
|
||||
|
||||
|
||||
tile_x = int(self.ui.width_tiles * self.ui.scale) - self.tile_width
|
||||
tile_x -= EditObjectPropertyDialog.tile_width
|
||||
# give dialog a handle to item
|
||||
|
|
@ -151,18 +168,18 @@ class EditObjectPanel(GamePanel):
|
|||
self.ui.active_dialog.field_texts[0] = str(item.prop_value)
|
||||
self.ui.active_dialog.tile_x, self.ui.active_dialog.tile_y = tile_x, self.tile_y
|
||||
self.ui.active_dialog.reset_loc()
|
||||
|
||||
|
||||
def get_label(self):
|
||||
# if 1 object seleted, show its name; if >1 selected, show #
|
||||
selected = len(self.world.selected_objects)
|
||||
# panel shouldn't draw when nothing selected, fill in anyway
|
||||
if selected == 0:
|
||||
return '[nothing selected]'
|
||||
return "[nothing selected]"
|
||||
elif selected == 1 and self.world.selected_objects[0]:
|
||||
return self.world.selected_objects[0].name
|
||||
else:
|
||||
return '[%s selected]' % selected
|
||||
|
||||
return "[%s selected]" % selected
|
||||
|
||||
def refresh_items(self):
|
||||
if len(self.world.selected_objects) == 0:
|
||||
return
|
||||
|
|
@ -174,7 +191,7 @@ class EditObjectPanel(GamePanel):
|
|||
if obj is None:
|
||||
continue
|
||||
for propname in obj.serialized + obj.editable:
|
||||
if not propname in propnames:
|
||||
if propname not in propnames:
|
||||
propnames.append(propname)
|
||||
# build list of items from properties
|
||||
items = []
|
||||
|
|
@ -188,33 +205,33 @@ class EditObjectPanel(GamePanel):
|
|||
item.set_value(getattr(obj, propname))
|
||||
items.append(item)
|
||||
# set each line
|
||||
for i,b in enumerate(self.property_buttons):
|
||||
for i, b in enumerate(self.property_buttons):
|
||||
item = None
|
||||
if i < len(items):
|
||||
item = items[i]
|
||||
self.draw_property_line(b, i, item)
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def draw_property_line(self, button, button_index, item):
|
||||
"set button + label appearance correctly"
|
||||
y = button_index + len(self.base_buttons) + 1
|
||||
self.art.clear_line(0, 0, y, self.fg_color, self.bg_color)
|
||||
if item is None:
|
||||
button.caption = ''
|
||||
button.caption = ""
|
||||
button.cb_arg = None
|
||||
button.can_hover = False
|
||||
return
|
||||
# set button caption, width, x based on value
|
||||
button.caption = '%s ' % item.prop_value
|
||||
button.caption = "%s " % item.prop_value
|
||||
button.width = len(button.caption) + 1
|
||||
button.x = self.tile_width - button.width
|
||||
button.cb_arg = item
|
||||
button.can_hover = True
|
||||
# set non-button text to the left correctly
|
||||
x = button.x + 1
|
||||
label = '%s: ' % item.prop_name
|
||||
label = "%s: " % item.prop_name
|
||||
self.art.write_string(0, 0, x, y, label, UIColors.darkgrey, None, True)
|
||||
|
||||
|
||||
def update(self):
|
||||
# redraw contents every update
|
||||
if self.is_visible():
|
||||
|
|
@ -222,6 +239,6 @@ class EditObjectPanel(GamePanel):
|
|||
self.refresh_items()
|
||||
GamePanel.update(self)
|
||||
self.renderable.alpha = 1 if self is self.ui.keyboard_focus_element else 0.5
|
||||
|
||||
|
||||
def is_visible(self):
|
||||
return GamePanel.is_visible(self) and len(self.world.selected_objects) > 0
|
||||
|
|
|
|||
372
ui_popup.py
372
ui_popup.py
|
|
@ -1,103 +1,122 @@
|
|||
|
||||
from ui_element import UIElement, UIArt, UIRenderable
|
||||
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
|
||||
from ui_swatch import CharacterSetSwatch, PaletteSwatch, MIN_CHARSET_WIDTH
|
||||
from art import UV_FLIPX, UV_FLIPY, UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270
|
||||
from renderable_line import SwatchSelectionBoxRenderable
|
||||
from ui_button import TEXT_CENTER, UIButton
|
||||
from ui_colors import UIColors
|
||||
from ui_tool import FillTool, FILL_BOUND_CHAR, FILL_BOUND_FG_COLOR, FILL_BOUND_BG_COLOR
|
||||
from renderable_line import LineRenderable, SwatchSelectionBoxRenderable
|
||||
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY
|
||||
from ui_element import UIArt, UIElement
|
||||
from ui_file_chooser_dialog import CharSetChooserDialog, PaletteChooserDialog
|
||||
from ui_swatch import MIN_CHARSET_WIDTH, CharacterSetSwatch, PaletteSwatch
|
||||
from ui_tool import FILL_BOUND_BG_COLOR, FILL_BOUND_CHAR, FILL_BOUND_FG_COLOR, FillTool
|
||||
|
||||
TOOL_PANE_WIDTH = 10
|
||||
|
||||
|
||||
class ToolTabButton(UIButton):
|
||||
x, y = 0, 0
|
||||
caption_y = 1
|
||||
# width is set on the fly by popup size in reset_art
|
||||
height = 3
|
||||
caption_justify = TEXT_CENTER
|
||||
caption = 'Tools'
|
||||
caption = "Tools"
|
||||
|
||||
|
||||
class CharColorTabButton(UIButton):
|
||||
caption_y = 1
|
||||
height = ToolTabButton.height
|
||||
caption_justify = TEXT_CENTER
|
||||
caption = 'Chars/Colors'
|
||||
caption = "Chars/Colors"
|
||||
|
||||
|
||||
# charset view scale up/down buttons
|
||||
|
||||
|
||||
class CharSetScaleUpButton(UIButton):
|
||||
width, height = 3, 1
|
||||
x, y = -width, ToolTabButton.height + 1
|
||||
caption = '+'
|
||||
caption = "+"
|
||||
caption_justify = TEXT_CENTER
|
||||
|
||||
|
||||
class CharSetScaleDownButton(CharSetScaleUpButton):
|
||||
x = -CharSetScaleUpButton.width + CharSetScaleUpButton.x
|
||||
caption = '-'
|
||||
caption = "-"
|
||||
|
||||
|
||||
# charset flip / rotate buttons
|
||||
|
||||
|
||||
class CharXformButton(UIButton):
|
||||
hovered_fg_color = UIColors.white
|
||||
hovered_bg_color = UIColors.medgrey
|
||||
|
||||
|
||||
class CharFlipNoButton(CharXformButton):
|
||||
x = 3 + len('Flip:') + 1
|
||||
x = 3 + len("Flip:") + 1
|
||||
y = CharSetScaleUpButton.y + 1
|
||||
caption = 'None'
|
||||
caption = "None"
|
||||
width = len(caption) + 2
|
||||
caption_justify = TEXT_CENTER
|
||||
|
||||
|
||||
class CharFlipXButton(CharFlipNoButton):
|
||||
x = CharFlipNoButton.x + CharFlipNoButton.width + 1
|
||||
width = 3
|
||||
caption = 'X'
|
||||
caption = "X"
|
||||
|
||||
|
||||
class CharFlipYButton(CharFlipXButton):
|
||||
x = CharFlipXButton.x + CharFlipXButton.width + 1
|
||||
caption = 'Y'
|
||||
caption = "Y"
|
||||
|
||||
|
||||
class CharRot0Button(CharXformButton):
|
||||
x = 3 + len('Rotation:') + 1
|
||||
x = 3 + len("Rotation:") + 1
|
||||
y = CharFlipNoButton.y + 1
|
||||
width = 3
|
||||
caption = '0'
|
||||
caption = "0"
|
||||
caption_justify = TEXT_CENTER
|
||||
|
||||
|
||||
class CharRot90Button(CharRot0Button):
|
||||
x = CharRot0Button.x + CharRot0Button.width + 1
|
||||
width = 4
|
||||
caption = '90'
|
||||
caption = "90"
|
||||
|
||||
|
||||
class CharRot180Button(CharRot0Button):
|
||||
x = CharRot90Button.x + CharRot90Button.width + 1
|
||||
width = 5
|
||||
caption = '180'
|
||||
caption = "180"
|
||||
|
||||
|
||||
class CharRot270Button(CharRot0Button):
|
||||
x = CharRot180Button.x + CharRot180Button.width + 1
|
||||
width = 5
|
||||
caption = '270'
|
||||
caption = "270"
|
||||
|
||||
|
||||
# tool and tool settings buttons
|
||||
|
||||
|
||||
class ToolButton(UIButton):
|
||||
"a tool entry in the tool tab's left hand pane. populated from UI.tools"
|
||||
|
||||
width = TOOL_PANE_WIDTH
|
||||
caption = 'TOOLZ'
|
||||
caption = "TOOLZ"
|
||||
y = ToolTabButton.height + 2
|
||||
|
||||
|
||||
class BrushSizeUpButton(UIButton):
|
||||
width = 3
|
||||
y = ToolTabButton.height + 3
|
||||
caption = '+'
|
||||
caption = "+"
|
||||
caption_justify = TEXT_CENTER
|
||||
normal_fg_color = UIColors.white
|
||||
normal_bg_color = UIColors.medgrey
|
||||
|
||||
|
||||
class BrushSizeDownButton(BrushSizeUpButton):
|
||||
caption = '-'
|
||||
caption = "-"
|
||||
|
||||
|
||||
class AffectCharToggleButton(UIButton):
|
||||
width = 3
|
||||
|
|
@ -108,38 +127,46 @@ class AffectCharToggleButton(UIButton):
|
|||
normal_fg_color = UIColors.white
|
||||
normal_bg_color = UIColors.medgrey
|
||||
|
||||
|
||||
class AffectFgToggleButton(AffectCharToggleButton):
|
||||
y = AffectCharToggleButton.y + 1
|
||||
|
||||
|
||||
class AffectBgToggleButton(AffectCharToggleButton):
|
||||
y = AffectCharToggleButton.y + 2
|
||||
|
||||
|
||||
class AffectXformToggleButton(AffectCharToggleButton):
|
||||
y = AffectCharToggleButton.y + 3
|
||||
|
||||
|
||||
# fill boundary mode items
|
||||
class FillBoundaryModeCharButton(AffectCharToggleButton):
|
||||
y = AffectXformToggleButton.y + 3
|
||||
|
||||
|
||||
class FillBoundaryModeFGButton(AffectCharToggleButton):
|
||||
y = FillBoundaryModeCharButton.y + 1
|
||||
|
||||
|
||||
class FillBoundaryModeBGButton(AffectCharToggleButton):
|
||||
y = FillBoundaryModeCharButton.y + 2
|
||||
|
||||
|
||||
# charset / palette chooser buttons
|
||||
|
||||
|
||||
class CharSetChooserButton(UIButton):
|
||||
caption = 'Set:'
|
||||
caption = "Set:"
|
||||
x = 1
|
||||
normal_fg_color = UIColors.black
|
||||
normal_bg_color = UIColors.white
|
||||
hovered_fg_color = UIColors.white
|
||||
hovered_bg_color = UIColors.medgrey
|
||||
|
||||
|
||||
class PaletteChooserButton(CharSetChooserButton):
|
||||
caption = 'Palette:'
|
||||
caption = "Palette:"
|
||||
|
||||
|
||||
TAB_TOOLS = 0
|
||||
|
|
@ -147,7 +174,6 @@ TAB_CHAR_COLOR = 1
|
|||
|
||||
|
||||
class ToolPopup(UIElement):
|
||||
|
||||
visible = False
|
||||
# actual width will be based on character set + palette size and scale
|
||||
tile_width, tile_height = 20, 15
|
||||
|
|
@ -156,19 +182,19 @@ class ToolPopup(UIElement):
|
|||
fg_color = UIColors.black
|
||||
bg_color = UIColors.lightgrey
|
||||
highlight_color = UIColors.white
|
||||
tool_settings_label = 'Tool Settings:'
|
||||
brush_size_label = 'Brush size:'
|
||||
affects_heading_label = 'Affects:'
|
||||
affects_char_label = 'Character'
|
||||
affects_fg_label = 'Foreground Color'
|
||||
affects_bg_label = 'Background Color'
|
||||
affects_xform_label = 'Rotation/Flip'
|
||||
fill_boundary_modes_label = 'Fill boundary mode:'
|
||||
tool_settings_label = "Tool Settings:"
|
||||
brush_size_label = "Brush size:"
|
||||
affects_heading_label = "Affects:"
|
||||
affects_char_label = "Character"
|
||||
affects_fg_label = "Foreground Color"
|
||||
affects_bg_label = "Background Color"
|
||||
affects_xform_label = "Rotation/Flip"
|
||||
fill_boundary_modes_label = "Fill boundary mode:"
|
||||
fill_boundary_char_label = affects_char_label
|
||||
fill_boundary_fg_label = affects_fg_label
|
||||
fill_boundary_bg_label = affects_bg_label
|
||||
flip_label = 'Flip:'
|
||||
rotation_label = 'Rotation:'
|
||||
flip_label = "Flip:"
|
||||
rotation_label = "Rotation:"
|
||||
# index of check mark character in UI charset
|
||||
check_char_index = 131
|
||||
# index of off and on radio button characters in UI charset
|
||||
|
|
@ -176,36 +202,36 @@ class ToolPopup(UIElement):
|
|||
radio_char_1_index = 127
|
||||
# map classes to member names / callbacks
|
||||
button_names = {
|
||||
ToolTabButton: 'tool_tab',
|
||||
CharColorTabButton: 'char_color_tab',
|
||||
ToolTabButton: "tool_tab",
|
||||
CharColorTabButton: "char_color_tab",
|
||||
}
|
||||
char_color_tab_button_names = {
|
||||
CharSetScaleUpButton: 'scale_charset_up',
|
||||
CharSetScaleDownButton: 'scale_charset_down',
|
||||
CharSetChooserButton: 'choose_charset',
|
||||
CharFlipNoButton: 'xform_normal',
|
||||
CharFlipXButton: 'xform_flipX',
|
||||
CharFlipYButton: 'xform_flipY',
|
||||
CharRot0Button: 'xform_0',
|
||||
CharRot90Button: 'xform_90',
|
||||
CharRot180Button: 'xform_180',
|
||||
CharRot270Button: 'xform_270',
|
||||
PaletteChooserButton: 'choose_palette',
|
||||
CharSetScaleUpButton: "scale_charset_up",
|
||||
CharSetScaleDownButton: "scale_charset_down",
|
||||
CharSetChooserButton: "choose_charset",
|
||||
CharFlipNoButton: "xform_normal",
|
||||
CharFlipXButton: "xform_flipX",
|
||||
CharFlipYButton: "xform_flipY",
|
||||
CharRot0Button: "xform_0",
|
||||
CharRot90Button: "xform_90",
|
||||
CharRot180Button: "xform_180",
|
||||
CharRot270Button: "xform_270",
|
||||
PaletteChooserButton: "choose_palette",
|
||||
}
|
||||
tool_tab_button_names = {
|
||||
BrushSizeUpButton: 'brush_size_up',
|
||||
BrushSizeDownButton: 'brush_size_down',
|
||||
AffectCharToggleButton: 'toggle_affect_char',
|
||||
AffectFgToggleButton: 'toggle_affect_fg',
|
||||
AffectBgToggleButton: 'toggle_affect_bg',
|
||||
AffectXformToggleButton: 'toggle_affect_xform',
|
||||
BrushSizeUpButton: "brush_size_up",
|
||||
BrushSizeDownButton: "brush_size_down",
|
||||
AffectCharToggleButton: "toggle_affect_char",
|
||||
AffectFgToggleButton: "toggle_affect_fg",
|
||||
AffectBgToggleButton: "toggle_affect_bg",
|
||||
AffectXformToggleButton: "toggle_affect_xform",
|
||||
}
|
||||
fill_boundary_mode_button_names = {
|
||||
FillBoundaryModeCharButton: 'set_fill_boundary_char',
|
||||
FillBoundaryModeFGButton: 'set_fill_boundary_fg',
|
||||
FillBoundaryModeBGButton: 'set_fill_boundary_bg'
|
||||
FillBoundaryModeCharButton: "set_fill_boundary_char",
|
||||
FillBoundaryModeFGButton: "set_fill_boundary_fg",
|
||||
FillBoundaryModeBGButton: "set_fill_boundary_bg",
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.charset_swatch = CharacterSetSwatch(ui, self)
|
||||
|
|
@ -219,19 +245,26 @@ class ToolPopup(UIElement):
|
|||
# create buttons from button:name map, button & callback names generated
|
||||
# group these into lists that can be combined into self.buttons
|
||||
self.common_buttons = self.create_buttons_from_map(self.button_names)
|
||||
self.char_color_tab_buttons = self.create_buttons_from_map(self.char_color_tab_button_names)
|
||||
self.fill_boundary_mode_buttons = self.create_buttons_from_map(self.fill_boundary_mode_button_names)
|
||||
self.tool_tab_buttons = self.create_buttons_from_map(self.tool_tab_button_names) + self.fill_boundary_mode_buttons
|
||||
self.char_color_tab_buttons = self.create_buttons_from_map(
|
||||
self.char_color_tab_button_names
|
||||
)
|
||||
self.fill_boundary_mode_buttons = self.create_buttons_from_map(
|
||||
self.fill_boundary_mode_button_names
|
||||
)
|
||||
self.tool_tab_buttons = (
|
||||
self.create_buttons_from_map(self.tool_tab_button_names)
|
||||
+ self.fill_boundary_mode_buttons
|
||||
)
|
||||
# populate more tool tab buttons from UI's list of tools
|
||||
# similar to create_buttons_from_map, but class name isn't known
|
||||
# MAYBE-TODO: is there a way to unify this?
|
||||
for tool in self.ui.tools:
|
||||
tool_button = ToolButton(self)
|
||||
# caption: 1-space padding from left
|
||||
tool_button.caption = ' %s' % tool.button_caption
|
||||
tool_button_name = '%s_tool_button' % tool.name
|
||||
tool_button.caption = " %s" % tool.button_caption
|
||||
tool_button_name = "%s_tool_button" % tool.name
|
||||
setattr(self, tool_button_name, tool_button)
|
||||
cb_name = '%s_pressed' % tool_button_name
|
||||
cb_name = "%s_pressed" % tool_button_name
|
||||
tool_button.callback = getattr(self, cb_name)
|
||||
# set a special property UI can refer to
|
||||
tool_button.tool_name = tool.name
|
||||
|
|
@ -239,19 +272,21 @@ class ToolPopup(UIElement):
|
|||
UIElement.__init__(self, ui)
|
||||
# set initial tab state
|
||||
self.char_color_tab_button_pressed()
|
||||
self.xform_0_button.normal_bg_color = self.xform_normal_button.normal_bg_color = self.highlight_color
|
||||
|
||||
self.xform_0_button.normal_bg_color = (
|
||||
self.xform_normal_button.normal_bg_color
|
||||
) = self.highlight_color
|
||||
|
||||
def create_buttons_from_map(self, button_dict):
|
||||
buttons = []
|
||||
for button_class in button_dict:
|
||||
button = button_class(self)
|
||||
button_name = '%s_button' % button_dict[button_class]
|
||||
button_name = "%s_button" % button_dict[button_class]
|
||||
setattr(self, button_name, button)
|
||||
cb_name = '%s_pressed' % button_name
|
||||
cb_name = "%s_pressed" % button_name
|
||||
button.callback = getattr(self, cb_name)
|
||||
buttons.append(button)
|
||||
return buttons
|
||||
|
||||
|
||||
def tool_tab_button_pressed(self):
|
||||
self.active_tab = TAB_TOOLS
|
||||
self.char_color_tab_button.can_hover = True
|
||||
|
|
@ -261,7 +296,7 @@ class ToolPopup(UIElement):
|
|||
self.buttons = self.common_buttons + self.tool_tab_buttons
|
||||
self.draw_tool_tab()
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def char_color_tab_button_pressed(self):
|
||||
self.active_tab = TAB_CHAR_COLOR
|
||||
self.tool_tab_button.can_hover = True
|
||||
|
|
@ -271,79 +306,79 @@ class ToolPopup(UIElement):
|
|||
self.buttons = self.common_buttons + self.char_color_tab_buttons
|
||||
self.draw_char_color_tab()
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def scale_charset_up_button_pressed(self):
|
||||
self.charset_swatch.increase_scale()
|
||||
self.reset_art()
|
||||
self.charset_swatch.reset_loc()
|
||||
self.palette_swatch.reset_loc()
|
||||
|
||||
|
||||
def scale_charset_down_button_pressed(self):
|
||||
self.charset_swatch.decrease_scale()
|
||||
self.reset_art()
|
||||
self.charset_swatch.reset_loc()
|
||||
self.palette_swatch.reset_loc()
|
||||
|
||||
|
||||
def brush_size_up_button_pressed(self):
|
||||
# any changes to tool's setting will force redraw of settings tab
|
||||
self.ui.selected_tool.increase_brush_size()
|
||||
|
||||
|
||||
def brush_size_down_button_pressed(self):
|
||||
self.ui.selected_tool.decrease_brush_size()
|
||||
|
||||
|
||||
def toggle_affect_char_button_pressed(self):
|
||||
self.ui.selected_tool.toggle_affects_char()
|
||||
|
||||
|
||||
def toggle_affect_fg_button_pressed(self):
|
||||
self.ui.selected_tool.toggle_affects_fg()
|
||||
|
||||
|
||||
def toggle_affect_bg_button_pressed(self):
|
||||
self.ui.selected_tool.toggle_affects_bg()
|
||||
|
||||
|
||||
def toggle_affect_xform_button_pressed(self):
|
||||
self.ui.selected_tool.toggle_affects_xform()
|
||||
|
||||
|
||||
def set_fill_boundary_char_button_pressed(self):
|
||||
self.ui.fill_tool.boundary_mode = FILL_BOUND_CHAR
|
||||
self.ui.tool_settings_changed = True
|
||||
|
||||
|
||||
def set_fill_boundary_fg_button_pressed(self):
|
||||
self.ui.fill_tool.boundary_mode = FILL_BOUND_FG_COLOR
|
||||
self.ui.tool_settings_changed = True
|
||||
|
||||
|
||||
def set_fill_boundary_bg_button_pressed(self):
|
||||
self.ui.fill_tool.boundary_mode = FILL_BOUND_BG_COLOR
|
||||
self.ui.tool_settings_changed = True
|
||||
|
||||
|
||||
def pencil_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.pencil_tool)
|
||||
|
||||
|
||||
def erase_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.erase_tool)
|
||||
|
||||
|
||||
def grab_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.grab_tool)
|
||||
|
||||
|
||||
def rotate_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.rotate_tool)
|
||||
|
||||
|
||||
def text_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.text_tool)
|
||||
|
||||
|
||||
def select_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.select_tool)
|
||||
|
||||
|
||||
def paste_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.paste_tool)
|
||||
|
||||
|
||||
def fill_tool_button_pressed(self):
|
||||
self.ui.set_selected_tool(self.ui.fill_tool)
|
||||
|
||||
|
||||
def set_xform(self, new_xform):
|
||||
"tells UI elements to respect new xform"
|
||||
self.charset_swatch.set_xform(new_xform)
|
||||
self.update_xform_buttons()
|
||||
|
||||
|
||||
def update_xform_buttons(self):
|
||||
# light up button for current selected option
|
||||
button_map = {
|
||||
|
|
@ -352,7 +387,7 @@ class ToolPopup(UIElement):
|
|||
UV_ROTATE180: self.xform_180_button,
|
||||
UV_ROTATE270: self.xform_270_button,
|
||||
UV_FLIPX: self.xform_flipX_button,
|
||||
UV_FLIPY: self.xform_flipY_button
|
||||
UV_FLIPY: self.xform_flipY_button,
|
||||
}
|
||||
for b in button_map:
|
||||
if b == self.ui.selected_xform:
|
||||
|
|
@ -361,50 +396,56 @@ class ToolPopup(UIElement):
|
|||
button_map[b].normal_bg_color = self.bg_color
|
||||
self.xform_0_button.normal_bg_color = self.xform_normal_button.normal_bg_color
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def xform_normal_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_NORMAL)
|
||||
|
||||
|
||||
def xform_flipX_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_FLIPX)
|
||||
|
||||
|
||||
def xform_flipY_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_FLIPY)
|
||||
|
||||
|
||||
def xform_0_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_NORMAL)
|
||||
|
||||
|
||||
def xform_90_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_ROTATE90)
|
||||
|
||||
|
||||
def xform_180_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_ROTATE180)
|
||||
|
||||
|
||||
def xform_270_button_pressed(self):
|
||||
self.ui.set_selected_xform(UV_ROTATE270)
|
||||
|
||||
|
||||
def choose_charset_button_pressed(self):
|
||||
self.hide()
|
||||
self.ui.open_dialog(CharSetChooserDialog)
|
||||
|
||||
|
||||
def choose_palette_button_pressed(self):
|
||||
self.hide()
|
||||
self.ui.open_dialog(PaletteChooserDialog)
|
||||
|
||||
|
||||
def draw_char_color_tab(self):
|
||||
"draw non-button bits of this tab"
|
||||
# charset renderable location will be set in update()
|
||||
charset = self.ui.active_art.charset
|
||||
palette = self.ui.active_art.palette
|
||||
cqw, cqh = self.charset_swatch.art.quad_width, self.charset_swatch.art.quad_height
|
||||
cqw, cqh = (
|
||||
self.charset_swatch.art.quad_width,
|
||||
self.charset_swatch.art.quad_height,
|
||||
)
|
||||
self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
|
||||
# position & caption charset button
|
||||
y = self.tab_height + 1
|
||||
self.choose_charset_button.y = y
|
||||
self.choose_charset_button.caption = ' %s %s ' % (CharSetChooserButton.caption, charset.name)
|
||||
self.choose_charset_button.caption = " %s %s " % (
|
||||
CharSetChooserButton.caption,
|
||||
charset.name,
|
||||
)
|
||||
self.choose_charset_button.width = len(self.choose_charset_button.caption)
|
||||
# charset scale
|
||||
charset_scale = '%.2fx' % self.charset_swatch.char_scale
|
||||
charset_scale = "%.2fx" % self.charset_swatch.char_scale
|
||||
x = -self.scale_charset_up_button.width * 2
|
||||
self.art.write_string(0, 0, x, y, charset_scale, None, None, True)
|
||||
# transform labels and buttons, eg
|
||||
|
|
@ -419,14 +460,17 @@ class ToolPopup(UIElement):
|
|||
pal_caption_y = (cqh * charset.map_height) / self.art.quad_height
|
||||
pal_caption_y += self.tab_height + 5
|
||||
self.choose_palette_button.y = int(pal_caption_y)
|
||||
self.choose_palette_button.caption = ' %s %s ' % (PaletteChooserButton.caption, palette.name)
|
||||
self.choose_palette_button.caption = " %s %s " % (
|
||||
PaletteChooserButton.caption,
|
||||
palette.name,
|
||||
)
|
||||
self.choose_palette_button.width = len(self.choose_palette_button.caption)
|
||||
# set button states so captions draw properly
|
||||
tab_width = int(self.tile_width / 2)
|
||||
self.tool_tab_button.width = tab_width
|
||||
self.char_color_tab_button.width = int(self.tile_width) - tab_width
|
||||
self.char_color_tab_button.x = tab_width
|
||||
|
||||
|
||||
def draw_tool_tab(self):
|
||||
self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
|
||||
# fill tool bar with dimmer color, highlight selected tool
|
||||
|
|
@ -435,7 +479,7 @@ class ToolPopup(UIElement):
|
|||
self.art.set_color_at(0, 0, x, y, self.ui.colors.medgrey, False)
|
||||
# set selected tool BG lighter
|
||||
y = self.tab_height + 1
|
||||
for i,tool in enumerate(self.ui.tools):
|
||||
for i, tool in enumerate(self.ui.tools):
|
||||
tool_button = None
|
||||
for button in self.tool_tab_buttons:
|
||||
try:
|
||||
|
|
@ -443,7 +487,7 @@ class ToolPopup(UIElement):
|
|||
tool_button = button
|
||||
except:
|
||||
pass
|
||||
tool_button.y = y+i
|
||||
tool_button.y = y + i
|
||||
if tool == self.ui.selected_tool:
|
||||
tool_button.normal_bg_color = self.ui.colors.lightgrey
|
||||
else:
|
||||
|
|
@ -451,7 +495,10 @@ class ToolPopup(UIElement):
|
|||
# draw current tool settings
|
||||
x = TOOL_PANE_WIDTH + 1
|
||||
y = self.tab_height + 1
|
||||
label = '%s %s' % (self.ui.selected_tool.button_caption, self.tool_settings_label)
|
||||
label = "%s %s" % (
|
||||
self.ui.selected_tool.button_caption,
|
||||
self.tool_settings_label,
|
||||
)
|
||||
self.art.write_string(0, 0, x, y, label)
|
||||
x += 1
|
||||
y += 2
|
||||
|
|
@ -462,8 +509,8 @@ class ToolPopup(UIElement):
|
|||
label = self.brush_size_label
|
||||
# calculate X of + and - buttons based on size string
|
||||
self.brush_size_down_button.x = TOOL_PANE_WIDTH + len(label) + 2
|
||||
label += ' ' * (self.brush_size_down_button.width + 1)
|
||||
label += '%s' % self.ui.selected_tool.brush_size
|
||||
label += " " * (self.brush_size_down_button.width + 1)
|
||||
label += "%s" % self.ui.selected_tool.brush_size
|
||||
self.brush_size_up_button.x = TOOL_PANE_WIDTH + len(label) + 3
|
||||
self.art.write_string(0, 0, x, y, label)
|
||||
else:
|
||||
|
|
@ -479,19 +526,29 @@ class ToolPopup(UIElement):
|
|||
y += 2
|
||||
self.art.write_string(0, 0, x, y, self.affects_heading_label)
|
||||
y += 1
|
||||
|
||||
# set affects-* button labels AND captions
|
||||
def get_affects_char(affects):
|
||||
return [0, self.check_char_index][affects]
|
||||
|
||||
w = self.toggle_affect_char_button.width
|
||||
label_toggle_pairs = []
|
||||
label_toggle_pairs += [(self.affects_char_label, self.ui.selected_tool.affects_char)]
|
||||
label_toggle_pairs += [(self.affects_fg_label, self.ui.selected_tool.affects_fg_color)]
|
||||
label_toggle_pairs += [(self.affects_bg_label, self.ui.selected_tool.affects_bg_color)]
|
||||
label_toggle_pairs += [(self.affects_xform_label, self.ui.selected_tool.affects_xform)]
|
||||
for label,toggle in label_toggle_pairs:
|
||||
self.art.write_string(0, 0, x+w+1, y, '%s' % label)
|
||||
#self.art.set_tile_at(0, 0, x, y, get_affects_char(toggle), 4, 2)
|
||||
self.art.set_char_index_at(0, 0, x+1, y, get_affects_char(toggle))
|
||||
label_toggle_pairs += [
|
||||
(self.affects_char_label, self.ui.selected_tool.affects_char)
|
||||
]
|
||||
label_toggle_pairs += [
|
||||
(self.affects_fg_label, self.ui.selected_tool.affects_fg_color)
|
||||
]
|
||||
label_toggle_pairs += [
|
||||
(self.affects_bg_label, self.ui.selected_tool.affects_bg_color)
|
||||
]
|
||||
label_toggle_pairs += [
|
||||
(self.affects_xform_label, self.ui.selected_tool.affects_xform)
|
||||
]
|
||||
for label, toggle in label_toggle_pairs:
|
||||
self.art.write_string(0, 0, x + w + 1, y, "%s" % label)
|
||||
# self.art.set_tile_at(0, 0, x, y, get_affects_char(toggle), 4, 2)
|
||||
self.art.set_char_index_at(0, 0, x + 1, y, get_affects_char(toggle))
|
||||
y += 1
|
||||
else:
|
||||
self.toggle_affect_char_button.visible = False
|
||||
|
|
@ -504,21 +561,27 @@ class ToolPopup(UIElement):
|
|||
self.art.write_string(0, 0, x, y, self.fill_boundary_modes_label)
|
||||
y += 1
|
||||
# boundary mode buttons + labels
|
||||
#x +=
|
||||
labels = [self.fill_boundary_char_label,
|
||||
self.fill_boundary_fg_label,
|
||||
self.fill_boundary_bg_label]
|
||||
for i,button in enumerate(self.fill_boundary_mode_buttons):
|
||||
# x +=
|
||||
labels = [
|
||||
self.fill_boundary_char_label,
|
||||
self.fill_boundary_fg_label,
|
||||
self.fill_boundary_bg_label,
|
||||
]
|
||||
for i, button in enumerate(self.fill_boundary_mode_buttons):
|
||||
button.visible = True
|
||||
char = [self.radio_char_0_index, self.radio_char_1_index][i == self.ui.fill_tool.boundary_mode]
|
||||
#self.ui.app.log(char)
|
||||
self.art.set_char_index_at(0, 0, x+1, y, char)
|
||||
self.art.write_string(0, 0, x + FillBoundaryModeCharButton.width + 1, y, labels[i])
|
||||
char = [self.radio_char_0_index, self.radio_char_1_index][
|
||||
i == self.ui.fill_tool.boundary_mode
|
||||
]
|
||||
# self.ui.app.log(char)
|
||||
self.art.set_char_index_at(0, 0, x + 1, y, char)
|
||||
self.art.write_string(
|
||||
0, 0, x + FillBoundaryModeCharButton.width + 1, y, labels[i]
|
||||
)
|
||||
y += 1
|
||||
else:
|
||||
for button in self.fill_boundary_mode_buttons:
|
||||
button.visible = False
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
|
|
@ -527,7 +590,10 @@ class ToolPopup(UIElement):
|
|||
# set panel size based on charset size
|
||||
margin = self.swatch_margin * 2
|
||||
charset = self.ui.active_art.charset
|
||||
cqw, cqh = self.charset_swatch.art.quad_width, self.charset_swatch.art.quad_height
|
||||
cqw, cqh = (
|
||||
self.charset_swatch.art.quad_width,
|
||||
self.charset_swatch.art.quad_height,
|
||||
)
|
||||
old_width, old_height = self.tile_width, self.tile_height
|
||||
# min width in case of tiny charsets
|
||||
charset_tile_width = max(charset.map_width, MIN_CHARSET_WIDTH)
|
||||
|
|
@ -537,7 +603,10 @@ class ToolPopup(UIElement):
|
|||
# account for popup info lines etc: charset name + palette name + 1 padding each
|
||||
extra_lines = 7
|
||||
# account for size of palette + bottom margin
|
||||
palette_height = ((self.palette_swatch.art.height * self.palette_swatch.art.quad_height) + self.swatch_margin) / UIArt.quad_height
|
||||
palette_height = (
|
||||
(self.palette_swatch.art.height * self.palette_swatch.art.quad_height)
|
||||
+ self.swatch_margin
|
||||
) / UIArt.quad_height
|
||||
self.tile_height += self.tab_height + palette_height + extra_lines
|
||||
if old_width != self.tile_width or old_height != self.tile_height:
|
||||
self.art.resize(int(self.tile_width), int(self.tile_height))
|
||||
|
|
@ -549,7 +618,7 @@ class ToolPopup(UIElement):
|
|||
self.update_xform_buttons()
|
||||
# draw button captions
|
||||
UIElement.reset_art(self)
|
||||
|
||||
|
||||
def show(self):
|
||||
# if already visible, bail - key repeat probably triggered this
|
||||
if self.visible:
|
||||
|
|
@ -564,19 +633,22 @@ class ToolPopup(UIElement):
|
|||
if self.ui.pulldown.visible:
|
||||
self.ui.menu_bar.close_active_menu()
|
||||
self.reset_loc()
|
||||
|
||||
|
||||
def toggle(self):
|
||||
if self.visible:
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
x, y = self.ui.get_screen_coords(self.ui.app.mouse_x, self.ui.app.mouse_y)
|
||||
# center on mouse
|
||||
w, h = self.tile_width * self.art.quad_width, self.tile_height * self.art.quad_height
|
||||
w, h = (
|
||||
self.tile_width * self.art.quad_width,
|
||||
self.tile_height * self.art.quad_height,
|
||||
)
|
||||
x -= w / 2
|
||||
y += h / 2
|
||||
# clamp to edges of screen
|
||||
|
|
@ -586,12 +658,12 @@ class ToolPopup(UIElement):
|
|||
self.renderable.x, self.renderable.y = self.x, self.y
|
||||
self.charset_swatch.reset_loc()
|
||||
self.palette_swatch.reset_loc()
|
||||
|
||||
|
||||
def hide(self):
|
||||
self.visible = False
|
||||
self.ui.keyboard_focus_element = None
|
||||
self.ui.refocus_keyboard()
|
||||
|
||||
|
||||
def set_active_charset(self, new_charset):
|
||||
self.charset_swatch.art.charset = new_charset
|
||||
self.palette_swatch.art.charset = new_charset
|
||||
|
|
@ -602,7 +674,7 @@ class ToolPopup(UIElement):
|
|||
# charset width drives palette swatch width
|
||||
self.palette_swatch.reset()
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def set_active_palette(self, new_palette):
|
||||
self.charset_swatch.art.palette = new_palette
|
||||
self.palette_swatch.art.palette = new_palette
|
||||
|
|
@ -612,7 +684,7 @@ class ToolPopup(UIElement):
|
|||
self.ui.status_bar.set_active_palette(new_palette)
|
||||
self.palette_swatch.reset()
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def update(self):
|
||||
UIElement.update(self)
|
||||
if not self.ui.active_art:
|
||||
|
|
@ -624,7 +696,9 @@ class ToolPopup(UIElement):
|
|||
self.cursor_box.visible = True
|
||||
elif mouse_moved and self in self.ui.hovered_elements:
|
||||
self.cursor_box.visible = False
|
||||
x, y = self.ui.get_screen_coords(self.ui.app.mouse_x, self.ui.app.mouse_y)
|
||||
x, y = self.ui.get_screen_coords(
|
||||
self.ui.app.mouse_x, self.ui.app.mouse_y
|
||||
)
|
||||
for e in [self.charset_swatch, self.palette_swatch]:
|
||||
if e.is_inside(x, y):
|
||||
self.cursor_box.visible = True
|
||||
|
|
@ -636,23 +710,25 @@ class ToolPopup(UIElement):
|
|||
elif self.active_tab == TAB_TOOLS and self.ui.tool_settings_changed:
|
||||
self.draw_tool_tab()
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def keyboard_navigate(self, dx, dy):
|
||||
active_swatch = self.charset_swatch if self.cursor_char != -1 else self.palette_swatch
|
||||
active_swatch = (
|
||||
self.charset_swatch if self.cursor_char != -1 else self.palette_swatch
|
||||
)
|
||||
# TODO: can't handle cross-swatch navigation properly, restrict to chars
|
||||
active_swatch = self.charset_swatch
|
||||
# reverse up/down direction
|
||||
active_swatch.move_cursor(self.cursor_box, dx, -dy)
|
||||
|
||||
|
||||
def keyboard_select_item(self):
|
||||
# called as ui.keyboard_focus_element
|
||||
# simulate left/right click in popup to select stuff
|
||||
self.select_key_pressed(self.ui.app.il.shift_pressed)
|
||||
|
||||
|
||||
def select_key_pressed(self, mod_pressed):
|
||||
mouse_button = [1, 3][mod_pressed]
|
||||
self.clicked(mouse_button)
|
||||
|
||||
|
||||
def clicked(self, mouse_button):
|
||||
handled = UIElement.clicked(self, mouse_button)
|
||||
if handled:
|
||||
|
|
@ -669,7 +745,7 @@ class ToolPopup(UIElement):
|
|||
elif mouse_button == 3:
|
||||
self.ui.selected_bg_color = self.cursor_color
|
||||
return True
|
||||
|
||||
|
||||
def render(self):
|
||||
if not self.visible:
|
||||
return
|
||||
|
|
|
|||
314
ui_status_bar.py
314
ui_status_bar.py
|
|
@ -1,77 +1,106 @@
|
|||
import os.path, time
|
||||
import os.path
|
||||
import time
|
||||
from math import ceil
|
||||
|
||||
from ui_element import UIElement, UIArt, UIRenderable
|
||||
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
|
||||
from ui_colors import UIColors
|
||||
from renderable_line import UIRenderableX
|
||||
from art import uv_names
|
||||
from renderable_line import UIRenderableX
|
||||
from ui_button import TEXT_CENTER, TEXT_RIGHT, UIButton
|
||||
from ui_colors import UIColors
|
||||
from ui_element import UIArt, UIElement, UIRenderable
|
||||
|
||||
# buttons to toggle "affects" status / cycle through choices, respectively
|
||||
|
||||
|
||||
class StatusBarToggleButton(UIButton):
|
||||
caption_justify = TEXT_RIGHT
|
||||
|
||||
|
||||
class StatusBarCycleButton(UIButton):
|
||||
# do different stuff for left vs right click
|
||||
pass_mouse_button = True
|
||||
should_draw_caption = False
|
||||
width = 3
|
||||
|
||||
|
||||
class CharToggleButton(StatusBarToggleButton):
|
||||
x = 0
|
||||
caption = 'ch:'
|
||||
caption = "ch:"
|
||||
width = len(caption) + 1
|
||||
tooltip_on_hover = True
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return 'character index: %s' % self.element.ui.selected_char
|
||||
return "character index: %s" % self.element.ui.selected_char
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return 1, self.element.get_tile_y() - 1
|
||||
|
||||
|
||||
class CharCycleButton(StatusBarCycleButton):
|
||||
x = CharToggleButton.width
|
||||
tooltip_on_hover = True
|
||||
|
||||
# reuse above
|
||||
def get_tooltip_text(self): return CharToggleButton.get_tooltip_text(self)
|
||||
def get_tooltip_location(self): return CharToggleButton.get_tooltip_location(self)
|
||||
def get_tooltip_text(self):
|
||||
return CharToggleButton.get_tooltip_text(self)
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return CharToggleButton.get_tooltip_location(self)
|
||||
|
||||
|
||||
class FGToggleButton(StatusBarToggleButton):
|
||||
x = CharCycleButton.x + CharCycleButton.width
|
||||
caption = 'fg:'
|
||||
caption = "fg:"
|
||||
width = len(caption) + 1
|
||||
tooltip_on_hover = True
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return 'foreground color index: %s' % self.element.ui.selected_fg_color
|
||||
return "foreground color index: %s" % self.element.ui.selected_fg_color
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return 8, self.element.get_tile_y() - 1
|
||||
|
||||
|
||||
class FGCycleButton(StatusBarCycleButton):
|
||||
x = FGToggleButton.x + FGToggleButton.width
|
||||
tooltip_on_hover = True
|
||||
def get_tooltip_text(self): return FGToggleButton.get_tooltip_text(self)
|
||||
def get_tooltip_location(self): return FGToggleButton.get_tooltip_location(self)
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return FGToggleButton.get_tooltip_text(self)
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return FGToggleButton.get_tooltip_location(self)
|
||||
|
||||
|
||||
class BGToggleButton(StatusBarToggleButton):
|
||||
x = FGCycleButton.x + FGCycleButton.width
|
||||
caption = 'bg:'
|
||||
caption = "bg:"
|
||||
width = len(caption) + 1
|
||||
tooltip_on_hover = True
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return 'background color index: %s' % self.element.ui.selected_bg_color
|
||||
return "background color index: %s" % self.element.ui.selected_bg_color
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return 15, self.element.get_tile_y() - 1
|
||||
|
||||
|
||||
class BGCycleButton(StatusBarCycleButton):
|
||||
x = BGToggleButton.x + BGToggleButton.width
|
||||
tooltip_on_hover = True
|
||||
def get_tooltip_text(self): return BGToggleButton.get_tooltip_text(self)
|
||||
def get_tooltip_location(self): return BGToggleButton.get_tooltip_location(self)
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return BGToggleButton.get_tooltip_text(self)
|
||||
|
||||
def get_tooltip_location(self):
|
||||
return BGToggleButton.get_tooltip_location(self)
|
||||
|
||||
|
||||
class XformToggleButton(StatusBarToggleButton):
|
||||
x = BGCycleButton.x + BGCycleButton.width
|
||||
caption = 'xform:'
|
||||
caption = "xform:"
|
||||
width = len(caption) + 1
|
||||
|
||||
|
||||
# class for things like xform and tool whose captions you can cycle through
|
||||
class StatusBarTextCycleButton(StatusBarCycleButton):
|
||||
should_draw_caption = True
|
||||
|
|
@ -83,32 +112,38 @@ class StatusBarTextCycleButton(StatusBarCycleButton):
|
|||
clicked_fg_color = UIColors.black
|
||||
clicked_bg_color = UIColors.white
|
||||
|
||||
|
||||
class XformCycleButton(StatusBarTextCycleButton):
|
||||
x = XformToggleButton.x + XformToggleButton.width
|
||||
width = len('Rotate 180')
|
||||
width = len("Rotate 180")
|
||||
caption = uv_names[0]
|
||||
|
||||
|
||||
class ToolCycleButton(StatusBarTextCycleButton):
|
||||
x = XformCycleButton.x + XformCycleButton.width + len('tool:') + 1
|
||||
x = XformCycleButton.x + XformCycleButton.width + len("tool:") + 1
|
||||
# width and caption are set during status bar init after button is created
|
||||
|
||||
|
||||
class FileCycleButton(StatusBarTextCycleButton):
|
||||
caption = '[nothing]'
|
||||
caption = "[nothing]"
|
||||
|
||||
|
||||
class LayerCycleButton(StatusBarTextCycleButton):
|
||||
caption = 'X/Y'
|
||||
caption = "X/Y"
|
||||
width = len(caption)
|
||||
|
||||
|
||||
class FrameCycleButton(StatusBarTextCycleButton):
|
||||
caption = 'X/Y'
|
||||
caption = "X/Y"
|
||||
width = len(caption)
|
||||
|
||||
|
||||
class ZoomSetButton(StatusBarTextCycleButton):
|
||||
caption = '100.0'
|
||||
caption = "100.0"
|
||||
width = len(caption)
|
||||
|
||||
|
||||
class StatusBarUI(UIElement):
|
||||
|
||||
snap_bottom = True
|
||||
snap_left = True
|
||||
always_consume_input = True
|
||||
|
|
@ -117,47 +152,71 @@ class StatusBarUI(UIElement):
|
|||
char_swatch_x = CharCycleButton.x
|
||||
fg_swatch_x = FGCycleButton.x
|
||||
bg_swatch_x = BGCycleButton.x
|
||||
tool_label = 'tool:'
|
||||
tool_label = "tool:"
|
||||
tool_label_x = XformCycleButton.x + XformCycleButton.width + 1
|
||||
tile_label = 'tile:'
|
||||
layer_label = 'layer:'
|
||||
frame_label = 'frame:'
|
||||
zoom_label = '%'
|
||||
right_items_width = len(tile_label) + len(layer_label) + len(frame_label) + (len('X/Y') + 2) * 2 + len('XX/YY') + 2 + len(zoom_label) + 10
|
||||
tile_label = "tile:"
|
||||
layer_label = "layer:"
|
||||
frame_label = "frame:"
|
||||
zoom_label = "%"
|
||||
right_items_width = (
|
||||
len(tile_label)
|
||||
+ len(layer_label)
|
||||
+ len(frame_label)
|
||||
+ (len("X/Y") + 2) * 2
|
||||
+ len("XX/YY")
|
||||
+ 2
|
||||
+ len(zoom_label)
|
||||
+ 10
|
||||
)
|
||||
button_names = {
|
||||
CharToggleButton: 'char_toggle',
|
||||
CharCycleButton: 'char_cycle',
|
||||
FGToggleButton: 'fg_toggle',
|
||||
FGCycleButton: 'fg_cycle',
|
||||
BGToggleButton: 'bg_toggle',
|
||||
BGCycleButton: 'bg_cycle',
|
||||
XformToggleButton: 'xform_toggle',
|
||||
XformCycleButton: 'xform_cycle',
|
||||
ToolCycleButton: 'tool_cycle',
|
||||
FileCycleButton: 'file_cycle',
|
||||
LayerCycleButton: 'layer_cycle',
|
||||
FrameCycleButton: 'frame_cycle',
|
||||
ZoomSetButton: 'zoom_set'
|
||||
CharToggleButton: "char_toggle",
|
||||
CharCycleButton: "char_cycle",
|
||||
FGToggleButton: "fg_toggle",
|
||||
FGCycleButton: "fg_cycle",
|
||||
BGToggleButton: "bg_toggle",
|
||||
BGCycleButton: "bg_cycle",
|
||||
XformToggleButton: "xform_toggle",
|
||||
XformCycleButton: "xform_cycle",
|
||||
ToolCycleButton: "tool_cycle",
|
||||
FileCycleButton: "file_cycle",
|
||||
LayerCycleButton: "layer_cycle",
|
||||
FrameCycleButton: "frame_cycle",
|
||||
ZoomSetButton: "zoom_set",
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
art = ui.active_art
|
||||
self.ui = ui
|
||||
# create 3 custom Arts w/ source charset and palette, renderables for each
|
||||
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
self.char_art = UIArt(art_name, ui.app, art.charset, art.palette, self.swatch_width, 1)
|
||||
art_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
self.char_art = UIArt(
|
||||
art_name, ui.app, art.charset, art.palette, self.swatch_width, 1
|
||||
)
|
||||
self.char_renderable = UIRenderable(ui.app, self.char_art)
|
||||
self.fg_art = UIArt(art_name, ui.app, art.charset, art.palette, self.swatch_width, 1)
|
||||
self.fg_art = UIArt(
|
||||
art_name, ui.app, art.charset, art.palette, self.swatch_width, 1
|
||||
)
|
||||
self.fg_renderable = UIRenderable(ui.app, self.fg_art)
|
||||
self.bg_art = UIArt(art_name, ui.app, art.charset, art.palette, self.swatch_width, 1)
|
||||
self.bg_art = UIArt(
|
||||
art_name, ui.app, art.charset, art.palette, self.swatch_width, 1
|
||||
)
|
||||
self.bg_renderable = UIRenderable(ui.app, self.bg_art)
|
||||
# "dimmed out" box
|
||||
self.dim_art = UIArt(art_name, ui.app, ui.charset, ui.palette, self.swatch_width + self.char_swatch_x, 1)
|
||||
self.dim_art = UIArt(
|
||||
art_name,
|
||||
ui.app,
|
||||
ui.charset,
|
||||
ui.palette,
|
||||
self.swatch_width + self.char_swatch_x,
|
||||
1,
|
||||
)
|
||||
self.dim_renderable = UIRenderable(ui.app, self.dim_art)
|
||||
self.dim_renderable.alpha = 0.75
|
||||
# separate dimmed out box for xform, easier this way
|
||||
xform_width = XformToggleButton.width + XformCycleButton.width
|
||||
self.dim_xform_art = UIArt(art_name, ui.app, ui.charset, ui.palette, xform_width, 1)
|
||||
self.dim_xform_art = UIArt(
|
||||
art_name, ui.app, ui.charset, ui.palette, xform_width, 1
|
||||
)
|
||||
self.dim_xform_renderable = UIRenderable(ui.app, self.dim_xform_art)
|
||||
self.dim_xform_renderable.alpha = 0.75
|
||||
# create clickable buttons
|
||||
|
|
@ -165,19 +224,26 @@ class StatusBarUI(UIElement):
|
|||
self.button_map = {}
|
||||
for button_class, button_name in self.button_names.items():
|
||||
button = button_class(self)
|
||||
setattr(self, button_name + '_button', button)
|
||||
cb_name = '%s_button_pressed' % button_name
|
||||
setattr(self, button_name + "_button", button)
|
||||
cb_name = "%s_button_pressed" % button_name
|
||||
button.callback = getattr(self, cb_name)
|
||||
self.buttons.append(button)
|
||||
# keep a mapping of button names to buttons, for eg tooltip updates
|
||||
self.button_map[button_name] = button
|
||||
# some button captions, widths, locations will be set in reset_art
|
||||
# determine total width of left-justified items
|
||||
self.left_items_width = self.tool_cycle_button.x + self.tool_cycle_button.width + 15
|
||||
self.left_items_width = (
|
||||
self.tool_cycle_button.x + self.tool_cycle_button.width + 15
|
||||
)
|
||||
# set some properties in bulk
|
||||
self.renderables = []
|
||||
for r in [self.char_renderable, self.fg_renderable, self.bg_renderable,
|
||||
self.dim_renderable, self.dim_xform_renderable]:
|
||||
for r in [
|
||||
self.char_renderable,
|
||||
self.fg_renderable,
|
||||
self.bg_renderable,
|
||||
self.dim_renderable,
|
||||
self.dim_xform_renderable,
|
||||
]:
|
||||
r.ui = ui
|
||||
r.grain_strength = 0
|
||||
# add to list of renderables to manage eg destroyed on quit
|
||||
|
|
@ -188,95 +254,112 @@ class StatusBarUI(UIElement):
|
|||
self.x_renderable.status_bar = self
|
||||
self.renderables.append(self.x_renderable)
|
||||
UIElement.__init__(self, ui)
|
||||
|
||||
|
||||
# button callbacks
|
||||
|
||||
|
||||
def char_toggle_button_pressed(self):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
self.ui.selected_tool.toggle_affects_char()
|
||||
|
||||
|
||||
def char_cycle_button_pressed(self, mouse_button):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.select_char(self.ui.selected_char + 1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.select_char(self.ui.selected_char - 1)
|
||||
|
||||
|
||||
def fg_toggle_button_pressed(self):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
self.ui.selected_tool.toggle_affects_fg()
|
||||
|
||||
|
||||
def fg_cycle_button_pressed(self, mouse_button):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.select_fg(self.ui.selected_fg_color + 1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.select_fg(self.ui.selected_fg_color - 1)
|
||||
|
||||
|
||||
def bg_toggle_button_pressed(self):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
self.ui.selected_tool.toggle_affects_bg()
|
||||
|
||||
|
||||
def bg_cycle_button_pressed(self, mouse_button):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.select_bg(self.ui.selected_bg_color + 1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.select_bg(self.ui.selected_bg_color - 1)
|
||||
|
||||
|
||||
def xform_toggle_button_pressed(self):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
self.ui.selected_tool.toggle_affects_xform()
|
||||
|
||||
|
||||
def xform_cycle_button_pressed(self, mouse_button):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.cycle_selected_xform()
|
||||
elif mouse_button == 3:
|
||||
self.ui.cycle_selected_xform(True)
|
||||
# update caption with new xform
|
||||
self.xform_cycle_button.caption = uv_names[self.ui.selected_xform]
|
||||
|
||||
|
||||
def tool_cycle_button_pressed(self, mouse_button):
|
||||
if self.ui.active_dialog: return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.cycle_selected_tool()
|
||||
elif mouse_button == 3:
|
||||
self.ui.cycle_selected_tool(True)
|
||||
self.tool_cycle_button.caption = self.ui.selected_tool.get_button_caption()
|
||||
|
||||
|
||||
def file_cycle_button_pressed(self, mouse_button):
|
||||
if not self.ui.active_art: return
|
||||
if self.ui.active_dialog: return
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.next_active_art()
|
||||
elif mouse_button == 3:
|
||||
self.ui.previous_active_art()
|
||||
|
||||
|
||||
def layer_cycle_button_pressed(self, mouse_button):
|
||||
if not self.ui.active_art: return
|
||||
if self.ui.active_dialog: return
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.set_active_layer(self.ui.active_art.active_layer + 1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.set_active_layer(self.ui.active_art.active_layer - 1)
|
||||
|
||||
|
||||
def frame_cycle_button_pressed(self, mouse_button):
|
||||
if not self.ui.active_art: return
|
||||
if self.ui.active_dialog: return
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.set_active_frame(self.ui.active_art.active_frame + 1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.set_active_frame(self.ui.active_art.active_frame - 1)
|
||||
|
||||
|
||||
def zoom_set_button_pressed(self, mouse_button):
|
||||
if not self.ui.active_art: return
|
||||
if self.ui.active_dialog: return
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
if self.ui.active_dialog:
|
||||
return
|
||||
if mouse_button == 1:
|
||||
self.ui.app.camera.zoom_proportional(1)
|
||||
elif mouse_button == 3:
|
||||
self.ui.app.camera.zoom_proportional(-1)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
UIElement.reset_art(self)
|
||||
self.tile_width = ceil(self.ui.width_tiles * self.ui.scale)
|
||||
|
|
@ -297,7 +380,7 @@ class StatusBarUI(UIElement):
|
|||
self.char_art.geo_changed = True
|
||||
self.fg_art.geo_changed = True
|
||||
self.bg_art.geo_changed = True
|
||||
|
||||
|
||||
def rewrite_art(self):
|
||||
bg = self.ui.colors.white
|
||||
self.art.clear_frame_layer(0, 0, bg)
|
||||
|
|
@ -305,8 +388,9 @@ class StatusBarUI(UIElement):
|
|||
if self.tile_width < self.left_items_width:
|
||||
return
|
||||
# draw tool label
|
||||
self.art.write_string(0, 0, self.tool_label_x, 0, self.tool_label,
|
||||
self.ui.palette.darkest_index)
|
||||
self.art.write_string(
|
||||
0, 0, self.tool_label_x, 0, self.tool_label, self.ui.palette.darkest_index
|
||||
)
|
||||
# only draw right side info if the window is wide enough
|
||||
if self.art.width > self.left_items_width + self.right_items_width:
|
||||
self.file_cycle_button.visible = True
|
||||
|
|
@ -319,19 +403,25 @@ class StatusBarUI(UIElement):
|
|||
self.layer_cycle_button.visible = False
|
||||
self.frame_cycle_button.visible = False
|
||||
self.zoom_set_button.visible = False
|
||||
|
||||
|
||||
def set_active_charset(self, new_charset):
|
||||
self.char_art.charset = self.fg_art.charset = self.bg_art.charset = new_charset
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def set_active_palette(self, new_palette):
|
||||
self.char_art.palette = self.fg_art.palette = self.bg_art.palette = new_palette
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def get_tile_y(self):
|
||||
"returns tile coordinate Y position of bar"
|
||||
return int(self.ui.app.window_height / (self.ui.charset.char_height * self.ui.scale)) - 1
|
||||
|
||||
return (
|
||||
int(
|
||||
self.ui.app.window_height
|
||||
/ (self.ui.charset.char_height * self.ui.scale)
|
||||
)
|
||||
- 1
|
||||
)
|
||||
|
||||
def update_button_captions(self):
|
||||
"set captions for buttons that change from selections"
|
||||
art = self.ui.active_art
|
||||
|
|
@ -339,22 +429,24 @@ class StatusBarUI(UIElement):
|
|||
self.tool_cycle_button.caption = self.ui.selected_tool.get_button_caption()
|
||||
self.tool_cycle_button.width = len(self.tool_cycle_button.caption) + 2
|
||||
# right edge elements
|
||||
self.file_cycle_button.caption = os.path.basename(art.filename) if art else FileCycleButton.caption
|
||||
self.file_cycle_button.caption = (
|
||||
os.path.basename(art.filename) if art else FileCycleButton.caption
|
||||
)
|
||||
self.file_cycle_button.width = len(self.file_cycle_button.caption) + 2
|
||||
# NOTE: button X offsets will be set in write_right_elements
|
||||
null = '---'
|
||||
null = "---"
|
||||
layers = art.layers if art else 0
|
||||
layer = '%s/%s' % (art.active_layer + 1, layers) if art else null
|
||||
layer = "%s/%s" % (art.active_layer + 1, layers) if art else null
|
||||
self.layer_cycle_button.caption = layer
|
||||
self.layer_cycle_button.width = len(self.layer_cycle_button.caption)
|
||||
frames = art.frames if art else 0
|
||||
frame = '%s/%s' % (art.active_frame + 1, frames) if art else null
|
||||
frame = "%s/%s" % (art.active_frame + 1, frames) if art else null
|
||||
self.frame_cycle_button.caption = frame
|
||||
self.frame_cycle_button.width = len(self.frame_cycle_button.caption)
|
||||
# zoom %
|
||||
zoom = '%.1f' % self.ui.app.camera.get_current_zoom_pct() if art else null
|
||||
self.zoom_set_button.caption = zoom[:5] # maintain size
|
||||
|
||||
zoom = "%.1f" % self.ui.app.camera.get_current_zoom_pct() if art else null
|
||||
self.zoom_set_button.caption = zoom[:5] # maintain size
|
||||
|
||||
def update(self):
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
|
|
@ -379,14 +471,14 @@ class StatusBarUI(UIElement):
|
|||
art.update()
|
||||
self.rewrite_art()
|
||||
self.draw_buttons()
|
||||
|
||||
|
||||
def position_swatch(self, renderable, x_offset):
|
||||
renderable.x = (self.char_art.quad_width * x_offset) - 1
|
||||
renderable.y = self.char_art.quad_height - 1
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
UIElement.reset_loc(self)
|
||||
|
||||
|
||||
def write_right_elements(self):
|
||||
"""
|
||||
fills in right-justified parts of status bar, eg current
|
||||
|
|
@ -406,7 +498,7 @@ class StatusBarUI(UIElement):
|
|||
self.zoom_set_button.x = x
|
||||
x -= padding
|
||||
# tile
|
||||
tile = 'X/Y'
|
||||
tile = "X/Y"
|
||||
color = light
|
||||
if self.ui.app.cursor and art:
|
||||
tile_x, tile_y = self.ui.app.cursor.get_tile()
|
||||
|
|
@ -420,22 +512,22 @@ class StatusBarUI(UIElement):
|
|||
color = self.dim_color
|
||||
tile_x = str(tile_x).rjust(3)
|
||||
tile_y = str(tile_y).rjust(3)
|
||||
tile = '%s,%s' % (tile_x, tile_y)
|
||||
tile = "%s,%s" % (tile_x, tile_y)
|
||||
self.art.write_string(0, 0, x, 0, tile, color, dark, True)
|
||||
# tile label
|
||||
x -= len(tile)
|
||||
self.art.write_string(0, 0, x, 0, self.tile_label, dark, light, True)
|
||||
# position layer button
|
||||
x -= (padding + len(self.tile_label) + self.layer_cycle_button.width)
|
||||
x -= padding + len(self.tile_label) + self.layer_cycle_button.width
|
||||
self.layer_cycle_button.x = x
|
||||
# layer label
|
||||
self.art.write_string(0, 0, x, 0, self.layer_label, dark, light, True)
|
||||
# position frame button
|
||||
x -= (padding + len(self.layer_label) + self.frame_cycle_button.width)
|
||||
x -= padding + len(self.layer_label) + self.frame_cycle_button.width
|
||||
self.frame_cycle_button.x = x
|
||||
# frame label
|
||||
self.art.write_string(0, 0, x, 0, self.frame_label, dark, light, True)
|
||||
|
||||
|
||||
def render(self):
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
|
|
|
|||
163
ui_swatch.py
163
ui_swatch.py
|
|
@ -1,25 +1,34 @@
|
|||
import math, time
|
||||
import math
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ui_element import UIElement, UIArt, UIRenderable
|
||||
from renderable_line import LineRenderable, SwatchSelectionBoxRenderable, UIRenderableX
|
||||
from ui_element import UIArt, UIElement, UIRenderable
|
||||
|
||||
# min width for charset; if charset is tiny adjust to this
|
||||
MIN_CHARSET_WIDTH = 16
|
||||
|
||||
|
||||
class UISwatch(UIElement):
|
||||
|
||||
def __init__(self, ui, popup):
|
||||
self.ui = ui
|
||||
self.popup = popup
|
||||
self.reset()
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.tile_width, self.tile_height = self.get_size()
|
||||
art = self.ui.active_art
|
||||
# generate a unique name for debug purposes
|
||||
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
self.art = UIArt(art_name, self.ui.app, art.charset, art.palette, self.tile_width, self.tile_height)
|
||||
art_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
self.art = UIArt(
|
||||
art_name,
|
||||
self.ui.app,
|
||||
art.charset,
|
||||
art.palette,
|
||||
self.tile_width,
|
||||
self.tile_height,
|
||||
)
|
||||
# tear down existing renderables if any
|
||||
if not self.renderables:
|
||||
self.renderables = []
|
||||
|
|
@ -32,20 +41,20 @@ class UISwatch(UIElement):
|
|||
self.renderable.grain_strength = 0
|
||||
self.renderables.append(self.renderable)
|
||||
self.reset_art()
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_size(self):
|
||||
return 1, 1
|
||||
|
||||
|
||||
def set_cursor_loc_from_mouse(self, cursor, mouse_x, mouse_y):
|
||||
# get location within char map
|
||||
w, h = self.art.quad_width, self.art.quad_height
|
||||
tile_x = (mouse_x - self.x) / w
|
||||
tile_y = (mouse_y - self.y) / h
|
||||
self.set_cursor_loc(cursor, tile_x, tile_y)
|
||||
|
||||
|
||||
def set_cursor_loc(self, cursor, tile_x, tile_y):
|
||||
"""
|
||||
common, generalized code for both character and palette swatches:
|
||||
|
|
@ -68,64 +77,69 @@ class UISwatch(UIElement):
|
|||
cursor.quad_size_ref = self.art
|
||||
cursor.tile_x, cursor.tile_y = tile_x, tile_y
|
||||
cursor.x, cursor.y = x, y
|
||||
|
||||
|
||||
def is_selection_index_valid(self, index):
|
||||
"returns True if given index is valid for choices this swatch offers"
|
||||
return False
|
||||
|
||||
|
||||
def set_cursor_selection_index(self, index):
|
||||
"another set_cursor_loc support method, overriden by subclasses"
|
||||
self.popup.blah = index
|
||||
|
||||
|
||||
def render(self):
|
||||
self.renderable.render()
|
||||
|
||||
|
||||
class CharacterSetSwatch(UISwatch):
|
||||
|
||||
# scale the character set will be drawn at
|
||||
char_scale = 2
|
||||
min_scale = 1
|
||||
max_scale = 5
|
||||
scale_increment = 0.25
|
||||
|
||||
|
||||
def increase_scale(self):
|
||||
if self.char_scale <= self.max_scale - self.scale_increment:
|
||||
self.char_scale += self.scale_increment
|
||||
|
||||
|
||||
def decrease_scale(self):
|
||||
if self.char_scale >= self.min_scale + self.scale_increment:
|
||||
self.char_scale -= self.scale_increment
|
||||
|
||||
|
||||
def reset(self):
|
||||
UISwatch.reset(self)
|
||||
self.selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
||||
self.grid = CharacterGridRenderable(self.ui.app, self.art)
|
||||
self.create_shade()
|
||||
self.renderables = [self.renderable, self.selection_box, self.grid,
|
||||
self.shade]
|
||||
|
||||
self.renderables = [self.renderable, self.selection_box, self.grid, self.shade]
|
||||
|
||||
def create_shade(self):
|
||||
# shaded box neath chars in case selected colors make em hard to see
|
||||
self.shade_art = UIArt('charset_shade', self.ui.app,
|
||||
self.ui.active_art.charset, self.ui.palette,
|
||||
self.tile_width, self.tile_height)
|
||||
self.shade_art = UIArt(
|
||||
"charset_shade",
|
||||
self.ui.app,
|
||||
self.ui.active_art.charset,
|
||||
self.ui.palette,
|
||||
self.tile_width,
|
||||
self.tile_height,
|
||||
)
|
||||
self.shade_art.clear_frame_layer(0, 0, self.ui.colors.black)
|
||||
self.shade = UIRenderable(self.ui.app, self.shade_art)
|
||||
self.shade.ui = self.ui
|
||||
self.shade.alpha = 0.2
|
||||
|
||||
|
||||
def get_size(self):
|
||||
art = self.ui.active_art
|
||||
return art.charset.map_width, art.charset.map_height
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
# MAYBE-TODO: using screen resolution, try to set quad size to an even
|
||||
# multiple of screen so the sampling doesn't get chunky
|
||||
aspect = self.ui.app.window_width / self.ui.app.window_height
|
||||
charset = self.art.charset
|
||||
self.art.quad_width = UIArt.quad_width * self.char_scale
|
||||
self.art.quad_height = self.art.quad_width * (charset.char_height / charset.char_width) * aspect
|
||||
self.art.quad_height = (
|
||||
self.art.quad_width * (charset.char_height / charset.char_width) * aspect
|
||||
)
|
||||
# only need to populate characters on reset_art, but update
|
||||
# colors every update()
|
||||
self.art.clear_frame_layer(0, 0, 0)
|
||||
|
|
@ -135,7 +149,7 @@ class CharacterSetSwatch(UISwatch):
|
|||
self.art.set_char_index_at(0, 0, x, y, i)
|
||||
i += 1
|
||||
self.art.geo_changed = True
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
self.x = self.popup.x + self.popup.swatch_margin
|
||||
self.y = self.popup.y
|
||||
|
|
@ -144,38 +158,36 @@ class CharacterSetSwatch(UISwatch):
|
|||
self.grid.x, self.grid.y = self.x, self.y
|
||||
self.grid.y -= self.art.quad_height
|
||||
self.shade.x, self.shade.y = self.x, self.y
|
||||
|
||||
|
||||
def set_xform(self, new_xform):
|
||||
for y in range(self.art.height):
|
||||
for x in range(self.art.width):
|
||||
self.art.set_char_transform_at(0, 0, x, y, new_xform)
|
||||
|
||||
|
||||
def is_selection_index_valid(self, index):
|
||||
return index < self.art.charset.last_index
|
||||
|
||||
|
||||
def set_cursor_selection_index(self, index):
|
||||
self.popup.cursor_char = index
|
||||
self.popup.cursor_color = -1
|
||||
|
||||
|
||||
def move_cursor(self, cursor, dx, dy):
|
||||
"moves cursor by specified amount in selection grid"
|
||||
# determine new cursor tile X/Y
|
||||
tile_x = cursor.tile_x + dx
|
||||
tile_y = cursor.tile_y + dy
|
||||
tile_index = (abs(tile_y) * self.art.width) + tile_x
|
||||
if tile_x < 0 or tile_x >= self.art.width:
|
||||
return
|
||||
elif tile_y > 0:
|
||||
if tile_x < 0 or tile_x >= self.art.width or tile_y > 0:
|
||||
return
|
||||
elif tile_y <= -self.art.height:
|
||||
# TODO: handle "jump" to palette swatch, and back
|
||||
#cursor.tile_y = 0
|
||||
#self.popup.palette_swatch.move_cursor(cursor, 0, 0)
|
||||
# cursor.tile_y = 0
|
||||
# self.popup.palette_swatch.move_cursor(cursor, 0, 0)
|
||||
return
|
||||
elif tile_index >= self.art.charset.last_index:
|
||||
return
|
||||
self.set_cursor_loc(cursor, tile_x, tile_y)
|
||||
|
||||
|
||||
def update(self):
|
||||
charset = self.ui.active_art.charset
|
||||
fg, bg = self.ui.selected_fg_color, self.ui.selected_bg_color
|
||||
|
|
@ -185,7 +197,10 @@ class CharacterSetSwatch(UISwatch):
|
|||
for x in range(charset.map_width):
|
||||
self.art.set_tile_at(0, 0, x, y, None, fg, bg, xform)
|
||||
self.art.update()
|
||||
if self.shade_art.quad_width != self.art.quad_width or self.shade_art.quad_height != self.art.quad_height:
|
||||
if (
|
||||
self.shade_art.quad_width != self.art.quad_width
|
||||
or self.shade_art.quad_height != self.art.quad_height
|
||||
):
|
||||
self.shade_art.quad_width = self.art.quad_width
|
||||
self.shade_art.quad_height = self.art.quad_height
|
||||
self.shade_art.geo_changed = True
|
||||
|
|
@ -203,17 +218,18 @@ class CharacterSetSwatch(UISwatch):
|
|||
self.selection_box.y = self.renderable.y
|
||||
selection_y = (self.ui.selected_char - selection_x) / charset.map_width
|
||||
self.selection_box.y -= selection_y * self.art.quad_height
|
||||
|
||||
|
||||
def render_bg(self):
|
||||
# draw shaded box beneath swatch if selected color(s) too similar to BG
|
||||
def is_hard_to_see(other_color_index):
|
||||
return self.ui.palette.are_colors_similar(self.popup.bg_color,
|
||||
self.art.palette,
|
||||
other_color_index)
|
||||
return self.ui.palette.are_colors_similar(
|
||||
self.popup.bg_color, self.art.palette, other_color_index
|
||||
)
|
||||
|
||||
fg, bg = self.ui.selected_fg_color, self.ui.selected_bg_color
|
||||
if is_hard_to_see(fg) or is_hard_to_see(bg):
|
||||
self.shade.render()
|
||||
|
||||
|
||||
def render(self):
|
||||
if not self.popup.visible:
|
||||
return
|
||||
|
|
@ -224,27 +240,34 @@ class CharacterSetSwatch(UISwatch):
|
|||
|
||||
|
||||
class PaletteSwatch(UISwatch):
|
||||
|
||||
def reset(self):
|
||||
UISwatch.reset(self)
|
||||
self.transparent_x = UIRenderableX(self.ui.app, self.art)
|
||||
self.fg_selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
||||
self.bg_selection_box = SwatchSelectionBoxRenderable(self.ui.app, self.art)
|
||||
# F label for FG color selection
|
||||
self.f_art = ColorSelectionLabelArt(self.ui, 'F')
|
||||
self.f_art = ColorSelectionLabelArt(self.ui, "F")
|
||||
# make character dark
|
||||
self.f_art.set_color_at(0, 0, 0, 0, self.f_art.palette.darkest_index, True)
|
||||
self.f_renderable = ColorSelectionLabelRenderable(self.ui.app, self.f_art)
|
||||
self.f_renderable.ui = self.ui
|
||||
# B label for BG color seletion
|
||||
self.b_art = ColorSelectionLabelArt(self.ui, 'B')
|
||||
self.b_art = ColorSelectionLabelArt(self.ui, "B")
|
||||
self.b_renderable = ColorSelectionLabelRenderable(self.ui.app, self.b_art)
|
||||
self.b_renderable.ui = self.ui
|
||||
self.renderables += self.transparent_x, self.fg_selection_box, self.bg_selection_box, self.f_renderable, self.b_renderable
|
||||
|
||||
self.renderables += (
|
||||
self.transparent_x,
|
||||
self.fg_selection_box,
|
||||
self.bg_selection_box,
|
||||
self.f_renderable,
|
||||
self.b_renderable,
|
||||
)
|
||||
|
||||
def get_size(self):
|
||||
# balance rows/columns according to character set swatch width
|
||||
charmap_width = max(self.popup.charset_swatch.art.charset.map_width, MIN_CHARSET_WIDTH)
|
||||
charmap_width = max(
|
||||
self.popup.charset_swatch.art.charset.map_width, MIN_CHARSET_WIDTH
|
||||
)
|
||||
colors = len(self.popup.charset_swatch.art.palette.colors)
|
||||
rows = math.ceil(colors / charmap_width)
|
||||
columns = math.ceil(colors / rows)
|
||||
|
|
@ -252,10 +275,13 @@ class PaletteSwatch(UISwatch):
|
|||
if colors == 129 and columns == 15:
|
||||
columns = 16
|
||||
return columns, rows
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
# base our quad size on charset's
|
||||
cqw, cqh = self.popup.charset_swatch.art.quad_width, self.popup.charset_swatch.art.quad_height
|
||||
cqw, cqh = (
|
||||
self.popup.charset_swatch.art.quad_width,
|
||||
self.popup.charset_swatch.art.quad_height,
|
||||
)
|
||||
# maximize item size based on row/column determined in get_size()
|
||||
charmap_width = max(self.art.charset.map_width, MIN_CHARSET_WIDTH)
|
||||
self.art.quad_width = (charmap_width / self.art.width) * cqw
|
||||
|
|
@ -271,12 +297,15 @@ class PaletteSwatch(UISwatch):
|
|||
self.art.set_color_at(0, 0, x, y, i, False)
|
||||
i += 1
|
||||
self.art.geo_changed = True
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
self.x = self.popup.x + self.popup.swatch_margin
|
||||
self.y = self.popup.charset_swatch.renderable.y
|
||||
# adjust Y for charset
|
||||
self.y -= self.popup.charset_swatch.art.quad_height * self.ui.active_art.charset.map_height
|
||||
self.y -= (
|
||||
self.popup.charset_swatch.art.quad_height
|
||||
* self.ui.active_art.charset.map_height
|
||||
)
|
||||
# adjust Y for palette caption and character scale
|
||||
self.y -= self.popup.art.quad_height * 2
|
||||
self.renderable.x, self.renderable.y = self.x, self.y
|
||||
|
|
@ -294,23 +323,26 @@ class PaletteSwatch(UISwatch):
|
|||
self.transparent_x.y = self.renderable.y - self.art.quad_height
|
||||
self.transparent_x.y -= (h - 1) * self.art.quad_height
|
||||
# set f/b_art's quad size
|
||||
self.f_art.quad_width, self.f_art.quad_height = self.b_art.quad_width, self.b_art.quad_height = self.popup.art.quad_width, self.popup.art.quad_height
|
||||
self.f_art.quad_width, self.f_art.quad_height = (
|
||||
self.b_art.quad_width,
|
||||
self.b_art.quad_height,
|
||||
) = self.popup.art.quad_width, self.popup.art.quad_height
|
||||
self.f_art.geo_changed = True
|
||||
self.b_art.geo_changed = True
|
||||
|
||||
|
||||
def is_selection_index_valid(self, index):
|
||||
return index < len(self.art.palette.colors)
|
||||
|
||||
|
||||
def set_cursor_selection_index(self, index):
|
||||
# modulo wrap if selecting last color
|
||||
self.popup.cursor_color = (index + 1) % len(self.art.palette.colors)
|
||||
self.popup.cursor_char = -1
|
||||
|
||||
|
||||
def move_cursor(self, cursor, dx, dy):
|
||||
# similar enough to charset swatch's move_cursor, different enough to
|
||||
# merit this small bit of duplicate code
|
||||
pass
|
||||
|
||||
|
||||
def update(self):
|
||||
self.art.update()
|
||||
self.f_art.update()
|
||||
|
|
@ -358,7 +390,7 @@ class PaletteSwatch(UISwatch):
|
|||
self.b_renderable.y = self.bg_selection_box.y
|
||||
self.b_renderable.x += x_offset
|
||||
self.b_renderable.y -= y_offset
|
||||
|
||||
|
||||
def render(self):
|
||||
if not self.popup.visible:
|
||||
return
|
||||
|
|
@ -373,7 +405,7 @@ class PaletteSwatch(UISwatch):
|
|||
class ColorSelectionLabelArt(UIArt):
|
||||
def __init__(self, ui, letter):
|
||||
letter_index = ui.charset.get_char_index(letter)
|
||||
art_name = '%s_%s' % (int(time.time()), self.__class__.__name__)
|
||||
art_name = "%s_%s" % (int(time.time()), self.__class__.__name__)
|
||||
UIArt.__init__(self, art_name, ui.app, ui.charset, ui.palette, 1, 1)
|
||||
label_color = ui.colors.white
|
||||
label_bg_color = 0
|
||||
|
|
@ -386,9 +418,8 @@ class ColorSelectionLabelRenderable(UIRenderable):
|
|||
|
||||
|
||||
class CharacterGridRenderable(LineRenderable):
|
||||
|
||||
color = (0.5, 0.5, 0.5, 0.25)
|
||||
|
||||
|
||||
def build_geo(self):
|
||||
w, h = self.quad_size_ref.width, self.quad_size_ref.height
|
||||
v = []
|
||||
|
|
@ -396,12 +427,12 @@ class CharacterGridRenderable(LineRenderable):
|
|||
c = self.color * 4 * w * h
|
||||
index = 0
|
||||
for x in range(1, w):
|
||||
v += [(x, -h+1), (x, 1)]
|
||||
e += [index, index+1]
|
||||
v += [(x, -h + 1), (x, 1)]
|
||||
e += [index, index + 1]
|
||||
index += 2
|
||||
for y in range(h-1):
|
||||
for y in range(h - 1):
|
||||
v += [(w, -y), (0, -y)]
|
||||
e += [index, index+1]
|
||||
e += [index, index + 1]
|
||||
index += 2
|
||||
self.vert_array = np.array(v, dtype=np.float32)
|
||||
self.elem_array = np.array(e, dtype=np.uint32)
|
||||
|
|
|
|||
286
ui_tool.py
286
ui_tool.py
|
|
@ -1,18 +1,26 @@
|
|||
import math
|
||||
import sdl2
|
||||
from PIL import Image
|
||||
|
||||
from texture import Texture
|
||||
from art import (
|
||||
UV_FLIP90,
|
||||
UV_FLIP270,
|
||||
UV_FLIPX,
|
||||
UV_FLIPY,
|
||||
UV_NORMAL,
|
||||
UV_ROTATE90,
|
||||
UV_ROTATE180,
|
||||
UV_ROTATE270,
|
||||
)
|
||||
from edit_command import EditCommandTile
|
||||
from art import UV_NORMAL, UV_ROTATE90, UV_ROTATE180, UV_ROTATE270, UV_FLIPX, UV_FLIPY, UV_FLIP90, UV_FLIP270
|
||||
from key_shifts import SHIFT_MAP
|
||||
from selection import SelectionRenderable
|
||||
from texture import Texture
|
||||
|
||||
|
||||
class UITool:
|
||||
|
||||
name = 'DEBUGTESTTOOL'
|
||||
name = "DEBUGTESTTOOL"
|
||||
# name visible in popup's tool tab
|
||||
button_caption = 'Debug Tool'
|
||||
button_caption = "Debug Tool"
|
||||
# paint continuously, ie every time mouse enters a new tile
|
||||
paint_while_dragging = True
|
||||
# show preview of paint result under cursor
|
||||
|
|
@ -25,8 +33,8 @@ class UITool:
|
|||
# (false for eg Selection tool)
|
||||
affects_masks = True
|
||||
# filename of icon in UI_ASSET_DIR, shown on cursor
|
||||
icon_filename = 'icon.png'
|
||||
|
||||
icon_filename = "icon.png"
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.affects_char = True
|
||||
|
|
@ -36,69 +44,89 @@ class UITool:
|
|||
# load icon, cursor's sprite renderable will reference this texture
|
||||
icon_filename = self.ui.asset_dir + self.icon_filename
|
||||
self.icon_texture = self.load_icon_texture(icon_filename)
|
||||
|
||||
|
||||
def load_icon_texture(self, img_filename):
|
||||
img = Image.open(img_filename)
|
||||
img = img.convert('RGBA')
|
||||
img = img.convert("RGBA")
|
||||
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
return Texture(img.tobytes(), *img.size)
|
||||
|
||||
|
||||
def get_icon_texture(self):
|
||||
"""
|
||||
Returns icon texture that should display for tool's current state.
|
||||
(override to eg choose from multiples for mod keys)
|
||||
"""
|
||||
return self.icon_texture
|
||||
|
||||
|
||||
def get_button_caption(self):
|
||||
# normally just returns button_caption, but can be overridden to
|
||||
# provide custom behavior (eg fill tool)
|
||||
return self.button_caption
|
||||
|
||||
|
||||
def toggle_affects_char(self):
|
||||
if not self.affects_masks or self.ui.app.game_mode:
|
||||
return
|
||||
self.affects_char = not self.affects_char
|
||||
self.ui.tool_settings_changed = True
|
||||
line = self.button_caption + ' '
|
||||
line = '%s %s' % (self.button_caption, [self.ui.affects_char_off_log, self.ui.affects_char_on_log][self.affects_char])
|
||||
line = self.button_caption + " "
|
||||
line = "%s %s" % (
|
||||
self.button_caption,
|
||||
[self.ui.affects_char_off_log, self.ui.affects_char_on_log][
|
||||
self.affects_char
|
||||
],
|
||||
)
|
||||
self.ui.message_line.post_line(line)
|
||||
|
||||
|
||||
def toggle_affects_fg(self):
|
||||
if not self.affects_masks or self.ui.app.game_mode:
|
||||
return
|
||||
self.affects_fg_color = not self.affects_fg_color
|
||||
self.ui.tool_settings_changed = True
|
||||
line = '%s %s' % (self.button_caption, [self.ui.affects_fg_off_log, self.ui.affects_fg_on_log][self.affects_fg_color])
|
||||
line = "%s %s" % (
|
||||
self.button_caption,
|
||||
[self.ui.affects_fg_off_log, self.ui.affects_fg_on_log][
|
||||
self.affects_fg_color
|
||||
],
|
||||
)
|
||||
self.ui.message_line.post_line(line)
|
||||
|
||||
|
||||
def toggle_affects_bg(self):
|
||||
if not self.affects_masks or self.ui.app.game_mode:
|
||||
return
|
||||
self.affects_bg_color = not self.affects_bg_color
|
||||
self.ui.tool_settings_changed = True
|
||||
line = '%s %s' % (self.button_caption, [self.ui.affects_bg_off_log, self.ui.affects_bg_on_log][self.affects_bg_color])
|
||||
line = "%s %s" % (
|
||||
self.button_caption,
|
||||
[self.ui.affects_bg_off_log, self.ui.affects_bg_on_log][
|
||||
self.affects_bg_color
|
||||
],
|
||||
)
|
||||
self.ui.message_line.post_line(line)
|
||||
|
||||
|
||||
def toggle_affects_xform(self):
|
||||
if not self.affects_masks or self.ui.app.game_mode:
|
||||
return
|
||||
self.affects_xform = not self.affects_xform
|
||||
self.ui.tool_settings_changed = True
|
||||
line = '%s %s' % (self.button_caption, [self.ui.affects_xform_off_log, self.ui.affects_xform_on_log][self.affects_xform])
|
||||
line = "%s %s" % (
|
||||
self.button_caption,
|
||||
[self.ui.affects_xform_off_log, self.ui.affects_xform_on_log][
|
||||
self.affects_xform
|
||||
],
|
||||
)
|
||||
self.ui.message_line.post_line(line)
|
||||
|
||||
|
||||
def get_paint_commands(self):
|
||||
"returns a list of EditCommandTiles for a given paint operation"
|
||||
return []
|
||||
|
||||
|
||||
def increase_brush_size(self):
|
||||
if not self.brush_size:
|
||||
return
|
||||
self.brush_size += 1
|
||||
self.ui.app.cursor.set_scale(self.brush_size)
|
||||
self.ui.tool_settings_changed = True
|
||||
|
||||
|
||||
def decrease_brush_size(self):
|
||||
if not self.brush_size:
|
||||
return
|
||||
|
|
@ -109,12 +137,11 @@ class UITool:
|
|||
|
||||
|
||||
class PencilTool(UITool):
|
||||
|
||||
name = 'pencil'
|
||||
name = "pencil"
|
||||
# "Paint" not Pencil so the A mnemonic works :/
|
||||
button_caption = 'Paint'
|
||||
icon_filename = 'tool_paint.png'
|
||||
|
||||
button_caption = "Paint"
|
||||
icon_filename = "tool_paint.png"
|
||||
|
||||
def get_tile_change(self, b_char, b_fg, b_bg, b_xform):
|
||||
"""
|
||||
return the tile value changes this tool would perform on a tile -
|
||||
|
|
@ -123,12 +150,12 @@ class PencilTool(UITool):
|
|||
a_char = self.ui.selected_char if self.affects_char else None
|
||||
# don't paint fg color for blank characters
|
||||
# (disabled, see BB issue #86)
|
||||
#a_fg = self.ui.selected_fg_color if self.affects_fg_color and a_char != 0 else None
|
||||
# a_fg = self.ui.selected_fg_color if self.affects_fg_color and a_char != 0 else None
|
||||
a_fg = self.ui.selected_fg_color if self.affects_fg_color else None
|
||||
a_bg = self.ui.selected_bg_color if self.affects_bg_color else None
|
||||
a_xform = self.ui.selected_xform if self.affects_xform else None
|
||||
return a_char, a_fg, a_bg, a_xform
|
||||
|
||||
|
||||
def get_paint_commands(self):
|
||||
commands = []
|
||||
art = self.ui.active_art
|
||||
|
|
@ -137,8 +164,8 @@ class PencilTool(UITool):
|
|||
cur = self.ui.app.cursor
|
||||
# handle dragging while painting (cursor does the heavy lifting here)
|
||||
# !!TODO!! finish this, work in progress
|
||||
if cur.moved_this_frame() and cur.current_command and False: #DEBUG
|
||||
#print('%s: cursor moved' % self.ui.app.get_elapsed_time()) #DEBUG
|
||||
if cur.moved_this_frame() and cur.current_command and False: # DEBUG
|
||||
# print('%s: cursor moved' % self.ui.app.get_elapsed_time()) #DEBUG
|
||||
tiles = cur.get_tiles_under_drag()
|
||||
else:
|
||||
tiles = cur.get_tiles_under_brush()
|
||||
|
|
@ -154,7 +181,9 @@ class PencilTool(UITool):
|
|||
new_tc.set_tile(frame, layer, *tile)
|
||||
b_char, b_fg, b_bg, b_xform = art.get_tile_at(frame, layer, *tile)
|
||||
new_tc.set_before(b_char, b_fg, b_bg, b_xform)
|
||||
a_char, a_fg, a_bg, a_xform = self.get_tile_change(b_char, b_fg, b_bg, b_xform)
|
||||
a_char, a_fg, a_bg, a_xform = self.get_tile_change(
|
||||
b_char, b_fg, b_bg, b_xform
|
||||
)
|
||||
new_tc.set_after(a_char, a_fg, a_bg, a_xform)
|
||||
# Note: even if command has same result as another in command_tiles,
|
||||
# add it anyway as it may be a tool for which subsequent edits to
|
||||
|
|
@ -165,11 +194,10 @@ class PencilTool(UITool):
|
|||
|
||||
|
||||
class EraseTool(PencilTool):
|
||||
|
||||
name = 'erase'
|
||||
button_caption = 'Erase'
|
||||
icon_filename = 'tool_erase.png'
|
||||
|
||||
name = "erase"
|
||||
button_caption = "Erase"
|
||||
icon_filename = "tool_erase.png"
|
||||
|
||||
def get_tile_change(self, b_char, b_fg, b_bg, b_xform):
|
||||
char = 0 if self.affects_char else None
|
||||
fg = 0 if self.affects_fg_color else None
|
||||
|
|
@ -180,9 +208,8 @@ class EraseTool(PencilTool):
|
|||
|
||||
|
||||
class RotateTool(PencilTool):
|
||||
|
||||
name = 'rotate'
|
||||
button_caption = 'Rotate'
|
||||
name = "rotate"
|
||||
button_caption = "Rotate"
|
||||
update_preview_after_paint = True
|
||||
rotation_shifts = {
|
||||
UV_NORMAL: UV_ROTATE90,
|
||||
|
|
@ -193,22 +220,21 @@ class RotateTool(PencilTool):
|
|||
UV_FLIPX: UV_FLIP270,
|
||||
UV_FLIP270: UV_FLIPY,
|
||||
UV_FLIPY: UV_ROTATE270,
|
||||
UV_FLIP90: UV_FLIPX
|
||||
UV_FLIP90: UV_FLIPX,
|
||||
}
|
||||
icon_filename = 'tool_rotate.png'
|
||||
|
||||
icon_filename = "tool_rotate.png"
|
||||
|
||||
def get_tile_change(self, b_char, b_fg, b_bg, b_xform):
|
||||
return b_char, b_fg, b_bg, self.rotation_shifts[b_xform]
|
||||
|
||||
|
||||
class GrabTool(UITool):
|
||||
|
||||
name = 'grab'
|
||||
button_caption = 'Grab'
|
||||
name = "grab"
|
||||
button_caption = "Grab"
|
||||
brush_size = None
|
||||
show_preview = False
|
||||
icon_filename = 'tool_grab.png'
|
||||
|
||||
icon_filename = "tool_grab.png"
|
||||
|
||||
def grab(self):
|
||||
x, y = self.ui.app.cursor.get_tile()
|
||||
art = self.ui.active_art
|
||||
|
|
@ -233,44 +259,49 @@ class GrabTool(UITool):
|
|||
|
||||
|
||||
class TextTool(UITool):
|
||||
|
||||
name = 'text'
|
||||
button_caption = 'Text'
|
||||
name = "text"
|
||||
button_caption = "Text"
|
||||
brush_size = None
|
||||
show_preview = False
|
||||
icon_filename = 'tool_text.png'
|
||||
|
||||
icon_filename = "tool_text.png"
|
||||
|
||||
def __init__(self, ui):
|
||||
UITool.__init__(self, ui)
|
||||
self.input_active = False
|
||||
self.cursor = None
|
||||
|
||||
|
||||
def start_entry(self):
|
||||
self.cursor = self.ui.app.cursor
|
||||
# popup gobbles keyboard input, so always dismiss it if it's up
|
||||
if self.ui.popup.visible:
|
||||
self.ui.popup.hide()
|
||||
if self.cursor.x < 0 or self.cursor.x > self.ui.active_art.width or \
|
||||
-self.cursor.y < 0 or -self.cursor.y > self.ui.active_art.height:
|
||||
if (
|
||||
self.cursor.x < 0
|
||||
or self.cursor.x > self.ui.active_art.width
|
||||
or -self.cursor.y < 0
|
||||
or -self.cursor.y > self.ui.active_art.height
|
||||
):
|
||||
return
|
||||
self.input_active = True
|
||||
self.reset_cursor_start(self.cursor.x, -self.cursor.y)
|
||||
self.cursor.start_paint()
|
||||
#self.ui.message_line.post_line('Started text entry at %s, %s' % (self.start_x + 1, self.start_y + 1))
|
||||
self.ui.message_line.post_line('Started text entry, press Escape to stop entering text.', 5)
|
||||
|
||||
# self.ui.message_line.post_line('Started text entry at %s, %s' % (self.start_x + 1, self.start_y + 1))
|
||||
self.ui.message_line.post_line(
|
||||
"Started text entry, press Escape to stop entering text.", 5
|
||||
)
|
||||
|
||||
def finish_entry(self):
|
||||
self.input_active = False
|
||||
self.ui.tool_settings_changed = True
|
||||
if self.cursor:
|
||||
x, y = int(self.cursor.x) + 1, int(-self.cursor.y) + 1
|
||||
self.cursor.finish_paint()
|
||||
#self.ui.message_line.post_line('Finished text entry at %s, %s' % (x, y))
|
||||
self.ui.message_line.post_line('Finished text entry.')
|
||||
|
||||
# self.ui.message_line.post_line('Finished text entry at %s, %s' % (x, y))
|
||||
self.ui.message_line.post_line("Finished text entry.")
|
||||
|
||||
def reset_cursor_start(self, new_x, new_y):
|
||||
self.start_x, self.start_y = int(new_x), int(new_y)
|
||||
|
||||
|
||||
def handle_keyboard_input(self, key, shift_pressed, ctrl_pressed, alt_pressed):
|
||||
# for now, do nothing on ctrl/alt
|
||||
if ctrl_pressed or alt_pressed:
|
||||
|
|
@ -284,30 +315,32 @@ class TextTool(UITool):
|
|||
x, y = int(self.cursor.x), int(-self.cursor.y)
|
||||
char_w, char_h = art.quad_width, art.quad_height
|
||||
# TODO: if cursor isn't inside selection, bail early
|
||||
if keystr == 'Return':
|
||||
if keystr == "Return":
|
||||
if self.cursor.y < art.width:
|
||||
self.cursor.x = self.start_x
|
||||
self.cursor.y -= 1
|
||||
elif keystr == 'Backspace':
|
||||
elif keystr == "Backspace":
|
||||
if self.cursor.x > self.start_x:
|
||||
self.cursor.x -= char_w
|
||||
# undo command on previous tile
|
||||
self.cursor.current_command.undo_commands_for_tile(frame, layer, x-1, y)
|
||||
elif keystr == 'Space':
|
||||
keystr = ' '
|
||||
elif keystr == 'Up':
|
||||
self.cursor.current_command.undo_commands_for_tile(
|
||||
frame, layer, x - 1, y
|
||||
)
|
||||
elif keystr == "Space":
|
||||
keystr = " "
|
||||
elif keystr == "Up":
|
||||
if -self.cursor.y > 0:
|
||||
self.cursor.y += 1
|
||||
elif keystr == 'Down':
|
||||
elif keystr == "Down":
|
||||
if -self.cursor.y < art.height - 1:
|
||||
self.cursor.y -= 1
|
||||
elif keystr == 'Left':
|
||||
elif keystr == "Left":
|
||||
if self.cursor.x > 0:
|
||||
self.cursor.x -= char_w
|
||||
elif keystr == 'Right':
|
||||
elif keystr == "Right":
|
||||
if self.cursor.x < art.width - 1:
|
||||
self.cursor.x += char_w
|
||||
elif keystr == 'Escape':
|
||||
elif keystr == "Escape":
|
||||
self.finish_entry()
|
||||
return
|
||||
# ignore any other non-character keys
|
||||
|
|
@ -322,9 +355,9 @@ class TextTool(UITool):
|
|||
if keystr.isalpha() and not shift_pressed and not self.ui.app.il.capslock_on:
|
||||
keystr = keystr.lower()
|
||||
elif not keystr.isalpha() and shift_pressed:
|
||||
keystr = SHIFT_MAP.get(keystr, ' ')
|
||||
keystr = SHIFT_MAP.get(keystr, " ")
|
||||
# if cursor got out of bounds, don't input
|
||||
if 0 > x or x >= art.width or 0 > y or y >= art.height:
|
||||
if x < 0 or x >= art.width or y < 0 or y >= art.height:
|
||||
return
|
||||
# create tile command
|
||||
new_tc = EditCommandTile(art)
|
||||
|
|
@ -340,7 +373,7 @@ class TextTool(UITool):
|
|||
if self.cursor.current_command:
|
||||
self.cursor.current_command.add_command_tiles([new_tc])
|
||||
else:
|
||||
self.ui.app.log('DEV WARNING: Cursor current command was expected')
|
||||
self.ui.app.log("DEV WARNING: Cursor current command was expected")
|
||||
new_tc.apply()
|
||||
self.cursor.x += char_w
|
||||
if self.cursor.x >= self.ui.active_art.width:
|
||||
|
|
@ -351,17 +384,16 @@ class TextTool(UITool):
|
|||
|
||||
|
||||
class SelectTool(UITool):
|
||||
|
||||
name = 'select'
|
||||
button_caption = 'Select'
|
||||
name = "select"
|
||||
button_caption = "Select"
|
||||
brush_size = None
|
||||
affects_masks = False
|
||||
show_preview = False
|
||||
icon_filename = 'tool_select_add.png' # used only for toolbar
|
||||
icon_filename_normal = 'tool_select.png'
|
||||
icon_filename_add = 'tool_select_add.png'
|
||||
icon_filename_sub = 'tool_select_sub.png'
|
||||
|
||||
icon_filename = "tool_select_add.png" # used only for toolbar
|
||||
icon_filename_normal = "tool_select.png"
|
||||
icon_filename_add = "tool_select_add.png"
|
||||
icon_filename_sub = "tool_select_sub.png"
|
||||
|
||||
def __init__(self, ui):
|
||||
UITool.__init__(self, ui)
|
||||
self.selection_in_progress = False
|
||||
|
|
@ -380,7 +412,7 @@ class SelectTool(UITool):
|
|||
self.icon_texture_add = self.load_icon_texture(icon)
|
||||
icon = self.ui.asset_dir + self.icon_filename_sub
|
||||
self.icon_texture_sub = self.load_icon_texture(icon)
|
||||
|
||||
|
||||
def get_icon_texture(self):
|
||||
# show different icons based on mod key status
|
||||
if self.ui.app.il.shift_pressed:
|
||||
|
|
@ -389,14 +421,14 @@ class SelectTool(UITool):
|
|||
return self.icon_texture_sub
|
||||
else:
|
||||
return self.icon_texture
|
||||
|
||||
|
||||
def start_select(self):
|
||||
self.selection_in_progress = True
|
||||
self.current_drag = {}
|
||||
x, y = self.ui.app.cursor.x, int(-self.ui.app.cursor.y)
|
||||
self.drag_start_x, self.drag_start_y = x, y
|
||||
#print('started select drag at %s,%s' % (x, y))
|
||||
|
||||
# print('started select drag at %s,%s' % (x, y))
|
||||
|
||||
def finish_select(self, add_to_selection, subtract_from_selection):
|
||||
self.selection_in_progress = False
|
||||
# selection boolean operations:
|
||||
|
|
@ -410,9 +442,9 @@ class SelectTool(UITool):
|
|||
for tile in self.current_drag:
|
||||
self.selected_tiles.pop(tile, None)
|
||||
self.current_drag = {}
|
||||
#x, y = self.ui.app.cursor.x, int(-self.ui.app.cursor.y)
|
||||
#print('finished select drag at %s,%s' % (x, y))
|
||||
|
||||
# x, y = self.ui.app.cursor.x, int(-self.ui.app.cursor.y)
|
||||
# print('finished select drag at %s,%s' % (x, y))
|
||||
|
||||
def update(self):
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
|
|
@ -423,9 +455,15 @@ class SelectTool(UITool):
|
|||
start_x, start_y = int(self.drag_start_x), int(self.drag_start_y)
|
||||
end_x, end_y = int(self.ui.app.cursor.x), int(-self.ui.app.cursor.y)
|
||||
if start_x > end_x:
|
||||
start_x, end_x, = end_x, start_x
|
||||
(
|
||||
start_x,
|
||||
end_x,
|
||||
) = end_x, start_x
|
||||
if start_y > end_y:
|
||||
start_y, end_y, = end_y, start_y
|
||||
(
|
||||
start_y,
|
||||
end_y,
|
||||
) = end_y, start_y
|
||||
# always grow to include cursor's tile
|
||||
end_x += 1
|
||||
end_y += 1
|
||||
|
|
@ -445,7 +483,7 @@ class SelectTool(UITool):
|
|||
self.drag_renderable.rebind_buffers()
|
||||
self.last_selection = self.selected_tiles.copy()
|
||||
self.last_drag = self.current_drag.copy()
|
||||
|
||||
|
||||
def render_selections(self):
|
||||
if len(self.selected_tiles) > 0:
|
||||
self.select_renderable.render()
|
||||
|
|
@ -454,12 +492,11 @@ class SelectTool(UITool):
|
|||
|
||||
|
||||
class PasteTool(UITool):
|
||||
|
||||
name = 'paste'
|
||||
button_caption = 'Paste'
|
||||
name = "paste"
|
||||
button_caption = "Paste"
|
||||
brush_size = None
|
||||
icon_filename = 'tool_paste.png'
|
||||
|
||||
icon_filename = "tool_paste.png"
|
||||
|
||||
# TODO!: dragging large pastes around seems heck of slow, investigate
|
||||
# why this function might be to blame and see if there's a fix!
|
||||
def get_paint_commands(self):
|
||||
|
|
@ -488,7 +525,9 @@ class PasteTool(UITool):
|
|||
if len(self.ui.select_tool.selected_tiles) > 0:
|
||||
if not self.ui.select_tool.selected_tiles.get((x, y), False):
|
||||
continue
|
||||
b_char, b_fg, b_bg, b_xform = self.ui.active_art.get_tile_at(frame, layer, x, y)
|
||||
b_char, b_fg, b_bg, b_xform = self.ui.active_art.get_tile_at(
|
||||
frame, layer, x, y
|
||||
)
|
||||
new_tc.set_before(b_char, b_fg, b_bg, b_xform)
|
||||
new_tc.set_tile(frame, layer, x, y)
|
||||
# respect affects masks like other tools
|
||||
|
|
@ -502,35 +541,36 @@ class PasteTool(UITool):
|
|||
commands.append(new_tc)
|
||||
return commands
|
||||
|
||||
|
||||
# "fill boundary" modes: character, fg color, bg color
|
||||
FILL_BOUND_CHAR = 0
|
||||
FILL_BOUND_FG_COLOR = 1
|
||||
FILL_BOUND_BG_COLOR = 2
|
||||
|
||||
|
||||
class FillTool(UITool):
|
||||
|
||||
name = 'fill'
|
||||
button_caption = 'Fill'
|
||||
name = "fill"
|
||||
button_caption = "Fill"
|
||||
brush_size = None
|
||||
icon_filename = 'tool_fill_char.png' # used only for toolbar
|
||||
icon_filename = "tool_fill_char.png" # used only for toolbar
|
||||
# icons and strings for different boundary modes
|
||||
icon_filename_char = 'tool_fill_char.png'
|
||||
icon_filename_fg = 'tool_fill_fg.png'
|
||||
icon_filename_bg = 'tool_fill_bg.png'
|
||||
icon_filename_char = "tool_fill_char.png"
|
||||
icon_filename_fg = "tool_fill_fg.png"
|
||||
icon_filename_bg = "tool_fill_bg.png"
|
||||
boundary_mode = FILL_BOUND_CHAR
|
||||
# user-facing names for the boundary modes
|
||||
boundary_mode_names = {
|
||||
FILL_BOUND_CHAR : 'character',
|
||||
FILL_BOUND_FG_COLOR : 'fg color',
|
||||
FILL_BOUND_BG_COLOR : 'bg color'
|
||||
FILL_BOUND_CHAR: "character",
|
||||
FILL_BOUND_FG_COLOR: "fg color",
|
||||
FILL_BOUND_BG_COLOR: "bg color",
|
||||
}
|
||||
# determine cycling order
|
||||
next_boundary_modes = {
|
||||
FILL_BOUND_CHAR : FILL_BOUND_FG_COLOR,
|
||||
FILL_BOUND_FG_COLOR : FILL_BOUND_BG_COLOR,
|
||||
FILL_BOUND_BG_COLOR : FILL_BOUND_CHAR
|
||||
FILL_BOUND_CHAR: FILL_BOUND_FG_COLOR,
|
||||
FILL_BOUND_FG_COLOR: FILL_BOUND_BG_COLOR,
|
||||
FILL_BOUND_BG_COLOR: FILL_BOUND_CHAR,
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
UITool.__init__(self, ui)
|
||||
icon = self.ui.asset_dir + self.icon_filename_char
|
||||
|
|
@ -539,11 +579,15 @@ class FillTool(UITool):
|
|||
self.icon_texture_fg = self.load_icon_texture(icon)
|
||||
icon = self.ui.asset_dir + self.icon_filename_bg
|
||||
self.icon_texture_bg = self.load_icon_texture(icon)
|
||||
|
||||
|
||||
def get_icon_texture(self):
|
||||
# show different icon based on boundary type
|
||||
return [self.icon_texture_char, self.icon_texture_fg,
|
||||
self.icon_texture_bg][self.boundary_mode]
|
||||
|
||||
return [self.icon_texture_char, self.icon_texture_fg, self.icon_texture_bg][
|
||||
self.boundary_mode
|
||||
]
|
||||
|
||||
def get_button_caption(self):
|
||||
return '%s (%s bounded)' % (self.button_caption, self.boundary_mode_names[self.boundary_mode])
|
||||
return "%s (%s bounded)" % (
|
||||
self.button_caption,
|
||||
self.boundary_mode_names[self.boundary_mode],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
|
||||
from ui_element import UIElement
|
||||
from ui_button import UIButton
|
||||
|
||||
from renderable_sprite import UISpriteRenderable
|
||||
from renderable_line import ToolSelectionBoxRenderable
|
||||
from renderable_sprite import UISpriteRenderable
|
||||
from ui_button import UIButton
|
||||
from ui_element import UIElement
|
||||
|
||||
|
||||
class ToolBar(UIElement):
|
||||
|
||||
tile_width, tile_height = 4, 1 # real size will be set based on buttons
|
||||
tile_width, tile_height = 4, 1 # real size will be set based on buttons
|
||||
icon_scale_factor = 4
|
||||
snap_left = True
|
||||
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.icon_renderables = []
|
||||
self.create_toolbar_buttons()
|
||||
UIElement.__init__(self, ui)
|
||||
self.selection_box = ToolSelectionBoxRenderable(ui.app, self.art)
|
||||
|
||||
|
||||
def reset_art(self):
|
||||
# by default, a 1D vertical bar
|
||||
self.tile_width = ToolBarButton.width
|
||||
|
|
@ -26,7 +23,7 @@ class ToolBar(UIElement):
|
|||
self.tile_height = ToolBarButton.height * len(self.buttons)
|
||||
self.art.resize(self.tile_width, self.tile_height)
|
||||
UIElement.reset_art(self)
|
||||
|
||||
|
||||
def reset_loc(self):
|
||||
UIElement.reset_loc(self)
|
||||
# by default, a vertical bar centered along left edge of the screen
|
||||
|
|
@ -36,19 +33,19 @@ class ToolBar(UIElement):
|
|||
self.renderable.x, self.renderable.y = self.x, self.y
|
||||
# scale and position button icons only now that we're positioned
|
||||
self.reset_button_icons()
|
||||
|
||||
|
||||
def create_toolbar_buttons(self):
|
||||
# (override in subclass)
|
||||
pass
|
||||
|
||||
|
||||
def update_selection_box(self):
|
||||
# (override in subclass)
|
||||
pass
|
||||
|
||||
|
||||
def update(self):
|
||||
UIElement.update(self)
|
||||
self.update_selection_box()
|
||||
|
||||
|
||||
def render(self):
|
||||
UIElement.render(self)
|
||||
for r in self.icon_renderables:
|
||||
|
|
@ -58,42 +55,47 @@ class ToolBar(UIElement):
|
|||
|
||||
class ToolBarButton(UIButton):
|
||||
width, height = 4, 2
|
||||
caption = ''
|
||||
caption = ""
|
||||
tooltip_on_hover = True
|
||||
|
||||
|
||||
def get_tooltip_text(self):
|
||||
return self.cb_arg.button_caption
|
||||
|
||||
|
||||
def get_tooltip_location(self):
|
||||
x = self.width
|
||||
window_height_chars = self.element.ui.app.window_height / (self.element.ui.charset.char_height * self.element.ui.scale)
|
||||
window_height_chars = self.element.ui.app.window_height / (
|
||||
self.element.ui.charset.char_height * self.element.ui.scale
|
||||
)
|
||||
cursor_y = self.element.ui.app.mouse_y / self.element.ui.app.window_height
|
||||
y = int(cursor_y * window_height_chars)
|
||||
return x, y
|
||||
|
||||
|
||||
class ArtToolBar(ToolBar):
|
||||
|
||||
def create_toolbar_buttons(self):
|
||||
for i,tool in enumerate(self.ui.tools):
|
||||
for i, tool in enumerate(self.ui.tools):
|
||||
button = ToolBarButton(self)
|
||||
# button.caption = tool.button_caption # DEBUG
|
||||
button.x = 0
|
||||
button.y = i * button.height
|
||||
# alternate colors
|
||||
button.normal_bg_color = self.ui.colors.white if i % 2 == 0 else self.ui.colors.lightgrey
|
||||
button.normal_bg_color = (
|
||||
self.ui.colors.white if i % 2 == 0 else self.ui.colors.lightgrey
|
||||
)
|
||||
button.hovered_bg_color = self.ui.colors.medgrey
|
||||
# callback: tell ui to set this tool as selected
|
||||
button.callback = self.ui.set_selected_tool
|
||||
button.cb_arg = tool
|
||||
self.buttons.append(button)
|
||||
# create button icon
|
||||
sprite = UISpriteRenderable(self.ui.app, self.ui.asset_dir + tool.icon_filename)
|
||||
sprite = UISpriteRenderable(
|
||||
self.ui.app, self.ui.asset_dir + tool.icon_filename
|
||||
)
|
||||
self.icon_renderables.append(sprite)
|
||||
|
||||
|
||||
def reset_button_icons(self):
|
||||
button_height = self.art.quad_height * ToolBarButton.height
|
||||
for i,icon in enumerate(self.icon_renderables):
|
||||
for i, icon in enumerate(self.icon_renderables):
|
||||
# scale: same screen size as cursor icon
|
||||
scale_x = icon.texture.width / self.ui.app.window_width
|
||||
scale_x *= self.icon_scale_factor * self.ui.scale
|
||||
|
|
@ -104,12 +106,12 @@ class ArtToolBar(ToolBar):
|
|||
# position
|
||||
# remember that in renderable space, (0, 0) = center of screen
|
||||
icon.x = self.x
|
||||
icon.x += (icon.scale_x / 8)
|
||||
icon.x += icon.scale_x / 8
|
||||
icon.y = self.y
|
||||
icon.y -= button_height * i
|
||||
icon.y -= icon.scale_y
|
||||
icon.y -= (icon.scale_y / 8)
|
||||
|
||||
icon.y -= icon.scale_y / 8
|
||||
|
||||
def update_selection_box(self):
|
||||
# scale and position box around currently selected tool
|
||||
self.selection_box.scale_x = ToolBarButton.width
|
||||
|
|
|
|||
281
uv.lock
Normal file
281
uv.lock
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "playscii"
|
||||
version = "9.18"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "appdirs" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyopengl" },
|
||||
{ name = "pysdl2" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "appdirs" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pyopengl" },
|
||||
{ name = "pysdl2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyopengl"
|
||||
version = "3.1.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pysdl2"
|
||||
version = "0.9.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/ff/8704d84ad4d25f0a7bf7912504f64575e432e8d57dfba2fe35f5b2db7e04/pysdl2-0.9.17.tar.gz", hash = "sha256:48c6ef01a4eb123db5f7e46e1a1b565675755b07e615f3fe20a623c94735b52b", size = 775955, upload-time = "2024-12-30T18:07:27.562Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/e2/399ea7900e7510096aeb41439e6f1540bef0bdf7e15cc2d8464e4adb71e8/PySDL2-0.9.17-py3-none-any.whl", hash = "sha256:fe923dbf5c7b27bbc1eb2bf58abfa793f8f13fd7ae8b27b1bc2de49920bcbd41", size = 583137, upload-time = "2024-12-30T18:07:25.987Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
]
|
||||
81
vector.py
81
vector.py
|
|
@ -1,26 +1,26 @@
|
|||
import math
|
||||
import numpy as np
|
||||
|
||||
from OpenGL import GL, GLU
|
||||
import numpy as np
|
||||
from OpenGL import GLU
|
||||
|
||||
|
||||
class Vec3:
|
||||
|
||||
"Basic 3D vector class. Not used very much currently."
|
||||
|
||||
|
||||
def __init__(self, x=0, y=0, z=0):
|
||||
self.x, self.y, self.z = x, y, z
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return 'Vec3 %.4f, %.4f, %.4f' % (self.x, self.y, self.z)
|
||||
|
||||
return "Vec3 %.4f, %.4f, %.4f" % (self.x, self.y, self.z)
|
||||
|
||||
def __sub__(self, b):
|
||||
"Return a new vector subtracted from given other vector."
|
||||
return Vec3(self.x - b.x, self.y - b.y, self.z - b.z)
|
||||
|
||||
|
||||
def length(self):
|
||||
"Return this vector's scalar length."
|
||||
return math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
|
||||
|
||||
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
|
||||
|
||||
def normalize(self):
|
||||
"Return a unit length version of this vector."
|
||||
n = Vec3()
|
||||
|
|
@ -31,26 +31,27 @@ class Vec3:
|
|||
n.y = self.y * ilength
|
||||
n.z = self.z * ilength
|
||||
return n
|
||||
|
||||
|
||||
def cross(self, b):
|
||||
"Return a new vector of cross product with given other vector."
|
||||
x = self.y * b.z - self.z * b.y
|
||||
y = self.z * b.x - self.x * b.z
|
||||
z = self.x * b.y - self.y * b.x
|
||||
return Vec3(x, y, z)
|
||||
|
||||
|
||||
def dot(self, b):
|
||||
"Return scalar dot product with given other vector."
|
||||
return self.x * b.x + self.y * b.y + self.z * b.z
|
||||
|
||||
|
||||
def inverse(self):
|
||||
"Return a new vector that is inverse of this vector."
|
||||
return Vec3(-self.x, -self.y, -self.z)
|
||||
|
||||
|
||||
def copy(self):
|
||||
"Return a copy of this vector."
|
||||
return Vec3(self.x, self.y, self.z)
|
||||
|
||||
|
||||
def get_tiles_along_line(x0, y0, x1, y1):
|
||||
"""
|
||||
Return list of (x,y) tuples for all tiles crossing given worldspace
|
||||
|
|
@ -63,7 +64,7 @@ def get_tiles_along_line(x0, y0, x1, y1):
|
|||
n = 1
|
||||
if dx == 0:
|
||||
x_inc = 0
|
||||
error = float('inf')
|
||||
error = float("inf")
|
||||
elif x1 > x0:
|
||||
x_inc = 1
|
||||
n += math.floor(x1) - x
|
||||
|
|
@ -74,7 +75,7 @@ def get_tiles_along_line(x0, y0, x1, y1):
|
|||
error = (x0 - math.floor(x0)) * dy
|
||||
if dy == 0:
|
||||
y_inc = 0
|
||||
error -= float('inf')
|
||||
error -= float("inf")
|
||||
elif y1 > y0:
|
||||
y_inc = 1
|
||||
n += math.floor(y1) - y
|
||||
|
|
@ -95,11 +96,12 @@ def get_tiles_along_line(x0, y0, x1, y1):
|
|||
n -= 1
|
||||
return tiles
|
||||
|
||||
|
||||
def get_tiles_along_integer_line(x0, y0, x1, y1, cut_corners=True):
|
||||
"""
|
||||
simplified version of get_tiles_along_line using only integer math,
|
||||
also from http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html
|
||||
|
||||
|
||||
cut_corners=True: when a 45 degree line is only intersecting the corners
|
||||
of two tiles, don't count them as overlapped.
|
||||
"""
|
||||
|
|
@ -128,6 +130,7 @@ def get_tiles_along_integer_line(x0, y0, x1, y1, cut_corners=True):
|
|||
n -= 1
|
||||
return tiles
|
||||
|
||||
|
||||
def cut_xyz(x, y, z, threshold):
|
||||
"""
|
||||
Return input x,y,z with each axis clamped to 0 if it's close enough to
|
||||
|
|
@ -138,10 +141,21 @@ def cut_xyz(x, y, z, threshold):
|
|||
z = z if abs(z) > threshold else 0
|
||||
return x, y, z
|
||||
|
||||
def ray_plane_intersection(plane_x, plane_y, plane_z,
|
||||
plane_dir_x, plane_dir_y, plane_dir_z,
|
||||
ray_x, ray_y, ray_z,
|
||||
ray_dir_x, ray_dir_y, ray_dir_z):
|
||||
|
||||
def ray_plane_intersection(
|
||||
plane_x,
|
||||
plane_y,
|
||||
plane_z,
|
||||
plane_dir_x,
|
||||
plane_dir_y,
|
||||
plane_dir_z,
|
||||
ray_x,
|
||||
ray_y,
|
||||
ray_z,
|
||||
ray_dir_x,
|
||||
ray_dir_y,
|
||||
ray_dir_z,
|
||||
):
|
||||
# from http://stackoverflow.com/a/39424162
|
||||
plane = np.array([plane_x, plane_y, plane_z])
|
||||
plane_dir = np.array([plane_dir_x, plane_dir_y, plane_dir_z])
|
||||
|
|
@ -149,13 +163,14 @@ def ray_plane_intersection(plane_x, plane_y, plane_z,
|
|||
ray_dir = np.array([ray_dir_x, ray_dir_y, ray_dir_z])
|
||||
ndotu = plane_dir.dot(ray_dir)
|
||||
if abs(ndotu) < 0.000001:
|
||||
#print ("no intersection or line is within plane")
|
||||
# print ("no intersection or line is within plane")
|
||||
return 0, 0, 0
|
||||
w = ray - plane
|
||||
si = -plane_dir.dot(w) / ndotu
|
||||
psi = w + si * ray_dir + plane
|
||||
return psi[0], psi[1], psi[2]
|
||||
|
||||
|
||||
def screen_to_world(app, screen_x, screen_y):
|
||||
"""
|
||||
Return 3D (float) world space coordinates for given 2D (int) screen space
|
||||
|
|
@ -174,12 +189,23 @@ def screen_to_world(app, screen_x, screen_y):
|
|||
# TODO: what Z is appropriate for game mode picking? test multiple planes?
|
||||
art = app.ui.active_art
|
||||
plane_z = art.layers_z[art.active_layer] if art and not app.game_mode else 0
|
||||
x, y, z = ray_plane_intersection(0, 0, plane_z, # plane loc
|
||||
0, 0, 1, # plane dir
|
||||
end_x, end_y, end_z, # ray origin
|
||||
dir_x, dir_y, dir_z) # ray dir
|
||||
x, y, z = ray_plane_intersection(
|
||||
0,
|
||||
0,
|
||||
plane_z, # plane loc
|
||||
0,
|
||||
0,
|
||||
1, # plane dir
|
||||
end_x,
|
||||
end_y,
|
||||
end_z, # ray origin
|
||||
dir_x,
|
||||
dir_y,
|
||||
dir_z,
|
||||
) # ray dir
|
||||
return x, y, z
|
||||
|
||||
|
||||
def world_to_screen(app, world_x, world_y, world_z):
|
||||
"""
|
||||
Return 2D screen pixel space coordinates for given 3D (float) world space
|
||||
|
|
@ -193,10 +219,11 @@ def world_to_screen(app, world_x, world_y, world_z):
|
|||
x, y, z = GLU.gluProject(world_x, world_y, world_z, vm, pjm, viewport)
|
||||
except:
|
||||
x, y, z = 0, 0, 0
|
||||
app.log('GLU.gluProject failed!')
|
||||
app.log("GLU.gluProject failed!")
|
||||
# does Z mean anything here?
|
||||
return x, y
|
||||
|
||||
|
||||
def world_to_screen_normalized(app, world_x, world_y, world_z):
|
||||
"""
|
||||
Return normalized (-1 to 1) 2D screen space coordinates for given 3D
|
||||
|
|
|
|||
Loading…
Reference in a new issue