Fix propose/resolve faction turn
This commit is contained in:
parent
1ec3b18bb1
commit
74d580e740
|
|
@ -40,6 +40,7 @@ from faction_utils import (
|
|||
roll_dice,
|
||||
roll_attribute_check,
|
||||
are_within_one_move,
|
||||
write_faction,
|
||||
)
|
||||
from wwn_assets_data import (
|
||||
get_available_assets,
|
||||
|
|
@ -279,7 +280,7 @@ def _build_hide_asset_options(faction_fm: dict, all_factions: list[dict], locati
|
|||
if tf.get("name") != faction_fm.get("name")
|
||||
)
|
||||
if hostile_boi_in_loc:
|
||||
return []
|
||||
return [] # <--- скрытие запрещено
|
||||
|
||||
lines = []
|
||||
for a in faction_fm.get("assets", []):
|
||||
|
|
@ -363,7 +364,7 @@ def _build_create_asset_options(faction_fm: dict, location_wikilink: str) -> lis
|
|||
"""
|
||||
available = get_available_assets(faction_fm, location_wikilink)
|
||||
if not available:
|
||||
return [" - *(нет доступных активов для покупки)*"]
|
||||
return [] # <--- теперь возвращаем пустой список, а не заглушку
|
||||
|
||||
lines = []
|
||||
for a in available:
|
||||
|
|
@ -478,19 +479,42 @@ def _format_faction_section(
|
|||
create_opts = _build_create_asset_options(fm, location_wikilink)
|
||||
sell_opts = _build_sell_options(fm)
|
||||
|
||||
# Expand Influence
|
||||
treasure = fm.get("treasure", 0)
|
||||
expand_str = (
|
||||
f" - Создать BoI в новой локации: 1 Treasure/HP нового BoI "
|
||||
f"(текущий Treasure: {treasure}, максимальный BoI HP: {treasure})\n"
|
||||
f" Потребует Cunning vs Cunning vs всех фракций с BoI в целевой локации"
|
||||
)
|
||||
|
||||
# ----- ИСПРАВЛЕННАЯ ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ -----
|
||||
# Возвращает пустую строку, если список опций пуст
|
||||
def _section(title: str, lines: list[str]) -> str:
|
||||
if not lines:
|
||||
return f"\n**[{title}]** *(недоступно)*"
|
||||
return "" # <-- теперь не выводится заголовок "недоступно"
|
||||
return f"\n**[{title}]**\n" + "\n".join(lines)
|
||||
|
||||
# ----- Блок Expand Influence (теперь только если доступен) -----
|
||||
treasure = fm.get("treasure", 0)
|
||||
has_regular_asset_in_loc = any(
|
||||
not a.get("is_base_of_influence") and normalize_wikilink(a.get("location", "")) == loc_norm
|
||||
for a in fm.get("assets", [])
|
||||
)
|
||||
expand_section = ""
|
||||
if has_regular_asset_in_loc:
|
||||
expand_str = (
|
||||
f" - Создать BoI в новой локации: 1 Treasure/HP нового BoI "
|
||||
f"(текущий Treasure: {treasure}, максимальный BoI HP: {treasure})\n"
|
||||
f" Потребует Cunning vs Cunning vs всех фракций с любым активом в целевой локации"
|
||||
)
|
||||
expand_section = f"\n**[EXPAND INFLUENCE]**\n{expand_str}"
|
||||
|
||||
# Собираем все секции, кроме EXPAND INFLUENCE (он уже отдельно)
|
||||
actions_sections = (
|
||||
_section("ATTACK", attack_opts) +
|
||||
_section("CREATE ASSET", create_opts) +
|
||||
_section("HIDE ASSET", hide_opts) +
|
||||
_section("MOVE ASSET", move_opts) +
|
||||
_section("REPAIR ASSET", repair_opts) +
|
||||
expand_section +
|
||||
_section("SELL ASSET", sell_opts)
|
||||
)
|
||||
|
||||
if not actions_sections.strip():
|
||||
actions_sections = "\n*(нет доступных действий — фракция пропускает ход)*"
|
||||
|
||||
return (
|
||||
f"### {idx}. {name} | Initiative: {initiative} | {state_label.upper()}\n"
|
||||
f"**Статы:** {status_str}"
|
||||
|
|
@ -500,16 +524,87 @@ def _format_faction_section(
|
|||
f"\n**Доход этого хода:** {income_str}{upkeep_str}"
|
||||
f"\n\n**Активы в локации:**\n{assets_str}"
|
||||
f"\n\n#### Доступные действия:"
|
||||
+ _section("ATTACK", attack_opts)
|
||||
+ _section("CREATE ASSET", create_opts)
|
||||
+ _section("HIDE ASSET", hide_opts)
|
||||
+ _section("MOVE ASSET", move_opts)
|
||||
+ _section("REPAIR ASSET", repair_opts)
|
||||
+ f"\n**[EXPAND INFLUENCE]**\n{expand_str}"
|
||||
+ _section("SELL ASSET", sell_opts)
|
||||
f"{actions_sections}"
|
||||
)
|
||||
|
||||
|
||||
def _ensure_startup_asset(
|
||||
faction: dict,
|
||||
factions_folder: str,
|
||||
default_location: str = "[[Уотердип]]"
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет, есть ли у фракции активы (включая BoI). Если нет, и фракция новая
|
||||
(нет last_turn_date и last_action), то добавляет самый дешёвый актив tier 1
|
||||
в первую локацию из списка locations (или default_location) БЕСПЛАТНО.
|
||||
"""
|
||||
fm = faction.get("frontmatter", {})
|
||||
# Пропускаем побеждённые или мёртвые
|
||||
if fm.get("state") == "defeated" or fm.get("hp", 0) <= 0:
|
||||
return False
|
||||
|
||||
# Если есть хоть один актив (включая BoI) — ничего не делаем
|
||||
if fm.get("assets"):
|
||||
return False
|
||||
|
||||
# Если фракция уже действовала ранее — не даём халявный актив
|
||||
if fm.get("last_turn_date") or fm.get("last_action"):
|
||||
return False
|
||||
|
||||
# Выбираем локацию
|
||||
locations = fm.get("locations", [])
|
||||
if not locations:
|
||||
loc = default_location
|
||||
fm.setdefault("locations", []).append(default_location)
|
||||
else:
|
||||
loc = locations[0]
|
||||
|
||||
# Определяем наивысший атрибут (приоритет: Cunning > Force > Wealth)
|
||||
attrs = {
|
||||
"Cunning": fm.get("cunning", 0),
|
||||
"Force": fm.get("force", 0),
|
||||
"Wealth": fm.get("wealth", 0),
|
||||
}
|
||||
priority = {"Cunning": 0, "Force": 1, "Wealth": 2}
|
||||
sorted_attrs = sorted(attrs.items(), key=lambda x: (-x[1], priority[x[0]]))
|
||||
best_attr = sorted_attrs[0][0]
|
||||
|
||||
from wwn_assets_data import get_assets_by_attribute, ALL_ASSETS
|
||||
candidates = [a for a in get_assets_by_attribute(best_attr) if a["tier_required"] == 1]
|
||||
if not candidates:
|
||||
candidates = [a for a in ALL_ASSETS if a["tier_required"] == 1]
|
||||
if not candidates:
|
||||
return False
|
||||
|
||||
best_asset = min(candidates, key=lambda a: a["cost"])
|
||||
|
||||
# Создаём актив бесплатно (не тратим Treasure)
|
||||
new_asset = {
|
||||
"name": best_asset["name"],
|
||||
"is_base_of_influence": False,
|
||||
"attribute": best_asset["attribute"],
|
||||
"tier_required": best_asset["tier_required"],
|
||||
"cost": best_asset["cost"],
|
||||
"hp": best_asset["hp_max"],
|
||||
"hp_max": best_asset["hp_max"],
|
||||
"location": loc,
|
||||
"stealthed": False, # по умолчанию активы не скрыты, даже если имеют Subtle
|
||||
"subtle": False,
|
||||
}
|
||||
fm.setdefault("assets", []).append(new_asset)
|
||||
# Treasure не изменяется
|
||||
|
||||
# Сохраняем изменения в файл
|
||||
file_path = faction.get("path")
|
||||
if file_path:
|
||||
body = faction.get("body", "")
|
||||
success = write_faction(file_path, fm, body)
|
||||
if success:
|
||||
faction["frontmatter"] = fm
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Главная функция
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -602,6 +697,9 @@ def propose_faction_turn(
|
|||
faction["_income"] = income
|
||||
faction["_upkeep"] = upkeep
|
||||
|
||||
# Добавляем бесплатный стартовый актив, если надо
|
||||
_ensure_startup_asset(faction, factions_folder, location_wikilink)
|
||||
|
||||
# ── Сборка вывода ────────────────────────────────────────────────
|
||||
header = (
|
||||
f"# Ход фракций: {location_wikilink} | {game_date}\n\n"
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ def _make_event_content(
|
|||
def _resolve_attack(
|
||||
action: dict,
|
||||
faction_fm: dict,
|
||||
all_factions_data: dict, # faction_file → (fm, body, path)
|
||||
all_factions_data: dict,
|
||||
factions_folder: str,
|
||||
) -> dict:
|
||||
"""
|
||||
|
|
@ -355,8 +355,6 @@ def _resolve_attack(
|
|||
Бросает 1d10+attr vs 1d10+attr с учётом faction tags.
|
||||
Применяет урон к target_asset или BoI.
|
||||
При ударе по BoI — также наносит урон HP фракции.
|
||||
|
||||
Возвращает словарь с результатами для отчёта.
|
||||
"""
|
||||
attacking_asset_name = action.get("attacking_asset", "")
|
||||
target_faction_file = action.get("target_faction", "")
|
||||
|
|
@ -377,6 +375,21 @@ def _resolve_attack(
|
|||
result["error"] = f"Актив '{attacking_asset_name}' не найден в таблицах WWN"
|
||||
return result
|
||||
|
||||
# ----- Проверка: атакующий актив должен находиться в той же локации, что и цель -----
|
||||
own_asset_ref = _find_asset_in_faction(faction_fm, attacking_asset_name)
|
||||
if not own_asset_ref:
|
||||
result["error"] = f"Актив '{attacking_asset_name}' не найден у атакующей фракции"
|
||||
return result
|
||||
|
||||
own_loc = normalize_wikilink(own_asset_ref.get("location", ""))
|
||||
target_loc = normalize_wikilink(location) # цель должна быть в этой локации
|
||||
if own_loc != target_loc:
|
||||
result["error"] = (
|
||||
f"Атакующий актив находится в {own_loc}, а цель указана в {target_loc}. "
|
||||
"Атака возможна только в одной локации."
|
||||
)
|
||||
return result
|
||||
|
||||
atk_attr = own_asset_def.get("attack_attribute", "Force")
|
||||
atk_vs = own_asset_def.get("attack_vs", "Force")
|
||||
atk_dmg_notation = own_asset_def.get("attack_damage", "1d6")
|
||||
|
|
@ -594,7 +607,12 @@ def _resolve_create_asset(
|
|||
return result
|
||||
|
||||
|
||||
def _resolve_hide_asset(action: dict, faction_fm: dict) -> dict:
|
||||
def _resolve_hide_asset(
|
||||
action: dict,
|
||||
faction_fm: dict,
|
||||
all_factions_data: dict,
|
||||
own_faction_file: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Выполняет действие Hide Asset по правилам WWN.
|
||||
|
||||
|
|
@ -602,6 +620,7 @@ def _resolve_hide_asset(action: dict, faction_fm: dict) -> dict:
|
|||
нельзя скрывать в локации с вражеским BoI.
|
||||
"""
|
||||
asset_name = action.get("asset_name", "")
|
||||
location = action.get("location", "")
|
||||
result = {"action": "Hide Asset", "asset": asset_name}
|
||||
|
||||
if faction_fm.get("cunning", 0) < 3:
|
||||
|
|
@ -617,6 +636,23 @@ def _resolve_hide_asset(action: dict, faction_fm: dict) -> dict:
|
|||
result["error"] = f"Актив '{asset_name}' не найден"
|
||||
return result
|
||||
|
||||
# ----- Проверка на вражеский BoI в той же локации -----
|
||||
loc_norm = normalize_wikilink(location)
|
||||
hostile_boi = any(
|
||||
normalize_wikilink(a.get("location", "")) == loc_norm
|
||||
and a.get("is_base_of_influence")
|
||||
for tf_file, (tf_fm, _, _) in all_factions_data.items()
|
||||
if tf_file != own_faction_file
|
||||
for a in tf_fm.get("assets", [])
|
||||
)
|
||||
if hostile_boi:
|
||||
result["error"] = (
|
||||
f"В локации {location} присутствует вражеский BoI — скрытие невозможно "
|
||||
"(правило стр. 326)."
|
||||
)
|
||||
return result
|
||||
|
||||
# Применяем скрытие
|
||||
asset["stealthed"] = True
|
||||
faction_fm["treasure"] = faction_fm.get("treasure", 0) - 2
|
||||
result["treasure_spent"] = 2
|
||||
|
|
@ -654,24 +690,32 @@ def _resolve_move_asset(action: dict, faction_fm: dict) -> dict:
|
|||
return result
|
||||
|
||||
|
||||
def _resolve_repair_asset(action: dict, faction_fm: dict) -> dict:
|
||||
def _resolve_repair_asset(
|
||||
action: dict,
|
||||
faction_fm: dict,
|
||||
repair_application_number: int, # номер применения ремонта для данного целевого объекта (актива или faction)
|
||||
) -> dict:
|
||||
"""
|
||||
Выполняет действие Repair Asset по правилам WWN.
|
||||
|
||||
repair_asset = "faction" → ремонтирует HP самой фракции.
|
||||
repair_asset = имя актива → ремонтирует актив.
|
||||
|
||||
Параметр repair_application_number — номер применения ремонта к этому объекту в текущем ходу
|
||||
(1 – первое применение, 2 – второе и т.д.).
|
||||
Стоимость = номер применения (1 Treasure за первое, 2 за второе, ...).
|
||||
"""
|
||||
repair_target = action.get("repair_asset", "")
|
||||
result = {"action": "Repair Asset", "target": repair_target}
|
||||
|
||||
treasure = faction_fm.get("treasure", 0)
|
||||
cost = repair_application_number # стоимость равна номеру применения
|
||||
|
||||
if repair_target == "faction":
|
||||
if treasure < 1:
|
||||
result["error"] = "Недостаточно Treasure для ремонта фракции"
|
||||
if treasure < cost:
|
||||
result["error"] = f"Недостаточно Treasure (нужно {cost}, есть {treasure})"
|
||||
return result
|
||||
repair_info = calc_faction_repair_heal(faction_fm)
|
||||
cost = repair_info["cost"]
|
||||
heal = repair_info["heal"]
|
||||
old_hp = faction_fm.get("hp", 0)
|
||||
hp_max = faction_fm.get("hp_max", 1)
|
||||
|
|
@ -686,15 +730,15 @@ def _resolve_repair_asset(action: dict, faction_fm: dict) -> dict:
|
|||
result["error"] = f"Актив '{repair_target}' не найден"
|
||||
return result
|
||||
|
||||
attr = asset.get("attribute", "Force")
|
||||
repair_info = calc_repair_heal(faction_fm, attr, 1)
|
||||
cost = repair_info["cost"]
|
||||
heal = repair_info["heal"]
|
||||
|
||||
if treasure < cost:
|
||||
result["error"] = f"Недостаточно Treasure (нужно {cost}, есть {treasure})"
|
||||
return result
|
||||
|
||||
attr = asset.get("attribute", "Force")
|
||||
# heal вычисляется по формуле из WWN: ceil(attr_value / 2)
|
||||
attr_value = faction_fm.get(attr.lower(), 0)
|
||||
heal = math.ceil(attr_value / 2)
|
||||
|
||||
old_hp = asset.get("hp", 0)
|
||||
hp_max = asset.get("hp_max", 1)
|
||||
asset["hp"] = min(hp_max, old_hp + heal)
|
||||
|
|
@ -716,7 +760,7 @@ def _resolve_expand_influence(
|
|||
Выполняет действие Expand Influence по правилам WWN.
|
||||
|
||||
Создаёт новый BoI в expand_location за expand_hp Treasure.
|
||||
Затем выполняет Cunning vs Cunning против каждой фракции с BoI в локации.
|
||||
Затем выполняет Cunning vs Cunning против каждой фракции с любым активом в локации.
|
||||
При проигрыше — враг получает право на немедленную атаку (помечается в отчёте).
|
||||
"""
|
||||
expand_loc = action.get("expand_location", action.get("location", ""))
|
||||
|
|
@ -727,32 +771,28 @@ def _resolve_expand_influence(
|
|||
"boi_hp": desired_hp,
|
||||
}
|
||||
|
||||
|
||||
# Получаем текущий список (если его нет, создаем пустой)
|
||||
if "locations" not in faction_fm:
|
||||
faction_fm["locations"] = []
|
||||
|
||||
# Проверяем, нет ли уже этой локации в списке (нормализованно)
|
||||
loc_norm = normalize_wikilink(expand_loc)
|
||||
existing_locs_norm = [normalize_wikilink(l) for l in faction_fm["locations"]]
|
||||
|
||||
if loc_norm not in existing_locs_norm:
|
||||
# Добавляем именно в том виде, в котором пришло (например, "[[Neverwinter]]")
|
||||
faction_fm["locations"].append(expand_loc)
|
||||
|
||||
# ----- Проверка: у создающей фракции должен быть хотя бы один актив в локации (не BoI) -----
|
||||
has_asset = any(
|
||||
not a.get("is_base_of_influence") and normalize_wikilink(a.get("location", "")) == loc_norm
|
||||
for a in faction_fm.get("assets", [])
|
||||
)
|
||||
if not has_asset:
|
||||
result["error"] = (
|
||||
f"Нет активов (кроме BoI) в локации {expand_loc} для создания нового BoI. "
|
||||
"Требуется хотя бы один обычный актив."
|
||||
)
|
||||
return result
|
||||
|
||||
# ----- Проверка Treasure -----
|
||||
cost = desired_hp # 1 Treasure per HP
|
||||
treasure = faction_fm.get("treasure", 0)
|
||||
|
||||
if treasure < cost:
|
||||
# Если денег не хватило, удаляем локацию обратно (откат)
|
||||
if expand_loc in faction_fm["locations"]:
|
||||
faction_fm["locations"].remove(expand_loc)
|
||||
result["error"] = f"Недостаточно Treasure (нужно {cost}, есть {treasure})"
|
||||
return result
|
||||
|
||||
# Создаём BoI
|
||||
loc_norm = normalize_wikilink(expand_loc)
|
||||
# ----- Создаём BoI -----
|
||||
boi_name = f"Base of Influence — {expand_loc.strip('[]')}"
|
||||
new_boi = {
|
||||
"name": boi_name,
|
||||
|
|
@ -770,17 +810,28 @@ def _resolve_expand_influence(
|
|||
result["treasure_after"] = faction_fm["treasure"]
|
||||
result["boi_created"] = boi_name
|
||||
|
||||
# Cunning checks против враждебных фракций в локации
|
||||
# ----- Добавляем локацию в список regions (если ещё нет) -----
|
||||
if "locations" not in faction_fm:
|
||||
faction_fm["locations"] = []
|
||||
existing_locs_norm = [normalize_wikilink(l) for l in faction_fm["locations"]]
|
||||
if loc_norm not in existing_locs_norm:
|
||||
faction_fm["locations"].append(expand_loc)
|
||||
|
||||
# ----- Cunning checks против ВСЕХ фракций, у которых есть ЛЮБОЙ актив в локации -----
|
||||
own_cunning = faction_fm.get("cunning", 0)
|
||||
contesting_checks = []
|
||||
|
||||
for tf_file, (tf_fm, _tf_body, _tf_path) in all_factions_data.items():
|
||||
# Проверяем есть ли у них BoI в expand_loc
|
||||
tf_has_boi = any(
|
||||
a.get("is_base_of_influence") and normalize_wikilink(a.get("location", "")) == loc_norm
|
||||
# Пропускаем себя
|
||||
if tf_file == action.get("faction_file", ""):
|
||||
continue
|
||||
|
||||
# Проверяем, есть ли у этой фракции хотя бы один актив (любой) в локации
|
||||
tf_has_asset = any(
|
||||
normalize_wikilink(a.get("location", "")) == loc_norm
|
||||
for a in tf_fm.get("assets", [])
|
||||
)
|
||||
if not tf_has_boi:
|
||||
if not tf_has_asset:
|
||||
continue
|
||||
|
||||
tf_cunning = tf_fm.get("cunning", 0)
|
||||
|
|
@ -952,6 +1003,9 @@ def resolve_faction_turn(
|
|||
action_results: list[dict] = []
|
||||
event_files_created: list[str] = []
|
||||
|
||||
# ---- В начале цикла обработки действий инициализируем счётчики ремонта ----
|
||||
repair_counters = {} # ключ: (faction_file, target_name) -> количество применений в этом ходу
|
||||
|
||||
for action in actions:
|
||||
faction_file = action.get("faction_file", "")
|
||||
action_type = action.get("action_type", "")
|
||||
|
|
@ -968,24 +1022,29 @@ def resolve_faction_turn(
|
|||
fm, body, path = all_factions_data[faction_file]
|
||||
result = {"faction": faction_file}
|
||||
|
||||
if action_type == "Attack":
|
||||
# ----- Для Repair Asset обновляем счётчик -----
|
||||
if action_type == "Repair Asset":
|
||||
repair_target = action.get("repair_asset", "")
|
||||
key = (faction_file, repair_target)
|
||||
repair_counters[key] = repair_counters.get(key, 0) + 1
|
||||
repair_application_number = repair_counters[key]
|
||||
result.update(_resolve_repair_asset(action, fm, repair_application_number))
|
||||
# ----- Остальные действия без изменений (кроме Hide Asset и Attack, которые теперь требуют дополнительные параметры) -----
|
||||
elif action_type == "Attack":
|
||||
result.update(_resolve_attack(action, fm, all_factions_data, factions_folder))
|
||||
|
||||
elif action_type == "Create Asset":
|
||||
result.update(_resolve_create_asset(action, fm, location))
|
||||
|
||||
elif action_type == "Hide Asset":
|
||||
result.update(_resolve_hide_asset(action, fm))
|
||||
|
||||
result.update(_resolve_hide_asset(action, fm, all_factions_data, faction_file))
|
||||
|
||||
elif action_type == "Move Asset":
|
||||
result.update(_resolve_move_asset(action, fm))
|
||||
|
||||
elif action_type == "Repair Asset":
|
||||
result.update(_resolve_repair_asset(action, fm))
|
||||
|
||||
elif action_type == "Expand Influence":
|
||||
result.update(_resolve_expand_influence(action, fm, all_factions_data))
|
||||
|
||||
|
||||
elif action_type == "Sell Asset":
|
||||
result.update(_resolve_sell_asset(action, fm))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user