Compare commits

23 Commits

Author SHA1 Message Date
1a5372f713 makes the tooltip expand to fit text if its long 2025-04-29 12:39:09 -04:00
c125d1b70a fixes missing merge conflict 2025-04-29 07:04:15 -04:00
bd5b734d79 fixes merge conflicts 2025-04-29 07:03:03 -04:00
31175b4ecd adds mana cost to the card tooltip 2025-04-29 07:01:46 -04:00
76a23aab5c formatting 2025-04-29 00:24:42 -04:00
6eb80f768b remove unused var 2025-04-28 20:28:43 -04:00
1fe7500dd2 adds better formatting + adds some TODOs 2025-04-28 20:21:45 -04:00
1c5c4728a3 finishes adding mana symbols into tooltip text 2025-04-28 20:00:26 -04:00
75767e927d adds images in the tooltip.. sort of 2025-04-28 19:23:54 -04:00
c684a00f3a adds a lot holy crap i forgot to commit 2025-04-28 18:14:41 -04:00
a496bb3982 hides useless warnings 2025-04-28 16:31:30 -04:00
a83261cf09 adds globale event bus, and tooltip hover trigger 2025-04-28 11:35:25 -04:00
32bf3be0cd converts card info to a dict 2025-04-28 11:12:35 -04:00
999c7989f3 cleans up some compiler warnings 2025-04-28 10:53:15 -04:00
a7aad7d99f exports card loading handling into its own node and script 2025-04-28 02:48:46 -04:00
2f5185d22c exports card input handling to seperate node and script 2025-04-28 02:34:41 -04:00
933e6b715c refines card movement 2025-04-28 01:54:10 -04:00
45919dc5ae formatting 2025-04-28 00:59:02 -04:00
709859abad fixes some small merge issues 2025-04-28 00:58:04 -04:00
04ae9b856b fixes merge conflicts 2025-04-27 23:51:36 -04:00
3c02ae63f9 organizes card scene, exports tween functionality to its own node and script 2025-04-27 22:49:45 -04:00
ce0bc104ff modifications to the card scene to make it sort of work with its new nodes 2025-04-27 22:02:36 -04:00
7cddb502b4 wip: reworking cards to use sprites and fixing up some stuff related to them 2025-04-27 20:05:28 -04:00
29 changed files with 528 additions and 229 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Godot 4+ specific ignores
.godot/
/android/
# custom ignores
symbol_cache/

View File

