Compare commits
64 Commits
ca64248ace
...
main
Author | SHA1 | Date | |
---|---|---|---|
1a5372f713 | |||
c125d1b70a | |||
bd5b734d79 | |||
31175b4ecd | |||
76a23aab5c | |||
6eb80f768b | |||
1fe7500dd2 | |||
1c5c4728a3 | |||
75767e927d | |||
c684a00f3a | |||
a496bb3982 | |||
a83261cf09 | |||
32bf3be0cd | |||
999c7989f3 | |||
a7aad7d99f | |||
2f5185d22c | |||
933e6b715c | |||
45919dc5ae | |||
709859abad | |||
04ae9b856b | |||
86cc3bf8a8 | |||
3c02ae63f9 | |||
ce0bc104ff | |||
7cddb502b4 | |||
d1ef1d6f4f | |||
fc3a31d144 | |||
1d9c3fc473 | |||
5937b4db69 | |||
9217834726 | |||
c463bf0adc | |||
98357b5dec | |||
1beb89ea84 | |||
dd73e1b477 | |||
9bb3308487 | |||
90912c062f | |||
b82f5e4c19 | |||
fe38fc49bf | |||
a40ccab3c8 | |||
f5edba7402 | |||
b9a07a8c47 | |||
0f88bd8f7b | |||
c75aec06ac | |||
d726290cf2 | |||
fafaf404ab | |||
77da7cf6b2 | |||
21f7d9de04 | |||
6100a50754 | |||
bf9244c2b7 | |||
a3fb627310 | |||
c085a93c49 | |||
29042d5d05 | |||
4489eb5f76 | |||
de5e107b86 | |||
5fc67ab5e5 | |||
05cdcf83cc | |||
37975021cb | |||
a3dfa96efd | |||
651b267dd2 | |||
970d7064ed | |||
3808955237 | |||
fcf736af92 | |||
a67f2e1096 | |||
c990b68e07 | |||
ddd6c15292 |
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
# Build artifacts
|
||||
build/
|
||||
|
||||
# Direnv stuff for the flake
|
||||
.envrc
|
||||
.direnv
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# custom ignores
|
||||
symbol_cache/
|
||||
|
33
Makefile
33
Makefile
@ -1,33 +0,0 @@
|
||||
CC=g++
|
||||
|
||||
CFLAGS= -c -g -Wall
|
||||
|
||||
LDLIBS = -lglfw
|
||||
|
||||
TARGET := untap
|
||||
|
||||
BUILD_DIR := ./build
|
||||
SRC_DIRS := ./src
|
||||
|
||||
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp')
|
||||
SRCS += $(shell find $(SRC_DIRS) -name '*.c')
|
||||
|
||||
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
|
||||
|
||||
$(BUILD_DIR)/$(TARGET): $(OBJS)
|
||||
$(CC) $(OBJS) -o $@ $(LDLIBS)
|
||||
cp -r $(SRC_DIRS)/shaders $(BUILD_DIR)/
|
||||
cp -r $(SRC_DIRS)/textures $(BUILD_DIR)/
|
||||
|
||||
$(BUILD_DIR)/%.cpp.o: %.cpp
|
||||
mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(BUILD_DIR)/%.c.o: %.c
|
||||
mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
294
caching.gd
Normal file
294
caching.gd
Normal file
@ -0,0 +1,294 @@
|
||||
extends Node
|
||||
|
||||
var _req_headers: PackedStringArray
|
||||
|
||||
var _bulk_data: Array
|
||||
|
||||
signal fetch_done
|
||||
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: */*"]
|
||||
)
|
||||
|
||||
fetch_done.connect(_on_end_emit)
|
||||
fetch_start.connect(_on_start_emit)
|
||||
|
||||
|
||||
func _on_start_emit() -> void:
|
||||
_emitted_start += 1
|
||||
|
||||
|
||||
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]
|
||||
## A wrapper for searching for a card by name. Use **get_card_data_from_id** where possible, as it avoids an expensive search for the new card, if the card has been cached already.
|
||||
func get_card_data_from_name(_name: String) -> Dictionary:
|
||||
return _get_card_data_from_bulk(_search_results_name(_name))
|
||||
|
||||
|
||||
## get_card_data_from_id
|
||||
##
|
||||
## id: String [br]
|
||||
## This is the preferred wrapper to use when fetching card data, it checks the cache for preexisting data and uses that if it's available. Otherwise, it will search the bulk json for the data.
|
||||
func get_card_data_from_id(id: String) -> Dictionary:
|
||||
if FileAccess.file_exists("user://card_cache/" + id + "/card.json"):
|
||||
return _get_dict_from_file("user://card_cache/" + id + "/card.json")
|
||||
|
||||
return _get_card_data_from_bulk(_search_results_generic("id", id))
|
||||
|
||||
|
||||
func _search_results_name(search_query: String) -> Dictionary:
|
||||
for entry in _bulk_data:
|
||||
if entry["layout"] == "art_series":
|
||||
continue
|
||||
var entry_name = entry["name"]
|
||||
if entry_name.contains("//"):
|
||||
entry_name = entry_name.left(entry_name.find("//") - 1)
|
||||
if entry_name == search_query:
|
||||
return entry
|
||||
push_error("Could not find desired card {" + search_query + "}")
|
||||
return {}
|
||||
|
||||
|
||||
func _search_results_generic(field: String, search_query: String) -> Dictionary:
|
||||
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
|
||||
)
|
||||
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
|
||||
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!"
|
||||
)
|
1
caching.gd.uid
Normal file
1
caching.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cml6c3fdbyy75
|
2
data/consts.gd
Normal file
2
data/consts.gd
Normal file
@ -0,0 +1,2 @@
|
||||
const APP_NAME = "MTG_UNTAP_CLONE"
|
||||
const APP_VERSION = "0.1"
|
1
data/consts.gd.uid
Normal file
1
data/consts.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bo0256822a2ss
|
1
data/mana.gd
Normal file
1
data/mana.gd
Normal file
@ -0,0 +1 @@
|
||||
enum ManaCosts { WHITE, BLUE, BLACK, RED, GREEN, COLOURLESS, GENERIC, LIFE }
|
1
data/mana.gd.uid
Normal file
1
data/mana.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://brn2sbfea8fyf
|
73
deck_input.gd
Normal file
73
deck_input.gd
Normal file
@ -0,0 +1,73 @@
|
||||
extends Node
|
||||
|
||||
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 []
|
||||
var file = FileAccess.open("user://decks.json", FileAccess.READ)
|
||||
var file_text = file.get_as_text()
|
||||
file.close()
|
||||
return JSON.parse_string(file_text)
|
||||
|
||||
|
||||
func _convert_mtgo_to_cache_lookup(decklist: String) -> Dictionary:
|
||||
var _cards = {}
|
||||
var lines = decklist.split("\n")
|
||||
for line in lines:
|
||||
var words = line.split(" ", false, 1)
|
||||
if words.size() != 2:
|
||||
continue
|
||||
|
||||
_cards[words[1]] = words[0]
|
||||
return _cards
|
||||
|
||||
|
||||
func _do_free(cache) -> void:
|
||||
if !cache.has_emitted_all():
|
||||
return
|
||||
cache.queue_free()
|
||||
|
||||
|
||||
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})
|
||||
_write_to_decks(_decks)
|
||||
|
||||
|
||||
func _do_decklist_cache(_queries: Dictionary) -> void:
|
||||
var cache = _caching.new()
|
||||
add_child(cache)
|
||||
cache.setup()
|
||||
|
||||
for query in _queries:
|
||||
var entry = cache.get_card_data_from_name(query)
|
||||
if entry.size() == 0:
|
||||
push_error("Failed to find card: " + query)
|
||||
continue
|
||||
_decklist[entry["id"]] = _queries[query]
|
||||
cache.fetch_done.connect(_do_free.bind(cache))
|
||||
|
||||
|
||||
func _show_decklist() -> void:
|
||||
for card in _decklist:
|
||||
print(card + " : " + _decklist[card])
|
1
deck_input.gd.uid
Normal file
1
deck_input.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c0vxigp5y302o
|
6
event_bus.gd
Normal file
6
event_bus.gd
Normal 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
1
event_bus.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b5re77jjgr8ca
|
44
field.gd
Normal file
44
field.gd
Normal file
@ -0,0 +1,44 @@
|
||||
extends TextureRect
|
||||
|
||||
var _screen_size: Vector2
|
||||
var _colors: Array[Color]
|
||||
|
||||
var _card_class = preload("res://scenes/card/card.tscn")
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
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_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.
|
||||
card.init("d3f10f07-7cfe-4a6f-8de6-373e367a731b", _screen_size)
|
||||
|
||||
add_child(card)
|
||||
|
||||
|
||||
func set_colors(colors: Array[Color]) -> void:
|
||||
_colors = colors
|
||||
|
||||
# TODO: Method to take list of colors, split into this format of dictionary, and apply as gradient.
|
||||
var gradient_data := {
|
||||
0.0: Color.MAROON,
|
||||
1.0: Color.MAROON,
|
||||
}
|
||||
|
||||
var gradient := Gradient.new()
|
||||
gradient.offsets = gradient_data.keys()
|
||||
gradient.colors = gradient_data.values()
|
||||
|
||||
var gradient_texture = GradientTexture1D.new()
|
||||
gradient_texture.gradient = gradient
|
||||
|
||||
texture = gradient_texture
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(_delta: float) -> void:
|
||||
pass
|
1
field.gd.uid
Normal file
1
field.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cqutu8u3qenu0
|
9
field.tscn
Normal file
9
field.tscn
Normal file
@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://clnevm4xcexrs"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cqutu8u3qenu0" path="res://field.gd" id="1_6e7u2"]
|
||||
|
||||
[node name="Field" type="TextureRect"]
|
||||
offset_top = 540.0
|
||||
offset_right = 1520.0
|
||||
offset_bottom = 1080.0
|
||||
script = ExtResource("1_6e7u2")
|
61
flake.lock
generated
61
flake.lock
generated
@ -1,61 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1744868846,
|
||||
"narHash": "sha256-5RJTdUHDmj12Qsv7XOhuospjAjATNiTMElplWnJE9Hs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ebe4301cbd8f81c4f8d3244b3632338bbeb6d49c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
30
flake.nix
30
flake.nix
@ -1,30 +0,0 @@
|
||||
{
|
||||
description = "Template node project";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
gnumake
|
||||
glfw
|
||||
pkg-config
|
||||
glib.dev
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
15
hand.gd
Normal file
15
hand.gd
Normal 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
1
hand.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dvu4gdhqjejeo
|
1
icon.svg
Normal file
1
icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
After Width: | Height: | Size: 994 B |
37
icon.svg.import
Normal file
37
icon.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://nrgr3sfkdosx"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
43
library.gd
Normal file
43
library.gd
Normal file
@ -0,0 +1,43 @@
|
||||
extends Node2D
|
||||
|
||||
var _card_class = preload("res://scenes/card/card.gd")
|
||||
|
||||
# Library cards are represented as an array of card IDs.
|
||||
var lib_cards: Array[String]
|
||||
var num_cards: int = 0
|
||||
|
||||
|
||||
func _load_card_callback(card) -> void:
|
||||
card.load_card()
|
||||
|
||||
|
||||
func _init(_decklist: Dictionary) -> void:
|
||||
lib_cards = Array()
|
||||
for card in _decklist:
|
||||
var _num = _decklist[card]
|
||||
num_cards += _num
|
||||
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)
|
||||
|
||||
|
||||
func add_card(card, top: bool) -> void:
|
||||
if top:
|
||||
lib_cards.push_front(card)
|
||||
else:
|
||||
lib_cards.push_back(card)
|
||||
|
||||
|
||||
func shuffle() -> void:
|
||||
lib_cards.shuffle()
|
||||
|
||||
|
||||
func draw_cards(num) -> Array:
|
||||
var ret = Array()
|
||||
for i in num:
|
||||
ret.push_back(lib_cards.pop_front())
|
||||
return ret
|
1
library.gd.uid
Normal file
1
library.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bc51go8t8uvts
|
36
player.gd
Normal file
36
player.gd
Normal file
@ -0,0 +1,36 @@
|
||||
extends Node2D
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#fields.append(field_scene.instantiate())
|
||||
#var colors: Array[Color] = [Color(1, 0, 1)]
|
||||
#fields[0].set_colors(colors)
|
||||
#add_child(fields[0])
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(_delta: float) -> void:
|
||||
pass
|
1
player.gd.uid
Normal file
1
player.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://w2rqm1u7p7im
|
21
player.tscn
Normal file
21
player.tscn
Normal file
@ -0,0 +1,21 @@
|
||||
[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="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")
|
43
project.godot
Normal file
43
project.godot
Normal file
@ -0,0 +1,43 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="mtg-tabletop"
|
||||
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
|
||||
window/size/viewport_height=1080
|
||||
window/size/mode=3
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray()
|
||||
|
||||
[input]
|
||||
|
||||
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
72
scenes/card/card.gd
Normal 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")
|
1
scenes/card/card.gd.uid
Normal file
1
scenes/card/card.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b3yqd1qu7dyq
|
31
scenes/card/card.tscn
Normal file
31
scenes/card/card.tscn
Normal 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
27
scenes/card/input.gd
Normal 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
1
scenes/card/input.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dhgk6fhw8oua0
|
83
scenes/card/load.gd
Normal file
83
scenes/card/load.gd
Normal 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
1
scenes/card/load.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://vckbno504iay
|
35
scenes/card/tween.gd
Normal file
35
scenes/card/tween.gd
Normal 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
1
scenes/card/tween.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bkk0pyypi1id7
|
17
scenes/tooltip/card_image.gd
Normal file
17
scenes/tooltip/card_image.gd
Normal 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)
|
1
scenes/tooltip/card_image.gd.uid
Normal file
1
scenes/tooltip/card_image.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cpvbftm0swoa6
|
39
scenes/tooltip/card_text.gd
Normal file
39
scenes/tooltip/card_text.gd
Normal 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)
|
1
scenes/tooltip/card_text.gd.uid
Normal file
1
scenes/tooltip/card_text.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b8tioen4n1rip
|
1140
src/glad.c
1140
src/glad.c
File diff suppressed because it is too large
Load Diff
3611
src/include/glad.h
3611
src/include/glad.h
File diff suppressed because it is too large
Load Diff
@ -1,114 +0,0 @@
|
||||
#ifndef SHADER_H
|
||||
#define SHADER_H
|
||||
|
||||
#include "glad.h"
|
||||
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
class Shader
|
||||
{
|
||||
public:
|
||||
// program ID
|
||||
unsigned int ID;
|
||||
|
||||
// constructor reads and builds the shader
|
||||
Shader(const char* vertexPath, const char* fragmentPath)
|
||||
{
|
||||
std::string vertexCode;
|
||||
std::string fragmentCode;
|
||||
std::ifstream vShaderFile;
|
||||
std::ifstream fShaderFile;
|
||||
|
||||
// setup exceptions for bad files
|
||||
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
|
||||
try {
|
||||
// open files
|
||||
vShaderFile.open(vertexPath);
|
||||
fShaderFile.open(fragmentPath);
|
||||
std::stringstream vShaderStream, fShaderStream;
|
||||
|
||||
// read file buffers into streams
|
||||
vShaderStream << vShaderFile.rdbuf();
|
||||
fShaderStream << fShaderFile.rdbuf();
|
||||
|
||||
// close file handlers
|
||||
vShaderFile.close();
|
||||
fShaderFile.close();
|
||||
|
||||
// convert stream into string
|
||||
vertexCode = vShaderStream.str();
|
||||
fragmentCode = fShaderStream.str();
|
||||
} catch (std::ifstream::failure& e) {
|
||||
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ:" << e.what() << "\n";
|
||||
}
|
||||
|
||||
const char* vShaderCode = vertexCode.c_str();
|
||||
const char* fShaderCode = fragmentCode.c_str();
|
||||
|
||||
unsigned int vertex, fragment;
|
||||
int success;
|
||||
char infoLog[512];
|
||||
vertex = glCreateShader(GL_VERTEX_SHADER);
|
||||
|
||||
glShaderSource(vertex, 1, &vShaderCode, NULL);
|
||||
glCompileShader(vertex);
|
||||
|
||||
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
|
||||
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << "\n";
|
||||
}
|
||||
|
||||
fragment = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fragment, 1, &fShaderCode, NULL);
|
||||
glCompileShader(fragment);
|
||||
|
||||
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
|
||||
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << "\n";
|
||||
}
|
||||
|
||||
ID = glCreateProgram();
|
||||
glAttachShader(ID, vertex);
|
||||
glAttachShader(ID, fragment);
|
||||
glLinkProgram(ID);
|
||||
|
||||
glGetProgramiv(ID, GL_LINK_STATUS, &success);
|
||||
if (!success) {
|
||||
glGetProgramInfoLog(ID, 512, NULL, infoLog);
|
||||
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << "\n";
|
||||
}
|
||||
|
||||
// delete shaders, no longer needed
|
||||
glDeleteShader(vertex);
|
||||
glDeleteShader(fragment);
|
||||
|
||||
}
|
||||
|
||||
// use/activate the shader
|
||||
void use()
|
||||
{
|
||||
glUseProgram(ID);
|
||||
}
|
||||
|
||||
// utility uniform functions
|
||||
void setBool(const std::string &name, bool value) const
|
||||
{
|
||||
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int) value);
|
||||
}
|
||||
void setInt(const std::string &name, int value) const
|
||||
{
|
||||
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
|
||||
}
|
||||
void setFloat(const std::string &name, float value) const
|
||||
{
|
||||
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
157
src/main.cpp
157
src/main.cpp
@ -1,157 +0,0 @@
|
||||
#include "include/glad.h"
|
||||
#include "include/shader.h"
|
||||
#include "include/stb_image.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <iostream>
|
||||
#include <math.h>
|
||||
|
||||
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
|
||||
void processInput(GLFWwindow* window);
|
||||
void setupGLFW(GLFWwindow** window);
|
||||
|
||||
typedef struct {
|
||||
int width, height, nrChannels;
|
||||
unsigned char* data;
|
||||
} texture_t;
|
||||
|
||||
float vertices[] = {
|
||||
// positions colours texture coords
|
||||
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
|
||||
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right
|
||||
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
|
||||
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // bottom left
|
||||
};
|
||||
|
||||
unsigned int indices[] = {
|
||||
0, 1, 3, 1, 2, 3,
|
||||
};
|
||||
|
||||
bool wireframe = false;
|
||||
|
||||
int main()
|
||||
{
|
||||
GLFWwindow* window;
|
||||
setupGLFW(&window);
|
||||
|
||||
Shader myShader("shaders/shader.vs", "shaders/shader.fs");
|
||||
|
||||
texture_t tex;
|
||||
|
||||
stbi_set_flip_vertically_on_load(true);
|
||||
|
||||
tex.data = stbi_load("textures/wg.jpg", &tex.width, &tex.height, &tex.nrChannels, 0);
|
||||
|
||||
unsigned int texture;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
|
||||
if (tex.data) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tex.width, tex.height, 0, GL_RGB, GL_UNSIGNED_BYTE, tex.data);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
} else {
|
||||
std::cout << "Failed to load texture\n";
|
||||
}
|
||||
|
||||
stbi_image_free(tex.data);
|
||||
|
||||
unsigned int VAO;
|
||||
glGenVertexArrays(1, &VAO);
|
||||
|
||||
unsigned int VBO;
|
||||
glGenBuffers(1, &VBO);
|
||||
|
||||
unsigned int EBO;
|
||||
glGenBuffers(1, &EBO);
|
||||
|
||||
// bind Vertex Array Object
|
||||
glBindVertexArray(VAO);
|
||||
// copy our vertices into a buffer for opengl to use
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||
// copy our indices
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
|
||||
|
||||
// set the vertex attributes pointers
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
|
||||
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
|
||||
glEnableVertexAttribArray(2);
|
||||
|
||||
myShader.use();
|
||||
myShader.setInt("tex", 0);
|
||||
|
||||
while (!glfwWindowShouldClose(window)) {
|
||||
// input
|
||||
processInput(window);
|
||||
|
||||
// rendering
|
||||
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
myShader.use();
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
glBindVertexArray(VAO);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
glBindVertexArray(0); // unbinds the VAO so its ready to be bound again for the next render
|
||||
|
||||
// check and call events and swap buffers
|
||||
glfwSwapBuffers(window);
|
||||
glfwPollEvents();
|
||||
}
|
||||
|
||||
glfwTerminate();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setupGLFW(GLFWwindow** window)
|
||||
{
|
||||
glfwInit();
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
|
||||
*window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
|
||||
if (window == NULL) {
|
||||
std::cout << "Failed to create GLFW window\n";
|
||||
glfwTerminate();
|
||||
exit(-1);
|
||||
}
|
||||
glfwMakeContextCurrent(*window);
|
||||
|
||||
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
|
||||
std::cout << "Failed to init GLAD\n";
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
glViewport(0, 0, 800, 600);
|
||||
glfwSetFramebufferSizeCallback(*window, framebuffer_size_callback);
|
||||
}
|
||||
|
||||
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
|
||||
{
|
||||
glViewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
void processInput(GLFWwindow* window)
|
||||
{
|
||||
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
|
||||
glfwSetWindowShouldClose(window, true);
|
||||
if (glfwGetKey(window, GLFW_KEY_ENTER) == GLFW_PRESS) // triggers wireframe mode
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||
if (glfwGetKey(window, GLFW_KEY_BACKSPACE) == GLFW_PRESS) // disables wireframe mode
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
#version 330 core
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec3 ourColor;
|
||||
in vec2 TexCoord;
|
||||
|
||||
uniform sampler2D tex;
|
||||
|
||||
void main()
|
||||
{
|
||||
FragColor = texture(tex, TexCoord);
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
#version 330 core
|
||||
|
||||
layout (location = 0) in vec3 aPos;
|
||||
layout (location = 1) in vec3 aColor;
|
||||
layout (location = 2) in vec2 aTexCoord;
|
||||
|
||||
out vec3 ourColor;
|
||||
out vec2 TexCoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(aPos, 1.0);
|
||||
ourColor = aColor;
|
||||
TexCoord = aTexCoord;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "include/stb_image.h"
|
Binary file not shown.
Before Width: | Height: | Size: 43 KiB |
Binary file not shown.
Before Width: | Height: | Size: 119 KiB |
Binary file not shown.
Before Width: | Height: | Size: 142 KiB |
125
tabletop.gd
Normal file
125
tabletop.gd
Normal file
@ -0,0 +1,125 @@
|
||||
extends Node2D
|
||||
|
||||
var player_class = preload("res://player.gd")
|
||||
var deck_input = preload("res://deck_input.gd")
|
||||
var _caching = preload("res://caching.gd")
|
||||
|
||||
|
||||
var cards = "1 Arcane Signet
|
||||
1 Austere Command
|
||||
1 Bartolomé del Presidio
|
||||
1 Blade of the Bloodchief
|
||||
1 Blood Artist
|
||||
1 Bloodghast
|
||||
1 Bloodline Necromancer
|
||||
1 Bloodtracker
|
||||
1 Bojuka Bog
|
||||
1 Butcher of Malakir
|
||||
1 Carmen, Cruel Skymarcher
|
||||
1 Champion of Dusk
|
||||
1 Charismatic Conqueror
|
||||
1 Command Tower
|
||||
1 Commander's Sphere
|
||||
1 Cordial Vampire
|
||||
1 Crossway Troublemakers
|
||||
1 Cruel Celebrant
|
||||
1 Damn
|
||||
1 Drana, Liberator of Malakir
|
||||
1 Dusk Legion Sergeant
|
||||
1 Dusk Legion Zealot
|
||||
1 Elenda, the Dusk Rose
|
||||
1 Elenda's Hierophant
|
||||
1 Etchings of the Chosen
|
||||
1 Exquisite Blood
|
||||
1 Falkenrath Noble
|
||||
1 Glass-Cast Heart
|
||||
1 Heirloom Blade
|
||||
1 Indulgent Aristocrat
|
||||
1 Isolated Chapel
|
||||
1 Kindred Boon
|
||||
1 Legion Lieutenant
|
||||
1 March of the Canonized
|
||||
1 Martyr of Dusk
|
||||
1 Master of Dark Rites
|
||||
1 Mavren Fein, Dusk Apostle
|
||||
1 Mind Stone
|
||||
1 Myriad Landscape
|
||||
1 New Blood
|
||||
1 Nighthawk Scavenger
|
||||
1 Oathsworn Vampire
|
||||
1 Olivia's Wrath
|
||||
1 Order of Sacred Dusk
|
||||
1 Orzhov Basilica
|
||||
1 Orzhov Signet
|
||||
1 Pact of the Serpent
|
||||
1 Path of Ancestry
|
||||
1 Patron of the Vein
|
||||
4 Plains
|
||||
4 Plains
|
||||
1 Promise of Aclazotz
|
||||
1 Radiant Destiny
|
||||
1 Redemption Choir
|
||||
1 Return to Dust
|
||||
1 Rogue's Passage
|
||||
1 Sanctum Seeker
|
||||
1 Secluded Courtyard
|
||||
1 Shineshadow Snarl
|
||||
1 Sol Ring
|
||||
1 Sorin, Lord of Innistrad
|
||||
7 Swamp
|
||||
6 Swamp
|
||||
1 Swiftfoot Boots
|
||||
1 Swords to Plowshares
|
||||
1 Tainted Field
|
||||
1 Talisman of Hierarchy
|
||||
1 Temple of Silence
|
||||
1 Temple of the False God
|
||||
1 Timothar, Baron of Bats
|
||||
1 Twilight Prophet
|
||||
1 Unclaimed Territory
|
||||
1 Utter End
|
||||
1 Vault of the Archangel
|
||||
1 Village Rites
|
||||
1 Viscera Seer
|
||||
1 Voldaren Estate
|
||||
1 Vona, Butcher of Magan
|
||||
1 Wayfarer's Bauble
|
||||
1 Welcoming Vampire
|
||||
1 Windbrisk Heights
|
||||
1 Yahenni, Undying Partisan
|
||||
|
||||
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.
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(_delta: float) -> void:
|
||||
pass
|
1
tabletop.gd.uid
Normal file
1
tabletop.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cfkew150yl1y3
|
65
tabletop.tscn
Normal file
65
tabletop.tscn
Normal file
@ -0,0 +1,65 @@
|
||||
[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)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_d43bn"]
|
||||
bg_color = Color(0.6, 0.6, 0.6, 0.709804)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pqag1"]
|
||||
bg_color = Color(0.6, 0.6, 0.6, 0)
|
||||
|
||||
[node name="Tabletop" type="Node2D"]
|
||||
script = ExtResource("1_3we3x")
|
||||
|
||||
[node name="UI" type="Control" parent="."]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
|
||||
[node name="BigBar" type="PanelContainer" parent="UI"]
|
||||
layout_mode = 0
|
||||
offset_left = 1520.0
|
||||
offset_right = 1920.0
|
||||
offset_bottom = 1080.0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_3we3x")
|
||||
|
||||
[node name="Items" type="VBoxContainer" parent="UI/BigBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="MenuArea" type="PanelContainer" parent="UI/BigBar/Items"]
|
||||
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
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_d43bn")
|
||||
|
||||
[node name="LibraryArea" type="PanelContainer" parent="UI/BigBar/Items"]
|
||||
custom_minimum_size = Vector2(0, 270)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_pqag1")
|
Reference in New Issue
Block a user