diff --git a/addons/godot_gameplay_systems/plugin.gd b/addons/godot_gameplay_systems/plugin.gd index 7481cf8..4657cdf 100644 --- a/addons/godot_gameplay_systems/plugin.gd +++ b/addons/godot_gameplay_systems/plugin.gd @@ -6,6 +6,7 @@ const camera_shake_plugin_script = preload("res://addons/godot_gameplay_systems/ const extended_character_nodes_script = preload("res://addons/godot_gameplay_systems/extended_character_nodes/plugin.gd") const inventory_system_script = preload("res://addons/godot_gameplay_systems/inventory_system/plugin.gd") const interactables_script = preload("res://addons/godot_gameplay_systems/interactables/plugin.gd") +const slideshow_script = preload("res://addons/godot_gameplay_systems/slideshow/plugin.gd") var attributes_and_abilities_plugin: EditorPlugin @@ -13,6 +14,7 @@ var camera_shake_plugin: EditorPlugin var extended_character_nodes: EditorPlugin var inventory_system: EditorPlugin var interactables: EditorPlugin +var slideshow: EditorPlugin func _init() -> void: @@ -21,6 +23,7 @@ func _init() -> void: extended_character_nodes = extended_character_nodes_script.new() inventory_system = inventory_system_script.new() interactables = interactables_script.new() + slideshow = slideshow_script.new() func _enter_tree(): @@ -29,6 +32,7 @@ func _enter_tree(): extended_character_nodes._enter_tree() inventory_system._enter_tree() interactables._enter_tree() + slideshow._enter_tree() func _exit_tree(): @@ -37,3 +41,4 @@ func _exit_tree(): extended_character_nodes._exit_tree() inventory_system._exit_tree() interactables._exit_tree() + slideshow._exit_tree() diff --git a/addons/godot_gameplay_systems/slideshow/plugin.gd b/addons/godot_gameplay_systems/slideshow/plugin.gd new file mode 100644 index 0000000..a7a8a3d --- /dev/null +++ b/addons/godot_gameplay_systems/slideshow/plugin.gd @@ -0,0 +1,13 @@ +extends EditorPlugin + + +const slideshow_script = preload("res://addons/godot_gameplay_systems/slideshow/slide_show.gd") + + +func _enter_tree() -> void: + add_custom_type("SlideShow", "Node2D", slideshow_script, null) + + +func _exit_tree() -> void: + remove_custom_type("SlideShow") + diff --git a/addons/godot_gameplay_systems/slideshow/slide_show.gd b/addons/godot_gameplay_systems/slideshow/slide_show.gd new file mode 100644 index 0000000..e14ba79 --- /dev/null +++ b/addons/godot_gameplay_systems/slideshow/slide_show.gd @@ -0,0 +1,158 @@ +class_name SlideShow extends Node2D + + +## The initial slideshow in a videogame +## +## This is made easy + + +enum { + SKIP_PREV = -1, + SKIP_NEXT = +1, +} + +## Emitted when the slideshow is finished +signal finished() +## Emitted when a slide is skipped +signal slide_skipped(skip_direction: int) + +@export_category("Presentation settings") +## Starts the presentation automatically when ready +@export var autoplay: bool = true +## How much long the slide is shown. It does not take in the [member SlideShow.slide_fade_duration] fadein/fadeout time. +@export_range(1.0, 10.0, 0.1) var slide_duration: float = 6.0 +@export_range(0.0, 3.0) var slide_fade_duration: float = 1.0 + +## Current slide index. +var current_slide: int = 0 +## Is [code]true[/code] if there is a previous slide, [code]false[/code] otherwise. +var has_prev: bool: + get: + return current_slide > 0 and slides.size() > 0 +## Is [code]true[/code] if there is a next slide, [code]false[/code] otherwise. +var has_next: bool: + get: + return current_slide < slides.size() +## If [code]true[/code] the slide is playing, [code]false[/code] otherwise. +var playing: bool = true: + get: + return playing + set(value): + playing = value + + if value and autoplay: + _handle_next_slide() +var slides: Array[Node2D]: + get: + var _s = [] as Array[Node2D] + + for child in get_children(): + if child is Node2D: + _s.append(child) + + return _s + + +func _handle_slide_in(slide: Node2D) -> void: + if slide.has_method("_slide_in"): + slide.call("_slide_in") + + +func _handle_slide_out(slide: Node2D) -> void: + if slide.has_method("_slide_out"): + slide.call("_slide_out") + + +## Forcefully +func _forcefully_fade_current() -> Tween: + var tween = create_tween() + var slide = slides[current_slide] as Node2D + + tween.tween_property(slide, "modulate:a", 0.0, slide_fade_duration) + + return tween + + +## Handles next slide. Called internally, use [method SlideShow.skip_to_prev], [method SlideShow.skip_to_next], [method SlideShow.skip_to_nth] or [method SlideShow.skip_all] +func _handle_next_slide(direction: int = SKIP_NEXT) -> void: + if current_slide >= slides.size(): + finished.emit() + else: + var tween = create_tween() + var slide = slides[current_slide] as Node2D + + if slide == null: + printerr("This should NEVER happen, what have you done?") + _handle_next_slide() + + _handle_slide_in(slide) + + tween.tween_property(slide, "modulate:a", 1.0, slide_fade_duration) + tween.tween_interval(slide_duration - (slide_fade_duration * 2)) + tween.tween_property(slide, "modulate:a", 0.0, slide_fade_duration) + + tween.finished.connect(func (): + _handle_slide_out(slide) + current_slide += direction + _handle_next_slide() + ) + +## Ready fn +func _ready() -> void: + playing = autoplay + + for slide in slides: + slide.modulate.a = 0.0 + + +## Sets [member SlideShow.playing] to [code]true[/code] +func play() -> void: + current_slide = 0 + playing = true + + +## Skips all slides and the [signal SlideShow.finished] is emitted. +## [br] +## GG mate, we worked hard for this. +func skip_all() -> void: + skip_slide_to_nth(get_child_count() + 1) + + +## Skips to the next slide if any, otherwise the slideshow ends and the [signal SlideShow.finished] is emitted. +func skip_slide_to_next() -> void: + skip_slide_to_nth(current_slide + 1) + + +## Skips to a nth slide. If out of bound, the slideshow ends and the [signal SlideShow.finished] is emitted. +func skip_slide_to_nth(slide_index: int) -> void: + var direction = SKIP_NEXT if slide_index > current_slide else SKIP_PREV + var inbound = slide_index >= 0 and slide_index <= slides.size() + + if not inbound: + playing = false + finished.emit() + return + + if current_slide >= slides.size(): + finished.emit() + else: + var tween = create_tween() + var slide = slides[current_slide] as Node2D + + slide_skipped.emit(direction) + + ## Forcefully fades out current slide. You asked for it, do not complain plis. + tween.tween_property(slide, "modulate:a", 0.0, slide_fade_duration) + + tween.finished.connect(func (): + current_slide += direction + _handle_slide_out(slide) + _handle_next_slide(direction) + ) + + +## Skips to the previous slide if any, otherwise the slideshow ends and the [signal SlideShow.finished] is emitted. +func skip_slide_to_prev() -> void: + skip_slide_to_nth(current_slide - 1) + + diff --git a/addons/godot_gameplay_systems/slideshow/test/unit/test_slideshow.gd b/addons/godot_gameplay_systems/slideshow/test/unit/test_slideshow.gd new file mode 100644 index 0000000..325e066 --- /dev/null +++ b/addons/godot_gameplay_systems/slideshow/test/unit/test_slideshow.gd @@ -0,0 +1,93 @@ +extends GutTest + + +func _add_slides(slideshow: SlideShow, count: int) -> void: + for x in range(0, count): + var slide = Node2D.new() + slide.name = "Slide" + str(count) + slideshow.add_child(slide) + + +func _slideshow() -> SlideShow: + var slideshow = SlideShow.new() + add_child_autofree(slideshow) + return slideshow + + + +func test_normal_flow() -> void: + var s = _slideshow() + + watch_signals(s) + + s.slide_duration = 1.0 + s.slide_fade_duration = 1.0 + s.autoplay = true + + assert_eq(s.current_slide, 0, "it should always start from the beginning") + + _add_slides(s, 3) + + s.skip_slide_to_nth(0) + + assert_eq(s.current_slide, 0, "even after adding slides programmatically, it should always start from the beginning") + + s.skip_slide_to_next() + + assert_signal_not_emitted(s, "finished", "finished should not have been emitted") + assert_signal_emitted(s, "slide_skipped", "slide_skipped should have been emitted") + + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + + await wait_seconds(4.0) + + assert_signal_emitted(s, "finished", "finished should have been emitted") + + # wow, it worked + + +func test_trying_to_break_everything() -> void: + var s = _slideshow() + + watch_signals(s) + + # Let's add unusable children + + s.add_child(Node3D.new()) + s.add_child(Node.new()) + + # copy and paste of the "good" scenario test + + s.slide_duration = 1.0 + s.slide_fade_duration = 1.0 + s.autoplay = true + + assert_eq(s.current_slide, 0, "it should always start from the beginning") + + _add_slides(s, 3) + + assert_eq(s.slides.size(), 3, "slides should be only 3") + + s.skip_slide_to_nth(0) + + assert_eq(s.current_slide, 0, "even after adding slides programmatically, it should always start from the beginning") + + s.skip_slide_to_next() + + assert_signal_not_emitted(s, "finished", "finished should not have been emitted") + assert_signal_emitted(s, "slide_skipped", "slide_skipped should have been emitted") + + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + s.skip_slide_to_next() + + await wait_seconds(4.0) + + assert_signal_emitted(s, "finished", "finished should have been emitted") + diff --git a/docs/readme.md b/docs/readme.md index fe43161..335cc2b 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -3,14 +3,23 @@ Some docs Surely this will need some better docs. They are on the road to 1.0.0. -[About ability system](ability-system.md) +## Abilities and attributes -[About gameplay attributes](gameplay-attributes.md) +- [About ability system](ability-system.md) +- [About gameplay attributes](gameplay-attributes.md) -[About inventory](inventory/inventory.md) +## Inventory and equipment -[About equipment](inventory/equipment.md) +- [About inventory](inventory/inventory.md) +- [About equipment](inventory/equipment.md) +- [About items dropping](inventory/drop.md) +- [About interacting with items](interactions-system.md) -[About items dropping](inventory/drop.md) +## Character related nodes -[About interacting with items](interactions-system.md) +- [About point and click](point-and-click.md) +- [About camera shake](camera-shake.md) + +## Miscellaneous + +- [About intro screens](slide_show.md) \ No newline at end of file diff --git a/docs/slide_show.md b/docs/slide_show.md new file mode 100644 index 0000000..d0ee362 --- /dev/null +++ b/docs/slide_show.md @@ -0,0 +1,13 @@ +SlideShow +========= + +You played videogames before don't you? Well, this node is used to create the intro presentation screen where usually the company/deb logo, tech used logos and more a put in. + +It's a 2D node, and accepts only `Node2D` nodes as children (all the other will be discarded). + +It has three parameters: + +- `autoplay`: if set to `true` *(default)*, the slideshow will start when ready. +- `slide_duration`: how long a slide will be visible +- `slide_fade_duration`: how long a fade in or fade out will take + diff --git a/examples/examples.gd b/examples/examples.gd index aa2c1b7..36551de 100644 --- a/examples/examples.gd +++ b/examples/examples.gd @@ -1,23 +1,32 @@ extends Node +@onready var intro: SlideShow = $Intro @onready var running_example: Node = $RunningExample @onready var examples_menu = $ExamplesMenu func _input(event: InputEvent) -> void: - if event.is_action_pressed("close_example"): + if event.is_action_pressed("close_example") and intro != null and not intro.playing: for child in running_example.get_children(): running_example.remove_child(child) Input.mouse_mode = Input.MOUSE_MODE_CONFINED examples_menu.show_menu() + elif event.is_action_pressed("close_example") and intro != null and intro.playing: + intro.skip_slide_to_next() func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_CONFINED + examples_menu.modulate.a = 0.0 examples_menu.scene_selected.connect(func (scene): examples_menu.hide_menu() running_example.add_child(scene) ) + + intro.finished.connect(func (): + create_tween().tween_property(examples_menu, "modulate:a", 1.0, 1.0) + intro.queue_free() + ) diff --git a/examples/examples.tscn b/examples/examples.tscn index 97f8065..a6f025f 100644 --- a/examples/examples.tscn +++ b/examples/examples.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://cml0q8o7ynipb"] +[gd_scene load_steps=4 format=3 uid="uid://cml0q8o7ynipb"] [ext_resource type="Script" path="res://examples/examples.gd" id="1_pax2c"] [ext_resource type="PackedScene" uid="uid://f4ax0ctfqkgv" path="res://examples/examples_menu.tscn" id="2_h6qvp"] +[ext_resource type="PackedScene" uid="uid://bxenr8kur4adq" path="res://examples/intro/intro.tscn" id="3_phc1k"] [node name="Examples" type="Node"] script = ExtResource("1_pax2c") @@ -9,3 +10,7 @@ script = ExtResource("1_pax2c") [node name="RunningExample" type="Node" parent="."] [node name="ExamplesMenu" parent="." instance=ExtResource("2_h6qvp")] + +[node name="Intro" parent="." instance=ExtResource("3_phc1k")] +slide_duration = 3.0 +slide_fade_duration = 0.5 diff --git a/examples/intro/contributors.gd b/examples/intro/contributors.gd new file mode 100644 index 0000000..2f07edb --- /dev/null +++ b/examples/intro/contributors.gd @@ -0,0 +1,32 @@ +extends Node2D + + +const contributors_list = "https://api.github.com/repos/octod/godot-gameplay-systems/contributors?per_page=10000&page=1" + + +@onready var http_request: HTTPRequest = $HTTPRequest +@onready var contributors_container: VBoxContainer = $VBoxContainer/VBoxContainer + + +func _ready() -> void: + http_request.request_completed.connect(func (result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + _render_contributors(JSON.parse_string(body.get_string_from_utf8())) + ) + http_request.request(contributors_list) + + +func _render_contributors(json: Variant) -> void: + if json == null: + return + + for contributor in json: + if str(contributor.login).to_lower() == "octod": + continue + + var label = Label.new() + + label.text = contributor.login + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.theme_type_variation = "HeaderMedium" + + contributors_container.add_child(label) diff --git a/examples/intro/contributors.tscn b/examples/intro/contributors.tscn new file mode 100644 index 0000000..232764b --- /dev/null +++ b/examples/intro/contributors.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=2 format=3 uid="uid://duklssj0j4fg7"] + +[ext_resource type="Script" path="res://examples/intro/contributors.gd" id="1_l0naa"] + +[node name="Contributors" type="Node2D"] +script = ExtResource("1_l0naa") + +[node name="HTTPRequest" type="HTTPRequest" parent="."] + +[node name="HTTPRequestImage" type="HTTPRequest" parent="."] + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +offset_left = -119.0 +offset_top = -13.0 +offset_right = 119.0 +offset_bottom = 13.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="VBoxContainer"] +custom_minimum_size = Vector2(2.08165e-12, 80) +layout_mode = 2 +theme_type_variation = &"HeaderLarge" +text = "A big thank to the contributors" +horizontal_alignment = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Label2" type="Label" parent="VBoxContainer"] +custom_minimum_size = Vector2(2.08165e-12, 80) +layout_mode = 2 +text = "And all other wonderful godot people helping out and using this addon!" +horizontal_alignment = 1 +vertical_alignment = 2 diff --git a/examples/intro/intro.tscn b/examples/intro/intro.tscn new file mode 100644 index 0000000..f998a90 --- /dev/null +++ b/examples/intro/intro.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=5 format=3 uid="uid://bxenr8kur4adq"] + +[ext_resource type="Script" path="res://addons/godot_gameplay_systems/slideshow/slide_show.gd" id="1_0owy3"] +[ext_resource type="PackedScene" uid="uid://bspbrxotiakta" path="res://examples/intro/logo.tscn" id="2_ohfca"] +[ext_resource type="PackedScene" uid="uid://b7minroklwmam" path="res://examples/intro/octod.tscn" id="3_kj10b"] +[ext_resource type="PackedScene" uid="uid://duklssj0j4fg7" path="res://examples/intro/contributors.tscn" id="4_1iqbv"] + +[node name="Intro" type="Node2D"] +script = ExtResource("1_0owy3") + +[node name="OctoD" parent="." instance=ExtResource("3_kj10b")] + +[node name="Contributors" parent="." instance=ExtResource("4_1iqbv")] + +[node name="Logo" parent="." instance=ExtResource("2_ohfca")] diff --git a/examples/intro/logo.tscn b/examples/intro/logo.tscn new file mode 100644 index 0000000..50ec75b --- /dev/null +++ b/examples/intro/logo.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=3 uid="uid://bspbrxotiakta"] + +[ext_resource type="Texture2D" uid="uid://c7lgk28i4ps8i" path="res://icon.svg" id="1_odnmw"] + +[node name="Logo" type="Node2D"] + +[node name="CenterContainer" type="CenterContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +layout_mode = 2 + +[node name="TextureRect" type="TextureRect" parent="CenterContainer/VBoxContainer"] +layout_mode = 2 +texture = ExtResource("1_odnmw") + +[node name="Label" type="Label" parent="CenterContainer/VBoxContainer"] +custom_minimum_size = Vector2(2.08165e-12, 40) +layout_mode = 2 +text = "Godot Gameplay Systems" +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/examples/intro/octod.png b/examples/intro/octod.png new file mode 100644 index 0000000..ce0d517 Binary files /dev/null and b/examples/intro/octod.png differ diff --git a/examples/intro/octod.png.import b/examples/intro/octod.png.import new file mode 100644 index 0000000..7dd3ae0 --- /dev/null +++ b/examples/intro/octod.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://if2fofmbm7xf" +path="res://.godot/imported/octod.png-22d9524a61e39a295bf7abea21120bcc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://examples/intro/octod.png" +dest_files=["res://.godot/imported/octod.png-22d9524a61e39a295bf7abea21120bcc.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/examples/intro/octod.tscn b/examples/intro/octod.tscn new file mode 100644 index 0000000..2bedc58 --- /dev/null +++ b/examples/intro/octod.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=3 uid="uid://b7minroklwmam"] + +[ext_resource type="Texture2D" uid="uid://if2fofmbm7xf" path="res://examples/intro/octod.png" id="1_fvtck"] + +[node name="OctoD" type="Node2D"] + +[node name="BoxContainer" type="VBoxContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +alignment = 1 + +[node name="TextureRect" type="TextureRect" parent="BoxContainer"] +layout_mode = 2 +texture = ExtResource("1_fvtck") + +[node name="Label" type="Label" parent="BoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderLarge" +text = "OctoD Presents" +horizontal_alignment = 1