@ -9,37 +9,42 @@ var _emitted_done = 0
signal fetch_start
var _emitted_start = 0
var _consts = preload("res://data/consts.gd")
func _all_downloads_done() -> bool:
return _emitted_done == _emitted_start
func _setup_cache_in_mem():
var file = FileAccess.open("user://bulk.json", FileAccess.READ)
_bulk_data = JSON.parse_string(file.get_as_text())
file.close()
func setup() -> Error:
if !FileAccess.file_exists("user://bulk.json"):
get_bulk_data(false)
push_error("Bulk Data was not downloaded! Downloading now!")
return FAILED
if !_all_downloads_done():
push_error("Not done downloading Bulk Data.")
return FAILED
_fetch_mana_symbols()
_setup_cache_in_mem()
return OK
func _init() -> void:
_req_headers = PackedStringArray(["User-Agent: " + _consts.APP_NAME + "/" + _consts.APP_VERSION, "Accept: */*"])
_req_headers = PackedStringArray(
["User-Agent: " + _consts.APP_NAME + "/" + _consts.APP_VERSION, "Accept: */*"]
)
fetch_done.connect(_on_end_emit)
fetch_start.connect(_on_start_emit)
func _on_start_emit() -> void:
@ -49,20 +54,22 @@ func _on_start_emit() -> void:
func _on_end_emit() -> void:
_emitted_done += 1
func has_emitted_all() -> bool:
return _emitted_start == _emitted_done
func _cache_error(err: String) -> String:
return "CACHE::ERROR::" + err + "\n"
func _get_dict_from_file(filepath: String) -> Dictionary:
var file = FileAccess.open(filepath, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
return data
## get_card_data_from_name
##
## _name: String [br]
@ -81,8 +88,8 @@ func get_card_data_from_id(id: String) -> Dictionary:
return _get_card_data_from_bulk(_search_results_generic("id", id))
func _search_results_name(search_query: String) -> Dictionary:
var selected_entry = null
for entry in _bulk_data:
if entry["layout"] == "art_series":
continue
@ -93,120 +100,195 @@ func _search_results_name(search_query: String) -> Dictionary:
return entry
push_error("Could not find desired card {" + search_query + "}")
return {}
func _search_results_generic(field: String, search_query: String) -> Dictionary:
var selected_entry = null
for entry in _bulk_data:
if entry["layout"] == "art_series":
continue
if entry[field] == search_query:
return entry[field]
push_error("Could not find desired card {" + search_query + "}")
return {}
func _get_card_data_from_bulk(dict_entry: Dictionary) -> Dictionary:
if dict_entry["image_status"] != "missing":
_fetch_card_img(dict_entry)
var dir = DirAccess.open("user://")
dir.make_dir_recursive("user://card_cache/" + dict_entry["id"] + "/")
dir = null
var file = FileAccess.open("user://card_cache/" + dict_entry["id"] + "/card.json", FileAccess.WRITE)
var file = FileAccess.open(
"user://card_cache/" + dict_entry["id"] + "/card.json", FileAccess.WRITE
)
file.store_line(JSON.stringify(dict_entry, "\t"))
file.close()
print("Card: " + dict_entry["name"] + " (" + dict_entry["id"] + ") found, and cached.")
return dict_entry
func _get_mana_img(symbol: String, img_url: String) -> Error:
fetch_start.emit()
if FileAccess.file_exists("res://symbol_cache/" + symbol + ".svg"):
return OK
var httpr = HTTPRequest.new()
add_child(httpr)
var err = httpr.request(img_url, _req_headers)
if err != OK:
push_error(_cache_error("GET_REQUEST") + "An error occured in the Scryfall request.")
return FAILED
var resp = await httpr.request_completed
var img = Image.new()
err = img.load_svg_from_buffer(resp[3])
if err != OK:
push_error(_cache_error("IMG_LOADING") + "Couldn't load the image.")
return FAILED
if img.get_size() == Vector2i(100, 100):
print("resizing")
img.resize(20, 20, Image.INTERPOLATE_LANCZOS)
img.save_png(
"res://symbol_cache/" + symbol.replace("/", "-").replace("{", "").replace("}", "") + ".png"
)
img = null
fetch_done.emit()
return OK
func _fetch_mana_symbols() -> Error:
var mana_symbols: Dictionary = Dictionary()
if DirAccess.dir_exists_absolute("res://symbol_cache"):
return OK
else:
DirAccess.make_dir_absolute("res://symbol_cache")
var httpr = HTTPRequest.new()
add_child(httpr)
var err = httpr.request("https://api.scryfall.com/symbology", _req_headers)
if err != OK:
push_error(_cache_error("GET_REQUEST") + "An error occured in the Scryfall request.")
return FAILED
var resp = await httpr.request_completed
var unprocessed_body = resp[3].get_string_from_utf8()
var json_body = JSON.parse_string(unprocessed_body)
for icon in json_body["data"]:
err = await _get_mana_img(icon["symbol"], icon["svg_uri"])
if err != OK:
push_error("Couldn't fetch mana symbol " + icon["symbol"])
mana_symbols[icon["symbol"]] = (
"res://symbol_cache/"
+ icon["symbol"].replace("/", "-").replace("{", "").replace("}", "")
+ ".png"
)
print(icon["symbol"] + " image cached.")
var file = FileAccess.open("res://symbol_cache/symbols.json", FileAccess.WRITE)
file.store_line(JSON.stringify(mana_symbols))
file.close()
print("Done caching mana symbols.")
return OK
func _fetch_card_img(data: Dictionary) -> Error:
fetch_start.emit()
if FileAccess.file_exists("user://card_cache/" + data["id"] + "card.png"):
return OK
var httpr = HTTPRequest.new()
add_child(httpr)
var err = httpr.request((data["image_uris"])["png"], _req_headers)
if err != OK:
push_error(_cache_error("GET_REQUEST") + "An error occured in the Scryfall request.")
return FAILED
var resp = await httpr.request_completed
var img = Image.new()
err = img.load_png_from_buffer(resp[3])
if err != OK:
push_error(_cache_error("IMG_LOADING") + "Couldn't load the image.")
return FAILED
var dir = DirAccess.open("user://")
dir.make_dir_recursive("user://card_cache/" + data["id"] + "/")
dir = null
img.save_png("user://card_cache/" + data["id"] + "/card.png")
img = null
fetch_done.emit()
return OK
func get_bulk_data(force: bool) -> Error:
if FileAccess.file_exists("user://bulk.json"):
if force:
DirAccess.remove_absolute("user://bulk.json")
else:
return OK
print("downloading ")
var httpr = HTTPRequest.new()
add_child(httpr)
var error = httpr.request("https://api.scryfall.com/bulk-data/unique-artwork", _req_headers)
if error != OK:
push_error(_cache_error("GET_REQUEST") + "An error occurred in the Scryfall request.")
return FAILED
var response = await httpr.request_completed
if response[0] != HTTPRequest.RESULT_SUCCESS:
push_error(_cache_error("GET_REQUEST") + "Failed to fetch card data from Scryfall")
return FAILED
var unprocessed_body = response[3].get_string_from_utf8()
var card_content = JSON.parse_string(unprocessed_body)
if card_content == null:
push_error(_cache_error("PARSING") + "Failed to parse the Scryfall card results.")
return FAILED
error = httpr.request(card_content["download_uri"], _req_headers)
if error != OK:
push_error(_cache_error("GET_REQUEST") + "An error occurred in the Scryfall request.")
return FAILED
response = await httpr.request_completed
if response[0] != HTTPRequest.RESULT_SUCCESS:
push_error(_cache_error("GET_REQUEST") + "Failed to fetch card data from Scryfall")
return FAILED
unprocessed_body = response[3].get_string_from_utf8()
card_content = JSON.parse_string(unprocessed_body)
if card_content == null:
push_error(_cache_error("PARSING") + "Failed to parse the Scryfall card results.")
return FAILED
var data_cache = FileAccess.open("user://bulk.json", FileAccess.WRITE)
data_cache.store_string(unprocessed_body)
data_cache.close()
fetch_done.emit()
return OK
func _notification(what):
if what == NOTIFICATION_PREDELETE:
if !_all_downloads_done():
push_error("ERR::MEM::CACHE\nCache being deleted before all threads have finished processing!")
push_error(
"ERR::MEM::CACHE\nCache being deleted before all threads have finished processing!"
)

155
card.gd
View File

@ -1,155 +0,0 @@
extends TextureRect
#extends Sprite2D
## The card class
##
## Represents an instance of a card to be displayed on the tabletop.
## Contains helper text for the text, the cards ID, and the image path.
enum pivot {
ROTATE_0,
ROTATE_90,
ROTATE_180,
ROTATE_270
}
var current_pivot = pivot.ROTATE_0
var card_id: String
var card_name: String
var card_type: String
var oracle_text: String
var is_dragging = false
var is_pivot = false
var delay = 5.0
var mouse_offset: Vector2
func _pivot() -> int:
var deg: int
match current_pivot:
pivot.ROTATE_0:
deg = 0
pivot.ROTATE_90:
deg = 90
pivot.ROTATE_180:
deg = 180
pivot.ROTATE_270:
deg = 270
return deg
func _physics_process(delta: float) -> void:
if is_dragging == true:
var tween = get_tree().create_tween()
tween.tween_property(self, "position", get_global_mouse_position() - mouse_offset, delay * delta)
if is_pivot == true:
var tween = get_tree().create_tween()
tween.tween_property(self, "rotation_degrees", _pivot(), delta * delay)
is_pivot = false
func _gui_input(event: InputEvent) -> void:
if event is not InputEventMouseButton:
return
match event.button_index:
MOUSE_BUTTON_LEFT:
if event.pressed:
is_dragging = true
mouse_offset = get_global_mouse_position() - global_position
else:
is_dragging = false
MOUSE_BUTTON_RIGHT:
pass
func _unhandled_key_input(event: InputEvent) -> void:
if not event.is_action_pressed("default_action"):
return
if current_pivot == pivot.ROTATE_0:
current_pivot = pivot.ROTATE_90
is_pivot = true
else:
current_pivot = pivot.ROTATE_0
is_pivot = true
set_pivot_offset(size / 2)
func _card_error(error_type: String) -> String:
return "ERROR::CARD::%s::%s::%s::\n" % [card_id, card_name, error_type]
func init(id: String) -> void:
card_id = id
func _ready() -> void:
var load_status = _load_card()
if load_status != OK:
# TODO: No need to push another error as the failure state of loading does that already,
# if the card is not cached, perhaps a placeholder blank card can be used instead?
# Setting that up can be put here later...
push_error("Failed to load card.")
set_pivot_offset(size / 2)
func _load_card() -> Error:
if _load_data() != OK:
return FAILED
if _load_image() != OK:
return FAILED
return OK
func _load_data() -> Error:
var cached_json = FileAccess.get_file_as_string("user://card_cache/" + card_id + "/card.json")
if cached_json.is_empty():
push_error("%s\nCard json data was not found in cache" % _card_error("CACHE"))
return FAILED
var card_json = JSON.parse_string(cached_json)
if card_json == null:
push_error("%s\nCard json data is could not be parsed as valid json" % _card_error("DATA"))
return FAILED
card_name = card_json["name"]
card_type = card_json["type_line"]
oracle_text = card_json["oracle_text"]
return OK
func _load_image() -> Error:
# NOTE: Assuming we're going with using the .png cards on board.
var cached_img = FileAccess.get_file_as_bytes("user://card_cache/" + card_id + "/card.png")
if cached_img.is_empty():
push_error("%sCard on-board image was not found in cache" % _card_error("CACHE"))
return FAILED
var image = Image.new()
var image_status: Error = image.load_png_from_buffer(cached_img)
if image_status != OK:
push_error("%sCard on-board image failed to load correctly" % _card_error("IMAGE"))
return FAILED
# TODO: Get the size from the node or some constant variable.
image.resize(int(size.x), int(size.y), Image.INTERPOLATE_LANCZOS)
var image_texture = ImageTexture.new()
image_texture.set_image(image)
#expand_mode = TextureRect.EXPAND_FIT_WIDTH
texture = image_texture
return OK

View File

@ -1,11 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://cah3mvdnom1xg"]
[ext_resource type="Script" uid="uid://b3yqd1qu7dyq" path="res://card.gd" id="1_kikvd"]
[node name="Card" type="TextureRect"]
offset_left = 794.0
offset_top = 79.0
offset_right = 919.0
offset_bottom = 254.0
mouse_default_cursor_shape = 2
script = ExtResource("1_kikvd")

View File

@ -4,18 +4,20 @@ var _caching = preload("res://caching.gd")
var _decklist
func _init() -> void:
_decklist = Dictionary()
func _write_to_decks(_decks: Array) -> void:
if FileAccess.file_exists("user://decks.json"):
DirAccess.remove_absolute("user://decks.json")
var file = FileAccess.open("user://decks.json", FileAccess.WRITE)
file.store_line(JSON.stringify(_decks))
file.close()
func load_decks() -> Array:
if !FileAccess.file_exists("user://decks.json"):
return []
@ -24,6 +26,7 @@ func load_decks() -> Array:
file.close()
return JSON.parse_string(file_text)
func _convert_mtgo_to_cache_lookup(decklist: String) -> Dictionary:
var _cards = {}
var lines = decklist.split("\n")
@ -31,7 +34,7 @@ func _convert_mtgo_to_cache_lookup(decklist: String) -> Dictionary:
var words = line.split(" ", false, 1)
if words.size() != 2:
continue
_cards[words[1]] = words[0]
return _cards
@ -42,12 +45,12 @@ func _do_free(cache) -> void:
cache.queue_free()
func add_new_deck(cards: String, name: String, about = "") -> void:
func add_new_deck(cards: String, _name: String, about = "") -> void:
var _queries = _convert_mtgo_to_cache_lookup(cards)
_do_decklist_cache(_queries)
var _decks = load_decks()
_decks.push_back({"name": name, "about": about, "decklist": _queries})
_decks.push_back({"name": _name, "about": about, "decklist": _queries})
_write_to_decks(_decks)
@ -62,7 +65,6 @@ func _do_decklist_cache(_queries: Dictionary) -> void:
push_error("Failed to find card: " + query)
continue
_decklist[entry["id"]] = _queries[query]
cache.fetch_done.connect(_do_free.bind(cache))

6
event_bus.gd Normal file
View File

@ -0,0 +1,6 @@
extends Node
@warning_ignore("unused_signal")
signal card_on_hover(card_info, card_image)
@warning_ignore("unused_signal")
signal card_on_unhover

1
event_bus.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://b5re77jjgr8ca

View File

@ -3,7 +3,7 @@ extends TextureRect
var _screen_size: Vector2
var _colors: Array[Color]
var _card_scene = preload("res://card.tscn")
var _card_class = preload("res://scenes/card/card.tscn")
# Called when the node enters the scene tree for the first time.
@ -11,7 +11,7 @@ func _ready() -> void:
# TODO: Calculate this field's scale and position based on which no# field this is.
_screen_size = get_viewport_rect().size
var card = _card_scene.instantiate()
var card = _card_class.instantiate()
# TODO: Currently working with an already-cached card with a known ID to load this.
# Later on, the cards should be pulling the IDs directly from the library's list of IDs.

15
hand.gd Normal file
View File

@ -0,0 +1,15 @@
extends StaticBody2D
var cards: Array[Node] = []
var _card_class = preload("res://scenes/card/card.tscn")
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass

1
hand.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://dvu4gdhqjejeo

View File

@ -1,6 +1,6 @@
extends Node2D
var _card_class = preload("res://card.gd")
var _card_class = preload("res://scenes/card/card.gd")
# Library cards are represented as an array of card IDs.
var lib_cards: Array[String]
@ -19,6 +19,7 @@ func _init(_decklist: Dictionary) -> void:
for i in _num:
lib_cards.push_back(card)
func add_cards(cards: Array, top: bool) -> void:
for card in cards:
add_card(card, top)

View File

@ -1,26 +1,30 @@
extends Node2D
var _card_class = preload("res://card.tscn")
var _card_class = preload("res://scenes/card/card.tscn")
var field_scene = preload("res://field.tscn")
var fields: Array[Node] = []
var decks: Array[Dictionary]
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# The first field in the array will be the player's own field.
# Might be a better idea to have that in a seperate variable? idk
var card = _card_class.instantiate()
card.init("d3f10f07-7cfe-4a6f-8de6-373e367a731b")
add_child(card)
card.position = Vector2(100, 100)
# TODO: Currently working with an already-cached card with a known ID to load this.
# Later on, the cards should be pulling the IDs directly from the library's list of IDs.
card.init("d3f10f07-7cfe-4a6f-8de6-373e367a731b")
add_child(card)
#fields.append(field_scene.instantiate())
#var colors: Array[Color] = [Color(1, 0, 1)]
#fields[0].set_colors(colors)

View File

@ -1,12 +1,21 @@
[gd_scene load_steps=3 format=3 uid="uid://cx0vga81xwckh"]
[gd_scene load_steps=5 format=3 uid="uid://cx0vga81xwckh"]
[ext_resource type="Script" uid="uid://w2rqm1u7p7im" path="res://player.gd" id="1_4flbx"]
[ext_resource type="Script" uid="uid://dvu4gdhqjejeo" path="res://hand.gd" id="2_i3pqv"]
[ext_resource type="Script" uid="uid://bc51go8t8uvts" path="res://library.gd" id="2_onrkg"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_onrkg"]
size = Vector2(1920, 200)
[node name="Player" type="Node2D"]
script = ExtResource("1_4flbx")
[node name="Hand" type="Node2D" parent="."]
[node name="Hand" type="StaticBody2D" parent="."]
script = ExtResource("2_i3pqv")
[node name="CollisionArea" type="CollisionShape2D" parent="Hand"]
position = Vector2(960, 980)
shape = SubResource("RectangleShape2D_onrkg")
[node name="Library" type="Node2D" parent="."]
script = ExtResource("2_onrkg")

View File

@ -15,6 +15,10 @@ run/main_scene="uid://b4ldtb3gw0jlu"
config/features=PackedStringArray("4.4", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
EventBus="*res://event_bus.gd"
[display]
window/size/viewport_width=1920
@ -27,8 +31,13 @@ enabled=PackedStringArray()
[input]
default_action={
MAIN={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
SELECT={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}

72
scenes/card/card.gd Normal file
View File

@ -0,0 +1,72 @@
extends Node2D
## The card class
##
## Represents an instance of a card to be displayed on the tabletop.
## Contains helper text for the text, the cards ID, and the image path.
# Card information.
var card_info: Dictionary
var cached_image: Image
# Card properties.
var tapped: bool
# Card input state.
var hovered: bool # Is the mouse currently on this card?
var dragging: bool # Is the card currently being dragged?
var focused: bool # Is this card currently a focus?
var mouse_offset: Vector2
func init(id: String) -> void:
card_info["id"] = id
# This is called when we want to apply the behaviour of the mouse being
# inside/outside the card when we can't trigger the enter/exit triggers.
func check_hover() -> void:
if hovered:
_on_mouse_entered()
else:
_on_mouse_exited()
func error(error_type: String) -> String:
return "ERROR::CARD::%s::%s::%s::\n" % [card_info["id"], card_info["name"], error_type]
func colission_size() -> Vector2:
return $Area2D/CollisionShape2D.shape.size
func _physics_process(delta: float) -> void:
focused = hovered or dragging
$InputHandler.handle_inputs(delta)
$TweenController.handle_constant_tweens(delta)
func _on_mouse_entered() -> void:
hovered = true
# Do not apply any more effects if we're dragging the card.
if dragging:
return
Input.set_default_cursor_shape(Input.CURSOR_POINTING_HAND)
$TweenController.scale(1.05)
EventBus.emit_signal("card_on_hover", card_info, cached_image)
func _on_mouse_exited() -> void:
hovered = false
# Do not apply any more effects if we're dragging the card.
if dragging:
return
Input.set_default_cursor_shape(Input.CURSOR_ARROW)
$TweenController.scale(1.0)
EventBus.emit_signal("card_on_unhover")

31
scenes/card/card.tscn Normal file
View File

@ -0,0 +1,31 @@
[gd_scene load_steps=6 format=3 uid="uid://cah3mvdnom1xg"]
[ext_resource type="Script" uid="uid://b3yqd1qu7dyq" path="res://scenes/card/card.gd" id="1_kikvd"]
[ext_resource type="Script" uid="uid://bkk0pyypi1id7" path="res://scenes/card/tween.gd" id="2_imta7"]
[ext_resource type="Script" uid="uid://dhgk6fhw8oua0" path="res://scenes/card/input.gd" id="3_vtcvk"]
[ext_resource type="Script" uid="uid://vckbno504iay" path="res://scenes/card/load.gd" id="4_g65cd"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_kikvd"]
size = Vector2(125, 175)
[node name="Card" type="Node2D"]
script = ExtResource("1_kikvd")
[node name="Sprite2D" type="Sprite2D" parent="."]
[node name="Area2D" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
shape = SubResource("RectangleShape2D_kikvd")
[node name="TweenController" type="Node" parent="."]
script = ExtResource("2_imta7")
[node name="InputHandler" type="Node" parent="."]
script = ExtResource("3_vtcvk")
[node name="DataLoader" type="Node" parent="."]
script = ExtResource("4_g65cd")
[connection signal="mouse_entered" from="Area2D" to="." method="_on_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="." method="_on_mouse_exited"]

27
scenes/card/input.gd Normal file
View File

@ -0,0 +1,27 @@
extends Node
var card: Node
var tween_controller: Node
func _ready() -> void:
card = get_parent()
tween_controller = card.get_node("TweenController")
func handle_inputs(delta: float) -> void:
if not card.focused:
# TODO: Global card actions, e.g. untapping everything.
return
if Input.is_action_just_pressed("MAIN"):
card.tapped = not card.tapped
tween_controller.tap(card.tapped, delta)
if Input.is_action_just_pressed("SELECT"):
card.dragging = true
Input.set_default_cursor_shape(Input.CURSOR_DRAG)
card.mouse_offset = card.get_global_mouse_position() - card.global_position
if Input.is_action_just_released("SELECT"):
card.dragging = false
card.check_hover()

1
scenes/card/input.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://dhgk6fhw8oua0

83
scenes/card/load.gd Normal file
View File

@ -0,0 +1,83 @@
extends Node
var card: Node
func _ready() -> void:
card = get_parent()
if _load_card() != OK:
# TODO: No need to push another error as the failure state of loading does that already,
# if the card is not cached, perhaps a placeholder blank card can be used instead?
# Setting that up can be put here later...
push_error("Failed to load card.")
func _load_card() -> Error:
if _load_data() != OK:
return FAILED
if _load_image() != OK:
return FAILED
return OK
func _load_data() -> Error:
var cached_json = FileAccess.get_file_as_string(
"user://card_cache/" + card.card_info["id"] + "/card.json"
)
if cached_json.is_empty():
push_error("%s\nCard json data was not found in cache" % card.error("CACHE"))
return FAILED
var card_json = JSON.parse_string(cached_json)
if card_json == null:
push_error("%s\nCard json data is could not be parsed as valid json" % card.error("DATA"))
return FAILED
card.card_info["name"] = card_json["name"]
card.card_info["type"] = card_json["type_line"]
card.card_info["desc"] = card_json["oracle_text"]
card.card_info["cost"] = card_json["mana_cost"]
return OK
func _load_image() -> Error:
var cached_img = FileAccess.get_file_as_bytes(
"user://card_cache/" + card.card_info["id"] + "/card.png"
)
if cached_img.is_empty():
push_error("%sCard on-board image was not found in cache" % card.error("CACHE"))
return FAILED
var cache_image = Image.new()
var image_status: Error = cache_image.load_png_from_buffer(cached_img)
if image_status != OK:
push_error("%sCard on-board image failed to load correctly" % card.error("IMAGE"))
return FAILED
card.cached_image = cache_image
var image = Image.new()
image_status = image.load_png_from_buffer(cached_img)
if image_status != OK:
push_error("%sCard on-board image failed to load correctly" % card.error("IMAGE"))
return FAILED
var size = card.colission_size()
image.resize(int(size.x), int(size.y), Image.INTERPOLATE_LANCZOS)
var image_texture = ImageTexture.new()
image_texture.set_image(image)
var card_sprite = card.get_node("Sprite2D")
card_sprite.texture = image_texture
return OK

1
scenes/card/load.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://vckbno504iay

35
scenes/card/tween.gd Normal file
View File

@ -0,0 +1,35 @@
extends Node
@export var move_speed = 1.0
@export var tap_speed = 5.0
@export var scale_speed = 0.1
var card: Node
# TODO: Figure out elastic tween transitions for bounciness.
func handle_constant_tweens(delta: float) -> void:
if card.dragging:
move_to(card.get_global_mouse_position() - card.mouse_offset, delta)
func move_to(location: Vector2, delta: float) -> void:
var tween = create_tween()
tween.tween_property(card, "position", location, delta * move_speed)
func tap(tapped: bool, delta: float) -> void:
var tween = create_tween()
var rotation = 90 if tapped else 0
tween.tween_property(card, "rotation_degrees", rotation, delta * tap_speed)
func scale(scalar: float) -> void:
var tween = create_tween()
var new_scale = Vector2.ONE * scalar
tween.tween_property(card, "scale", new_scale, scale_speed)
func _ready() -> void:
card = get_parent()

1
scenes/card/tween.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://bkk0pyypi1id7

View File

@ -0,0 +1,17 @@
extends TextureRect
func _set_tip_image(_card_info: Dictionary, card_image: Image) -> void:
card_image.resize(int(size.x / 1.75), int(size.y), Image.INTERPOLATE_LANCZOS)
var tex = ImageTexture.new()
tex.set_image(card_image)
texture = tex
func _clear_tip_image() -> void:
texture = null
func _ready() -> void:
EventBus.connect("card_on_hover", _set_tip_image)
EventBus.connect("card_on_unhover", _clear_tip_image)

View File

@ -0,0 +1 @@
uid://cpvbftm0swoa6

View File

@ -0,0 +1,39 @@
extends RichTextLabel
var mana_symbols: Dictionary
func _convert_text_to_symbol(_text: String):
var last_idx = 0
for symbol in mana_symbols:
last_idx = 0
while _text.find(symbol, last_idx) != -1:
_text = _text.replace(symbol, "[img]" + mana_symbols[symbol] + "[/img]")
last_idx = _text.find(symbol, last_idx) + symbol.length()
return _text
func _set_tip_text(card_info: Dictionary, _card_image: Image) -> void:
# TODO: add more card formatting, check all of the logos, very niche icons will be affected i believe since they're
# different sizes
# shrink text if we use too much space for it, etc
text = "[b]" + card_info["name"] + "[/b]\t"
text += _convert_text_to_symbol(card_info["cost"]) + "\n"
text += "[i]" + card_info["type"] + "[/i]\n"
text += _convert_text_to_symbol(card_info["desc"])
func _clear_tip_text() -> void:
text = ""
func _ready() -> void:
if !FileAccess.file_exists("res://symbol_cache/symbols.json"):
push_error("Symbols haven't been cached yet!")
return
var file = FileAccess.open("res://symbol_cache/symbols.json", FileAccess.READ)
mana_symbols = JSON.parse_string(file.get_as_text())
file.close()
EventBus.connect("card_on_hover", _set_tip_text)
EventBus.connect("card_on_unhover", _clear_tip_text)

View File

@ -0,0 +1 @@
uid://b8tioen4n1rip

View File

@ -90,14 +90,16 @@ var cards = "1 Arcane Signet
1 Clavileño, First of the Blessed"
func _bulk_callback(cache) -> void:
cache.setup()
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
var cache = _caching.new()
add_child(cache)
if cache.setup() != OK:
cache.fetch_done.connect(_bulk_callback.bind(cache))
# TODO: Create 2-4 player instances as children of this tabletop node.
@ -105,15 +107,17 @@ func _ready() -> void:
var player = player_class.new()
add_child(player)
move_child(player, 0)
cache.get_card_data_from_name("1996 World Champion")
var deck = deck_input.new()
add_child(deck)
deck.add_new_deck(cards, "Blood rites")
deck.add_new_deck(cards, "Blood rites")
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.

View File

@ -1,6 +1,8 @@
[gd_scene load_steps=5 format=3 uid="uid://b4ldtb3gw0jlu"]
[gd_scene load_steps=7 format=3 uid="uid://b4ldtb3gw0jlu"]
[ext_resource type="Script" uid="uid://cfkew150yl1y3" path="res://tabletop.gd" id="1_3we3x"]
[ext_resource type="Script" uid="uid://b8tioen4n1rip" path="res://scenes/tooltip/card_text.gd" id="2_d43bn"]
[ext_resource type="Script" uid="uid://cpvbftm0swoa6" path="res://scenes/tooltip/card_image.gd" id="2_pqag1"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3we3x"]
bg_color = Color(0, 0, 0, 1)
@ -35,6 +37,23 @@ theme_override_constants/separation = 0
custom_minimum_size = Vector2(0, 540)
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="UI/BigBar/Items/MenuArea"]
layout_mode = 2
[node name="TextureRect" type="TextureRect" parent="UI/BigBar/Items/MenuArea/VBoxContainer"]
custom_minimum_size = Vector2(400, 300)
layout_direction = 2
layout_mode = 2
stretch_mode = 5
script = ExtResource("2_pqag1")
[node name="RichTextLabel" type="RichTextLabel" parent="UI/BigBar/Items/MenuArea/VBoxContainer"]
custom_minimum_size = Vector2(0, 230)
layout_mode = 2
bbcode_enabled = true
fit_content = true
script = ExtResource("2_d43bn")
[node name="ChatArea" type="PanelContainer" parent="UI/BigBar/Items"]
custom_minimum_size = Vector2(0, 270)
layout_mode = 2