diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 29f3db0..ef590d8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,7 +32,11 @@ "Bash(git -C /home/ckoch/Documents/Development/FFCardGame log --oneline -20)", "Bash(git clone:*)", "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:*)" ] } } diff --git a/.godot/editor/editor_layout.cfg b/.godot/editor/editor_layout.cfg index 823edfe..d1a9518 100644 --- a/.godot/editor/editor_layout.cfg +++ b/.godot/editor/editor_layout.cfg @@ -12,14 +12,14 @@ dock_floating={} dock_split_2=0 dock_split_3=0 dock_hsplit_1=0 -dock_hsplit_2=540 +dock_hsplit_2=1040 dock_hsplit_3=-540 dock_hsplit_4=0 dock_filesystem_split=0 dock_filesystem_display_mode=0 dock_filesystem_file_sort=0 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_3="Scene,Import" dock_4="FileSystem" @@ -30,14 +30,14 @@ dock_5="Inspector,Node,History" open_scenes=PackedStringArray("res://scenes/main.tscn") current_scene="res://scenes/main.tscn" center_split_offset=-491 -selected_default_debugger_tab_idx=1 +selected_default_debugger_tab_idx=0 selected_main_editor_idx=2 -selected_bottom_panel_item=1 +selected_bottom_panel_item=0 [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"] -selected_script="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/ui/GameSetupMenu.gd" open_help=[] script_split_offset=140 list_split_offset=0 diff --git a/.godot/editor/filesystem_cache8 b/.godot/editor/filesystem_cache8 index 3f0d438..1c41a04 100644 --- a/.godot/editor/filesystem_cache8 +++ b/.godot/editor/filesystem_cache8 @@ -1,5 +1,5 @@ ea4bc82a6ad023ab7ee23ee620429895 -::res://::1769613134 +::res://::1769647407 background_1.png::CompressedTexture2D::259206091835802070::1769464008::1769464212::1::::<><>:: card_back.png::CompressedTexture2D::4833498016096001590::1769466370::1769466517::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::::<><>:: 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::::<><>:: +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::::<><>:: ::res://addons/::1769611855 ::res://addons/gut/::1769611855 @@ -127,44 +148,63 @@ card_back.png::CompressedTexture2D::7787125851359297441::1769466418::1769466517: ::res://assets/table/::1769464212 background_1.png::CompressedTexture2D::102728058489724503::1769464097::1769464212::1::::<><>:: playmat.webp::CompressedTexture2D::3235866490631872101::1769279471::1769280957::1::::<><>:: -::res://assets/ui/::1769542991 +::res://assets/ui/::1769638945 icon.svg::CompressedTexture2D::2912283608529879130::1769280588::1769280956::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_progress.json::JSON::-1::1769539572::0::1::::<><>:: scan_errors.log::TextFile::-1::1769539203::0::1::::<><>:: +starter_decks.json::JSON::-1::1769639015::0::1::::<><>:: ::res://docs/::1769279608 CARD_FORMAT.md::TextFile::-1::1769279608::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 -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/main/::1769279430 ::res://scenes/table/::1769279430 ::res://scenes/ui/::1769279430 -::res://scripts/::1769558794 -GameController.gd::GDScript::-1::1769557430::0::1::::<>Node<>:: -Main.gd::GDScript::-1::1769558794::0::1::::<>Node3D<>:: -::res://scripts/autoload/::1769308378 -CardDatabase.gd::GDScript::-1::1769308329::0::1::::<>Node<>:: -GameManager.gd::GDScript::-1::1769308378::0::1::::<>Node<>:: -::res://scripts/game/::1769471419 +::res://scripts/::1769645475 +GameController.gd::GDScript::-1::1769645475::0::1::::<>Node<>:: +Main.gd::GDScript::-1::1769627747::0::1::::<>Node3D<>:: +::res://scripts/autoload/::1769639030 +CardDatabase.gd::GDScript::-1::1769639030::0::1::::<>Node<>:: +GameManager.gd::GDScript::-1::1769627732::0::1::::<>Node<>:: +::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<>:: 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<>:: Player.gd::GDScript::-1::1769302256::0::1::::Player<>RefCounted<>:: TurnManager.gd::GDScript::-1::1769302284::0::1::::TurnManager<>RefCounted<>:: UndoSystem.gd::GDScript::-1::1769301595::0::1::::UndoSystem<>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<>:: +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<>:: +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<>:: 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<>:: ::res://scripts/visual/::1769471729 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-200L-19-128L.jpg::CompressedTexture2D::8097057901487536182::1769306356::1769308614::1::::<><>:: ::res://tests/::1769610959 -::res://tests/fixtures/::1769610993 -test_card_data.gd::GDScript::-1::1769610993::0::1::::TestCardData<>RefCounted<>:: -::res://tests/integration/::1769611345 -test_game_state.gd::GDScript::-1::1769611345::0::1::::<>GutTest<>:: -::res://tests/unit/::1769611266 +::res://tests/fixtures/::1769614165 +test_card_data.gd::GDScript::-1::1769614165::0::1::::TestCardData<>RefCounted<>:: +::res://tests/integration/::1769615059 +test_game_state.gd::GDScript::-1::1769615059::0::1::::<>GutTest<>:: +::res://tests/unit/::1769615032 test_card_instance.gd::GDScript::-1::1769611194::0::1::::<>GutTest<>:: test_cppool.gd::GDScript::-1::1769611095::0::1::::<>GutTest<>:: test_player.gd::GDScript::-1::1769611266::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 diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg index dd09b65..69a6856 100644 --- a/.godot/editor/project_metadata.cfg +++ b/.godot/editor/project_metadata.cfg @@ -10,7 +10,7 @@ run_reload_scripts=true [recent_files] 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] diff --git a/.godot/editor/script_editor_cache.cfg b/.godot/editor/script_editor_cache.cfg index 8457007..cce7bba 100644 --- a/.godot/editor/script_editor_cache.cfg +++ b/.godot/editor/script_editor_cache.cfg @@ -3,11 +3,11 @@ state={ "bookmarks": PackedInt32Array(), "breakpoints": PackedInt32Array(), -"column": 8, +"column": 15, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 314, -"scroll_position": 309.0, +"row": 316, +"scroll_position": 313.0, "selection": false, "syntax_highlighter": "GDScript" } @@ -115,11 +115,11 @@ state={ state={ "bookmarks": PackedInt32Array(), "breakpoints": PackedInt32Array(), -"column": 0, +"column": 3, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 0, -"scroll_position": 39.0, +"row": 9, +"scroll_position": 0.0, "selection": false, "syntax_highlighter": "GDScript" } @@ -147,7 +147,7 @@ state={ "folded_lines": Array[int]([]), "h_scroll_position": 0, "row": 33, -"scroll_position": 0.0, +"scroll_position": 20.0, "selection": false, "syntax_highlighter": "GDScript" } @@ -301,7 +301,63 @@ state={ "folded_lines": Array[int]([]), "h_scroll_position": 0, "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, "syntax_highlighter": "GDScript" } diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg index ad421e4..fa4ab0d 100644 --- a/.godot/global_script_class_cache.cfg +++ b/.godot/global_script_class_cache.cfg @@ -11,6 +11,24 @@ list=Array[Dictionary]([{ "language": &"GDScript", "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", "class": &"CardInstance", "icon": "", @@ -30,11 +48,41 @@ list=Array[Dictionary]([{ "path": "res://scripts/ui/DamageDisplay.gd" }, { "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", "icon": "", "language": &"GDScript", "path": "res://scripts/game/Enums.gd" }, { +"base": &"CanvasLayer", +"class": &"GameSetupMenu", +"icon": "", +"language": &"GDScript", +"path": "res://scripts/ui/GameSetupMenu.gd" +}, { "base": &"RefCounted", "class": &"GameState", "icon": "", diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin index 8758e77..fe81d22 100644 Binary files a/.godot/uid_cache.bin and b/.godot/uid_cache.bin differ diff --git a/assets/ui/starter_decks/opus1_vii_fire_earth.png b/assets/ui/starter_decks/opus1_vii_fire_earth.png new file mode 100644 index 0000000..ba784bb Binary files /dev/null and b/assets/ui/starter_decks/opus1_vii_fire_earth.png differ diff --git a/assets/ui/starter_decks/opus1_vii_fire_earth.png.import b/assets/ui/starter_decks/opus1_vii_fire_earth.png.import new file mode 100644 index 0000000..bfdc49e --- /dev/null +++ b/assets/ui/starter_decks/opus1_vii_fire_earth.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus1_x_water_wind.png b/assets/ui/starter_decks/opus1_x_water_wind.png new file mode 100644 index 0000000..2a2714c Binary files /dev/null and b/assets/ui/starter_decks/opus1_x_water_wind.png differ diff --git a/assets/ui/starter_decks/opus1_x_water_wind.png.import b/assets/ui/starter_decks/opus1_x_water_wind.png.import new file mode 100644 index 0000000..511df09 --- /dev/null +++ b/assets/ui/starter_decks/opus1_x_water_wind.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus1_xiii_ice_lightning.png b/assets/ui/starter_decks/opus1_xiii_ice_lightning.png new file mode 100644 index 0000000..15756be Binary files /dev/null and b/assets/ui/starter_decks/opus1_xiii_ice_lightning.png differ diff --git a/assets/ui/starter_decks/opus1_xiii_ice_lightning.png.import b/assets/ui/starter_decks/opus1_xiii_ice_lightning.png.import new file mode 100644 index 0000000..ef95a67 --- /dev/null +++ b/assets/ui/starter_decks/opus1_xiii_ice_lightning.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus3_ix_fire_water.png b/assets/ui/starter_decks/opus3_ix_fire_water.png new file mode 100644 index 0000000..5d67696 Binary files /dev/null and b/assets/ui/starter_decks/opus3_ix_fire_water.png differ diff --git a/assets/ui/starter_decks/opus3_ix_fire_water.png.import b/assets/ui/starter_decks/opus3_ix_fire_water.png.import new file mode 100644 index 0000000..fe19b51 --- /dev/null +++ b/assets/ui/starter_decks/opus3_ix_fire_water.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus3_type0_wind_lightning.png b/assets/ui/starter_decks/opus3_type0_wind_lightning.png new file mode 100644 index 0000000..fa208dc Binary files /dev/null and b/assets/ui/starter_decks/opus3_type0_wind_lightning.png differ diff --git a/assets/ui/starter_decks/opus3_type0_wind_lightning.png.import b/assets/ui/starter_decks/opus3_type0_wind_lightning.png.import new file mode 100644 index 0000000..62f0ac0 --- /dev/null +++ b/assets/ui/starter_decks/opus3_type0_wind_lightning.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus5_xii_wind_water.png b/assets/ui/starter_decks/opus5_xii_wind_water.png new file mode 100644 index 0000000..d25d924 Binary files /dev/null and b/assets/ui/starter_decks/opus5_xii_wind_water.png differ diff --git a/assets/ui/starter_decks/opus5_xii_wind_water.png.import b/assets/ui/starter_decks/opus5_xii_wind_water.png.import new file mode 100644 index 0000000..dc0c6e7 --- /dev/null +++ b/assets/ui/starter_decks/opus5_xii_wind_water.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus5_xiii2_ice_fire.png b/assets/ui/starter_decks/opus5_xiii2_ice_fire.png new file mode 100644 index 0000000..f1006e9 Binary files /dev/null and b/assets/ui/starter_decks/opus5_xiii2_ice_fire.png differ diff --git a/assets/ui/starter_decks/opus5_xiii2_ice_fire.png.import b/assets/ui/starter_decks/opus5_xiii2_ice_fire.png.import new file mode 100644 index 0000000..3637e89 --- /dev/null +++ b/assets/ui/starter_decks/opus5_xiii2_ice_fire.png.import @@ -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 diff --git a/assets/ui/starter_decks/opus5_xiv_earth_lightning.png b/assets/ui/starter_decks/opus5_xiv_earth_lightning.png new file mode 100644 index 0000000..b7f428b Binary files /dev/null and b/assets/ui/starter_decks/opus5_xiv_earth_lightning.png differ diff --git a/assets/ui/starter_decks/opus5_xiv_earth_lightning.png.import b/assets/ui/starter_decks/opus5_xiv_earth_lightning.png.import new file mode 100644 index 0000000..43187a7 --- /dev/null +++ b/assets/ui/starter_decks/opus5_xiv_earth_lightning.png.import @@ -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 diff --git a/data/starter_decks.json b/data/starter_decks.json new file mode 100644 index 0000000..e085999 --- /dev/null +++ b/data/starter_decks.json @@ -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" + ] + } + ] +} diff --git a/scripts/GameController.gd b/scripts/GameController.gd index e554b10..a5fc9f7 100644 --- a/scripts/GameController.gd +++ b/scripts/GameController.gd @@ -4,6 +4,8 @@ extends Node enum State { MENU, + DECK_BUILDER, + GAME_SETUP, PLAYING, PAUSED } @@ -12,14 +14,25 @@ var current_state: State = State.MENU # Menu window size (matches project.godot viewport) 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 const GAME_SIZE := Vector2i(2160, 980) # Scene references var main_menu: MainMenu = null +var deck_builder: DeckBuilder = null +var game_setup_menu: GameSetupMenu = null var game_scene: Node3D = 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 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.keycode == KEY_ESCAPE: match current_state: + State.DECK_BUILDER: + _on_deck_builder_back() + State.GAME_SETUP: + _on_game_setup_back() State.PLAYING: _show_pause_menu() State.PAUSED: @@ -56,12 +73,25 @@ func _show_main_menu() -> void: (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 if not main_menu: main_menu = MainMenu.new() add_child(main_menu) - main_menu.quick_play.connect(_on_start_game) main_menu.play_game.connect(_on_start_game) + main_menu.deck_builder.connect(_on_deck_builder) main_menu.visible = true current_state = State.MENU @@ -71,7 +101,82 @@ func _on_start_game() -> void: if main_menu: 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_size(GAME_SIZE) var screen := DisplayServer.screen_get_size() @@ -80,7 +185,29 @@ func _on_start_game() -> void: (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() func _start_new_game() -> void: @@ -100,6 +227,13 @@ func _start_new_game() -> void: # Create new game scene game_scene = Node3D.new() 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) # Create pause menu diff --git a/scripts/Main.gd b/scripts/Main.gd index 2377b9d..caab8e6 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -12,6 +12,10 @@ var action_log: ActionLog # Player damage displays var damage_displays: Array[DamageDisplay] = [] +# Deck configurations (set by GameController before game starts) +var player1_deck: Array = [] +var player2_deck: Array = [] + func _ready() -> void: _setup_table() _setup_ui() @@ -133,7 +137,7 @@ func _connect_signals() -> void: call_deferred("_connect_field_card_signals") 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 call_deferred("_force_initial_update") diff --git a/scripts/autoload/CardDatabase.gd b/scripts/autoload/CardDatabase.gd index 7897214..7021021 100644 --- a/scripts/autoload/CardDatabase.gd +++ b/scripts/autoload/CardDatabase.gd @@ -4,12 +4,14 @@ extends Node ## Loads card definitions from JSON and provides lookup methods const CARDS_PATH = "res://data/cards.json" +const STARTER_DECKS_PATH = "res://data/starter_decks.json" # Loaded card data var _cards: Dictionary = {} # id -> CardData var _cards_by_element: Dictionary = {} # Element -> Array[CardData] var _cards_by_type: Dictionary = {} # CardType -> Array[CardData] var _card_textures: Dictionary = {} # id -> Texture2D +var _starter_decks: Array = [] # Array of StarterDeckData # Signals signal database_loaded @@ -17,6 +19,7 @@ signal load_error(message: String) func _ready() -> void: _load_database() + _load_starter_decks() func _load_database() -> void: 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: 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 func get_card_texture(card: CardData) -> Texture2D: if card.id in _card_textures: @@ -234,8 +368,82 @@ func create_test_deck(player_index: int) -> Array[String]: 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 +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: var id: String = "" var name: String = "" diff --git a/scripts/autoload/GameManager.gd b/scripts/autoload/GameManager.gd index 31739a7..8d1168a 100644 --- a/scripts/autoload/GameManager.gd +++ b/scripts/autoload/GameManager.gd @@ -52,7 +52,9 @@ func _on_database_loaded() -> void: print("GameManager: Ready") ## 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: push_error("GameManager not initialized") return @@ -68,12 +70,24 @@ func start_new_game() -> void: # Connect signals _connect_game_signals() - # Create test decks - var deck1 = CardDatabase.create_test_deck(0) - var deck2 = CardDatabase.create_test_deck(1) + # Use provided decks or create test decks + var player1_deck: Array[String] = [] + 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 - game_state.setup_game(deck1, deck2) + game_state.setup_game(player1_deck, player2_deck) game_state.start_game() is_game_active = true diff --git a/scripts/data/Deck.gd b/scripts/data/Deck.gd new file mode 100644 index 0000000..e3b95fc --- /dev/null +++ b/scripts/data/Deck.gd @@ -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()] diff --git a/scripts/data/DeckManager.gd b/scripts/data/DeckManager.gd new file mode 100644 index 0000000..3096883 --- /dev/null +++ b/scripts/data/DeckManager.gd @@ -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 diff --git a/scripts/game/Enums.gd b/scripts/game/Enums.gd index fa62681..5db8a83 100644 --- a/scripts/game/Enums.gd +++ b/scripts/game/Enums.gd @@ -121,6 +121,15 @@ static func card_type_to_string(t: CardType) -> String: CardType.MONSTER: return "Monster" 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 static func phase_to_string(p: TurnPhase) -> String: match p: diff --git a/scripts/ui/CardDetailViewer.gd b/scripts/ui/CardDetailViewer.gd new file mode 100644 index 0000000..48b8894 --- /dev/null +++ b/scripts/ui/CardDetailViewer.gd @@ -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) diff --git a/scripts/ui/CardFilterBar.gd b/scripts/ui/CardFilterBar.gd new file mode 100644 index 0000000..acd3f31 --- /dev/null +++ b/scripts/ui/CardFilterBar.gd @@ -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() diff --git a/scripts/ui/CardGrid.gd b/scripts/ui/CardGrid.gd new file mode 100644 index 0000000..d7f7c12 --- /dev/null +++ b/scripts/ui/CardGrid.gd @@ -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() diff --git a/scripts/ui/DeckBuilder.gd b/scripts/ui/DeckBuilder.gd new file mode 100644 index 0000000..10fcd62 --- /dev/null +++ b/scripts/ui/DeckBuilder.gd @@ -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) diff --git a/scripts/ui/DeckListPanel.gd b/scripts/ui/DeckListPanel.gd new file mode 100644 index 0000000..768de35 --- /dev/null +++ b/scripts/ui/DeckListPanel.gd @@ -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() diff --git a/scripts/ui/GameSetupMenu.gd b/scripts/ui/GameSetupMenu.gd new file mode 100644 index 0000000..55907dd --- /dev/null +++ b/scripts/ui/GameSetupMenu.gd @@ -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) diff --git a/scripts/ui/MainMenu.gd b/scripts/ui/MainMenu.gd index 947444f..8819436 100644 --- a/scripts/ui/MainMenu.gd +++ b/scripts/ui/MainMenu.gd @@ -5,8 +5,8 @@ extends CanvasLayer ## The window is sized to match the image (67% of 1024x1536). ## The image fills the entire window; buttons overlay the pre-drawn slots. -signal quick_play signal play_game +signal deck_builder signal online_game signal open_settings signal quit_game @@ -14,8 +14,8 @@ signal quit_game # UI Components var bg_texture: TextureRect var buttons_container: Control -var quick_play_button: Button var play_button: Button +var deck_builder_button: Button var online_button: Button var settings_button: Button var quit_button: Button @@ -64,15 +64,17 @@ func _create_menu() -> void: buttons_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # Create buttons overlaying the pre-drawn slots - quick_play_button = _create_overlay_button("Quick Play", 0) - quick_play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1)) - quick_play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2)) - quick_play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05)) - quick_play_button.pressed.connect(_on_quick_play_pressed) - - play_button = _create_overlay_button("Play", 1) + # "Play" is now in the top golden slot (formerly Quick Play) + play_button = _create_overlay_button("Play", 0) + play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1)) + play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2)) + play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05)) 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.disabled = true @@ -158,12 +160,14 @@ func _reposition_elements() -> void: version_label.position = Vector2(win_size.x - 80, win_size.y - 24) version_label.size = Vector2(72, 18) -func _on_quick_play_pressed() -> void: - quick_play.emit() - func _on_play_pressed() -> void: play_game.emit() + +func _on_deck_builder_pressed() -> void: + deck_builder.emit() + + func _on_quit_pressed() -> void: quit_game.emit() get_tree().quit() diff --git a/sleeve_1.jpg b/sleeve_1.jpg new file mode 100644 index 0000000..0b7edd4 Binary files /dev/null and b/sleeve_1.jpg differ diff --git a/sleeve_1.jpg.import b/sleeve_1.jpg.import new file mode 100644 index 0000000..8de22a0 --- /dev/null +++ b/sleeve_1.jpg.import @@ -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 diff --git a/sleeve_10.jpg b/sleeve_10.jpg new file mode 100644 index 0000000..88c0dca Binary files /dev/null and b/sleeve_10.jpg differ diff --git a/sleeve_10.jpg.import b/sleeve_10.jpg.import new file mode 100644 index 0000000..1217f59 --- /dev/null +++ b/sleeve_10.jpg.import @@ -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 diff --git a/sleeve_11.jpg b/sleeve_11.jpg new file mode 100644 index 0000000..e274da7 Binary files /dev/null and b/sleeve_11.jpg differ diff --git a/sleeve_11.jpg.import b/sleeve_11.jpg.import new file mode 100644 index 0000000..615b831 --- /dev/null +++ b/sleeve_11.jpg.import @@ -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 diff --git a/sleeve_12.jpg b/sleeve_12.jpg new file mode 100644 index 0000000..332557c Binary files /dev/null and b/sleeve_12.jpg differ diff --git a/sleeve_12.jpg.import b/sleeve_12.jpg.import new file mode 100644 index 0000000..3c20b87 --- /dev/null +++ b/sleeve_12.jpg.import @@ -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 diff --git a/sleeve_13.jpg b/sleeve_13.jpg new file mode 100644 index 0000000..332557c Binary files /dev/null and b/sleeve_13.jpg differ diff --git a/sleeve_13.jpg.import b/sleeve_13.jpg.import new file mode 100644 index 0000000..9e53f7e --- /dev/null +++ b/sleeve_13.jpg.import @@ -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 diff --git a/sleeve_14.jpg b/sleeve_14.jpg new file mode 100644 index 0000000..f4b63cd Binary files /dev/null and b/sleeve_14.jpg differ diff --git a/sleeve_14.jpg.import b/sleeve_14.jpg.import new file mode 100644 index 0000000..a46c095 --- /dev/null +++ b/sleeve_14.jpg.import @@ -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 diff --git a/sleeve_15.jpg b/sleeve_15.jpg new file mode 100644 index 0000000..c2d0d8d Binary files /dev/null and b/sleeve_15.jpg differ diff --git a/sleeve_15.jpg.import b/sleeve_15.jpg.import new file mode 100644 index 0000000..7586f15 --- /dev/null +++ b/sleeve_15.jpg.import @@ -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 diff --git a/sleeve_16.jpg b/sleeve_16.jpg new file mode 100644 index 0000000..721b26a Binary files /dev/null and b/sleeve_16.jpg differ diff --git a/sleeve_16.jpg.import b/sleeve_16.jpg.import new file mode 100644 index 0000000..cdde139 --- /dev/null +++ b/sleeve_16.jpg.import @@ -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 diff --git a/sleeve_17.jpg b/sleeve_17.jpg new file mode 100644 index 0000000..964d15c Binary files /dev/null and b/sleeve_17.jpg differ diff --git a/sleeve_17.jpg.import b/sleeve_17.jpg.import new file mode 100644 index 0000000..f5dfd9a --- /dev/null +++ b/sleeve_17.jpg.import @@ -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 diff --git a/sleeve_18.jpg b/sleeve_18.jpg new file mode 100644 index 0000000..90a440c Binary files /dev/null and b/sleeve_18.jpg differ diff --git a/sleeve_18.jpg.import b/sleeve_18.jpg.import new file mode 100644 index 0000000..b7e8dc2 --- /dev/null +++ b/sleeve_18.jpg.import @@ -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 diff --git a/sleeve_19.jpg b/sleeve_19.jpg new file mode 100644 index 0000000..fbda497 Binary files /dev/null and b/sleeve_19.jpg differ diff --git a/sleeve_19.jpg.import b/sleeve_19.jpg.import new file mode 100644 index 0000000..d9fece3 --- /dev/null +++ b/sleeve_19.jpg.import @@ -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 diff --git a/sleeve_2.jpg b/sleeve_2.jpg new file mode 100644 index 0000000..f96f833 Binary files /dev/null and b/sleeve_2.jpg differ diff --git a/sleeve_2.jpg.import b/sleeve_2.jpg.import new file mode 100644 index 0000000..675dce6 --- /dev/null +++ b/sleeve_2.jpg.import @@ -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 diff --git a/sleeve_20.jpg b/sleeve_20.jpg new file mode 100644 index 0000000..ad51e6c Binary files /dev/null and b/sleeve_20.jpg differ diff --git a/sleeve_20.jpg.import b/sleeve_20.jpg.import new file mode 100644 index 0000000..e98b75a --- /dev/null +++ b/sleeve_20.jpg.import @@ -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 diff --git a/sleeve_21.jpg b/sleeve_21.jpg new file mode 100644 index 0000000..b27cc86 Binary files /dev/null and b/sleeve_21.jpg differ diff --git a/sleeve_21.jpg.import b/sleeve_21.jpg.import new file mode 100644 index 0000000..4647c0d --- /dev/null +++ b/sleeve_21.jpg.import @@ -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 diff --git a/sleeve_3.jpg b/sleeve_3.jpg new file mode 100644 index 0000000..aaa59c0 Binary files /dev/null and b/sleeve_3.jpg differ diff --git a/sleeve_3.jpg.import b/sleeve_3.jpg.import new file mode 100644 index 0000000..90c2fa3 --- /dev/null +++ b/sleeve_3.jpg.import @@ -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 diff --git a/sleeve_4.jpg b/sleeve_4.jpg new file mode 100644 index 0000000..8514893 Binary files /dev/null and b/sleeve_4.jpg differ diff --git a/sleeve_4.jpg.import b/sleeve_4.jpg.import new file mode 100644 index 0000000..5ec2668 --- /dev/null +++ b/sleeve_4.jpg.import @@ -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 diff --git a/sleeve_5.jpg b/sleeve_5.jpg new file mode 100644 index 0000000..df70f58 Binary files /dev/null and b/sleeve_5.jpg differ diff --git a/sleeve_5.jpg.import b/sleeve_5.jpg.import new file mode 100644 index 0000000..fd6dce7 --- /dev/null +++ b/sleeve_5.jpg.import @@ -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 diff --git a/sleeve_6.jpg b/sleeve_6.jpg new file mode 100644 index 0000000..7e0fc51 Binary files /dev/null and b/sleeve_6.jpg differ diff --git a/sleeve_6.jpg.import b/sleeve_6.jpg.import new file mode 100644 index 0000000..803134f --- /dev/null +++ b/sleeve_6.jpg.import @@ -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 diff --git a/sleeve_7.jpg b/sleeve_7.jpg new file mode 100644 index 0000000..33326db Binary files /dev/null and b/sleeve_7.jpg differ diff --git a/sleeve_7.jpg.import b/sleeve_7.jpg.import new file mode 100644 index 0000000..2b6a412 --- /dev/null +++ b/sleeve_7.jpg.import @@ -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 diff --git a/sleeve_8.jpg b/sleeve_8.jpg new file mode 100644 index 0000000..9fa23b6 Binary files /dev/null and b/sleeve_8.jpg differ diff --git a/sleeve_8.jpg.import b/sleeve_8.jpg.import new file mode 100644 index 0000000..87d99be --- /dev/null +++ b/sleeve_8.jpg.import @@ -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 diff --git a/sleeve_9.jpg b/sleeve_9.jpg new file mode 100644 index 0000000..bcb9e07 Binary files /dev/null and b/sleeve_9.jpg differ diff --git a/sleeve_9.jpg.import b/sleeve_9.jpg.import new file mode 100644 index 0000000..a91df9e --- /dev/null +++ b/sleeve_9.jpg.import @@ -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