from collections import defaultdict
from functools import partial
from itertools import chain
from time import monotonic
from kivy.clock import Clock
from kivy.lang.builder import Builder
from kivy.logger import Logger
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
ReferenceListProperty,
)
from kivy.uix.widget import Widget
from kivy.vector import Vector
from elide.boardscatter import BoardScatterPlane
from ..boardview import BoardView
from ..kivygarden.texturestack import Stack, TextureStackPlane
class GridPawn(Stack):
default_image_paths = ["atlas://rltiles/base.atlas/unseen"]
@property
def _stack_plane(self):
return self.board.pawn_plane
class GridSpot(Stack):
default_image_paths = ["atlas://rltiles/floor.atlas/floor-stone"]
@property
def _stack_plane(self):
return self.board.spot_plane
[docs]
class GridBoard(Widget):
selection = ObjectProperty()
selection_candidates = ListProperty()
character = ObjectProperty()
tile_width = NumericProperty(32)
tile_height = NumericProperty(32)
tile_size = ReferenceListProperty(tile_width, tile_height)
pawn_cls = GridPawn
spot_cls = GridSpot
def __init__(self, **kwargs):
self.pawn = {}
self.spot = {}
self.contained = defaultdict(set)
super().__init__(**kwargs)
def update(self):
make_spot = self.make_spot
make_pawn = self.make_pawn
self.spot_plane.data = list(
map(make_spot, self.character.place.values())
)
self.pawn_plane.data = list(
map(make_pawn, self.character.thing.values())
)
self.spot_plane.redraw()
self.pawn_plane.redraw()
def add_spot(self, placen, *args):
if placen not in self.character.place:
raise KeyError(f"No such place for spot: {placen}")
if placen in self.spot:
raise KeyError("Already have a Spot for this Place")
spt = self.make_spot(self.character.place[placen])
self.spot_plane.add_datum(spt)
self.spot[placen] = self.spot_cls(board=self, proxy=spt["proxy"])
def make_spot(self, place):
placen = place["name"]
if not isinstance(placen, tuple) or len(placen) != 2:
raise TypeError(
"Can only make spot from places with tuple names of length 2",
placen,
)
if (
not isinstance(placen, tuple)
or len(placen) != 2
or not isinstance(placen[0], int)
or not isinstance(placen[1], int)
):
raise TypeError(
"GridBoard can only display places named with pairs of ints"
)
if "_image_paths" in place:
textures = list(place["_image_paths"])
else:
textures = list(self.spot_cls.default_image_paths)
r = {
"name": placen,
"x": int(placen[0] * self.tile_width),
"y": int(placen[1] * self.tile_height),
"width": int(self.tile_width),
"height": int(self.tile_height),
"textures": textures,
"proxy": place,
}
return r
def make_pawn(self, thing) -> dict:
location = self.spot[thing["location"]]
r = {
"name": thing["name"],
"x": int(location.x),
"y": int(location.y),
"width": int(self.tile_width),
"height": int(self.tile_height),
"location": location,
"textures": list(
thing.get("_image_paths", self.pawn_cls.default_image_paths)
),
"proxy": thing,
}
return r
def add_pawn(self, thingn, *args):
if thingn not in self.character.thing:
raise KeyError(f"No such thing: {thingn}")
if thingn in self.pawn:
raise KeyError(f"Already have a pawn for {thingn}")
thing = self.character.thing[thingn]
if thing["location"] not in self.spot:
# The location is not in the grid. That's fine.
return
pwn = self.make_pawn(thing)
self.pawn[thingn] = self.pawn_cls(board=self, proxy=pwn["proxy"])
location = pwn["location"]
self.contained[location].add(thingn)
self.pawn_plane.add_datum(pwn)
def _trigger_add_pawn(self, thingn):
part = partial(self.add_pawn, thingn)
Clock.unschedule(part)
Clock.schedule_once(part, 0)
def on_parent(self, *args):
if self.character is None:
Clock.schedule_once(self.on_parent, 0)
return
if self.character.engine.closed:
return
Logger.debug("GridBoard: on_parent start")
start_ts = monotonic()
if not hasattr(self, "pawn_plane"):
self.pawn_plane = TextureStackPlane(pos=self.pos, size=self.size)
self.spot_plane = TextureStackPlane(pos=self.pos, size=self.size)
self.bind(
pos=self.pawn_plane.setter("pos"),
size=self.pawn_plane.setter("size"),
)
self.bind(
pos=self.spot_plane.setter("pos"),
size=self.spot_plane.setter("size"),
)
self.add_widget(self.spot_plane)
self.add_widget(self.pawn_plane)
spot_data = list(
map(
self.make_spot,
filter(
lambda spot: isinstance(spot["name"], tuple)
and len(spot["name"]) == 2,
self.character.place.values(),
),
)
)
if not spot_data:
if hasattr(self.spot_plane, "_redraw_bind_uid"):
self.spot_plane.unbind_uid(
"data", self.spot_plane._redraw_bind_data_uid
)
del self.spot_plane._redraw_bind_data_uid
if hasattr(self.pawn_plane, "_redraw_bind_uid"):
self.pawn_plane.unbind_uid(
"data", self.pawn_plane._redraw_bind_data_uid
)
del self.pawn_plane._redraw_bind_data_uid
self.spot_plane.data = self.pawn_plane.data = []
self.spot_plane.redraw()
self.pawn_plane.redraw()
self.spot_plane._redraw_bind_data_uid = self.spot_plane.fbind(
"data", self.spot_plane._trigger_redraw
)
self.pawn_plane._redraw_bind_data_uid = self.pawn_plane.fbind(
"data", self.pawn_plane._trigger_redraw
)
self.character.thing.connect(self.update_from_thing)
self.character.place.connect(self.update_from_place)
return
for spt in spot_data:
self.spot[spt["name"]] = self.spot_cls(
board=self, proxy=spt["proxy"]
)
self.spot_plane.unbind_uid(
"data", self.spot_plane._redraw_bind_data_uid
)
self.spot_plane.data = spot_data
self.spot_plane.redraw()
self.spot_plane._redraw_bind_data_uid = self.spot_plane.fbind(
"data", self.spot_plane._trigger_redraw
)
wide = max(datum["x"] for datum in spot_data) + self.tile_width
high = max(datum["y"] for datum in spot_data) + self.tile_width
self.size = self.spot_plane.size = self.pawn_plane.size = wide, high
pawn_data = list(
map(
self.make_pawn,
filter(
lambda thing: thing["location"] in self.spot,
self.character.thing.values(),
),
)
)
for pwn in pawn_data:
self.pawn[pwn["name"]] = self.pawn_cls(
board=self, proxy=pwn["proxy"]
)
self.pawn_plane.data = pawn_data
self.character.thing.connect(self.update_from_thing)
self.character.place.connect(self.update_from_place)
Logger.debug(
f"GridBoard: on_parent end, took {monotonic() - start_ts:,.2f}"
f" seconds"
)
def rm_spot(self, name):
spot = self.spot.pop(name)
if spot in self.selection_candidates:
self.selection_candidates.remove(spot)
for thing in spot.proxy.contents():
self.rm_pawn(thing.name)
self.spot_plane.remove(name)
def rm_pawn(self, name):
pwn = self.pawn.pop(name)
if pwn in self.selection_candidates:
self.selection_candidates.remove(pwn)
self.pawn_plane.remove(name)
def update_from_thing(self, thing, key, value):
if thing and not (key is None and not value):
if thing.name not in self.pawn:
self.add_pawn(thing.name)
elif key == "location":
if value in self.spot:
loc = self.spot[value]
pwn = self.pawn[thing.name]
pwn.pos = loc.pos
elif thing.name in self.pawn:
self.rm_pawn(thing.name)
elif key == "_image_paths":
self.pawn[thing.name].paths = value
else:
if thing.name in self.pawn:
self.rm_pawn(thing.name)
def update_from_place(self, place, key, value):
if place and not (key is None and not value):
if key == "_image_paths":
self.spot[place.name].paths = value
else:
if place.name in self.spot:
self.rm_spot(place.name)
[docs]
def on_touch_down(self, touch):
self.selection_candidates.extend(
chain(
map(
self.pawn.get,
self.pawn_plane.iter_collided_keys(*touch.pos),
),
map(
self.spot.get,
self.spot_plane.iter_collided_keys(*touch.pos),
),
)
)
return super().on_touch_down(touch)
[docs]
def on_touch_up(self, touch):
touched = {
candidate
for candidate in self.selection_candidates
if candidate.collide_point(*touch.pos)
}
if not touched:
self.selection_candidates = []
return super().on_touch_up(touch)
if len(touched) == 1:
self.selection = touched.pop()
self.selection_candidates = []
return super().on_touch_up(touch)
pawns_touched = {
node for node in touched if isinstance(node, self.pawn_cls)
}
if len(pawns_touched) == 1:
self.selection = pawns_touched.pop()
self.selection_candidates = []
return super().on_touch_up(touch)
elif pawns_touched:
# TODO: Repeatedly touching a spot with multiple pawns on it
# should cycle through the pawns, and then finally the spot.
self.selection = pawns_touched.pop()
self.selection_candidates = []
return super().on_touch_up(touch)
spots_touched = touched - pawns_touched
if len(spots_touched) == 1:
self.selection = spots_touched.pop()
self.selection_candidates = []
return super().on_touch_up(touch)
assert not spots_touched, (
"How do you have overlapping spots on a GridBoard??"
)
self.selection_candidates = []
return super().on_touch_up(touch)
[docs]
class GridBoardScatterPlane(BoardScatterPlane):
selection_candidates = ListProperty([])
selection = ObjectProperty(allownone=True)
keep_selection = BooleanProperty(False)
board = ObjectProperty()
def spot_from_dummy(self, dummy):
raise NotImplementedError("oop")
def pawn_from_dummy(self, dummy):
dummy_center = self.to_local(*dummy.center)
candidates = list(
self.board.spot_plane.iter_collided_keys(*dummy_center)
)
if not candidates:
return
whereat_d = self.board.spot[candidates.pop()]
half_wide = self.board.tile_width / 2
half_high = self.board.tile_height / 2
if candidates:
whereat_center = whereat_d.x + half_wide, whereat_d.y + half_high
dist = Vector(*whereat_center).distance(dummy_center)
while candidates:
thereat_d = self.board.spot[candidates.pop()]
thereat_center = (
thereat_d.x + half_wide,
thereat_d.y + half_high,
)
thereto = Vector(*thereat_center).distance(dummy_center)
if thereto < dist:
whereat_d, dist = thereat_d, thereto
self.board.pawn_plane.add_datum(
self.board.make_pawn(
self.board.character.new_thing(
dummy.name, whereat_d.name, _image_paths=list(dummy.paths)
)
)
)
dummy.num += 1
def on_board(self, *args):
self.clear_widgets()
self.add_widget(self.board)
[docs]
class GridBoardView(BoardView):
pass
kv = """
<GridBoard>:
app: app
size_hint: None, None
<GridBoardView>:
plane: boardplane
GridBoardScatterPlane:
id: boardplane
board: root.board
scale_min: root.scale_min
scale_max: root.scale_max
pos: root.pos
size: root.size
"""
kv_loaded = False
if not kv_loaded:
Builder.load_string(kv)
kv_loaded = True