Compare commits
15
Commits
0f3e3c3636
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1819339b0 | ||
|
|
0e46f9697e | ||
|
|
cd9c05afb3 | ||
|
|
f1572374f5 | ||
|
|
4e38e5c936 | ||
|
|
c6063cb9e3 | ||
|
|
ad57e6e634 | ||
|
|
18616852e1 | ||
|
|
6f61990b71 | ||
|
|
704b69d93d | ||
|
|
00aa70e302 | ||
|
|
ba2c08b1dc | ||
|
|
86e3de904e | ||
|
|
28046f901e | ||
|
|
68f763510a |
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
|
||||
@@ -12,6 +12,7 @@ obj/
|
||||
gen/
|
||||
obj/
|
||||
bin/
|
||||
art_sources/
|
||||
|
||||
*.tres~
|
||||
*.tscn~
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"key": "battle.core",
|
||||
"value": "ff_battle",
|
||||
"name": "collection_settings/ff"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle — battles with a command menu
|
||||
|
||||
FF Battle is a battle system in the style of classic console RPGs: the party at the bottom left, the enemies at the top, and a command menu — Attack, Items, Defend, Escape. It is the battle system of RPG Plus. The battle itself — its start, backdrop, victory and defeat screens, experience and loot — comes from Turn Battle Base; FF Battle is the way the player fights.
|
||||
|
||||
## The battle screen
|
||||
|
||||
The RPG Plus warrior meets a wolf in the village:
|
||||
|
||||

|
||||
|
||||
- **Cards.** Each character shows health, the attack and defence (DEF is a shield that absorbs damage first), mana if the game has it, and the yellow dots of action points (AP).
|
||||
- **Battle panel** — the round, how many enemies and party members are left, and whose turn it is.
|
||||
- **Battle log** — every action and its result, newest at the bottom.
|
||||
|
||||
## A turn
|
||||
|
||||
1. **Pick a character** — click the card, or press **Enter**/**Space** for the first one with action points.
|
||||
2. **Choose a command** with the mouse or **W/S**/arrows and **Enter**:
|
||||
- **Attack** — the character's battle actions;
|
||||
- **Items** — usable items from the inventory, 1 AP each;
|
||||
- **Defend** — the character's defence action on itself;
|
||||
- **Escape** — 70% chance; on failure the character loses 1 AP.
|
||||
3. **Choose the action.** The panel next to the menu shows the action card and what it will do:
|
||||
|
||||

|
||||
|
||||
4. **Click the target.** "Select a target · RMB / Esc — cancel" appears and the enemy cards light up:
|
||||
|
||||

|
||||
|
||||
When the party has spent its action points, the enemies take their turn, then a new round starts and the points are restored.
|
||||
|
||||
## What the log says
|
||||
|
||||
After one basic attack and the wolf's answer:
|
||||
|
||||

|
||||
|
||||
The hero's attack is 12. The wolf's shield of 2 took the first 2 points and was used up, so the wolf lost 10 health: 31 → 21. The wolf then used Defence and raised its shield to 4 for the next blow.
|
||||
|
||||
## Actions
|
||||
|
||||
The menu lists the actions the character has (see Characters Engine). FF Battle itself carries out the basic kinds:
|
||||
|
||||
- `Attack=1.5` — damage of the attack times the number, absorbed first by the target's shield;
|
||||
- `Defence=4` — a shield on an ally;
|
||||
- `Turn=2` — stun for that many turns;
|
||||
- item use.
|
||||
|
||||
Actions written as formulas, such as Classic RPG Rules' Power strike or Fireball, work through Battle Formulas; mana costs (`Cost=mp:4`) are spent from the character's mana.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
1. **The menu does not open.** The character has no action points left this round, or it is the enemies' turn.
|
||||
2. **Items is greyed out.** There are no items in the inventory that can be used in battle.
|
||||
3. **A skill does nothing.** Its values need Battle Formulas, or it is not marked as usable in battle.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle — combates con menú de comandos
|
||||
|
||||
FF Battle es un sistema de combate al estilo de las consolas RPG clásicas: el grupo abajo a la izquierda, los enemigos arriba y un menú de comandos — Attack, Items, Defend, Escape. Es el sistema de combate de RPG Plus. El combate en sí — el inicio, el fondo, las pantallas de victoria y derrota, la experiencia y el botín — lo pone Turn Battle Base; FF Battle es la manera de luchar del jugador.
|
||||
|
||||
## La pantalla de combate
|
||||
|
||||
El guerrero de RPG Plus se encuentra con un lobo en la aldea:
|
||||
|
||||

|
||||
|
||||
- **Las cartas.** Cada personaje muestra la salud, el ataque y la defensa (DEF es un escudo que absorbe el daño primero), el maná si el juego lo tiene, y los puntos amarillos de acción (AP).
|
||||
- **El panel Battle** — la ronda, cuántos enemigos y miembros del grupo quedan, y de quién es el turno.
|
||||
- **El registro de combate** — cada acción y su resultado, lo más reciente abajo.
|
||||
|
||||
## Un turno
|
||||
|
||||
1. **Elige un personaje** — haz clic en su carta o pulsa **Enter**/**Space** para tomar al primero que tenga puntos de acción.
|
||||
2. **Elige un comando** con el ratón o con **W/S**/las flechas y **Enter**:
|
||||
- **Attack** — las acciones de combate del personaje;
|
||||
- **Items** — objetos usables del inventario, 1 AP cada uno;
|
||||
- **Defend** — la acción de defensa del personaje sobre sí mismo;
|
||||
- **Escape** — 70% de probabilidad; si falla, el personaje pierde 1 AP.
|
||||
3. **Elige la acción.** El panel junto al menú muestra la carta de acción y lo que hará:
|
||||
|
||||

|
||||
|
||||
4. **Haz clic en el objetivo.** Aparece «Selecciona un objetivo · RMB / Esc — cancelar» y las cartas enemigas se iluminan:
|
||||
|
||||

|
||||
|
||||
Cuando el grupo gasta sus puntos de acción, los enemigos hacen su turno; después empieza una ronda nueva y los puntos se restauran.
|
||||
|
||||
## Lo que dice el registro
|
||||
|
||||
Tras un ataque básico y la respuesta del lobo:
|
||||
|
||||

|
||||
|
||||
El ataque del héroe es 12. El escudo del lobo (2) absorbió los primeros 2 puntos y se agotó, así que el lobo perdió 10 de salud: 31 → 21. Después el lobo usó Defence y subió su escudo a 4 para el próximo golpe.
|
||||
|
||||
## Las acciones
|
||||
|
||||
El menú enumera las acciones que tiene el personaje (véase Characters Engine). Los tipos básicos los ejecuta el propio FF Battle:
|
||||
|
||||
- `Attack=1.5` — el daño del ataque multiplicado por el número; lo absorbe primero el escudo del objetivo;
|
||||
- `Defence=4` — un escudo sobre un aliado;
|
||||
- `Turn=2` — aturdimiento durante ese número de turnos;
|
||||
- el uso de objetos.
|
||||
|
||||
Las acciones escritas como fórmulas, como el Power strike o el Fireball de Classic RPG Rules, funcionan mediante Battle Formulas; los costes de maná (`Cost=mp:4`) se pagan con el maná del personaje.
|
||||
|
||||
## Si algo sale mal
|
||||
|
||||
1. **El menú no se abre.** El personaje se quedó sin puntos de acción en esta ronda, o es el turno de los enemigos.
|
||||
2. **Items aparece en gris.** No hay objetos en el inventario que se puedan usar en combate.
|
||||
3. **Una habilidad no hace nada.** Sus valores necesitan Battle Formulas, o no está marcada como usable en combate.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle —— コマンドメニューで戦う戦闘
|
||||
|
||||
FF Battle は、古典的なコンソールRPG風のバトルシステムです。左下にパーティ、上に敵、そして Attack・Items・Defend・Escape のコマンドメニュー。RPG Plus のバトルシステムです。戦闘そのもの——開始、背景、勝利・敗北の画面、経験値と戦利品——は Turn Battle Base が担い、FF Battle はプレイヤーの戦い方を担当します。
|
||||
|
||||
## 戦闘画面
|
||||
|
||||
RPG Plus の戦士が村で狼に出くわします:
|
||||
|
||||

|
||||
|
||||
- **カード。** キャラクターごとに、体力、攻撃と防御(DEF はダメージを最初に受け止めるシールド)、ゲームにあればマナ、そして行動ポイント(AP)の黄色い点が表示されます。
|
||||
- **Battle パネル**——ラウンド数、残りの敵とパーティメンバーの数、そして今誰のターンかを示します。
|
||||
- **戦闘ログ**——すべてのアクションとその結果を記録します。新しいものが下に追加されます。
|
||||
|
||||
## ターンの流れ
|
||||
|
||||
1. **キャラクターを選びます**——カードをクリックするか、**Enter**/**Space** で行動ポイントのある最初のキャラクターを選べます。
|
||||
2. **コマンドを選びます**——マウス、または **W/S**/方向キーと **Enter** で:
|
||||
- **Attack**——キャラクターの戦闘アクション;
|
||||
- **Items**——インベントリから使えるアイテム。それぞれ 1 AP;
|
||||
- **Defend**——自分自身への防御アクション;
|
||||
- **Escape**——成功率 70%。失敗するとキャラクターは 1 AP 失います。
|
||||
3. **アクションを選びます。** メニューの横のパネルにアクションカードとその効果が表示されます:
|
||||
|
||||

|
||||
|
||||
4. **ターゲットをクリックします。**「対象を選択 · 右クリック / Esc — キャンセル」というヒントが出て、敵のカードが光ります:
|
||||
|
||||

|
||||
|
||||
パーティが行動ポイントを使い切ると敵のターンになり、その後新しいラウンドが始まってポイントが回復します。
|
||||
|
||||
## ログの読み方
|
||||
|
||||
通常攻撃を一度と、それに対する狼の応答のあと:
|
||||
|
||||

|
||||
|
||||
英雄の攻撃は 12。狼のシールド(2)が最初の 2 点を受け止めて使い切ったため、狼の体力は 10 減って 31 → 21。その後狼は Defence を使い、次の一撃に備えてシールドを 4 まで上げます。
|
||||
|
||||
## アクション
|
||||
|
||||
メニューには、そのキャラクターが持つアクションが並びます(Characters Engine を参照)。基本の種類そのものは FF Battle が処理します:
|
||||
|
||||
- `Attack=1.5`——攻撃ダメージに数値を掛けたもの。まず対象のシールドが受け止めます;
|
||||
- `Defence=4`——味方にシールドを付与;
|
||||
- `Turn=2`——そのターン数だけスタン;
|
||||
- アイテムの使用。
|
||||
|
||||
数式で書かれたアクション(Classic RPG Rules の Power strike や Fireball など)は Battle Formulas を通して機能します。マナのコスト(`Cost=mp:4`)はキャラクターのマナから支払われます。
|
||||
|
||||
## うまくいかないときは
|
||||
|
||||
1. **メニューが開かない。** そのキャラクターの行動ポイントがこのラウンドでもうないか、敵のターンです。
|
||||
2. **Items がグレーになっている。** インベントリに戦闘で使えるアイテムがありません。
|
||||
3. **スキルが何もしない。** その値には Battle Formulas が必要か、戦闘で使える印が付いていません。
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle — 커맨드 메뉴 방식의 전투
|
||||
|
||||
FF Battle은 고전 콘솔 RPG 스타일의 전투 시스템입니다. 왼쪽 아래에 파티, 위에 적, 그리고 Attack·Items·Defend·Escape 커맨드 메뉴가 있습니다. RPG Plus의 전투 시스템이죠. 전투 자체 — 시작, 배경, 승리·패배 화면, 경험치와 전리품 — 은 Turn Battle Base가 맡고, FF Battle은 플레이어의 싸움 방식을 담당합니다.
|
||||
|
||||
## 전투 화면
|
||||
|
||||
RPG Plus의 전사가 마을에서 늑대를 만납니다:
|
||||
|
||||

|
||||
|
||||
- **카드.** 캐릭터마다 체력, 공격력과 방어력(DEF은 피해를 먼저 흡수하는 방어막), 게임에 있다면 마나, 그리고 행동점(AP)을 나타내는 노란 점이 표시됩니다.
|
||||
- **Battle 패널** — 라운드, 남은 적과 파티원 수, 지금 누구의 차례인지 보여줍니다.
|
||||
- **전투 로그** — 모든 행동과 그 결과를 기록하며, 최신 기록이 아래에 쌓입니다.
|
||||
|
||||
## 한 턴의 진행
|
||||
|
||||
1. **캐릭터를 고릅니다** — 카드를 클릭하거나 **Enter**/**Space**를 눌러 행동점이 남은 첫 캐릭터를 선택하세요.
|
||||
2. **커맨드를 고릅니다** — 마우스나 **W/S**/방향키, **Enter**로 선택하세요:
|
||||
- **Attack** — 캐릭터의 전투 행동;
|
||||
- **Items** — 인벤토리에서 사용할 수 있는 아이템, 각 1 AP;
|
||||
- **Defend** — 자기 자신에게 거는 방어 행동;
|
||||
- **Escape** — 성공 확률 70%. 실패하면 캐릭터가 1 AP를 잃습니다.
|
||||
3. **행동을 고릅니다.** 메뉴 옆 패널에 행동 카드와 그 효과가 표시됩니다:
|
||||
|
||||

|
||||
|
||||
4. **대상을 클릭합니다.** "대상 선택 · 오른쪽 버튼 / Esc — 취소" 안내가 뜨고 적 카드가 밝게 표시됩니다:
|
||||
|
||||

|
||||
|
||||
파티가 행동점을 다 쓰면 적들의 차례가 되고, 이후 새 라운드가 시작되며 행동점이 회복됩니다.
|
||||
|
||||
## 로그가 말해주는 것
|
||||
|
||||
기본 공격 한 번과 늑대의 대답 이후:
|
||||
|
||||

|
||||
|
||||
영웅의 공격은 12입니다. 늑대의 방어막(2)이 처음 2점을 흡수하고 소진되어, 늑대는 체력을 10 잃고 31 → 21이 됐습니다. 이어서 늑대는 Defence를 써서 다음 공격에 대비해 방어막을 4까지 올립니다.
|
||||
|
||||
## 행동
|
||||
|
||||
메뉴에는 캐릭터가 가진 행동이 나열됩니다(Characters Engine 참조). 기본 유형은 FF Battle 자체가 처리합니다:
|
||||
|
||||
- `Attack=1.5` — 공격 피해에 그 수를 곱한 값입니다. 대상의 방어막이 먼저 흡수합니다;
|
||||
- `Defence=4` — 아군에게 방어막을 겁니다;
|
||||
- `Turn=2` — 그만큼의 턴 동안 기절;
|
||||
- 아이템 사용.
|
||||
|
||||
수식으로 쓰인 행동(Classic RPG Rules의 Power strike, Fireball 등)은 Battle Formulas를 통해 작동하며, 마나 비용(`Cost=mp:4`)은 캐릭터의 마나에서 차감됩니다.
|
||||
|
||||
## 문제가 생겼을 때
|
||||
|
||||
1. **메뉴가 열리지 않음.** 캐릭터의 행동점이 이번 라운드에 바닥났거나, 지금은 적의 차례입니다.
|
||||
2. **Items가 회색으로 비활성화됨.** 인벤토리에 전투에서 쓸 수 있는 아이템이 없습니다.
|
||||
3. **스킬이 아무 효과가 없음.** 수치에 Battle Formulas가 필요하거나, 전투 사용 가능 표시가 없는 스킬입니다.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle — бои с меню команд
|
||||
|
||||
FF Battle — боевая система в стиле классических консольных RPG: отряд слева внизу, враги вверху и меню команд — Attack, Items, Defend, Escape. Это боевая система RPG Plus. Сам бой — начало, фон, экраны победы и поражения, опыт и добыча — берётся из Turn Battle Base, а FF Battle определяет, как именно сражается игрок.
|
||||
|
||||
## Экран боя
|
||||
|
||||
Воин из RPG Plus встречает волка в деревне:
|
||||
|
||||

|
||||
|
||||
- **Карты.** У каждого персонажа видны здоровье, атака и защита (DEF — щит, который поглощает урон первым), мана, если она есть в игре, и жёлтые точки очков действий (AP).
|
||||
- **Панель Battle** — номер раунда, сколько врагов и бойцов отряда осталось и чей сейчас ход.
|
||||
- **Журнал боя** — каждое действие и его результат; свежие записи внизу.
|
||||
|
||||
## Ход
|
||||
|
||||
1. **Выберите персонажа** — щёлкните по его карте или нажмите **Enter**/**Space**, чтобы взять первого, у кого есть очки действий.
|
||||
2. **Выберите команду** мышью или клавишами **W/S**, стрелками и **Enter**:
|
||||
- **Attack** — боевые действия персонажа;
|
||||
- **Items** — предметы из инвентаря, которые можно использовать; по 1 AP;
|
||||
- **Defend** — защитное действие персонажа на самого себя;
|
||||
- **Escape** — шанс 70%; при неудаче персонаж теряет 1 AP.
|
||||
3. **Выберите действие.** Панель рядом с меню покажет карту действия и то, что она сделает:
|
||||
|
||||

|
||||
|
||||
4. **Щёлкните по цели.** Появится подсказка «Выберите цель · ПКМ / Esc — отмена», и карты врагов подсветятся:
|
||||
|
||||

|
||||
|
||||
Когда отряд потратит очки действий, ход переходит к врагам; затем начинается новый раунд, и очки восстанавливаются.
|
||||
|
||||
## Что говорит журнал
|
||||
|
||||
После одной обычной атаки и ответа волка:
|
||||
|
||||

|
||||
|
||||
Атака героя — 12. Щит волка (2) взял на себя первые 2 очка и исчерпался, так что волк потерял 10 здоровья: 31 → 21. Затем волк применил Defence и поднял щит до 4 перед следующим ударом.
|
||||
|
||||
## Действия
|
||||
|
||||
В меню перечислены действия персонажа (см. Characters Engine). Сами базовые виды действий выполняет FF Battle:
|
||||
|
||||
- `Attack=1.5` — урон атаки, умноженный на число; первым его поглощает щит цели;
|
||||
- `Defence=4` — щит на союзнике;
|
||||
- `Turn=2` — оглушение на столько ходов;
|
||||
- использование предметов.
|
||||
|
||||
Действия-формулы, например Power strike или Fireball из Classic RPG Rules, работают через Battle Formulas; затраты маны (`Cost=mp:4`) списываются с маны персонажа.
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
1. **Меню не открывается.** У персонажа не осталось очков действий в этом раунде либо сейчас ход врагов.
|
||||
2. **Пункт Items неактивен.** В инвентаре нет предметов, которые можно использовать в бою.
|
||||
3. **Навык ничего не делает.** Его значениям нужен Battle Formulas, либо навык не помечен как пригодный для боя.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle — chiến đấu bằng menu lệnh
|
||||
|
||||
FF Battle là hệ thống chiến đấu mang phong cách RPG máy chơi game cổ điển: đội của bạn ở góc dưới bên trái, kẻ địch ở trên, cùng một menu lệnh — Attack, Items, Defend, Escape. Đây là hệ thống chiến đấu của RPG Plus. Bản thân trận chiến — khởi đầu, nền, màn hình thắng thua, kinh nghiệm và chiến lợi phẩm — do Turn Battle Base đảm nhiệm; còn FF Battle quyết định người chơi đánh thế nào.
|
||||
|
||||
## Màn hình chiến đấu
|
||||
|
||||
Chiến binh RPG Plus chạm trán một con sói trong làng:
|
||||
|
||||

|
||||
|
||||
- **Thẻ.** Mỗi nhân vật cho thấy máu, sát thương và phòng thủ (DEF là lớp khiên hấp thụ sát thương trước), mana nếu game có, và các chấm vàng là điểm hành động (AP).
|
||||
- **Bảng Battle** — vòng đấu, số kẻ địch và thành viên đội còn lại, và đến lượt của ai.
|
||||
- **Nhật ký chiến đấu** — ghi lại từng hành động và kết quả của nó, cái mới nhất nằm dưới cùng.
|
||||
|
||||
## Diễn biến một lượt
|
||||
|
||||
1. **Chọn nhân vật** — nhấp vào thẻ, hoặc nhấn **Enter**/**Space** để chọn nhân vật đầu tiên còn điểm hành động.
|
||||
2. **Chọn một lệnh** bằng chuột hoặc **W/S**/các phím mũi tên và **Enter**:
|
||||
- **Attack** — các hành động chiến đấu của nhân vật;
|
||||
- **Items** — vật phẩm dùng được từ túi đồ, mỗi món 1 AP;
|
||||
- **Defend** — hành động phòng thủ của nhân vật lên chính nó;
|
||||
- **Escape** — tỉ lệ thành công 70%; nếu hụt, nhân vật mất 1 AP.
|
||||
3. **Chọn hành động.** Bảng cạnh menu sẽ hiện thẻ hành động và hiệu ứng của nó:
|
||||
|
||||

