Compare commits
7
Commits
10f5b3455b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e440f66405 | ||
|
|
0a08384da2 | ||
|
|
d2f710a6a1 | ||
|
|
44462624cd | ||
|
|
9e4d07df45 | ||
|
|
1018c90c25 | ||
|
|
392b1caf0a |
No files matched your search
@@ -0,0 +1,30 @@
|
||||
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
|
||||
* text=auto eol=lf
|
||||
|
||||
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Binaries: never converted, never diffed as text.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.webp binary
|
||||
*.svg text
|
||||
*.ogg binary
|
||||
*.wav binary
|
||||
*.mp3 binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.dll binary
|
||||
*.so binary
|
||||
*.dylib binary
|
||||
*.pdb binary
|
||||
*.exe binary
|
||||
*.zip binary
|
||||
*.pck binary
|
||||
*.moc3 binary
|
||||
*.res binary
|
||||
*.scn binary
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Godot 4.x
|
||||
.godot/
|
||||
.idea/
|
||||
sdk/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
#ai
|
||||
.omo/
|
||||
.opencode/
|
||||
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
art_sources/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
# GDScript LSP
|
||||
.godot/
|
||||
|
||||
# Export
|
||||
*.exe
|
||||
*.dmg
|
||||
*.apk
|
||||
*.aab
|
||||
*.pck
|
||||
*.zip
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
*.import
|
||||
*.godot.uid
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"key": "battle.core",
|
||||
"name": "collection_settings/battle",
|
||||
"description": "collection_settings/battle_hint",
|
||||
"group": "collection_settings/battle_group",
|
||||
"type": "choice",
|
||||
"source": null,
|
||||
"allow_custom": false,
|
||||
"automatic": "collection_settings/battle_auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base — the platform under every battle
|
||||
|
||||
Turn Battle Base is what all turn-based battles in the collection share: starting a battle from a scenario, the battle scene with the backdrop of the place where it began, the victory and defeat screens, experience and loot, status effects such as stun, and the choice of a **battle system**. It does not decide how the player fights. That is the job of a battle system mod: **FF Battle** (a command menu) or **Card Battle** (cards dragged onto targets).
|
||||
|
||||
## Two battle systems, one platform
|
||||
|
||||
The same fight in RPG Plus — the warrior against a wolf in the village — in the two systems:
|
||||
|
||||

|
||||
|
||||
Everything around the fight is the same: the village backdrop, the wolf's numbers, the speed buttons **x1 / x4 / x8** at the top right (they speed up the animations; the choice is remembered), and what happens after it.
|
||||
|
||||
A game chooses its system with the collection setting `battle.core`; RPG Plus uses `ff_battle`. Without the setting the first installed system is used. In the collection editor the setting is called *Battle system*.
|
||||
|
||||
## How a battle goes
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## Victory and defeat
|
||||
|
||||
When the last enemy falls, the victory screen shows each surviving party member's experience and the loot:
|
||||
|
||||

|
||||
|
||||
- **Experience.** Each defeated enemy gives its `Level × Exp`, shared equally among the party members still standing. The wolf is level 2 with `Exp` 12 — **+24**.
|
||||
- **Loot.** Each defeated enemy rolls its `Loot` list; the items go straight into the inventory.
|
||||
- **Continue** returns to the scene the battle came from. Health lost in battle stays lost.
|
||||
|
||||
If the whole party falls, the defeat screen offers only **Main menu**: no experience, no loot.
|
||||
|
||||

|
||||
|
||||
## Starting a battle from a scenario
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies` — character ids, comma-separated; the same id twice gives two enemies.
|
||||
- `scene` — the scene to return to after a victory.
|
||||
- `quest_id`, `new_tag` (optional) — bind the battle to a quest and give it a tag (a journal notification).
|
||||
- The backdrop is the background shown at that moment, so set it before `battle.init`.
|
||||
|
||||
On a tile map, use `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead` from the RPG mod instead: it returns to the same map cell and sets the flag on victory.
|
||||
|
||||
`action battle.set_core core=card_battle` switches the battle system for the following battles.
|
||||
|
||||
## Enemies, loot and status effects in data
|
||||
|
||||
Enemies are ordinary characters (see Characters Engine). The loot of the RPG Plus wolf:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` is a percentage; the count is a random number from `MinCount` to `MaxCount` inclusive.
|
||||
|
||||
**Status effects** are entities of kind `status_effect`. The platform brings stun — the character skips its turns:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
A battle action that applies it may set its own number of turns; durations count down at the start of each turn.
|
||||
|
||||
## For mod authors
|
||||
|
||||
A battle system is a mod that registers an `IBattleCore` (with `IBattleAutoPlayable` for the automatic tests). Its actions run through the platform's executors, so a mod such as Battle Formulas adds new kinds of actions to every system at once. Code can react to the end of a battle through `BattleEndedEvent` and to the victory through an `IWingroundListener`; Recruiting uses this to offer defeated enemies a place in the party.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
1. **The battle has an empty backdrop.** `battle.init` ran before the scene's background was set.
|
||||
2. **The wrong battle system opens.** Check the game's `battle.core` setting, and whether a scenario switched it with `battle.set_core`.
|
||||
3. **No experience after a victory.** Enemies with `Exp` 0 give none; party members who fell get none.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base — la plataforma común de todos los combates
|
||||
|
||||
Turn Battle Base es lo que comparten todos los combates por turnos de la colección: iniciar un combate desde un escenario, la escena de batalla con el fondo del lugar donde empezó, las pantallas de victoria y derrota, la experiencia y el botín, los efectos de estado como el aturdimiento y la elección del **sistema de combate**. Cómo lucha el jugador no lo decide la plataforma: ese es el trabajo de un mod de sistema de combate, **FF Battle** (menú de comandos) o **Card Battle** (cartas que se arrastran sobre los objetivos).
|
||||
|
||||
## Dos sistemas de combate, una plataforma
|
||||
|
||||
La misma pelea de RPG Plus — el guerrero contra un lobo en la aldea — en los dos sistemas:
|
||||
|
||||

|
||||
|
||||
Todo lo que rodea al combate es igual: el fondo de la aldea, los números del lobo, los botones de velocidad **x1 / x4 / x8** arriba a la derecha (aceleran las animaciones, y la elección se recuerda) y lo que ocurre después.
|
||||
|
||||
Cada juego elige su sistema con el ajuste de colección `battle.core`; RPG Plus usa `ff_battle`. Sin ese ajuste se toma el primer sistema instalado. En el editor de colecciones el ajuste se llama «Sistema de combate».
|
||||
|
||||
## Cómo se desarrolla un combate
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## Victoria y derrota
|
||||
|
||||
Cuando cae el último enemigo, la pantalla de victoria muestra la experiencia de cada miembro del grupo que siga en pie y el botín:
|
||||
|
||||

|
||||
|
||||
- **Experiencia.** Cada enemigo derrotado aporta su `Level × Exp`, que se reparte a partes iguales entre quienes sigan en pie. El lobo es de nivel 2 con `Exp` 12: **+24**.
|
||||
- **Botín.** Para cada enemigo derrotado se tira su lista de `Loot`; los objetos van directos al inventario.
|
||||
- **Continue** devuelve a la escena de la que salió el combate. La vida perdida en el combate sigue perdida.
|
||||
|
||||
Si cae todo el grupo, la pantalla de derrota solo ofrece **Main menu**: ni experiencia ni botín.
|
||||
|
||||

|
||||
|
||||
## Iniciar un combate desde un escenario
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies` — ids de personajes separados por comas; el mismo id dos veces son dos enemigos.
|
||||
- `scene` — la escena a la que volver tras la victoria.
|
||||
- `quest_id`, `new_tag` (opcionales) — vinculan el combate a una misión y le ponen una etiqueta (una notificación en el diario).
|
||||
- El fondo del combate es el que se muestre en ese momento, así que defínelo antes de `battle.init`.
|
||||
|
||||
Sobre un mapa de tiles usa en su lugar el `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead` del mod RPG: el grupo vuelve a la misma casilla y, al ganar, se activa la bandera.
|
||||
|
||||
La acción `action battle.set_core core=card_battle` cambia el sistema de combate para los combates siguientes.
|
||||
|
||||
## Enemigos, botín y efectos de estado en los datos
|
||||
|
||||
Los enemigos son personajes normales (véase Characters Engine). El botín del lobo de RPG Plus:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` es un porcentaje; la cantidad es un número aleatorio entre `MinCount` y `MaxCount`, ambos incluidos.
|
||||
|
||||
Los **efectos de estado** son entidades de tipo `status_effect`. La plataforma trae de serie el aturdimiento: el personaje pierde sus turnos:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
Una acción de combate que lo aplique puede fijar su propio número de turnos; las duraciones se descuentan al comienzo de cada turno.
|
||||
|
||||
## Para autores de mods
|
||||
|
||||
Un sistema de combate es un mod que registra un `IBattleCore` (y `IBattleAutoPlayable` para las pruebas automáticas). Sus acciones pasan por los ejecutores de la plataforma, así que un mod como Battle Formulas añade nuevos tipos de acciones a todos los sistemas a la vez. El código puede reaccionar al final del combate con `BattleEndedEvent` y a la victoria con un `IWingroundListener`; Recruiting lo aprovecha para ofrecer un puesto en el grupo a los enemigos derrotados.
|
||||
|
||||
## Si algo sale mal
|
||||
|
||||
1. **El combate sale con el fondo vacío.** `battle.init` se ejecutó antes de definir el fondo de la escena.
|
||||
2. **Se abre el sistema de combate equivocado.** Revisa el ajuste `battle.core` del juego y si algún escenario lo cambió con `battle.set_core`.
|
||||
3. **No hay experiencia tras la victoria.** Los enemigos con `Exp` 0 no dan experiencia, y los miembros del grupo caídos tampoco reciben.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base —— すべての戦闘を支えるプラットフォーム
|
||||
|
||||
Turn Battle Base は、コレクション内のすべてのターン制戦闘に共通する土台です。シナリオからの戦闘開始、戦闘が始まった場所を背景にした戦闘シーン、勝利・敗北の画面、経験値と戦利品、スタンなどのステータス効果、そして**バトルシステム**の選択を担います。プレイヤーがどう戦うかは、ここでは決まりません。それはバトルシステムのModの仕事です。**FF Battle**(コマンドメニュー)と **Card Battle**(カードをターゲットへドラッグ)がそれに当たります。
|
||||
|
||||
## 2つのバトルシステム、1つのプラットフォーム
|
||||
|
||||
RPG Plus での同じ戦闘——村で戦士が狼と対峙する場面——を、2つのシステムで並べます:
|
||||
|
||||

|
||||
|
||||
戦闘を取り巻く部分はどちらも同じです。村の背景、狼の数値、右上の速度ボタン **x1 / x4 / x8**(アニメーションを速めるもので、選択は記憶されます)、そして戦闘後の流れです。
|
||||
|
||||
ゲームはコレクション設定 `battle.core` でシステムを選びます。RPG Plus は `ff_battle` を使用します。設定がなければ、最初にインストールされているシステムが使われます。コレクションエディターでは、この設定は「戦闘システム」という名前です。
|
||||
|
||||
## 戦闘の流れ
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## 勝利と敗北
|
||||
|
||||
最後の敵が倒れると、勝利画面には生存しているパーティメンバーそれぞれの経験値と戦利品が表示されます:
|
||||
|
||||

|
||||
|
||||
- **経験値。** 倒した敵はそれぞれ `Level × Exp` を出し、それが生存しているパーティメンバーに均等に分配されます。狼はレベル 2・`Exp` 12 なので **+24** です。
|
||||
- **戦利品。** 倒した敵ごとに `Loot` リストが抽選されます。アイテムはそのままインベントリに入ります。
|
||||
- **Continue** で戦闘が始まったシーンに戻ります。戦闘中に失った体力は戻りません。
|
||||
|
||||
パーティが全滅すると、敗北画面は **Main menu** だけを提示します。経験値も戦利品もありません。
|
||||
|
||||

|
||||
|
||||
## シナリオから戦闘を開始する
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies`——キャラクター id をカンマ区切りで指定します。同じ id を 2 回書けば敵が 2 体になります。
|
||||
- `scene`——勝利後に戻るシーンです。
|
||||
- `quest_id`、`new_tag`(省略可)——戦闘をクエストに結び付け、タグ(ジャーナル通知)を付けます。
|
||||
- 背景にはその時点で表示されている背景が使われます。`battle.init` の前に設定しておいてください。
|
||||
|
||||
タイルマップでは、代わりに RPG Mod の `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead` を使ってください。同じマップセルに戻り、勝利時にフラグが立ちます。
|
||||
|
||||
`action battle.set_core core=card_battle` は、以降の戦闘で使うバトルシステムを切り替えます。
|
||||
|
||||
## データとしての敵・戦利品・ステータス効果
|
||||
|
||||
敵はごく普通のキャラクターです(Characters Engine を参照)。RPG Plus の狼の戦利品:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` はパーセント値です。個数は `MinCount` から `MaxCount` まで(両端を含む)の乱数です。
|
||||
|
||||
**ステータス効果**は `status_effect` 種別のエンティティです。プラットフォームにはスタンが用意されていて、かかるとキャラクターは自分のターンを飛ばします:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
この効果をかける戦闘アクションは、独自のターン数を指定できます。持続ターン数は毎ターンの開始時に減っていきます。
|
||||
|
||||
## Mod作者向け
|
||||
|
||||
バトルシステムとは、`IBattleCore` を登録するModです(自動テストのため `IBattleAutoPlayable` も実装します)。そのアクションはプラットフォームのエグゼキューターを通って実行されるため、Battle Formulas のようなModは新しい種類のアクションをすべてのシステムに一度に追加できます。コードは `BattleEndedEvent` で戦闘終了を、`IWingroundListener` で勝利を受け取れます。Recruiting はこれを利用して、倒した敵をパーティに勧誘します。
|
||||
|
||||
## うまくいかないときは
|
||||
|
||||
1. **戦闘の背景が空白になる。** シーン背景の設定より先に `battle.init` が実行されています。
|
||||
2. **別のバトルシステムが開く。** ゲームの `battle.core` 設定と、シナリオが `battle.set_core` で切り替えていないかを確認してください。
|
||||
3. **勝利後に経験値が入らない。** `Exp` が 0 の敵は経験値を出しません。倒れたパーティメンバーも得られません。
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base — 모든 전투를 받치는 플랫폼
|
||||
|
||||
Turn Battle Base는 컬렉션의 모든 턴제 전투가 공유하는 부분입니다. 시나리오에서 전투를 여는 일, 전투가 시작된 장소를 배경으로 삼는 전투 장면, 승리·패배 화면, 경험치와 전리품, 기절 같은 상태 이상, 그리고 **전투 시스템** 선택이 모두 여기서 이뤄집니다. 다만 플레이어가 어떻게 싸울지는 정하지 않습니다. 그것은 전투 시스템 모드의 몫입니다. 명령 메뉴 방식의 **FF Battle**이나 카드를 대상에 끌어다 놓는 **Card Battle**이 그 예입니다.
|
||||
|
||||
## 두 가지 전투 시스템, 하나의 플랫폼
|
||||
|
||||
RPG Plus의 같은 전투 — 마을에서 전사가 늑대와 맞서는 장면 — 을 두 시스템으로 보여주면 다음과 같습니다:
|
||||
|
||||

|
||||
|
||||
전투를 둘러싼 모든 것은 같습니다. 마을 배경, 늑대의 수치, 오른쪽 위의 속도 버튼 **x1 / x4 / x8**(애니메이션을 빠르게 하며 선택은 기억됩니다), 그리고 전투가 끝난 뒤의 흐름까지요.
|
||||
|
||||
게임은 컬렉션 설정 `battle.core`로 시스템을 고릅니다. RPG Plus는 `ff_battle`을 사용합니다. 설정이 없으면 설치된 첫 번째 시스템을 씁니다. 컬렉션 편집기에서는 이 설정이 "전투 시스템"이라는 이름으로 표시됩니다.
|
||||
|
||||
## 전투의 진행
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## 승리와 패배
|
||||
|
||||
마지막 적이 쓰러지면 승리 화면에 살아남은 파티원 각자의 경험치와 전리품이 표시됩니다:
|
||||
|
||||

|
||||
|
||||
- **경험치.** 쓰러뜨린 적은 각자 자신의 `Level × Exp`를 주며, 살아 있는 파티원들이 똑같이 나눕니다. 늑대는 2레벨에 `Exp` 12이므로 **+24**입니다.
|
||||
- **전리품.** 쓰러뜨린 적마다 자신의 `Loot` 목록이 굴려집니다. 아이템은 곧바로 인벤토리에 들어갑니다.
|
||||
- **Continue**는 전투가 시작됐던 장면으로 돌아갑니다. 전투에서 잃은 체력은 그대로 남습니다.
|
||||
|
||||
파티 전원이 쓰러지면 패배 화면은 **Main menu**만 제안합니다. 경험치도 전리품도 없습니다.
|
||||
|
||||

|
||||
|
||||
## 시나리오에서 전투 시작하기
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies` — 캐릭터 id를 쉼표로 나열합니다. 같은 id를 두 번 쓰면 적이 둘 나옵니다.
|
||||
- `scene` — 승리 후 돌아갈 장면입니다.
|
||||
- `quest_id`, `new_tag`(선택) — 전투를 퀘스트에 묶고 태그(저널 알림)를 붙입니다.
|
||||
- 전투 배경은 그 시점에 표시된 배경입니다. `battle.init` 전에 배경을 정해 두세요.
|
||||
|
||||
타일 맵에서는 대신 RPG 모드의 `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead`를 사용하세요. 같은 맵 칸으로 돌아오고, 승리 시 플래그가 설정됩니다.
|
||||
|
||||
`action battle.set_core core=card_battle` 액션은 이후 전투의 전투 시스템을 바꿉니다.
|
||||
|
||||
## 데이터 속의 적·전리품·상태 이상
|
||||
|
||||
적은 평범한 캐릭터입니다(Characters Engine 참조). RPG Plus 늑대의 전리품:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability`는 백분율입니다. 개수는 `MinCount`부터 `MaxCount`까지(양 끝 포함)의 무작위 수입니다.
|
||||
|
||||
**상태 이상**은 `status_effect` 종류의 엔티티입니다. 플랫폼에는 기절이 기본으로 들어 있으며, 걸린 캐릭터는 자기 턴을 건너뜁니다:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
이 효과를 거는 전투 액션이 자체 턴 수를 정할 수도 있습니다. 지속 시간은 매 턴 시작 때 줄어듭니다.
|
||||
|
||||
## 모드 작성자를 위해
|
||||
|
||||
전투 시스템은 `IBattleCore`를 등록하는 모드입니다(자동 테스트를 위한 `IBattleAutoPlayable`도). 이 모드의 액션은 플랫폼의 실행기를 거치므로, Battle Formulas 같은 모드가 새 종류의 액션을 모든 시스템에 한꺼번에 추가할 수 있습니다. 코드는 `BattleEndedEvent`로 전투 종료를, `IWingroundListener`로 승리를 받아볼 수 있습니다. Recruiting이 이를 이용해 쓰러뜨린 적에게 파티 자리를 제안합니다.
|
||||
|
||||
## 문제가 생겼을 때
|
||||
|
||||
1. **전투 배경이 비어 있음.** 장면 배경 설정보다 `battle.init`이 먼저 실행됐습니다.
|
||||
2. **다른 전투 시스템이 열림.** 게임의 `battle.core` 설정과, 시나리오가 `battle.set_core`로 바꾸지 않았는지 확인하세요.
|
||||
3. **승리 후 경험치가 없음.** `Exp`가 0인 적은 경험치를 주지 않고, 쓰러진 파티원도 받지 못합니다.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base — общая платформа для всех боёв
|
||||
|
||||
Turn Battle Base — это то общее, что есть у всех пошаговых боёв в коллекции: запуск боя из сценария, боевая сцена с фоном того места, где бой начался, экраны победы и поражения, опыт и добыча, статусные эффекты вроде оглушения — и выбор **боевой системы**. Как именно сражается игрок, платформа не решает: это задача мода боевой системы — **FF Battle** (меню команд) или **Card Battle** (карты, которые перетаскивают на цели).
|
||||
|
||||
## Две боевые системы, одна платформа
|
||||
|
||||
Один и тот же бой в RPG Plus — воин против волка в деревне — в обеих системах:
|
||||
|
||||

