def __init__(self, server_address: str) -> None: self._server_address = server_address self._character_serializer = serpyco.Serializer(CharacterModel) self._characters_serializer = serpyco.Serializer(CharacterModel, many=True) self._stuffs_serializer = serpyco.Serializer(StuffModel, many=True) self._zone_serializer = serpyco.Serializer(ZoneMapModel) self._tiles_serializer = serpyco.Serializer(ZoneTileTypeModel, many=True) self._gui_description_serializer = serpyco.Serializer(Description) self._zone_required_character_data_serializer = serpyco.Serializer(ZoneRequiredPlayerData) self._zone_build_serializers = serpyco.Serializer(ZoneBuildModel, many=True) self._move_zone_infos_serializer = serpyco.Serializer(MoveZoneInfos)
class ZoneEventSerializerFactory: serializers: typing.Dict[ZoneEventType, serpyco.Serializer] = {} for zone_event_type, zone_event_data_type in zone_event_data_types.items(): serializers[zone_event_type] = serpyco.Serializer(ZoneEvent[zone_event_data_type]) def get_serializer(self, zone_event_data_type: ZoneEventType) -> serpyco.Serializer: return self.serializers[zone_event_data_type]
class UseAsArmorAction(WithStuffAction): input_model: typing.Type[EmptyModel] = EmptyModel input_model_serializer = serpyco.Serializer(EmptyModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: if not stuff.armor: raise ImpossibleAction("Ce n'est pas une armure/protection") if not stuff.ready_for_use: raise ImpossibleAction(f"{stuff.name} n'est pas utilisable") if character.armor: raise ImpossibleAction("Vous utilisez déjà une armure/protection") def check_request_is_possible( self, character: "CharacterModel", stuff: "StuffModel", input_: EmptyModel ) -> None: # TODO BS 2019-09-03: check stuff owned self.check_is_possible(character, stuff) def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel" ) -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [ CharacterActionLink( name=f"Utiliser {stuff.name} comme armure/protection", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.USE_AS_ARMOR, stuff_id=stuff.id, query_params={}, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return actions def perform( self, character: "CharacterModel", stuff: "StuffModel", input_: EmptyModel ) -> Description: self._kernel.stuff_lib.set_as_used_as_armor(character.id, stuff.id) return Description( title="Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action=f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
class NotUseAsShieldAction(WithStuffAction): input_model: typing.Type[EmptyModel] = EmptyModel input_model_serializer = serpyco.Serializer(EmptyModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: if character.shield and character.shield.id == stuff.id: return raise ImpossibleAction("Vous n'utilisez pas ce bouclier") def check_request_is_possible( self, character: "CharacterModel", stuff: "StuffModel", input_: EmptyModel ) -> None: self.check_is_possible(character, stuff) def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel" ) -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [ CharacterActionLink( name=f"Ne plus utiliser {stuff.name} comme bouclier", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.NOT_USE_AS_SHIELD, stuff_id=stuff.id, query_params={}, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return actions def perform( self, character: "CharacterModel", stuff: "StuffModel", input_: EmptyModel ) -> Description: self._kernel.stuff_lib.unset_as_used_as_shield(character.id, stuff.id) return Description( title="Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action=f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
def test_can_use_custom_field_decorator(self, spec_fixture): @dataclass class CustomPetASchema(PetSchema): email: str = serpyco.string_field( format_=serpyco.StringFormat.EMAIL, pattern="^[A-Z]", min_length=3, max_length=24, ) @dataclass class CustomPetBSchema(PetSchema): age: int = serpyco.number_field(minimum=1, maximum=120) @dataclass class WithStringField(object): """String field test class""" foo: str = serpyco.string_field( format_=serpyco.StringFormat.EMAIL, pattern="^[A-Z]", min_length=3, max_length=24, ) serializer = serpyco.Serializer(WithStringField) serializer.json_schema() spec_fixture.spec.components.schema("Pet", schema=PetSchema) spec_fixture.spec.components.schema("CustomPetA", schema=CustomPetASchema) spec_fixture.spec.components.schema("CustomPetB", schema=CustomPetBSchema) props_0 = get_definitions(spec_fixture.spec)["Pet"]["properties"] props_a = get_definitions( spec_fixture.spec)["CustomPetA"]["properties"] props_b = get_definitions( spec_fixture.spec)["CustomPetB"]["properties"] assert props_0["name"]["type"] == "string" assert "format" not in props_0["name"] assert props_a["email"]["type"] == "string" assert json.dumps(props_a["email"]["format"]) == '"email"' assert props_a["email"]["pattern"] == "^[A-Z]" assert props_a["email"]["maxLength"] == 24 assert props_a["email"]["minLength"] == 3 assert props_b["age"]["type"] == "integer" assert props_b["age"]["minimum"] == 1 assert props_b["age"]["maximum"] == 120
def serializer(self) -> Serializer: """ Return cached (create id if not yet created) serializer :return: serializer instance """ if self._serializer is None: self._serializer = serpyco.Serializer( self.schema, only=self._only, exclude=self._exclude, omit_none=False, ) return self._serializer
class KillCharacterAction(WithCharacterAction): input_model = EmptyModel input_model_serializer = serpyco.Serializer(EmptyModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", with_character: "CharacterModel") -> None: if not with_character.vulnerable: raise ImpossibleAction( f"{with_character.name} est en capacité de se defendre") def check_request_is_possible(self, character: "CharacterModel", with_character: "CharacterModel", input_: typing.Any) -> None: self.check_is_possible(character, with_character) def get_character_actions( self, character: "CharacterModel", with_character: "CharacterModel" ) -> typing.List[CharacterActionLink]: return [ CharacterActionLink( name="Tuer", link=get_with_character_action_url( character_id=character.id, with_character_id=with_character.id, action_type=ActionType.KILL_CHARACTER, query_params={}, action_description_id=self._description.id, ), ) ] def perform(self, character: "CharacterModel", with_character: "CharacterModel", input_: typing.Any) -> Description: self._kernel.character_lib.kill(with_character.id) return Description( title=f"Vous avez tué {with_character.name}", footer_links=[Part(is_link=True, go_back_zone=True)], )
class GiveToCharacterAction(WithCharacterAction): input_model = GiveToModel input_model_serializer = serpyco.Serializer(GiveToModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible( self, character: "CharacterModel", with_character: "CharacterModel" ) -> None: pass # TODO: user config to refuse receiving ? def check_request_is_possible( self, character: "CharacterModel", with_character: "CharacterModel", input_: GiveToModel ) -> None: self.check_is_possible(character, with_character) if input_.give_resource_id is not None and input_.give_resource_quantity: if not self._kernel.resource_lib.have_resource( character_id=character.id, resource_id=input_.give_resource_id, quantity=input_.give_resource_quantity, ): raise ImpossibleAction(f"{character.name} n'en à pas assez") if input_.give_stuff_id: try: stuff: StuffModel = self._kernel.stuff_lib.get_stuff(input_.give_stuff_id) except NoResultFound: raise ImpossibleAction(f"Objet inexistant") carried_count = self._kernel.stuff_lib.have_stuff_count( character_id=character.id, stuff_id=stuff.stuff_id ) if carried_count < (input_.give_stuff_quantity or 1): raise ImpossibleAction(f"{character.name} n'en à pas assez") def get_character_actions( self, character: "CharacterModel", with_character: "CharacterModel" ) -> typing.List[CharacterActionLink]: return [CharacterActionLink(name="Donner", link=self._get_url(character, with_character))] def _get_url( self, character: "CharacterModel", with_character: "CharacterModel", input_: typing.Optional[GiveToModel] = None, ) -> str: return get_with_character_action_url( character_id=character.id, with_character_id=with_character.id, action_type=ActionType.GIVE_TO_CHARACTER, query_params=self.input_model_serializer.dump(input_) if input_ else {}, action_description_id=self._description.id, ) def _get_give_something_description( self, character: "CharacterModel", with_character: "CharacterModel", input_: GiveToModel ) -> Description: parts = [] carried_stuffs = self._kernel.stuff_lib.get_carried_by(character.id, exclude_crafting=False) carried_resources = self._kernel.resource_lib.get_carried_by(character.id) displayed_stuff_ids: typing.List[str] = [] for carried_stuff in carried_stuffs: if carried_stuff.stuff_id not in displayed_stuff_ids: parts.append( Part( is_link=True, label=f"Donner {carried_stuff.name}", form_action=self._get_url( character, with_character, GiveToModel(give_stuff_id=carried_stuff.id) ), ) ) displayed_stuff_ids.append(carried_stuff.stuff_id) for carried_resource in carried_resources: parts.append( Part( is_link=True, label=f"Donner {carried_resource.name}", form_action=self._get_url( character, with_character, GiveToModel(give_resource_id=carried_resource.id) ), ) ) return Description( title=f"Donner à {with_character.name}", items=parts + [ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id ), ), ], can_be_back_url=True, ) def perform( self, character: "CharacterModel", with_character: "CharacterModel", input_: GiveToModel ) -> Description: if input_.give_stuff_id is not None: stuff: StuffModel = self._kernel.stuff_lib.get_stuff(input_.give_stuff_id) likes_this_stuff = self._kernel.stuff_lib.get_carried_by( character.id, exclude_crafting=False, stuff_id=stuff.stuff_id ) if input_.give_stuff_quantity is None: if len(likes_this_stuff) > 1: return Description( title=f"Donner {stuff.name} à {with_character.name}", items=[ Part( is_form=True, form_values_in_query=True, form_action=self._get_url(character, with_character, input_), submit_label="Prendre", items=[ Part( label="Quantité ?", type_=Type.NUMBER, name="give_stuff_quantity", default_value=str(len(likes_this_stuff)), ) ], ) ], can_be_back_url=True, ) input_.give_stuff_quantity = 1 for i in range(input_.give_stuff_quantity): self._kernel.stuff_lib.set_carried_by(likes_this_stuff[i].id, with_character.id) if input_.give_resource_id is not None: resource_description = self._kernel.game.config.resources[input_.give_resource_id] carried_resource = self._kernel.resource_lib.get_one_carried_by( character.id, input_.give_resource_id ) if input_.give_resource_quantity is None: unit_str = self._kernel.translation.get(resource_description.unit) return Description( title=f"Donner {resource_description.name} à {with_character.name}", items=[ Part( is_form=True, form_values_in_query=True, form_action=self._get_url(character, with_character, input_), submit_label="Prendre", items=[ Part( label=f"Quantité ({unit_str}) ?", type_=Type.NUMBER, name="give_resource_quantity", default_value=str(carried_resource.quantity), ) ], ) ], can_be_back_url=True, ) self._kernel.resource_lib.reduce_carried_by( character_id=character.id, resource_id=input_.give_resource_id, quantity=input_.give_resource_quantity, ) self._kernel.resource_lib.add_resource_to( character_id=with_character.id, resource_id=input_.give_resource_id, quantity=input_.give_resource_quantity, ) return self._get_give_something_description(character, with_character, input_)
class CollectResourceAction(CharacterAction): input_model: typing.Type[CollectResourceModel] = CollectResourceModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel") -> None: for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, ): return raise ImpossibleAction("Il n'y a rien à collecter ici") def check_request_is_possible(self, character: "CharacterModel", input_: input_model) -> None: # FIXME BS 2019-08-27: check input_.row_i and input_.col_i are near # FIXME BS 2019-08-29: check if quantity is possible resources = self._kernel.game.world_manager.get_resources_at( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=input_.row_i, zone_col_i=input_.col_i, ) resource_ids = [resource.id for resource in resources] if input_.resource_id in resource_ids: return raise ImpossibleAction( f"Il n'y a pas de '{input_.resource_id}' à cet endroit") def get_character_actions( self, character: "CharacterModel") -> typing.List[CharacterActionLink]: inspect_zone_positions = get_on_and_around_coordinates( character.zone_row_i, character.zone_col_i) character_actions: typing.List[CharacterActionLink] = [] for row_i, col_i in inspect_zone_positions: for resource in self._kernel.game.world_manager.get_resources_at( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=row_i, zone_col_i=col_i, ): tile_type = self._kernel.tile_maps_by_position[( character.world_row_i, character.world_col_i)].source.geography.rows[row_i][col_i] query_params = self.input_model(resource_id=resource.id, row_i=row_i, col_i=col_i) character_actions.append( CharacterActionLink( name=f"Récupérer {resource.name} sur {tile_type.name}", link=get_character_action_url( character_id=character.id, action_type=ActionType.COLLECT_RESOURCE, action_description_id=self._description.id, query_params=self.input_model_serializer.dump( query_params), ), cost=None, merge_by=(ActionType.COLLECT_RESOURCE, resource.id), group_name="Ramasser du matériel ou des ressources", )) return character_actions def _get_resource_and_cost( self, character: "CharacterDocument", input_: input_model ) -> typing.Tuple[ResourceDescriptionModel, ExtractableResourceDescriptionModel, float]: tile_type = self._kernel.tile_maps_by_position[( character.world_row_i, character.world_col_i )].source.geography.rows[input_.row_i][input_.col_i] extractable_resources: typing.Dict[ str, ExtractableResourceDescriptionModel] = self._kernel.game.config.extractions[ tile_type.id].resources resource_extraction_description: ExtractableResourceDescriptionModel = extractable_resources[ input_.resource_id] resource: ResourceDescriptionModel = self._kernel.game.config.resources[ input_.resource_id] # TODO BS 2019-08-29: cost per unit modified by competence / stuff cost_per_unit = resource_extraction_description.cost_per_unit return resource, resource_extraction_description, cost_per_unit def get_cost( self, character: "CharacterModel", input_: typing.Optional[input_model] = None ) -> typing.Optional[float]: if input_.quantity is not None: character_doc = self._character_lib.get_document(character.id) resource, resource_extraction_description, cost_per_unit = self._get_resource_and_cost( character_doc, input_) return input_.quantity * resource_extraction_description.cost_per_unit def perform(self, character: "CharacterModel", input_: input_model) -> Description: character_doc = self._character_lib.get_document(character.id) resource, resource_extraction_description, cost_per_unit = self._get_resource_and_cost( character_doc, input_) if input_.quantity is None: unit_name = self._kernel.translation.get(resource.unit) return Description( title=f"Récupérer du {resource.name}", items=[ Part( is_form=True, form_values_in_query=True, form_action=get_character_action_url( character_id=character.id, action_type=ActionType.COLLECT_RESOURCE, action_description_id=self._description.id, query_params=self.input_model_serializer.dump( input_), ), items=[ Part( label= f"Quantité (coût: {cost_per_unit} par {unit_name}) ?", type_=Type.NUMBER, name="quantity", ) ], ) ], ) self._kernel.resource_lib.add_resource_to( character_id=character_doc.id, resource_id=input_.resource_id, quantity=input_.quantity, commit=False, ) cost = self.get_cost(character, input_=input_) if cost is None: raise RollingError("Cost compute should not be None !") self._kernel.character_lib.reduce_action_points(character.id, cost, commit=False) self._kernel.server_db_session.commit() return Description( title= f"{input_.quantity} {self._kernel.translation.get(resource.unit)} récupéré", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements") ], )
from data import ParentTestObject name = "serpyco" def get_x(obj): return obj.x + 10 @dataclasses.dataclass class SubM: w: int y: str z: int x: int = serpyco.field(getter=get_x) @dataclasses.dataclass class ComplexM: foo: str sub: SubM subs: typing.List[SubM] bar: int = serpyco.field(getter=ParentTestObject.bar) serializer = serpyco.Serializer(ComplexM) def serialization_func(obj, many): return serializer.dump(obj, many=many)
class EatResourceAction(WithResourceAction): input_model: typing.Type[EatResourceModel] = EatResourceModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return { "accept_resources": [ game_config.resources[r] for r in action_config_raw["accept_resources"] ], "effects": [ game_config.character_effects[e] for e in action_config_raw["character_effects"] ], "require": action_config_raw["require"], } def check_is_possible(self, character: "CharacterModel", resource_id: str) -> None: accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] if resource_id in accept_resources_ids: return raise ImpossibleAction("Non consommable") def check_request_is_possible(self, character: "CharacterModel", resource_id: str, input_: EatResourceModel) -> None: self.check_is_possible(character, resource_id) # TODO BS 2019-09-14: perf carried_resource = next( (cr for cr in self._kernel.resource_lib.get_carried_by(character.id) if cr.id == resource_id)) require = self._description.properties["require"] if carried_resource.quantity >= require: return unit_name = self._kernel.translation.get(carried_resource.unit) raise ImpossibleAction( f"Vous ne possédez pas assez de {carried_resource.name} " f"({require} {unit_name} requis)") def get_character_actions( self, character: "CharacterModel", resource_id: str) -> typing.List[CharacterActionLink]: accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] # TODO BS 2019-09-14: perf carried_resource = next( (cr for cr in self._kernel.resource_lib.get_carried_by(character.id) if cr.id == resource_id)) if carried_resource.id in accept_resources_ids: return [ # FIXME BS NOW: il semblerait que que comme on ne donne pas le description_id, # lorsque on veux consommer la resource, l'action factory prend la première, et donc # pas la bonne. Revoir ça, je pense qu'il faut systématiquement donner un # description_id. Voir les conséquences. CharacterActionLink( name=f"Manger {carried_resource.name}", link=get_with_resource_action_url( character_id=character.id, action_type=ActionType.EAT_RESOURCE, resource_id=resource_id, query_params={}, action_description_id=self._description.id, ), cost=None, ) ] return [] def perform(self, character: "CharacterModel", resource_id: str, input_: input_model) -> Description: character_doc = self._character_lib.get_document(character.id) effects: typing.List[ CharacterEffectDescriptionModel] = self._description.properties[ "effects"] self._kernel.resource_lib.reduce_carried_by( character.id, resource_id, quantity=self._description.properties["require"], commit=False, ) for effect in effects: self._effect_manager.enable_effect(character_doc, effect) self._kernel.server_db_session.add(character_doc) self._kernel.server_db_session.commit() return Description( title="Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action= f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
class EmptyStuffAction(WithStuffAction): input_model: typing.Type[EmptyModel] = EmptyModel input_model_serializer = serpyco.Serializer(input_model) def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: if not stuff.filled_with_resource: raise ImpossibleAction("Ne contient rien") def check_request_is_possible(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> None: if not stuff.filled_with_resource: raise ImpossibleAction("Ne contient rien") @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel") -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [ CharacterActionLink( name=f"Vider {stuff.name}", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.EMPTY_STUFF, stuff_id=stuff.id, query_params={}, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return actions def perform(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> Description: footer_links = [ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action=f"/_describe/character/{character.id}/inventory", ), Part( is_link=True, label="Voir l'objet", form_action=DESCRIBE_LOOK_AT_STUFF_URL.format( character_id=character.id, stuff_id=stuff.id), classes=["primary"], ), ] try: self._kernel.stuff_lib.empty_stuff(stuff) except CantEmpty as exc: return Description(title=str(exc), footer_links=footer_links) return Description(title=f"{stuff.name} vidé(e)", footer_links=footer_links)
class ContinueStuffConstructionAction(WithStuffAction): input_model = ContinueStuffModel input_model_serializer = serpyco.Serializer(ContinueStuffModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: properties = fill_base_action_properties(cls, game_config, {}, action_config_raw) return properties def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: if not stuff.under_construction: raise ImpossibleAction("Non concérné") def check_request_is_possible( self, character: "CharacterModel", stuff: "StuffModel", input_: ContinueStuffModel ) -> None: self.check_is_possible(character, stuff) if input_.ap: if character.action_points < input_.ap: raise ImpossibleAction(f"{character.name} ne possède passez de points d'actions") def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel" ) -> typing.List[CharacterActionLink]: return [ CharacterActionLink( name=f"Continuer le travail", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.CONTINUE_STUFF_CONSTRUCTION, action_description_id=self._description.id, query_params={}, stuff_id=stuff.id, ), cost=self.get_cost(character, stuff), merge_by="continue_craft", ) ] def get_cost( self, character: "CharacterModel", stuff: "StuffModel", input_: typing.Optional[CraftInput] = None, ) -> typing.Optional[float]: return 0.0 # we use only one action description in config and we don't want ap for continue def perform( self, character: "CharacterModel", stuff: "StuffModel", input_: ContinueStuffModel ) -> Description: bonus = character.get_skill_value("intelligence") + character.get_skill_value("crafts") bonus_divider = max(1.0, (bonus * 2) / DEFAULT_MAXIMUM_SKILL) remain_ap = stuff.ap_required - stuff.ap_spent remain_ap_for_character = remain_ap / bonus_divider if not input_.ap: return Description( title=f"Continuer de travailler sur {stuff.name}", items=[ Part( is_form=True, form_values_in_query=True, form_action=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.CONTINUE_STUFF_CONSTRUCTION, query_params={}, action_description_id=self._description.id, stuff_id=stuff.id, ), items=[ Part( text=f"Il reste {round(remain_ap, 3)} PA à passer ({round(remain_ap_for_character, 3)} avec vos bonus)" ), Part( label=f"Combien de points d'actions dépenser ?", type_=Type.NUMBER, name="ap", ), ], ) ], ) consume_ap = min(remain_ap, input_.ap * bonus_divider) stuff_doc = self._kernel.stuff_lib.get_stuff_doc(stuff.id) stuff_doc.ap_spent = float(stuff_doc.ap_spent) + consume_ap self._kernel.character_lib.reduce_action_points(character.id, consume_ap, commit=False) if stuff_doc.ap_spent >= stuff_doc.ap_required: stuff_doc.under_construction = False title = f"Construction de {stuff.name} terminé" else: title = f"Construction de {stuff.name} avancé" self._kernel.server_db_session.commit() return Description( title=title, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'objet commencé", form_action=DESCRIBE_LOOK_AT_STUFF_URL.format( character_id=character.id, stuff_id=stuff_doc.id ), classes=["primary"], ), ], force_back_url=f"/_describe/character/{character.id}/on_place_actions", )
class ConstructBuildAction(WithBuildAction): input_model = ConstructBuildModel input_model_serializer = serpyco.Serializer(ConstructBuildModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", build_id: int) -> None: build_doc = self._kernel.build_lib.get_build_doc(build_id) if not build_doc.under_construction: raise ImpossibleAction("Cette construction est terminée") def check_request_is_possible( self, character: "CharacterModel", build_id: int, input_: typing.Any ) -> None: # FIXME BS 2019-10-03: delete all check_request_is_possible and move into perform pass def get_character_actions( self, character: "CharacterModel", build_id: int ) -> typing.List[CharacterActionLink]: build_doc = self._kernel.build_lib.get_build_doc(build_id) if build_doc.under_construction: return [ CharacterActionLink( name=f"Faire avancer la construction", link=get_with_build_action_url( character_id=character.id, build_id=build_id, action_type=ActionType.CONSTRUCT_BUILD, action_description_id=self._description.id, query_params={}, ), cost=None, ) ] return [] def _get_biggest_left_percent( self, build_doc: BuildDocument, raise_if_missing: bool = False ) -> float: build_description = self._kernel.game.config.builds[build_doc.build_id] biggest_left_percent = 0.0 for required_resource in build_description.build_require_resources: resource_description, left, left_percent = BringResourcesOnBuild.get_resource_infos( self._kernel, required_resource, build_doc=build_doc, raise_if_missing=raise_if_missing, ) if left_percent > biggest_left_percent: biggest_left_percent = left_percent return biggest_left_percent def perform( self, character: "CharacterModel", build_id: int, input_: input_model ) -> Description: build_doc = self._kernel.build_lib.get_build_doc(build_id) build_progress = get_build_progress(build_doc, kernel=self._kernel) build_description = self._kernel.game.config.builds[build_doc.build_id] try: lowest_required_left_percent = self._get_biggest_left_percent( build_doc, raise_if_missing=True ) except MissingResource as exc: raise ImpossibleAction(str(exc)) able_to_percent = 100 - lowest_required_left_percent maximum_pa_to_reach = build_description.cost * (able_to_percent / 100) max_pa_to_spent = maximum_pa_to_reach - float(build_doc.ap_spent) if input_.cost_to_spent is None: title = ( f"Cette construction est avancée à {round(build_progress)}%. Compte tenu des " f"réserves, vous pouvez avancer jusqu'a " f"{round(able_to_percent)}%, soit y passer maximum " f"{round(max_pa_to_spent, 2)} point d'actions" ) return Description( title=title, items=[ Part( is_form=True, form_values_in_query=True, form_action=get_with_build_action_url( character_id=character.id, build_id=build_id, action_type=ActionType.CONSTRUCT_BUILD, action_description_id=self._description.id, query_params=self.input_model_serializer.dump(input_), ), items=[ Part( label=f"Y passer combien de temps (point d'actions) ?", type_=Type.NUMBER, name="cost_to_spent", ) ], ) ], ) input_cost_to_spent = input_.cost_to_spent if input_.cost_to_spent > max_pa_to_spent: input_cost_to_spent = max_pa_to_spent # TODO BS 2019-10-08: When skills/stuff improve ap spent, compute real ap spent here and # indicate by error how much time to spent to spent max real_progress_cost = input_cost_to_spent if character.action_points < input_cost_to_spent: raise ImpossibleAction("Pas assez de Points d'Actions") consume_resources_percent = (real_progress_cost * 100) / build_description.cost self._kernel.build_lib.progress_build( build_doc.id, real_progress_cost=real_progress_cost, consume_resources_percent=consume_resources_percent, commit=False, ) self._kernel.character_lib.reduce_action_points( character.id, cost=input_cost_to_spent, commit=False ) self._kernel.server_db_session.commit() return Description( title=f"Travail effectué", force_back_url=f"/_describe/character/{character.id}/build_actions", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( label="Voir le batiment", is_link=True, form_action=DESCRIBE_BUILD.format( build_id=build_doc.id, character_id=character.id ), classes=["primary"], ), ], )
class BringResourcesOnBuild(WithBuildAction): input_model = BringResourceModel input_model_serializer = serpyco.Serializer(BringResourceModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", build_id: int) -> None: return def check_request_is_possible( self, character: "CharacterModel", build_id: int, input_: typing.Any ) -> None: return @classmethod def get_resource_infos( cls, kernel: "Kernel", required_resource: BuildBuildRequireResourceDescription, build_doc: BuildDocument, raise_if_missing: bool = False, ) -> typing.Tuple[ResourceDescriptionModel, float, float]: build_progress = get_build_progress(build_doc, kernel=kernel) stored_resources = kernel.resource_lib.get_stored_in_build(build_doc.id) stored_resources_by_resource_id: typing.Dict[str, CarriedResourceDescriptionModel] = { stored_resource.id: stored_resource for stored_resource in stored_resources } resource_description = kernel.game.config.resources[required_resource.resource_id] try: stored_resource = stored_resources_by_resource_id[required_resource.resource_id] stored_resource_quantity = stored_resource.quantity except KeyError: if raise_if_missing: raise MissingResource(f"Il manque {resource_description.name}") stored_resource_quantity = 0.0 absolute_left = required_resource.quantity - ( required_resource.quantity * (build_progress / 100) ) with_stored_left = absolute_left - stored_resource_quantity if with_stored_left < 0.0: with_stored_left = 0.0 left = with_stored_left if left: left_percent = (left * 100) / required_resource.quantity else: left_percent = 0.0 return resource_description, left, left_percent def get_character_actions( self, character: "CharacterModel", build_id: int ) -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [] build_doc = self._kernel.build_lib.get_build_doc(build_id) build_description = self._kernel.game.config.builds[build_doc.build_id] for required_resource in build_description.build_require_resources: resource_description, left, left_percent = self.get_resource_infos( self._kernel, required_resource, build_doc=build_doc ) if left <= 0: continue left_str = quantity_to_str(left, resource_description.unit, kernel=self._kernel) query_params = BringResourcesOnBuild.input_model_serializer.dump( BringResourcesOnBuild.input_model(resource_id=required_resource.resource_id) ) name = ( f"Apporter {resource_description.name} pour la construction " f"(manque {left_str} soit {round(left_percent)}%)" ) actions.append( CharacterActionLink( name=name, link=get_with_build_action_url( character_id=character.id, build_id=build_id, action_type=ActionType.BRING_RESOURCE_ON_BUILD, action_description_id=self._description.id, query_params=query_params, ), cost=None, ) ) return actions def perform( self, character: "CharacterModel", build_id: int, input_: typing.Any ) -> Description: build_doc = self._kernel.build_lib.get_build_doc(build_id) if input_.quantity is None: build_description = self._kernel.game.config.builds[build_doc.build_id] required_resource = next( ( brr for brr in build_description.build_require_resources if brr.resource_id == input_.resource_id ) ) resource_description, left, left_percent = self.get_resource_infos( self._kernel, required_resource, build_doc=build_doc ) left_str = quantity_to_str(left, resource_description.unit, kernel=self._kernel) unit_str = self._kernel.translation.get(resource_description.unit) return Description( title=f"Cette construction nécessite encore {left_str} " f"de {resource_description.name} (soit {round(left_percent)}%)", can_be_back_url=True, items=[ Part( is_form=True, form_values_in_query=True, form_action=get_with_build_action_url( character_id=character.id, build_id=build_id, action_type=ActionType.BRING_RESOURCE_ON_BUILD, action_description_id=self._description.id, query_params=self.input_model_serializer.dump(input_), ), items=[ Part( label=f"Quantité ({unit_str}) ?", type_=Type.NUMBER, name="quantity" ) ], ) ], ) resource_description = self._kernel.game.config.resources[input_.resource_id] try: self._kernel.resource_lib.reduce_carried_by( character.id, resource_id=input_.resource_id, quantity=input_.quantity, commit=False ) except (NotEnoughResource, NoCarriedResource): raise ImpossibleAction( f"{character.name} ne possède pas assez de {resource_description.name}" ) self._kernel.resource_lib.add_resource_to( build_id=build_doc.id, resource_id=input_.resource_id, quantity=input_.quantity, commit=False, ) self._kernel.server_db_session.commit() build_description = self._kernel.game.config.builds[build_doc.build_id] quantity_str = quantity_to_str( input_.quantity, resource_description.unit, kernel=self._kernel ) return Description( title=f"{quantity_str} {resource_description.name} déposé pour {build_description.name}", items=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( label="Voir le batiment", is_link=True, form_action=DESCRIBE_BUILD.format( build_id=build_doc.id, character_id=character.id ), ), ], force_back_url=f"/_describe/character/{character.id}/build_actions", )
@dataclass class About(object): current_datetime: datetime.datetime ip: str @staticmethod @serpyco.post_dump def add_python_version(data: dict) -> dict: data["python_version"] = utils.get_python_version() return data sensor = Sensor(name="<no name>", location=Location(lon=19, lat=32)) sensor_serializer = serpyco.Serializer(Sensor) # This list will contains all client websockets client_websockets: typing.List[web.WebSocketResponse] = [] @dataclass class EmptyPath(object): pass @dataclass class SensorName: name: str
class DrinkResourceAction(CharacterAction): input_model: typing.Type[DrinkResourceModel] = DrinkResourceModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return { "accept_resources": [ game_config.resources[r] for r in action_config_raw["accept_resources"] ], "effects": [ game_config.character_effects[e] for e in action_config_raw["character_effects"] ], } def check_is_possible(self, character: "CharacterModel") -> None: accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, material_type=self._kernel.game.config.liquid_material_id, ): if resource.type_.value in accept_resources_ids: return raise ImpossibleAction("Il n'y a pas à boire à proximité") def check_request_is_possible(self, character: "CharacterModel", input_: DrinkResourceModel) -> None: accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, material_type=self._kernel.game.config.liquid_material_id, ): if resource.id == input_.resource_id and input_.resource_id in accept_resources_ids: return raise ImpossibleAction( f"Il n'y a pas de {input_.resource_id} à proximité") def get_character_actions( self, character: "CharacterModel") -> typing.List[CharacterActionLink]: character_actions: typing.List[CharacterActionLink] = [] accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, material_type=self._kernel.game.config.liquid_material_id, ): if resource.id in accept_resources_ids: query_params = self.input_model(resource_id=resource.id) character_actions.append( CharacterActionLink( name=f"Drink {resource.name}", link=get_character_action_url( character_id=character.id, action_type=ActionType.DRINK_RESOURCE, action_description_id=self._description.id, query_params=self.input_model_serializer.dump( query_params), ), cost=self.get_cost(character), )) return character_actions def perform(self, character: "CharacterModel", input_: input_model) -> Description: character_doc = self._character_lib.get_document(character.id) effects: typing.List[ CharacterEffectDescriptionModel] = self._description.properties[ "effects"] for effect in effects: self._effect_manager.enable_effect(character_doc, effect) self._kernel.server_db_session.add(character_doc) self._kernel.server_db_session.commit() return Description( title="Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements") ], )
class DrinkStuffAction(WithStuffAction): input_model: typing.Type[DrinkStuffModel] = DrinkStuffModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return { "accept_resources": [ game_config.resources[r] for r in action_config_raw["accept_resources"] ], "effects": [ game_config.character_effects[e] for e in action_config_raw["character_effects"] ], } def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: # TODO BS 2019-07-31: check is owned stuff accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] if (stuff.filled_with_resource is not None and stuff.filled_with_resource in accept_resources_ids): return raise ImpossibleAction(f"Il n'y a pas de quoi boire la dedans") def check_request_is_possible(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> None: # TODO BS 2019-07-31: check is owned stuff accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] if (stuff.filled_with_resource is not None and stuff.filled_with_resource in accept_resources_ids): return raise ImpossibleAction(f"Il n'y a pas de quoi boire la dedans") def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel") -> typing.List[CharacterActionLink]: accept_resources_ids = [ rd.id for rd in self._description.properties["accept_resources"] ] if (stuff.filled_with_resource is not None and stuff.filled_with_resource in accept_resources_ids): query_params = self.input_model(stuff_id=stuff.id) resource_description = self._kernel.game.config.resources[ stuff.filled_with_resource] return [ CharacterActionLink( name=f"Boire {resource_description.name}", link=get_with_stuff_action_url( character.id, ActionType.DRINK_STUFF, query_params=self.input_model_serializer.dump( query_params), stuff_id=stuff.id, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return [] def perform(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> Description: character_doc = self._character_lib.get_document(character.id) effects: typing.List[ CharacterEffectDescriptionModel] = self._description.properties[ "effects"] for effect in effects: self._effect_manager.enable_effect(character_doc, effect) self._kernel.server_db_session.add(character_doc) self._kernel.character_lib.drink_stuff(character.id, stuff.id) self._kernel.server_db_session.commit() return Description( title="Vous avez bu", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements") ], )
class FollowCharacterAction(WithCharacterAction): input_model = FollowModel input_model_serializer = serpyco.Serializer(FollowModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", with_character: "CharacterModel") -> None: if not (character.world_row_i == with_character.world_row_i and character.world_col_i == with_character.world_col_i and not self._kernel.character_lib.is_following( character.id, with_character.id)): raise ImpossibleAction( f"{with_character.name} ne se trouve pas ici") def check_request_is_possible(self, character: "CharacterModel", with_character: "CharacterModel", input_: FollowModel) -> None: self.check_is_possible(character, with_character) def _get_url( self, character: "CharacterModel", with_character: "CharacterModel", input_: typing.Optional[FollowModel] = None, ) -> str: return get_with_character_action_url( character_id=character.id, with_character_id=with_character.id, action_type=ActionType.FOLLOW_CHARACTER, query_params=self.input_model_serializer.dump(input_) if input_ else {}, action_description_id=self._description.id, ) def get_character_actions( self, character: "CharacterModel", with_character: "CharacterModel" ) -> typing.List[CharacterActionLink]: return [ CharacterActionLink(name=f"Suivre {with_character.name}", link=self._get_url(character, with_character)), CharacterActionLink( name=f"Suivre {with_character.name} discrètement", link=self._get_url(character, with_character, input_=FollowModel(discreetly=True)), ), ] def perform(self, character: "CharacterModel", with_character: "CharacterModel", input_: FollowModel) -> Description: self._kernel.character_lib.set_following(character.id, with_character.id, discreetly=input_.discreetly) return Description( title=(f"Vous suivez {with_character.name}" + (" discrètement" if input_.discreetly else "")), items=[], footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), ), ], )
class AttackCharacterAction(WithCharacterAction): input_model = AttackModel input_model_serializer = serpyco.Serializer(AttackModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", with_character: "CharacterModel") -> None: pass def check_request_is_possible(self, character: "CharacterModel", with_character: "CharacterModel", input_: AttackModel) -> None: # lonely attack when exhausted is not possible if input_.lonely is not None and input_.lonely and not character.is_attack_ready( ): raise ImpossibleAction( f"{character.name} n'est pas en mesure de mener cette attaque !" ) # with_character must not been part of attacking affinity if input_.as_affinity is not None and self._kernel.affinity_lib.character_is_in_affinity( affinity_id=input_.as_affinity, character_id=with_character.id): raise ImpossibleAction( f"Vous ne pouvez pas attaquer un membre d'une même affinités") # It must have ready fighter to fight if input_.as_affinity is not None and not self._kernel.affinity_lib.count_ready_fighter( affinity_id=input_.as_affinity, world_row_i=character.world_row_i, world_col_i=character.world_col_i, ): raise ImpossibleAction( f"Personne n'est en état de se battre actuellement") def get_character_actions( self, character: "CharacterModel", with_character: "CharacterModel" ) -> typing.List[CharacterActionLink]: return [ CharacterActionLink(name=f"Attaquer", link=self._get_here_url( character, with_character), cost=0.0) ] def _get_here_url(self, character: "CharacterModel", with_character: "CharacterModel") -> str: return get_with_character_action_url( character_id=character.id, with_character_id=with_character.id, action_type=ActionType.ATTACK_CHARACTER, query_params={}, action_description_id=self._description.id, ) def _get_root_description(self, character: "CharacterModel", with_character: "CharacterModel") -> Description: here_url = self._get_here_url(character, with_character) parts = [] for affinity_relation in self._kernel.affinity_lib.get_accepted_affinities( character.id, warlord=True): affinity = self._kernel.affinity_lib.get_affinity( affinity_relation.affinity_id) parts.append( Part( is_link=True, form_action=here_url + f"&as_affinity={affinity.id}", label=f"Attaquer en tant que {affinity.name}", )) return Description( title=f"Attaquer {with_character.name}", items=[ Part(text="Veuillez préciser votre intention:"), Part( is_link=True, form_action=here_url + "&lonely=1", label="Attaquer seul et en mon nom uniquement", ), ] + parts, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), classes=["primary"], ), ], can_be_back_url=True, ) def _get_attack_lonely_description( self, character: "CharacterModel", with_character: "CharacterModel") -> Description: here_url = self._get_here_url(character, with_character) defense_description: DefendDescription = self._kernel.fight_lib.get_defense_description( origin_target=with_character, world_row_i=character.world_row_i, world_col_i=character.world_col_i, ) aff = ", ".join([a.name for a in defense_description.affinities]) self._check_attack_lonely(character, defense_description, aff) if 1 == len(defense_description.all_fighters): fighter = defense_description.all_fighters[0] text = f"Engager ce combat implique de vous battre contre {fighter.name} seul à seul" else: fighters = defense_description.all_fighters text = ( f"Engager ce combat implique de vous battre contre {len(fighters)} combattants " f"appartenants aux affinités: {aff}") return Description( title=f"Attaquer {with_character.name} seul", items=[ Part(text=text), Part( is_link=True, form_action=here_url + "&lonely=1&confirm=1", label= f"Je confirme, attaquer {with_character.name} maintenant !", ), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), ), ], ) def _check_attack_lonely(self, character: CharacterModel, defense: DefendDescription, aff: str) -> None: if not character.is_attack_ready(): raise ImpossibleAction("Vous n'etes pas en état de vous battre") # by affinities, character can be in defense side. In that case, don't permit the fight if character.id in [f.id for f in defense.all_fighters]: raise ImpossibleAction( "Vous ne pouvez pas mener cette attaque car parmis les defenseur se trouve " f"des personnes avec lesquelles vous etes affiliés. Affinités en défense: {aff}" ) def _perform_attack_lonely( self, character: "CharacterModel", with_character: "CharacterModel") -> Description: defense_description: DefendDescription = self._kernel.fight_lib.get_defense_description( origin_target=with_character, world_row_i=character.world_row_i, world_col_i=character.world_col_i, ) aff = ", ".join([a.name for a in defense_description.affinities]) self._check_attack_lonely(character, defense_description, aff) story = self._kernel.fight_lib.fight( attack=AttackDescription(all_fighters=[character], ready_fighters=[character]), defense=defense_description, ) parts = [Part(text=p) for p in story] self._proceed_events( attacker_title="Vous avez participé à un combat", attacked_title="Vous avez subit une attaque", characters=[character] + defense_description.all_fighters, author=character, story=story, ) self._kill_deads([character] + defense_description.all_fighters) return Description( title=f"Attaquer {with_character.name} seul", items=parts, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), classes=["primary"], ), ], ) def _proceed_events( self, attacker_title: str, attacked_title: str, characters: typing.List[CharacterModel], author: CharacterModel, story: typing.List[str], ) -> None: for character in characters: title = attacker_title if character == author else attacked_title read = character == author self._kernel.character_lib.add_event( character.id, title=title, read=read, story_pages=[ StoryPageDocument(text="\n".join([p for p in story])) ], ) def _get_attack_defense_pair( self, target: CharacterModel, as_affinity: AffinityDocument, world_row_i: int, world_col_i: int, ) -> typing.Tuple[AttackDescription, DefendDescription]: defense_description: DefendDescription = self._kernel.fight_lib.get_defense_description( origin_target=target, world_row_i=world_row_i, world_col_i=world_col_i, attacker_affinity=as_affinity, ) attack_description: AttackDescription = self._kernel.fight_lib.get_attack_description( target=defense_description, attacker=as_affinity, world_row_i=world_row_i, world_col_i=world_col_i, ) # remove attacker in conflict from defense defense_description.reduce_fighters(attack_description.all_fighters) defense_description.reduce_affinities([attack_description.affinity]) return attack_description, defense_description def _check_attack_as_affinity( self, character: "CharacterModel", with_character: "CharacterModel", as_affinity: AffinityDocument, attack_description: AttackDescription, defense_description: DefendDescription, ): title = f"Attaquer {with_character.name} en tant que {as_affinity.name}" here_url = self._get_here_url(character, with_character) character_relation = self._kernel.affinity_lib.get_active_relation( character_id=character.id, affinity_id=as_affinity.id) if character_relation.status_id not in (MEMBER_STATUS[0], WARLORD_STATUS[0]): raise ImpossibleAction( "Vous ne pouvez impliquer cette affinité qu'avec le role de Chef ou Chef de guerre" ) try: self._kernel.affinity_lib.get_active_relation( character_id=with_character.id, affinity_id=as_affinity.id) return Description( title=title, items=[ Part( text= f"Vous ne pouvez pas attaquer {with_character.name} " f"en tant que {as_affinity.name} car il/elle est affilié à " f"{as_affinity.name}") ], footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), ), Part(is_link=True, form_action=here_url, label="Retour", classes=["primary"]), ], ) except NoResultFound: pass parts = [] in_conflict_strs: typing.List[str] = [] for fighter in attack_description.all_fighters: if fighter.id in defense_description.helpers: affinities_str = ", ".join( [a.name for a in defense_description.helpers[fighter.id]]) in_conflict_strs.append( f"{fighter.name}, car affilié à: {affinities_str}") if in_conflict_strs: parts.append( Part( text= f"Le combat ne peut avoir lieu car des membres de votre parti ont des " f"affinités avec les defenseurs:")) for in_conflict_str in in_conflict_strs: parts.append(Part(text=f"- {in_conflict_str}")) return Description( title=title, items=parts, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), classes=["primary"], ), Part(is_link=True, form_action=here_url, label="Retour"), ], ) def _get_attack_as_affinity_description( self, character: "CharacterModel", with_character: "CharacterModel", as_affinity_id: int) -> Description: here_url = self._get_here_url(character, with_character) as_affinity = self._kernel.affinity_lib.get_affinity(as_affinity_id) title = f"Attaquer {with_character.name} en tant que {as_affinity.name}" attack_description, defense_description = self._get_attack_defense_pair( target=with_character, as_affinity=as_affinity, world_row_i=character.world_row_i, world_col_i=character.world_col_i, ) resp = self._check_attack_as_affinity( character=character, with_character=with_character, as_affinity=as_affinity, attack_description=attack_description, defense_description=defense_description, ) if resp: return resp defense_affinities_str = "" if defense_description.affinities: aff = ", ".join([f.name for f in defense_description.affinities]) defense_affinities_str = f" représenté(s) par le/les afinité(s): {aff}" defense_text = ( f"Le parti adverse compte " f"{len(defense_description.all_fighters)} combattant(s)" f"{defense_affinities_str}") attack_text = ( f"Votre parti est composé de {len(attack_description.all_fighters)} combattat(s) " f"dont {len(attack_description.ready_fighters)} en état de combattre" ) return Description( title=title, items=[ Part(text=attack_text), Part(text=defense_text), Part( is_link=True, form_action=here_url + f"&as_affinity={as_affinity_id}&confirm=1", label= f"Je confirme, attaquer {with_character.name} maintenant !", ), ], footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), ), Part(is_link=True, form_action=here_url, label=f"Retour", classes=["Primary"]), ], ) def _perform_attack_as_affinity(self, character: "CharacterModel", with_character: "CharacterModel", as_affinity_id: int) -> Description: as_affinity = self._kernel.affinity_lib.get_affinity(as_affinity_id) title = f"Attaquer {with_character.name} en tant que {as_affinity.name}" attack_description, defense_description = self._get_attack_defense_pair( target=with_character, as_affinity=as_affinity, world_row_i=character.world_row_i, world_col_i=character.world_col_i, ) resp = self._check_attack_as_affinity( character=character, with_character=with_character, as_affinity=as_affinity, attack_description=attack_description, defense_description=defense_description, ) if resp: return resp story = self._kernel.fight_lib.fight(attack=attack_description, defense=defense_description) parts = [Part(text=p) for p in story] self._proceed_events( attacker_title="Vous avez participé à une attaque", attacked_title="Vous avez subit une attaque", characters=attack_description.all_fighters + defense_description.all_fighters, author=character, story=story, ) self._kill_deads(attack_description.all_fighters + defense_description.all_fighters) return Description( title=title, items=parts, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Retourner à la fiche personnage", form_action=DESCRIBE_LOOK_AT_CHARACTER_URL.format( character_id=character.id, with_character_id=with_character.id), classes=["primary"], ), ], ) def perform(self, character: "CharacterModel", with_character: "CharacterModel", input_: AttackModel) -> Description: if input_.lonely is None and input_.as_affinity is None: return self._get_root_description(character, with_character) elif input_.lonely: if not input_.confirm: return self._get_attack_lonely_description( character, with_character) else: return self._perform_attack_lonely(character, with_character) elif input_.as_affinity: if not input_.confirm: return self._get_attack_as_affinity_description( character, with_character, as_affinity_id=input_.as_affinity) else: return self._perform_attack_as_affinity( character, with_character, as_affinity_id=input_.as_affinity) def _kill_deads(self, check_characters: typing.List[CharacterModel]) -> None: for check_character in check_characters: if check_character.life_points <= 0: self._kernel.character_lib.kill(check_character.id)
class FillStuffAction(WithStuffAction): input_model: typing.Type[ FillStuffWithResourceModel] = FillStuffWithResourceModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: for fill_acceptable_type in self._kernel.game.config.fill_with_material_ids: for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, material_type=fill_acceptable_type, ): return raise ImpossibleAction("Rien à proximité ne correspond") def check_request_is_possible(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> None: # TODO BS 2019-08-01: check owned self.check_is_possible(character, stuff) def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel") -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [] for fill_acceptable_type in self._kernel.game.config.fill_with_material_ids: for resource in self._kernel.game.world_manager.get_resource_on_or_around( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, material_type=fill_acceptable_type, ): query_params = self.input_model(resource_id=resource.id) actions.append( CharacterActionLink( name=f"Remplir {stuff.name} avec {resource.name}", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.FILL_STUFF, stuff_id=stuff.id, query_params=self.input_model_serializer.dump( query_params), action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), )) return actions def perform(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> Description: footer_links = [ Part( is_link=True, label="Voir l'inventaire", form_action=f"/_describe/character/{character.id}/inventory", ), Part( is_link=True, label="Voir l'objet", form_action=DESCRIBE_LOOK_AT_STUFF_URL.format( character_id=character.id, stuff_id=stuff.id), classes=["primary"], ), Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), ] try: self._kernel.stuff_lib.fill_stuff_with_resource( stuff, input_.resource_id) except CantFill as exc: return Description(title=str(exc), footer_links=footer_links) resource_description = self._kernel.game.config.resources[ input_.resource_id] return Description( title=f"{stuff.name} rempli(e) avec {resource_description.name}", footer_links=footer_links, )
class DropResourceAction(WithResourceAction): input_model: typing.Type[DropResourceModel] = DropResourceModel input_model_serializer = serpyco.Serializer(input_model) def check_is_possible(self, character: "CharacterModel", resource_id: str) -> None: if not self._kernel.resource_lib.have_resource(character.id, resource_id): raise ImpossibleAction("Vous ne possedez pas cette resource") def check_request_is_possible(self, character: "CharacterModel", resource_id: str, input_: input_model) -> None: if not self._kernel.resource_lib.have_resource( character.id, resource_id, quantity=input_.quantity): raise ImpossibleAction( "Vous ne possedez pas assez de cette resource") @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def get_character_actions( self, character: "CharacterModel", resource_id: str) -> typing.List[CharacterActionLink]: # TODO BS 2019-09-09: perfs carried_resources = self._kernel.resource_lib.get_carried_by( character.id) carried_resource = next( (r for r in carried_resources if r.id == resource_id)) actions: typing.List[CharacterActionLink] = [ CharacterActionLink( name=f"Laisser de {carried_resource.name} ici", link=get_with_resource_action_url( character_id=character.id, action_type=ActionType.DROP_RESOURCE, resource_id=carried_resource.id, query_params={}, action_description_id=self._description.id, ), cost=None, ) ] return actions def perform(self, character: "CharacterModel", resource_id: str, input_: input_model) -> Description: # TODO BS 2019-09-09: perfs carried_resources = self._kernel.resource_lib.get_carried_by( character.id) carried_resource = next( (r for r in carried_resources if r.id == resource_id)) if input_.quantity is None: unit_trans = self._kernel.translation.get(carried_resource.unit) return Description( title=carried_resource.get_full_description(self._kernel), items=[ Part( is_form=True, form_values_in_query=True, form_action=get_with_resource_action_url( character_id=character.id, action_type=ActionType.DROP_RESOURCE, resource_id=resource_id, query_params={}, action_description_id=self._description.id, ), items=[ Part( label= f"Quantité à laisser ici ({unit_trans}) ?", type_=Type.NUMBER, name="quantity", default_value=str(carried_resource.quantity), ) ], ) ], ) self._kernel.resource_lib.drop( character.id, resource_id, quantity=input_.quantity, world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, ) return Description( title=f"Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action= f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
class SearchFoodAction(CharacterAction): input_model = EmptyModel input_model_serializer = serpyco.Serializer(EmptyModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: for produce in action_config_raw["produce"]: if "resource" not in produce and "stuff" not in produce: raise RollingError( "Misconfiguration for action SearchFoodAction (production " "must contain stuff or resource key" ) properties = fill_base_action_properties(cls, game_config, {}, action_config_raw) properties.update({"produce": action_config_raw["produce"]}) return properties def check_is_possible(self, character: "CharacterModel") -> None: check_common_is_possible( kernel=self._kernel, description=self._description, character=character ) def check_request_is_possible(self, character: "CharacterModel", input_: typing.Any) -> None: self.check_is_possible(character) def get_character_actions( self, character: "CharacterModel" ) -> typing.List[CharacterActionLink]: try: self.check_is_possible(character) except ImpossibleAction: return [] return [ CharacterActionLink( name=self._description.name, link=get_character_action_url( character_id=character.id, action_type=ActionType.SEARCH_FOOD, action_description_id=self._description.id, query_params={}, ), group_name="Chercher de la nourriture", cost=self.get_cost(character), ) ] def perform(self, character: "CharacterModel", input_: typing.Any) -> Description: productions = self._description.properties["produce"] production_per_resource_ids: typing.Dict[str, dict] = {} production_per_stuff_ids: typing.Dict[str, dict] = {} zone_available_production_resource_ids: typing.List[str] = [] zone_available_production_stuff_ids: typing.List[str] = [] zone_state = self._kernel.game.world_manager.get_zone_state( world_row_i=character.world_row_i, world_col_i=character.world_col_i ) minimum_by_skill = 10 * character.get_skill_value(HUNTING_AND_GATHERING_SKILL_ID) for production in productions: if "resource" in production: resource_id = production["resource"] if zone_state.is_there_resource( resource_id, check_from_absolute=True, check_from_tiles=False ): zone_available_production_resource_ids.append(resource_id) production_per_resource_ids[resource_id] = production # FIXME BS: clarify "stuff" in hunt context elif "stuff" in production: stuff_id = production["stuff"] if zone_state.is_there_stuff(stuff_id): zone_available_production_stuff_ids.append(stuff_id) production_per_stuff_ids[stuff_id] = production else: raise NotImplementedError() # idée: Pour chaques resource/stuff faire une proba pour voir si on en trouve # pour chaque trouvé, faire une proba pour voir combien on en trouve # appliquer des facteurs de résussite à chaque fois. found_resource_ids: typing.List[str] = [] found_stuff_ids: typing.List[str] = [] for resource_id in zone_available_production_resource_ids: probability = production_per_resource_ids[resource_id]["probability"] probability += minimum_by_skill if random.randint(0, 100) <= probability: found_resource_ids.append(resource_id) for stuff_id in zone_available_production_stuff_ids: probability = production_per_stuff_ids[stuff_id]["probability"] probability += minimum_by_skill if random.randint(0, 100) <= probability: found_stuff_ids.append(stuff_id) result_resource_strs = [] for resource_id in found_resource_ids: resource_description = self._kernel.game.config.resources[resource_id] quantity_found_coeff = max(minimum_by_skill, random.randint(0, 100)) / 100 quantity_found = ( production_per_resource_ids[resource_id]["quantity"] * quantity_found_coeff ) if resource_description.unit == Unit.UNIT: quantity_found = round(quantity_found) if not quantity_found: continue unit_str = self._kernel.translation.get(resource_description.unit) result_resource_strs.append( f"{quantity_found} {unit_str} de {resource_description.name} " ) self._kernel.resource_lib.add_resource_to( character_id=character.id, resource_id=resource_id, quantity=quantity_found, commit=False, ) zone_state.reduce_resource(resource_id, quantity_found, commit=False) result_stuff_strs = [] for stuff_id in found_stuff_ids: stuff_properties = self._kernel.game.stuff_manager.get_stuff_properties_by_id(stuff_id) quantity_found_coeff = max(minimum_by_skill, random.randint(0, 100)) / 100 quantity_found = round( production_per_stuff_ids[stuff_id]["quantity"] * quantity_found_coeff ) if not quantity_found: continue result_stuff_strs.append(f"{quantity_found} de {stuff_properties.name} ") for i in range(quantity_found): stuff_doc = self._kernel.stuff_lib.create_document_from_properties( stuff_properties, stuff_id=stuff_id, world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_col_i=character.zone_row_i, zone_row_i=character.zone_col_i, ) stuff_doc.carried_by_id = character.id self._kernel.stuff_lib.add_stuff(stuff_doc, commit=False) zone_state.reduce_stuff(stuff_id, quantity_found, commit=False) self._kernel.character_lib.reduce_action_points( character.id, cost=self.get_cost(character, input_) ) self._kernel.server_db_session.commit() parts = [] for result_resource_str in result_resource_strs: parts.append(Part(text=result_resource_str)) for result_stuff_str in result_stuff_strs: parts.append(Part(text=result_stuff_str)) return Description( title="Vous avez trouvé", items=parts, footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements") ], )
class BeginStuffConstructionAction(CharacterAction): input_model = BeginStuffModel input_model_serializer = serpyco.Serializer(BeginStuffModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: for consume in action_config_raw["consume"]: if "resource" not in consume and "stuff" not in consume: raise RollingError(f"Action config is not correct: {action_config_raw}") properties = fill_base_action_properties(cls, game_config, {}, action_config_raw) properties["produce_stuff_id"] = action_config_raw["produce_stuff_id"] properties["consume"] = action_config_raw["consume"] properties["craft_ap"] = action_config_raw["craft_ap"] properties["default_description"] = action_config_raw.get("default_description", "") properties["link_group_name"] = action_config_raw.get("link_group_name", None) return properties def check_is_possible(self, character: "CharacterModel") -> None: pass # Always accept to display this action def check_request_is_possible( self, character: "CharacterModel", input_: BeginStuffModel ) -> None: self.check_is_possible(character) check_common_is_possible(self._kernel, description=self._description, character=character) cost = self.get_cost(character) if character.action_points < cost: raise ImpossibleAction( f"{character.name} no possède pas assez de points d'actions " f"({round(cost, 2)} nécessaires)" ) for consume in self._description.properties["consume"]: if "resource" in consume: resource_id = consume["resource"] resource_description = self._kernel.game.config.resources[resource_id] quantity = consume["quantity"] quantity_str = quantity_to_str(quantity, resource_description.unit, self._kernel) if not self._kernel.resource_lib.have_resource( character.id, resource_id=resource_id, quantity=quantity ): resource_description = self._kernel.game.config.resources[resource_id] raise ImpossibleAction( f"Vous ne possédez pas assez de {resource_description.name}: {quantity_str} nécessaire(s)" ) elif "stuff" in consume: stuff_id = consume["stuff"] quantity = consume["quantity"] if ( self._kernel.stuff_lib.have_stuff_count(character.id, stuff_id=stuff_id) < quantity ): stuff_properties = self._kernel.game.stuff_manager.get_stuff_properties_by_id( stuff_id ) raise ImpossibleAction( f"Vous ne possédez pas assez de {stuff_properties.name}: {quantity} nécessaire(s)" ) def get_character_actions( self, character: "CharacterModel" ) -> typing.List[CharacterActionLink]: return [ CharacterActionLink( name=f"Commencer {self._description.name}", link=get_character_action_url( character_id=character.id, action_type=ActionType.BEGIN_STUFF_CONSTRUCTION, action_description_id=self._description.id, query_params={}, ), cost=self.get_cost(character), group_name=self._description.properties["link_group_name"], ) ] def perform(self, character: "CharacterModel", input_: BeginStuffModel) -> Description: if not input_.description: require_txts = [] for consume in self._description.properties["consume"]: if "resource" in consume: resource_id = consume["resource"] resource_description = self._kernel.game.config.resources[resource_id] quantity_str = quantity_to_str( consume["quantity"], resource_description.unit, self._kernel ) require_txts.append(f"{quantity_str} de {resource_description.name}") elif "stuff" in consume: stuff_id = consume["stuff"] stuff_properties = self._kernel.game.stuff_manager.get_stuff_properties_by_id( stuff_id ) require_txts.append(f"{consume['quantity']} de {stuff_properties.name}") return Description( title=f"Commencer {self._description.name}", items=[ Part( is_form=True, form_values_in_query=True, form_action=get_character_action_url( character_id=character.id, action_type=ActionType.BEGIN_STUFF_CONSTRUCTION, query_params={}, action_description_id=self._description.id, ), items=[Part(text="Consommera :")] + [Part(text=txt) for txt in require_txts] + [ Part( label=f"Vous pouvez fournir une description de l'objet", type_=Type.STRING, name="description", default_value=self._description.properties["default_description"], ) ], ) ], ) for consume in self._description.properties["consume"]: if "resource" in consume: resource_id = consume["resource"] self._kernel.resource_lib.reduce_carried_by( character.id, resource_id=resource_id, quantity=consume["quantity"], commit=False, ) elif "stuff" in consume: stuff_id = consume["stuff"] carried_stuffs = self._kernel.stuff_lib.get_carried_by( character.id, stuff_id=stuff_id ) for i in range(consume["quantity"]): self._kernel.stuff_lib.destroy(carried_stuffs[i].id, commit=False) stuff_id = self._description.properties["produce_stuff_id"] stuff_properties = self._kernel.game.stuff_manager.get_stuff_properties_by_id(stuff_id) stuff_doc = self._kernel.stuff_lib.create_document_from_stuff_properties( properties=stuff_properties, world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, ) stuff_doc.description = input_.description or "" stuff_doc.carried_by_id = character.id stuff_doc.ap_spent = 0.0 stuff_doc.ap_required = self._description.properties["craft_ap"] stuff_doc.under_construction = True self._kernel.stuff_lib.add_stuff(stuff_doc, commit=False) self._kernel.character_lib.reduce_action_points( character.id, cost=self.get_cost(character), commit=False ) self._kernel.server_db_session.commit() return Description( title=f"{stuff_properties.name} commencé", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'objet commencé", form_action=DESCRIBE_LOOK_AT_STUFF_URL.format( character_id=character.id, stuff_id=stuff_doc.id ), classes=["primary"], ), ], force_back_url=f"/_describe/character/{character.id}/on_place_actions", )
class BuildAction(CharacterAction): input_model = BuildModel input_model_serializer = serpyco.Serializer(BuildModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: properties = fill_base_action_properties(cls, game_config, {}, action_config_raw) properties["build_id"] = action_config_raw["build"] return properties def check_is_possible(self, character: "CharacterModel") -> None: pass def check_request_is_possible(self, character: "CharacterModel", input_: BuildModel) -> None: check_common_is_possible( kernel=self._kernel, description=self._description, character=character ) build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] if character.action_points < self.get_cost(character, input_): raise ImpossibleAction("Pas assez de points d'actions") for require in build_description.build_require_resources: if not self._kernel.resource_lib.have_resource( character.id, resource_id=require.resource_id, quantity=require.quantity ): resource_properties = self._kernel.game.config.resources[require.resource_id] required_quantity_str = quantity_to_str( require.quantity, resource_properties.unit, self._kernel ) raise ImpossibleAction( f"Vous ne possedez pas assez de {resource_properties.name} " f"({required_quantity_str} requis)" ) def get_character_actions( self, character: "CharacterModel" ) -> typing.List[CharacterActionLink]: build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] return [ CharacterActionLink( name=build_description.name, link=self.get_base_url(character), cost=self.get_cost(character), ) ] def get_base_url(self, character: "CharacterModel") -> str: return get_character_action_url( character_id=character.id, action_type=ActionType.BUILD, action_description_id=self._description.id, query_params={}, ) def perform(self, character: "CharacterModel", input_: BuildModel) -> Description: build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] return Description( request_clicks=RequestClicks( base_url=self.get_base_url(character), cursor_classes=build_description.classes, many=build_description.many, ) ) def perform_from_event( self, character: "CharacterModel", input_: BuildModel ) -> typing.Tuple[typing.List[ZoneEvent], typing.List[ZoneEvent]]: assert input_.row_i assert input_.col_i build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] if self._kernel.build_lib.get_zone_build( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=input_.row_i, zone_col_i=input_.col_i, ): return [], [] build_doc = self._kernel.build_lib.place_build( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=input_.row_i, zone_col_i=input_.col_i, build_id=build_description.id, under_construction=False, commit=False, ) self._kernel.character_lib.reduce_action_points( character_id=character.id, cost=self.get_cost(character, input_), commit=False ) for require in build_description.build_require_resources: self._kernel.resource_lib.reduce_carried_by( character.id, resource_id=require.resource_id, quantity=require.quantity, commit=False, ) self._kernel.server_db_session.commit() return ( [ ZoneEvent( type=ZoneEventType.NEW_BUILD, data=NewBuildData( build=ZoneBuildModelContainer(doc=build_doc, desc=build_description) ), ) ], [ ZoneEvent( type=ZoneEventType.NEW_RESUME_TEXT, data=NewResumeTextData( resume=self._kernel.character_lib.get_resume_text(character.id) ), ) ], )
class EatStuffAction(WithStuffAction): input_model: typing.Type[EatStuffModel] = EatStuffModel input_model_serializer = serpyco.Serializer(input_model) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: a = 1 return { "accept_stuff_ids": action_config_raw["accept_stuffs"], "effects": [ game_config.character_effects[e] for e in action_config_raw["character_effects"] ], } def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: # TODO BS 2019-07-31: check is owned stuff if stuff.stuff_id in self._description.properties["accept_stuff_ids"]: return raise ImpossibleAction(f"Vous ne pouvez pas le manger") def check_request_is_possible(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> None: self.check_is_possible(character, stuff) def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel") -> typing.List[CharacterActionLink]: if stuff.stuff_id in self._description.properties["accept_stuff_ids"]: return [ CharacterActionLink( name=f"Manger {stuff.name}", link=get_with_stuff_action_url( character.id, ActionType.EAT_STUFF, query_params={}, stuff_id=stuff.id, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return [] def perform(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> Description: character_doc = self._character_lib.get_document(character.id) effects: typing.List[ CharacterEffectDescriptionModel] = self._description.properties[ "effects"] self._kernel.stuff_lib.destroy(stuff.id, commit=False) for effect in effects: self._effect_manager.enable_effect(character_doc, effect) self._kernel.server_db_session.add(character_doc) self._kernel.server_db_session.commit() return Description( title="Action effectué", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action= f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
return doc @pytest.fixture def worldmapc_web_app(worldmapc_kernel: Kernel, loop, aiohttp_client) -> TestClient: app = get_application(worldmapc_kernel) context = AiohttpContext(app, debug=True, default_error_builder=ErrorBuilder()) context.handle_exception(HTTPNotFound, http_code=404) context.handle_exception(Exception, http_code=500) hapic.reset_context() hapic.set_processor_class(RollingSerpycoProcessor) hapic.set_context(context) return loop.run_until_complete(aiohttp_client(app)) description_serializer = serpyco.Serializer(Description) @pytest.fixture(scope="session") def descr_serializer() -> serpyco.Serializer: return description_serializer @pytest.fixture def france_affinity(worldmapc_kernel: Kernel) -> AffinityDocument: doc = AffinityDocument(name="France") worldmapc_kernel.server_db_session.add(doc) worldmapc_kernel.server_db_session.commit() return doc
class BeginBuildAction(CharacterAction): input_model = EmptyModel input_model_serializer = serpyco.Serializer(EmptyModel) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return { "build_id": action_config_raw["build"], "require_resources": [ BuildRequireResourceDescription(resource_id=r["resource"], quantity=r["quantity"]) for r in action_config_raw.get("require_resources", []) ], } def check_is_possible(self, character: "CharacterModel") -> None: # TODO BS 2019-09-30: check is character have skill and stuff (but not resources # because we want to permit begin construction) pass def check_request_is_possible(self, character: "CharacterModel", input_: typing.Any) -> None: self.check_is_possible(character) def get_character_actions( self, character: "CharacterModel" ) -> typing.List[CharacterActionLink]: try: self.check_is_possible(character) except ImpossibleAction: pass build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] return [ CharacterActionLink( name=build_description.name, link=get_character_action_url( character_id=character.id, action_type=ActionType.BEGIN_BUILD, action_description_id=self._description.id, query_params={}, ), cost=self.get_cost(character, input_=None), ) ] def perform(self, character: "CharacterModel", input_: typing.Any) -> Description: build_id = self._description.properties["build_id"] build_description = self._kernel.game.config.builds[build_id] build_doc = self._kernel.build_lib.place_build( world_row_i=character.world_row_i, world_col_i=character.world_col_i, zone_row_i=character.zone_row_i, zone_col_i=character.zone_col_i, build_id=build_description.id, under_construction=True, ) self._kernel.character_lib.reduce_action_points( character_id=character.id, cost=self.get_cost(character, input_) ) return Description( title=f"{build_description.name} commencé", items=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( label="Voir le batiment", is_link=True, form_action=DESCRIBE_BUILD.format( build_id=build_doc.id, character_id=character.id ), ), ], force_back_url=f"/_describe/character/{character.id}/build_actions", )
class DropStuffAction(WithStuffAction): input_model: typing.Type[DropStuffModel] = DropStuffModel input_model_serializer = serpyco.Serializer(DropStuffModel) def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: if stuff.carried_by != character.id: raise ImpossibleAction("Vous ne possedez pas cet objet") def check_request_is_possible(self, character: "CharacterModel", stuff: "StuffModel", input_: input_model) -> None: self.check_is_possible(character, stuff) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return {} def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel") -> typing.List[CharacterActionLink]: actions: typing.List[CharacterActionLink] = [ CharacterActionLink( name=f"Laisser {stuff.name} ici", link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.DROP_STUFF, stuff_id=stuff.id, query_params={}, action_description_id=self._description.id, ), cost=self.get_cost(character, stuff), ) ] return actions def perform(self, character: "CharacterModel", stuff: "StuffModel", input_: DropStuffModel) -> Description: def do_for_one(character_: "CharacterModel", stuff_: "StuffModel", input__: DropStuffModel) -> typing.List[Part]: self._kernel.stuff_lib.drop( stuff_.id, world_row_i=character_.world_row_i, world_col_i=character_.world_col_i, zone_row_i=character_.zone_row_i, zone_col_i=character_.zone_col_i, ) return [Part(text=f"{stuff_.name} laissé ici")] return with_multiple_carried_stuffs( self, self._kernel, character=character, stuff=stuff, input_=input_, action_type=ActionType.DROP_STUFF, do_for_one_func=do_for_one, title="Laisser quelque-chose ici", success_parts=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action= f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], )
class CraftStuffWithStuffAction(WithStuffAction, BaseCraftStuff): input_model = CraftInput input_model_serializer = serpyco.Serializer(CraftInput) @classmethod def get_properties_from_config(cls, game_config: "GameConfig", action_config_raw: dict) -> dict: return cls._get_properties_from_config(game_config, action_config_raw) def check_is_possible(self, character: "CharacterModel", stuff: "StuffModel") -> None: # Consider action ca be possible (displayed in interface) if at least one of required stuff # is owned by character carried = self._kernel.stuff_lib.get_carried_by(character.id) carried_stuff_ids = [r.stuff_id for r in carried] for require in self._description.properties["require"]: if "stuff" in require and require["stuff"] in carried_stuff_ids: return raise ImpossibleAction("Aucune resource requise n'est possédé") def get_cost( self, character: "CharacterModel", stuff: "StuffModel", input_: typing.Optional[typing.Any] = None, ) -> typing.Optional[float]: bonus = character.get_skill_value("intelligence") + character.get_skill_value("crafts") base_cost = max(self._description.base_cost / 2, self._description.base_cost - bonus) if input_ and input_.quantity: return base_cost * input_.quantity return base_cost def check_request_is_possible( self, character: "CharacterModel", stuff: "StuffModel", input_: CraftInput ) -> None: self.check_is_possible(character, stuff=stuff) check_common_is_possible( kernel=self._kernel, description=self._description, character=character ) if input_.quantity is not None: cost = self.get_cost(character, stuff=stuff, input_=input_) self._perform( character, description=self._description, input_=input_, cost=cost, dry_run=True ) def get_character_actions( self, character: "CharacterModel", stuff: "StuffModel" ) -> typing.List[CharacterActionLink]: try: self.check_is_possible(character, stuff) except ImpossibleAction: return [] return [ # FIXME BS NOW: all CharacterActionLink must generate a can_be_back_url=True CharacterActionLink( name=self._description.name, link=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.CRAFT_STUFF_WITH_STUFF, action_description_id=self._description.id, stuff_id=stuff.id, query_params={}, ), cost=self.get_cost(character, stuff), ) ] def perform( self, character: "CharacterModel", stuff: "StuffModel", input_: CraftInput ) -> Description: if input_.quantity is None: return Description( title=self._description.name, items=[ Part( is_form=True, form_values_in_query=True, form_action=get_with_stuff_action_url( character_id=character.id, action_type=ActionType.CRAFT_STUFF_WITH_STUFF, stuff_id=stuff.id, query_params=self.input_model_serializer.dump(input_), action_description_id=self._description.id, ), items=[ Part(label=f"Quelle quantité ?", type_=Type.NUMBER, name="quantity") ], ) ], ) cost = self.get_cost(character, stuff=stuff, input_=input_) self._perform( character, description=self._description, input_=input_, cost=cost, dry_run=True ) self._perform( character, description=self._description, input_=input_, cost=cost, dry_run=False ) return Description( title="Action effectué avec succès", footer_links=[ Part(is_link=True, go_back_zone=True, label="Retourner à l'écran de déplacements"), Part( is_link=True, label="Voir l'inventaire", form_action=f"/_describe/character/{character.id}/inventory", classes=["primary"], ), ], force_back_url=f"/_describe/character/{character.id}/on_place_actions", )