class Vector3(geometry.Vector3, record.ProtobufRecord): PROTOBUF_TYPE = geometry_pb2.Vector3 FIELDS = [ record.Field("x", record.Scalar), record.Field("y", record.Scalar), record.Field("z", record.Scalar), ] def __init__(self, x=None, y=None, z=None): super().__init__(x, y, z) def __repr__(self): return "models.geometry." + super().__repr__()
class Vector2(geometry.Vector2, record.ProtobufRecord): PROTOBUF_TYPE = geometry_pb2.Vector2 FIELDS = [ record.Field("x", record.Scalar), record.Field("y", record.Scalar), ] def __init__(self, x=None, y=None): # We need to make the x and y parameters optional for deserialization to # work. super().__init__(x, y) def __repr__(self): return "models.geometry." + super().__repr__()
class Realm(record.ProtobufRecord): PROTOBUF_TYPE = realm_pb2.Realm FIELDS = [ record.Field("name", record.Scalar), record.Field("size", geometry.Vector2), ] def __init__(self, *args, **kwargs): self.regions = None super().__init__(*args, **kwargs) @property def bounds(self): return geometry.Rectangle(0, 0, self.size.x, self.size.y) def load_intersecting_regions(self, bounds): left = max([Region.floor(bounds.left), 0]) top = max([Region.floor(bounds.top), 0]) right = min([Region.ceil(bounds.right), self.size.x]) bottom = min([Region.ceil(bounds.bottom), self.size.y]) for y in range(top, bottom, Region.SIZE): for x in range(left, right, Region.SIZE): yield self.regions.load(geometry.Vector2(x, y)) def is_terrain_passable_by(self, entity, bounds, direction): if not self.bounds.contains(bounds): return False try: regions = list(self.load_intersecting_regions(bounds)) except KeyError: return False return all( region.is_terrain_passable_by(entity, bounds, direction) for region in regions) def is_passable_by(self, entity, bounds, direction): if not self.bounds.contains(bounds): return False try: regions = list(self.load_intersecting_regions(bounds)) except KeyError: return False return all( region.is_passable_by(entity, bounds, direction) for region in regions)
class Rectangle(geometry.Rectangle, record.ProtobufRecord): PROTOBUF_TYPE = geometry_pb2.Rectangle FIELDS = [ record.Field("left", record.Scalar), record.Field("top", record.Scalar), record.Field("width", record.Scalar), record.Field("height", record.Scalar), ] def __init__(self, left=None, top=None, width=None, height=None): # We need to make all the parameters optional for deserialization to work. super().__init__(left, top, width, height) def __repr__(self): return "models.geometry." + super().__repr__()
class Building(Entity): FIELDS = [ record.Field("size", geometry.Rectangle, extension=entities_pb2.Building) ] TYPE = "building"
class Item(record.ProtobufRecord): REGISTRY = {} PROTOBUF_TYPE = items_pb2.Item FIELDS = [ record.Field("id", record.Scalar), record.Field("type", record.Scalar, record_field="TYPE") ] @classmethod def register(cls, subclass): cls.REGISTRY[subclass.TYPE] = subclass return subclass @classmethod def from_protobuf_polymorphic(cls, proto): return cls.REGISTRY[proto.type].from_protobuf(proto)
class Player(Actor): FIELDS = [ record.Field("online", record.Scalar, extension=entities_pb2.Player.ext) ] TYPE = "player" def __init__(self, *args, **kwargs): self.bbox = geometry.Rectangle(0, 0, 1, 1) super().__init__(*args, **kwargs)
class NPC(Actor): FIELDS = [ record.Field("behavior", record.Scalar, extension=entities_pb2.NPC.ext) ] TYPE = "npc" def __init__(self, *args, **kwargs): self.bbox = geometry.Rectangle(0, 0, 1, 1) super().__init__(*args, **kwargs) def to_public_protobuf(self): proto = super().to_public_protobuf() proto.Extensions[entities_pb2.NPC.ext].ClearField("behavior") return proto
class Drop(Entity): FIELDS = [ record.Field("item", record.Polymorphic(items.Item), extension=entities_pb2.Drop.ext) ] TYPE = "drop" def __init__(self, *args, **kwargs): self.bbox = geometry.Rectangle(0, 0, 1, 1) self.direction = 0 super().__init__(*args, **kwargs) def is_passable_by(self, entity, direction): return True
class Entity(record.ProtobufRecord): REGISTRY = {} FIELDS = [ record.Field("type", record.Scalar, record_field="TYPE"), record.Field("realm_id", record.Scalar), record.Field("location", geometry.Vector3), record.Field("bbox", geometry.Rectangle), record.Field("direction", record.Scalar) ] PROTOBUF_TYPE = entities_pb2.Entity DIRECTION_VECTORS = { 0: geometry.Vector3(0, -1, 0), # N 1: geometry.Vector3(-1, 0, 0), # W 2: geometry.Vector3(0, 1, 0), # S 3: geometry.Vector3(1, 0, 0) # E } DIRECTIONS = {v: k for k, v in DIRECTION_VECTORS.items()} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.location_log = collections.deque() self.location_lock = asyncio.Lock() def log_location(self, time, location): self.location_log.append((time, location)) def retain_log_after(self, since): while self.location_log: time, head = self.location_log[0] if time < since: self.location_log.popleft() else: break @property def all_locations(self): for _, location in self.location_log: yield location yield self.location @property def direction_vector(self): return self.DIRECTION_VECTORS[self.direction] @classmethod def register(cls, subclass): cls.REGISTRY[subclass.TYPE] = subclass return subclass def to_public_protobuf(self): return self.to_protobuf() def to_protected_protobuf(self): protobuf = self.to_protobuf() protobuf.type = "avatar" return protobuf @classmethod def from_protobuf_polymorphic(cls, proto): return cls.REGISTRY[proto.type].from_protobuf(proto) @property def target_location(self): return self.location.offset(self.direction_vector) @property def target_bounds(self): return self.bounds.offset(self.direction_vector) @property def bounds(self): return self.bbox.offset(self.location) @property def regions(self): return self.realm.load_intersecting_regions(self.bounds) @contextlib.contextmanager def movement(self): initial_regions = list(self.regions) yield for region in initial_regions: region.entities.remove(self) for region in self.regions: region.entities.add(self) def is_passable_by(self, entity, direction): return False def send(self, protocol, message): protocol.send(self.id, message) def send_via_bus(self, bus, target, message): return asyncio.gather(*[ green.coroutine(self.send)(protocol, message) for protocol in bus.get_protocols_for_channel(target.channel) ]) def broadcast(self, bus, channel, message): return asyncio.gather(*[ green.coroutine(self.send)(protocol, message) for protocol in bus.get_protocols_for_channel(channel) if protocol.policy.can_receive_broadcasts_from(self) ]) def broadcast_to_regions(self, bus, message): return asyncio.gather(*[ self.broadcast(bus, region.channel, message) for region in self.regions ]) @property def bus_key(self): return (self.TYPE, self.id) @property def channel(self): return (self.TYPE, self.id) def subscribe(self, bus, channel): bus.subscribe(self.bus_key, channel) def unsubscribe(self, bus, channel): bus.unsubscribe(self.bus_key, channel) def add_to_bus(self, bus, protocol): bus.add(self.bus_key, protocol) def remove_from_bus(self, bus): bus.remove(self.bus_key)
class Actor(Entity): FIELDS = [ record.Field("name", record.Scalar, extension=entities_pb2.Actor.ext), record.Field("health", record.Scalar, extension=entities_pb2.Actor.ext), record.Field("gender", record.Scalar, extension=entities_pb2.Actor.ext), record.Field("body", record.Scalar, extension=entities_pb2.Actor.ext), record.RepeatedField("inventory", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext), record.Field("facial", record.Scalar, extension=entities_pb2.Actor.ext), record.Field("hair", record.Scalar, extension=entities_pb2.Actor.ext), record.Field("head_item", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext), record.Field("torso_item", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext), record.Field("legs_item", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext), record.Field("feet_item", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext), record.Field("weapon", record.Polymorphic(items.Item), extension=entities_pb2.Actor.ext) ] BASE_SPEED = 4 @property def speed(self): return self.BASE_SPEED def to_public_protobuf(self): proto = super().to_public_protobuf() proto.Extensions[entities_pb2.Actor.ext].ClearField("inventory") return proto @property def equipment(self): return { item for item in [ self.head_item, self.torso_item, self.legs_item, self.feet_item, self.weapon ] if item is not None } @property def inventory(self): return self.inventory_dict.values() @inventory.setter def inventory(self, items): self.inventory_dict = {item.id: item for item in items} @property def full_inventory(self): return set(self.inventory) | self.equipment def get_realm(self, realm_store): return realm_store.find(self.realm_id) def is_passable_by(self, entity, direction): return False
class Layer(record.ProtobufRecord): PROTOBUF_TYPE = realm_pb2.Layer FIELDS = [ record.Field("terrain", record.Scalar), record.RepeatedField("tiles", record.Scalar), ]