straywild/tests/inventory_notepad_art_validation.gd

706 lines
23 KiB
GDScript

extends SceneTree
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
const HotbarScene = preload("res://ui/hotbar.tscn")
const UIReferencePresentationType = preload(
"res://ui/ui_reference_presentation.gd"
)
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const PelicanBuyer = preload("res://economy/buyers/pelicans.tres")
const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0)
const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8)
const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4)
const LEGACY_COOLER_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
const LEGACY_COOLER_NOTEPAD_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
const INVENTORY_PANEL_RECT := Rect2(199.0, 166.0, 882.0, 484.0)
const NOTEPAD_HOST_RECT := Rect2(501.0, 166.0, 278.0, 484.0)
const NOTEPAD_ART_RECT := Rect2(
NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
EXPECTED_ART_SIZE,
)
const HOTBAR_BUBBLE_FIELD_RECT := Rect2(245.0, 608.0, 790.0, 104.0)
const TARGET_SIZES: Array[Vector2] = [
Vector2(1280.0, 720.0),
Vector2(1920.0, 1080.0),
Vector2(2560.0, 1440.0),
Vector2(3840.0, 2160.0),
Vector2(640.0, 480.0),
Vector2(1024.0, 768.0),
Vector2(1729.0, 973.0),
Vector2(2560.0, 1080.0),
Vector2(3440.0, 1440.0),
]
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_print_runtime_parameters()
var presentation_stage := Control.new()
presentation_stage.name = "CanonicalUIStage"
_apply_stage_layout(presentation_stage, Vector2(root.size))
root.add_child(presentation_stage)
var player_menu := PlayerMenuScene.instantiate() as PlayerMenu
presentation_stage.add_child(player_menu)
await process_frame
var notepads: Array[Node] = player_menu.find_children(
"*", "InventoryNotepad", true, false
)
assert(notepads.size() == 3)
for node: Node in notepads:
var notepad := node as InventoryNotepad
assert(notepad != null)
assert(notepad.size.is_equal_approx(EXPECTED_HOST_SIZE))
assert(notepad.mouse_filter == Control.MOUSE_FILTER_PASS)
var art_rect: Rect2 = notepad.get_art_rect()
assert(art_rect.size.is_equal_approx(EXPECTED_ART_SIZE))
assert(art_rect.position.is_equal_approx(EXPECTED_ART_POSITION))
assert(art_rect.get_center().is_equal_approx(
EXPECTED_HOST_SIZE * 0.5
+ InventoryNotepad.NOTEPAD_ART_OFFSET
))
_validate_shared_inventory_geometry(player_menu)
_validate_first_open_inventory(player_menu)
await _validate_inventory_grid_centering(player_menu, presentation_stage)
_validate_utility_page_geometry(player_menu)
await _validate_profile_content_bounds(player_menu)
await _validate_hotbar_centering(presentation_stage)
_validate_inventory_layering(player_menu)
_validate_sale_confirmation_copy(player_menu)
_validate_tackle_to_items_transition(player_menu)
_validate_cooler_notepad_typography(player_menu)
_validate_resolution_matrix()
await _capture_inventory_pages(player_menu)
print("Inventory notepad artwork validation: PASS")
presentation_stage.queue_free()
for _frame: int in 10:
await process_frame
await create_timer(0.1).timeout
quit()
func _print_runtime_parameters() -> void:
var window: Window = root
var screen_index: int = window.current_screen
print(
(
"Runtime UI: window=%s screen=%s screen_scale=%.3f dpi=%d "
+ "content_size=%s content_mode=%d content_aspect=%d "
+ "content_factor=%.3f"
)
% [
window.size,
DisplayServer.screen_get_size(screen_index),
DisplayServer.screen_get_scale(screen_index),
DisplayServer.screen_get_dpi(screen_index),
window.content_scale_size,
window.content_scale_mode,
window.content_scale_aspect,
window.content_scale_factor,
]
)
func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
stage.position = UIReferencePresentationType.get_offset(display_size)
stage.size = UIReferencePresentationType.REFERENCE_SIZE
stage.scale = Vector2.ONE * UIReferencePresentationType.get_scale(
display_size
)
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
for node_name: StringName in [
&"BagOuterWall",
&"TackleMainPanel",
]:
var main_panel := player_menu.get_node("%%%s" % node_name) as Control
assert(main_panel != null)
assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
assert(
main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size),
"%s was %s, expected %s" % [
node_name,
main_panel.size,
INVENTORY_PANEL_RECT.size,
],
)
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
var cooler_notepad := player_menu.get_node("%DetailConstellation") as Control
assert(cooler_panel.position.is_equal_approx(
LEGACY_COOLER_PANEL_RECT.position
))
assert(cooler_panel.size.is_equal_approx(LEGACY_COOLER_PANEL_RECT.size))
assert(cooler_notepad.position.is_equal_approx(
LEGACY_COOLER_NOTEPAD_RECT.position
))
assert(cooler_notepad.size.is_equal_approx(
LEGACY_COOLER_NOTEPAD_RECT.size
))
for node_name: StringName in [
&"BagDetailConstellation",
&"TackleDetailPanel",
]:
var host := player_menu.get_node("%%%s" % node_name) as Control
assert(host != null)
assert(host.position.is_equal_approx(NOTEPAD_HOST_RECT.position))
assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
assert(is_equal_approx(INVENTORY_PANEL_RECT.get_center().x, 640.0))
func _validate_first_open_inventory(player_menu: PlayerMenu) -> void:
var legacy_slots: Array = player_menu.get("_bag_slot_nodes") as Array
assert(legacy_slots.is_empty())
var old_slot_nodes: Array[Node] = player_menu.find_children(
"*", "BagStorageSlot", true, false
)
assert(old_slot_nodes.is_empty())
func _validate_inventory_grid_centering(
player_menu: PlayerMenu,
stage: Control,
) -> void:
var grid := player_menu.get("_general_inventory_grid") as Control
assert(grid != null)
grid.custom_minimum_size = Vector2(782.0, 342.0)
grid.size = grid.custom_minimum_size
player_menu.call("_layout_general_inventory_grid")
await process_frame
assert(is_equal_approx(grid.position.y, 39.0))
assert(
is_equal_approx(
grid.get_global_rect().get_center().x,
stage.get_global_rect().get_center().x,
),
"inventory center %s did not match stage center %s" % [
grid.get_global_rect().get_center().x,
stage.get_global_rect().get_center().x,
],
)
func _validate_utility_page_geometry(player_menu: PlayerMenu) -> void:
for page_name: StringName in [
&"TheNetPage",
&"MailPage",
&"ProfilePage",
&"PlayersPage",
]:
var page := player_menu.get_node("%%%s" % page_name) as Control
assert(page != null)
var shell := page.find_child(
"UtilityMainBox", true, false
) as Control
assert(shell != null)
assert(shell.position.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.position))
assert(shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size))
assert(is_equal_approx(shell.get_rect().get_center().x, 640.0))
func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void:
var profile := player_menu.get_node("%ProfilePage") as ProfilePage
assert(profile != null)
var menu_was_visible: bool = player_menu.visible
var profile_was_visible: bool = profile.visible
player_menu.visible = true
profile.visible = true
await process_frame
await process_frame
var option_list := profile.get("_option_list") as Control
var body := option_list.get_parent() as Control
var preview := profile.get("_preview") as Control
var shell := profile.find_child("UtilityMainBox", true, false) as Control
var content_zone := profile.find_child(
"ProfileContentZone", true, false
) as Control
var profile_layout := profile.find_child(
"ProfileLayout", true, false
) as Control
var account_row := profile.find_child(
"ProfileAccountRow", true, false
) as Control
var body_panel := profile.find_child(
"ProfileBodyPanel", true, false
) as Control
assert(option_list != null and body != null and preview != null)
assert(shell != null and content_zone != null and profile_layout != null)
assert(account_row != null and body_panel != null)
var content_frame_size := Vector2(
UtilityPageStyle.LAPTOP_RECT.size.x - 40.0,
UtilityPageStyle.LAPTOP_RECT.size.y - 42.0,
)
assert(
content_zone.size.is_equal_approx(content_frame_size),
(
"profile content zone changed size: %s != %s; "
+ "layout min=%s account min=%s body min=%s"
) % [
content_zone.size,
content_frame_size,
profile_layout.get_combined_minimum_size(),
account_row.get_combined_minimum_size(),
body_panel.get_combined_minimum_size(),
],
)
assert(
profile_layout.size.is_equal_approx(
UtilityPageStyle.LAPTOP_CONTENT_SIZE
),
"profile layout changed size: %s != %s" % [
profile_layout.size,
UtilityPageStyle.LAPTOP_CONTENT_SIZE,
],
)
assert(
profile_layout.get_combined_minimum_size().x
<= profile_layout.size.x,
"profile layout exceeded shared content width: %s > %s" % [
profile_layout.get_combined_minimum_size().x,
profile_layout.size.x,
],
)
var content_rect: Rect2 = profile_layout.get_global_rect()
for section: Control in [account_row, body_panel]:
var section_rect: Rect2 = section.get_global_rect()
assert(section_rect.position.x >= content_rect.position.x - 0.5)
assert(section_rect.end.x <= content_rect.end.x + 0.5)
var category_ids: Array[String] = []
for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS:
category_ids.append(category_id)
category_ids.append("voice")
for category_id: String in category_ids:
profile.call("_select_category", category_id)
await process_frame
await process_frame
assert(
shell.position.is_equal_approx(
UtilityPageStyle.LAPTOP_RECT.position
),
"%s profile shell position changed: %s" % [
category_id,
shell.position,
],
)
assert(
shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size),
"%s profile shell size changed: %s != %s" % [
category_id,
shell.size,
UtilityPageStyle.LAPTOP_RECT.size,
],
)
assert(
content_zone.size.is_equal_approx(content_frame_size),
"%s profile content zone changed size: %s != %s" % [
category_id,
content_zone.size,
content_frame_size,
],
)
assert(
profile_layout.size.is_equal_approx(
UtilityPageStyle.LAPTOP_CONTENT_SIZE
),
"%s profile layout changed size: %s != %s" % [
category_id,
profile_layout.size,
UtilityPageStyle.LAPTOP_CONTENT_SIZE,
],
)
assert(
profile_layout.get_combined_minimum_size().x
<= profile_layout.size.x,
"%s profile layout exceeded shared content width: %s > %s" % [
category_id,
profile_layout.get_combined_minimum_size().x,
profile_layout.size.x,
],
)
var category_body_rect: Rect2 = body_panel.get_global_rect()
assert(
category_body_rect.end.x <= content_rect.end.x + 0.5,
"%s body overflowed shared content: %s > %s" % [
category_id,
category_body_rect.end.x,
content_rect.end.x,
],
)
assert(
category_body_rect.end.y <= content_rect.end.y + 0.5,
"%s body overflowed shared content vertically: %s > %s" % [
category_id,
category_body_rect.end.y,
content_rect.end.y,
],
)
assert(
body.get_combined_minimum_size().x <= body.size.x,
"%s customization content overflowed its body: %s > %s" % [
category_id,
body.get_combined_minimum_size().x,
body.size.x,
],
)
assert(
preview.get_global_rect().end.x
<= content_rect.end.x + 0.5,
"%s preview overflowed the utility content area" % category_id,
)
profile.call("_select_category", "fur_pattern")
profile.call("_select_fur_section", "colors")
await process_frame
await process_frame
var color_panel := profile.find_child(
"FurColorOptionsPanel", true, false
) as Control
var color_scroll := profile.find_child(
"FurColorScroll", true, false
) as ScrollContainer
assert(color_panel != null and color_scroll != null)
assert(
color_panel.get_global_rect().end.y
<= body_panel.get_global_rect().end.y + 0.5,
"fur color panel overflowed profile body: %s > %s" % [
color_panel.get_global_rect().end.y,
body_panel.get_global_rect().end.y,
],
)
assert(
profile_layout.get_combined_minimum_size().y
<= profile_layout.size.y,
"fur colors expanded profile layout vertically: %s > %s" % [
profile_layout.get_combined_minimum_size().y,
profile_layout.size.y,
],
)
profile.visible = profile_was_visible
player_menu.visible = menu_was_visible
func _validate_hotbar_centering(parent: Control) -> void:
var hotbar := HotbarScene.instantiate() as HotbarUI
parent.add_child(hotbar)
await process_frame
var presentation := hotbar.get_node(
"%HotbarPresentationScaleRoot"
) as Control
var field := hotbar.get_node("%BubbleField") as Control
hotbar.set_player_menu_context(true)
var displayed_center_x: float = (
presentation.position.x
+ field.get_rect().get_center().x * presentation.scale.x
)
assert(is_equal_approx(displayed_center_x, 640.0))
var displayed_top: float = (
presentation.position.y + field.position.y * presentation.scale.y
)
var displayed_bottom: float = (
displayed_top + field.size.y * presentation.scale.y
)
assert(displayed_top < INVENTORY_PANEL_RECT.end.y)
assert(displayed_bottom > INVENTORY_PANEL_RECT.end.y)
hotbar.queue_free()
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
var bag_panel := player_menu.get_node("%BagOuterWall") as Control
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
var tackle_panel := player_menu.get_node("%TackleMainPanel") as Control
var inventory_tabs := player_menu.get_node("%InventorySubTabs") as Control
var items_tab := player_menu.get_node("%ItemsSubTab") as Button
var bait_list := player_menu.get_node("%BaitItemList") as GridContainer
var lure_list := player_menu.get_node("%LureItemList") as GridContainer
assert(bag_panel != null)
assert(sale_confirmation != null)
assert(cooler_panel != null)
assert(tackle_panel != null)
assert(inventory_tabs != null)
assert(items_tab != null and not items_tab.visible)
assert(not (player_menu.get_node("%CoolerSubTab") as Button).visible)
assert((player_menu.get_node("%BagSubTab") as Button).text == "Inventory")
assert(bait_list != null and bait_list.columns == 4)
assert(lure_list != null and lure_list.columns == 4)
assert(bait_list.get_theme_constant("h_separation") == 28)
assert(lure_list.get_theme_constant("h_separation") == 28)
assert(sale_confirmation.z_index > cooler_panel.z_index)
assert(sale_confirmation.z_index > tackle_panel.z_index)
assert(
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
> bag_panel.z_index
)
assert(
(player_menu.get_node("%BagModalBlocker") as Control).z_index
> bag_panel.z_index
)
assert(
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
> (player_menu.get_node("%BagModalBlocker") as Control).z_index
)
assert(
(player_menu.get_node("%TackleDetailPanel") as Control).z_index
> tackle_panel.z_index
)
# The contextual Hotbar uses z=90 while the Player Menu is open.
assert(sale_confirmation.z_index > 90)
assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0)))
assert(sale_confirmation.size.is_equal_approx(Vector2(520.0, 200.0)))
var confirmation_style := sale_confirmation.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(confirmation_style != null)
assert(
confirmation_style.bg_color.is_equal_approx(
UtilityPageStyle.OCEAN_PANEL_DEEP
)
)
func _validate_sale_confirmation_copy(player_menu: PlayerMenu) -> void:
var preview := FishSaleResultType.new()
preview.fish_count = 1
preview.payout = 3
preview.base_value = 12
var message: String = str(player_menu.call(
"_sale_confirmation_text",
preview,
PelicanBuyer,
preview.base_value,
))
assert(message == (
"[center]You can sell this fish to the pelicans now for %s, "
+ "but the shop is willing to pay %s![/center]"
) % [
CurrencyPresentationType.bbcode_amount(preview.payout, 22),
CurrencyPresentationType.bbcode_amount(preview.base_value, 22),
])
assert((player_menu.get_node("%ConfirmSaleButton") as Button).text == "sell now")
assert((player_menu.get_node("%CancelSaleButton") as Button).text == "nevermind")
func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
var inventory_notepad := player_menu.get_node(
"%BagDetailBubble"
) as InventoryNotepad
assert(inventory_notepad != null)
assert(inventory_notepad.title_text == "inventory notes")
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
)
player_menu.call("_show_inventory_tab", 0)
player_menu.call("_cancel_page_tween")
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
player_menu.call("_update_bag_detail")
assert(player_menu.get("_current_section") == PlayerMenu.Section.BAG)
assert(not (player_menu.get_node("%BagDetailConstellation") as Control).visible)
assert(
(player_menu.get_node("%BagSpriteDetailData") as Label).text
== "select an item for details."
)
player_menu.visible = true
player_menu.call("_set_content_interactive", true)
var no_actions: Array[BaseButton] = []
player_menu.call(
"_open_inventory_notepad",
PlayerMenu.Section.BAG,
StringName("modal-test"),
no_actions,
)
assert((player_menu.get_node("%BagDetailConstellation") as Control).visible)
assert((player_menu.get_node("%BagModalBlocker") as Control).visible)
assert(
(player_menu.get_node("%BagModalBlocker") as Control).mouse_filter
== Control.MOUSE_FILTER_STOP
)
assert(
(player_menu.get_node("%InventoryTab") as Button).focus_mode
== Control.FOCUS_NONE
)
assert(
(player_menu.get_node("%BagSpriteDetailData") as Label).get_theme_font(
"font"
) == InventoryNotepad.NOTEPAD_FONT
)
player_menu.call(
"_release_controller_ownership", false, false
)
assert(not (player_menu.get_node("%BagModalBlocker") as Control).visible)
player_menu.call("_set_content_interactive", false)
player_menu.visible = false
player_menu.call("_cancel_page_tween")
func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
var sort_choice := player_menu.get_node("%CoolerSortOption") as Control
var sort_direction := player_menu.get_node(
"%CoolerSortDirection"
) as Button
var empty_selection := player_menu.get_node(
"%CoolerSelectionEmpty"
) as Label
var favorite := player_menu.get_node("%FavoriteBubble") as Button
var sell := player_menu.get_node("%SellBubble") as Button
assert(sort_choice != null)
assert(sort_direction != null)
assert(empty_selection != null)
assert(favorite != null and sell != null)
assert(player_menu.get_node_or_null("%SellAllBubble") == null)
var displayed_value := sort_choice.get_node("%DisplayedValue") as Label
assert(displayed_value != null)
assert(displayed_value.get_theme_font_size("font_size") == 17)
for choice_name: StringName in [
&"CatchOrderChoice",
&"NameChoice",
&"RarityChoice",
]:
var choice := sort_choice.get_node("%%%s" % choice_name) as Button
assert(choice != null)
assert(choice.get_theme_font_size("font_size") == 17)
assert(sort_direction.get_theme_font_size("font_size") == 17)
assert(empty_selection.get_theme_font_size("font_size") == 20)
assert(favorite.get_theme_font_size("font_size") == 19)
assert(sell.get_theme_font_size("font_size") == 19)
func _validate_resolution_matrix() -> void:
for target_size: Vector2 in TARGET_SIZES:
var stage_rect: Rect2 = UIReferencePresentationType.get_rect(
target_size
)
var stage_scale: float = UIReferencePresentationType.get_scale(
target_size
)
assert(is_equal_approx(
stage_rect.size.x / stage_rect.size.y,
1280.0 / 720.0,
))
assert(stage_rect.get_center().is_equal_approx(target_size * 0.5))
_validate_transformed_rect(
"Inventory panel", INVENTORY_PANEL_RECT, stage_rect, stage_scale
)
_validate_transformed_rect(
"Notepad host", NOTEPAD_HOST_RECT, stage_rect, stage_scale
)
_validate_transformed_rect(
"Notepad art", NOTEPAD_ART_RECT, stage_rect, stage_scale
)
_validate_transformed_rect(
"Hotbar", HOTBAR_BUBBLE_FIELD_RECT, stage_rect, stage_scale
)
var screen_host: Rect2 = _to_screen_rect(
NOTEPAD_HOST_RECT, stage_rect.position, stage_scale
)
var screen_art: Rect2 = _to_screen_rect(
NOTEPAD_ART_RECT, stage_rect.position, stage_scale
)
assert(is_equal_approx(
screen_art.size.y,
screen_host.size.y * InventoryNotepad.NOTEPAD_CANONICAL_OVERSCAN,
))
assert(screen_art.get_center().is_equal_approx(
screen_host.get_center()
+ InventoryNotepad.NOTEPAD_ART_OFFSET * stage_scale
))
print(
(
"UI geometry: window=%s stage=%s scale=%.6f "
+ "inventory=%s notepad=%s art=%s hotbar=%s"
)
% [
target_size,
stage_rect,
stage_scale,
_to_screen_rect(
INVENTORY_PANEL_RECT,
stage_rect.position,
stage_scale,
),
screen_host,
screen_art,
_to_screen_rect(
HOTBAR_BUBBLE_FIELD_RECT,
stage_rect.position,
stage_scale,
),
]
)
func _validate_transformed_rect(
label: String,
canonical_rect: Rect2,
stage_rect: Rect2,
stage_scale: float,
) -> void:
var screen_rect: Rect2 = _to_screen_rect(
canonical_rect, stage_rect.position, stage_scale
)
assert(
screen_rect.position.is_equal_approx(
stage_rect.position + canonical_rect.position * stage_scale
),
"%s position does not use the single stage transform" % label,
)
assert(
screen_rect.size.is_equal_approx(canonical_rect.size * stage_scale),
"%s size does not use the single stage transform" % label,
)
func _to_screen_rect(
canonical_rect: Rect2,
stage_offset: Vector2,
stage_scale: float,
) -> Rect2:
return Rect2(
stage_offset + canonical_rect.position * stage_scale,
canonical_rect.size * stage_scale,
)
func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
if not OS.has_environment("straywild_NOTEPAD_CAPTURE"):
return
player_menu.visible = true
var sections: Array[PlayerMenu.Section] = [
PlayerMenu.Section.BAG,
PlayerMenu.Section.TACKLE_BOX,
]
var suffixes: Array[String] = ["inventory", "tackle"]
for index: int in sections.size():
player_menu.call("_show_section_immediate", sections[index])
await process_frame
await process_frame
await _save_capture(suffixes[index])
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var confirmation_message := player_menu.get_node(
"%ConfirmationMessage"
) as Label
assert(sale_confirmation != null)
assert(confirmation_message != null)
confirmation_message.text = (
"You can sell this fish to the pelicans now, "
+ "but the shop will pay more!"
)
sale_confirmation.visible = true
await process_frame
await process_frame
await _save_capture("sale-confirmation")
sale_confirmation.visible = false
func _save_capture(suffix: String) -> void:
var image: Image = root.get_texture().get_image()
var path: String = "%s-%s.png" % [
OS.get_environment("straywild_NOTEPAD_CAPTURE"),
suffix,
]
assert(image.save_png(path) == OK)