|
||||
|
||||
Всё вокруг боя одинаковое: фон деревни, цифры волка, кнопки скорости **x1 / x4 / x8** в правом верхнем углу (они ускоряют анимации, а выбор запоминается) и то, что происходит после боя.
|
||||
|
||||
Свою систему игра выбирает настройкой коллекции `battle.core`; в RPG Plus это `ff_battle`. Без настройки берётся первая из установленных систем. В редакторе коллекций настройка называется «Боевая система».
|
||||
|
||||
## Как проходит бой
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## Победа и поражение
|
||||
|
||||
Когда падает последний враг, экран победы показывает опыт каждого уцелевшего члена отряда и добычу:
|
||||
|
||||

|
||||
|
||||
- **Опыт.** Каждый побеждённый враг приносит свой `Level × Exp`, который поровну делится между теми, кто остался на ногах. Волк — 2-го уровня с `Exp` 12, итого **+24**.
|
||||
- **Добыча.** Для каждого побеждённого врага разыгрывается его список `Loot`; предметы сразу попадают в инвентарь.
|
||||
- **Continue** возвращает в сцену, откуда пришёл бой. Потерянное в бою здоровье не восстанавливается.
|
||||
|
||||
Если падает весь отряд, экран поражения предлагает только **Main menu** — без опыта и без добычи.
|
||||
|
||||

|
||||
|
||||
## Запуск боя из сценария
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies` — id персонажей через запятую; один и тот же id дважды — это два врага.
|
||||
- `scene` — сцена, в которую игра вернётся после победы.
|
||||
- `quest_id`, `new_tag` (необязательные) — привязывают бой к заданию и помечают его тегом (уведомление в журнале).
|
||||
- Фон боя — тот, что показан в этот момент, поэтому задайте его до `battle.init`.
|
||||
|
||||
На тайловой карте вместо этого используйте `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead` из мода RPG: отряд вернётся в ту же клетку карты, а при победе выставится флаг.
|
||||
|
||||
Команда `action battle.set_core core=card_battle` переключает боевую систему для последующих боёв.
|
||||
|
||||
## Враги, добыча и статусные эффекты в данных
|
||||
|
||||
Враги — обычные персонажи (см. Characters Engine). Добыча волка из RPG Plus:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` — вероятность в процентах; количество — случайное число от `MinCount` до `MaxCount` включительно.
|
||||
|
||||
**Статусные эффекты** — это сущности вида `status_effect`. Уже в платформе есть оглушение: персонаж пропускает свои ходы:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
Боевое действие, накладывающее эффект, может задать своё число ходов; длительность уменьшается в начале каждого хода.
|
||||
|
||||
## Авторам модов
|
||||
|
||||
Боевая система — мод, регистрирующий `IBattleCore` (и `IBattleAutoPlayable` для автоматических тестов). Её действия выполняются через исполнители платформы, поэтому мод вроде Battle Formulas добавляет новые виды действий сразу всем системам. Код может узнавать о конце боя через `BattleEndedEvent`, а о победе — через `IWingroundListener`; так работает Recruiting, предлагая побеждённым врагам место в отряде.
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
1. **У боя пустой фон.** `battle.init` выполнился раньше, чем у сцены появился фон.
|
||||
2. **Открывается не та система.** Проверьте настройку игры `battle.core` и не переключал ли её какой-нибудь сценарий через `battle.set_core`.
|
||||
3. **После победы нет опыта.** Враги с `Exp` 0 не дают опыта; павшие члены отряда его не получают.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base — nền tảng của mọi trận chiến
|
||||
|
||||
Turn Battle Base là phần chung của mọi trận chiến theo lượt trong bộ sưu tập: mở trận từ kịch bản, cảnh chiến đấu lấy nền theo nơi trận nổ ra, màn hình thắng và thua, kinh nghiệm và chiến lợi phẩm, các hiệu ứng trạng thái như choáng, và cả việc chọn **hệ thống chiến đấu**. Còn người chơi đánh thế nào, nó không quyết định — việc đó thuộc về mod hệ thống chiến đấu: **FF Battle** (menu lệnh) hoặc **Card Battle** (kéo thẻ vào mục tiêu).
|
||||
|
||||
## Hai hệ thống chiến đấu, một nền tảng
|
||||
|
||||
Cùng một trận đánh trong RPG Plus — chiến binh đấu với con sói trong làng — dưới hai hệ thống:
|
||||
|
||||

|
||||
|
||||
Mọi thứ xung quanh trận đánh đều giống nhau: nền làng, các con số của con sói, các nút tốc độ **x1 / x4 / x8** ở góc trên bên phải (chúng tăng tốc hiệu ứng, lựa chọn được ghi nhớ) và những gì diễn ra sau trận đánh.
|
||||
|
||||
Game chọn hệ thống bằng thiết lập `battle.core` của bộ sưu tập; RPG Plus dùng `ff_battle`. Không có thiết lập thì hệ thống được cài đầu tiên sẽ được dùng. Trong trình soạn thảo bộ sưu tập, thiết lập này có tên "Hệ thống chiến đấu".
|
||||
|
||||
## Một trận chiến diễn ra thế nào
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## Thắng và thua
|
||||
|
||||
Khi kẻ địch cuối cùng gục xuống, màn hình thắng sẽ hiển thị kinh nghiệm của từng thành viên đội còn sống và phần chiến lợi phẩm:
|
||||
|
||||

|
||||
|
||||
- **Kinh nghiệm.** Mỗi kẻ địch bị hạ mang lại `Level × Exp` của nó, chia đều cho những thành viên đội còn trụ. Con sói cấp 2 với `Exp` 12 — **+24**.
|
||||
- **Chiến lợi phẩm.** Với mỗi kẻ địch bị hạ, trò chơi quay số theo danh sách `Loot` của nó; vật phẩm vào thẳng túi đồ.
|
||||
- **Continue** đưa bạn về cảnh mà trận chiến bắt đầu. Máu đã mất trong trận chiến vẫn mất.
|
||||
|
||||
Cả đội gục ngã thì màn hình thua chỉ còn **Main menu**: không kinh nghiệm, không chiến lợi phẩm.
|
||||
|
||||

|
||||
|
||||
## Mở trận chiến từ kịch bản
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies` — id nhân vật, phân tách bằng dấu phẩy; cùng một id xuất hiện hai lần nghĩa là hai kẻ địch.
|
||||
- `scene` — cảnh sẽ quay về sau khi thắng.
|
||||
- `quest_id`, `new_tag` (tùy chọn) — gắn trận chiến với nhiệm vụ và gán cho nó một thẻ (thông báo trên nhật ký).
|
||||
- Nền trận chiến là nền đang hiển thị tại thời điểm đó, nên hãy đặt nền trước khi gọi `battle.init`.
|
||||
|
||||
Trên bản đồ tile, hãy dùng `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead` của mod RPG thay thế: đội sẽ về đúng ô cũ và cờ được đặt khi thắng.
|
||||
|
||||
Hành động `action battle.set_core core=card_battle` đổi hệ thống chiến đấu cho các trận chiến tiếp theo.
|
||||
|
||||
## Kẻ địch, chiến lợi phẩm và hiệu ứng trạng thái trong dữ liệu
|
||||
|
||||
Kẻ địch là nhân vật bình thường (xem Characters Engine). Chiến lợi phẩm của con sói trong RPG Plus:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` là phần trăm; số lượng là một số ngẫu nhiên từ `MinCount` đến `MaxCount` (bao gồm cả hai đầu).
|
||||
|
||||
**Hiệu ứng trạng thái** là thực thể loại `status_effect`. Nền tảng có sẵn choáng — nhân vật bị bỏ qua các lượt của mình:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
Hành động chiến đấu gây hiệu ứng này có thể tự đặt số lượt; thời gian giảm dần vào đầu mỗi lượt.
|
||||
|
||||
## Dành cho tác giả mod
|
||||
|
||||
Hệ thống chiến đấu là một mod đăng ký `IBattleCore` (cùng `IBattleAutoPlayable` cho bộ kiểm thử tự động). Các hành động của nó chạy qua bộ thực thi của nền tảng, nên một mod như Battle Formulas có thể thêm loại hành động mới cho tất cả các hệ thống cùng lúc. Mã nguồn có thể phản ứng khi trận chiến kết thúc qua `BattleEndedEvent` và khi thắng qua `IWingroundListener`; Recruiting dùng đúng cơ chế này để mời kẻ địch bị hạ gia nhập đội.
|
||||
|
||||
## Khi có sự cố
|
||||
|
||||
1. **Nền trận chiến trống trơn.** `battle.init` chạy trước khi nền của cảnh được đặt.
|
||||
2. **Mở nhầm hệ thống chiến đấu.** Kiểm tra thiết lập `battle.core` của game và xem có kịch bản nào đã đổi nó bằng `battle.set_core` không.
|
||||
3. **Thắng rồi mà không có kinh nghiệm.** Kẻ địch có `Exp` 0 không cho kinh nghiệm; thành viên đội đã gục cũng không nhận.
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Turn Battle Base —— 每场战斗背后的平台
|
||||
|
||||
Turn Battle Base 汇集了合集中所有回合制战斗的公共部分:从剧本发起战斗、以开战地点为背景的战斗场景、胜利与失败画面、经验和掉落、眩晕等状态效果,以及**战斗系统**的选择。玩家具体怎么打,平台并不决定——那是战斗系统模组的职责:**FF Battle**(指令菜单)或 **Card Battle**(把卡牌拖到目标上)。
|
||||
|
||||
## 两套战斗系统,一个平台
|
||||
|
||||
RPG Plus 里的同一场战斗——村庄中战士对狼——在两套系统下分别是这样:
|
||||
|
||||

|
||||
|
||||
战斗之外的一切都相同:村庄背景、狼的数值、右上角的 **x1 / x4 / x8** 倍速按钮(用于加快动画,选择会被记住),以及战斗结束后的流程。
|
||||
|
||||
游戏通过合集设置 `battle.core` 选择战斗系统;RPG Plus 使用 `ff_battle`。未设置时采用第一个已安装的系统。在合集编辑器中,这项设置名为「战斗系统」。
|
||||
|
||||
## 一场战斗如何进行
|
||||
|
||||
```text
|
||||
scenario: battle.init / rpg.battle
|
||||
|
|
||||
v
|
||||
battle scene: backdrop of the current place, party from the roster, enemies from their files
|
||||
|
|
||||
v
|
||||
battle system (ff_battle or card_battle): player turn <-> enemy turn
|
||||
| |
|
||||
all enemies defeated party defeated
|
||||
v v
|
||||
Victory: experience, loot Defeat: Main menu
|
||||
|
|
||||
v
|
||||
back to the scene the battle started from
|
||||
```
|
||||
|
||||
## 胜利与失败
|
||||
|
||||
当最后一个敌人倒下,胜利画面会显示每位幸存队员获得的经验和掉落:
|
||||
|
||||

|
||||
|
||||
- **经验。** 每个被击败的敌人提供其 `Level × Exp`,由仍然站立的队员平分。狼为 2 级、`Exp` 12,即 **+24**。
|
||||
- **掉落。** 每个被击败的敌人按其 `Loot` 列表掷点;物品直接进入背包。
|
||||
- **Continue** 返回战斗开始前的场景。战斗中损失的生命不会恢复。
|
||||
|
||||
如果全队倒下,失败画面只提供 **Main menu**:没有经验,也没有掉落。
|
||||
|
||||

