Ejemplo n.º 1
0
 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)
Ejemplo n.º 2
0
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]
Ejemplo n.º 3
0
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"],
                ),
            ],
        )
Ejemplo n.º 4
0
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"],
                ),
            ],
        )
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
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)],
        )
Ejemplo n.º 8
0
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_)
Ejemplo n.º 9
0
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)
Ejemplo n.º 11
0
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"],
                ),
            ],
        )
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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",
        )
Ejemplo n.º 14
0
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"],
                ),
            ],
        )
Ejemplo n.º 15
0
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

Ejemplo n.º 17
0
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")
            ],
        )
Ejemplo n.º 18
0
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")
            ],
        )
Ejemplo n.º 19
0
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),
                ),
            ],
        )
Ejemplo n.º 20
0
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)
Ejemplo n.º 21
0
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,
        )
Ejemplo n.º 22
0
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"],
                ),
            ],
        )
Ejemplo n.º 23
0
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")
            ],
        )
Ejemplo n.º 24
0
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",
        )
Ejemplo n.º 25
0
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)
                    ),
                )
            ],
        )
Ejemplo n.º 26
0
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"],
                ),
            ],
        )
Ejemplo n.º 27
0
    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

Ejemplo n.º 28
0
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",
        )
Ejemplo n.º 29
0
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"],
                ),
            ],
        )
Ejemplo n.º 30
0
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",
        )