Gouvernathor
c0bc1cd69f
Not exactly sure what to do with the screens for saves compat tbh, should be fine either way and maybe removing the definitions will work
372 lines
14 KiB
Plaintext
372 lines
14 KiB
Plaintext
# Screens left for saves compat
|
|
screen chibi(chibi_object):
|
|
zorder chibi_object.zorder
|
|
sensitive False
|
|
fixed:
|
|
at chibi_object.transform
|
|
fit_first True
|
|
for d in chibi_object.displayables():
|
|
add d
|
|
# frame: # Debug frame
|
|
# background "#00ff0055"
|
|
|
|
screen chibi_emote(emote, chibi_object):
|
|
zorder chibi_object.zorder
|
|
sensitive False
|
|
add f"emo_{emote}":
|
|
at emote_effect
|
|
anchor (0.5, 1.0)
|
|
pos chibi_object.pos
|
|
zoom ChibiRoom.get().scale
|
|
xzoom (-1 if chibi_object.flip else 1)
|
|
if chibi_object.tag in ("genie", "snape"):
|
|
offset (int(75*ChibiRoom.get().scale), int(-200*ChibiRoom.get().scale))
|
|
else:
|
|
offset (int(50*ChibiRoom.get().scale), int(-170*ChibiRoom.get().scale))
|
|
|
|
label chibi_emote(emote, name):
|
|
python:
|
|
if emote == "hide":
|
|
emote = None
|
|
get_chibi_object(name).emote(emote)
|
|
return
|
|
|
|
default chibi_moves = {}
|
|
|
|
init -1 python:
|
|
def chibi_displayable(chibi_object):
|
|
return At(Fixed(*chibi_object.displayables(), fit_first=True), chibi_object.transform)
|
|
|
|
def chibi_emote_displayable(emote, chibi_object):
|
|
if chibi_object.flip:
|
|
xzoom = -1
|
|
else:
|
|
xzoom = 1
|
|
if chibi_object.tag in ("genie", "snape"):
|
|
offset = (int(75*ChibiRoom.get().scale), int(-200*ChibiRoom.get().scale))
|
|
else:
|
|
offset = (int(50*ChibiRoom.get().scale), int(-170*ChibiRoom.get().scale))
|
|
# TODO: test in which order the transforms should be put
|
|
return At(f"emo_{emote}", emote_effect, Transform(anchor=(.5, 1.), pos=chibi_object.pos, zoom=ChibiRoom.get().scale, xzoom=xzoom, offset=offset))
|
|
|
|
def update_chibi(name):
|
|
"""Update the chibi object for a given character."""
|
|
# TODO: Remove once chibi is ready.
|
|
if "hooch" == name:
|
|
return
|
|
|
|
get_chibi_object(name).update()
|
|
|
|
def get_chibi_object(name):
|
|
"""Get a chibi object by its character's name."""
|
|
name = f"{name}_chibi"
|
|
c = getattr(renpy.store, name, None)
|
|
if c and isinstance(c, Chibi):
|
|
return c
|
|
else:
|
|
raise Exception(f"Chibi object not found. {name}")
|
|
|
|
def complete_chibi_moves(**elapsed):
|
|
"""Resume old chibi action after (multiple) reduced move calls."""
|
|
q = []
|
|
for chibi, (t, a) in chibi_moves.items():
|
|
et = elapsed.get(chibi, 0)
|
|
t -= et
|
|
q.append((chibi, t, a))
|
|
|
|
q.sort(key=lambda x: x[1]) # Sort by time
|
|
pt = 0
|
|
for chibi, t, a in q:
|
|
renpy.pause(t - pt)
|
|
pt += t
|
|
get_chibi_object(chibi).do(a)
|
|
|
|
chibi_moves.clear()
|
|
|
|
class Chibi(object):
|
|
""" Manages a character's chibi.
|
|
|
|
Actions:
|
|
* Represent what a chibi is doing.
|
|
* Determine which transform is applied.
|
|
* Allow layers to be changed to the relevant images (via update callback).
|
|
|
|
There are two types of actions, one is used in place and the other while moving.
|
|
|
|
Actions are defined in the `actions` dict as a tuple: (special, transform, move_action|loop_time).
|
|
* `special` (bool) specifies whether layer images should come from a folder with the same name as the action.
|
|
This can be useful to prevent repetitive code in update callbacks.
|
|
* `transform` (string) is the name of the transform that is used for this action.
|
|
It will be combined with a base transform.
|
|
* `move_action` (string) if set, it's the action that will be used when the chibi starts moving after the current action.
|
|
It should not be set for move actions.
|
|
* `loop_time` (float) if set, it's the time in seconds for one animation loop of this action. Used to calculate movement time.
|
|
It should only be set for move actions. Set to zero to disable time adjustments.
|
|
|
|
Layers:
|
|
A chibi is made up of one or more named layers. These are cleared on update and should be set by a callback function.
|
|
* Layers can be accessed as `chibi_object[key]`.
|
|
* A layer can be set to either a filename or any kind of displayable.
|
|
* When setting an image filename, this class will look for it in `image_path` (or `image_path/action` if the action is special).
|
|
* Adding `~` as a prefix to a filename will ignore the special action folder.
|
|
This can be useful for images that are compatible with multiple actions.
|
|
* Layers are updated whenever the action changes by calling `update_callback`, which is expected to set the layers again.
|
|
"""
|
|
|
|
actions = {
|
|
None: (False, None, "walk"),
|
|
"walk": (False, "chibi_walk", 0.32),
|
|
"run": (False, "chibi_walk", 0),
|
|
"fly": (True, "chibi_fly", "fly_move"),
|
|
"fly_move": (True, "chibi_fly_move", 0),
|
|
"wand": (True, "chibi_wand", "walk"),
|
|
}
|
|
|
|
def __init__(self, tag, layers, update_callback, zorder=3, speed=100, image_path=None, actions=None, places=None):
|
|
self.tag = tag
|
|
|
|
# Use a tuple/list to specify the order of layers in a dict
|
|
self.layers_order = layers
|
|
self.layers = dict.fromkeys(layers)
|
|
|
|
self.update_callback = update_callback
|
|
|
|
if image_path:
|
|
self.image_path = image_path
|
|
else:
|
|
self.image_path = f"characters/{tag}/chibis"
|
|
|
|
if actions:
|
|
# Override class variable for this instance
|
|
self.actions = Chibi.actions | actions
|
|
|
|
self.zorder = zorder
|
|
self.speed = speed # pixels/sec
|
|
|
|
self.pos = (0,0)
|
|
self.flip = False
|
|
self.action = None
|
|
self.action_info = self.resolve_action(None)
|
|
self.special = None
|
|
self.transform = None
|
|
|
|
self.screen_tag = screen_tag = f"{tag}_chibi"
|
|
config.tag_layer[screen_tag] = "master"
|
|
|
|
self.emote_tag = emote_tag = f"{tag}_chibi_emote"
|
|
config.tag_layer[emote_tag] = "master"
|
|
|
|
def show(self):
|
|
renpy.show(self.screen_tag, zorder=self.zorder, what=chibi_displayable(self))
|
|
|
|
def hide(self):
|
|
renpy.hide(self.screen_tag)
|
|
renpy.hide(self.emote_tag)
|
|
|
|
def emote(self, emote=None):
|
|
if renpy.showing(self.emote_tag):
|
|
renpy.hide(self.emote_tag)
|
|
renpy.pause(0.2) # Pause for duration of emote_effect
|
|
if emote:
|
|
renpy.show(self.emote_tag, zorder=self.zorder, what=chibi_emote_displayable(emote, self))
|
|
|
|
def update(self):
|
|
self.clear()
|
|
if self.update_callback:
|
|
self.update_callback(self)
|
|
|
|
def move(self, path=None, speed=1.0, reduce=False, action=None):
|
|
""" Moves to a certain point or along a path.
|
|
Movement takes into account the action, direction, time and transitions.
|
|
Flipping is not possible mid-path, so the character should always face the same way.
|
|
"""
|
|
if isinstance(path, tuple):
|
|
path = [path]
|
|
|
|
real_path = [self.pos]
|
|
for x, y in path:
|
|
pos = self.resolve_position(x,y)
|
|
real_path.append(pos)
|
|
path = real_path
|
|
|
|
flip = self.pos[0] <= path[-1][0]
|
|
if self.flip != flip:
|
|
self.flip = flip
|
|
self.do(self.action) # Do a flip!
|
|
renpy.with_statement(d3)
|
|
|
|
# Resolve the move action
|
|
old_action = self.action
|
|
if action:
|
|
move_action = action
|
|
elif isinstance(self.action_info[2], str):
|
|
# Action info provides a move action
|
|
move_action = self.action_info[2]
|
|
else:
|
|
# Current action is already a move action
|
|
move_action = self.action
|
|
|
|
_, trans_name, loop_time = self.resolve_action(move_action)
|
|
|
|
# Calculate movement time
|
|
times = []
|
|
for i in range(len(path) - 1):
|
|
dist = math.sqrt((path[i][0] - path[i+1][0])**2 + (path[i][1] - path[i+1][1])**2)
|
|
time = dist / (float(self.speed) * speed)
|
|
if loop_time > 0:
|
|
# Round to nearest multiple of loop time to end on the right frame
|
|
time = loop_time * round(time/loop_time)
|
|
times.append(time)
|
|
|
|
time = sum(times)
|
|
|
|
# Apply the action with a transform
|
|
trans = self.resolve_transform(trans_name, path, times)
|
|
self.do(move_action, trans)
|
|
self.position(*path[-1])
|
|
|
|
if reduce:
|
|
# Reduce the pause and don't do the old action
|
|
if reduce == "all":
|
|
reduce = time
|
|
elif isinstance(reduce, bool):
|
|
reduce = 0
|
|
|
|
time -= float(reduce)
|
|
chibi_moves[self.tag] = (reduce, old_action)
|
|
|
|
if time > 0:
|
|
renpy.pause(time)
|
|
else:
|
|
# Pause while moving and then do the old action
|
|
renpy.pause(time)
|
|
if old_action != move_action:
|
|
self.do(old_action)
|
|
renpy.with_statement(None)
|
|
|
|
def do(self, action=None, trans=None):
|
|
"""Performs an action. Applies a transform and updates the chibi."""
|
|
self.set_action(action)
|
|
|
|
# Set the transform (static version by default)
|
|
if trans is None:
|
|
trans = self.resolve_transform(self.action_info[1])
|
|
|
|
# Hide the screen so transform is reset properly
|
|
self.hide()
|
|
self.transform = trans
|
|
self.update()
|
|
self.show()
|
|
|
|
def set_action(self, action):
|
|
"""Set the action state (no screen update)."""
|
|
self.action = action
|
|
self.action_info = self.resolve_action(action)
|
|
self.special = self.action_info[0]
|
|
|
|
def position(self, x=None, y=None, flip=None):
|
|
"""Set the position to be used on next update."""
|
|
(x,y) = self.resolve_position(x,y)
|
|
if flip is not None:
|
|
self.flip = flip
|
|
self.pos = (x,y)
|
|
|
|
def resolve_position(self, x=None, y=None):
|
|
"""Compute new position from place keywords (or just ints) for one or both of the coordinates."""
|
|
return ChibiRoom.place((x,y), self.pos)
|
|
|
|
def resolve_transform(self, name, *args):
|
|
"""Get transform from the store by name and apply arguments."""
|
|
if name:
|
|
trans = getattr(renpy.store, name, None)
|
|
if isinstance(trans, renpy.display.transform.ATLTransform):
|
|
# Combine with base transform
|
|
return combine_transforms(self.base_transform(), trans(*args))
|
|
elif config.developer:
|
|
raise Exception(f"Expected an ATL transform: {name}")
|
|
# No transform was given or found
|
|
return self.base_transform()
|
|
|
|
def base_transform(self):
|
|
scale = ChibiRoom.get().scale
|
|
return chibi_base(self.pos, self.flip, scale)
|
|
|
|
def resolve_action(self, name):
|
|
"""Get action info by name (falling back to "parent" action or default)."""
|
|
while True:
|
|
if name in self.actions:
|
|
return self.actions[name]
|
|
elif '_' in name:
|
|
name = name.rsplit('_', 1)[0]
|
|
else:
|
|
return self.actions[None]
|
|
|
|
def displayables(self):
|
|
"""Yields non-empty layers in an iterable manner."""
|
|
for k in self.layers_order:
|
|
d = self.layers.get(k, None)
|
|
if d is not None:
|
|
yield d
|
|
|
|
def clear(self):
|
|
for k in self.layers.keys():
|
|
self.layers[k] = None
|
|
|
|
def __getitem__(self, key):
|
|
return self.layers[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
if key not in self.layers:
|
|
# Layer must be defined at init
|
|
raise KeyError(key)
|
|
|
|
if isinstance(value, str) and '.' in value:
|
|
# Assume value is a filename and resolve it
|
|
if value.startswith('~') or not self.special:
|
|
# Avoid special directory
|
|
value = self.image_path + "/" + value.lstrip("~/")
|
|
else:
|
|
value = self.image_path + "/" + self.action + "/" + value
|
|
|
|
self.layers[key] = value
|
|
|
|
class ChibiRoom(object):
|
|
"""Defines chibi scale factor and named positions (places) for a room."""
|
|
|
|
# Rooms by name
|
|
rooms = dict()
|
|
|
|
def __init__(self, name, scale, places):
|
|
self.name = name
|
|
self.scale = scale
|
|
self.places = places
|
|
|
|
ChibiRoom.rooms[self.name] = self
|
|
|
|
def resolve(self, p, d, x_or_y):
|
|
"""Resolve p as coordinate, if None return d, else return p as integer."""
|
|
if not isinstance(p, int):
|
|
if p is None:
|
|
return d
|
|
elif p in self.places:
|
|
return self.places[p][int(x_or_y)] or d
|
|
else:
|
|
return int(p)
|
|
return p
|
|
|
|
@staticmethod
|
|
def get(room=None):
|
|
room = room or renpy.store.states.room
|
|
chibi_room = ChibiRoom.rooms.get(room, None)
|
|
if not chibi_room:
|
|
raise Exception(f"Chibi room is not defined for {room}")
|
|
return chibi_room
|
|
|
|
@staticmethod
|
|
def place(place, position, room=None):
|
|
"""Resolve place coordinates in the current room, or a given room (by name)."""
|
|
chibi_room = ChibiRoom.get(room)
|
|
x = chibi_room.resolve(place[0], position[0], False)
|
|
y = chibi_room.resolve(place[1], position[1], True)
|
|
return (x,y)
|