diff --git a/economy/art_shop_stock.gd b/economy/art_shop_stock.gd index 6da6a4c..acc5382 100644 --- a/economy/art_shop_stock.gd +++ b/economy/art_shop_stock.gd @@ -2,7 +2,7 @@ class_name ArtShopStock extends RefCounted const ART_KIT_ITEM_ID: StringName = &"art_kit" -const ART_KIT_PRICE: int = 1000 +const ART_KIT_PRICE: int = 50 const UPGRADE_PRICE: int = 50 const MARKER_PRODUCTS: Array[StringName] = [ diff --git a/main/main.gd b/main/main.gd index 677240b..d05639f 100644 --- a/main/main.gd +++ b/main/main.gd @@ -931,6 +931,10 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void: settings.auto_click_enabled, settings.auto_click_interval ) + _game_ui.set_edge_docks( + settings.chat_dock_right, + settings.paint_dock_right, + ) func _apply_world_pixelation(pixel_size: int) -> void: diff --git a/network/network_surface_drawing_service.gd b/network/network_surface_drawing_service.gd index 0cf33d0..35c28ab 100644 --- a/network/network_surface_drawing_service.gd +++ b/network/network_surface_drawing_service.gd @@ -28,6 +28,13 @@ const MAX_RECENT_REQUEST_IDS: int = 64 const GRID_PREVIEW_SURFACE_OFFSET: float = 0.03 const POINTER_EDGE_MARGIN: float = 2.0 +enum GuideAction { + NONE, + HIDE, + RESTORE, + FINALIZE, +} + var _session: NetworkSession var _spawn_service: PlayerSpawnService var _relationships: PlayerRelationshipStore @@ -51,6 +58,10 @@ var _color_ids: Array[StringName] = [] var _color_index: int = 0 var _painting: bool = false var _erasing: bool = false +var _eraser_mode: bool = false +var _armed_guide_action: int = GuideAction.NONE +var _armed_return_placement_mode: bool = false +var _armed_return_eraser_mode: bool = false var _camera_look_active: bool = false var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _last_edited_cell := Vector3i(-1, -1, -1) @@ -127,6 +138,9 @@ func handle_input(event: InputEvent, can_open: bool) -> bool: deactivate() return false if event.is_action_pressed("ui_cancel"): + if _armed_guide_action != GuideAction.NONE: + _cancel_armed_guide_action() + return true if _placing_grid: _set_placement_mode(false) return true @@ -173,7 +187,9 @@ func handle_input(event: InputEvent, can_open: bool) -> bool: MOUSE_BUTTON_LEFT: if _placing_grid: if mouse_event.pressed: - if mouse_event.shift_pressed and mouse_event.ctrl_pressed: + if _armed_guide_action != GuideAction.NONE: + _execute_armed_guide_action() + elif mouse_event.shift_pressed and mouse_event.ctrl_pressed: _finalize_selected_guide() elif mouse_event.shift_pressed: _remove_selected_guide() @@ -182,8 +198,11 @@ func handle_input(event: InputEvent, can_open: bool) -> bool: return true if mouse_event.pressed: _begin_stroke() - _painting = mouse_event.pressed and not mouse_event.shift_pressed - _erasing = mouse_event.pressed and mouse_event.shift_pressed + _erasing = ( + mouse_event.pressed + and (mouse_event.shift_pressed or _eraser_mode) + ) + _painting = mouse_event.pressed and not _erasing if _painting or _erasing: _submit_current_brush(_erasing, true) else: @@ -210,6 +229,8 @@ func activate() -> void: return _active = true _placing_grid = true + _eraser_mode = false + _clear_armed_guide_action(false) _refresh_local_unlocks() _rebuild_placement_preview() _reset_stroke() @@ -228,6 +249,8 @@ func deactivate() -> void: return _active = false _placing_grid = false + _eraser_mode = false + _clear_armed_guide_action(false) _camera_look_active = false _reset_stroke() _selected_canvas_id = "" @@ -258,12 +281,63 @@ func set_color_id(color_id: StringName) -> bool: var color_index: int = _color_ids.find(color_id) if color_index < 0: return false + _clear_armed_guide_action(true) + _eraser_mode = false _color_index = color_index _reset_stroke() _emit_hud_state("") return true +func set_eraser_mode(enabled: bool) -> void: + if not _active: + return + _clear_armed_guide_action(true) + _eraser_mode = enabled + if _eraser_mode and _placing_grid: + _placing_grid = false + _reset_stroke() + _selected_canvas_id = "" + _update_aim() + _refresh_stencil_visibility() + _emit_hud_state("eraser selected" if _eraser_mode else "marker selected") + + +func is_eraser_mode() -> bool: + return _eraser_mode + + +func arm_guide_action(action: int) -> bool: + if ( + not _active + or action not in [GuideAction.HIDE, GuideAction.RESTORE, GuideAction.FINALIZE] + ): + return false + if _armed_guide_action == action: + _cancel_armed_guide_action() + return false + if _armed_guide_action == GuideAction.NONE: + _armed_return_placement_mode = _placing_grid + _armed_return_eraser_mode = _eraser_mode + _armed_guide_action = action + _placing_grid = true + _eraser_mode = false + _reset_stroke() + _selected_canvas_id = "" + _update_aim() + _refresh_stencil_visibility() + _emit_hud_state("click a grid to %s it" % _guide_action_verb(action)) + return true + + +func get_armed_guide_action() -> int: + return _armed_guide_action + + +func cancel_armed_guide_action() -> void: + _cancel_armed_guide_action() + + func set_brush_size(value: int) -> bool: if _art_unlocks == null or not _art_unlocks.is_brush_size_unlocked(value): return false @@ -1751,7 +1825,11 @@ func _rebuild_placement_preview() -> void: func _set_placement_mode(enabled: bool) -> void: - if not _active or _placing_grid == enabled: + if not _active: + return + _clear_armed_guide_action(true) + if _placing_grid == enabled: + _emit_hud_state("") return _placing_grid = enabled _reset_stroke() @@ -1767,6 +1845,51 @@ func _set_placement_mode(enabled: bool) -> void: ) +func _execute_armed_guide_action() -> void: + var action: int = _armed_guide_action + match action: + GuideAction.HIDE: + _remove_selected_guide() + GuideAction.RESTORE: + _restore_selected_guide() + GuideAction.FINALIZE: + _finalize_selected_guide() + _clear_armed_guide_action(true) + _emit_hud_state("") + + +func _cancel_armed_guide_action() -> void: + if _armed_guide_action == GuideAction.NONE: + return + _clear_armed_guide_action(true) + _emit_hud_state("") + + +func _clear_armed_guide_action(restore_tool: bool) -> void: + if _armed_guide_action == GuideAction.NONE: + return + _armed_guide_action = GuideAction.NONE + if restore_tool: + _placing_grid = _armed_return_placement_mode + _eraser_mode = _armed_return_eraser_mode + _armed_return_placement_mode = false + _armed_return_eraser_mode = false + _reset_stroke() + _update_aim() + _refresh_stencil_visibility() + + +func _guide_action_verb(action: int) -> String: + match action: + GuideAction.HIDE: + return "hide" + GuideAction.RESTORE: + return "restore" + GuideAction.FINALIZE: + return "finish" + return "use" + + func _remove_selected_guide() -> void: if _selected_canvas_id.is_empty(): _emit_hud_state("aim at a placed grid before removing its guide") @@ -1782,6 +1905,21 @@ func _remove_selected_guide() -> void: _emit_hud_state("hiding grid guide...") +func _restore_selected_guide() -> void: + if _selected_canvas_id.is_empty(): + _emit_hud_state("aim at a hidden grid before restoring its guide") + return + var state: Dictionary = _canvas_states.get(_selected_canvas_id, {}) + if state.is_empty() or bool(state.get("finalized", false)): + return + if bool(state.get("guide_visible", true)): + _emit_hud_state("this grid guide is already visible") + return + if request_guide_visibility(_selected_canvas_id, true): + if not _session.is_host(): + _emit_hud_state("restoring grid guide...") + + func _finalize_selected_guide() -> void: if _selected_canvas_id.is_empty(): _emit_hud_state("aim at an active grid before finishing it") @@ -1805,6 +1943,8 @@ func _clamped_pointer_position(value: Vector2) -> Vector2: func _cycle_color(direction: int) -> void: if _color_ids.is_empty(): return + _clear_armed_guide_action(true) + _eraser_mode = false _color_index = posmod(_color_index + direction, _color_ids.size()) _reset_stroke() _emit_hud_state("") diff --git a/settings/player_settings.gd b/settings/player_settings.gd index ba5b6ff..c83e534 100644 --- a/settings/player_settings.gd +++ b/settings/player_settings.gd @@ -29,6 +29,8 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192] @export var invert_camera_y: bool = false @export var chat_draft: String = "" @export var chat_collapsed: bool = false +@export var chat_dock_right: bool = false +@export var paint_dock_right: bool = true @export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE @export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE @@ -62,6 +64,8 @@ func copy() -> PlayerSettings: result.invert_camera_y = invert_camera_y result.chat_draft = chat_draft result.chat_collapsed = chat_collapsed + result.chat_dock_right = chat_dock_right + result.paint_dock_right = paint_dock_right result.world_pixel_size = world_pixel_size result.ui_pixel_size = ui_pixel_size return result diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd index 27e69a7..acaf23d 100644 --- a/settings/player_settings_manager.gd +++ b/settings/player_settings_manager.gd @@ -57,6 +57,14 @@ func load_settings() -> bool: presentation.has("chat_collapsed") and typeof(presentation["chat_collapsed"]) != TYPE_BOOL ) + or ( + presentation.has("chat_dock_right") + and typeof(presentation["chat_dock_right"]) != TYPE_BOOL + ) + or ( + presentation.has("paint_dock_right") + and typeof(presentation["paint_dock_right"]) != TYPE_BOOL + ) ): return _use_defaults_after_corruption("Player settings values are invalid.") var loaded := PlayerSettings.new() @@ -98,6 +106,8 @@ func load_settings() -> bool: PlayerSettings.MAX_CHAT_DRAFT_CHARACTERS ) loaded.chat_collapsed = bool(presentation.get("chat_collapsed", false)) + loaded.chat_dock_right = bool(presentation.get("chat_dock_right", false)) + loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true)) if not loaded.is_valid(): return _use_defaults_after_corruption("Player settings values are invalid.") current_settings = loaded @@ -170,6 +180,8 @@ func save_now() -> bool: "ui_pixel_size": current_settings.ui_pixel_size, "chat_draft": current_settings.chat_draft, "chat_collapsed": current_settings.chat_collapsed, + "chat_dock_right": current_settings.chat_dock_right, + "paint_dock_right": current_settings.paint_dock_right, }, } if current_settings.chat_draft.is_empty(): diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index cc4e014..ab04d4a 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -34,7 +34,50 @@ func _run() -> void: var toolbar := game_ui.get_node( "%SurfaceDrawingToolbar" ) as SurfaceDrawingToolbar + var chat_ui := game_ui.get_node("%ChatUI") as ChatUI assert(player != null and service != null and toolbar != null) + assert(chat_ui != null) + var settings_panel := game_ui.get( + "_pause_settings_panel" + ) as SettingsPanel + var chat_dock_button := settings_panel.get_node("%ChatDock") as BubbleButton + var paint_dock_button := settings_panel.get_node("%PaintDock") as BubbleButton + assert(chat_dock_button.neutral_size.x == chat_dock_button.neutral_size.y) + assert(paint_dock_button.neutral_size.x == paint_dock_button.neutral_size.y) + assert( + chat_dock_button.compact_minimum_size.x + == chat_dock_button.compact_minimum_size.y + ) + assert( + paint_dock_button.compact_minimum_size.x + == paint_dock_button.compact_minimum_size.y + ) + assert(not chat_ui.is_docked_right()) + assert(toolbar.is_docked_right()) + var settings_manager := main.get_node( + "%PlayerSettingsManager" + ) as PlayerSettingsManager + assert(settings_manager != null) + var changed_docks: PlayerSettings = settings_manager.current_settings.copy() + changed_docks.chat_dock_right = true + changed_docks.paint_dock_right = false + assert(settings_manager.apply_settings(changed_docks)) + await process_frame + assert(chat_ui.is_docked_right()) + assert(not toolbar.is_docked_right()) + var reloaded_settings := PlayerSettingsManager.new() + root.add_child(reloaded_settings) + assert(reloaded_settings.load_settings()) + assert(reloaded_settings.current_settings.chat_dock_right) + assert(not reloaded_settings.current_settings.paint_dock_right) + reloaded_settings.queue_free() + var default_docks: PlayerSettings = settings_manager.current_settings.copy() + default_docks.chat_dock_right = false + default_docks.paint_dock_right = true + assert(settings_manager.apply_settings(default_docks)) + await process_frame + assert(not chat_ui.is_docked_right()) + assert(toolbar.is_docked_right()) assert(not service.can_activate()) assert(not service.is_active() and not toolbar.visible) assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1)) @@ -47,12 +90,53 @@ func _run() -> void: await process_frame assert(service.is_active() and service.is_placement_mode()) assert(toolbar.visible) - assert(toolbar.position.is_equal_approx(Vector2(18.0, 18.0))) + var ui_root := game_ui.get_node("%UIRoot") as Control + assert(toolbar.get_parent() == ui_root) + assert(is_zero_approx(toolbar.position.y)) + assert( + is_equal_approx( + toolbar.get_global_rect().end.x, + ui_root.get_global_rect().end.x, + ) + ) assert(toolbar.get_global_rect().end.x <= 1280.0) assert(toolbar.get_global_rect().end.y <= 720.0) + var top_panel := toolbar.get_node("%TopPanel") as PanelContainer + var color_panel := toolbar.get_node("%ColorPanel") as PanelContainer + assert(top_panel.position.is_equal_approx(Vector2.ZERO)) + assert(color_panel.position.x > 0.0) + assert(is_equal_approx(color_panel.get_rect().end.x, top_panel.get_rect().end.x)) + assert(color_panel.position.y < top_panel.get_rect().end.y) + assert(color_panel.get_rect().end.y > top_panel.get_rect().end.y) + var toolbar_origin: Vector2 = toolbar.get_global_rect().position + var top_pointer := InputEventMouseMotion.new() + top_pointer.position = toolbar_origin + Vector2(200.0, 24.0) + assert(toolbar.owns_pointer_event(top_pointer)) + var rail_pointer := InputEventMouseMotion.new() + rail_pointer.position = color_panel.get_global_rect().get_center() + assert(toolbar.owns_pointer_event(rail_pointer)) + var empty_pointer := InputEventMouseMotion.new() + empty_pointer.position = toolbar_origin + Vector2(200.0, 180.0) + assert(not toolbar.owns_pointer_event(empty_pointer)) var brush_option := toolbar.get_node("%BrushOption") as OptionButton var grid_option := toolbar.get_node("%GridOption") as OptionButton + var mode_button := toolbar.get_node("%ModeButton") as Button + var eraser_button := toolbar.get_node("%EraserButton") as Button + var undo_button := toolbar.get_node("%UndoButton") as Button + var hide_button := toolbar.get_node("%HideGuideButton") as Button + var restore_button := toolbar.get_node("%RestoreGuideButton") as Button + var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button + var close_button := toolbar.get_node("%CloseButton") as Button + assert( + mode_button != null + and eraser_button != null + and undo_button != null + and hide_button != null + and restore_button != null + and finalize_button != null + and close_button != null + ) assert(brush_option.item_count == 4) assert(grid_option.item_count == 4) assert(not brush_option.get_popup().is_item_disabled(0)) @@ -79,6 +163,58 @@ func _run() -> void: assert(service.get_brush_size() == 4) assert(service.get_grid_size() == 128) + mode_button.pressed.emit() + assert(not service.is_placement_mode()) + eraser_button.pressed.emit() + assert(service.is_eraser_mode()) + assert(eraser_button.button_pressed) + hide_button.pressed.emit() + assert(service.is_placement_mode()) + assert(not service.is_eraser_mode()) + assert( + service.get_armed_guide_action() + == NetworkSurfaceDrawingService.GuideAction.HIDE + ) + var world_click := InputEventMouseButton.new() + world_click.button_index = MOUSE_BUTTON_LEFT + world_click.pressed = true + assert(service.handle_input(world_click, true)) + assert( + service.get_armed_guide_action() + == NetworkSurfaceDrawingService.GuideAction.NONE + ) + assert(not service.is_placement_mode()) + assert(service.is_eraser_mode()) + + restore_button.pressed.emit() + assert( + service.get_armed_guide_action() + == NetworkSurfaceDrawingService.GuideAction.RESTORE + ) + restore_button.pressed.emit() + assert( + service.get_armed_guide_action() + == NetworkSurfaceDrawingService.GuideAction.NONE + ) + assert(not service.is_placement_mode() and service.is_eraser_mode()) + finalize_button.pressed.emit() + assert( + service.get_armed_guide_action() + == NetworkSurfaceDrawingService.GuideAction.FINALIZE + ) + assert(service.handle_input(world_click, true)) + assert(not service.is_placement_mode() and service.is_eraser_mode()) + (color_buttons[&"ocean_teal"] as Button).pressed.emit() + assert(not service.is_eraser_mode()) + undo_button.pressed.emit() + assert(service.is_active()) + close_button.pressed.emit() + assert(not service.is_active() and not toolbar.visible) + + assert(service.handle_input(paint_key, true)) + await process_frame + assert(service.is_active() and toolbar.visible) + assert(service.handle_input(paint_key, true)) await process_frame assert(not service.is_active() and not toolbar.visible) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 481d04a..71001f3 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -61,6 +61,7 @@ var _pending_send_body: String = "" var _prior_movement: bool = true var _prior_camera: bool = true var _output_scale: float = 1.0 +var _dock_right: bool = false func _ready() -> void: @@ -85,6 +86,7 @@ func setup( _player = player _fishing_spot = fishing_spot _settings = settings + set_dock_right(_settings.current_settings.chat_dock_right) _service.message_received.connect(_on_message) _service.local_message_confirmed.connect(_on_local_message_confirmed) _service.history_replaced.connect(_on_history) @@ -333,10 +335,10 @@ func _build_ui() -> void: func _chat_panel_style() -> StyleBoxFlat: var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94)) - style.corner_radius_top_left = 0 - style.corner_radius_bottom_left = 0 - style.corner_radius_top_right = 10 - style.corner_radius_bottom_right = 10 + style.corner_radius_top_left = 10 if _dock_right else 0 + style.corner_radius_bottom_left = 10 if _dock_right else 0 + style.corner_radius_top_right = 0 if _dock_right else 10 + style.corner_radius_bottom_right = 0 if _dock_right else 10 style.content_margin_left = 12 style.content_margin_right = 12 style.content_margin_top = 10 @@ -568,10 +570,26 @@ func set_output_scale(output_scale: float) -> void: _output_scale = maxf(output_scale, 0.001) +func set_dock_right(should_dock_right: bool) -> void: + _dock_right = should_dock_right + if not is_node_ready() or _panel == null: + return + _panel.add_theme_stylebox_override("panel", _chat_panel_style()) + _refresh_handle_labels(_presentation_state) + _layout_presentation(false) + + +func is_docked_right() -> bool: + return _dock_right + + func _refresh_handle_labels(state: PresentationState) -> void: var collapsed := state == PresentationState.COLLAPSED var height_state := _visible_state_before_collapse if collapsed else state - _collapse_button.text = ">" if collapsed else "<" + if _dock_right: + _collapse_button.text = "<" if collapsed else ">" + else: + _collapse_button.text = ">" if collapsed else "<" _collapse_button.tooltip_text = ( "Show chat" if collapsed else "Hide chat" ) @@ -687,6 +705,9 @@ func _set_status(text: String) -> void: func _layout_presentation(animate: bool) -> void: if _panel == null: return + var viewport_width := size.x + if viewport_width <= 0.0: + viewport_width = 1280.0 var viewport_height := size.y if viewport_height <= 0.0: viewport_height = 720.0 @@ -704,32 +725,40 @@ func _layout_presentation(animate: bool) -> void: if layout_state == PresentationState.EXPANDED: height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin) height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN)) - var target_position := Vector2( - -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) - if _presentation_state == PresentationState.COLLAPSED - else 0.0, - bottom - height, - ) + var collapsed := _presentation_state == PresentationState.COLLAPSED + var panel_x: float + if _dock_right: + panel_x = ( + viewport_width - COLLAPSED_REVEAL_WIDTH + if collapsed + else viewport_width - PANEL_WIDTH + ) + else: + panel_x = -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0 + var target_position := Vector2(panel_x, bottom - height) var target_size := Vector2(PANEL_WIDTH, height) var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP var handle_stack_top := ( bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5 ) + var handle_x: float + if _dock_right: + handle_x = panel_x - HANDLE_SIZE.x + else: + handle_x = COLLAPSED_REVEAL_WIDTH if collapsed else PANEL_WIDTH var collapse_position := Vector2( - COLLAPSED_REVEAL_WIDTH - if _presentation_state == PresentationState.COLLAPSED - else PANEL_WIDTH, + handle_x, handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP, ) - var height_position := Vector2( - COLLAPSED_REVEAL_WIDTH - if _presentation_state == PresentationState.COLLAPSED - else PANEL_WIDTH, - handle_stack_top, - ) - var unread_position := collapse_position + Vector2(6.0, -18.0) + var height_position := Vector2(handle_x, handle_stack_top) + var unread_offset_x := -18.0 if _dock_right else 6.0 + var unread_position := collapse_position + Vector2(unread_offset_x, -18.0) var hint_position := Vector2( - HINT_EDGE_MARGIN, + ( + viewport_width - _hint.size.x - HINT_EDGE_MARGIN + if _dock_right + else HINT_EDGE_MARGIN + ), viewport_height - _hint.size.y - HINT_EDGE_MARGIN, ) if _height_tween != null and _height_tween.is_valid(): diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 738fcbb..d1796d6 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -219,6 +219,10 @@ func setup( _shop_interaction = shop_interaction _surface_drawing = surface_drawing _surface_drawing_toolbar.setup(_surface_drawing, art_unlocks) + set_edge_docks( + settings_manager.current_settings.chat_dock_right, + settings_manager.current_settings.paint_dock_right, + ) func _input(event: InputEvent) -> void: @@ -409,6 +413,11 @@ func set_effective_ui_pixel_size(pixel_size: int) -> void: _pause_settings_panel.set_effective_ui_pixel_size(pixel_size) +func set_edge_docks(chat_dock_right: bool, paint_dock_right: bool) -> void: + _chat_ui.set_dock_right(chat_dock_right) + _surface_drawing_toolbar.set_dock_right(paint_dock_right) + + func focus_open_settings_back_button() -> void: if _title_settings_panel.visible: _title_settings_panel.focus_back_button() diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 4740dc0..4289771 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -46,7 +46,7 @@ mouse_filter = 2 unique_name_in_owner = true mouse_filter = 2 -[node name="SurfaceDrawingToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("11_drawing_toolbar")] +[node name="SurfaceDrawingToolbar" parent="UIRoot" instance=ExtResource("11_drawing_toolbar")] unique_name_in_owner = true [node name="GameplayTransientHUD" type="Control" parent="UIRoot/CanonicalStage"] diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 2e329a6..05b0c07 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -41,6 +41,8 @@ enum PresentationMode { @onready var _world_value: BubbleButton = %WorldValue @onready var _ui_value: BubbleButton = %UIValue +@onready var _chat_dock: BubbleButton = %ChatDock +@onready var _paint_dock: BubbleButton = %PaintDock @onready var _mouse_value: BubbleButton = %MouseValue @onready var _controller_value: BubbleButton = %ControllerValue @onready var _invert_y_toggle: BubbleButton = %InvertYToggle @@ -133,6 +135,8 @@ func _ready() -> void: _ui_options[index].pressed.connect( _set_ui_pixelation.bind(index + 1) ) + _chat_dock.pressed.connect(_toggle_chat_dock) + _paint_dock.pressed.connect(_toggle_paint_dock) %MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1)) %MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1)) _mouse_value.pressed.connect(_adjust_mouse_sensitivity.bind(1)) @@ -642,14 +646,12 @@ func _apply_settings() -> void: if _settings_manager == null: _feedback.text = "settings are unavailable." return - var edited := PlayerSettings.new() + var edited: PlayerSettings = _settings_manager.current_settings.copy() edited.auto_click_enabled = _auto_click_enabled edited.auto_click_interval = _auto_click_interval_value edited.mouse_camera_sensitivity = _mouse_sensitivity edited.controller_camera_sensitivity = _controller_sensitivity edited.invert_camera_y = _invert_camera_y - edited.world_pixel_size = _settings_manager.current_settings.world_pixel_size - edited.ui_pixel_size = _settings_manager.current_settings.ui_pixel_size if _settings_manager.apply_settings(edited): if ( _presentation_mode == PresentationMode.TITLE_EMBEDDED @@ -686,6 +688,10 @@ func _refresh_value_labels() -> void: "ui\npixelation\n" + _pixel_size_label(settings.ui_pixel_size) ) + _chat_dock.text = "chat dock\n" + ("right" if settings.chat_dock_right else "left") + _paint_dock.text = ( + "paint dock\n" + ("right" if settings.paint_dock_right else "left") + ) _mouse_value.text = "mouse\nsensitivity\n%.4f" % _mouse_sensitivity _controller_value.text = ( "controller\nsensitivity\n%.1f" % _controller_sensitivity @@ -757,6 +763,28 @@ func _set_ui_pixelation(pixel_size: int) -> void: _refresh_value_labels() +func _toggle_chat_dock() -> void: + if _settings_manager == null: + return + var edited: PlayerSettings = _settings_manager.current_settings.copy() + edited.chat_dock_right = not edited.chat_dock_right + if not _settings_manager.apply_settings(edited): + _feedback.text = "failed to save chat dock setting." + return + _refresh_value_labels() + + +func _toggle_paint_dock() -> void: + if _settings_manager == null: + return + var edited: PlayerSettings = _settings_manager.current_settings.copy() + edited.paint_dock_right = not edited.paint_dock_right + if not _settings_manager.apply_settings(edited): + _feedback.text = "failed to save paint dock setting." + return + _refresh_value_labels() + + func _adjust_mouse_sensitivity(direction: int) -> void: _mouse_sensitivity = clampf( _mouse_sensitivity + float(direction) * 0.0005, diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index 595d055..90e2a13 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -157,8 +157,8 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_page") page_id = &"display" -bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/DisplayBackButton")]) -focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/DisplayBackButton")]) +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")]) initial_focus_path = NodePath("BubbleCluster/WorldRetro") back_focus_path = NodePath("BubbleCluster/DisplayBackButton") compact_maximum_layout_size = Vector2(544, 400) @@ -178,12 +178,12 @@ custom_minimum_size = Vector2(0, 0) mouse_filter = 2 focus_mode = 0 text = "world\npixelation\nretro" -neutral_size = Vector2(160, 150) +neutral_size = Vector2(140, 130) desktop_anchor = Vector2(160, 240) compact_anchor = Vector2(175, 210) -compact_minimum_size = Vector2(132, 126) +compact_minimum_size = Vector2(118, 110) minimum_font_size = 15 -maximum_font_size = 25 +maximum_font_size = 22 motion_phase = 0.2 [node name="WorldLegible" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] @@ -258,12 +258,12 @@ custom_minimum_size = Vector2(0, 0) mouse_filter = 2 focus_mode = 0 text = "ui\npixelation\nretro" -neutral_size = Vector2(160, 150) +neutral_size = Vector2(140, 130) desktop_anchor = Vector2(560, 240) compact_anchor = Vector2(545, 210) -compact_minimum_size = Vector2(132, 126) +compact_minimum_size = Vector2(118, 110) minimum_font_size = 15 -maximum_font_size = 25 +maximum_font_size = 22 motion_phase = 3.55 [node name="UILegible" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] @@ -332,6 +332,30 @@ minimum_font_size = 12 maximum_font_size = 13 motion_phase = 0.55 +[node name="ChatDock" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "chat dock\nleft" +neutral_size = Vector2(96, 96) +desktop_anchor = Vector2(190, 435) +compact_anchor = Vector2(160, 378) +compact_minimum_size = Vector2(82, 82) +minimum_font_size = 12 +maximum_font_size = 16 +motion_phase = 1.1 + +[node name="PaintDock" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "paint dock\nright" +neutral_size = Vector2(96, 96) +desktop_anchor = Vector2(530, 435) +compact_anchor = Vector2(448, 378) +compact_minimum_size = Vector2(82, 82) +minimum_font_size = 12 +maximum_font_size = 16 +motion_phase = 1.65 + [node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) @@ -342,7 +366,7 @@ compact_anchor = Vector2(315, 390) compact_minimum_size = Vector2(82, 78) minimum_font_size = 13 maximum_font_size = 16 -motion_phase = 1.1 +motion_phase = 2.2 [node name="ControlsPage" type="Control" parent="."] unique_name_in_owner = true diff --git a/ui/surface_drawing_toolbar.gd b/ui/surface_drawing_toolbar.gd index c4cac1b..0c13705 100644 --- a/ui/surface_drawing_toolbar.gd +++ b/ui/surface_drawing_toolbar.gd @@ -1,48 +1,94 @@ class_name SurfaceDrawingToolbar -extends PanelContainer +extends Control const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const TOOLBAR_WIDTH: float = 580.0 +const TOOLBAR_HEIGHT: float = 373.0 +const COLOR_RAIL_WIDTH: float = 46.0 @onready var _mode_button: Button = %ModeButton @onready var _brush_option: OptionButton = %BrushOption @onready var _grid_option: OptionButton = %GridOption @onready var _color_list: VBoxContainer = %ColorList +@onready var _top_panel: PanelContainer = %TopPanel +@onready var _color_panel: PanelContainer = %ColorPanel +@onready var _eraser_button: Button = %EraserButton +@onready var _undo_button: Button = %UndoButton +@onready var _hide_guide_button: Button = %HideGuideButton +@onready var _restore_guide_button: Button = %RestoreGuideButton +@onready var _finalize_guide_button: Button = %FinalizeGuideButton +@onready var _close_button: Button = %CloseButton var _service: NetworkSurfaceDrawingService var _unlocks: PlayerArtUnlocks var _color_buttons: Dictionary[StringName, Button] = {} +var _dock_right: bool = true func _ready() -> void: UtilityPageStyleType.apply_page(self) - var panel: StyleBoxFlat = UtilityPageStyleType.panel_style() - panel.content_margin_left = 10.0 - panel.content_margin_top = 10.0 - panel.content_margin_right = 10.0 - panel.content_margin_bottom = 10.0 - add_theme_stylebox_override("panel", panel) + _apply_toolbar_panel(_top_panel) + _apply_toolbar_panel(_color_panel) UtilityPageStyleType.apply_ocean_button(_mode_button) UtilityPageStyleType.apply_ocean_button(_brush_option) UtilityPageStyleType.apply_ocean_button(_grid_option) + for button: Button in _action_buttons(): + UtilityPageStyleType.apply_ocean_button(button) + button.custom_minimum_size = Vector2(48, 44) + _make_button_round(button, 22) _mode_button.custom_minimum_size = Vector2(48, 48) - _make_mode_button_round() + _make_button_round(_mode_button, 24) _mode_button.pressed.connect(_toggle_mode) + _eraser_button.pressed.connect(_toggle_eraser) + _undo_button.pressed.connect(_undo_last_stroke) + _hide_guide_button.pressed.connect( + _arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.HIDE) + ) + _restore_guide_button.pressed.connect( + _arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.RESTORE) + ) + _finalize_guide_button.pressed.connect( + _arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.FINALIZE) + ) + _close_button.pressed.connect(_close_toolbar) _brush_option.item_selected.connect(_select_brush) _grid_option.item_selected.connect(_select_grid) _build_options() + _apply_dock_side() hide() -func _make_mode_button_round() -> void: +func _apply_toolbar_panel(panel: PanelContainer) -> void: + var style: StyleBoxFlat = UtilityPageStyleType.panel_style() + style.content_margin_left = 6.0 + style.content_margin_top = 6.0 + style.content_margin_right = 6.0 + style.content_margin_bottom = 6.0 + style.set_corner_radius_all(10) + panel.add_theme_stylebox_override("panel", style) + + +func _action_buttons() -> Array[Button]: + return [ + _eraser_button, + _undo_button, + _hide_guide_button, + _restore_guide_button, + _finalize_guide_button, + _close_button, + ] + + +func _make_button_round(button: Button, radius: int) -> void: for style_name: StringName in [ &"normal", &"hover", &"pressed", &"focus", &"disabled", ]: - var existing: StyleBox = _mode_button.get_theme_stylebox(style_name) + var existing: StyleBox = button.get_theme_stylebox(style_name) var style := existing.duplicate() as StyleBoxFlat if style == null: continue - style.set_corner_radius_all(24) - _mode_button.add_theme_stylebox_override(style_name, style) + style.set_corner_radius_all(radius) + button.add_theme_stylebox_override(style_name, style) func setup( @@ -62,13 +108,42 @@ func setup( _refresh_unlocks() +func set_dock_right(should_dock_right: bool) -> void: + _dock_right = should_dock_right + if is_node_ready(): + _apply_dock_side() + + +func is_docked_right() -> bool: + return _dock_right + + +func _apply_dock_side() -> void: + anchor_left = 1.0 if _dock_right else 0.0 + anchor_right = anchor_left + offset_left = -TOOLBAR_WIDTH if _dock_right else 0.0 + offset_right = 0.0 if _dock_right else TOOLBAR_WIDTH + offset_top = 0.0 + offset_bottom = TOOLBAR_HEIGHT + _color_panel.offset_left = ( + TOOLBAR_WIDTH - COLOR_RAIL_WIDTH if _dock_right else 0.0 + ) + _color_panel.offset_right = ( + TOOLBAR_WIDTH if _dock_right else COLOR_RAIL_WIDTH + ) + + func owns_pointer_event(event: InputEvent) -> bool: if not visible: return false if _brush_option.get_popup().visible or _grid_option.get_popup().visible: return true if event is InputEventMouse: - return get_global_rect().has_point((event as InputEventMouse).position) + var pointer_position: Vector2 = (event as InputEventMouse).position + return ( + _top_panel.get_global_rect().has_point(pointer_position) + or _color_panel.get_global_rect().has_point(pointer_position) + ) return false @@ -114,7 +189,11 @@ func _refresh_unlocks() -> void: _apply_color_button_style( button, color_id, - _service != null and _service.get_color_id() == color_id, + ( + _service != null + and not _service.is_eraser_mode() + and _service.get_color_id() == color_id + ), ) @@ -161,6 +240,26 @@ func _select_color(color_id: StringName) -> void: _service.set_color_id(color_id) +func _toggle_eraser() -> void: + if _service != null: + _service.set_eraser_mode(not _service.is_eraser_mode()) + + +func _undo_last_stroke() -> void: + if _service != null: + _service.request_undo_last_stroke() + + +func _arm_guide_action(action: int) -> void: + if _service != null: + _service.arm_guide_action(action) + + +func _close_toolbar() -> void: + if _service != null: + _service.deactivate() + + func _on_unlocks_changed(_unlock_mask: int) -> void: _refresh_unlocks() @@ -185,9 +284,26 @@ func _on_service_state_changed( ) _select_option_by_id(_brush_option, brush_size) _select_option_by_id(_grid_option, grid_size) + _refresh_action_state() _refresh_unlocks() +func _refresh_action_state() -> void: + if _service == null: + return + _eraser_button.button_pressed = _service.is_eraser_mode() + var guide_action: int = _service.get_armed_guide_action() + _hide_guide_button.button_pressed = ( + guide_action == NetworkSurfaceDrawingService.GuideAction.HIDE + ) + _restore_guide_button.button_pressed = ( + guide_action == NetworkSurfaceDrawingService.GuideAction.RESTORE + ) + _finalize_guide_button.button_pressed = ( + guide_action == NetworkSurfaceDrawingService.GuideAction.FINALIZE + ) + + func _select_option_by_id(option: OptionButton, item_id: int) -> void: for index: int in range(option.item_count): if option.get_item_id(index) == item_id: diff --git a/ui/surface_drawing_toolbar.tscn b/ui/surface_drawing_toolbar.tscn index dbc81a5..61db805 100644 --- a/ui/surface_drawing_toolbar.tscn +++ b/ui/surface_drawing_toolbar.tscn @@ -3,55 +3,97 @@ [ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] -[node name="SurfaceDrawingToolbar" type="PanelContainer"] +[node name="SurfaceDrawingToolbar" type="Control"] visible = false z_index = 85 layout_mode = 0 -offset_left = 18.0 -offset_top = 18.0 -offset_right = 284.0 -offset_bottom = 340.0 -mouse_filter = 0 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -580.0 +offset_bottom = 373.0 +grow_horizontal = 0 +mouse_filter = 2 theme = ExtResource("2_theme") script = ExtResource("1_script") -[node name="Layout" type="HBoxContainer" parent="."] -layout_mode = 2 -theme_override_constants/separation = 10 - -[node name="ColorList" type="VBoxContainer" parent="Layout"] +[node name="TopPanel" type="PanelContainer" parent="."] unique_name_in_owner = true +layout_mode = 0 +offset_right = 580.0 +offset_bottom = 60.0 + +[node name="Top" type="HBoxContainer" parent="TopPanel"] layout_mode = 2 theme_override_constants/separation = 5 -[node name="Tools" type="VBoxContainer" parent="Layout"] -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/separation = 8 - -[node name="Top" type="HBoxContainer" parent="Layout/Tools"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="ModeButton" type="Button" parent="Layout/Tools/Top"] +[node name="ModeButton" type="Button" parent="TopPanel/Top"] unique_name_in_owner = true layout_mode = 2 +tooltip_text = "switch between grid and marker mode" text = "▦" -[node name="BrushOption" type="OptionButton" parent="Layout/Tools/Top"] +[node name="BrushOption" type="OptionButton" parent="TopPanel/Top"] unique_name_in_owner = true custom_minimum_size = Vector2(80, 48) layout_mode = 2 tooltip_text = "brush size" -[node name="GridOption" type="OptionButton" parent="Layout/Tools/Top"] +[node name="GridOption" type="OptionButton" parent="TopPanel/Top"] unique_name_in_owner = true custom_minimum_size = Vector2(92, 48) layout_mode = 2 tooltip_text = "grid size" -[node name="Help" type="Label" parent="Layout/Tools"] +[node name="EraserButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true layout_mode = 2 -text = "click place/draw\nshift click hide/erase\nctrl z undo\nshift scroll zoom" -theme_override_colors/font_color = Color(0.623529, 0.811765, 0.823529, 1) -theme_override_font_sizes/font_size = 14 +tooltip_text = "toggle eraser" +toggle_mode = true +text = "⌫" + +[node name="UndoButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "undo last stroke" +text = "↶" + +[node name="HideGuideButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "hide grid on next click" +toggle_mode = true +text = "◌" + +[node name="RestoreGuideButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "restore hidden grid on next click" +toggle_mode = true +text = "▤" + +[node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "finish grid on next click" +toggle_mode = true +text = "✓" + +[node name="CloseButton" type="Button" parent="TopPanel/Top"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "close art tools" +text = "×" + +[node name="ColorPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 534.0 +offset_top = 54.0 +offset_right = 580.0 +offset_bottom = 373.0 + +[node name="ColorList" type="VBoxContainer" parent="ColorPanel"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 5