support for temporary and immediate/permanent effects
This commit is contained in:
parent
0859bca2fd
commit
2aaf531454
|
@ -14,8 +14,9 @@ from adventure.context import (
|
||||||
world_context,
|
world_context,
|
||||||
)
|
)
|
||||||
from adventure.generate import generate_item, generate_room, link_rooms
|
from adventure.generate import generate_item, generate_room, link_rooms
|
||||||
from adventure.utils.effect import apply_effect
|
from adventure.utils.effect import apply_effects
|
||||||
from adventure.utils.search import find_actor_in_room
|
from adventure.utils.search import find_actor_in_room
|
||||||
|
from adventure.utils.string import normalize_name
|
||||||
from adventure.utils.world import describe_actor, describe_entity
|
from adventure.utils.world import describe_actor, describe_entity
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -134,13 +135,13 @@ def action_use(item: str, target: str) -> str:
|
||||||
"Which effect should be applied? Specify the name of the effect to apply."
|
"Which effect should be applied? Specify the name of the effect to apply."
|
||||||
"Do not include the question or any JSON. Only include the name of the effect to apply."
|
"Do not include the question or any JSON. Only include the name of the effect to apply."
|
||||||
)
|
)
|
||||||
chosen_name = chosen_name.strip()
|
chosen_name = normalize_name(chosen_name)
|
||||||
|
|
||||||
chosen_effect = next(
|
chosen_effect = next(
|
||||||
(
|
(
|
||||||
search_effect
|
search_effect
|
||||||
for search_effect in action_item.effects
|
for search_effect in action_item.effects
|
||||||
if search_effect.name == chosen_name
|
if normalize_name(search_effect.name) == chosen_name
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
@ -148,7 +149,11 @@ def action_use(item: str, target: str) -> str:
|
||||||
# TODO: should retry the question if the effect is not found
|
# TODO: should retry the question if the effect is not found
|
||||||
return f"The {chosen_name} effect is not available to apply."
|
return f"The {chosen_name} effect is not available to apply."
|
||||||
|
|
||||||
apply_effect(chosen_effect, target_actor.attributes)
|
try:
|
||||||
|
apply_effects(target_actor, [chosen_effect])
|
||||||
|
except Exception:
|
||||||
|
logger.exception("error applying effect: %s", chosen_effect)
|
||||||
|
return f"There was a problem applying the {chosen_name} effect."
|
||||||
|
|
||||||
broadcast(
|
broadcast(
|
||||||
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
|
f"{action_actor.name} uses the {chosen_name} effect of {item} on {target}"
|
||||||
|
|
|
@ -4,22 +4,19 @@ from typing import List, Tuple
|
||||||
|
|
||||||
from packit.agent import Agent
|
from packit.agent import Agent
|
||||||
from packit.loops import loop_retry
|
from packit.loops import loop_retry
|
||||||
|
from packit.results import enum_result, int_result
|
||||||
from packit.utils import could_be_json
|
from packit.utils import could_be_json
|
||||||
|
|
||||||
from adventure.context import broadcast, set_current_world
|
from adventure.context import broadcast, set_current_world
|
||||||
from adventure.game_system import GameSystem
|
from adventure.game_system import GameSystem
|
||||||
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
from adventure.models.config import DEFAULT_CONFIG, WorldConfig
|
||||||
from adventure.models.entity import (
|
from adventure.models.effect import (
|
||||||
Actor,
|
EffectPattern,
|
||||||
Effect,
|
FloatEffectPattern,
|
||||||
Item,
|
IntEffectPattern,
|
||||||
NumberAttributeEffect,
|
StringEffectPattern,
|
||||||
Portal,
|
|
||||||
Room,
|
|
||||||
StringAttributeEffect,
|
|
||||||
World,
|
|
||||||
WorldEntity,
|
|
||||||
)
|
)
|
||||||
|
from adventure.models.entity import Actor, Item, Portal, Room, World, WorldEntity
|
||||||
from adventure.models.event import GenerateEvent
|
from adventure.models.event import GenerateEvent
|
||||||
from adventure.utils import try_parse_float, try_parse_int
|
from adventure.utils import try_parse_float, try_parse_int
|
||||||
from adventure.utils.search import (
|
from adventure.utils.search import (
|
||||||
|
@ -36,24 +33,6 @@ logger = getLogger(__name__)
|
||||||
|
|
||||||
world_config: WorldConfig = DEFAULT_CONFIG.world
|
world_config: WorldConfig = DEFAULT_CONFIG.world
|
||||||
|
|
||||||
PROMPT_TYPE_FRAGMENTS = {
|
|
||||||
"both": "Enter a positive or negative number, or a string value",
|
|
||||||
"number": "Enter a positive or negative number",
|
|
||||||
"string": "Enter a string value",
|
|
||||||
}
|
|
||||||
|
|
||||||
PROMPT_OPERATION_TYPES = {
|
|
||||||
"set": "both",
|
|
||||||
"add": "number",
|
|
||||||
"subtract": "number",
|
|
||||||
"multiply": "number",
|
|
||||||
"divide": "number",
|
|
||||||
"append": "string",
|
|
||||||
"prepend": "string",
|
|
||||||
}
|
|
||||||
|
|
||||||
OPERATIONS = list(PROMPT_OPERATION_TYPES.keys())
|
|
||||||
|
|
||||||
|
|
||||||
def duplicate_name_parser(existing_names: List[str]):
|
def duplicate_name_parser(existing_names: List[str]):
|
||||||
def name_parser(value: str, **kwargs):
|
def name_parser(value: str, **kwargs):
|
||||||
|
@ -369,7 +348,7 @@ def generate_actor(
|
||||||
return actor
|
return actor
|
||||||
|
|
||||||
|
|
||||||
def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
def generate_effect(agent: Agent, world: World, entity: Item) -> EffectPattern:
|
||||||
entity_type = entity.type
|
entity_type = entity.type
|
||||||
existing_effects = [effect.name for effect in entity.effects]
|
existing_effects = [effect.name for effect in entity.effects]
|
||||||
|
|
||||||
|
@ -405,65 +384,78 @@ def generate_effect(agent: Agent, world: World, entity: Item) -> Effect:
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def operation_parser(value: str, **kwargs):
|
|
||||||
if value not in OPERATIONS:
|
|
||||||
raise ValueError(
|
|
||||||
f'"{value}" is not a valid operation. Choose from: {OPERATIONS}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
attributes = []
|
attributes = []
|
||||||
for attribute_name in attribute_names.split(","):
|
for attribute_name in attribute_names.split(","):
|
||||||
attribute_name = normalize_name(attribute_name)
|
attribute_name = normalize_name(attribute_name)
|
||||||
if attribute_name:
|
if attribute_name:
|
||||||
operation = loop_retry(
|
|
||||||
agent,
|
|
||||||
f"How does the {name} effect modify the {attribute_name} attribute? "
|
|
||||||
"For example, 'heal' might 'add' to the 'health' attribute, while 'poison' might 'subtract' from it."
|
|
||||||
"Another example is 'writing' might 'set' the 'text' attribute, while 'break' might 'set' the 'condition' attribute."
|
|
||||||
"Reply with the operation only, without any other text. Respond with a single word for the list of operations."
|
|
||||||
"Choose from the following operations: {operations}",
|
|
||||||
context={
|
|
||||||
"name": name,
|
|
||||||
"attribute_name": attribute_name,
|
|
||||||
"operations": OPERATIONS,
|
|
||||||
},
|
|
||||||
result_parser=operation_parser,
|
|
||||||
toolbox=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
operation_type = PROMPT_OPERATION_TYPES[operation]
|
|
||||||
operation_prompt = PROMPT_TYPE_FRAGMENTS[operation_type]
|
|
||||||
|
|
||||||
value = agent(
|
value = agent(
|
||||||
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
f"How much does the {name} effect modify the {attribute_name} attribute? "
|
||||||
"For example, heal might add '10' to the health attribute, while poison might subtract '5' from it."
|
"For example, heal might add 10 to the health attribute, while poison might remove -5 from it."
|
||||||
f"{operation_prompt}. Do not include any other text. Do not use JSON.",
|
"Enter a positive number to increase the attribute or a negative number to decrease it. "
|
||||||
|
"Do not include any other text. Do not use JSON.",
|
||||||
name=name,
|
name=name,
|
||||||
attribute_name=attribute_name,
|
attribute_name=attribute_name,
|
||||||
)
|
)
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
|
|
||||||
|
# TODO: support more than just set: offset and multiply
|
||||||
int_value = try_parse_int(value)
|
int_value = try_parse_int(value)
|
||||||
if int_value is not None:
|
if int_value is not None:
|
||||||
attribute_effect = NumberAttributeEffect(
|
attribute_effect = IntEffectPattern(name=attribute_name, set=int_value)
|
||||||
name=attribute_name, operation=operation, value=int_value
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
float_value = try_parse_float(value)
|
float_value = try_parse_float(value)
|
||||||
if float_value is not None:
|
if float_value is not None:
|
||||||
attribute_effect = NumberAttributeEffect(
|
attribute_effect = FloatEffectPattern(
|
||||||
name=attribute_name, operation=operation, value=float_value
|
name=attribute_name, set=float_value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
attribute_effect = StringAttributeEffect(
|
attribute_effect = StringEffectPattern(
|
||||||
name=attribute_name, operation=operation, value=value
|
name=attribute_name, set=value
|
||||||
)
|
)
|
||||||
|
|
||||||
attributes.append(attribute_effect)
|
attributes.append(attribute_effect)
|
||||||
|
|
||||||
return Effect(name=name, description=description, attributes=attributes)
|
duration = loop_retry(
|
||||||
|
agent,
|
||||||
|
f"How many turns does the {name} effect last? Enter a positive number to set a duration, or 0 for an instant effect. "
|
||||||
|
"Do not include any other text. Do not use JSON.",
|
||||||
|
context={
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
result_parser=int_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_application(value: str, **kwargs) -> str:
|
||||||
|
value = enum_result(value, ["temporary", "permanent"])
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
|
||||||
|
raise ValueError("The application must be 'temporary' or 'permanent'.")
|
||||||
|
|
||||||
|
application = loop_retry(
|
||||||
|
agent,
|
||||||
|
(
|
||||||
|
f"How should the {name} effect be applied? Respond with 'temporary' for a temporary effect that lasts for a duration, "
|
||||||
|
"or 'permanent' for a permanent effect that immediately modifies the target. "
|
||||||
|
"For example, a healing potion would be a permanent effect that increases health every turn, "
|
||||||
|
"while bleeding would be a temporary effect that decreases health every turn. "
|
||||||
|
"A haste potion would be a temporary effect that increases speed for a duration, "
|
||||||
|
"while a slow spell would be a temporary effect that decreases speed for a duration. "
|
||||||
|
"Do not include any other text. Do not use JSON."
|
||||||
|
),
|
||||||
|
context={
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
result_parser=parse_application,
|
||||||
|
)
|
||||||
|
|
||||||
|
return EffectPattern(
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
application,
|
||||||
|
duration=duration,
|
||||||
|
attributes=attributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def link_rooms(
|
def link_rooms(
|
||||||
|
|
|
@ -173,7 +173,10 @@ def load_logic(filename: str):
|
||||||
for rule in logic_rules.rules:
|
for rule in logic_rules.rules:
|
||||||
if rule.trigger:
|
if rule.trigger:
|
||||||
for trigger in rule.trigger:
|
for trigger in rule.trigger:
|
||||||
logic_triggers[trigger] = get_plugin_function(trigger)
|
function_name = (
|
||||||
|
trigger if isinstance(trigger, str) else trigger.function
|
||||||
|
)
|
||||||
|
logic_triggers[trigger] = get_plugin_function(function_name)
|
||||||
|
|
||||||
logger.info("initialized logic system")
|
logger.info("initialized logic system")
|
||||||
system_simulate = wraps(update_logic)(
|
system_simulate = wraps(update_logic)(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -7,6 +7,10 @@ else:
|
||||||
from pydantic.dataclasses import dataclass as dataclass # noqa
|
from pydantic.dataclasses import dataclass as dataclass # noqa
|
||||||
|
|
||||||
|
|
||||||
|
AttributeValue = bool | float | int | str
|
||||||
|
Attributes = Dict[str, AttributeValue]
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
type: str
|
type: str
|
||||||
id: str
|
id: str
|
||||||
|
@ -14,3 +18,17 @@ class BaseModel:
|
||||||
|
|
||||||
def uuid() -> str:
|
def uuid() -> str:
|
||||||
return uuid4().hex
|
return uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FloatRange:
|
||||||
|
min: float
|
||||||
|
max: float
|
||||||
|
interval: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntRange:
|
||||||
|
min: int
|
||||||
|
max: int
|
||||||
|
interval: int = 1
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from .base import dataclass
|
from .base import IntRange, dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Range:
|
|
||||||
min: int
|
|
||||||
max: int
|
|
||||||
interval: int = 1
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -29,11 +22,11 @@ class BotConfig:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RenderConfig:
|
class RenderConfig:
|
||||||
cfg: Range
|
cfg: IntRange
|
||||||
checkpoints: List[str]
|
checkpoints: List[str]
|
||||||
path: str
|
path: str
|
||||||
sizes: Dict[str, Size]
|
sizes: Dict[str, Size]
|
||||||
steps: Range
|
steps: IntRange
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -49,12 +42,12 @@ class ServerConfig:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WorldSizeConfig:
|
class WorldSizeConfig:
|
||||||
actor_items: Range
|
actor_items: IntRange
|
||||||
item_effects: Range
|
item_effects: IntRange
|
||||||
portals: Range
|
portals: IntRange
|
||||||
room_actors: Range
|
room_actors: IntRange
|
||||||
room_items: Range
|
room_items: IntRange
|
||||||
rooms: Range
|
rooms: IntRange
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -73,7 +66,7 @@ class Config:
|
||||||
DEFAULT_CONFIG = Config(
|
DEFAULT_CONFIG = Config(
|
||||||
bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])),
|
bot=BotConfig(discord=DiscordBotConfig(channels=["adventure"])),
|
||||||
render=RenderConfig(
|
render=RenderConfig(
|
||||||
cfg=Range(min=5, max=8),
|
cfg=IntRange(min=5, max=8),
|
||||||
checkpoints=[
|
checkpoints=[
|
||||||
"diffusion-sdxl-dynavision-0-5-5-7.safetensors",
|
"diffusion-sdxl-dynavision-0-5-5-7.safetensors",
|
||||||
],
|
],
|
||||||
|
@ -83,17 +76,17 @@ DEFAULT_CONFIG = Config(
|
||||||
"portrait": Size(width=768, height=1024),
|
"portrait": Size(width=768, height=1024),
|
||||||
"square": Size(width=768, height=768),
|
"square": Size(width=768, height=768),
|
||||||
},
|
},
|
||||||
steps=Range(min=30, max=30),
|
steps=IntRange(min=30, max=30),
|
||||||
),
|
),
|
||||||
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
server=ServerConfig(websocket=WebsocketServerConfig(host="localhost", port=8001)),
|
||||||
world=WorldConfig(
|
world=WorldConfig(
|
||||||
size=WorldSizeConfig(
|
size=WorldSizeConfig(
|
||||||
actor_items=Range(min=0, max=2),
|
actor_items=IntRange(min=0, max=2),
|
||||||
item_effects=Range(min=1, max=2),
|
item_effects=IntRange(min=1, max=2),
|
||||||
portals=Range(min=1, max=3),
|
portals=IntRange(min=1, max=3),
|
||||||
rooms=Range(min=3, max=6),
|
rooms=IntRange(min=3, max=6),
|
||||||
room_actors=Range(min=1, max=3),
|
room_actors=IntRange(min=1, max=3),
|
||||||
room_items=Range(min=1, max=3),
|
room_items=IntRange(min=1, max=3),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
from typing import List, Literal
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from .base import FloatRange, IntRange, dataclass, uuid
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StringEffectPattern:
|
||||||
|
name: str
|
||||||
|
append: str | List[str] | None = None
|
||||||
|
prepend: str | List[str] | None = None
|
||||||
|
set: str | List[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FloatEffectPattern:
|
||||||
|
name: str
|
||||||
|
set: float | FloatRange | None = None
|
||||||
|
offset: float | FloatRange | None = None
|
||||||
|
multiply: float | FloatRange | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntEffectPattern:
|
||||||
|
name: str
|
||||||
|
set: int | IntRange | None = None
|
||||||
|
offset: int | IntRange | None = None
|
||||||
|
multiply: float | FloatRange | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BooleanEffectPattern:
|
||||||
|
name: str
|
||||||
|
set: bool | None = None
|
||||||
|
toggle: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
AttributeEffectPattern = (
|
||||||
|
StringEffectPattern | FloatEffectPattern | IntEffectPattern | BooleanEffectPattern
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EffectPattern:
|
||||||
|
"""
|
||||||
|
TODO: should this be an EffectTemplate?
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
application: Literal["permanent", "temporary"]
|
||||||
|
duration: int | IntRange | None = None
|
||||||
|
attributes: List[AttributeEffectPattern] = Field(default_factory=list)
|
||||||
|
id: str = Field(default_factory=uuid)
|
||||||
|
type: Literal["effect_pattern"] = "effect_pattern"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BooleanEffectResult:
|
||||||
|
name: str
|
||||||
|
set: bool | None = None
|
||||||
|
toggle: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FloatEffectResult:
|
||||||
|
name: str
|
||||||
|
set: float | None = None
|
||||||
|
offset: float | None = None
|
||||||
|
multiply: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntEffectResult:
|
||||||
|
name: str
|
||||||
|
set: int | None = None
|
||||||
|
offset: int | None = None
|
||||||
|
multiply: float | None = None # still needs to be a float for decimals/division
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StringEffectResult:
|
||||||
|
name: str
|
||||||
|
append: str | None = None
|
||||||
|
prepend: str | None = None
|
||||||
|
set: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
AttributeEffectResult = (
|
||||||
|
BooleanEffectResult | FloatEffectResult | IntEffectResult | StringEffectResult
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EffectResult:
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
duration: int | None = None
|
||||||
|
attributes: List[AttributeEffectResult] = Field(default_factory=list)
|
||||||
|
id: str = Field(default_factory=uuid)
|
||||||
|
type: Literal["effect_result"] = "effect_result"
|
|
@ -2,45 +2,10 @@ from typing import Callable, Dict, List, Literal
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from .base import BaseModel, dataclass, uuid
|
from .base import Attributes, BaseModel, dataclass, uuid
|
||||||
|
from .effect import EffectPattern, EffectResult
|
||||||
|
|
||||||
Actions = Dict[str, Callable]
|
Actions = Dict[str, Callable]
|
||||||
AttributeValue = bool | float | int | str
|
|
||||||
Attributes = Dict[str, AttributeValue]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StringAttributeEffect:
|
|
||||||
name: str
|
|
||||||
operation: Literal["set", "append", "prepend"]
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NumberAttributeEffect:
|
|
||||||
name: str
|
|
||||||
operation: Literal["set", "add", "subtract", "multiply", "divide"]
|
|
||||||
# TODO: make this a range
|
|
||||||
value: int | float
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BooleanAttributeEffect:
|
|
||||||
name: str
|
|
||||||
operation: Literal["set", "toggle"]
|
|
||||||
value: bool
|
|
||||||
|
|
||||||
|
|
||||||
AttributeEffect = StringAttributeEffect | NumberAttributeEffect | BooleanAttributeEffect
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Effect(BaseModel):
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
attributes: list[AttributeEffect] = Field(default_factory=list)
|
|
||||||
id: str = Field(default_factory=uuid)
|
|
||||||
type: Literal["effect"] = "effect"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -48,8 +13,9 @@ class Item(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
effects: List[Effect] = Field(default_factory=list)
|
effects: List[EffectPattern] = Field(default_factory=list)
|
||||||
items: List["Item"] = Field(default_factory=list)
|
items: List["Item"] = Field(default_factory=list)
|
||||||
id: str = Field(default_factory=uuid)
|
id: str = Field(default_factory=uuid)
|
||||||
type: Literal["item"] = "item"
|
type: Literal["item"] = "item"
|
||||||
|
@ -61,6 +27,7 @@ class Actor(BaseModel):
|
||||||
backstory: str
|
backstory: str
|
||||||
description: str
|
description: str
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
items: List[Item] = Field(default_factory=list)
|
items: List[Item] = Field(default_factory=list)
|
||||||
id: str = Field(default_factory=uuid)
|
id: str = Field(default_factory=uuid)
|
||||||
|
@ -84,6 +51,7 @@ class Room(BaseModel):
|
||||||
description: str
|
description: str
|
||||||
actors: List[Actor] = Field(default_factory=list)
|
actors: List[Actor] = Field(default_factory=list)
|
||||||
actions: Actions = Field(default_factory=dict)
|
actions: Actions = Field(default_factory=dict)
|
||||||
|
active_effects: List[EffectResult] = Field(default_factory=list)
|
||||||
attributes: Attributes = Field(default_factory=dict)
|
attributes: Attributes = Field(default_factory=dict)
|
||||||
items: List[Item] = Field(default_factory=list)
|
items: List[Item] = Field(default_factory=list)
|
||||||
portals: List[Portal] = Field(default_factory=list)
|
portals: List[Portal] = Field(default_factory=list)
|
||||||
|
|
|
@ -117,7 +117,7 @@ def scene_from_event(event: GameEvent) -> str | None:
|
||||||
def scene_from_entity(entity: WorldEntity) -> str:
|
def scene_from_entity(entity: WorldEntity) -> str:
|
||||||
logger.debug("generating scene from entity: %s", entity)
|
logger.debug("generating scene from entity: %s", entity)
|
||||||
|
|
||||||
return f"Describe the {entity.type} called {entity.name}. {describe_entity(entity)}"
|
return f"Describe the {entity.type} named {entity.name} in vivid, visual terms. {describe_entity(entity)}"
|
||||||
|
|
||||||
|
|
||||||
def make_example_prompts(keywords: List[str], k=5, q=10) -> List[str]:
|
def make_example_prompts(keywords: List[str], k=5, q=10) -> List[str]:
|
||||||
|
@ -162,6 +162,7 @@ def generate_prompt_from_scene(scene: str, example_prompts: List[str]) -> str:
|
||||||
"Reply with a comma-separated list of keywords that summarize the visual details of the scene."
|
"Reply with a comma-separated list of keywords that summarize the visual details of the scene."
|
||||||
"Make sure you describe the location, all of the characters, and any items present using keywords and phrases. "
|
"Make sure you describe the location, all of the characters, and any items present using keywords and phrases. "
|
||||||
"Be creative with the details. Avoid using proper nouns or character names. Describe any actions being taken. "
|
"Be creative with the details. Avoid using proper nouns or character names. Describe any actions being taken. "
|
||||||
|
"Describe the characters first, then the location, then the other visual details and general atmosphere. "
|
||||||
"Do not include the question or any JSON. Only include the list of keywords on a single line.",
|
"Do not include the question or any JSON. Only include the list of keywords on a single line.",
|
||||||
examples=example_prompts,
|
examples=example_prompts,
|
||||||
scene=scene,
|
scene=scene,
|
||||||
|
|
|
@ -31,6 +31,7 @@ from adventure.context import (
|
||||||
from adventure.game_system import GameSystem
|
from adventure.game_system import GameSystem
|
||||||
from adventure.models.entity import World
|
from adventure.models.entity import World
|
||||||
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
|
from adventure.models.event import ActionEvent, ReplyEvent, ResultEvent
|
||||||
|
from adventure.utils.effect import is_active_effect
|
||||||
from adventure.utils.search import find_room_with_actor
|
from adventure.utils.search import find_room_with_actor
|
||||||
from adventure.utils.world import describe_entity, format_attributes
|
from adventure.utils.world import describe_entity, format_attributes
|
||||||
|
|
||||||
|
@ -97,6 +98,16 @@ def simulate_world(
|
||||||
logger.error(f"Actor {actor_name} is not in a room")
|
logger.error(f"Actor {actor_name} is not in a room")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# decrement effects on the actor and remove any that have expired
|
||||||
|
for effect in actor.active_effects:
|
||||||
|
if effect.duration is not None:
|
||||||
|
effect.duration -= 1
|
||||||
|
|
||||||
|
actor.active_effects[:] = [
|
||||||
|
effect for effect in actor.active_effects if is_active_effect(effect)
|
||||||
|
]
|
||||||
|
|
||||||
|
# collect data for the prompt
|
||||||
room_actors = [actor.name for actor in room.actors]
|
room_actors = [actor.name for actor in room.actors]
|
||||||
room_items = [item.name for item in room.items]
|
room_items = [item.name for item in room.items]
|
||||||
room_directions = [portal.name for portal in room.portals]
|
room_directions = [portal.name for portal in room.portals]
|
||||||
|
@ -104,6 +115,7 @@ def simulate_world(
|
||||||
actor_attributes = format_attributes(actor)
|
actor_attributes = format_attributes(actor)
|
||||||
actor_items = [item.name for item in actor.items]
|
actor_items = [item.name for item in actor.items]
|
||||||
|
|
||||||
|
# set up a result parser for the agent
|
||||||
def result_parser(value, agent, **kwargs):
|
def result_parser(value, agent, **kwargs):
|
||||||
if not room or not actor:
|
if not room or not actor:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -119,6 +131,7 @@ def simulate_world(
|
||||||
|
|
||||||
return world_result_parser(value, agent, **kwargs)
|
return world_result_parser(value, agent, **kwargs)
|
||||||
|
|
||||||
|
# prompt and act
|
||||||
logger.info("starting turn for actor: %s", actor_name)
|
logger.info("starting turn for actor: %s", actor_name)
|
||||||
result = loop_retry(
|
result = loop_retry(
|
||||||
agent,
|
agent,
|
||||||
|
|
|
@ -22,14 +22,14 @@ rules:
|
||||||
type: room
|
type: room
|
||||||
temperature: hot
|
temperature: hot
|
||||||
chance: 0.2
|
chance: 0.2
|
||||||
trigger: [adventure.sim_systems.environment_triggers:hot_room]
|
trigger: [adventure.systems.sim.environment_triggers:hot_room]
|
||||||
|
|
||||||
- group: environment-temperature
|
- group: environment-temperature
|
||||||
match:
|
match:
|
||||||
type: room
|
type: room
|
||||||
temperature: cold
|
temperature: cold
|
||||||
chance: 0.2
|
chance: 0.2
|
||||||
trigger: [adventure.sim_systems.environment_triggers:cold_room]
|
trigger: [adventure.systems.sim.environment_triggers:cold_room]
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- match:
|
- match:
|
||||||
|
|
|
@ -1,4 +1,38 @@
|
||||||
from adventure.models.entity import Attributes, AttributeValue
|
from adventure.models.base import Attributes, AttributeValue
|
||||||
|
|
||||||
|
|
||||||
|
def add_value(value: AttributeValue, offset: int | float) -> AttributeValue:
|
||||||
|
"""
|
||||||
|
Add an offset to a value.
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
raise ValueError(f"Cannot add a number to a string attribute: {value}")
|
||||||
|
|
||||||
|
return value + offset
|
||||||
|
|
||||||
|
|
||||||
|
def multiply_value(value: AttributeValue, factor: int | float) -> AttributeValue:
|
||||||
|
"""
|
||||||
|
Multiply a value by a factor.
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
raise ValueError(f"Cannot multiply a string attribute: {value}")
|
||||||
|
|
||||||
|
return value * factor
|
||||||
|
|
||||||
|
|
||||||
|
def append_value(value: AttributeValue, suffix: str) -> str:
|
||||||
|
"""
|
||||||
|
Append a suffix to a string.
|
||||||
|
"""
|
||||||
|
return str(value) + suffix
|
||||||
|
|
||||||
|
|
||||||
|
def prepend_value(value: AttributeValue, prefix: str) -> str:
|
||||||
|
"""
|
||||||
|
Prepend a prefix to a string.
|
||||||
|
"""
|
||||||
|
return prefix + str(value)
|
||||||
|
|
||||||
|
|
||||||
def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
|
def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attributes:
|
||||||
|
@ -7,10 +41,7 @@ def add_attribute(attributes: Attributes, name: str, value: int | float) -> Attr
|
||||||
"""
|
"""
|
||||||
if name in attributes:
|
if name in attributes:
|
||||||
previous_value = attributes[name]
|
previous_value = attributes[name]
|
||||||
if isinstance(previous_value, str):
|
attributes[name] = add_value(previous_value, value)
|
||||||
raise ValueError(f"Cannot add a number to a string attribute: {name}")
|
|
||||||
|
|
||||||
attributes[name] = value + previous_value
|
|
||||||
else:
|
else:
|
||||||
attributes[name] = value
|
attributes[name] = value
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,314 @@
|
||||||
|
import random
|
||||||
|
from logging import getLogger
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from adventure.models.entity import Attributes, Effect
|
from adventure.models.base import FloatRange, IntRange
|
||||||
|
from adventure.models.effect import (
|
||||||
|
BooleanEffectPattern,
|
||||||
|
BooleanEffectResult,
|
||||||
|
EffectPattern,
|
||||||
|
EffectResult,
|
||||||
|
FloatEffectPattern,
|
||||||
|
FloatEffectResult,
|
||||||
|
IntEffectPattern,
|
||||||
|
IntEffectResult,
|
||||||
|
StringEffectPattern,
|
||||||
|
StringEffectResult,
|
||||||
|
)
|
||||||
|
from adventure.models.entity import Actor, Attributes
|
||||||
from adventure.utils.attribute import (
|
from adventure.utils.attribute import (
|
||||||
add_attribute,
|
add_value,
|
||||||
append_attribute,
|
append_value,
|
||||||
divide_attribute,
|
multiply_value,
|
||||||
multiply_attribute,
|
prepend_value,
|
||||||
prepend_attribute,
|
|
||||||
set_attribute,
|
|
||||||
subtract_attribute,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
def apply_effect(effect: Effect, attributes: Attributes) -> Attributes:
|
|
||||||
|
def effective_boolean(attributes: Attributes, effect: BooleanEffectResult) -> bool:
|
||||||
|
"""
|
||||||
|
Apply a boolean effect to a value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if effect.set is not None:
|
||||||
|
return effect.set
|
||||||
|
|
||||||
|
value = attributes.get(effect.name, False)
|
||||||
|
|
||||||
|
if effect.toggle is not None:
|
||||||
|
return not value
|
||||||
|
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
|
def effective_float(attributes: Attributes, effect: FloatEffectResult) -> float:
|
||||||
|
"""
|
||||||
|
Apply a float effect to a value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if effect.set is not None:
|
||||||
|
return effect.set
|
||||||
|
|
||||||
|
value = attributes.get(effect.name, 0.0)
|
||||||
|
|
||||||
|
if effect.offset is not None:
|
||||||
|
value = add_value(value, effect.offset)
|
||||||
|
|
||||||
|
if effect.multiply is not None:
|
||||||
|
value = multiply_value(value, effect.multiply)
|
||||||
|
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def effective_int(attributes: Attributes, effect: IntEffectResult) -> int:
|
||||||
|
"""
|
||||||
|
Apply an integer effect to a value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if effect.set is not None:
|
||||||
|
return effect.set
|
||||||
|
|
||||||
|
value = attributes.get(effect.name, 0)
|
||||||
|
|
||||||
|
if effect.offset is not None:
|
||||||
|
value = add_value(value, effect.offset)
|
||||||
|
|
||||||
|
if effect.multiply is not None:
|
||||||
|
value = multiply_value(value, effect.multiply)
|
||||||
|
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
|
def effective_string(attributes: Attributes, effect: StringEffectResult) -> str:
|
||||||
|
"""
|
||||||
|
Apply a string effect to a value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if effect.set:
|
||||||
|
return effect.set
|
||||||
|
|
||||||
|
value = attributes.get(effect.name, "")
|
||||||
|
|
||||||
|
if effect.append:
|
||||||
|
value = append_value(value, effect.append)
|
||||||
|
|
||||||
|
if effect.prepend:
|
||||||
|
value = prepend_value(value, effect.prepend)
|
||||||
|
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def effective_attributes(
|
||||||
|
effects: List[EffectResult], base_attributes: Attributes
|
||||||
|
) -> Attributes:
|
||||||
"""
|
"""
|
||||||
Apply an effect to a set of attributes.
|
Apply an effect to a set of attributes.
|
||||||
"""
|
"""
|
||||||
for attribute in effect.attributes:
|
|
||||||
if attribute.operation == "set":
|
|
||||||
set_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "add":
|
|
||||||
add_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "subtract":
|
|
||||||
subtract_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "multiply":
|
|
||||||
multiply_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "divide":
|
|
||||||
divide_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "append":
|
|
||||||
append_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
elif attribute.operation == "prepend":
|
|
||||||
prepend_attribute(attributes, attribute.name, attribute.value)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid operation: {attribute.operation}")
|
|
||||||
|
|
||||||
return attributes
|
attributes = base_attributes.copy()
|
||||||
|
|
||||||
|
|
||||||
def apply_effects(effects: List[Effect], attributes: Attributes) -> Attributes:
|
|
||||||
"""
|
|
||||||
Apply a list of effects to a set of attributes.
|
|
||||||
"""
|
|
||||||
for effect in effects:
|
for effect in effects:
|
||||||
attributes = apply_effect(effect, attributes)
|
for attribute in effect.attributes:
|
||||||
|
if isinstance(attribute, BooleanEffectResult):
|
||||||
|
attributes[attribute.name] = effective_boolean(attributes, attribute)
|
||||||
|
elif isinstance(attribute, FloatEffectResult):
|
||||||
|
attributes[attribute.name] = effective_float(attributes, attribute)
|
||||||
|
elif isinstance(attribute, IntEffectResult):
|
||||||
|
attributes[attribute.name] = effective_int(attributes, attribute)
|
||||||
|
elif isinstance(attribute, StringEffectResult):
|
||||||
|
attributes[attribute.name] = effective_string(attributes, attribute)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_float_range(range: float | FloatRange | None) -> float | None:
|
||||||
|
"""
|
||||||
|
Resolve a float range to a single value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if range is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(
|
||||||
|
range, (float, int)
|
||||||
|
): # int is not really necessary here, but mypy complains without it
|
||||||
|
return range
|
||||||
|
|
||||||
|
return random.uniform(range.min, range.max)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_int_range(range: int | IntRange | None) -> int | None:
|
||||||
|
"""
|
||||||
|
Resolve an integer range to a single value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if range is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(range, int):
|
||||||
|
return range
|
||||||
|
|
||||||
|
return random.randint(range.min, range.max)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_string_list(result: str | List[str] | None) -> str | None:
|
||||||
|
"""
|
||||||
|
Resolve a string result to a single value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(result, str):
|
||||||
|
return result
|
||||||
|
|
||||||
|
return random.choice(result)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_boolean_effect(effect: BooleanEffectPattern) -> BooleanEffectResult:
|
||||||
|
"""
|
||||||
|
Apply a boolean effect pattern to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return BooleanEffectResult(
|
||||||
|
name=effect.name,
|
||||||
|
set=effect.set,
|
||||||
|
toggle=effect.toggle,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_float_effect(effect: FloatEffectPattern) -> FloatEffectResult:
|
||||||
|
"""
|
||||||
|
Apply a float effect pattern to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return FloatEffectResult(
|
||||||
|
name=effect.name,
|
||||||
|
set=resolve_float_range(effect.set),
|
||||||
|
offset=resolve_float_range(effect.offset),
|
||||||
|
multiply=resolve_float_range(effect.multiply),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_int_effect(effect: IntEffectPattern) -> IntEffectResult:
|
||||||
|
"""
|
||||||
|
Apply an integer effect pattern to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return IntEffectResult(
|
||||||
|
name=effect.name,
|
||||||
|
set=resolve_int_range(effect.set),
|
||||||
|
offset=resolve_int_range(effect.offset),
|
||||||
|
multiply=resolve_float_range(effect.multiply),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_string_effect(effect: StringEffectPattern) -> StringEffectResult:
|
||||||
|
"""
|
||||||
|
Apply a string effect pattern to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return StringEffectResult(
|
||||||
|
name=effect.name,
|
||||||
|
set=resolve_string_list(effect.set),
|
||||||
|
append=resolve_string_list(effect.append),
|
||||||
|
prepend=resolve_string_list(effect.prepend),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_effects(effects: List[EffectPattern]) -> List[EffectResult]:
|
||||||
|
"""
|
||||||
|
Generate results for a set of effect patterns, rolling all of the random values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for effect in effects:
|
||||||
|
attributes = []
|
||||||
|
for attribute in effect.attributes:
|
||||||
|
if isinstance(attribute, BooleanEffectPattern):
|
||||||
|
result = resolve_boolean_effect(attribute)
|
||||||
|
elif isinstance(attribute, FloatEffectPattern):
|
||||||
|
result = resolve_float_effect(attribute)
|
||||||
|
elif isinstance(attribute, IntEffectPattern):
|
||||||
|
result = resolve_int_effect(attribute)
|
||||||
|
elif isinstance(attribute, StringEffectPattern):
|
||||||
|
result = resolve_string_effect(attribute)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||||
|
attributes.append(result)
|
||||||
|
|
||||||
|
duration = resolve_int_range(effect.duration)
|
||||||
|
|
||||||
|
result = EffectResult(
|
||||||
|
name=effect.name,
|
||||||
|
description=effect.description,
|
||||||
|
duration=duration,
|
||||||
|
attributes=attributes,
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def is_active_effect(effect: EffectResult) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if an effect is active.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return effect.duration is None or effect.duration > 0
|
||||||
|
|
||||||
|
|
||||||
|
def apply_permanent_results(
|
||||||
|
attributes: Attributes, effects: List[EffectResult]
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Permanently apply a set of effects to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for effect in effects:
|
||||||
|
for attribute in effect.attributes:
|
||||||
|
if isinstance(attribute, BooleanEffectResult):
|
||||||
|
attributes[attribute.name] = effective_boolean(attributes, attribute)
|
||||||
|
elif isinstance(attribute, FloatEffectResult):
|
||||||
|
attributes[attribute.name] = effective_float(attributes, attribute)
|
||||||
|
elif isinstance(attribute, IntEffectResult):
|
||||||
|
attributes[attribute.name] = effective_int(attributes, attribute)
|
||||||
|
elif isinstance(attribute, StringEffectResult):
|
||||||
|
attributes[attribute.name] = effective_string(attributes, attribute)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid operation: {attribute.operation}")
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
def apply_permanent_effects(
|
||||||
|
attributes: Attributes, effects: List[EffectPattern]
|
||||||
|
) -> Attributes:
|
||||||
|
"""
|
||||||
|
Permanently apply a set of effects to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = resolve_effects(effects)
|
||||||
|
return apply_permanent_results(attributes, results)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_effects(target: Actor, effects: List[EffectPattern]) -> None:
|
||||||
|
"""
|
||||||
|
Apply a set of effects to a set of attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
permanent_effects = [
|
||||||
|
effect for effect in effects if effect.application == "permanent"
|
||||||
|
]
|
||||||
|
permanent_effects = resolve_effects(permanent_effects)
|
||||||
|
target.attributes = apply_permanent_results(target.attributes, permanent_effects)
|
||||||
|
|
||||||
|
temporary_effects = [
|
||||||
|
effect for effect in effects if effect.application == "temporary"
|
||||||
|
]
|
||||||
|
temporary_effects = resolve_effects(temporary_effects)
|
||||||
|
target.active_effects.extend(temporary_effects)
|
||||||
|
|
Loading…
Reference in New Issue