|
||||
|
||||
4. **Nhấp vào mục tiêu.** Dòng gợi ý "Chọn mục tiêu · chuột phải / Esc — hủy" xuất hiện và các thẻ địch sáng lên:
|
||||
|
||||

|
||||
|
||||
Khi đội dùng hết điểm hành động, quân địch sẽ đến lượt hành động; sau đó một vòng mới bắt đầu và điểm được hồi lại.
|
||||
|
||||
## Đọc nhật ký
|
||||
|
||||
Sau một đòn đánh thường và đòn đáp trả của con sói:
|
||||
|
||||

|
||||
|
||||
Đòn đánh của anh hùng là 12. Lớp khiên 2 điểm của sói hấp thụ phần đầu rồi cạn, nên con sói mất 10 máu: 31 → 21. Sau đó nó dùng Defence, nâng lớp khiên lên 4 để đỡ đòn tiếp theo.
|
||||
|
||||
## Các hành động
|
||||
|
||||
Menu liệt kê các hành động mà nhân vật có (xem Characters Engine). Còn các loại cơ bản thì FF Battle tự xử lý:
|
||||
|
||||
- `Attack=1.5` — sát thương của đòn đánh nhân với con số; lớp khiên của mục tiêu hấp thụ trước;
|
||||
- `Defence=4` — đặt lớp khiên lên đồng đội;
|
||||
- `Turn=2` — khiến mục tiêu choáng đúng số lượt đó;
|
||||
- dùng vật phẩm.
|
||||
|
||||
Các hành động viết bằng công thức, chẳng hạn Power strike hay Fireball của Classic RPG Rules, hoạt động thông qua Battle Formulas; chi phí mana (`Cost=mp:4`) trừ vào mana của nhân vật.
|
||||
|
||||
## Khi có sự cố
|
||||
|
||||
1. **Menu không mở ra.** Nhân vật đã hết điểm hành động trong vòng này, hoặc đang là lượt của quân địch.
|
||||
2. **Items bị mờ đi.** Trong túi đồ không còn vật phẩm nào dùng được trong chiến đấu.
|
||||
3. **Kỹ năng chẳng có tác dụng.** Giá trị của nó cần Battle Formulas, hoặc nó không được đánh dấu là dùng được trong chiến đấu.
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# FF Battle —— 指令菜单式战斗
|
||||
|
||||
FF Battle 是一套经典主机 RPG 风格的战斗系统:队伍在左下角,敌人在上方,还有一个指令菜单——Attack、Items、Defend、Escape。它是 RPG Plus 的战斗系统。战斗本身——发起、背景、胜利与失败画面、经验和掉落——来自 Turn Battle Base;FF Battle 决定的是玩家的战斗方式。
|
||||
|
||||
## 战斗画面
|
||||
|
||||
RPG Plus 的战士在村庄里遭遇一只狼:
|
||||
|
||||