|
||||
|
||||
## 从剧本发起战斗
|
||||
|
||||
```scenario
|
||||
action battle.init scene=town_entry enemies=wolf_1,wolf_2 quest_id=MeetingMurakamiQuest new_tag=wolf_battle
|
||||
switch_scene Battle
|
||||
end
|
||||
```
|
||||
|
||||
- `enemies`——角色 id,逗号分隔;同一个 id 出现两次就是两个敌人。
|
||||
- `scene`——胜利后返回的场景。
|
||||
- `quest_id`、`new_tag`(可选)——把战斗绑定到任务,并给它一个标签(日志通知)。
|
||||
- 战斗背景就是当时显示的背景,所以要在 `battle.init` 之前设置好。
|
||||
|
||||
在图块地图上,请改用 RPG 模组的 `action rpg.battle enemies=rpg_wolf win_flag=rpg_wolf_dead`:队伍会回到同一格,胜利时还会设置标记。
|
||||
|
||||
动作 `action battle.set_core core=card_battle` 为之后的战斗切换战斗系统。
|
||||
|
||||
## 数据中的敌人、掉落与状态效果
|
||||
|
||||
敌人就是普通角色(参见 Characters Engine)。RPG Plus 中狼的掉落:
|
||||
|
||||
```json
|
||||
{
|
||||
"Id": "rpg_wolf",
|
||||
"Loot": [
|
||||
{ "ItemId": "rpg_plus/wolf_pelt", "MinCount": 2, "MaxCount": 3, "Probability": 100 },
|
||||
{ "ItemId": "money/coin", "MinCount": 8, "MaxCount": 14, "Probability": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`Probability` 是百分比;数量是 `MinCount` 到 `MaxCount` 之间(含两端)的随机数。
|
||||
|
||||
**状态效果**是 `status_effect` 类型的实体。平台自带眩晕——角色会跳过自己的回合:
|
||||
|
||||
```json
|
||||
{ "Id": "effect/physics/stun", "Icon": "characters/effects/stun", "Type": "Stun", "Duration": 3, "Actions": "basic/stun" }
|
||||
```
|
||||
|
||||
施加该效果的动作可以自行指定回合数;持续时间在每个回合开始时递减。
|
||||
|
||||
## 给模组作者
|
||||
|
||||
战斗系统是一个注册了 `IBattleCore` 的模组(自动测试还需要 `IBattleAutoPlayable`)。它的动作经由平台的执行器运行,因此像 Battle Formulas 这样的模组能一次为所有系统添加新类型的动作。代码可以通过 `BattleEndedEvent` 响应战斗结束,通过 `IWingroundListener` 响应胜利;Recruiting 就是这样邀请被击败的敌人加入队伍的。
|
||||
|
||||
## 出问题怎么办
|
||||
|
||||
1. **战斗背景为空。** `battle.init` 在场景背景设置之前就执行了。
|
||||
2. **打开的战斗系统不对。** 检查游戏的 `battle.core` 设置,以及是否有剧本用 `battle.set_core` 切换过它。
|
||||
3. **胜利后没有经验。** `Exp` 为 0 的敌人不给经验;倒下的队员也拿不到。
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = Battle system
|
||||
collection_settings/battle_hint = Choose a battle system provided by the mods in this collection.
|
||||
collection_settings/battle_auto = Uses the first registered battle system when unspecified.
|
||||
collection_settings/battle_group = Combat
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = Sistema de combate
|
||||
collection_settings/battle_hint = Elige un sistema de combate de los mods de esta colección.
|
||||
collection_settings/battle_auto = Sin valor se usa el primer sistema de combate registrado.
|
||||
collection_settings/battle_group = Combate
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = 戦闘システム
|
||||
collection_settings/battle_hint = 構成に含まれるMODの戦闘システムを選びます。
|
||||
collection_settings/battle_auto = 未指定の場合は最初に登録された戦闘システムを使用。
|
||||
collection_settings/battle_group = 戦闘
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = 전투 시스템
|
||||
collection_settings/battle_hint = 컬렉션에 포함된 모드의 전투 시스템을 선택합니다.
|
||||
collection_settings/battle_auto = 지정하지 않으면 처음 등록된 전투 시스템을 사용합니다.
|
||||
collection_settings/battle_group = 전투
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = Боевая система
|
||||
collection_settings/battle_hint = Выберите боевую систему из включённых в коллекцию модов.
|
||||
collection_settings/battle_auto = Без настройки используется первая зарегистрированная боевая система.
|
||||
collection_settings/battle_group = Бой
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = Hệ thống chiến đấu
|
||||
collection_settings/battle_hint = Chọn hệ thống chiến đấu từ các mod trong bộ sưu tập.
|
||||
collection_settings/battle_auto = Nếu không đặt, dùng hệ thống chiến đấu được đăng ký đầu tiên.
|
||||
collection_settings/battle_group = Chiến đấu
|
||||
@@ -0,0 +1,4 @@
|
||||
collection_settings/battle = 战斗系统
|
||||
collection_settings/battle_hint = 选择此合集模组提供的战斗系统。
|
||||
collection_settings/battle_auto = 未指定时使用第一个注册的战斗系统。
|
||||
collection_settings/battle_group = 战斗
|
||||
@@ -1,2 +1,2 @@
|
||||
mod_turn_battle_base_name = Turn-based Battle Engine
|
||||
mod_turn_battle_base_desc = Turn-based battle platform: battle ruleset contracts (IBattleCore), the core registry, action executors, status effects, the battle scene, victory/defeat screens and EXP/loot reports.
|
||||
mod_turn_battle_base_name = Turn Battle Base
|
||||
mod_turn_battle_base_desc = The platform under every turn-based battle: starting from a scenario, the battle scene, victory and defeat, experience and loot — how the player fights is a separate battle system.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_turn_battle_base_name = Motor de combate por turnos
|
||||
mod_turn_battle_base_desc = La plataforma bajo cada combate por turnos: inicio desde un escenario, la escena de batalla, victoria y derrota, experiencia y botín; cómo lucha el jugador es cosa de un sistema de combate aparte.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_turn_battle_base_name = ターン制バトルエンジン
|
||||
mod_turn_battle_base_desc = すべてのターン制戦闘の土台:シナリオからの開始、戦闘シーン、勝利と敗北、経験値と戦利品。プレイヤーの戦い方は、別のバトルシステムが担います。
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_turn_battle_base_name = 턴제 전투 엔진
|
||||
mod_turn_battle_base_desc = 모든 턴제 전투의 기반: 시나리오에서 시작, 전투 장면, 승리와 패배, 경험치와 전리품 — 플레이어가 어떻게 싸울지는 별도의 전투 시스템이 담당합니다.
|
||||
@@ -1,2 +1,2 @@
|
||||
mod_turn_battle_base_name = Движок пошаговых боёв
|
||||
mod_turn_battle_base_desc = Платформа пошаговых боёв: контракты боевых ядер (IBattleCore), реестр ядер, исполнители действий, статус-эффекты, боевая сцена, экраны победы/поражения и отчёты опыта и добычи.
|
||||
mod_turn_battle_base_desc = Платформа любого пошагового боя: запуск из сценария, боевая сцена, победа и поражение, опыт и добыча — а как именно сражаться, решает отдельная боевая система.
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = Start a battle
|
||||
battle/schema/builtin/battle_init_desc = A battle from a novel scene: enemies, the return scene, a quest
|
||||
battle/schema/field/enemies = Enemies
|
||||
battle/schema/field/scene = Scene
|
||||
battle/schema/field/quest = Quest
|
||||
battle/schema/field/tag = Tag
|
||||
battle/schema/builtin/set_core = Battle core
|
||||
battle/schema/builtin/set_core_desc = Which rules the next battle follows
|
||||
battle/schema/field/core = Core
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = Empezar un combate
|
||||
battle/schema/builtin/battle_init_desc = Un combate desde una escena de novela: enemigos, escena de regreso, misión
|
||||
battle/schema/field/enemies = Enemigos
|
||||
battle/schema/field/scene = Escena
|
||||
battle/schema/field/quest = Misión
|
||||
battle/schema/field/tag = Etiqueta
|
||||
battle/schema/builtin/set_core = Núcleo de combate
|
||||
battle/schema/builtin/set_core_desc = Qué reglas sigue el próximo combate
|
||||
battle/schema/field/core = Núcleo
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = 戦闘を始める
|
||||
battle/schema/builtin/battle_init_desc = ノベルのシーンからの戦闘:敵、戻り先のシーン、クエスト
|
||||
battle/schema/field/enemies = 敵
|
||||
battle/schema/field/scene = シーン
|
||||
battle/schema/field/quest = クエスト
|
||||
battle/schema/field/tag = タグ
|
||||
battle/schema/builtin/set_core = バトルコア
|
||||
battle/schema/builtin/set_core_desc = 次の戦闘がどのルールに従うか
|
||||
battle/schema/field/core = コア
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = 전투 시작
|
||||
battle/schema/builtin/battle_init_desc = 노벨 장면에서의 전투: 적, 돌아올 장면, 퀘스트
|
||||
battle/schema/field/enemies = 적
|
||||
battle/schema/field/scene = 장면
|
||||
battle/schema/field/quest = 퀘스트
|
||||
battle/schema/field/tag = 태그
|
||||
battle/schema/builtin/set_core = 전투 코어
|
||||
battle/schema/builtin/set_core_desc = 다음 전투가 따를 규칙
|
||||
battle/schema/field/core = 코어
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = Начать бой
|
||||
battle/schema/builtin/battle_init_desc = Бой из сцены новеллы: враги, сцена возврата, квест
|
||||
battle/schema/field/enemies = Враги
|
||||
battle/schema/field/scene = Сцена
|
||||
battle/schema/field/quest = Квест
|
||||
battle/schema/field/tag = Метка
|
||||
battle/schema/builtin/set_core = Боевое ядро
|
||||
battle/schema/builtin/set_core_desc = Каким правилам подчиняется следующий бой
|
||||
battle/schema/field/core = Ядро
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = Bắt đầu trận đấu
|
||||
battle/schema/builtin/battle_init_desc = Trận đấu từ cảnh tiểu thuyết: kẻ địch, cảnh quay về, nhiệm vụ
|
||||
battle/schema/field/enemies = Kẻ địch
|
||||
battle/schema/field/scene = Cảnh
|
||||
battle/schema/field/quest = Nhiệm vụ
|
||||
battle/schema/field/tag = Thẻ
|
||||
battle/schema/builtin/set_core = Lõi chiến đấu
|
||||
battle/schema/builtin/set_core_desc = Trận tới theo luật nào
|
||||
battle/schema/field/core = Lõi
|
||||
@@ -0,0 +1,9 @@
|
||||
battle/schema/builtin/battle_init = 开始战斗
|
||||
battle/schema/builtin/battle_init_desc = 来自小说场景的战斗:敌人、返回场景、任务
|
||||
battle/schema/field/enemies = 敌人
|
||||
battle/schema/field/scene = 场景
|
||||
battle/schema/field/quest = 任务
|
||||
battle/schema/field/tag = 标签
|
||||
battle/schema/builtin/set_core = 战斗核心
|
||||
battle/schema/builtin/set_core_desc = 下一场战斗遵循哪套规则
|
||||
battle/schema/field/core = 核心
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_turn_battle_base_name = Engine chiến đấu theo lượt
|
||||
mod_turn_battle_base_desc = Nền tảng của mọi trận chiến theo lượt: khởi đầu từ kịch bản, cảnh chiến đấu, thắng và thua, kinh nghiệm và chiến lợi phẩm — người chơi đánh thế nào là việc của một hệ thống chiến đấu riêng.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_turn_battle_base_name = 回合制战斗引擎
|
||||
mod_turn_battle_base_desc = 每场回合制战斗背后的平台:从剧本发起战斗、战斗场景、胜利与失败、经验和掉落——玩家怎么打,由独立的战斗系统决定。
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "turn_battle_base",
|
||||
"name": "Turn-based battle engine",
|
||||
"name": "Turn Battle Base",
|
||||
"version": "2.0.0",
|
||||
"author": "ct",
|
||||
"description": "Turn-based battle platform: battle ruleset contracts, core registry, action executors, status effects, battle scene, victory/defeat screens, EXP and loot reports",
|
||||
"description": "The platform under every turn-based battle: starting from a scenario, the battle scene, victory and defeat, experience and loot — how the player fights is a separate battle system.",
|
||||
"category": "engine, gameplay",
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"Actions": [
|
||||
{
|
||||
"Id": "battle.init",
|
||||
"Name": "battle/schema/builtin/battle_init",
|
||||
"Description": "battle/schema/builtin/battle_init_desc",
|
||||
"Category": "battle",
|
||||
"Params": [
|
||||
{
|
||||
"Key": "enemies",
|
||||
"Type": "ref_list",
|
||||
"Kind": "character",
|
||||
"Required": true,
|
||||
"Name": "battle/schema/field/enemies"
|
||||
},
|
||||
{
|
||||
"Key": "scene",
|
||||
"Type": "scene",
|
||||
"Name": "battle/schema/field/scene"
|
||||
},
|
||||
{
|
||||
"Key": "quest_id",
|
||||
"Type": "ref",
|
||||
"Kind": "quest",
|
||||
"Name": "battle/schema/field/quest"
|
||||
},
|
||||
{
|
||||
"Key": "new_tag",
|
||||
"Type": "text",
|
||||
"Name": "battle/schema/field/tag"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": "battle.set_core",
|
||||
"Name": "battle/schema/builtin/set_core",
|
||||
"Description": "battle/schema/builtin/set_core_desc",
|
||||
"Category": "battle",
|
||||
"Params": [
|
||||
{
|
||||
"Key": "core",
|
||||
"Type": "choice",
|
||||
"Options": [
|
||||
"ff_battle",
|
||||
"card_battle"
|
||||
],
|
||||
"Required": true,
|
||||
"Name": "battle/schema/field/core"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,79 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Central action-execution dispatcher. Resolves an action ID to an
|
||||
/// IActionExecutor in two tiers: first the active provider (set by
|
||||
/// the current IBattleCore, allowing per-battle-engine overrides),
|
||||
/// then a global fallback registry. This two-level lookup lets mods
|
||||
/// supply custom executors for specific battle cores without replacing
|
||||
/// the global ones. Execute returns ChangedAttributes to allow the
|
||||
/// caller to inspect the result (success/failure, stat deltas).
|
||||
/// </summary>
|
||||
public class ActionExecutorHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton hub instance. There is one global dispatcher shared
|
||||
/// across all battle cores; cores swap providers via
|
||||
/// <see cref="SetActiveProvider"/> when they activate.
|
||||
/// </summary>
|
||||
public static readonly ActionExecutorHub Instance = new();
|
||||
|
||||
private IActionExecutorProvider _activeProvider;
|
||||
private readonly Dictionary<string, IActionExecutor> _globalExecutors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a per-core executor provider. The active provider is
|
||||
/// consulted before the global registry, allowing the current
|
||||
/// battle core to override specific action handlers.
|
||||
/// </summary>
|
||||
public void SetActiveProvider(IActionExecutorProvider provider)
|
||||
{
|
||||
_activeProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fallback executor in the global pool, keyed by
|
||||
/// <see cref="IActionExecutor.ActionId"/>. Global executors are
|
||||
/// used when no active provider supplies a match.
|
||||
/// </summary>
|
||||
public void RegisterGlobal(IActionExecutor executor)
|
||||
{
|
||||
if (executor != null && !string.IsNullOrEmpty(executor.ActionId))
|
||||
_globalExecutors[executor.ActionId] = executor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an action to the appropriate executor. Resolution
|
||||
/// order: active provider first, then global registry. Returns
|
||||
/// <c>Result = false</c> if no executor is found or if
|
||||
/// <paramref name="action"/> is null.
|
||||
/// </summary>
|
||||
public ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (action == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var executor = Resolve(action.ID);
|
||||
if (executor == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
return executor.Execute(action, user, target);
|
||||
}
|
||||
|
||||
private IActionExecutor Resolve(string actionId)
|
||||
{
|
||||
if (_activeProvider != null)
|
||||
{
|
||||
var executor = _activeProvider.GetExecutor(actionId);
|
||||
if (executor != null)
|
||||
return executor;
|
||||
}
|
||||
|
||||
_globalExecutors.TryGetValue(actionId, out var fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Central action-execution dispatcher. Resolves an action ID to an
|
||||
/// IActionExecutor in two tiers: first the active provider (set by
|
||||
/// the current IBattleCore, allowing per-battle-engine overrides),
|
||||
/// then a global fallback registry. This two-level lookup lets mods
|
||||
/// supply custom executors for specific battle cores without replacing
|
||||
/// the global ones. Execute returns ChangedAttributes to allow the
|
||||
/// caller to inspect the result (success/failure, stat deltas).
|
||||
/// </summary>
|
||||
public class ActionExecutorHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton hub instance. There is one global dispatcher shared
|
||||
/// across all battle cores; cores swap providers via
|
||||
/// <see cref="SetActiveProvider"/> when they activate.
|
||||
/// </summary>
|
||||
public static readonly ActionExecutorHub Instance = new();
|
||||
|
||||
private IActionExecutorProvider _activeProvider;
|
||||
private readonly Dictionary<string, IActionExecutor> _globalExecutors = new();
|
||||
|
||||
/// <summary>
|
||||
/// Installs a per-core executor provider. The active provider is
|
||||
/// consulted before the global registry, allowing the current
|
||||
/// battle core to override specific action handlers.
|
||||
/// </summary>
|
||||
public void SetActiveProvider(IActionExecutorProvider provider)
|
||||
{
|
||||
_activeProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fallback executor in the global pool, keyed by
|
||||
/// <see cref="IActionExecutor.ActionId"/>. Global executors are
|
||||
/// used when no active provider supplies a match.
|
||||
/// </summary>
|
||||
public void RegisterGlobal(IActionExecutor executor)
|
||||
{
|
||||
if (executor != null && !string.IsNullOrEmpty(executor.ActionId))
|
||||
_globalExecutors[executor.ActionId] = executor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an action to the appropriate executor. Resolution
|
||||
/// order: active provider first, then global registry. Returns
|
||||
/// <c>Result = false</c> if no executor is found or if
|
||||
/// <paramref name="action"/> is null.
|
||||
/// </summary>
|
||||
public ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (action == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var executor = Resolve(action.ID);
|
||||
if (executor == null)
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
return executor.Execute(action, user, target);
|
||||
}
|
||||
|
||||
private IActionExecutor Resolve(string actionId)
|
||||
{
|
||||
if (_activeProvider != null)
|
||||
{
|
||||
var executor = _activeProvider.GetExecutor(actionId);
|
||||
if (executor != null)
|
||||
return executor;
|
||||
}
|
||||
|
||||
_globalExecutors.TryGetValue(actionId, out var fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
-121
@@ -1,122 +1,122 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base;
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sequential action queue that processes one IBattleAction per frame
|
||||
/// until it signals completion (DoAction returns true), then moves to
|
||||
/// the next. Fires OnStopMachine when the queue drains naturally or
|
||||
/// is explicitly stopped — used by battle cores to detect when a turn's
|
||||
/// animation sequence is finished. Actions can be added before or during
|
||||
/// processing; the machine starts automatically once at least one action
|
||||
/// is queued and no current action is running.
|
||||
/// <para/>
|
||||
/// Derives from <see cref="ModNode"/> so the engine lifecycle callbacks reach
|
||||
/// this runtime-compiled script through the core base class (mod assemblies
|
||||
/// have no Godot source generation of their own).
|
||||
/// </summary>
|
||||
public partial class BattleActionMachine : ModNode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Pending action queue. Actions are dequeued one-by-one in the process
|
||||
/// hook, starting the next as soon as the current signals completion.
|
||||
/// Can be extended while the machine is running — new actions will
|
||||
/// be processed after the current queue drains.
|
||||
/// </summary>
|
||||
public List<IBattleAction> Actions { get; set; } = new();
|
||||
private IBattleAction CurrentAction { get; set; }
|
||||
private bool _hadActions;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the machine drains naturally or is explicitly stopped.
|
||||
/// Battle cores subscribe to advance the turn phase without polling.
|
||||
/// </summary>
|
||||
public event StopMachineDelegate OnStopMachine;
|
||||
|
||||
/// <summary>Stops the machine and fires <see cref="OnStopMachine"/> when the node leaves the tree, so subscribers never wait on a dead node.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
StopMachine();
|
||||
}
|
||||
|
||||
/// <summary>Drives the queued actions once per frame; fires <see cref="OnStopMachine"/> when the queue drains.</summary>
|
||||
protected override void OnProcess(double delta)
|
||||
{
|
||||
if (Actions.Count == 0 && CurrentAction == null)
|
||||
{
|
||||
if (_hadActions)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
CurrentAction = Actions[0];
|
||||
Actions.RemoveAt(0);
|
||||
action = CurrentAction;
|
||||
_hadActions = true;
|
||||
action?.DoStart();
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.DoAction())
|
||||
{
|
||||
action.DoEnd();
|
||||
CurrentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a single action. If the machine is idle (no current
|
||||
/// action), processing starts on the next _Process tick.
|
||||
/// </summary>
|
||||
public void AddAction(IBattleAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
Actions.Add(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-enqueues multiple actions. All are appended to the existing
|
||||
/// queue and processed in FIFO order on subsequent frames.
|
||||
/// </summary>
|
||||
public void AddActions(List<IBattleAction> actions)
|
||||
{
|
||||
if(actions == null || actions.Count == 0)
|
||||
return;
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately clears the queue, aborts the current action, and
|
||||
/// fires <see cref="OnStopMachine"/>. Called on tree exit and
|
||||
/// when the queue drains naturally.
|
||||
/// </summary>
|
||||
public void StopMachine()
|
||||
{
|
||||
CurrentAction = null;
|
||||
Actions.Clear();
|
||||
_hadActions = false;
|
||||
OnStopMachine?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sequential action queue that processes one IBattleAction per frame
|
||||
/// until it signals completion (DoAction returns true), then moves to
|
||||
/// the next. Fires OnStopMachine when the queue drains naturally or
|
||||
/// is explicitly stopped — used by battle cores to detect when a turn's
|
||||
/// animation sequence is finished. Actions can be added before or during
|
||||
/// processing; the machine starts automatically once at least one action
|
||||
/// is queued and no current action is running.
|
||||
/// <para/>
|
||||
/// Derives from <see cref="ModNode"/> so the engine lifecycle callbacks reach
|
||||
/// this runtime-compiled script through the core base class (mod assemblies
|
||||
/// have no Godot source generation of their own).
|
||||
/// </summary>
|
||||
public partial class BattleActionMachine : ModNode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Pending action queue. Actions are dequeued one-by-one in the process
|
||||
/// hook, starting the next as soon as the current signals completion.
|
||||
/// Can be extended while the machine is running — new actions will
|
||||
/// be processed after the current queue drains.
|
||||
/// </summary>
|
||||
public List<IBattleAction> Actions { get; set; } = new();
|
||||
private IBattleAction CurrentAction { get; set; }
|
||||
private bool _hadActions;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the machine drains naturally or is explicitly stopped.
|
||||
/// Battle cores subscribe to advance the turn phase without polling.
|
||||
/// </summary>
|
||||
public event StopMachineDelegate OnStopMachine;
|
||||
|
||||
/// <summary>Stops the machine and fires <see cref="OnStopMachine"/> when the node leaves the tree, so subscribers never wait on a dead node.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
StopMachine();
|
||||
}
|
||||
|
||||
/// <summary>Drives the queued actions once per frame; fires <see cref="OnStopMachine"/> when the queue drains.</summary>
|
||||
protected override void OnProcess(double delta)
|
||||
{
|
||||
if (Actions.Count == 0 && CurrentAction == null)
|
||||
{
|
||||
if (_hadActions)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
CurrentAction = Actions[0];
|
||||
Actions.RemoveAt(0);
|
||||
action = CurrentAction;
|
||||
_hadActions = true;
|
||||
action?.DoStart();
|
||||
}
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
_hadActions = false;
|
||||
StopMachine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.DoAction())
|
||||
{
|
||||
action.DoEnd();
|
||||
CurrentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a single action. If the machine is idle (no current
|
||||
/// action), processing starts on the next _Process tick.
|
||||
/// </summary>
|
||||
public void AddAction(IBattleAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
Actions.Add(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch-enqueues multiple actions. All are appended to the existing
|
||||
/// queue and processed in FIFO order on subsequent frames.
|
||||
/// </summary>
|
||||
public void AddActions(List<IBattleAction> actions)
|
||||
{
|
||||
if(actions == null || actions.Count == 0)
|
||||
return;
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately clears the queue, aborts the current action, and
|
||||
/// fires <see cref="OnStopMachine"/>. Called on tree exit and
|
||||
/// when the queue drains naturally.
|
||||
/// </summary>
|
||||
public void StopMachine()
|
||||
{
|
||||
CurrentAction = null;
|
||||
Actions.Clear();
|
||||
_hadActions = false;
|
||||
OnStopMachine?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a specific battle action. The executor is keyed by ActionId
|
||||
/// (matching the action's ID) so the hub can look it up without
|
||||
/// knowing the concrete type. Returns ChangedAttributes so callers
|
||||
/// can apply stat deltas and check whether the action succeeded.
|
||||
/// </summary>
|
||||
public interface IActionExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// String key that matches <see cref="ActionCharacter.ID"/>.
|
||||
/// The hub uses this to resolve executors without knowing
|
||||
/// concrete types.
|
||||
/// </summary>
|
||||
string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the action's effects. Receives the action definition
|
||||
/// and the user/target <see cref="Character"/> instances; returns
|
||||
/// <see cref="ChangedAttributes"/> with deltas and success/failure.
|
||||
/// </summary>
|
||||
ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a specific battle action. The executor is keyed by ActionId
|
||||
/// (matching the action's ID) so the hub can look it up without
|
||||
/// knowing the concrete type. Returns ChangedAttributes so callers
|
||||
/// can apply stat deltas and check whether the action succeeded.
|
||||
/// </summary>
|
||||
public interface IActionExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// String key that matches <see cref="ActionCharacter.ID"/>.
|
||||
/// The hub uses this to resolve executors without knowing
|
||||
/// concrete types.
|
||||
/// </summary>
|
||||
string ActionId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the action's effects. Receives the action definition
|
||||
/// and the user/target <see cref="Character"/> instances; returns
|
||||
/// <see cref="ChangedAttributes"/> with deltas and success/failure.
|
||||
/// </summary>
|
||||
ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Typed executor source. Each IBattleCore returns its own provider,
|
||||
/// giving cores the ability to supply custom executor sets without
|
||||
/// polluting the global registry.
|
||||
/// </summary>
|
||||
public interface IActionExecutorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the executor registered for <paramref name="actionId"/>,
|
||||
/// or null if no override is configured. Called by
|
||||
/// <see cref="ActionExecutorHub"/> before falling back to the
|
||||
/// global registry.
|
||||
/// </summary>
|
||||
IActionExecutor GetExecutor(string actionId);
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Typed executor source. Each IBattleCore returns its own provider,
|
||||
/// giving cores the ability to supply custom executor sets without
|
||||
/// polluting the global registry.
|
||||
/// </summary>
|
||||
public interface IActionExecutorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the executor registered for <paramref name="actionId"/>,
|
||||
/// or null if no override is configured. Called by
|
||||
/// <see cref="ActionExecutorHub"/> before falling back to the
|
||||
/// global registry.
|
||||
/// </summary>
|
||||
IActionExecutor GetExecutor(string actionId);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// A single step in the battle action queue (animation, damage, effect).
|
||||
/// DoStart is called once when the action begins, DoAction is polled
|
||||
/// every frame — it returns true when the action is done — and DoEnd
|
||||
/// is the cleanup callback. This polling model lets actions drive
|
||||
/// their own duration (e.g. a tween runs until finished) without
|
||||
/// the machine needing timers.
|
||||
/// </summary>
|
||||
public interface IBattleAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once when the action is dequeued and begins execution.
|
||||
/// Use for setup: spawning nodes, caching references, starting
|
||||
/// tweens or coroutines.
|
||||
/// </summary>
|
||||
void DoStart();
|
||||
/// <summary>
|
||||
/// Polled every frame. Returns <c>true</c> when the action has
|
||||
/// finished its work; returns <c>false</c> to remain active.
|
||||
/// This model eliminates the need for the machine to manage timers.
|
||||
/// </summary>
|
||||
bool DoAction();
|
||||
/// <summary>
|
||||
/// Cleanup callback fired after <see cref="DoAction"/> returns
|
||||
/// <c>true</c>. Use for teardown: freeing nodes, resetting state,
|
||||
/// applying final stat deltas.
|
||||
/// </summary>
|
||||
void DoEnd();
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// A single step in the battle action queue (animation, damage, effect).
|
||||
/// DoStart is called once when the action begins, DoAction is polled
|
||||
/// every frame — it returns true when the action is done — and DoEnd
|
||||
/// is the cleanup callback. This polling model lets actions drive
|
||||
/// their own duration (e.g. a tween runs until finished) without
|
||||
/// the machine needing timers.
|
||||
/// </summary>
|
||||
public interface IBattleAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once when the action is dequeued and begins execution.
|
||||
/// Use for setup: spawning nodes, caching references, starting
|
||||
/// tweens or coroutines.
|
||||
/// </summary>
|
||||
void DoStart();
|
||||
/// <summary>
|
||||
/// Polled every frame. Returns <c>true</c> when the action has
|
||||
/// finished its work; returns <c>false</c> to remain active.
|
||||
/// This model eliminates the need for the machine to manage timers.
|
||||
/// </summary>
|
||||
bool DoAction();
|
||||
/// <summary>
|
||||
/// Cleanup callback fired after <see cref="DoAction"/> returns
|
||||
/// <c>true</c>. Use for teardown: freeing nodes, resetting state,
|
||||
/// applying final stat deltas.
|
||||
/// </summary>
|
||||
void DoEnd();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Global animation-player index keyed by AnimationType string.
|
||||
/// Mods or cores register players; the action machine looks up the
|
||||
/// player for a given animation type. Singleton — there is one set
|
||||
/// of players for the entire runtime, but each player can make
|
||||
/// per-core decisions internally.
|
||||
/// </summary>
|
||||
public class AnimationRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton registry. One set of animation players serves the
|
||||
/// entire runtime; individual players dispatch per-core via
|
||||
/// internal logic.
|
||||
/// </summary>
|
||||
public static readonly AnimationRegistry Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IBattleAnimationPlayer> _players = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player under <paramref name="animType"/>.
|
||||
/// Replaces any existing player for the same key — last write wins.
|
||||
/// </summary>
|
||||
public void Register(string animType, IBattleAnimationPlayer player)
|
||||
{
|
||||
_players[animType] = player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the player registered for <paramref name="animType"/>,
|
||||
/// or null if no match exists.
|
||||
/// </summary>
|
||||
public IBattleAnimationPlayer Get(string animType)
|
||||
{
|
||||
_players.TryGetValue(animType, out var player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Global animation-player index keyed by AnimationType string.
|
||||
/// Mods or cores register players; the action machine looks up the
|
||||
/// player for a given animation type. Singleton — there is one set
|
||||
/// of players for the entire runtime, but each player can make
|
||||
/// per-core decisions internally.
|
||||
/// </summary>
|
||||
public class AnimationRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton registry. One set of animation players serves the
|
||||
/// entire runtime; individual players dispatch per-core via
|
||||
/// internal logic.
|
||||
/// </summary>
|
||||
public static readonly AnimationRegistry Instance = new();
|
||||
|
||||
private readonly Dictionary<string, IBattleAnimationPlayer> _players = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a player under <paramref name="animType"/>.
|
||||
/// Replaces any existing player for the same key — last write wins.
|
||||
/// </summary>
|
||||
public void Register(string animType, IBattleAnimationPlayer player)
|
||||
{
|
||||
_players[animType] = player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the player registered for <paramref name="animType"/>,
|
||||
/// or null if no match exists.
|
||||
/// </summary>
|
||||
public IBattleAnimationPlayer Get(string animType)
|
||||
{
|
||||
_players.TryGetValue(animType, out var player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,79 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Default IBattleContext wired to the current BattleSceneContext.
|
||||
/// ApplyDamage clamps at 0 (no negative health from defence overflow),
|
||||
/// EndBattle delegates to ShowWinground/ShowDeadground on the scene
|
||||
/// context. ScheduleAnimation and EndTurn are stubs — specific battle
|
||||
/// cores are expected to override or supplement this behaviour.
|
||||
/// </summary>
|
||||
public class BattleContextImpl : IBattleContext
|
||||
{
|
||||
private readonly BattleSceneContext _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Root Godot <see cref="Node"/> of the battle scene, used by
|
||||
/// animation actions to parent spawned visual effects.
|
||||
/// </summary>
|
||||
public Node RootNode => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="BattleSceneContext"/> that manages
|
||||
/// the battle lifecycle.
|
||||
/// </summary>
|
||||
public BattleSceneContext SceneContext => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context adapter around the given scene context.
|
||||
/// </summary>
|
||||
public BattleContextImpl(BattleSceneContext sceneContext)
|
||||
{
|
||||
_sceneContext = sceneContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts <paramref name="defence"/> from
|
||||
/// <paramref name="rawDamage"/>, clamping at 0 to prevent negative
|
||||
/// health from defence overflow, and applies the result to
|
||||
/// <paramref name="target"/>'s health.
|
||||
/// </summary>
|
||||
public void ApplyDamage(Character source, Character target, int rawDamage, int defence)
|
||||
{
|
||||
var finalDamage = Mathf.Max(0, rawDamage - defence);
|
||||
target.Attributes.Health.Value -= finalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — specific battle cores override this to enqueue
|
||||
/// animation actions into the <see cref="BattleActionMachine"/>.
|
||||
/// </summary>
|
||||
public void ScheduleAnimation(IBattleAnimation animation)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — cores implement this to advance the turn sequence
|
||||
/// when a side finishes its action phase.
|
||||
/// </summary>
|
||||
public void EndTurn(BattleSide side)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the battle outcome by delegating to
|
||||
/// <see cref="BattleSceneContext.ShowWinground"/> for player
|
||||
/// victory or <see cref="BattleSceneContext.ShowDeadground"/>
|
||||
/// for enemy victory.
|
||||
/// </summary>
|
||||
public void EndBattle(BattleSide winner)
|
||||
{
|
||||
if (winner == BattleSide.Player)
|
||||
_sceneContext.ShowWinground();
|
||||
else
|
||||
_sceneContext.ShowDeadground();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Default IBattleContext wired to the current BattleSceneContext.
|
||||
/// ApplyDamage clamps at 0 (no negative health from defence overflow),
|
||||
/// EndBattle delegates to ShowWinground/ShowDeadground on the scene
|
||||
/// context. ScheduleAnimation and EndTurn are stubs — specific battle
|
||||
/// cores are expected to override or supplement this behaviour.
|
||||
/// </summary>
|
||||
public class BattleContextImpl : IBattleContext
|
||||
{
|
||||
private readonly BattleSceneContext _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Root Godot <see cref="Node"/> of the battle scene, used by
|
||||
/// animation actions to parent spawned visual effects.
|
||||
/// </summary>
|
||||
public Node RootNode => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// The owning <see cref="BattleSceneContext"/> that manages
|
||||
/// the battle lifecycle.
|
||||
/// </summary>
|
||||
public BattleSceneContext SceneContext => _sceneContext;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context adapter around the given scene context.
|
||||
/// </summary>
|
||||
public BattleContextImpl(BattleSceneContext sceneContext)
|
||||
{
|
||||
_sceneContext = sceneContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts <paramref name="defence"/> from
|
||||
/// <paramref name="rawDamage"/>, clamping at 0 to prevent negative
|
||||
/// health from defence overflow, and applies the result to
|
||||
/// <paramref name="target"/>'s health.
|
||||
/// </summary>
|
||||
public void ApplyDamage(Character source, Character target, int rawDamage, int defence)
|
||||
{
|
||||
var finalDamage = Mathf.Max(0, rawDamage - defence);
|
||||
target.Attributes.Health.Value -= finalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — specific battle cores override this to enqueue
|
||||
/// animation actions into the <see cref="BattleActionMachine"/>.
|
||||
/// </summary>
|
||||
public void ScheduleAnimation(IBattleAnimation animation)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub — cores implement this to advance the turn sequence
|
||||
/// when a side finishes its action phase.
|
||||
/// </summary>
|
||||
public void EndTurn(BattleSide side)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the battle outcome by delegating to
|
||||
/// <see cref="BattleSceneContext.ShowWinground"/> for player
|
||||
/// victory or <see cref="BattleSceneContext.ShowDeadground"/>
|
||||
/// for enemy victory.
|
||||
/// </summary>
|
||||
public void EndBattle(BattleSide winner)
|
||||
{
|
||||
if (winner == BattleSide.Player)
|
||||
_sceneContext.ShowWinground();
|
||||
else
|
||||
_sceneContext.ShowDeadground();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a battle animation without binding to a concrete player.
|
||||
/// Carries Source/Target characters, an AnimationType key for dispatch,
|
||||
/// a Speed multiplier, and an open-ended Parameters dictionary so
|
||||
/// custom animations can pass arbitrary data (damage value, element
|
||||
/// type, etc.) without interface changes.
|
||||
/// </summary>
|
||||
public interface IBattleAnimation
|
||||
{
|
||||
/// <summary>The <see cref="Character"/> performing the action.</summary>
|
||||
Character Source { get; }
|
||||
/// <summary>The <see cref="Character"/> receiving the action.</summary>
|
||||
Character Target { get; }
|
||||
/// <summary>
|
||||
/// Key used by <see cref="AnimationRegistry"/> to dispatch to the
|
||||
/// correct <see cref="IBattleAnimationPlayer"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
/// <summary>Multiplier for the playback speed (1.0 = normal).</summary>
|
||||
float Speed { get; }
|
||||
/// <summary>
|
||||
/// Open-ended data bag for animation-specific values
|
||||
/// (e.g. damage amount, elemental flag) — no interface changes
|
||||
/// needed when adding new animation types.
|
||||
/// </summary>
|
||||
Dictionary<string, object> Parameters { get; }
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a battle animation without binding to a concrete player.
|
||||
/// Carries Source/Target characters, an AnimationType key for dispatch,
|
||||
/// a Speed multiplier, and an open-ended Parameters dictionary so
|
||||
/// custom animations can pass arbitrary data (damage value, element
|
||||
/// type, etc.) without interface changes.
|
||||
/// </summary>
|
||||
public interface IBattleAnimation
|
||||
{
|
||||
/// <summary>The <see cref="Character"/> performing the action.</summary>
|
||||
Character Source { get; }
|
||||
/// <summary>The <see cref="Character"/> receiving the action.</summary>
|
||||
Character Target { get; }
|
||||
/// <summary>
|
||||
/// Key used by <see cref="AnimationRegistry"/> to dispatch to the
|
||||
/// correct <see cref="IBattleAnimationPlayer"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
/// <summary>Multiplier for the playback speed (1.0 = normal).</summary>
|
||||
float Speed { get; }
|
||||
/// <summary>
|
||||
/// Open-ended data bag for animation-specific values
|
||||
/// (e.g. damage amount, elemental flag) — no interface changes
|
||||
/// needed when adding new animation types.
|
||||
/// </summary>
|
||||
Dictionary<string, object> Parameters { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a specific animation type. Each player is keyed by
|
||||
/// AnimationType so the registry can dispatch animations without
|
||||
/// knowing concrete player types. The onComplete callback chains
|
||||
/// into the action machine's polling loop.
|
||||
/// </summary>
|
||||
public interface IBattleAnimationPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// String key used by <see cref="AnimationRegistry"/> to select
|
||||
/// this player for a given <see cref="IBattleAnimation.AnimationType"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts playing the animation described by <paramref name="anim"/>.
|
||||
/// Must invoke <paramref name="onComplete"/> once the animation
|
||||
/// finishes, so the action machine can advance to the next step.
|
||||
/// </summary>
|
||||
void Play(IBattleAnimation anim, Action onComplete);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a specific animation type. Each player is keyed by
|
||||
/// AnimationType so the registry can dispatch animations without
|
||||
/// knowing concrete player types. The onComplete callback chains
|
||||
/// into the action machine's polling loop.
|
||||
/// </summary>
|
||||
public interface IBattleAnimationPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// String key used by <see cref="AnimationRegistry"/> to select
|
||||
/// this player for a given <see cref="IBattleAnimation.AnimationType"/>.
|
||||
/// </summary>
|
||||
string AnimationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts playing the animation described by <paramref name="anim"/>.
|
||||
/// Must invoke <paramref name="onComplete"/> once the animation
|
||||
/// finishes, so the action machine can advance to the next step.
|
||||
/// </summary>
|
||||
void Play(IBattleAnimation anim, Action onComplete);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridge from the battle core back to the Godot scene. Cores work
|
||||
/// against this interface so they remain engine-agnostic.
|
||||
/// ApplyDamage computes final-damage-minus-defence and mutates
|
||||
/// the target's Health directly (no event — the UI layer polls).
|
||||
/// ScheduleAnimation is the hook for visual feedback; EndTurn/EndBattle
|
||||
/// are phase signals. RootNode gives cores a Godot anchor for
|
||||
/// instantiating nodes (e.g. damage numbers).
|
||||
/// </summary>
|
||||
public interface IBattleContext
|
||||
{
|
||||
void ApplyDamage(Character source, Character target, int rawDamage, int defence);
|
||||
|
||||
void ScheduleAnimation(IBattleAnimation animation);
|
||||
|
||||
void EndTurn(BattleSide side);
|
||||
|
||||
void EndBattle(BattleSide winner);
|
||||
|
||||
Node RootNode { get; }
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridge from the battle core back to the Godot scene. Cores work
|
||||
/// against this interface so they remain engine-agnostic.
|
||||
/// ApplyDamage computes final-damage-minus-defence and mutates
|
||||
/// the target's Health directly (no event — the UI layer polls).
|
||||
/// ScheduleAnimation is the hook for visual feedback; EndTurn/EndBattle
|
||||
/// are phase signals. RootNode gives cores a Godot anchor for
|
||||
/// instantiating nodes (e.g. damage numbers).
|
||||
/// </summary>
|
||||
public interface IBattleContext
|
||||
{
|
||||
void ApplyDamage(Character source, Character target, int rawDamage, int defence);
|
||||
|
||||
void ScheduleAnimation(IBattleAnimation animation);
|
||||
|
||||
void EndTurn(BattleSide side);
|
||||
|
||||
void EndBattle(BattleSide winner);
|
||||
|
||||
Node RootNode { get; }
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable battle engine contract. Each implementation is a
|
||||
/// self-contained battle rule set (turn order, AI, phases).
|
||||
/// Init receives the two character arrays and a context adapter;
|
||||
/// Start begins the loop. The ActionProvider property lets the
|
||||
/// core inject its own executor overrides via the hub.
|
||||
/// Cores are instantiated fresh per battle — they carry no
|
||||
/// persistent state between encounters.
|
||||
/// </summary>
|
||||
public interface IBattleCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this battle engine (e.g. "card_battle",
|
||||
/// "ff_battle"). Used by <see cref="BattleCoreRegistry"/> and
|
||||
/// <see cref="BattleData.ActiveBattleCore"/> for lookup.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that returns core-specific <see cref="IActionExecutor"/>
|
||||
/// overrides. Set on the <see cref="ActionExecutorHub"/> when
|
||||
/// this core activates, so actions route through custom logic
|
||||
/// before falling back to the global registry.
|
||||
/// </summary>
|
||||
IActionExecutorProvider ActionProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the core with the two character arrays and a
|
||||
/// <see cref="IBattleContext"/> for callbacks. Called once per
|
||||
/// battle before <see cref="Start"/>.
|
||||
/// </summary>
|
||||
void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Begins the turn loop. Cores may use coroutines, polling, or
|
||||
/// event-driven state machines — the core owns its own lifecycle
|
||||
/// after this call.
|
||||
/// </summary>
|
||||
void Start();
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable battle engine contract. Each implementation is a
|
||||
/// self-contained battle rule set (turn order, AI, phases).
|
||||
/// Init receives the two character arrays and a context adapter;
|
||||
/// Start begins the loop. The ActionProvider property lets the
|
||||
/// core inject its own executor overrides via the hub.
|
||||
/// Cores are instantiated fresh per battle — they carry no
|
||||
/// persistent state between encounters.
|
||||
/// </summary>
|
||||
public interface IBattleCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this battle engine (e.g. "card_battle",
|
||||
/// "ff_battle"). Used by <see cref="BattleCoreRegistry"/> and
|
||||
/// <see cref="BattleData.ActiveBattleCore"/> for lookup.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that returns core-specific <see cref="IActionExecutor"/>
|
||||
/// overrides. Set on the <see cref="ActionExecutorHub"/> when
|
||||
/// this core activates, so actions route through custom logic
|
||||
/// before falling back to the global registry.
|
||||
/// </summary>
|
||||
IActionExecutorProvider ActionProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the core with the two character arrays and a
|
||||
/// <see cref="IBattleContext"/> for callbacks. Called once per
|
||||
/// battle before <see cref="Start"/>.
|
||||
/// </summary>
|
||||
void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// Begins the turn loop. Cores may use coroutines, polling, or
|
||||
/// event-driven state machines — the core owns its own lifecycle
|
||||
/// after this call.
|
||||
/// </summary>
|
||||
void Start();
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -1,15 +1,15 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which side of the battle a character or turn belongs to.
|
||||
/// Used by IBattleContext.EndTurn and EndBattle to route results
|
||||
/// (victory vs defeat screen).
|
||||
/// </summary>
|
||||
public enum BattleSide
|
||||
{
|
||||
/// <summary>The player's party.</summary>
|
||||
Player,
|
||||
/// <summary>The opposing force (monsters, NPCs).</summary>
|
||||
Enemy,
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which side of the battle a character or turn belongs to.
|
||||
/// Used by IBattleContext.EndTurn and EndBattle to route results
|
||||
/// (victory vs defeat screen).
|
||||
/// </summary>
|
||||
public enum BattleSide
|
||||
{
|
||||
/// <summary>The player's party.</summary>
|
||||
Player,
|
||||
/// <summary>The opposing force (monsters, NPCs).</summary>
|
||||
Enemy,
|
||||
}
|
||||
}
|
||||
+62
-62
@@ -1,62 +1,62 @@
|
||||
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convenience base for IActionExecutor implementations. Provides
|
||||
/// CheckRequiredAndUsePoint which validates that the user has enough
|
||||
/// "Point" attribute (a generic action-point / mana pool) before
|
||||
/// deducting it — returning false lets the action fail gracefully.
|
||||
/// ID is abstract so each action declares its own identity.
|
||||
/// </summary>
|
||||
public abstract class ActionBase : IActionExecutor
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maps to the abstract <see cref="ID"/> so the hub can resolve
|
||||
/// this executor by action name.
|
||||
/// </summary>
|
||||
public string ActionId => ID;
|
||||
|
||||
/// <summary>
|
||||
/// Unique action identifier that matches
|
||||
/// <see cref="ActionCharacter.ID"/> in data.
|
||||
/// </summary>
|
||||
public abstract string ID { get; }
|
||||
/// <summary>
|
||||
/// Applies the action's effects to <paramref name="target"/>
|
||||
/// using <paramref name="user"/>'s stats and the
|
||||
/// <paramref name="action"/> definition. Returns stat deltas
|
||||
/// and a success/failure flag.
|
||||
/// </summary>
|
||||
public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <paramref name="user"/> has at least
|
||||
/// <c>"RequiredPoint"</c> remaining, deducts it, and
|
||||
/// returns <c>true</c>. Returns <c>false</c> with a warning log
|
||||
/// if the user lacks sufficient points, allowing the action to
|
||||
/// fail gracefully.
|
||||
/// </summary>
|
||||
protected bool CheckRequiredAndUsePoint(ActionCharacter action, Character user)
|
||||
{
|
||||
var requiredPoint = action.GetInt("RequiredPoint");
|
||||
|
||||
if (user.Attributes.Point.Value < requiredPoint)
|
||||
{
|
||||
GameLogger.Log("BATTLE", user.ID + " not checked point required [" + user.Attributes.Point.Value + "] for action " + action.ID + " [" + requiredPoint + "]", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", user.ID + " use [" + requiredPoint + "] points for action " + action.ID, LogLevel.Debug);
|
||||
|
||||
user.Attributes.Point.Value -= requiredPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Convenience base for IActionExecutor implementations. Provides
|
||||
/// CheckRequiredAndUsePoint which validates that the user has enough
|
||||
/// "Point" attribute (a generic action-point / mana pool) before
|
||||
/// deducting it — returning false lets the action fail gracefully.
|
||||
/// ID is abstract so each action declares its own identity.
|
||||
/// </summary>
|
||||
public abstract class ActionBase : IActionExecutor
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Maps to the abstract <see cref="ID"/> so the hub can resolve
|
||||
/// this executor by action name.
|
||||
/// </summary>
|
||||
public string ActionId => ID;
|
||||
|
||||
/// <summary>
|
||||
/// Unique action identifier that matches
|
||||
/// <see cref="ActionCharacter.ID"/> in data.
|
||||
/// </summary>
|
||||
public abstract string ID { get; }
|
||||
/// <summary>
|
||||
/// Applies the action's effects to <paramref name="target"/>
|
||||
/// using <paramref name="user"/>'s stats and the
|
||||
/// <paramref name="action"/> definition. Returns stat deltas
|
||||
/// and a success/failure flag.
|
||||
/// </summary>
|
||||
public abstract ChangedAttributes Execute(ActionCharacter action, Character user, Character target);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that <paramref name="user"/> has at least
|
||||
/// <c>"RequiredPoint"</c> remaining, deducts it, and
|
||||
/// returns <c>true</c>. Returns <c>false</c> with a warning log
|
||||
/// if the user lacks sufficient points, allowing the action to
|
||||
/// fail gracefully.
|
||||
/// </summary>
|
||||
protected bool CheckRequiredAndUsePoint(ActionCharacter action, Character user)
|
||||
{
|
||||
var requiredPoint = action.GetInt("RequiredPoint");
|
||||
|
||||
if (user.Attributes.Point.Value < requiredPoint)
|
||||
{
|
||||
GameLogger.Log("BATTLE", user.ID + " not checked point required [" + user.Attributes.Point.Value + "] for action " + action.ID + " [" + requiredPoint + "]", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", user.ID + " use [" + requiredPoint + "] points for action " + action.ID, LogLevel.Debug);
|
||||
|
||||
user.Attributes.Point.Value -= requiredPoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+98
-98
@@ -1,98 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Quests;
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable descriptor for a battle encounter, constructed via the
|
||||
/// static factory <see cref="InitBattle"/> overloads. Holds the
|
||||
/// background texture, enemy roster, quest reference, card order side,
|
||||
/// and the scene to return to when the battle ends. The private
|
||||
/// constructor forces creation through the factories so that all
|
||||
/// required data is supplied upfront.
|
||||
/// </summary>
|
||||
public class BattleData
|
||||
{
|
||||
/// <summary>The quest that triggered this battle; may be <c>null</c>
|
||||
/// for non-quest skirmishes.</summary>
|
||||
public QuestBase Quest { get; set; }
|
||||
/// <summary>Auxiliary tag set on new characters acquired via recruitment
|
||||
/// after this battle.</summary>
|
||||
public string NewTag { get; set; }
|
||||
|
||||
/// <summary>List of enemy <see cref="Character"/> instances spawned from
|
||||
/// the provided IDs or default templates.</summary>
|
||||
public List<Character> EnemiesCards { get; private set; }
|
||||
/// <summary>Battle background texture (full-resolution).</summary>
|
||||
public Texture2D Background { get; private set; }
|
||||
/// <summary>Depth map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D DepthMap { get; set; }
|
||||
/// <summary>Albedo/colour map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D AlbedoMap { get; set; }
|
||||
/// <summary>Which side of the screen the player's cards are drawn on.</summary>
|
||||
public BattleSide BattleSide { get; private set; } = BattleSide.Player;
|
||||
/// <summary>Identifier for the active battle core logic module.</summary>
|
||||
public string ActiveBattleCore { get; set; }
|
||||
/// <summary>Scene name to transition to when the battle finishes or is
|
||||
/// fled. Stored as a string to avoid type coupling.</summary>
|
||||
public string ReturnScene { get; private set; }
|
||||
|
||||
private BattleData()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that defaults <see cref="BattleSide"/> to
|
||||
/// <c>BattleSide.Player</c>. Constructs enemies from their string IDs
|
||||
/// via <see cref="Cthangover.Core.Factories.Impls.CharacterFactory"/>,
|
||||
/// falling back to a level-1 template when the ID is unknown.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies)
|
||||
{
|
||||
return InitBattle(background, returnScene, BattleSide.Player, enemies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a battle with a specified <see cref="BattleSide"/> and
|
||||
/// enemy IDs. Each ID is resolved through
|
||||
/// <see cref="Cthangover.Core.Factories.Impls.CharacterFactory.Get"/>.
|
||||
/// Unknown IDs produce a fallback <see cref="Character"/> with 20 HP
|
||||
/// and 3 action points.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, BattleSide cardOrder = BattleSide.Player, params string[] enemies)
|
||||
{
|
||||
var list = enemies.Select(o => CharacterFactory.Instance.Get(o) ?? new Character
|
||||
{
|
||||
ID = o,
|
||||
Attributes = new CharacterAttributes
|
||||
{
|
||||
Health = new Attribute { Value = 20, BaseValue = 20 },
|
||||
Point = new Attribute { Value = 3, BaseValue = 3 },
|
||||
},
|
||||
Level = 1,
|
||||
Exp = 10,
|
||||
}).ToList();
|
||||
return InitBattle(background, returnScene, list, cardOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core factory that directly accepts a pre-built
|
||||
/// <see cref="List{Character}"/> for the enemy roster. This is the
|
||||
/// overload that all other <c>InitBattle</c> variants delegate to.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, List<Character> enemiesCards, BattleSide cardOrder = BattleSide.Player)
|
||||
{
|
||||
return new BattleData
|
||||
{
|
||||
Background = background,
|
||||
ReturnScene = returnScene,
|
||||
EnemiesCards = enemiesCards,
|
||||
BattleSide = cardOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Quests;
|
||||
using Cthangover.Core.Characters;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable descriptor for a battle encounter, constructed via the
|
||||
/// static factory <see cref="InitBattle"/> overloads. Holds the
|
||||
/// background texture, enemy roster, quest reference, card order side,
|
||||
/// and the scene to return to when the battle ends. The private
|
||||
/// constructor forces creation through the factories so that all
|
||||
/// required data is supplied upfront.
|
||||
/// </summary>
|
||||
public class BattleData
|
||||
{
|
||||
/// <summary>The quest that triggered this battle; may be <c>null</c>
|
||||
/// for non-quest skirmishes.</summary>
|
||||
public QuestBase Quest { get; set; }
|
||||
/// <summary>Auxiliary tag set on new characters acquired via recruitment
|
||||
/// after this battle.</summary>
|
||||
public string NewTag { get; set; }
|
||||
|
||||
/// <summary>List of enemy <see cref="Character"/> instances spawned from
|
||||
/// the provided IDs or default templates.</summary>
|
||||
public List<Character> EnemiesCards { get; private set; }
|
||||
/// <summary>Battle background texture (full-resolution).</summary>
|
||||
public Texture2D Background { get; private set; }
|
||||
/// <summary>Depth map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D DepthMap { get; set; }
|
||||
/// <summary>Albedo/colour map texture for the parallax post-process effect.</summary>
|
||||
public Texture2D AlbedoMap { get; set; }
|
||||
/// <summary>Which side of the screen the player's cards are drawn on.</summary>
|
||||
public BattleSide BattleSide { get; private set; } = BattleSide.Player;
|
||||
/// <summary>Identifier for the active battle core logic module.</summary>
|
||||
public string ActiveBattleCore { get; set; }
|
||||
/// <summary>Scene name to transition to when the battle finishes or is
|
||||
/// fled. Stored as a string to avoid type coupling.</summary>
|
||||
public string ReturnScene { get; private set; }
|
||||
|
||||
private BattleData()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that defaults <see cref="BattleSide"/> to
|
||||
/// <c>BattleSide.Player</c>. Constructs enemies from their string IDs
|
||||
/// via <see cref="Cthangover.Core.Factories.Impls.CharacterFactory"/>,
|
||||
/// falling back to a level-1 template when the ID is unknown.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, params string[] enemies)
|
||||
{
|
||||
return InitBattle(background, returnScene, BattleSide.Player, enemies);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a battle with a specified <see cref="BattleSide"/> and
|
||||
/// enemy IDs. Each ID is resolved through
|
||||
/// <see cref="Cthangover.Core.Factories.Impls.CharacterFactory.Get"/>.
|
||||
/// Unknown IDs produce a fallback <see cref="Character"/> with 20 HP
|
||||
/// and 3 action points.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, BattleSide cardOrder = BattleSide.Player, params string[] enemies)
|
||||
{
|
||||
var list = enemies.Select(o => CharacterFactory.Instance.Get(o) ?? new Character
|
||||
{
|
||||
ID = o,
|
||||
Attributes = new CharacterAttributes
|
||||
{
|
||||
Health = new Attribute { Value = 20, BaseValue = 20 },
|
||||
Point = new Attribute { Value = 3, BaseValue = 3 },
|
||||
},
|
||||
Level = 1,
|
||||
Exp = 10,
|
||||
}).ToList();
|
||||
return InitBattle(background, returnScene, list, cardOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core factory that directly accepts a pre-built
|
||||
/// <see cref="List{Character}"/> for the enemy roster. This is the
|
||||
/// overload that all other <c>InitBattle</c> variants delegate to.
|
||||
/// </summary>
|
||||
public static BattleData InitBattle(Texture2D background, string returnScene, List<Character> enemiesCards, BattleSide cardOrder = BattleSide.Player)
|
||||
{
|
||||
return new BattleData
|
||||
{
|
||||
Background = background,
|
||||
ReturnScene = returnScene,
|
||||
EnemiesCards = enemiesCards,
|
||||
BattleSide = cardOrder,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+366
-360
@@ -1,362 +1,368 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Time;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Time;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton scene-root for battle, wired as a self-registering Node
|
||||
/// (sets Instance on enter-tree, clears on exit-tree). On ready it
|
||||
/// assembles player characters from CharacterData.BattleSet, enemy
|
||||
/// cards from the pending <see cref="BattleRequest"/>, resolves the active
|
||||
/// <see cref="IBattleCore"/> via <see cref="BattleCoreRegistry"/>, and defers
|
||||
/// Init+Start to the next frame so the scene tree is fully built before core
|
||||
/// logic runs. Tracks PlayerExpGained (per-character, split evenly among
|
||||
/// survivors) and DefeatedEnemies for the victory/loot screen. Exposes
|
||||
/// OnCharacterDied and OnBattleCleared events so external systems (e.g.
|
||||
/// quests, the action machine) can react without coupling.
|
||||
/// ShowWinground / ShowDeadground are mutually exclusive end-of-battle
|
||||
/// paths — both set IsDestroyed to prevent double-triggering and
|
||||
/// reset battle speed before clearing state.
|
||||
/// <para/>
|
||||
/// The context derives from <see cref="ModNode"/> because mod assemblies get
|
||||
/// no Godot source generation: the core base receives the engine lifecycle
|
||||
/// callbacks and forwards them to the <c>OnXxx</c> overrides. It also
|
||||
/// implements <see cref="IActiveCharacterProvider"/> so core UI (the inventory
|
||||
/// panel's action-point row) can read the acting character without knowing
|
||||
/// anything about battle.
|
||||
/// </summary>
|
||||
public partial class BattleSceneContext : ModNode, IActiveCharacterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton reference to the active battle scene context.
|
||||
/// Set on enter-tree, cleared on exit-tree.
|
||||
/// External systems read this to access battle state.
|
||||
/// </summary>
|
||||
public static BattleSceneContext Instance { get; private set; }
|
||||
|
||||
private TextureRect battleBg;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Actions.BattleActionMachine"/> node that
|
||||
/// processes the animation queue for this battle. Cores push
|
||||
/// animation actions through the context and the machine drives
|
||||
/// playback frame-by-frame.
|
||||
/// </summary>
|
||||
public Actions.BattleActionMachine BattleActionMachine { get; private set; }
|
||||
|
||||
private IBattleSpeedControl _battleSpeedControl;
|
||||
private Widget deadground;
|
||||
private Widget winground;
|
||||
|
||||
/// <summary>
|
||||
/// Per-character EXP earned during this battle, keyed by
|
||||
/// <see cref="Character.ID"/>. Populated by
|
||||
/// <see cref="RecordEnemyDefeated"/>; consumed by the victory screen
|
||||
/// provided by the turn-battle GUI mod.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> PlayerExpGained { get; private set; } = new();
|
||||
/// <summary>
|
||||
/// Enemies defeated during this battle (for loot generation
|
||||
/// and recruitment rolls).
|
||||
/// </summary>
|
||||
public List<Character> DefeatedEnemies { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against double-triggering end-of-battle paths.
|
||||
/// Set to <c>true</c> by <see cref="ShowWinground"/> or
|
||||
/// <see cref="ShowDeadground"/>.
|
||||
/// </summary>
|
||||
public bool IsDestroyed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the battle core pauses processing (e.g.
|
||||
/// while a UI overlay is open). Set externally by pause widgets.
|
||||
/// </summary>
|
||||
public bool IsWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Character"/> whose turn is currently active
|
||||
/// in battle. Set by the battle core when a character is selected
|
||||
/// and cleared when the turn ends or selection is cancelled.
|
||||
/// <c>null</c> means no character is active (out of battle,
|
||||
/// between turns, or idle state). External panels such as the
|
||||
/// inventory panel read this to check whether the active
|
||||
/// character has enough action points before performing
|
||||
/// item-related actions.
|
||||
/// </summary>
|
||||
public Character ActiveCharacter { get; set; }
|
||||
|
||||
/// <summary>Fired when a character's health reaches 0.</summary>
|
||||
public event System.Action<Character> OnCharacterDied;
|
||||
/// <summary>
|
||||
/// Fired after battle state is cleared (both victory and defeat).
|
||||
/// External systems can hook this to release battle resources.
|
||||
/// </summary>
|
||||
public event System.Action OnBattleCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="OnCharacterDied"/> to notify listeners
|
||||
/// (quests, achievements, the battle core) that a character
|
||||
/// has been defeated.
|
||||
/// </summary>
|
||||
public void NotifyCharacterDied(Character character)
|
||||
{
|
||||
OnCharacterDied?.Invoke(character);
|
||||
ModEventBus.Publish(new CharacterDiedEvent
|
||||
{
|
||||
CharacterId = character?.ID,
|
||||
CharacterName = character?.Name,
|
||||
Source = "battle",
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Claims the scene singleton and registers as the <see cref="ActiveCharacterRegistry"/> provider.</summary>
|
||||
protected override void OnEnterTree()
|
||||
{
|
||||
if (Instance != null && GodotObject.IsInstanceValid(Instance))
|
||||
{
|
||||
var scene = GetTree()?.CurrentScene?.Name ?? "?";
|
||||
var existingPath = Instance.GetPath().ToString();
|
||||
var myPath = GetPath().ToString();
|
||||
GameLogger.Log("DUPLICATE", $"BattleSceneContext.OnEnterTree: Instance ALREADY SET by '{existingPath}', overwriting with duplicate at '{myPath}' on scene '{scene}'", LogLevel.Error);
|
||||
}
|
||||
Instance = this;
|
||||
ActiveCharacterRegistry.Provider = this;
|
||||
}
|
||||
|
||||
/// <summary>Releases the singleton slot and the active-character provider so a later scene can claim them.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
if (ReferenceEquals(ActiveCharacterRegistry.Provider, this))
|
||||
ActiveCharacterRegistry.Provider = null;
|
||||
}
|
||||
|
||||
private List<Character> pendingPlayerCards;
|
||||
private List<Character> pendingEnemyCards;
|
||||
private IBattleCore activeBattleCore;
|
||||
|
||||
/// <summary>
|
||||
/// The battle core driving this scene, resolved from
|
||||
/// <see cref="BattleCoreRegistry"/> when the scene becomes ready. Exposed
|
||||
/// so tooling and tests can query the active core (for example for
|
||||
/// <see cref="IBattleAutoPlayable"/>) without reaching into UI nodes.
|
||||
/// </summary>
|
||||
public IBattleCore ActiveBattleCore => activeBattleCore;
|
||||
|
||||
/// <summary>Caches the battle backdrop node, resolves the active <see cref="IBattleCore"/> and starts the battle.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
battleBg = GetNodeOrNull<TextureRect>("BattleBg");
|
||||
|
||||
if (battleBg == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
BattleActionMachine = GetNodeOrNull<Actions.BattleActionMachine>("ToolPanel/BattleActionMachine");
|
||||
_battleSpeedControl = GetNodeOrNull<Control>("BattleSpeedControl") as IBattleSpeedControl;
|
||||
deadground = GetNodeOrNull<Widget>("Deadground");
|
||||
|
||||
winground = GetNodeOrNull<Widget>("Winground");
|
||||
|
||||
var data = BattleRequest.Current;
|
||||
if (data == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "BattleRequest.Current is null!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", $"set battle background: battleBg={(battleBg != null ? "found" : "NULL")}, texture={(data.Background != null ? "has" : "NULL")}", battleBg == null || data.Background == null ? LogLevel.Error : LogLevel.Debug);
|
||||
|
||||
if (data.Background != null)
|
||||
{
|
||||
battleBg.Texture = data.Background;
|
||||
GameLogger.Log("BATTLE", $" battleBg.Texture assigned: width={battleBg.Texture.GetWidth()}, height={battleBg.Texture.GetHeight()}", LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", $" data.Background is NULL — no background texture set", LogLevel.Error);
|
||||
}
|
||||
|
||||
var timedShader = Shaders.Resolve("timed_sprite");
|
||||
if (timedShader != null)
|
||||
{
|
||||
var bgMaterial = new ShaderMaterial { Shader = timedShader };
|
||||
battleBg.Material = bgMaterial;
|
||||
|
||||
if (data.DepthMap != null)
|
||||
bgMaterial.SetShaderParameter("depth_mask", data.DepthMap);
|
||||
if (data.AlbedoMap != null)
|
||||
bgMaterial.SetShaderParameter("albedo_map", data.AlbedoMap);
|
||||
|
||||
var lights = GetNodeOrNull<UiLightController>("Lights");
|
||||
if (lights != null)
|
||||
{
|
||||
var viewportSize = GetViewport().GetVisibleRect().Size;
|
||||
lights.SetPlayerLight(viewportSize / 2, 500f, 1f);
|
||||
lights.RegisterMaterial(bgMaterial);
|
||||
}
|
||||
GameLogger.Log("BATTLE", $"battleBg shader: timed_sprite applied, depthMap={(data.DepthMap != null ? "has" : "NULL")}, albedoMap={(data.AlbedoMap != null ? "has" : "NULL")}, lightsCtrl={(lights != null ? "found" : "NULL")}", lights == null ? LogLevel.Error : LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg shader: timed_sprite NOT FOUND", LogLevel.Error);
|
||||
}
|
||||
|
||||
pendingPlayerCards = new List<Character>();
|
||||
foreach (var characterType in GameData.Instance.Runtime.CharacterData.BattleSet)
|
||||
{
|
||||
var characterInfo = GameData.Instance.Runtime.CharacterData.Characters[characterType];
|
||||
var template = CharacterFactory.Instance.Get(characterInfo.ID);
|
||||
var character = template != null ? template.Copy() : new Character { ID = characterInfo.ID };
|
||||
character.Attributes = characterInfo.Attributes ?? new CharacterAttributes();
|
||||
character.Level = characterInfo.Level;
|
||||
character.Exp = characterInfo.Exp;
|
||||
if (characterInfo.ActionSlots != null && characterInfo.ActionSlots.Any(id => !string.IsNullOrEmpty(id)))
|
||||
{
|
||||
character.Actions = characterInfo.ActionSlots
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Select(id => ActionCharacterFactory.Instance.Get(id))
|
||||
.Where(a => a != null)
|
||||
.ToList();
|
||||
}
|
||||
pendingPlayerCards.Add(character);
|
||||
}
|
||||
|
||||
pendingEnemyCards = data.EnemiesCards?.Where(c => c != null).ToList() ?? new List<Character>();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ActiveBattleCore) && BattleCoreRegistry.Instance.HasCore(data.ActiveBattleCore))
|
||||
BattleCoreRegistry.Instance.SetActive(data.ActiveBattleCore);
|
||||
|
||||
try
|
||||
{
|
||||
activeBattleCore = BattleCoreRegistry.Instance.GetActive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
activeBattleCore = null;
|
||||
}
|
||||
|
||||
if (activeBattleCore == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "No battle core available", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Callable.From(DeferredInit).CallDeferred();
|
||||
}
|
||||
|
||||
private void DeferredInit()
|
||||
{
|
||||
if (IsDestroyed || activeBattleCore == null)
|
||||
return;
|
||||
|
||||
var ctx = new BattleContextImpl(this);
|
||||
var playerArray = pendingPlayerCards?.ToArray() ?? new Character[0];
|
||||
var enemyArray = pendingEnemyCards?.ToArray() ?? new Character[0];
|
||||
|
||||
activeBattleCore.Init(playerArray, enemyArray, ctx);
|
||||
activeBattleCore.Start();
|
||||
|
||||
ModEventBus.Publish(new BattleStartedEvent
|
||||
{
|
||||
BattleType = activeBattleCore.GetType().Name,
|
||||
Enemies = enemyArray.Select(c => c.Name).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the defeat screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// deadground widget.
|
||||
/// </summary>
|
||||
public void ShowDeadground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle();
|
||||
deadground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the victory screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// winground widget.
|
||||
/// </summary>
|
||||
public void ShowWinground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle();
|
||||
winground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a defeated enemy: adds it to
|
||||
/// <see cref="DefeatedEnemies"/> and distributes its EXP
|
||||
/// (<c>Level * Exp</c>) equally among all surviving player
|
||||
/// characters in <see cref="PlayerExpGained"/>.
|
||||
/// </summary>
|
||||
public void RecordEnemyDefeated(Character enemy)
|
||||
{
|
||||
if (enemy == null)
|
||||
return;
|
||||
DefeatedEnemies.Add(enemy);
|
||||
|
||||
var enemyExp = enemy.Level * enemy.Exp;
|
||||
var alivePlayers = 0;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value > 0)
|
||||
alivePlayers++;
|
||||
}
|
||||
if (alivePlayers == 0)
|
||||
return;
|
||||
|
||||
var expPerPlayer = enemyExp / alivePlayers;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value <= 0)
|
||||
continue;
|
||||
var id = c.ID;
|
||||
if (id == null)
|
||||
continue;
|
||||
PlayerExpGained.TryGetValue(id, out var current);
|
||||
PlayerExpGained[id] = current + expPerPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearBattle()
|
||||
{
|
||||
BattleActionMachine?.StopMachine();
|
||||
ActiveCharacter = null;
|
||||
pendingPlayerCards?.Clear();
|
||||
pendingEnemyCards?.Clear();
|
||||
OnBattleCleared?.Invoke();
|
||||
ModEventBus.Publish(new BattleEndedEvent
|
||||
{
|
||||
Result = IsDestroyed ? "defeat" : "victory",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton scene-root for battle, wired as a self-registering Node
|
||||
/// (sets Instance on enter-tree, clears on exit-tree). On ready it
|
||||
/// assembles player characters from CharacterData.BattleSet, enemy
|
||||
/// cards from the pending <see cref="BattleRequest"/>, resolves the active
|
||||
/// <see cref="IBattleCore"/> via <see cref="BattleCoreRegistry"/>, and defers
|
||||
/// Init+Start to the next frame so the scene tree is fully built before core
|
||||
/// logic runs. Tracks PlayerExpGained (per-character, split evenly among
|
||||
/// survivors) and DefeatedEnemies for the victory/loot screen. Exposes
|
||||
/// OnCharacterDied and OnBattleCleared events so external systems (e.g.
|
||||
/// quests, the action machine) can react without coupling.
|
||||
/// ShowWinground / ShowDeadground are mutually exclusive end-of-battle
|
||||
/// paths — both set IsDestroyed to prevent double-triggering and
|
||||
/// reset battle speed before clearing state.
|
||||
/// <para/>
|
||||
/// The context derives from <see cref="ModNode"/> because mod assemblies get
|
||||
/// no Godot source generation: the core base receives the engine lifecycle
|
||||
/// callbacks and forwards them to the <c>OnXxx</c> overrides. It also
|
||||
/// implements <see cref="IActiveCharacterProvider"/> so core UI (the inventory
|
||||
/// panel's action-point row) can read the acting character without knowing
|
||||
/// anything about battle.
|
||||
/// </summary>
|
||||
public partial class BattleSceneContext : ModNode, IActiveCharacterProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton reference to the active battle scene context.
|
||||
/// Set on enter-tree, cleared on exit-tree.
|
||||
/// External systems read this to access battle state.
|
||||
/// </summary>
|
||||
public static BattleSceneContext Instance { get; private set; }
|
||||
|
||||
private TextureRect battleBg;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Actions.BattleActionMachine"/> node that
|
||||
/// processes the animation queue for this battle. Cores push
|
||||
/// animation actions through the context and the machine drives
|
||||
/// playback frame-by-frame.
|
||||
/// </summary>
|
||||
public Actions.BattleActionMachine BattleActionMachine { get; private set; }
|
||||
|
||||
private IBattleSpeedControl _battleSpeedControl;
|
||||
private Widget deadground;
|
||||
private Widget winground;
|
||||
|
||||
/// <summary>
|
||||
/// Per-character EXP earned during this battle, keyed by
|
||||
/// <see cref="Character.ID"/>. Populated by
|
||||
/// <see cref="RecordEnemyDefeated"/>; consumed by the victory screen
|
||||
/// provided by the turn-battle GUI mod.
|
||||
/// </summary>
|
||||
public Dictionary<string, int> PlayerExpGained { get; private set; } = new();
|
||||
/// <summary>
|
||||
/// Enemies defeated during this battle (for loot generation
|
||||
/// and recruitment rolls).
|
||||
/// </summary>
|
||||
public List<Character> DefeatedEnemies { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against double-triggering end-of-battle paths.
|
||||
/// Set to <c>true</c> by <see cref="ShowWinground"/> or
|
||||
/// <see cref="ShowDeadground"/>.
|
||||
/// </summary>
|
||||
public bool IsDestroyed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the battle core pauses processing (e.g.
|
||||
/// while a UI overlay is open). Set externally by pause widgets.
|
||||
/// </summary>
|
||||
public bool IsWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Character"/> whose turn is currently active
|
||||
/// in battle. Set by the battle core when a character is selected
|
||||
/// and cleared when the turn ends or selection is cancelled.
|
||||
/// <c>null</c> means no character is active (out of battle,
|
||||
/// between turns, or idle state). External panels such as the
|
||||
/// inventory panel read this to check whether the active
|
||||
/// character has enough action points before performing
|
||||
/// item-related actions.
|
||||
/// </summary>
|
||||
public Character ActiveCharacter { get; set; }
|
||||
|
||||
/// <summary>Fired when a character's health reaches 0.</summary>
|
||||
public event System.Action<Character> OnCharacterDied;
|
||||
/// <summary>
|
||||
/// Fired after battle state is cleared (both victory and defeat).
|
||||
/// External systems can hook this to release battle resources.
|
||||
/// </summary>
|
||||
public event System.Action OnBattleCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="OnCharacterDied"/> to notify listeners
|
||||
/// (quests, achievements, the battle core) that a character
|
||||
/// has been defeated.
|
||||
/// </summary>
|
||||
public void NotifyCharacterDied(Character character)
|
||||
{
|
||||
OnCharacterDied?.Invoke(character);
|
||||
ModEventBus.Publish(new CharacterDiedEvent
|
||||
{
|
||||
CharacterId = character?.ID,
|
||||
CharacterName = character?.Name,
|
||||
Source = "battle",
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Claims the scene singleton and registers as the <see cref="ActiveCharacterRegistry"/> provider.</summary>
|
||||
protected override void OnEnterTree()
|
||||
{
|
||||
if (Instance != null && GodotObject.IsInstanceValid(Instance))
|
||||
{
|
||||
var scene = GetTree()?.CurrentScene?.Name ?? "?";
|
||||
var existingPath = Instance.GetPath().ToString();
|
||||
var myPath = GetPath().ToString();
|
||||
GameLogger.Log("DUPLICATE", $"BattleSceneContext.OnEnterTree: Instance ALREADY SET by '{existingPath}', overwriting with duplicate at '{myPath}' on scene '{scene}'", LogLevel.Error);
|
||||
}
|
||||
Instance = this;
|
||||
ActiveCharacterRegistry.Provider = this;
|
||||
}
|
||||
|
||||
/// <summary>Releases the singleton slot and the active-character provider so a later scene can claim them.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
base.OnExitTree();
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
if (ReferenceEquals(ActiveCharacterRegistry.Provider, this))
|
||||
ActiveCharacterRegistry.Provider = null;
|
||||
}
|
||||
|
||||
private List<Character> pendingPlayerCards;
|
||||
private List<Character> pendingEnemyCards;
|
||||
private IBattleCore activeBattleCore;
|
||||
|
||||
/// <summary>
|
||||
/// The battle core driving this scene, resolved from
|
||||
/// <see cref="BattleCoreRegistry"/> when the scene becomes ready. Exposed
|
||||
/// so tooling and tests can query the active core (for example for
|
||||
/// <see cref="IBattleAutoPlayable"/>) without reaching into UI nodes.
|
||||
/// </summary>
|
||||
public IBattleCore ActiveBattleCore => activeBattleCore;
|
||||
|
||||
/// <summary>Caches the battle backdrop node, resolves the active <see cref="IBattleCore"/> and starts the battle.</summary>
|
||||
protected override void OnReady()
|
||||
{
|
||||
battleBg = GetNodeOrNull<TextureRect>("BattleBg");
|
||||
|
||||
if (battleBg == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg not found!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
BattleActionMachine = GetNodeOrNull<Actions.BattleActionMachine>("ToolPanel/BattleActionMachine");
|
||||
_battleSpeedControl = GetNodeOrNull<Control>("BattleSpeedControl") as IBattleSpeedControl;
|
||||
deadground = GetNodeOrNull<Widget>("Deadground");
|
||||
|
||||
winground = GetNodeOrNull<Widget>("Winground");
|
||||
|
||||
var data = BattleRequest.Current;
|
||||
if (data == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "BattleRequest.Current is null!", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("BATTLE", $"set battle background: battleBg={(battleBg != null ? "found" : "NULL")}, texture={(data.Background != null ? "has" : "NULL")}", battleBg == null || data.Background == null ? LogLevel.Error : LogLevel.Debug);
|
||||
|
||||
if (data.Background != null)
|
||||
{
|
||||
battleBg.Texture = data.Background;
|
||||
GameLogger.Log("BATTLE", $" battleBg.Texture assigned: width={battleBg.Texture.GetWidth()}, height={battleBg.Texture.GetHeight()}", LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", $" data.Background is NULL — no background texture set", LogLevel.Error);
|
||||
}
|
||||
|
||||
var timedShader = Shaders.Resolve("timed_sprite");
|
||||
if (timedShader != null)
|
||||
{
|
||||
var bgMaterial = new ShaderMaterial { Shader = timedShader };
|
||||
battleBg.Material = bgMaterial;
|
||||
|
||||
if (data.DepthMap != null)
|
||||
bgMaterial.SetShaderParameter("depth_mask", data.DepthMap);
|
||||
if (data.AlbedoMap != null)
|
||||
bgMaterial.SetShaderParameter("albedo_map", data.AlbedoMap);
|
||||
|
||||
var lights = GetNodeOrNull<UiLightController>("Lights");
|
||||
if (lights != null)
|
||||
{
|
||||
var viewportSize = GetViewport().GetVisibleRect().Size;
|
||||
lights.SetPlayerLight(viewportSize / 2, 500f, 1f);
|
||||
lights.RegisterMaterial(bgMaterial);
|
||||
}
|
||||
GameLogger.Log("BATTLE", $"battleBg shader: timed_sprite applied, depthMap={(data.DepthMap != null ? "has" : "NULL")}, albedoMap={(data.AlbedoMap != null ? "has" : "NULL")}, lightsCtrl={(lights != null ? "found" : "NULL")}", lights == null ? LogLevel.Error : LogLevel.Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("BATTLE", "battleBg shader: timed_sprite NOT FOUND", LogLevel.Error);
|
||||
}
|
||||
|
||||
pendingPlayerCards = new List<Character>();
|
||||
foreach (var characterType in GameData.Instance.Runtime.CharacterData.BattleSet)
|
||||
{
|
||||
var characterInfo = GameData.Instance.Runtime.CharacterData.Characters[characterType];
|
||||
var template = CharacterFactory.Instance.Get(characterInfo.ID);
|
||||
var character = template != null ? template.Copy() : new Character { ID = characterInfo.ID };
|
||||
character.Attributes = characterInfo.Attributes ?? new CharacterAttributes();
|
||||
character.Level = characterInfo.Level;
|
||||
character.Exp = characterInfo.Exp;
|
||||
if (characterInfo.ActionSlots != null && characterInfo.ActionSlots.Any(id => !string.IsNullOrEmpty(id)))
|
||||
{
|
||||
character.Actions = characterInfo.ActionSlots
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.Select(id => ActionCharacterFactory.Instance.Get(id))
|
||||
.Where(a => a != null)
|
||||
.ToList();
|
||||
}
|
||||
pendingPlayerCards.Add(character);
|
||||
}
|
||||
|
||||
pendingEnemyCards = data.EnemiesCards?.Where(c => c != null).ToList() ?? new List<Character>();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ActiveBattleCore) && BattleCoreRegistry.Instance.HasCore(data.ActiveBattleCore))
|
||||
BattleCoreRegistry.Instance.SetActive(data.ActiveBattleCore);
|
||||
|
||||
try
|
||||
{
|
||||
activeBattleCore = BattleCoreRegistry.Instance.GetActive();
|
||||
}
|
||||
catch
|
||||
{
|
||||
activeBattleCore = null;
|
||||
}
|
||||
|
||||
if (activeBattleCore == null)
|
||||
{
|
||||
GameLogger.Log("BATTLE", "No battle core available", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Callable.From(DeferredInit).CallDeferred();
|
||||
}
|
||||
|
||||
private void DeferredInit()
|
||||
{
|
||||
if (IsDestroyed || activeBattleCore == null)
|
||||
return;
|
||||
|
||||
var ctx = new BattleContextImpl(this);
|
||||
var playerArray = pendingPlayerCards?.ToArray() ?? new Character[0];
|
||||
var enemyArray = pendingEnemyCards?.ToArray() ?? new Character[0];
|
||||
|
||||
activeBattleCore.Init(playerArray, enemyArray, ctx);
|
||||
activeBattleCore.Start();
|
||||
|
||||
ModEventBus.Publish(new BattleStartedEvent
|
||||
{
|
||||
BattleType = activeBattleCore.GetType().Name,
|
||||
Enemies = enemyArray.Select(c => c.Name).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the defeat screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// deadground widget.
|
||||
/// </summary>
|
||||
public void ShowDeadground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("defeat");
|
||||
deadground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the victory screen: sets <see cref="IsDestroyed"/>,
|
||||
/// resets battle speed, clears battle state, and shows the
|
||||
/// winground widget.
|
||||
/// </summary>
|
||||
public void ShowWinground()
|
||||
{
|
||||
if (IsDestroyed)
|
||||
return;
|
||||
IsDestroyed = true;
|
||||
_battleSpeedControl?.ResetToNormal();
|
||||
ClearBattle("victory");
|
||||
winground?.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a defeated enemy: adds it to
|
||||
/// <see cref="DefeatedEnemies"/> and distributes its EXP
|
||||
/// (<c>Level * Exp</c>) equally among all surviving player
|
||||
/// characters in <see cref="PlayerExpGained"/>.
|
||||
/// </summary>
|
||||
public void RecordEnemyDefeated(Character enemy)
|
||||
{
|
||||
if (enemy == null)
|
||||
return;
|
||||
DefeatedEnemies.Add(enemy);
|
||||
|
||||
var enemyExp = enemy.Level * enemy.Exp;
|
||||
var alivePlayers = 0;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value > 0)
|
||||
alivePlayers++;
|
||||
}
|
||||
if (alivePlayers == 0)
|
||||
return;
|
||||
|
||||
var expPerPlayer = enemyExp / alivePlayers;
|
||||
foreach (var c in pendingPlayerCards)
|
||||
{
|
||||
if (c?.Attributes?.Health?.Value <= 0)
|
||||
continue;
|
||||
var id = c.ID;
|
||||
if (id == null)
|
||||
continue;
|
||||
PlayerExpGained.TryGetValue(id, out var current);
|
||||
PlayerExpGained[id] = current + expPerPlayer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the battle and publishes <see cref="BattleEndedEvent"/>. The
|
||||
/// outcome is passed in by the caller: both screens set
|
||||
/// <see cref="IsDestroyed"/> before clearing, so it cannot be derived
|
||||
/// from that flag (it used to be, and every battle reported "defeat").
|
||||
/// </summary>
|
||||
private void ClearBattle(string result)
|
||||
{
|
||||
BattleActionMachine?.StopMachine();
|
||||
ActiveCharacter = null;
|
||||
pendingPlayerCards?.Clear();
|
||||
pendingEnemyCards?.Clear();
|
||||
OnBattleCleared?.Invoke();
|
||||
ModEventBus.Publish(new BattleEndedEvent
|
||||
{
|
||||
Result = result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-39
@@ -1,40 +1,40 @@
|
||||
using Cthangover.Core.Actions;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects which battle core (combat ruleset) the next battle will use.
|
||||
/// The "core" variable maps to a <see cref="BattleCoreRegistry"/> entry by
|
||||
/// string ID. Must be called before a battle starts, as
|
||||
/// <see cref="BattleService.Init"/> reads the active core at initialization time.
|
||||
/// </summary>
|
||||
public class BattleSetCoreAction : IScenarioAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Registered as "battle.set_core" — selects the combat ruleset
|
||||
/// for the next battle encounter. If "core" is empty or missing, logs a
|
||||
/// warning and returns without modifying the registry.
|
||||
/// </summary>
|
||||
public string Name => "battle.set_core";
|
||||
|
||||
/// <summary>
|
||||
/// Reads the "core" variable and sets it as the active battle core
|
||||
/// via <see cref="BattleCoreRegistry.SetActive"/>. The core determines
|
||||
/// combat mechanics (turn order, ability system, etc.) for the
|
||||
/// next initiated battle.
|
||||
/// </summary>
|
||||
public void Run(IActionContext ctx)
|
||||
{
|
||||
var core = ctx.GetParam("core");
|
||||
if (string.IsNullOrEmpty(core))
|
||||
{
|
||||
ctx.Log("EVENT", "BattleSetCoreAction: missing 'core' variable");
|
||||
return;
|
||||
}
|
||||
|
||||
BattleCoreRegistry.Instance.SetActive(core);
|
||||
ctx.Log("EVENT", $"BattleSetCoreAction: battle core set to '{core}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Actions;
|
||||
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects which battle core (combat ruleset) the next battle will use.
|
||||
/// The "core" variable maps to a <see cref="BattleCoreRegistry"/> entry by
|
||||
/// string ID. Must be called before a battle starts, as
|
||||
/// <see cref="BattleService.Init"/> reads the active core at initialization time.
|
||||
/// </summary>
|
||||
public class BattleSetCoreAction : IScenarioAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Registered as "battle.set_core" — selects the combat ruleset
|
||||
/// for the next battle encounter. If "core" is empty or missing, logs a
|
||||
/// warning and returns without modifying the registry.
|
||||
/// </summary>
|
||||
public string Name => "battle.set_core";
|
||||
|
||||
/// <summary>
|
||||
/// Reads the "core" variable and sets it as the active battle core
|
||||
/// via <see cref="BattleCoreRegistry.SetActive"/>. The core determines
|
||||
/// combat mechanics (turn order, ability system, etc.) for the
|
||||
/// next initiated battle.
|
||||
/// </summary>
|
||||
public void Run(IActionContext ctx)
|
||||
{
|
||||
var core = ctx.GetParam("core");
|
||||
if (string.IsNullOrEmpty(core))
|
||||
{
|
||||
ctx.Log("EVENT", "BattleSetCoreAction: missing 'core' variable");
|
||||
return;
|
||||
}
|
||||
|
||||
BattleCoreRegistry.Instance.SetActive(core);
|
||||
ctx.Log("EVENT", $"BattleSetCoreAction: battle core set to '{core}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+110
-110
@@ -1,110 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
|
||||
/// Godot.Engine.TimeScale to globally accelerate the entire game loop —
|
||||
/// not just animations. Persists the choice to GameData.Settings
|
||||
/// so it survives scene reloads. <see cref="ResetToNormal"/> restores 1x
|
||||
/// without saving and is the part the core scene consumes through
|
||||
/// <see cref="IBattleSpeedControl"/> when battle ends.
|
||||
/// </summary>
|
||||
public partial class BattleSpeedControl : Control, IBattleSpeedControl
|
||||
{
|
||||
private readonly Dictionary<int, Button> _buttons = new();
|
||||
private int _activeSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the speed bar in the constructor. Godot never runs a
|
||||
/// <c>_Ready</c> override declared in a mod assembly, so the buttons and the
|
||||
/// saved-speed restore must happen here. Anchors are used instead of a
|
||||
/// viewport-size lookup because the viewport is unavailable before the node
|
||||
/// enters the tree.
|
||||
/// </summary>
|
||||
public BattleSpeedControl()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
var speeds = new[] { 1, 4, 8 };
|
||||
var hbox = new HBoxContainer { Name = "SpeedButtons" };
|
||||
hbox.Alignment = BoxContainer.AlignmentMode.Begin;
|
||||
|
||||
foreach (var speed in speeds)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = $"x{speed}",
|
||||
CustomMinimumSize = new Vector2(48, 32)
|
||||
};
|
||||
btn.Pressed += () => OnSpeedSelected(speed);
|
||||
hbox.AddChild(btn);
|
||||
_buttons[speed] = btn;
|
||||
}
|
||||
|
||||
AddChild(hbox);
|
||||
|
||||
AnchorLeft = 1f;
|
||||
AnchorRight = 1f;
|
||||
AnchorTop = 0f;
|
||||
AnchorBottom = 0f;
|
||||
OffsetLeft = -200f;
|
||||
OffsetRight = -20f;
|
||||
OffsetTop = 20f;
|
||||
OffsetBottom = 52f;
|
||||
|
||||
ApplySavedSpeed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the speed from <see cref="GameData.Settings.BattleSpeed"/>.
|
||||
/// Called on <c>_Ready</c> to reapply the saved preference.
|
||||
/// </summary>
|
||||
public void ApplySavedSpeed()
|
||||
{
|
||||
var speed = GameData.Instance.Settings.BattleSpeed;
|
||||
if (speed < 1 || speed > 8)
|
||||
speed = 1;
|
||||
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="Godot.Engine.TimeScale"/> to 1.0x without
|
||||
/// persisting to settings. Used when battle ends so the lobby
|
||||
/// and menus run at normal speed regardless of the player's
|
||||
/// in-battle preference.
|
||||
/// </summary>
|
||||
public void ResetToNormal()
|
||||
{
|
||||
Godot.Engine.TimeScale = 1.0f;
|
||||
}
|
||||
|
||||
private void OnSpeedSelected(int speed)
|
||||
{
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
|
||||
var settings = GameData.Instance.Settings;
|
||||
settings.BattleSpeed = speed;
|
||||
settings.Save();
|
||||
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
foreach (var kv in _buttons)
|
||||
{
|
||||
var isActive = kv.Key == _activeSpeed;
|
||||
kv.Value.Modulate = isActive ? UiTokens.Success : UiTokens.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
|
||||
/// Godot.Engine.TimeScale to globally accelerate the entire game loop —
|
||||
/// not just animations. Persists the choice to GameData.Settings
|
||||
/// so it survives scene reloads. <see cref="ResetToNormal"/> restores 1x
|
||||
/// without saving and is the part the core scene consumes through
|
||||
/// <see cref="IBattleSpeedControl"/> when battle ends.
|
||||
/// </summary>
|
||||
public partial class BattleSpeedControl : Control, IBattleSpeedControl
|
||||
{
|
||||
private readonly Dictionary<int, Button> _buttons = new();
|
||||
private int _activeSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the speed bar in the constructor. Godot never runs a
|
||||
/// <c>_Ready</c> override declared in a mod assembly, so the buttons and the
|
||||
/// saved-speed restore must happen here. Anchors are used instead of a
|
||||
/// viewport-size lookup because the viewport is unavailable before the node
|
||||
/// enters the tree.
|
||||
/// </summary>
|
||||
public BattleSpeedControl()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
var speeds = new[] { 1, 4, 8 };
|
||||
var hbox = new HBoxContainer { Name = "SpeedButtons" };
|
||||
hbox.Alignment = BoxContainer.AlignmentMode.Begin;
|
||||
|
||||
foreach (var speed in speeds)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = $"x{speed}",
|
||||
CustomMinimumSize = new Vector2(48, 32)
|
||||
};
|
||||
btn.Pressed += () => OnSpeedSelected(speed);
|
||||
hbox.AddChild(btn);
|
||||
_buttons[speed] = btn;
|
||||
}
|
||||
|
||||
AddChild(hbox);
|
||||
|
||||
AnchorLeft = 1f;
|
||||
AnchorRight = 1f;
|
||||
AnchorTop = 0f;
|
||||
AnchorBottom = 0f;
|
||||
OffsetLeft = -200f;
|
||||
OffsetRight = -20f;
|
||||
OffsetTop = 20f;
|
||||
OffsetBottom = 52f;
|
||||
|
||||
ApplySavedSpeed();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the speed from <see cref="GameData.Settings.BattleSpeed"/>.
|
||||
/// Called on <c>_Ready</c> to reapply the saved preference.
|
||||
/// </summary>
|
||||
public void ApplySavedSpeed()
|
||||
{
|
||||
var speed = GameData.Instance.Settings.BattleSpeed;
|
||||
if (speed < 1 || speed > 8)
|
||||
speed = 1;
|
||||
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="Godot.Engine.TimeScale"/> to 1.0x without
|
||||
/// persisting to settings. Used when battle ends so the lobby
|
||||
/// and menus run at normal speed regardless of the player's
|
||||
/// in-battle preference.
|
||||
/// </summary>
|
||||
public void ResetToNormal()
|
||||
{
|
||||
Godot.Engine.TimeScale = 1.0f;
|
||||
}
|
||||
|
||||
private void OnSpeedSelected(int speed)
|
||||
{
|
||||
_activeSpeed = speed;
|
||||
Godot.Engine.TimeScale = speed;
|
||||
|
||||
var settings = GameData.Instance.Settings;
|
||||
settings.BattleSpeed = speed;
|
||||
settings.Save();
|
||||
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
foreach (var kv in _buttons)
|
||||
{
|
||||
var isActive = kv.Key == _activeSpeed;
|
||||
kv.Value.Modulate = isActive ? UiTokens.Success : UiTokens.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-30
@@ -1,30 +1,30 @@
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Result struct for battle action resolution. Source and Target carry
|
||||
/// Damage/Defence snapshots for both sides of the exchange. Result indicates
|
||||
/// whether the action succeeded (e.g. hit landed vs. missed/blocked).
|
||||
/// A struct rather than a class because these are short-lived stack values
|
||||
/// created in tight battle loops — no heap allocation.
|
||||
/// </summary>
|
||||
public struct ChangedAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Snapshot of the action source's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Source;
|
||||
/// <summary>
|
||||
/// Snapshot of the action target's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Target;
|
||||
/// <summary>
|
||||
/// Whether the action succeeded (e.g. hit landed vs. missed or
|
||||
/// blocked).
|
||||
/// </summary>
|
||||
public bool Result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Result struct for battle action resolution. Source and Target carry
|
||||
/// Damage/Defence snapshots for both sides of the exchange. Result indicates
|
||||
/// whether the action succeeded (e.g. hit landed vs. missed/blocked).
|
||||
/// A struct rather than a class because these are short-lived stack values
|
||||
/// created in tight battle loops — no heap allocation.
|
||||
/// </summary>
|
||||
public struct ChangedAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Snapshot of the action source's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Source;
|
||||
/// <summary>
|
||||
/// Snapshot of the action target's damage and defence after the
|
||||
/// exchange.
|
||||
/// </summary>
|
||||
public CharacterSet Target;
|
||||
/// <summary>
|
||||
/// Whether the action succeeded (e.g. hit landed vs. missed or
|
||||
/// blocked).
|
||||
/// </summary>
|
||||
public bool Result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight snapshot of damage/defence values for battle action results.
|
||||
/// A struct to avoid heap allocation in the battle loop where these are
|
||||
/// created for every action exchange. Used by ChangedAttributes to carry
|
||||
/// both source and target state.
|
||||
/// </summary>
|
||||
public struct CharacterSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage dealt or received in this exchange.
|
||||
/// </summary>
|
||||
public int Damage;
|
||||
/// <summary>
|
||||
/// Defence value at the time of the exchange.
|
||||
/// </summary>
|
||||
public int Defence;
|
||||
}
|
||||
}
|
||||
namespace Cthangover.Core.Characters
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight snapshot of damage/defence values for battle action results.
|
||||
/// A struct to avoid heap allocation in the battle loop where these are
|
||||
/// created for every action exchange. Used by ChangedAttributes to carry
|
||||
/// both source and target state.
|
||||
/// </summary>
|
||||
public struct CharacterSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage dealt or received in this exchange.
|
||||
/// </summary>
|
||||
public int Damage;
|
||||
/// <summary>
|
||||
/// Defence value at the time of the exchange.
|
||||
/// </summary>
|
||||
public int Defence;
|
||||
}
|
||||
}
|
||||
+63
-63
@@ -1,63 +1,63 @@
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Defeat screen widget. Minimal compared to WingroundBehaviour —
|
||||
/// only sets the deadground background texture (resolved from mods)
|
||||
/// and offers a Main Menu button that clears BattleData and reloads
|
||||
/// the menu scene. No loot, no EXP.
|
||||
/// </summary>
|
||||
public partial class DeadgroundBehaviour : TransitionWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears battle data and switches to the main menu scene.
|
||||
/// Bound to the "Main Menu" button on the defeat screen.
|
||||
/// </summary>
|
||||
public void ToMainMenuClick()
|
||||
{
|
||||
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.SwitchToMenu();
|
||||
BattleRequest.Current = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the defeat sound, resolves the deadground background
|
||||
/// texture from mods, and shows the transition widget.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
CthangoverTheme.ApplyPrimary(GetNodeOrNull<Button>("Content/CenterPanel/MainMenuButton"));
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
|
||||
var audioService = GetNodeOrNull<Node>("/root/AudioService") as IAudioService;
|
||||
audioService?.PlaySound("battle/deadground", SoundType.UI);
|
||||
|
||||
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||
|
||||
if (bg != null && bg.Texture == null)
|
||||
{
|
||||
var tex = Textures.Resolve("deadground");
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: ResolveTexture('deadground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||
bg.Texture = tex;
|
||||
}
|
||||
|
||||
base.Show();
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Defeat screen widget. Minimal compared to WingroundBehaviour —
|
||||
/// only sets the deadground background texture (resolved from mods)
|
||||
/// and offers a Main Menu button that clears BattleData and reloads
|
||||
/// the menu scene. No loot, no EXP.
|
||||
/// </summary>
|
||||
public partial class DeadgroundBehaviour : TransitionWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// Clears battle data and switches to the main menu scene.
|
||||
/// Bound to the "Main Menu" button on the defeat screen.
|
||||
/// </summary>
|
||||
public void ToMainMenuClick()
|
||||
{
|
||||
var sceneService = GetNode<GodotSceneService>("/root/GodotSceneService");
|
||||
sceneService?.SwitchToMenu();
|
||||
BattleRequest.Current = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the defeat sound, resolves the deadground background
|
||||
/// texture from mods, and shows the transition widget.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
CthangoverTheme.ApplyPrimary(GetNodeOrNull<Button>("Content/CenterPanel/MainMenuButton"));
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
|
||||
var audioService = GetNodeOrNull<Node>("/root/AudioService") as IAudioService;
|
||||
audioService?.PlaySound("battle/deadground", SoundType.UI);
|
||||
|
||||
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
||||
|
||||
if (bg != null && bg.Texture == null)
|
||||
{
|
||||
var tex = Textures.Resolve("deadground");
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show: ResolveTexture('deadground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
||||
bg.Texture = tex;
|
||||
}
|
||||
|
||||
base.Show();
|
||||
|
||||
GameLogger.Log("BATTLE", $"Deadground.Show() EXIT: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Single character EXP report in the victory screen. Displays the
|
||||
/// character icon, localized name, and "+N" EXP value. Uses the same
|
||||
/// ResolveMissingNodes pattern as LootReportItem for robustness
|
||||
/// against missing scene links.
|
||||
/// </summary>
|
||||
public partial class ExpReportCard : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label nameLabel;
|
||||
[Export] private Label expLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
nameLabel ??= GetNodeOrNull<Label>("Panel/NameLabel");
|
||||
expLabel ??= GetNodeOrNull<Label>("Panel/ExpLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the card with a character icon, localised name, and
|
||||
/// "+N" EXP value. Calls <c>ResolveMissingNodes</c> to tolerate
|
||||
/// incomplete scene wiring in the editor.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D characterIcon, string characterName, int exp)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && characterIcon != null)
|
||||
icon.Texture = characterIcon;
|
||||
|
||||
if (nameLabel != null)
|
||||
nameLabel.Text = characterName;
|
||||
|
||||
if (expLabel != null)
|
||||
expLabel.Text = "+" + exp;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Single character EXP report in the victory screen. Displays the
|
||||
/// character icon, localized name, and "+N" EXP value. Uses the same
|
||||
/// ResolveMissingNodes pattern as LootReportItem for robustness
|
||||
/// against missing scene links.
|
||||
/// </summary>
|
||||
public partial class ExpReportCard : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label nameLabel;
|
||||
[Export] private Label expLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
nameLabel ??= GetNodeOrNull<Label>("Panel/NameLabel");
|
||||
expLabel ??= GetNodeOrNull<Label>("Panel/ExpLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the card with a character icon, localised name, and
|
||||
/// "+N" EXP value. Calls <c>ResolveMissingNodes</c> to tolerate
|
||||
/// incomplete scene wiring in the editor.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D characterIcon, string characterName, int exp)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && characterIcon != null)
|
||||
icon.Texture = characterIcon;
|
||||
|
||||
if (nameLabel != null)
|
||||
nameLabel.Text = characterName;
|
||||
|
||||
if (expLabel != null)
|
||||
expLabel.Text = "+" + exp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
/// <summary>
|
||||
/// One extra bar of a battle unit next to its health (mana, rage, ammo…), as an
|
||||
/// <see cref="IBattleUnitGauge"/> reads it at this moment.
|
||||
/// </summary>
|
||||
public class BattleGauge
|
||||
{
|
||||
/// <summary>Stable id (a stat id, for example) the cores key their rows by.</summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>Short label, a locale key or plain text ("MP").</summary>
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>Current value.</summary>
|
||||
public int Current { get; set; }
|
||||
|
||||
/// <summary>Maximum; a bar with Max <= 0 is not drawn.</summary>
|
||||
public int Max { get; set; }
|
||||
|
||||
/// <summary>Fill colour as HTML ("#4a8cff"); null = the theme's info colour.</summary>
|
||||
public string Color { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
using Cthangover.Core.Mods.Consumers;
|
||||
|
||||
/// <summary>Registers every <see cref="IBattleUnitGauge"/> of a loaded mod assembly in <see cref="BattleUnitGauges"/>.</summary>
|
||||
public class BattleUnitGaugeConsumer : ModTypeConsumer<IBattleUnitGauge>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Id => "turn_battle_base.unit_gauges";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Add(IBattleUnitGauge instance, string modId) => BattleUnitGauges.Add(instance);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Remove(IBattleUnitGauge instance, string modId) => BattleUnitGauges.Remove(instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// The registered <see cref="IBattleUnitGauge"/>s and the bars they give for a unit. A
|
||||
/// provider calls <see cref="NotifyChanged"/> when a value moved (mana spent), so the
|
||||
/// cores redraw that unit; cores also read the bars whenever they redraw it themselves.
|
||||
/// </summary>
|
||||
public static class BattleUnitGauges
|
||||
{
|
||||
private static readonly List<IBattleUnitGauge> Providers = new();
|
||||
|
||||
/// <summary>Raised with a unit id (null = every unit) when its bars changed.</summary>
|
||||
public static event Action<string> Changed;
|
||||
|
||||
/// <summary>Registers a provider.</summary>
|
||||
public static void Add(IBattleUnitGauge gauge)
|
||||
{
|
||||
if (gauge != null && !Providers.Contains(gauge))
|
||||
Providers.Add(gauge);
|
||||
}
|
||||
|
||||
/// <summary>Removes a provider (its mod is unloaded).</summary>
|
||||
public static void Remove(IBattleUnitGauge gauge) => Providers.Remove(gauge);
|
||||
|
||||
/// <summary>Every bar of <paramref name="unit"/> with a positive maximum, providers in order; a failing provider is logged and skipped.</summary>
|
||||
public static List<BattleGauge> Read(Character unit)
|
||||
{
|
||||
var result = new List<BattleGauge>();
|
||||
if (unit == null)
|
||||
return result;
|
||||
foreach (var provider in Providers.OrderBy(p => p.Order).ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AddRange((provider.Read(unit) ?? Enumerable.Empty<BattleGauge>()).Where(g => g != null && g.Max > 0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GameLogger.Log("BATTLE", $"gauge provider {provider.GetType().Name} failed for '{unit.ID}': {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Tells the cores that the bars of <paramref name="unitId"/> changed.</summary>
|
||||
public static void NotifyChanged(string unitId) => Changed?.Invoke(unitId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Cthangover.Core.Battle
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
/// <summary>
|
||||
/// Supplies extra bars of battle units (found in mod assemblies by
|
||||
/// <see cref="BattleUnitGaugeConsumer"/>): a mod that adds a resource — mana of
|
||||
/// <c>battle_formulas</c>, a rage meter — shows it in every battle core that draws
|
||||
/// <see cref="BattleUnitGauges"/>, without knowing the cores.
|
||||
/// </summary>
|
||||
public interface IBattleUnitGauge
|
||||
{
|
||||
/// <summary>Order among the providers (lower first).</summary>
|
||||
int Order { get; }
|
||||
|
||||
/// <summary>The bars of <paramref name="unit"/> now; empty when it has none.</summary>
|
||||
IEnumerable<BattleGauge> Read(Character unit);
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,46 +1,46 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// A single loot cell in the victory grid. Shows icon with an optional
|
||||
/// "xN" count label (hidden when count is 1). ResolveMissingNodes
|
||||
/// supports both editor-assigned exports and runtime lookups so the
|
||||
/// prefab works with incomplete scene wiring. TooltipText is set to
|
||||
/// the localized item name for hover.
|
||||
/// </summary>
|
||||
public partial class LootReportItem : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label countLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
countLabel ??= GetNodeOrNull<Label>("Panel/CountLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the loot cell: sets the item icon, a "xN" count
|
||||
/// label (hidden when <paramref name="count"/> is 1), and the
|
||||
/// localised name as the tooltip. Calls
|
||||
/// <c>ResolveMissingNodes</c> to handle missing editor
|
||||
/// assignments gracefully.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D itemIcon, string localizedName, int count)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && itemIcon != null)
|
||||
icon.Texture = itemIcon;
|
||||
|
||||
if (countLabel != null)
|
||||
countLabel.Text = count > 1 ? "x" + count : "";
|
||||
|
||||
TooltipText = localizedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// A single loot cell in the victory grid. Shows icon with an optional
|
||||
/// "xN" count label (hidden when count is 1). ResolveMissingNodes
|
||||
/// supports both editor-assigned exports and runtime lookups so the
|
||||
/// prefab works with incomplete scene wiring. TooltipText is set to
|
||||
/// the localized item name for hover.
|
||||
/// </summary>
|
||||
public partial class LootReportItem : Control
|
||||
{
|
||||
[Export] private TextureRect icon;
|
||||
[Export] private Label countLabel;
|
||||
|
||||
private void ResolveMissingNodes()
|
||||
{
|
||||
icon ??= GetNodeOrNull<TextureRect>("Panel/Icon");
|
||||
countLabel ??= GetNodeOrNull<Label>("Panel/CountLabel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the loot cell: sets the item icon, a "xN" count
|
||||
/// label (hidden when <paramref name="count"/> is 1), and the
|
||||
/// localised name as the tooltip. Calls
|
||||
/// <c>ResolveMissingNodes</c> to handle missing editor
|
||||
/// assignments gracefully.
|
||||
/// </summary>
|
||||
public void Setup(Texture2D itemIcon, string localizedName, int count)
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
ResolveMissingNodes();
|
||||
|
||||
if (icon != null && itemIcon != null)
|
||||
icon.Texture = itemIcon;
|
||||
|
||||
if (countLabel != null)
|
||||
countLabel.Text = count > 1 ? "x" + count : "";
|
||||
|
||||
TooltipText = localizedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-117
@@ -1,117 +1,117 @@
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Floating damage-number overlay. Self-contained: creates its own Label
|
||||
/// in _Ready, floats upward with random horizontal drift, fades alpha,
|
||||
/// then QueueFrees itself. Static factory methods (SpawnDamage,
|
||||
/// SpawnDefence) instantiate and parent it in one call — damage is red,
|
||||
/// heal is green, defence is blue. MouseFilter is Ignore so clicks pass
|
||||
/// through to cards underneath.
|
||||
/// </summary>
|
||||
public partial class ShowDamageBehaviour : Control
|
||||
{
|
||||
private Label _label;
|
||||
|
||||
private float duration = 1.0f;
|
||||
private float floatDistance = 80f;
|
||||
private float driftRange = 30f;
|
||||
private Vector2 startPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the label on first use. Godot does not dispatch <c>_Ready</c>
|
||||
/// overrides declared in mod assemblies (only methods generated in the core
|
||||
/// project run), so the label cannot be created in <c>_Ready</c> — it is
|
||||
/// created here, right before the first animation starts.
|
||||
/// </summary>
|
||||
private void EnsureLabel()
|
||||
{
|
||||
if (_label != null)
|
||||
return;
|
||||
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
_label = new Label();
|
||||
_label.SetAnchorsAndOffsetsPreset(LayoutPreset.Center);
|
||||
_label.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
_label.VerticalAlignment = VerticalAlignment.Center;
|
||||
_label.AddThemeFontSizeOverride("font_size", 28);
|
||||
_label.AddThemeColorOverride("font_outline_color", UiTokens.Alpha(Colors.Black, 1f));
|
||||
_label.AddThemeConstantOverride("outline_size", 6);
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating damage (or heal) number.
|
||||
/// Damage is red, heal is green. The label floats upward with
|
||||
/// a random horizontal drift and auto-destructs after the
|
||||
/// tween completes. Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDamage(int amount, Node parent, Vector2 position, bool isHeal = false)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var color = isHeal
|
||||
? UiTokens.SuccessText
|
||||
: UiTokens.DangerText;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating defence number (blue).
|
||||
/// Same floating behaviour as <see cref="SpawnDamage"/>.
|
||||
/// Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDefence(int amount, Node parent, Vector2 position)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, UiTokens.Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label text, colour, and world position, then starts
|
||||
/// the floating animation tween. Called by the static factory
|
||||
/// methods after instantiation.
|
||||
/// </summary>
|
||||
public void Init(int amount, Vector2 worldPosition, Color? color = null)
|
||||
{
|
||||
EnsureLabel();
|
||||
|
||||
_label.Text = amount.ToString();
|
||||
if (color.HasValue)
|
||||
_label.Modulate = color.Value;
|
||||
|
||||
GlobalPosition = worldPosition;
|
||||
startPosition = Position;
|
||||
|
||||
RunAnimation();
|
||||
}
|
||||
|
||||
private async void RunAnimation()
|
||||
{
|
||||
float driftX = (float)GD.RandRange(-driftRange, driftRange);
|
||||
Vector2 targetPos = startPosition + new Vector2(driftX, -floatDistance);
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.SetParallel(true);
|
||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||
tween.TweenProperty(this, "position", targetPos, duration);
|
||||
|
||||
var fadeTween = CreateTween();
|
||||
fadeTween.TweenProperty(_label, "modulate:a", 0f, duration * 0.8f);
|
||||
|
||||
await ToSignal(tween, "finished");
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.TurnBattle
|
||||
{
|
||||
/// <summary>
|
||||
/// Floating damage-number overlay. Self-contained: creates its own Label
|
||||
/// in _Ready, floats upward with random horizontal drift, fades alpha,
|
||||
/// then QueueFrees itself. Static factory methods (SpawnDamage,
|
||||
/// SpawnDefence) instantiate and parent it in one call — damage is red,
|
||||
/// heal is green, defence is blue. MouseFilter is Ignore so clicks pass
|
||||
/// through to cards underneath.
|
||||
/// </summary>
|
||||
public partial class ShowDamageBehaviour : Control
|
||||
{
|
||||
private Label _label;
|
||||
|
||||
private float duration = 1.0f;
|
||||
private float floatDistance = 80f;
|
||||
private float driftRange = 30f;
|
||||
private Vector2 startPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the label on first use. Godot does not dispatch <c>_Ready</c>
|
||||
/// overrides declared in mod assemblies (only methods generated in the core
|
||||
/// project run), so the label cannot be created in <c>_Ready</c> — it is
|
||||
/// created here, right before the first animation starts.
|
||||
/// </summary>
|
||||
private void EnsureLabel()
|
||||
{
|
||||
if (_label != null)
|
||||
return;
|
||||
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
_label = new Label();
|
||||
_label.SetAnchorsAndOffsetsPreset(LayoutPreset.Center);
|
||||
_label.HorizontalAlignment = HorizontalAlignment.Center;
|
||||
_label.VerticalAlignment = VerticalAlignment.Center;
|
||||
_label.AddThemeFontSizeOverride("font_size", 28);
|
||||
_label.AddThemeColorOverride("font_outline_color", UiTokens.Alpha(Colors.Black, 1f));
|
||||
_label.AddThemeConstantOverride("outline_size", 6);
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating damage (or heal) number.
|
||||
/// Damage is red, heal is green. The label floats upward with
|
||||
/// a random horizontal drift and auto-destructs after the
|
||||
/// tween completes. Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDamage(int amount, Node parent, Vector2 position, bool isHeal = false)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var color = isHeal
|
||||
? UiTokens.SuccessText
|
||||
: UiTokens.DangerText;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and parents a floating defence number (blue).
|
||||
/// Same floating behaviour as <see cref="SpawnDamage"/>.
|
||||
/// Skips values <= 0.
|
||||
/// </summary>
|
||||
public static void SpawnDefence(int amount, Node parent, Vector2 position)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var hint = new ShowDamageBehaviour();
|
||||
parent.AddChild(hint);
|
||||
hint.Init(amount, position, UiTokens.Info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the label text, colour, and world position, then starts
|
||||
/// the floating animation tween. Called by the static factory
|
||||
/// methods after instantiation.
|
||||
/// </summary>
|
||||
public void Init(int amount, Vector2 worldPosition, Color? color = null)
|
||||
{
|
||||
EnsureLabel();
|
||||
|
||||
_label.Text = amount.ToString();
|
||||
if (color.HasValue)
|
||||
_label.Modulate = color.Value;
|
||||
|
||||
GlobalPosition = worldPosition;
|
||||
startPosition = Position;
|
||||
|
||||
RunAnimation();
|
||||
}
|
||||
|
||||
private async void RunAnimation()
|
||||
{
|
||||
float driftX = (float)GD.RandRange(-driftRange, driftRange);
|
||||
Vector2 targetPos = startPosition + new Vector2(driftX, -floatDistance);
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.SetParallel(true);
|
||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||
tween.TweenProperty(this, "position", targetPos, duration);
|
||||
|
||||
var fadeTween = CreateTween();
|
||||
fadeTween.TweenProperty(_label, "modulate:a", 0f, duration * 0.8f);
|
||||
|
||||
await ToSignal(tween, "finished");
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,58 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Behaviour hooks for a status effect type. Each hook receives the
|
||||
/// owning character so a single IStatusActions instance can be
|
||||
/// shared across multiple characters. OnDealDamage / OnTakeDamage
|
||||
/// use ref int to let effects amplify or reduce damage before it
|
||||
/// is applied. ModifyAttributes is a passive stat modifier called
|
||||
/// independently of turn phases. OnFinalAction fires once when the
|
||||
/// effect expires. All hooks have empty default semantics — effects
|
||||
/// override only the hooks they care about.
|
||||
/// </summary>
|
||||
public interface IStatusActions
|
||||
{
|
||||
/// <summary>Factory key matching the <c>Actions</c> field in
|
||||
/// <see cref="StatusEffectInfo"/>, e.g. <c>"basic/stun"</c>.</summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>Fires when the owning character's turn begins. Use for
|
||||
/// start-of-turn damage-over-time or regeneration effects.</summary>
|
||||
void OnTurnStart(Character target);
|
||||
/// <summary>Fires when the owning character's turn ends. Mirrors
|
||||
/// <see cref="OnTurnStart"/> for symmetry; useful for effects that
|
||||
/// activate after the character acts.</summary>
|
||||
void OnTurnEnd(Character target);
|
||||
/// <summary>Called before the owning character deals damage to
|
||||
/// <paramref name="target"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to amplify or reduce outgoing damage —
|
||||
/// all active effects stack multiplicatively through the queue's
|
||||
/// iteration.</summary>
|
||||
void OnDealDamage(Character source, Character target, ref int damage);
|
||||
/// <summary>Called before the owning character receives damage from
|
||||
/// <paramref name="source"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to add damage reduction or vulnerability.
|
||||
/// Fires after <see cref="OnDealDamage"/> in the damage
|
||||
/// pipeline.</summary>
|
||||
void OnTakeDamage(Character target, Character source, ref int damage);
|
||||
/// <summary>Fires immediately when the effect is first placed on a
|
||||
/// character. Use for one-shot stat boosts, visual feedback, or
|
||||
/// initialisation logic.</summary>
|
||||
void OnApply(Character target);
|
||||
/// <summary>Fires when the effect is manually removed (e.g. via a
|
||||
/// cleanse card). Not called on natural expiration — use
|
||||
/// <see cref="OnFinalAction"/> for that path.</summary>
|
||||
void OnRemove(Character target);
|
||||
/// <summary>Fires once when the effect expires naturally (timer reaches
|
||||
/// zero). Use for end-of-effect triggers like explosion damage or
|
||||
/// stat restoration. Not called on manual removal.</summary>
|
||||
void OnFinalAction(Character target);
|
||||
/// <summary>Passive stat modifier invoked independently of turn phases.
|
||||
/// Receives a reference to the character's attribute container;
|
||||
/// implementors can add flat or percentage bonuses that other
|
||||
/// effects may also contribute to.</summary>
|
||||
void ModifyAttributes(CharacterAttributes attributes);
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Behaviour hooks for a status effect type. Each hook receives the
|
||||
/// owning character so a single IStatusActions instance can be
|
||||
/// shared across multiple characters. OnDealDamage / OnTakeDamage
|
||||
/// use ref int to let effects amplify or reduce damage before it
|
||||
/// is applied. ModifyAttributes is a passive stat modifier called
|
||||
/// independently of turn phases. OnFinalAction fires once when the
|
||||
/// effect expires. All hooks have empty default semantics — effects
|
||||
/// override only the hooks they care about.
|
||||
/// </summary>
|
||||
public interface IStatusActions
|
||||
{
|
||||
/// <summary>Factory key matching the <c>Actions</c> field in
|
||||
/// <see cref="StatusEffectInfo"/>, e.g. <c>"basic/stun"</c>.</summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>Fires when the owning character's turn begins. Use for
|
||||
/// start-of-turn damage-over-time or regeneration effects.</summary>
|
||||
void OnTurnStart(Character target);
|
||||
/// <summary>Fires when the owning character's turn ends. Mirrors
|
||||
/// <see cref="OnTurnStart"/> for symmetry; useful for effects that
|
||||
/// activate after the character acts.</summary>
|
||||
void OnTurnEnd(Character target);
|
||||
/// <summary>Called before the owning character deals damage to
|
||||
/// <paramref name="target"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to amplify or reduce outgoing damage —
|
||||
/// all active effects stack multiplicatively through the queue's
|
||||
/// iteration.</summary>
|
||||
void OnDealDamage(Character source, Character target, ref int damage);
|
||||
/// <summary>Called before the owning character receives damage from
|
||||
/// <paramref name="source"/>. Modify <paramref name="damage"/> via
|
||||
/// <see langword="ref"/> to add damage reduction or vulnerability.
|
||||
/// Fires after <see cref="OnDealDamage"/> in the damage
|
||||
/// pipeline.</summary>
|
||||
void OnTakeDamage(Character target, Character source, ref int damage);
|
||||
/// <summary>Fires immediately when the effect is first placed on a
|
||||
/// character. Use for one-shot stat boosts, visual feedback, or
|
||||
/// initialisation logic.</summary>
|
||||
void OnApply(Character target);
|
||||
/// <summary>Fires when the effect is manually removed (e.g. via a
|
||||
/// cleanse card). Not called on natural expiration — use
|
||||
/// <see cref="OnFinalAction"/> for that path.</summary>
|
||||
void OnRemove(Character target);
|
||||
/// <summary>Fires once when the effect expires naturally (timer reaches
|
||||
/// zero). Use for end-of-effect triggers like explosion damage or
|
||||
/// stat restoration. Not called on manual removal.</summary>
|
||||
void OnFinalAction(Character target);
|
||||
/// <summary>Passive stat modifier invoked independently of turn phases.
|
||||
/// Receives a reference to the character's attribute container;
|
||||
/// implementors can add flat or percentage bonuses that other
|
||||
/// effects may also contribute to.</summary>
|
||||
void ModifyAttributes(CharacterAttributes attributes);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,49 @@
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Stun behaviour set — intentionally all no-ops. The stun condition
|
||||
/// is checked separately via StatusEffectQueue.HasStun() rather than
|
||||
/// through action hooks, so the Actions implementation is empty.
|
||||
/// Exists as a concrete type so the factory can resolve "basic/stun"
|
||||
/// to a valid instance.
|
||||
/// </summary>
|
||||
public class StunStatusEffect : IStatusActions
|
||||
{
|
||||
/// <inheritdoc cref="IStatusActions.ID"/>
|
||||
public string ID => "basic/stun";
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnStart"/>
|
||||
public void OnTurnStart(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnEnd"/>
|
||||
public void OnTurnEnd(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnDealDamage"/>
|
||||
public void OnDealDamage(Character source, Character target, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTakeDamage"/>
|
||||
public void OnTakeDamage(Character target, Character source, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnApply"/>
|
||||
public void OnApply(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnRemove"/>
|
||||
public void OnRemove(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnFinalAction"/>
|
||||
public void OnFinalAction(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.ModifyAttributes"/>
|
||||
public void ModifyAttributes(CharacterAttributes attributes)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
using Cthangover.Core.Characters;
|
||||
|
||||
namespace Cthangover.Core.Cards.StatusEffect.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Stun behaviour set — intentionally all no-ops. The stun condition
|
||||
/// is checked separately via StatusEffectQueue.HasStun() rather than
|
||||
/// through action hooks, so the Actions implementation is empty.
|
||||
/// Exists as a concrete type so the factory can resolve "basic/stun"
|
||||
/// to a valid instance.
|
||||
/// </summary>
|
||||
public class StunStatusEffect : IStatusActions
|
||||
{
|
||||
/// <inheritdoc cref="IStatusActions.ID"/>
|
||||
public string ID => "basic/stun";
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnStart"/>
|
||||
public void OnTurnStart(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTurnEnd"/>
|
||||
public void OnTurnEnd(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnDealDamage"/>
|
||||
public void OnDealDamage(Character source, Character target, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnTakeDamage"/>
|
||||
public void OnTakeDamage(Character target, Character source, ref int damage)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnApply"/>
|
||||
public void OnApply(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnRemove"/>
|
||||
public void OnRemove(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.OnFinalAction"/>
|
||||
public void OnFinalAction(Character target)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc cref="IStatusActions.ModifyAttributes"/>
|
||||
public void ModifyAttributes(CharacterAttributes attributes)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
using System;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
|
||||
namespace Cthangover.Core.Factories.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin <c>FileFactory</c> for status effect metadata — duration, stack
|
||||
/// behaviour, visual icon, etc. Works in tandem with
|
||||
/// <c>StatusEffectActionFactory</c>: this factory provides the <b>data</b>
|
||||
/// (what the effect is), while the action factory provides the
|
||||
/// <b>behaviour</b> (what the effect does each turn). Files are discovered
|
||||
/// by the shared entity resolver through the DTO's
|
||||
/// <c>[EntityKind("status_effect")]</c> attribute.
|
||||
/// </summary>
|
||||
public class StatusEffectInfoFactory : FileFactory<StatusEffectInfo>
|
||||
{
|
||||
private static readonly Lazy<StatusEffectInfoFactory> lazy = new(() => new StatusEffectInfoFactory());
|
||||
/// <summary>Lazy singleton facade over the AppServices-owned factory.</summary>
|
||||
public static StatusEffectInfoFactory Instance => lazy.Value;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
|
||||
namespace Cthangover.Core.Factories.Impls
|
||||
{
|
||||
/// <summary>
|
||||
/// Thin <c>FileFactory</c> for status effect metadata — duration, stack
|
||||
/// behaviour, visual icon, etc. Works in tandem with
|
||||
/// <c>StatusEffectActionFactory</c>: this factory provides the <b>data</b>
|
||||
/// (what the effect is), while the action factory provides the
|
||||
/// <b>behaviour</b> (what the effect does each turn). Files are discovered
|
||||
/// by the shared entity resolver through the DTO's
|
||||
/// <c>[EntityKind("status_effect")]</c> attribute.
|
||||
/// </summary>
|
||||
public class StatusEffectInfoFactory : FileFactory<StatusEffectInfo>
|
||||
{
|
||||
private static readonly Lazy<StatusEffectInfoFactory> lazy = new(() => new StatusEffectInfoFactory());
|
||||
/// <summary>Lazy singleton facade over the AppServices-owned factory.</summary>
|
||||
public static StatusEffectInfoFactory Instance => lazy.Value;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"Kind": "status_effect",
|
||||
"Items": [
|
||||
{
|
||||
"Id": "effect/physics/stun",
|
||||
"Name": "status/",
|
||||
"Description": "",
|
||||
"Icon": "characters/effects/stun",
|
||||
"Type": "Stun",
|
||||
"Duration": 3,
|
||||
"Actions": "basic/stun"
|
||||
}
|
||||
]
|
||||
{
|
||||
"Kind": "status_effect",
|
||||
"Items": [
|
||||
{
|
||||
"Id": "effect/physics/stun",
|
||||
"Name": "status/",
|
||||
"Description": "",
|
||||
"Icon": "characters/effects/stun",
|
||||
"Type": "Stun",
|
||||
"Duration": 3,
|
||||
"Actions": "basic/stun"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user