Apply ruff auto-fixes and formatting
This commit is contained in:
parent
0bdd700350
commit
1e4d31121b
87 changed files with 8157 additions and 5443 deletions
478
art.py
478
art.py
|
|
@ -1,5 +1,9 @@
|
|||
import os.path, json, time, traceback
|
||||
import random # import random only so art scripts don't have to
|
||||
import json
|
||||
import os.path
|
||||
import random # noqa: F401 -- art scripts exec'd in this scope use it
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import numpy as np
|
||||
|
||||
from edit_command import CommandStack, EntireArtCommand
|
||||
|
|
@ -15,22 +19,22 @@ ELEM_STRIDE = 6
|
|||
UV_STRIDE = 2 * 4
|
||||
|
||||
# starting document defaults
|
||||
DEFAULT_CHARSET = 'c64_petscii'
|
||||
DEFAULT_PALETTE = 'c64_original'
|
||||
DEFAULT_CHARSET = "c64_petscii"
|
||||
DEFAULT_PALETTE = "c64_original"
|
||||
DEFAULT_WIDTH, DEFAULT_HEIGHT = 40, 25
|
||||
DEFAULT_ART_FILENAME = 'new'
|
||||
DEFAULT_ART_FILENAME = "new"
|
||||
|
||||
DEFAULT_FRAME_DELAY = 0.1
|
||||
DEFAULT_LAYER_Z = 0
|
||||
DEFAULT_LAYER_Z_OFFSET = 0.5
|
||||
|
||||
ART_DIR = 'art/'
|
||||
ART_FILE_EXTENSION = 'psci'
|
||||
ART_DIR = "art/"
|
||||
ART_FILE_EXTENSION = "psci"
|
||||
|
||||
THUMBNAIL_CACHE_DIR = 'thumbnails/'
|
||||
THUMBNAIL_CACHE_DIR = "thumbnails/"
|
||||
|
||||
ART_SCRIPT_DIR = 'artscripts/'
|
||||
SCRIPT_FILE_EXTENSION = 'arsc'
|
||||
ART_SCRIPT_DIR = "artscripts/"
|
||||
SCRIPT_FILE_EXTENSION = "arsc"
|
||||
|
||||
# flip/rotate UV constants
|
||||
UV_NORMAL = 0
|
||||
|
|
@ -45,14 +49,14 @@ UV_FLIP90 = 6
|
|||
UV_FLIP270 = 7
|
||||
|
||||
uv_names = {
|
||||
UV_NORMAL: 'Normal',
|
||||
UV_ROTATE90: 'Rotate 90',
|
||||
UV_ROTATE180: 'Rotate 180',
|
||||
UV_ROTATE270: 'Rotate 270',
|
||||
UV_FLIPX: 'Flip X',
|
||||
UV_FLIPY: 'Flip Y',
|
||||
UV_FLIP90: 'Flipped90',
|
||||
UV_FLIP270: 'Flipped270'
|
||||
UV_NORMAL: "Normal",
|
||||
UV_ROTATE90: "Rotate 90",
|
||||
UV_ROTATE180: "Rotate 180",
|
||||
UV_ROTATE270: "Rotate 270",
|
||||
UV_FLIPX: "Flip X",
|
||||
UV_FLIPY: "Flip Y",
|
||||
UV_FLIP90: "Flipped90",
|
||||
UV_FLIP270: "Flipped270",
|
||||
}
|
||||
|
||||
uv_types = {
|
||||
|
|
@ -63,7 +67,7 @@ uv_types = {
|
|||
UV_FLIPX: (1, 0, 0, 0, 1, 1, 0, 1),
|
||||
UV_FLIPY: (0, 1, 1, 1, 0, 0, 1, 0),
|
||||
UV_FLIP90: (0, 0, 0, 1, 1, 0, 1, 1),
|
||||
UV_FLIP270: (1, 1, 1, 0, 0, 1, 0, 0)
|
||||
UV_FLIP270: (1, 1, 1, 0, 0, 1, 0, 0),
|
||||
}
|
||||
|
||||
# reverse dict for easy (+ fast?) lookup in eg get_char_transform_at
|
||||
|
|
@ -75,7 +79,7 @@ uv_types_reverse = {
|
|||
uv_types[UV_FLIPX]: UV_FLIPX,
|
||||
uv_types[UV_FLIPY]: UV_FLIPY,
|
||||
uv_types[UV_FLIP90]: UV_FLIP90,
|
||||
uv_types[UV_FLIP270]: UV_FLIP270
|
||||
uv_types[UV_FLIP270]: UV_FLIP270,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -93,6 +97,7 @@ class Art:
|
|||
- char/color tile values are expressed as indices into charset / palette
|
||||
- all layers in an Art are the same dimensions
|
||||
"""
|
||||
|
||||
quad_width, quad_height = 1.0, 1.0
|
||||
"size of each tile in world space"
|
||||
log_size_changes = False
|
||||
|
|
@ -102,8 +107,8 @@ class Art:
|
|||
def __init__(self, filename, app, charset, palette, width, height):
|
||||
"Creates a new, blank document with given parameters."
|
||||
self.valid = False
|
||||
if filename and not filename.endswith('.%s' % ART_FILE_EXTENSION):
|
||||
filename += '.%s' % ART_FILE_EXTENSION
|
||||
if filename and not filename.endswith(".%s" % ART_FILE_EXTENSION):
|
||||
filename += ".%s" % ART_FILE_EXTENSION
|
||||
self.filename = filename
|
||||
self.app = app
|
||||
# save "time loaded" for menu sorting
|
||||
|
|
@ -113,7 +118,7 @@ class Art:
|
|||
self.unsaved_changes = False
|
||||
self.width, self.height = width, height
|
||||
# selected char/fg/bg/xform
|
||||
self.selected_char = self.charset.get_char_index('A') or 2
|
||||
self.selected_char = self.charset.get_char_index("A") or 2
|
||||
self.selected_fg_color = self.palette.lightest_index
|
||||
self.selected_bg_color = self.palette.darkest_index
|
||||
self.selected_xform = UV_NORMAL
|
||||
|
|
@ -156,12 +161,12 @@ class Art:
|
|||
self.valid = True
|
||||
|
||||
def log_init(self):
|
||||
self.app.log('created new document:')
|
||||
self.app.log(' character set: %s' % self.charset.name)
|
||||
self.app.log(' palette: %s' % self.palette.name)
|
||||
self.app.log(' width/height: %s x %s' % (self.width, self.height))
|
||||
self.app.log(' frames: %s' % self.frames)
|
||||
self.app.log(' layers: %s' % self.layers)
|
||||
self.app.log("created new document:")
|
||||
self.app.log(" character set: %s" % self.charset.name)
|
||||
self.app.log(" palette: %s" % self.palette.name)
|
||||
self.app.log(" width/height: %s x %s" % (self.width, self.height))
|
||||
self.app.log(" frames: %s" % self.frames)
|
||||
self.app.log(" layers: %s" % self.layers)
|
||||
|
||||
def init_layers(self):
|
||||
self.layers = 1
|
||||
|
|
@ -170,7 +175,7 @@ class Art:
|
|||
# lists of layer Z values and names
|
||||
self.layers_z = [DEFAULT_LAYER_Z]
|
||||
self.layers_visibility = [True]
|
||||
self.layer_names = ['Layer 1']
|
||||
self.layer_names = ["Layer 1"]
|
||||
|
||||
def init_frames(self):
|
||||
self.frames = 0
|
||||
|
|
@ -214,7 +219,7 @@ class Art:
|
|||
if self.app.ui and self is self.app.ui.active_art:
|
||||
self.app.ui.set_active_frame(index)
|
||||
if log:
|
||||
self.app.log('Created new frame at index %s' % str(index))
|
||||
self.app.log("Created new frame at index %s" % str(index))
|
||||
|
||||
def add_frame_to_end(self, delay=DEFAULT_FRAME_DELAY, log=True):
|
||||
"Add a blank frame at the end of the current animation."
|
||||
|
|
@ -223,7 +228,9 @@ class Art:
|
|||
def duplicate_frame(self, src_frame_index, dest_frame_index=None, delay=None):
|
||||
"Create a duplicate of given frame at given index."
|
||||
# stick new frame at end if no destination index given
|
||||
dest_frame_index = dest_frame_index if dest_frame_index is not None else self.frames
|
||||
dest_frame_index = (
|
||||
dest_frame_index if dest_frame_index is not None else self.frames
|
||||
)
|
||||
# copy source frame's delay if none given
|
||||
delay = delay or self.frame_delays[src_frame_index]
|
||||
self.frames += 1
|
||||
|
|
@ -238,7 +245,9 @@ class Art:
|
|||
# set new frame as active
|
||||
if self is self.app.ui.active_art:
|
||||
self.app.ui.set_active_frame(dest_frame_index - 1)
|
||||
self.app.log('Duplicated frame %s at frame %s' % (src_frame_index+1, dest_frame_index))
|
||||
self.app.log(
|
||||
"Duplicated frame %s at frame %s" % (src_frame_index + 1, dest_frame_index)
|
||||
)
|
||||
|
||||
def delete_frame_at(self, index):
|
||||
"Delete frame at given index."
|
||||
|
|
@ -282,9 +291,11 @@ class Art:
|
|||
|
||||
def duplicate_layer(self, src_index, z=None, new_name=None):
|
||||
"Duplicate layer with given index. Duplicate uses given Z and name."
|
||||
|
||||
def duplicate_layer_array(array):
|
||||
src_data = np.array([array[src_index]])
|
||||
return np.append(array, src_data, 0)
|
||||
|
||||
for frame in range(self.frames):
|
||||
self.chars[frame] = duplicate_layer_array(self.chars[frame])
|
||||
self.fg_colors[frame] = duplicate_layer_array(self.fg_colors[frame])
|
||||
|
|
@ -295,7 +306,7 @@ class Art:
|
|||
z = z if z is not None else self.layers_z[src_index]
|
||||
self.layers_z.append(z)
|
||||
self.layers_visibility.append(True)
|
||||
new_name = new_name or 'Copy of %s' % self.layer_names[src_index]
|
||||
new_name = new_name or "Copy of %s" % self.layer_names[src_index]
|
||||
self.layer_names.append(new_name)
|
||||
# rebuild geo with added verts for new layer
|
||||
self.geo_changed = True
|
||||
|
|
@ -304,7 +315,7 @@ class Art:
|
|||
self.app.ui.set_active_layer(self.layers - 1)
|
||||
# don't log new layers created on the fly in game mode
|
||||
if not self.app.game_mode:
|
||||
self.app.log('Added new layer %s' % new_name)
|
||||
self.app.log("Added new layer %s" % new_name)
|
||||
self.set_unsaved_changes(True)
|
||||
|
||||
def clear_frame_layer(self, frame, layer, bg_color=0, fg_color=None):
|
||||
|
|
@ -348,11 +359,15 @@ class Art:
|
|||
self.charset = new_charset
|
||||
if self.recalc_quad_height:
|
||||
self.quad_width = 1.0
|
||||
self.quad_height = 1.0 * (self.charset.char_height / self.charset.char_width)
|
||||
self.quad_height = 1.0 * (
|
||||
self.charset.char_height / self.charset.char_width
|
||||
)
|
||||
self.set_unsaved_changes(True)
|
||||
self.geo_changed = True
|
||||
if log:
|
||||
self.app.ui.message_line.post_line('Character set changed to %s' % self.charset.name)
|
||||
self.app.ui.message_line.post_line(
|
||||
"Character set changed to %s" % self.charset.name
|
||||
)
|
||||
|
||||
def set_charset_by_name(self, new_charset_name):
|
||||
charset = self.app.load_charset(new_charset_name)
|
||||
|
|
@ -366,7 +381,9 @@ class Art:
|
|||
self.palette = new_palette
|
||||
self.set_unsaved_changes(True)
|
||||
if log:
|
||||
self.app.ui.message_line.post_line('Color palette changed to %s' % self.palette.name)
|
||||
self.app.ui.message_line.post_line(
|
||||
"Color palette changed to %s" % self.palette.name
|
||||
)
|
||||
|
||||
def set_palette_by_name(self, new_palette_name):
|
||||
palette = self.app.load_palette(new_palette_name)
|
||||
|
|
@ -396,8 +413,13 @@ class Art:
|
|||
crop_x = new_width < self.width
|
||||
crop_y = new_height < self.height
|
||||
for frame in range(self.frames):
|
||||
for array in [self.chars, self.fg_colors, self.bg_colors,
|
||||
self.uv_mods, self.uv_maps]:
|
||||
for array in [
|
||||
self.chars,
|
||||
self.fg_colors,
|
||||
self.bg_colors,
|
||||
self.uv_mods,
|
||||
self.uv_maps,
|
||||
]:
|
||||
if crop_x:
|
||||
array[frame] = array[frame].take(range(x0, x1), axis=2)
|
||||
if crop_y:
|
||||
|
|
@ -406,6 +428,7 @@ class Art:
|
|||
def expand(self, new_width, new_height, bg_fill):
|
||||
x_add = new_width - self.width
|
||||
y_add = new_height - self.height
|
||||
|
||||
# print('%s expand: %sw + %s = %s, %sh + %s = %s' % (self.filename,
|
||||
# self.width, x_add, new_width, self.height, y_add, new_height))
|
||||
def expand_array(array, fill_value, stride):
|
||||
|
|
@ -424,6 +447,7 @@ class Art:
|
|||
array = np.append(array, add, 1)
|
||||
# can't modify passed array in-place
|
||||
return array
|
||||
|
||||
for frame in range(self.frames):
|
||||
self.chars[frame] = expand_array(self.chars[frame], 0, 4)
|
||||
fg, bg = 0, 0
|
||||
|
|
@ -435,7 +459,9 @@ class Art:
|
|||
bg = self.app.ui.selected_bg_color
|
||||
self.fg_colors[frame] = expand_array(self.fg_colors[frame], fg, 4)
|
||||
self.bg_colors[frame] = expand_array(self.bg_colors[frame], bg, 4)
|
||||
self.uv_mods[frame] = expand_array(self.uv_mods[frame], uv_types[UV_NORMAL], UV_STRIDE)
|
||||
self.uv_mods[frame] = expand_array(
|
||||
self.uv_mods[frame], uv_types[UV_NORMAL], UV_STRIDE
|
||||
)
|
||||
self.uv_maps[frame] = expand_array(self.uv_maps[frame], UV_NORMAL, 4)
|
||||
|
||||
def mark_frame_changed(self, frame):
|
||||
|
|
@ -562,7 +588,8 @@ class Art:
|
|||
Set (fg or bg) color index for given frame/layer/x,y tile.
|
||||
Foreground or background specified with "fg" boolean.
|
||||
"""
|
||||
if color_index is None: return
|
||||
if color_index is None:
|
||||
return
|
||||
# modulo to resolve any negative indices
|
||||
if 0 < color_index >= len(self.palette.colors):
|
||||
color_index %= len(self.palette.colors)
|
||||
|
|
@ -602,8 +629,18 @@ class Art:
|
|||
self.uv_maps[frame][layer][y][x] = transform
|
||||
self.uv_changed_frames[frame] = True
|
||||
|
||||
def set_tile_at(self, frame, layer, x, y, char_index=None, fg=None, bg=None,
|
||||
transform=None, set_all=False):
|
||||
def set_tile_at(
|
||||
self,
|
||||
frame,
|
||||
layer,
|
||||
x,
|
||||
y,
|
||||
char_index=None,
|
||||
fg=None,
|
||||
bg=None,
|
||||
transform=None,
|
||||
set_all=False,
|
||||
):
|
||||
"""
|
||||
Convenience function for setting all tile attributes (character index,
|
||||
foreground and background color, and transofmr) at once.
|
||||
|
|
@ -632,13 +669,25 @@ class Art:
|
|||
for layer in range(self.layers):
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_char_transform_at(frame, layer, x, y, flip_dict[self.get_char_transform_at(frame, layer, x, y)])
|
||||
self.set_char_transform_at(
|
||||
frame,
|
||||
layer,
|
||||
x,
|
||||
y,
|
||||
flip_dict[self.get_char_transform_at(frame, layer, x, y)],
|
||||
)
|
||||
|
||||
def flip_horizontal(self, frame, layer):
|
||||
"Mirrors Art left-to-right."
|
||||
command = EntireArtCommand(self)
|
||||
command.save_tiles(before=True)
|
||||
for a in [self.chars, self.fg_colors, self.bg_colors, self.uv_mods, self.uv_maps]:
|
||||
for a in [
|
||||
self.chars,
|
||||
self.fg_colors,
|
||||
self.bg_colors,
|
||||
self.uv_mods,
|
||||
self.uv_maps,
|
||||
]:
|
||||
a[frame][layer] = np.fliplr(a[frame][layer])
|
||||
if self.app.ui.flip_affects_xforms:
|
||||
flips = {
|
||||
|
|
@ -649,7 +698,7 @@ class Art:
|
|||
UV_ROTATE90: UV_FLIP90,
|
||||
UV_FLIP90: UV_ROTATE90,
|
||||
UV_ROTATE270: UV_FLIP270,
|
||||
UV_FLIP270: UV_ROTATE270
|
||||
UV_FLIP270: UV_ROTATE270,
|
||||
}
|
||||
self.flip_all_xforms(flips)
|
||||
self.mark_frame_changed(frame)
|
||||
|
|
@ -661,7 +710,13 @@ class Art:
|
|||
"Flips Art upside down."
|
||||
command = EntireArtCommand(self)
|
||||
command.save_tiles(before=True)
|
||||
for a in [self.chars, self.fg_colors, self.bg_colors, self.uv_mods, self.uv_maps]:
|
||||
for a in [
|
||||
self.chars,
|
||||
self.fg_colors,
|
||||
self.bg_colors,
|
||||
self.uv_mods,
|
||||
self.uv_maps,
|
||||
]:
|
||||
a[frame][layer] = np.flipud(a[frame][layer])
|
||||
if self.app.ui.flip_affects_xforms:
|
||||
flips = {
|
||||
|
|
@ -673,7 +728,7 @@ class Art:
|
|||
UV_ROTATE90: UV_FLIP270,
|
||||
UV_FLIP270: UV_ROTATE90,
|
||||
UV_ROTATE270: UV_FLIP90,
|
||||
UV_FLIP90: UV_ROTATE270
|
||||
UV_FLIP90: UV_ROTATE270,
|
||||
}
|
||||
self.flip_all_xforms(flips)
|
||||
self.mark_frame_changed(frame)
|
||||
|
|
@ -683,7 +738,13 @@ class Art:
|
|||
|
||||
def shift(self, frame, layer, amount_x, amount_y):
|
||||
"Shift + wrap art on given frame and layer by given amount in X and Y."
|
||||
for a in [self.chars, self.fg_colors, self.bg_colors, self.uv_mods, self.uv_maps]:
|
||||
for a in [
|
||||
self.chars,
|
||||
self.fg_colors,
|
||||
self.bg_colors,
|
||||
self.uv_mods,
|
||||
self.uv_maps,
|
||||
]:
|
||||
a[frame][layer] = np.roll(a[frame][layer], amount_x, 1)
|
||||
a[frame][layer] = np.roll(a[frame][layer], amount_y, 0)
|
||||
self.mark_frame_changed(frame)
|
||||
|
|
@ -704,11 +765,13 @@ class Art:
|
|||
self.selected_xform = self.app.ui.selected_xform
|
||||
|
||||
def changed_this_frame(self):
|
||||
return self.geo_changed or \
|
||||
True in self.char_changed_frames.values() or \
|
||||
True in self.fg_changed_frames.values() or \
|
||||
True in self.bg_changed_frames.values() or \
|
||||
True in self.uv_changed_frames.values()
|
||||
return (
|
||||
self.geo_changed
|
||||
or True in self.char_changed_frames.values()
|
||||
or True in self.fg_changed_frames.values()
|
||||
or True in self.bg_changed_frames.values()
|
||||
or True in self.uv_changed_frames.values()
|
||||
)
|
||||
|
||||
def update(self):
|
||||
self.update_scripts()
|
||||
|
|
@ -752,33 +815,39 @@ class Art:
|
|||
filedir = os.path.dirname(self.filename)
|
||||
if not os.path.exists(filedir):
|
||||
# self.app.log('Tried to save to directory %s which does not exist!' % filedir, error=True)
|
||||
new_path = self.app.documents_dir + ART_DIR + os.path.basename(self.filename)
|
||||
new_path = (
|
||||
self.app.documents_dir + ART_DIR + os.path.basename(self.filename)
|
||||
)
|
||||
self.set_filename(new_path)
|
||||
start_time = time.time()
|
||||
# cursor might be hovering, undo any preview changes
|
||||
for edit in self.app.cursor.preview_edits:
|
||||
edit.undo()
|
||||
d = {'width': self.width, 'height': self.height,
|
||||
'charset': self.charset.name, 'palette': self.palette.name,
|
||||
'active_frame': self.active_frame,
|
||||
'active_layer': self.active_layer,
|
||||
'camera': (self.camera_x, self.camera_y, self.camera_z),
|
||||
'selected_char': int(self.selected_char),
|
||||
'selected_fg_color': int(self.selected_fg_color),
|
||||
'selected_bg_color': int(self.selected_bg_color),
|
||||
'selected_xform': int(self.selected_xform)
|
||||
d = {
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"charset": self.charset.name,
|
||||
"palette": self.palette.name,
|
||||
"active_frame": self.active_frame,
|
||||
"active_layer": self.active_layer,
|
||||
"camera": (self.camera_x, self.camera_y, self.camera_z),
|
||||
"selected_char": int(self.selected_char),
|
||||
"selected_fg_color": int(self.selected_fg_color),
|
||||
"selected_bg_color": int(self.selected_bg_color),
|
||||
"selected_xform": int(self.selected_xform),
|
||||
}
|
||||
# preferred character set and palette, default used if not found
|
||||
# remember camera location
|
||||
# frames and layers are dicts w/ lists of their data + a few properties
|
||||
frames = []
|
||||
for frame_index in range(self.frames):
|
||||
frame = { 'delay': self.frame_delays[frame_index] }
|
||||
frame = {"delay": self.frame_delays[frame_index]}
|
||||
layers = []
|
||||
for layer_index in range(self.layers):
|
||||
layer = {'z': self.layers_z[layer_index],
|
||||
'visible': int(self.layers_visibility[layer_index]),
|
||||
'name': self.layer_names[layer_index]
|
||||
layer = {
|
||||
"z": self.layers_z[layer_index],
|
||||
"visible": int(self.layers_visibility[layer_index]),
|
||||
"name": self.layer_names[layer_index],
|
||||
}
|
||||
tiles = []
|
||||
for y in range(self.height):
|
||||
|
|
@ -787,28 +856,32 @@ class Art:
|
|||
fg = int(self.fg_colors[frame_index][layer_index][y][x][0])
|
||||
bg = int(self.bg_colors[frame_index][layer_index][y][x][0])
|
||||
# use get method for transform, data's not simply an int
|
||||
xform = int(self.get_char_transform_at(frame_index, layer_index, x, y))
|
||||
tiles.append({'char': char, 'fg': fg, 'bg': bg, 'xform': xform})
|
||||
layer['tiles'] = tiles
|
||||
xform = int(
|
||||
self.get_char_transform_at(frame_index, layer_index, x, y)
|
||||
)
|
||||
tiles.append({"char": char, "fg": fg, "bg": bg, "xform": xform})
|
||||
layer["tiles"] = tiles
|
||||
layers.append(layer)
|
||||
frame['layers'] = layers
|
||||
frame["layers"] = layers
|
||||
frames.append(frame)
|
||||
d['frames'] = frames
|
||||
d["frames"] = frames
|
||||
# MAYBE-TODO: below gives not-so-pretty-printing, find out way to control
|
||||
# formatting for better output
|
||||
json.dump(d, open(self.filename, 'w'), sort_keys=True, indent=1)
|
||||
json.dump(d, open(self.filename, "w"), sort_keys=True, indent=1)
|
||||
end_time = time.time()
|
||||
self.set_unsaved_changes(False)
|
||||
# self.app.log('saved %s to disk in %.5f seconds' % (self.filename, end_time - start_time))
|
||||
self.app.log('saved %s' % self.filename)
|
||||
self.app.log("saved %s" % self.filename)
|
||||
# remove old thumbnail
|
||||
thumb_dir = self.app.cache_dir + THUMBNAIL_CACHE_DIR
|
||||
if os.path.exists(self.filename):
|
||||
old_thumb_filename = thumb_dir + self.app.get_file_hash(self.filename) + '.png'
|
||||
old_thumb_filename = (
|
||||
thumb_dir + self.app.get_file_hash(self.filename) + ".png"
|
||||
)
|
||||
if os.path.exists(old_thumb_filename):
|
||||
os.remove(old_thumb_filename)
|
||||
# write thumbnail
|
||||
new_thumb_filename = thumb_dir + self.app.get_file_hash(self.filename) + '.png'
|
||||
new_thumb_filename = thumb_dir + self.app.get_file_hash(self.filename) + ".png"
|
||||
write_thumbnail(self.app, self.filename, new_thumb_filename)
|
||||
# thumbnail write process actually sets active frame! set it back
|
||||
for r in self.renderables:
|
||||
|
|
@ -824,34 +897,48 @@ class Art:
|
|||
# - support multiple save+load code paths for different save versions
|
||||
def get_flat_int_list(layer_array):
|
||||
return list(map(int, layer_array.flatten()))[::4]
|
||||
|
||||
start_time = time.time()
|
||||
d = {'width': self.width, 'height': self.height,
|
||||
'charset': self.charset.name, 'palette': self.palette.name,
|
||||
'active_frame': self.active_frame,
|
||||
'active_layer': self.active_layer,
|
||||
'camera': (self.camera_x, self.camera_y, self.camera_z)
|
||||
d = {
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"charset": self.charset.name,
|
||||
"palette": self.palette.name,
|
||||
"active_frame": self.active_frame,
|
||||
"active_layer": self.active_layer,
|
||||
"camera": (self.camera_x, self.camera_y, self.camera_z),
|
||||
}
|
||||
frames = []
|
||||
for frame_index in range(self.frames):
|
||||
frame = { 'delay': self.frame_delays[frame_index] }
|
||||
frame = {"delay": self.frame_delays[frame_index]}
|
||||
layers = []
|
||||
for layer_index in range(self.layers):
|
||||
layer = {'z': self.layers_z[layer_index],
|
||||
'visible': int(self.layers_visibility[layer_index]),
|
||||
'name': self.layer_names[layer_index]
|
||||
layer = {
|
||||
"z": self.layers_z[layer_index],
|
||||
"visible": int(self.layers_visibility[layer_index]),
|
||||
"name": self.layer_names[layer_index],
|
||||
}
|
||||
# compile lists-of-ints for chars, colors, xforms
|
||||
layer['chars'] = get_flat_int_list(self.chars[frame_index][layer_index])
|
||||
layer['fgs'] = get_flat_int_list(self.fg_colors[frame_index][layer_index])
|
||||
layer['bgs'] = get_flat_int_list(self.bg_colors[frame_index][layer_index])
|
||||
layer['xforms'] = get_flat_int_list(self.uv_maps[frame_index][layer_index])
|
||||
layer["chars"] = get_flat_int_list(self.chars[frame_index][layer_index])
|
||||
layer["fgs"] = get_flat_int_list(
|
||||
self.fg_colors[frame_index][layer_index]
|
||||
)
|
||||
layer["bgs"] = get_flat_int_list(
|
||||
self.bg_colors[frame_index][layer_index]
|
||||
)
|
||||
layer["xforms"] = get_flat_int_list(
|
||||
self.uv_maps[frame_index][layer_index]
|
||||
)
|
||||
layers.append(layer)
|
||||
frame['layers'] = layers
|
||||
frame["layers"] = layers
|
||||
frames.append(frame)
|
||||
d['frames'] = frames
|
||||
json.dump(d, open(self.filename + '2', 'w'), sort_keys=True, indent=None)
|
||||
d["frames"] = frames
|
||||
json.dump(d, open(self.filename + "2", "w"), sort_keys=True, indent=None)
|
||||
end_time = time.time()
|
||||
self.app.log('ALT saved %s to disk in %.5f seconds' % (self.filename, end_time - start_time))
|
||||
self.app.log(
|
||||
"ALT saved %s to disk in %.5f seconds"
|
||||
% (self.filename, end_time - start_time)
|
||||
)
|
||||
|
||||
def set_unsaved_changes(self, new_status):
|
||||
"Mark this Art as having unsaved changes in Art Mode."
|
||||
|
|
@ -863,8 +950,8 @@ class Art:
|
|||
def set_filename(self, new_filename):
|
||||
"Change Art's filename to new given string."
|
||||
# append extension if missing
|
||||
if not new_filename.endswith('.' + ART_FILE_EXTENSION):
|
||||
new_filename += '.' + ART_FILE_EXTENSION
|
||||
if not new_filename.endswith("." + ART_FILE_EXTENSION):
|
||||
new_filename += "." + ART_FILE_EXTENSION
|
||||
# if no dir given, assume documents/art/ dir
|
||||
if os.path.basename(new_filename) == new_filename:
|
||||
new_dir = self.app.documents_dir
|
||||
|
|
@ -898,15 +985,16 @@ class Art:
|
|||
exec(open(script_filename).read())
|
||||
# (assume script changed art)
|
||||
self.unsaved_changes = True
|
||||
logline = 'Executed %s' % script_filename
|
||||
if log: self.app.log(logline)
|
||||
logline = "Executed %s" % script_filename
|
||||
if log:
|
||||
self.app.log(logline)
|
||||
error = False
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
error = True
|
||||
logline = 'Error executing %s:' % script_filename
|
||||
logline = "Error executing %s:" % script_filename
|
||||
self.app.log(logline)
|
||||
# skip first 3 lines of callstack before artscript exec
|
||||
for line in traceback.format_exc().split('\n')[3:]:
|
||||
for line in traceback.format_exc().split("\n")[3:]:
|
||||
if line.strip():
|
||||
self.app.log(line.rstrip())
|
||||
# write "after" state of command and commit
|
||||
|
|
@ -921,9 +1009,11 @@ class Art:
|
|||
return script_filename and script_filename in self.scripts
|
||||
|
||||
def get_valid_script_filename(self, script_filename):
|
||||
if not type(script_filename) is str: return None
|
||||
return self.app.find_filename_path(script_filename, ART_SCRIPT_DIR,
|
||||
SCRIPT_FILE_EXTENSION)
|
||||
if type(script_filename) is not str:
|
||||
return None
|
||||
return self.app.find_filename_path(
|
||||
script_filename, ART_SCRIPT_DIR, SCRIPT_FILE_EXTENSION
|
||||
)
|
||||
|
||||
def run_script_every(self, script_filename, rate=0.1):
|
||||
"Start a script running on this Art at a regular rate."
|
||||
|
|
@ -931,7 +1021,7 @@ class Art:
|
|||
if not script_filename:
|
||||
return
|
||||
if script_filename in self.scripts:
|
||||
self.app.log('script %s is already running.' % script_filename)
|
||||
self.app.log("script %s is already running." % script_filename)
|
||||
return
|
||||
# add to "scripts currently running" list
|
||||
self.scripts.append(script_filename)
|
||||
|
|
@ -946,7 +1036,7 @@ class Art:
|
|||
script_filename = self.get_valid_script_filename(script_filename)
|
||||
if not script_filename:
|
||||
return
|
||||
if not script_filename in self.scripts:
|
||||
if script_filename not in self.scripts:
|
||||
self.app.log("script %s exists but isn't running." % script_filename)
|
||||
return
|
||||
script_index = self.scripts.index(script_filename)
|
||||
|
|
@ -972,8 +1062,9 @@ class Art:
|
|||
self.unsaved_changes = True
|
||||
self.scripts_next_exec_time[i] += self.script_rates[i]
|
||||
|
||||
def clear_line(self, frame, layer, line_y, fg_color_index=None,
|
||||
bg_color_index=None):
|
||||
def clear_line(
|
||||
self, frame, layer, line_y, fg_color_index=None, bg_color_index=None
|
||||
):
|
||||
"Clear characters on given horizontal line, to optional given colors."
|
||||
# TODO: use numpy slicing to do this much more quickly!
|
||||
for x in range(self.width):
|
||||
|
|
@ -983,8 +1074,17 @@ class Art:
|
|||
if bg_color_index:
|
||||
self.set_color_at(frame, layer, x, line_y, bg_color_index, False)
|
||||
|
||||
def write_string(self, frame, layer, x, y, text, fg_color_index=None,
|
||||
bg_color_index=None, right_justify=False):
|
||||
def write_string(
|
||||
self,
|
||||
frame,
|
||||
layer,
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
fg_color_index=None,
|
||||
bg_color_index=None,
|
||||
right_justify=False,
|
||||
):
|
||||
"""
|
||||
Write given string starting at given frame/layer/x,y tile, with
|
||||
optional given colors, left-justified by default.
|
||||
|
|
@ -1008,8 +1108,20 @@ class Art:
|
|||
self.set_color_at(frame, layer, x + x_offset, y, bg_color_index, False)
|
||||
x_offset += 1
|
||||
|
||||
def composite_to(self, src_frame, src_layer, src_x, src_y, width, height,
|
||||
dest_art, dest_frame, dest_layer, dest_x, dest_y):
|
||||
def composite_to(
|
||||
self,
|
||||
src_frame,
|
||||
src_layer,
|
||||
src_x,
|
||||
src_y,
|
||||
width,
|
||||
height,
|
||||
dest_art,
|
||||
dest_frame,
|
||||
dest_layer,
|
||||
dest_x,
|
||||
dest_y,
|
||||
):
|
||||
for y in range(src_y, src_y + height):
|
||||
for x in range(src_x, src_x + width):
|
||||
# never try to write out of bounds on dest art; let user be lazy
|
||||
|
|
@ -1028,15 +1140,36 @@ class Art:
|
|||
dy = dest_y + (y - src_y)
|
||||
# transparent bg -> keep dest bg, else use entire src tile
|
||||
if self.get_bg_color_index_at(src_frame, src_layer, x, y) == 0:
|
||||
bg = dest_art.get_bg_color_index_at(dest_frame, dest_layer,
|
||||
dx, dy)
|
||||
dest_art.set_tile_at(dest_frame, dest_layer, dx, dy,
|
||||
ch, fg, bg, xform)
|
||||
bg = dest_art.get_bg_color_index_at(dest_frame, dest_layer, dx, dy)
|
||||
dest_art.set_tile_at(dest_frame, dest_layer, dx, dy, ch, fg, bg, xform)
|
||||
|
||||
def composite_from(self, src_art, src_frame, src_layer, src_x, src_y,
|
||||
width, height, dest_frame, dest_layer, dest_x, dest_y):
|
||||
src_art.composite_to(src_frame, src_layer, src_x, src_y, width, height,
|
||||
self, dest_frame, dest_layer, dest_x, dest_y)
|
||||
def composite_from(
|
||||
self,
|
||||
src_art,
|
||||
src_frame,
|
||||
src_layer,
|
||||
src_x,
|
||||
src_y,
|
||||
width,
|
||||
height,
|
||||
dest_frame,
|
||||
dest_layer,
|
||||
dest_x,
|
||||
dest_y,
|
||||
):
|
||||
src_art.composite_to(
|
||||
src_frame,
|
||||
src_layer,
|
||||
src_x,
|
||||
src_y,
|
||||
width,
|
||||
height,
|
||||
self,
|
||||
dest_frame,
|
||||
dest_layer,
|
||||
dest_x,
|
||||
dest_y,
|
||||
)
|
||||
|
||||
def get_filtered_tiles(self, frame, layer, char_value, invert_filter=False):
|
||||
"Return list of (x,y) tile coords that match (or don't) a char value."
|
||||
|
|
@ -1044,8 +1177,9 @@ class Art:
|
|||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
char = self.get_char_index_at(frame, layer, x, y)
|
||||
if (not invert_filter and char == char_value) or \
|
||||
(invert_filter and char != char_value):
|
||||
if (not invert_filter and char == char_value) or (
|
||||
invert_filter and char != char_value
|
||||
):
|
||||
tiles.append((x, y))
|
||||
return tiles
|
||||
|
||||
|
|
@ -1063,91 +1197,91 @@ class Art:
|
|||
|
||||
class ArtFromDisk(Art):
|
||||
"Subclass of Art that loads from a file. Main difference is initialization."
|
||||
|
||||
def __init__(self, filename, app):
|
||||
self.valid = False
|
||||
try:
|
||||
d = json.load(open(filename))
|
||||
except:
|
||||
return
|
||||
width = d['width']
|
||||
height = d['height']
|
||||
charset = app.load_charset(d['charset'])
|
||||
width = d["width"]
|
||||
height = d["height"]
|
||||
charset = app.load_charset(d["charset"])
|
||||
if not charset:
|
||||
app.log('Character set %s not found!' % d['charset'])
|
||||
app.log("Character set %s not found!" % d["charset"])
|
||||
return
|
||||
palette = app.load_palette(d['palette'])
|
||||
palette = app.load_palette(d["palette"])
|
||||
if not palette:
|
||||
app.log('Palette %s not found!' % d['palette'])
|
||||
app.log("Palette %s not found!" % d["palette"])
|
||||
return
|
||||
# store loaded data for init_layers/frames
|
||||
self.loaded_data = d
|
||||
# base Art class initializes all vars, thereafter we just populate
|
||||
Art.__init__(self, filename, app, charset, palette,
|
||||
width, height)
|
||||
Art.__init__(self, filename, app, charset, palette, width, height)
|
||||
# still loading...
|
||||
self.valid = False
|
||||
if not self.app.override_saved_camera:
|
||||
cam = d['camera']
|
||||
cam = d["camera"]
|
||||
self.camera_x, self.camera_y, self.camera_z = cam[0], cam[1], cam[2]
|
||||
else:
|
||||
self.update_saved_camera(self.app.camera)
|
||||
# read saved tile attributes, which won't exist in pre-0.9.6 PSCI files
|
||||
if 'selected_char' in d:
|
||||
self.selected_char = d['selected_char']
|
||||
if 'selected_fg_color' in d:
|
||||
self.selected_fg_color = d['selected_fg_color']
|
||||
if 'selected_bg_color' in d:
|
||||
self.selected_bg_color = d['selected_bg_color']
|
||||
if 'selected_xform' in d:
|
||||
self.selected_xform = d['selected_xform']
|
||||
if "selected_char" in d:
|
||||
self.selected_char = d["selected_char"]
|
||||
if "selected_fg_color" in d:
|
||||
self.selected_fg_color = d["selected_fg_color"]
|
||||
if "selected_bg_color" in d:
|
||||
self.selected_bg_color = d["selected_bg_color"]
|
||||
if "selected_xform" in d:
|
||||
self.selected_xform = d["selected_xform"]
|
||||
# update renderables with new data
|
||||
self.update()
|
||||
# signify to app that this file loaded successfully
|
||||
self.valid = True
|
||||
|
||||
def log_init(self):
|
||||
self.app.log('Loaded %s from disk:' % filename)
|
||||
self.app.log(' character set: %s' % self.charset.name)
|
||||
self.app.log(' palette: %s' % self.palette.name)
|
||||
self.app.log(' width/height: %s x %s' % (self.width, self.height))
|
||||
self.app.log(' frames: %s' % self.frames)
|
||||
self.app.log(' layers: %s' % self.layers)
|
||||
self.app.log("Loaded %s from disk:" % filename)
|
||||
self.app.log(" character set: %s" % self.charset.name)
|
||||
self.app.log(" palette: %s" % self.palette.name)
|
||||
self.app.log(" width/height: %s x %s" % (self.width, self.height))
|
||||
self.app.log(" frames: %s" % self.frames)
|
||||
self.app.log(" layers: %s" % self.layers)
|
||||
|
||||
def init_layers(self):
|
||||
frames = self.loaded_data['frames']
|
||||
frames = self.loaded_data["frames"]
|
||||
# number of layers should be same for all frames
|
||||
self.layers = len(frames[0]['layers'])
|
||||
self.layers = len(frames[0]["layers"])
|
||||
self.layers_z, self.layers_visibility, self.layer_names = [], [], []
|
||||
for i,layer in enumerate(frames[0]['layers']):
|
||||
self.layers_z.append(layer['z'])
|
||||
self.layers_visibility.append(bool(layer.get('visible', 1)))
|
||||
for i, layer in enumerate(frames[0]["layers"]):
|
||||
self.layers_z.append(layer["z"])
|
||||
self.layers_visibility.append(bool(layer.get("visible", 1)))
|
||||
layer_num = str(i + 1)
|
||||
self.layer_names.append(layer.get('name', 'Layer %s' % layer_num))
|
||||
active_layer = self.loaded_data.get('active_layer', 0)
|
||||
self.layer_names.append(layer.get("name", "Layer %s" % layer_num))
|
||||
active_layer = self.loaded_data.get("active_layer", 0)
|
||||
self.set_active_layer(active_layer)
|
||||
|
||||
def init_frames(self):
|
||||
frames = self.loaded_data['frames']
|
||||
frames = self.loaded_data["frames"]
|
||||
self.frames = len(frames)
|
||||
self.active_frame = 0
|
||||
self.frame_delays = []
|
||||
# build tile data arrays from frame+layer lists
|
||||
shape = (self.layers, self.height, self.width, 4)
|
||||
for frame in frames:
|
||||
self.frame_delays.append(frame['delay'])
|
||||
self.frame_delays.append(frame["delay"])
|
||||
chars = np.zeros(shape, dtype=np.float32)
|
||||
uvs = self.new_uv_layers(self.layers)
|
||||
uv_maps = np.zeros(shape, dtype=np.uint32)
|
||||
fg_colors = chars.copy()
|
||||
bg_colors = chars.copy()
|
||||
for layer_index,layer in enumerate(frame['layers']):
|
||||
for layer_index, layer in enumerate(frame["layers"]):
|
||||
x, y = 0, 0
|
||||
for tile in layer['tiles']:
|
||||
chars[layer_index][y][x] = tile['char']
|
||||
fg_colors[layer_index][y][x] = tile['fg']
|
||||
bg_colors[layer_index][y][x] = tile['bg']
|
||||
uvs[layer_index][y][x] = uv_types[tile.get('xform', UV_NORMAL)]
|
||||
uv_maps[layer_index][y][x] = tile.get('xform', UV_NORMAL)
|
||||
for tile in layer["tiles"]:
|
||||
chars[layer_index][y][x] = tile["char"]
|
||||
fg_colors[layer_index][y][x] = tile["fg"]
|
||||
bg_colors[layer_index][y][x] = tile["bg"]
|
||||
uvs[layer_index][y][x] = uv_types[tile.get("xform", UV_NORMAL)]
|
||||
uv_maps[layer_index][y][x] = tile.get("xform", UV_NORMAL)
|
||||
x += 1
|
||||
if x >= self.width:
|
||||
x = 0
|
||||
|
|
@ -1158,7 +1292,7 @@ class ArtFromDisk(Art):
|
|||
self.uv_mods.append(uvs)
|
||||
self.uv_maps.append(uv_maps)
|
||||
# set active frame properly
|
||||
active_frame = self.loaded_data.get('active_frame', 0)
|
||||
active_frame = self.loaded_data.get("active_frame", 0)
|
||||
self.set_active_frame(active_frame)
|
||||
|
||||
def first_update(self):
|
||||
|
|
@ -1171,12 +1305,14 @@ class ArtInstance(Art):
|
|||
Deep copy / clone of a source Art that can hold unique changes and be
|
||||
restored to its source.
|
||||
"""
|
||||
|
||||
update_when_source_changes = True
|
||||
"Set False if you want to manually update this Art."
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
# unique(?) filename
|
||||
self.filename = '%s_Instance%i' % (source.filename, time.time())
|
||||
self.filename = "%s_Instance%i" % (source.filename, time.time())
|
||||
self.app = source.app
|
||||
self.instances = None
|
||||
self.char_changed_frames, self.uv_changed_frames = {}, {}
|
||||
|
|
@ -1197,8 +1333,17 @@ class ArtInstance(Art):
|
|||
def restore_from_source(self):
|
||||
"Restore ArtInstance to its source Art's new values."
|
||||
# copy common references/values
|
||||
for prop in ['app', 'width', 'height', 'charset', 'palette',
|
||||
'quad_width', 'quad_height', 'layers', 'frames']:
|
||||
for prop in [
|
||||
"app",
|
||||
"width",
|
||||
"height",
|
||||
"charset",
|
||||
"palette",
|
||||
"quad_width",
|
||||
"quad_height",
|
||||
"layers",
|
||||
"frames",
|
||||
]:
|
||||
setattr(self, prop, getattr(self.source, prop))
|
||||
# copy lists
|
||||
self.layers_z = self.source.layers_z[:]
|
||||
|
|
@ -1225,6 +1370,7 @@ class ArtInstance(Art):
|
|||
|
||||
class TileIter:
|
||||
"Iterator for iterating over all tiles in all layers and frames in an Art."
|
||||
|
||||
def __init__(self, art):
|
||||
self.width, self.height = art.width, art.height
|
||||
self.frames, self.layers = art.frames, art.layers
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
|
||||
import traceback
|
||||
|
||||
from art import ART_DIR
|
||||
|
||||
class ArtExporter:
|
||||
|
||||
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."
|
||||
|
|
@ -24,8 +23,10 @@ class ArtExporter:
|
|||
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,11 +41,14 @@ 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
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
|
|
|
|||
29
audio.py
29
audio.py
|
|
@ -1,25 +1,26 @@
|
|||
|
||||
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:
|
||||
|
||||
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_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
|
||||
|
|
@ -44,11 +45,11 @@ 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 = {}
|
||||
|
|
@ -56,12 +57,13 @@ class AudioLord:
|
|||
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,8 +73,9 @@ 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:
|
||||
|
|
@ -80,7 +83,7 @@ class AudioLord:
|
|||
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]:
|
||||
|
|
@ -102,7 +105,7 @@ class AudioLord:
|
|||
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):
|
||||
|
|
|
|||
51
camera.py
51
camera.py
|
|
@ -1,12 +1,15 @@
|
|||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
import vector
|
||||
|
||||
|
||||
def clamp(val, lowest, highest):
|
||||
return min(highest, max(lowest, val))
|
||||
|
||||
class Camera:
|
||||
|
||||
class Camera:
|
||||
# good starting values
|
||||
start_x, start_y = 0, 0
|
||||
start_zoom = 2.5
|
||||
|
|
@ -65,10 +68,12 @@ class Camera:
|
|||
forward = (target - eye).normalize()
|
||||
side = forward.cross(up).normalize()
|
||||
upward = side.cross(forward)
|
||||
m = [[side.x, upward.x, -forward.x, 0],
|
||||
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]]
|
||||
[-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
|
||||
|
||||
|
|
@ -77,10 +82,7 @@ class Camera:
|
|||
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):
|
||||
|
|
@ -95,10 +97,7 @@ 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):
|
||||
|
|
@ -181,8 +180,8 @@ class Camera:
|
|||
# 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
|
||||
|
|
@ -199,9 +198,17 @@ class Camera:
|
|||
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
|
||||
|
|
@ -244,8 +251,9 @@ class Camera:
|
|||
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
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
76
charset.py
76
charset.py
|
|
@ -1,14 +1,16 @@
|
|||
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
|
||||
|
||||
|
|
@ -17,7 +19,10 @@ class CharacterSetLord:
|
|||
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
|
||||
|
|
@ -65,16 +77,18 @@ class CharacterSet:
|
|||
|
||||
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
|
||||
|
|
@ -125,7 +139,7 @@ class CharacterSet:
|
|||
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
|
||||
|
|
@ -152,16 +166,26 @@ class CharacterSet:
|
|||
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
|
||||
|
|
|
|||
196
collision.py
196
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 = []
|
||||
|
|
@ -118,15 +122,26 @@ 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
|
||||
|
|
@ -134,7 +149,12 @@ class CircleCollisionShape(CollisionShape):
|
|||
|
||||
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."
|
||||
|
|
@ -147,20 +167,26 @@ class CircleCollisionShape(CollisionShape):
|
|||
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
|
||||
|
|
@ -169,7 +195,12 @@ class AABBCollisionShape(CollisionShape):
|
|||
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."
|
||||
|
|
@ -183,15 +214,26 @@ class AABBCollisionShape(CollisionShape):
|
|||
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
|
||||
|
|
@ -251,10 +295,9 @@ class Collideable:
|
|||
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)
|
||||
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)]
|
||||
|
||||
|
|
@ -262,19 +305,28 @@ class Collideable:
|
|||
"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):
|
||||
|
|
@ -322,9 +375,9 @@ class Collideable:
|
|||
"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
|
||||
|
||||
|
|
@ -373,11 +426,13 @@ 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
|
||||
|
|
@ -386,9 +441,10 @@ class CollisionLord:
|
|||
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 = [], []
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
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,6 +589,7 @@ 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
|
||||
|
|
@ -529,6 +600,7 @@ def point_circle_penetration(point_x, point_y, circle_x, circle_y, 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,15 +625,25 @@ 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))
|
||||
|
|
|
|||
160
cursor.py
160
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,10 +56,10 @@ 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'
|
||||
class Cursor:
|
||||
vert_shader_source = "cursor_v.glsl"
|
||||
frag_shader_source = "cursor_f.glsl"
|
||||
alpha = 1
|
||||
icon_scale_factor = 4
|
||||
logg = False
|
||||
|
|
@ -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)
|
||||
|
|
@ -136,7 +140,10 @@ 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
|
||||
|
|
@ -178,7 +185,7 @@ 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
|
||||
|
||||
|
|
@ -209,7 +216,10 @@ class Cursor:
|
|||
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()
|
||||
|
|
@ -223,7 +233,10 @@ class Cursor:
|
|||
|
||||
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:
|
||||
|
|
@ -237,14 +250,16 @@ class Cursor:
|
|||
# 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
|
||||
|
|
@ -270,7 +285,9 @@ class Cursor:
|
|||
# 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()
|
||||
|
|
@ -311,12 +328,20 @@ class Cursor:
|
|||
|
||||
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:
|
||||
|
|
@ -324,9 +349,15 @@ class Cursor:
|
|||
else:
|
||||
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!
|
||||
|
|
@ -339,8 +370,9 @@ class Cursor:
|
|||
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:
|
||||
|
|
|
|||
129
edit_command.py
129
edit_command.py
|
|
@ -1,7 +1,4 @@
|
|||
import time
|
||||
|
||||
class EditCommand:
|
||||
|
||||
"undo/redo-able representation of an art edit (eg paint, erase) operation"
|
||||
|
||||
def __init__(self, art):
|
||||
|
|
@ -25,25 +22,27 @@ class EditCommand:
|
|||
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):
|
||||
|
|
@ -51,8 +50,10 @@ class EditCommand:
|
|||
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()
|
||||
|
||||
|
|
@ -72,14 +73,13 @@ 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
|
||||
|
|
@ -91,11 +91,11 @@ class EntireArtCommand:
|
|||
|
||||
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:
|
||||
|
|
@ -114,7 +114,7 @@ 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
|
||||
|
|
@ -126,7 +126,7 @@ class EntireArtCommand:
|
|||
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()
|
||||
|
|
@ -146,18 +145,32 @@ class EditCommandTile:
|
|||
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"
|
||||
|
|
@ -187,7 +200,12 @@ class EditCommandTile:
|
|||
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
|
||||
|
|
@ -196,31 +214,58 @@ 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):
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
|
||||
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"
|
||||
|
|
@ -26,24 +26,24 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
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,7 +57,8 @@ 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
|
||||
|
|
@ -68,7 +69,7 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
# 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
|
||||
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,10 +151,10 @@ 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)
|
||||
increment += len(seq)
|
||||
|
|
@ -158,12 +163,14 @@ Assumes 80 columns, DOS character set and EGA palette.
|
|||
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,4 +1,3 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
# import as white on black for ease of edit + export
|
||||
|
|
@ -6,21 +5,22 @@ 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,24 +1,24 @@
|
|||
|
||||
# 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'
|
||||
class ConvertImageChooserDialog(ImageFileChooserDialog):
|
||||
title = "Convert image"
|
||||
confirm_caption = "Choose"
|
||||
|
||||
def confirm_pressed(self):
|
||||
filename = self.field_texts[0]
|
||||
|
|
@ -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'
|
||||
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"
|
||||
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,17 +58,17 @@ 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
|
||||
|
||||
|
|
@ -81,38 +77,42 @@ class ConvertImageOptionsDialog(ImportOptionsDialog):
|
|||
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()
|
||||
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
|
||||
|
|
@ -130,48 +130,62 @@ class ConvertImageOptionsDialog(ImportOptionsDialog):
|
|||
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
|
|
@ -181,11 +195,11 @@ Bitmap image in PNG, JPEG, or BMP format.
|
|||
|
||||
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']
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
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,18 +19,18 @@ 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):
|
||||
|
|
@ -37,18 +40,17 @@ class TwoColorConvertImageOptionsDialog(ConvertImageOptionsDialog):
|
|||
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
|
||||
|
|
@ -72,12 +74,12 @@ fg/bg color swaps. Suitable for plaintext export.
|
|||
|
||||
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']
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -51,7 +50,7 @@ class ImageSequenceConverter:
|
|||
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):
|
||||
|
|
@ -64,19 +63,23 @@ class ImageSequenceConverter:
|
|||
|
||||
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
|
||||
|
|
@ -86,28 +89,28 @@ image chosen.
|
|||
# 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']
|
||||
width, height = options["art_width"], options["art_height"]
|
||||
self.art.resize(width, height) # Importer.init will adjust UI
|
||||
bicubic_scale = options['bicubic_scale']
|
||||
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) :]
|
||||
# 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,45 +1,44 @@
|
|||
|
||||
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.
|
||||
|
|
@ -48,28 +47,30 @@ 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]
|
||||
|
||||
# 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'):
|
||||
if line[-2] == ord("\r") and line[-1] == ord("\n"):
|
||||
# self.app.log('EDSCIIImporter: windows-style line breaks detected')
|
||||
lb_length = 2
|
||||
break
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
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.
|
||||
|
|
@ -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):
|
||||
if i % 2 != 0:
|
||||
continue
|
||||
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,9 +1,8 @@
|
|||
|
||||
from art_import import ArtImporter
|
||||
|
||||
class TextImporter(ArtImporter):
|
||||
|
||||
format_name = 'Plain text'
|
||||
class TextImporter(ArtImporter):
|
||||
format_name = "Plain text"
|
||||
format_description = """
|
||||
ASCII art in ordinary text format.
|
||||
Assumes single frame, single layer document.
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
|
||||
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):
|
||||
|
|
@ -35,7 +35,7 @@ Exports active layer of active frame.
|
|||
|
||||
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()
|
||||
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,
|
||||
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))
|
||||
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'
|
||||
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'
|
||||
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()
|
||||
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,43 +91,51 @@ 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.
|
||||
"""
|
||||
|
|
@ -134,8 +143,8 @@ PNG image set for each frame and/or layer.
|
|||
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,
|
||||
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)):
|
||||
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,17 +1,19 @@
|
|||
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)
|
||||
|
|
@ -23,7 +25,7 @@ any extended characters you want translated.
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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,27 +22,34 @@ 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)
|
||||
|
|
@ -50,26 +59,40 @@ class Framebuffer:
|
|||
|
||||
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):
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from art import Art
|
||||
from renderable import TileRenderable
|
||||
|
||||
|
|
@ -13,12 +12,12 @@ 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):
|
||||
|
|
|
|||
280
game_object.py
280
game_object.py
|
|
@ -1,14 +1,22 @@
|
|||
import os, math, random
|
||||
|
||||
from collections import namedtuple
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import vector
|
||||
|
||||
from art import Art, ArtInstance
|
||||
from art import ArtInstance
|
||||
from collision import (
|
||||
CST_AABB,
|
||||
CST_CIRCLE,
|
||||
CST_NONE,
|
||||
CST_TILE,
|
||||
CT_NONE,
|
||||
CTG_DYNAMIC,
|
||||
Collideable,
|
||||
Contact,
|
||||
point_in_box,
|
||||
)
|
||||
from renderable import GameObjectRenderable
|
||||
from renderable_line import OriginIndicatorRenderable, BoundsIndicatorRenderable
|
||||
|
||||
from collision import Contact, Collideable, CST_NONE, CST_CIRCLE, CST_AABB, CST_TILE, CT_NONE, CTG_STATIC, CTG_DYNAMIC, point_in_box
|
||||
from renderable_line import BoundsIndicatorRenderable, OriginIndicatorRenderable
|
||||
|
||||
# facings
|
||||
GOF_LEFT = 0
|
||||
|
|
@ -20,23 +28,18 @@ GOF_FRONT = 2
|
|||
GOF_BACK = 3
|
||||
"Object is facing back"
|
||||
|
||||
FACINGS = {
|
||||
GOF_LEFT: 'left',
|
||||
GOF_RIGHT: 'right',
|
||||
GOF_FRONT: 'front',
|
||||
GOF_BACK: 'back'
|
||||
}
|
||||
FACINGS = {GOF_LEFT: "left", GOF_RIGHT: "right", GOF_FRONT: "front", GOF_BACK: "back"}
|
||||
"Dict mapping GOF_* facing enum values to strings"
|
||||
|
||||
FACING_DIRS = {
|
||||
GOF_LEFT: (-1, 0),
|
||||
GOF_RIGHT: (1, 0),
|
||||
GOF_FRONT: (0, -1),
|
||||
GOF_BACK: (0, 1)
|
||||
GOF_BACK: (0, 1),
|
||||
}
|
||||
"Dict mapping GOF_* facing enum values to (x,y) orientations"
|
||||
|
||||
DEFAULT_STATE = 'stand'
|
||||
DEFAULT_STATE = "stand"
|
||||
|
||||
# timer slots
|
||||
TIMER_PRE_UPDATE = 0
|
||||
|
|
@ -44,7 +47,7 @@ TIMER_UPDATE = 1
|
|||
TIMER_POST_UPDATE = 2
|
||||
|
||||
__pdoc__ = {}
|
||||
__pdoc__['GameObject.x'] = "Object's location in 3D space."
|
||||
__pdoc__["GameObject.x"] = "Object's location in 3D space."
|
||||
|
||||
|
||||
class GameObject:
|
||||
|
|
@ -57,7 +60,8 @@ class GameObject:
|
|||
See game_util_object module for some generic subclasses for things like
|
||||
a player, spawners, triggers, attachments etc.
|
||||
"""
|
||||
art_src = 'game_object_default'
|
||||
|
||||
art_src = "game_object_default"
|
||||
"""
|
||||
If specified, this art file will be loaded from disk and used as object's
|
||||
default appearance. If object has states/facings, this is the "base"
|
||||
|
|
@ -84,7 +88,7 @@ class GameObject:
|
|||
art_charset, art_palette = None, None
|
||||
y_sort = False
|
||||
"If True, object will sort according to its Y position a la Zelda LttP"
|
||||
lifespan = 0.
|
||||
lifespan = 0.0
|
||||
"If >0, object will self-destroy after this many seconds"
|
||||
kill_distance_from_origin = 1000
|
||||
"""
|
||||
|
|
@ -103,11 +107,11 @@ class GameObject:
|
|||
# 2 = each step is half object's size
|
||||
# N = each step is 1/N object's size
|
||||
"""
|
||||
move_accel_x = move_accel_y = 200.
|
||||
move_accel_x = move_accel_y = 200.0
|
||||
"Acceleration per update from player movement"
|
||||
ground_friction = 10.0
|
||||
air_friction = 25.0
|
||||
mass = 1.
|
||||
mass = 1.0
|
||||
"Mass: negative number = infinitely dense"
|
||||
bounciness = 0.25
|
||||
"Bounciness aka restitution, % of velocity reflected on bounce"
|
||||
|
|
@ -117,7 +121,7 @@ class GameObject:
|
|||
log_load = False
|
||||
log_spawn = False
|
||||
visible = True
|
||||
alpha = 1.
|
||||
alpha = 1.0
|
||||
locked = False
|
||||
"If True, location is protected from edit mode drags, can't click to select"
|
||||
show_origin = False
|
||||
|
|
@ -127,15 +131,15 @@ class GameObject:
|
|||
"Collision shape: tile, circle, AABB - see the CST_* enum values"
|
||||
collision_type = CT_NONE
|
||||
"Type of collision (static, dynamic)"
|
||||
col_layer_name = 'collision'
|
||||
col_layer_name = "collision"
|
||||
"Collision layer name for CST_TILE objects"
|
||||
draw_col_layer = False
|
||||
"If True, collision layer will draw normally"
|
||||
col_offset_x, col_offset_y = 0., 0.
|
||||
col_offset_x, col_offset_y = 0.0, 0.0
|
||||
"Collision circle/box offset from origin"
|
||||
col_radius = 1.
|
||||
col_radius = 1.0
|
||||
"Collision circle size, if CST_CIRCLE"
|
||||
col_width, col_height = 1., 1.
|
||||
col_width, col_height = 1.0, 1.0
|
||||
"Collision AABB size, if CST_AABB"
|
||||
art_off_pct_x, art_off_pct_y = 0.5, 0.5
|
||||
"""
|
||||
|
|
@ -144,21 +148,47 @@ class GameObject:
|
|||
"""
|
||||
should_save = True
|
||||
"If True, write this object to state save files"
|
||||
serialized = ['name', 'x', 'y', 'z', 'art_src', 'visible', 'locked', 'y_sort',
|
||||
'art_off_pct_x', 'art_off_pct_y', 'alpha', 'state', 'facing',
|
||||
'animating', 'scale_x', 'scale_y']
|
||||
serialized = [
|
||||
"name",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"art_src",
|
||||
"visible",
|
||||
"locked",
|
||||
"y_sort",
|
||||
"art_off_pct_x",
|
||||
"art_off_pct_y",
|
||||
"alpha",
|
||||
"state",
|
||||
"facing",
|
||||
"animating",
|
||||
"scale_x",
|
||||
"scale_y",
|
||||
]
|
||||
"List of members to serialize (no weak refs!)"
|
||||
editable = ['show_collision', 'col_radius', 'col_width', 'col_height',
|
||||
'mass', 'bounciness', 'stop_velocity']
|
||||
editable = [
|
||||
"show_collision",
|
||||
"col_radius",
|
||||
"col_width",
|
||||
"col_height",
|
||||
"mass",
|
||||
"bounciness",
|
||||
"stop_velocity",
|
||||
]
|
||||
"""
|
||||
Members that don't need to be serialized, but should be exposed to
|
||||
object edit UI
|
||||
"""
|
||||
set_methods = {'art_src': 'set_art_src', 'alpha': '_set_alpha',
|
||||
'scale_x': '_set_scale_x', 'scale_y': '_set_scale_y',
|
||||
'name': '_rename', 'col_radius': '_set_col_radius',
|
||||
'col_width': '_set_col_width',
|
||||
'col_height': '_set_col_height'
|
||||
set_methods = {
|
||||
"art_src": "set_art_src",
|
||||
"alpha": "_set_alpha",
|
||||
"scale_x": "_set_scale_x",
|
||||
"scale_y": "_set_scale_y",
|
||||
"name": "_rename",
|
||||
"col_radius": "_set_col_radius",
|
||||
"col_width": "_set_col_width",
|
||||
"col_height": "_set_col_height",
|
||||
}
|
||||
"If setting a given member should run some logic, specify the method here"
|
||||
selectable = True
|
||||
|
|
@ -190,13 +220,14 @@ class GameObject:
|
|||
"If True, handle mouse click/wheel events passed in from world / input handler"
|
||||
consume_mouse_events = False
|
||||
"If True, prevent any other mouse click/wheel events from being processed"
|
||||
|
||||
def __init__(self, world, obj_data=None):
|
||||
"""
|
||||
Create new GameObject in world, from serialized data if provided.
|
||||
"""
|
||||
self.x, self.y, self.z = 0., 0., 0.
|
||||
self.x, self.y, self.z = 0.0, 0.0, 0.0
|
||||
"Object's location in 3D space."
|
||||
self.scale_x, self.scale_y, self.scale_z = 1., 1., 1.
|
||||
self.scale_x, self.scale_y, self.scale_z = 1.0, 1.0, 1.0
|
||||
"Object's scale in 3D space."
|
||||
self.rooms = {}
|
||||
"Dict of rooms we're in - if empty, object appears in all rooms"
|
||||
|
|
@ -209,9 +240,11 @@ class GameObject:
|
|||
# properties that need non-None defaults should be declared above
|
||||
if obj_data:
|
||||
for v in self.serialized:
|
||||
if not v in obj_data:
|
||||
if v not in obj_data:
|
||||
if self.log_load:
|
||||
self.app.dev_log("Serialized property '%s' not found for %s" % (v, self.name))
|
||||
self.app.dev_log(
|
||||
"Serialized property '%s' not found for %s" % (v, self.name)
|
||||
)
|
||||
continue
|
||||
# if value is in data and serialized list but undeclared, do so
|
||||
if not hasattr(self, v):
|
||||
|
|
@ -253,10 +286,14 @@ class GameObject:
|
|||
"Dict of all Arts this object can reference, eg for states"
|
||||
# if art_src not specified, create a new art according to dimensions
|
||||
if self.generate_art:
|
||||
self.art_src = '%s_art' % self.name
|
||||
self.art = self.app.new_art(self.art_src, self.art_width,
|
||||
self.art_height, self.art_charset,
|
||||
self.art_palette)
|
||||
self.art_src = "%s_art" % self.name
|
||||
self.art = self.app.new_art(
|
||||
self.art_src,
|
||||
self.art_width,
|
||||
self.art_height,
|
||||
self.art_charset,
|
||||
self.art_palette,
|
||||
)
|
||||
# generated art will likely be only entry in this dict,
|
||||
# but make sure it's there (eg generated art for Characters)
|
||||
self.arts[self.art_src] = self.art
|
||||
|
|
@ -278,7 +315,7 @@ class GameObject:
|
|||
self.bounds_renderable = BoundsIndicatorRenderable(self.app, self)
|
||||
"1px LineRenderable showing object's bounding box"
|
||||
for art in self.arts.values():
|
||||
if not art in self.world.art_loaded:
|
||||
if art not in self.world.art_loaded:
|
||||
self.world.art_loaded.append(art)
|
||||
self.orig_collision_type = self.collision_type
|
||||
"Remember last collision type for enable/disable - don't set manually!"
|
||||
|
|
@ -305,12 +342,15 @@ class GameObject:
|
|||
if self.animating and self.art.frames > 0:
|
||||
self.start_animating()
|
||||
if self.log_spawn:
|
||||
self.app.log('Spawned %s with Art %s' % (self.name, os.path.basename(self.art.filename)))
|
||||
self.app.log(
|
||||
"Spawned %s with Art %s"
|
||||
% (self.name, os.path.basename(self.art.filename))
|
||||
)
|
||||
|
||||
def get_unique_name(self):
|
||||
"Generate and return a somewhat human-readable unique name for object"
|
||||
name = str(self)
|
||||
return '%s_%s' % (type(self).__name__, name[name.rfind('x')+1:-1])
|
||||
return "%s_%s" % (type(self).__name__, name[name.rfind("x") + 1 : -1])
|
||||
|
||||
def _rename(self, new_name):
|
||||
# pass thru to world, this method exists for edit set method
|
||||
|
|
@ -336,13 +376,13 @@ class GameObject:
|
|||
if self.facing_changes_art:
|
||||
# load each facing for each state
|
||||
for facing in FACINGS.values():
|
||||
art_name = '%s_%s_%s' % (self.art_src, state, facing)
|
||||
art_name = "%s_%s_%s" % (self.art_src, state, facing)
|
||||
art = self.app.load_art(art_name, False)
|
||||
if art:
|
||||
self.arts[art_name] = art
|
||||
else:
|
||||
# load each state
|
||||
art_name = '%s_%s' % (self.art_src, state)
|
||||
art_name = "%s_%s" % (self.art_src, state)
|
||||
art = self.app.load_art(art_name, False)
|
||||
if art:
|
||||
self.arts[art_name] = art
|
||||
|
|
@ -406,8 +446,7 @@ class GameObject:
|
|||
# use sound_name as filename if it's not in our filenames dict
|
||||
sound_filename = self.sound_filenames.get(sound_name, sound_name)
|
||||
sound_filename = self.world.sounds_dir + sound_filename
|
||||
self.world.app.al.object_play_sound(self, sound_filename,
|
||||
loops, allow_multiple)
|
||||
self.world.app.al.object_play_sound(self, sound_filename, loops, allow_multiple)
|
||||
|
||||
def stop_sound(self, sound_name):
|
||||
"Stop playing given sound."
|
||||
|
|
@ -444,7 +483,7 @@ class GameObject:
|
|||
|
||||
def stopped_colliding(self, other):
|
||||
"Run when object stops colliding with another object."
|
||||
if not other.name in self.collision.contacts:
|
||||
if other.name not in self.collision.contacts:
|
||||
# TODO: understand why this spams when player has a MazePickup
|
||||
# self.world.app.log("%s stopped colliding with %s but wasn't in its contacts!" % (self.name, other.name))
|
||||
return
|
||||
|
|
@ -460,8 +499,10 @@ class GameObject:
|
|||
total_vel = self.vel_x + self.vel_y + other.vel_x + other.vel_y
|
||||
# negative mass = infinite
|
||||
total_mass = max(0, self.mass) + max(0, other.mass)
|
||||
if other.name not in self.collision.contacts or \
|
||||
self.name not in other.collision.contacts:
|
||||
if (
|
||||
other.name not in self.collision.contacts
|
||||
or self.name not in other.collision.contacts
|
||||
):
|
||||
return
|
||||
# redistribute velocity based on mass we're colliding with
|
||||
if self.is_dynamic() and self.mass >= 0:
|
||||
|
|
@ -546,7 +587,9 @@ class GameObject:
|
|||
y = math.ceil(-y)
|
||||
return x, y
|
||||
|
||||
def get_tiles_overlapping_box(self, box_left, box_top, box_right, box_bottom, log=False):
|
||||
def get_tiles_overlapping_box(
|
||||
self, box_left, box_top, box_right, box_bottom, log=False
|
||||
):
|
||||
"Returns x,y coords for each tile overlapping given box"
|
||||
if self.collision_shape_type != CST_TILE:
|
||||
return []
|
||||
|
|
@ -573,8 +616,7 @@ class GameObject:
|
|||
"""
|
||||
started = other.name not in self.collision.contacts
|
||||
# create or update contact info: (overlap, timestamp)
|
||||
self.collision.contacts[other.name] = Contact(overlap,
|
||||
self.world.cl.ticks)
|
||||
self.collision.contacts[other.name] = Contact(overlap, self.world.cl.ticks)
|
||||
can_collide = self.can_collide_with(other)
|
||||
if not can_collide and started:
|
||||
self.started_overlapping(other)
|
||||
|
|
@ -622,14 +664,14 @@ class GameObject:
|
|||
"Return Art (and 'flip X' bool) that best represents current state"
|
||||
# use current state if none specified
|
||||
state = self.state if state is None else state
|
||||
art_state_name = '%s_%s' % (self.art_src, self.state)
|
||||
art_state_name = "%s_%s" % (self.art_src, self.state)
|
||||
# simple case: no facing, just state
|
||||
if not self.facing_changes_art:
|
||||
# return art for current state, use default if not available
|
||||
if art_state_name in self.arts:
|
||||
return self.arts[art_state_name], False
|
||||
else:
|
||||
default_name = '%s_%s' % (self.art_src, self.state or DEFAULT_STATE)
|
||||
default_name = "%s_%s" % (self.art_src, self.state or DEFAULT_STATE)
|
||||
# assert(default_name in self.arts
|
||||
# don't assert - if base+state name available, use that
|
||||
if default_name in self.arts:
|
||||
|
|
@ -640,7 +682,7 @@ class GameObject:
|
|||
# more complex case: art determined by both state and facing
|
||||
facing_suffix = FACINGS[self.facing]
|
||||
# first see if anim exists for this exact state, skip subsequent logic
|
||||
exact_name = '%s_%s' % (art_state_name, facing_suffix)
|
||||
exact_name = "%s_%s" % (art_state_name, facing_suffix)
|
||||
if exact_name in self.arts:
|
||||
return self.arts[exact_name], False
|
||||
# see what anims are available and try to choose best for facing
|
||||
|
|
@ -651,12 +693,12 @@ class GameObject:
|
|||
break
|
||||
# if NO anims for current state, fall back to default
|
||||
if not has_state:
|
||||
default_name = '%s_%s' % (self.art_src, DEFAULT_STATE)
|
||||
default_name = "%s_%s" % (self.art_src, DEFAULT_STATE)
|
||||
art_state_name = default_name
|
||||
front_name = '%s_%s' % (art_state_name, FACINGS[GOF_FRONT])
|
||||
left_name = '%s_%s' % (art_state_name, FACINGS[GOF_LEFT])
|
||||
right_name = '%s_%s' % (art_state_name, FACINGS[GOF_RIGHT])
|
||||
back_name = '%s_%s' % (art_state_name, FACINGS[GOF_BACK])
|
||||
front_name = "%s_%s" % (art_state_name, FACINGS[GOF_FRONT])
|
||||
left_name = "%s_%s" % (art_state_name, FACINGS[GOF_LEFT])
|
||||
right_name = "%s_%s" % (art_state_name, FACINGS[GOF_RIGHT])
|
||||
back_name = "%s_%s" % (art_state_name, FACINGS[GOF_BACK])
|
||||
has_front = front_name in self.arts
|
||||
has_left = left_name in self.arts
|
||||
has_right = right_name in self.arts
|
||||
|
|
@ -782,10 +824,10 @@ class GameObject:
|
|||
self.move_y += dir_y
|
||||
|
||||
def is_on_ground(self):
|
||||
'''
|
||||
"""
|
||||
Return True if object is "on the ground". Subclasses define custom
|
||||
logic here.
|
||||
'''
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_friction(self):
|
||||
|
|
@ -831,7 +873,9 @@ class GameObject:
|
|||
Apply current acceleration / velocity to position using Verlet
|
||||
integration with half-step velocity estimation.
|
||||
"""
|
||||
accel_x, accel_y, accel_z = self.get_acceleration(self.vel_x, self.vel_y, self.vel_z)
|
||||
accel_x, accel_y, accel_z = self.get_acceleration(
|
||||
self.vel_x, self.vel_y, self.vel_z
|
||||
)
|
||||
timestep = self.world.app.timestep / 1000
|
||||
hsvel_x = self.vel_x + 0.5 * timestep * accel_x
|
||||
hsvel_y = self.vel_y + 0.5 * timestep * accel_y
|
||||
|
|
@ -843,11 +887,17 @@ class GameObject:
|
|||
self.vel_x = hsvel_x + 0.5 * timestep * accel_x
|
||||
self.vel_y = hsvel_y + 0.5 * timestep * accel_y
|
||||
self.vel_z = hsvel_z + 0.5 * timestep * accel_z
|
||||
self.vel_x, self.vel_y, self.vel_z = vector.cut_xyz(self.vel_x, self.vel_y, self.vel_z, self.stop_velocity)
|
||||
self.vel_x, self.vel_y, self.vel_z = vector.cut_xyz(
|
||||
self.vel_x, self.vel_y, self.vel_z, self.stop_velocity
|
||||
)
|
||||
|
||||
def moved_this_frame(self):
|
||||
"Return True if object changed locations this frame."
|
||||
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 warped_recently(self):
|
||||
|
|
@ -905,8 +955,15 @@ class GameObject:
|
|||
"""
|
||||
pass
|
||||
|
||||
def set_timer_function(self, timer_name, timer_function, delay_min,
|
||||
delay_max=0, repeats=-1, slot=TIMER_PRE_UPDATE):
|
||||
def set_timer_function(
|
||||
self,
|
||||
timer_name,
|
||||
timer_function,
|
||||
delay_min,
|
||||
delay_max=0,
|
||||
repeats=-1,
|
||||
slot=TIMER_PRE_UPDATE,
|
||||
):
|
||||
"""
|
||||
Run given function in X seconds or every X seconds Y times.
|
||||
If max is given, next execution will be between min and max time.
|
||||
|
|
@ -914,29 +971,42 @@ class GameObject:
|
|||
"Slot" determines whether function will run in pre_update, update, or
|
||||
post_update.
|
||||
"""
|
||||
timer = GameObjectTimerFunction(self, timer_name, timer_function,
|
||||
delay_min, delay_max, repeats, slot)
|
||||
timer = GameObjectTimerFunction(
|
||||
self, timer_name, timer_function, delay_min, delay_max, repeats, slot
|
||||
)
|
||||
# add to slot-appropriate dict
|
||||
d = [self.timer_functions_pre_update, self.timer_functions_update,
|
||||
self.timer_functions_post_update][slot]
|
||||
d = [
|
||||
self.timer_functions_pre_update,
|
||||
self.timer_functions_update,
|
||||
self.timer_functions_post_update,
|
||||
][slot]
|
||||
d[timer_name] = timer
|
||||
|
||||
def stop_timer_function(self, timer_name):
|
||||
"Stop currently running timer function with given name."
|
||||
timer = self.timer_functions_pre_update.get(timer_name, None) or \
|
||||
self.timer_functions_update.get(timer_name, None) or \
|
||||
self.timer_functions_post_update.get(timer_name, None)
|
||||
timer = (
|
||||
self.timer_functions_pre_update.get(timer_name, None)
|
||||
or self.timer_functions_update.get(timer_name, None)
|
||||
or self.timer_functions_post_update.get(timer_name, None)
|
||||
)
|
||||
if not timer:
|
||||
self.app.log('Timer named %s not found on object %s' % (timer_name,
|
||||
self.name))
|
||||
d = [self.timer_functions_pre_update, self.timer_functions_update,
|
||||
self.timer_functions_post_update][timer.slot]
|
||||
self.app.log(
|
||||
"Timer named %s not found on object %s" % (timer_name, self.name)
|
||||
)
|
||||
d = [
|
||||
self.timer_functions_pre_update,
|
||||
self.timer_functions_update,
|
||||
self.timer_functions_post_update,
|
||||
][timer.slot]
|
||||
d.pop(timer_name)
|
||||
|
||||
def update_state(self):
|
||||
"Update object state based on current context, eg movement."
|
||||
if self.state_changes_art and self.stand_if_not_moving and \
|
||||
not self.moved_this_frame():
|
||||
if (
|
||||
self.state_changes_art
|
||||
and self.stand_if_not_moving
|
||||
and not self.moved_this_frame()
|
||||
):
|
||||
self.state = DEFAULT_STATE
|
||||
|
||||
def update_facing(self):
|
||||
|
|
@ -1034,7 +1104,7 @@ class GameObject:
|
|||
if 0 < self.destroy_time <= self.world.get_elapsed_time():
|
||||
self.destroy()
|
||||
# don't apply physics to selected objects being dragged
|
||||
if self.physics_move and not self.name in self.world.drag_objects:
|
||||
if self.physics_move and self.name not in self.world.drag_objects:
|
||||
self.apply_move()
|
||||
if self.fast_move_steps > 0:
|
||||
self.fast_move()
|
||||
|
|
@ -1044,9 +1114,14 @@ class GameObject:
|
|||
self.update_facing()
|
||||
# update collision shape before CollisionLord resolves any collisions
|
||||
self.collision.update()
|
||||
if abs(self.x) > self.kill_distance_from_origin or \
|
||||
abs(self.y) > self.kill_distance_from_origin:
|
||||
self.app.log('%s reached %s from origin, destroying.' % (self.name, self.kill_distance_from_origin))
|
||||
if (
|
||||
abs(self.x) > self.kill_distance_from_origin
|
||||
or abs(self.y) > self.kill_distance_from_origin
|
||||
):
|
||||
self.app.log(
|
||||
"%s reached %s from origin, destroying."
|
||||
% (self.name, self.kill_distance_from_origin)
|
||||
)
|
||||
self.destroy()
|
||||
|
||||
def update_renderables(self):
|
||||
|
|
@ -1057,8 +1132,11 @@ class GameObject:
|
|||
# even if debug viz are off, update once on init to set correct state
|
||||
if self.show_origin or self in self.world.selected_objects:
|
||||
self.origin_renderable.update()
|
||||
if self.show_bounds or self in self.world.selected_objects or \
|
||||
(self is self.world.hovered_focus_object and self.selectable):
|
||||
if (
|
||||
self.show_bounds
|
||||
or self in self.world.selected_objects
|
||||
or (self is self.world.hovered_focus_object and self.selectable)
|
||||
):
|
||||
self.bounds_renderable.update()
|
||||
if self.show_collision and self.is_dynamic():
|
||||
self.collision.update_renderables()
|
||||
|
|
@ -1086,7 +1164,9 @@ class GameObject:
|
|||
|
||||
def is_in_current_room(self):
|
||||
"Return True if this object is in the world's currently active Room."
|
||||
return len(self.rooms) == 0 or (self.world.current_room and self.world.current_room.name in self.rooms)
|
||||
return len(self.rooms) == 0 or (
|
||||
self.world.current_room and self.world.current_room.name in self.rooms
|
||||
)
|
||||
|
||||
def room_entered(self, room, old_room):
|
||||
"Run when a room we're in is entered."
|
||||
|
|
@ -1103,8 +1183,11 @@ class GameObject:
|
|||
return
|
||||
if self.show_origin or self in self.world.selected_objects:
|
||||
self.origin_renderable.render()
|
||||
if self.show_bounds or self in self.world.selected_objects or \
|
||||
(self.selectable and self is self.world.hovered_focus_object):
|
||||
if (
|
||||
self.show_bounds
|
||||
or self in self.world.selected_objects
|
||||
or (self.selectable and self is self.world.hovered_focus_object)
|
||||
):
|
||||
self.bounds_renderable.render()
|
||||
if self.show_collision and self.collision_type != CT_NONE:
|
||||
self.collision.render()
|
||||
|
|
@ -1120,7 +1203,7 @@ class GameObject:
|
|||
this object's "serialized" list are stored. Direct object references
|
||||
are not safe to serialize, use only primitive types like strings.
|
||||
"""
|
||||
d = { 'class_name': type(self).__name__ }
|
||||
d = {"class_name": type(self).__name__}
|
||||
# serialize whatever other vars are declared in self.serialized
|
||||
for prop_name in self.serialized:
|
||||
if hasattr(self, prop_name):
|
||||
|
|
@ -1145,8 +1228,10 @@ class GameObject:
|
|||
if self in self.world.selected_objects:
|
||||
self.world.selected_objects.remove(self)
|
||||
if self.spawner:
|
||||
if hasattr(self.spawner, 'spawned_objects') and \
|
||||
self in self.spawner.spawned_objects:
|
||||
if (
|
||||
hasattr(self.spawner, "spawned_objects")
|
||||
and self in self.spawner.spawned_objects
|
||||
):
|
||||
self.spawner.spawned_objects.remove(self)
|
||||
self.origin_renderable.destroy()
|
||||
self.bounds_renderable.destroy()
|
||||
|
|
@ -1162,6 +1247,7 @@ class GameObjectTimerFunction:
|
|||
Object that manages a function's execution schedule for a GameObject.
|
||||
Use GameObject.set_timer_function to create these.
|
||||
"""
|
||||
|
||||
def __init__(self, go, name, function, delay_min, delay_max, repeats, slot):
|
||||
self.go = go
|
||||
"GameObject using this timer"
|
||||
|
|
|
|||
71
game_room.py
71
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,7 +57,7 @@ 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):
|
||||
|
|
@ -58,7 +68,8 @@ class GameRoom:
|
|||
# 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,14 +77,19 @@ 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
|
||||
|
|
@ -81,7 +97,7 @@ class GameRoom:
|
|||
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)
|
||||
|
|
@ -135,16 +151,20 @@ class GameRoom:
|
|||
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):
|
||||
|
|
@ -155,7 +175,12 @@ class GameRoom:
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -1,21 +1,31 @@
|
|||
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."
|
||||
|
|
@ -36,46 +46,56 @@ 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
|
||||
|
||||
|
|
@ -93,17 +113,19 @@ class Projectile(GameObject):
|
|||
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
|
||||
|
|
@ -115,13 +137,20 @@ class Character(GameObject):
|
|||
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:
|
||||
|
|
@ -139,9 +168,8 @@ class Player(Character):
|
|||
|
||||
|
||||
class TopDownPlayer(Player):
|
||||
|
||||
y_sort = True
|
||||
attachment_classes = { 'shadow': 'BlobShadow' }
|
||||
attachment_classes = {"shadow": "BlobShadow"}
|
||||
facing_changes_art = True
|
||||
|
||||
def get_facing_dir(self):
|
||||
|
|
@ -150,20 +178,37 @@ class TopDownPlayer(Player):
|
|||
|
||||
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,26 +234,31 @@ 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):
|
||||
|
|
@ -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)
|
||||
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,15 +328,20 @@ 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():
|
||||
|
|
@ -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,8 +406,11 @@ 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):
|
||||
|
|
@ -403,26 +476,35 @@ 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)
|
||||
|
|
|
|||
360
game_world.py
360
game_world.py
|
|
@ -1,22 +1,31 @@
|
|||
|
||||
import os, sys, math, time, importlib, json, traceback
|
||||
import importlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
|
||||
import sdl2
|
||||
|
||||
import game_object, game_util_objects, game_hud, game_room
|
||||
import collision, vector
|
||||
from camera import Camera
|
||||
from grid import GameGrid
|
||||
import collision
|
||||
import game_hud
|
||||
import game_object
|
||||
import game_room
|
||||
import game_util_objects
|
||||
import vector
|
||||
from art import ART_DIR
|
||||
from camera import Camera
|
||||
from charset import CHARSET_DIR
|
||||
from grid import GameGrid
|
||||
from palette import PALETTE_DIR
|
||||
|
||||
TOP_GAME_DIR = 'games/'
|
||||
DEFAULT_STATE_FILENAME = 'start'
|
||||
STATE_FILE_EXTENSION = 'gs'
|
||||
GAME_SCRIPTS_DIR = 'scripts/'
|
||||
SOUNDS_DIR = 'sounds/'
|
||||
TOP_GAME_DIR = "games/"
|
||||
DEFAULT_STATE_FILENAME = "start"
|
||||
STATE_FILE_EXTENSION = "gs"
|
||||
GAME_SCRIPTS_DIR = "scripts/"
|
||||
SOUNDS_DIR = "sounds/"
|
||||
|
||||
# generic starter script with a GO and Player subclass
|
||||
STARTER_SCRIPT = """
|
||||
|
|
@ -43,29 +52,31 @@ class MyGameObject(GameObject):
|
|||
|
||||
|
||||
# Quickie class to debug render order
|
||||
RenderItem = namedtuple('RenderItem', ['obj', 'layer', 'sort_value'])
|
||||
RenderItem = namedtuple("RenderItem", ["obj", "layer", "sort_value"])
|
||||
|
||||
|
||||
class GameCamera(Camera):
|
||||
pan_friction = 0.2
|
||||
use_bounds = False
|
||||
|
||||
|
||||
class GameWorld:
|
||||
"""
|
||||
Holds global state for Game Mode. Spawns, manages, and renders GameObjects.
|
||||
Properties serialized via WorldPropertiesObject.
|
||||
Global state can be controlled via a WorldGlobalsObject.
|
||||
"""
|
||||
game_title = 'Untitled Game'
|
||||
|
||||
game_title = "Untitled Game"
|
||||
"Title for game, shown in window titlebar when not editing"
|
||||
gravity_x, gravity_y, gravity_z = 0., 0., 0.
|
||||
gravity_x, gravity_y, gravity_z = 0.0, 0.0, 0.0
|
||||
"Gravity applied to all objects who are affected by gravity."
|
||||
bg_color = [0., 0., 0., 1.]
|
||||
bg_color = [0.0, 0.0, 0.0, 1.0]
|
||||
"OpenGL wiewport color to render behind everything else, ie the void."
|
||||
hud_class_name = 'GameHUD'
|
||||
hud_class_name = "GameHUD"
|
||||
"String name of HUD class to use"
|
||||
properties_object_class_name = 'WorldPropertiesObject'
|
||||
globals_object_class_name = 'WorldGlobalsObject'
|
||||
properties_object_class_name = "WorldPropertiesObject"
|
||||
globals_object_class_name = "WorldGlobalsObject"
|
||||
"String name of WorldGlobalsObject class to use."
|
||||
player_camera_lock = True
|
||||
"If True, camera will be locked to player's location."
|
||||
|
|
@ -91,10 +102,12 @@ class GameWorld:
|
|||
"If True, snap camera to new room's associated camera marker."
|
||||
list_only_current_room_objects = False
|
||||
"If True, list UI will only show objects in current room."
|
||||
builtin_module_names = ['game_object', 'game_util_objects', 'game_hud',
|
||||
'game_room']
|
||||
builtin_base_classes = (game_object.GameObject, game_hud.GameHUD,
|
||||
game_room.GameRoom)
|
||||
builtin_module_names = ["game_object", "game_util_objects", "game_hud", "game_room"]
|
||||
builtin_base_classes = (
|
||||
game_object.GameObject,
|
||||
game_hud.GameHUD,
|
||||
game_room.GameRoom,
|
||||
)
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
|
@ -122,9 +135,12 @@ class GameWorld:
|
|||
self._pause_time = 0
|
||||
self.updates = 0
|
||||
"Number of updates this we have performed."
|
||||
self.modules = {'game_object': game_object,
|
||||
'game_util_objects': game_util_objects,
|
||||
'game_hud': game_hud, 'game_room': game_room}
|
||||
self.modules = {
|
||||
"game_object": game_object,
|
||||
"game_util_objects": game_util_objects,
|
||||
"game_hud": game_hud,
|
||||
"game_room": game_room,
|
||||
}
|
||||
self.classname_to_spawn = None
|
||||
self.objects = {}
|
||||
"Dict of objects by name:object"
|
||||
|
|
@ -167,8 +183,11 @@ class GameWorld:
|
|||
if len(objects) == 0:
|
||||
return None
|
||||
# don't bother cycling if only one object found
|
||||
if len(objects) == 1 and objects[0].selectable and \
|
||||
not objects[0] in self.selected_objects:
|
||||
if (
|
||||
len(objects) == 1
|
||||
and objects[0].selectable
|
||||
and objects[0] not in self.selected_objects
|
||||
):
|
||||
return objects[0]
|
||||
# cycle through objects at point til an unselected one is found
|
||||
for obj in objects:
|
||||
|
|
@ -194,15 +213,14 @@ class GameWorld:
|
|||
return objects
|
||||
|
||||
def select_click(self):
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y)
|
||||
# remember last place we clicked
|
||||
self.last_mouse_click_x, self.last_mouse_click_y = x, y
|
||||
if self.classname_to_spawn:
|
||||
new_obj = self.spawn_object_of_class(self.classname_to_spawn, x, y)
|
||||
if self.current_room:
|
||||
self.current_room.add_object(new_obj)
|
||||
self.app.ui.message_line.post_line('Spawned %s' % new_obj.name)
|
||||
self.app.ui.message_line.post_line("Spawned %s" % new_obj.name)
|
||||
return
|
||||
objects = self.get_objects_at(x, y)
|
||||
next_obj = self.pick_next_object_at(x, y)
|
||||
|
|
@ -237,13 +255,15 @@ class GameWorld:
|
|||
self.select_click()
|
||||
# else pass clicks to any objects under mouse
|
||||
else:
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(
|
||||
self.app, self.app.mouse_x, self.app.mouse_y
|
||||
)
|
||||
# 'locked" only relevant to edit mode, ignore it if in play mode
|
||||
objects = self.get_objects_at(x, y, allow_locked=True)
|
||||
for obj in objects:
|
||||
if obj.handle_mouse_events and \
|
||||
(not obj.locked or not self.app.can_edit):
|
||||
if obj.handle_mouse_events and (
|
||||
not obj.locked or not self.app.can_edit
|
||||
):
|
||||
obj.clicked(button, x, y)
|
||||
if obj.consume_mouse_events:
|
||||
break
|
||||
|
|
@ -260,8 +280,7 @@ class GameWorld:
|
|||
# if we're clicking to spawn something, don't drag/select
|
||||
if self.classname_to_spawn:
|
||||
return
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y)
|
||||
# remember selected objects now, they might be deselected but still
|
||||
# need to have their collision turned back on.
|
||||
selected_objects = self.selected_objects[:]
|
||||
|
|
@ -289,8 +308,9 @@ class GameWorld:
|
|||
if button == sdl2.SDL_BUTTON_LEFT:
|
||||
self.select_unclick()
|
||||
else:
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(
|
||||
self.app, self.app.mouse_x, self.app.mouse_y
|
||||
)
|
||||
objects = self.get_objects_at(x, y)
|
||||
for obj in objects:
|
||||
if obj.handle_mouse_events:
|
||||
|
|
@ -298,8 +318,7 @@ class GameWorld:
|
|||
|
||||
def check_hovers(self):
|
||||
"Update objects on their mouse hover status"
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y)
|
||||
new_hovers = self.get_objects_at(x, y)
|
||||
# if this object will be selected on left click; draw bounds & label
|
||||
if self.app.ui.is_game_edit_ui_visible():
|
||||
|
|
@ -308,21 +327,19 @@ class GameWorld:
|
|||
# if in play mode, notify objects who have begun to be hovered
|
||||
else:
|
||||
for obj in new_hovers:
|
||||
if obj.handle_mouse_events and not obj in self.hovered_objects:
|
||||
if obj.handle_mouse_events and obj not in self.hovered_objects:
|
||||
obj.hovered(x, y)
|
||||
# check for objects un-hovered by this move
|
||||
for obj in self.hovered_objects:
|
||||
if obj.handle_mouse_events and not obj in new_hovers:
|
||||
if obj.handle_mouse_events and obj not in new_hovers:
|
||||
obj.unhovered(x, y)
|
||||
self.hovered_objects = new_hovers
|
||||
|
||||
def mouse_wheeled(self, wheel_y):
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y)
|
||||
objects = self.get_objects_at(x, y, allow_locked=True)
|
||||
for obj in objects:
|
||||
if obj.handle_mouse_events and \
|
||||
(not obj.locked or not self.app.can_edit):
|
||||
if obj.handle_mouse_events and (not obj.locked or not self.app.can_edit):
|
||||
obj.mouse_wheeled(wheel_y)
|
||||
if obj.consume_mouse_events:
|
||||
break
|
||||
|
|
@ -331,8 +348,11 @@ class GameWorld:
|
|||
if self.app.ui.active_dialog:
|
||||
return
|
||||
# bail if mouse didn't move (in world space - include camera) last input
|
||||
if self.app.mouse_dx == 0 and self.app.mouse_dy == 0 and \
|
||||
not self.camera.moved_this_frame:
|
||||
if (
|
||||
self.app.mouse_dx == 0
|
||||
and self.app.mouse_dy == 0
|
||||
and not self.camera.moved_this_frame
|
||||
):
|
||||
return
|
||||
# if last onclick was a UI element, don't drag
|
||||
if self.last_click_on_ui:
|
||||
|
|
@ -345,8 +365,7 @@ class GameWorld:
|
|||
if dx == 0 and dy == 0:
|
||||
return
|
||||
# set dragged objects to mouse + offset from mouse when drag started
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x,
|
||||
self.app.mouse_y)
|
||||
x, y, z = vector.screen_to_world(self.app, self.app.mouse_x, self.app.mouse_y)
|
||||
for obj_name, offset in self.drag_objects.items():
|
||||
obj = self.objects[obj_name]
|
||||
if obj.locked:
|
||||
|
|
@ -369,7 +388,7 @@ class GameWorld:
|
|||
"Add given object to our list of selected objects."
|
||||
if not self.app.can_edit:
|
||||
return
|
||||
if obj and (obj.selectable or force) and not obj in self.selected_objects:
|
||||
if obj and (obj.selectable or force) and obj not in self.selected_objects:
|
||||
self.selected_objects.append(obj)
|
||||
self.app.ui.object_selection_changed()
|
||||
|
||||
|
|
@ -387,9 +406,9 @@ class GameWorld:
|
|||
def create_new_game(self, new_game_dir, new_game_title):
|
||||
"Create appropriate dirs and files for a new game, return success."
|
||||
self.unload_game()
|
||||
new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + '/'
|
||||
new_dir = self.app.documents_dir + TOP_GAME_DIR + new_game_dir + "/"
|
||||
if os.path.exists(new_dir):
|
||||
self.app.log('Game dir %s already exists!' % new_game_dir)
|
||||
self.app.log("Game dir %s already exists!" % new_game_dir)
|
||||
return False
|
||||
os.mkdir(new_dir)
|
||||
os.mkdir(new_dir + ART_DIR)
|
||||
|
|
@ -398,12 +417,12 @@ class GameWorld:
|
|||
os.mkdir(new_dir + CHARSET_DIR)
|
||||
os.mkdir(new_dir + PALETTE_DIR)
|
||||
# create a generic starter script with a GO and Player subclass
|
||||
f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + '.py', 'w')
|
||||
f = open(new_dir + GAME_SCRIPTS_DIR + new_game_dir + ".py", "w")
|
||||
f.write(STARTER_SCRIPT)
|
||||
f.close()
|
||||
# load game
|
||||
self.set_game_dir(new_game_dir)
|
||||
self.properties = self.spawn_object_of_class('WorldPropertiesObject')
|
||||
self.properties = self.spawn_object_of_class("WorldPropertiesObject")
|
||||
self.objects.update(self.new_objects)
|
||||
self.new_objects = {}
|
||||
# HACK: set some property defaults, no idea why they don't take :[
|
||||
|
|
@ -493,9 +512,9 @@ class GameWorld:
|
|||
continue
|
||||
self.game_dir = d
|
||||
self.game_name = dir_name
|
||||
if not d.endswith('/'):
|
||||
self.game_dir += '/'
|
||||
self.app.log('Game data folder is now %s' % self.game_dir)
|
||||
if not d.endswith("/"):
|
||||
self.game_dir += "/"
|
||||
self.app.log("Game data folder is now %s" % self.game_dir)
|
||||
# set sounds dir before loading state; some obj inits depend on it
|
||||
self.sounds_dir = self.game_dir + SOUNDS_DIR
|
||||
if reset:
|
||||
|
|
@ -515,14 +534,16 @@ class GameWorld:
|
|||
GameWorld's dicts.
|
||||
"""
|
||||
modules_to_remove = []
|
||||
games_dir_prefix = TOP_GAME_DIR.replace('/', '')
|
||||
this_game_dir_prefix = '%s.%s' % (games_dir_prefix, self.game_name)
|
||||
games_dir_prefix = TOP_GAME_DIR.replace("/", "")
|
||||
this_game_dir_prefix = "%s.%s" % (games_dir_prefix, self.game_name)
|
||||
for module_name in sys.modules:
|
||||
# remove any module that isn't for this game or part of its path
|
||||
if module_name != games_dir_prefix and \
|
||||
module_name != this_game_dir_prefix and \
|
||||
module_name.startswith(games_dir_prefix) and \
|
||||
not module_name.startswith(this_game_dir_prefix + '.'):
|
||||
if (
|
||||
module_name != games_dir_prefix
|
||||
and module_name != this_game_dir_prefix
|
||||
and module_name.startswith(games_dir_prefix)
|
||||
and not module_name.startswith(this_game_dir_prefix + ".")
|
||||
):
|
||||
modules_to_remove.append(module_name)
|
||||
for module_name in modules_to_remove:
|
||||
sys.modules.pop(module_name)
|
||||
|
|
@ -534,16 +555,18 @@ class GameWorld:
|
|||
# build list of module files
|
||||
modules_list = self.builtin_module_names[:]
|
||||
# create appropriately-formatted python import path
|
||||
module_path_prefix = '%s.%s.%s.' % (TOP_GAME_DIR.replace('/', ''),
|
||||
module_path_prefix = "%s.%s.%s." % (
|
||||
TOP_GAME_DIR.replace("/", ""),
|
||||
self.game_name,
|
||||
GAME_SCRIPTS_DIR.replace('/', ''))
|
||||
GAME_SCRIPTS_DIR.replace("/", ""),
|
||||
)
|
||||
for filename in os.listdir(self.game_dir + GAME_SCRIPTS_DIR):
|
||||
# exclude emacs temp files and special world start script
|
||||
if not filename.endswith('.py'):
|
||||
if not filename.endswith(".py"):
|
||||
continue
|
||||
if filename.startswith('.#'):
|
||||
if filename.startswith(".#"):
|
||||
continue
|
||||
new_module_name = module_path_prefix + filename.replace('.py', '')
|
||||
new_module_name = module_path_prefix + filename.replace(".py", "")
|
||||
modules_list.append(new_module_name)
|
||||
return modules_list
|
||||
|
||||
|
|
@ -553,7 +576,7 @@ class GameWorld:
|
|||
refers to when finding classes to spawn.
|
||||
"""
|
||||
# on first load, documents dir may not be in import path
|
||||
if not self.app.documents_dir in sys.path:
|
||||
if self.app.documents_dir not in sys.path:
|
||||
sys.path += [self.app.documents_dir]
|
||||
# clean modules dict before (re)loading anything
|
||||
self._remove_non_current_game_modules()
|
||||
|
|
@ -564,8 +587,10 @@ class GameWorld:
|
|||
for module_name in self._get_game_modules_list():
|
||||
try:
|
||||
# always reload built in modules
|
||||
if module_name in self.builtin_module_names or \
|
||||
module_name in old_modules:
|
||||
if (
|
||||
module_name in self.builtin_module_names
|
||||
or module_name in old_modules
|
||||
):
|
||||
m = importlib.reload(old_modules[module_name])
|
||||
else:
|
||||
m = importlib.import_module(module_name)
|
||||
|
|
@ -578,7 +603,7 @@ class GameWorld:
|
|||
if not self.allow_pause:
|
||||
return
|
||||
self.paused = not self.paused
|
||||
s = 'Game %spaused.' % ['un', ''][self.paused]
|
||||
s = "Game %spaused." % ["un", ""][self.paused]
|
||||
self.app.ui.message_line.post_line(s)
|
||||
|
||||
def get_elapsed_time(self):
|
||||
|
|
@ -609,7 +634,7 @@ class GameWorld:
|
|||
|
||||
def handle_input(self, event, shift_pressed, alt_pressed, ctrl_pressed):
|
||||
# pass event's key to any objects that want to handle it
|
||||
if not event.type in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]:
|
||||
if event.type not in [sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP]:
|
||||
return
|
||||
key = sdl2.SDL_GetKeyName(event.key.keysym.sym).decode()
|
||||
key = key.lower()
|
||||
|
|
@ -622,11 +647,15 @@ class GameWorld:
|
|||
self.try_object_method(obj, obj.handle_key_up, args)
|
||||
# TODO: handle_ functions for other types of input
|
||||
|
||||
def get_colliders_at_point(self, point_x, point_y,
|
||||
def get_colliders_at_point(
|
||||
self,
|
||||
point_x,
|
||||
point_y,
|
||||
include_object_names=[],
|
||||
include_class_names=[],
|
||||
exclude_object_names=[],
|
||||
exclude_class_names=[]):
|
||||
exclude_class_names=[],
|
||||
):
|
||||
"""
|
||||
Return lists of colliding objects and shapes at given point that pass
|
||||
given filters.
|
||||
|
|
@ -645,28 +674,34 @@ class GameWorld:
|
|||
check_objects = []
|
||||
if whitelist_objects or whitelist_classes:
|
||||
# list of class names -> list of classes
|
||||
include_classes = [self.get_class_by_name(class_name) for class_name in include_class_names]
|
||||
include_classes = [
|
||||
self.get_class_by_name(class_name) for class_name in include_class_names
|
||||
]
|
||||
# only given objects of given classes
|
||||
if whitelist_objects and whitelist_classes:
|
||||
for obj_name in include_object_names:
|
||||
obj = self.objects[obj_name]
|
||||
for c in include_classes:
|
||||
if isinstance(obj, c) and not obj in check_objects:
|
||||
if isinstance(obj, c) and obj not in check_objects:
|
||||
check_objects.append(obj)
|
||||
# only given objects of any class
|
||||
elif whitelist_objects and not whitelist_classes:
|
||||
check_objects += [self.objects[obj_name] for obj_name in include_object_names]
|
||||
check_objects += [
|
||||
self.objects[obj_name] for obj_name in include_object_names
|
||||
]
|
||||
# all colliders of given classes
|
||||
elif whitelist_classes:
|
||||
for obj in colliders:
|
||||
for c in include_classes:
|
||||
if isinstance(obj, c) and not obj in check_objects:
|
||||
if isinstance(obj, c) and obj not in check_objects:
|
||||
check_objects.append(obj)
|
||||
else:
|
||||
check_objects = colliders[:]
|
||||
check_objects_unfiltered = check_objects[:]
|
||||
if blacklist_objects or blacklist_classes:
|
||||
exclude_classes = [self.get_class_by_name(class_name) for class_name in exclude_class_names]
|
||||
exclude_classes = [
|
||||
self.get_class_by_name(class_name) for class_name in exclude_class_names
|
||||
]
|
||||
for obj in check_objects_unfiltered:
|
||||
if obj.name in exclude_object_names:
|
||||
check_objects.remove(obj)
|
||||
|
|
@ -710,16 +745,20 @@ class GameWorld:
|
|||
# print('running %s.%s' % (obj.name, method.__name__))
|
||||
try:
|
||||
method(*args)
|
||||
if method.__name__ == 'update':
|
||||
if method.__name__ == "update":
|
||||
obj.last_update_failed = False
|
||||
except Exception as e:
|
||||
if method.__name__ == 'update' and obj.last_update_failed:
|
||||
except Exception:
|
||||
if method.__name__ == "update" and obj.last_update_failed:
|
||||
return
|
||||
obj.last_update_failed = True
|
||||
for line in traceback.format_exc().split('\n'):
|
||||
if line and not 'try_object_method' in line and line.strip() != 'method()':
|
||||
for line in traceback.format_exc().split("\n"):
|
||||
if (
|
||||
line
|
||||
and "try_object_method" not in line
|
||||
and line.strip() != "method()"
|
||||
):
|
||||
self.app.log(line.rstrip())
|
||||
s = 'Error in %s.%s! See console.' % (obj.name, method.__name__)
|
||||
s = "Error in %s.%s! See console." % (obj.name, method.__name__)
|
||||
self.app.ui.message_line.post_line(s, 10, True)
|
||||
|
||||
def pre_update(self):
|
||||
|
|
@ -733,7 +772,9 @@ class GameWorld:
|
|||
self.try_object_method(obj, obj.pre_first_update)
|
||||
obj.pre_first_update_run = True
|
||||
# only run pre_update if not paused
|
||||
elif not self.paused and (obj.is_in_current_room() or obj.update_if_outside_room):
|
||||
elif not self.paused and (
|
||||
obj.is_in_current_room() or obj.update_if_outside_room
|
||||
):
|
||||
# update timers
|
||||
# (copy timers list in case a timer removes itself from object)
|
||||
for timer in list(obj.timer_functions_pre_update.values())[:]:
|
||||
|
|
@ -807,8 +848,7 @@ class GameWorld:
|
|||
in_room = self.current_room is None or obj.is_in_current_room()
|
||||
hide_debug = obj.is_debug and not self.draw_debug_objects
|
||||
# respect object's "should render at all" flag
|
||||
if obj.visible and not hide_debug and \
|
||||
(self.show_all_rooms or in_room):
|
||||
if obj.visible and not hide_debug and (self.show_all_rooms or in_room):
|
||||
visible_objects.append(obj)
|
||||
#
|
||||
# process non "Y sort" objects first
|
||||
|
|
@ -826,9 +866,11 @@ class GameWorld:
|
|||
continue
|
||||
# only draw collision layer if show collision is set, OR if
|
||||
# "draw collision layer" is set
|
||||
if obj.collision_shape_type == collision.CST_TILE and \
|
||||
obj.col_layer_name == obj.art.layer_names[i] and \
|
||||
not obj.draw_col_layer:
|
||||
if (
|
||||
obj.collision_shape_type == collision.CST_TILE
|
||||
and obj.col_layer_name == obj.art.layer_names[i]
|
||||
and not obj.draw_col_layer
|
||||
):
|
||||
if obj.show_collision:
|
||||
item = RenderItem(obj, i, z + obj.z)
|
||||
collision_items.append(item)
|
||||
|
|
@ -846,8 +888,10 @@ class GameWorld:
|
|||
for i, z in enumerate(obj.art.layers_z):
|
||||
if not obj.art.layers_visibility[i]:
|
||||
continue
|
||||
if obj.collision_shape_type == collision.CST_TILE and \
|
||||
obj.col_layer_name == obj.art.layer_names[i]:
|
||||
if (
|
||||
obj.collision_shape_type == collision.CST_TILE
|
||||
and obj.col_layer_name == obj.art.layer_names[i]
|
||||
):
|
||||
if obj.show_collision:
|
||||
item = RenderItem(obj, i, 0)
|
||||
collision_items.append(item)
|
||||
|
|
@ -884,26 +928,24 @@ class GameWorld:
|
|||
for obj in self.objects.values():
|
||||
if obj.should_save:
|
||||
objects.append(obj.get_dict())
|
||||
d = {'objects': objects}
|
||||
d = {"objects": objects}
|
||||
# save rooms if any exist
|
||||
if len(self.rooms) > 0:
|
||||
rooms = [room.get_dict() for room in self.rooms.values()]
|
||||
d['rooms'] = rooms
|
||||
d["rooms"] = rooms
|
||||
if self.current_room:
|
||||
d['current_room'] = self.current_room.name
|
||||
if filename and filename != '':
|
||||
d["current_room"] = self.current_room.name
|
||||
if filename and filename != "":
|
||||
if not filename.endswith(STATE_FILE_EXTENSION):
|
||||
filename += '.' + STATE_FILE_EXTENSION
|
||||
filename = '%s%s' % (self.game_dir, filename)
|
||||
filename += "." + STATE_FILE_EXTENSION
|
||||
filename = "%s%s" % (self.game_dir, filename)
|
||||
else:
|
||||
# state filename example:
|
||||
# games/mytestgame2/1431116386.gs
|
||||
timestamp = int(time.time())
|
||||
filename = '%s%s.%s' % (self.game_dir, timestamp,
|
||||
STATE_FILE_EXTENSION)
|
||||
json.dump(d, open(filename, 'w'),
|
||||
sort_keys=True, indent=1)
|
||||
self.app.log('Saved game state %s to disk.' % filename)
|
||||
filename = "%s%s.%s" % (self.game_dir, timestamp, STATE_FILE_EXTENSION)
|
||||
json.dump(d, open(filename, "w"), sort_keys=True, indent=1)
|
||||
self.app.log("Saved game state %s to disk." % filename)
|
||||
self.app.update_window_title()
|
||||
|
||||
def _get_all_loaded_classes(self):
|
||||
|
|
@ -914,9 +956,13 @@ class GameWorld:
|
|||
for module in self.modules.values():
|
||||
for k, v in module.__dict__.items():
|
||||
# skip anything that's not a game class
|
||||
if not type(v) is type:
|
||||
if type(v) is not type:
|
||||
continue
|
||||
base_classes = (game_object.GameObject, game_hud.GameHUD, game_room.GameRoom)
|
||||
base_classes = (
|
||||
game_object.GameObject,
|
||||
game_hud.GameHUD,
|
||||
game_room.GameRoom,
|
||||
)
|
||||
# TODO: find out why above works but below doesn't!! O___O
|
||||
# base_classes = self.builtin_base_classes
|
||||
if issubclass(v, base_classes):
|
||||
|
|
@ -936,7 +982,7 @@ class GameWorld:
|
|||
obj_class = obj.__class__.__name__
|
||||
spawned = self.spawn_object_of_class(obj_class, x, y)
|
||||
if spawned:
|
||||
self.app.log('%s reset to class defaults' % obj.name)
|
||||
self.app.log("%s reset to class defaults" % obj.name)
|
||||
if obj is self.player:
|
||||
self.player = spawned
|
||||
obj.destroy()
|
||||
|
|
@ -948,21 +994,21 @@ class GameWorld:
|
|||
new_objects.append(self.duplicate_object(obj))
|
||||
# report on objects created
|
||||
if len(new_objects) == 1:
|
||||
self.app.log('%s created from %s' % (new_objects[0].name, obj.name))
|
||||
self.app.log("%s created from %s" % (new_objects[0].name, obj.name))
|
||||
elif len(new_objects) > 1:
|
||||
self.app.log('%s new objects created' % len(new_objects))
|
||||
self.app.log("%s new objects created" % len(new_objects))
|
||||
|
||||
def duplicate_object(self, obj):
|
||||
"Create a duplicate of given object."
|
||||
d = obj.get_dict()
|
||||
# offset new object's location
|
||||
x, y = d['x'], d['y']
|
||||
x, y = d["x"], d["y"]
|
||||
x += obj.renderable.width
|
||||
y -= obj.renderable.height
|
||||
d['x'], d['y'] = x, y
|
||||
d["x"], d["y"] = x, y
|
||||
# new object needs a unique name, use a temp one until object exists
|
||||
# for real and we can give it a proper, more-likely-to-be-unique one
|
||||
d['name'] = obj.name + ' TEMP COPY NAME'
|
||||
d["name"] = obj.name + " TEMP COPY NAME"
|
||||
new_obj = self.spawn_object_from_data(d)
|
||||
# give object a non-duplicate name
|
||||
self.rename_object(new_obj, new_obj.get_unique_name())
|
||||
|
|
@ -977,8 +1023,10 @@ class GameWorld:
|
|||
"Give specified object a new name. Doesn't accept already-in-use names."
|
||||
self.objects.update(self.new_objects)
|
||||
for other_obj in self.objects.values():
|
||||
if not other_obj is self and other_obj.name == new_name:
|
||||
self.app.ui.message_line.post_line("Can't rename %s to %s, name already in use" % (obj.name, new_name))
|
||||
if other_obj is not self and other_obj.name == new_name:
|
||||
self.app.ui.message_line.post_line(
|
||||
"Can't rename %s to %s, name already in use" % (obj.name, new_name)
|
||||
)
|
||||
return
|
||||
self.objects.pop(obj.name)
|
||||
old_name = obj.name
|
||||
|
|
@ -991,13 +1039,13 @@ class GameWorld:
|
|||
|
||||
def spawn_object_of_class(self, class_name, x=None, y=None):
|
||||
"Spawn a new object of given class name at given location."
|
||||
if not class_name in self.classes:
|
||||
if class_name not in self.classes:
|
||||
# no need for log here, import_all prints exception cause
|
||||
# self.app.log("Couldn't find class %s" % class_name)
|
||||
return
|
||||
d = {'class_name': class_name}
|
||||
d = {"class_name": class_name}
|
||||
if x is not None and y is not None:
|
||||
d['x'], d['y'] = x, y
|
||||
d["x"], d["y"] = x, y
|
||||
new_obj = self.spawn_object_from_data(d)
|
||||
self.app.ui.edit_list_panel.items_changed()
|
||||
return new_obj
|
||||
|
|
@ -1005,8 +1053,8 @@ class GameWorld:
|
|||
def spawn_object_from_data(self, object_data):
|
||||
"Spawn a new object with properties populated from given data dict."
|
||||
# load module and class
|
||||
class_name = object_data.get('class_name', None)
|
||||
if not class_name or not class_name in self.classes:
|
||||
class_name = object_data.get("class_name", None)
|
||||
if not class_name or class_name not in self.classes:
|
||||
# no need for log here, import_all prints exception cause
|
||||
# self.app.log("Couldn't parse class %s" % class_name)
|
||||
return
|
||||
|
|
@ -1015,10 +1063,10 @@ class GameWorld:
|
|||
new_object = obj_class(self, object_data)
|
||||
return new_object
|
||||
|
||||
def add_room(self, new_room_name, new_room_classname='GameRoom'):
|
||||
def add_room(self, new_room_name, new_room_classname="GameRoom"):
|
||||
"Add a new Room with given name of (optional) given class."
|
||||
if new_room_name in self.rooms:
|
||||
self.log('Room called %s already exists!' % new_room_name)
|
||||
self.log("Room called %s already exists!" % new_room_name)
|
||||
return
|
||||
new_room_class = self.classes[new_room_classname]
|
||||
new_room = new_room_class(self, new_room_name)
|
||||
|
|
@ -1026,7 +1074,7 @@ class GameWorld:
|
|||
|
||||
def remove_room(self, room_name):
|
||||
"Delete Room with given name."
|
||||
if not room_name in self.rooms:
|
||||
if room_name not in self.rooms:
|
||||
return
|
||||
room = self.rooms.pop(room_name)
|
||||
if room is self.current_room:
|
||||
|
|
@ -1035,7 +1083,7 @@ class GameWorld:
|
|||
|
||||
def change_room(self, new_room_name):
|
||||
"Set world's current active room to Room with given name."
|
||||
if not new_room_name in self.rooms:
|
||||
if new_room_name not in self.rooms:
|
||||
self.app.log("Couldn't change to missing room %s" % new_room_name)
|
||||
return
|
||||
old_room = self.current_room
|
||||
|
|
@ -1062,7 +1110,7 @@ class GameWorld:
|
|||
if not os.path.exists(filename):
|
||||
filename = self.game_dir + filename
|
||||
if not filename.endswith(STATE_FILE_EXTENSION):
|
||||
filename += '.' + STATE_FILE_EXTENSION
|
||||
filename += "." + STATE_FILE_EXTENSION
|
||||
self.app.enter_game_mode()
|
||||
self.unload_game()
|
||||
# tell list panel to reset, its contents might get jostled
|
||||
|
|
@ -1079,7 +1127,7 @@ class GameWorld:
|
|||
return
|
||||
errors = False
|
||||
# spawn objects
|
||||
for obj_data in d['objects']:
|
||||
for obj_data in d["objects"]:
|
||||
obj = self.spawn_object_from_data(obj_data)
|
||||
if not obj:
|
||||
errors = True
|
||||
|
|
@ -1089,44 +1137,46 @@ class GameWorld:
|
|||
self.properties = obj
|
||||
break
|
||||
if not self.properties:
|
||||
self.properties = self.spawn_object_of_class(self.properties_object_class_name, 0, 0)
|
||||
self.properties = self.spawn_object_of_class(
|
||||
self.properties_object_class_name, 0, 0
|
||||
)
|
||||
# spawn a WorldGlobalStateObject
|
||||
self.globals = self.spawn_object_of_class(self.globals_object_class_name, 0, 0)
|
||||
# just for first update, merge new objects list into objects list
|
||||
self.objects.update(self.new_objects)
|
||||
# create rooms
|
||||
for room_data in d.get('rooms', []):
|
||||
for room_data in d.get("rooms", []):
|
||||
# get room class
|
||||
room_class_name = room_data.get('class_name', None)
|
||||
room_class_name = room_data.get("class_name", None)
|
||||
room_class = self.classes.get(room_class_name, game_room.GameRoom)
|
||||
room = room_class(self, room_data['name'], room_data)
|
||||
room = room_class(self, room_data["name"], room_data)
|
||||
self.rooms[room.name] = room
|
||||
start_room = self.rooms.get(d.get('current_room', None), None)
|
||||
start_room = self.rooms.get(d.get("current_room", None), None)
|
||||
if start_room:
|
||||
self.change_room(start_room.name)
|
||||
# spawn hud
|
||||
hud_class = self.classes[d.get('hud_class', self.hud_class_name)]
|
||||
hud_class = self.classes[d.get("hud_class", self.hud_class_name)]
|
||||
self.hud = hud_class(self)
|
||||
self.hud_class_name = hud_class.__name__
|
||||
if not errors and self.app.init_success:
|
||||
self.app.log('Loaded game state from %s' % filename)
|
||||
self.app.log("Loaded game state from %s" % filename)
|
||||
self.last_state_loaded = filename
|
||||
self.set_for_all_objects('show_collision', self.show_collision_all)
|
||||
self.set_for_all_objects('show_bounds', self.show_bounds_all)
|
||||
self.set_for_all_objects('show_origin', self.show_origin_all)
|
||||
self.set_for_all_objects("show_collision", self.show_collision_all)
|
||||
self.set_for_all_objects("show_bounds", self.show_bounds_all)
|
||||
self.set_for_all_objects("show_origin", self.show_origin_all)
|
||||
self.app.update_window_title()
|
||||
self.app.ui.edit_list_panel.items_changed()
|
||||
# self.report()
|
||||
|
||||
def report(self):
|
||||
"Print (not log) information about current world state."
|
||||
print('--------------\n%s report:' % self)
|
||||
print("--------------\n%s report:" % self)
|
||||
obj_arts, obj_rends, obj_dbg_rends, obj_cols, obj_col_rends = 0, 0, 0, 0, 0
|
||||
attachments = 0
|
||||
# create merged dict of existing and just-spawned objects
|
||||
all_objects = self.objects.copy()
|
||||
all_objects.update(self.new_objects)
|
||||
print('%s objects:' % len(all_objects))
|
||||
print("%s objects:" % len(all_objects))
|
||||
for obj in all_objects.values():
|
||||
obj_arts += len(obj.arts)
|
||||
if obj.renderable is not None:
|
||||
|
|
@ -1139,34 +1189,46 @@ class GameWorld:
|
|||
obj_cols += 1
|
||||
obj_col_rends += len(obj.collision.renderables)
|
||||
attachments += len(obj.attachments)
|
||||
print("""
|
||||
print(
|
||||
"""
|
||||
%s arts in objects, %s arts loaded,
|
||||
%s HUD arts, %s HUD renderables,
|
||||
%s renderables, %s debug renderables,
|
||||
%s collideables, %s collideable viz renderables,
|
||||
%s attachments""" % (obj_arts, len(self.art_loaded), len(self.hud.arts),
|
||||
%s attachments"""
|
||||
% (
|
||||
obj_arts,
|
||||
len(self.art_loaded),
|
||||
len(self.hud.arts),
|
||||
len(self.hud.renderables),
|
||||
obj_rends, obj_dbg_rends,
|
||||
obj_cols, obj_col_rends, attachments))
|
||||
obj_rends,
|
||||
obj_dbg_rends,
|
||||
obj_cols,
|
||||
obj_col_rends,
|
||||
attachments,
|
||||
)
|
||||
)
|
||||
self.cl.report()
|
||||
print('%s charsets loaded, %s palettes' % (len(self.app.charsets),
|
||||
len(self.app.palettes)))
|
||||
print('%s arts loaded for edit' % len(self.app.art_loaded_for_edit))
|
||||
print(
|
||||
"%s charsets loaded, %s palettes"
|
||||
% (len(self.app.charsets), len(self.app.palettes))
|
||||
)
|
||||
print("%s arts loaded for edit" % len(self.app.art_loaded_for_edit))
|
||||
|
||||
def toggle_all_origin_viz(self):
|
||||
"Toggle visibility of XYZ markers for all object origins."
|
||||
self.show_origin_all = not self.show_origin_all
|
||||
self.set_for_all_objects('show_origin', self.show_origin_all)
|
||||
self.set_for_all_objects("show_origin", self.show_origin_all)
|
||||
|
||||
def toggle_all_bounds_viz(self):
|
||||
"Toggle visibility of boxes for all object bounds."
|
||||
self.show_bounds_all = not self.show_bounds_all
|
||||
self.set_for_all_objects('show_bounds', self.show_bounds_all)
|
||||
self.set_for_all_objects("show_bounds", self.show_bounds_all)
|
||||
|
||||
def toggle_all_collision_viz(self):
|
||||
"Toggle visibility of debug lines for all object Collideables."
|
||||
self.show_collision_all = not self.show_collision_all
|
||||
self.set_for_all_objects('show_collision', self.show_collision_all)
|
||||
self.set_for_all_objects("show_collision", self.show_collision_all)
|
||||
|
||||
def destroy(self):
|
||||
self.unload_game()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -26,8 +24,8 @@ class LineTester(GameObject):
|
|||
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)
|
||||
|
|
@ -37,14 +35,14 @@ class LineTester(GameObject):
|
|||
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
|
||||
|
|
@ -53,5 +51,5 @@ class LineTester(GameObject):
|
|||
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,15 +1,21 @@
|
|||
|
||||
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
|
||||
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
|
||||
|
|
@ -17,11 +23,7 @@ class CrawlPlayer(Player):
|
|||
|
||||
view_range_tiles = 8
|
||||
fg_color = 8 # yellow
|
||||
dir_chars = { DIR_NORTH: 147,
|
||||
DIR_SOUTH: 163,
|
||||
DIR_EAST: 181,
|
||||
DIR_WEST: 180
|
||||
}
|
||||
dir_chars = {DIR_NORTH: 147, DIR_SOUTH: 163, DIR_EAST: 181, DIR_WEST: 180}
|
||||
|
||||
def pre_first_update(self):
|
||||
Player.pre_first_update(self)
|
||||
|
|
@ -32,14 +34,14 @@ class CrawlPlayer(Player):
|
|||
|
||||
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,16 +1,16 @@
|
|||
|
||||
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
|
||||
|
|
@ -25,12 +25,17 @@ class CrawlTopDownView(GameObject):
|
|||
# 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
|
||||
|
|
@ -80,11 +85,15 @@ 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
|
||||
|
|
@ -99,16 +108,19 @@ 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)
|
||||
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:
|
||||
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)
|
||||
|
|
@ -118,6 +130,5 @@ class CrawlTopDownView(GameObject):
|
|||
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,10 +1,8 @@
|
|||
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
|
||||
|
||||
|
|
@ -16,16 +14,19 @@ class CronoPlayer(TopDownPlayer):
|
|||
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,29 +28,28 @@ 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_charset = "c64_petscii"
|
||||
art_width, art_height = 54, 30 # approximately 16x9 aspect
|
||||
art_palette = 'fireplace'
|
||||
art_palette = "fireplace"
|
||||
handle_key_events = True
|
||||
|
||||
def pre_first_update(self):
|
||||
|
|
@ -80,7 +79,7 @@ class Fireplace(GameObject):
|
|||
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,7 +88,7 @@ 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):
|
||||
|
|
@ -147,7 +146,9 @@ class Fireplace(GameObject):
|
|||
# (looks nicer if we don't clear between frames, actually)
|
||||
# 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)
|
||||
|
|
@ -155,7 +156,9 @@ class Fireplace(GameObject):
|
|||
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)
|
||||
|
|
@ -168,34 +171,37 @@ class Fireplace(GameObject):
|
|||
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):
|
||||
|
|
@ -239,15 +245,14 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from random import choice
|
||||
|
||||
from art import TileIter
|
||||
|
|
@ -21,8 +20,8 @@ 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):
|
||||
|
|
@ -58,7 +57,7 @@ class Board(GameObject):
|
|||
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:
|
||||
|
|
@ -80,8 +79,8 @@ class Board(GameObject):
|
|||
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,8 +89,8 @@ 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)
|
||||
|
|
@ -103,18 +102,18 @@ 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:
|
||||
|
|
@ -122,9 +121,9 @@ class TurnsBar(GameObject):
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,9 +17,9 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
|
||||
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):
|
||||
|
|
@ -30,17 +38,18 @@ 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
|
||||
|
|
@ -52,14 +61,13 @@ 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)
|
||||
|
|
@ -80,13 +88,13 @@ class MazePickup(GameObject):
|
|||
|
||||
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()
|
||||
|
||||
|
|
@ -109,26 +117,26 @@ class MazePickup(GameObject):
|
|||
|
||||
|
||||
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)
|
||||
|
|
@ -146,7 +154,7 @@ 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
|
||||
|
|
@ -159,7 +167,7 @@ class MazeLock(StaticTileBG):
|
|||
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()
|
||||
|
|
@ -168,13 +176,12 @@ 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
|
||||
|
|
@ -182,8 +189,8 @@ class MazePortalGate(MazeLock):
|
|||
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,16 +223,16 @@ 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):
|
||||
|
|
|
|||
|
|
@ -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,12 +17,12 @@ 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)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
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):
|
||||
|
||||
class PlatformPlayer(Player):
|
||||
# from http://www.piratehearts.com/blog/2010/08/30/40/:
|
||||
# JumpSpeed = sqrt(2.0f * Gravity * JumpHeight);
|
||||
|
||||
art_src = 'player'
|
||||
art_src = "player"
|
||||
# collision_shape_type = CST_AABB
|
||||
col_width = 2
|
||||
col_height = 3
|
||||
|
|
@ -25,8 +24,8 @@ 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)
|
||||
|
|
@ -61,15 +60,23 @@ class PlatformPlayer(Player):
|
|||
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
|
||||
|
|
@ -85,18 +92,20 @@ class PlatformPlayer(Player):
|
|||
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
|
||||
|
|
@ -105,7 +114,7 @@ class PlatformMonster(Character):
|
|||
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
|
||||
|
|
@ -125,11 +134,13 @@ class PlatformMonster(Character):
|
|||
# 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,
|
||||
hits, shapes = self.world.get_colliders_at_point(
|
||||
x,
|
||||
y,
|
||||
# include_object_names=[],
|
||||
include_class_names=['PlatformWorld',
|
||||
'PlatformMonster'],
|
||||
exclude_object_names=[self.name])
|
||||
include_class_names=["PlatformWorld", "PlatformMonster"],
|
||||
exclude_object_names=[self.name],
|
||||
)
|
||||
if len(hits) > 0:
|
||||
self.move_dir_x = -self.move_dir_x
|
||||
|
||||
|
|
@ -139,4 +150,4 @@ class PlatformMonster(Character):
|
|||
|
||||
|
||||
class PlatformWarpTrigger(WarpTrigger):
|
||||
warp_class_names = ['Player', 'PlatformMonster']
|
||||
warp_class_names = ["Player", "PlatformMonster"]
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
|
||||
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']
|
||||
serialized = Player.serialized + ["invincible"]
|
||||
respawn_delay = 3
|
||||
# refire delay, else holding X chokes game
|
||||
fire_delay = 0.15
|
||||
|
|
@ -24,11 +25,11 @@ class ShmupPlayer(Player):
|
|||
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
|
||||
|
||||
|
|
@ -37,18 +38,18 @@ class ShmupPlayer(Player):
|
|||
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,12 +59,15 @@ 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
|
||||
|
||||
|
|
@ -73,22 +77,25 @@ class EnemySpawner(ObjectSpawner):
|
|||
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)
|
||||
|
|
@ -106,19 +113,23 @@ 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']
|
||||
serialized = Character.serialized + ["invincible"]
|
||||
|
||||
def started_colliding(self, other):
|
||||
if isinstance(other, ShmupPlayer):
|
||||
|
|
@ -133,8 +144,9 @@ class ShmupEnemy(Character):
|
|||
self.move(0, -1)
|
||||
Character.update(self)
|
||||
|
||||
|
||||
class Enemy1(ShmupEnemy):
|
||||
art_src = 'enemy1'
|
||||
art_src = "enemy1"
|
||||
move_accel_y = 100
|
||||
|
||||
def update(self):
|
||||
|
|
@ -146,8 +158,9 @@ class Enemy1(ShmupEnemy):
|
|||
self.fire_proj()
|
||||
ShmupEnemy.update(self)
|
||||
|
||||
|
||||
class Enemy2(ShmupEnemy):
|
||||
art_src = 'enemy2'
|
||||
art_src = "enemy2"
|
||||
animating = True
|
||||
move_accel_y = 50
|
||||
|
||||
|
|
@ -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,34 +206,38 @@ 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):
|
||||
|
||||
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'
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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,7 +14,6 @@ FLOWER_WIDTH, FLOWER_HEIGHT = 16, 16
|
|||
|
||||
|
||||
class FlowerObject(GameObject):
|
||||
|
||||
generate_art = True
|
||||
should_save = False
|
||||
physics_move = False
|
||||
|
|
@ -54,11 +51,13 @@ 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
|
||||
|
|
@ -91,11 +90,18 @@ class FlowerObject(GameObject):
|
|||
# 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.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.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
|
||||
|
|
@ -111,7 +117,7 @@ class FlowerObject(GameObject):
|
|||
|
||||
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,7 +142,7 @@ 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
|
||||
|
|
@ -148,12 +154,11 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
|
||||
import random
|
||||
|
||||
from games.wildflowers.scripts.ramps import RampIterator
|
||||
|
||||
|
||||
# growth direction consts
|
||||
NONE = (0, 0)
|
||||
LEFT = (-1, 0)
|
||||
|
|
@ -18,18 +16,23 @@ 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
|
||||
|
|
@ -42,8 +45,11 @@ class Frond:
|
|||
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
|
||||
# for straight line growers, set a consistent direction
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -55,7 +67,16 @@ class Petal:
|
|||
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
|
||||
|
|
@ -70,10 +91,11 @@ class Petal:
|
|||
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)
|
||||
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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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,11 +1,10 @@
|
|||
|
||||
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
|
||||
|
|
@ -21,9 +20,9 @@ PALETTE_RAMPS = {
|
|||
(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
|
||||
(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
|
||||
|
|
@ -37,9 +36,9 @@ PALETTE_RAMPS = {
|
|||
(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
|
||||
|
|
@ -57,9 +56,9 @@ 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
|
||||
|
|
@ -74,9 +73,9 @@ PALETTE_RAMPS = {
|
|||
(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
|
||||
(247, 7, -1), # red to yellow
|
||||
],
|
||||
'atari': [
|
||||
"atari": [
|
||||
(113, 8, -16), # white to black
|
||||
(114, 8, -16), # yellow to muddy brown
|
||||
(115, 8, -16), # dull gold to brown
|
||||
|
|
@ -92,13 +91,12 @@ PALETTE_RAMPS = {
|
|||
(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
|
||||
]
|
||||
(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
|
||||
|
|
|
|||
|
|
@ -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,7 +19,6 @@ 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
|
||||
|
|
@ -33,21 +29,21 @@ class FlowerGlobals(WorldGlobalsObject):
|
|||
def pre_first_update(self):
|
||||
# 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
|
||||
|
|
@ -65,19 +61,22 @@ class FlowerGlobals(WorldGlobalsObject):
|
|||
# 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)
|
||||
|
|
|
|||
4
grid.py
4
grid.py
|
|
@ -8,8 +8,8 @@ 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):
|
||||
|
||||
class Grid(LineRenderable):
|
||||
visible = True
|
||||
draw_axes = False
|
||||
|
||||
|
|
@ -74,7 +74,6 @@ class Grid(LineRenderable):
|
|||
|
||||
|
||||
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]
|
||||
|
|
@ -88,7 +87,6 @@ class ArtGrid(Grid):
|
|||
|
||||
|
||||
class GameGrid(Grid):
|
||||
|
||||
draw_axes = True
|
||||
base_size = 800
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
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)
|
||||
|
|
@ -113,7 +119,11 @@ class ImageConverter:
|
|||
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_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):
|
||||
|
|
@ -138,7 +148,9 @@ class ImageConverter:
|
|||
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:
|
||||
|
|
@ -152,8 +164,15 @@ 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)
|
||||
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:
|
||||
|
|
@ -209,7 +228,7 @@ class ImageConverter:
|
|||
# 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
|
||||
|
|
@ -231,22 +250,25 @@ class ImageConverter:
|
|||
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),
|
||||
art.palette.colors[0] = (
|
||||
round(bg_color[0] * 255),
|
||||
round(bg_color[1] * 255),
|
||||
round(bg_color[2] * 255),
|
||||
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')
|
||||
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,8 +112,9 @@ 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
|
||||
|
|
@ -104,13 +122,13 @@ def export_animation(app, art, out_filename, bg_color=None, loop=True):
|
|||
# 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'
|
||||
output_format = "Animated GIF"
|
||||
# app.log('%s exported (%s)' % (out_filename, output_format))
|
||||
|
||||
|
||||
|
|
@ -124,19 +142,19 @@ 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'
|
||||
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()
|
||||
|
|
|
|||
348
input_handler.py
348
input_handler.py
|
|
@ -1,26 +1,74 @@
|
|||
import ctypes, os, platform
|
||||
import sdl2
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import platform
|
||||
from sys import exit
|
||||
|
||||
from ui import SCALE_INCREMENT, OIS_WIDTH, OIS_HEIGHT, OIS_FILL
|
||||
from renderable import LAYER_VIS_FULL, LAYER_VIS_DIM, LAYER_VIS_NONE
|
||||
from ui_art_dialog import NewArtDialog, SaveAsDialog, QuitUnsavedChangesDialog, CloseUnsavedChangesDialog, RevertChangesDialog, ResizeArtDialog, AddFrameDialog, DuplicateFrameDialog, FrameDelayDialog, FrameDelayAllDialog, FrameIndexDialog, AddLayerDialog, DuplicateLayerDialog, SetLayerNameDialog, SetLayerZDialog, PaletteFromFileDialog, ImportFileDialog, ExportFileDialog, SetCameraZoomDialog, ExportOptionsDialog, OverlayImageOpacityDialog
|
||||
from ui_game_dialog import NewGameDirDialog, LoadGameStateDialog, SaveGameStateDialog, AddRoomDialog, SetRoomCamDialog, SetRoomEdgeWarpsDialog, SetRoomBoundsObjDialog, RenameRoomDialog
|
||||
from ui_info_dialog import PagedInfoDialog
|
||||
from ui_file_chooser_dialog import ArtChooserDialog, CharSetChooserDialog, PaletteChooserDialog, PaletteFromImageChooserDialog, RunArtScriptDialog, OverlayImageFileChooserDialog
|
||||
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_WARP, LO_SET_ROOM_EDGE_OBJ, LO_SET_ROOM_CAMERA
|
||||
from collision import CT_NONE
|
||||
from art import ART_DIR, ART_FILE_EXTENSION
|
||||
from key_shifts import NUMLOCK_ON_MAP, NUMLOCK_OFF_MAP
|
||||
import sdl2
|
||||
|
||||
BINDS_FILENAME = 'binds.cfg'
|
||||
BINDS_TEMPLATE_FILENAME = 'binds.cfg.default'
|
||||
from art import ART_DIR, ART_FILE_EXTENSION
|
||||
from collision import CT_NONE
|
||||
from key_shifts import NUMLOCK_OFF_MAP, NUMLOCK_ON_MAP
|
||||
from renderable import LAYER_VIS_DIM, LAYER_VIS_FULL, LAYER_VIS_NONE
|
||||
from ui import OIS_FILL, OIS_HEIGHT, OIS_WIDTH, SCALE_INCREMENT
|
||||
from ui_art_dialog import (
|
||||
AddFrameDialog,
|
||||
AddLayerDialog,
|
||||
CloseUnsavedChangesDialog,
|
||||
DuplicateFrameDialog,
|
||||
DuplicateLayerDialog,
|
||||
ExportFileDialog,
|
||||
ExportOptionsDialog,
|
||||
FrameDelayAllDialog,
|
||||
FrameDelayDialog,
|
||||
FrameIndexDialog,
|
||||
ImportFileDialog,
|
||||
NewArtDialog,
|
||||
OverlayImageOpacityDialog,
|
||||
QuitUnsavedChangesDialog,
|
||||
ResizeArtDialog,
|
||||
RevertChangesDialog,
|
||||
SaveAsDialog,
|
||||
SetCameraZoomDialog,
|
||||
SetLayerNameDialog,
|
||||
SetLayerZDialog,
|
||||
)
|
||||
from ui_file_chooser_dialog import (
|
||||
ArtChooserDialog,
|
||||
CharSetChooserDialog,
|
||||
OverlayImageFileChooserDialog,
|
||||
PaletteChooserDialog,
|
||||
PaletteFromImageChooserDialog,
|
||||
RunArtScriptDialog,
|
||||
)
|
||||
from ui_game_dialog import (
|
||||
AddRoomDialog,
|
||||
NewGameDirDialog,
|
||||
RenameRoomDialog,
|
||||
SaveGameStateDialog,
|
||||
SetRoomBoundsObjDialog,
|
||||
SetRoomCamDialog,
|
||||
SetRoomEdgeWarpsDialog,
|
||||
)
|
||||
from ui_list_operations import (
|
||||
LO_LOAD_STATE,
|
||||
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,
|
||||
)
|
||||
|
||||
BINDS_FILENAME = "binds.cfg"
|
||||
BINDS_TEMPLATE_FILENAME = "binds.cfg.default"
|
||||
|
||||
|
||||
class InputLord:
|
||||
|
||||
"sets up key binds and handles input"
|
||||
|
||||
wheel_zoom_amount = 3.0
|
||||
keyboard_zoom_amount = 1.0
|
||||
|
||||
|
|
@ -36,19 +84,21 @@ class InputLord:
|
|||
# TODO: better solution is find any binds in template but not binds.cfg
|
||||
# and add em
|
||||
binds_filename = self.app.config_dir + BINDS_FILENAME
|
||||
binds_outdated = not os.path.exists(binds_filename) or os.path.getmtime(binds_filename) < os.path.getmtime(BINDS_TEMPLATE_FILENAME)
|
||||
binds_outdated = not os.path.exists(binds_filename) or os.path.getmtime(
|
||||
binds_filename
|
||||
) < os.path.getmtime(BINDS_TEMPLATE_FILENAME)
|
||||
if not binds_outdated and os.path.exists(binds_filename):
|
||||
exec(open(binds_filename).read())
|
||||
self.app.log('Loaded key binds from %s' % binds_filename)
|
||||
self.app.log("Loaded key binds from %s" % binds_filename)
|
||||
else:
|
||||
default_data = open(BINDS_TEMPLATE_FILENAME).readlines()[1:]
|
||||
new_binds = open(binds_filename, 'w')
|
||||
new_binds = open(binds_filename, "w")
|
||||
new_binds.writelines(default_data)
|
||||
new_binds.close()
|
||||
self.app.log('Created new key binds file %s' % binds_filename)
|
||||
exec(''.join(default_data))
|
||||
self.app.log("Created new key binds file %s" % binds_filename)
|
||||
exec("".join(default_data))
|
||||
if not self.edit_bind_src:
|
||||
self.app.log('No bind data found, Is binds.cfg.default present?')
|
||||
self.app.log("No bind data found, Is binds.cfg.default present?")
|
||||
exit()
|
||||
# associate key + mod combos with methods
|
||||
self.edit_binds = {}
|
||||
|
|
@ -59,9 +109,9 @@ class InputLord:
|
|||
# bind data could be a single item (string) or a list/tuple
|
||||
bind_data = self.edit_bind_src[bind_string]
|
||||
if type(bind_data) is str:
|
||||
bind_fnames = ['BIND_%s' % bind_data]
|
||||
bind_fnames = ["BIND_%s" % bind_data]
|
||||
else:
|
||||
bind_fnames = ['BIND_%s' % s for s in bind_data]
|
||||
bind_fnames = ["BIND_%s" % s for s in bind_data]
|
||||
bind_functions = []
|
||||
for bind_fname in bind_fnames:
|
||||
if not hasattr(self, bind_fname):
|
||||
|
|
@ -72,7 +122,9 @@ class InputLord:
|
|||
# TODO: use kewl SDL2 gamepad system
|
||||
js_init = sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_JOYSTICK)
|
||||
if js_init != 0:
|
||||
self.app.log("SDL2: Couldn't initialize joystick subsystem, code %s" % js_init)
|
||||
self.app.log(
|
||||
"SDL2: Couldn't initialize joystick subsystem, code %s" % js_init
|
||||
)
|
||||
return
|
||||
sticks = sdl2.SDL_NumJoysticks()
|
||||
# self.app.log('%s gamepads found' % sticks)
|
||||
|
|
@ -81,10 +133,13 @@ class InputLord:
|
|||
# for now, just grab first pad
|
||||
if sticks > 0:
|
||||
pad = sdl2.SDL_JoystickOpen(0)
|
||||
pad_name = sdl2.SDL_JoystickName(pad).decode('utf-8')
|
||||
pad_name = sdl2.SDL_JoystickName(pad).decode("utf-8")
|
||||
pad_axes = sdl2.SDL_JoystickNumAxes(pad)
|
||||
pad_buttons = sdl2.SDL_JoystickNumButtons(pad)
|
||||
self.app.log('Gamepad found: %s with %s axes, %s buttons' % (pad_name, pad_axes, pad_buttons))
|
||||
self.app.log(
|
||||
"Gamepad found: %s with %s axes, %s buttons"
|
||||
% (pad_name, pad_axes, pad_buttons)
|
||||
)
|
||||
self.gamepad = pad
|
||||
# before main loop begins, set initial mouse position -
|
||||
# SDL_GetMouseState returns 0,0 if the mouse hasn't yet moved
|
||||
|
|
@ -107,11 +162,11 @@ class InputLord:
|
|||
ctrl = False
|
||||
key = None
|
||||
for i in in_string.split():
|
||||
if i.lower() == 'shift':
|
||||
if i.lower() == "shift":
|
||||
shift = True
|
||||
elif i.lower() == 'alt':
|
||||
elif i.lower() == "alt":
|
||||
alt = True
|
||||
elif i.lower() == 'ctrl':
|
||||
elif i.lower() == "ctrl":
|
||||
ctrl = True
|
||||
else:
|
||||
key = i
|
||||
|
|
@ -138,7 +193,7 @@ class InputLord:
|
|||
for bind in self.edit_bind_src:
|
||||
if command_function == self.edit_bind_src[bind]:
|
||||
return bind
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_menu_items_for_command_function(self, function):
|
||||
# search both menus for items; command checks
|
||||
|
|
@ -146,10 +201,10 @@ class InputLord:
|
|||
items = []
|
||||
for button in buttons:
|
||||
# skip eg playscii button
|
||||
if not hasattr(button, 'menu_data'):
|
||||
if not hasattr(button, "menu_data"):
|
||||
continue
|
||||
for item in button.menu_data.items:
|
||||
if function.__name__ == 'BIND_%s' % item.command:
|
||||
if function.__name__ == "BIND_%s" % item.command:
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
|
@ -208,7 +263,9 @@ class InputLord:
|
|||
ms = sdl2.SDL_GetModState()
|
||||
self.capslock_on = bool(ms & sdl2.KMOD_CAPS)
|
||||
# macOS: treat command as interchangeable with control, is this kosher?
|
||||
if platform.system() == 'Darwin' and (ks[sdl2.SDL_SCANCODE_LGUI] or ks[sdl2.SDL_SCANCODE_RGUI]):
|
||||
if platform.system() == "Darwin" and (
|
||||
ks[sdl2.SDL_SCANCODE_LGUI] or ks[sdl2.SDL_SCANCODE_RGUI]
|
||||
):
|
||||
self.ctrl_pressed = True
|
||||
if app.capslock_is_ctrl and ks[sdl2.SDL_SCANCODE_CAPSLOCK]:
|
||||
self.ctrl_pressed = True
|
||||
|
|
@ -216,8 +273,14 @@ class InputLord:
|
|||
mods = self.shift_pressed, self.alt_pressed, self.ctrl_pressed
|
||||
# get controller state
|
||||
if self.gamepad:
|
||||
self.gamepad_left_x = sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTX) / 32768
|
||||
self.gamepad_left_y = sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTY) / -32768
|
||||
self.gamepad_left_x = (
|
||||
sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTX)
|
||||
/ 32768
|
||||
)
|
||||
self.gamepad_left_y = (
|
||||
sdl2.SDL_JoystickGetAxis(self.gamepad, sdl2.SDL_CONTROLLER_AXIS_LEFTY)
|
||||
/ -32768
|
||||
)
|
||||
for event in sdl2.ext.get_events():
|
||||
if event.type == sdl2.SDL_QUIT:
|
||||
app.should_quit = True
|
||||
|
|
@ -239,13 +302,19 @@ class InputLord:
|
|||
if self.ui.console.visible:
|
||||
self.ui.console.handle_input(keysym, *mods)
|
||||
# same with dialog box
|
||||
elif self.ui.active_dialog and self.ui.active_dialog is self.ui.keyboard_focus_element:
|
||||
elif (
|
||||
self.ui.active_dialog
|
||||
and self.ui.active_dialog is self.ui.keyboard_focus_element
|
||||
):
|
||||
self.ui.active_dialog.handle_input(keysym, *mods)
|
||||
# bail, process no further input
|
||||
# sdl2.SDL_PumpEvents()
|
||||
# return
|
||||
# handle text input if text tool is active
|
||||
elif self.ui.selected_tool is self.ui.text_tool and self.ui.text_tool.input_active:
|
||||
elif (
|
||||
self.ui.selected_tool is self.ui.text_tool
|
||||
and self.ui.text_tool.input_active
|
||||
):
|
||||
self.ui.text_tool.handle_keyboard_input(keysym, *mods)
|
||||
# see if there's a function for this bind and run it
|
||||
else:
|
||||
|
|
@ -272,7 +341,11 @@ class InputLord:
|
|||
# keyup shouldn't have any special meaning in a dialog
|
||||
pass
|
||||
elif self.BIND_game_grab in flist:
|
||||
if self.app.game_mode and not self.ui.active_dialog and self.app.gw.player:
|
||||
if (
|
||||
self.app.game_mode
|
||||
and not self.ui.active_dialog
|
||||
and self.app.gw.player
|
||||
):
|
||||
self.app.gw.player.button_unpressed(0)
|
||||
return
|
||||
elif self.BIND_toggle_picker in flist:
|
||||
|
|
@ -281,7 +354,10 @@ class InputLord:
|
|||
self.ui.popup.hide()
|
||||
elif self.BIND_select_or_paint in flist:
|
||||
app.keyboard_editing = True
|
||||
if not self.ui.selected_tool is self.ui.text_tool and not self.ui.text_tool.input_active:
|
||||
if (
|
||||
self.ui.selected_tool is not self.ui.text_tool
|
||||
and not self.ui.text_tool.input_active
|
||||
):
|
||||
self.app.cursor.finish_paint()
|
||||
#
|
||||
# mouse events aren't handled by bind table for now
|
||||
|
|
@ -292,8 +368,9 @@ class InputLord:
|
|||
if self.app.can_edit:
|
||||
if event.wheel.y > 0:
|
||||
# only zoom in should track towards cursor
|
||||
app.camera.zoom(-self.wheel_zoom_amount,
|
||||
towards_cursor=True)
|
||||
app.camera.zoom(
|
||||
-self.wheel_zoom_amount, towards_cursor=True
|
||||
)
|
||||
elif event.wheel.y < 0:
|
||||
app.camera.zoom(self.wheel_zoom_amount)
|
||||
else:
|
||||
|
|
@ -308,14 +385,26 @@ class InputLord:
|
|||
self.app.gw.unclicked(event.button.button)
|
||||
# LMB up: finish paint for most tools, end select drag
|
||||
if event.button.button == sdl2.SDL_BUTTON_LEFT:
|
||||
if self.ui.selected_tool is self.ui.select_tool and self.ui.select_tool.selection_in_progress:
|
||||
self.ui.select_tool.finish_select(self.shift_pressed, self.ctrl_pressed)
|
||||
elif not self.ui.selected_tool is self.ui.text_tool and not self.ui.text_tool.input_active:
|
||||
if (
|
||||
self.ui.selected_tool is self.ui.select_tool
|
||||
and self.ui.select_tool.selection_in_progress
|
||||
):
|
||||
self.ui.select_tool.finish_select(
|
||||
self.shift_pressed, self.ctrl_pressed
|
||||
)
|
||||
elif (
|
||||
self.ui.selected_tool is not self.ui.text_tool
|
||||
and not self.ui.text_tool.input_active
|
||||
):
|
||||
app.cursor.finish_paint()
|
||||
elif event.type == sdl2.SDL_MOUSEBUTTONDOWN:
|
||||
ui_clicked = self.ui.clicked(event.button.button)
|
||||
# don't register edit commands if a menu is up
|
||||
if ui_clicked or self.ui.menu_bar.active_menu_name or self.ui.active_dialog:
|
||||
if (
|
||||
ui_clicked
|
||||
or self.ui.menu_bar.active_menu_name
|
||||
or self.ui.active_dialog
|
||||
):
|
||||
sdl2.SDL_PumpEvents()
|
||||
if self.app.game_mode:
|
||||
self.app.gw.last_click_on_ui = True
|
||||
|
|
@ -330,15 +419,20 @@ class InputLord:
|
|||
return
|
||||
elif self.ui.selected_tool is self.ui.text_tool:
|
||||
# text tool: only start entry if click is outside popup
|
||||
if not self.ui.text_tool.input_active and \
|
||||
not self.ui.popup in self.ui.hovered_elements:
|
||||
if (
|
||||
not self.ui.text_tool.input_active
|
||||
and self.ui.popup not in self.ui.hovered_elements
|
||||
):
|
||||
self.ui.text_tool.start_entry()
|
||||
elif self.ui.selected_tool is self.ui.select_tool:
|
||||
# select tool: accept clicks if they're outside the popup
|
||||
if not self.ui.select_tool.selection_in_progress and \
|
||||
(not self.ui.keyboard_focus_element or \
|
||||
(self.ui.keyboard_focus_element is self.ui.popup and \
|
||||
not self.ui.popup in self.ui.hovered_elements)):
|
||||
if not self.ui.select_tool.selection_in_progress and (
|
||||
not self.ui.keyboard_focus_element
|
||||
or (
|
||||
self.ui.keyboard_focus_element is self.ui.popup
|
||||
and self.ui.popup not in self.ui.hovered_elements
|
||||
)
|
||||
):
|
||||
self.ui.select_tool.start_select()
|
||||
else:
|
||||
app.cursor.start_paint()
|
||||
|
|
@ -349,20 +443,49 @@ class InputLord:
|
|||
if self.ui.active_dialog:
|
||||
sdl2.SDL_PumpEvents()
|
||||
return
|
||||
|
||||
# directly query keys we don't want affected by OS key repeat delay
|
||||
# TODO: these are hard-coded for the moment, think of a good way
|
||||
# to expose this functionality to the key bind system
|
||||
def pressing_up(ks):
|
||||
return ks[sdl2.SDL_SCANCODE_W] or ks[sdl2.SDL_SCANCODE_UP] or ks[sdl2.SDL_SCANCODE_KP_8]
|
||||
return (
|
||||
ks[sdl2.SDL_SCANCODE_W]
|
||||
or ks[sdl2.SDL_SCANCODE_UP]
|
||||
or ks[sdl2.SDL_SCANCODE_KP_8]
|
||||
)
|
||||
|
||||
def pressing_down(ks):
|
||||
return ks[sdl2.SDL_SCANCODE_S] or ks[sdl2.SDL_SCANCODE_DOWN] or ks[sdl2.SDL_SCANCODE_KP_2]
|
||||
return (
|
||||
ks[sdl2.SDL_SCANCODE_S]
|
||||
or ks[sdl2.SDL_SCANCODE_DOWN]
|
||||
or ks[sdl2.SDL_SCANCODE_KP_2]
|
||||
)
|
||||
|
||||
def pressing_left(ks):
|
||||
return ks[sdl2.SDL_SCANCODE_A] or ks[sdl2.SDL_SCANCODE_LEFT] or ks[sdl2.SDL_SCANCODE_KP_4]
|
||||
return (
|
||||
ks[sdl2.SDL_SCANCODE_A]
|
||||
or ks[sdl2.SDL_SCANCODE_LEFT]
|
||||
or ks[sdl2.SDL_SCANCODE_KP_4]
|
||||
)
|
||||
|
||||
def pressing_right(ks):
|
||||
return ks[sdl2.SDL_SCANCODE_D] or ks[sdl2.SDL_SCANCODE_RIGHT] or ks[sdl2.SDL_SCANCODE_KP_6]
|
||||
return (
|
||||
ks[sdl2.SDL_SCANCODE_D]
|
||||
or ks[sdl2.SDL_SCANCODE_RIGHT]
|
||||
or ks[sdl2.SDL_SCANCODE_KP_6]
|
||||
)
|
||||
|
||||
# prevent camera move if: console is up, text input is active, editing
|
||||
# is not allowed
|
||||
if self.shift_pressed and not self.alt_pressed and not self.ctrl_pressed and not self.ui.console.visible and not self.ui.text_tool.input_active and self.app.can_edit and self.ui.keyboard_focus_element is None:
|
||||
if (
|
||||
self.shift_pressed
|
||||
and not self.alt_pressed
|
||||
and not self.ctrl_pressed
|
||||
and not self.ui.console.visible
|
||||
and not self.ui.text_tool.input_active
|
||||
and self.app.can_edit
|
||||
and self.ui.keyboard_focus_element is None
|
||||
):
|
||||
if pressing_up(ks):
|
||||
app.camera.pan(0, 1, True)
|
||||
if pressing_down(ks):
|
||||
|
|
@ -372,14 +495,24 @@ class InputLord:
|
|||
if pressing_right(ks):
|
||||
app.camera.pan(1, 0, True)
|
||||
if ks[sdl2.SDL_SCANCODE_X]:
|
||||
app.camera.zoom(-self.keyboard_zoom_amount, keyboard=True,
|
||||
towards_cursor=True)
|
||||
app.camera.zoom(
|
||||
-self.keyboard_zoom_amount, keyboard=True, towards_cursor=True
|
||||
)
|
||||
if ks[sdl2.SDL_SCANCODE_Z]:
|
||||
app.camera.zoom(self.keyboard_zoom_amount, keyboard=True)
|
||||
if self.app.can_edit and app.middle_mouse and (app.mouse_dx != 0 or app.mouse_dy != 0):
|
||||
if (
|
||||
self.app.can_edit
|
||||
and app.middle_mouse
|
||||
and (app.mouse_dx != 0 or app.mouse_dy != 0)
|
||||
):
|
||||
app.camera.mouse_pan(app.mouse_dx, app.mouse_dy)
|
||||
# game mode: arrow keys and left gamepad stick move player
|
||||
if self.app.game_mode and not self.ui.console.visible and not self.ui.active_dialog and self.ui.keyboard_focus_element is None:
|
||||
if (
|
||||
self.app.game_mode
|
||||
and not self.ui.console.visible
|
||||
and not self.ui.active_dialog
|
||||
and self.ui.keyboard_focus_element is None
|
||||
):
|
||||
if pressing_up(ks):
|
||||
# shift = move selected
|
||||
if self.shift_pressed and self.app.can_edit:
|
||||
|
|
@ -409,7 +542,7 @@ class InputLord:
|
|||
|
||||
def is_key_pressed(self, key):
|
||||
"returns True if given key is pressed"
|
||||
key = bytes(key, encoding='utf-8')
|
||||
key = bytes(key, encoding="utf-8")
|
||||
scancode = sdl2.keyboard.SDL_GetScancodeFromName(key)
|
||||
return sdl2.SDL_GetKeyboardState(None)[scancode]
|
||||
|
||||
|
|
@ -443,8 +576,9 @@ class InputLord:
|
|||
out_filename = self.ui.active_art.filename
|
||||
out_filename = os.path.basename(out_filename)
|
||||
out_filename = os.path.splitext(out_filename)[0]
|
||||
ExportOptionsDialog.do_export(self.ui.app, out_filename,
|
||||
self.ui.app.last_export_options)
|
||||
ExportOptionsDialog.do_export(
|
||||
self.ui.app, out_filename, self.ui.app.last_export_options
|
||||
)
|
||||
else:
|
||||
self.ui.open_dialog(ExportFileDialog)
|
||||
|
||||
|
|
@ -657,7 +791,7 @@ class InputLord:
|
|||
self.app.gw.save_last_state()
|
||||
elif self.ui.active_art:
|
||||
# if new document, ask for a name
|
||||
default_name = ART_DIR + 'new.' + ART_FILE_EXTENSION
|
||||
default_name = ART_DIR + "new." + ART_FILE_EXTENSION
|
||||
if self.ui.active_art.filename == default_name:
|
||||
self.ui.open_dialog(SaveAsDialog)
|
||||
else:
|
||||
|
|
@ -742,10 +876,10 @@ class InputLord:
|
|||
def BIND_toggle_camera_tilt(self):
|
||||
if self.app.camera.y_tilt == 2:
|
||||
self.app.camera.y_tilt = 0
|
||||
self.ui.message_line.post_line('Camera tilt disengaged.')
|
||||
self.ui.message_line.post_line("Camera tilt disengaged.")
|
||||
else:
|
||||
self.app.camera.y_tilt = 2
|
||||
self.ui.message_line.post_line('Camera tilt engaged.')
|
||||
self.ui.message_line.post_line("Camera tilt engaged.")
|
||||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
||||
def BIND_select_overlay_image(self):
|
||||
|
|
@ -791,7 +925,10 @@ class InputLord:
|
|||
return
|
||||
if not self.ui.active_art:
|
||||
return
|
||||
elif self.ui.selected_tool is self.ui.text_tool and not self.ui.text_tool.input_active:
|
||||
elif (
|
||||
self.ui.selected_tool is self.ui.text_tool
|
||||
and not self.ui.text_tool.input_active
|
||||
):
|
||||
self.ui.text_tool.start_entry()
|
||||
elif self.ui.selected_tool is self.ui.select_tool:
|
||||
if self.ui.select_tool.selection_in_progress:
|
||||
|
|
@ -806,10 +943,10 @@ class InputLord:
|
|||
self.app.screenshot()
|
||||
|
||||
def BIND_run_test_mutate(self):
|
||||
if self.ui.active_art.is_script_running('conway'):
|
||||
self.ui.active_art.stop_script('conway')
|
||||
if self.ui.active_art.is_script_running("conway"):
|
||||
self.ui.active_art.stop_script("conway")
|
||||
else:
|
||||
self.ui.active_art.run_script_every('conway', 0.05)
|
||||
self.ui.active_art.run_script_every("conway", 0.05)
|
||||
|
||||
def BIND_arrow_up(self):
|
||||
if self.ui.keyboard_focus_element:
|
||||
|
|
@ -841,60 +978,60 @@ class InputLord:
|
|||
return
|
||||
if self.ui.active_art.layers == 1:
|
||||
return
|
||||
message_text = 'Non-active layers: '
|
||||
message_text = "Non-active layers: "
|
||||
if self.app.inactive_layer_visibility == LAYER_VIS_FULL:
|
||||
self.app.inactive_layer_visibility = LAYER_VIS_DIM
|
||||
message_text += 'dim'
|
||||
message_text += "dim"
|
||||
elif self.app.inactive_layer_visibility == LAYER_VIS_DIM:
|
||||
self.app.inactive_layer_visibility = LAYER_VIS_NONE
|
||||
message_text += 'invisible'
|
||||
message_text += "invisible"
|
||||
else:
|
||||
self.app.inactive_layer_visibility = LAYER_VIS_FULL
|
||||
message_text += 'visible'
|
||||
message_text += "visible"
|
||||
self.ui.message_line.post_line(message_text)
|
||||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
||||
def BIND_open_file_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('file')
|
||||
self.ui.menu_bar.open_menu_by_name("file")
|
||||
|
||||
def BIND_open_edit_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('edit')
|
||||
self.ui.menu_bar.open_menu_by_name("edit")
|
||||
|
||||
def BIND_open_tool_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('tool')
|
||||
self.ui.menu_bar.open_menu_by_name("tool")
|
||||
|
||||
def BIND_open_view_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('view')
|
||||
self.ui.menu_bar.open_menu_by_name("view")
|
||||
|
||||
def BIND_open_art_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('art')
|
||||
self.ui.menu_bar.open_menu_by_name("art")
|
||||
|
||||
def BIND_open_frame_menu(self):
|
||||
if self.app.game_mode:
|
||||
self.ui.menu_bar.open_menu_by_name('room')
|
||||
self.ui.menu_bar.open_menu_by_name("room")
|
||||
else:
|
||||
self.ui.menu_bar.open_menu_by_name('frame')
|
||||
self.ui.menu_bar.open_menu_by_name("frame")
|
||||
|
||||
def BIND_open_layer_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('layer')
|
||||
self.ui.menu_bar.open_menu_by_name("layer")
|
||||
|
||||
def BIND_open_char_color_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('char_color')
|
||||
self.ui.menu_bar.open_menu_by_name("char_color")
|
||||
|
||||
def BIND_open_help_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('help')
|
||||
self.ui.menu_bar.open_menu_by_name("help")
|
||||
|
||||
def BIND_open_game_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('game')
|
||||
self.ui.menu_bar.open_menu_by_name("game")
|
||||
|
||||
def BIND_open_state_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('state')
|
||||
self.ui.menu_bar.open_menu_by_name("state")
|
||||
|
||||
def BIND_open_world_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('world')
|
||||
self.ui.menu_bar.open_menu_by_name("world")
|
||||
|
||||
def BIND_open_object_menu(self):
|
||||
self.ui.menu_bar.open_menu_by_name('object')
|
||||
self.ui.menu_bar.open_menu_by_name("object")
|
||||
|
||||
def BIND_new_art(self):
|
||||
self.ui.open_dialog(NewArtDialog)
|
||||
|
|
@ -943,12 +1080,14 @@ class InputLord:
|
|||
self.ui.open_dialog(ResizeArtDialog)
|
||||
|
||||
def BIND_art_flip_horizontal(self):
|
||||
self.ui.active_art.flip_horizontal(self.ui.active_art.active_frame,
|
||||
self.ui.active_art.active_layer)
|
||||
self.ui.active_art.flip_horizontal(
|
||||
self.ui.active_art.active_frame, self.ui.active_art.active_layer
|
||||
)
|
||||
|
||||
def BIND_art_flip_vertical(self):
|
||||
self.ui.active_art.flip_vertical(self.ui.active_art.active_frame,
|
||||
self.ui.active_art.active_layer)
|
||||
self.ui.active_art.flip_vertical(
|
||||
self.ui.active_art.active_frame, self.ui.active_art.active_layer
|
||||
)
|
||||
|
||||
def BIND_art_toggle_flip_affects_xforms(self):
|
||||
self.ui.flip_affects_xforms = not self.ui.flip_affects_xforms
|
||||
|
|
@ -1085,10 +1224,10 @@ class InputLord:
|
|||
for obj in self.app.gw.selected_objects:
|
||||
if obj.orig_collision_type and obj.collision_type == CT_NONE:
|
||||
obj.enable_collision()
|
||||
self.ui.message_line.post_line('Collision enabled for %s' % obj.name)
|
||||
self.ui.message_line.post_line("Collision enabled for %s" % obj.name)
|
||||
elif obj.collision_type != CT_NONE:
|
||||
obj.disable_collision()
|
||||
self.ui.message_line.post_line('Collision disabled for %s' % obj.name)
|
||||
self.ui.message_line.post_line("Collision disabled for %s" % obj.name)
|
||||
|
||||
def BIND_toggle_game_edit_ui(self):
|
||||
self.ui.toggle_game_edit_ui()
|
||||
|
|
@ -1097,7 +1236,12 @@ class InputLord:
|
|||
# game mode binds
|
||||
#
|
||||
def accept_normal_game_input(self):
|
||||
return self.app.game_mode and self.app.gw.player and not self.ui.active_dialog and not self.ui.pulldown.visible
|
||||
return (
|
||||
self.app.game_mode
|
||||
and self.app.gw.player
|
||||
and not self.ui.active_dialog
|
||||
and not self.ui.pulldown.visible
|
||||
)
|
||||
|
||||
# TODO: generalize these two somehow
|
||||
def BIND_game_frob(self):
|
||||
|
|
@ -1153,7 +1297,9 @@ class InputLord:
|
|||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
||||
def BIND_toggle_room_camera_changes(self):
|
||||
self.app.gw.properties.set_object_property('room_camera_changes_enabled', not self.app.gw.room_camera_changes_enabled)
|
||||
self.app.gw.properties.set_object_property(
|
||||
"room_camera_changes_enabled", not self.app.gw.room_camera_changes_enabled
|
||||
)
|
||||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
||||
def BIND_set_room_camera_marker(self):
|
||||
|
|
@ -1199,7 +1345,9 @@ class InputLord:
|
|||
self.ui.edit_list_panel.set_list_operation(LO_SET_ROOM_EDGE_OBJ)
|
||||
|
||||
def BIND_toggle_list_only_room_objects(self):
|
||||
self.app.gw.list_only_current_room_objects = not self.app.gw.list_only_current_room_objects
|
||||
self.app.gw.list_only_current_room_objects = (
|
||||
not self.app.gw.list_only_current_room_objects
|
||||
)
|
||||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
||||
def BIND_rename_current_room(self):
|
||||
|
|
@ -1208,5 +1356,7 @@ class InputLord:
|
|||
def BIND_toggle_debug_objects(self):
|
||||
if not self.app.gw.properties:
|
||||
return
|
||||
self.app.gw.properties.set_object_property('draw_debug_objects', not self.app.gw.draw_debug_objects)
|
||||
self.app.gw.properties.set_object_property(
|
||||
"draw_debug_objects", not self.app.gw.draw_debug_objects
|
||||
)
|
||||
self.ui.menu_bar.refresh_active_menu()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import math
|
||||
|
||||
|
||||
def rgb_to_xyz(r, g, b):
|
||||
r /= 255.0
|
||||
g /= 255.0
|
||||
|
|
@ -28,6 +29,7 @@ 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
|
||||
|
|
@ -50,10 +52,12 @@ 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
|
||||
|
|
|
|||
91
palette.py
91
palette.py
|
|
@ -1,16 +1,19 @@
|
|||
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:
|
||||
|
||||
class PaletteLord:
|
||||
# time in ms between checks for hot reload
|
||||
hot_reload_check_interval = 2 * 1000
|
||||
|
||||
|
|
@ -19,7 +22,10 @@ class PaletteLord:
|
|||
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,7 +83,7 @@ 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
|
||||
|
|
@ -86,7 +94,7 @@ 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)
|
||||
|
|
@ -106,7 +114,7 @@ class Palette:
|
|||
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):
|
||||
|
|
@ -122,10 +130,11 @@ class Palette:
|
|||
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):
|
||||
|
|
@ -137,20 +146,23 @@ class Palette:
|
|||
|
||||
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):
|
||||
|
|
@ -169,8 +181,7 @@ class Palette:
|
|||
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.
|
||||
|
|
@ -202,14 +213,13 @@ class Palette:
|
|||
|
||||
|
||||
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:
|
||||
|
|
@ -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,9 +248,9 @@ 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"
|
||||
|
|
@ -248,7 +258,6 @@ class PaletteFromList(Palette):
|
|||
|
||||
|
||||
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)
|
||||
|
|
|
|||
583
playscii.py
583
playscii.py
File diff suppressed because it is too large
Load diff
266
renderable.py
266
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,17 +19,18 @@ 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
|
||||
|
|
@ -69,65 +73,125 @@ 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):
|
||||
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))
|
||||
|
||||
|
|
@ -143,21 +207,47 @@ 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)
|
||||
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):
|
||||
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)
|
||||
|
||||
|
|
@ -177,7 +267,9 @@ 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."
|
||||
|
|
@ -227,7 +319,9 @@ class TileRenderable:
|
|||
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
|
||||
|
|
@ -301,11 +395,21 @@ class TileRenderable:
|
|||
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):
|
||||
"""
|
||||
|
|
@ -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)
|
||||
|
|
@ -392,24 +498,34 @@ class TileRenderable:
|
|||
# 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,7 +578,6 @@ class TileRenderable:
|
|||
|
||||
|
||||
class OnionTileRenderable(TileRenderable):
|
||||
|
||||
"TileRenderable subclass used for onion skin display in Art Mode animation."
|
||||
|
||||
# never animate
|
||||
|
|
@ -464,7 +589,6 @@ class OnionTileRenderable(TileRenderable):
|
|||
|
||||
|
||||
class GameObjectRenderable(TileRenderable):
|
||||
|
||||
"""
|
||||
TileRenderable subclass used by GameObjects. Almost no custom logic for now.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -22,7 +27,7 @@ class LineRenderable():
|
|||
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,44 +41,60 @@ 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"
|
||||
|
|
@ -101,18 +122,30 @@ class LineRenderable():
|
|||
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):
|
||||
|
|
@ -145,14 +178,18 @@ class LineRenderable():
|
|||
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,8 +243,8 @@ 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
|
||||
|
||||
|
|
@ -211,7 +255,6 @@ class UIRenderableX(LineRenderable):
|
|||
|
||||
|
||||
class SwatchSelectionBoxRenderable(LineRenderable):
|
||||
|
||||
"used for UI selection boxes etc"
|
||||
|
||||
color = (0.5, 0.5, 0.5, 1)
|
||||
|
|
@ -226,7 +269,9 @@ class SwatchSelectionBoxRenderable(LineRenderable):
|
|||
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):
|
||||
|
|
@ -241,6 +286,7 @@ class ToolSelectionBoxRenderable(LineRenderable):
|
|||
|
||||
class WorldLineRenderable(LineRenderable):
|
||||
"any LineRenderable that draws in world, ie in 3D perspective"
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.camera.projection_matrix
|
||||
|
||||
|
|
@ -249,7 +295,6 @@ class WorldLineRenderable(LineRenderable):
|
|||
|
||||
|
||||
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
|
||||
|
|
@ -268,8 +313,9 @@ 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):
|
||||
|
|
@ -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,13 +346,16 @@ 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):
|
||||
|
|
@ -326,7 +375,6 @@ class DebugLineRenderable(WorldLineRenderable):
|
|||
|
||||
|
||||
class OriginIndicatorRenderable(WorldLineRenderable):
|
||||
|
||||
"classic 3-axis thingy showing location/rotation/scale"
|
||||
|
||||
red = (1.0, 0.1, 0.1, 1.0)
|
||||
|
|
@ -357,13 +405,23 @@ class OriginIndicatorRenderable(WorldLineRenderable):
|
|||
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)
|
||||
|
|
@ -393,19 +451,27 @@ class BoundsIndicatorRenderable(WorldLineRenderable):
|
|||
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)
|
||||
|
|
@ -435,7 +501,6 @@ def get_circle_points(radius, steps=24):
|
|||
|
||||
|
||||
class CircleCollisionRenderable(CollisionRenderable):
|
||||
|
||||
line_width = 2
|
||||
segments = 24
|
||||
|
||||
|
|
@ -469,7 +534,6 @@ class CircleCollisionRenderable(CollisionRenderable):
|
|||
|
||||
|
||||
class BoxCollisionRenderable(CollisionRenderable):
|
||||
|
||||
line_width = 2
|
||||
|
||||
def get_quad_size(self):
|
||||
|
|
@ -483,12 +547,16 @@ class BoxCollisionRenderable(CollisionRenderable):
|
|||
|
||||
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,18 +1,20 @@
|
|||
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:
|
||||
|
||||
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
|
||||
|
|
@ -21,7 +23,7 @@ class SpriteRenderable:
|
|||
|
||||
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,29 +35,36 @@ 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)
|
||||
|
|
@ -87,8 +96,12 @@ class SpriteRenderable:
|
|||
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,7 +125,6 @@ class SpriteRenderable:
|
|||
|
||||
|
||||
class UISpriteRenderable(SpriteRenderable):
|
||||
|
||||
def get_projection_matrix(self):
|
||||
return self.app.ui.view_matrix
|
||||
|
||||
|
|
@ -122,7 +135,7 @@ 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):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from renderable_line import LineRenderable
|
||||
|
||||
class SelectionRenderable(LineRenderable):
|
||||
|
||||
class SelectionRenderable(LineRenderable):
|
||||
color = (0.8, 0.8, 0.8, 1)
|
||||
line_width = 2
|
||||
x, y, z = 0, 0, 0
|
||||
|
|
@ -35,10 +36,12 @@ class SelectionRenderable(LineRenderable):
|
|||
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]
|
||||
colors += self.color * 2
|
||||
|
||||
# verts = corners
|
||||
if not above:
|
||||
# top edge
|
||||
|
|
|
|||
62
shader.py
62
shader.py
|
|
@ -1,11 +1,14 @@
|
|||
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
|
||||
|
||||
|
|
@ -17,7 +20,10 @@ class ShaderLord:
|
|||
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:
|
||||
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)
|
||||
|
|
@ -25,7 +31,10 @@ class ShaderLord:
|
|||
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:
|
||||
|
|
@ -41,7 +50,6 @@ class ShaderLord:
|
|||
|
||||
|
||||
class Shader:
|
||||
|
||||
log_compile = False
|
||||
"If True, log shader compilation"
|
||||
# per-platform shader versions, declared here for easier CFG fiddling
|
||||
|
|
@ -57,36 +65,46 @@ 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):
|
||||
|
|
@ -94,12 +112,12 @@ class Shader:
|
|||
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
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
15
texture.py
15
texture.py
|
|
@ -1,8 +1,8 @@
|
|||
import numpy as np
|
||||
from OpenGL import GL
|
||||
|
||||
class Texture:
|
||||
|
||||
class Texture:
|
||||
# TODO: move texture data init to a set method to make hot reload trivial(?)
|
||||
|
||||
mag_filter = GL.GL_NEAREST
|
||||
|
|
@ -18,8 +18,17 @@ 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)
|
||||
|
||||
|
|
|
|||
244
ui.py
244
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,39 +51,46 @@ 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
|
||||
|
|
@ -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.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.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)
|
||||
|
|
@ -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,7 +207,10 @@ 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:
|
||||
|
|
@ -231,13 +276,14 @@ 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):
|
||||
|
|
@ -272,7 +318,7 @@ class UI:
|
|||
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,16 +331,22 @@ 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)
|
||||
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)
|
||||
|
|
@ -322,11 +374,12 @@ class UI:
|
|||
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
|
||||
|
|
@ -340,18 +393,20 @@ class UI:
|
|||
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):
|
||||
total_onion_frames += 1
|
||||
|
|
@ -398,10 +453,12 @@ class UI:
|
|||
# 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
|
||||
|
||||
|
|
@ -415,10 +472,20 @@ 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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 = {}
|
||||
|
|
@ -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:
|
||||
|
|
@ -642,7 +716,11 @@ 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
|
||||
|
||||
|
|
@ -660,10 +738,12 @@ 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.active_dialog.handle_input(
|
||||
keycode,
|
||||
self.app.il.shift_pressed,
|
||||
self.app.il.alt_pressed,
|
||||
self.app.il.ctrl_pressed)
|
||||
self.app.il.ctrl_pressed,
|
||||
)
|
||||
handled = True
|
||||
elif len(self.hovered_elements) > 0:
|
||||
for e in self.hovered_elements:
|
||||
|
|
@ -712,11 +792,11 @@ 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):
|
||||
|
|
@ -727,9 +807,11 @@ class UI:
|
|||
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:
|
||||
|
|
@ -764,9 +846,15 @@ 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):
|
||||
|
|
@ -774,9 +862,7 @@ class UI:
|
|||
|
||||
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)
|
||||
|
||||
|
|
|
|||
430
ui_art_dialog.py
430
ui_art_dialog.py
|
|
@ -1,30 +1,35 @@
|
|||
import os.path
|
||||
|
||||
from ui_dialog import UIDialog, Field
|
||||
from ui_chooser_dialog import ChooserDialog, ChooserItemButton, ChooserItem
|
||||
|
||||
from ui_console import OpenCommand, SaveCommand
|
||||
from art import ART_DIR, ART_FILE_EXTENSION, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_FRAME_DELAY, DEFAULT_LAYER_Z_OFFSET
|
||||
from art import (
|
||||
ART_DIR,
|
||||
ART_FILE_EXTENSION,
|
||||
DEFAULT_FRAME_DELAY,
|
||||
DEFAULT_HEIGHT,
|
||||
DEFAULT_LAYER_Z_OFFSET,
|
||||
DEFAULT_WIDTH,
|
||||
)
|
||||
from palette import PaletteFromFile
|
||||
from ui_chooser_dialog import ChooserDialog, ChooserItem, ChooserItemButton
|
||||
from ui_console import SaveCommand
|
||||
from ui_dialog import Field, UIDialog
|
||||
|
||||
|
||||
class BaseFileDialog(UIDialog):
|
||||
|
||||
invalid_filename_error = 'Filename is not valid.'
|
||||
filename_exists_error = 'File by that name already exists.'
|
||||
invalid_filename_error = "Filename is not valid."
|
||||
filename_exists_error = "File by that name already exists."
|
||||
|
||||
def get_file_extension(self):
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_dir(self):
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_full_filename(self, filename, dir=None):
|
||||
for forbidden_char in self.ui.app.forbidden_filename_chars:
|
||||
if forbidden_char in filename:
|
||||
return
|
||||
full_filename = self.get_dir() + '/' + filename
|
||||
full_filename += '.' + self.get_file_extension()
|
||||
full_filename = self.get_dir() + "/" + filename
|
||||
full_filename += "." + self.get_file_extension()
|
||||
return full_filename
|
||||
|
||||
def is_filename_valid(self, field_number):
|
||||
|
|
@ -40,41 +45,41 @@ class BaseFileDialog(UIDialog):
|
|||
return True, self.filename_exists_error
|
||||
return True, None
|
||||
|
||||
class NewArtDialog(BaseFileDialog):
|
||||
|
||||
title = 'New art'
|
||||
field0_label = 'Filename of new art:'
|
||||
field2_label = 'Width:'
|
||||
field4_label = 'Height:'
|
||||
field6_label = 'Save folder:'
|
||||
field7_label = ' %s'
|
||||
class NewArtDialog(BaseFileDialog):
|
||||
title = "New art"
|
||||
field0_label = "Filename of new art:"
|
||||
field2_label = "Width:"
|
||||
field4_label = "Height:"
|
||||
field6_label = "Save folder:"
|
||||
field7_label = " %s"
|
||||
tile_width = 60
|
||||
field0_width = 56
|
||||
y_spacing = 0
|
||||
field1_width = field2_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field0_width, oneline=False),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field2_label, type=int, width=field1_width, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field4_label, type=int, width=field2_width, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field6_label, type=None, width=0, oneline=True),
|
||||
Field(label=field7_label, type=None, width=0, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True)
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
]
|
||||
confirm_caption = 'Create'
|
||||
invalid_width_error = 'Invalid width.'
|
||||
invalid_height_error = 'Invalid height.'
|
||||
confirm_caption = "Create"
|
||||
invalid_width_error = "Invalid width."
|
||||
invalid_height_error = "Invalid height."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return 'new%s' % len(self.ui.app.art_loaded_for_edit)
|
||||
return "new%s" % len(self.ui.app.art_loaded_for_edit)
|
||||
elif field_number == 2:
|
||||
return str(DEFAULT_WIDTH)
|
||||
elif field_number == 4:
|
||||
return str(DEFAULT_HEIGHT)
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_field_label(self, field_index):
|
||||
label = self.fields[field_index].label
|
||||
|
|
@ -98,37 +103,39 @@ class NewArtDialog(BaseFileDialog):
|
|||
return self.is_filename_valid(0)
|
||||
|
||||
def is_valid_dimension(self, dimension, max_dimension):
|
||||
try: dimension = int(dimension)
|
||||
except: return False
|
||||
try:
|
||||
dimension = int(dimension)
|
||||
except:
|
||||
return False
|
||||
return 0 < dimension <= max_dimension
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
name = self.field_texts[0]
|
||||
w, h = int(self.field_texts[2]), int(self.field_texts[4])
|
||||
self.ui.app.new_art_for_edit(name, w, h)
|
||||
self.ui.app.log('Created %s.psci with size %s x %s' % (name, w, h))
|
||||
self.ui.app.log("Created %s.psci with size %s x %s" % (name, w, h))
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class SaveAsDialog(BaseFileDialog):
|
||||
|
||||
title = 'Save art'
|
||||
field0_label = 'New filename for art:'
|
||||
field2_label = 'Save folder:'
|
||||
field3_label = ' %s'
|
||||
title = "Save art"
|
||||
field0_label = "New filename for art:"
|
||||
field2_label = "Save folder:"
|
||||
field3_label = " %s"
|
||||
tile_width = 60
|
||||
field0_width = 56
|
||||
y_spacing = 0
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field0_width, oneline=False),
|
||||
Field(label='', type=None, width=0, oneline=True),
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
Field(label=field2_label, type=None, width=0, oneline=True),
|
||||
Field(label=field3_label, type=None, width=0, oneline=True),
|
||||
Field(label='', type=None, width=0, oneline=True)
|
||||
Field(label="", type=None, width=0, oneline=True),
|
||||
]
|
||||
confirm_caption = 'Save'
|
||||
confirm_caption = "Save"
|
||||
always_redraw_labels = True
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
|
|
@ -137,12 +144,14 @@ class SaveAsDialog(BaseFileDialog):
|
|||
# it to documents dir to avoid writing to application dir
|
||||
# (still possible if you open other files)
|
||||
if os.path.dirname(self.ui.active_art.filename) == ART_DIR[:-1]:
|
||||
self.ui.active_art.filename = self.ui.app.documents_dir + self.ui.active_art.filename
|
||||
self.ui.active_art.filename = (
|
||||
self.ui.app.documents_dir + self.ui.active_art.filename
|
||||
)
|
||||
# TODO: handle other files from app dir as well? not as important
|
||||
filename = os.path.basename(self.ui.active_art.filename)
|
||||
filename = os.path.splitext(filename)[0]
|
||||
return filename
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_file_extension(self):
|
||||
"""
|
||||
|
|
@ -166,16 +175,18 @@ class SaveAsDialog(BaseFileDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
SaveCommand.execute(self.ui.console, [self.field_texts[0]])
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class ConvertItemButton(ChooserItemButton):
|
||||
width = 15
|
||||
big_width = 20
|
||||
|
||||
class ConvertChooserItem(ChooserItem):
|
||||
|
||||
class ConvertChooserItem(ChooserItem):
|
||||
def picked(self, element):
|
||||
|
||||
# TODO: following is c+p'd from BaseFileChooserItem.picked,
|
||||
|
|
@ -196,13 +207,15 @@ class ConvertChooserItem(ChooserItem):
|
|||
element.first_selection_made = False
|
||||
|
||||
def get_description_lines(self):
|
||||
return self.description.split('\n')
|
||||
return self.description.split("\n")
|
||||
|
||||
|
||||
class ConvertFileDialog(ChooserDialog):
|
||||
"Common functionality for importer and exporter selection dialogs"
|
||||
|
||||
tile_width, big_width = 85, 90
|
||||
tile_height, big_height = 15, 25
|
||||
confirm_caption = 'Choose'
|
||||
confirm_caption = "Choose"
|
||||
show_preview_image = False
|
||||
item_button_class = ConvertItemButton
|
||||
chooser_item_class = ConvertChooserItem
|
||||
|
|
@ -227,7 +240,7 @@ class ConvertFileDialog(ChooserDialog):
|
|||
|
||||
|
||||
class ImportFileDialog(ConvertFileDialog):
|
||||
title = 'Choose an importer'
|
||||
title = "Choose an importer"
|
||||
|
||||
def get_converters(self):
|
||||
return self.ui.app.get_importers()
|
||||
|
|
@ -241,30 +254,37 @@ class ImportFileDialog(ConvertFileDialog):
|
|||
self.dismiss()
|
||||
self.ui.open_dialog(self.ui.app.importer.file_chooser_dialog_class)
|
||||
|
||||
|
||||
class ImportOptionsDialog(UIDialog):
|
||||
"Generic base class for importer options"
|
||||
confirm_caption = 'Import'
|
||||
|
||||
confirm_caption = "Import"
|
||||
|
||||
def do_import(app, filename, options):
|
||||
"Common 'run importer' code for end of import options dialog"
|
||||
# if importer needs no options, run it
|
||||
importer = app.importer(app, filename, options)
|
||||
if importer.success:
|
||||
if app.importer.completes_instantly:
|
||||
app.log('Imported %s successfully.' % filename)
|
||||
app.log("Imported %s successfully." % filename)
|
||||
app.importer = None
|
||||
|
||||
|
||||
class ExportOptionsDialog(UIDialog):
|
||||
"Generic base class for exporter options"
|
||||
confirm_caption = 'Export'
|
||||
|
||||
confirm_caption = "Export"
|
||||
|
||||
def do_export(app, filename, options):
|
||||
"Common 'run exporter' code for end of import options dialog"
|
||||
# if importer needs no options, run it
|
||||
exporter = app.exporter(app, filename, options)
|
||||
if exporter.success:
|
||||
app.log('Exported %s successfully.' % exporter.out_filename)
|
||||
app.log("Exported %s successfully." % exporter.out_filename)
|
||||
|
||||
|
||||
class ExportFileDialog(ConvertFileDialog):
|
||||
title = 'Choose an exporter'
|
||||
title = "Choose an exporter"
|
||||
|
||||
def get_converters(self):
|
||||
return self.ui.app.get_exporters()
|
||||
|
|
@ -280,9 +300,9 @@ class ExportFileDialog(ConvertFileDialog):
|
|||
|
||||
|
||||
class ExportFilenameInputDialog(SaveAsDialog):
|
||||
title = 'Export art'
|
||||
field0_label = 'New filename for exported art:'
|
||||
confirm_caption = 'Export'
|
||||
title = "Export art"
|
||||
field0_label = "New filename for exported art:"
|
||||
confirm_caption = "Export"
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# base output filename on art filename
|
||||
|
|
@ -297,24 +317,23 @@ class ExportFilenameInputDialog(SaveAsDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
filename = self.field_texts[0]
|
||||
self.dismiss()
|
||||
# invoke options dialog if exporter has one, else invoke exporter
|
||||
if self.ui.app.exporter.options_dialog_class:
|
||||
# pass filename into new dialog
|
||||
options = {'filename': filename}
|
||||
self.ui.open_dialog(self.ui.app.exporter.options_dialog_class,
|
||||
options)
|
||||
options = {"filename": filename}
|
||||
self.ui.open_dialog(self.ui.app.exporter.options_dialog_class, options)
|
||||
else:
|
||||
ExportOptionsDialog.do_export(self.ui.app, filename, {})
|
||||
|
||||
|
||||
class QuitUnsavedChangesDialog(UIDialog):
|
||||
|
||||
title = 'Unsaved changes'
|
||||
message = 'Save changes to %s?'
|
||||
confirm_caption = 'Save'
|
||||
title = "Unsaved changes"
|
||||
message = "Save changes to %s?"
|
||||
confirm_caption = "Save"
|
||||
other_button_visible = True
|
||||
other_caption = "Don't Save"
|
||||
|
||||
|
|
@ -338,7 +357,6 @@ class QuitUnsavedChangesDialog(UIDialog):
|
|||
|
||||
|
||||
class CloseUnsavedChangesDialog(QuitUnsavedChangesDialog):
|
||||
|
||||
def confirm_pressed(self):
|
||||
SaveCommand.execute(self.ui.console, [])
|
||||
self.dismiss()
|
||||
|
|
@ -351,10 +369,9 @@ class CloseUnsavedChangesDialog(QuitUnsavedChangesDialog):
|
|||
|
||||
|
||||
class RevertChangesDialog(UIDialog):
|
||||
|
||||
title = 'Revert changes'
|
||||
message = 'Revert changes to %s?'
|
||||
confirm_caption = 'Revert'
|
||||
title = "Revert changes"
|
||||
message = "Revert changes to %s?"
|
||||
confirm_caption = "Revert"
|
||||
|
||||
def confirm_pressed(self):
|
||||
self.ui.app.revert_active_art()
|
||||
|
|
@ -366,25 +383,24 @@ class RevertChangesDialog(UIDialog):
|
|||
|
||||
|
||||
class ResizeArtDialog(UIDialog):
|
||||
|
||||
title = 'Resize art'
|
||||
title = "Resize art"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
field0_label = 'New Width:'
|
||||
field1_label = 'New Height:'
|
||||
field2_label = 'Crop Start X:'
|
||||
field3_label = 'Crop Start Y:'
|
||||
field4_label = 'Fill new tiles with BG color'
|
||||
field0_label = "New Width:"
|
||||
field1_label = "New Height:"
|
||||
field2_label = "Crop Start X:"
|
||||
field3_label = "Crop Start Y:"
|
||||
field4_label = "Fill new tiles with BG color"
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=field_width, oneline=True),
|
||||
Field(label=field1_label, type=int, width=field_width, oneline=True),
|
||||
Field(label=field2_label, type=int, width=field_width, oneline=True),
|
||||
Field(label=field3_label, type=int, width=field_width, oneline=True),
|
||||
Field(label=field4_label, type=bool, width=0, oneline=True)
|
||||
Field(label=field4_label, type=bool, width=0, oneline=True),
|
||||
]
|
||||
confirm_caption = 'Resize'
|
||||
invalid_width_error = 'Invalid width.'
|
||||
invalid_height_error = 'Invalid height.'
|
||||
invalid_start_error = 'Invalid crop origin.'
|
||||
confirm_caption = "Resize"
|
||||
invalid_width_error = "Invalid width."
|
||||
invalid_height_error = "Invalid height."
|
||||
invalid_start_error = "Invalid crop origin."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
|
|
@ -394,7 +410,7 @@ class ResizeArtDialog(UIDialog):
|
|||
elif field_number == 4:
|
||||
return UIDialog.true_field_text
|
||||
else:
|
||||
return '0'
|
||||
return "0"
|
||||
|
||||
def is_input_valid(self):
|
||||
"file can't already exist, dimensions must be >0 and <= max"
|
||||
|
|
@ -402,24 +418,31 @@ class ResizeArtDialog(UIDialog):
|
|||
return False, self.invalid_width_error
|
||||
if not self.is_valid_dimension(self.field_texts[1], self.ui.app.max_art_height):
|
||||
return False, self.invalid_height_error
|
||||
try: int(self.field_texts[2])
|
||||
except: return False, self.invalid_start_error
|
||||
try:
|
||||
int(self.field_texts[2])
|
||||
except:
|
||||
return False, self.invalid_start_error
|
||||
if not 0 <= int(self.field_texts[2]) < self.ui.active_art.width:
|
||||
return False, self.invalid_start_error
|
||||
try: int(self.field_texts[3])
|
||||
except: return False, self.invalid_start_error
|
||||
try:
|
||||
int(self.field_texts[3])
|
||||
except:
|
||||
return False, self.invalid_start_error
|
||||
if not 0 <= int(self.field_texts[3]) < self.ui.active_art.height:
|
||||
return False, self.invalid_start_error
|
||||
return True, None
|
||||
|
||||
def is_valid_dimension(self, dimension, max_dimension):
|
||||
try: dimension = int(dimension)
|
||||
except: return False
|
||||
try:
|
||||
dimension = int(dimension)
|
||||
except:
|
||||
return False
|
||||
return 0 < dimension <= max_dimension
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
w, h = int(self.field_texts[0]), int(self.field_texts[1])
|
||||
start_x, start_y = int(self.field_texts[2]), int(self.field_texts[3])
|
||||
bg_fill = bool(self.field_texts[4].strip())
|
||||
|
|
@ -431,19 +454,19 @@ class ResizeArtDialog(UIDialog):
|
|||
# layer menu dialogs
|
||||
#
|
||||
|
||||
class AddFrameDialog(UIDialog):
|
||||
|
||||
title = 'Add new frame'
|
||||
field0_label = 'Index to add frame before:'
|
||||
field1_label = 'Hold time (in seconds) for new frame:'
|
||||
class AddFrameDialog(UIDialog):
|
||||
title = "Add new frame"
|
||||
field0_label = "Index to add frame before:"
|
||||
field1_label = "Hold time (in seconds) for new frame:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=field_width, oneline=True),
|
||||
Field(label=field1_label, type=float, width=field_width, oneline=False)
|
||||
Field(label=field1_label, type=float, width=field_width, oneline=False),
|
||||
]
|
||||
confirm_caption = 'Add'
|
||||
invalid_index_error = 'Invalid index. (1-%s allowed)'
|
||||
invalid_delay_error = 'Invalid hold time.'
|
||||
confirm_caption = "Add"
|
||||
invalid_index_error = "Invalid index. (1-%s allowed)"
|
||||
invalid_delay_error = "Invalid hold time."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
|
|
@ -452,15 +475,19 @@ class AddFrameDialog(UIDialog):
|
|||
return str(DEFAULT_FRAME_DELAY)
|
||||
|
||||
def is_valid_frame_index(self, index):
|
||||
try: index = int(index)
|
||||
except: return False
|
||||
try:
|
||||
index = int(index)
|
||||
except:
|
||||
return False
|
||||
if index < 1 or index > self.ui.active_art.frames + 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_valid_frame_delay(self, delay):
|
||||
try: delay = float(delay)
|
||||
except: return False
|
||||
try:
|
||||
delay = float(delay)
|
||||
except:
|
||||
return False
|
||||
return delay > 0
|
||||
|
||||
def is_input_valid(self):
|
||||
|
|
@ -472,31 +499,35 @@ class AddFrameDialog(UIDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
index = int(self.field_texts[0])
|
||||
delay = float(self.field_texts[1])
|
||||
self.ui.active_art.insert_frame_before_index(index - 1, delay)
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class DuplicateFrameDialog(AddFrameDialog):
|
||||
title = 'Duplicate frame'
|
||||
confirm_caption = 'Duplicate'
|
||||
title = "Duplicate frame"
|
||||
confirm_caption = "Duplicate"
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
index = int(self.field_texts[0])
|
||||
delay = float(self.field_texts[1])
|
||||
self.ui.active_art.duplicate_frame(self.ui.active_art.active_frame, index - 1, delay)
|
||||
self.ui.active_art.duplicate_frame(
|
||||
self.ui.active_art.active_frame, index - 1, delay
|
||||
)
|
||||
self.dismiss()
|
||||
|
||||
class FrameDelayDialog(AddFrameDialog):
|
||||
|
||||
field0_label = 'New hold time (in seconds) for frame:'
|
||||
class FrameDelayDialog(AddFrameDialog):
|
||||
field0_label = "New hold time (in seconds) for frame:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=float, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
fields = [Field(label=field0_label, type=float, width=field_width, oneline=False)]
|
||||
confirm_caption = "Set"
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
|
|
@ -509,33 +540,33 @@ class FrameDelayDialog(AddFrameDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
delay = float(self.field_texts[0])
|
||||
self.ui.active_art.frame_delays[self.ui.active_art.active_frame] = delay
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class FrameDelayAllDialog(FrameDelayDialog):
|
||||
field0_label = 'New hold time (in seconds) for all frames:'
|
||||
field0_label = "New hold time (in seconds) for all frames:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=float, width=field_width, oneline=False)
|
||||
]
|
||||
fields = [Field(label=field0_label, type=float, width=field_width, oneline=False)]
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
delay = float(self.field_texts[0])
|
||||
for i in range(self.ui.active_art.frames):
|
||||
self.ui.active_art.frame_delays[i] = delay
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class FrameIndexDialog(AddFrameDialog):
|
||||
field0_label = 'Move this frame before index:'
|
||||
field0_label = "Move this frame before index:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=int, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
fields = [Field(label=field0_label, type=int, width=field_width, oneline=False)]
|
||||
confirm_caption = "Set"
|
||||
|
||||
def is_input_valid(self):
|
||||
if not self.is_valid_frame_index(self.field_texts[0]):
|
||||
|
|
@ -544,10 +575,13 @@ class FrameIndexDialog(AddFrameDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
# set new frame index (effectively moving it in the sequence)
|
||||
dest_index = int(self.field_texts[0])
|
||||
self.ui.active_art.move_frame_to_index(self.ui.active_art.active_frame, dest_index)
|
||||
self.ui.active_art.move_frame_to_index(
|
||||
self.ui.active_art.active_frame, dest_index
|
||||
)
|
||||
self.dismiss()
|
||||
|
||||
|
||||
|
|
@ -555,26 +589,29 @@ class FrameIndexDialog(AddFrameDialog):
|
|||
# layer menu dialogs
|
||||
#
|
||||
|
||||
class AddLayerDialog(UIDialog):
|
||||
|
||||
title = 'Add new layer'
|
||||
field0_label = 'Name for new layer:'
|
||||
field1_label = 'Z-depth for new layer:'
|
||||
class AddLayerDialog(UIDialog):
|
||||
title = "Add new layer"
|
||||
field0_label = "Name for new layer:"
|
||||
field1_label = "Z-depth for new layer:"
|
||||
field0_width = UIDialog.default_field_width
|
||||
field1_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field0_width, oneline=False),
|
||||
Field(label=field1_label, type=float, width=field1_width, oneline=True)
|
||||
Field(label=field1_label, type=float, width=field1_width, oneline=True),
|
||||
]
|
||||
confirm_caption = 'Add'
|
||||
name_exists_error = 'Layer by that name already exists.'
|
||||
invalid_z_error = 'Invalid number.'
|
||||
confirm_caption = "Add"
|
||||
name_exists_error = "Layer by that name already exists."
|
||||
invalid_z_error = "Invalid number."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return 'Layer %s' % str(self.ui.active_art.layers + 1)
|
||||
return "Layer %s" % str(self.ui.active_art.layers + 1)
|
||||
elif field_number == 1:
|
||||
return str(self.ui.active_art.layers_z[self.ui.active_art.active_layer] + DEFAULT_LAYER_Z_OFFSET)
|
||||
return str(
|
||||
self.ui.active_art.layers_z[self.ui.active_art.active_layer]
|
||||
+ DEFAULT_LAYER_Z_OFFSET
|
||||
)
|
||||
|
||||
def is_valid_layer_name(self, name, exclude_active_layer=False):
|
||||
for i, layer_name in enumerate(self.ui.active_art.layer_names):
|
||||
|
|
@ -588,13 +625,16 @@ class AddLayerDialog(UIDialog):
|
|||
valid_name = self.is_valid_layer_name(self.field_texts[0])
|
||||
if not valid_name:
|
||||
return False, self.name_exists_error
|
||||
try: z = float(self.field_texts[1])
|
||||
except: return False, self.invalid_z_error
|
||||
try:
|
||||
z = float(self.field_texts[1])
|
||||
except:
|
||||
return False, self.invalid_z_error
|
||||
return True, None
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
name = self.field_texts[0]
|
||||
z = float(self.field_texts[1])
|
||||
self.ui.active_art.add_layer(z, name)
|
||||
|
|
@ -602,12 +642,13 @@ class AddLayerDialog(UIDialog):
|
|||
|
||||
|
||||
class DuplicateLayerDialog(AddLayerDialog):
|
||||
title = 'Duplicate layer'
|
||||
confirm_caption = 'Duplicate'
|
||||
title = "Duplicate layer"
|
||||
confirm_caption = "Duplicate"
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
name = self.field_texts[0]
|
||||
z = float(self.field_texts[1])
|
||||
self.ui.active_art.duplicate_layer(self.ui.active_art.active_layer, z, name)
|
||||
|
|
@ -615,14 +656,11 @@ class DuplicateLayerDialog(AddLayerDialog):
|
|||
|
||||
|
||||
class SetLayerNameDialog(AddLayerDialog):
|
||||
|
||||
title = 'Set layer name'
|
||||
field0_label = 'New name for this layer:'
|
||||
title = "Set layer name"
|
||||
field0_label = "New name for this layer:"
|
||||
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"
|
||||
|
||||
def confirm_pressed(self):
|
||||
new_name = self.field_texts[0]
|
||||
|
|
@ -632,14 +670,12 @@ class SetLayerNameDialog(AddLayerDialog):
|
|||
|
||||
|
||||
class SetLayerZDialog(UIDialog):
|
||||
title = 'Set layer Z-depth'
|
||||
field0_label = 'Z-depth for layer:'
|
||||
title = "Set layer Z-depth"
|
||||
field0_label = "Z-depth for layer:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=float, width=field_width, oneline=False)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
invalid_z_error = 'Invalid number.'
|
||||
fields = [Field(label=field0_label, type=float, width=field_width, oneline=False)]
|
||||
confirm_caption = "Set"
|
||||
invalid_z_error = "Invalid number."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# populate with existing z
|
||||
|
|
@ -647,13 +683,16 @@ class SetLayerZDialog(UIDialog):
|
|||
return str(self.ui.active_art.layers_z[self.ui.active_art.active_layer])
|
||||
|
||||
def is_input_valid(self):
|
||||
try: z = float(self.field_texts[0])
|
||||
except: return False, self.invalid_z_error
|
||||
try:
|
||||
z = float(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_z_error
|
||||
return True, None
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
new_z = float(self.field_texts[0])
|
||||
self.ui.active_art.layers_z[self.ui.active_art.active_layer] = new_z
|
||||
self.ui.active_art.set_unsaved_changes(True)
|
||||
|
|
@ -662,31 +701,33 @@ class SetLayerZDialog(UIDialog):
|
|||
|
||||
|
||||
class PaletteFromFileDialog(UIDialog):
|
||||
title = 'Create palette from file'
|
||||
field0_label = 'Filename to create palette from:'
|
||||
field1_label = 'Filename for new palette:'
|
||||
field2_label = 'Colors in new palette:'
|
||||
title = "Create palette from file"
|
||||
field0_label = "Filename to create palette from:"
|
||||
field1_label = "Filename for new palette:"
|
||||
field2_label = "Colors in new palette:"
|
||||
field0_width = field1_width = UIDialog.default_field_width
|
||||
field2_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=str, width=field0_width, oneline=False),
|
||||
Field(label=field1_label, type=str, width=field1_width, oneline=False),
|
||||
Field(label=field2_label, type=int, width=field2_width, oneline=True)
|
||||
Field(label=field2_label, type=int, width=field2_width, oneline=True),
|
||||
]
|
||||
confirm_caption = 'Create'
|
||||
invalid_color_error = 'Palettes must be between 2 and 256 colors.'
|
||||
bad_output_filename_error = 'Enter a filename for the new palette.'
|
||||
confirm_caption = "Create"
|
||||
invalid_color_error = "Palettes must be between 2 and 256 colors."
|
||||
bad_output_filename_error = "Enter a filename for the new palette."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
# NOTE: PaletteFromImageChooserDialog.confirm_pressed which invokes us
|
||||
# sets fields 0 and 1
|
||||
if field_number == 2:
|
||||
return str(256)
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def valid_colors(self, colors):
|
||||
try: c = int(colors)
|
||||
except: return False
|
||||
try:
|
||||
c = int(colors)
|
||||
except:
|
||||
return False
|
||||
return 2 <= c <= 256
|
||||
|
||||
def is_input_valid(self):
|
||||
|
|
@ -699,7 +740,8 @@ class PaletteFromFileDialog(UIDialog):
|
|||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
src_filename = self.field_texts[0]
|
||||
palette_filename = self.field_texts[1]
|
||||
colors = int(self.field_texts[2])
|
||||
|
|
@ -708,32 +750,33 @@ class PaletteFromFileDialog(UIDialog):
|
|||
|
||||
|
||||
class SetCameraZoomDialog(UIDialog):
|
||||
title = 'Set camera zoom'
|
||||
field0_label = 'New camera zoom %:'
|
||||
title = "Set camera zoom"
|
||||
field0_label = "New camera zoom %:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=float, width=field_width, oneline=True)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
invalid_zoom_error = 'Zoom % must be a number greater than zero.'
|
||||
fields = [Field(label=field0_label, type=float, width=field_width, oneline=True)]
|
||||
confirm_caption = "Set"
|
||||
invalid_zoom_error = "Zoom % must be a number greater than zero."
|
||||
all_modes_visible = True
|
||||
game_mode_visible = True
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return '%.1f' % self.ui.app.camera.get_current_zoom_pct()
|
||||
return ''
|
||||
return "%.1f" % self.ui.app.camera.get_current_zoom_pct()
|
||||
return ""
|
||||
|
||||
def is_input_valid(self):
|
||||
try: zoom = float(self.field_texts[0])
|
||||
except: return False, self.invalid_zoom_error
|
||||
try:
|
||||
zoom = float(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_zoom_error
|
||||
if zoom <= 0:
|
||||
return False, self.invalid_zoom_error
|
||||
return True, None
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
new_zoom_pct = float(self.field_texts[0])
|
||||
camera = self.ui.app.camera
|
||||
camera.z = camera.get_base_zoom() / (new_zoom_pct / 100)
|
||||
|
|
@ -741,30 +784,31 @@ class SetCameraZoomDialog(UIDialog):
|
|||
|
||||
|
||||
class OverlayImageOpacityDialog(UIDialog):
|
||||
title = 'Set overlay image opacity'
|
||||
field0_label = 'New overlay opacity %:'
|
||||
title = "Set overlay image opacity"
|
||||
field0_label = "New overlay opacity %:"
|
||||
field_width = UIDialog.default_short_field_width
|
||||
fields = [
|
||||
Field(label=field0_label, type=float, width=field_width, oneline=True)
|
||||
]
|
||||
confirm_caption = 'Set'
|
||||
invalid_opacity_error = 'Opacity % must be between 0 and 100.'
|
||||
fields = [Field(label=field0_label, type=float, width=field_width, oneline=True)]
|
||||
confirm_caption = "Set"
|
||||
invalid_opacity_error = "Opacity % must be between 0 and 100."
|
||||
|
||||
def get_initial_field_text(self, field_number):
|
||||
if field_number == 0:
|
||||
return '%.1f' % (self.ui.app.overlay_renderable.alpha * 100)
|
||||
return ''
|
||||
return "%.1f" % (self.ui.app.overlay_renderable.alpha * 100)
|
||||
return ""
|
||||
|
||||
def is_input_valid(self):
|
||||
try: opacity = float(self.field_texts[0])
|
||||
except: return False, self.invalid_opacity_error
|
||||
try:
|
||||
opacity = float(self.field_texts[0])
|
||||
except:
|
||||
return False, self.invalid_opacity_error
|
||||
if opacity <= 0 or opacity > 100:
|
||||
return False, self.invalid_opacity_error
|
||||
return True, None
|
||||
|
||||
def confirm_pressed(self):
|
||||
valid, reason = self.is_input_valid()
|
||||
if not valid: return
|
||||
if not valid:
|
||||
return
|
||||
new_opacity = float(self.field_texts[0])
|
||||
self.ui.app.overlay_renderable.alpha = new_opacity / 100
|
||||
self.dismiss()
|
||||
|
|
|
|||
56
ui_button.py
56
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
|
||||
|
|
@ -47,36 +46,43 @@ class UIButton:
|
|||
|
||||
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):
|
||||
|
|
@ -91,18 +97,18 @@ class UIButton:
|
|||
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,
|
||||
|
|
@ -117,11 +123,11 @@ class UIButton:
|
|||
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:
|
||||
|
|
@ -129,7 +135,7 @@ class UIButton:
|
|||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
# 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
|
||||
|
|
@ -33,7 +31,6 @@ class ChooserItemButton(UIButton):
|
|||
|
||||
|
||||
class ScrollArrowButton(UIButton):
|
||||
|
||||
"button that scrolls up or down in a chooser item view"
|
||||
|
||||
arrow_char = 129
|
||||
|
|
@ -44,8 +41,9 @@ class ScrollArrowButton(UIButton):
|
|||
|
||||
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:
|
||||
|
|
@ -59,8 +57,7 @@ class ScrollArrowButton(UIButton):
|
|||
|
||||
|
||||
class ChooserItem:
|
||||
|
||||
label = 'Chooser item'
|
||||
label = "Chooser item"
|
||||
|
||||
def __init__(self, index, name):
|
||||
self.index = index
|
||||
|
|
@ -70,13 +67,17 @@ class ChooserItem:
|
|||
# validity flag lets ChooserItem subclasses exclude themselves
|
||||
self.valid = True
|
||||
|
||||
def get_label(self): return self.name
|
||||
def get_label(self):
|
||||
return self.name
|
||||
|
||||
def get_description_lines(self): return []
|
||||
def get_description_lines(self):
|
||||
return []
|
||||
|
||||
def get_preview_texture(self): return None
|
||||
def get_preview_texture(self):
|
||||
return None
|
||||
|
||||
def load(self, app): pass
|
||||
def load(self, app):
|
||||
pass
|
||||
|
||||
def picked(self, element):
|
||||
# set item selected and refresh preview
|
||||
|
|
@ -84,22 +85,19 @@ class ChooserItem:
|
|||
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -170,7 +169,7 @@ class ChooserDialog(UIDialog):
|
|||
|
||||
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:
|
||||
|
|
@ -178,13 +177,13 @@ class ChooserDialog(UIDialog):
|
|||
# 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
|
||||
|
|
@ -193,8 +192,7 @@ class ChooserDialog(UIDialog):
|
|||
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())
|
||||
|
|
@ -231,7 +237,11 @@ class ChooserDialog(UIDialog):
|
|||
|
||||
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()
|
||||
|
|
@ -261,16 +271,25 @@ 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
|
||||
|
|
@ -310,9 +329,9 @@ 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)
|
||||
|
|
@ -324,7 +343,7 @@ class ChooserDialog(UIDialog):
|
|||
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):
|
||||
|
|
@ -343,8 +362,7 @@ 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
|
||||
|
||||
|
|
@ -363,8 +381,9 @@ 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)
|
||||
|
|
@ -376,20 +395,20 @@ class ChooserDialog(UIDialog):
|
|||
# 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
|
||||
|
|
@ -422,7 +441,7 @@ class ChooserDialog(UIDialog):
|
|||
|
||||
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()
|
||||
|
|
@ -432,7 +451,7 @@ class ChooserDialog(UIDialog):
|
|||
# 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]
|
||||
|
|
@ -445,7 +464,7 @@ class ChooserDialog(UIDialog):
|
|||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
class UIColors:
|
||||
"color indices for UI (c64 original) palette"
|
||||
|
||||
white = 2
|
||||
lightgrey = 16
|
||||
medgrey = 13
|
||||
|
|
|
|||
294
ui_console.py
294
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,12 +160,14 @@ 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
|
||||
|
|
@ -156,102 +176,118 @@ class PaletteFromImageCommand(ConsoleCommand):
|
|||
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,12 +296,12 @@ 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
|
||||
|
||||
|
|
@ -283,18 +319,18 @@ 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
|
||||
|
||||
|
|
@ -302,7 +338,9 @@ class ConsoleUI(UIElement):
|
|||
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
|
||||
|
|
@ -314,7 +352,7 @@ class ConsoleUI(UIElement):
|
|||
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):
|
||||
|
|
@ -364,25 +402,34 @@ class ConsoleUI(UIElement):
|
|||
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):
|
||||
|
|
@ -436,31 +483,34 @@ class ConsoleUI(UIElement):
|
|||
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,15 +531,15 @@ 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
|
||||
|
||||
|
|
@ -511,23 +561,27 @@ 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):
|
||||
|
|
@ -535,4 +589,4 @@ class ConsoleUI(UIElement):
|
|||
|
||||
|
||||
# delimiters - alt-backspace deletes to most recent one of these
|
||||
delimiters = [' ', '.', ')', ']', ',', '_']
|
||||
delimiters = [" ", ".", ")", "]", ",", "_"]
|
||||
|
|
|
|||
155
ui_dialog.py
155
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,7 +80,7 @@ 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
|
||||
|
||||
|
|
@ -84,11 +92,13 @@ class UIDialog(UIElement):
|
|||
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)
|
||||
|
|
@ -108,7 +118,7 @@ class UIDialog(UIElement):
|
|||
|
||||
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)"
|
||||
|
|
@ -138,12 +148,11 @@ 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)
|
||||
|
|
@ -175,7 +184,9 @@ 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
|
||||
|
|
@ -191,8 +202,9 @@ class UIDialog(UIElement):
|
|||
|
||||
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()
|
||||
|
||||
|
|
@ -206,21 +218,21 @@ 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
|
||||
|
|
@ -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,14 +291,14 @@ 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)
|
||||
|
|
@ -312,11 +328,11 @@ 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
|
||||
|
|
@ -326,86 +342,96 @@ class UIDialog(UIElement):
|
|||
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]
|
||||
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
|
||||
|
|
@ -437,10 +463,9 @@ class UIDialog(UIElement):
|
|||
|
||||
|
||||
class DialogFieldButton(UIButton):
|
||||
|
||||
"invisible button that provides clickability for input fields"
|
||||
|
||||
caption = ''
|
||||
caption = ""
|
||||
# re-set by dialog constructor
|
||||
field_number = 0
|
||||
never_draw = True
|
||||
|
|
@ -450,6 +475,8 @@ class DialogFieldButton(UIButton):
|
|||
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)
|
||||
|
|
|
|||
141
ui_edit_panel.py
141
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
|
||||
|
|
@ -31,10 +42,15 @@ class GamePanel(UIElement):
|
|||
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):
|
||||
|
|
@ -58,8 +74,16 @@ class GamePanel(UIElement):
|
|||
|
||||
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:
|
||||
|
|
@ -82,8 +106,11 @@ class GamePanel(UIElement):
|
|||
|
||||
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,34 +141,37 @@ 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
|
||||
|
|
@ -149,7 +183,8 @@ 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,
|
||||
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,
|
||||
|
|
@ -159,10 +194,11 @@ class EditListPanel(GamePanel):
|
|||
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
|
||||
LO_SET_ROOM_CAMERA: self.list_objects,
|
||||
}
|
||||
# map list operations to "item clicked" functions
|
||||
self.click_functions = {LO_SELECT_OBJECTS: self.select_object,
|
||||
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,
|
||||
|
|
@ -171,7 +207,7 @@ class EditListPanel(GamePanel):
|
|||
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
|
||||
LO_SET_ROOM_CAMERA: self.set_room_camera,
|
||||
}
|
||||
# separate lists for item buttons vs other controls
|
||||
self.list_buttons = []
|
||||
|
|
@ -181,8 +217,10 @@ class EditListPanel(GamePanel):
|
|||
|
||||
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):
|
||||
button = ListButton(self)
|
||||
button.y = y + 1
|
||||
|
|
@ -203,8 +241,9 @@ class EditListPanel(GamePanel):
|
|||
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)
|
||||
|
|
@ -257,9 +296,12 @@ class EditListPanel(GamePanel):
|
|||
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
|
||||
|
|
@ -280,9 +322,14 @@ 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):
|
||||
|
|
@ -298,7 +345,7 @@ class EditListPanel(GamePanel):
|
|||
def refresh_items(self):
|
||||
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
|
||||
|
|
@ -355,7 +402,7 @@ class EditListPanel(GamePanel):
|
|||
#
|
||||
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():
|
||||
# ignore non-GO classes, eg GameRoom, GameHUD
|
||||
|
|
@ -377,7 +424,10 @@ 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)
|
||||
|
|
@ -389,7 +439,7 @@ class EditListPanel(GamePanel):
|
|||
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)
|
||||
|
|
@ -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 = []
|
||||
|
|
@ -424,7 +475,7 @@ class EditListPanel(GamePanel):
|
|||
items = self.list_rooms()
|
||||
# prefix room names with "ROOM:"
|
||||
for i, item in enumerate(items):
|
||||
item.name = 'ROOM: %s' % item.name
|
||||
item.name = "ROOM: %s" % item.name
|
||||
items += self.list_objects()
|
||||
return items
|
||||
|
||||
|
|
@ -447,7 +498,9 @@ class EditListPanel(GamePanel):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -40,8 +37,15 @@ class UIElement:
|
|||
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
|
||||
|
|
@ -82,17 +86,17 @@ class UIElement:
|
|||
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
|
||||
|
|
@ -118,7 +122,7 @@ class UIElement:
|
|||
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()
|
||||
|
|
@ -128,16 +132,22 @@ class UIElement:
|
|||
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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -191,16 +204,16 @@ class UIElement:
|
|||
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:
|
||||
|
|
@ -225,12 +238,16 @@ 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()
|
||||
|
|
@ -258,7 +275,6 @@ class UIArt(Art):
|
|||
|
||||
|
||||
class UIRenderable(TileRenderable):
|
||||
|
||||
grain_strength = 0.2
|
||||
|
||||
def get_projection_matrix(self):
|
||||
|
|
@ -271,7 +287,6 @@ class UIRenderable(TileRenderable):
|
|||
|
||||
|
||||
class FPSCounterUI(UIElement):
|
||||
|
||||
tile_y = 1
|
||||
tile_width, tile_height = 12, 2
|
||||
snap_right = True
|
||||
|
|
@ -288,11 +303,11 @@ 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):
|
||||
|
|
@ -302,7 +317,6 @@ class FPSCounterUI(UIElement):
|
|||
|
||||
|
||||
class MessageLineUI(UIElement):
|
||||
|
||||
"when console outputs something new, show last line here before fading out"
|
||||
|
||||
tile_y = 2
|
||||
|
|
@ -318,7 +332,7 @@ class MessageLineUI(UIElement):
|
|||
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
|
||||
|
|
@ -352,12 +366,14 @@ class MessageLineUI(UIElement):
|
|||
|
||||
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
|
||||
|
|
@ -395,7 +411,6 @@ class DebugTextUI(UIElement):
|
|||
|
||||
|
||||
class ToolTip(UIElement):
|
||||
|
||||
"popup text label that is invoked and controlled by a UIButton hover"
|
||||
|
||||
visible = False
|
||||
|
|
@ -403,16 +418,16 @@ class ToolTip(UIElement):
|
|||
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,8 +438,7 @@ class GameLabel(UIElement):
|
|||
|
||||
|
||||
class GameSelectionLabel(GameLabel):
|
||||
|
||||
multi_select_label = '[%s selected]'
|
||||
multi_select_label = "[%s selected]"
|
||||
|
||||
def update(self):
|
||||
self.visible = False
|
||||
|
|
@ -453,8 +467,8 @@ class GameSelectionLabel(GameLabel):
|
|||
self.x, self.y = vector.world_to_screen_normalized(self.ui.app, x, y, z)
|
||||
self.reset_loc()
|
||||
|
||||
class GameHoverLabel(GameLabel):
|
||||
|
||||
class GameHoverLabel(GameLabel):
|
||||
alpha = 0.75
|
||||
|
||||
def update(self):
|
||||
|
|
|
|||
|
|
@ -1,26 +1,32 @@
|
|||
|
||||
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):
|
||||
|
|
@ -34,8 +40,8 @@ class BaseFileChooserItem(ChooserItem):
|
|||
|
||||
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
|
||||
|
|
@ -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,9 +68,10 @@ class BaseFileChooserItem(ChooserItem):
|
|||
element.confirm_pressed()
|
||||
element.first_selection_made = False
|
||||
|
||||
class BaseFileChooserDialog(ChooserDialog):
|
||||
|
||||
class BaseFileChooserDialog(ChooserDialog):
|
||||
"base class for choosers whose items correspond with files"
|
||||
|
||||
chooser_item_class = BaseFileChooserItem
|
||||
show_filenames = True
|
||||
file_extensions = []
|
||||
|
|
@ -80,21 +87,21 @@ class BaseFileChooserDialog(ChooserDialog):
|
|||
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]
|
||||
|
|
@ -123,8 +130,8 @@ class BaseFileChooserDialog(ChooserDialog):
|
|||
# art chooser
|
||||
#
|
||||
|
||||
class ArtChooserItem(BaseFileChooserItem):
|
||||
|
||||
class ArtChooserItem(BaseFileChooserItem):
|
||||
# set in load()
|
||||
art_width = None
|
||||
hide_file_extension = True
|
||||
|
|
@ -136,28 +143,30 @@ 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)
|
||||
|
||||
|
|
@ -172,18 +181,17 @@ 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
|
||||
|
|
@ -195,7 +203,11 @@ class ArtChooserDialog(BaseFileChooserDialog):
|
|||
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
|
||||
|
||||
|
|
@ -211,10 +223,9 @@ class ArtChooserDialog(BaseFileChooserDialog):
|
|||
# generic file chooser for importers
|
||||
#
|
||||
class GenericImportChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Import %s'
|
||||
confirm_caption = 'Import'
|
||||
cancel_caption = 'Cancel'
|
||||
title = "Import %s"
|
||||
confirm_caption = "Import"
|
||||
cancel_caption = "Cancel"
|
||||
# allowed extensions set by invoking
|
||||
file_extensions = []
|
||||
show_preview_image = False
|
||||
|
|
@ -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,25 +267,25 @@ 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'
|
||||
class ImageFileChooserDialog(BaseFileChooserDialog):
|
||||
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]):
|
||||
|
|
@ -291,18 +300,19 @@ 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):
|
||||
|
||||
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
|
||||
|
|
@ -312,8 +322,7 @@ class PaletteChooserItem(BaseFileChooserItem):
|
|||
|
||||
|
||||
class PaletteChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Choose palette'
|
||||
title = "Choose palette"
|
||||
chooser_item_class = PaletteChooserItem
|
||||
|
||||
def get_initial_selection(self):
|
||||
|
|
@ -343,24 +352,25 @@ class PaletteChooserDialog(BaseFileChooserDialog):
|
|||
self.ui.active_art.set_palette(item.palette, log=True)
|
||||
self.ui.popup.set_active_palette(item.palette)
|
||||
|
||||
|
||||
#
|
||||
# charset chooser
|
||||
#
|
||||
|
||||
class CharsetChooserItem(BaseFileChooserItem):
|
||||
|
||||
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):
|
||||
|
|
@ -371,8 +381,7 @@ class CharsetChooserItem(BaseFileChooserItem):
|
|||
|
||||
|
||||
class CharSetChooserDialog(BaseFileChooserDialog):
|
||||
|
||||
title = 'Choose character set'
|
||||
title = "Choose character set"
|
||||
flip_preview_y = False
|
||||
chooser_item_class = CharsetChooserItem
|
||||
|
||||
|
|
@ -405,7 +414,6 @@ class CharSetChooserDialog(BaseFileChooserDialog):
|
|||
|
||||
|
||||
class ArtScriptChooserItem(BaseFileChooserItem):
|
||||
|
||||
def get_label(self):
|
||||
label = os.path.splitext(self.name)[0]
|
||||
return os.path.basename(label)
|
||||
|
|
@ -417,10 +425,10 @@ 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
|
||||
|
||||
|
|
@ -429,8 +437,7 @@ class ArtScriptChooserItem(BaseFileChooserItem):
|
|||
|
||||
|
||||
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
|
||||
|
|
@ -454,9 +461,8 @@ 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]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
|
||||
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
|
||||
|
|
@ -22,7 +22,7 @@ class NewGameDirDialog(UIDialog):
|
|||
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
|
||||
|
||||
|
|
@ -31,15 +31,13 @@ class NewGameDirDialog(UIDialog):
|
|||
self.ui.app.enter_game_mode()
|
||||
self.dismiss()
|
||||
|
||||
class LoadGameStateDialog(UIDialog):
|
||||
|
||||
title = 'Open game state'
|
||||
field_label = 'Game state file to open:'
|
||||
class LoadGameStateDialog(UIDialog):
|
||||
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
|
||||
|
|
@ -48,59 +46,58 @@ class LoadGameStateDialog(UIDialog):
|
|||
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:'
|
||||
class SaveGameStateDialog(UIDialog):
|
||||
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):
|
||||
|
|
@ -111,28 +108,33 @@ class SetRoomCamDialog(UIDialog):
|
|||
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):
|
||||
|
|
@ -148,14 +150,13 @@ 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):
|
||||
|
|
@ -172,27 +173,27 @@ class SetRoomBoundsObjDialog(UIDialog):
|
|||
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,297 +19,405 @@ 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
|
||||
|
||||
|
|
@ -321,17 +437,20 @@ class GameRoomMenuData(PulldownMenuData):
|
|||
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
|
||||
|
||||
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,19 +1,19 @@
|
|||
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
|
||||
|
||||
|
|
@ -26,29 +26,29 @@ class PagedInfoDialog(UIDialog):
|
|||
# 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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
163
ui_menu_bar.py
163
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
|
||||
|
|
@ -44,98 +62,116 @@ 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
|
||||
|
|
@ -143,17 +179,18 @@ class ModeMenuButton(UIButton):
|
|||
# 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
|
||||
|
|
@ -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,7 +234,9 @@ 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)
|
||||
|
||||
|
|
@ -227,7 +266,7 @@ class MenuBar(UIElement):
|
|||
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
|
||||
|
|
@ -253,7 +292,7 @@ class MenuBar(UIElement):
|
|||
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()
|
||||
|
||||
|
|
@ -280,7 +319,9 @@ 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()
|
||||
|
|
@ -293,15 +334,31 @@ class MenuBar(UIElement):
|
|||
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,9 +1,8 @@
|
|||
|
||||
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):
|
||||
|
|
@ -26,7 +25,6 @@ class MenuItemButton(UIButton):
|
|||
|
||||
|
||||
class PulldownMenu(UIElement):
|
||||
|
||||
"element that's moved and resized based on currently active pulldown"
|
||||
|
||||
label_shortcut_padding = 5
|
||||
|
|
@ -85,7 +83,14 @@ class PulldownMenu(UIElement):
|
|||
# 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
|
||||
|
|
@ -105,14 +110,17 @@ class PulldownMenu(UIElement):
|
|||
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:
|
||||
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)
|
||||
|
|
@ -157,26 +165,27 @@ class PulldownMenu(UIElement):
|
|||
# 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,8 +85,8 @@ class PropertyItem:
|
|||
|
||||
|
||||
class EditObjectPanel(GamePanel):
|
||||
|
||||
"panel showing info for selected game object"
|
||||
|
||||
tile_width = 36
|
||||
tile_height = 36
|
||||
snap_right = True
|
||||
|
|
@ -97,16 +102,19 @@ class EditObjectPanel(GamePanel):
|
|||
# buttons for persistent unique commands, eg reset object
|
||||
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
|
||||
|
|
@ -130,17 +138,26 @@ 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
|
||||
|
|
@ -157,11 +174,11 @@ class EditObjectPanel(GamePanel):
|
|||
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:
|
||||
|
|
@ -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 = []
|
||||
|
|
@ -200,19 +217,19 @@ class EditObjectPanel(GamePanel):
|
|||
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):
|
||||
|
|
|
|||
254
ui_popup.py
254
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,34 +202,34 @@ 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):
|
||||
|
|
@ -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,15 +272,17 @@ 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
|
||||
|
|
@ -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:
|
||||
|
|
@ -396,15 +431,21 @@ class ToolPopup(UIElement):
|
|||
# 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,7 +460,10 @@ 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)
|
||||
|
|
@ -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,17 +526,27 @@ 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)]
|
||||
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.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
|
||||
|
|
@ -505,15 +562,21 @@ class ToolPopup(UIElement):
|
|||
y += 1
|
||||
# boundary mode buttons + labels
|
||||
# x +=
|
||||
labels = [self.fill_boundary_char_label,
|
||||
labels = [
|
||||
self.fill_boundary_char_label,
|
||||
self.fill_boundary_fg_label,
|
||||
self.fill_boundary_bg_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]
|
||||
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])
|
||||
self.art.write_string(
|
||||
0, 0, x + FillBoundaryModeCharButton.width + 1, y, labels[i]
|
||||
)
|
||||
y += 1
|
||||
else:
|
||||
for button in self.fill_boundary_mode_buttons:
|
||||
|
|
@ -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))
|
||||
|
|
@ -576,7 +645,10 @@ class ToolPopup(UIElement):
|
|||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -638,7 +712,9 @@ class ToolPopup(UIElement):
|
|||
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
|
||||
|
|
|
|||
260
ui_status_bar.py
260
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):
|
||||
|
||||
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
|
||||
|
|
@ -192,44 +258,52 @@ class StatusBarUI(UIElement):
|
|||
# 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:
|
||||
|
|
@ -238,7 +312,8 @@ class StatusBarUI(UIElement):
|
|||
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:
|
||||
|
|
@ -246,32 +321,40 @@ class StatusBarUI(UIElement):
|
|||
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:
|
||||
|
|
@ -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
|
||||
|
|
@ -330,7 +414,13 @@ class StatusBarUI(UIElement):
|
|||
|
||||
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"
|
||||
|
|
@ -339,20 +429,22 @@ 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
|
||||
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):
|
||||
|
|
@ -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,18 +512,18 @@ 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)
|
||||
|
|
|
|||
89
ui_swatch.py
89
ui_swatch.py
|
|
@ -1,14 +1,16 @@
|
|||
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):
|
||||
|
||||
class UISwatch(UIElement):
|
||||
def __init__(self, ui, popup):
|
||||
self.ui = ui
|
||||
self.popup = popup
|
||||
|
|
@ -18,8 +20,15 @@ class UISwatch(UIElement):
|
|||
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 = []
|
||||
|
|
@ -82,7 +91,6 @@ class UISwatch(UIElement):
|
|||
|
||||
|
||||
class CharacterSetSwatch(UISwatch):
|
||||
|
||||
# scale the character set will be drawn at
|
||||
char_scale = 2
|
||||
min_scale = 1
|
||||
|
|
@ -102,14 +110,18 @@ class CharacterSetSwatch(UISwatch):
|
|||
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
|
||||
|
|
@ -125,7 +137,9 @@ class CharacterSetSwatch(UISwatch):
|
|||
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)
|
||||
|
|
@ -163,9 +177,7 @@ class CharacterSetSwatch(UISwatch):
|
|||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -207,9 +222,10 @@ class CharacterSetSwatch(UISwatch):
|
|||
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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -255,7 +278,10 @@ class PaletteSwatch(UISwatch):
|
|||
|
||||
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
|
||||
|
|
@ -276,7 +302,10 @@ class PaletteSwatch(UISwatch):
|
|||
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,7 +323,10 @@ 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
|
||||
|
||||
|
|
@ -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,7 +418,6 @@ class ColorSelectionLabelRenderable(UIRenderable):
|
|||
|
||||
|
||||
class CharacterGridRenderable(LineRenderable):
|
||||
|
||||
color = (0.5, 0.5, 0.5, 0.25)
|
||||
|
||||
def build_geo(self):
|
||||
|
|
|
|||
204
ui_tool.py
204
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,7 +33,7 @@ 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
|
||||
|
|
@ -39,7 +47,7 @@ class UITool:
|
|||
|
||||
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)
|
||||
|
||||
|
|
@ -60,8 +68,13 @@ class UITool:
|
|||
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):
|
||||
|
|
@ -69,7 +82,12 @@ class UITool:
|
|||
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):
|
||||
|
|
@ -77,7 +95,12 @@ class UITool:
|
|||
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):
|
||||
|
|
@ -85,7 +108,12 @@ class UITool:
|
|||
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):
|
||||
|
|
@ -109,11 +137,10 @@ 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):
|
||||
"""
|
||||
|
|
@ -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,10 +194,9 @@ 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
|
||||
|
|
@ -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,21 +220,20 @@ 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()
|
||||
|
|
@ -233,12 +259,11 @@ 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)
|
||||
|
|
@ -250,14 +275,20 @@ class TextTool(UITool):
|
|||
# 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, press Escape to stop entering text.", 5
|
||||
)
|
||||
|
||||
def finish_entry(self):
|
||||
self.input_active = False
|
||||
|
|
@ -266,7 +297,7 @@ class TextTool(UITool):
|
|||
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.")
|
||||
|
||||
def reset_cursor_start(self, new_x, new_y):
|
||||
self.start_x, self.start_y = int(new_x), int(new_y)
|
||||
|
|
@ -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,16 +384,15 @@ 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)
|
||||
|
|
@ -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
|
||||
|
|
@ -454,11 +492,10 @@ 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!
|
||||
|
|
@ -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,33 +541,34 @@ 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'
|
||||
class FillTool(UITool):
|
||||
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_BG_COLOR: FILL_BOUND_CHAR,
|
||||
}
|
||||
|
||||
def __init__(self, ui):
|
||||
|
|
@ -542,8 +582,12 @@ class FillTool(UITool):
|
|||
|
||||
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,13 +1,10 @@
|
|||
|
||||
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
|
||||
icon_scale_factor = 4
|
||||
snap_left = True
|
||||
|
|
@ -58,7 +55,7 @@ class ToolBar(UIElement):
|
|||
|
||||
class ToolBarButton(UIButton):
|
||||
width, height = 4, 2
|
||||
caption = ''
|
||||
caption = ""
|
||||
tooltip_on_hover = True
|
||||
|
||||
def get_tooltip_text(self):
|
||||
|
|
@ -66,14 +63,15 @@ class ToolBarButton(UIButton):
|
|||
|
||||
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):
|
||||
button = ToolBarButton(self)
|
||||
|
|
@ -81,14 +79,18 @@ class ArtToolBar(ToolBar):
|
|||
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):
|
||||
|
|
@ -104,11 +106,11 @@ 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
|
||||
|
|
|
|||
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" },
|
||||
]
|
||||
57
vector.py
57
vector.py
|
|
@ -1,17 +1,17 @@
|
|||
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."
|
||||
|
|
@ -51,6 +51,7 @@ class Vec3:
|
|||
"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,6 +96,7 @@ 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,
|
||||
|
|
@ -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])
|
||||
|
|
@ -156,6 +170,7 @@ def ray_plane_intersection(plane_x, plane_y, plane_z,
|
|||
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