2022-05-16 23:48:22 +00:00
|
|
|
|
|
|
|
# Screen used by chibi class (each chibi object derives its own uniquely tagged screen from this one)
|
|
|
|
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
|
2024-03-26 19:31:41 +00:00
|
|
|
add f"emo_{emote}":
|
2022-05-16 23:48:22 +00:00
|
|
|
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 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."""
|
2024-03-26 19:31:41 +00:00
|
|
|
name = f"{name}_chibi"
|
2022-05-16 23:48:22 +00:00
|
|
|
c = getattr(renpy.store, name, None)
|
|
|
|
if c and isinstance(c, Chibi):
|
|
|
|
return c
|
|
|
|
else:
|
2024-03-26 19:31:41 +00:00
|
|
|
raise Exception(f"Chibi object not found. {name}")
|
2022-05-16 23:48:22 +00:00
|
|
|
|
|
|
|
def complete_chibi_moves(**elapsed):
|
|
|
|
"""Resume old chibi action after (multiple) reduced move calls."""
|
|
|
|
q = []
|
2022-09-29 21:19:55 +00:00
|
|
|
for chibi, (t, a) in chibi_moves.items():
|
2022-05-16 23:48:22 +00:00
|
|
|
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.
|
2024-04-02 15:18:18 +00:00
|
|
|
This can be useful to prevent repetitive code in update callbacks.
|
2022-05-16 23:48:22 +00:00
|
|
|
* `transform` (string) is the name of the transform that is used for this action.
|
2024-04-02 15:18:18 +00:00
|
|
|
It will be combined with a base transform.
|
2022-05-16 23:48:22 +00:00
|
|
|
* `move_action` (string) if set, it's the action that will be used when the chibi starts moving after the current action.
|
2024-04-02 15:18:18 +00:00
|
|
|
It should not be set for move actions.
|
2022-05-16 23:48:22 +00:00
|
|
|
* `loop_time` (float) if set, it's the time in seconds for one animation loop of this action. Used to calculate movement time.
|
2024-04-02 15:18:18 +00:00
|
|
|
It should only be set for move actions. Set to zero to disable time adjustments.
|
2022-05-16 23:48:22 +00:00
|
|
|
|
|
|
|
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.
|
2024-04-02 15:18:18 +00:00
|
|
|
This can be useful for images that are compatible with multiple actions.
|
2022-05-16 23:48:22 +00:00
|
|
|
* 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([(k, None) for k in layers])
|
|
|
|
|
|
|
|
self.update_callback = update_callback
|
|
|
|
|
|
|
|
if image_path:
|
|
|
|
self.image_path = image_path
|
|
|
|
else:
|
2024-03-26 19:31:41 +00:00
|
|
|
self.image_path = f"characters/{tag}/chibis"
|
2022-05-16 23:48:22 +00:00
|
|
|
|
|
|
|
if actions:
|
|
|
|
# Override class variable for this instance
|
|
|
|
self.actions = dict(Chibi.actions)
|
|
|
|
self.actions.update(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
|
|
|
|
|
|
|
|
# Define a screen for the chibi
|
2024-03-26 19:31:41 +00:00
|
|
|
self.screen_tag = f"{tag}_chibi"
|
2022-05-16 23:48:22 +00:00
|
|
|
renpy.define_screen(self.screen_tag, Chibi._screen, tag=self.screen_tag, zorder="chibi_object.zorder")
|
|
|
|
|
|
|
|
# Define a screen for the chibi emote
|
2024-03-26 19:31:41 +00:00
|
|
|
self.emote_tag = f"{tag}_chibi_emote"
|
2022-05-16 23:48:22 +00:00
|
|
|
renpy.define_screen(self.emote_tag, Chibi._emote_screen, tag=self.emote_tag, zorder="chibi_object.zorder")
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _screen(chibi_object, **kwargs):
|
|
|
|
# Emulate a Ren'py `use` statement to derive a chibi screen from the generic one
|
|
|
|
renpy.use_screen("chibi", chibi_object, _name=kwargs["_name"], _scope=kwargs["_scope"])
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _emote_screen(emote, chibi_object, **kwargs):
|
|
|
|
# Emulate a Ren'py `use` statement to derive a chibi_emote screen from the generic one
|
|
|
|
renpy.use_screen("chibi_emote", emote, chibi_object, _name=kwargs["_name"], _scope=kwargs["_scope"])
|
|
|
|
|
|
|
|
def show(self):
|
|
|
|
renpy.show_screen(self.screen_tag, chibi_object=self)
|
|
|
|
|
|
|
|
def hide(self):
|
|
|
|
renpy.hide_screen(self.screen_tag)
|
|
|
|
renpy.hide_screen(self.emote_tag)
|
2024-05-16 14:24:01 +00:00
|
|
|
renpy.hide(f"{self.tag}_main", layer="dolls")
|
2022-05-16 23:48:22 +00:00
|
|
|
|
|
|
|
def emote(self, emote=None):
|
|
|
|
if renpy.get_screen(self.emote_tag):
|
|
|
|
renpy.hide_screen(self.emote_tag)
|
|
|
|
renpy.pause(0.2) # Pause for duration of emote_effect
|
|
|
|
if emote:
|
|
|
|
renpy.show_screen(self.emote_tag, emote=emote, chibi_object=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
|
2022-09-29 21:19:55 +00:00
|
|
|
elif isinstance(self.action_info[2], str):
|
2022-05-16 23:48:22 +00:00
|
|
|
# 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 = []
|
2022-09-29 21:19:55 +00:00
|
|
|
for i in range(len(path) - 1):
|
2022-05-16 23:48:22 +00:00
|
|
|
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:
|
2024-03-26 19:31:41 +00:00
|
|
|
raise Exception(f"Expected an ATL transform: {name}")
|
2022-05-16 23:48:22 +00:00
|
|
|
# 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):
|
2022-09-29 21:19:55 +00:00
|
|
|
for k in self.layers.keys():
|
2022-05-16 23:48:22 +00:00
|
|
|
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)
|
|
|
|
|
2022-09-29 21:19:55 +00:00
|
|
|
if isinstance(value, str) and '.' in value:
|
2022-05-16 23:48:22 +00:00
|
|
|
# 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):
|
2023-03-31 22:41:48 +00:00
|
|
|
room = room or renpy.store.states.room
|
2022-05-16 23:48:22 +00:00
|
|
|
chibi_room = ChibiRoom.rooms.get(room, None)
|
|
|
|
if not chibi_room:
|
2024-03-26 19:31:41 +00:00
|
|
|
raise Exception(f"Chibi room is not defined for {room}")
|
2022-05-16 23:48:22 +00:00
|
|
|
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)
|