|
||||
|
||||
- **卡牌。** 每个角色显示生命、攻击和防御(DEF 是优先吸收伤害的护盾)、魔力(若游戏有此设定),以及代表行动点的黄色圆点(AP)。
|
||||
- **Battle 面板**——显示回合数、敌我双方剩余数量,以及当前轮到谁行动。
|
||||
- **战斗日志**——记录每个动作及其结果,最新的在最下方。
|
||||
|
||||
## 一个回合的流程
|
||||
|
||||
1. **选择角色**——点击其卡牌,或按 **Enter**/**Space** 选中第一个还有行动点的角色。
|
||||
2. **选择指令**——可用鼠标,或用 **W/S**、方向键加 **Enter**:
|
||||
- **Attack**——角色的战斗动作;
|
||||
- **Items**——背包中可使用的物品,每件 1 AP;
|
||||
- **Defend**——角色对自身的防御动作;
|
||||
- **Escape**——70% 概率逃跑;失败则该角色失去 1 AP。
|
||||
3. **选择动作。** 菜单旁边的面板会显示动作卡及其效果:
|
||||
|
||||

|
||||
|
||||
4. **点击目标。** 屏幕上会出现「选择目标 · 鼠标右键 / Esc — 取消」的提示,敌人卡牌随之高亮:
|
||||
|
||||

|
||||
|
||||
队伍用完行动点后,轮到敌人行动;随后新回合开始,行动点也会恢复。
|
||||
|
||||
## 日志里写了什么
|
||||
|
||||
一次普通攻击和狼的回击之后:
|
||||
|
||||

|
||||
|
||||
英雄的攻击为 12。狼的护盾(2)先吸收了最初的 2 点并耗尽,因此狼损失了 10 点生命:31 → 21。随后狼使用 Defence,把护盾提升到 4 以迎接下一击。
|
||||
|
||||
## 动作类型
|
||||
|
||||
菜单里列出角色拥有的动作(参见 Characters Engine)。而以下几种基础类型由 FF Battle 自身处理:
|
||||
|
||||
- `Attack=1.5`——攻击伤害乘以该数值;优先被目标的护盾吸收;
|
||||
- `Defence=4`——给队友加护盾;
|
||||
- `Turn=2`——眩晕相应的回合数;
|
||||
- 使用物品。
|
||||
|
||||
以公式书写的动作,比如 Classic RPG Rules 的 Power strike 或 Fireball,通过 Battle Formulas 生效;魔力消耗(`Cost=mp:4`)从角色的魔力中扣除。
|
||||
|
||||
## 出问题怎么办
|
||||
|
||||
1. **菜单打不开。** 该角色本回合的行动点已用完,或者当前是敌人的回合。
|
||||
2. **Items 呈灰色。** 背包里没有可在战斗中使用的物品。
|
||||
3. **技能没有任何效果。** 它的数值需要 Battle Formulas,或者它没有被标记为可在战斗中使用。
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = Final Fantasy battles
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = Combate Final Fantasy
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = Final Fantasy式バトル
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = 파이널 판타지 전투
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = Бои Final Fantasy
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = Chiến đấu Final Fantasy
|
||||
@@ -0,0 +1 @@
|
||||
collection_settings/ff = 最终幻想式战斗
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF Battle
|
||||
mod_ff_battle_desc = A battle system in the style of classic console RPGs: the party, the enemies and a command menu — Attack, Items, Defend, Escape. The battle of RPG Plus.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF Battle
|
||||
mod_ff_battle_desc = Un sistema de combate al estilo de las consolas RPG clásicas: el grupo, los enemigos y un menú de comandos — Attack, Items, Defend, Escape. El combate de RPG Plus.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FFバトル
|
||||
mod_ff_battle_desc = 古典的なコンソールRPG風のバトルシステム。パーティ、敵、そして Attack・Items・Defend・Escape のコマンドメニュー。RPG Plus の戦闘です。
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF 배틀
|
||||
mod_ff_battle_desc = 고전 콘솔 RPG 스타일의 전투 시스템입니다. 파티, 적, 그리고 Attack·Items·Defend·Escape 커맨드 메뉴. RPG Plus의 전투죠.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF Battle
|
||||
mod_ff_battle_desc = Боевая система в стиле классических консольных RPG: отряд, враги и меню команд — Attack, Items, Defend, Escape. Бой игры RPG Plus.
|
||||
+46
-2
@@ -1,5 +1,3 @@
|
||||
mod_ff_battle_name = FFBattle
|
||||
mod_ff_battle_desc = Final Fantasy style battle system
|
||||
|
||||
ff_battle/menu_attack = Attack
|
||||
ff_battle/menu_items = Items
|
||||
@@ -12,3 +10,49 @@ ff_battle/select_target = Select a target
|
||||
ff_battle/escape_fail = Escape failed!
|
||||
ff_battle/escape_success = You escaped!
|
||||
ff_battle/no_actions = No actions
|
||||
|
||||
ff_battle/hud_title = Battle
|
||||
ff_battle/round = Round
|
||||
ff_battle/enemies = Enemies
|
||||
ff_battle/allies = Party
|
||||
ff_battle/phase_player = Player turn
|
||||
ff_battle/phase_enemy = Enemy turn
|
||||
ff_battle/banner_player = Player turn
|
||||
ff_battle/banner_enemy = Enemy turn
|
||||
ff_battle/hint_target = Select a target · RMB / Esc — cancel
|
||||
ff_battle/hint_ally = Select an ally · RMB / Esc — cancel
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = Actions
|
||||
|
||||
ff_battle/log_title = Battle log
|
||||
ff_battle/log_round = round {0}
|
||||
ff_battle/log_battle_start = Battle started
|
||||
ff_battle/log_turn_player = — Player turn · round {0} —
|
||||
ff_battle/log_turn_enemy = — Enemy turn · round {0} —
|
||||
ff_battle/log_action = {0}: {1} → {2}
|
||||
ff_battle/log_action_self = {0}: {1}
|
||||
ff_battle/log_damage = {0}: {1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = {0}'s shield absorbed {1} · left {2}
|
||||
ff_battle/log_defence = Defend: {0} · +{1} DEF (total {2})
|
||||
ff_battle/log_stun = Stunned: {0} · {1} turns
|
||||
ff_battle/log_stun_skip = {0} is stunned — skips the turn
|
||||
ff_battle/log_heal = {0}: {1} restored {2} HP
|
||||
ff_battle/log_death = {0} defeated
|
||||
ff_battle/log_escape_success = Escape succeeded
|
||||
ff_battle/log_escape_failed = Escape failed! {0} loses 1 AP
|
||||
ff_battle/log_no_points = The party is out of AP
|
||||
|
||||
ff_battle/detail_attack = Character action submenu. AP cost is shown next to the name.
|
||||
ff_battle/detail_items = 1 AP per item. Targeted items require an ally.
|
||||
ff_battle/detail_defend = Shortcut: the ally-target action is applied to self. Shield +4 DEF.
|
||||
ff_battle/detail_escape = 70% success chance. On failure the character loses 1 AP.
|
||||
ff_battle/detail_back = Return to the main command menu.
|
||||
ff_battle/detail_action = Special action without numeric parameters.
|
||||
ff_battle/detail_item = An item from the shared inventory.
|
||||
ff_battle/detail_damage = Damage {0} (ATK {1} × {2}) · cost {3} AP
|
||||
ff_battle/detail_stun = Stun for {0} turns · cost {1} AP
|
||||
ff_battle/detail_defence = Target shield +{0} · cost {1} AP
|
||||
|
||||
ff_battle/status_stun = Stunned {0}
|
||||
ff_battle/badge_turns = {0} turns
|
||||
ff_battle/badge_self = Self
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
ff_battle/menu_attack = Atacar
|
||||
ff_battle/menu_items = Objetos
|
||||
ff_battle/menu_defend = Defender
|
||||
ff_battle/menu_escape = Huir
|
||||
ff_battle/end_turn = Terminar turno
|
||||
ff_battle/back = Atrás
|
||||
ff_battle/no_items = No hay objetos
|
||||
ff_battle/select_target = Elige un objetivo
|
||||
ff_battle/escape_fail = ¡No has podido huir!
|
||||
ff_battle/escape_success = ¡Has huido!
|
||||
ff_battle/no_actions = No hay acciones
|
||||
|
||||
ff_battle/hud_title = Combate
|
||||
ff_battle/round = Ronda
|
||||
ff_battle/enemies = Enemigos
|
||||
ff_battle/allies = Grupo
|
||||
ff_battle/phase_player = Turno del jugador
|
||||
ff_battle/phase_enemy = Turno del enemigo
|
||||
ff_battle/banner_player = Turno del jugador
|
||||
ff_battle/banner_enemy = Turno del enemigo
|
||||
ff_battle/hint_target = Elige un objetivo · clic derecho / Esc: cancelar
|
||||
ff_battle/hint_ally = Elige un aliado · clic derecho / Esc: cancelar
|
||||
ff_battle/menu_caption = {0} · PA {1}/{2}
|
||||
ff_battle/menu_actions = Acciones
|
||||
|
||||
ff_battle/log_title = Registro de combate
|
||||
ff_battle/log_round = ronda {0}
|
||||
ff_battle/log_battle_start = Comienza el combate
|
||||
ff_battle/log_turn_player = — Turno del jugador · ronda {0} —
|
||||
ff_battle/log_turn_enemy = — Turno del enemigo · ronda {0} —
|
||||
ff_battle/log_action = {0}: {1} → {2}
|
||||
ff_battle/log_action_self = {0}: {1}
|
||||
ff_battle/log_damage = {0}: {1} → {2} · -{3} PV
|
||||
ff_battle/log_shield = El escudo de {0} absorbió {1} · quedan {2}
|
||||
ff_battle/log_defence = Defensa: {0} · +{1} DEF (total {2})
|
||||
ff_battle/log_stun = Aturdido: {0} · {1} turnos
|
||||
ff_battle/log_stun_skip = {0} está aturdido: pierde el turno
|
||||
ff_battle/log_heal = {0}: {1} restauró {2} PV
|
||||
ff_battle/log_death = {0} derrotado
|
||||
ff_battle/log_escape_success = Huida con éxito
|
||||
ff_battle/log_escape_failed = ¡No has podido huir! {0} pierde 1 PA
|
||||
ff_battle/log_no_points = Al grupo no le quedan PA
|
||||
|
||||
ff_battle/detail_attack = Submenú de acciones del personaje. El coste en PA aparece junto al nombre.
|
||||
ff_battle/detail_items = 1 PA por objeto. Los objetos con objetivo requieren un aliado.
|
||||
ff_battle/detail_defend = Atajo: la acción sobre un aliado se aplica a uno mismo. Escudo +4 DEF.
|
||||
ff_battle/detail_escape = 70 % de probabilidad de éxito. Si falla, el personaje pierde 1 PA.
|
||||
ff_battle/detail_back = Volver al menú principal de órdenes.
|
||||
ff_battle/detail_action = Acción especial sin parámetros numéricos.
|
||||
ff_battle/detail_item = Un objeto del inventario compartido.
|
||||
ff_battle/detail_damage = Daño {0} (ATQ {1} × {2}) · coste {3} PA
|
||||
ff_battle/detail_stun = Aturde {0} turnos · coste {1} PA
|
||||
ff_battle/detail_defence = Escudo del objetivo +{0} · coste {1} PA
|
||||
|
||||
ff_battle/status_stun = Aturdido {0}
|
||||
ff_battle/badge_turns = {0} turnos
|
||||
ff_battle/badge_self = A sí mismo
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
ff_battle/menu_attack = たたかう
|
||||
ff_battle/menu_items = アイテム
|
||||
ff_battle/menu_defend = ぼうぎょ
|
||||
ff_battle/menu_escape = にげる
|
||||
ff_battle/end_turn = ターン終了
|
||||
ff_battle/back = 戻る
|
||||
ff_battle/no_items = アイテムがない
|
||||
ff_battle/select_target = 対象を選択
|
||||
ff_battle/escape_fail = 逃げられなかった!
|
||||
ff_battle/escape_success = うまく逃げきった!
|
||||
ff_battle/no_actions = アクションがない
|
||||
|
||||
ff_battle/hud_title = バトル
|
||||
ff_battle/round = ラウンド
|
||||
ff_battle/enemies = 敵
|
||||
ff_battle/allies = パーティ
|
||||
ff_battle/phase_player = プレイヤーのターン
|
||||
ff_battle/phase_enemy = 敵のターン
|
||||
ff_battle/banner_player = プレイヤーのターン
|
||||
ff_battle/banner_enemy = 敵のターン
|
||||
ff_battle/hint_target = 対象を選択 · 右クリック / Esc——キャンセル
|
||||
ff_battle/hint_ally = 味方を選択 · 右クリック / Esc——キャンセル
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = アクション
|
||||
|
||||
ff_battle/log_title = バトルログ
|
||||
ff_battle/log_round = ラウンド {0}
|
||||
ff_battle/log_battle_start = バトル開始
|
||||
ff_battle/log_turn_player = — プレイヤーのターン · ラウンド {0} —
|
||||
ff_battle/log_turn_enemy = — 敵のターン · ラウンド {0} —
|
||||
ff_battle/log_action = {0}:{1} → {2}
|
||||
ff_battle/log_action_self = {0}:{1}
|
||||
ff_battle/log_damage = {0}:{1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = {0} のシールドが {1} を吸収 · 残り {2}
|
||||
ff_battle/log_defence = 防御:{0} · +{1} DEF(合計 {2})
|
||||
ff_battle/log_stun = スタン:{0} · {1} ターン
|
||||
ff_battle/log_stun_skip = {0} はスタン中——ターンを飛ばす
|
||||
ff_battle/log_heal = {0}:{1} が HP を {2} 回復
|
||||
ff_battle/log_death = {0} を倒した
|
||||
ff_battle/log_escape_success = 逃走成功
|
||||
ff_battle/log_escape_failed = 逃げられなかった! {0} は AP を 1 失う
|
||||
ff_battle/log_no_points = パーティの AP が尽きた
|
||||
|
||||
ff_battle/detail_attack = キャラクターのアクションのサブメニュー。名前の横に AP コストを表示。
|
||||
ff_battle/detail_items = アイテム 1 つにつき 1 AP。対象付きのアイテムには味方が必要。
|
||||
ff_battle/detail_defend = ショートカット:味方対象のアクションを自分に使う。シールド +4 DEF。
|
||||
ff_battle/detail_escape = 成功率 70%。失敗するとキャラクターは AP を 1 失う。
|
||||
ff_battle/detail_back = メインのコマンドメニューに戻る。
|
||||
ff_battle/detail_action = 数値パラメーターのない特殊アクション。
|
||||
ff_battle/detail_item = 共有インベントリのアイテム。
|
||||
ff_battle/detail_damage = ダメージ {0}(攻撃 {1} × {2})· コスト {3} AP
|
||||
ff_battle/detail_stun = {0} ターンのスタン · コスト {1} AP
|
||||
ff_battle/detail_defence = 対象のシールド +{0} · コスト {1} AP
|
||||
|
||||
ff_battle/status_stun = スタン {0}
|
||||
ff_battle/badge_turns = {0} ターン
|
||||
ff_battle/badge_self = 自分
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
ff_battle/menu_attack = 공격
|
||||
ff_battle/menu_items = 아이템
|
||||
ff_battle/menu_defend = 방어
|
||||
ff_battle/menu_escape = 도망
|
||||
ff_battle/end_turn = 턴 종료
|
||||
ff_battle/back = 뒤로
|
||||
ff_battle/no_items = 아이템 없음
|
||||
ff_battle/select_target = 대상 선택
|
||||
ff_battle/escape_fail = 도망치지 못했다!
|
||||
ff_battle/escape_success = 도망쳤다!
|
||||
ff_battle/no_actions = 액션 없음
|
||||
|
||||
ff_battle/hud_title = 전투
|
||||
ff_battle/round = 라운드
|
||||
ff_battle/enemies = 적
|
||||
ff_battle/allies = 파티
|
||||
ff_battle/phase_player = 플레이어 턴
|
||||
ff_battle/phase_enemy = 적 턴
|
||||
ff_battle/banner_player = 플레이어 턴
|
||||
ff_battle/banner_enemy = 적 턴
|
||||
ff_battle/hint_target = 대상 선택 · 우클릭 / Esc — 취소
|
||||
ff_battle/hint_ally = 아군 선택 · 우클릭 / Esc — 취소
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = 액션
|
||||
|
||||
ff_battle/log_title = 전투 로그
|
||||
ff_battle/log_round = {0}라운드
|
||||
ff_battle/log_battle_start = 전투 시작
|
||||
ff_battle/log_turn_player = — 플레이어 턴 · {0}라운드 —
|
||||
ff_battle/log_turn_enemy = — 적 턴 · {0}라운드 —
|
||||
ff_battle/log_action = {0}: {1} → {2}
|
||||
ff_battle/log_action_self = {0}: {1}
|
||||
ff_battle/log_damage = {0}: {1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = {0}의 보호막이 {1}을(를) 흡수 · 남은 양 {2}
|
||||
ff_battle/log_defence = 방어: {0} · +{1} DEF (합계 {2})
|
||||
ff_battle/log_stun = 기절: {0} · {1}턴
|
||||
ff_battle/log_stun_skip = {0}은(는) 기절 — 턴을 건너뜀
|
||||
ff_battle/log_heal = {0}: {1}이(가) HP {2} 회복
|
||||
ff_battle/log_death = {0} 쓰러짐
|
||||
ff_battle/log_escape_success = 도망 성공
|
||||
ff_battle/log_escape_failed = 도망 실패! {0}은(는) AP를 1 잃는다
|
||||
ff_battle/log_no_points = 파티의 AP가 바닥났다
|
||||
|
||||
ff_battle/detail_attack = 캐릭터의 액션 하위 메뉴. 이름 옆에 AP 비용이 표시됩니다.
|
||||
ff_battle/detail_items = 아이템 하나에 1 AP. 대상이 있는 아이템은 아군이 필요합니다.
|
||||
ff_battle/detail_defend = 단축키: 아군 대상 액션을 자신에게 사용. 보호막 +4 DEF.
|
||||
ff_battle/detail_escape = 성공 확률 70%. 실패하면 캐릭터가 AP를 1 잃습니다.
|
||||
ff_battle/detail_back = 메인 명령 메뉴로 돌아갑니다.
|
||||
ff_battle/detail_action = 수치 매개변수가 없는 특수 액션.
|
||||
ff_battle/detail_item = 공용 인벤토리의 아이템.
|
||||
ff_battle/detail_damage = 피해 {0} (공격 {1} × {2}) · 비용 {3} AP
|
||||
ff_battle/detail_stun = {0}턴 기절 · 비용 {1} AP
|
||||
ff_battle/detail_defence = 대상 보호막 +{0} · 비용 {1} AP
|
||||
|
||||
ff_battle/status_stun = 기절 {0}
|
||||
ff_battle/badge_turns = {0}턴
|
||||
ff_battle/badge_self = 자신
|
||||
+46
-2
@@ -1,5 +1,3 @@
|
||||
mod_ff_battle_name=FF Битва
|
||||
mod_ff_battle_desc=Боевая система в стиле Final Fantasy
|
||||
|
||||
ff_battle/menu_attack=Атака
|
||||
ff_battle/menu_items=Предметы
|
||||
@@ -12,3 +10,49 @@ ff_battle/select_target=Выберите цель
|
||||
ff_battle/escape_fail=Побег не удался!
|
||||
ff_battle/escape_success=Вы сбежали!
|
||||
ff_battle/no_actions=Нет действий
|
||||
|
||||
ff_battle/hud_title=Бой
|
||||
ff_battle/round=Раунд
|
||||
ff_battle/enemies=Врагов
|
||||
ff_battle/allies=Отряд
|
||||
ff_battle/phase_player=Ход игрока
|
||||
ff_battle/phase_enemy=Ход врага
|
||||
ff_battle/banner_player=Ход игрока
|
||||
ff_battle/banner_enemy=Ход врага
|
||||
ff_battle/hint_target=Выберите цель · ПКМ / Esc — отмена
|
||||
ff_battle/hint_ally=Выберите союзника · ПКМ / Esc — отмена
|
||||
ff_battle/menu_caption={0} · ОД {1}/{2}
|
||||
ff_battle/menu_actions=Действия
|
||||
|
||||
ff_battle/log_title=Журнал боя
|
||||
ff_battle/log_round=раунд {0}
|
||||
ff_battle/log_battle_start=Бой начался
|
||||
ff_battle/log_turn_player=— Ход игрока · раунд {0} —
|
||||
ff_battle/log_turn_enemy=— Ход врага · раунд {0} —
|
||||
ff_battle/log_action={0}: {1} → {2}
|
||||
ff_battle/log_action_self={0}: {1}
|
||||
ff_battle/log_damage={0}: {1} → {2} · −{3} HP
|
||||
ff_battle/log_shield=Щит {0} поглотил {1} · осталось {2}
|
||||
ff_battle/log_defence=Защита: {0} · +{1} ЗАЩ (всего {2})
|
||||
ff_battle/log_stun=Ошеломление: {0} · {1} хода
|
||||
ff_battle/log_stun_skip={0} ошеломлён — пропускает ход
|
||||
ff_battle/log_heal={0}: {1} восстановил {2} HP
|
||||
ff_battle/log_death={0} повержен
|
||||
ff_battle/log_escape_success=Побег удался
|
||||
ff_battle/log_escape_failed=Побег не удался! {0} теряет 1 ОД
|
||||
ff_battle/log_no_points=У отряда закончились ОД
|
||||
|
||||
ff_battle/detail_attack=Подменю действий персонажа. Стоимость в ОД указана справа от названия.
|
||||
ff_battle/detail_items=Расход 1 ОД за предмет. Предметы «по цели» требуют выбора союзника.
|
||||
ff_battle/detail_defend=Шорткат: действие «по союзнику» применяется на себя. Щит +4 ЗАЩ.
|
||||
ff_battle/detail_escape=Шанс успеха 70%. При провале персонаж теряет 1 ОД.
|
||||
ff_battle/detail_back=Вернуться в главное меню команд.
|
||||
ff_battle/detail_action=Особое действие без числовых параметров.
|
||||
ff_battle/detail_item=Предмет из общего инвентаря.
|
||||
ff_battle/detail_damage=Урон {0} (АТК {1} × {2}) · расход {3} ОД
|
||||
ff_battle/detail_stun=Ошеломление на {0} хода · расход {1} ОД
|
||||
ff_battle/detail_defence=Щит цели +{0} · расход {1} ОД
|
||||
|
||||
ff_battle/status_stun=Ошеломлён {0}
|
||||
ff_battle/badge_turns={0} хода
|
||||
ff_battle/badge_self=На себя
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
ff_battle/menu_attack = Tấn công
|
||||
ff_battle/menu_items = Vật phẩm
|
||||
ff_battle/menu_defend = Phòng thủ
|
||||
ff_battle/menu_escape = Bỏ chạy
|
||||
ff_battle/end_turn = Kết thúc lượt
|
||||
ff_battle/back = Quay lại
|
||||
ff_battle/no_items = Không có vật phẩm
|
||||
ff_battle/select_target = Chọn mục tiêu
|
||||
ff_battle/escape_fail = Bỏ chạy thất bại!
|
||||
ff_battle/escape_success = Bạn đã chạy thoát!
|
||||
ff_battle/no_actions = Không có hành động
|
||||
|
||||
ff_battle/hud_title = Trận đấu
|
||||
ff_battle/round = Vòng
|
||||
ff_battle/enemies = Kẻ địch
|
||||
ff_battle/allies = Tổ đội
|
||||
ff_battle/phase_player = Lượt người chơi
|
||||
ff_battle/phase_enemy = Lượt kẻ địch
|
||||
ff_battle/banner_player = Lượt người chơi
|
||||
ff_battle/banner_enemy = Lượt kẻ địch
|
||||
ff_battle/hint_target = Chọn mục tiêu · chuột phải / Esc — hủy
|
||||
ff_battle/hint_ally = Chọn đồng đội · chuột phải / Esc — hủy
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = Hành động
|
||||
|
||||
ff_battle/log_title = Nhật ký trận đấu
|
||||
ff_battle/log_round = vòng {0}
|
||||
ff_battle/log_battle_start = Trận đấu bắt đầu
|
||||
ff_battle/log_turn_player = — Lượt người chơi · vòng {0} —
|
||||
ff_battle/log_turn_enemy = — Lượt kẻ địch · vòng {0} —
|
||||
ff_battle/log_action = {0}: {1} → {2}
|
||||
ff_battle/log_action_self = {0}: {1}
|
||||
ff_battle/log_damage = {0}: {1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = Khiên của {0} hấp thụ {1} · còn {2}
|
||||
ff_battle/log_defence = Phòng thủ: {0} · +{1} DEF (tổng {2})
|
||||
ff_battle/log_stun = Bị choáng: {0} · {1} lượt
|
||||
ff_battle/log_stun_skip = {0} bị choáng — mất lượt
|
||||
ff_battle/log_heal = {0}: {1} hồi {2} HP
|
||||
ff_battle/log_death = {0} đã bị hạ
|
||||
ff_battle/log_escape_success = Bỏ chạy thành công
|
||||
ff_battle/log_escape_failed = Bỏ chạy thất bại! {0} mất 1 AP
|
||||
ff_battle/log_no_points = Tổ đội đã hết AP
|
||||
|
||||
ff_battle/detail_attack = Menu con hành động của nhân vật. Chi phí AP hiển thị cạnh tên.
|
||||
ff_battle/detail_items = 1 AP cho mỗi vật phẩm. Vật phẩm có mục tiêu cần một đồng đội.
|
||||
ff_battle/detail_defend = Phím tắt: hành động nhắm đồng đội được áp dụng cho bản thân. Khiên +4 DEF.
|
||||
ff_battle/detail_escape = Tỉ lệ thành công 70%. Nếu thất bại, nhân vật mất 1 AP.
|
||||
ff_battle/detail_back = Quay lại menu lệnh chính.
|
||||
ff_battle/detail_action = Hành động đặc biệt không có tham số số.
|
||||
ff_battle/detail_item = Một vật phẩm trong túi đồ chung.
|
||||
ff_battle/detail_damage = Sát thương {0} (ATK {1} × {2}) · tốn {3} AP
|
||||
ff_battle/detail_stun = Choáng {0} lượt · tốn {1} AP
|
||||
ff_battle/detail_defence = Khiên mục tiêu +{0} · tốn {1} AP
|
||||
|
||||
ff_battle/status_stun = Bị choáng {0}
|
||||
ff_battle/badge_turns = {0} lượt
|
||||
ff_battle/badge_self = Bản thân
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
ff_battle/menu_attack = 攻击
|
||||
ff_battle/menu_items = 物品
|
||||
ff_battle/menu_defend = 防御
|
||||
ff_battle/menu_escape = 逃跑
|
||||
ff_battle/end_turn = 结束回合
|
||||
ff_battle/back = 返回
|
||||
ff_battle/no_items = 没有物品
|
||||
ff_battle/select_target = 选择目标
|
||||
ff_battle/escape_fail = 逃跑失败!
|
||||
ff_battle/escape_success = 你逃走了!
|
||||
ff_battle/no_actions = 没有行动
|
||||
|
||||
ff_battle/hud_title = 战斗
|
||||
ff_battle/round = 回合
|
||||
ff_battle/enemies = 敌人
|
||||
ff_battle/allies = 队伍
|
||||
ff_battle/phase_player = 玩家回合
|
||||
ff_battle/phase_enemy = 敌方回合
|
||||
ff_battle/banner_player = 玩家回合
|
||||
ff_battle/banner_enemy = 敌方回合
|
||||
ff_battle/hint_target = 选择目标 · 右键 / Esc——取消
|
||||
ff_battle/hint_ally = 选择队友 · 右键 / Esc——取消
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = 行动
|
||||
|
||||
ff_battle/log_title = 战斗日志
|
||||
ff_battle/log_round = 第 {0} 回合
|
||||
ff_battle/log_battle_start = 战斗开始
|
||||
ff_battle/log_turn_player = — 玩家回合 · 第 {0} 回合 —
|
||||
ff_battle/log_turn_enemy = — 敌方回合 · 第 {0} 回合 —
|
||||
ff_battle/log_action = {0}:{1} → {2}
|
||||
ff_battle/log_action_self = {0}:{1}
|
||||
ff_battle/log_damage = {0}:{1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = {0} 的护盾吸收了 {1} · 剩余 {2}
|
||||
ff_battle/log_defence = 防御:{0} · +{1} 防御(共 {2})
|
||||
ff_battle/log_stun = 眩晕:{0} · {1} 回合
|
||||
ff_battle/log_stun_skip = {0} 处于眩晕——跳过回合
|
||||
ff_battle/log_heal = {0}:{1} 恢复了 {2} HP
|
||||
ff_battle/log_death = {0} 被击败
|
||||
ff_battle/log_escape_success = 逃跑成功
|
||||
ff_battle/log_escape_failed = 逃跑失败!{0} 失去 1 AP
|
||||
ff_battle/log_no_points = 队伍的 AP 用完了
|
||||
|
||||
ff_battle/detail_attack = 角色的行动子菜单。名称旁显示 AP 消耗。
|
||||
ff_battle/detail_items = 每个物品消耗 1 AP。需要目标的物品要选择队友。
|
||||
ff_battle/detail_defend = 快捷方式:对队友的行动作用于自己。护盾 +4 防御。
|
||||
ff_battle/detail_escape = 成功率 70%。失败时角色失去 1 AP。
|
||||
ff_battle/detail_back = 返回主指令菜单。
|
||||
ff_battle/detail_action = 没有数值参数的特殊行动。
|
||||
ff_battle/detail_item = 共享背包中的物品。
|
||||
ff_battle/detail_damage = 伤害 {0}(攻击 {1} × {2})· 消耗 {3} AP
|
||||
ff_battle/detail_stun = 眩晕 {0} 回合 · 消耗 {1} AP
|
||||
ff_battle/detail_defence = 目标护盾 +{0} · 消耗 {1} AP
|
||||
|
||||
ff_battle/status_stun = 眩晕 {0}
|
||||
ff_battle/badge_turns = {0} 回合
|
||||
ff_battle/badge_self = 自己
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF Battle
|
||||
mod_ff_battle_desc = Hệ thống chiến đấu mang phong cách RPG máy chơi game cổ điển: đội của bạn, kẻ địch và một menu lệnh — Attack, Items, Defend, Escape. Trận chiến của RPG Plus.
|
||||
@@ -0,0 +1,2 @@
|
||||
mod_ff_battle_name = FF 战斗
|
||||
mod_ff_battle_desc = 经典主机 RPG 风格的战斗系统:队伍、敌人和一个指令菜单——Attack、Items、Defend、Escape。RPG Plus 的战斗。
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"id": "ff_battle",
|
||||
"name": "FFBattle",
|
||||
"name": "FF Battle",
|
||||
"version": "1.0.0",
|
||||
"author": "ct",
|
||||
"description": "Final Fantasy style battle system",
|
||||
"description": "A battle system in the style of classic console RPGs: the party, the enemies and a command menu — Attack, Items, Defend, Escape. The battle of RPG Plus.",
|
||||
"category": "gameplay",
|
||||
"sources": [
|
||||
"src/**/*.cs"
|
||||
],
|
||||
"depends": [ "core>=1", "interface>=1" ]
|
||||
"depends": [ "ui_base>=1", "turn_battle_base>=1" ]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFActionDetailPanel.cs" id="1_script"]
|
||||
|
||||
[node name="FFActionDetailPanel" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(300, 160)
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Box" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Card" type="PanelContainer" parent="Box"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="CardRow" type="HBoxContainer" parent="Box/Card"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="Stripe" type="ColorRect" parent="Box/Card/CardRow"]
|
||||
custom_minimum_size = Vector2(4, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="IconMargin" type="MarginContainer" parent="Box/Card/CardRow"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 8
|
||||
theme_override_constants/margin_top = 8
|
||||
theme_override_constants/margin_right = 12
|
||||
theme_override_constants/margin_bottom = 8
|
||||
|
||||
[node name="CardIcon" type="TextureRect" parent="Box/Card/CardRow/IconMargin"]
|
||||
custom_minimum_size = Vector2(52, 52)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Body" type="VBoxContainer" parent="Box/Card/CardRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="NameRow" type="HBoxContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="NameLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="IdLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 9
|
||||
|
||||
[node name="Badges" type="HFlowContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 5
|
||||
theme_override_constants/v_separation = 5
|
||||
|
||||
[node name="Props" type="HFlowContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 5
|
||||
theme_override_constants/v_separation = 3
|
||||
|
||||
[node name="Note" type="PanelContainer" parent="Box"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="NoteLabel" type="Label" parent="Box/Note"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
autowrap_mode = 3
|
||||
@@ -0,0 +1,26 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleBanner.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleBanner" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="Center"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Center/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
horizontal_alignment = 1
|
||||
uppercase = true
|
||||
@@ -0,0 +1,88 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleHud.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleHud" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(380, 92)
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Column" type="VBoxContainer" parent="Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 9
|
||||
|
||||
[node name="TopRow" type="HBoxContainer" parent="Panel/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Panel/Column/TopRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/hud_title"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="PhaseBadge" type="PanelContainer" parent="Panel/Column/TopRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="PhaseLabel" type="Label" parent="Panel/Column/TopRow/PhaseBadge"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
uppercase = true
|
||||
|
||||
[node name="MetaRow" type="HBoxContainer" parent="Panel/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="RoundCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/round"
|
||||
auto_translate = true
|
||||
|
||||
[node name="RoundValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "1"
|
||||
|
||||
[node name="EnemiesCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/enemies"
|
||||
auto_translate = true
|
||||
|
||||
[node name="EnemiesValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "0"
|
||||
|
||||
[node name="AlliesCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/allies"
|
||||
auto_translate = true
|
||||
|
||||
[node name="AlliesValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "0"
|
||||
@@ -0,0 +1,65 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleLogPanel.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleLogPanel" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Window" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Column" type="VBoxContainer" parent="Window"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="Header" type="MarginContainer" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 13
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="HeaderRow" type="HBoxContainer" parent="Window/Column/Header"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/log_title"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="Spacer" type="Control" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="RoundLabel" type="Label" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
|
||||
[node name="Rule" type="HSeparator" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="Window/Column/Scroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 0
|
||||
mouse_filter = 2
|
||||
@@ -0,0 +1,35 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleTargetHint.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleTargetHint" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Toast" type="PanelContainer" parent="Center"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Row" type="HBoxContainer" parent="Center/Toast"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="IconLabel" type="Label" parent="Center/Toast/Row"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "◎"
|
||||
|
||||
[node name="TextLabel" type="Label" parent="Center/Toast/Row"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 13
|
||||
+150
-28
@@ -5,51 +5,173 @@
|
||||
[node name="FFCharacterWidget" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(180, 260)
|
||||
layout_mode = 3
|
||||
mouse_filter = 0
|
||||
|
||||
[node name="Sprite" type="TextureRect" parent="."]
|
||||
[node name="TeamLine" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 2.0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Art" type="TextureRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 3.0
|
||||
offset_top = 3.0
|
||||
offset_right = -3.0
|
||||
offset_bottom = -92.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = -44.0
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="HpBg" type="ColorRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 218.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 236.0
|
||||
[node name="Info" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -92.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
mouse_filter = 2
|
||||
color = Color(0.15, 0.15, 0.15, 1)
|
||||
|
||||
[node name="HpFill" type="ColorRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 218.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 236.0
|
||||
[node name="Column" type="VBoxContainer" parent="Info"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="NameRow" type="HBoxContainer" parent="Info/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="TeamDot" type="Panel" parent="Info/Column/NameRow"]
|
||||
custom_minimum_size = Vector2(7, 7)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
color = Color(0.1, 0.85, 0.1, 1)
|
||||
|
||||
[node name="NameLabel" type="Label" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 238.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 256.0
|
||||
[node name="NameLabel" type="Label" parent="Info/Column/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="HpNumLabel" type="Label" parent="Info/Column/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 11
|
||||
|
||||
[node name="HpBarStack" type="Control" parent="Info/Column"]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 13
|
||||
|
||||
[node name="Selection" type="TextureRect" parent="."]
|
||||
[node name="ShieldBar" type="ProgressBar" parent="Info/Column/HpBarStack"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
show_percentage = false
|
||||
|
||||
[node name="HpBar" type="ProgressBar" parent="Info/Column/HpBarStack"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
show_percentage = false
|
||||
|
||||
[node name="Foot" type="HBoxContainer" parent="Info/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="PipsBox" type="HBoxContainer" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="AtkLabel" type="Label" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
|
||||
[node name="Spacer" type="Control" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ShieldChip" type="PanelContainer" parent="Info/Column/Foot"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ShieldRow" type="HBoxContainer" parent="Info/Column/Foot/ShieldChip"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="ShieldKey" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "ui/character_panel/stat_def"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="ShieldValue" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "0"
|
||||
|
||||
[node name="StatusBox" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -140.0
|
||||
offset_top = 8.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = 40.0
|
||||
grow_horizontal = 0
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="StunBadge" type="PanelContainer" parent="StatusBox"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="StunRow" type="HBoxContainer" parent="StatusBox/StunBadge"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="StunIcon" type="TextureRect" parent="StatusBox/StunBadge/StunRow"]
|
||||
custom_minimum_size = Vector2(14, 14)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="StunLabel" type="Label" parent="StatusBox/StunBadge/StunRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
uppercase = true
|
||||
|
||||
[node name="Selection" type="Panel" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
|
||||
@@ -13,9 +13,9 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.02, 0.05, 0.12, 0.88)
|
||||
color = Color(0.062745, 0.066667, 0.094118, 1)
|
||||
|
||||
[node name="Border" type="ColorRect" parent="."]
|
||||
[node name="Border" type="Panel" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -27,11 +27,20 @@ offset_top = -3.0
|
||||
offset_right = 3.0
|
||||
offset_bottom = 3.0
|
||||
mouse_filter = 2
|
||||
color = Color(0.3, 0.5, 0.9, 0.9)
|
||||
|
||||
[node name="Cursor" type="Label" parent="."]
|
||||
text = "►"
|
||||
mouse_filter = 2
|
||||
visible = false
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_colors/font_color = Color(1, 0.95, 0.7, 1)
|
||||
theme_override_colors/font_color = Color(0.941176, 0.756863, 0.411765, 1)
|
||||
|
||||
[node name="Caption" type="Label" parent="."]
|
||||
offset_left = 2.0
|
||||
offset_top = -25.0
|
||||
offset_right = 218.0
|
||||
offset_bottom = -7.0
|
||||
mouse_filter = 2
|
||||
visible = false
|
||||
theme_override_font_sizes/font_size = 11
|
||||
uppercase = true
|
||||
@@ -49,9 +49,17 @@ namespace Cthangover.FFBattle.Actions
|
||||
{
|
||||
if (Source == null || Target == null || Action == null)
|
||||
return true;
|
||||
// The target's death animation may free its widget mid-animation;
|
||||
// finish early and let DoEnd snap the source back.
|
||||
if (!IsAlive(Source) || !IsAlive(Target))
|
||||
return true;
|
||||
return DoInternalAction();
|
||||
}
|
||||
|
||||
/// <summary>True while the widget is still in the tree and not queued for deletion.</summary>
|
||||
protected static bool IsAlive(FFCharacterWidget widget)
|
||||
=> GodotObject.IsInstanceValid(widget) && !widget.IsQueuedForDeletion();
|
||||
|
||||
/// <summary>Captures the initial world positions of source and target, then delegates to subclass setup.</summary>
|
||||
public void DoStart()
|
||||
{
|
||||
@@ -71,8 +79,11 @@ namespace Cthangover.FFBattle.Actions
|
||||
DoInternalEnd();
|
||||
}
|
||||
|
||||
/// <summary>Frame tick of the animation; implementors advance phases and return true when finished.</summary>
|
||||
protected abstract bool DoInternalAction();
|
||||
/// <summary>Phase setup called once by <see cref="DoStart"/> after positions are captured.</summary>
|
||||
protected abstract void DoInternalStart();
|
||||
/// <summary>Cleanup called once by <see cref="DoEnd"/> — restore captured positions here.</summary>
|
||||
protected abstract void DoInternalEnd();
|
||||
|
||||
/// <summary>Quadratic ease-out: fast start, decelerating to the target. Used for return-to-start motion.</summary>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.TurnBattle;
|
||||
using Cthangover.FFBattle.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.Actions
|
||||
@@ -27,9 +29,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
public FFAttackAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f)
|
||||
: base(source, target, action, speed) { }
|
||||
|
||||
/// <summary>Starts the attack sequence: compute the stop point short of the target and reset the phase clock.</summary>
|
||||
protected override void DoInternalStart()
|
||||
{
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
_damageApplied = false;
|
||||
_phase = Phase.MoveForward;
|
||||
|
||||
@@ -37,9 +40,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
_midPoint = TargetPos - dir * 30f;
|
||||
}
|
||||
|
||||
/// <summary>Advances move-in → impact (damage via <see cref="ActionExecutorHub"/>, hit flash/shake, damage popup) → move-back.</summary>
|
||||
protected override bool DoInternalAction()
|
||||
{
|
||||
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale;
|
||||
var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
@@ -51,7 +55,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.Impact;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -69,7 +73,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
|
||||
if (result.Result)
|
||||
{
|
||||
Target.Flash(new Color(1f, 0.3f, 0.3f, 1f), 0.2f);
|
||||
Target.Flash(UiTokens.Danger, 0.2f);
|
||||
Target.Shake(4f, 0.25f);
|
||||
|
||||
if (result.Target.Damage > 0)
|
||||
@@ -84,7 +88,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.MoveBack;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -106,11 +110,14 @@ namespace Cthangover.FFBattle.Actions
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Returns the attacker to its original position and refreshes both widgets.</summary>
|
||||
protected override void DoInternalEnd()
|
||||
{
|
||||
if (IsAlive(Source))
|
||||
Source.GlobalPosition = SourcePos;
|
||||
Source.UpdateInfo();
|
||||
Target.UpdateInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
@@ -15,16 +16,18 @@ namespace Cthangover.FFBattle.Actions
|
||||
/// </summary>
|
||||
public class FFDamageExecutor : ActionBase
|
||||
{
|
||||
/// <summary>Hub registry key for this executor.</summary>
|
||||
public override string ID => "FFDamageExecutor";
|
||||
|
||||
/// <summary>Applies attack damage with status hooks and defence absorption; consumes the action's required points.</summary>
|
||||
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var damageDelta = Mathf.RoundToInt(action.GetFloat("Attack", 1f) * user.Attributes.Attack.Value);
|
||||
target.StatusEffectQueue.OnTakeDamage(user, ref damageDelta);
|
||||
user.StatusEffectQueue.OnDealDamage(target, ref damageDelta);
|
||||
StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta);
|
||||
StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta);
|
||||
|
||||
var defenceDelta = 0;
|
||||
var ignoreArmor = user.HasPassiveEffect("IgnoreArmor");
|
||||
|
||||
@@ -12,8 +12,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
/// </summary>
|
||||
public class FFDefenceExecutor : ActionBase
|
||||
{
|
||||
/// <summary>Hub registry key for this executor.</summary>
|
||||
public override string ID => "FFDefenceExecutor";
|
||||
|
||||
/// <summary>Adds the action's "Defence" value (default 4) to the target's defence shield.</summary>
|
||||
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.TurnBattle;
|
||||
using Cthangover.FFBattle.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.Actions
|
||||
@@ -28,17 +30,19 @@ namespace Cthangover.FFBattle.Actions
|
||||
public FFDefendAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f)
|
||||
: base(source, target, action, speed) { }
|
||||
|
||||
/// <summary>Starts the defend sequence from the bounce phase and captures the glow state.</summary>
|
||||
protected override void DoInternalStart()
|
||||
{
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
_effectApplied = false;
|
||||
_phase = Phase.Bounce;
|
||||
_bounceOffset = new Vector2(0, -12f);
|
||||
}
|
||||
|
||||
/// <summary>Advances bounce → glow (defence application with a green flash) → settle.</summary>
|
||||
protected override bool DoInternalAction()
|
||||
{
|
||||
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale;
|
||||
var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
@@ -51,7 +55,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.Glow;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -69,7 +73,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
|
||||
if (result.Result)
|
||||
{
|
||||
Target.Flash(new Color(0.3f, 1f, 0.3f, 1f), 0.4f);
|
||||
Target.Flash(UiTokens.Success, 0.4f);
|
||||
|
||||
if (result.Target.Defence > 0)
|
||||
ShowDamageBehaviour.SpawnDefence(result.Target.Defence, Target, Target.GlobalPosition);
|
||||
@@ -82,7 +86,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.Recover;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -104,11 +108,14 @@ namespace Cthangover.FFBattle.Actions
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Returns the defender to its original position and refreshes both widgets.</summary>
|
||||
protected override void DoInternalEnd()
|
||||
{
|
||||
if (IsAlive(Source))
|
||||
Source.GlobalPosition = SourcePos;
|
||||
Source.UpdateInfo();
|
||||
Target.UpdateInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.FFBattle.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.Actions
|
||||
@@ -30,9 +31,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
_item = item;
|
||||
}
|
||||
|
||||
/// <summary>Spawns the raised item icon above the user and starts the item sequence.</summary>
|
||||
protected override void DoInternalStart()
|
||||
{
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
_effectApplied = false;
|
||||
_phase = Phase.RaiseItem;
|
||||
|
||||
@@ -48,9 +50,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Advances raise-item → apply (item use with effect flash) → lower-icon.</summary>
|
||||
protected override bool DoInternalAction()
|
||||
{
|
||||
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale;
|
||||
var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
@@ -70,7 +73,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.ApplyEffect;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -84,7 +87,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
var localTarget = Target.GlobalPosition - Source.Position;
|
||||
var iconStart = Source.GlobalPosition - Source.Position + new Vector2(20, -70);
|
||||
_itemIcon.Position = iconStart.Lerp(localTarget + new Vector2(20, -20), EaseInOutQuad(progress));
|
||||
_itemIcon.Modulate = new Color(1, 1, 1, 1f - progress * 0.5f);
|
||||
_itemIcon.Modulate = UiTokens.Alpha(UiTokens.White, 1f - progress * 0.5f);
|
||||
}
|
||||
|
||||
if (!_effectApplied && progress >= 0.3f)
|
||||
@@ -96,13 +99,13 @@ namespace Cthangover.FFBattle.Actions
|
||||
|
||||
Source.UpdateInfo();
|
||||
Target.UpdateInfo();
|
||||
Target.Flash(new Color(0.5f, 1f, 0.5f, 1f), 0.3f);
|
||||
Target.Flash(UiTokens.Success, 0.3f);
|
||||
}
|
||||
|
||||
if (progress >= 1f)
|
||||
{
|
||||
_phase = Phase.Recover;
|
||||
Timestamp = Time.GetTicksUsec() / 1_000_000.0;
|
||||
Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -124,8 +127,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Frees the item icon, returns the user to its original position and refreshes both widgets.</summary>
|
||||
protected override void DoInternalEnd()
|
||||
{
|
||||
if (IsAlive(Source))
|
||||
Source.GlobalPosition = SourcePos;
|
||||
|
||||
if (_itemIcon != null)
|
||||
@@ -139,3 +144,5 @@ namespace Cthangover.FFBattle.Actions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
@@ -12,8 +13,10 @@ namespace Cthangover.FFBattle.Actions
|
||||
/// </summary>
|
||||
public class FFStunExecutor : ActionBase
|
||||
{
|
||||
/// <summary>Hub registry key for this executor.</summary>
|
||||
public override string ID => "FFStunExecutor";
|
||||
|
||||
/// <summary>Adds the stun effect for the action's "Turn" count (default 3); stun-immune targets consume the points but stay un-stunned.</summary>
|
||||
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
|
||||
{
|
||||
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
|
||||
@@ -23,8 +26,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
return new ChangedAttributes { Result = true };
|
||||
|
||||
var turns = action.GetInt("Turn", 3);
|
||||
target.StatusEffectQueue.Add("effect/physics/stun", turns);
|
||||
|
||||
StatusEffectService.Of(target).Add("effect/physics/stun", turns);
|
||||
GameLogger.Log("FF_BATTLE",
|
||||
$"{user.Name} stuns {target.Name} for {turns} turns",
|
||||
LogLevel.Debug);
|
||||
|
||||
+297
-12
@@ -1,15 +1,19 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.CharacterPanel;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.FFBattle.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle
|
||||
@@ -37,6 +41,11 @@ namespace Cthangover.FFBattle
|
||||
private FFPlayerPanel _playerPanel;
|
||||
private FFEnemyPanel _enemyPanel;
|
||||
private FFMenuPanel _menuPanel;
|
||||
private FFActionDetailPanel _detailPanel;
|
||||
private FFBattleHud _hud;
|
||||
private FFBattleLogPanel _logPanel;
|
||||
private FFBattleBanner _banner;
|
||||
private FFBattleTargetHint _targetHint;
|
||||
private FFBattleController _controller;
|
||||
private Control _toolPanel;
|
||||
|
||||
@@ -48,6 +57,12 @@ namespace Cthangover.FFBattle
|
||||
private bool _targetSelectMode;
|
||||
private bool _targetAlly;
|
||||
private System.Action<FFCharacterWidget> _onTargetSelected;
|
||||
private int _round;
|
||||
private int _detailKind;
|
||||
|
||||
private const int DetailMain = 0;
|
||||
private const int DetailActions = 1;
|
||||
private const int DetailItems = 2;
|
||||
|
||||
private const float PLAYER_SCALE = 0.85f;
|
||||
|
||||
@@ -90,6 +105,33 @@ namespace Cthangover.FFBattle
|
||||
_menuPanel.HideMenu();
|
||||
panel.AddChild(_menuPanel);
|
||||
|
||||
_detailPanel = (FFActionDetailPanel)TscnScenes.LoadAndBuild("scenes/ff_action_detail_panel.tscn");
|
||||
_detailPanel.Name = "FFActionDetailPanel";
|
||||
_detailPanel.EnsureConstructed();
|
||||
_detailPanel.HidePassport();
|
||||
panel.AddChild(_detailPanel);
|
||||
|
||||
_hud = (FFBattleHud)TscnScenes.LoadAndBuild("scenes/ff_battle_hud.tscn");
|
||||
_hud.Name = "FFBattleHud";
|
||||
_hud.EnsureConstructed();
|
||||
panel.AddChild(_hud);
|
||||
|
||||
_logPanel = (FFBattleLogPanel)TscnScenes.LoadAndBuild("scenes/ff_battle_log_panel.tscn");
|
||||
_logPanel.Name = "FFBattleLogPanel";
|
||||
_logPanel.EnsureConstructed();
|
||||
panel.AddChild(_logPanel);
|
||||
|
||||
_banner = (FFBattleBanner)TscnScenes.LoadAndBuild("scenes/ff_battle_banner.tscn");
|
||||
_banner.Name = "FFBattleBanner";
|
||||
_banner.EnsureConstructed();
|
||||
panel.AddChild(_banner);
|
||||
|
||||
_targetHint = (FFBattleTargetHint)TscnScenes.LoadAndBuild("scenes/ff_battle_target_hint.tscn");
|
||||
_targetHint.Name = "FFBattleTargetHint";
|
||||
_targetHint.EnsureConstructed();
|
||||
_targetHint.HideHint();
|
||||
panel.AddChild(_targetHint);
|
||||
|
||||
_endTurnButton = new Button();
|
||||
_endTurnButton.Text = TranslationServer.Translate("ff_battle/end_turn");
|
||||
_endTurnButton.Pressed += OnEndTurnPressed;
|
||||
@@ -110,7 +152,9 @@ namespace Cthangover.FFBattle
|
||||
_playerPanel.OnWidgetClicked += OnWidgetClicked;
|
||||
_enemyPanel.OnEnemyClicked += OnEnemyClicked;
|
||||
_enemyPanel.OnEnemyDead += OnEnemyDied;
|
||||
_enemyPanel.OnEnemyDefeated += OnEnemyDefeated;
|
||||
_menuPanel.OnItemSelected += OnMenuItemSelected;
|
||||
_menuPanel.OnSelectionChanged += OnMenuSelectionChanged;
|
||||
_menuPanel.OnCancelled += OnMenuCancel;
|
||||
}
|
||||
|
||||
@@ -137,6 +181,20 @@ namespace Cthangover.FFBattle
|
||||
_menuPanel.Position = new Vector2(viewportSize.X * 0.66f, viewportSize.Y * 0.58f);
|
||||
_menuPanel.Size = new Vector2(viewportSize.X * 0.3f, 160f);
|
||||
|
||||
_detailPanel.Position = new Vector2(viewportSize.X * 0.66f - 320f, viewportSize.Y * 0.58f);
|
||||
_detailPanel.Size = new Vector2(300f, 190f);
|
||||
|
||||
_hud.Position = new Vector2(26f, 20f);
|
||||
|
||||
_logPanel.Position = new Vector2(viewportSize.X - 466f, 64f);
|
||||
_logPanel.Size = new Vector2(440f, 440f);
|
||||
|
||||
_banner.Position = new Vector2(0f, viewportSize.Y * 0.43f);
|
||||
_banner.Size = new Vector2(viewportSize.X, 70f);
|
||||
|
||||
_targetHint.Position = new Vector2(0f, viewportSize.Y * 0.48f);
|
||||
_targetHint.Size = new Vector2(viewportSize.X, 40f);
|
||||
|
||||
if (_endTurnButton != null)
|
||||
_endTurnButton.Position = new Vector2(viewportSize.X - 180f, viewportSize.Y * 0.92f);
|
||||
|
||||
@@ -146,6 +204,9 @@ namespace Cthangover.FFBattle
|
||||
BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared;
|
||||
|
||||
_isPlayerTurn = true;
|
||||
_round = 0;
|
||||
UpdateHud();
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_battle_start"), FFBattleLogKind.Turn);
|
||||
StartPlayerTurn();
|
||||
}
|
||||
|
||||
@@ -159,13 +220,15 @@ namespace Cthangover.FFBattle
|
||||
_currentCharacter = null;
|
||||
_targetSelectMode = false;
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
BattleSceneContext.Instance.ActiveCharacter = null;
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
{
|
||||
if (!widget.IsDead)
|
||||
{
|
||||
widget.Card.StatusEffectQueue.OnTurnStart();
|
||||
StatusEffectService.Of(widget.Card).OnTurnStart();
|
||||
widget.Card.Attributes.Point.Value = widget.Card.Attributes.Point.BaseValue;
|
||||
}
|
||||
widget.UpdateInfo();
|
||||
@@ -179,6 +242,13 @@ namespace Cthangover.FFBattle
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
|
||||
_round += 1;
|
||||
UpdateHud();
|
||||
_hud.SetPhase(true);
|
||||
_banner.Show(Translate("ff_battle/banner_player"));
|
||||
_logPanel.SetRound(_round);
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_player"), _round), FFBattleLogKind.Turn);
|
||||
}
|
||||
|
||||
private void OnWidgetClicked(FFCharacterWidget widget)
|
||||
@@ -200,6 +270,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
return;
|
||||
@@ -249,12 +320,14 @@ namespace Cthangover.FFBattle
|
||||
};
|
||||
|
||||
_currentMenuAction = null;
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailMain;
|
||||
_menuPanel.ShowMenu(entries, string.Format(Translate("ff_battle/menu_caption"),
|
||||
ActorName(), _currentCharacter.Card.Attributes.Point.Value, _currentCharacter.Card.Attributes.Point.BaseValue));
|
||||
_controller.State = FFBattleState.MenuOpen;
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
_currentCharacter?.Highlight(new Color(0, 0.6f, 1f, 0.6f));
|
||||
_currentCharacter?.Highlight(UiTokens.Accent);
|
||||
}
|
||||
|
||||
private void OnMenuItemSelected(int index)
|
||||
@@ -366,7 +439,8 @@ namespace Cthangover.FFBattle
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailActions;
|
||||
_menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_actions"));
|
||||
_controller.State = FFBattleState.SubMenuOpen;
|
||||
}
|
||||
|
||||
@@ -407,7 +481,8 @@ namespace Cthangover.FFBattle
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailItems;
|
||||
_menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_items"));
|
||||
_controller.State = FFBattleState.SubMenuOpen;
|
||||
}
|
||||
|
||||
@@ -417,6 +492,7 @@ namespace Cthangover.FFBattle
|
||||
_targetAlly = targetAlly;
|
||||
_menuPanel.HideMenu();
|
||||
_controller.State = FFBattleState.TargetSelect;
|
||||
_targetHint.ShowHint(Translate(targetAlly ? "ff_battle/hint_ally" : "ff_battle/hint_target"));
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
@@ -427,13 +503,13 @@ namespace Cthangover.FFBattle
|
||||
{
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
if (!widget.IsDead)
|
||||
widget.Highlight(new Color(0.3f, 1f, 0.3f, 0.5f));
|
||||
widget.Highlight(UiTokens.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var widget in _enemyPanel.Widgets)
|
||||
if (!widget.IsDead)
|
||||
widget.Highlight(new Color(1f, 0.3f, 0.3f, 0.5f));
|
||||
widget.Highlight(UiTokens.Danger);
|
||||
}
|
||||
|
||||
_onTargetSelected = null;
|
||||
@@ -450,6 +526,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
}
|
||||
@@ -468,6 +545,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
}
|
||||
@@ -479,12 +557,22 @@ namespace Cthangover.FFBattle
|
||||
|
||||
_controller.State = FFBattleState.AnimationPlaying;
|
||||
_endTurnButton.Visible = false;
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
|
||||
var source = _currentCharacter;
|
||||
var actionName = string.Empty;
|
||||
var hpBefore = target.Card.Attributes.Health.Value;
|
||||
var defBefore = target.Card.Attributes.Defence.Value;
|
||||
var stunBefore = CountStunTurns(target.Card);
|
||||
|
||||
switch (_currentMenuAction.Key)
|
||||
{
|
||||
case "action":
|
||||
{
|
||||
var action = _currentMenuAction.Data as ActionCharacter;
|
||||
actionName = TranslationServer.Translate(action.Name).ToString();
|
||||
LogActionDeclared(source, actionName, target);
|
||||
IBattleAction anim = null;
|
||||
|
||||
if (action.Type == ActionCharacterType.ToEnemy)
|
||||
@@ -503,6 +591,8 @@ namespace Cthangover.FFBattle
|
||||
case "item":
|
||||
{
|
||||
var item = _currentMenuAction.Data as IItem;
|
||||
actionName = TranslationServer.Translate(item.Name).ToString();
|
||||
LogActionDeclared(source, actionName, target);
|
||||
var dummyAction = new ActionCharacter { ID = "ff/item" };
|
||||
var anim = new Actions.FFItemAnimation(_currentCharacter, target, dummyAction, item);
|
||||
await RunAnimation(anim);
|
||||
@@ -510,6 +600,8 @@ namespace Cthangover.FFBattle
|
||||
}
|
||||
}
|
||||
|
||||
LogOutcome(source, target, actionName, hpBefore, defBefore, stunBefore);
|
||||
|
||||
_currentMenuAction = null;
|
||||
_endTurnButton.Visible = true;
|
||||
|
||||
@@ -536,12 +628,15 @@ namespace Cthangover.FFBattle
|
||||
private void DoEscapeAction()
|
||||
{
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_controller.State = FFBattleState.Idle;
|
||||
|
||||
var escapeRoll = (float)GD.RandRange(0, 100);
|
||||
if (escapeRoll < 70f)
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", "Escape succeeded", LogLevel.Debug);
|
||||
_banner.Show(Translate("ff_battle/escape_success"));
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_escape_success"), FFBattleLogKind.Turn);
|
||||
_ctx.EndBattle(BattleSide.Player);
|
||||
}
|
||||
else
|
||||
@@ -549,6 +644,8 @@ namespace Cthangover.FFBattle
|
||||
GameLogger.Log("FF_BATTLE", "Escape failed", LogLevel.Debug);
|
||||
_currentCharacter.Card.Attributes.Point.Value -= 1;
|
||||
_currentCharacter.UpdateInfo();
|
||||
_banner.Show(Translate("ff_battle/escape_fail"));
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_escape_failed"), NameOf(_currentCharacter)), FFBattleLogKind.Status);
|
||||
_currentCharacter = null;
|
||||
BattleSceneContext.Instance.ActiveCharacter = null;
|
||||
TryNextCharacterOrEndTurn();
|
||||
@@ -587,6 +684,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
ShowMainMenu();
|
||||
return;
|
||||
}
|
||||
@@ -598,6 +696,7 @@ namespace Cthangover.FFBattle
|
||||
}
|
||||
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_controller.State = FFBattleState.Idle;
|
||||
_currentCharacter = null;
|
||||
_currentMenuAction = null;
|
||||
@@ -634,11 +733,12 @@ namespace Cthangover.FFBattle
|
||||
if (widget.IsDead || widget.Card?.Attributes?.Point?.Value <= 0)
|
||||
continue;
|
||||
|
||||
if (widget.Card.StatusEffectQueue.HasStun())
|
||||
if (StatusEffectService.Of(widget.Card).HasStun())
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", $"{widget.Card.Name} is stunned, skipping", LogLevel.Debug);
|
||||
widget.Card.Attributes.Point.Value = 0;
|
||||
widget.UpdateInfo();
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(widget)), FFBattleLogKind.Status);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -648,6 +748,7 @@ namespace Cthangover.FFBattle
|
||||
return;
|
||||
}
|
||||
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_no_points"), FFBattleLogKind.Info);
|
||||
OnPlayerTurnEnd();
|
||||
}
|
||||
|
||||
@@ -667,6 +768,8 @@ namespace Cthangover.FFBattle
|
||||
_isPlayerTurn = false;
|
||||
_endTurnButton.Visible = false;
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
_controller.State = FFBattleState.AnimationPlaying;
|
||||
BattleSceneContext.Instance.IsWait = true;
|
||||
|
||||
@@ -675,6 +778,10 @@ namespace Cthangover.FFBattle
|
||||
foreach (var widget in _enemyPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
|
||||
_hud.SetPhase(false);
|
||||
_banner.Show(Translate("ff_battle/banner_enemy"));
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_enemy"), _round), FFBattleLogKind.Turn);
|
||||
|
||||
var root = (Node)_ctx.RootNode;
|
||||
await root.ToSignal(root.GetTree(), "process_frame");
|
||||
await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout");
|
||||
@@ -693,11 +800,13 @@ namespace Cthangover.FFBattle
|
||||
if (enemyWidget.IsDead || enemyWidget.Card?.Actions == null || enemyWidget.Card.Actions.Count == 0)
|
||||
continue;
|
||||
|
||||
enemyWidget.Card.StatusEffectQueue.OnTurnStart();
|
||||
StatusEffectService.Of(enemyWidget.Card).OnTurnStart();
|
||||
enemyWidget.UpdateInfo();
|
||||
|
||||
if (enemyWidget.Card.StatusEffectQueue.HasStun())
|
||||
if (StatusEffectService.Of(enemyWidget.Card).HasStun())
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", $"{enemyWidget.Card.Name} is stunned, skipping", LogLevel.Debug);
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(enemyWidget)), FFBattleLogKind.Status);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -715,6 +824,12 @@ namespace Cthangover.FFBattle
|
||||
$"Enemy: {enemyWidget.Card.Name} → {action.Name} → {target.Card.Name}",
|
||||
LogLevel.Debug);
|
||||
|
||||
var actionName = TranslationServer.Translate(action.Name).ToString();
|
||||
LogActionDeclared(enemyWidget, actionName, target);
|
||||
var hpBefore = target.Card.Attributes.Health.Value;
|
||||
var defBefore = target.Card.Attributes.Defence.Value;
|
||||
var stunBefore = CountStunTurns(target.Card);
|
||||
|
||||
var anim = CreateEnemyAnimation(enemyWidget, target, action);
|
||||
if (anim != null)
|
||||
{
|
||||
@@ -724,6 +839,8 @@ namespace Cthangover.FFBattle
|
||||
anim.DoEnd();
|
||||
}
|
||||
|
||||
LogOutcome(enemyWidget, target, actionName, hpBefore, defBefore, stunBefore);
|
||||
|
||||
await root.ToSignal(root.GetTree().CreateTimer(0.2f), "timeout");
|
||||
|
||||
if (!_playerPanel.HasAlive())
|
||||
@@ -793,7 +910,7 @@ namespace Cthangover.FFBattle
|
||||
OnPlayerTurnEnd();
|
||||
}
|
||||
|
||||
private void OnEnemyDied(FFCharacterWidget widget)
|
||||
private void OnEnemyDefeated(FFCharacterWidget widget)
|
||||
{
|
||||
var ctx = BattleSceneContext.Instance;
|
||||
if (ctx != null && widget?.Card != null)
|
||||
@@ -801,6 +918,11 @@ namespace Cthangover.FFBattle
|
||||
ctx.RecordEnemyDefeated(widget.Card);
|
||||
ctx.NotifyCharacterDied(widget.Card);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnemyDied(FFCharacterWidget widget)
|
||||
{
|
||||
UpdateHudCounts();
|
||||
|
||||
if (!_enemyPanel.HasAlive())
|
||||
{
|
||||
@@ -851,10 +973,170 @@ namespace Cthangover.FFBattle
|
||||
_playerPanel?.ClearAll();
|
||||
_enemyPanel?.ClearAll();
|
||||
_menuPanel?.HideMenu();
|
||||
_detailPanel?.HidePassport();
|
||||
_targetHint?.HideHint();
|
||||
_logPanel?.Clear();
|
||||
if (_endTurnButton != null)
|
||||
_endTurnButton.Visible = false;
|
||||
}
|
||||
|
||||
private void OnMenuSelectionChanged(int index)
|
||||
{
|
||||
if (_menuPanel.Entries == null || index < 0 || index >= _menuPanel.Entries.Count)
|
||||
{
|
||||
_detailPanel.HidePassport();
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = _menuPanel.Entries[index];
|
||||
_detailPanel.ShowPassport();
|
||||
|
||||
if (_detailKind == DetailActions)
|
||||
{
|
||||
if (entry.Data is ActionCharacter action)
|
||||
_detailPanel.ShowAction(action, BuildActionNote(action));
|
||||
else
|
||||
_detailPanel.ShowHint(Translate("ff_battle/detail_back"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_detailKind == DetailItems)
|
||||
{
|
||||
if (entry.Data is IItem item)
|
||||
_detailPanel.ShowItem(item, item.ItemType.HasFlag(ItemType.TargetUsed), BuildItemNote(item));
|
||||
else
|
||||
_detailPanel.ShowHint(Translate("ff_battle/detail_back"));
|
||||
return;
|
||||
}
|
||||
|
||||
_detailPanel.ShowHint(MainHintFor(entry.Key));
|
||||
}
|
||||
|
||||
private string BuildActionNote(ActionCharacter action)
|
||||
{
|
||||
if (action.Properties == null)
|
||||
return TranslationServer.Translate("ff_battle/detail_action").ToString();
|
||||
|
||||
var cost = action.GetInt("RequiredPoint", 1);
|
||||
var attack = action.GetFloat("Attack", 0f);
|
||||
if (attack > 0f)
|
||||
{
|
||||
var attacker = _currentCharacter?.Card?.Attributes?.Attack?.Value ?? 0;
|
||||
return string.Format(Translate("ff_battle/detail_damage"),
|
||||
Mathf.RoundToInt(attack * attacker), attacker, attack.ToString("0.0"), cost);
|
||||
}
|
||||
|
||||
var turns = action.GetInt("Turn", 0);
|
||||
if (turns > 0)
|
||||
return string.Format(Translate("ff_battle/detail_stun"), turns, cost);
|
||||
|
||||
var defence = action.GetInt("Defence", 0);
|
||||
if (defence > 0)
|
||||
return string.Format(Translate("ff_battle/detail_defence"), defence, cost);
|
||||
|
||||
var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(action.Description));
|
||||
return !string.IsNullOrEmpty(description) ? description : Translate("ff_battle/detail_action");
|
||||
}
|
||||
|
||||
private static string BuildItemNote(IItem item)
|
||||
{
|
||||
var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(item.Description));
|
||||
return !string.IsNullOrEmpty(description)
|
||||
? description
|
||||
: TranslationServer.Translate("ff_battle/detail_item").ToString();
|
||||
}
|
||||
|
||||
private static string MainHintFor(string key)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "attack": return TranslationServer.Translate("ff_battle/detail_attack").ToString();
|
||||
case "items": return TranslationServer.Translate("ff_battle/detail_items").ToString();
|
||||
case "defend": return TranslationServer.Translate("ff_battle/detail_defend").ToString();
|
||||
case "escape": return TranslationServer.Translate("ff_battle/detail_escape").ToString();
|
||||
default: return TranslationServer.Translate("ff_battle/detail_back").ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogActionDeclared(FFCharacterWidget source, string actionName, FFCharacterWidget target)
|
||||
{
|
||||
var entry = source == target
|
||||
? string.Format(Translate("ff_battle/log_action_self"), NameOf(source), actionName)
|
||||
: string.Format(Translate("ff_battle/log_action"), NameOf(source), actionName, NameOf(target));
|
||||
_logPanel.AddEntry(entry, FFBattleLogKind.Info);
|
||||
}
|
||||
|
||||
private void LogOutcome(FFCharacterWidget source, FFCharacterWidget target, string actionName,
|
||||
int hpBefore, int defBefore, int stunBefore)
|
||||
{
|
||||
var hpAfter = target.Card.Attributes.Health.Value;
|
||||
var defAfter = target.Card.Attributes.Defence.Value;
|
||||
var stunAfter = CountStunTurns(target.Card);
|
||||
|
||||
if (defAfter > defBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_defence"),
|
||||
NameOf(target), defAfter - defBefore, defAfter), FFBattleLogKind.Buff);
|
||||
|
||||
var shieldLost = defBefore - defAfter;
|
||||
if (shieldLost > 0)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_shield"),
|
||||
NameOf(target), shieldLost, defAfter), FFBattleLogKind.Shield);
|
||||
|
||||
if (stunAfter > stunBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun"),
|
||||
NameOf(target), stunAfter), FFBattleLogKind.Status);
|
||||
|
||||
if (hpAfter > hpBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_heal"),
|
||||
actionName, NameOf(target), hpAfter - hpBefore), FFBattleLogKind.Buff);
|
||||
|
||||
if (hpAfter < hpBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_damage"),
|
||||
actionName, NameOf(source), NameOf(target), hpBefore - hpAfter), FFBattleLogKind.Damage);
|
||||
|
||||
if (hpAfter <= 0 && hpBefore > 0)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_death"), NameOf(target)), FFBattleLogKind.Dead);
|
||||
|
||||
UpdateHudCounts();
|
||||
}
|
||||
|
||||
private static int CountStunTurns(Character character)
|
||||
{
|
||||
var stun = StatusEffectService.Of(character)?.ActiveStatusEffects?
|
||||
.FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun);
|
||||
return stun?.RemainingTurns ?? 0;
|
||||
}
|
||||
|
||||
private string ActorName()
|
||||
=> _currentCharacter?.Card == null
|
||||
? string.Empty
|
||||
: TranslationServer.Translate(_currentCharacter.Card.Name).ToString();
|
||||
|
||||
private static string NameOf(FFCharacterWidget widget)
|
||||
=> widget?.Card == null
|
||||
? string.Empty
|
||||
: TranslationServer.Translate(widget.Card.Name).ToString();
|
||||
|
||||
private void UpdateHud()
|
||||
{
|
||||
if (_hud == null)
|
||||
return;
|
||||
_hud.SetRound(_round);
|
||||
UpdateHudCounts();
|
||||
}
|
||||
|
||||
private void UpdateHudCounts()
|
||||
{
|
||||
if (_hud == null)
|
||||
return;
|
||||
_hud.SetCounts(
|
||||
_enemyPanel.Widgets.Count(w => !w.IsDead),
|
||||
_playerPanel.Widgets.Count(w => !w.IsDead));
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> while the core accepts a player turn advance; flips to
|
||||
/// <c>false</c> synchronously at the start of <see cref="OnPlayerTurnEnd"/>.
|
||||
@@ -868,3 +1150,6 @@ namespace Cthangover.FFBattle
|
||||
public void AdvancePlayerTurn() => OnPlayerTurnEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.CharacterPanel;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Action/item passport shown to the left of the command menu. It mirrors the
|
||||
/// action-card component of the unified style guide: a type accent stripe, the
|
||||
/// action icon, name and ID, type/cost/cooldown badges, property chips and a
|
||||
/// note line with the computed effect preview. The panel exists so the small
|
||||
/// FF-style menu can stay a compact list while the player still sees what the
|
||||
/// highlighted command actually does. Node structure lives in
|
||||
/// <c>scenes/ff_action_detail_panel.tscn</c>; the class fills it and reuses
|
||||
/// <see cref="ActionCharacterUi"/> for badges and properties so the battle and
|
||||
/// the character panel can never disagree about an action's presentation.
|
||||
/// </summary>
|
||||
public partial class FFActionDetailPanel : ModWidget
|
||||
{
|
||||
private PanelContainer _card;
|
||||
private ColorRect _stripe;
|
||||
private TextureRect _icon;
|
||||
private Label _nameLabel;
|
||||
private Label _idLabel;
|
||||
private HFlowContainer _badges;
|
||||
private HFlowContainer _props;
|
||||
private PanelContainer _note;
|
||||
private Label _noteLabel;
|
||||
|
||||
/// <summary>Builds the action detail passport panel (description plate shown when an action is inspected).</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_card = GetNode<PanelContainer>("Box/Card");
|
||||
_stripe = GetNode<ColorRect>("Box/Card/CardRow/Stripe");
|
||||
_icon = GetNode<TextureRect>("Box/Card/CardRow/IconMargin/CardIcon");
|
||||
_nameLabel = GetNode<Label>("Box/Card/CardRow/Body/NameRow/NameLabel");
|
||||
_idLabel = GetNode<Label>("Box/Card/CardRow/Body/NameRow/IdLabel");
|
||||
_badges = GetNode<HFlowContainer>("Box/Card/CardRow/Body/Badges");
|
||||
_props = GetNode<HFlowContainer>("Box/Card/CardRow/Body/Props");
|
||||
_note = GetNode<PanelContainer>("Box/Note");
|
||||
_noteLabel = GetNode<Label>("Box/Note/NoteLabel");
|
||||
|
||||
var cardStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface1, 0.92f), UiTokens.LineCard);
|
||||
cardStyle.ContentMarginLeft = 0;
|
||||
cardStyle.ContentMarginRight = 0;
|
||||
cardStyle.ContentMarginTop = 0;
|
||||
cardStyle.ContentMarginBottom = 0;
|
||||
_card.AddThemeStyleboxOverride("panel", cardStyle);
|
||||
|
||||
var noteStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface2, 0.9f), UiTokens.Line);
|
||||
noteStyle.ContentMarginLeft = 12;
|
||||
noteStyle.ContentMarginRight = 12;
|
||||
noteStyle.ContentMarginTop = 9;
|
||||
noteStyle.ContentMarginBottom = 9;
|
||||
_note.AddThemeStyleboxOverride("panel", noteStyle);
|
||||
|
||||
_nameLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
_idLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
_noteLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the card with an action passport. <paramref name="note"/> is a
|
||||
/// ready localised effect preview built by the core, because only the core
|
||||
/// knows the acting character's attack value for the damage estimate.
|
||||
/// </summary>
|
||||
public void ShowAction(ActionCharacter action, string note)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
ShowHint(note);
|
||||
return;
|
||||
}
|
||||
|
||||
_stripe.Color = ActionCharacterUi.AccentColor(action.Type);
|
||||
_icon.Texture = action.Image;
|
||||
_nameLabel.Text = ActionCharacterUi.Translate(action.Name);
|
||||
_idLabel.Text = action.ID ?? string.Empty;
|
||||
_card.Visible = true;
|
||||
|
||||
ResetBody();
|
||||
|
||||
AddBadge(ActionCharacterUi.Translate(ActionCharacterUi.TypeLabelKey(action.Type)), ActionCharacterUi.AccentColor(action.Type));
|
||||
|
||||
var costText = ActionCharacterUi.CostText(action);
|
||||
if (!string.IsNullOrEmpty(costText))
|
||||
AddBadge(costText, UiTokens.TypeResource);
|
||||
|
||||
var turns = action.GetInt("Turn", 0);
|
||||
if (turns > 0)
|
||||
AddBadge(string.Format(Translate("ff_battle/badge_turns"), turns), UiTokens.Cooldown);
|
||||
|
||||
foreach (var pair in ActionCharacterUi.SortedProperties(action))
|
||||
{
|
||||
if (pair.Key == "RequiredPoint")
|
||||
continue;
|
||||
_props.AddChild(ActionCharacterUi.MakePropertyChip(pair.Key, pair.Value));
|
||||
}
|
||||
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>Fills the card with an inventory item passport for the item sub-menu.</summary>
|
||||
public void ShowItem(IItem item, bool targetAlly, string note)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
ShowHint(note);
|
||||
return;
|
||||
}
|
||||
|
||||
_stripe.Color = UiTokens.TypeUsed;
|
||||
_icon.Texture = item.Sprite;
|
||||
_nameLabel.Text = ActionCharacterUi.Translate(item.Name);
|
||||
_idLabel.Text = item.ID ?? string.Empty;
|
||||
_card.Visible = true;
|
||||
|
||||
ResetBody();
|
||||
|
||||
if (item.ItemType.HasFlag(ItemType.Used))
|
||||
AddBadge(ActionCharacterUi.Translate("ui/inventory/type_used"), UiTokens.TypeUsed);
|
||||
AddBadge(ActionCharacterUi.Translate(targetAlly ? "ui/inventory/type_target" : "ff_battle/badge_self"),
|
||||
targetAlly ? UiTokens.TypeTarget : UiTokens.TypeSelf);
|
||||
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>Shows a plain directive note under the card, used for highlighted menu hints.</summary>
|
||||
public void ShowHint(string note)
|
||||
{
|
||||
ResetBody();
|
||||
_card.Visible = false;
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the whole passport; the core calls it when the menu closes. The
|
||||
/// inherited <see cref="CanvasItem.Hide"/> semantics are enough, so the
|
||||
/// method is only a named alias for readability at call sites.
|
||||
/// </summary>
|
||||
public void HidePassport()
|
||||
=> Visible = false;
|
||||
|
||||
/// <summary>Shows the panel; the core calls it before filling any content.</summary>
|
||||
public void ShowPassport()
|
||||
=> Visible = true;
|
||||
|
||||
private void ResetBody()
|
||||
{
|
||||
foreach (var child in _badges.GetChildren())
|
||||
{
|
||||
_badges.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
foreach (var child in _props.GetChildren())
|
||||
{
|
||||
_props.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBadge(string text, Color color)
|
||||
=> _badges.AddChild(ActionCharacterUi.MakeBadge(text, color));
|
||||
|
||||
private void SetNote(string note)
|
||||
{
|
||||
var hasNote = !string.IsNullOrEmpty(note);
|
||||
_note.Visible = hasNote;
|
||||
_noteLabel.Text = note ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Centered announcement strip ("Player turn", "Enemy turn", escape results).
|
||||
/// The node tree lives in <c>scenes/ff_battle_banner.tscn</c>; this class
|
||||
/// dresses it with tokens and owns the fade in/hold/fade out tween. The banner
|
||||
/// is purely informational, so it ignores the mouse and the core places it on
|
||||
/// its own anchored strip between the enemy grid and the party row.
|
||||
/// </summary>
|
||||
public partial class FFBattleBanner : ModWidget
|
||||
{
|
||||
private Label _label;
|
||||
private Tween _tween;
|
||||
|
||||
/// <summary>Builds the centre-screen turn/outcome banner label.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_label = GetNode<Label>("Center/Panel/Label");
|
||||
_label.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
|
||||
var style = UiKit.Frame(0, UiTokens.Alpha(UiTokens.Page, 0.62f), new Color(0f, 0f, 0f, 0f));
|
||||
style.BorderWidthTop = 1;
|
||||
style.BorderWidthBottom = 1;
|
||||
style.BorderColor = UiTokens.AccentLine;
|
||||
style.ContentMarginLeft = 44;
|
||||
style.ContentMarginRight = 44;
|
||||
style.ContentMarginTop = 12;
|
||||
style.ContentMarginBottom = 12;
|
||||
GetNode<PanelContainer>("Center/Panel").AddThemeStyleboxOverride("panel", style);
|
||||
|
||||
Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
}
|
||||
|
||||
/// <summary>Shows the given caption and restarts the 1.6s fade cycle.</summary>
|
||||
public void Show(string text)
|
||||
{
|
||||
_label.Text = text;
|
||||
_tween?.Kill();
|
||||
Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
|
||||
_tween = CreateTween();
|
||||
_tween.TweenProperty(this, "modulate:a", 1f, 0.2f);
|
||||
_tween.TweenInterval(1.1f);
|
||||
_tween.TweenProperty(this, "modulate:a", 0f, 0.3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
@@ -77,6 +78,7 @@ namespace Cthangover.FFBattle.UI
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
}
|
||||
|
||||
/// <summary>Routes menu/confirm input into the FF battle state machine (target select, turn advance).</summary>
|
||||
protected override void OnInput(InputEvent @event)
|
||||
{
|
||||
if (State == FFBattleState.AnimationPlaying || State == FFBattleState.Ended)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Top-left battle HUD: title, phase badge and the round / roster counters.
|
||||
/// The node tree comes from <c>scenes/ff_battle_hud.tscn</c>; this class only
|
||||
/// fetches nodes, dresses them with <see cref="UiTokens"/> styles and exposes
|
||||
/// narrow setters so <see cref="FFBattleCore"/> never assembles controls by
|
||||
/// hand. The phase badge reuses the type-accent palette: blue for the player
|
||||
/// turn, red for the enemy turn.
|
||||
/// </summary>
|
||||
public partial class FFBattleHud : ModWidget
|
||||
{
|
||||
private Label _title;
|
||||
private PanelContainer _phaseBadge;
|
||||
private Label _phaseLabel;
|
||||
private Label _roundValue;
|
||||
private Label _enemiesValue;
|
||||
private Label _alliesValue;
|
||||
|
||||
/// <summary>Builds the FF battle HUD frame: command menu host, speed control and log toggle.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_title = GetNode<Label>("Panel/Column/TopRow/TitleLabel");
|
||||
_phaseBadge = GetNode<PanelContainer>("Panel/Column/TopRow/PhaseBadge");
|
||||
_phaseLabel = GetNode<Label>("Panel/Column/TopRow/PhaseBadge/PhaseLabel");
|
||||
_roundValue = GetNode<Label>("Panel/Column/MetaRow/RoundValue");
|
||||
_enemiesValue = GetNode<Label>("Panel/Column/MetaRow/EnemiesValue");
|
||||
_alliesValue = GetNode<Label>("Panel/Column/MetaRow/AlliesValue");
|
||||
|
||||
var panelStyle = UiKit.Frame(UiTokens.RadiusLg, UiTokens.Alpha(UiTokens.Surface0, 0.86f), UiTokens.Line);
|
||||
panelStyle.ContentMarginLeft = 18;
|
||||
panelStyle.ContentMarginRight = 18;
|
||||
panelStyle.ContentMarginTop = 12;
|
||||
panelStyle.ContentMarginBottom = 13;
|
||||
panelStyle.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||
panelStyle.ShadowSize = 18;
|
||||
panelStyle.ShadowOffset = new Vector2(0f, 8f);
|
||||
GetNode<PanelContainer>("Panel").AddThemeStyleboxOverride("panel", panelStyle);
|
||||
|
||||
_title.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
|
||||
foreach (var name in new[] { "RoundCaption", "EnemiesCaption", "AlliesCaption" })
|
||||
GetNode<Label>("Panel/Column/MetaRow/" + name).AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
|
||||
foreach (var name in new[] { "RoundValue", "EnemiesValue", "AlliesValue" })
|
||||
GetNode<Label>("Panel/Column/MetaRow/" + name).AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
|
||||
ApplyPhaseStyle(true);
|
||||
}
|
||||
|
||||
/// <summary>Switches the phase badge between the player and enemy turns.</summary>
|
||||
public void SetPhase(bool playerTurn)
|
||||
{
|
||||
_phaseLabel.Text = Translate(playerTurn ? "ff_battle/phase_player" : "ff_battle/phase_enemy");
|
||||
ApplyPhaseStyle(playerTurn);
|
||||
}
|
||||
|
||||
/// <summary>Updates the round counter.</summary>
|
||||
public void SetRound(int round)
|
||||
=> _roundValue.Text = round.ToString();
|
||||
|
||||
/// <summary>Updates living combatant counters.</summary>
|
||||
public void SetCounts(int enemies, int allies)
|
||||
{
|
||||
_enemiesValue.Text = enemies.ToString();
|
||||
_alliesValue.Text = allies.ToString();
|
||||
}
|
||||
|
||||
private void ApplyPhaseStyle(bool playerTurn)
|
||||
{
|
||||
var accent = playerTurn ? UiTokens.TypeSelf : UiTokens.TypeEnemy;
|
||||
var style = UiKit.Frame(UiTokens.RadiusXs, UiTokens.BadgeFill(accent), UiTokens.BadgeLine(accent));
|
||||
style.ContentMarginLeft = 7;
|
||||
style.ContentMarginRight = 7;
|
||||
style.ContentMarginTop = 2;
|
||||
style.ContentMarginBottom = 2;
|
||||
_phaseBadge.AddThemeStyleboxOverride("panel", style);
|
||||
_phaseLabel.AddThemeColorOverride("font_color", accent);
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Semantic categories of <see cref="FFBattleLogPanel"/> entries. The kind
|
||||
/// selects the row colour and the left border accent: turn separators are
|
||||
/// gold, damage red, absorbed shield blue, buffs green, statuses amber and
|
||||
/// deaths grey. <see cref="Info"/> is the neutral narration fallback.
|
||||
/// </summary>
|
||||
public enum FFBattleLogKind
|
||||
{
|
||||
Info,
|
||||
Turn,
|
||||
Damage,
|
||||
Shield,
|
||||
Buff,
|
||||
Status,
|
||||
Dead
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Battle journal docked to the upper right: a window with a header carrying
|
||||
/// the round counter and a scrollable list of colour-coded rows. The panel is
|
||||
/// a proposal on top of the bare battle screen — it gives damage, shield
|
||||
/// absorption, status and death events a readable history that the original
|
||||
/// FF-style UI lacked. Node structure lives in
|
||||
/// <c>scenes/ff_battle_log_panel.tscn</c>; rows are created at runtime because
|
||||
/// they are data, not layout, and are capped to <see cref="MaxRows"/> so a long
|
||||
/// battle cannot grow unbounded.
|
||||
/// </summary>
|
||||
public partial class FFBattleLogPanel : ModWidget
|
||||
{
|
||||
private const int MaxRows = 80;
|
||||
|
||||
private Label _title;
|
||||
private Label _roundLabel;
|
||||
private ScrollContainer _scroll;
|
||||
private VBoxContainer _rows;
|
||||
|
||||
/// <summary>Builds the scrollable FF battle log with coloured entries per <see cref="FFBattleLogKind"/>.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_title = GetNode<Label>("Window/Column/Header/HeaderRow/TitleLabel");
|
||||
_roundLabel = GetNode<Label>("Window/Column/Header/HeaderRow/RoundLabel");
|
||||
_scroll = GetNode<ScrollContainer>("Window/Column/Scroll");
|
||||
_rows = GetNode<VBoxContainer>("Window/Column/Scroll/Rows");
|
||||
|
||||
var windowStyle = UiKit.Frame(UiTokens.RadiusLg, UiTokens.Alpha(UiTokens.Surface0, 0.84f), UiTokens.Line);
|
||||
windowStyle.ContentMarginLeft = 0;
|
||||
windowStyle.ContentMarginRight = 0;
|
||||
windowStyle.ContentMarginTop = 0;
|
||||
windowStyle.ContentMarginBottom = 0;
|
||||
windowStyle.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||
windowStyle.ShadowSize = 18;
|
||||
windowStyle.ShadowOffset = new Vector2(0f, 8f);
|
||||
GetNode<PanelContainer>("Window").AddThemeStyleboxOverride("panel", windowStyle);
|
||||
|
||||
_title.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_roundLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
}
|
||||
|
||||
/// <summary>Updates the round caption in the header.</summary>
|
||||
public void SetRound(int round)
|
||||
=> _roundLabel.Text = string.Format(Translate("ff_battle/log_round"), round);
|
||||
|
||||
/// <summary>Drops every row; used when the battle is cleared or restarted.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var child in _rows.GetChildren())
|
||||
{
|
||||
_rows.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one coloured row and trims the oldest entries past the cap.
|
||||
/// The scroll is pushed to the bottom so the newest event is always visible.
|
||||
/// </summary>
|
||||
public void AddEntry(string text, FFBattleLogKind kind)
|
||||
{
|
||||
var style = UiKit.Frame(0, BackgroundOf(kind), new Color(0f, 0f, 0f, 0f));
|
||||
style.BorderWidthLeft = 2;
|
||||
style.BorderColor = BorderOf(kind);
|
||||
style.ContentMarginLeft = 16;
|
||||
style.ContentMarginRight = 12;
|
||||
style.ContentMarginTop = 7;
|
||||
style.ContentMarginBottom = 7;
|
||||
|
||||
var row = new PanelContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||
row.AddThemeStyleboxOverride("panel", style);
|
||||
|
||||
var label = UiKit.Label(text, UiTokens.FontBodySmall, TextOf(kind));
|
||||
label.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
row.AddChild(label);
|
||||
|
||||
_rows.AddChild(row);
|
||||
|
||||
while (_rows.GetChildCount() > MaxRows)
|
||||
{
|
||||
var first = _rows.GetChild(0);
|
||||
_rows.RemoveChild(first);
|
||||
first.QueueFree();
|
||||
}
|
||||
|
||||
_scroll.ScrollVertical = (int)_scroll.GetVScrollBar().MaxValue;
|
||||
}
|
||||
|
||||
private static Color BackgroundOf(FFBattleLogKind kind)
|
||||
=> kind == FFBattleLogKind.Turn ? UiTokens.AccentSoft : new Color(0f, 0f, 0f, 0f);
|
||||
|
||||
private static Color BorderOf(FFBattleLogKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case FFBattleLogKind.Turn: return UiTokens.Accent;
|
||||
case FFBattleLogKind.Damage: return UiTokens.Danger;
|
||||
case FFBattleLogKind.Shield: return UiTokens.TypeResource;
|
||||
case FFBattleLogKind.Buff: return UiTokens.TypePassive;
|
||||
case FFBattleLogKind.Status: return UiTokens.Cooldown;
|
||||
case FFBattleLogKind.Dead: return UiTokens.LineFaint;
|
||||
default: return UiTokens.Line;
|
||||
}
|
||||
}
|
||||
|
||||
private static Color TextOf(FFBattleLogKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case FFBattleLogKind.Turn: return UiTokens.AccentBright;
|
||||
case FFBattleLogKind.Damage: return UiTokens.DangerText;
|
||||
case FFBattleLogKind.Shield: return UiTokens.Info;
|
||||
case FFBattleLogKind.Buff: return UiTokens.SuccessText;
|
||||
case FFBattleLogKind.Status: return UiTokens.Cooldown;
|
||||
case FFBattleLogKind.Dead: return UiTokens.Faint;
|
||||
default: return UiTokens.Muted;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Toast-style directive shown while the player picks a battle target
|
||||
/// ("Select a target · RMB / Esc — cancel"). It is a deliberate addition over
|
||||
/// the bare FF UI: the original mod highlighted widgets without ever telling
|
||||
/// the player what the highlight meant, which is easy to miss on a large enemy
|
||||
/// grid. The node tree lives in <c>scenes/ff_battle_target_hint.tscn</c>; the
|
||||
/// class dresses it with tokens and toggles visibility.
|
||||
/// </summary>
|
||||
public partial class FFBattleTargetHint : ModWidget
|
||||
{
|
||||
private Label _icon;
|
||||
private Label _text;
|
||||
|
||||
/// <summary>Builds the floating hint shown above the hovered target.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_icon = GetNode<Label>("Center/Toast/Row/IconLabel");
|
||||
_text = GetNode<Label>("Center/Toast/Row/TextLabel");
|
||||
_icon.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
_text.AddThemeColorOverride("font_color", UiTokens.Ink);
|
||||
|
||||
var style = UiKit.Frame(UiTokens.RadiusMd, UiTokens.ToastBg, UiTokens.Line);
|
||||
style.ContentMarginLeft = 14;
|
||||
style.ContentMarginRight = 14;
|
||||
style.ContentMarginTop = 10;
|
||||
style.ContentMarginBottom = 10;
|
||||
style.ShadowColor = new Color(0f, 0f, 0f, 0.5f);
|
||||
style.ShadowSize = 10;
|
||||
style.ShadowOffset = new Vector2(0f, 4f);
|
||||
GetNode<PanelContainer>("Center/Toast").AddThemeStyleboxOverride("panel", style);
|
||||
}
|
||||
|
||||
/// <summary>Shows the hint with the given caption.</summary>
|
||||
public void ShowHint(string text)
|
||||
{
|
||||
_text.Text = text;
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
/// <summary>Hides the hint; called when a target is confirmed or selection is cancelled.</summary>
|
||||
public void HideHint()
|
||||
=> Visible = false;
|
||||
}
|
||||
}
|
||||
+331
-75
@@ -1,30 +1,63 @@
|
||||
using System;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Visual representation of a single character (player or enemy) in the FF battle UI.
|
||||
/// Composite widget containing a sprite, HP bar, name label, and selection overlay.
|
||||
/// Supports highlight, flash, shake, and dissolve-death animations via Godot tweens.
|
||||
/// Created per-character by <see cref="FFPlayerPanel.Init"/> and
|
||||
/// <see cref="FFEnemyPanel.Init"/>. Clickable via <c>GuiInput</c> events;
|
||||
/// embedding panels wire these to <see cref="FFBattleCore"/> for action flow.
|
||||
/// Battle card of a single combatant, shared by the party and enemy panels.
|
||||
/// The node tree lives in <c>scenes/ff_character_widget.tscn</c>; this class
|
||||
/// only fetches the nodes, dresses them with <see cref="UiTokens"/> styles and
|
||||
/// refreshes their content. Visually the card follows the unified style guide:
|
||||
/// a team accent line on top, a sprite well, status badges in the upper right
|
||||
/// and an info block holding name, HP numbers, the layered health bar
|
||||
/// (<see cref="UiKit.StyleShieldedHealthBar"/>: track → health fill → blue
|
||||
/// defence segment drawn past the health edge while
|
||||
/// <see cref="CharacterAttributes.Defence"/> absorbs damage), action point pips
|
||||
/// (party) or attack caption (enemies) and the defence shield chip.
|
||||
/// <para/>
|
||||
/// The public surface is animation-facing, not layout-facing:
|
||||
/// <see cref="Highlight"/> paints the selection frame, <see cref="Flash"/> and
|
||||
/// <see cref="Shake"/> give impact feedback, <see cref="PlayDeathAnimation"/>
|
||||
/// dissolves the card. Battle code never touches labels directly — it mutates
|
||||
/// the <see cref="Character"/> and calls <see cref="UpdateInfo"/>, which
|
||||
/// re-reads health, defence, points and stun in one place. Panels must assign
|
||||
/// <see cref="IsPlayer"/> before <see cref="Init"/>, because the team colour is
|
||||
/// applied during initialisation.
|
||||
/// </summary>
|
||||
public partial class FFCharacterWidget : ModWidget
|
||||
{
|
||||
private TextureRect _sprite;
|
||||
private TextureRect _selection;
|
||||
/// <summary>Diameter of one action point pip.</summary>
|
||||
private const int PipSize = 9;
|
||||
|
||||
private TextureRect _art;
|
||||
private ColorRect _teamLine;
|
||||
private Panel _selection;
|
||||
private Panel _teamDot;
|
||||
private Label _nameLabel;
|
||||
private ColorRect _hpBg;
|
||||
private ColorRect _hpFill;
|
||||
private Label _hpNumLabel;
|
||||
private ProgressBar _shieldBar;
|
||||
private ProgressBar _hpBar;
|
||||
private HBoxContainer _pipsBox;
|
||||
private Label _atkLabel;
|
||||
private PanelContainer _shieldChip;
|
||||
private Label _shieldValue;
|
||||
private VBoxContainer _statusBox;
|
||||
private PanelContainer _stunBadge;
|
||||
private TextureRect _stunIcon;
|
||||
private Label _stunLabel;
|
||||
private Tween _animTween;
|
||||
private static Texture2D _cachedSelectTex;
|
||||
private static bool _selectTexLoaded;
|
||||
private VBoxContainer _gaugesBox;
|
||||
private bool _gaugesSubscribed;
|
||||
private static Shader _dissolveShader;
|
||||
|
||||
/// <summary>Whether this widget belongs to the player party (affects highlight/targeting logic).</summary>
|
||||
@@ -33,40 +66,47 @@ namespace Cthangover.FFBattle.UI
|
||||
public bool IsDead { get; set; }
|
||||
/// <summary>The character data model backing this widget: stats, actions, status effects.</summary>
|
||||
public Character Card { get; set; }
|
||||
/// <summary>Original scale captured at construction; used to restore after death animation.</summary>
|
||||
/// <summary>Original scale captured at initialisation; used to restore after death animation.</summary>
|
||||
public Vector2 BaseScale { get; private set; }
|
||||
/// <summary>Exposes this widget as a <see cref="Godot.Control"/> for layout calculations.</summary>
|
||||
public Control ControlNode => this;
|
||||
|
||||
/// <summary>Builds the character widget: portrait frame, HP bar and status chips.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
_sprite = GetNode<TextureRect>("Sprite");
|
||||
_hpBg = GetNode<ColorRect>("HpBg");
|
||||
_hpFill = GetNode<ColorRect>("HpFill");
|
||||
_nameLabel = GetNode<Label>("NameLabel");
|
||||
_selection = GetNode<TextureRect>("Selection");
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
Size = CustomMinimumSize;
|
||||
_teamLine = GetNode<ColorRect>("TeamLine");
|
||||
_art = GetNode<TextureRect>("Art");
|
||||
_nameLabel = GetNode<Label>("Info/Column/NameRow/NameLabel");
|
||||
_teamDot = GetNode<Panel>("Info/Column/NameRow/TeamDot");
|
||||
_hpNumLabel = GetNode<Label>("Info/Column/NameRow/HpNumLabel");
|
||||
_shieldBar = GetNode<ProgressBar>("Info/Column/HpBarStack/ShieldBar");
|
||||
_hpBar = GetNode<ProgressBar>("Info/Column/HpBarStack/HpBar");
|
||||
_pipsBox = GetNode<HBoxContainer>("Info/Column/Foot/PipsBox");
|
||||
_atkLabel = GetNode<Label>("Info/Column/Foot/AtkLabel");
|
||||
_shieldChip = GetNode<PanelContainer>("Info/Column/Foot/ShieldChip");
|
||||
_shieldValue = GetNode<Label>("Info/Column/Foot/ShieldChip/ShieldRow/ShieldValue");
|
||||
_statusBox = GetNode<VBoxContainer>("StatusBox");
|
||||
_stunBadge = GetNode<PanelContainer>("StatusBox/StunBadge");
|
||||
_stunIcon = GetNode<TextureRect>("StatusBox/StunBadge/StunRow/StunIcon");
|
||||
_stunLabel = GetNode<Label>("StatusBox/StunBadge/StunRow/StunLabel");
|
||||
_selection = GetNode<Panel>("Selection");
|
||||
|
||||
if (!_selectTexLoaded)
|
||||
{
|
||||
_cachedSelectTex = Textures.Resolve("select");
|
||||
_selectTexLoaded = true;
|
||||
}
|
||||
if (_cachedSelectTex != null)
|
||||
{
|
||||
_selection.Texture = _cachedSelectTex;
|
||||
_selection.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize;
|
||||
_selection.StretchMode = TextureRect.StretchModeEnum.Scale;
|
||||
_selection.Modulate = new Color(0, 0, 0, 0);
|
||||
}
|
||||
// Extra bars of other mods (mana of battle_formulas…) go under the health bar.
|
||||
var column = GetNode<VBoxContainer>("Info/Column");
|
||||
_gaugesBox = new VBoxContainer { Name = "Gauges", MouseFilter = MouseFilterEnum.Ignore, Visible = false };
|
||||
_gaugesBox.AddThemeConstantOverride("separation", 1);
|
||||
column.AddChild(_gaugesBox);
|
||||
column.MoveChild(_gaugesBox, GetNode<Control>("Info/Column/HpBarStack").GetIndex() + 1);
|
||||
|
||||
ApplyStyles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds this widget to a <see cref="Character"/> model. Sets the sprite from
|
||||
/// <see cref="Character.Image"/>, localises the name via
|
||||
/// <see cref="TranslationServer"/>, and calls <see cref="UpdateInfo"/>
|
||||
/// to refresh the HP bar.
|
||||
/// Binds the widget to a <see cref="Character"/>: sprite, localised name,
|
||||
/// team colour and per-side info layout. The team colour is derived from
|
||||
/// <see cref="IsPlayer"/>, which is why callers set it before calling here.
|
||||
/// </summary>
|
||||
public void Init(Character character)
|
||||
{
|
||||
@@ -74,33 +114,149 @@ namespace Cthangover.FFBattle.UI
|
||||
BaseScale = Scale;
|
||||
IsDead = false;
|
||||
|
||||
if (_sprite != null && character.Image != null)
|
||||
_sprite.Texture = character.Image;
|
||||
if (_art != null && character?.Image != null)
|
||||
_art.Texture = character.Image;
|
||||
|
||||
if (_nameLabel != null)
|
||||
_nameLabel.Text = TranslationServer.Translate(character.Name);
|
||||
_nameLabel.Text = TranslationServer.Translate(character?.Name ?? string.Empty).ToString();
|
||||
|
||||
var teamColor = IsPlayer ? UiTokens.Info : UiTokens.Danger;
|
||||
if (_teamLine != null)
|
||||
_teamLine.Color = UiTokens.Alpha(teamColor, 0.55f);
|
||||
if (_teamDot != null)
|
||||
_teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Alpha(teamColor, 0.9f)));
|
||||
if (_pipsBox != null)
|
||||
_pipsBox.Visible = IsPlayer;
|
||||
if (_atkLabel != null)
|
||||
_atkLabel.Visible = !IsPlayer;
|
||||
|
||||
if (!_gaugesSubscribed)
|
||||
{
|
||||
_gaugesSubscribed = true;
|
||||
BattleUnitGauges.Changed += OnGaugesChanged;
|
||||
TreeExiting += () => BattleUnitGauges.Changed -= OnGaugesChanged;
|
||||
}
|
||||
|
||||
UpdateInfo();
|
||||
}
|
||||
|
||||
/// <summary>Refreshes the HP bar fill width and colour gradient based on current health percentage.</summary>
|
||||
/// <summary>
|
||||
/// Single refresh entry point for all mutable character state: layered
|
||||
/// HP/defence bar and numbers with threshold colours, action point pips,
|
||||
/// shield chip, attack caption and the stun badge with its remaining
|
||||
/// turns. Called by battle code and by animations after each resolved
|
||||
/// action.
|
||||
/// </summary>
|
||||
public void UpdateInfo()
|
||||
{
|
||||
if (_hpFill == null || Card == null)
|
||||
// A lethal hit frees the widget through the death animation while
|
||||
// the attack animation and Health.OnChange subscribers still hold it.
|
||||
if (!IsInstanceValid(this) || IsQueuedForDeletion() || Card?.Attributes == null)
|
||||
return;
|
||||
|
||||
var hpPercent = Card.Attributes.Health.Percent;
|
||||
_hpFill.Size = new Vector2((Size.X - 8) * hpPercent, 18);
|
||||
var health = Card.Attributes.Health;
|
||||
var percent = Mathf.Clamp(health.Percent, 0f, 1f);
|
||||
var defence = Card.Attributes.Defence?.Value ?? 0;
|
||||
var fillColor = percent > 0.5f ? UiTokens.HpOk : percent > 0.25f ? UiTokens.Warning : UiTokens.HpLow;
|
||||
var textColor = percent > 0.5f ? UiTokens.HpText : percent > 0.25f ? UiTokens.Warning : UiTokens.DangerText;
|
||||
|
||||
if (hpPercent > 0.5f)
|
||||
_hpFill.Color = new Color(0.1f, 0.85f, 0.1f, 1f);
|
||||
else if (hpPercent > 0.25f)
|
||||
_hpFill.Color = new Color(0.85f, 0.85f, 0.1f, 1f);
|
||||
else
|
||||
_hpFill.Color = new Color(0.85f, 0.1f, 0.1f, 1f);
|
||||
if (_hpBar != null)
|
||||
{
|
||||
_hpBar.Value = percent * 100f;
|
||||
_hpBar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, fillColor));
|
||||
}
|
||||
|
||||
/// <summary>Shows a coloured selection overlay with a 0.15s tween fade-in. Used for target highlighting and selection indication.</summary>
|
||||
// Blue defence segment on top of the health fill; collapses onto
|
||||
// the health edge when the shield is gone.
|
||||
UiKit.UpdateShieldedHealthBar(_shieldBar, percent, defence, health.BaseValue);
|
||||
|
||||
if (_hpNumLabel != null)
|
||||
{
|
||||
_hpNumLabel.Text = $"{health.Value}/{health.BaseValue}";
|
||||
_hpNumLabel.AddThemeColorOverride("font_color", textColor);
|
||||
}
|
||||
|
||||
if (_pipsBox != null && IsPlayer)
|
||||
RefreshPips();
|
||||
|
||||
if (_atkLabel != null && !IsPlayer)
|
||||
_atkLabel.Text = string.Format("{0} {1}", Translate("ui/character_panel/stat_atk"), Card.Attributes.Attack.Value);
|
||||
|
||||
if (_shieldChip != null)
|
||||
{
|
||||
_shieldChip.Visible = defence > 0;
|
||||
if (_shieldValue != null)
|
||||
_shieldValue.Text = defence.ToString();
|
||||
}
|
||||
|
||||
RefreshStun();
|
||||
RefreshGauges();
|
||||
}
|
||||
|
||||
private void OnGaugesChanged(string unitId)
|
||||
{
|
||||
if (unitId == null || unitId == Card?.ID)
|
||||
RefreshGauges();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the <see cref="BattleUnitGauges"/> of a party member, one thin bar with its label
|
||||
/// and numbers per gauge; rows are reused by position.
|
||||
/// </summary>
|
||||
private void RefreshGauges()
|
||||
{
|
||||
if (_gaugesBox == null || !IsInstanceValid(this) || IsQueuedForDeletion())
|
||||
return;
|
||||
var gauges = IsPlayer && Card != null ? BattleUnitGauges.Read(Card) : new List<BattleGauge>();
|
||||
_gaugesBox.Visible = gauges.Count > 0;
|
||||
while (_gaugesBox.GetChildCount() > gauges.Count)
|
||||
{
|
||||
var extra = _gaugesBox.GetChild(_gaugesBox.GetChildCount() - 1);
|
||||
_gaugesBox.RemoveChild(extra);
|
||||
extra.QueueFree();
|
||||
}
|
||||
for (var i = 0; i < gauges.Count; i++)
|
||||
{
|
||||
var row = i < _gaugesBox.GetChildCount() ? (HBoxContainer)_gaugesBox.GetChild(i) : CreateGaugeRow();
|
||||
var gauge = gauges[i];
|
||||
row.GetNode<Label>("Key").Text = TranslationServer.Translate(gauge.Label ?? gauge.Id ?? string.Empty).ToString();
|
||||
var bar = row.GetNode<ProgressBar>("Bar");
|
||||
bar.Value = Mathf.Clamp(gauge.Current / (float)gauge.Max, 0f, 1f) * 100f;
|
||||
var color = string.IsNullOrWhiteSpace(gauge.Color) ? UiTokens.Info : Color.FromString(gauge.Color, UiTokens.Info);
|
||||
bar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, color));
|
||||
row.GetNode<Label>("Value").Text = $"{gauge.Current}/{gauge.Max}";
|
||||
}
|
||||
}
|
||||
|
||||
private HBoxContainer CreateGaugeRow()
|
||||
{
|
||||
var row = new HBoxContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||
row.AddThemeConstantOverride("separation", 4);
|
||||
var key = new Label { Name = "Key", MouseFilter = MouseFilterEnum.Ignore };
|
||||
key.AddThemeFontSizeOverride("font_size", UiTokens.FontTiny);
|
||||
key.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
var bar = new ProgressBar
|
||||
{
|
||||
Name = "Bar",
|
||||
ShowPercentage = false,
|
||||
MaxValue = 100,
|
||||
CustomMinimumSize = new Vector2(0, 5),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
bar.AddThemeStyleboxOverride("background", UiKit.Frame(UiTokens.RadiusSm, UiTokens.StatBg));
|
||||
var value = new Label { Name = "Value", MouseFilter = MouseFilterEnum.Ignore };
|
||||
value.AddThemeFontSizeOverride("font_size", UiTokens.FontTiny);
|
||||
value.AddThemeColorOverride("font_color", UiTokens.Info);
|
||||
row.AddChild(key);
|
||||
row.AddChild(bar);
|
||||
row.AddChild(value);
|
||||
_gaugesBox.AddChild(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
/// <summary>Shows a coloured selection frame over the card with a 0.15s fade.</summary>
|
||||
public void Highlight(Color color)
|
||||
{
|
||||
if (_selection == null)
|
||||
@@ -112,7 +268,7 @@ namespace Cthangover.FFBattle.UI
|
||||
_animTween.TweenProperty(_selection, "modulate", color, 0.15f);
|
||||
}
|
||||
|
||||
/// <summary>Fades out the selection overlay to transparent via a 0.15s tween.</summary>
|
||||
/// <summary>Fades the selection frame out via a 0.15s tween.</summary>
|
||||
public void ClearHighlight()
|
||||
{
|
||||
if (_selection == null)
|
||||
@@ -121,10 +277,10 @@ namespace Cthangover.FFBattle.UI
|
||||
_animTween?.Kill();
|
||||
_animTween = CreateTween();
|
||||
_animTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Quad);
|
||||
_animTween.TweenProperty(_selection, "modulate", new Color(0, 0, 0, 0), 0.15f);
|
||||
_animTween.TweenProperty(_selection, "modulate", UiTokens.Alpha(UiTokens.White, 0f), 0.15f);
|
||||
}
|
||||
|
||||
/// <summary>Rapidly oscillates the widget's position with random offsets (6 iterations) to simulate impact feedback.</summary>
|
||||
/// <summary>Rapidly oscillates the widget with random offsets to simulate impact feedback.</summary>
|
||||
public void Shake(float intensity, float duration)
|
||||
{
|
||||
var originalPos = Position;
|
||||
@@ -142,7 +298,7 @@ namespace Cthangover.FFBattle.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Briefly tints the entire widget with a colour pulse (30% rise, 70% fall) for damage/heal feedback.</summary>
|
||||
/// <summary>Briefly tints the whole card with a colour pulse (30% rise, 70% fall).</summary>
|
||||
public void Flash(Color color, float duration)
|
||||
{
|
||||
var originalModulate = Modulate;
|
||||
@@ -153,11 +309,11 @@ namespace Cthangover.FFBattle.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a dissolve-and-shrink death sequence. If a <c>"scene_transition"</c>
|
||||
/// shader is available, applies a dissolve material to the sprite. Fades the
|
||||
/// widget alpha, scales it to zero, and hides the HP bar and name label.
|
||||
/// Invokes <paramref name="onComplete"/> when finished so the parent panel
|
||||
/// can remove the widget from the grid.
|
||||
/// Plays a dissolve-and-shrink death sequence. The sprite gets the
|
||||
/// <c>scene_transition</c> shader with a growing dissolve progress while the
|
||||
/// card fades and squashes; the info block fades with it. Invokes
|
||||
/// <paramref name="onComplete"/> when finished so the parent panel can
|
||||
/// remove the widget and re-grid the survivors.
|
||||
/// </summary>
|
||||
public void PlayDeathAnimation(Action onComplete)
|
||||
{
|
||||
@@ -177,21 +333,20 @@ namespace Cthangover.FFBattle.UI
|
||||
if (_dissolveShader == null)
|
||||
_dissolveShader = Shaders.Resolve("scene_transition");
|
||||
|
||||
if (_dissolveShader != null && _sprite?.Texture != null)
|
||||
if (_dissolveShader != null && _art?.Texture != null)
|
||||
{
|
||||
var mat = new ShaderMaterial { Shader = _dissolveShader };
|
||||
mat.SetShaderParameter("noise_scale", 5f);
|
||||
mat.SetShaderParameter("smoothness", 0.25f);
|
||||
mat.SetShaderParameter("distortion", 0.092f);
|
||||
mat.SetShaderParameter("glow_intensity", 0f);
|
||||
mat.SetShaderParameter("hue_shift", 0f);
|
||||
mat.SetShaderParameter("invert_direction", false);
|
||||
mat.SetShaderParameter("progress", 0f);
|
||||
_sprite.Material = mat;
|
||||
var material = new ShaderMaterial { Shader = _dissolveShader };
|
||||
material.SetShaderParameter("noise_scale", 5f);
|
||||
material.SetShaderParameter("smoothness", 0.25f);
|
||||
material.SetShaderParameter("distortion", 0.092f);
|
||||
material.SetShaderParameter("glow_intensity", 0f);
|
||||
material.SetShaderParameter("hue_shift", 0f);
|
||||
material.SetShaderParameter("invert_direction", false);
|
||||
material.SetShaderParameter("progress", 0f);
|
||||
_art.Material = material;
|
||||
|
||||
var matTween = CreateTween();
|
||||
matTween.TweenMethod(
|
||||
Callable.From<float>(t => mat.SetShaderParameter("progress", t)),
|
||||
CreateTween().TweenMethod(
|
||||
Callable.From<float>(t => material.SetShaderParameter("progress", t)),
|
||||
0f, 1f, duration);
|
||||
}
|
||||
|
||||
@@ -199,9 +354,14 @@ namespace Cthangover.FFBattle.UI
|
||||
tween.SetParallel(true);
|
||||
tween.TweenProperty(this, "modulate:a", 0f, duration * 0.5f);
|
||||
tween.TweenProperty(this, "scale", BaseScale * new Vector2(0.1f, 1.2f), duration);
|
||||
tween.TweenProperty(_hpBg, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpFill, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_shieldBar, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpBar, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpNumLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_nameLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_pipsBox, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_atkLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_shieldChip, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_statusBox, "modulate:a", 0f, duration);
|
||||
|
||||
tween.Chain().TweenProperty(this, "scale", Vector2.Zero, 0.15f);
|
||||
|
||||
@@ -210,5 +370,101 @@ namespace Cthangover.FFBattle.UI
|
||||
onComplete?.Invoke();
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyStyles()
|
||||
{
|
||||
_teamLine.Color = UiTokens.Alpha(UiTokens.Danger, 0.55f);
|
||||
|
||||
var infoStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface2, 0.93f), UiTokens.Line);
|
||||
infoStyle.CornerRadiusTopLeft = 0;
|
||||
infoStyle.CornerRadiusTopRight = 0;
|
||||
infoStyle.ContentMarginLeft = 8;
|
||||
infoStyle.ContentMarginRight = 8;
|
||||
infoStyle.ContentMarginTop = 6;
|
||||
infoStyle.ContentMarginBottom = 8;
|
||||
GetNode<PanelContainer>("Info").AddThemeStyleboxOverride("panel", infoStyle);
|
||||
|
||||
_nameLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
_hpNumLabel.AddThemeColorOverride("font_color", UiTokens.HpText);
|
||||
_atkLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Danger));
|
||||
|
||||
var hpFill = UiKit.Frame(UiTokens.RadiusSm, UiTokens.HpOk);
|
||||
_hpBar.AddThemeStyleboxOverride("fill", hpFill);
|
||||
// Layered pie: the shield bar owns the track and the blue defence
|
||||
// fill; the health bar paints only its fill on top of it.
|
||||
UiKit.StyleShieldedHealthBar(_shieldBar, _hpBar);
|
||||
|
||||
var shieldStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.StatBg, UiTokens.StatLine);
|
||||
shieldStyle.ContentMarginLeft = 7;
|
||||
shieldStyle.ContentMarginRight = 7;
|
||||
shieldStyle.ContentMarginTop = 2;
|
||||
shieldStyle.ContentMarginBottom = 2;
|
||||
_shieldChip.AddThemeStyleboxOverride("panel", shieldStyle);
|
||||
GetNode<Label>("Info/Column/Foot/ShieldChip/ShieldRow/ShieldKey").AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_shieldValue.AddThemeColorOverride("font_color", UiTokens.Info);
|
||||
|
||||
var stunStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.CooldownSoft, UiTokens.CooldownLine);
|
||||
stunStyle.ContentMarginLeft = 5;
|
||||
stunStyle.ContentMarginRight = 8;
|
||||
stunStyle.ContentMarginTop = 3;
|
||||
stunStyle.ContentMarginBottom = 3;
|
||||
_stunBadge.AddThemeStyleboxOverride("panel", stunStyle);
|
||||
_stunLabel.AddThemeColorOverride("font_color", UiTokens.Cooldown);
|
||||
|
||||
var selectionStyle = UiKit.Frame(UiTokens.RadiusMd, new Color(1f, 1f, 1f, 0.07f), Colors.White, 2);
|
||||
_selection.AddThemeStyleboxOverride("panel", selectionStyle);
|
||||
_selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
}
|
||||
|
||||
private void RefreshPips()
|
||||
{
|
||||
foreach (var child in _pipsBox.GetChildren())
|
||||
{
|
||||
_pipsBox.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
|
||||
var basePoints = Mathf.Max(0, Card.Attributes.Point.BaseValue);
|
||||
var points = Card.Attributes.Point.Value;
|
||||
for (var i = 0; i < basePoints; i++)
|
||||
{
|
||||
var on = i < points;
|
||||
var pip = new Panel
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
CustomMinimumSize = new Vector2(PipSize, PipSize),
|
||||
};
|
||||
pip.AddThemeStyleboxOverride("panel", UiKit.Frame(
|
||||
PipSize / 2,
|
||||
on ? UiTokens.AccentBright : UiTokens.LineDisabled,
|
||||
on ? UiTokens.Accent : UiTokens.LineFaint));
|
||||
_pipsBox.AddChild(pip);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStun()
|
||||
{
|
||||
if (_stunBadge == null)
|
||||
return;
|
||||
|
||||
var stun = StatusEffectService.Of(Card)?.ActiveStatusEffects?
|
||||
.FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun);
|
||||
|
||||
if (stun == null)
|
||||
{
|
||||
_stunBadge.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_stunBadge.Visible = true;
|
||||
if (_stunIcon != null)
|
||||
_stunIcon.Texture = stun.Icon;
|
||||
if (_stunLabel != null)
|
||||
_stunLabel.Text = string.Format(Translate("ff_battle/status_stun"), stun.RemainingTurns);
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -4,8 +4,10 @@ using System.Linq;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
@@ -29,10 +31,16 @@ namespace Cthangover.FFBattle.UI
|
||||
|
||||
/// <summary>Raised when a widget's death animation completes and the widget is removed.</summary>
|
||||
public event System.Action<FFCharacterWidget> OnEnemyDead;
|
||||
/// <summary>Raised at lethal damage, before any victory check can consume the loot ledger.</summary>
|
||||
public event System.Action<FFCharacterWidget> OnEnemyDefeated;
|
||||
/// <summary>Raised when an enemy widget is clicked (used for target selection).</summary>
|
||||
public event System.Action<FFCharacterWidget> OnEnemyClicked;
|
||||
|
||||
protected override void Construct() { }
|
||||
/// <summary>Builds the enemy grid panel and lays out enemy widgets.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
}
|
||||
|
||||
/// <summary>Number of enemy widget instances currently managed (including dead).</summary>
|
||||
public int EnemyCount => Widgets.Count;
|
||||
@@ -53,8 +61,8 @@ namespace Cthangover.FFBattle.UI
|
||||
var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn");
|
||||
widget.EnsureConstructed();
|
||||
widget.Scale = new Vector2(scale, scale);
|
||||
widget.Init(enemies[i]);
|
||||
widget.IsPlayer = false;
|
||||
widget.Init(enemies[i]);
|
||||
widget.MouseFilter = MouseFilterEnum.Stop;
|
||||
widget.GuiInput += (evt) =>
|
||||
{
|
||||
@@ -162,6 +170,9 @@ namespace Cthangover.FFBattle.UI
|
||||
GridLayout(true);
|
||||
OnEnemyDead?.Invoke(widget);
|
||||
});
|
||||
// Victory can be resolved while the dissolve is still running.
|
||||
// Record rewards synchronously instead of waiting for its callback.
|
||||
OnEnemyDefeated?.Invoke(widget);
|
||||
}
|
||||
|
||||
/// <summary>Removes a widget from the panel, frees it, and re-grids the remaining widgets.</summary>
|
||||
|
||||
+44
-5
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Mods.Interface.Theming;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
@@ -36,8 +39,11 @@ namespace Cthangover.FFBattle.UI
|
||||
public partial class FFMenuPanel : ModWidget
|
||||
{
|
||||
private ColorRect _background;
|
||||
private ColorRect _border;
|
||||
private Panel _border;
|
||||
private Label _cursorLabel;
|
||||
private Label _caption;
|
||||
private StyleBoxFlat _rowActiveStyle;
|
||||
private StyleBoxFlat _rowIdleStyle;
|
||||
private List<Label> _itemLabels = new();
|
||||
private Tween _cursorTween;
|
||||
private int _selectedIndex;
|
||||
@@ -69,14 +75,31 @@ namespace Cthangover.FFBattle.UI
|
||||
|
||||
/// <summary>Raised when an entry is confirmed (Enter/Space or left click).</summary>
|
||||
public event System.Action<int> OnItemSelected;
|
||||
/// <summary>Raised when the highlighted entry changes (keyboard or mouse hover); carries the new index.</summary>
|
||||
public event System.Action<int> OnSelectionChanged;
|
||||
/// <summary>Raised when the menu is cancelled (right click, or Escape handled externally).</summary>
|
||||
public event System.Action OnCancelled;
|
||||
|
||||
/// <summary>Builds the command menu list (Attack/Defence/Item/...) used during the player turn.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_background = GetNode<ColorRect>("Background");
|
||||
_border = GetNode<ColorRect>("Border");
|
||||
_background.Color = UiTokens.Surface2;
|
||||
|
||||
_border = GetNode<Panel>("Border");
|
||||
_border.AddThemeStyleboxOverride("panel",
|
||||
UiKit.Frame(UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.White, 0f), UiTokens.AccentLine, 2));
|
||||
|
||||
_cursorLabel = GetNode<Label>("Cursor");
|
||||
_cursorLabel.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
|
||||
_caption = GetNode<Label>("Caption");
|
||||
_caption.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
|
||||
_rowActiveStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.AccentTrace, UiTokens.Alpha(UiTokens.White, 0f));
|
||||
_rowIdleStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.White, 0f), UiTokens.Alpha(UiTokens.White, 0f));
|
||||
|
||||
GuiInput += OnPanelGuiInput;
|
||||
}
|
||||
@@ -101,8 +124,9 @@ namespace Cthangover.FFBattle.UI
|
||||
label.Text = entries[i].Label;
|
||||
label.AddThemeFontSizeOverride("font_size", 17);
|
||||
label.AddThemeColorOverride("font_color", entries[i].Enabled
|
||||
? new Color(1f, 1f, 1f, 1f)
|
||||
: new Color(0.5f, 0.5f, 0.5f, 1f));
|
||||
? UiTokens.Ink
|
||||
: UiTokens.Faint);
|
||||
label.AddThemeStyleboxOverride("normal", _rowIdleStyle);
|
||||
label.MouseFilter = MouseFilterEnum.Stop;
|
||||
label.Position = new Vector2(PADDING + 24, PADDING + i * ITEM_HEIGHT + CURSOR_OFFSET_Y);
|
||||
label.Size = new Vector2(width - PADDING * 2 - 24, ITEM_HEIGHT);
|
||||
@@ -136,16 +160,19 @@ namespace Cthangover.FFBattle.UI
|
||||
_selectedIndex = 0;
|
||||
MoveCursor(0);
|
||||
_cursorLabel.Visible = true;
|
||||
_caption.Text = title ?? string.Empty;
|
||||
_caption.Visible = !string.IsNullOrEmpty(title);
|
||||
_visible = true;
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
/// <summary>Hides the menu and cursor. Does not clear entries — call <see cref="ShowMenu"/> to repopulate.</summary>
|
||||
/// <summary>Hides the menu, caption and cursor. Does not clear entries — call <see cref="ShowMenu"/> to repopulate.</summary>
|
||||
public void HideMenu()
|
||||
{
|
||||
_visible = false;
|
||||
Visible = false;
|
||||
_cursorLabel.Visible = false;
|
||||
_caption.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>Moves selection down, wrapping around and skipping disabled entries.</summary>
|
||||
@@ -199,6 +226,17 @@ namespace Cthangover.FFBattle.UI
|
||||
_cursorTween.SetLoops();
|
||||
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X + 3f, 0.3f);
|
||||
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X, 0.3f);
|
||||
|
||||
for (var i = 0; i < _itemLabels.Count; i++)
|
||||
{
|
||||
var enabled = i < Entries.Count && Entries[i].Enabled;
|
||||
var selected = i == index && enabled;
|
||||
_itemLabels[i].AddThemeStyleboxOverride("normal", selected ? _rowActiveStyle : _rowIdleStyle);
|
||||
_itemLabels[i].AddThemeColorOverride("font_color",
|
||||
selected ? UiTokens.AccentBright : enabled ? UiTokens.Ink : UiTokens.Faint);
|
||||
}
|
||||
|
||||
OnSelectionChanged?.Invoke(index);
|
||||
}
|
||||
|
||||
private void OnPanelGuiInput(InputEvent evt)
|
||||
@@ -216,6 +254,7 @@ namespace Cthangover.FFBattle.UI
|
||||
_itemLabels.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Frees the menu entries and disconnects selection handlers when the panel closes.</summary>
|
||||
protected override void Destruct()
|
||||
{
|
||||
ClearEntries();
|
||||
|
||||
@@ -3,8 +3,10 @@ using System.Linq;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,7 +27,11 @@ namespace Cthangover.FFBattle.UI
|
||||
/// <summary>Raised when a player widget is clicked — routed to <see cref="FFBattleCore"/> for character selection.</summary>
|
||||
public event System.Action<FFCharacterWidget> OnWidgetClicked;
|
||||
|
||||
protected override void Construct() { }
|
||||
/// <summary>Builds the bottom player-party panel hosting one widget per party member.</summary>
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="FFCharacterWidget"/> instances for each player, scales
|
||||
@@ -43,8 +49,8 @@ namespace Cthangover.FFBattle.UI
|
||||
var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn");
|
||||
widget.EnsureConstructed();
|
||||
widget.Scale = new Vector2(scale, scale);
|
||||
widget.Init(players[i]);
|
||||
widget.IsPlayer = true;
|
||||
widget.Init(players[i]);
|
||||
widget.MouseFilter = MouseFilterEnum.Stop;
|
||||
widget.GuiInput += (evt) =>
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user