new features, play menu, deck builder, deck selection
@@ -32,7 +32,11 @@
|
|||||||
"Bash(git -C /home/ckoch/Documents/Development/FFCardGame log --oneline -20)",
|
"Bash(git -C /home/ckoch/Documents/Development/FFCardGame log --oneline -20)",
|
||||||
"Bash(git clone:*)",
|
"Bash(git clone:*)",
|
||||||
"Bash(godot --headless --script res://addons/gut/gut_cmdln.gd -gdir=res://tests -gexit:*)",
|
"Bash(godot --headless --script res://addons/gut/gut_cmdln.gd -gdir=res://tests -gexit:*)",
|
||||||
"Bash(/home/ckoch/Downloads/Godot_v4.2-stable_linux.x86_64:*)"
|
"Bash(/home/ckoch/Downloads/Godot_v4.2-stable_linux.x86_64:*)",
|
||||||
|
"Bash(timeout 120 godot:*)",
|
||||||
|
"Bash(convert:*)",
|
||||||
|
"Bash(timeout 60 godot:*)",
|
||||||
|
"Bash(timeout 60 ~/.local/share/Godot/bin/godot:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ dock_floating={}
|
|||||||
dock_split_2=0
|
dock_split_2=0
|
||||||
dock_split_3=0
|
dock_split_3=0
|
||||||
dock_hsplit_1=0
|
dock_hsplit_1=0
|
||||||
dock_hsplit_2=540
|
dock_hsplit_2=1040
|
||||||
dock_hsplit_3=-540
|
dock_hsplit_3=-540
|
||||||
dock_hsplit_4=0
|
dock_hsplit_4=0
|
||||||
dock_filesystem_split=0
|
dock_filesystem_split=0
|
||||||
dock_filesystem_display_mode=0
|
dock_filesystem_display_mode=0
|
||||||
dock_filesystem_file_sort=0
|
dock_filesystem_file_sort=0
|
||||||
dock_filesystem_file_list_display_mode=1
|
dock_filesystem_file_list_display_mode=1
|
||||||
dock_filesystem_selected_paths=PackedStringArray("res://scripts/ui/ActionLog.gd")
|
dock_filesystem_selected_paths=PackedStringArray("res://scripts/ui/GameSetupMenu.gd")
|
||||||
dock_filesystem_uncollapsed_paths=PackedStringArray("Favorites", "res://", "res://scripts/", "res://scripts/visual/", "res://scripts/ui/", "res://scripts/game/", "res://scenes/")
|
dock_filesystem_uncollapsed_paths=PackedStringArray("Favorites", "res://", "res://scripts/", "res://scripts/visual/", "res://scripts/ui/", "res://scripts/game/", "res://scenes/")
|
||||||
dock_3="Scene,Import"
|
dock_3="Scene,Import"
|
||||||
dock_4="FileSystem"
|
dock_4="FileSystem"
|
||||||
@@ -30,14 +30,14 @@ dock_5="Inspector,Node,History"
|
|||||||
open_scenes=PackedStringArray("res://scenes/main.tscn")
|
open_scenes=PackedStringArray("res://scenes/main.tscn")
|
||||||
current_scene="res://scenes/main.tscn"
|
current_scene="res://scenes/main.tscn"
|
||||||
center_split_offset=-491
|
center_split_offset=-491
|
||||||
selected_default_debugger_tab_idx=1
|
selected_default_debugger_tab_idx=0
|
||||||
selected_main_editor_idx=2
|
selected_main_editor_idx=2
|
||||||
selected_bottom_panel_item=1
|
selected_bottom_panel_item=0
|
||||||
|
|
||||||
[ScriptEditor]
|
[ScriptEditor]
|
||||||
|
|
||||||
open_scripts=["res://scripts/ui/ActionLog.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/game/CardInstance.gd", "res://scripts/visual/CardVisual.gd", "res://scripts/game/CPPool.gd", "res://scripts/ui/DamageDisplay.gd", "res://scripts/GameController.gd", "res://scripts/game/GameState.gd", "res://scripts/ui/GameUI.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/Main.gd", "res://scripts/ui/MainMenu.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/game/Player.gd", "res://scripts/visual/PlaymatRenderer.gd", "res://scripts/visual/TableCamera.gd", "res://scripts/visual/TableSetup.gd", "res://tests/fixtures/test_card_data.gd", "res://tests/integration/test_game_state.gd", "res://tests/unit/test_zone.gd", "res://scripts/game/UndoSystem.gd", "res://scripts/game/Zone.gd"]
|
open_scripts=["res://scripts/ui/ActionLog.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/ui/CardDetailViewer.gd", "res://scripts/game/CardInstance.gd", "res://scripts/visual/CardVisual.gd", "res://scripts/game/CPPool.gd", "res://scripts/ui/DamageDisplay.gd", "res://scripts/ui/DeckListPanel.gd", "res://scripts/game/Enums.gd", "res://scripts/GameController.gd", "res://scripts/ui/GameSetupMenu.gd", "res://scripts/game/GameState.gd", "res://scripts/ui/GameUI.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/Main.gd", "res://scripts/ui/MainMenu.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/game/Player.gd", "res://scripts/visual/PlaymatRenderer.gd", "res://scripts/visual/TableCamera.gd", "res://scripts/visual/TableSetup.gd", "res://tests/fixtures/test_card_data.gd", "res://tests/integration/test_game_state.gd", "res://tests/unit/test_zone.gd", "res://scripts/game/UndoSystem.gd", "res://scripts/game/Zone.gd"]
|
||||||
selected_script="res://scripts/game/Zone.gd"
|
selected_script="res://scripts/ui/GameSetupMenu.gd"
|
||||||
open_help=[]
|
open_help=[]
|
||||||
script_split_offset=140
|
script_split_offset=140
|
||||||
list_split_offset=0
|
list_split_offset=0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
ea4bc82a6ad023ab7ee23ee620429895
|
ea4bc82a6ad023ab7ee23ee620429895
|
||||||
::res://::1769613134
|
::res://::1769647407
|
||||||
background_1.png::CompressedTexture2D::259206091835802070::1769464008::1769464212::1::::<><>::
|
background_1.png::CompressedTexture2D::259206091835802070::1769464008::1769464212::1::::<><>::
|
||||||
card_back.png::CompressedTexture2D::4833498016096001590::1769466370::1769466517::1::::<><>::
|
card_back.png::CompressedTexture2D::4833498016096001590::1769466370::1769466517::1::::<><>::
|
||||||
FF14_Playmat__12516.webp::CompressedTexture2D::1641665221299209414::1769277769::1769280957::1::::<><>::
|
FF14_Playmat__12516.webp::CompressedTexture2D::1641665221299209414::1769277769::1769280957::1::::<><>::
|
||||||
@@ -7,6 +7,27 @@ FF_mat_option_1.png::CompressedTexture2D::4359709237641823626::1769451897::17694
|
|||||||
JimNightshade-Regular.ttf::FontFile::7644275900508645331::1757609064::1769555265::1::::<><>::
|
JimNightshade-Regular.ttf::FontFile::7644275900508645331::1757609064::1769555265::1::::<><>::
|
||||||
README.md::TextFile::-1::1769279531::0::1::::<><>::
|
README.md::TextFile::-1::1769279531::0::1::::<><>::
|
||||||
Screenshot 2026-01-24 at 12-53-03 Untitled-3 - fftcgrulesheet-en.pdf.png::CompressedTexture2D::5958662832102035034::1769277183::1769280957::1::::<><>::
|
Screenshot 2026-01-24 at 12-53-03 Untitled-3 - fftcgrulesheet-en.pdf.png::CompressedTexture2D::5958662832102035034::1769277183::1769280957::1::::<><>::
|
||||||
|
sleeve_1.jpg::CompressedTexture2D::1482248397063355742::1769617800::1769618827::1::::<><>::
|
||||||
|
sleeve_2.jpg::CompressedTexture2D::6383066359122151379::1769617926::1769618827::1::::<><>::
|
||||||
|
sleeve_3.jpg::CompressedTexture2D::601887052280553192::1769617938::1769618827::1::::<><>::
|
||||||
|
sleeve_4.jpg::CompressedTexture2D::6060458880840563199::1769617950::1769618827::1::::<><>::
|
||||||
|
sleeve_5.jpg::CompressedTexture2D::5882622769392279266::1769617965::1769618827::1::::<><>::
|
||||||
|
sleeve_6.jpg::CompressedTexture2D::8915184504686677023::1769618359::1769618827::1::::<><>::
|
||||||
|
sleeve_7.jpg::CompressedTexture2D::6894860378666619339::1769618370::1769618827::1::::<><>::
|
||||||
|
sleeve_8.jpg::CompressedTexture2D::7601270914850012809::1769618385::1769618827::1::::<><>::
|
||||||
|
sleeve_9.jpg::CompressedTexture2D::5656371699412624542::1769618396::1769618827::1::::<><>::
|
||||||
|
sleeve_10.jpg::CompressedTexture2D::3792829908701748015::1769618409::1769618827::1::::<><>::
|
||||||
|
sleeve_11.jpg::CompressedTexture2D::3100688219177629970::1769618692::1769618827::1::::<><>::
|
||||||
|
sleeve_12.jpg::CompressedTexture2D::1346926720015876285::1769618708::1769618827::1::::<><>::
|
||||||
|
sleeve_13.jpg::CompressedTexture2D::1194020321927022207::1769619087::1769644241::1::::<><>::
|
||||||
|
sleeve_14.jpg::CompressedTexture2D::7503502150336033005::1769619098::1769644241::1::::<><>::
|
||||||
|
sleeve_15.jpg::CompressedTexture2D::8717635135292546825::1769619110::1769644241::1::::<><>::
|
||||||
|
sleeve_16.jpg::CompressedTexture2D::166845585734163091::1769619123::1769644241::1::::<><>::
|
||||||
|
sleeve_17.jpg::CompressedTexture2D::4080901806752110915::1769619134::1769644241::1::::<><>::
|
||||||
|
sleeve_18.jpg::CompressedTexture2D::7870841060308936216::1769619160::1769644241::1::::<><>::
|
||||||
|
sleeve_19.jpg::CompressedTexture2D::4959713250773028751::1769619172::1769644241::1::::<><>::
|
||||||
|
sleeve_20.jpg::CompressedTexture2D::4615949045548257824::1769619186::1769644241::1::::<><>::
|
||||||
|
sleeve_21.jpg::CompressedTexture2D::4455193360889187036::1769619197::1769644241::1::::<><>::
|
||||||
title_menu.png::CompressedTexture2D::4103292590061137586::1769543314::1769543405::1::::<><>::
|
title_menu.png::CompressedTexture2D::4103292590061137586::1769543314::1769543405::1::::<><>::
|
||||||
::res://addons/::1769611855
|
::res://addons/::1769611855
|
||||||
::res://addons/gut/::1769611855
|
::res://addons/gut/::1769611855
|
||||||
@@ -127,44 +148,63 @@ card_back.png::CompressedTexture2D::7787125851359297441::1769466418::1769466517:
|
|||||||
::res://assets/table/::1769464212
|
::res://assets/table/::1769464212
|
||||||
background_1.png::CompressedTexture2D::102728058489724503::1769464097::1769464212::1::::<><>::
|
background_1.png::CompressedTexture2D::102728058489724503::1769464097::1769464212::1::::<><>::
|
||||||
playmat.webp::CompressedTexture2D::3235866490631872101::1769279471::1769280957::1::::<><>::
|
playmat.webp::CompressedTexture2D::3235866490631872101::1769279471::1769280957::1::::<><>::
|
||||||
::res://assets/ui/::1769542991
|
::res://assets/ui/::1769638945
|
||||||
icon.svg::CompressedTexture2D::2912283608529879130::1769280588::1769280956::1::::<><>::
|
icon.svg::CompressedTexture2D::2912283608529879130::1769280588::1769280956::1::::<><>::
|
||||||
title_menu.png::CompressedTexture2D::8625156175856392101::1769542458::1769542991::1::::<><>::
|
title_menu.png::CompressedTexture2D::8625156175856392101::1769542458::1769542991::1::::<><>::
|
||||||
::res://data/::1769541933
|
::res://assets/ui/starter_decks/::1769644241
|
||||||
|
opus1_vii_fire_earth.png::CompressedTexture2D::8249821348761355382::1769638945::1769644241::1::::<><>::
|
||||||
|
opus1_xiii_ice_lightning.png::CompressedTexture2D::5441208453357402287::1769638945::1769644241::1::::<><>::
|
||||||
|
opus1_x_water_wind.png::CompressedTexture2D::1129182255694527899::1769638945::1769644241::1::::<><>::
|
||||||
|
opus3_ix_fire_water.png::CompressedTexture2D::5381369350901260001::1769638945::1769644240::1::::<><>::
|
||||||
|
opus3_type0_wind_lightning.png::CompressedTexture2D::6074265651240728071::1769638945::1769644240::1::::<><>::
|
||||||
|
opus5_xiii2_ice_fire.png::CompressedTexture2D::3841226189606794188::1769638945::1769644240::1::::<><>::
|
||||||
|
opus5_xii_wind_water.png::CompressedTexture2D::5241364105917462364::1769638945::1769644240::1::::<><>::
|
||||||
|
opus5_xiv_earth_lightning.png::CompressedTexture2D::4811331662772755880::1769638945::1769644240::1::::<><>::
|
||||||
|
::res://data/::1769639015
|
||||||
cards.json::JSON::-1::1769541579::0::1::::<><>::
|
cards.json::JSON::-1::1769541579::0::1::::<><>::
|
||||||
cards_progress.json::JSON::-1::1769539572::0::1::::<><>::
|
cards_progress.json::JSON::-1::1769539572::0::1::::<><>::
|
||||||
scan_errors.log::TextFile::-1::1769539203::0::1::::<><>::
|
scan_errors.log::TextFile::-1::1769539203::0::1::::<><>::
|
||||||
|
starter_decks.json::JSON::-1::1769639015::0::1::::<><>::
|
||||||
::res://docs/::1769279608
|
::res://docs/::1769279608
|
||||||
CARD_FORMAT.md::TextFile::-1::1769279608::0::1::::<><>::
|
CARD_FORMAT.md::TextFile::-1::1769279608::0::1::::<><>::
|
||||||
DESIGN.md::TextFile::-1::1769279572::0::1::::<><>::
|
DESIGN.md::TextFile::-1::1769279572::0::1::::<><>::
|
||||||
::res://scenes/::1769558936
|
::res://scenes/::1769646947
|
||||||
game_controller.tscn::PackedScene::3882700613993784342::1769285267::0::1::::<><>::res://scripts/GameController.gd
|
game_controller.tscn::PackedScene::3882700613993784342::1769285267::0::1::::<><>::res://scripts/GameController.gd
|
||||||
main.tscn::PackedScene::5942992277112036945::1769558936::0::1::::<><>::res://scripts/Main.gd
|
main.tscn::PackedScene::5942992277112036945::1769646947::0::1::::<><>::res://scripts/Main.gd
|
||||||
::res://scenes/card/::1769279430
|
::res://scenes/card/::1769279430
|
||||||
::res://scenes/main/::1769279430
|
::res://scenes/main/::1769279430
|
||||||
::res://scenes/table/::1769279430
|
::res://scenes/table/::1769279430
|
||||||
::res://scenes/ui/::1769279430
|
::res://scenes/ui/::1769279430
|
||||||
::res://scripts/::1769558794
|
::res://scripts/::1769645475
|
||||||
GameController.gd::GDScript::-1::1769557430::0::1::::<>Node<>::
|
GameController.gd::GDScript::-1::1769645475::0::1::::<>Node<>::
|
||||||
Main.gd::GDScript::-1::1769558794::0::1::::<>Node3D<>::
|
Main.gd::GDScript::-1::1769627747::0::1::::<>Node3D<>::
|
||||||
::res://scripts/autoload/::1769308378
|
::res://scripts/autoload/::1769639030
|
||||||
CardDatabase.gd::GDScript::-1::1769308329::0::1::::<>Node<>::
|
CardDatabase.gd::GDScript::-1::1769639030::0::1::::<>Node<>::
|
||||||
GameManager.gd::GDScript::-1::1769308378::0::1::::<>Node<>::
|
GameManager.gd::GDScript::-1::1769627732::0::1::::<>Node<>::
|
||||||
::res://scripts/game/::1769471419
|
::res://scripts/data/::1769625750
|
||||||
|
Deck.gd::GDScript::-1::1769625732::0::1::::Deck<>RefCounted<>::
|
||||||
|
DeckManager.gd::GDScript::-1::1769625750::0::1::::DeckManager<>RefCounted<>::
|
||||||
|
::res://scripts/game/::1769626106
|
||||||
CardInstance.gd::GDScript::-1::1769279755::0::1::::CardInstance<>RefCounted<>::
|
CardInstance.gd::GDScript::-1::1769279755::0::1::::CardInstance<>RefCounted<>::
|
||||||
CPPool.gd::GDScript::-1::1769302515::0::1::::CPPool<>RefCounted<>::
|
CPPool.gd::GDScript::-1::1769302515::0::1::::CPPool<>RefCounted<>::
|
||||||
Enums.gd::GDScript::-1::1769281049::0::1::::Enums<>RefCounted<>::
|
Enums.gd::GDScript::-1::1769626106::0::1::::Enums<>RefCounted<>::
|
||||||
GameState.gd::GDScript::-1::1769471419::0::1::::GameState<>RefCounted<>::
|
GameState.gd::GDScript::-1::1769471419::0::1::::GameState<>RefCounted<>::
|
||||||
Player.gd::GDScript::-1::1769302256::0::1::::Player<>RefCounted<>::
|
Player.gd::GDScript::-1::1769302256::0::1::::Player<>RefCounted<>::
|
||||||
TurnManager.gd::GDScript::-1::1769302284::0::1::::TurnManager<>RefCounted<>::
|
TurnManager.gd::GDScript::-1::1769302284::0::1::::TurnManager<>RefCounted<>::
|
||||||
UndoSystem.gd::GDScript::-1::1769301595::0::1::::UndoSystem<>RefCounted<>::
|
UndoSystem.gd::GDScript::-1::1769301595::0::1::::UndoSystem<>RefCounted<>::
|
||||||
Zone.gd::GDScript::-1::1769302225::0::1::::Zone<>RefCounted<>::
|
Zone.gd::GDScript::-1::1769302225::0::1::::Zone<>RefCounted<>::
|
||||||
::res://scripts/ui/::1769558772
|
::res://scripts/ui/::1769646748
|
||||||
ActionLog.gd::GDScript::-1::1769298563::0::1::::ActionLog<>Control<>::
|
ActionLog.gd::GDScript::-1::1769298563::0::1::::ActionLog<>Control<>::
|
||||||
|
CardDetailViewer.gd::GDScript::-1::1769625828::0::1::::CardDetailViewer<>Control<>::
|
||||||
|
CardFilterBar.gd::GDScript::-1::1769625881::0::1::::CardFilterBar<>Control<>::
|
||||||
|
CardGrid.gd::GDScript::-1::1769625920::0::1::::CardGrid<>Control<>::
|
||||||
DamageDisplay.gd::GDScript::-1::1769280183::0::1::::DamageDisplay<>Control<>::
|
DamageDisplay.gd::GDScript::-1::1769280183::0::1::::DamageDisplay<>Control<>::
|
||||||
|
DeckBuilder.gd::GDScript::-1::1769626028::0::1::::DeckBuilder<>CanvasLayer<>::
|
||||||
|
DeckListPanel.gd::GDScript::-1::1769625968::0::1::::DeckListPanel<>Control<>::
|
||||||
|
GameSetupMenu.gd::GDScript::-1::1769646748::0::1::::GameSetupMenu<>CanvasLayer<>::
|
||||||
GameUI.gd::GDScript::-1::1769472787::0::1::::GameUI<>CanvasLayer<>::
|
GameUI.gd::GDScript::-1::1769472787::0::1::::GameUI<>CanvasLayer<>::
|
||||||
HandDisplay.gd::GDScript::-1::1769558772::0::1::::HandDisplay<>Control<>::
|
HandDisplay.gd::GDScript::-1::1769558772::0::1::::HandDisplay<>Control<>::
|
||||||
MainMenu.gd::GDScript::-1::1769557191::0::1::::MainMenu<>CanvasLayer<>::
|
MainMenu.gd::GDScript::-1::1769626090::0::1::::MainMenu<>CanvasLayer<>::
|
||||||
PauseMenu.gd::GDScript::-1::1769287615::0::1::::PauseMenu<>CanvasLayer<>::
|
PauseMenu.gd::GDScript::-1::1769287615::0::1::::PauseMenu<>CanvasLayer<>::
|
||||||
::res://scripts/visual/::1769471729
|
::res://scripts/visual/::1769471729
|
||||||
CardVisual.gd::GDScript::-1::1769460118::0::1::::CardVisual<>Node3D<>::
|
CardVisual.gd::GDScript::-1::1769460118::0::1::::CardVisual<>Node3D<>::
|
||||||
@@ -4190,14 +4230,14 @@ Re-198H-13-127H.jpg::CompressedTexture2D::5413433616482804995::1769306351::17693
|
|||||||
Re-199H-19-125H.jpg::CompressedTexture2D::4308488034767344289::1769306351::1769308619::1::::<><>::
|
Re-199H-19-125H.jpg::CompressedTexture2D::4308488034767344289::1769306351::1769308619::1::::<><>::
|
||||||
Re-200L-19-128L.jpg::CompressedTexture2D::8097057901487536182::1769306356::1769308614::1::::<><>::
|
Re-200L-19-128L.jpg::CompressedTexture2D::8097057901487536182::1769306356::1769308614::1::::<><>::
|
||||||
::res://tests/::1769610959
|
::res://tests/::1769610959
|
||||||
::res://tests/fixtures/::1769610993
|
::res://tests/fixtures/::1769614165
|
||||||
test_card_data.gd::GDScript::-1::1769610993::0::1::::TestCardData<>RefCounted<>::
|
test_card_data.gd::GDScript::-1::1769614165::0::1::::TestCardData<>RefCounted<>::
|
||||||
::res://tests/integration/::1769611345
|
::res://tests/integration/::1769615059
|
||||||
test_game_state.gd::GDScript::-1::1769611345::0::1::::<>GutTest<>::
|
test_game_state.gd::GDScript::-1::1769615059::0::1::::<>GutTest<>::
|
||||||
::res://tests/unit/::1769611266
|
::res://tests/unit/::1769615032
|
||||||
test_card_instance.gd::GDScript::-1::1769611194::0::1::::<>GutTest<>::
|
test_card_instance.gd::GDScript::-1::1769611194::0::1::::<>GutTest<>::
|
||||||
test_cppool.gd::GDScript::-1::1769611095::0::1::::<>GutTest<>::
|
test_cppool.gd::GDScript::-1::1769611095::0::1::::<>GutTest<>::
|
||||||
test_player.gd::GDScript::-1::1769611266::0::1::::<>GutTest<>::
|
test_player.gd::GDScript::-1::1769611266::0::1::::<>GutTest<>::
|
||||||
test_turn_manager.gd::GDScript::-1::1769611145::0::1::::<>GutTest<>::
|
test_turn_manager.gd::GDScript::-1::1769611145::0::1::::<>GutTest<>::
|
||||||
test_zone.gd::GDScript::-1::1769611040::0::1::::<>GutTest<>::
|
test_zone.gd::GDScript::-1::1769615032::0::1::::<>GutTest<>::
|
||||||
::res://tools/::1769541891
|
::res://tools/::1769541891
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ run_reload_scripts=true
|
|||||||
[recent_files]
|
[recent_files]
|
||||||
|
|
||||||
scenes=["res://scenes/main.tscn"]
|
scenes=["res://scenes/main.tscn"]
|
||||||
scripts=["res://scripts/game/Zone.gd", "res://tests/unit/test_zone.gd", "res://tests/integration/test_game_state.gd", "res://tests/fixtures/test_card_data.gd", "res://scripts/ui/DamageDisplay.gd", "res://scripts/visual/CardVisual.gd", "res://scripts/GameController.gd", "res://scripts/visual/PlaymatRenderer.gd", "res://scripts/ui/MainMenu.gd", "res://scripts/ui/GameUI.gd"]
|
scripts=["res://scripts/ui/DeckListPanel.gd", "res://scripts/ui/GameSetupMenu.gd", "res://scripts/game/Enums.gd", "res://scripts/ui/CardDetailViewer.gd", "res://scripts/game/Zone.gd", "res://tests/unit/test_zone.gd", "res://tests/integration/test_game_state.gd", "res://tests/fixtures/test_card_data.gd", "res://scripts/ui/DamageDisplay.gd", "res://scripts/visual/CardVisual.gd"]
|
||||||
|
|
||||||
[linked_properties]
|
[linked_properties]
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
state={
|
state={
|
||||||
"bookmarks": PackedInt32Array(),
|
"bookmarks": PackedInt32Array(),
|
||||||
"breakpoints": PackedInt32Array(),
|
"breakpoints": PackedInt32Array(),
|
||||||
"column": 8,
|
"column": 15,
|
||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 314,
|
"row": 316,
|
||||||
"scroll_position": 309.0,
|
"scroll_position": 313.0,
|
||||||
"selection": false,
|
"selection": false,
|
||||||
"syntax_highlighter": "GDScript"
|
"syntax_highlighter": "GDScript"
|
||||||
}
|
}
|
||||||
@@ -115,11 +115,11 @@ state={
|
|||||||
state={
|
state={
|
||||||
"bookmarks": PackedInt32Array(),
|
"bookmarks": PackedInt32Array(),
|
||||||
"breakpoints": PackedInt32Array(),
|
"breakpoints": PackedInt32Array(),
|
||||||
"column": 0,
|
"column": 3,
|
||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 0,
|
"row": 9,
|
||||||
"scroll_position": 39.0,
|
"scroll_position": 0.0,
|
||||||
"selection": false,
|
"selection": false,
|
||||||
"syntax_highlighter": "GDScript"
|
"syntax_highlighter": "GDScript"
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ state={
|
|||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 33,
|
"row": 33,
|
||||||
"scroll_position": 0.0,
|
"scroll_position": 20.0,
|
||||||
"selection": false,
|
"selection": false,
|
||||||
"syntax_highlighter": "GDScript"
|
"syntax_highlighter": "GDScript"
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,63 @@ state={
|
|||||||
"folded_lines": Array[int]([]),
|
"folded_lines": Array[int]([]),
|
||||||
"h_scroll_position": 0,
|
"h_scroll_position": 0,
|
||||||
"row": 22,
|
"row": 22,
|
||||||
"scroll_position": 22.0,
|
"scroll_position": 0.0,
|
||||||
|
"selection": false,
|
||||||
|
"syntax_highlighter": "GDScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
[res://scripts/ui/CardDetailViewer.gd]
|
||||||
|
|
||||||
|
state={
|
||||||
|
"bookmarks": PackedInt32Array(),
|
||||||
|
"breakpoints": PackedInt32Array(),
|
||||||
|
"column": 0,
|
||||||
|
"folded_lines": Array[int]([]),
|
||||||
|
"h_scroll_position": 0,
|
||||||
|
"row": 0,
|
||||||
|
"scroll_position": 0.0,
|
||||||
|
"selection": false,
|
||||||
|
"syntax_highlighter": "GDScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
[res://scripts/game/Enums.gd]
|
||||||
|
|
||||||
|
state={
|
||||||
|
"bookmarks": PackedInt32Array(),
|
||||||
|
"breakpoints": PackedInt32Array(),
|
||||||
|
"column": 7,
|
||||||
|
"folded_lines": Array[int]([]),
|
||||||
|
"h_scroll_position": 0,
|
||||||
|
"row": 11,
|
||||||
|
"scroll_position": 0.0,
|
||||||
|
"selection": false,
|
||||||
|
"syntax_highlighter": "GDScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
[res://scripts/ui/GameSetupMenu.gd]
|
||||||
|
|
||||||
|
state={
|
||||||
|
"bookmarks": PackedInt32Array(),
|
||||||
|
"breakpoints": PackedInt32Array(),
|
||||||
|
"column": 0,
|
||||||
|
"folded_lines": Array[int]([]),
|
||||||
|
"h_scroll_position": 0,
|
||||||
|
"row": 0,
|
||||||
|
"scroll_position": 0.0,
|
||||||
|
"selection": false,
|
||||||
|
"syntax_highlighter": "GDScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
[res://scripts/ui/DeckListPanel.gd]
|
||||||
|
|
||||||
|
state={
|
||||||
|
"bookmarks": PackedInt32Array(),
|
||||||
|
"breakpoints": PackedInt32Array(),
|
||||||
|
"column": 0,
|
||||||
|
"folded_lines": Array[int]([]),
|
||||||
|
"h_scroll_position": 0,
|
||||||
|
"row": 0,
|
||||||
|
"scroll_position": 0.0,
|
||||||
"selection": false,
|
"selection": false,
|
||||||
"syntax_highlighter": "GDScript"
|
"syntax_highlighter": "GDScript"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,24 @@ list=Array[Dictionary]([{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/game/CPPool.gd"
|
"path": "res://scripts/game/CPPool.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"CardDetailViewer",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/CardDetailViewer.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"CardFilterBar",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/CardFilterBar.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"CardGrid",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/CardGrid.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"CardInstance",
|
"class": &"CardInstance",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@@ -30,11 +48,41 @@ list=Array[Dictionary]([{
|
|||||||
"path": "res://scripts/ui/DamageDisplay.gd"
|
"path": "res://scripts/ui/DamageDisplay.gd"
|
||||||
}, {
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
|
"class": &"Deck",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/data/Deck.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"CanvasLayer",
|
||||||
|
"class": &"DeckBuilder",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/DeckBuilder.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"DeckListPanel",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/DeckListPanel.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"RefCounted",
|
||||||
|
"class": &"DeckManager",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/data/DeckManager.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"RefCounted",
|
||||||
"class": &"Enums",
|
"class": &"Enums",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/game/Enums.gd"
|
"path": "res://scripts/game/Enums.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"CanvasLayer",
|
||||||
|
"class": &"GameSetupMenu",
|
||||||
|
"icon": "",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/GameSetupMenu.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"GameState",
|
"class": &"GameState",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
|||||||
BIN
assets/ui/starter_decks/opus1_vii_fire_earth.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
34
assets/ui/starter_decks/opus1_vii_fire_earth.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dpsjl8i2le5j3"
|
||||||
|
path="res://.godot/imported/opus1_vii_fire_earth.png-a0dcf77bf18a42296deb31ab2d816137.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus1_vii_fire_earth.png"
|
||||||
|
dest_files=["res://.godot/imported/opus1_vii_fire_earth.png-a0dcf77bf18a42296deb31ab2d816137.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus1_x_water_wind.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
34
assets/ui/starter_decks/opus1_x_water_wind.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://qc8p4uffxpyh"
|
||||||
|
path="res://.godot/imported/opus1_x_water_wind.png-b14090a32fe828f93d379678880ec622.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus1_x_water_wind.png"
|
||||||
|
dest_files=["res://.godot/imported/opus1_x_water_wind.png-b14090a32fe828f93d379678880ec622.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus1_xiii_ice_lightning.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
34
assets/ui/starter_decks/opus1_xiii_ice_lightning.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cjr0547dt0ibv"
|
||||||
|
path="res://.godot/imported/opus1_xiii_ice_lightning.png-8ef9408ef8d479fbb86b1607d9a57420.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus1_xiii_ice_lightning.png"
|
||||||
|
dest_files=["res://.godot/imported/opus1_xiii_ice_lightning.png-8ef9408ef8d479fbb86b1607d9a57420.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus3_ix_fire_water.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
34
assets/ui/starter_decks/opus3_ix_fire_water.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ciw1mqh3rodl6"
|
||||||
|
path="res://.godot/imported/opus3_ix_fire_water.png-35f3e50a1b7abe756b8299cca924963d.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus3_ix_fire_water.png"
|
||||||
|
dest_files=["res://.godot/imported/opus3_ix_fire_water.png-35f3e50a1b7abe756b8299cca924963d.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus3_type0_wind_lightning.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cssojdugkwlnr"
|
||||||
|
path="res://.godot/imported/opus3_type0_wind_lightning.png-3ca22de9eb3d66e201940c1fcf8f1421.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus3_type0_wind_lightning.png"
|
||||||
|
dest_files=["res://.godot/imported/opus3_type0_wind_lightning.png-3ca22de9eb3d66e201940c1fcf8f1421.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus5_xii_wind_water.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
34
assets/ui/starter_decks/opus5_xii_wind_water.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cgw7rbhljbdii"
|
||||||
|
path="res://.godot/imported/opus5_xii_wind_water.png-2041d32cfed3ce78d97b8e3eaa6a200c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus5_xii_wind_water.png"
|
||||||
|
dest_files=["res://.godot/imported/opus5_xii_wind_water.png-2041d32cfed3ce78d97b8e3eaa6a200c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus5_xiii2_ice_fire.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
34
assets/ui/starter_decks/opus5_xiii2_ice_fire.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://buyyo04fdhdwm"
|
||||||
|
path="res://.godot/imported/opus5_xiii2_ice_fire.png-ca26691c1b1954bf23bcfcfd43c8ccf7.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus5_xiii2_ice_fire.png"
|
||||||
|
dest_files=["res://.godot/imported/opus5_xiii2_ice_fire.png-ca26691c1b1954bf23bcfcfd43c8ccf7.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/ui/starter_decks/opus5_xiv_earth_lightning.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
34
assets/ui/starter_decks/opus5_xiv_earth_lightning.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://casv6usx12xsq"
|
||||||
|
path="res://.godot/imported/opus5_xiv_earth_lightning.png-eb2707ab7e0181bd66f13c2378320b22.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/ui/starter_decks/opus5_xiv_earth_lightning.png"
|
||||||
|
dest_files=["res://.godot/imported/opus5_xiv_earth_lightning.png-eb2707ab7e0181bd66f13c2378320b22.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
207
data/starter_decks.json
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"starter_decks": [
|
||||||
|
{
|
||||||
|
"id": "opus1_vii_fire_earth",
|
||||||
|
"name": "VII Fire/Earth",
|
||||||
|
"opus": "Opus I",
|
||||||
|
"elements": ["Fire", "Earth"],
|
||||||
|
"description": "Cloud, Tifa, and the AVALANCHE crew",
|
||||||
|
"image": "starter_decks/opus1_vii_fire_earth.png",
|
||||||
|
"cards": [
|
||||||
|
"1-001R", "1-001R", "1-001R",
|
||||||
|
"1-003C", "1-003C", "1-003C",
|
||||||
|
"1-005C", "1-005C", "1-005C",
|
||||||
|
"1-017C", "1-017C", "1-017C",
|
||||||
|
"1-019R", "1-019R", "1-019R",
|
||||||
|
"1-021H",
|
||||||
|
"1-182S", "1-182S", "1-182S",
|
||||||
|
"1-093R", "1-093R", "1-093R",
|
||||||
|
"1-094H", "1-094H", "1-094H",
|
||||||
|
"1-100C", "1-100C", "1-100C",
|
||||||
|
"1-104C", "1-104C", "1-104C",
|
||||||
|
"1-107L",
|
||||||
|
"1-108C", "1-108C", "1-108C",
|
||||||
|
"1-110R", "1-110R", "1-110R",
|
||||||
|
"1-111C", "1-111C", "1-111C",
|
||||||
|
"1-184S", "1-184S", "1-184S",
|
||||||
|
"1-185S", "1-185S"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus1_x_water_wind",
|
||||||
|
"name": "X Water/Wind",
|
||||||
|
"opus": "Opus I",
|
||||||
|
"elements": ["Water", "Wind"],
|
||||||
|
"description": "Tidus, Yuna, and the FFX cast",
|
||||||
|
"image": "starter_decks/opus1_x_water_wind.png",
|
||||||
|
"cards": [
|
||||||
|
"1-063R", "1-063R", "1-063R",
|
||||||
|
"1-067C", "1-067C", "1-067C",
|
||||||
|
"1-068C", "1-068C", "1-068C",
|
||||||
|
"1-080H", "1-080H", "1-080H",
|
||||||
|
"1-083H", "1-083H",
|
||||||
|
"1-176H",
|
||||||
|
"1-177R", "1-177R", "1-177R",
|
||||||
|
"1-160R", "1-160R", "1-160R",
|
||||||
|
"1-155C", "1-155C", "1-155C",
|
||||||
|
"1-163R", "1-163R", "1-163R",
|
||||||
|
"1-170C", "1-170C", "1-170C",
|
||||||
|
"1-171H",
|
||||||
|
"1-172C", "1-172C", "1-172C",
|
||||||
|
"1-159R", "1-159R", "1-159R",
|
||||||
|
"1-144C", "1-144C", "1-144C",
|
||||||
|
"1-175C", "1-175C", "1-175C"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus1_xiii_ice_lightning",
|
||||||
|
"name": "XIII Ice/Lightning",
|
||||||
|
"opus": "Opus I",
|
||||||
|
"elements": ["Ice", "Lightning"],
|
||||||
|
"description": "Lightning, Snow, and the FFXIII cast",
|
||||||
|
"image": "starter_decks/opus1_xiii_ice_lightning.png",
|
||||||
|
"cards": [
|
||||||
|
"1-038R", "1-038R", "1-038R",
|
||||||
|
"1-041R", "1-041R", "1-041R",
|
||||||
|
"1-043C", "1-043C", "1-043C",
|
||||||
|
"1-048R", "1-048R", "1-048R",
|
||||||
|
"1-181S", "1-181S", "1-181S",
|
||||||
|
"1-036R", "1-036R", "1-036R",
|
||||||
|
"1-033C", "1-033C", "1-033C",
|
||||||
|
"1-116R", "1-116R", "1-116R",
|
||||||
|
"1-121R", "1-121R", "1-121R",
|
||||||
|
"1-135R", "1-135R", "1-135R",
|
||||||
|
"1-136C", "1-136C", "1-136C",
|
||||||
|
"1-140R", "1-140R", "1-140R",
|
||||||
|
"1-141H",
|
||||||
|
"1-142H",
|
||||||
|
"1-183S", "1-183S", "1-183S"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus3_type0_wind_lightning",
|
||||||
|
"name": "Type-0 Wind/Lightning",
|
||||||
|
"opus": "Opus III",
|
||||||
|
"elements": ["Wind", "Lightning"],
|
||||||
|
"description": "Class Zero cadets from Type-0",
|
||||||
|
"image": "starter_decks/opus3_type0_wind_lightning.png",
|
||||||
|
"cards": [
|
||||||
|
"3-054R", "3-054R", "3-054R",
|
||||||
|
"3-056H", "3-056H",
|
||||||
|
"3-059C", "3-059C", "3-059C",
|
||||||
|
"3-060C", "3-060C", "3-060C",
|
||||||
|
"3-061R", "3-061R", "3-061R",
|
||||||
|
"3-062C", "3-062C", "3-062C",
|
||||||
|
"3-065C", "3-065C", "3-065C",
|
||||||
|
"3-118C", "3-118C", "3-118C",
|
||||||
|
"3-119C", "3-119C", "3-119C",
|
||||||
|
"3-120C", "3-120C", "3-120C",
|
||||||
|
"3-121R", "3-121R", "3-121R",
|
||||||
|
"3-124R", "3-124R", "3-124R",
|
||||||
|
"3-125R", "3-125R", "3-125R",
|
||||||
|
"3-127R", "3-127R", "3-127R"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus3_ix_fire_water",
|
||||||
|
"name": "IX Fire/Water",
|
||||||
|
"opus": "Opus III",
|
||||||
|
"elements": ["Fire", "Water"],
|
||||||
|
"description": "Zidane, Vivi, and the FFIX cast",
|
||||||
|
"image": "starter_decks/opus3_ix_fire_water.png",
|
||||||
|
"cards": [
|
||||||
|
"3-002R", "3-002R", "3-002R",
|
||||||
|
"3-003C", "3-003C", "3-003C",
|
||||||
|
"3-008C", "3-008C", "3-008C",
|
||||||
|
"3-012R", "3-012R", "3-012R",
|
||||||
|
"3-017C", "3-017C", "3-017C",
|
||||||
|
"3-018C", "3-018C", "3-018C",
|
||||||
|
"3-019R", "3-019R", "3-019R",
|
||||||
|
"3-130H",
|
||||||
|
"3-131R", "3-131R", "3-131R",
|
||||||
|
"3-139C", "3-139C", "3-139C",
|
||||||
|
"3-140C", "3-140C", "3-140C",
|
||||||
|
"3-141R", "3-141R", "3-141R",
|
||||||
|
"3-144L",
|
||||||
|
"3-148R", "3-148R", "3-148R",
|
||||||
|
"3-154C", "3-154C", "3-154C"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus5_xii_wind_water",
|
||||||
|
"name": "XII Wind/Water",
|
||||||
|
"opus": "Opus V",
|
||||||
|
"elements": ["Wind", "Water"],
|
||||||
|
"description": "Vaan, Ashe, and the FFXII cast",
|
||||||
|
"image": "starter_decks/opus5_xii_wind_water.png",
|
||||||
|
"cards": [
|
||||||
|
"5-062R", "5-062R", "5-062R",
|
||||||
|
"5-063H",
|
||||||
|
"5-064C", "5-064C", "5-064C",
|
||||||
|
"5-067C", "5-067C", "5-067C",
|
||||||
|
"5-068C", "5-068C", "5-068C",
|
||||||
|
"5-069C", "5-069C", "5-069C",
|
||||||
|
"5-070C", "5-070C", "5-070C",
|
||||||
|
"5-139L",
|
||||||
|
"5-140C", "5-140C", "5-140C",
|
||||||
|
"5-141C", "5-141C", "5-141C",
|
||||||
|
"5-142R", "5-142R", "5-142R",
|
||||||
|
"5-144R", "5-144R", "5-144R",
|
||||||
|
"5-145H",
|
||||||
|
"5-148C", "5-148C", "5-148C",
|
||||||
|
"5-156C", "5-156C", "5-156C",
|
||||||
|
"5-158C", "5-158C", "5-158C"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus5_xiii2_ice_fire",
|
||||||
|
"name": "XIII-2 Ice/Fire",
|
||||||
|
"opus": "Opus V",
|
||||||
|
"elements": ["Ice", "Fire"],
|
||||||
|
"description": "Noel, Serah, and the FFXIII-2 cast",
|
||||||
|
"image": "starter_decks/opus5_xiii2_ice_fire.png",
|
||||||
|
"cards": [
|
||||||
|
"5-019R", "5-019R", "5-019R",
|
||||||
|
"5-022R", "5-022R", "5-022R",
|
||||||
|
"5-024C", "5-024C", "5-024C",
|
||||||
|
"5-027C", "5-027C", "5-027C",
|
||||||
|
"5-029R", "5-029R", "5-029R",
|
||||||
|
"5-032R", "5-032R", "5-032R",
|
||||||
|
"5-034C", "5-034C", "5-034C",
|
||||||
|
"5-001C", "5-001C", "5-001C",
|
||||||
|
"5-002R", "5-002R", "5-002R",
|
||||||
|
"5-008H",
|
||||||
|
"5-010C", "5-010C", "5-010C",
|
||||||
|
"5-012R", "5-012R", "5-012R",
|
||||||
|
"5-015C", "5-015C", "5-015C",
|
||||||
|
"5-017C", "5-017C", "5-017C",
|
||||||
|
"5-018H"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opus5_xiv_earth_lightning",
|
||||||
|
"name": "XIV Earth/Lightning",
|
||||||
|
"opus": "Opus V",
|
||||||
|
"elements": ["Earth", "Lightning"],
|
||||||
|
"description": "Warriors of Light from FFXIV",
|
||||||
|
"image": "starter_decks/opus5_xiv_earth_lightning.png",
|
||||||
|
"cards": [
|
||||||
|
"5-075C", "5-075C", "5-075C",
|
||||||
|
"5-076C", "5-076C", "5-076C",
|
||||||
|
"5-078C", "5-078C", "5-078C",
|
||||||
|
"5-080R", "5-080R", "5-080R",
|
||||||
|
"5-085C", "5-085C", "5-085C",
|
||||||
|
"5-086R", "5-086R", "5-086R",
|
||||||
|
"5-091H",
|
||||||
|
"5-094C", "5-094C", "5-094C",
|
||||||
|
"5-096H",
|
||||||
|
"5-097C", "5-097C", "5-097C",
|
||||||
|
"5-099C", "5-099C", "5-099C",
|
||||||
|
"5-101R", "5-101R", "5-101R",
|
||||||
|
"5-103C", "5-103C", "5-103C",
|
||||||
|
"5-104R", "5-104R", "5-104R",
|
||||||
|
"5-112C", "5-112C", "5-112C"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ extends Node
|
|||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
MENU,
|
MENU,
|
||||||
|
DECK_BUILDER,
|
||||||
|
GAME_SETUP,
|
||||||
PLAYING,
|
PLAYING,
|
||||||
PAUSED
|
PAUSED
|
||||||
}
|
}
|
||||||
@@ -12,14 +14,25 @@ var current_state: State = State.MENU
|
|||||||
|
|
||||||
# Menu window size (matches project.godot viewport)
|
# Menu window size (matches project.godot viewport)
|
||||||
const MENU_SIZE := Vector2i(460, 689)
|
const MENU_SIZE := Vector2i(460, 689)
|
||||||
|
# Deck builder window size
|
||||||
|
const DECK_BUILDER_SIZE := Vector2i(1600, 900)
|
||||||
|
# Game setup window size
|
||||||
|
const GAME_SETUP_SIZE := Vector2i(800, 600)
|
||||||
# Game window size
|
# Game window size
|
||||||
const GAME_SIZE := Vector2i(2160, 980)
|
const GAME_SIZE := Vector2i(2160, 980)
|
||||||
|
|
||||||
# Scene references
|
# Scene references
|
||||||
var main_menu: MainMenu = null
|
var main_menu: MainMenu = null
|
||||||
|
var deck_builder: DeckBuilder = null
|
||||||
|
var game_setup_menu: GameSetupMenu = null
|
||||||
var game_scene: Node3D = null
|
var game_scene: Node3D = null
|
||||||
var pause_menu: PauseMenu = null
|
var pause_menu: PauseMenu = null
|
||||||
|
|
||||||
|
# Selected decks for gameplay
|
||||||
|
var selected_deck: Deck = null
|
||||||
|
var player1_deck: Array = [] # Card IDs for player 1
|
||||||
|
var player2_deck: Array = [] # Card IDs for player 2
|
||||||
|
|
||||||
# Preload the main game scene script
|
# Preload the main game scene script
|
||||||
const MainScript = preload("res://scripts/Main.gd")
|
const MainScript = preload("res://scripts/Main.gd")
|
||||||
|
|
||||||
@@ -31,6 +44,10 @@ func _input(event: InputEvent) -> void:
|
|||||||
if event is InputEventKey and event.pressed:
|
if event is InputEventKey and event.pressed:
|
||||||
if event.keycode == KEY_ESCAPE:
|
if event.keycode == KEY_ESCAPE:
|
||||||
match current_state:
|
match current_state:
|
||||||
|
State.DECK_BUILDER:
|
||||||
|
_on_deck_builder_back()
|
||||||
|
State.GAME_SETUP:
|
||||||
|
_on_game_setup_back()
|
||||||
State.PLAYING:
|
State.PLAYING:
|
||||||
_show_pause_menu()
|
_show_pause_menu()
|
||||||
State.PAUSED:
|
State.PAUSED:
|
||||||
@@ -56,12 +73,25 @@ func _show_main_menu() -> void:
|
|||||||
(screen.y - MENU_SIZE.y) / 2
|
(screen.y - MENU_SIZE.y) / 2
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Reset viewport to main menu size
|
||||||
|
get_tree().root.content_scale_size = MENU_SIZE
|
||||||
|
|
||||||
|
# Clean up deck builder if exists
|
||||||
|
if deck_builder:
|
||||||
|
deck_builder.queue_free()
|
||||||
|
deck_builder = null
|
||||||
|
|
||||||
|
# Clean up game setup menu if exists
|
||||||
|
if game_setup_menu:
|
||||||
|
game_setup_menu.queue_free()
|
||||||
|
game_setup_menu = null
|
||||||
|
|
||||||
# Create main menu
|
# Create main menu
|
||||||
if not main_menu:
|
if not main_menu:
|
||||||
main_menu = MainMenu.new()
|
main_menu = MainMenu.new()
|
||||||
add_child(main_menu)
|
add_child(main_menu)
|
||||||
main_menu.quick_play.connect(_on_start_game)
|
|
||||||
main_menu.play_game.connect(_on_start_game)
|
main_menu.play_game.connect(_on_start_game)
|
||||||
|
main_menu.deck_builder.connect(_on_deck_builder)
|
||||||
|
|
||||||
main_menu.visible = true
|
main_menu.visible = true
|
||||||
current_state = State.MENU
|
current_state = State.MENU
|
||||||
@@ -71,7 +101,82 @@ func _on_start_game() -> void:
|
|||||||
if main_menu:
|
if main_menu:
|
||||||
main_menu.visible = false
|
main_menu.visible = false
|
||||||
|
|
||||||
# Switch to windowed gameplay size
|
# Show game setup menu
|
||||||
|
_show_game_setup_menu()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_builder() -> void:
|
||||||
|
# Hide menu
|
||||||
|
if main_menu:
|
||||||
|
main_menu.visible = false
|
||||||
|
|
||||||
|
# Switch to deck builder window size
|
||||||
|
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
|
||||||
|
DisplayServer.window_set_size(DECK_BUILDER_SIZE)
|
||||||
|
var screen := DisplayServer.screen_get_size()
|
||||||
|
DisplayServer.window_set_position(Vector2i(
|
||||||
|
(screen.x - DECK_BUILDER_SIZE.x) / 2,
|
||||||
|
(screen.y - DECK_BUILDER_SIZE.y) / 2
|
||||||
|
))
|
||||||
|
|
||||||
|
# Set viewport to deck builder size
|
||||||
|
get_tree().root.content_scale_size = DECK_BUILDER_SIZE
|
||||||
|
|
||||||
|
# Create deck builder
|
||||||
|
if not deck_builder:
|
||||||
|
deck_builder = DeckBuilder.new()
|
||||||
|
add_child(deck_builder)
|
||||||
|
deck_builder.back_pressed.connect(_on_deck_builder_back)
|
||||||
|
deck_builder.deck_selected.connect(_on_deck_selected)
|
||||||
|
|
||||||
|
deck_builder.visible = true
|
||||||
|
current_state = State.DECK_BUILDER
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_builder_back() -> void:
|
||||||
|
if deck_builder:
|
||||||
|
deck_builder.visible = false
|
||||||
|
_show_main_menu()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_game_setup_menu() -> void:
|
||||||
|
# Switch to game setup window size (borderless like main menu)
|
||||||
|
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
|
||||||
|
DisplayServer.window_set_size(GAME_SETUP_SIZE)
|
||||||
|
var screen := DisplayServer.screen_get_size()
|
||||||
|
DisplayServer.window_set_position(Vector2i(
|
||||||
|
(screen.x - GAME_SETUP_SIZE.x) / 2,
|
||||||
|
(screen.y - GAME_SETUP_SIZE.y) / 2
|
||||||
|
))
|
||||||
|
|
||||||
|
# Resize the root viewport to match the window size to avoid letterboxing
|
||||||
|
get_tree().root.content_scale_size = GAME_SETUP_SIZE
|
||||||
|
|
||||||
|
# Create game setup menu
|
||||||
|
if not game_setup_menu:
|
||||||
|
game_setup_menu = GameSetupMenu.new()
|
||||||
|
add_child(game_setup_menu)
|
||||||
|
game_setup_menu.back_pressed.connect(_on_game_setup_back)
|
||||||
|
game_setup_menu.start_game_requested.connect(_on_game_setup_start)
|
||||||
|
|
||||||
|
game_setup_menu.visible = true
|
||||||
|
current_state = State.GAME_SETUP
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_setup_back() -> void:
|
||||||
|
if game_setup_menu:
|
||||||
|
game_setup_menu.visible = false
|
||||||
|
_show_main_menu()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_setup_start(p1_deck: Array, p2_deck: Array) -> void:
|
||||||
|
player1_deck = p1_deck
|
||||||
|
player2_deck = p2_deck
|
||||||
|
|
||||||
|
if game_setup_menu:
|
||||||
|
game_setup_menu.visible = false
|
||||||
|
|
||||||
|
# Switch to game window size
|
||||||
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
|
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
|
||||||
DisplayServer.window_set_size(GAME_SIZE)
|
DisplayServer.window_set_size(GAME_SIZE)
|
||||||
var screen := DisplayServer.screen_get_size()
|
var screen := DisplayServer.screen_get_size()
|
||||||
@@ -80,7 +185,29 @@ func _on_start_game() -> void:
|
|||||||
(screen.y - GAME_SIZE.y) / 2
|
(screen.y - GAME_SIZE.y) / 2
|
||||||
))
|
))
|
||||||
|
|
||||||
# Create game scene
|
# Set viewport to game size
|
||||||
|
get_tree().root.content_scale_size = GAME_SIZE
|
||||||
|
|
||||||
|
_start_new_game()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_selected(deck: Deck) -> void:
|
||||||
|
selected_deck = deck
|
||||||
|
if deck_builder:
|
||||||
|
deck_builder.visible = false
|
||||||
|
|
||||||
|
# Switch to game window size and start game
|
||||||
|
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
|
||||||
|
DisplayServer.window_set_size(GAME_SIZE)
|
||||||
|
var screen := DisplayServer.screen_get_size()
|
||||||
|
DisplayServer.window_set_position(Vector2i(
|
||||||
|
(screen.x - GAME_SIZE.x) / 2,
|
||||||
|
(screen.y - GAME_SIZE.y) / 2
|
||||||
|
))
|
||||||
|
|
||||||
|
# Set viewport to game size
|
||||||
|
get_tree().root.content_scale_size = GAME_SIZE
|
||||||
|
|
||||||
_start_new_game()
|
_start_new_game()
|
||||||
|
|
||||||
func _start_new_game() -> void:
|
func _start_new_game() -> void:
|
||||||
@@ -100,6 +227,13 @@ func _start_new_game() -> void:
|
|||||||
# Create new game scene
|
# Create new game scene
|
||||||
game_scene = Node3D.new()
|
game_scene = Node3D.new()
|
||||||
game_scene.set_script(MainScript)
|
game_scene.set_script(MainScript)
|
||||||
|
|
||||||
|
# Pass deck configurations if available
|
||||||
|
if player1_deck.size() > 0:
|
||||||
|
game_scene.player1_deck = player1_deck
|
||||||
|
if player2_deck.size() > 0:
|
||||||
|
game_scene.player2_deck = player2_deck
|
||||||
|
|
||||||
add_child(game_scene)
|
add_child(game_scene)
|
||||||
|
|
||||||
# Create pause menu
|
# Create pause menu
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ var action_log: ActionLog
|
|||||||
# Player damage displays
|
# Player damage displays
|
||||||
var damage_displays: Array[DamageDisplay] = []
|
var damage_displays: Array[DamageDisplay] = []
|
||||||
|
|
||||||
|
# Deck configurations (set by GameController before game starts)
|
||||||
|
var player1_deck: Array = []
|
||||||
|
var player2_deck: Array = []
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_setup_table()
|
_setup_table()
|
||||||
_setup_ui()
|
_setup_ui()
|
||||||
@@ -133,7 +137,7 @@ func _connect_signals() -> void:
|
|||||||
call_deferred("_connect_field_card_signals")
|
call_deferred("_connect_field_card_signals")
|
||||||
|
|
||||||
func _start_game() -> void:
|
func _start_game() -> void:
|
||||||
GameManager.start_new_game()
|
GameManager.start_new_game(player1_deck, player2_deck)
|
||||||
# Force an update of visuals after a frame to ensure everything is ready
|
# Force an update of visuals after a frame to ensure everything is ready
|
||||||
call_deferred("_force_initial_update")
|
call_deferred("_force_initial_update")
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ extends Node
|
|||||||
## Loads card definitions from JSON and provides lookup methods
|
## Loads card definitions from JSON and provides lookup methods
|
||||||
|
|
||||||
const CARDS_PATH = "res://data/cards.json"
|
const CARDS_PATH = "res://data/cards.json"
|
||||||
|
const STARTER_DECKS_PATH = "res://data/starter_decks.json"
|
||||||
|
|
||||||
# Loaded card data
|
# Loaded card data
|
||||||
var _cards: Dictionary = {} # id -> CardData
|
var _cards: Dictionary = {} # id -> CardData
|
||||||
var _cards_by_element: Dictionary = {} # Element -> Array[CardData]
|
var _cards_by_element: Dictionary = {} # Element -> Array[CardData]
|
||||||
var _cards_by_type: Dictionary = {} # CardType -> Array[CardData]
|
var _cards_by_type: Dictionary = {} # CardType -> Array[CardData]
|
||||||
var _card_textures: Dictionary = {} # id -> Texture2D
|
var _card_textures: Dictionary = {} # id -> Texture2D
|
||||||
|
var _starter_decks: Array = [] # Array of StarterDeckData
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
signal database_loaded
|
signal database_loaded
|
||||||
@@ -17,6 +19,7 @@ signal load_error(message: String)
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_load_database()
|
_load_database()
|
||||||
|
_load_starter_decks()
|
||||||
|
|
||||||
func _load_database() -> void:
|
func _load_database() -> void:
|
||||||
var file = FileAccess.open(CARDS_PATH, FileAccess.READ)
|
var file = FileAccess.open(CARDS_PATH, FileAccess.READ)
|
||||||
@@ -158,6 +161,137 @@ func get_cards_by_element(element: Enums.Element) -> Array:
|
|||||||
func get_cards_by_type(card_type: Enums.CardType) -> Array:
|
func get_cards_by_type(card_type: Enums.CardType) -> Array:
|
||||||
return _cards_by_type.get(card_type, [])
|
return _cards_by_type.get(card_type, [])
|
||||||
|
|
||||||
|
|
||||||
|
## Filter cards by multiple criteria
|
||||||
|
## filters: Dictionary with optional keys:
|
||||||
|
## - name: String (substring search, case-insensitive)
|
||||||
|
## - elements: Array[Enums.Element] (OR logic - card has any of these)
|
||||||
|
## - type: Enums.CardType (-1 or omit for any)
|
||||||
|
## - cost_min: int
|
||||||
|
## - cost_max: int
|
||||||
|
## - job: String (exact match, case-insensitive)
|
||||||
|
## - category: String (exact match)
|
||||||
|
## - power_min: int
|
||||||
|
## - power_max: int
|
||||||
|
## - ex_burst_only: bool
|
||||||
|
## - set: String (card ID prefix, e.g. "1-", "2-")
|
||||||
|
func filter_cards(filters: Dictionary) -> Array[CardData]:
|
||||||
|
var results: Array[CardData] = []
|
||||||
|
|
||||||
|
for card in _cards.values():
|
||||||
|
if _matches_filters(card, filters):
|
||||||
|
results.append(card)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
func _matches_filters(card: CardData, filters: Dictionary) -> bool:
|
||||||
|
# Name search (case-insensitive substring)
|
||||||
|
if filters.has("name") and not filters.name.is_empty():
|
||||||
|
if not card.name.to_lower().contains(filters.name.to_lower()):
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Element filter (OR logic for multi-select)
|
||||||
|
if filters.has("elements") and filters.elements.size() > 0:
|
||||||
|
var has_element = false
|
||||||
|
for elem in card.elements:
|
||||||
|
if elem in filters.elements:
|
||||||
|
has_element = true
|
||||||
|
break
|
||||||
|
if not has_element:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Type filter
|
||||||
|
if filters.has("type") and filters.type != -1:
|
||||||
|
if card.type != filters.type:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Cost filter (range)
|
||||||
|
if filters.has("cost_min") and card.cost < filters.cost_min:
|
||||||
|
return false
|
||||||
|
if filters.has("cost_max") and card.cost > filters.cost_max:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Job filter (case-insensitive)
|
||||||
|
if filters.has("job") and not filters.job.is_empty():
|
||||||
|
if card.job.to_lower() != filters.job.to_lower():
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Category filter
|
||||||
|
if filters.has("category") and not filters.category.is_empty():
|
||||||
|
if card.category != filters.category:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Power range
|
||||||
|
if filters.has("power_min") and card.power < filters.power_min:
|
||||||
|
return false
|
||||||
|
if filters.has("power_max") and card.power > filters.power_max:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# EX Burst filter
|
||||||
|
if filters.has("ex_burst_only") and filters.ex_burst_only:
|
||||||
|
if not card.has_ex_burst:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Set/Opus filter (card ID prefix)
|
||||||
|
if filters.has("set") and not filters.set.is_empty():
|
||||||
|
if not card.id.begins_with(filters.set):
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Get all unique job values from loaded cards
|
||||||
|
func get_unique_jobs() -> Array[String]:
|
||||||
|
var jobs: Dictionary = {}
|
||||||
|
for card in _cards.values():
|
||||||
|
if not card.job.is_empty():
|
||||||
|
jobs[card.job] = true
|
||||||
|
var result: Array[String] = []
|
||||||
|
for job in jobs.keys():
|
||||||
|
result.append(job)
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Get all unique category values from loaded cards
|
||||||
|
func get_unique_categories() -> Array[String]:
|
||||||
|
var categories: Dictionary = {}
|
||||||
|
for card in _cards.values():
|
||||||
|
if not card.category.is_empty():
|
||||||
|
categories[card.category] = true
|
||||||
|
var result: Array[String] = []
|
||||||
|
for category in categories.keys():
|
||||||
|
result.append(category)
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Get all unique set/opus prefixes from card IDs
|
||||||
|
func get_card_sets() -> Array[String]:
|
||||||
|
var sets: Dictionary = {}
|
||||||
|
for card in _cards.values():
|
||||||
|
# Extract prefix before first dash (e.g., "1" from "1-001H")
|
||||||
|
var dash_pos = card.id.find("-")
|
||||||
|
if dash_pos > 0:
|
||||||
|
var prefix = card.id.substr(0, dash_pos)
|
||||||
|
sets[prefix] = true
|
||||||
|
var result: Array[String] = []
|
||||||
|
for set_id in sets.keys():
|
||||||
|
result.append(set_id)
|
||||||
|
# Sort numerically if possible
|
||||||
|
result.sort_custom(func(a, b):
|
||||||
|
var a_num = a.to_int() if a.is_valid_int() else 9999
|
||||||
|
var b_num = b.to_int() if b.is_valid_int() else 9999
|
||||||
|
return a_num < b_num
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Get card count
|
||||||
|
func get_card_count() -> int:
|
||||||
|
return _cards.size()
|
||||||
|
|
||||||
## Get or load a card texture
|
## Get or load a card texture
|
||||||
func get_card_texture(card: CardData) -> Texture2D:
|
func get_card_texture(card: CardData) -> Texture2D:
|
||||||
if card.id in _card_textures:
|
if card.id in _card_textures:
|
||||||
@@ -234,8 +368,82 @@ func create_test_deck(player_index: int) -> Array[String]:
|
|||||||
return deck
|
return deck
|
||||||
|
|
||||||
|
|
||||||
|
## Starter Deck Methods
|
||||||
|
|
||||||
|
func _load_starter_decks() -> void:
|
||||||
|
var file = FileAccess.open(STARTER_DECKS_PATH, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
push_warning("Failed to open starter decks: " + STARTER_DECKS_PATH)
|
||||||
|
return
|
||||||
|
|
||||||
|
var json_text = file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
var json = JSON.new()
|
||||||
|
var error = json.parse(json_text)
|
||||||
|
if error != OK:
|
||||||
|
push_error("Failed to parse starter decks JSON: " + json.get_error_message())
|
||||||
|
return
|
||||||
|
|
||||||
|
var data = json.get_data()
|
||||||
|
if not data.has("starter_decks"):
|
||||||
|
push_error("Starter decks file missing 'starter_decks' array")
|
||||||
|
return
|
||||||
|
|
||||||
|
for deck_data in data["starter_decks"]:
|
||||||
|
var deck = StarterDeckData.new()
|
||||||
|
deck.id = deck_data.get("id", "")
|
||||||
|
deck.name = deck_data.get("name", "")
|
||||||
|
deck.opus = deck_data.get("opus", "")
|
||||||
|
deck.description = deck_data.get("description", "")
|
||||||
|
deck.elements = deck_data.get("elements", [])
|
||||||
|
deck.cards = deck_data.get("cards", [])
|
||||||
|
deck.image = deck_data.get("image", "")
|
||||||
|
_starter_decks.append(deck)
|
||||||
|
|
||||||
|
print("CardDatabase: Loaded ", _starter_decks.size(), " starter decks")
|
||||||
|
|
||||||
|
|
||||||
|
## Get all starter decks
|
||||||
|
func get_starter_decks() -> Array:
|
||||||
|
return _starter_decks
|
||||||
|
|
||||||
|
|
||||||
|
## Get a starter deck by ID
|
||||||
|
func get_starter_deck(deck_id: String) -> StarterDeckData:
|
||||||
|
for deck in _starter_decks:
|
||||||
|
if deck.id == deck_id:
|
||||||
|
return deck
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Get a random starter deck
|
||||||
|
func get_random_starter_deck() -> StarterDeckData:
|
||||||
|
if _starter_decks.is_empty():
|
||||||
|
return null
|
||||||
|
return _starter_decks[randi() % _starter_decks.size()]
|
||||||
|
|
||||||
|
|
||||||
## Data Classes
|
## Data Classes
|
||||||
|
|
||||||
|
class StarterDeckData:
|
||||||
|
var id: String = ""
|
||||||
|
var name: String = ""
|
||||||
|
var opus: String = ""
|
||||||
|
var description: String = ""
|
||||||
|
var elements: Array = [] # Array of element name strings
|
||||||
|
var cards: Array = [] # Array of card IDs
|
||||||
|
var image: String = "" # Path to box art image
|
||||||
|
|
||||||
|
func get_texture() -> Texture2D:
|
||||||
|
if image.is_empty():
|
||||||
|
return null
|
||||||
|
var texture_path = "res://assets/ui/" + image
|
||||||
|
if ResourceLoader.exists(texture_path):
|
||||||
|
return load(texture_path)
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
class CardData:
|
class CardData:
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
var name: String = ""
|
var name: String = ""
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ func _on_database_loaded() -> void:
|
|||||||
print("GameManager: Ready")
|
print("GameManager: Ready")
|
||||||
|
|
||||||
## Start a new game
|
## Start a new game
|
||||||
func start_new_game() -> void:
|
## deck1 and deck2 are optional arrays of card IDs
|
||||||
|
## If empty, test decks will be created
|
||||||
|
func start_new_game(deck1: Array = [], deck2: Array = []) -> void:
|
||||||
if not is_initialized:
|
if not is_initialized:
|
||||||
push_error("GameManager not initialized")
|
push_error("GameManager not initialized")
|
||||||
return
|
return
|
||||||
@@ -68,12 +70,24 @@ func start_new_game() -> void:
|
|||||||
# Connect signals
|
# Connect signals
|
||||||
_connect_game_signals()
|
_connect_game_signals()
|
||||||
|
|
||||||
# Create test decks
|
# Use provided decks or create test decks
|
||||||
var deck1 = CardDatabase.create_test_deck(0)
|
var player1_deck: Array[String] = []
|
||||||
var deck2 = CardDatabase.create_test_deck(1)
|
var player2_deck: Array[String] = []
|
||||||
|
|
||||||
|
if deck1.is_empty():
|
||||||
|
player1_deck = CardDatabase.create_test_deck(0)
|
||||||
|
else:
|
||||||
|
for card_id in deck1:
|
||||||
|
player1_deck.append(card_id)
|
||||||
|
|
||||||
|
if deck2.is_empty():
|
||||||
|
player2_deck = CardDatabase.create_test_deck(1)
|
||||||
|
else:
|
||||||
|
for card_id in deck2:
|
||||||
|
player2_deck.append(card_id)
|
||||||
|
|
||||||
# Setup and start
|
# Setup and start
|
||||||
game_state.setup_game(deck1, deck2)
|
game_state.setup_game(player1_deck, player2_deck)
|
||||||
game_state.start_game()
|
game_state.start_game()
|
||||||
|
|
||||||
is_game_active = true
|
is_game_active = true
|
||||||
|
|||||||
165
scripts/data/Deck.gd
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
class_name Deck
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Deck - Data model for a player's deck
|
||||||
|
|
||||||
|
signal deck_changed
|
||||||
|
|
||||||
|
const MIN_CARDS: int = 50
|
||||||
|
const MAX_CARDS: int = 50
|
||||||
|
const MAX_COPIES: int = 3
|
||||||
|
|
||||||
|
var name: String = "New Deck"
|
||||||
|
var cards: Dictionary = {} # card_id -> count
|
||||||
|
|
||||||
|
|
||||||
|
## Add a card to the deck
|
||||||
|
## Returns empty string on success, error message on failure
|
||||||
|
func add_card(card_id: String) -> String:
|
||||||
|
var current_count = cards.get(card_id, 0)
|
||||||
|
if current_count >= MAX_COPIES:
|
||||||
|
return "Maximum %d copies allowed" % MAX_COPIES
|
||||||
|
|
||||||
|
var total = get_total_cards()
|
||||||
|
if total >= MAX_CARDS:
|
||||||
|
return "Deck is full (%d cards)" % MAX_CARDS
|
||||||
|
|
||||||
|
cards[card_id] = current_count + 1
|
||||||
|
deck_changed.emit()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
## Remove a card from the deck
|
||||||
|
## Returns true if successful
|
||||||
|
func remove_card(card_id: String) -> bool:
|
||||||
|
if not cards.has(card_id):
|
||||||
|
return false
|
||||||
|
|
||||||
|
cards[card_id] -= 1
|
||||||
|
if cards[card_id] <= 0:
|
||||||
|
cards.erase(card_id)
|
||||||
|
|
||||||
|
deck_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Set card count directly
|
||||||
|
func set_card_count(card_id: String, count: int) -> void:
|
||||||
|
if count <= 0:
|
||||||
|
cards.erase(card_id)
|
||||||
|
else:
|
||||||
|
cards[card_id] = mini(count, MAX_COPIES)
|
||||||
|
deck_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Get total number of cards in deck
|
||||||
|
func get_total_cards() -> int:
|
||||||
|
var total = 0
|
||||||
|
for count in cards.values():
|
||||||
|
total += count
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
## Get count for a specific card
|
||||||
|
func get_card_count(card_id: String) -> int:
|
||||||
|
return cards.get(card_id, 0)
|
||||||
|
|
||||||
|
|
||||||
|
## Get all unique card IDs in deck
|
||||||
|
func get_card_ids() -> Array[String]:
|
||||||
|
var ids: Array[String] = []
|
||||||
|
for card_id in cards.keys():
|
||||||
|
ids.append(card_id)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
## Validate the deck
|
||||||
|
## Returns array of error messages (empty if valid)
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
var total = get_total_cards()
|
||||||
|
|
||||||
|
if total < MIN_CARDS:
|
||||||
|
errors.append("Deck needs %d more cards" % (MIN_CARDS - total))
|
||||||
|
elif total > MAX_CARDS:
|
||||||
|
errors.append("Deck has %d too many cards" % (total - MAX_CARDS))
|
||||||
|
|
||||||
|
for card_id in cards:
|
||||||
|
if cards[card_id] > MAX_COPIES:
|
||||||
|
var card_data = CardDatabase.get_card(card_id)
|
||||||
|
var card_name = card_data.name if card_data else card_id
|
||||||
|
errors.append("%s has too many copies (%d)" % [card_name, cards[card_id]])
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
## Check if deck is valid
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return validate().size() == 0
|
||||||
|
|
||||||
|
|
||||||
|
## Convert deck to array of card IDs (for gameplay)
|
||||||
|
func to_card_array() -> Array[String]:
|
||||||
|
var result: Array[String] = []
|
||||||
|
for card_id in cards:
|
||||||
|
for i in range(cards[card_id]):
|
||||||
|
result.append(card_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Clear the deck
|
||||||
|
func clear() -> void:
|
||||||
|
cards.clear()
|
||||||
|
deck_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Get deck statistics
|
||||||
|
func get_stats() -> Dictionary:
|
||||||
|
var stats = {
|
||||||
|
"total": get_total_cards(),
|
||||||
|
"unique": cards.size(),
|
||||||
|
"elements": {},
|
||||||
|
"types": {},
|
||||||
|
"cost_curve": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for card_id in cards:
|
||||||
|
var count = cards[card_id]
|
||||||
|
var card_data = CardDatabase.get_card(card_id)
|
||||||
|
if not card_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Element breakdown
|
||||||
|
for element in card_data.elements:
|
||||||
|
var elem_name = Enums.element_to_string(element)
|
||||||
|
stats.elements[elem_name] = stats.elements.get(elem_name, 0) + count
|
||||||
|
|
||||||
|
# Type breakdown
|
||||||
|
var type_name = Enums.card_type_to_string(card_data.type)
|
||||||
|
stats.types[type_name] = stats.types.get(type_name, 0) + count
|
||||||
|
|
||||||
|
# Cost curve
|
||||||
|
var cost_key = str(card_data.cost)
|
||||||
|
stats.cost_curve[cost_key] = stats.cost_curve.get(cost_key, 0) + count
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
## Serialize deck to dictionary (for saving)
|
||||||
|
func to_dict() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"cards": cards.duplicate(),
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Load deck from dictionary
|
||||||
|
func from_dict(data: Dictionary) -> void:
|
||||||
|
name = data.get("name", "Unnamed Deck")
|
||||||
|
cards = data.get("cards", {}).duplicate()
|
||||||
|
deck_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _to_string() -> String:
|
||||||
|
return "[Deck: %s (%d cards)]" % [name, get_total_cards()]
|
||||||
126
scripts/data/DeckManager.gd
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
class_name DeckManager
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## DeckManager - Handles deck persistence (save/load)
|
||||||
|
|
||||||
|
const DECKS_DIR = "user://decks/"
|
||||||
|
|
||||||
|
|
||||||
|
## Save a deck to file
|
||||||
|
## Returns true on success
|
||||||
|
static func save_deck(deck: Deck, filename: String) -> bool:
|
||||||
|
# Ensure directory exists
|
||||||
|
DirAccess.make_dir_recursive_absolute(DECKS_DIR)
|
||||||
|
|
||||||
|
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||||
|
var file = FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if not file:
|
||||||
|
push_error("DeckManager: Failed to open file for writing: " + path)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var data = deck.to_dict()
|
||||||
|
file.store_string(JSON.stringify(data, "\t"))
|
||||||
|
file.close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Load a deck from file
|
||||||
|
## Returns null on failure
|
||||||
|
static func load_deck(filename: String) -> Deck:
|
||||||
|
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
push_error("DeckManager: File not found: " + path)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
push_error("DeckManager: Failed to open file for reading: " + path)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var json_text = file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
var json = JSON.new()
|
||||||
|
var error = json.parse(json_text)
|
||||||
|
if error != OK:
|
||||||
|
push_error("DeckManager: JSON parse error: " + json.get_error_message())
|
||||||
|
return null
|
||||||
|
|
||||||
|
var data = json.get_data()
|
||||||
|
if not data is Dictionary:
|
||||||
|
push_error("DeckManager: Invalid deck data format")
|
||||||
|
return null
|
||||||
|
|
||||||
|
var deck = Deck.new()
|
||||||
|
deck.from_dict(data)
|
||||||
|
return deck
|
||||||
|
|
||||||
|
|
||||||
|
## Delete a deck file
|
||||||
|
## Returns true on success
|
||||||
|
static func delete_deck(filename: String) -> bool:
|
||||||
|
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var dir = DirAccess.open(DECKS_DIR)
|
||||||
|
if dir:
|
||||||
|
return dir.remove(_sanitize_filename(filename) + ".json") == OK
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## List all saved decks
|
||||||
|
## Returns array of deck names (without .json extension)
|
||||||
|
static func list_decks() -> Array[String]:
|
||||||
|
var decks: Array[String] = []
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
DirAccess.make_dir_recursive_absolute(DECKS_DIR)
|
||||||
|
|
||||||
|
var dir = DirAccess.open(DECKS_DIR)
|
||||||
|
if not dir:
|
||||||
|
return decks
|
||||||
|
|
||||||
|
dir.list_dir_begin()
|
||||||
|
var filename = dir.get_next()
|
||||||
|
while filename != "":
|
||||||
|
if not dir.current_is_dir() and filename.ends_with(".json"):
|
||||||
|
decks.append(filename.trim_suffix(".json"))
|
||||||
|
filename = dir.get_next()
|
||||||
|
dir.list_dir_end()
|
||||||
|
|
||||||
|
decks.sort()
|
||||||
|
return decks
|
||||||
|
|
||||||
|
|
||||||
|
## Check if a deck exists
|
||||||
|
static func deck_exists(filename: String) -> bool:
|
||||||
|
var path = DECKS_DIR + _sanitize_filename(filename) + ".json"
|
||||||
|
return FileAccess.file_exists(path)
|
||||||
|
|
||||||
|
|
||||||
|
## Generate a unique deck name
|
||||||
|
static func generate_unique_name(base_name: String = "New Deck") -> String:
|
||||||
|
var name = base_name
|
||||||
|
var counter = 1
|
||||||
|
|
||||||
|
while deck_exists(name):
|
||||||
|
counter += 1
|
||||||
|
name = "%s %d" % [base_name, counter]
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
## Sanitize filename to prevent path traversal
|
||||||
|
static func _sanitize_filename(filename: String) -> String:
|
||||||
|
# Remove path separators and dangerous characters
|
||||||
|
var sanitized = filename.replace("/", "_").replace("\\", "_")
|
||||||
|
sanitized = sanitized.replace("..", "_").replace(":", "_")
|
||||||
|
# Trim whitespace
|
||||||
|
sanitized = sanitized.strip_edges()
|
||||||
|
# Ensure not empty
|
||||||
|
if sanitized.is_empty():
|
||||||
|
sanitized = "deck"
|
||||||
|
return sanitized
|
||||||
@@ -121,6 +121,15 @@ static func card_type_to_string(t: CardType) -> String:
|
|||||||
CardType.MONSTER: return "Monster"
|
CardType.MONSTER: return "Monster"
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
## Helper functions for AbilityType
|
||||||
|
static func ability_type_to_string(t: AbilityType) -> String:
|
||||||
|
match t:
|
||||||
|
AbilityType.FIELD: return "FIELD"
|
||||||
|
AbilityType.AUTO: return "AUTO"
|
||||||
|
AbilityType.ACTION: return "ACTION"
|
||||||
|
AbilityType.SPECIAL: return "SPECIAL"
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
## Helper functions for TurnPhase
|
## Helper functions for TurnPhase
|
||||||
static func phase_to_string(p: TurnPhase) -> String:
|
static func phase_to_string(p: TurnPhase) -> String:
|
||||||
match p:
|
match p:
|
||||||
|
|||||||
355
scripts/ui/CardDetailViewer.gd
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
class_name CardDetailViewer
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## CardDetailViewer - Left panel showing enlarged card with details and add-to-deck controls
|
||||||
|
|
||||||
|
signal add_to_deck_requested(card: CardDatabase.CardData, quantity: int)
|
||||||
|
signal card_info_requested(card: CardDatabase.CardData)
|
||||||
|
|
||||||
|
const CARD_WIDTH: float = 405.0
|
||||||
|
const CARD_HEIGHT: float = 567.0
|
||||||
|
const PANEL_WIDTH: float = 450.0
|
||||||
|
|
||||||
|
var current_card: CardDatabase.CardData = null
|
||||||
|
var current_deck_count: int = 0
|
||||||
|
var quantity_to_add: int = 1
|
||||||
|
|
||||||
|
# UI elements
|
||||||
|
var card_image: TextureRect
|
||||||
|
var fallback_rect: ColorRect
|
||||||
|
var fallback_label: Label
|
||||||
|
var name_label: Label
|
||||||
|
var type_label: Label
|
||||||
|
var element_label: Label
|
||||||
|
var cost_label: Label
|
||||||
|
var power_label: Label
|
||||||
|
var job_label: Label
|
||||||
|
var category_label: Label
|
||||||
|
var abilities_label: Label
|
||||||
|
var quantity_label: Label
|
||||||
|
var decrease_btn: Button
|
||||||
|
var increase_btn: Button
|
||||||
|
var add_button: Button
|
||||||
|
var no_card_label: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
custom_minimum_size = Vector2(PANEL_WIDTH, 0)
|
||||||
|
_create_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
# Main panel
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
panel.add_theme_stylebox_override("panel", _create_panel_style())
|
||||||
|
add_child(panel)
|
||||||
|
|
||||||
|
var main_vbox = VBoxContainer.new()
|
||||||
|
main_vbox.add_theme_constant_override("separation", 12)
|
||||||
|
panel.add_child(main_vbox)
|
||||||
|
|
||||||
|
# Card image container
|
||||||
|
var image_container = Control.new()
|
||||||
|
image_container.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||||
|
main_vbox.add_child(image_container)
|
||||||
|
|
||||||
|
# Actual card image
|
||||||
|
card_image = TextureRect.new()
|
||||||
|
card_image.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
card_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||||
|
card_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
image_container.add_child(card_image)
|
||||||
|
|
||||||
|
# Fallback colored rect (when no image)
|
||||||
|
fallback_rect = ColorRect.new()
|
||||||
|
fallback_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
fallback_rect.visible = false
|
||||||
|
image_container.add_child(fallback_rect)
|
||||||
|
|
||||||
|
fallback_label = Label.new()
|
||||||
|
fallback_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
fallback_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
fallback_label.visible = false
|
||||||
|
image_container.add_child(fallback_label)
|
||||||
|
|
||||||
|
# No card selected label
|
||||||
|
no_card_label = Label.new()
|
||||||
|
no_card_label.text = "Select a card to view details"
|
||||||
|
no_card_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
no_card_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
no_card_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||||
|
image_container.add_child(no_card_label)
|
||||||
|
|
||||||
|
# Card info section
|
||||||
|
var info_vbox = VBoxContainer.new()
|
||||||
|
info_vbox.add_theme_constant_override("separation", 4)
|
||||||
|
main_vbox.add_child(info_vbox)
|
||||||
|
|
||||||
|
name_label = _create_info_label("", 20, Color(1.0, 0.95, 0.8))
|
||||||
|
info_vbox.add_child(name_label)
|
||||||
|
|
||||||
|
var details_grid = GridContainer.new()
|
||||||
|
details_grid.columns = 2
|
||||||
|
details_grid.add_theme_constant_override("h_separation", 12)
|
||||||
|
details_grid.add_theme_constant_override("v_separation", 4)
|
||||||
|
info_vbox.add_child(details_grid)
|
||||||
|
|
||||||
|
type_label = _create_info_label("")
|
||||||
|
cost_label = _create_info_label("")
|
||||||
|
element_label = _create_info_label("")
|
||||||
|
power_label = _create_info_label("")
|
||||||
|
job_label = _create_info_label("")
|
||||||
|
category_label = _create_info_label("")
|
||||||
|
|
||||||
|
details_grid.add_child(_create_info_label("Type:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(type_label)
|
||||||
|
details_grid.add_child(_create_info_label("Cost:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(cost_label)
|
||||||
|
details_grid.add_child(_create_info_label("Element:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(element_label)
|
||||||
|
details_grid.add_child(_create_info_label("Power:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(power_label)
|
||||||
|
details_grid.add_child(_create_info_label("Job:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(job_label)
|
||||||
|
details_grid.add_child(_create_info_label("Category:", 14, Color(0.6, 0.6, 0.6)))
|
||||||
|
details_grid.add_child(category_label)
|
||||||
|
|
||||||
|
# Abilities section
|
||||||
|
var abilities_header = _create_info_label("Abilities", 16, Color(0.8, 0.75, 0.6))
|
||||||
|
info_vbox.add_child(abilities_header)
|
||||||
|
|
||||||
|
var abilities_scroll = ScrollContainer.new()
|
||||||
|
abilities_scroll.custom_minimum_size = Vector2(0, 80)
|
||||||
|
abilities_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
info_vbox.add_child(abilities_scroll)
|
||||||
|
|
||||||
|
abilities_label = Label.new()
|
||||||
|
abilities_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
abilities_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
abilities_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||||
|
abilities_scroll.add_child(abilities_label)
|
||||||
|
|
||||||
|
# Quantity selector
|
||||||
|
var qty_hbox = HBoxContainer.new()
|
||||||
|
qty_hbox.add_theme_constant_override("separation", 10)
|
||||||
|
qty_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
main_vbox.add_child(qty_hbox)
|
||||||
|
|
||||||
|
quantity_label = Label.new()
|
||||||
|
quantity_label.text = "In deck: 0/3"
|
||||||
|
quantity_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
qty_hbox.add_child(quantity_label)
|
||||||
|
|
||||||
|
decrease_btn = _create_quantity_button("-")
|
||||||
|
decrease_btn.pressed.connect(_on_decrease_quantity)
|
||||||
|
qty_hbox.add_child(decrease_btn)
|
||||||
|
|
||||||
|
var qty_display = Label.new()
|
||||||
|
qty_display.text = "1"
|
||||||
|
qty_display.name = "QtyDisplay"
|
||||||
|
qty_display.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
qty_display.custom_minimum_size = Vector2(30, 0)
|
||||||
|
qty_hbox.add_child(qty_display)
|
||||||
|
|
||||||
|
increase_btn = _create_quantity_button("+")
|
||||||
|
increase_btn.pressed.connect(_on_increase_quantity)
|
||||||
|
qty_hbox.add_child(increase_btn)
|
||||||
|
|
||||||
|
# Add to deck button
|
||||||
|
add_button = _create_styled_button("Add to Deck")
|
||||||
|
add_button.pressed.connect(_on_add_to_deck)
|
||||||
|
main_vbox.add_child(add_button)
|
||||||
|
|
||||||
|
# Initial state
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_panel_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.08, 0.08, 0.12, 0.95)
|
||||||
|
style.border_color = Color(0.5, 0.4, 0.2)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(6)
|
||||||
|
style.content_margin_left = 15
|
||||||
|
style.content_margin_right = 15
|
||||||
|
style.content_margin_top = 15
|
||||||
|
style.content_margin_bottom = 15
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _create_info_label(text: String, font_size: int = 14, color: Color = Color(0.9, 0.9, 0.9)) -> Label:
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = text
|
||||||
|
label.add_theme_font_size_override("font_size", font_size)
|
||||||
|
label.add_theme_color_override("font_color", color)
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
func _create_quantity_button(text: String) -> Button:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = text
|
||||||
|
button.custom_minimum_size = Vector2(36, 36)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = Color(0.2, 0.2, 0.25)
|
||||||
|
style_normal.border_color = Color(0.4, 0.4, 0.5)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = Color(0.3, 0.3, 0.4)
|
||||||
|
style_hover.border_color = Color(0.6, 0.5, 0.3)
|
||||||
|
style_hover.set_border_width_all(1)
|
||||||
|
style_hover.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 18)
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
func _create_styled_button(text: String) -> Button:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = text
|
||||||
|
button.custom_minimum_size = Vector2(0, 44)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = Color(0.25, 0.25, 0.3)
|
||||||
|
style_normal.border_color = Color(0.5, 0.5, 0.6)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(5)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = Color(0.35, 0.35, 0.45)
|
||||||
|
style_hover.border_color = Color(0.7, 0.6, 0.3)
|
||||||
|
style_hover.set_border_width_all(2)
|
||||||
|
style_hover.set_corner_radius_all(5)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
var style_disabled = StyleBoxFlat.new()
|
||||||
|
style_disabled.bg_color = Color(0.15, 0.15, 0.18)
|
||||||
|
style_disabled.border_color = Color(0.3, 0.3, 0.35)
|
||||||
|
style_disabled.set_border_width_all(1)
|
||||||
|
style_disabled.set_corner_radius_all(5)
|
||||||
|
button.add_theme_stylebox_override("disabled", style_disabled)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 16)
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
## Show a card in the detail viewer
|
||||||
|
func show_card(card: CardDatabase.CardData, deck_count: int = 0) -> void:
|
||||||
|
current_card = card
|
||||||
|
current_deck_count = deck_count
|
||||||
|
quantity_to_add = 1
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
## Update deck count for current card
|
||||||
|
func update_deck_count(count: int) -> void:
|
||||||
|
current_deck_count = count
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
## Clear the detail viewer
|
||||||
|
func clear() -> void:
|
||||||
|
current_card = null
|
||||||
|
current_deck_count = 0
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_ui_state() -> void:
|
||||||
|
var has_card = current_card != null
|
||||||
|
no_card_label.visible = not has_card
|
||||||
|
card_image.visible = has_card
|
||||||
|
add_button.disabled = not has_card
|
||||||
|
|
||||||
|
if not has_card:
|
||||||
|
name_label.text = ""
|
||||||
|
type_label.text = ""
|
||||||
|
cost_label.text = ""
|
||||||
|
element_label.text = ""
|
||||||
|
power_label.text = ""
|
||||||
|
job_label.text = ""
|
||||||
|
category_label.text = ""
|
||||||
|
abilities_label.text = ""
|
||||||
|
quantity_label.text = "In deck: 0/3"
|
||||||
|
fallback_rect.visible = false
|
||||||
|
fallback_label.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load card image
|
||||||
|
var texture = CardDatabase.get_card_texture(current_card)
|
||||||
|
if texture:
|
||||||
|
card_image.texture = texture
|
||||||
|
card_image.visible = true
|
||||||
|
fallback_rect.visible = false
|
||||||
|
fallback_label.visible = false
|
||||||
|
else:
|
||||||
|
card_image.visible = false
|
||||||
|
fallback_rect.visible = true
|
||||||
|
fallback_label.visible = true
|
||||||
|
fallback_rect.color = Enums.element_to_color(current_card.get_primary_element())
|
||||||
|
fallback_label.text = current_card.name
|
||||||
|
|
||||||
|
# Update info labels
|
||||||
|
name_label.text = current_card.name
|
||||||
|
type_label.text = Enums.card_type_to_string(current_card.type)
|
||||||
|
cost_label.text = str(current_card.cost)
|
||||||
|
|
||||||
|
var elements_str = ""
|
||||||
|
for i in range(current_card.elements.size()):
|
||||||
|
if i > 0:
|
||||||
|
elements_str += " / "
|
||||||
|
elements_str += Enums.element_to_string(current_card.elements[i])
|
||||||
|
element_label.text = elements_str
|
||||||
|
|
||||||
|
power_label.text = str(current_card.power) if current_card.power > 0 else "-"
|
||||||
|
job_label.text = current_card.job if not current_card.job.is_empty() else "-"
|
||||||
|
category_label.text = current_card.category if not current_card.category.is_empty() else "-"
|
||||||
|
|
||||||
|
# Update abilities
|
||||||
|
var abilities_text = ""
|
||||||
|
for ability in current_card.abilities:
|
||||||
|
if not abilities_text.is_empty():
|
||||||
|
abilities_text += "\n\n"
|
||||||
|
var ability_type = Enums.ability_type_to_string(ability.type)
|
||||||
|
abilities_text += "[%s]" % ability_type
|
||||||
|
if not ability.trigger.is_empty():
|
||||||
|
abilities_text += " %s:" % ability.trigger
|
||||||
|
abilities_text += " %s" % ability.effect
|
||||||
|
abilities_label.text = abilities_text if not abilities_text.is_empty() else "No abilities"
|
||||||
|
|
||||||
|
# Update quantity display
|
||||||
|
var max_addable = Deck.MAX_COPIES - current_deck_count
|
||||||
|
quantity_label.text = "In deck: %d/%d" % [current_deck_count, Deck.MAX_COPIES]
|
||||||
|
|
||||||
|
var qty_display = get_node_or_null("PanelContainer/VBoxContainer/HBoxContainer/QtyDisplay")
|
||||||
|
if qty_display:
|
||||||
|
qty_display.text = str(quantity_to_add)
|
||||||
|
|
||||||
|
decrease_btn.disabled = quantity_to_add <= 1
|
||||||
|
increase_btn.disabled = quantity_to_add >= max_addable or max_addable <= 0
|
||||||
|
add_button.disabled = max_addable <= 0
|
||||||
|
|
||||||
|
|
||||||
|
func _on_decrease_quantity() -> void:
|
||||||
|
if quantity_to_add > 1:
|
||||||
|
quantity_to_add -= 1
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_increase_quantity() -> void:
|
||||||
|
var max_addable = Deck.MAX_COPIES - current_deck_count
|
||||||
|
if quantity_to_add < max_addable:
|
||||||
|
quantity_to_add += 1
|
||||||
|
_update_ui_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_to_deck() -> void:
|
||||||
|
if current_card:
|
||||||
|
add_to_deck_requested.emit(current_card, quantity_to_add)
|
||||||
432
scripts/ui/CardFilterBar.gd
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
class_name CardFilterBar
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## CardFilterBar - Filter controls for the deck builder card grid
|
||||||
|
|
||||||
|
signal filters_changed(filters: Dictionary)
|
||||||
|
|
||||||
|
const FILTER_BAR_HEIGHT: float = 120.0
|
||||||
|
const EXPANDED_HEIGHT: float = 200.0
|
||||||
|
|
||||||
|
var current_filters: Dictionary = {}
|
||||||
|
var is_expanded: bool = false
|
||||||
|
|
||||||
|
# UI elements
|
||||||
|
var search_field: LineEdit
|
||||||
|
var element_buttons: Dictionary = {} # Enums.Element -> Button
|
||||||
|
var type_dropdown: OptionButton
|
||||||
|
var cost_slider: HSlider
|
||||||
|
var cost_label: Label
|
||||||
|
var expand_button: Button
|
||||||
|
var expanded_container: Control
|
||||||
|
|
||||||
|
# Expanded filter elements
|
||||||
|
var job_dropdown: OptionButton
|
||||||
|
var category_dropdown: OptionButton
|
||||||
|
var power_min_spin: SpinBox
|
||||||
|
var power_max_spin: SpinBox
|
||||||
|
var ex_burst_check: CheckBox
|
||||||
|
var set_dropdown: OptionButton
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
custom_minimum_size = Vector2(0, FILTER_BAR_HEIGHT)
|
||||||
|
_create_ui()
|
||||||
|
_populate_dropdowns()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
panel.add_theme_stylebox_override("panel", _create_panel_style())
|
||||||
|
add_child(panel)
|
||||||
|
|
||||||
|
var main_vbox = VBoxContainer.new()
|
||||||
|
main_vbox.add_theme_constant_override("separation", 8)
|
||||||
|
panel.add_child(main_vbox)
|
||||||
|
|
||||||
|
# Row 1: Search and type
|
||||||
|
var row1 = HBoxContainer.new()
|
||||||
|
row1.add_theme_constant_override("separation", 12)
|
||||||
|
main_vbox.add_child(row1)
|
||||||
|
|
||||||
|
# Search field
|
||||||
|
var search_label = Label.new()
|
||||||
|
search_label.text = "Search:"
|
||||||
|
search_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
row1.add_child(search_label)
|
||||||
|
|
||||||
|
search_field = LineEdit.new()
|
||||||
|
search_field.placeholder_text = "Card name..."
|
||||||
|
search_field.custom_minimum_size = Vector2(180, 0)
|
||||||
|
search_field.text_changed.connect(_on_search_changed)
|
||||||
|
row1.add_child(search_field)
|
||||||
|
|
||||||
|
# Type dropdown
|
||||||
|
var type_label = Label.new()
|
||||||
|
type_label.text = "Type:"
|
||||||
|
type_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
row1.add_child(type_label)
|
||||||
|
|
||||||
|
type_dropdown = OptionButton.new()
|
||||||
|
type_dropdown.custom_minimum_size = Vector2(100, 0)
|
||||||
|
type_dropdown.add_item("All", -1)
|
||||||
|
type_dropdown.add_item("Forward", Enums.CardType.FORWARD)
|
||||||
|
type_dropdown.add_item("Backup", Enums.CardType.BACKUP)
|
||||||
|
type_dropdown.add_item("Summon", Enums.CardType.SUMMON)
|
||||||
|
type_dropdown.add_item("Monster", Enums.CardType.MONSTER)
|
||||||
|
type_dropdown.item_selected.connect(_on_type_selected)
|
||||||
|
row1.add_child(type_dropdown)
|
||||||
|
|
||||||
|
# Cost slider
|
||||||
|
var cost_container = HBoxContainer.new()
|
||||||
|
cost_container.add_theme_constant_override("separation", 8)
|
||||||
|
row1.add_child(cost_container)
|
||||||
|
|
||||||
|
var cost_title = Label.new()
|
||||||
|
cost_title.text = "Max Cost:"
|
||||||
|
cost_title.add_theme_font_size_override("font_size", 12)
|
||||||
|
cost_container.add_child(cost_title)
|
||||||
|
|
||||||
|
cost_slider = HSlider.new()
|
||||||
|
cost_slider.min_value = 1
|
||||||
|
cost_slider.max_value = 14
|
||||||
|
cost_slider.value = 14
|
||||||
|
cost_slider.step = 1
|
||||||
|
cost_slider.custom_minimum_size = Vector2(100, 0)
|
||||||
|
cost_slider.value_changed.connect(_on_cost_changed)
|
||||||
|
cost_container.add_child(cost_slider)
|
||||||
|
|
||||||
|
cost_label = Label.new()
|
||||||
|
cost_label.text = "14"
|
||||||
|
cost_label.custom_minimum_size = Vector2(24, 0)
|
||||||
|
cost_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
cost_container.add_child(cost_label)
|
||||||
|
|
||||||
|
# Expand/collapse button
|
||||||
|
expand_button = Button.new()
|
||||||
|
expand_button.text = "More Filters"
|
||||||
|
expand_button.custom_minimum_size = Vector2(100, 0)
|
||||||
|
expand_button.pressed.connect(_toggle_expanded)
|
||||||
|
_apply_button_style(expand_button)
|
||||||
|
row1.add_child(expand_button)
|
||||||
|
|
||||||
|
# Clear filters button
|
||||||
|
var clear_btn = Button.new()
|
||||||
|
clear_btn.text = "Clear"
|
||||||
|
clear_btn.custom_minimum_size = Vector2(60, 0)
|
||||||
|
clear_btn.pressed.connect(_clear_filters)
|
||||||
|
_apply_button_style(clear_btn)
|
||||||
|
row1.add_child(clear_btn)
|
||||||
|
|
||||||
|
# Row 2: Element buttons
|
||||||
|
var row2 = HBoxContainer.new()
|
||||||
|
row2.add_theme_constant_override("separation", 6)
|
||||||
|
main_vbox.add_child(row2)
|
||||||
|
|
||||||
|
var elem_label = Label.new()
|
||||||
|
elem_label.text = "Elements:"
|
||||||
|
elem_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
row2.add_child(elem_label)
|
||||||
|
|
||||||
|
for element in Enums.Element.values():
|
||||||
|
var btn = Button.new()
|
||||||
|
btn.text = Enums.element_to_string(element).substr(0, 3).to_upper()
|
||||||
|
btn.custom_minimum_size = Vector2(44, 28)
|
||||||
|
btn.toggle_mode = true
|
||||||
|
btn.button_pressed = false
|
||||||
|
btn.pressed.connect(_on_element_toggled.bind(element))
|
||||||
|
_apply_element_button_style(btn, element)
|
||||||
|
row2.add_child(btn)
|
||||||
|
element_buttons[element] = btn
|
||||||
|
|
||||||
|
# Expanded filters container (hidden by default)
|
||||||
|
expanded_container = VBoxContainer.new()
|
||||||
|
expanded_container.visible = false
|
||||||
|
expanded_container.add_theme_constant_override("separation", 8)
|
||||||
|
main_vbox.add_child(expanded_container)
|
||||||
|
|
||||||
|
var expanded_row = HBoxContainer.new()
|
||||||
|
expanded_row.add_theme_constant_override("separation", 16)
|
||||||
|
expanded_container.add_child(expanded_row)
|
||||||
|
|
||||||
|
# Job dropdown
|
||||||
|
var job_container = HBoxContainer.new()
|
||||||
|
job_container.add_theme_constant_override("separation", 4)
|
||||||
|
expanded_row.add_child(job_container)
|
||||||
|
|
||||||
|
var job_label_el = Label.new()
|
||||||
|
job_label_el.text = "Job:"
|
||||||
|
job_label_el.add_theme_font_size_override("font_size", 12)
|
||||||
|
job_container.add_child(job_label_el)
|
||||||
|
|
||||||
|
job_dropdown = OptionButton.new()
|
||||||
|
job_dropdown.custom_minimum_size = Vector2(120, 0)
|
||||||
|
job_dropdown.item_selected.connect(_on_job_selected)
|
||||||
|
job_container.add_child(job_dropdown)
|
||||||
|
|
||||||
|
# Category dropdown
|
||||||
|
var cat_container = HBoxContainer.new()
|
||||||
|
cat_container.add_theme_constant_override("separation", 4)
|
||||||
|
expanded_row.add_child(cat_container)
|
||||||
|
|
||||||
|
var cat_label = Label.new()
|
||||||
|
cat_label.text = "Category:"
|
||||||
|
cat_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
cat_container.add_child(cat_label)
|
||||||
|
|
||||||
|
category_dropdown = OptionButton.new()
|
||||||
|
category_dropdown.custom_minimum_size = Vector2(80, 0)
|
||||||
|
category_dropdown.item_selected.connect(_on_category_selected)
|
||||||
|
cat_container.add_child(category_dropdown)
|
||||||
|
|
||||||
|
# Power range
|
||||||
|
var power_container = HBoxContainer.new()
|
||||||
|
power_container.add_theme_constant_override("separation", 4)
|
||||||
|
expanded_row.add_child(power_container)
|
||||||
|
|
||||||
|
var power_label_el = Label.new()
|
||||||
|
power_label_el.text = "Power:"
|
||||||
|
power_label_el.add_theme_font_size_override("font_size", 12)
|
||||||
|
power_container.add_child(power_label_el)
|
||||||
|
|
||||||
|
power_min_spin = SpinBox.new()
|
||||||
|
power_min_spin.min_value = 0
|
||||||
|
power_min_spin.max_value = 20000
|
||||||
|
power_min_spin.step = 1000
|
||||||
|
power_min_spin.value = 0
|
||||||
|
power_min_spin.custom_minimum_size = Vector2(70, 0)
|
||||||
|
power_min_spin.value_changed.connect(_on_power_min_changed)
|
||||||
|
power_container.add_child(power_min_spin)
|
||||||
|
|
||||||
|
var dash = Label.new()
|
||||||
|
dash.text = "-"
|
||||||
|
power_container.add_child(dash)
|
||||||
|
|
||||||
|
power_max_spin = SpinBox.new()
|
||||||
|
power_max_spin.min_value = 0
|
||||||
|
power_max_spin.max_value = 20000
|
||||||
|
power_max_spin.step = 1000
|
||||||
|
power_max_spin.value = 20000
|
||||||
|
power_max_spin.custom_minimum_size = Vector2(70, 0)
|
||||||
|
power_max_spin.value_changed.connect(_on_power_max_changed)
|
||||||
|
power_container.add_child(power_max_spin)
|
||||||
|
|
||||||
|
# EX Burst checkbox
|
||||||
|
ex_burst_check = CheckBox.new()
|
||||||
|
ex_burst_check.text = "EX Burst only"
|
||||||
|
ex_burst_check.add_theme_font_size_override("font_size", 12)
|
||||||
|
ex_burst_check.toggled.connect(_on_ex_burst_toggled)
|
||||||
|
expanded_row.add_child(ex_burst_check)
|
||||||
|
|
||||||
|
# Set/Opus dropdown
|
||||||
|
var set_container = HBoxContainer.new()
|
||||||
|
set_container.add_theme_constant_override("separation", 4)
|
||||||
|
expanded_row.add_child(set_container)
|
||||||
|
|
||||||
|
var set_label = Label.new()
|
||||||
|
set_label.text = "Set:"
|
||||||
|
set_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
set_container.add_child(set_label)
|
||||||
|
|
||||||
|
set_dropdown = OptionButton.new()
|
||||||
|
set_dropdown.custom_minimum_size = Vector2(70, 0)
|
||||||
|
set_dropdown.item_selected.connect(_on_set_selected)
|
||||||
|
set_container.add_child(set_dropdown)
|
||||||
|
|
||||||
|
|
||||||
|
func _populate_dropdowns() -> void:
|
||||||
|
# Populate job dropdown
|
||||||
|
job_dropdown.add_item("All", -1)
|
||||||
|
for job in CardDatabase.get_unique_jobs():
|
||||||
|
job_dropdown.add_item(job)
|
||||||
|
|
||||||
|
# Populate category dropdown
|
||||||
|
category_dropdown.add_item("All", -1)
|
||||||
|
for category in CardDatabase.get_unique_categories():
|
||||||
|
category_dropdown.add_item(category)
|
||||||
|
|
||||||
|
# Populate set dropdown
|
||||||
|
set_dropdown.add_item("All", -1)
|
||||||
|
for set_id in CardDatabase.get_card_sets():
|
||||||
|
set_dropdown.add_item("Opus " + set_id, set_dropdown.item_count)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_panel_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.1, 0.1, 0.14, 0.95)
|
||||||
|
style.border_color = Color(0.4, 0.35, 0.25)
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(4)
|
||||||
|
style.content_margin_left = 12
|
||||||
|
style.content_margin_right = 12
|
||||||
|
style.content_margin_top = 8
|
||||||
|
style.content_margin_bottom = 8
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_button_style(button: Button) -> void:
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = Color(0.2, 0.2, 0.25)
|
||||||
|
style_normal.border_color = Color(0.4, 0.4, 0.5)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(3)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = Color(0.28, 0.28, 0.35)
|
||||||
|
style_hover.border_color = Color(0.5, 0.45, 0.3)
|
||||||
|
style_hover.set_border_width_all(1)
|
||||||
|
style_hover.set_corner_radius_all(3)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 12)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_element_button_style(button: Button, element: Enums.Element) -> void:
|
||||||
|
var element_color = Enums.element_to_color(element)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = element_color.darkened(0.6)
|
||||||
|
style_normal.border_color = element_color.darkened(0.3)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(3)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = element_color.darkened(0.4)
|
||||||
|
style_hover.border_color = element_color
|
||||||
|
style_hover.set_border_width_all(2)
|
||||||
|
style_hover.set_corner_radius_all(3)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
var style_pressed = StyleBoxFlat.new()
|
||||||
|
style_pressed.bg_color = element_color.darkened(0.2)
|
||||||
|
style_pressed.border_color = Color.WHITE
|
||||||
|
style_pressed.set_border_width_all(2)
|
||||||
|
style_pressed.set_corner_radius_all(3)
|
||||||
|
button.add_theme_stylebox_override("pressed", style_pressed)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 10)
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_expanded() -> void:
|
||||||
|
is_expanded = not is_expanded
|
||||||
|
expanded_container.visible = is_expanded
|
||||||
|
expand_button.text = "Less Filters" if is_expanded else "More Filters"
|
||||||
|
custom_minimum_size.y = EXPANDED_HEIGHT if is_expanded else FILTER_BAR_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_filters() -> void:
|
||||||
|
search_field.text = ""
|
||||||
|
type_dropdown.select(0)
|
||||||
|
cost_slider.value = 14
|
||||||
|
for btn in element_buttons.values():
|
||||||
|
btn.button_pressed = false
|
||||||
|
job_dropdown.select(0)
|
||||||
|
category_dropdown.select(0)
|
||||||
|
power_min_spin.value = 0
|
||||||
|
power_max_spin.value = 20000
|
||||||
|
ex_burst_check.button_pressed = false
|
||||||
|
set_dropdown.select(0)
|
||||||
|
current_filters.clear()
|
||||||
|
filters_changed.emit(current_filters)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_filters() -> void:
|
||||||
|
filters_changed.emit(current_filters)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_search_changed(text: String) -> void:
|
||||||
|
if text.is_empty():
|
||||||
|
current_filters.erase("name")
|
||||||
|
else:
|
||||||
|
current_filters["name"] = text
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_type_selected(index: int) -> void:
|
||||||
|
var type_id = type_dropdown.get_item_id(index)
|
||||||
|
if type_id == -1:
|
||||||
|
current_filters.erase("type")
|
||||||
|
else:
|
||||||
|
current_filters["type"] = type_id
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cost_changed(value: float) -> void:
|
||||||
|
cost_label.text = str(int(value))
|
||||||
|
if value >= 14:
|
||||||
|
current_filters.erase("cost_max")
|
||||||
|
else:
|
||||||
|
current_filters["cost_max"] = int(value)
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_element_toggled(element: Enums.Element) -> void:
|
||||||
|
var selected_elements: Array[Enums.Element] = []
|
||||||
|
for elem in element_buttons:
|
||||||
|
if element_buttons[elem].button_pressed:
|
||||||
|
selected_elements.append(elem)
|
||||||
|
|
||||||
|
if selected_elements.is_empty():
|
||||||
|
current_filters.erase("elements")
|
||||||
|
else:
|
||||||
|
current_filters["elements"] = selected_elements
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_job_selected(index: int) -> void:
|
||||||
|
if index == 0:
|
||||||
|
current_filters.erase("job")
|
||||||
|
else:
|
||||||
|
current_filters["job"] = job_dropdown.get_item_text(index)
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_category_selected(index: int) -> void:
|
||||||
|
if index == 0:
|
||||||
|
current_filters.erase("category")
|
||||||
|
else:
|
||||||
|
current_filters["category"] = category_dropdown.get_item_text(index)
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_power_min_changed(value: float) -> void:
|
||||||
|
if value <= 0:
|
||||||
|
current_filters.erase("power_min")
|
||||||
|
else:
|
||||||
|
current_filters["power_min"] = int(value)
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_power_max_changed(value: float) -> void:
|
||||||
|
if value >= 20000:
|
||||||
|
current_filters.erase("power_max")
|
||||||
|
else:
|
||||||
|
current_filters["power_max"] = int(value)
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ex_burst_toggled(pressed: bool) -> void:
|
||||||
|
if pressed:
|
||||||
|
current_filters["ex_burst_only"] = true
|
||||||
|
else:
|
||||||
|
current_filters.erase("ex_burst_only")
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_set_selected(index: int) -> void:
|
||||||
|
if index == 0:
|
||||||
|
current_filters.erase("set")
|
||||||
|
else:
|
||||||
|
# Extract set number from "Opus X" text
|
||||||
|
var text = set_dropdown.get_item_text(index)
|
||||||
|
var set_num = text.replace("Opus ", "")
|
||||||
|
current_filters["set"] = set_num + "-"
|
||||||
|
_emit_filters()
|
||||||
|
|
||||||
|
|
||||||
|
## Get current filters
|
||||||
|
func get_filters() -> Dictionary:
|
||||||
|
return current_filters.duplicate()
|
||||||
241
scripts/ui/CardGrid.gd
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
class_name CardGrid
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## CardGrid - Virtualized scrolling grid for displaying cards in the deck builder
|
||||||
|
|
||||||
|
signal card_selected(card: CardDatabase.CardData)
|
||||||
|
signal card_double_clicked(card: CardDatabase.CardData)
|
||||||
|
|
||||||
|
const CARD_WIDTH: float = 140.0
|
||||||
|
const CARD_HEIGHT: float = 196.0
|
||||||
|
const CARD_GAP: float = 8.0
|
||||||
|
const COLUMNS: int = 5
|
||||||
|
const VISIBLE_ROWS_BUFFER: int = 2
|
||||||
|
|
||||||
|
var filtered_cards: Array = [] # Array of CardData
|
||||||
|
var card_cells: Array[Control] = []
|
||||||
|
var scroll_container: ScrollContainer
|
||||||
|
var grid_content: Control
|
||||||
|
var visible_start_row: int = 0
|
||||||
|
var total_rows: int = 0
|
||||||
|
var last_click_time: float = 0.0
|
||||||
|
var last_clicked_card: CardDatabase.CardData = null
|
||||||
|
|
||||||
|
# Loading indicator
|
||||||
|
var loading_label: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_create_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
# Main scroll container
|
||||||
|
scroll_container = ScrollContainer.new()
|
||||||
|
scroll_container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
scroll_container.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
scroll_container.get_v_scroll_bar().value_changed.connect(_on_scroll_changed)
|
||||||
|
add_child(scroll_container)
|
||||||
|
|
||||||
|
# Grid content container (sized to fit all cards)
|
||||||
|
grid_content = Control.new()
|
||||||
|
grid_content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
scroll_container.add_child(grid_content)
|
||||||
|
|
||||||
|
# Loading label
|
||||||
|
loading_label = Label.new()
|
||||||
|
loading_label.text = "Loading cards..."
|
||||||
|
loading_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
loading_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
loading_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||||
|
add_child(loading_label)
|
||||||
|
|
||||||
|
# Pre-create card cell pool
|
||||||
|
_create_cell_pool()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_cell_pool() -> void:
|
||||||
|
# Calculate max visible cells needed
|
||||||
|
var viewport_height = get_viewport_rect().size.y if get_viewport() else 900.0
|
||||||
|
var max_visible_rows = ceili(viewport_height / (CARD_HEIGHT + CARD_GAP)) + VISIBLE_ROWS_BUFFER * 2
|
||||||
|
var pool_size = max_visible_rows * COLUMNS
|
||||||
|
|
||||||
|
for i in range(pool_size):
|
||||||
|
var cell = _create_card_cell()
|
||||||
|
cell.visible = false
|
||||||
|
grid_content.add_child(cell)
|
||||||
|
card_cells.append(cell)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_card_cell() -> Control:
|
||||||
|
var cell = Panel.new()
|
||||||
|
cell.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||||
|
cell.size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||||
|
cell.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.15, 0.15, 0.2, 0.8)
|
||||||
|
style.border_color = Color(0.3, 0.3, 0.35)
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(3)
|
||||||
|
cell.add_theme_stylebox_override("panel", style)
|
||||||
|
|
||||||
|
# Card image
|
||||||
|
var tex_rect = TextureRect.new()
|
||||||
|
tex_rect.name = "TextureRect"
|
||||||
|
tex_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
cell.add_child(tex_rect)
|
||||||
|
|
||||||
|
# Fallback color rect
|
||||||
|
var fallback = ColorRect.new()
|
||||||
|
fallback.name = "Fallback"
|
||||||
|
fallback.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
fallback.visible = false
|
||||||
|
cell.add_child(fallback)
|
||||||
|
|
||||||
|
# Card name label (shown on fallback)
|
||||||
|
var name_label = Label.new()
|
||||||
|
name_label.name = "NameLabel"
|
||||||
|
name_label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
name_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
name_label.visible = false
|
||||||
|
cell.add_child(name_label)
|
||||||
|
|
||||||
|
# Hover highlight
|
||||||
|
var highlight = ColorRect.new()
|
||||||
|
highlight.name = "Highlight"
|
||||||
|
highlight.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
highlight.color = Color(1.0, 1.0, 1.0, 0.0)
|
||||||
|
highlight.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
cell.add_child(highlight)
|
||||||
|
|
||||||
|
# Input handling
|
||||||
|
cell.gui_input.connect(_on_cell_input.bind(cell))
|
||||||
|
cell.mouse_entered.connect(_on_cell_hover.bind(cell, true))
|
||||||
|
cell.mouse_exited.connect(_on_cell_hover.bind(cell, false))
|
||||||
|
|
||||||
|
return cell
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_input(event: InputEvent, cell: Control) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
var card = cell.get_meta("card", null)
|
||||||
|
if card:
|
||||||
|
var current_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
if card == last_clicked_card and current_time - last_click_time < 0.4:
|
||||||
|
# Double click
|
||||||
|
card_double_clicked.emit(card)
|
||||||
|
last_clicked_card = null
|
||||||
|
else:
|
||||||
|
# Single click
|
||||||
|
card_selected.emit(card)
|
||||||
|
last_clicked_card = card
|
||||||
|
last_click_time = current_time
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cell_hover(cell: Control, entered: bool) -> void:
|
||||||
|
var highlight = cell.get_node("Highlight") as ColorRect
|
||||||
|
if highlight:
|
||||||
|
highlight.color.a = 0.15 if entered else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func set_cards(cards: Array) -> void:
|
||||||
|
filtered_cards = cards
|
||||||
|
total_rows = ceili(float(cards.size()) / COLUMNS) if cards.size() > 0 else 0
|
||||||
|
|
||||||
|
# Update content size
|
||||||
|
var content_width = COLUMNS * (CARD_WIDTH + CARD_GAP) - CARD_GAP
|
||||||
|
var content_height = total_rows * (CARD_HEIGHT + CARD_GAP)
|
||||||
|
grid_content.custom_minimum_size = Vector2(content_width, content_height)
|
||||||
|
|
||||||
|
loading_label.visible = cards.is_empty()
|
||||||
|
|
||||||
|
# Reset scroll and update visible cells
|
||||||
|
scroll_container.scroll_vertical = 0
|
||||||
|
_update_visible_cells()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_scroll_changed(_value: float) -> void:
|
||||||
|
_update_visible_cells()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_visible_cells() -> void:
|
||||||
|
if filtered_cards.is_empty():
|
||||||
|
for cell in card_cells:
|
||||||
|
cell.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var scroll_y = scroll_container.scroll_vertical
|
||||||
|
var viewport_height = scroll_container.size.y
|
||||||
|
|
||||||
|
# Calculate visible row range
|
||||||
|
var first_visible_row = int(scroll_y / (CARD_HEIGHT + CARD_GAP))
|
||||||
|
var last_visible_row = ceili((scroll_y + viewport_height) / (CARD_HEIGHT + CARD_GAP))
|
||||||
|
|
||||||
|
# Add buffer
|
||||||
|
first_visible_row = maxi(0, first_visible_row - VISIBLE_ROWS_BUFFER)
|
||||||
|
last_visible_row = mini(total_rows - 1, last_visible_row + VISIBLE_ROWS_BUFFER)
|
||||||
|
|
||||||
|
# Update cells
|
||||||
|
var cell_index = 0
|
||||||
|
for row in range(first_visible_row, last_visible_row + 1):
|
||||||
|
for col in range(COLUMNS):
|
||||||
|
var card_index = row * COLUMNS + col
|
||||||
|
if card_index >= filtered_cards.size():
|
||||||
|
break
|
||||||
|
|
||||||
|
if cell_index < card_cells.size():
|
||||||
|
var cell = card_cells[cell_index]
|
||||||
|
var card = filtered_cards[card_index]
|
||||||
|
|
||||||
|
# Position cell
|
||||||
|
cell.position = Vector2(
|
||||||
|
col * (CARD_WIDTH + CARD_GAP),
|
||||||
|
row * (CARD_HEIGHT + CARD_GAP)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update cell content
|
||||||
|
_update_cell_content(cell, card)
|
||||||
|
cell.visible = true
|
||||||
|
cell_index += 1
|
||||||
|
|
||||||
|
# Hide unused cells
|
||||||
|
for i in range(cell_index, card_cells.size()):
|
||||||
|
card_cells[i].visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _update_cell_content(cell: Control, card: CardDatabase.CardData) -> void:
|
||||||
|
var current_id = cell.get_meta("card_id", "")
|
||||||
|
if current_id == card.id:
|
||||||
|
return # Already showing this card
|
||||||
|
|
||||||
|
cell.set_meta("card_id", card.id)
|
||||||
|
cell.set_meta("card", card)
|
||||||
|
|
||||||
|
var tex_rect = cell.get_node("TextureRect") as TextureRect
|
||||||
|
var fallback = cell.get_node("Fallback") as ColorRect
|
||||||
|
var name_label = cell.get_node("NameLabel") as Label
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
var texture = CardDatabase.get_card_texture(card)
|
||||||
|
if texture:
|
||||||
|
tex_rect.texture = texture
|
||||||
|
tex_rect.visible = true
|
||||||
|
fallback.visible = false
|
||||||
|
name_label.visible = false
|
||||||
|
else:
|
||||||
|
tex_rect.visible = false
|
||||||
|
fallback.visible = true
|
||||||
|
fallback.color = Enums.element_to_color(card.get_primary_element()).darkened(0.3)
|
||||||
|
name_label.visible = true
|
||||||
|
name_label.text = card.name
|
||||||
|
|
||||||
|
|
||||||
|
## Get currently displayed card count
|
||||||
|
func get_card_count() -> int:
|
||||||
|
return filtered_cards.size()
|
||||||
470
scripts/ui/DeckBuilder.gd
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
class_name DeckBuilder
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
## DeckBuilder - Main deck builder screen with three-panel layout
|
||||||
|
|
||||||
|
signal back_pressed
|
||||||
|
signal deck_selected(deck: Deck)
|
||||||
|
|
||||||
|
const WINDOW_SIZE = Vector2i(1600, 900)
|
||||||
|
|
||||||
|
var current_deck: Deck = null
|
||||||
|
var current_deck_filename: String = ""
|
||||||
|
|
||||||
|
# UI Components
|
||||||
|
var detail_viewer: CardDetailViewer
|
||||||
|
var filter_bar: CardFilterBar
|
||||||
|
var card_grid: CardGrid
|
||||||
|
var deck_panel: DeckListPanel
|
||||||
|
|
||||||
|
# Header elements
|
||||||
|
var back_button: Button
|
||||||
|
var deck_name_field: LineEdit
|
||||||
|
var card_count_label: Label
|
||||||
|
var save_button: Button
|
||||||
|
var load_button: Button
|
||||||
|
var new_button: Button
|
||||||
|
var play_button: Button
|
||||||
|
|
||||||
|
# Dialogs
|
||||||
|
var save_dialog: Control
|
||||||
|
var load_dialog: Control
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = 10
|
||||||
|
_create_ui()
|
||||||
|
_connect_signals()
|
||||||
|
_new_deck()
|
||||||
|
_load_all_cards()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
# Root control
|
||||||
|
var root = Control.new()
|
||||||
|
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(root)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
var bg = ColorRect.new()
|
||||||
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
bg.color = Color(0.05, 0.05, 0.08, 1.0)
|
||||||
|
root.add_child(bg)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
var main_vbox = VBoxContainer.new()
|
||||||
|
main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
main_vbox.add_theme_constant_override("separation", 0)
|
||||||
|
root.add_child(main_vbox)
|
||||||
|
|
||||||
|
# Header bar
|
||||||
|
_create_header(main_vbox)
|
||||||
|
|
||||||
|
# Content area (3 panels)
|
||||||
|
var content_hbox = HBoxContainer.new()
|
||||||
|
content_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
content_hbox.add_theme_constant_override("separation", 0)
|
||||||
|
main_vbox.add_child(content_hbox)
|
||||||
|
|
||||||
|
# Left panel - Card Detail Viewer
|
||||||
|
detail_viewer = CardDetailViewer.new()
|
||||||
|
content_hbox.add_child(detail_viewer)
|
||||||
|
|
||||||
|
# Center panel container
|
||||||
|
var center_panel = VBoxContainer.new()
|
||||||
|
center_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
center_panel.add_theme_constant_override("separation", 0)
|
||||||
|
content_hbox.add_child(center_panel)
|
||||||
|
|
||||||
|
# Filter bar
|
||||||
|
filter_bar = CardFilterBar.new()
|
||||||
|
center_panel.add_child(filter_bar)
|
||||||
|
|
||||||
|
# Results count
|
||||||
|
var results_bar = HBoxContainer.new()
|
||||||
|
results_bar.custom_minimum_size = Vector2(0, 30)
|
||||||
|
var results_style = StyleBoxFlat.new()
|
||||||
|
results_style.bg_color = Color(0.08, 0.08, 0.1)
|
||||||
|
results_style.content_margin_left = 12
|
||||||
|
results_style.content_margin_top = 4
|
||||||
|
|
||||||
|
var results_panel = PanelContainer.new()
|
||||||
|
results_panel.add_theme_stylebox_override("panel", results_style)
|
||||||
|
results_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
center_panel.add_child(results_panel)
|
||||||
|
|
||||||
|
card_count_label = Label.new()
|
||||||
|
card_count_label.text = "Loading cards..."
|
||||||
|
card_count_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
card_count_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||||
|
results_panel.add_child(card_count_label)
|
||||||
|
|
||||||
|
# Card grid
|
||||||
|
card_grid = CardGrid.new()
|
||||||
|
card_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
center_panel.add_child(card_grid)
|
||||||
|
|
||||||
|
# Right panel - Deck List
|
||||||
|
deck_panel = DeckListPanel.new()
|
||||||
|
content_hbox.add_child(deck_panel)
|
||||||
|
|
||||||
|
# Create dialogs (hidden)
|
||||||
|
_create_save_dialog(root)
|
||||||
|
_create_load_dialog(root)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_header(parent: Control) -> void:
|
||||||
|
var header = PanelContainer.new()
|
||||||
|
header.custom_minimum_size = Vector2(0, 50)
|
||||||
|
var header_style = StyleBoxFlat.new()
|
||||||
|
header_style.bg_color = Color(0.1, 0.1, 0.14)
|
||||||
|
header_style.border_color = Color(0.3, 0.25, 0.15)
|
||||||
|
header_style.border_width_bottom = 2
|
||||||
|
header_style.content_margin_left = 15
|
||||||
|
header_style.content_margin_right = 15
|
||||||
|
header.add_theme_stylebox_override("panel", header_style)
|
||||||
|
parent.add_child(header)
|
||||||
|
|
||||||
|
var header_hbox = HBoxContainer.new()
|
||||||
|
header_hbox.add_theme_constant_override("separation", 15)
|
||||||
|
header_hbox.alignment = BoxContainer.ALIGNMENT_BEGIN
|
||||||
|
header.add_child(header_hbox)
|
||||||
|
|
||||||
|
# Back button
|
||||||
|
back_button = _create_header_button("< Back")
|
||||||
|
header_hbox.add_child(back_button)
|
||||||
|
|
||||||
|
# Deck name
|
||||||
|
var name_label = Label.new()
|
||||||
|
name_label.text = "Deck:"
|
||||||
|
name_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
header_hbox.add_child(name_label)
|
||||||
|
|
||||||
|
deck_name_field = LineEdit.new()
|
||||||
|
deck_name_field.text = "New Deck"
|
||||||
|
deck_name_field.custom_minimum_size = Vector2(200, 0)
|
||||||
|
deck_name_field.text_changed.connect(_on_deck_name_changed)
|
||||||
|
header_hbox.add_child(deck_name_field)
|
||||||
|
|
||||||
|
# Spacer
|
||||||
|
var spacer = Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header_hbox.add_child(spacer)
|
||||||
|
|
||||||
|
# Action buttons
|
||||||
|
new_button = _create_header_button("New")
|
||||||
|
save_button = _create_header_button("Save")
|
||||||
|
load_button = _create_header_button("Load")
|
||||||
|
play_button = _create_header_button("Play with Deck")
|
||||||
|
play_button.add_theme_color_override("font_color", Color(0.4, 0.8, 0.4))
|
||||||
|
|
||||||
|
header_hbox.add_child(new_button)
|
||||||
|
header_hbox.add_child(save_button)
|
||||||
|
header_hbox.add_child(load_button)
|
||||||
|
header_hbox.add_child(play_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_header_button(text: String) -> Button:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = text
|
||||||
|
button.custom_minimum_size = Vector2(80, 32)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = Color(0.2, 0.2, 0.25)
|
||||||
|
style_normal.border_color = Color(0.4, 0.4, 0.5)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = Color(0.3, 0.3, 0.38)
|
||||||
|
style_hover.border_color = Color(0.6, 0.5, 0.3)
|
||||||
|
style_hover.set_border_width_all(1)
|
||||||
|
style_hover.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 13)
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
func _create_save_dialog(parent: Control) -> void:
|
||||||
|
save_dialog = _create_dialog_base("Save Deck")
|
||||||
|
parent.add_child(save_dialog)
|
||||||
|
|
||||||
|
var content = save_dialog.get_node("Panel/VBox")
|
||||||
|
|
||||||
|
var name_hbox = HBoxContainer.new()
|
||||||
|
name_hbox.add_theme_constant_override("separation", 8)
|
||||||
|
content.add_child(name_hbox)
|
||||||
|
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = "Filename:"
|
||||||
|
name_hbox.add_child(label)
|
||||||
|
|
||||||
|
var save_name_field = LineEdit.new()
|
||||||
|
save_name_field.name = "SaveNameField"
|
||||||
|
save_name_field.custom_minimum_size = Vector2(200, 0)
|
||||||
|
name_hbox.add_child(save_name_field)
|
||||||
|
|
||||||
|
var btn_hbox = HBoxContainer.new()
|
||||||
|
btn_hbox.add_theme_constant_override("separation", 10)
|
||||||
|
btn_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
content.add_child(btn_hbox)
|
||||||
|
|
||||||
|
var save_btn = _create_header_button("Save")
|
||||||
|
save_btn.pressed.connect(_on_save_confirmed)
|
||||||
|
btn_hbox.add_child(save_btn)
|
||||||
|
|
||||||
|
var cancel_btn = _create_header_button("Cancel")
|
||||||
|
cancel_btn.pressed.connect(func(): save_dialog.visible = false)
|
||||||
|
btn_hbox.add_child(cancel_btn)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_load_dialog(parent: Control) -> void:
|
||||||
|
load_dialog = _create_dialog_base("Load Deck")
|
||||||
|
parent.add_child(load_dialog)
|
||||||
|
|
||||||
|
var content = load_dialog.get_node("Panel/VBox")
|
||||||
|
|
||||||
|
var deck_list = ItemList.new()
|
||||||
|
deck_list.name = "DeckList"
|
||||||
|
deck_list.custom_minimum_size = Vector2(300, 200)
|
||||||
|
deck_list.item_activated.connect(_on_deck_item_activated)
|
||||||
|
content.add_child(deck_list)
|
||||||
|
|
||||||
|
var btn_hbox = HBoxContainer.new()
|
||||||
|
btn_hbox.add_theme_constant_override("separation", 10)
|
||||||
|
btn_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
content.add_child(btn_hbox)
|
||||||
|
|
||||||
|
var load_btn = _create_header_button("Load")
|
||||||
|
load_btn.pressed.connect(_on_load_confirmed)
|
||||||
|
btn_hbox.add_child(load_btn)
|
||||||
|
|
||||||
|
var delete_btn = _create_header_button("Delete")
|
||||||
|
delete_btn.add_theme_color_override("font_color", Color(1.0, 0.5, 0.5))
|
||||||
|
delete_btn.pressed.connect(_on_delete_deck)
|
||||||
|
btn_hbox.add_child(delete_btn)
|
||||||
|
|
||||||
|
var cancel_btn = _create_header_button("Cancel")
|
||||||
|
cancel_btn.pressed.connect(func(): load_dialog.visible = false)
|
||||||
|
btn_hbox.add_child(cancel_btn)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_dialog_base(title: String) -> Control:
|
||||||
|
var overlay = Control.new()
|
||||||
|
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
overlay.visible = false
|
||||||
|
|
||||||
|
var bg = ColorRect.new()
|
||||||
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
bg.color = Color(0, 0, 0, 0.6)
|
||||||
|
bg.gui_input.connect(func(event):
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
overlay.visible = false
|
||||||
|
)
|
||||||
|
overlay.add_child(bg)
|
||||||
|
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.name = "Panel"
|
||||||
|
panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
panel.custom_minimum_size = Vector2(350, 250)
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.1, 0.1, 0.14, 0.98)
|
||||||
|
style.border_color = Color(0.5, 0.4, 0.2)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(8)
|
||||||
|
style.content_margin_left = 20
|
||||||
|
style.content_margin_right = 20
|
||||||
|
style.content_margin_top = 15
|
||||||
|
style.content_margin_bottom = 15
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
overlay.add_child(panel)
|
||||||
|
|
||||||
|
var vbox = VBoxContainer.new()
|
||||||
|
vbox.name = "VBox"
|
||||||
|
vbox.add_theme_constant_override("separation", 15)
|
||||||
|
panel.add_child(vbox)
|
||||||
|
|
||||||
|
var title_label = Label.new()
|
||||||
|
title_label.text = title
|
||||||
|
title_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
|
||||||
|
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vbox.add_child(title_label)
|
||||||
|
|
||||||
|
return overlay
|
||||||
|
|
||||||
|
|
||||||
|
func _connect_signals() -> void:
|
||||||
|
back_button.pressed.connect(_on_back_pressed)
|
||||||
|
new_button.pressed.connect(_new_deck)
|
||||||
|
save_button.pressed.connect(_show_save_dialog)
|
||||||
|
load_button.pressed.connect(_show_load_dialog)
|
||||||
|
play_button.pressed.connect(_on_play_pressed)
|
||||||
|
|
||||||
|
filter_bar.filters_changed.connect(_on_filters_changed)
|
||||||
|
card_grid.card_selected.connect(_on_card_selected)
|
||||||
|
card_grid.card_double_clicked.connect(_on_card_double_clicked)
|
||||||
|
detail_viewer.add_to_deck_requested.connect(_on_add_to_deck)
|
||||||
|
|
||||||
|
deck_panel.card_clicked.connect(_on_deck_card_clicked)
|
||||||
|
deck_panel.card_removed.connect(_on_deck_card_removed)
|
||||||
|
deck_panel.deck_cleared.connect(_on_deck_cleared)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_all_cards() -> void:
|
||||||
|
var all_cards = CardDatabase.get_all_cards()
|
||||||
|
card_grid.set_cards(all_cards)
|
||||||
|
card_count_label.text = "Showing %d of %d cards" % [all_cards.size(), all_cards.size()]
|
||||||
|
|
||||||
|
|
||||||
|
func _new_deck() -> void:
|
||||||
|
current_deck = Deck.new()
|
||||||
|
current_deck.name = DeckManager.generate_unique_name()
|
||||||
|
current_deck_filename = ""
|
||||||
|
deck_name_field.text = current_deck.name
|
||||||
|
deck_panel.set_deck(current_deck)
|
||||||
|
detail_viewer.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_pressed() -> void:
|
||||||
|
back_pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_play_pressed() -> void:
|
||||||
|
if current_deck and current_deck.is_valid():
|
||||||
|
deck_selected.emit(current_deck)
|
||||||
|
else:
|
||||||
|
# Show validation errors
|
||||||
|
var errors = current_deck.validate() if current_deck else ["No deck loaded"]
|
||||||
|
push_warning("Cannot play - deck invalid: " + ", ".join(errors))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_name_changed(new_name: String) -> void:
|
||||||
|
if current_deck:
|
||||||
|
current_deck.name = new_name
|
||||||
|
|
||||||
|
|
||||||
|
func _on_filters_changed(filters: Dictionary) -> void:
|
||||||
|
var results = CardDatabase.filter_cards(filters)
|
||||||
|
card_grid.set_cards(results)
|
||||||
|
card_count_label.text = "Showing %d of %d cards" % [results.size(), CardDatabase.get_card_count()]
|
||||||
|
|
||||||
|
|
||||||
|
func _on_card_selected(card: CardDatabase.CardData) -> void:
|
||||||
|
var deck_count = current_deck.get_card_count(card.id) if current_deck else 0
|
||||||
|
detail_viewer.show_card(card, deck_count)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_card_double_clicked(card: CardDatabase.CardData) -> void:
|
||||||
|
if current_deck:
|
||||||
|
var error = current_deck.add_card(card.id)
|
||||||
|
if error.is_empty():
|
||||||
|
detail_viewer.update_deck_count(current_deck.get_card_count(card.id))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_add_to_deck(card: CardDatabase.CardData, quantity: int) -> void:
|
||||||
|
if not current_deck:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(quantity):
|
||||||
|
var error = current_deck.add_card(card.id)
|
||||||
|
if not error.is_empty():
|
||||||
|
break
|
||||||
|
|
||||||
|
detail_viewer.update_deck_count(current_deck.get_card_count(card.id))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_card_clicked(card_id: String) -> void:
|
||||||
|
var card_data = CardDatabase.get_card(card_id)
|
||||||
|
if card_data:
|
||||||
|
var deck_count = current_deck.get_card_count(card_id) if current_deck else 0
|
||||||
|
detail_viewer.show_card(card_data, deck_count)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_card_removed(card_id: String) -> void:
|
||||||
|
if current_deck:
|
||||||
|
current_deck.remove_card(card_id)
|
||||||
|
# Update detail viewer if showing this card
|
||||||
|
var card_data = CardDatabase.get_card(card_id)
|
||||||
|
if card_data:
|
||||||
|
detail_viewer.update_deck_count(current_deck.get_card_count(card_id))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_cleared() -> void:
|
||||||
|
if current_deck:
|
||||||
|
current_deck.clear()
|
||||||
|
detail_viewer.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_save_dialog() -> void:
|
||||||
|
var save_name_field = save_dialog.get_node("Panel/VBox/HBoxContainer/SaveNameField") as LineEdit
|
||||||
|
save_name_field.text = current_deck.name if current_deck else "New Deck"
|
||||||
|
save_dialog.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save_confirmed() -> void:
|
||||||
|
var save_name_field = save_dialog.get_node("Panel/VBox/HBoxContainer/SaveNameField") as LineEdit
|
||||||
|
var filename = save_name_field.text.strip_edges()
|
||||||
|
if filename.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
if current_deck:
|
||||||
|
current_deck.name = filename
|
||||||
|
deck_name_field.text = filename
|
||||||
|
if DeckManager.save_deck(current_deck, filename):
|
||||||
|
current_deck_filename = filename
|
||||||
|
print("Deck saved: ", filename)
|
||||||
|
else:
|
||||||
|
push_error("Failed to save deck")
|
||||||
|
|
||||||
|
save_dialog.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _show_load_dialog() -> void:
|
||||||
|
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
|
||||||
|
deck_list.clear()
|
||||||
|
|
||||||
|
for deck_name in DeckManager.list_decks():
|
||||||
|
deck_list.add_item(deck_name)
|
||||||
|
|
||||||
|
load_dialog.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_item_activated(index: int) -> void:
|
||||||
|
_on_load_confirmed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_load_confirmed() -> void:
|
||||||
|
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
|
||||||
|
var selected = deck_list.get_selected_items()
|
||||||
|
if selected.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var filename = deck_list.get_item_text(selected[0])
|
||||||
|
var loaded_deck = DeckManager.load_deck(filename)
|
||||||
|
if loaded_deck:
|
||||||
|
current_deck = loaded_deck
|
||||||
|
current_deck_filename = filename
|
||||||
|
deck_name_field.text = current_deck.name
|
||||||
|
deck_panel.set_deck(current_deck)
|
||||||
|
detail_viewer.clear()
|
||||||
|
print("Deck loaded: ", filename)
|
||||||
|
else:
|
||||||
|
push_error("Failed to load deck")
|
||||||
|
|
||||||
|
load_dialog.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_delete_deck() -> void:
|
||||||
|
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
|
||||||
|
var selected = deck_list.get_selected_items()
|
||||||
|
if selected.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var filename = deck_list.get_item_text(selected[0])
|
||||||
|
if DeckManager.delete_deck(filename):
|
||||||
|
deck_list.remove_item(selected[0])
|
||||||
|
print("Deck deleted: ", filename)
|
||||||
352
scripts/ui/DeckListPanel.gd
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
class_name DeckListPanel
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## DeckListPanel - Right panel showing deck contents with thumbnails and stats
|
||||||
|
|
||||||
|
signal card_clicked(card_id: String)
|
||||||
|
signal card_removed(card_id: String)
|
||||||
|
signal deck_cleared
|
||||||
|
|
||||||
|
const PANEL_WIDTH: float = 400.0
|
||||||
|
const CARD_WIDTH: float = 90.0
|
||||||
|
const CARD_HEIGHT: float = 126.0
|
||||||
|
const CARD_GAP: float = 4.0
|
||||||
|
const COLUMNS: int = 4
|
||||||
|
|
||||||
|
var current_deck: Deck = null
|
||||||
|
var card_cells: Dictionary = {} # card_id -> Control
|
||||||
|
|
||||||
|
# UI elements
|
||||||
|
var stats_panel: Control
|
||||||
|
var element_labels: Dictionary = {}
|
||||||
|
var type_labels: Dictionary = {}
|
||||||
|
var total_label: Label
|
||||||
|
var deck_scroll: ScrollContainer
|
||||||
|
var deck_grid: Control
|
||||||
|
var validation_label: Label
|
||||||
|
var clear_button: Button
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
custom_minimum_size = Vector2(PANEL_WIDTH, 0)
|
||||||
|
_create_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
panel.add_theme_stylebox_override("panel", _create_panel_style())
|
||||||
|
add_child(panel)
|
||||||
|
|
||||||
|
var main_vbox = VBoxContainer.new()
|
||||||
|
main_vbox.add_theme_constant_override("separation", 10)
|
||||||
|
panel.add_child(main_vbox)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
var header = Label.new()
|
||||||
|
header.text = "Deck"
|
||||||
|
header.add_theme_font_size_override("font_size", 18)
|
||||||
|
header.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
|
||||||
|
main_vbox.add_child(header)
|
||||||
|
|
||||||
|
# Stats panel
|
||||||
|
_create_stats_panel(main_vbox)
|
||||||
|
|
||||||
|
# Deck grid
|
||||||
|
deck_scroll = ScrollContainer.new()
|
||||||
|
deck_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
deck_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
main_vbox.add_child(deck_scroll)
|
||||||
|
|
||||||
|
deck_grid = Control.new()
|
||||||
|
deck_grid.custom_minimum_size = Vector2(COLUMNS * (CARD_WIDTH + CARD_GAP) - CARD_GAP, 0)
|
||||||
|
deck_scroll.add_child(deck_grid)
|
||||||
|
|
||||||
|
# Validation label
|
||||||
|
validation_label = Label.new()
|
||||||
|
validation_label.text = ""
|
||||||
|
validation_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
validation_label.add_theme_font_size_override("font_size", 12)
|
||||||
|
validation_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
|
||||||
|
main_vbox.add_child(validation_label)
|
||||||
|
|
||||||
|
# Clear deck button
|
||||||
|
clear_button = _create_styled_button("Clear Deck")
|
||||||
|
clear_button.pressed.connect(_on_clear_pressed)
|
||||||
|
main_vbox.add_child(clear_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_stats_panel(parent: Control) -> void:
|
||||||
|
stats_panel = PanelContainer.new()
|
||||||
|
var stats_style = StyleBoxFlat.new()
|
||||||
|
stats_style.bg_color = Color(0.12, 0.12, 0.16, 0.8)
|
||||||
|
stats_style.set_corner_radius_all(4)
|
||||||
|
stats_style.content_margin_left = 8
|
||||||
|
stats_style.content_margin_right = 8
|
||||||
|
stats_style.content_margin_top = 6
|
||||||
|
stats_style.content_margin_bottom = 6
|
||||||
|
stats_panel.add_theme_stylebox_override("panel", stats_style)
|
||||||
|
parent.add_child(stats_panel)
|
||||||
|
|
||||||
|
var stats_vbox = VBoxContainer.new()
|
||||||
|
stats_vbox.add_theme_constant_override("separation", 4)
|
||||||
|
stats_panel.add_child(stats_vbox)
|
||||||
|
|
||||||
|
# Total cards
|
||||||
|
total_label = Label.new()
|
||||||
|
total_label.text = "Cards: 0/50"
|
||||||
|
total_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
total_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
stats_vbox.add_child(total_label)
|
||||||
|
|
||||||
|
# Element breakdown
|
||||||
|
var elem_hbox = HBoxContainer.new()
|
||||||
|
elem_hbox.add_theme_constant_override("separation", 8)
|
||||||
|
stats_vbox.add_child(elem_hbox)
|
||||||
|
|
||||||
|
for element in Enums.Element.values():
|
||||||
|
var elem_container = HBoxContainer.new()
|
||||||
|
elem_container.add_theme_constant_override("separation", 2)
|
||||||
|
|
||||||
|
var elem_icon = ColorRect.new()
|
||||||
|
elem_icon.custom_minimum_size = Vector2(12, 12)
|
||||||
|
elem_icon.color = Enums.element_to_color(element)
|
||||||
|
elem_container.add_child(elem_icon)
|
||||||
|
|
||||||
|
var elem_label = Label.new()
|
||||||
|
elem_label.text = "0"
|
||||||
|
elem_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
elem_container.add_child(elem_label)
|
||||||
|
element_labels[element] = elem_label
|
||||||
|
|
||||||
|
elem_hbox.add_child(elem_container)
|
||||||
|
|
||||||
|
# Type breakdown
|
||||||
|
var type_hbox = HBoxContainer.new()
|
||||||
|
type_hbox.add_theme_constant_override("separation", 12)
|
||||||
|
stats_vbox.add_child(type_hbox)
|
||||||
|
|
||||||
|
for card_type in [Enums.CardType.FORWARD, Enums.CardType.BACKUP, Enums.CardType.SUMMON]:
|
||||||
|
var type_label = Label.new()
|
||||||
|
type_label.text = "%s: 0" % Enums.card_type_to_string(card_type).substr(0, 3)
|
||||||
|
type_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
type_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||||
|
type_hbox.add_child(type_label)
|
||||||
|
type_labels[card_type] = type_label
|
||||||
|
|
||||||
|
|
||||||
|
func _create_panel_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.08, 0.08, 0.12, 0.95)
|
||||||
|
style.border_color = Color(0.5, 0.4, 0.2)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(6)
|
||||||
|
style.content_margin_left = 12
|
||||||
|
style.content_margin_right = 12
|
||||||
|
style.content_margin_top = 12
|
||||||
|
style.content_margin_bottom = 12
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _create_styled_button(text: String) -> Button:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = text
|
||||||
|
button.custom_minimum_size = Vector2(0, 36)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = Color(0.4, 0.2, 0.2)
|
||||||
|
style_normal.border_color = Color(0.6, 0.3, 0.3)
|
||||||
|
style_normal.set_border_width_all(1)
|
||||||
|
style_normal.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = Color(0.5, 0.25, 0.25)
|
||||||
|
style_hover.border_color = Color(0.8, 0.4, 0.4)
|
||||||
|
style_hover.set_border_width_all(1)
|
||||||
|
style_hover.set_corner_radius_all(4)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
button.add_theme_font_size_override("font_size", 14)
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
## Set the deck to display
|
||||||
|
func set_deck(deck: Deck) -> void:
|
||||||
|
if current_deck:
|
||||||
|
current_deck.deck_changed.disconnect(_on_deck_changed)
|
||||||
|
|
||||||
|
current_deck = deck
|
||||||
|
if current_deck:
|
||||||
|
current_deck.deck_changed.connect(_on_deck_changed)
|
||||||
|
|
||||||
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_changed() -> void:
|
||||||
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_display() -> void:
|
||||||
|
# Clear existing cells
|
||||||
|
for cell in card_cells.values():
|
||||||
|
cell.queue_free()
|
||||||
|
card_cells.clear()
|
||||||
|
|
||||||
|
if not current_deck:
|
||||||
|
total_label.text = "Cards: 0/50"
|
||||||
|
validation_label.text = ""
|
||||||
|
_update_stats({})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get stats
|
||||||
|
var stats = current_deck.get_stats()
|
||||||
|
_update_stats(stats)
|
||||||
|
|
||||||
|
# Update total
|
||||||
|
total_label.text = "Cards: %d/50" % stats.total
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
var errors = current_deck.validate()
|
||||||
|
validation_label.text = "\n".join(errors)
|
||||||
|
|
||||||
|
# Create card cells
|
||||||
|
var card_ids = current_deck.get_card_ids()
|
||||||
|
card_ids.sort() # Sort alphabetically
|
||||||
|
|
||||||
|
var row = 0
|
||||||
|
var col = 0
|
||||||
|
for card_id in card_ids:
|
||||||
|
var count = current_deck.get_card_count(card_id)
|
||||||
|
var cell = _create_deck_cell(card_id, count)
|
||||||
|
cell.position = Vector2(col * (CARD_WIDTH + CARD_GAP), row * (CARD_HEIGHT + CARD_GAP))
|
||||||
|
deck_grid.add_child(cell)
|
||||||
|
card_cells[card_id] = cell
|
||||||
|
|
||||||
|
col += 1
|
||||||
|
if col >= COLUMNS:
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# Update grid size
|
||||||
|
var total_rows = ceili(float(card_ids.size()) / COLUMNS)
|
||||||
|
deck_grid.custom_minimum_size.y = total_rows * (CARD_HEIGHT + CARD_GAP)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_stats(stats: Dictionary) -> void:
|
||||||
|
# Update element counts
|
||||||
|
for element in element_labels:
|
||||||
|
var elem_name = Enums.element_to_string(element)
|
||||||
|
var count = stats.get("elements", {}).get(elem_name, 0)
|
||||||
|
element_labels[element].text = str(count)
|
||||||
|
|
||||||
|
# Update type counts
|
||||||
|
for card_type in type_labels:
|
||||||
|
var type_name = Enums.card_type_to_string(card_type)
|
||||||
|
var count = stats.get("types", {}).get(type_name, 0)
|
||||||
|
type_labels[card_type].text = "%s: %d" % [type_name.substr(0, 3), count]
|
||||||
|
|
||||||
|
|
||||||
|
func _create_deck_cell(card_id: String, count: int) -> Control:
|
||||||
|
var cell = Panel.new()
|
||||||
|
cell.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||||
|
cell.size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||||
|
cell.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
cell.set_meta("card_id", card_id)
|
||||||
|
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.15, 0.15, 0.2, 0.8)
|
||||||
|
style.border_color = Color(0.3, 0.3, 0.35)
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(2)
|
||||||
|
cell.add_theme_stylebox_override("panel", style)
|
||||||
|
|
||||||
|
# Card image
|
||||||
|
var card_data = CardDatabase.get_card(card_id)
|
||||||
|
if card_data:
|
||||||
|
var texture = CardDatabase.get_card_texture(card_data)
|
||||||
|
if texture:
|
||||||
|
var tex_rect = TextureRect.new()
|
||||||
|
tex_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
tex_rect.texture = texture
|
||||||
|
cell.add_child(tex_rect)
|
||||||
|
else:
|
||||||
|
var fallback = ColorRect.new()
|
||||||
|
fallback.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
fallback.color = Enums.element_to_color(card_data.get_primary_element()).darkened(0.3)
|
||||||
|
cell.add_child(fallback)
|
||||||
|
|
||||||
|
var name_label = Label.new()
|
||||||
|
name_label.text = card_data.name
|
||||||
|
name_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||||
|
name_label.add_theme_font_size_override("font_size", 8)
|
||||||
|
cell.add_child(name_label)
|
||||||
|
|
||||||
|
# Quantity badge
|
||||||
|
if count > 1:
|
||||||
|
var badge = Panel.new()
|
||||||
|
badge.size = Vector2(22, 22)
|
||||||
|
badge.position = Vector2(CARD_WIDTH - 26, 4)
|
||||||
|
var badge_style = StyleBoxFlat.new()
|
||||||
|
badge_style.bg_color = Color(0.8, 0.6, 0.2, 0.95)
|
||||||
|
badge_style.set_corner_radius_all(11)
|
||||||
|
badge.add_theme_stylebox_override("panel", badge_style)
|
||||||
|
cell.add_child(badge)
|
||||||
|
|
||||||
|
var badge_label = Label.new()
|
||||||
|
badge_label.text = "x%d" % count
|
||||||
|
badge_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
badge_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
badge_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
badge.add_child(badge_label)
|
||||||
|
|
||||||
|
# Remove button (X in top-left)
|
||||||
|
var remove_btn = Button.new()
|
||||||
|
remove_btn.text = "X"
|
||||||
|
remove_btn.size = Vector2(20, 20)
|
||||||
|
remove_btn.position = Vector2(4, 4)
|
||||||
|
remove_btn.flat = true
|
||||||
|
remove_btn.add_theme_font_size_override("font_size", 10)
|
||||||
|
remove_btn.add_theme_color_override("font_color", Color(1.0, 0.5, 0.5))
|
||||||
|
remove_btn.add_theme_color_override("font_hover_color", Color(1.0, 0.3, 0.3))
|
||||||
|
remove_btn.pressed.connect(_on_remove_card.bind(card_id))
|
||||||
|
remove_btn.visible = false
|
||||||
|
remove_btn.name = "RemoveBtn"
|
||||||
|
cell.add_child(remove_btn)
|
||||||
|
|
||||||
|
# Hover effects
|
||||||
|
cell.mouse_entered.connect(func():
|
||||||
|
remove_btn.visible = true
|
||||||
|
style.border_color = Color(0.6, 0.5, 0.3)
|
||||||
|
cell.add_theme_stylebox_override("panel", style)
|
||||||
|
)
|
||||||
|
cell.mouse_exited.connect(func():
|
||||||
|
remove_btn.visible = false
|
||||||
|
style.border_color = Color(0.3, 0.3, 0.35)
|
||||||
|
cell.add_theme_stylebox_override("panel", style)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Click to select
|
||||||
|
cell.gui_input.connect(func(event):
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
card_clicked.emit(card_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
|
||||||
|
|
||||||
|
func _on_remove_card(card_id: String) -> void:
|
||||||
|
card_removed.emit(card_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_clear_pressed() -> void:
|
||||||
|
deck_cleared.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Refresh display for a specific card (when count changes)
|
||||||
|
func refresh_card(card_id: String) -> void:
|
||||||
|
_refresh_display()
|
||||||
588
scripts/ui/GameSetupMenu.gd
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
class_name GameSetupMenu
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
## GameSetupMenu - Setup screen for configuring game before starting
|
||||||
|
## Allows selection of game type and decks for each player
|
||||||
|
|
||||||
|
signal back_pressed
|
||||||
|
signal start_game_requested(p1_deck: Array, p2_deck: Array)
|
||||||
|
|
||||||
|
const WINDOW_SIZE := Vector2(800, 600)
|
||||||
|
|
||||||
|
# UI Components
|
||||||
|
var background: PanelContainer
|
||||||
|
var main_vbox: VBoxContainer
|
||||||
|
var title_label: Label
|
||||||
|
var game_type_container: HBoxContainer
|
||||||
|
var game_type_dropdown: OptionButton
|
||||||
|
var players_container: HBoxContainer
|
||||||
|
var player1_panel: Control
|
||||||
|
var player2_panel: Control
|
||||||
|
var p1_deck_dropdown: OptionButton
|
||||||
|
var p2_deck_dropdown: OptionButton
|
||||||
|
var p1_preview: Control
|
||||||
|
var p2_preview: Control
|
||||||
|
var buttons_container: HBoxContainer
|
||||||
|
var start_button: Button
|
||||||
|
var back_button: Button
|
||||||
|
|
||||||
|
# Deck data
|
||||||
|
var saved_decks: Array[String] = []
|
||||||
|
var starter_decks: Array = [] # Array of StarterDeckData
|
||||||
|
var p1_selected_deck: Array = [] # Card IDs
|
||||||
|
var p2_selected_deck: Array = [] # Card IDs
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Set high layer to be on top of everything
|
||||||
|
layer = 100
|
||||||
|
_load_deck_options()
|
||||||
|
_create_ui()
|
||||||
|
_select_random_decks()
|
||||||
|
|
||||||
|
|
||||||
|
func _load_deck_options() -> void:
|
||||||
|
# Load saved decks
|
||||||
|
saved_decks = DeckManager.list_decks()
|
||||||
|
|
||||||
|
# Load starter decks
|
||||||
|
starter_decks = CardDatabase.get_starter_decks()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_ui() -> void:
|
||||||
|
# Background panel - use the window size constant
|
||||||
|
background = PanelContainer.new()
|
||||||
|
add_child(background)
|
||||||
|
background.position = Vector2.ZERO
|
||||||
|
background.size = WINDOW_SIZE
|
||||||
|
background.add_theme_stylebox_override("panel", _create_panel_style())
|
||||||
|
|
||||||
|
# Main vertical layout with padding
|
||||||
|
var margin = MarginContainer.new()
|
||||||
|
background.add_child(margin)
|
||||||
|
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
margin.add_theme_constant_override("margin_left", 25)
|
||||||
|
margin.add_theme_constant_override("margin_right", 25)
|
||||||
|
margin.add_theme_constant_override("margin_top", 15)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 15)
|
||||||
|
|
||||||
|
main_vbox = VBoxContainer.new()
|
||||||
|
margin.add_child(main_vbox)
|
||||||
|
main_vbox.add_theme_constant_override("separation", 8)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
_create_title()
|
||||||
|
|
||||||
|
# Game type selector
|
||||||
|
_create_game_type_selector()
|
||||||
|
|
||||||
|
# Player panels
|
||||||
|
_create_player_panels()
|
||||||
|
|
||||||
|
# Spacer
|
||||||
|
var spacer = Control.new()
|
||||||
|
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
main_vbox.add_child(spacer)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
_create_buttons()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_title() -> void:
|
||||||
|
title_label = Label.new()
|
||||||
|
title_label.text = "GAME SETUP"
|
||||||
|
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title_label.add_theme_font_size_override("font_size", 32)
|
||||||
|
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
|
||||||
|
main_vbox.add_child(title_label)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
var separator = HSeparator.new()
|
||||||
|
separator.add_theme_stylebox_override("separator", _create_separator_style())
|
||||||
|
main_vbox.add_child(separator)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_game_type_selector() -> void:
|
||||||
|
game_type_container = HBoxContainer.new()
|
||||||
|
game_type_container.add_theme_constant_override("separation", 15)
|
||||||
|
main_vbox.add_child(game_type_container)
|
||||||
|
|
||||||
|
var label = Label.new()
|
||||||
|
label.text = "Game Type:"
|
||||||
|
label.add_theme_font_size_override("font_size", 18)
|
||||||
|
label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
game_type_container.add_child(label)
|
||||||
|
|
||||||
|
game_type_dropdown = OptionButton.new()
|
||||||
|
game_type_dropdown.custom_minimum_size = Vector2(250, 36)
|
||||||
|
game_type_dropdown.add_item("2-Player Local (Share Screen)")
|
||||||
|
game_type_dropdown.add_item("vs AI (Coming Soon)")
|
||||||
|
game_type_dropdown.set_item_disabled(1, true)
|
||||||
|
game_type_dropdown.add_theme_font_size_override("font_size", 14)
|
||||||
|
_style_dropdown(game_type_dropdown)
|
||||||
|
game_type_container.add_child(game_type_dropdown)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_player_panels() -> void:
|
||||||
|
players_container = HBoxContainer.new()
|
||||||
|
players_container.add_theme_constant_override("separation", 20)
|
||||||
|
players_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
main_vbox.add_child(players_container)
|
||||||
|
|
||||||
|
player1_panel = _create_player_panel("PLAYER 1", 1)
|
||||||
|
players_container.add_child(player1_panel)
|
||||||
|
|
||||||
|
player2_panel = _create_player_panel("PLAYER 2", 2)
|
||||||
|
players_container.add_child(player2_panel)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_player_panel(title: String, player_num: int) -> Control:
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = Vector2(320, 280)
|
||||||
|
panel.add_theme_stylebox_override("panel", _create_player_panel_style())
|
||||||
|
|
||||||
|
var vbox = VBoxContainer.new()
|
||||||
|
vbox.add_theme_constant_override("separation", 8)
|
||||||
|
panel.add_child(vbox)
|
||||||
|
|
||||||
|
var margin = MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 12)
|
||||||
|
margin.add_theme_constant_override("margin_right", 12)
|
||||||
|
margin.add_theme_constant_override("margin_top", 8)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 8)
|
||||||
|
vbox.add_child(margin)
|
||||||
|
|
||||||
|
var inner_vbox = VBoxContainer.new()
|
||||||
|
inner_vbox.add_theme_constant_override("separation", 6)
|
||||||
|
margin.add_child(inner_vbox)
|
||||||
|
|
||||||
|
# Player title
|
||||||
|
var title_label = Label.new()
|
||||||
|
title_label.text = title
|
||||||
|
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
title_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
|
||||||
|
inner_vbox.add_child(title_label)
|
||||||
|
|
||||||
|
# Deck dropdown
|
||||||
|
var dropdown = OptionButton.new()
|
||||||
|
dropdown.custom_minimum_size = Vector2(300, 32)
|
||||||
|
dropdown.add_theme_font_size_override("font_size", 12)
|
||||||
|
_style_dropdown(dropdown)
|
||||||
|
_populate_deck_dropdown(dropdown)
|
||||||
|
dropdown.item_selected.connect(_on_deck_selected.bind(player_num))
|
||||||
|
inner_vbox.add_child(dropdown)
|
||||||
|
|
||||||
|
if player_num == 1:
|
||||||
|
p1_deck_dropdown = dropdown
|
||||||
|
else:
|
||||||
|
p2_deck_dropdown = dropdown
|
||||||
|
|
||||||
|
# Deck preview panel (with box art)
|
||||||
|
var preview = _create_deck_preview(player_num)
|
||||||
|
inner_vbox.add_child(preview)
|
||||||
|
|
||||||
|
if player_num == 1:
|
||||||
|
p1_preview = preview
|
||||||
|
else:
|
||||||
|
p2_preview = preview
|
||||||
|
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
func _create_deck_preview(player_num: int) -> Control:
|
||||||
|
var panel = PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = Vector2(280, 170)
|
||||||
|
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.06, 0.06, 0.1, 0.8)
|
||||||
|
style.border_color = Color(0.3, 0.3, 0.35)
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(4)
|
||||||
|
style.content_margin_left = 8
|
||||||
|
style.content_margin_right = 8
|
||||||
|
style.content_margin_top = 6
|
||||||
|
style.content_margin_bottom = 6
|
||||||
|
panel.add_theme_stylebox_override("panel", style)
|
||||||
|
|
||||||
|
var vbox = VBoxContainer.new()
|
||||||
|
vbox.name = "VBoxContainer"
|
||||||
|
vbox.add_theme_constant_override("separation", 4)
|
||||||
|
panel.add_child(vbox)
|
||||||
|
|
||||||
|
# Box art container (centered)
|
||||||
|
var art_container = CenterContainer.new()
|
||||||
|
art_container.name = "ArtContainer"
|
||||||
|
art_container.custom_minimum_size = Vector2(260, 90)
|
||||||
|
vbox.add_child(art_container)
|
||||||
|
|
||||||
|
# Box art image
|
||||||
|
var box_art = TextureRect.new()
|
||||||
|
box_art.name = "BoxArt"
|
||||||
|
box_art.custom_minimum_size = Vector2(90, 85)
|
||||||
|
box_art.expand_mode = TextureRect.EXPAND_FIT_HEIGHT_PROPORTIONAL
|
||||||
|
box_art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
art_container.add_child(box_art)
|
||||||
|
|
||||||
|
# Placeholder when no art available
|
||||||
|
var placeholder = ColorRect.new()
|
||||||
|
placeholder.name = "Placeholder"
|
||||||
|
placeholder.custom_minimum_size = Vector2(60, 85)
|
||||||
|
placeholder.color = Color(0.15, 0.15, 0.2, 0.5)
|
||||||
|
placeholder.visible = true
|
||||||
|
art_container.add_child(placeholder)
|
||||||
|
|
||||||
|
# Placeholder icon/text
|
||||||
|
var placeholder_label = Label.new()
|
||||||
|
placeholder_label.text = "?"
|
||||||
|
placeholder_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
placeholder_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
placeholder_label.add_theme_font_size_override("font_size", 48)
|
||||||
|
placeholder_label.add_theme_color_override("font_color", Color(0.3, 0.3, 0.35))
|
||||||
|
placeholder.add_child(placeholder_label)
|
||||||
|
placeholder_label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
|
||||||
|
# Deck name
|
||||||
|
var name_label = Label.new()
|
||||||
|
name_label.name = "DeckName"
|
||||||
|
name_label.text = "No deck selected"
|
||||||
|
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
name_label.add_theme_font_size_override("font_size", 14)
|
||||||
|
name_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
vbox.add_child(name_label)
|
||||||
|
|
||||||
|
# Elements row (centered)
|
||||||
|
var elements_center = CenterContainer.new()
|
||||||
|
elements_center.name = "ElementsCenter"
|
||||||
|
vbox.add_child(elements_center)
|
||||||
|
|
||||||
|
var elements_hbox = HBoxContainer.new()
|
||||||
|
elements_hbox.name = "ElementsRow"
|
||||||
|
elements_hbox.add_theme_constant_override("separation", 6)
|
||||||
|
elements_center.add_child(elements_hbox)
|
||||||
|
|
||||||
|
# Info row (card count + description)
|
||||||
|
var info_hbox = HBoxContainer.new()
|
||||||
|
info_hbox.name = "InfoRow"
|
||||||
|
info_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
info_hbox.add_theme_constant_override("separation", 10)
|
||||||
|
vbox.add_child(info_hbox)
|
||||||
|
|
||||||
|
# Card count
|
||||||
|
var count_label = Label.new()
|
||||||
|
count_label.name = "CardCount"
|
||||||
|
count_label.text = "0 cards"
|
||||||
|
count_label.add_theme_font_size_override("font_size", 11)
|
||||||
|
count_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
|
||||||
|
info_hbox.add_child(count_label)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
var sep = Label.new()
|
||||||
|
sep.text = "•"
|
||||||
|
sep.add_theme_font_size_override("font_size", 11)
|
||||||
|
sep.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||||
|
info_hbox.add_child(sep)
|
||||||
|
|
||||||
|
# Description
|
||||||
|
var desc_label = Label.new()
|
||||||
|
desc_label.name = "Description"
|
||||||
|
desc_label.text = ""
|
||||||
|
desc_label.add_theme_font_size_override("font_size", 11)
|
||||||
|
desc_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.55))
|
||||||
|
info_hbox.add_child(desc_label)
|
||||||
|
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
func _populate_deck_dropdown(dropdown: OptionButton) -> void:
|
||||||
|
dropdown.clear()
|
||||||
|
|
||||||
|
# Add "My Decks" section if there are saved decks
|
||||||
|
if not saved_decks.is_empty():
|
||||||
|
dropdown.add_separator("-- My Decks --")
|
||||||
|
for deck_name in saved_decks:
|
||||||
|
dropdown.add_item(deck_name)
|
||||||
|
dropdown.set_item_metadata(dropdown.get_item_count() - 1, {"type": "saved", "name": deck_name})
|
||||||
|
|
||||||
|
# Add "Starter Decks" section
|
||||||
|
dropdown.add_separator("-- Starter Decks --")
|
||||||
|
for starter_deck in starter_decks:
|
||||||
|
var display_name = "%s (%s)" % [starter_deck.name, starter_deck.opus]
|
||||||
|
dropdown.add_item(display_name)
|
||||||
|
dropdown.set_item_metadata(dropdown.get_item_count() - 1, {"type": "starter", "id": starter_deck.id})
|
||||||
|
|
||||||
|
|
||||||
|
func _select_random_decks() -> void:
|
||||||
|
# Select random starter decks for both players
|
||||||
|
if starter_decks.size() >= 2:
|
||||||
|
var indices = range(starter_decks.size())
|
||||||
|
indices.shuffle()
|
||||||
|
|
||||||
|
var p1_index = _find_dropdown_index_for_starter(p1_deck_dropdown, starter_decks[indices[0]].id)
|
||||||
|
var p2_index = _find_dropdown_index_for_starter(p2_deck_dropdown, starter_decks[indices[1]].id)
|
||||||
|
|
||||||
|
if p1_index >= 0:
|
||||||
|
p1_deck_dropdown.select(p1_index)
|
||||||
|
_on_deck_selected(p1_index, 1)
|
||||||
|
|
||||||
|
if p2_index >= 0:
|
||||||
|
p2_deck_dropdown.select(p2_index)
|
||||||
|
_on_deck_selected(p2_index, 2)
|
||||||
|
elif starter_decks.size() == 1:
|
||||||
|
var index = _find_dropdown_index_for_starter(p1_deck_dropdown, starter_decks[0].id)
|
||||||
|
if index >= 0:
|
||||||
|
p1_deck_dropdown.select(index)
|
||||||
|
_on_deck_selected(index, 1)
|
||||||
|
p2_deck_dropdown.select(index)
|
||||||
|
_on_deck_selected(index, 2)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_dropdown_index_for_starter(dropdown: OptionButton, starter_id: String) -> int:
|
||||||
|
for i in range(dropdown.get_item_count()):
|
||||||
|
var meta = dropdown.get_item_metadata(i)
|
||||||
|
if meta is Dictionary and meta.get("type") == "starter" and meta.get("id") == starter_id:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_selected(index: int, player_num: int) -> void:
|
||||||
|
var dropdown = p1_deck_dropdown if player_num == 1 else p2_deck_dropdown
|
||||||
|
var preview = p1_preview if player_num == 1 else p2_preview
|
||||||
|
|
||||||
|
var meta = dropdown.get_item_metadata(index)
|
||||||
|
if not meta is Dictionary:
|
||||||
|
return
|
||||||
|
|
||||||
|
var deck_cards: Array = []
|
||||||
|
var deck_name: String = ""
|
||||||
|
var deck_elements: Array = []
|
||||||
|
var deck_description: String = ""
|
||||||
|
var deck_texture: Texture2D = null
|
||||||
|
|
||||||
|
if meta.get("type") == "saved":
|
||||||
|
var deck = DeckManager.load_deck(meta.get("name"))
|
||||||
|
if deck:
|
||||||
|
deck_cards = deck.to_card_array()
|
||||||
|
deck_name = deck.name
|
||||||
|
deck_elements = _get_elements_from_deck(deck_cards)
|
||||||
|
deck_description = "Custom deck"
|
||||||
|
# No box art for custom decks
|
||||||
|
elif meta.get("type") == "starter":
|
||||||
|
var starter = CardDatabase.get_starter_deck(meta.get("id"))
|
||||||
|
if starter:
|
||||||
|
deck_cards = starter.cards.duplicate()
|
||||||
|
deck_name = starter.name
|
||||||
|
deck_elements = starter.elements
|
||||||
|
deck_description = starter.description
|
||||||
|
deck_texture = starter.get_texture()
|
||||||
|
|
||||||
|
# Store selected deck
|
||||||
|
if player_num == 1:
|
||||||
|
p1_selected_deck = deck_cards
|
||||||
|
else:
|
||||||
|
p2_selected_deck = deck_cards
|
||||||
|
|
||||||
|
# Update preview
|
||||||
|
_update_preview(preview, deck_name, deck_elements, deck_cards.size(), deck_description, deck_texture)
|
||||||
|
|
||||||
|
# Update start button state
|
||||||
|
_update_start_button()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_elements_from_deck(card_ids: Array) -> Array:
|
||||||
|
var elements: Dictionary = {}
|
||||||
|
for card_id in card_ids:
|
||||||
|
var card = CardDatabase.get_card(card_id)
|
||||||
|
if card:
|
||||||
|
for element in card.elements:
|
||||||
|
var elem_name = Enums.element_to_string(element)
|
||||||
|
elements[elem_name] = elements.get(elem_name, 0) + 1
|
||||||
|
|
||||||
|
# Sort by count and return top elements
|
||||||
|
var sorted_elements: Array = []
|
||||||
|
for elem_name in elements.keys():
|
||||||
|
sorted_elements.append({"name": elem_name, "count": elements[elem_name]})
|
||||||
|
sorted_elements.sort_custom(func(a, b): return a.count > b.count)
|
||||||
|
|
||||||
|
var result: Array = []
|
||||||
|
for i in range(mini(2, sorted_elements.size())):
|
||||||
|
result.append(sorted_elements[i].name)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _update_preview(preview: Control, deck_name: String, elements: Array, card_count: int, description: String, texture: Texture2D = null) -> void:
|
||||||
|
var box_art = preview.get_node_or_null("VBoxContainer/ArtContainer/BoxArt") as TextureRect
|
||||||
|
var placeholder = preview.get_node_or_null("VBoxContainer/ArtContainer/Placeholder") as ColorRect
|
||||||
|
var name_label = preview.get_node_or_null("VBoxContainer/DeckName") as Label
|
||||||
|
var elements_row = preview.get_node_or_null("VBoxContainer/ElementsCenter/ElementsRow") as HBoxContainer
|
||||||
|
var count_label = preview.get_node_or_null("VBoxContainer/InfoRow/CardCount") as Label
|
||||||
|
var desc_label = preview.get_node_or_null("VBoxContainer/InfoRow/Description") as Label
|
||||||
|
|
||||||
|
# Update box art
|
||||||
|
if box_art and placeholder:
|
||||||
|
if texture:
|
||||||
|
box_art.texture = texture
|
||||||
|
box_art.visible = true
|
||||||
|
placeholder.visible = false
|
||||||
|
else:
|
||||||
|
box_art.texture = null
|
||||||
|
box_art.visible = false
|
||||||
|
placeholder.visible = true
|
||||||
|
|
||||||
|
if name_label:
|
||||||
|
name_label.text = deck_name if not deck_name.is_empty() else "No deck selected"
|
||||||
|
|
||||||
|
if elements_row:
|
||||||
|
# Clear existing elements
|
||||||
|
for child in elements_row.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
|
||||||
|
# Add element indicators
|
||||||
|
for elem_name in elements:
|
||||||
|
var elem_container = HBoxContainer.new()
|
||||||
|
elem_container.add_theme_constant_override("separation", 4)
|
||||||
|
|
||||||
|
var color_rect = ColorRect.new()
|
||||||
|
color_rect.custom_minimum_size = Vector2(12, 12)
|
||||||
|
var element = Enums.element_from_string(elem_name)
|
||||||
|
color_rect.color = Enums.element_to_color(element)
|
||||||
|
elem_container.add_child(color_rect)
|
||||||
|
|
||||||
|
var elem_label = Label.new()
|
||||||
|
elem_label.text = elem_name
|
||||||
|
elem_label.add_theme_font_size_override("font_size", 11)
|
||||||
|
elem_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
|
||||||
|
elem_container.add_child(elem_label)
|
||||||
|
|
||||||
|
elements_row.add_child(elem_container)
|
||||||
|
|
||||||
|
if count_label:
|
||||||
|
count_label.text = "%d cards" % card_count
|
||||||
|
|
||||||
|
if desc_label:
|
||||||
|
desc_label.text = description
|
||||||
|
|
||||||
|
|
||||||
|
func _update_start_button() -> void:
|
||||||
|
# Require at least 1 card in each deck to start (relaxed from 50 for testing with incomplete card databases)
|
||||||
|
var can_start = p1_selected_deck.size() >= 1 and p2_selected_deck.size() >= 1
|
||||||
|
start_button.disabled = not can_start
|
||||||
|
|
||||||
|
if can_start:
|
||||||
|
start_button.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
else:
|
||||||
|
start_button.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||||
|
|
||||||
|
|
||||||
|
func _create_buttons() -> void:
|
||||||
|
buttons_container = HBoxContainer.new()
|
||||||
|
buttons_container.add_theme_constant_override("separation", 20)
|
||||||
|
buttons_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
main_vbox.add_child(buttons_container)
|
||||||
|
|
||||||
|
back_button = _create_styled_button("Back", Color(0.3, 0.25, 0.25))
|
||||||
|
back_button.pressed.connect(_on_back_pressed)
|
||||||
|
buttons_container.add_child(back_button)
|
||||||
|
|
||||||
|
start_button = _create_styled_button("Start Game", Color(0.2, 0.35, 0.25))
|
||||||
|
start_button.custom_minimum_size.x = 180
|
||||||
|
start_button.pressed.connect(_on_start_pressed)
|
||||||
|
start_button.disabled = true
|
||||||
|
buttons_container.add_child(start_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_styled_button(text: String, base_color: Color) -> Button:
|
||||||
|
var button = Button.new()
|
||||||
|
button.text = text
|
||||||
|
button.custom_minimum_size = Vector2(140, 44)
|
||||||
|
button.add_theme_font_size_override("font_size", 16)
|
||||||
|
|
||||||
|
var style_normal = StyleBoxFlat.new()
|
||||||
|
style_normal.bg_color = base_color
|
||||||
|
style_normal.border_color = Color(0.5, 0.4, 0.2)
|
||||||
|
style_normal.set_border_width_all(2)
|
||||||
|
style_normal.set_corner_radius_all(6)
|
||||||
|
button.add_theme_stylebox_override("normal", style_normal)
|
||||||
|
|
||||||
|
var style_hover = StyleBoxFlat.new()
|
||||||
|
style_hover.bg_color = base_color.lightened(0.15)
|
||||||
|
style_hover.border_color = Color(0.7, 0.55, 0.3)
|
||||||
|
style_hover.set_border_width_all(2)
|
||||||
|
style_hover.set_corner_radius_all(6)
|
||||||
|
button.add_theme_stylebox_override("hover", style_hover)
|
||||||
|
|
||||||
|
var style_pressed = StyleBoxFlat.new()
|
||||||
|
style_pressed.bg_color = base_color.darkened(0.1)
|
||||||
|
style_pressed.border_color = Color(0.5, 0.4, 0.2)
|
||||||
|
style_pressed.set_border_width_all(2)
|
||||||
|
style_pressed.set_corner_radius_all(6)
|
||||||
|
button.add_theme_stylebox_override("pressed", style_pressed)
|
||||||
|
|
||||||
|
var style_disabled = StyleBoxFlat.new()
|
||||||
|
style_disabled.bg_color = Color(0.15, 0.15, 0.18)
|
||||||
|
style_disabled.border_color = Color(0.3, 0.3, 0.3)
|
||||||
|
style_disabled.set_border_width_all(2)
|
||||||
|
style_disabled.set_corner_radius_all(6)
|
||||||
|
button.add_theme_stylebox_override("disabled", style_disabled)
|
||||||
|
|
||||||
|
button.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
button.add_theme_color_override("font_hover_color", Color(1.0, 0.95, 0.8))
|
||||||
|
button.add_theme_color_override("font_pressed_color", Color(0.7, 0.65, 0.55))
|
||||||
|
button.add_theme_color_override("font_disabled_color", Color(0.4, 0.4, 0.4))
|
||||||
|
|
||||||
|
return button
|
||||||
|
|
||||||
|
|
||||||
|
func _style_dropdown(dropdown: OptionButton) -> void:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.12, 0.12, 0.16)
|
||||||
|
style.border_color = Color(0.4, 0.35, 0.25)
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(4)
|
||||||
|
style.content_margin_left = 10
|
||||||
|
style.content_margin_right = 10
|
||||||
|
style.content_margin_top = 6
|
||||||
|
style.content_margin_bottom = 6
|
||||||
|
|
||||||
|
dropdown.add_theme_stylebox_override("normal", style)
|
||||||
|
|
||||||
|
var hover_style = style.duplicate()
|
||||||
|
hover_style.border_color = Color(0.6, 0.5, 0.3)
|
||||||
|
dropdown.add_theme_stylebox_override("hover", hover_style)
|
||||||
|
|
||||||
|
dropdown.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
|
||||||
|
dropdown.add_theme_color_override("font_hover_color", Color(1.0, 0.95, 0.8))
|
||||||
|
|
||||||
|
|
||||||
|
func _create_panel_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.08, 0.08, 0.12, 1.0)
|
||||||
|
# No border on the outer panel to avoid gaps at window edges
|
||||||
|
style.set_border_width_all(0)
|
||||||
|
style.set_corner_radius_all(0)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _create_player_panel_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.1, 0.1, 0.14, 0.9)
|
||||||
|
style.border_color = Color(0.4, 0.35, 0.25)
|
||||||
|
style.set_border_width_all(2)
|
||||||
|
style.set_corner_radius_all(6)
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _create_separator_style() -> StyleBoxFlat:
|
||||||
|
var style = StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.5, 0.4, 0.2, 0.5)
|
||||||
|
style.content_margin_top = 1
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_pressed() -> void:
|
||||||
|
back_pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_start_pressed() -> void:
|
||||||
|
if p1_selected_deck.size() >= 1 and p2_selected_deck.size() >= 1:
|
||||||
|
start_game_requested.emit(p1_selected_deck, p2_selected_deck)
|
||||||
@@ -5,8 +5,8 @@ extends CanvasLayer
|
|||||||
## The window is sized to match the image (67% of 1024x1536).
|
## The window is sized to match the image (67% of 1024x1536).
|
||||||
## The image fills the entire window; buttons overlay the pre-drawn slots.
|
## The image fills the entire window; buttons overlay the pre-drawn slots.
|
||||||
|
|
||||||
signal quick_play
|
|
||||||
signal play_game
|
signal play_game
|
||||||
|
signal deck_builder
|
||||||
signal online_game
|
signal online_game
|
||||||
signal open_settings
|
signal open_settings
|
||||||
signal quit_game
|
signal quit_game
|
||||||
@@ -14,8 +14,8 @@ signal quit_game
|
|||||||
# UI Components
|
# UI Components
|
||||||
var bg_texture: TextureRect
|
var bg_texture: TextureRect
|
||||||
var buttons_container: Control
|
var buttons_container: Control
|
||||||
var quick_play_button: Button
|
|
||||||
var play_button: Button
|
var play_button: Button
|
||||||
|
var deck_builder_button: Button
|
||||||
var online_button: Button
|
var online_button: Button
|
||||||
var settings_button: Button
|
var settings_button: Button
|
||||||
var quit_button: Button
|
var quit_button: Button
|
||||||
@@ -64,15 +64,17 @@ func _create_menu() -> void:
|
|||||||
buttons_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
buttons_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
|
||||||
# Create buttons overlaying the pre-drawn slots
|
# Create buttons overlaying the pre-drawn slots
|
||||||
quick_play_button = _create_overlay_button("Quick Play", 0)
|
# "Play" is now in the top golden slot (formerly Quick Play)
|
||||||
quick_play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1))
|
play_button = _create_overlay_button("Play", 0)
|
||||||
quick_play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2))
|
play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1))
|
||||||
quick_play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05))
|
play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2))
|
||||||
quick_play_button.pressed.connect(_on_quick_play_pressed)
|
play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05))
|
||||||
|
|
||||||
play_button = _create_overlay_button("Play", 1)
|
|
||||||
play_button.pressed.connect(_on_play_pressed)
|
play_button.pressed.connect(_on_play_pressed)
|
||||||
|
|
||||||
|
# "Deck Builder" is in slot 1 (formerly Play)
|
||||||
|
deck_builder_button = _create_overlay_button("Deck Builder", 1)
|
||||||
|
deck_builder_button.pressed.connect(_on_deck_builder_pressed)
|
||||||
|
|
||||||
online_button = _create_overlay_button("Online", 2)
|
online_button = _create_overlay_button("Online", 2)
|
||||||
online_button.disabled = true
|
online_button.disabled = true
|
||||||
|
|
||||||
@@ -158,12 +160,14 @@ func _reposition_elements() -> void:
|
|||||||
version_label.position = Vector2(win_size.x - 80, win_size.y - 24)
|
version_label.position = Vector2(win_size.x - 80, win_size.y - 24)
|
||||||
version_label.size = Vector2(72, 18)
|
version_label.size = Vector2(72, 18)
|
||||||
|
|
||||||
func _on_quick_play_pressed() -> void:
|
|
||||||
quick_play.emit()
|
|
||||||
|
|
||||||
func _on_play_pressed() -> void:
|
func _on_play_pressed() -> void:
|
||||||
play_game.emit()
|
play_game.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_deck_builder_pressed() -> void:
|
||||||
|
deck_builder.emit()
|
||||||
|
|
||||||
|
|
||||||
func _on_quit_pressed() -> void:
|
func _on_quit_pressed() -> void:
|
||||||
quit_game.emit()
|
quit_game.emit()
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|||||||
BIN
sleeve_1.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
34
sleeve_1.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://veaodhdkku1k"
|
||||||
|
path="res://.godot/imported/sleeve_1.jpg-828b14defa3c890edfb04fc7152753e3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_1.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_1.jpg-828b14defa3c890edfb04fc7152753e3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_10.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
34
sleeve_10.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bubjmb0ttbqxj"
|
||||||
|
path="res://.godot/imported/sleeve_10.jpg-6b87b4206fd640015f62418b5be0e3e2.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_10.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_10.jpg-6b87b4206fd640015f62418b5be0e3e2.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_11.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
34
sleeve_11.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bkf848sknpryw"
|
||||||
|
path="res://.godot/imported/sleeve_11.jpg-4cef09584868eb833e55f6634e82a354.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_11.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_11.jpg-4cef09584868eb833e55f6634e82a354.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_12.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
34
sleeve_12.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://tgpxll7kpfj6"
|
||||||
|
path="res://.godot/imported/sleeve_12.jpg-ef513d625ecbc7b02b21ece8fe3a9a16.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_12.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_12.jpg-ef513d625ecbc7b02b21ece8fe3a9a16.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_13.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
34
sleeve_13.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ranltlld65ux"
|
||||||
|
path="res://.godot/imported/sleeve_13.jpg-bd0d7a3a4c382a780f8f7512508b549b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_13.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_13.jpg-bd0d7a3a4c382a780f8f7512508b549b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_14.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
34
sleeve_14.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://de50t4jcgekyv"
|
||||||
|
path="res://.godot/imported/sleeve_14.jpg-72257451e81f7010f799e78e9ec04bda.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_14.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_14.jpg-72257451e81f7010f799e78e9ec04bda.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_15.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
34
sleeve_15.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dwg5gavovr0nt"
|
||||||
|
path="res://.godot/imported/sleeve_15.jpg-39cedca8328e316d43325603d5de5231.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_15.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_15.jpg-39cedca8328e316d43325603d5de5231.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_16.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
34
sleeve_16.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cm26mk2356ah"
|
||||||
|
path="res://.godot/imported/sleeve_16.jpg-6356cf830caef42a0fbd7965f94b0312.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_16.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_16.jpg-6356cf830caef42a0fbd7965f94b0312.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_17.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
34
sleeve_17.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bye24efb4lleh"
|
||||||
|
path="res://.godot/imported/sleeve_17.jpg-6558905b1e7e452217a009aa42b920f0.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_17.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_17.jpg-6558905b1e7e452217a009aa42b920f0.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_18.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
34
sleeve_18.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dkexuqjst7num"
|
||||||
|
path="res://.godot/imported/sleeve_18.jpg-63ea6753a4b8d6914d0eb322bfa97eb4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_18.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_18.jpg-63ea6753a4b8d6914d0eb322bfa97eb4.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_19.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
34
sleeve_19.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ccwr0ua4jdq1t"
|
||||||
|
path="res://.godot/imported/sleeve_19.jpg-2e762cf830e4b4bcc6a7d3aa40eacc28.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_19.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_19.jpg-2e762cf830e4b4bcc6a7d3aa40eacc28.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_2.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
34
sleeve_2.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cw7af16yj7uyd"
|
||||||
|
path="res://.godot/imported/sleeve_2.jpg-918e652ebbe97a6ae6923ec8d32b61b6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_2.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_2.jpg-918e652ebbe97a6ae6923ec8d32b61b6.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_20.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
34
sleeve_20.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b61aahm268m2c"
|
||||||
|
path="res://.godot/imported/sleeve_20.jpg-1a1b47d2cd041a2b2388e47624b38d37.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_20.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_20.jpg-1a1b47d2cd041a2b2388e47624b38d37.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_21.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
34
sleeve_21.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b4qenbuuge8rw"
|
||||||
|
path="res://.godot/imported/sleeve_21.jpg-c77170263b749b40aca5104b25bdfc36.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_21.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_21.jpg-c77170263b749b40aca5104b25bdfc36.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_3.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
34
sleeve_3.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://its8ysjdrsu7"
|
||||||
|
path="res://.godot/imported/sleeve_3.jpg-99f1dda00ab7d9cca2ab4d88b06f882c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_3.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_3.jpg-99f1dda00ab7d9cca2ab4d88b06f882c.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_4.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
34
sleeve_4.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://csly4wn2b6kff"
|
||||||
|
path="res://.godot/imported/sleeve_4.jpg-cf00333aa851a3e65cdf4b8440a58ede.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_4.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_4.jpg-cf00333aa851a3e65cdf4b8440a58ede.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_5.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
34
sleeve_5.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cp2t642p0n8re"
|
||||||
|
path="res://.godot/imported/sleeve_5.jpg-b290d795e98200ea69bfd2e27322cd2a.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_5.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_5.jpg-b290d795e98200ea69bfd2e27322cd2a.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_6.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
34
sleeve_6.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d0at1y6pqfvhv"
|
||||||
|
path="res://.godot/imported/sleeve_6.jpg-2ce2153e9c3c63195efaaca0d51ffe06.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_6.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_6.jpg-2ce2153e9c3c63195efaaca0d51ffe06.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_7.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
34
sleeve_7.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c5h6lwuu0kkb8"
|
||||||
|
path="res://.godot/imported/sleeve_7.jpg-f27c4a176ec9ff9b860a6e6b14d3e1c3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_7.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_7.jpg-f27c4a176ec9ff9b860a6e6b14d3e1c3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_8.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
34
sleeve_8.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dgkd26svc1ly4"
|
||||||
|
path="res://.godot/imported/sleeve_8.jpg-3707d20d01dbad6e64ac2f60b37eabac.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_8.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_8.jpg-3707d20d01dbad6e64ac2f60b37eabac.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
sleeve_9.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
34
sleeve_9.jpg.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cmt8u6q7gved7"
|
||||||
|
path="res://.godot/imported/sleeve_9.jpg-728b2039981a320830826ca572949dc3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sleeve_9.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeve_9.jpg-728b2039981a320830826ca572949dc3.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||