commit 8be7c8e7c150bbb50c51e5cc93ac0fa186692b1f Author: kujiu (@ciredutemps) Date: Sat Apr 18 22:06:40 2020 +0200 Release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..111dbc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.import/ +logs/ +*.translation + +# Platform specific metadata files +.DS_Store +thumbs.db +.*.swp +*~ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..b88d444 --- /dev/null +++ b/LICENCE @@ -0,0 +1,74 @@ +Texte +===== + +Kujiu + +Graphisme +========= + +Springouille + +Voix, SFX et programmation +========================== + +Kujiu + +Musique +======= + +Dreaming Under the Stars + par Darren Curtis - + Licence Creative Commons CC-BY - + https://www.darrencurtismusic.com + +Robot Lover Song + par Neku (feat. Aline) - + Licence Creative Commons + +Caractères +========== + +Lavi font + par Ruben Holthuijsen - + Licence GPLv3 + +Rendu avec FreeType + The FreeType Project. All rights reserved. - + http://www.freetype.org + +Moteur +====== + +Godot Engine + par Juan Linietsky, Ariel Manzur et contributeurs - + Licence MIT - + https://www.godotengine.org + +Escoria + par Juan Linietsky, Ariel Manzur et contributeurs - + Licence MIT - + https://www.github.com/godotengine/escoria + +ENet + par Lee Salzman - + Licence MIT-like + +MBedTLS + Licence Apache version 2.0 + +Production +========== + +Une Narration Expérimentale de Réalité Vectographiée + +Nerv Project + +https://www.nerv-project.eu + +Licence +======= +Distribué sous licence Creative Commons CC-BY-SA 4.0 + +http://creativecommons.org/licenses/by-sa/4.0/ + +(Sauf travaux sous licence spécifique) diff --git a/device/actors/dragon/assets/dragon.png b/device/actors/dragon/assets/dragon.png new file mode 100644 index 0000000..1067357 Binary files /dev/null and b/device/actors/dragon/assets/dragon.png differ diff --git a/device/actors/dragon/assets/dragon.png.import b/device/actors/dragon/assets/dragon.png.import new file mode 100644 index 0000000..2bc5bf3 --- /dev/null +++ b/device/actors/dragon/assets/dragon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/dragon.png-b942aab86d4b9317a870944d76fcaaeb.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://actors/dragon/assets/dragon.png" +dest_files=[ "res://.import/dragon.png-b942aab86d4b9317a870944d76fcaaeb.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/device/actors/dragon/dragon.tscn b/device/actors/dragon/dragon.tscn new file mode 100644 index 0000000..5eebe17 --- /dev/null +++ b/device/actors/dragon/dragon.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/item.gd" type="Script" id=1] +[ext_resource path="res://actors/dragon/assets/dragon.png" type="Texture" id=2] +[ext_resource path="res://rooms/garden/assets/rain.ogg" type="AudioStream" id=3] + +[sub_resource type="Animation" id=1] +resource_name = "rain" +length = 23.0 +tracks/0/type = "audio" +tracks/0/path = NodePath("RainPlayer") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"clips": [ { +"end_offset": 0.0, +"start_offset": 0.0, +"stream": ExtResource( 3 ) +} ], +"times": PoolRealArray( 0 ) +} + +[node name="Dragon" type="Node2D"] +script = ExtResource( 1 ) +global_id = "Dragon" +active = false +use_action_menu = false + +[node name="area" type="Area2D" parent="."] + +[node name="Sprite" type="Sprite" parent="area"] +scale = Vector2( 1.32615, 1.31784 ) +texture = ExtResource( 2 ) +centered = false + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="area"] +position = Vector2( -24.1903, -25.9038 ) +scale = Vector2( 1.38245, 1.38245 ) +polygon = PoolVector2Array( 379.768, 88.1424, 298.145, 208.428, 301.009, 272.867, 273.802, 294.346, 227.979, 338.737, 202.203, 401.744, 226.547, 438.975, 216.523, 454.727, 189.315, 451.863, 196.475, 494.822, 176.428, 557.829, 104.829, 559.261, 93.3732, 627.996, 89.0773, 672.387, 67.5977, 731.098, 67.5977, 759.737, 106.261, 748.281, 154.948, 733.962, 179.292, 705.322, 226.547, 688.138, 270.938, 692.434, 302.441, 696.73, 296.713, 658.067, 289.553, 626.564, 323.921, 640.883, 376.904, 636.587, 402.679, 620.836, 478.574, 646.611, 563.06, 643.747, 608.883, 620.836, 634.658, 586.468, 594.563, 569.285, 570.22, 569.285, 558.764, 527.758, 584.539, 493.39, 610.315, 447.567, 613.179, 390.288, 604.587, 350.193, 585.971, 304.37, 547.308, 287.186, 512.941, 308.666, 550.172, 351.625, 497.189, 312.962, 441.342, 312.962, 462.822, 350.193, 429.887, 324.418, 432.75, 277.163, 468.55, 254.251, 538.716, 241.363, 620.339, 241.363, 677.618, 251.387, 716.281, 282.891, 776.424, 255.683, 832.271, 272.867, 873.798, 300.074, 888.117, 348.761, 865.206, 400.312, 812.223, 437.543, 773.56, 430.384, 733.464, 407.472, 684.777, 407.472, 693.369, 447.567, 713.417, 469.047, 670.458, 509.142, 670.458, 577.877, 683.345, 623.7, 724.873, 633.724, 753.512, 678.115, 825.111, 676.683, 908.165, 675.251, 982.628, 665.227, 962.527, 734.022, 996.064, 765.323, 1084.3, 728.234, 1105.78, 686.706, 1100.05, 642.315, 1090.03, 593.628, 1027.02, 583.604, 975.468, 583.604, 953.988, 567.853, 956.852, 534.917, 913.893, 537.781, 880.958, 572.149, 852.318, 539.213, 890.981, 513.438, 952.556, 491.958, 1005.54, 460.455, 1034.18, 407.472, 1065.68, 335.873, 1042.77, 277.163, 1088.59, 317.258, 1097.19, 265.707, 1052.79, 224.18, 1024.15, 221.316, 998.379, 181.221, 952.556, 188.38, 942.532, 151.149, 879.526, 105.326, 830.839, 95.3022, 803.631, 56.639, 736.328, 22.2717, 677.618, 43.7513, 620.339, 65.2309, 567.356, 99.5982, 492.893, 103.894, 467.118, 145.421, 427.023, 115.35 ) + +[node name="animation" type="AnimationPlayer" parent="."] +anims/rain = SubResource( 1 ) + +[node name="RainPlayer" type="AudioStreamPlayer2D" parent="."] +stream = ExtResource( 3 ) +volume_db = 4.0 +max_distance = 20000.0 diff --git a/device/actors/god/assets/god.png b/device/actors/god/assets/god.png new file mode 100644 index 0000000..b54cbaf Binary files /dev/null and b/device/actors/god/assets/god.png differ diff --git a/device/actors/god/assets/god.png.import b/device/actors/god/assets/god.png.import new file mode 100644 index 0000000..dd5b576 --- /dev/null +++ b/device/actors/god/assets/god.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/god.png-3f4e537a13a15c396d20f3618c703300.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://actors/god/assets/god.png" +dest_files=[ "res://.import/god.png-3f4e537a13a15c396d20f3618c703300.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/device/actors/god/god.tscn b/device/actors/god/god.tscn new file mode 100644 index 0000000..c022242 --- /dev/null +++ b/device/actors/god/god.tscn @@ -0,0 +1,45 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/item.gd" type="Script" id=1] +[ext_resource path="res://actors/god/assets/god.png" type="Texture" id=2] +[ext_resource path="res://rooms/garden/assets/plouf.ogg" type="AudioStream" id=3] + +[sub_resource type="Animation" id=1] +resource_name = "plouf" +length = 1.6 +tracks/0/type = "audio" +tracks/0/path = NodePath("PloufPlayer") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"clips": [ { +"end_offset": 0.0, +"start_offset": 0.0, +"stream": ExtResource( 3 ) +} ], +"times": PoolRealArray( 0 ) +} + +[node name="God" type="Node2D"] +script = ExtResource( 1 ) +global_id = "God" + +[node name="area" type="Area2D" parent="."] + +[node name="Sprite" type="Sprite" parent="area"] +position = Vector2( 280, 559 ) +scale = Vector2( 1.8, 1.8 ) +texture = ExtResource( 2 ) + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="area"] +polygon = PoolVector2Array( 215.206, -35.7623, 287.913, -3.95319, 56.1606, 89.202, 24.3516, 289.145, -46.0829, 480, -84.7083, 602.692, -32.4504, 636.773, 72.0652, 604.964, 97.058, 643.589, -100.613, 993.489, 67.5211, 1032.11, 219.75, 982.129, 346.987, 995.761, 540.113, 1016.21, 521.937, 916.239, 408.333, 679.942, 419.693, 618.596, 537.841, 654.95, 687.799, 668.582, 683.254, 564.066, 553.746, 400.477, 574.195, 214.166, 487.856, -3.95319, 297.001, -149.366, 224.294, -117.557, 224.294, -117.557, 219.75, -119.829 ) + +[node name="animation" type="AnimationPlayer" parent="."] +anims/plouf = SubResource( 1 ) + +[node name="PloufPlayer" type="AudioStreamPlayer2D" parent="."] +stream = ExtResource( 3 ) +volume_db = 4.0 +max_distance = 20000.0 diff --git a/device/actors/narrator/narrator.tscn b/device/actors/narrator/narrator.tscn new file mode 100644 index 0000000..50a4bd1 --- /dev/null +++ b/device/actors/narrator/narrator.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://globals/item.gd" type="Script" id=1] + +[node name="Narrator" type="Node2D"] +script = ExtResource( 1 ) +global_id = "Narrator" +active = false diff --git a/device/bin/win64/release/libgitapi.dll b/device/bin/win64/release/libgitapi.dll new file mode 100644 index 0000000..b3148b5 Binary files /dev/null and b/device/bin/win64/release/libgitapi.dll differ diff --git a/device/bin/win64/release/libgitapi.exp b/device/bin/win64/release/libgitapi.exp new file mode 100644 index 0000000..a4f600a Binary files /dev/null and b/device/bin/win64/release/libgitapi.exp differ diff --git a/device/bin/win64/release/libgitapi.pdb b/device/bin/win64/release/libgitapi.pdb new file mode 100644 index 0000000..2a618af Binary files /dev/null and b/device/bin/win64/release/libgitapi.pdb differ diff --git a/device/bin/x11/release/libgitapi.so b/device/bin/x11/release/libgitapi.so new file mode 100644 index 0000000..a11dd5a Binary files /dev/null and b/device/bin/x11/release/libgitapi.so differ diff --git a/device/contrib/custom/spine.gd b/device/contrib/custom/spine.gd new file mode 100644 index 0000000..6f5f26b --- /dev/null +++ b/device/contrib/custom/spine.gd @@ -0,0 +1,17 @@ +extends Node2D + +# Custom functions to be used in your game. + +# Spine example +func spine_change_skin(params): + # Show-and-tell, ignore + var obj = params[0] + var func_name = params[1] + + # Change all parent's Spine nodes' skins + var skin = params[2] + + for node in get_parent().get_children(): + if node.get_class() == "Spine": + node.set_indexed("playback/skin", skin) + diff --git a/device/contrib/scenes/shadow/cap.png b/device/contrib/scenes/shadow/cap.png new file mode 100644 index 0000000..270b17a Binary files /dev/null and b/device/contrib/scenes/shadow/cap.png differ diff --git a/device/contrib/scenes/shadow/cap.png.import b/device/contrib/scenes/shadow/cap.png.import new file mode 100644 index 0000000..b6ea228 --- /dev/null +++ b/device/contrib/scenes/shadow/cap.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/cap.png-3d49dcef40d6831b180435be18953959.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://contrib/scenes/shadow/cap.png" +dest_files=[ "res://.import/cap.png-3d49dcef40d6831b180435be18953959.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/contrib/scenes/shadow/shadow.png b/device/contrib/scenes/shadow/shadow.png new file mode 100644 index 0000000..a66c1ea Binary files /dev/null and b/device/contrib/scenes/shadow/shadow.png differ diff --git a/device/contrib/scenes/shadow/shadow.png.import b/device/contrib/scenes/shadow/shadow.png.import new file mode 100644 index 0000000..3765630 --- /dev/null +++ b/device/contrib/scenes/shadow/shadow.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/shadow.png-acc6ebdec702b2dc2e8d7d7cc818c744.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://contrib/scenes/shadow/shadow.png" +dest_files=[ "res://.import/shadow.png-acc6ebdec702b2dc2e8d7d7cc818c744.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/contrib/scenes/shadow/shadow.tscn b/device/contrib/scenes/shadow/shadow.tscn new file mode 100644 index 0000000..3fa3790 --- /dev/null +++ b/device/contrib/scenes/shadow/shadow.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/shadow.gd" type="Script" id=1] +[ext_resource path="res://contrib/shaders/shadow.tres" type="Material" id=2] +[ext_resource path="res://contrib/scenes/shadow/shadow.png" type="Texture" id=3] +[ext_resource path="res://contrib/scenes/shadow/cap.png" type="Texture" id=4] + +[node name="shadow" type="Node2D"] +script = ExtResource( 1 ) + +[node name="perspective_scale" type="Node2D" parent="."] +scale = Vector2( 1, 0.5 ) + +[node name="rotation" type="Node2D" parent="perspective_scale"] + +[node name="shadow" type="Sprite" parent="perspective_scale/rotation"] +material = ExtResource( 2 ) +z_index = -1 +texture = ExtResource( 3 ) +centered = false +offset = Vector2( 0, -32 ) + +[node name="shadow_cap" type="Sprite" parent="perspective_scale/rotation"] +material = ExtResource( 2 ) +z_index = -1 +texture = ExtResource( 4 ) +offset = Vector2( -16, 0 ) diff --git a/device/contrib/shaders/shadow.shader b/device/contrib/shaders/shadow.shader new file mode 100644 index 0000000..ecbe29d --- /dev/null +++ b/device/contrib/shaders/shadow.shader @@ -0,0 +1,10 @@ +shader_type canvas_item; +render_mode blend_mix; + +uniform float alpha_value = 0.5; + +void fragment() { + float v = texture(TEXTURE, UV).a; + COLOR = vec4(vec3(0), v*alpha_value); +} + diff --git a/device/contrib/shaders/shadow.tres b/device/contrib/shaders/shadow.tres new file mode 100644 index 0000000..cc0155b --- /dev/null +++ b/device/contrib/shaders/shadow.tres @@ -0,0 +1,11 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=2] + +[ext_resource path="res://contrib/shaders/shadow.shader" type="Shader" id=1] + +[resource] + +render_priority = 0 +shader = ExtResource( 1 ) +shader_param/alpha_value = 1.0 +_sections_unfolded = [ "shader_param" ] + diff --git a/device/default_bus_layout.tres b/device/default_bus_layout.tres new file mode 100644 index 0000000..bc2fb66 --- /dev/null +++ b/device/default_bus_layout.tres @@ -0,0 +1,29 @@ +[gd_resource type="AudioBusLayout" format=2] + +[resource] + +bus/0/name = "Master" +bus/0/solo = false +bus/0/mute = false +bus/0/bypass_fx = false +bus/0/volume_db = 0.0 +bus/0/send = "" +bus/1/name = "Music" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = "Master" +bus/2/name = "Speech" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = "Master" +bus/3/name = "SFX" +bus/3/solo = false +bus/3/mute = false +bus/3/bypass_fx = false +bus/3/volume_db = 0.0 +bus/3/send = "Master" + diff --git a/device/export_presets.cfg b/device/export_presets.cfg new file mode 100644 index 0000000..8fc0f59 --- /dev/null +++ b/device/export_presets.cfg @@ -0,0 +1,67 @@ +[preset.0] + +name="Linux/X11" +platform="Linux/X11" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="*.esc" +exclude_filter="" +export_path="../../../../../../../../home/kujiu/dieu_du_jardin/linux/dieu_du_jardin.x86_64" +patch_list=PoolStringArray( ) +script_export_mode=1 +script_encryption_key="" + +[preset.0.options] + +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=true +texture_format/etc2=true +texture_format/no_bptc_fallbacks=false +binary_format/64_bits=true +binary_format/embed_pck=true +custom_template/release="" +custom_template/debug="" + +[preset.1] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +custom_features="" +export_filter="all_resources" +include_filter="*.esc" +exclude_filter="" +export_path="dieu_du_jardin.exe" +patch_list=PoolStringArray( ) +script_export_mode=1 +script_encryption_key="" + +[preset.1.options] + +texture_format/bptc=false +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +texture_format/no_bptc_fallbacks=true +binary_format/64_bits=true +binary_format/embed_pck=true +custom_template/release="" +custom_template/debug="" +codesign/enable=false +codesign/identity="" +codesign/password="" +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PoolStringArray( ) +application/icon="" +application/file_version="" +application/product_version="" +application/company_name="" +application/product_name="" +application/file_description="" +application/copyright="" +application/trademarks="" diff --git a/device/game.esc b/device/game.esc new file mode 100644 index 0000000..5e9e73f --- /dev/null +++ b/device/game.esc @@ -0,0 +1,7 @@ +:start + +cut_scene telon fade_out + +change_scene res://rooms/garden/garden.tscn + +cut_scene telon fade_in diff --git a/device/git_api.gdnlib b/device/git_api.gdnlib new file mode 100644 index 0000000..42f2fb6 --- /dev/null +++ b/device/git_api.gdnlib @@ -0,0 +1,16 @@ +[general] + +singleton=true +load_once=true +symbol_prefix="godot_" +reloadable=false + +[entry] + +Windows.64="res://bin/win64/release/libgitapi.dll" +X11.64="res://bin/x11/release/libgitapi.so" + +[dependencies] + +Windows.64=[ ] +X11.64=[ ] diff --git a/device/git_api.gdns b/device/git_api.gdns new file mode 100644 index 0000000..adab80a --- /dev/null +++ b/device/git_api.gdns @@ -0,0 +1,9 @@ +[gd_resource type="NativeScript" load_steps=2 format=2] + +[ext_resource path="res://git_api.gdnlib" type="GDNativeLibrary" id=1] + +[resource] +resource_name = "GitAPI" +class_name = "GitAPI" +library = ExtResource( 1 ) +script_class_name = "GitAPI" diff --git a/device/globals/achievements.gd b/device/globals/achievements.gd new file mode 100644 index 0000000..b864db3 --- /dev/null +++ b/device/globals/achievements.gd @@ -0,0 +1,50 @@ +var GameCenter = null +var iOS = null +var achieves_left = 2 + +func flush(): + while GameCenter.get_pending_event_count() > 0: + GameCenter.pop_pending_event() + +func award(aid): + if GameCenter == null: + return + + flush() + + printt("******** awarding achievement ", aid) + GameCenter.award_achievement( { "name": aid, "progress": 100, "show_completion_banner": true } ) + + if vm.settings.rate_shown: + return + + achieves_left -= 1 + if achieves_left > 0: + return + + printt("showing rate screen") + var url = iOS.get_rate_url(ProjectSettings.get_setting("ios/app_id")) + vm.show_rate(url) + + vm.settings.rate_shown = true + vm.save_settings() + + +func reset(): + if GameCenter == null: + return + + flush() + GameCenter.reset_achievements() + +func is_ready(): + return true + +func start(): + # TODO: This initialization should probably be somewhere else + # if ProjectSettings.has_setting("GameCenter"): + # GameCenter = ProjectSettings.get_singleton("GameCenter") + # iOS = ProjectSettings.get_singleton("iOS") + + pass + diff --git a/device/globals/action_menu.gd b/device/globals/action_menu.gd new file mode 100644 index 0000000..3c2f03b --- /dev/null +++ b/device/globals/action_menu.gd @@ -0,0 +1,74 @@ +extends Container + +var target +func action_pressed(action): + if !is_visible(): + return + if !get_node("/root/vm").can_interact(): + return + + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "action_menu_selected", target, action) + +func target_visibility_changed(): + stop() + +func _clamp(click_pos): + var scale = ProjectSettings.get_setting("escoria/platform/action_menu_scale") + set_scale(Vector2(scale, scale)) + + var width = float(ProjectSettings.get("display/window/size/width")) + var height = float(ProjectSettings.get("display/window/size/height")) + var my_size = get_size() * Vector2(scale, scale) + var center_offset = my_size / Vector2(2, 2) # Half to the left, half up + + # Set the action menu in the middle + click_pos -= center_offset + + var dist_from_right = width - (click_pos.x + my_size.x) + var dist_from_left = click_pos.x + var dist_from_bottom = height - (click_pos.y + my_size.y) + var dist_from_top = click_pos.y + + if dist_from_right < 0: + click_pos.x += dist_from_right + if dist_from_left < 0: + click_pos.x -= dist_from_left + if dist_from_bottom < 0: + click_pos.y += dist_from_bottom + if dist_from_top < 0: + click_pos.y -= dist_from_top + + return click_pos + +func start(p_target): + if target != p_target: + target = p_target + target.connect("visibility_changed", self, "target_visibility_changed") + + # Do not display the tooltip alongside the menu + if vm.tooltip and ProjectSettings.get_setting("escoria/ui/tooltip_follows_mouse"): + vm.tooltip.hide() # XXX: Maybe the tooltip should hide itself automatically if the action menu is visible + vm.hover_teardown() + +func stop(show_tooltip=true): + if target != null: + target.disconnect("visibility_changed", self, "target_visibility_changed") + target = null + hide() + if vm.tooltip and ProjectSettings.get_setting("escoria/ui/tooltip_follows_mouse") and show_tooltip: + vm.hover_rebuild() + +func set_position(pos, keep_margins=false): + .set_position(_clamp(pos), keep_margins) + +func _ready(): + var acts = get_node("actions") + + for i in range(acts.get_child_count()): + var c = acts.get_child(i) + if !(c is BaseButton): + continue + c.connect("pressed", self, "action_pressed", [c.get_name()]) + + vm.register_action_menu(self) + diff --git a/device/globals/assets/fonts/Lavi.ttf b/device/globals/assets/fonts/Lavi.ttf new file mode 100644 index 0000000..2dd2996 Binary files /dev/null and b/device/globals/assets/fonts/Lavi.ttf differ diff --git a/device/globals/assets/fonts/lavi.tres b/device/globals/assets/fonts/lavi.tres new file mode 100644 index 0000000..9b8b2cd --- /dev/null +++ b/device/globals/assets/fonts/lavi.tres @@ -0,0 +1,8 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/Lavi.ttf" type="DynamicFontData" id=1] + +[resource] +size = 64 +outline_color = Color( 0, 0, 0, 1 ) +font_data = ExtResource( 1 ) diff --git a/device/globals/assets/fonts/lavi_credit.tres b/device/globals/assets/fonts/lavi_credit.tres new file mode 100644 index 0000000..b17a689 --- /dev/null +++ b/device/globals/assets/fonts/lavi_credit.tres @@ -0,0 +1,13 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/Lavi.ttf" type="DynamicFontData" id=1] + +[resource] +size = 92 +outline_size = 12 +outline_color = Color( 0, 0, 0, 1 ) +extra_spacing_top = 20 +extra_spacing_bottom = 20 +extra_spacing_char = 8 +extra_spacing_space = 20 +font_data = ExtResource( 1 ) diff --git a/device/globals/assets/fonts/lavi_credit_title.tres b/device/globals/assets/fonts/lavi_credit_title.tres new file mode 100644 index 0000000..9cd29d6 --- /dev/null +++ b/device/globals/assets/fonts/lavi_credit_title.tres @@ -0,0 +1,13 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/Lavi.ttf" type="DynamicFontData" id=1] + +[resource] +size = 150 +outline_size = 10 +outline_color = Color( 0, 0, 0, 1 ) +extra_spacing_top = 20 +extra_spacing_bottom = 20 +extra_spacing_char = 8 +extra_spacing_space = 20 +font_data = ExtResource( 1 ) diff --git a/device/globals/assets/fonts/license.txt b/device/globals/assets/fonts/license.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/device/globals/assets/fonts/license.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/device/globals/assets/fonts/readme.txt b/device/globals/assets/fonts/readme.txt new file mode 100644 index 0000000..818433e --- /dev/null +++ b/device/globals/assets/fonts/readme.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/device/globals/assets/icon.png b/device/globals/assets/icon.png new file mode 100644 index 0000000..f3dde64 Binary files /dev/null and b/device/globals/assets/icon.png differ diff --git a/device/globals/assets/icon.png.import b/device/globals/assets/icon.png.import new file mode 100644 index 0000000..fbc221d --- /dev/null +++ b/device/globals/assets/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-def7f3b9d27680badc8b63c9e232bd0d.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://globals/assets/icon.png" +dest_files=[ "res://.import/icon.png-def7f3b9d27680badc8b63c9e232bd0d.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/globals/assets/musics/dreaming_under_the_stars.ogg b/device/globals/assets/musics/dreaming_under_the_stars.ogg new file mode 100644 index 0000000..c5f4b46 Binary files /dev/null and b/device/globals/assets/musics/dreaming_under_the_stars.ogg differ diff --git a/device/globals/assets/musics/dreaming_under_the_stars.ogg.import b/device/globals/assets/musics/dreaming_under_the_stars.ogg.import new file mode 100644 index 0000000..08d288b --- /dev/null +++ b/device/globals/assets/musics/dreaming_under_the_stars.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/dreaming_under_the_stars.ogg-0b8bed0517102fd70d5fd3c4f101a176.oggstr" + +[deps] + +source_file="res://globals/assets/musics/dreaming_under_the_stars.ogg" +dest_files=[ "res://.import/dreaming_under_the_stars.ogg-0b8bed0517102fd70d5fd3c4f101a176.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/musics/end_credits.ogg b/device/globals/assets/musics/end_credits.ogg new file mode 100644 index 0000000..ba539a0 Binary files /dev/null and b/device/globals/assets/musics/end_credits.ogg differ diff --git a/device/globals/assets/musics/end_credits.ogg.import b/device/globals/assets/musics/end_credits.ogg.import new file mode 100644 index 0000000..af433e0 --- /dev/null +++ b/device/globals/assets/musics/end_credits.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/end_credits.ogg-dd304750412eabcabfb61beec53568e1.oggstr" + +[deps] + +source_file="res://globals/assets/musics/end_credits.ogg" +dest_files=[ "res://.import/end_credits.ogg-dd304750412eabcabfb61beec53568e1.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/musics/end_credits.opus b/device/globals/assets/musics/end_credits.opus new file mode 100644 index 0000000..febb57c Binary files /dev/null and b/device/globals/assets/musics/end_credits.opus differ diff --git a/device/globals/assets/speech/fr/player_speak_1_1.ogg b/device/globals/assets/speech/fr/player_speak_1_1.ogg new file mode 100644 index 0000000..972d32c Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_1_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_1_1.ogg.import b/device/globals/assets/speech/fr/player_speak_1_1.ogg.import new file mode 100644 index 0000000..852ff81 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_1_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_1_1.ogg-d9371869a5c54909461671798ce50534.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_1_1.ogg" +dest_files=[ "res://.import/player_speak_1_1.ogg-d9371869a5c54909461671798ce50534.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_1_2.ogg b/device/globals/assets/speech/fr/player_speak_1_2.ogg new file mode 100644 index 0000000..9acad0a Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_1_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_1_2.ogg.import b/device/globals/assets/speech/fr/player_speak_1_2.ogg.import new file mode 100644 index 0000000..661f4b1 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_1_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_1_2.ogg-502257ada099ffd6390d707c6bf3d9d8.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_1_2.ogg" +dest_files=[ "res://.import/player_speak_1_2.ogg-502257ada099ffd6390d707c6bf3d9d8.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_2_1.ogg b/device/globals/assets/speech/fr/player_speak_2_1.ogg new file mode 100644 index 0000000..0347064 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_2_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_2_1.ogg.import b/device/globals/assets/speech/fr/player_speak_2_1.ogg.import new file mode 100644 index 0000000..fa025b0 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_2_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_2_1.ogg-a3ca3305840eb0aafe813f6b45f43c8d.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_2_1.ogg" +dest_files=[ "res://.import/player_speak_2_1.ogg-a3ca3305840eb0aafe813f6b45f43c8d.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_2_2.ogg b/device/globals/assets/speech/fr/player_speak_2_2.ogg new file mode 100644 index 0000000..8d6a4e8 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_2_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_2_2.ogg.import b/device/globals/assets/speech/fr/player_speak_2_2.ogg.import new file mode 100644 index 0000000..7362c57 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_2_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_2_2.ogg-8b81201a31f56a692ca3155ecd36e501.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_2_2.ogg" +dest_files=[ "res://.import/player_speak_2_2.ogg-8b81201a31f56a692ca3155ecd36e501.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_3_1.ogg b/device/globals/assets/speech/fr/player_speak_3_1.ogg new file mode 100644 index 0000000..64b085b Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_3_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_3_1.ogg.import b/device/globals/assets/speech/fr/player_speak_3_1.ogg.import new file mode 100644 index 0000000..7a39a82 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_3_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_3_1.ogg-e611aa6a79b2e975dc1657ab2ad9f190.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_3_1.ogg" +dest_files=[ "res://.import/player_speak_3_1.ogg-e611aa6a79b2e975dc1657ab2ad9f190.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_4_1.ogg b/device/globals/assets/speech/fr/player_speak_4_1.ogg new file mode 100644 index 0000000..f34d227 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_4_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_4_1.ogg.import b/device/globals/assets/speech/fr/player_speak_4_1.ogg.import new file mode 100644 index 0000000..0b0b911 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_4_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_4_1.ogg-df1a79565682b2572d3db332ced70a48.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_4_1.ogg" +dest_files=[ "res://.import/player_speak_4_1.ogg-df1a79565682b2572d3db332ced70a48.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_4_2.ogg b/device/globals/assets/speech/fr/player_speak_4_2.ogg new file mode 100644 index 0000000..235c5c8 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_4_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_4_2.ogg.import b/device/globals/assets/speech/fr/player_speak_4_2.ogg.import new file mode 100644 index 0000000..2d00cdc --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_4_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_4_2.ogg-1b22b3091e7789a3d5f14827b230bc76.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_4_2.ogg" +dest_files=[ "res://.import/player_speak_4_2.ogg-1b22b3091e7789a3d5f14827b230bc76.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_5_1.ogg b/device/globals/assets/speech/fr/player_speak_5_1.ogg new file mode 100644 index 0000000..899c24b Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_5_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_5_1.ogg.import b/device/globals/assets/speech/fr/player_speak_5_1.ogg.import new file mode 100644 index 0000000..89e9927 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_5_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_5_1.ogg-7a975be5a670b9220f3f093489c3cb99.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_5_1.ogg" +dest_files=[ "res://.import/player_speak_5_1.ogg-7a975be5a670b9220f3f093489c3cb99.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_5_2.ogg b/device/globals/assets/speech/fr/player_speak_5_2.ogg new file mode 100644 index 0000000..c4b172a Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_5_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_5_2.ogg.import b/device/globals/assets/speech/fr/player_speak_5_2.ogg.import new file mode 100644 index 0000000..6683fe1 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_5_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_5_2.ogg-a0aa549d098f204d36a0159cfeb85619.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_5_2.ogg" +dest_files=[ "res://.import/player_speak_5_2.ogg-a0aa549d098f204d36a0159cfeb85619.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_6_1.ogg b/device/globals/assets/speech/fr/player_speak_6_1.ogg new file mode 100644 index 0000000..89d3a9e Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_6_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_6_1.ogg.import b/device/globals/assets/speech/fr/player_speak_6_1.ogg.import new file mode 100644 index 0000000..311f506 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_6_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_6_1.ogg-eba85ad2bc93334c86ba34ff7db5b83c.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_6_1.ogg" +dest_files=[ "res://.import/player_speak_6_1.ogg-eba85ad2bc93334c86ba34ff7db5b83c.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_6_2.ogg b/device/globals/assets/speech/fr/player_speak_6_2.ogg new file mode 100644 index 0000000..1f657d6 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_6_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_6_2.ogg.import b/device/globals/assets/speech/fr/player_speak_6_2.ogg.import new file mode 100644 index 0000000..9e30b14 --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_6_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_6_2.ogg-ee74e6f6fddf1b5b4631904a0716f3ff.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_6_2.ogg" +dest_files=[ "res://.import/player_speak_6_2.ogg-ee74e6f6fddf1b5b4631904a0716f3ff.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_7_1.ogg b/device/globals/assets/speech/fr/player_speak_7_1.ogg new file mode 100644 index 0000000..e93f2d1 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_7_1.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_7_1.ogg.import b/device/globals/assets/speech/fr/player_speak_7_1.ogg.import new file mode 100644 index 0000000..21804aa --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_7_1.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_7_1.ogg-1a3fb2794d0772cef9b262a0e1156a02.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_7_1.ogg" +dest_files=[ "res://.import/player_speak_7_1.ogg-1a3fb2794d0772cef9b262a0e1156a02.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech/fr/player_speak_7_2.ogg b/device/globals/assets/speech/fr/player_speak_7_2.ogg new file mode 100644 index 0000000..607b803 Binary files /dev/null and b/device/globals/assets/speech/fr/player_speak_7_2.ogg differ diff --git a/device/globals/assets/speech/fr/player_speak_7_2.ogg.import b/device/globals/assets/speech/fr/player_speak_7_2.ogg.import new file mode 100644 index 0000000..bb5dfdd --- /dev/null +++ b/device/globals/assets/speech/fr/player_speak_7_2.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/player_speak_7_2.ogg-f6ee24a4eff25f046a3e575fd0228fcf.oggstr" + +[deps] + +source_file="res://globals/assets/speech/fr/player_speak_7_2.ogg" +dest_files=[ "res://.import/player_speak_7_2.ogg-f6ee24a4eff25f046a3e575fd0228fcf.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/globals/assets/speech_locales.gd b/device/globals/assets/speech_locales.gd new file mode 100644 index 0000000..6406842 --- /dev/null +++ b/device/globals/assets/speech_locales.gd @@ -0,0 +1,7 @@ +# Define your game's speech/voice locales within the game +# because ProjectSettings doesn't deal well with arrays. +# These can be used in your game to change the spoken language. + +#warning-ignore:unused_class_variable +var speech_locales = ['fr'] + diff --git a/device/globals/background.gd b/device/globals/background.gd new file mode 100644 index 0000000..3fc784c --- /dev/null +++ b/device/globals/background.gd @@ -0,0 +1,69 @@ +extends Sprite + +signal left_click_on_bg +signal right_click_on_bg # Connect this in your game/signal_script + +export var action = "walk" +var area + +# Godot doesn't do doubleclicks so we must +var last_lmb_dt = 0 +var waiting_dblclick = null # null or [pos, event] + +func input(_viewport, event, _shape_idx): + if event is InputEventMouseButton and event.pressed: + # If we are hovering items, do not allow background to receive a click + # and let the items sort out who's on top and gets to be `clicked` + if vm.hover_stack: + return + + if event.is_action("game_general"): + last_lmb_dt = 0 + waiting_dblclick = [get_global_mouse_position(), event] + elif event.is_action("game_rmb"): + emit_signal("right_click_on_bg", self, get_global_mouse_position(), event) + +func get_action(): + return action + +func _physics_process(dt): + last_lmb_dt += dt + + if waiting_dblclick and last_lmb_dt > vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_click_on_bg", self, waiting_dblclick[0], waiting_dblclick[1]) + last_lmb_dt = 0 + waiting_dblclick = null + +func _enter_tree(): + # Use size of background texture to calculate collision shape + var size = get_texture().get_size() + + area = Area2D.new() + var shape = RectangleShape2D.new() + + var sid = area.create_shape_owner(area) + + # Move origin of Area2D to center of Sprite + var transform = area.shape_owner_get_transform(sid) + transform.origin = size / 2 + area.shape_owner_set_transform(sid, transform) + + # Set extents of RectangleShape2D to cover entire Sprite + shape.set_extents(size / 2) + area.shape_owner_add_shape(sid, shape) + + add_child(area) + +func _ready(): + var conn_err + + conn_err = area.connect("input_event", self, "input") + if conn_err: + vm.report_errors("item", ["area.input_event -> input error: " + String(conn_err)]) + + conn_err = connect("left_click_on_bg", $"/root/scene/game", "ev_left_click_on_bg") + if conn_err: + vm.report_errors("item", ["left_click_on_bg -> ev_left_click_on_bg error: " + String(conn_err)]) + + add_to_group("background") + diff --git a/device/globals/bg_music.gd b/device/globals/bg_music.gd new file mode 100644 index 0000000..eae76ce --- /dev/null +++ b/device/globals/bg_music.gd @@ -0,0 +1,41 @@ +extends Control + +var stream + +var state = "default" + +export var global_id = "bg_music" + +func game_cleared(): + set_state("off", true) + self.disconnect("tree_exited", vm, "object_exit_scene") + vm.register_object(global_id, self) + +func set_state(p_state, p_force = false): + # If already playing this stream, keep playing, unless p_force + if p_state == state and not p_force and stream.is_playing(): + return + + state = p_state + + # If state is "off"/"default", turn off music + if state == "off" or state == "default": + stream.stream = null + return + + var resource = load(p_state) + + stream.stream = resource + + if stream.stream: + resource.set_loop(true) + stream.play() + stream.volume_db = vm.settings.music_volume + +func _ready(): + stream = $"stream" + + vm.register_object(global_id, self) + + add_to_group("game") + diff --git a/device/globals/bg_music.tscn b/device/globals/bg_music.tscn new file mode 100644 index 0000000..b9ddf33 --- /dev/null +++ b/device/globals/bg_music.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://globals/bg_music.gd" type="Script" id=1] + +[node name="bg_music" type="Control" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 40.0 +margin_bottom = 40.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +global_id = "bg_music" + +[node name="stream" type="AudioStreamPlayer" parent="." index="0"] + +stream = null +volume_db = 0.0 +pitch_scale = 1.0 +autoplay = false +mix_target = 0 +bus = "Music" + + diff --git a/device/globals/bg_snd.gd b/device/globals/bg_snd.gd new file mode 100644 index 0000000..188ae3b --- /dev/null +++ b/device/globals/bg_snd.gd @@ -0,0 +1,31 @@ +extends Control + +var stream + +#warning-ignore:unused_class_variable +var state = "default" # To play along with the `set_state` semantics + +export var global_id = "bg_snd" + +func game_cleared(): + stream.stream = null + self.disconnect("tree_exited", vm, "object_exit_scene") + vm.register_object(global_id, self) + +func play_snd(p_snd, p_loop=false): + var resource = load(p_snd) + if !resource: + vm.report_errors("bg_snd", ["play_snd resource not found " + p_snd]) + return + stream.stream = resource + stream.stream.set_loop(p_loop) + stream.volume_db = vm.settings.sfx_volume + stream.play() + +func _ready(): + stream = $"stream" + + vm.register_object(global_id, self) + + add_to_group("game") + diff --git a/device/globals/bg_snd.tscn b/device/globals/bg_snd.tscn new file mode 100644 index 0000000..c08b831 --- /dev/null +++ b/device/globals/bg_snd.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://globals/bg_snd.gd" type="Script" id=1] + +[node name="bg_snd" type="Control"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 40.0 +margin_bottom = 40.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +global_id = "bg_snd" + +[node name="stream" type="AudioStreamPlayer" parent="." index="0"] + +stream = null +volume_db = 0.0 +pitch_scale = 1.0 +autoplay = false +mix_target = 0 +bus = "SFX" + + diff --git a/device/globals/camera.gd b/device/globals/camera.gd new file mode 100644 index 0000000..191b349 --- /dev/null +++ b/device/globals/camera.gd @@ -0,0 +1,167 @@ +extends Camera2D + +onready var tween = $"tween" + +var default_limits = {} # This does not change once set + +var speed = 0 +var target +var target_pos + +var zoom_time +var zoom_target + +# This is needed to adjust dialog positions and such, see dialog_instance.gd +var zoom_transform + +func set_limits(kwargs=null): + if not kwargs: + kwargs = { + "limit_left": -10000, + "limit_right": 10000, + "limit_top": -10000, + "limit_bottom": 10000, + "set_default": false, + } + print_stack() + + self.limit_left = kwargs["limit_left"] + self.limit_right = kwargs["limit_right"] + self.limit_top = kwargs["limit_top"] + self.limit_bottom = kwargs["limit_bottom"] + + if "set_default" in kwargs and kwargs["set_default"] and not default_limits: + default_limits = kwargs + +func resolve_target_pos(): + if typeof(target) == TYPE_VECTOR2: + target_pos = target + elif typeof(target) == TYPE_ARRAY: + var count = 0 + + for obj in target: + target_pos += obj.get_camera_pos() + count += 1 + + # Let the error in if an empty array was passed (divzero) + target_pos = target_pos / count + else: + target_pos = target.get_camera_pos() + + return target_pos + +func set_drag_margin_enabled(p_dm_h_enabled, p_dm_v_enabled): + self.drag_margin_h_enabled = p_dm_h_enabled + self.drag_margin_v_enabled = p_dm_v_enabled + +func set_target(p_speed, p_target): + speed = p_speed + target = p_target + + resolve_target_pos() + + if speed == 0: + self.global_position = target_pos + else: + var time = self.global_position.distance_to(target_pos) / speed + + if tween.is_active(): + var tweenstat = String(tween.tell()) + "/" + String(tween.get_runtime()) + vm.report_warnings("camera", ["Tween still active running camera_set_target: " + tweenstat]) + tween.emit_signal("tween_completed") + + tween.interpolate_property(self, "global_position", self.global_position, target_pos, time, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) + + tween.start() + +func set_camera_zoom(p_zoom_level, p_time): + if p_zoom_level <= 0.0: + vm.report_errors("camera", ["Tried to set negative or zero zoom level"]) + + zoom_time = p_time + zoom_target = Vector2(1, 1) * p_zoom_level + + if zoom_time == 0: + self.zoom = zoom_target + else: + if tween.is_active(): + var tweenstat = String(tween.tell()) + "/" + String(tween.get_runtime()) + vm.report_warnings("camera", ["Tween still active running camera_set_zoom: " + tweenstat]) + tween.emit_signal("tween_completed") + + tween.interpolate_property(self, "zoom", self.zoom, zoom_target, zoom_time, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) + + tween.start() + +func push(p_target, p_time, p_type): + var time = float(p_time) + var type = "TRANS_" + p_type + + target = p_target + + var camera_pos + var camera_pos_coords + if target.has_node("camera_pos"): + camera_pos = target.get_node("camera_pos") + camera_pos_coords = camera_pos.global_position + else: + camera_pos_coords = target.global_position + + if time == 0: + self.global_position = camera_pos_coords + + if camera_pos and camera_pos is Camera2D: + self.zoom = camera_pos.zoom + else: + if tween.is_active(): + var tweenstat = String(tween.tell()) + "/" + String(tween.get_runtime()) + vm.report_warnings("camera", ["Tween still active running camera_push: " + tweenstat]) + tween.emit_signal("tween_completed") + + if camera_pos and camera_pos is Camera2D: + tween.interpolate_property(self, "zoom", self.zoom, camera_pos.zoom, time, tween.get(type), Tween.EASE_IN_OUT) + + tween.interpolate_property(self, "global_position", self.global_position, camera_pos_coords, time, tween.get(type), Tween.EASE_IN_OUT) + + tween.start() + +func shift(p_x, p_y, p_time, p_type): + var x = int(p_x) + var y = int(p_y) + var time = float(p_time) + var type = "TRANS_" + p_type + + var new_pos = self.global_position + Vector2(x, y) + + target = new_pos + + if tween.is_active(): + var tweenstat = String(tween.tell()) + "/" + String(tween.get_runtime()) + vm.report_warnings("camera", ["Tween still active running camera_shift: " + tweenstat]) + tween.emit_signal("tween_completed") + + tween.interpolate_property(self, "global_position", self.global_position, new_pos, time, tween.get(type), Tween.EASE_IN_OUT) + + tween.start() + +func target_reached(_obj=null, _key=null): + tween.stop_all() + +func _process(_delta): + zoom_transform = self.get_canvas_transform() + + if target and not tween.is_active(): + if typeof(target) == TYPE_VECTOR2 or typeof(target) == TYPE_ARRAY: + self.global_position = resolve_target_pos() + elif "moved" in target and target.moved: + self.global_position = resolve_target_pos() + +func _ready(): + # Init some kind of target if there is none + if not target: + target = vm.get_object("player") + if not target: + target = Vector2(0, 0) + + tween.connect("tween_completed", self, "target_reached") + diff --git a/device/globals/credits.gd b/device/globals/credits.gd new file mode 100644 index 0000000..57fd483 --- /dev/null +++ b/device/globals/credits.gd @@ -0,0 +1,34 @@ +extends Control + +func close(): + main.menu_close(self) + queue_free() + +func menu_collapsed(): + close() + +func input(event): + if event.is_action("menu_request") && event.is_pressed() && !event.is_echo(): + close() + +func back_pressed(): + close() + +func _find_menu_buttons(node=self): + for c in node.get_children(): + if c is preload("res://ui/menu_button.gd") or c is preload("res://ui/menu_texturebutton.gd"): + var sighandler_name = c.name + "_pressed" + + c.connect("pressed", self, sighandler_name) + else: + _find_menu_buttons(c) + +func _ready(): + _find_menu_buttons() + + add_to_group("ui") + + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "ui", "language_changed") + + main.menu_open(self) + diff --git a/device/globals/dd_player.gd b/device/globals/dd_player.gd new file mode 100644 index 0000000..36e56a5 --- /dev/null +++ b/device/globals/dd_player.gd @@ -0,0 +1,24 @@ +extends ResourcePreloader + +func start(params, level): + var type + if params.size() < 2 || !has_resource(params[1]): + type = "default" + else: + type = params[1] + + type = type + ProjectSettings.get_setting("escoria/platform/dialog_type_suffix") + + printt("******* instancing dialog ", type) + + var type_resource = get_resource(type) + # De-instantiated in dialog_dialog.gd stop() and game_cleared() + var inst = type_resource.instance() + get_parent().add_child(inst) + + # check the type and instance it here? + inst.call_deferred("start", params, level) + +func _ready(): + add_to_group("dialog_dialog") + diff --git a/device/globals/dialog_dialog.gd b/device/globals/dialog_dialog.gd new file mode 100644 index 0000000..933faa7 --- /dev/null +++ b/device/globals/dialog_dialog.gd @@ -0,0 +1,175 @@ +extends Control + +export(bool) var use_mouse_enter_shadow = true +export(bool) var use_mouse_exit_shadow = true +export(Color) var mouse_enter_color = Color(1, 1, 1) +export(Color) var mouse_enter_shadow_color = Color(0.6, 0.4, 0) +export(Color) var mouse_exit_color = Color(1, 1, 1) +export(Color) var mouse_exit_shadow_color = Color(1, 1, 1) + +var cmd +var container +var context +var item +var animation +var ready = false +var option_selected +var timer +var timer_timeout = 0 +var timeout_option = 0 + +func selected(n): + if !ready: + return + option_selected = n + animation.play("hide") + ready = false + if timer != null: + timer.stop() + animation.set_speed(1) + +func timer_timed_out(): + selected(timeout_option) + +# called from global_vm.gd::dialog() function +func start(params, p_context): + printt("dialog start with params ", params.size()) + context = p_context + cmd = params[0] + var i = 0 + for q in cmd: + if !vm.test(q): + i+=1 + continue + var it = item.duplicate() + var but = it.get_node("button") + var label = but.get_node("label") + + var force_ids = ProjectSettings.get_setting("escoria/platform/force_text_ids") + var tag_untranslated = ProjectSettings.get_setting("escoria/debug/tag_untranslated_strings") + var text = q.params[0] + var sep = text.find(":\"") + if sep > 0: + var tid = text.substr(0, sep) + text = text.substr(sep + 2, text.length() - (sep + 2)) + + if TranslationServer.get_locale() != ProjectSettings.get_setting("escoria/platform/development_lang"): + var ptext = TranslationServer.translate(tid) + # An untranslated string comes back as it was, so we must tag it as not translated + if ptext != tid and ptext != text: + text = ptext + elif tag_untranslated: + text = "(NOT TRANSLATED)\n\n" + text + + elif force_ids: + vm.report_errors("dialog_dialog", ["Missing text_id for string '" + text + "'"]) + text = "(no id) " + text + + if label is RichTextLabel and label.bbcode_enabled: + label.bbcode_text = text + else: + label.text = text + + but.connect("pressed", self, "selected", [i]) + label.connect("mouse_entered", self, "_on_mouse_enter", [label]) + label.connect("mouse_exited", self, "_on_mouse_exit", [label]) + + var height_ratio = ProjectSettings.get_setting("escoria/platform/dialog_option_height") + var size = it.get_custom_minimum_size() + size.y = size.y * height_ratio + it.set_custom_minimum_size(size) + + container.add_child(it) + if i == 0: + but.grab_focus() + i+=1 + + _on_mouse_exit(label) + + if has_node("wrapper/avatars"): + var avatar = "default" + if params.size() >= 3: + avatar = params[2] + var avatars = get_node("wrapper/avatars") + for i in range(avatars.get_child_count()): + var c = avatars.get_child(i) + if c.get_name() == avatar: + c.show() + else: + c.hide() + + timer_timeout = 0 + timeout_option = 0 + if params.size() >= 4: + timer_timeout = float(params[3]) + if params.size() >= 5: + timeout_option = int(params[4]) + + if timer_timeout > 0: + printt("********* dialog has timeout", timer_timeout, timeout_option) + timer = Timer.new() + add_child(timer) + timer.set_one_shot(true) + timer.set_wait_time(timer_timeout) + timer.connect("timeout", self, "timer_timed_out") + #timer.start() + + ready = false + animation.play("show") + animation.seek(0, true) + +func _on_mouse_enter(label): + label.set("custom_colors/default_color", mouse_enter_color) + if use_mouse_enter_shadow: + label.set("custom_colors/font_color_shadow", mouse_enter_shadow_color) + +func _on_mouse_exit(label): + label.set("custom_colors/default_color", mouse_exit_color) + if use_mouse_exit_shadow: + label.set("custom_colors/font_color_shadow", mouse_exit_shadow_color) + +func stop(): + hide() + while container.get_child_count() > 0: + var c = container.get_child(0) + container.remove_child(c) + c.free() + vm.request_autosave() + queue_free() + +func game_cleared(): + queue_free() + +func anim_finished(anim_name): + if anim_name == "show": + ready = true + if timer_timeout > 0: + timer.start() + if animation.has_animation("timer"): + animation.set_current_animation("timer") + var length = animation.get_current_animation_length() + animation.set_speed(length / timer_timeout) + animation.play() + + if anim_name == "hide": + vm.finished(context) + var ev_level = cmd[option_selected].params[1] + var ev = vm.compiler.EscoriaEvent.new("dialog_choice_done", ev_level, []) + vm.add_level(ev, false) + stop() + +func _ready(): + printt("dialog ready") + hide() + container = get_node("wrapper/margin/scroll/container") + container.set_mouse_filter(MOUSE_FILTER_PASS) + add_to_group("dialog_dialog") + item = get_node("item") + item.get_node("button/label").set_mouse_filter(MOUSE_FILTER_PASS) + item.get_node("button").set_mouse_filter(MOUSE_FILTER_PASS) + item.set_mouse_filter(MOUSE_FILTER_PASS) + call_deferred("remove_child", item) + animation = get_node("animation") + animation.connect("animation_finished", self, "anim_finished") + add_to_group("game") + diff --git a/device/globals/dialog_instance.gd b/device/globals/dialog_instance.gd new file mode 100644 index 0000000..cadfc91 --- /dev/null +++ b/device/globals/dialog_instance.gd @@ -0,0 +1,319 @@ +extends Node2D + +var animation + +var context +var text +var elapsed = 0 +var total_time +var character +export var typewriter_text = true +export var characters_per_second = 45.0 +var force_disable_typewriter_text = ProjectSettings.get_setting("escoria/platform/force_disable_typewriter_text") +var finished = false +var play_intro = true +var play_outro = true +var label +var text_done = false +var text_timeout_seconds = ProjectSettings.get_setting("escoria/application/text_timeout_seconds") + +var speech_stream +var speech_player +var speech_suffix +var speech_paused = false + +var dialog_pos # The designated position +var pos # The transformed position +var prev_pos # Check this to see if `character` moved and reposition + +var dialog_min_vis = ProjectSettings.get_setting("escoria/application/dialog_minimum_visible_seconds") + +var damp_db = ProjectSettings.get_setting("escoria/application/dialog_damp_music_by_db") +onready var bg_music = $"/root/main/layers/telon/bg_music/stream" + +export var fixed_pos = false + +func _process(time): + if finished: + return + + if not fixed_pos: + dialog_pos = character.get_dialog_pos() + pos = vm.camera.zoom_transform.xform(dialog_pos) + + if prev_pos != pos: + set_position(pos) + prev_pos = pos + + if force_disable_typewriter_text or !typewriter_text: + label.set_visible_characters(label.get_total_character_count()) + text_done = true + + elapsed += time + + # For eg. a short fallback dialog, leave the text for it visible. The player + # will be able to skip it anyway. + if dialog_min_vis and elapsed < dialog_min_vis: + return + + if !text_done: + if elapsed >= total_time: + label.set_visible_characters(label.get_total_character_count()) + text_done = true + + return + else: + label.set_visible_characters(text.length() * elapsed / (total_time)) + pass + + if text_done && !vm.settings.skip_dialog: + finish() + return + + if elapsed > text_timeout_seconds and (!speech_player or !speech_player.is_playing()): + finish() + return + + if vm.settings.skip_dialog && speech_stream != null && !speech_player.is_playing() && text_done: + finish() + return + +func skipped(): + if finished: + return + if elapsed < total_time: + elapsed = total_time + else: + finish() + +func finish(): + character.set_speaking(false) + set_process(false) + finished = true + + if bg_music.is_playing(): + bg_music.volume_db += damp_db + + if animation and play_outro: + if animation.has_animation("hide"): + animation.play("hide") + elif not animation: + _queue_free() + +func _clamp(p_dialog_pos): + var width = float(ProjectSettings.get("display/window/size/width")) + var height = float(ProjectSettings.get("display/window/size/height")) + var my_size = $"anchor/text".get_size() + var center_offset = my_size.x / 2 + + var dist_from_right = width - (p_dialog_pos.x + center_offset) + var dist_from_left = p_dialog_pos.x - center_offset + var dist_from_bottom = height - (p_dialog_pos.y + my_size.y) + var dist_from_top = p_dialog_pos.y - my_size.y + + if dist_from_right < 0: + p_dialog_pos.x += dist_from_right + if dist_from_left < 0: + p_dialog_pos.x -= dist_from_left + if dist_from_bottom < 0: + p_dialog_pos.y += dist_from_bottom + if dist_from_top < 0: + p_dialog_pos.y -= dist_from_top + + return p_dialog_pos + +func init(p_params, p_context, p_intro, p_outro): + character = vm.get_object(p_params[0]) + context = p_context + text = p_params[1] + var force_ids = ProjectSettings.get_setting("escoria/platform/force_text_ids") + var tag_untranslated = ProjectSettings.get_setting("escoria/debug/tag_untranslated_strings") + var sep = text.find(":\"") + var text_id = null + if sep > 0: + text_id = text.substr(0, sep) + text = text.substr(sep + 2, text.length() - (sep + 2)) + + if TranslationServer.get_locale() != ProjectSettings.get_setting("escoria/platform/development_lang"): + var ptext = TranslationServer.translate(text_id) + # An untranslated string comes back as it was, so we must tag it as not translated + if ptext != text_id and ptext != text: + text = ptext + elif tag_untranslated: + text = "(NOT TRANSLATED)\n\n" + text + + elif force_ids: + vm.report_errors("dialog_instance", ["Missing text_id for string '" + text + "'"]) + text = "(no id) " + text + + # This BBCode may be the only way to center text for a RichTextLabel + if ProjectSettings.get_setting("escoria/platform/dialog_force_centered"): + text = "[center]" + text + "[/center]" + + play_intro = p_intro + play_outro = p_outro + total_time = text.length() / characters_per_second + if !fixed_pos: + dialog_pos = character.get_dialog_pos() + + pos = vm.camera.zoom_transform.xform(dialog_pos) + set_position(pos) + prev_pos = pos + + if has_node("anchor/avatars"): + var avatars = get_node("anchor/avatars") + if avatars.get_child_count() > 0: + var name + if p_params.size() < 4: + name = avatars.get_child(0).get_name() + else: + name = p_params[3] + for i in range(0, avatars.get_child_count()): + var c = avatars.get_child(i) + if c.get_name() == name: + c.show() + else: + c.hide() + + character.set_speaking(true) + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "set_mode", "dialog") + if animation and play_intro: + if animation.has_animation("show"): + animation.play("show") + animation.seek(0, true) + else: + show() + set_process(true) + else: + show() + set_process(true) + + if character.dialog_color: + label["custom_colors/default_color"] = character.dialog_color + + label.bbcode_enabled = true + + var parsed_ok = label.parse_bbcode(text) + + if parsed_ok != OK: + vm.report_errors("dialog_instance", ["Label failed parsing bbcode: " + text]) + + label.bbcode_text = "\n" + text + label.set_visible_characters(0) # This length is always adjusted later + + set_z_index(1) + + setup_speech(text_id) + +func setup_speech(tid): + if tid == null || tid == "": + return + + if !vm.settings.speech_enabled: + return + + var use_binaural = ProjectSettings.get_setting("escoria/application/binaural_speech") + + var speech_path = ProjectSettings.get_setting("escoria/application/speech_path") + var fname = speech_path + vm.settings.voice_lang + "/" + tid + speech_suffix + printt(" ** loading speech ", fname) + speech_stream = load(fname) + if !speech_stream: + printt("*** unable to load speech stream ", fname) + return + + speech_stream.set_loop(false) + + if use_binaural: + speech_player = AudioStreamPlayer2D.new() + else: + speech_player = AudioStreamPlayer.new() + + speech_player.set_name("speech_player_" + character.global_id if "global_id" in character else "player") + speech_player.bus = "Speech" + + if use_binaural: + character.add_child(speech_player) + else: + add_child(speech_player) + + speech_player.set_stream(speech_stream) + speech_player.volume_db = vm.settings.voice_volume * ProjectSettings.get_setting("escoria/application/max_voice_volume") + + if bg_music.is_playing(): + bg_music.volume_db -= damp_db + + speech_player.play() + + # XXX: For whatever reason the AudioStreamPlayer2D does not appear to be playing, but it actually is + if not use_binaural and not speech_player.is_playing(): + print(" *** not playing? :(") + # error? + speech_stream = null + speech_player.free() + return + + #total_time = speech_stream.get_length() * 0.8 + 1.5 + #printt("total time ", total_time, speech_stream.get_length()) + +func game_paused(p_pause): + if speech_stream == null || speech_player == null: + return + + if p_pause: + if speech_player.is_playing(): + speech_player.set_paused(true) + speech_paused = true + else: + if speech_paused: + speech_player.set_paused(false) + + +func _queue_free(): + if speech_player != null: + speech_player.free() + + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "set_mode", "default") + vm.finished(context) + queue_free() + + +#warning-ignore:unused_argument +func anim_finished(anim_name): + # TODO use the parameter here? + var cur = animation.get_current_animation() + if cur == "show": + set_process(true) + if cur == "hide": + _queue_free() + +func set_position(p_pos): + .set_position(_clamp(p_pos)) + +func _ready(): + var conn_err + + speech_suffix = ProjectSettings.get_setting("escoria/application/speech_suffix") + add_to_group("events") + if has_node("animation"): + animation = $"animation" + + conn_err = animation.connect("animation_finished", self, "anim_finished") + if conn_err: + vm.report_errors("dialog_instance", ["animation_finished -> anim_finished error: " + String(conn_err)]) + + label = get_node("anchor/text") + + # Ensure a supported speech locale has been set, or not set if no speech is desired + var speech_locales_path = ProjectSettings.get_setting("escoria/application/speech_locales_path") + if speech_locales_path: + var speech_locales_def = load(speech_locales_path).new() + + if not vm.settings.voice_lang in speech_locales_def.speech_locales: + vm.report_errors("dialog_instance", ["Settings voice_lang not in defined speech locales: " + vm.settings.voice_lang]) + + conn_err = vm.connect("paused", self, "game_paused") + if conn_err: + vm.report_errors("dialog_instance", ["paused -> game_paused error: " + String(conn_err)]) + + diff --git a/device/globals/dialog_player.gd b/device/globals/dialog_player.gd new file mode 100644 index 0000000..0e94c41 --- /dev/null +++ b/device/globals/dialog_player.gd @@ -0,0 +1,51 @@ +extends ResourcePreloader + +var types = {} + +func say(params, callback): + var character = vm.get_object(params[0]) + + var type + if params.size() < 3 || !has_resource(params[2]): + type = "default" + else: + type = params[2] + + # Zooming will also affect the type. But account for the fact floats are actual floats and hard to compare. + var zoom_diff = abs(vm.camera.zoom.x - $"/root/scene".default_zoom) + var need_zoomed = round(zoom_diff) > 0 + + # Check if we have an inventory, because it might affect dialog positioning + if (vm.inventory and vm.inventory.blocks_tooltip()) or need_zoomed: + type = "bottom" + # Same for a hidden character + elif not character.visible: + type = "bottom" + + type = type + ProjectSettings.get_setting("escoria/platform/dialog_type_suffix") + + # De-instantiated in dialog_instance.gd _queue_free() + var inst = get_resource(type).instance() + var z = inst.get_z_index() + + if (vm.inventory and vm.inventory.blocks_tooltip()) or need_zoomed: + inst.fixed_pos = true + elif not character.visible: + inst.fixed_pos = true + + $"/root/scene/game/dialog_layer".add_child(inst) + + var intro = true + var outro = true + if type in types: + intro = types[type][0] + outro = types[type][1] + inst.init(params, callback, intro, outro) + inst.set_z_index(z) + +func config(params): + types[params[0]] = [params[1], params[2]] + +func _ready(): + add_to_group("dialog") + diff --git a/device/globals/errors.gd b/device/globals/errors.gd new file mode 100644 index 0000000..9485ab8 --- /dev/null +++ b/device/globals/errors.gd @@ -0,0 +1,7 @@ + + +func dialog_confirmed(): + queue_free() + +func _ready(): + connect("confirmed", self, "dialog_confirmed") diff --git a/device/globals/esc_compile.gd b/device/globals/esc_compile.gd new file mode 100644 index 0000000..422a4e0 --- /dev/null +++ b/device/globals/esc_compile.gd @@ -0,0 +1,456 @@ +class EscoriaEvent: + var ev_name + var ev_level + var ev_flags + + func _init(p_name, p_level, p_flags): + ev_name = p_name + ev_level = p_level + ev_flags = p_flags + +var commands = { + "set_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING] }, + "dec_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] }, + "inc_global": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] }, + "set_globals": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] }, + "accept_input": { "min_args": 1, "types": [TYPE_STRING] }, + "debug": { "min_args": 1 }, + "anim": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL, TYPE_BOOL, TYPE_BOOL] }, + "play_snd": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL] }, + "set_state": { "min_args": 2 }, + "set_hud_visible": { "min_args": 1, "types": [TYPE_BOOL]}, + "say": { "min_args": 2 }, + "?": { "alias": "dialog"}, + "!": { "alias": "end_dialog", "min_args": 0 }, + "cut_scene": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL, TYPE_BOOL, TYPE_BOOL] }, + ">": { "alias": "branch"}, + "inventory_add": { "min_args": 1 }, + "inventory_remove": { "min_args": 1 }, + "inventory_open": { "min_args": 1, "types": [TYPE_BOOL] }, + "set_active": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] }, + "set_interactive": { "min_args": 2, "types": [TYPE_STRING, TYPE_BOOL] }, + "stop": true, + "repeat": true, + "wait": true, + "set_speed": { "min_args": 2, "types": [TYPE_STRING, TYPE_INT] }, + "teleport": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_INT] }, + "teleport_pos": { "min_args": 3 }, + "slide": { "min_args": 2 }, + "slide_block": { "min_args": 2 }, + "walk": { "min_args": 2 }, + "walk_block": { "min_args": 2 }, + "turn_to": { "min_args": 2 }, + "set_angle": { "min_args": 2 }, + "change_scene": { "min_args": 1, "types": [TYPE_STRING, TYPE_BOOL] }, + "spawn": { "min_args": 1 }, + "%": { "alias": "label", "min_args": 1}, + "jump": { "min_args": 1 }, + "dialog_config": { "min_args": 3, "types": [TYPE_STRING, TYPE_BOOL, TYPE_BOOL] }, + "sched_event": { "min_args": 3, "types": [TYPE_REAL, TYPE_STRING, TYPE_STRING] }, + "custom": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING] }, + "camera_set_drag_margin_enabled": { "min_args": 2, "types": [TYPE_BOOL, TYPE_BOOL] }, + "camera_set_target": { "min_args": 1, "types": [TYPE_REAL] }, + "camera_set_pos": { "min_args": 3, "types": [TYPE_REAL, TYPE_INT, TYPE_INT] }, + "camera_set_zoom": { "min_args": 1, "types": [TYPE_REAL] }, + "camera_set_zoom_height": { "min_args": 1, "types": [TYPE_INT] }, + "camera_push": { "min_args": 1, "types": [TYPE_STRING] }, + "camera_shift": { "min_args": 2, "types": [TYPE_INT, TYPE_INT] }, + "autosave": { "min_args": 0 }, + "queue_resource": { "min_args": 1, "types": [TYPE_STRING, TYPE_BOOL] }, + "queue_animation": { "min_args": 2, "types": [TYPE_STRING, TYPE_STRING, TYPE_BOOL] }, + "game_over": { "min_args": 1, "types": [TYPE_BOOL] }, +} + +func check_command(cmd, state, errors): + if !(cmd.name in commands): + errors.push_back("line "+str(state.line_count)+": command "+cmd.name+" not valid.") + return false + + var cmd_data = commands[cmd.name] + if typeof(cmd_data) == TYPE_BOOL: + return true + + if "alias" in cmd_data: + cmd.name = cmd_data.alias + + if "min_args" in cmd_data: + if cmd.params.size() < cmd_data.min_args: + errors.push_back("line "+str(state.line_count)+": command "+cmd.name+" takes "+str(cmd_data.min_args)+" parameters ("+str(cmd.params.size())+" were given).") + return false + + var ret = true + if "types" in cmd_data: + var i = 0 + for t in cmd_data.types: + if i >= cmd.params.size(): + break + if t == TYPE_BOOL: + if cmd.params[i] == "true": + cmd.params[i] = true + elif cmd.params[i] == "false": + cmd.params[i] = false + else: + errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Must be 'true' or 'false'.") + ret = false + if t == TYPE_INT: + if not cmd.params[i].is_valid_integer(): + errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Expected integer.") + cmd.params[i] = int(cmd.params[i]) + if t == TYPE_REAL: + if not cmd.params[i].is_valid_float(): + errors.push_back("line " + str(state.line_count) + ": Invalid parameter " + cmd.params[i] + " for command " + cmd.name + ". Expected float.") + cmd.params[i] = float(cmd.params[i]) + i+=1 + + return ret + +func read_line(state): + while true: + if _eof_reached(state.file): + state.line = null + return + else: + state.line = _get_line(state.file) + state.line_count += 1 + if !is_comment(state.line): + return + +func is_comment(line): + for i in range(0, line.length()): + var c = line[i] + if c == "#": + return true + if c != " " && c != "\t": + return false + return true + +func get_indent(line): + for i in range(0, line.length()): + if line[i] != " " && line[i] != "\t": + return i + +func is_event(line): + var trimmed = trim(line) + if trimmed.find(":") == 0: + return trimmed.substr(1, trimmed.length()-1) + return false + +func is_flags(tk): + var trimmed = trim(tk) + if trimmed.find("[") == 0 && trimmed.find("]") == trimmed.length()-1: + return true + return false + +func add_level(state, level, errors): + read_line(state) + while typeof(state.line) != typeof(null): + if is_event(state.line): + return + var ind_level = get_indent(state.line) + if ind_level < state.indent: + return + if ind_level > state.indent: + errors.push_back("line "+str(state.line_count)+": invalid indentation for group") + read_line(state) + continue + + read_cmd(state, level, errors) + +func add_dialog(state, level, errors): + read_line(state) + + while typeof(state.line) != typeof(null): + if is_event(state.line): + return + + var ind_level = get_indent(state.line) + + if ind_level < state.indent: + return + + if ind_level > state.indent: + errors.push_back("line "+str(state.line_count)+": invalid indentation for dialog") + read_line(state) + continue + + read_dialog_option(state, level, errors) + +func get_token(line, p_from, line_count, errors): + while p_from < line.length(): + if line[p_from] == " " || line[p_from] == "\t": + p_from += 1 + else: + break + if p_from >= line.length(): + return -1 + var tk_end + if line[p_from] == "[": + tk_end = line.find("]", p_from) + if tk_end == -1: + errors.push_back("line "+str(line_count)+": unterminated flags") + tk_end += 1 + elif line[p_from] == "\"": + tk_end = line.find("\"", p_from+1) + if tk_end == -1: + errors.push_back("line "+str(line_count)+": unterminated quotes, line '"+line+"'") + else: + tk_end = p_from + while tk_end < line.length(): + if line[tk_end] == ":": + var ntk = get_token(line, tk_end+1, line_count, errors) + tk_end = ntk + break + if line[tk_end] == " " || line[tk_end] == "\t": + break + tk_end += 1 + return tk_end + +func trim(p_str): + while p_str.length() && (p_str[0] == " " || p_str[0] == "\t"): + p_str = p_str.substr(1, p_str.length()-1) + while p_str.length() && p_str[p_str.length()-1] == " " || p_str[p_str.length()-1] == "\t": + p_str = p_str.substr(0, p_str.length()-1) + + if p_str[0] == "\"": + p_str = p_str.substr(1, p_str.length()-1) + if p_str[p_str.length()-1] == "\"": + p_str = p_str.substr(0, p_str.length()-1) + return p_str + +func parse_flags(p_flags, flags_list, ifs): + var from = 1 + while true: + var next = p_flags.find(",", from) + var flag + if next == -1: + flag = p_flags.substr(from, (p_flags.length()-1) - from) + else: + flag = p_flags.substr(from, next - from) + flag = trim(flag) + var list = [] + + if flag[0] == "!": + list.push_back(true) + flag = trim(flag.substr(1, flag.length()-1)) + if flag.find("inv-") == 0: + ifs["if_not_inv"].push_back(trim(flag).substr(4, flag.length()-1)) + elif flag.find("a/") == 0: + ifs["if_not_active"].push_back(trim(flag).substr(2, flag.length() - 1)) + elif flag.substr(0, 3) in ["eq ", "gt ", "lt "]: + var elems = flag.split(" ", true, 2) + var comparison = "ne" if elems[0] == "eq" else "le" if elems[0] == "gt" else "ge" + ifs["if_" + comparison].push_back([elems[1], elems[2]]) + else: + ifs["if_false"].push_back(trim(flag)) + else: + list.push_back(false) + if flag.find("inv-") == 0: + ifs["if_inv"].push_back(trim(flag).substr(4, flag.length()-1)) + elif flag.find("a/") == 0: + ifs["if_active"].push_back(trim(flag).substr(2, flag.length() - 1)) + elif flag.substr(0, 3) in ["eq ", "gt ", "lt "]: + var elems = flag.split(" ", true, 2) + ifs["if_" + elems[0]].push_back([elems[1], elems[2]]) + else: + ifs["if_true"].push_back(trim(flag)) + + if flag.find(":") >= 0: + var pos = flag.substr(0, flag.find(":")) + var inv = flag.substr(0, pos) + inv = trim(inv) + list.push_back(inv) + flag = flag.substr(pos, flag.length() - pos) + elif flag.find("inv-") == 0: + flag = trim(flag).substr(4, flag.length()-1) + list.push_back("i") + else: + list.push_back("g") + + list.push_back(trim(flag)) + # printt("adding flag ", list) + flags_list.push_back(list) + if next == -1: + return + from = next+1 + +func read_dialog_option(state, level, errors): + var tk_end = get_token(state.line, 0, state.line_count, errors) + var tk = trim(state.line.substr(0, tk_end)) + if tk != "*" && tk != "-": + errors.append("line "+str(state.line_count)+": Ivalid dialog option") + read_line(state) + return + + # Remove inline comments + var comment_idx = state.line.find("#") + if comment_idx > -1: + state.line = state.line.substr(0, comment_idx) + + tk_end += 1 + # var c_start = state.line.find("\"", 0) + var c_end = state.line.find_last("\"") + var q_end = state.line.find("[", c_end) + var q_flags = null + #printt("flags before", q_flags) + if q_end == -1: + q_end = state.line.length() + else: + var f_end = state.line.find("]", q_end) + if f_end == -1: + errors.append("line "+str(state.line_count)+": unterminated flags") + else: + f_end += 1 + q_flags = state.line.substr(q_end, f_end - q_end) + var question = trim(state.line.substr(tk_end, q_end - tk_end)) + var cmd = { "name": "*", "params": [question, []] } + + if q_flags: + var ifs = { + "if_true": [], "if_false": [], "if_inv": [], "if_not_inv": [], + "if_active": [], "if_not_active": [], + "if_eq": [], "if_ne": [], # string and integer comparison + "if_gt": [], "if_ge": [], "if_lt": [], "if_le": [] # integer comparison + } + var flag_list = [] + parse_flags(q_flags, flag_list, ifs) + for key in ifs: + if ifs[key].size(): + cmd[key] = ifs[key] + if flag_list.size(): + cmd.flags = flag_list + + state.indent += 1 + add_level(state, cmd.params[1], errors) + state.indent -= 1 + + level.push_back(cmd) + +func read_cmd(state, level, errors): + var params = [] + var from = 0 + var tk_end = get_token(state.line, from, state.line_count, errors) + var ifs = { + "if_true": [], "if_false": [], "if_inv": [], "if_not_inv": [], + "if_active": [], "if_not_active": [], + "if_eq": [], "if_ne": [], # string and integer comparison + "if_gt": [], "if_ge": [], "if_lt": [], "if_le": [] # integer comparison + } + var flags = [] + while tk_end != -1: + var tk = trim(state.line.substr(from, tk_end - from)) + from = tk_end + 1 + if is_flags(tk): + parse_flags(tk, flags, ifs) + else: + params.push_back(tk) + tk_end = get_token(state.line, from, state.line_count, errors) + + if params.size() == 0: + errors.append("line "+str(state.line_count)+": Invalid command.") + read_line(state) + return + + var cmd = {"name": params[0]} + + if params[0] == ">": + cmd.params = [] + state.indent += 1 + add_level(state, cmd.params, errors) + state.indent -= 1 + elif params[0] == "?": + params.remove(0) + var dialog_params = [] + state.indent += 1 + add_dialog(state, dialog_params, errors) + cmd.params = params + cmd.params.insert(0, dialog_params) + state.indent -= 1 + elif params[0] == "*": + errors.push_back("line "+str(state.line_count)+": Invalid command: dialog option outside dialog") + read_line(state) + return + else: + params.remove(0) + + # Remove inline comments + var comment_idx = params.find("#") + if comment_idx > -1: + params.resize(comment_idx) + + cmd.params = params + read_line(state) + + for key in ifs: + if ifs[key].size(): + cmd[key] = ifs[key] + if flags.size(): + cmd.flags = flags + + var valid = check_command(cmd, state, errors) + if valid: + level.push_back(cmd) + +func read_events(f, ret, errors): + + var state = { "file": f, "line": _get_line(f), "indent": 0, "line_count": 0 } + + while typeof(state.line) != typeof(null): + if is_comment(state.line): + read_line(state) + continue + var ev = is_event(state.line) + if typeof(ev) != typeof(null): + var level = [] + var abort = add_level(state, level, errors) + var ev_flags = [] + if "|" in ev: + var ev_split = ev.split("|", true, 1) + ev = ev_split[0] + ev = ev.strip_edges() + if ev_split.size() > 1: + ev_split[1] = ev_split[1].strip_edges() + ev_flags = ev_split[1].split(" ") + + ret[ev] = EscoriaEvent.new(ev, level, Array(ev_flags)) + if abort: + return abort + +func _get_line(f): + if typeof(f) == typeof({}): + if f.line >= f.lines.size(): + return null + var line = f.lines[f.line] + f.line += 1 + #printt("reading line ", line) + return line + else: + return f.get_line() + +func _eof_reached(f): + if typeof(f) == typeof({}): + return f.line >= f.lines.size() + else: + return f.eof_reached() + +func compile_str(p_str, errors): + var f = { "line": 0, "lines": p_str.split("\n") } + + #printt("esc compile str ", f) + + var ret = {} + read_events(f, ret, errors) + + #printt("returning ", p_fname, ret) + return ret + +func compile(p_fname, errors): + var f = File.new() + f.open(p_fname, File.READ) + if !f.is_open(): + return {} + + var ret = {} + read_events(f, ret, errors) + + #printt("returning ", p_fname, ret) + return ret diff --git a/device/globals/escoria_types.gd b/device/globals/escoria_types.gd new file mode 100644 index 0000000..fde02eb --- /dev/null +++ b/device/globals/escoria_types.gd @@ -0,0 +1,15 @@ +extends Node + +const ACTION_MENU = preload("res://globals/action_menu.gd") +const INTERACTIVE = preload("res://globals/interactive.gd") +const INVENTORY = preload("res://globals/inventory.gd") +var ITEM = load("res://globals/item.gd") +const NPC = preload("res://globals/npc.gd") +const TRIGGER = preload("res://globals/trigger.gd") +var EXIT = load("res://globals/exit.gd") +const BACKGROUND = preload("res://globals/background.gd") +const PLAYER = preload("res://globals/player.gd") +const SCENE = preload("res://globals/scene.gd") +const TARGET = preload("res://globals/target.gd") +const TOOLTIP = preload("res://globals/tooltip.gd") + diff --git a/device/globals/exit.gd b/device/globals/exit.gd new file mode 100644 index 0000000..9b14f11 --- /dev/null +++ b/device/globals/exit.gd @@ -0,0 +1,16 @@ +extends "res://globals/trigger.gd" + +func body_entered(body): + # Entering an exit node runs the `:exit_scene` event + if body is esc_type.PLAYER: + if self.visible: + run_event("exit_scene") + +func body_exited(_body): + # Do nothing when exiting an exit node. Usually this code should not be hit. + return + +func _ready(): + if not "exit_scene" in event_table: + vm.report_errors("exit", [":exit_scene missing for " + self.name]) + diff --git a/device/globals/game.gd b/device/globals/game.gd new file mode 100644 index 0000000..fcf01be --- /dev/null +++ b/device/globals/game.gd @@ -0,0 +1,790 @@ +extends Node + +var player +var mode = "default" +export(String,FILE) var fallbacks_path = "" +export var inventory_enabled = true setget set_inventory_enabled +var fallbacks +var check_joystick = false +var joystick_mode = false +var min_interact_dist = 50*50 +var last_obj = null + +export var drag_margin_left = 0.2 +export var drag_margin_top = 0.2 +export var drag_margin_right = 0.2 +export var drag_margin_bottom = 0.2 + +var click +var click_anim + +var default_obj_action +var obj_action_req_dblc + +var camera +export var camera_limits = Rect2() + +var hud +var action_menu + +func set_mode(p_mode): + mode = p_mode + +func can_click(): + # Check certain global state to see if an object could be clicked + + if !vm.can_interact(): + return false + + if !player: + return false + + if mode != "default": + return false + + if vm.accept_input != vm.acceptable_inputs.INPUT_ALL: + return false + + return true + +func ev_left_click_on_bg(obj, pos, event): + printt(obj.name, "left-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu: + vm.action_menu.stop() + + # If it's possible to click outside the inventory, don't walk but only close it + if vm.inventory and vm.inventory.blocks_tooltip(): + vm.inventory.close() + return + + var walk_context = {"fast": event.doubleclick} + + # Make it possible to abort an interaction before the player interacts + if player.interact_status == player.interact_statuses.INTERACT_WALKING: + # by overriding the interaction status + player.interact_status = player.interact_statuses.INTERACT_NONE + player.params_queue = null + player.walk_to(pos, walk_context) + return + + if click: + click.set_position(pos) + if click_anim: + click_anim.play("click") + + player.walk_to(pos, walk_context) + + if vm.tooltip: + if not vm.current_tool: + vm.tooltip.hide() + +func ev_left_click_on_trigger(obj, pos, event): + printt(obj.name, "left-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + # Do not allow input on triggers/exits with inventory open + if vm.inventory and vm.inventory.blocks_tooltip(): + return + + if vm.action_menu and vm.action_menu.is_visible(): + vm.action_menu.stop() + + player.walk_to(pos) + +func ev_left_dblclick_on_trigger(obj, pos, event): + printt(obj.name, "left-dblclicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu and vm.action_menu.is_visible(): + vm.action_menu.stop() + return + + if obj.dblclick_teleport and obj.tooltip: + player.set_position(pos) + else: + player.walk_to(pos, {"fast": true}) + +func ev_left_click_on_item(obj, pos, event): + printt(obj.name, "left-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu and obj.use_action_menu: + if !ProjectSettings.get_setting("escoria/ui/right_mouse_button_action_menu"): + spawn_action_menu(obj) + return + elif vm.action_menu.is_visible(): + # XXX: Can't close action menu here or doubleclick would cause an action + if obj == vm.hover_object: + return + else: + vm.action_menu.stop() + + if vm.inventory and vm.inventory.blocks_tooltip(): + vm.inventory.close() + if vm.tooltip: + vm.tooltip.update() + return + + # Many overlapping items can receive the click event, + # but only the topmost is really `clicked` + if not obj.clicked: + return + + var walk_context = {"fast": event.doubleclick} + + # Make it possible to abort an interaction before the player interacts + if player.interact_status == player.interact_statuses.INTERACT_WALKING: + # by overriding the interaction status + player.interact_status = player.interact_statuses.INTERACT_NONE + player.params_queue = null + player.walk_to(pos, walk_context) + return + + var obj_action = obj.get_action() + var action = "walk" + + # Start off by checking a non-doubleclick default action + if not obj_action_req_dblc: + if obj_action: + action = obj_action + elif default_obj_action: + action = default_obj_action + + if click: + click.set_position(pos) + if click_anim: + click_anim.play("click") + + if obj.has_method("get_interact_pos"): + pos = obj.get_interact_pos() + + player.walk_to(pos, walk_context) + + # XXX: Interacting with current_tool etc should be a signal + if action != "walk" or vm.current_action: + if vm.inventory and not vm.inventory.blocks_tooltip(): + player.interact([obj, vm.current_action, vm.current_tool]) + +func ev_left_dblclick_on_item(obj, pos, event): + printt(obj.name, "left-dblclicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu and vm.action_menu.is_visible(): + vm.action_menu.stop() + return + + var walk_context = {"fast": event.doubleclick} + + # Make it possible to abort an interaction before the player interacts + if player.interact_status == player.interact_statuses.INTERACT_WALKING: + # by overriding the interaction status + player.interact_status = player.interact_statuses.INTERACT_NONE + player.params_queue = null + player.walk_to(pos, walk_context) + + var action = "walk" + + # Cannot use the object action if an action and tool is selected, + # because the event is probably not defined + var obj_action + if vm.current_action and vm.current_tool: + obj_action = vm.current_action + else: + obj_action = obj.get_action() + + # See if there's a doubleclick default action + if obj_action_req_dblc: + if obj_action: + action = obj_action + elif default_obj_action: + action = default_obj_action + + if click: + click.set_position(pos) + if click_anim: + click_anim.play("click") + + if action != "walk": + # Resolve telekinesis + if action in obj.event_table: + if player.task == "walk": + player.walk_stop(player.get_position()) + vm.clear_current_action() + + player.interact([obj, action, vm.current_tool]) + else: + player.walk_to(pos, walk_context) + +func ev_right_click_on_item(obj, pos, event): + printt(obj.name, "right-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + var inventory_open = vm.inventory and vm.inventory.blocks_tooltip() + + if obj.use_action_menu and not inventory_open: + if ProjectSettings.get_setting("escoria/ui/right_mouse_button_action_menu"): + spawn_action_menu(obj) + return + elif inventory_open: + vm.inventory.close() + +func ev_left_click_on_inventory_item(obj, pos, event): + printt(obj.name, "left-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + # Use and look are the only valid choices with an action menu + if vm.action_menu: + vm.set_current_action("use") + + if vm.current_action == "use" and obj.use_combine: + if not vm.current_tool: + vm.set_current_tool(obj) + elif vm.current_tool != obj: + interact([obj, vm.current_action, vm.current_tool]) + + if vm.tooltip: + vm.tooltip.update() + +func ev_right_click_on_inventory_item(obj, pos, event): + printt(obj.name, "right-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.current_tool: + vm.clear_current_tool() + if vm.tooltip: + vm.tooltip.update() + else: + # Do not set `look` as permanent action + interact([obj, "look"]) + # XXX: Moving the mouse during `:look` will cause the tooltip to disappear + # so the following is a good-enough-for-now fix for it + if vm.tooltip: + vm.tooltip.update() + +func ev_mouse_enter_item(obj): + if obj.inventory: + vm.report_errors("game", ["Mouse entered inventory non-inventory item: " + obj.global_id]) + + printt(obj.name, "mouse_enter_item") + + vm.hover_push(obj) + + if vm.tooltip: + # XXX: The warning report may be removed if it turns out to be too annoying in practice + if not obj.get_tooltip(): + # For a passive item, it's fine to set an empty tooltip, but if we have a passive and + # an active esc_type.ITEM overlapping, say a window and a light that will move later, + # the tooltip may be emptied by the light not having a tooltip. This is because the + # `mouse_enter` events have no guaranteed order. + vm.report_warnings("game", ["No tooltip for item " + obj.name]) + +func ev_mouse_enter_inventory_item(obj): + if not vm.inventory: + vm.report_errors("game", ["Mouse entered inventory item without inventory: " + obj.global_id]) + if not obj.inventory: + vm.report_errors("game", ["Mouse entered non-inventory inventory item: " + obj.global_id]) + + printt(obj.name, "mouse_enter_inventory_item") + + if vm.tooltip: + # XXX: The warning report may be removed if it turns out to be too annoying in practice + if not obj.get_tooltip(): + # For a passive item, it's fine to set an empty tooltip, but if we have a passive and + # an active esc_type.ITEM overlapping, say a window and a light that will move later, + # the tooltip may be emptied by the light not having a tooltip. This is because the + # `mouse_enter` events have no guaranteed order. + vm.report_warnings("game", ["No tooltip for item " + obj.name]) + + vm.hover_push(obj) + +func ev_mouse_exit_item(obj): + printt(obj.name, "mouse_exit_item") + + vm.hover_pop(obj) + +func ev_mouse_exit_inventory_item(obj): + printt(obj.name, "mouse_exit_inventory_item") + + vm.hover_pop(obj) + +func ev_mouse_enter_trigger(obj): + printt(obj.name, "mouse_enter_trigger") + + vm.hover_push(obj) + + if vm.tooltip: + # XXX: The warning report may be removed if it turns out to be too annoying in practice + if not obj.get_tooltip(): + # For a passive item, it's fine to set an empty tooltip, but if we have a passive and + # an active esc_type.ITEM overlapping, say a window and a light that will move later, + # the tooltip may be emptied by the light not having a tooltip. This is because the + # `mouse_enter` events have no guaranteed order. + vm.report_warnings("game", ["No tooltip for trigger " + obj.name]) + +func ev_mouse_exit_trigger(obj): + printt(obj.name, "mouse_exit_trigger") + + vm.hover_pop(obj) + +func ev_mouse_enter_npc(obj): + printt(obj.name, "mouse_enter_npc") + + vm.hover_push(obj) + + if vm.tooltip: + # XXX: The warning report may be removed if it turns out to be too annoying in practice + if not obj.get_tooltip(): + vm.report_warnings("game", ["No tooltip for npc " + obj.name]) + +func ev_left_click_on_npc(obj, pos, event): + printt(obj.name, "left-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu and obj.use_action_menu: + if !ProjectSettings.get_setting("escoria/ui/right_mouse_button_action_menu"): + spawn_action_menu(obj) + return + elif vm.action_menu.is_visible(): + # XXX: Can't close action menu here or doubleclick would cause an action + if obj == vm.hover_object: + return + else: + vm.action_menu.stop() + + if vm.inventory and vm.inventory.blocks_tooltip(): + vm.inventory.close() + if vm.tooltip: + vm.tooltip.update() + return + + # Many overlapping items can receive the click event, + # but only the topmost is really `clicked` + if not obj.clicked: + return + + var walk_context = {"fast": event.doubleclick} + + # Make it possible to abort an interaction before the player interacts + if player.interact_status == player.interact_statuses.INTERACT_WALKING: + # by overriding the interaction status + player.interact_status = player.interact_statuses.INTERACT_NONE + player.params_queue = null + player.walk_to(pos, walk_context) + return + + var obj_action = obj.get_action() + var action = "walk" + + # Start off by checking a non-doubleclick default action + if not obj_action_req_dblc: + if obj_action: + action = obj_action + elif default_obj_action: + action = default_obj_action + + if click: + click.set_position(pos) + if click_anim: + click_anim.play("click") + + if obj.has_method("get_interact_pos"): + pos = obj.get_interact_pos() + + player.walk_to(pos, walk_context) + + # XXX: Interacting with current_tool etc should be a signal + if action != "walk" or vm.current_action: + if vm.inventory and not vm.inventory.blocks_tooltip(): + player.interact([obj, vm.current_action, vm.current_tool]) + +func ev_left_dblclick_on_npc(obj, pos, event): + printt(obj.name, "left-dblclicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + if vm.action_menu and vm.action_menu.is_visible(): + vm.action_menu.stop() + return + + var walk_context = {"fast": event.doubleclick} + + # Make it possible to abort an interaction before the player interacts + if player.interact_status == player.interact_statuses.INTERACT_WALKING: + # by overriding the interaction status + player.interact_status = player.interact_statuses.INTERACT_NONE + player.params_queue = null + player.walk_to(pos, walk_context) + + var action = "walk" + + # Cannot use the object action if an action and tool is selected, + # because the event is probably not defined + var obj_action + if vm.current_action and vm.current_tool: + obj_action = vm.current_action + else: + obj_action = obj.get_action() + + # See if there's a doubleclick default action + if obj_action_req_dblc: + if obj_action: + action = obj_action + elif default_obj_action: + action = default_obj_action + + if click: + click.set_position(pos) + if click_anim: + click_anim.play("click") + + if action != "walk": + # Resolve telekinesis + if action in obj.event_table: + if player.task == "walk": + player.walk_stop(player.get_position()) + vm.clear_current_action() + + player.interact([obj, action, vm.current_tool]) + else: + player.walk_to(pos, walk_context) + +func ev_right_click_on_npc(obj, pos, event): + printt(obj.name, "right-clicked at", pos, "with", event, can_click()) + + if not can_click(): + return + + var inventory_open = vm.inventory and vm.inventory.blocks_tooltip() + + if obj.use_action_menu and not inventory_open: + if ProjectSettings.get_setting("escoria/ui/right_mouse_button_action_menu"): + spawn_action_menu(obj) + return + elif inventory_open: + vm.inventory.close() + +func ev_mouse_exit_npc(obj): + printt(obj.name, "mouse_exit_npc") + + vm.hover_pop(obj) + +func spawn_action_menu(obj): + if vm.action_menu == null: + return + + if vm.tooltip and (vm.current_tool or vm.current_action): + vm.tooltip.hide() + vm.clear_current_tool() + vm.clear_current_action() + + if player: + player.walk_stop(player.position) + + var pos = get_viewport().get_mouse_position() + vm.action_menu.set_position(pos) + vm.action_menu.show() + vm.action_menu.start(obj) + +func action_menu_selected(obj, action): + if action == "use" && obj.get_action() != "": + action = obj.get_action() + if player != null: + player.interact([obj, action]) + else: + interact([obj, action]) + vm.action_menu.stop() + +func interact(p_params): + if mode == "default": + var obj = p_params[0] + vm.clear_action() + var action = p_params[1] + if !action: + action = obj.get_action() + + if p_params.size() > 2: + vm.clear_action() + if obj == p_params[2]: + return + #inventory.close() + activate(obj, action, p_params[2]) + else: + #inventory.close() + activate(obj, action) + vm.clear_action() + return + +func activate(obj, action, param = null): + if !vm.can_interact(): + #printt("******************** vm can't interact during activate") + #print_stack() + return + + if !obj.activate(action, param): + if param != null: # try opposite way + if param.activate(action, obj): + return + fallback(obj, action, param) + +func fallback(obj, action, param = null): + if fallbacks == null: + return + var comb = action + if typeof(param) != typeof(null): + comb = action + " " + param.global_id + if comb in fallbacks: + vm.run_event(fallbacks[comb]) + return + if action in fallbacks: + if player: + # Resolve the angle to look toward and call `walk_stop` to make magic happen + player.resolve_angle_to(obj) + player.walk_stop(player.position) + + vm.run_event(fallbacks[action]) + return + vm.report_errors(fallbacks_path, ["Invalid action " + comb + " in fallbacks."]) + +func scene_input(event): + if event.is_action("quick_save") && event.is_pressed() && !event.is_echo(): + vm.request_autosave() + if event is InputEventJoypadMotion: + if event.axis == 0 || event.axis == 1: + joystick_mode = true + check_joystick = true + set_process(true) + + if event.is_action("menu_request") && event.is_pressed() && !event.is_echo(): + handle_menu_request() + +func handle_menu_request(): + var menu_enabled = vm.menu_enabled() + if vm.can_save() and vm.can_interact() and menu_enabled: + # Do not display overlay menu with action menu or inventory, it looks silly and weird + if vm.action_menu: + if vm.action_menu.is_visible(): + vm.action_menu.stop() + + # Forcibly close inventory without animation if collapsible and visible + if vm.inventory and vm.inventory.blocks_tooltip(): + vm.inventory.force_close() + + # Clear hover/tooltip state to be rebuilt when menu is closed + vm.hover_teardown() + + # Finally show the menu + main.load_menu(ProjectSettings.get_setting("escoria/ui/in_game_menu")) + elif not menu_enabled: + get_tree().call_group("game", "ui_blocked") + +func menu_closed(): + printt("menu_closed") + ## We may be hovering a HUD button which is over an item; + # make it look like it would in gameplay + if hud: + # These are set up in hud.gd only if the corresponding button is available + if hud.menu_toggle_rect: + if hud.menu_toggle_rect.has_point($"/root".get_mouse_position()): + hud.hud_button_entered() + return + + if hud.inv_toggle_rect: + if hud.inv_toggle_rect.has_point($"/root".get_mouse_position()): + hud.hud_button_entered() + return + + ## Otherwise rebuild hover stack + vm.hover_rebuild() + +func _process(_delta): + if !vm.can_interact(): + check_joystick = false + return + + if player == null || player == self: + return + if joystick_mode: + var objs = vm.get_registered_objects() + var mobj = null + var mdist + var pos = player.get_position() + for key in objs: + if key == "player": + continue + if !objs[key].is_type("Control"): + continue + if !objs[key].is_visible(): + continue + if objs[key].tooltip == "": + continue + var objpos = objs[key].get_interact_pos() + var odist = pos.distance_squared_to(objpos) + if typeof(mobj) == typeof(null): + mobj = objs[key] + mdist = odist + else: + if odist < mdist: + mdist = odist + mobj = objs[key] + if typeof(mobj) != typeof(null): + if mdist < min_interact_dist: + if mobj != last_obj: + spawn_action_menu(mobj) +# mouse_enter(mobj) + last_obj = mobj + else: +# mouse_exit(mobj) + last_obj = null + + if !check_joystick: + return + + var dir = Vector2(Input.get_joy_axis(0, 0), Input.get_joy_axis(0, 1)); + if dir.length_squared() < 0.1: + check_joystick = false + return + + player.walk_to(player.get_position() + dir * 20) + +func set_inventory_enabled(p_enabled): + inventory_enabled = p_enabled + if !has_node("hud_layer/hud/inv_toggle"): + return + if inventory_enabled: + $"hud_layer/hud/inv_toggle".show() + else: + $"hud_layer/hud/inv_toggle".hide() + +func set_camera_limits(): + var limits = {} + if camera_limits.size.x == 0 and camera_limits.size.y == 0: + var area = Rect2() + for child in get_parent().get_children(): + if child is esc_type.BACKGROUND: + var pos = child.get_global_position() + var size = child.get_texture().get_size() + if child.global_scale.x != 1 or child.global_scale.y != 1: + size.x *= child.global_scale.x + size.y *= child.global_scale.y + + area = area.expand(pos) + area = area.expand(pos + size) + + # if the background is smaller than the viewport, we want the camera to stick centered on the background + if area.size.x == 0 or area.size.y == 0 or area.size < get_viewport().size: + printt("No limit area! Using viewport") + area.size = get_viewport().size + + printt("setting camera limits from scene ", area) + limits = { + "limit_left": area.position.x, + "limit_right": area.position.x + area.size.x, + "limit_top": area.position.y, + "limit_bottom": area.position.y + area.size.y, + "set_default": true, + } + else: + limits = { + "limit_left": camera_limits.position.x, + "limit_right": camera_limits.position.x + camera_limits.size.x, + "limit_top": camera_limits.position.y, + "limit_bottom": camera_limits.position.y + camera_limits.size.y + main.screen_ofs.y * 2, + "set_default": true, + } + printt("setting camera limits from parameter ", camera_limits) + + camera.set_limits(limits) + camera.set_offset(main.screen_ofs * 2) + + #vm.update_camera(0.000000001) + +func load_hud(): + var hres = vm.res_cache.get_resource(vm.get_hud_scene()) + $"hud_layer/hud".replace_by_instance(hres) + + hud = $"hud_layer/hud" + + # Add inventory to hud layer, usually hud_minimal.tscn, if found in project settings + # and not present in the `game` scene's hud. + if inventory_enabled: + if hud.has_node("inventory"): + var inventory = hud.get_node("inventory") + vm.register_inventory(inventory) + printt("Found inventory in hud", hud) + else: + var inventory_scene = ProjectSettings.get_setting("escoria/ui/inventory") + if inventory_scene: + var inventory = load(inventory_scene).instance() + if inventory and inventory is esc_type.INVENTORY: + vm.register_inventory(inventory) + if vm.inventory.is_collapsible: + vm.inventory.hide() + hud.add_child(vm.inventory) + hud.move_child(vm.inventory, 0) + printt("Added inventory to hud", hud) + + if has_node("hud_layer/hud/inv_toggle"): + if inventory_enabled: + $"hud_layer/hud/inv_toggle".show() + else: + $"hud_layer/hud/inv_toggle".hide() + + # Add action menu to hud layer if found in project settings + if ProjectSettings.get_setting("escoria/ui/action_menu"): + action_menu = load(ProjectSettings.get_setting("escoria/ui/action_menu")).instance() + if action_menu: + $"hud_layer".add_child(action_menu) + +func _ready(): + add_to_group("game") + if has_node("../player"): + player = $"../player" + + if fallbacks_path != "": + fallbacks = vm.compile(fallbacks_path) + + if has_node("click"): + click = get_node("click") + if has_node("click_anim"): + click_anim = get_node("click_anim") + + default_obj_action = ProjectSettings.get_setting("escoria/platform/default_object_action") + obj_action_req_dblc = ProjectSettings.get_setting("escoria/platform/object_action_requires_doubleclick") + + camera = get_node("camera") + + camera.drag_margin_left = drag_margin_left + camera.drag_margin_top = drag_margin_top + camera.drag_margin_right = drag_margin_right + camera.drag_margin_bottom = drag_margin_bottom + + vm.set_camera(camera) + + call_deferred("set_camera_limits") + call_deferred("load_hud") + +func _exit_tree(): + if action_menu: + action_menu.free() + diff --git a/device/globals/game.tscn b/device/globals/game.tscn new file mode 100644 index 0000000..67ae7f8 --- /dev/null +++ b/device/globals/game.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://globals/game.gd" type="Script" id=1] +[ext_resource path="res://ui/dialog_player.tscn" type="PackedScene" id=2] +[ext_resource path="res://globals/camera.gd" type="Script" id=3] + +[node name="game" type="Node"] +script = ExtResource( 1 ) +fallbacks_path = "res://fallbacks.esc" +inventory_enabled = false + +[node name="dialog_layer" type="CanvasLayer" parent="."] +layer = 128 + +[node name="dialog_player" parent="dialog_layer" instance=ExtResource( 2 )] + +[node name="hud_layer" type="CanvasLayer" parent="."] +layer = 126 + +[node name="hud" parent="hud_layer" instance_placeholder="res://ui/hud.tscn"] +visible = false + +[node name="wait_timer" type="Timer" parent="."] + +[node name="camera" type="Camera2D" parent="."] +current = true +script = ExtResource( 3 ) + +[node name="tween" type="Tween" parent="camera"] diff --git a/device/globals/game_am.tscn b/device/globals/game_am.tscn new file mode 100644 index 0000000..27fed21 --- /dev/null +++ b/device/globals/game_am.tscn @@ -0,0 +1,80 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://globals/game.gd" type="Script" id=1] +[ext_resource path="res://ui/dialog_player.tscn" type="PackedScene" id=2] +[ext_resource path="res://globals/camera.gd" type="Script" id=3] + +[node name="game" type="Node" index="0"] + +script = ExtResource( 1 ) +fallbacks_path = "res://fallbacks.esc" +inventory_enabled = true +drag_margin_left = 0.2 +drag_margin_top = 0.2 +drag_margin_right = 0.2 +drag_margin_bottom = 0.2 +camera_limits = Rect2( 0, 0, 0, 0 ) + +[node name="dialog_layer" type="CanvasLayer" parent="." index="0"] + +layer = 128 +offset = Vector2( 0, 0 ) +rotation = 0.0 +scale = Vector2( 1, 1 ) +transform = Transform2D( 1, 0, 0, 1, 0, 0 ) + +[node name="dialog_player" parent="dialog_layer" index="0" instance=ExtResource( 2 )] + +[node name="hud_layer" type="CanvasLayer" parent="." index="1"] + +layer = 126 +offset = Vector2( 0, 0 ) +rotation = 0.0 +scale = Vector2( 1, 1 ) +transform = Transform2D( 1, 0, 0, 1, 0, 0 ) + +[node name="hud" parent="hud_layer" index="0" instance_placeholder="res://ui/hud_minimal.tscn"] + +[node name="wait_timer" type="Timer" parent="." index="2"] + +process_mode = 1 +wait_time = 1.0 +one_shot = false +autostart = false + +[node name="camera" type="Camera2D" parent="." index="3"] + +anchor_mode = 1 +rotating = false +current = true +zoom = Vector2( 1, 1 ) +limit_left = -10000000 +limit_top = -10000000 +limit_right = 10000000 +limit_bottom = 10000000 +limit_smoothed = false +drag_margin_h_enabled = true +drag_margin_v_enabled = true +smoothing_enabled = false +smoothing_speed = 5.0 +offset_v = 0.0 +offset_h = 0.0 +drag_margin_left = 0.2 +drag_margin_top = 0.2 +drag_margin_right = 0.2 +drag_margin_bottom = 0.2 +editor_draw_screen = true +editor_draw_limits = false +editor_draw_drag_margin = false +script = ExtResource( 3 ) + +[node name="tween" type="Tween" parent="camera" index="0"] + +repeat = false +playback_process_mode = 1 +playback_speed = 1.0 +playback/active = false +playback/repeat = false +playback/speed = 1.0 + + diff --git a/device/globals/global_vm.gd b/device/globals/global_vm.gd new file mode 100644 index 0000000..d778bed --- /dev/null +++ b/device/globals/global_vm.gd @@ -0,0 +1,1305 @@ +extends Node + +class QueuedEvent: + var qe_time + var qe_objname + var qe_event + + func _init(p_time, p_objname, p_event): + qe_time = p_time + qe_objname = p_objname + qe_event = p_event + +var running_event + +var stack = [] +var globals = {} +var objects = {} +var customs = [] + +var event_queue = [] + +enum acceptable_inputs {INPUT_NONE, INPUT_ALL, INPUT_SKIP} +var accept_input = acceptable_inputs.INPUT_ALL + +var state_return = 0 +var state_yield = 1 +var state_break = 2 +#warning-ignore:unused_class_variable +var state_repeat = 3 # vm_level.gd +var state_call = 4 +#warning-ignore:unused_class_variable +var state_jump = 5 # vm_level.gd + +var states = {} +var actives = {} +var interactives = {} + +var game_size + +var compiler +var level + +var game + +var res_cache + +var cam_target = null +var camera + +## One game, one VM; there are many things we can have only one of, track them here. +var action_menu = null # If the game uses an action menu, register it here +var inventory = null # Register an inventory if used +var tooltip = null # The tooltip scene, registered by game.gd +var hover_object = null # Best-effort attempt to track what's under the mouse +var hover_stack = [] # Register mouse_enter events here based on z-index + +var current_action = "" # Verb or action menu button +var current_tool = null # Item chosen from inventory + +var last_autosave = 0 +var autosave_pending = false +const AUTOSAVE_TIME_MS = 64 * 1000 +const DOUBLECLICK_TIMEOUT = 0.2 # seconds + +var save_data + +var continue_enabled = true + +var focus_pause = false + +var loading_game = false +var achievements = null +var rate_url = "" + +# These are settings that the player can affect and save/load later +var settings_default = { + "text_lang": ProjectSettings.get_setting("escoria/application/text_lang"), + "voice_lang": ProjectSettings.get_setting("escoria/application/voice_lang"), + "speech_enabled": ProjectSettings.get_setting("escoria/application/speech_enabled"), + "music_volume": 1, + "sfx_volume": 1, + "voice_volume": 1, + "fullscreen": false, + "skip_dialog": true, + "rate_shown": false, # XXX: What is this? `achievements.gd` looks like iOS-only +} + + +var scenes_cache_list = preload("res://globals/scenes_cache.gd").scenes + +var scenes_cache = {} # this will eventually have everything in scenes_cache_list forever + +var settings + +# Helpers to deal with player's and items' angles +func _get_deg_from_rad(rad_angle): + # We need some special magic to rotate the node + var deg = rad2deg(rad_angle) + 180 + 45 + if deg >= 360: + deg = deg - 360 + + return deg + +func _get_dir(angle, animations): + var deg = _get_deg_from_rad(angle) + return _get_dir_deg(deg, animations) + +func _get_dir_deg(deg, animations): + var dir = -1 + var i = 0 + for ang in animations.dir_angles: + if deg <= ang: + dir = i + break + i+=2 + + # It's an error to have the animations misconfigured + if dir == -1: + report_errors("obj_name", ["No direction found for " + str(deg)]) + return dir + + +func save_settings(): + save_data.save_settings(settings, null) + +func load_settings(): + save_data.load_settings([self, "settings_loaded"]) + +func settings_loaded(p_settings): + printt("******* settings loaded", p_settings) + if p_settings != null: + settings = p_settings + else: + settings = {} + + for k in settings_default: + if !(k in settings): + settings[k] = settings_default[k] + + # TODO Apply globally +# AudioServer.set_fx_global_volume_scale(settings.sfx_volume) + AudioServer.set_bus_volume_db(0, settings.sfx_volume) + TranslationServer.set_locale(settings.text_lang) + music_volume_changed() + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "ui", "language_changed") + +func music_volume_changed(): + emit_signal("music_volume_changed") + +func hover_debug(s): + var ids = [] + for h in hover_stack: + ids.push_back([h.z_index, h.global_id]) + printt("HOVER DEBUG: " + s, ids) + +func hover_push(obj): + if not obj is esc_type.ITEM and not obj is esc_type.TRIGGER and not obj is esc_type.NPC: + report_errors("global_vm", ["Trying to hover " + obj.global_id + " which is not ITEM, TRIGGER or NPC"]) + + var stacked + var for_else = true + var need_new_hover = false + if not hover_stack: + hover_stack.push_back(obj) + need_new_hover = true + else: + if obj in hover_stack: + # report_warnings("global_vm", ["Hovering push obj " + obj.global_id + " in hover_stack!"]) + return + + for i in hover_stack.size(): + stacked = hover_stack[i] + if obj.z_index >= stacked.z_index: + hover_stack.insert(i, obj) + for_else = false + need_new_hover = i == 0 + break + if for_else: + hover_stack.push_back(obj) + + # hover_debug("PUSH") + if need_new_hover: + # hover_debug("PUSHED, hovering " + obj.global_id) + hover_begin(obj) + # else: + # hover_debug("KEEP HOVERING " + hover_object.global_id) + +func hover_pop(obj): + if not hover_stack: + # report_warnings("global_vm", ["Hovering trying to pop from empty stack"]) + return + + if not obj in hover_stack: + # report_warnings("global_vm", ["Hovering pop obj " + obj.global_id + " not in hover_stack!"]) + return + + var next + if hover_stack[0] == obj: + hover_stack.pop_front() + if hover_stack: + next = hover_stack[0] + else: + for i in range(1, hover_stack.size()): + if hover_stack[i] == obj: + next = hover_stack[i - 1] + hover_stack.remove(i) + break + + # hover_debug("POP") + if not hover_stack: + # printt("\tENDING ALL HOVERS", hover_object) + hover_end() + elif next and next != hover_object: + # printt("\tNEW HOVER", next, next.global_id) + hover_begin(next) + +func hover_begin(obj): +# var escoria_types = load("res://globals/escoria_types.gd") + if not obj is esc_type.ITEM and not obj is esc_type.TRIGGER and not obj is esc_type.NPC: + report_errors("global_vm", ["Trying to hover " + obj.global_id + " which is not ITEM, TRIGGER or NPC"]) + + hover_object = obj + + if tooltip: + tooltip.update() + +func hover_end(): + if not hover_object: + report_errors("global_vm", ["Hover ended without hover_object"]) + + # Without this, it's possible the event remains perpetually clicked + # when doing a drag-and-drop motion. Then the player will never move. + if "clicked" in hover_object: + hover_object.clicked = false + + hover_object = null + + if tooltip: + tooltip.update() + +func hover_clear_stack(): + hover_stack = [] + hover_object = null + +func hover_teardown(): + # printt("hover_teardown") + # Popping an object with an underlying object will start hovering that one, + # so we must be very brutish about popping everything + while hover_stack.size(): + hover_pop(hover_stack[0]) + +func hover_rebuild(): + # printt("hover_rebuild") + ## Rebuild the hover stack when eg. closing the in-game menu or exiting a hud button + + # `Control` nodes are not seen by `Physics2DDirectSpaceState` because of general suckyness + # Look at inventory first +# for item in vm.inventory.get_node("items").get_children(): +# if not item.visible: +# continue +# +# assert(item.rect) +# if item.rect.has_point($"/root".get_mouse_position()): +# vm.hover_push(item) + + # Don't care about scene hovers if we are over an inventory here + if vm.hover_stack: + return + return + + # Items and NPCs and such next + var scene = $"/root/scene" + var mouse_pos = scene.get_global_mouse_position() + var space = scene.get_world_2d().get_direct_space_state() + var intersect_points = space.intersect_point(mouse_pos) + + var objs = [] + for p in intersect_points: + var obj + if p.collider.name == "area": + obj = p.collider.get_parent() + else: + obj = p.collider + + if not obj.visible: + continue + + # If it quacks like a duck... + if "global_id" in obj and obj.global_id and "tooltip" in obj and obj.tooltip: + vm.hover_push(obj) + +func camera_set_drag_margin_enabled(p_dm_h_enabled, p_dm_v_enabled): + if not camera: + return + + camera.set_drag_margin_enabled(p_dm_h_enabled, p_dm_v_enabled) + +func camera_set_target(p_speed, p_target): + if not camera: + return + + assert(typeof(p_target) in [TYPE_VECTOR2, TYPE_ARRAY, TYPE_STRING, TYPE_NIL]) + + var target = p_target + + # change_scene will pass in TYPE_NIL, see if it's the player + if not target: + target = get_object("player") + + # So no player was found and nothing else was given, error out + if not target: + report_errors("global_vm", ["No valid camera target given: " + p_target]) + + # Vector2 goes unprocessed, but Array not so much; `camera` expects it to contain objects + if typeof(target) == TYPE_ARRAY: + for i in range(target.size()): + var n = target[i] + var obj = get_object(n) + + if not obj: + report_errors("global_vm", ["Camera target array contains invalid name " + n]) + + target[i] = obj + elif typeof(target) == TYPE_STRING: + target = get_object(target) + if not target: + report_errors("global_vm", ["Camera target not found: " + cam_target]) + else: + assert(typeof(target) == TYPE_VECTOR2) + + # Set state for savegames + cam_target = p_target + + # Kick it + camera.set_target(p_speed, target) + +func camera_set_zoom(p_zoom_level, p_time): + camera.set_camera_zoom(p_zoom_level, p_time) + +func camera_push(p_target, p_time, p_type): + var target = get_object(p_target) + + camera.push(target, p_time, p_type) + + cam_target = p_target + +func camera_shift(p_x, p_y, p_time, p_type): + camera.shift(p_x, p_y, p_time, p_type) + + cam_target = camera.target + +func inventory_has(p_obj): + return get_global("i/"+p_obj) + +func inventory_set(p_obj, p_has): + set_global("i/"+p_obj, p_has) + +func say(params, p_level): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "dialog", "say", params, p_level) + +func set_hud_visible(p_visible): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "hud", "set_visible", p_visible) + +func dialog_config(params): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "dialog", "config", params) + +func wait(params, p_level): + var time = float(params[0]) + printt("wait time ", params[0], time) + if time <= 0: + return state_return + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "wait", time, p_level) + p_level.waiting = true + return state_yield + +func is_global_equal_to(name, val): + var global = get_global(name) + if global and val and global == val: + return true + +func is_global_greater_than(name, val): + var global = get_global(name) + if global and val and int(global) > int(val): + return true + +func is_global_less_than(name, val): + var global = get_global(name) + if global and val and int(global) < int(val): + return true + +func test(cmd): + if "if_true" in cmd: + for flag in cmd.if_true: + if !get_global(flag): + return false + if "if_false" in cmd: + for flag in cmd.if_false: + if get_global(flag): + return false + if "if_inv" in cmd: + for flag in cmd.if_inv: + if !inventory_has(flag): + return false + if "if_not_inv" in cmd: + for flag in cmd.if_not_inv: + if inventory_has(flag): + return false + if "if_active" in cmd: + for flag in cmd.if_active: + if not flag in actives or not actives[flag]: + return false + if "if_not_active" in cmd: + for flag in cmd.if_not_active: + if flag in actives and actives[flag]: + return false + if "if_eq" in cmd: + for flag in cmd.if_eq: + if !is_global_equal_to(flag[0], flag[1]): + return false + if "if_ne" in cmd: + for flag in cmd.if_ne: + if is_global_equal_to(flag[0], flag[1]): + return false + if "if_gt" in cmd: + for flag in cmd.if_gt: + if !is_global_greater_than(flag[0], flag[1]): + return false + if "if_ge" in cmd: + for flag in cmd.if_ge: + if is_global_less_than(flag[0], flag[1]): + return false + if "if_lt" in cmd: + for flag in cmd.if_lt: + if !is_global_less_than(flag[0], flag[1]): + return false + if "if_le" in cmd: + for flag in cmd.if_le: + if is_global_greater_than(flag[0], flag[1]): + return false + + return true + +func dialog(params, p_level): + set_hud_visible(false) + + # Ensure tooltip is hidden. It may remain visible when the NPC finishes saying something + # and then `dialog` is called. + if tooltip: + tooltip.hide() + get_tree().call_group("dialog_dialog", "start", params, p_level) + +#warning-ignore:unused_argument +func end_dialog(params): + if not running_event or not "NO_HUD" in running_event.ev_flags: + set_hud_visible(true) + + +func instance_level(p_event, p_root): + var new_level = { + "ip": 0, + "instructions": p_event.ev_level, + "waiting": false, + "break_stop": p_root, + "labels": {}, + "flags": p_event.ev_flags + } + + for i in range(p_event.ev_level.size()): + if p_event.ev_level[i].name == "label": + var lname = p_event.ev_level[i].params[0] + new_level.labels[lname] = i + return new_level + +func compile(p_path): + + var ev_table + + if p_path.find(".gd") != -1: + var res = ResourceLoader.load(p_path) + if res == null: + return null + ev_table = res.new().get_events() + else: + var errors = [] + ev_table = compiler.compile(p_path, errors) + if errors.size() > 0: + call_deferred("report_errors", p_path, errors) + + return ev_table + +func compile_str(p_str): + var errors = [] + var ev_table = compiler.compile_str(p_str, errors) + if errors.size() > 0: + call_deferred("report_errors", "[string]", errors) + + return ev_table + +func report_warnings(p_path, warnings): + var text = "Warnings in file "+p_path+"\n\n" + for e in warnings: + text += e+"\n" + + print("warning is ", text) + + if ProjectSettings.get_setting("escoria/debug/terminate_on_warnings"): + print_stack() + assert(false) + +func report_errors(p_path, errors): + var text = "Errors in file "+p_path+"\n\n" + for e in errors: + text += e+"\n" + + print("error is ", text) + + # The only way to - optionally - make errors matter + if ProjectSettings.get_setting("escoria/debug/terminate_on_errors"): + print_stack() + assert(false) + +func add_level(p_event, p_root): + stack.push_back(instance_level(p_event, p_root)) + + return state_call + +func _find_hiding_commands(p_level): + # Recursive helper to see if we have the `say` command, + # so we can auto-hide a tooltip that follows the mouse + for command in p_level: + if command.name == "say": + return true + elif command.name == "change_scene": + return true + elif command.name == "branch": + return _find_hiding_commands(command.params) + + return false + +func _find_accept_input(p_level): + # Recursive helper to see if we have the `say` command, + # so we can set the acceptable input to INPUT_SKIP + for command in p_level: + if command.name == "say": + return acceptable_inputs.INPUT_SKIP + elif command.name == "branch": + return _find_accept_input(command.params) + + return acceptable_inputs.INPUT_ALL + +func run_event(p_event): + # If a new event is triggered while another is running, defer it by + # appending its code into the current one, as a kind of "unroll" or "unpack". + # + # `:start` is a special built-in, it should be ignored + if running_event and running_event.ev_name != "start": + # Without this, something like `:setup | CUT_BLACK` with a `teleport` + # causing `:exit` or `:enter` would never be properly finished and the screen + # would remain black. + printt("run_event, defer:", p_event.ev_name, p_event.ev_flags, accept_input) + # NOTE: The added code will be the last thing run in the current event + add_level(p_event, false) + return + + # If we're accepting input, see if we need to set SKIP or ALL + if accept_input != acceptable_inputs.INPUT_NONE: + accept_input = _find_accept_input(p_event.ev_level) + printt("run_event: ", p_event.ev_name, p_event.ev_flags, accept_input) + + # When the tooltip follows the mouse, you must use `NO_TT` to hide it + # during dialog and scene changes or it looks bad. It's easy to miss, so let's automate! + if ProjectSettings.get_setting("escoria/ui/tooltip_follows_mouse") and not "NO_TT" in p_event.ev_flags: + var need_no_tt = _find_hiding_commands(p_event.ev_level) + if need_no_tt: + p_event.ev_flags.push_back("NO_TT") + + running_event = p_event + if p_event.ev_name == "setup": + if "CUT_BLACK" in p_event.ev_flags or "LEAVE_BLACK" in p_event.ev_flags: + main.telon.cut_to_black() + + add_level(p_event, true) + else: + if "NO_TT" in p_event.ev_flags: + if tooltip: + tooltip.force_tooltip_visible(false) + + if "NO_HUD" in p_event.ev_flags: + set_hud_visible(false) + + if "NO_SAVE" in p_event.ev_flags: + set_global("save_disabled", str(true)) + + add_level(p_event, true) + +func sched_event(time, objname, event): + event_queue.push_back(QueuedEvent.new(time, objname, event)) + +func event_done(ev_name): + if ev_name != running_event.ev_name: + report_errors("global_vm", ["Done event mismatch: ", ev_name, " != ", running_event.ev_name]) + + if ev_name == "setup": + if not "LEAVE_BLACK" in running_event.ev_flags: + main.telon.cut_to_scene() + else: + if "NO_TT" in running_event.ev_flags: + # If the event was NO_TT, explicitly or because of `say`, we shouldn't keep it hidden + if tooltip and tooltip.force_hide_tooltip: + tooltip.force_tooltip_visible(true) + + # Do not restore hud if next event is also NO_HUD + # because that would cause the hud to flash between the events + # and treat other such flags similarly + if event_queue.size(): + var queued_next + for e in event_queue: + if not queued_next: + queued_next = e + elif e.qe_time < queued_next.qe_time: + queued_next = e + + if queued_next.qe_time <= 0: + var obj = get_object(queued_next.qe_objname) + var next_event = obj.event_table[queued_next.qe_event] + + if not "NO_HUD" in next_event.ev_flags: + set_hud_visible(true) + + if not "NO_SAVE" in next_event.ev_flags: + set_global("save_disabled", str(false)) + else: + if "NO_HUD" in running_event.ev_flags: + set_hud_visible(true) + + if "NO_SAVE" in running_event.ev_flags: + set_global("save_disabled", str(false)) + else: + if "NO_HUD" in running_event.ev_flags: + set_hud_visible(true) + + if "NO_SAVE" in running_event.ev_flags: + set_global("save_disabled", str(false)) + + # For example dialog will hide the tooltip, but we may want to see it again + if tooltip: + if not action_menu or not action_menu.is_visible(): + tooltip.update() + + # We can reset only INPUT_SKIP because INPUT_NONE is used mainly in :setup and :ready + # whereas INPUT_SKIP is mainly for dialog. + # TODO: If people want to use INPUT_SKIP explicitly, change this logic + if accept_input == acceptable_inputs.INPUT_SKIP: + accept_input = acceptable_inputs.INPUT_ALL + + printt("event_done: ", running_event.ev_name, running_event.ev_flags, accept_input) + + running_event = null + +func get_global(name): + # If no value or looks like boolean, return boolean for backwards compatibility + if not name in globals or globals[name].to_lower() == "false": + return false + if globals[name].to_lower() == "true": + return true + return globals[name] + +func set_global(name, val): + globals[name] = val + # printt("global changed at global_vm, emitting for ", name, val) + emit_signal("global_changed", name) + +func dec_global(name, diff): + var global = get_global(name) + global = int(global) if global else 0 + set_global(name, str(global - diff)) + +func inc_global(name, diff): + var global = get_global(name) + global = int(global) if global else 0 + set_global(name, str(global + diff)) + +func set_globals(pat, val): + for key in globals: + if key.match(pat): + globals[key] = val + emit_signal("global_changed", key) + +func set_accept_input(p_accept_input): + if p_accept_input == acceptable_inputs.INPUT_NONE || p_accept_input == acceptable_inputs.INPUT_SKIP: + vm.set_global("save_disabled", "true") + accept_input = p_accept_input + return + elif p_accept_input == acceptable_inputs.INPUT_ALL: + vm.set_global("save_disabled", "false") + accept_input = p_accept_input + return + report_errors("global_vm", ["Unknown accept_input given"]) + +func register_tooltip(p_tooltip): + if tooltip and p_tooltip != tooltip: + if not p_tooltip is esc_type.TOOLTIP: + report_errors("global_vm", ["Trying to re-register non-TOOLTIP type tooltip"]) + elif not tooltip: + if not p_tooltip is esc_type.TOOLTIP: + report_errors("global_vm", ["Trying to register non-TOOLTIP type tooltip"]) + + tooltip = p_tooltip + +func register_action_menu(p_action_menu): + if action_menu and p_action_menu != action_menu: + if not p_action_menu is esc_type.ACTION_MENU: + report_errors("global_vm", ["Trying to re-register non-ACTION_MENU type action_menu"]) + elif not action_menu: + if not p_action_menu is esc_type.ACTION_MENU: + report_errors("global_vm", ["Trying to register non-ACTION_MENU type action_menu"]) + + action_menu = p_action_menu + +func register_inventory(p_inventory): + if inventory and p_inventory != inventory: + if not p_inventory is esc_type.INVENTORY: + report_errors("global_vm", ["Trying to re-register non-INVENTORY type inventory"]) + elif not inventory: + if not p_inventory is esc_type.INVENTORY: + report_errors("global_vm", ["Trying to register non-INVENTORY type inventory"]) + + inventory = p_inventory + +func get_object(name): + if !(name in objects): + return null + return objects[name] + +func register_object(name, val, force=false): + if !name: + report_errors("register_object", ["global_id not given for " + val.get_class() + " " + val.name]) + + if name in objects and not force: + report_errors("register_object", ["Trying to register already registered object " + name + ": " + val.get_class() + " (" + val.name + ")"]) + + objects[name] = val + + if not val.is_connected("tree_exited", self, "object_exit_scene"): + val.connect("tree_exited", self, "object_exit_scene", [name]) + + # Most objects have states/animations, but don't count on it + if val.has_method("set_state"): + if name in states: + val.set_state(states[name]) + else: + val.set_state("default") + + if val.has_method("set_active"): + if name in actives: + val.set_active(actives[name]) + + if val.has_method("set_interactive"): + if name in interactives: + val.set_interactive(interactives[name]) + +func get_registered_objects(): + return objects + +func save_custom(params): + # Store `custom obj func_name foo bar` style custom data into savegames + # by passing in the params as your `custom` node's function takes them + var param_str = "" + for param in params: + param_str += (param + " ") + + customs.push_back("custom " + param_str) + +func set_state(name, state): + states[name] = state + +func set_active(name, active): + actives[name] = active + +func set_interactive(name, interactive): + interactives[name] = interactive + +func set_speed(obj, speed): + if obj is esc_type.INTERACTIVE: + obj.speed = speed + +func set_current_action(p_action): + if typeof(p_action) != TYPE_STRING: + report_errors("global_vm", ["Trying to set_current_action type: " + str(typeof(p_action))]) + + if p_action != current_action: + clear_current_tool() + + current_action = p_action + +func clear_current_action(): + set_current_action("") + +func clear_action(): + clear_current_tool() + + # It is logical for action menus' actions to be cleared, but verb menus to persist + if action_menu: + clear_current_action() + +func set_current_tool(p_tool): + if p_tool: + if not p_tool is esc_type.ITEM: + report_errors("global_vm", ["Trying to set non-item tool"]) + if not p_tool.inventory: + report_errors("global_vm", ["Trying to set non-inventory tool"]) + + current_tool = p_tool + +func clear_current_tool(): + current_tool = null + +func clear_inventory(): + inventory = null + +func clear_tooltip(): + tooltip = null + +func clear_action_menu(): + action_menu = null + +func object_exit_scene(name): + objects.erase(name) + +func check_event_queue(time): + for e in event_queue: + if e.qe_time > 0: + e.qe_time -= time + + if !can_interact() or running_event: + return + + var i = event_queue.size() + while i: + i -= 1 + var queued_next = event_queue[i] + if queued_next.qe_time <= 0: + var obj = get_object(queued_next.qe_objname) + run_event(obj.event_table[queued_next.qe_event]) + event_queue.remove(i) + break + +func _process(time): + check_event_queue(time) + run() + check_autosave() + +func run_top(): + var top = stack[stack.size()-1] + # printt("-----> TOP:", top) + var ret = level.resume(top) + if ret == state_return || ret == state_break: + stack.remove(stack.size()-1) + return ret + +func jump(p_label): + while stack.size() > 0: + var top = stack[stack.size()-1] + printt("top labels: ", top.labels, p_label) + if p_label in top.labels: + top.ip = top.labels[p_label] + return + else: + if top.break_stop || stack.size() == 1: + report_errors("", ["Label not found: "+p_label+", can't jump"]) + stack.remove(stack.size()-1) + break + else: + stack.remove(stack.size()-1) + +func run(): + if stack.size() == 0: + # Constantly run in _process: we may have an empty stack and no event + if running_event: + emit_signal("event_done", running_event.ev_name) + return + while stack.size() > 0: + var ret = run_top() + if ret == state_yield: + return + if ret == state_break: + while stack.size() > 0 && !(stack[stack.size()-1].break_stop): + stack.remove(stack.size()-1) + stack.remove(stack.size()-1) + + loading_game = false + +func can_save(): + return can_interact() && !get_global("save_disabled") + +func menu_enabled(): + printt("*** menu disabled is ", get_global("menu_disabled")) + return !get_global("menu_disabled") + +func can_interact(): + return stack.size() == 0 + +func finished(context): + if !context: + return + context.waiting = false + +func change_scene(params, context, run_events=true): + # It might be tempting to use `get_tree().change_scene(params[0])`, + # but this custom solution is safer around your scene structure + printt("change scene to ", params[0], " with run_events ", run_events) + #var res = ResourceLoader.load(params[0]) + check_cache() + main.clear_scene() + camera = null + event_queue = [] + + # Regular events need to be reset immediately, so we don't + # accidentally `yield()` on them, for performance reasons. + # This does not affect `stack` so execution is fine anyway. + if running_event and running_event.ev_name != "load": + emit_signal("event_done", running_event.ev_name) + running_event = null + + var res = res_cache.get_resource(params[0]) + res_cache.clear() + var scene = res.instance() + if scene: + main.set_scene(scene, run_events) + else: + report_errors("", ["Failed loading scene "+params[0]+" for change_scene"]) + + if context != null: + context.waiting = false + + cam_target = null + autosave_pending = true + +func spawn(params): + var res = ResourceLoader.load(params[0]) + var scene = res.instance() + if scene: + main.get_tree().add_child(scene) + else: + report_errors("", ["Failed loading scene "+params[0]+" for spawn"]) + return state_return + if params.size() > 1: + var obj = get_object(params[1]) + if obj: + scene.set_position(obj.get_global_position()); + else: + report_errors("", ["Global id "+params[1]+" not found for spawn"]) + return state_return + +func request_autosave(): + autosave_pending = true + + +func set_pause(p_pause): + get_tree().set_pause(p_pause) + emit_signal("paused", p_pause) + +func is_game_active(): + return main.get_current_scene() != null && (main.get_current_scene() is esc_type.SCENE) + +func check_autosave(): + if get_global("save_disabled") or not is_game_active(): + return + + var time = OS.get_ticks_msec() + if autosave_pending || (time - last_autosave) > AUTOSAVE_TIME_MS: + if running_event: + if running_event.ev_name == "setup": + return + elif running_event.ev_name == "load": + return + + autosave_pending = true + var data = save() + if typeof(data) == TYPE_BOOL && data == false: + return + autosave_pending = false + save_data.autosave(data, [self, "autosave_done"]) + +func autosave_done(err): + if err != OK: + return + last_autosave = OS.get_ticks_msec() + +func check_cache(): + # Warm the cache from the hard-coded list, unless configured to skip + if ProjectSettings.get_setting("escoria/platform/skip_cache"): + return + + for s in scenes_cache_list: + if s in scenes_cache: + continue + scenes_cache[s] = res_cache.get_resource(s) + +func load_file(p_game): + var f = File.new() + if !f.file_exists(p_game): + return + + game = compile(p_game) + +func run_game(): + # `load` and `ready` are exclusive because you probably don't want to + # reset the game state when a scene becomes ready, and `ready` is + # redundant when `load`ing state anyway. + # `start` is used only in your `game.esc` file to start the game. + if "load" in game: + clear() + loading_game = true + run_event(game["load"]) + main.menu_collapse() + elif "start" in game: + clear() + run_event(game["start"]) + main.menu_collapse() + elif "ready" in game: + run_event(game["ready"]) + +func load_slot(p_game): + var cb = [self, "game_str_loaded"] + save_data.load_slot(p_game, cb) + +func load_autosave(): + printt("calling load autosave") + save_data.load_autosave([self, "game_str_loaded"]) + +func game_str_loaded(p_data = null): + if p_data == null: + return + + game = compile_str(p_data) + clear() + loading_game = true + run_event(game["load"]) + main.menu_collapse() + +func save(): + if stack.size() != 0: + return false + + var ret = [] + + ret.append(":load\n\n") + + ret.append("cut_scene telon fade_out\n\n") + + # Change the scene up-front so objects and states can be loaded properly, + # and with events disabled to not confuse the game + ret.append("change_scene " + main.get_current_scene().get_filename() + " false\n\n") + + ret.append("## Global flags\n\n") + for k in globals.keys(): + if !globals[k]: + continue + ret.append("set_global %s \"%s\"\n" % [k, globals[k]]) + ret.append("\n") + + ret.append("## Objects\n\n") + var objs = {} + for k in states.keys(): + objs[k] = true + for k in actives.keys(): + objs[k] = true + for k in interactives.keys(): + objs[k] = true + for k in objs.keys(): + if k in actives: + var s = "true" + if !actives[k]: + s = "false" + ret.append("set_active " + k + " " + s + "\n") + + if k in interactives: + var s = "true" + if !interactives[k]: + s = "false" + ret.append("set_interactive " + k + " " + s + "\n") + + if k in states && states[k] != "default": + ret.append("set_state " + k + " " + states[k] + "\n") + + ret.append("\n") + + # check global states of moved objects + for k in objects: + if k == "player" || objects[k] == null: + continue + + if "moved" in objects[k] and objects[k].moved: + var pos = objects[k].get_position() + ret.append("teleport_pos " + k + " " + str(int(pos.x)) + " " + str(int(pos.y)) + "\n") + if objects[k].last_deg != null: + if objects[k].last_deg < 0 or objects[k].last_deg > 360: + report_errors("global_vm", ["Trying to save game with " + objects[k].name + " at invalid angle " + str(objects[k].last_deg)]) + ret.append("set_angle " + k + " " + str(objects[k].last_deg) + "\n") + + ret.append("\n") + ret.append("## Player\n\n") + + if main.get_current_scene().has_node("player"): + var player = main.get_current_scene().get_node("player") + var pos = player.get_global_position() + var angle = player.last_deg + ret.append("teleport_pos player " + str(pos.x) + " " + str(pos.y) + "\n") + # Angle may be unset if saving occurs when entering another room + if angle: + if angle < 0 or angle > 360: + report_errors("global_vm", ["Trying to save game with player at invalid angle " + str(angle)]) + ret.append("set_angle player " + str(angle) + "\n") + + ret.append("\n") + ret.append("## Camera\n\n") + if cam_target != null: + if typeof(cam_target) == TYPE_VECTOR2: + ret.append("camera_set_pos 0 " + str(int(cam_target.x)) + " " + str(int(cam_target.y)) + "\n") + else: + var tlist = "" + + if typeof(cam_target) == TYPE_ARRAY: + for t in cam_target: + tlist = tlist + " " + t + elif typeof(cam_target) == TYPE_STRING: + var target_obj = get_object(cam_target) + + if "global_id" in target_obj: + tlist = tlist + " " + target_obj.global_id + elif cam_target == "player": + tlist = tlist + " player" + else: + report_warnings("global_vm", ["Unknown cam_target " + str(cam_target) + ", defaulting to player"]) + tlist = tlist + " player" + elif cam_target is esc_type.PLAYER: + tlist = tlist + " player" + else: + report_errors("global_vm", ["Messed up cam_target " + str(cam_target)]) + + ret.append("camera_set_target 0" + tlist + "\n") + + if customs: + ret.append("\n") + ret.append("## Custom\n\n") + + for custom in customs: + ret.append(custom + "\n") + + ret.append("\n") + + # Always save the zoom, but assume symmetrical zoom because esc scripts don't support anything else + ret.append("camera_set_zoom " + str(camera.zoom.x) + "\n") + + for e in event_queue: + ret.append("sched_event " + str(e.qe_time) + " " + e.qe_objname + " " + e.qe_event + "\n") + + ret.append("\ncut_scene telon fade_in\n") + + emit_signal("saved") + + return ret + +func set_camera(p_cam): + camera = p_cam + register_object("_camera", p_cam) + +func clear(): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "game_cleared") + stack = [] + globals = {} + objects = {} + states = {} + actives = {} + interactives = {} + event_queue = [] + continue_enabled = true + loading_game = false + +func game_over(p_enable_continue, p_show_credits, context): + clear() + continue_enabled = p_enable_continue + change_scene(["res://globals/scene_main.tscn"], context) + if p_show_credits: + var end = true # game over, show separate end credits if available + main.get_current_scene().show_credits(end) + +func focus_out(): + #AudioServer.set_fx_global_volume_scale(0) + AudioServer.set_bus_volume_db(0, 0) + #AudioServer.set_stream_global_volume_scale(0) + focus_pause = get_tree().is_paused() + #if !focus_pause: + # set_pause(true) + +func focus_in(): + #AudioServer.set_stream_global_volume_scale(1) + AudioServer.set_bus_volume_db(0, 1) + #AudioServer.set_fx_global_volume_scale(settings.sfx_volume) + #if !focus_pause: + # set_pause(false) + +func _notification(what): + + if what == MainLoop.NOTIFICATION_WM_FOCUS_OUT: + focus_out() + elif what == MainLoop.NOTIFICATION_WM_FOCUS_IN: + focus_in() + elif what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST: + quit_request() + +func quit_request(): + #if main.menu_stack.size() > 0 && (main.menu_stack[main.menu_stack.size()-1] is preload("res://______/ui/confirmation_popup.gd")): + # return + #var ConfPopup = main.load_menu("res://_____/ui/confirmation_popup.tscn") + #ConfPopup.PopupConfirmation("KEY_QUIT_GAME",self,"","_quit_game") + pass + +func _quit_game(): + get_tree().quit() + +func check_achievement(name): + #printt("********* checking achievement ", name, loading_game) + if name.find("A/") != 0: + return + + if loading_game: + return + + var achiev = name.substr(2, name.length() - 2) + #if get_global(achiev) == false: + # return + + achievements.award(achiev) + +func show_rate(url): + rate_url = url + var ConfPopup = main.load_menu("res://ui/confirmation_popup.tscn") + ConfPopup.PopupConfirmation("rate2",self,"","_rate_game") + ConfPopup.set_buttons("rate3", "rate5") + +func _rate_game(): + var err = OS.shell_open(rate_url) + + if err != 0: + report_warnings("global_vm", ["Unhandled error while rating game"]) + +func get_hud_scene(): + var hpath = ProjectSettings.get_setting("escoria/ui/hud") + return hpath + +func _ready(): + save_data = load(ProjectSettings.get_setting("escoria/application/save_data")).new() + save_data.start() + + get_tree().set_auto_accept_quit(ProjectSettings.get('escoria/platform/force_quit')) + randomize() + add_user_signal("music_volume_changed") + add_user_signal("paused", ["p_paused"]) + load_settings() + + add_user_signal("global_changed", ["name"]) + add_user_signal("inventory_changed") + add_user_signal("open_inventory") + add_user_signal("saved") + add_user_signal("run_event", ["ev_name", "ev_data"]) + add_user_signal("event_done", ["ev_name"]) + + res_cache = preload("res://globals/resource_queue.gd").new() + printt("calling res cache start") + res_cache.start() + compiler = preload("res://globals/esc_compile.gd").new() + level = preload("res://globals/vm_level.gd").new() + game_size = get_viewport().size + + if !ProjectSettings.get_setting("escoria/platform/skip_cache"): + scenes_cache_list.push_back(ProjectSettings.get_setting("escoria/platform/telon")) + scenes_cache_list.push_back(get_hud_scene()) + + printt("cache list ", scenes_cache_list) + for s in scenes_cache_list: + print("s is ", s) + res_cache.queue_resource(s, false, true) + + printt("********** vm calling get scene", get_tree(), get_node("/root")) + + achievements = preload("res://globals/achievements.gd").new() + achievements.start() + + var conn_err + conn_err = connect("global_changed", self, "check_achievement") + if conn_err != 0: + report_errors("global_vm", ["global_changed -> check_achievement error: " + String(conn_err)]) + + conn_err = connect("run_event", self, "run_event") + if conn_err != 0: + report_errors("global_vm", ["run_event -> run_event error: " + String(conn_err)]) + + conn_err = connect("event_done", self, "event_done") + if conn_err != 0: + report_errors("global_vm", ["event_done -> event_done error: " + String(conn_err)]) + + set_process(true) + diff --git a/device/globals/hud.gd b/device/globals/hud.gd new file mode 100644 index 0000000..635167a --- /dev/null +++ b/device/globals/hud.gd @@ -0,0 +1,90 @@ +extends Control + +var background = null + +var inv_toggle_rect = null +var menu_toggle_rect = null + +func inv_toggle(): + $"inventory".toggle() + +func _on_inventory_toggle_visibility_changed(): + if $inv_toggle.is_hidden(): + $buttons.hide() + else: + $buttons.show() + +func _on_hint_pressed(): + printt("hint pressed") + if background != null: + background.emit_right_click() + +func _on_menu_pressed(): + printt("menu pressed") + get_tree().call_group("game", "handle_menu_request") + +func hud_button_entered(): + # printt("hud button mouse entered") + vm.hover_teardown() + +func hud_button_exited(): + # printt("hud button mouse exited") + vm.hover_rebuild() + +func menu_opened(): + hide() + +func menu_closed(): + show() + +func set_visible(p_visible): + visible = p_visible + +func setup_inv_toggle(): + var conn_err = $"inv_toggle".connect("pressed", self, "inv_toggle") + if conn_err: + vm.report_errors("hud", ["inv_toggle.pressed -> inv_toggle error: " + String(conn_err)]) + + conn_err = $"inv_toggle".connect("mouse_entered", self, "hud_button_entered") + if conn_err: + vm.report_errors("hud", ["inv_toggle.mouse_entered -> hud_button_entered error: " + String(conn_err)]) + + conn_err = $"inv_toggle".connect("mouse_exited", self, "hud_button_exited") + if conn_err: + vm.report_errors("hud", ["inv_toggle.mouse_exited -> hud_button_exited error: " + String(conn_err)]) + + $"inv_toggle".set_focus_mode(Control.FOCUS_NONE) + + inv_toggle_rect = Rect2($"inv_toggle".rect_global_position, $"inv_toggle".rect_size) + +func setup_menu_toggle(): + var conn_err = $"menu".connect("pressed", self, "_on_menu_pressed") + if conn_err: + vm.report_errors("hud", ["menu.pressed -> _on_menu_pressed error: " + String(conn_err)]) + + conn_err = $"menu".connect("mouse_entered", self, "hud_button_entered") + if conn_err: + vm.report_errors("hud", ["menu.mouse_entered -> hud_button_entered error: " + String(conn_err)]) + + conn_err = $"menu".connect("mouse_exited", self, "hud_button_exited") + if conn_err: + vm.report_errors("hud", ["menu.mouse_exited -> hud_button_exited error: " + String(conn_err)]) + + $"menu".set_focus_mode(Control.FOCUS_NONE) + + menu_toggle_rect = Rect2($"menu".rect_global_position, $"menu".rect_size) + +func _ready(): + add_to_group("hud") + add_to_group("game") + + if has_node("inv_toggle"): + setup_inv_toggle() + + if has_node("menu"): + setup_menu_toggle() + + # Hide verb menu if hud layer has an action menu + if has_node("../action_menu"): + $verb_menu.hide() + diff --git a/device/globals/interactive.gd b/device/globals/interactive.gd new file mode 100644 index 0000000..2676803 --- /dev/null +++ b/device/globals/interactive.gd @@ -0,0 +1,93 @@ +extends Node2D + +#warning-ignore:unused_class_variable +export var global_id = "" # API property +#warning-ignore:unused_class_variable +export(String, FILE, ".esc") var events_path = "" # API property +export var active = true setget set_active,get_active + +var event_table = {} + +# This'll contain a highlight tooltip if `tooltip_pos` is set as a child +var highlight_tooltip + +var width = float(ProjectSettings.get("display/window/size/width")) +var height = float(ProjectSettings.get("display/window/size/height")) + +func get_tooltip(): + pass + +func show_highlight(): + if not self.visible: + return + + # This is essentially is_interactive, but triggers and exit don't have separate areas + if "area" in self and self.area and not self.area.visible: + return + + if not has_node("tooltip_pos"): + return + + highlight_tooltip = vm.tooltip.duplicate() + assert(highlight_tooltip != vm.tooltip) + + var tt_pos = $tooltip_pos.global_position + var tt_text = get_tooltip() + + highlight_tooltip.highlight_only = true + highlight_tooltip.follow_mouse = false + highlight_tooltip.text = tt_text + + tt_pos = vm.camera.zoom_transform.xform(tt_pos) + + # Bail out if we're hopelessly out-of-view + if tt_pos.x < 0 or tt_pos.x > width or tt_pos.y < 0 or tt_pos.y > height: + highlight_tooltip.free() + highlight_tooltip = null + return + + vm.tooltip.get_parent().add_child(highlight_tooltip) + + highlight_tooltip.set_position(tt_pos) + + highlight_tooltip.show() + +func hide_highlight(): + if not highlight_tooltip: + return + + assert(highlight_tooltip.visible) + + highlight_tooltip.hide() + highlight_tooltip.free() + highlight_tooltip = null + +func run_event(p_ev): + vm.emit_signal("run_event", p_ev) + +func activate(p_action, p_param = null): + if p_param != null: + p_action = p_action + " " + p_param.global_id + + if p_action in event_table: + run_event(event_table[p_action]) + else: + return false + return true + +func set_active(p_active): + active = p_active + if p_active: + show() + else: + hide() + +func set_interactive(p_interactive): + self.area.visible = p_interactive + +func get_active(): + return active + +func _ready(): + add_to_group("highlight_tooltip") + diff --git a/device/globals/inventory.gd b/device/globals/inventory.gd new file mode 100644 index 0000000..30f13ea --- /dev/null +++ b/device/globals/inventory.gd @@ -0,0 +1,242 @@ +extends TextureRect + +export(bool) var is_collapsible = false + +var page = 0 + +var page_max +var page_size +var current_action = "" + +func blocks_tooltip(): + return self.is_collapsible and self.is_visible() + +func change_page(dir): + page += dir + if page < 0: + page = 0 + if page > page_max: + page = page_max + sort_items() + +func open(): + if is_visible(): + return + + if vm.action_menu: + # `false` is for show_tooltip=false + vm.action_menu.stop(false) + + sort_items() + + show() + + print("inventory open") + + if has_node("animation"): + get_node("animation").play("show") + +func show(): + .show() + + if vm.tooltip: + vm.tooltip.update() + +func close(): + if !is_visible(): + return + + if vm.tooltip: + # If we are closing while hovering ... + if vm.hover_object: + # ... an inventory item ... + if vm.hover_object is esc_type.ITEM and vm.hover_object.inventory: + # ... we must exit it to sort out the tooltip + vm.hover_object.emit_signal("mouse_exit_inventory_item", vm.hover_object) + + if has_node("animation"): + if $"animation".is_playing(): + return + $"animation".play("hide") + + # XXX: What is this `look` node? A verb menu thing? + if has_node("look"): + get_node("look").set_pressed(false) + + current_action = "" + hide() + print("inventory close") + +func hide(): + .hide() + + if vm.tooltip: + vm.tooltip.update() + +func force_close(): + if !is_visible(): + return + + if vm.tooltip: + vm.tooltip.hide() + hide() + printt("inventory force_close") + + +func toggle(): + if is_visible(): + close() + else: + open() + +func anim_finished(name): + if name == "hide": + hide() + +func sort_items(): + var items = get_node("items") + var slots = get_node("slots") + var count = 0 + var focus = false + for i in range(0, items.get_child_count()): + var c = items.get_child(i) + if !vm.inventory_has(c.global_id): + c.hide() + continue + if count >= page_size * (page+1): + #printt("past page", count, page_size, page) + c.hide() + elif count >= page_size * page: + var slot = count - page_size * page + c.show() + printt("showing item", c.global_id, slots.get_child(slot).get_global_position()) + + c.set_global_position(slots.get_child(slot).get_global_position()) + c.update_rect() # To see if we're hovered when in-game menu closes + + if !focus: + focus = true + else: + c.hide() + count += 1 + page_max = int(count / page_size) + + if count > 0 && page > page_max: + page -= 1 + sort_items() + +func inventory_changed(): + printt("** inventory changed") + if is_visible(): + printt("sorting") + sort_items() + +func global_changed(name): + if name.find("i/") != 0: + return + inventory_changed() + +func input(event): + if event.type == InputEvent.MOUSE_BUTTON && event.pressed: + toggle() + +func get_action(): + return current_action + +func look_toggled(pressed): + if pressed: + current_action = "look" + else: + current_action = "use" + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "clear_action") + +func _input(event): + if !vm.can_interact(): + return + if event.is_pressed() and event.is_action("inventory_toggle") and is_collapsible: + toggle() + +func log_button_pressed(): + close() + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "open_log") + +func _on_open_inventory_signal(open): + if (open): + open() + else: + close() + +func mouse_entered(): + # Must tear down the hovers or we might get side-effects when something is under + # a non-blocking inventory + vm.hover_teardown() + +func mouse_exited(): + # Restore the expected state if it was reset when entering + vm.hover_rebuild() + +func _ready(): + var conn_err + + conn_err = vm.connect("inventory_changed", self, "inventory_changed") + if conn_err: + vm.report_errors("inventory", ["inventory_changed -> inventory_changed error: " + String(conn_err)]) + + conn_err = vm.connect("open_inventory", self, "_on_open_inventory_signal") + if conn_err: + vm.report_errors("inventory", ["open_inventory -> _on_open_inventory_signal error: " + String(conn_err)]) + + conn_err = vm.connect("global_changed", self, "global_changed") + if conn_err: + vm.report_errors("inventory", ["global_changed -> global_changed error: " + String(conn_err)]) + + page_size = get_node("slots").get_child_count() + + # XXX: Not sure why, but sorting a collapsible inventory here causes textures to disappear + if not self.is_collapsible: + sort_items() + + if has_node("arrow_prev"): + conn_err = $"arrow_prev".connect("pressed", self, "change_page", [-1]) + if conn_err: + vm.report_errors("inventory", ["arrow_prev.pressed -> change_page -1 error: " + String(conn_err)]) + + if has_node("arrow_next"): + conn_err = $"arrow_next".connect("pressed", self, "change_page", [1]) + if conn_err: + vm.report_errors("inventory", ["arrow_next.pressed -> change_page +1 error: " + String(conn_err)]) + + # Block problems when tooltip follows mouse and open inventory overlaps with exits + if ProjectSettings.get_setting("escoria/ui/tooltip_follows_mouse") and self is TextureRect: + conn_err = connect("mouse_entered", self, "mouse_entered") + if conn_err: + vm.report_errors("inventory", ["mouse_entered -> mouse_entered error: " + String(conn_err)]) + + conn_err = connect("mouse_exited", self, "mouse_exited") + if conn_err: + vm.report_errors("inventory", ["mouse_exited -> mouse_exited error: " + String(conn_err)]) + + + var items = get_node("items") + for i in range(0, items.get_child_count()): + var c = items.get_child(i) + #printt("c path is ", c.get_name(), c.get_filename()) + c.inventory = true + c.use_action_menu = false + c.hide() + + items.show() + set_focus_mode(Control.FOCUS_NONE) + #get_node("mask").set_focus_mode(Control.FOCUS_NONE) + set_process_input(true) + + #get_node("log_button").connect("pressed", self, "log_button_pressed") + + if has_node("animation"): + conn_err = $"animation".connect("animation_finished", self, "anim_finished") + if conn_err: + vm.report_errors("inventory", ["animation_finished -> anim_finished error: " + String(conn_err)]) + + add_to_group("game") + + call_deferred("sort_items") diff --git a/device/globals/item.gd b/device/globals/item.gd new file mode 100644 index 0000000..b50abec --- /dev/null +++ b/device/globals/item.gd @@ -0,0 +1,718 @@ +extends "res://globals/interactive.gd" + +signal left_click_on_item +signal left_dblclick_on_item +signal left_click_on_inventory_item +signal right_click_on_item +signal right_click_on_inventory_item +signal mouse_enter_item +signal mouse_enter_inventory_item +signal mouse_exit_item +signal mouse_exit_inventory_item + +var area + +# For inventory items, when in-game menu is closed, look at this to see if we're hovered +var rect + +export var tooltip = "" +export var action = "" + +export(NodePath) var interact_position = null +#warning-ignore:unused_class_variable +export var use_combine = false # game.gd +export var inventory = false +#warning-ignore:unused_class_variable +export var use_action_menu = true # game.gd + +#warning-ignore:unused_class_variable +export(int, -1, 360) var interact_angle = -1 +#warning-ignore:unused_class_variable +export(Color) var dialog_color = null +export(Script) var animations +export var talk_animation = "talk" +export var placeholders = {} +export var dynamic_z_index = true +export var speed = 300 +export var scale_on_map = false +export var light_on_map = false setget set_light_on_map +export var is_interactive = true + +var orig_speed + +var anim_notify = null +var anim_scale_override = null +var ui_anim = null + +var clicked = false +var interact_pos +var camera_pos +var terrain +var terrain_is_scalenodes +var walk_path +var walk_context +var moved +var last_scale = Vector2(1, 1) +var last_deg = null +var last_dir = 0 +var animation +var state = "" +var walk_destination +var path_ofs +var pose_scale = 1 +var task +var sprites = [] +var audio + +# Godot doesn't do doubleclicks so we must +var last_lmb_dt = 0 +var waiting_dblclick = false + +func get_dialog_pos(): + if has_node("dialog_pos"): + return $"dialog_pos".global_position + + return global_position + +func get_interact_pos(): + if interact_pos: + return interact_pos.global_position + + return global_position + +func get_camera_pos(): + if camera_pos: + return camera_pos.global_position + + return global_position + +#warning-ignore:unused_argument +func anim_finished(anim_name): + # TODO use parameter here? + if anim_notify != null: + vm.finished(anim_notify) + anim_notify = null + + if anim_scale_override != null: + set_scale(get_scale() * anim_scale_override) + anim_scale_override = null + + # Although states are permanent until changed, the underlying animations are not, + # so we must re-set the state for the appearance of permanence + set_state(state, true) + + if animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + + + #return is_visible() + +func get_action(): + return action + +func mouse_enter(): + _check_focus(true, false) + if self.inventory: + emit_signal("mouse_enter_inventory_item", self) + else: + emit_signal("mouse_enter_item", self) + +func mouse_exit(): + _check_focus(false, false) + if self.inventory: + emit_signal("mouse_exit_inventory_item", self) + else: + emit_signal("mouse_exit_item", self) + +func area_input(_viewport, event, _shape_idx): + input(event) + +func input(event): + # TODO: Expand this for other input events than mouse + if event is InputEventMouseButton || event.is_action("ui_accept"): + if event.is_pressed(): + clicked = vm.hover_stack.front() == self + + var ev_pos = get_global_mouse_position() + if event.is_action("game_general"): + if self.inventory: + emit_signal("left_click_on_inventory_item", self, ev_pos, event) + last_lmb_dt = 0 + waiting_dblclick = null + elif last_lmb_dt <= vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_dblclick_on_item", self, ev_pos, event) + last_lmb_dt = 0 + waiting_dblclick = null + else: + last_lmb_dt = 0 + waiting_dblclick = [ev_pos, event] + + elif event.is_action("game_rmb"): + if self.inventory: + emit_signal("right_click_on_inventory_item", self, ev_pos, event) + else: + emit_signal("right_click_on_item", self, ev_pos, event) + _check_focus(true, true) + +func _check_focus(focus, pressed): + if has_node("_focus_in"): + if focus: + get_node("_focus_in").show() + else: + get_node("_focus_in").hide() + + if has_node("_pressed"): + if pressed: + get_node("_pressed").show() + else: + get_node("_pressed").hide() + +func get_tooltip(hint=null): + if not tooltip: + return null + + # if `development_lang` matches `text_lang`, don't translate + if TranslationServer.get_locale() == ProjectSettings.get_setting("escoria/platform/development_lang"): + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("item", ["Missing global_id in item with tooltip '" + tooltip + "'"]) + return tooltip + + # Otherwise try to return the translated tooltip + var tooltip_identifier = global_id + ".tooltip" + if hint: + tooltip_identifier += "." + hint + + var translated = tr(tooltip_identifier) + + # Try again if there's no translation for this hint + if translated == "\"": + tooltip_identifier = global_id + ".tooltip" + translated = tr(tooltip_identifier) + + # But if translation isn't found, ensure it can be translated and return placeholder + if translated == tooltip_identifier: + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("item", ["Missing global_id in item with tooltip '" + tooltip + "'"]) + + return tooltip_identifier + + return translated + +func global_changed(name): + var ev = "global_changed "+name + if ev in event_table: + run_event(event_table[ev]) + elif "global_changed" in event_table: + run_event(event_table.global_changed) + +func anim_get_ph_paths(p_anim): + if !(p_anim in placeholders): + return null + + var ret = [] + for p in placeholders[p_anim]: + var n = get_node(p) + if !(n is InstancePlaceholder): + continue + ret.push_back(n.get_instance_path()) + return ret + +func play_anim(p_anim, p_notify = null, p_reverse = false, p_flip = null): + if p_notify == null and (!animation or !animation.has_animation(p_anim)): + print("skipping cut scene '", p_anim, "'") + vm.finished(p_notify) + #_debug_states() + return + + if p_anim in placeholders: + for npath in placeholders[p_anim]: + var node = get_node(npath) + if !node is InstancePlaceholder: + continue + var path = node.get_instance_path() + var res = vm.res_cache.get_resource(path) + node.replace_by_instance(res) + _find_sprites(get_node(npath)) + if p_flip != null: + var s = get_scale() + set_scale(s * p_flip) + anim_scale_override = p_flip + else: + anim_scale_override = null + + if p_reverse: + animation.play(p_anim, -1, -1, true) + else: + animation.play(p_anim) + anim_notify = p_notify + + #_debug_states() + +func play_snd(p_snd, p_loop=false): + if !audio: + vm.report_errors("item", ["play_snd called with no audio node"]) + return + + var resource = load(p_snd) + if !resource: + vm.report_errors("item", ["play_snd resource not found " + p_snd]) + return + + audio.stream = resource + audio.stream.set_loop(p_loop) + audio.play() + + #_debug_states() + +func set_speaking(p_speaking): + printt("item set speaking! ", global_id, p_speaking, state) + #print_stack() + if !animation: + return + if talk_animation == "": + return + if p_speaking: + if animation.has_animation(talk_animation): + animation.play(talk_animation) + animation.seek(0, true) + else: + set_state(state, true) + if animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func set_state(p_state, p_force = false): + if state == p_state && !p_force: + return + + # printt("set state ", "global_id: ", global_id, "state: ", state, "p_state: ", p_state, "p_force: ", p_force) + + state = p_state + + if animation != null: + # Though calling `.play()` probably stops the animation, be safe. + animation.stop() + if animation.has_animation(p_state): + animation.play(p_state) + +func teleport(obj, angle=null): + set_position(obj.global_position) + if angle: + set_angle(angle) + moved = true + _update_terrain(true) + +func teleport_pos(x, y, angle=null): + set_position(Vector2(x, y)) + if angle: + set_angle(angle) + moved = true + _update_terrain(true) + +func _update_terrain(need_z_update=false): + var pos = get_position() + + if dynamic_z_index and need_z_update: + z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX + + if !scale_on_map && !light_on_map: + return + + # Items in the scene tree will issue errors unless this is conditional + if not terrain: + return + + var scale_range + if terrain_is_scalenodes: + scale_range = terrain.get_terrain(pos) + else: + var color = terrain.get_terrain(pos) + scale_range = terrain.get_scale_range(color.b) + + # The item's - as the player's - `animations` define the direction + # as 1 or -1. This is stored as `pose_scale` and the easiest way + # to flip a node is multiply its x-axis scale. + scale_range.x *= pose_scale + + if scale_on_map and scale_range != get_scale(): + # Check if `interact_pos` is a child of ours, and if so, + # take a backup of the global position, because it will be affected by scaling. + var interact_global_position + if has_node("interact_pos"): + interact_global_position = interact_pos.get_global_position() + + # Same for camera_pos + var camera_global_position + if has_node("camera_pos"): + camera_global_position = camera_pos.get_global_position() + + self.scale = scale_range + + # If `interact_pos` is a child, it was affected by scaling, so reset it + # to the expected location. + if interact_global_position: + interact_pos.global_position = interact_global_position + + # And camera position + if camera_global_position: + camera_pos.global_position = camera_global_position + + if light_on_map: + var c = terrain.get_light(pos) + if c: + modulate(c) + +func _check_bounds(): + #printt("checking bouds for pos ", get_position(), terrain.is_solid(get_position())) + if !scale_on_map: + return + if !Engine.is_editor_hint(): + return + if terrain.is_solid(get_position()): + if has_node("terrain_icon"): + get_node("terrain_icon").hide() + else: + if !has_node("terrain_icon"): + var node = Sprite.new() + var tex = load("res://globals/terrain.png") + node.set_texture(tex) + add_child(node) + node.set_name("terrain_icon") + get_node("terrain_icon").show() + +func _notification(what): + if !is_inside_tree() || !Engine.is_editor_hint(): + return + if what == Node2D.NOTIFICATION_TRANSFORM_CHANGED: + _update_terrain() + _check_bounds() + +func hint_request(): + if !get_active(): + return + if !vm.can_interact(): + return + + if ui_anim == null: + return + + if ui_anim.is_playing(): + return + + ui_anim.play("hint") + +func setup_ui_anim(): + if has_node("ui_anims"): + ui_anim = get_node("ui_anims") + + for bg in get_tree().get_nodes_in_group("background"): + bg.connect("right_click_on_bg", self, "hint_request") + + var conn_err = vm.connect("global_changed", self, "global_changed") + if conn_err: + vm.report_errors("item", ["global_changed -> global_changed error: " + String(conn_err)]) + +func set_light_on_map(p_light): + light_on_map = p_light + if light_on_map: + _update_terrain() + else: + modulate(Color(1, 1, 1, 1)) + +func slide_to(pos, context = null): + # Assume a straight line, and leverage some walk functionality + walk_path = [get_position(), pos] + walk_context = context + if walk_path.size() == 0: + walk_stop(get_position()) + set_process(false) + task = null + return + + moved = true + + walk_destination = walk_path[walk_path.size()-1] + + path_ofs = 0.0 + task = "slide" + set_process(true) + +func slide(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + slide_to(pos, context) + +func walk_stop(pos): + set_position(pos) + walk_path = [] + + if orig_speed: + speed = orig_speed + orig_speed = null + + # Walking is not a state, but we must re-set our previous state to reset the animation + set_state(state) + + if task == "walk": + if "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain(true) + + task = null + + if walk_context != null: + vm.finished(walk_context) + walk_context = null + +func walk_to(pos, context = null): + walk_path = terrain.get_terrain_path(get_position(), pos) + walk_context = context + if walk_path.size() == 0: + walk_stop(get_position()) + set_process(false) + task = null + return + moved = true + walk_destination = walk_path[walk_path.size()-1] + if terrain.is_solid(pos): + walk_destination = walk_path[walk_path.size()-1] + path_ofs = 0.0 + task = "walk" + set_process(true) + +func walk(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + walk_to(pos, context) + +func modulate(color): + for s in sprites: + s.set_modulate(color) + +func _physics_process(dt): + last_lmb_dt += dt + + if waiting_dblclick and last_lmb_dt > vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_click_on_item", self, waiting_dblclick[0], waiting_dblclick[1]) + last_lmb_dt = 0 + waiting_dblclick = null + +func _process(time): + if task == "walk" or task == "slide": + var to_walk = speed * last_scale.x * time + var pos = get_position() + var old_pos = pos + if walk_path.size() > 0: + while to_walk > 0: + var next + if walk_path.size() > 1: + next = walk_path[path_ofs + 1] + else: + next = walk_path[path_ofs] + + var dist = pos.distance_to(next) + + if dist > to_walk: + var n = (next - pos).normalized() + pos = pos + n * to_walk + break + pos = next + to_walk -= dist + path_ofs += 1 + if path_ofs >= walk_path.size() - 1: + walk_stop(walk_destination) + set_process(false) + return + + var angle = (old_pos.angle_to_point(pos)) * -1 + + set_position(pos) + + if task == "walk": + last_deg = vm._get_deg_from_rad(angle) + last_dir = vm._get_dir_deg(last_deg, animations) + + if animation: + if animation.get_current_animation() != animations.directions[last_dir]: + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir+1] + + # If a z-indexed item is moved, forcibly update its z index + _update_terrain(true) + +func turn_to(deg): + if deg < 0 or deg > 360: + vm.report_errors("interactive", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + if animation and animations and "directions" in animations: + if !animation.get_current_animation() or animation.get_current_animation() != animations.directions[last_dir]: + # XXX: This requires manually scripting a wait + # and setting the correct idle animation + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir + 1] + _update_terrain() + +func set_angle(deg): + if deg < 0 or deg > 360: + # Compensate for savegame files during a broken version of Escoria + if vm.loading_game: + vm.report_warnings("interactive", ["Reset invalid degree " + str(deg)]) + deg = 0 + else: + vm.report_errors("interactive", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + if animation and animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func _find_sprites(p = null): + if p is CanvasItem: + sprites.push_back(p) + for i in range(0, p.get_child_count()): + _find_sprites(p.get_child(i)) + +func update_rect(): + # Called from inventory.gd when items get sorted + assert(self.inventory) + assert(area is TextureRect) + + # Now that we know we are TextureRect, create a Rect2 so we can check `.has_point()` when closing the menu + rect = Rect2(area.rect_global_position, area.rect_size) + +func _ready(): + add_to_group("item") + + if Engine.is_editor_hint(): + return + + var conn_err + + # {{{ Check for interaction area and connect signals only if the item is interactive + if is_interactive: + if has_node("area"): + area = get_node("area") + # XXX: Inventory items as Area2D did not work. z-index? + if not self.inventory and not area is Area2D: + vm.report_errors("item", ["Child area is not Area2D in " + self.global_id]) + elif self.inventory and not area is TextureRect: + vm.report_errors("inventory item", ["Child area is not TextureRect in " + self.global_id]) + else: + area = self + if area is Position2D: + vm.report_warnings("item", ["The Position2D node named " + self.global_id + " is probably erroneously marked as interactive."]) + elif not area is Area2D and not area is Position2D: + vm.report_errors("item", ["Background item area is not Area2D nor Position2D in " + self.global_id]) + + if ClassDB.class_has_signal(area.get_class(), "input_event"): + conn_err = area.connect("input_event", self, "area_input") + if conn_err: + vm.report_errors("item", ["area.input_event -> area_input error: " + String(conn_err)]) + elif ClassDB.class_has_signal(area.get_class(), "gui_input"): + conn_err = area.connect("gui_input", self, "input") + if conn_err: + vm.report_errors("item", ["area.gui_input -> input error: " + String(conn_err)]) + else: + vm.report_warnings("item", ["No input events possible for global_id " + global_id]) + + # These signals proxy the proper signals for regular and inventory items + if ClassDB.class_has_signal(area.get_class(), "mouse_entered"): + conn_err = area.connect("mouse_entered", self, "mouse_enter") + if conn_err: + vm.report_errors("item", ["mouse_entered -> mouse_enter error: " + String(conn_err)]) + + conn_err = area.connect("mouse_exited", self, "mouse_exit") + if conn_err: + vm.report_errors("item", ["mouse_exited -> mouse_exit error: " + String(conn_err)]) + + conn_err = connect("left_click_on_item", $"/root/scene/game", "ev_left_click_on_item") + if conn_err: + vm.report_errors("item", ["left_click_on_item -> ev_left_click_on_item error: " + String(conn_err)]) + + conn_err = connect("left_dblclick_on_item", $"/root/scene/game", "ev_left_dblclick_on_item") + if conn_err: + vm.report_errors("item", ["left_dblclick_on_item -> ev_left_dblclick_on_item error: " + String(conn_err)]) + + conn_err = connect("left_click_on_inventory_item", $"/root/scene/game", "ev_left_click_on_inventory_item") + if conn_err: + vm.report_errors("item", ["left_click_on_inventory_item -> ev_left_click_on_inventory_item error: " + String(conn_err)]) + + conn_err = connect("right_click_on_item", $"/root/scene/game", "ev_right_click_on_item") + if conn_err: + vm.report_errors("item", ["right_click_on_item -> ev_right_click_on_item error: " + String(conn_err)]) + + conn_err = connect("right_click_on_inventory_item", $"/root/scene/game", "ev_right_click_on_inventory_item") + if conn_err: + vm.report_errors("item", ["right_click_on_inventory_item -> ev_right_click_on_inventory_item error: " + String(conn_err)]) + + + conn_err = connect("mouse_enter_item", $"/root/scene/game", "ev_mouse_enter_item") + if conn_err: + vm.report_errors("item", ["mouse_enter_item -> ev_mouse_enter_item error: " + String(conn_err)]) + + conn_err = connect("mouse_enter_inventory_item", $"/root/scene/game", "ev_mouse_enter_inventory_item") + if conn_err: + vm.report_errors("item", ["mouse_enter_inventory_item -> ev_mouse_enter_inventory_item error: " + String(conn_err)]) + + conn_err = connect("mouse_exit_item", $"/root/scene/game", "ev_mouse_exit_item") + if conn_err: + vm.report_errors("item", ["mouse_exit_item -> ev_mouse_exit_item error: " + String(conn_err)]) + + conn_err = connect("mouse_exit_inventory_item", $"/root/scene/game", "ev_mouse_exit_inventory_item") + if conn_err: + vm.report_errors("item", ["mouse_exit_inventory_item -> ev_mouse_exit_inventory_item error: " + String(conn_err)]) + + if interact_position: + interact_pos = get_node(interact_position) + elif has_node("interact_pos"): + interact_pos = $"interact_pos" + # }}} + + if has_node("camera_pos"): + camera_pos = $"camera_pos" + + if events_path != "": + event_table = vm.compile(events_path) + + # Forbit pipe because it's used to separate flags from actions, like in `:use item | TK`. And space for good measure. + for c in ["|", " "]: + if c in global_id: + vm.report_errors("item", ["Forbidden character '" + c + "' in global_id: " + global_id]) + + if has_node("animation"): + animation = $"animation" + conn_err = animation.connect("animation_finished", self, "anim_finished") + if conn_err: + vm.report_errors("item", ["animation_finished -> anim_finished error: " + String(conn_err)]) + + if has_node("audio"): + audio = $"audio" + audio.set_bus("SFX") + + _check_focus(false, false) + + if has_node("../terrain"): + terrain = $"../terrain" + terrain_is_scalenodes = terrain is preload("terrain_scalenodes.gd") + + _find_sprites(self) + + # Initialize Node2D items' terrain status like z-index. + # Stationary items will be set up correctly and + # if an item moves, it will handle this in its _process() loop + _update_terrain(true) + + vm.register_object(global_id, self) + diff --git a/device/globals/lightmap_area.gd b/device/globals/lightmap_area.gd new file mode 100644 index 0000000..e684645 --- /dev/null +++ b/device/globals/lightmap_area.gd @@ -0,0 +1,36 @@ +extends Area2D + +export var global_id = "" +export var active = true setget set_active,get_active + +func set_active(p_active): + active = p_active + if p_active: + show() + else: + hide() + +func get_active(): + return active + +func body_entered(body): + if "check_maps" in body: + body.check_maps = true + +func body_exited(body): + if "check_maps" in body: + body.check_maps = false + +func _ready(): + var conn_err + + conn_err = connect("body_entered", self, "body_entered") + if conn_err: + vm.report_errors("lightmap_area", ["body_entered -> body_entered error: " + String(conn_err)]) + + conn_err = connect("body_exited", self, "body_exited") + if conn_err: + vm.report_errors("item", ["body_exited -> body_exited error: " + String(conn_err)]) + + vm.register_object(global_id, self) + diff --git a/device/globals/main.gd b/device/globals/main.gd new file mode 100644 index 0000000..bd9cec0 --- /dev/null +++ b/device/globals/main.gd @@ -0,0 +1,217 @@ +extends Node + +var telon +var menu_layer +var wait_timer +var wait_level +var current +var menu_stack = [] +var game_size + +#warning-ignore:unused_class_variable +var screen_ofs = Vector2(0, 0) # game.gd + +func clear_scene(): + if current == null: + return + + vm.clear_current_action() + vm.clear_current_tool() + vm.hover_clear_stack() + vm.clear_inventory() + vm.clear_tooltip() + vm.clear_action_menu() + + get_node("/root").remove_child(current) + current.free() + current = null + +func set_scene(p_scene, run_events=true): + if not p_scene: + vm.report_errors("main", ["Trying to set empty scene"]) + + # Ensure we don't have a regular event running when changing scenes + if vm.running_event: + assert(vm.running_event.ev_name == "load") + + if "events_path" in p_scene and p_scene.events_path and run_events: + vm.load_file(p_scene.events_path) + + # :setup is pretty much required in the code, but fortunately + # we can help out with cases where one isn't necessary otherwise + if not "setup" in vm.game: + var fake_setup = vm.compile_str(":setup\n") + vm.game["setup"] = fake_setup["setup"] + + vm.run_event(vm.game["setup"]) + + if current != null: + clear_scene() + + get_node("/root").add_child(p_scene) + + set_current_scene(p_scene, run_events) + +func get_current_scene(): + return current + +func menu_open(menu): + menu_stack.push_back(menu) + if menu_stack.size() == 1: + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "menu_opened") + vm.set_pause(true) + #get_tree().set_pause(true) + pass + +func menu_close(p_menu): + if menu_stack.size() == 0 || menu_stack[menu_stack.size()-1] != p_menu: + print("***** warning! closing unknown menu?") + menu_stack.remove(menu_stack.size() - 1) + + if menu_stack.size() == 0: + vm.set_pause(false) + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "menu_closed") + #get_tree().set_pause(false) + pass + +func load_menu(path): + if path == "": + printt("error: loading empty menu") + return + var menu = load(path) + if menu == null: + printt("error loading menu ", path) + return + printt("************* loding menu ", path, menu) + menu = menu.instance() + menu_layer.add_child(menu) + return menu + +func menu_collapse(): + var i = menu_stack.size() + while i > 0: + i -= 1 + menu_stack[i].menu_collapsed() + +func set_current_scene(p_scene, run_events=true): + #print_stack() + + current = p_scene + $"/root".move_child(current, 0) + + # Loading a save game must set the scene but not run events + if "events_path" in current and current.events_path and run_events: + if vm.game: + # Having a game with `:setup` means we must wait for it to finish + if "setup" in vm.game: + if not vm.running_event: + vm.report_errors("main", ["vm.game has setup but no running_event"]) + + if vm.running_event.ev_name != "setup": + vm.report_errors("main", ["vm.game has setup but it is not running: " + vm.running_event.ev_name]) + + yield(vm, "event_done") + else: + vm.load_file(current.events_path) + # For a new game, we must run `:setup` if available + # and wait for it to finish + if "setup" in vm.game: + vm.run_event(vm.game["setup"]) + yield(vm, "event_done") + + # Because 1) changing a scene and 2) having a scene become ready + # both call `set_current_scene`, we don't want to duplicate thing + if not vm.running_event: + vm.run_game() + + vm.register_object("_scene", p_scene, true) # Force overwrite of global + +func wait_finished(): + vm.finished(wait_level) + +func wait(time, level): + wait_level = level + wait_timer.set_wait_time(time) + wait_timer.set_one_shot(true) + wait_timer.start() + +func _input(event): + match vm.accept_input: + vm.acceptable_inputs.INPUT_NONE: + # The only acceptable input here is releasing the MMB + if event is InputEventMouseButton: + if not event.pressed: + if event.is_action("game_highlight"): + vm.accept_input = vm.acceptable_inputs.INPUT_ALL + if vm.tooltip: + get_tree().call_group_flags(SceneTree.GROUP_CALL_REALTIME, "highlight_tooltip", "hide_highlight") + vm.tooltip.force_tooltip_visible(true) + vm.tooltip.update() + return + vm.acceptable_inputs.INPUT_SKIP: + if event is InputEventMouseButton and event.pressed and event.is_action("game_general"): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "events", "skipped") + return + vm.acceptable_inputs.INPUT_ALL: + if event is InputEventMouseButton: + if event.pressed: + if event.is_action("game_general"): + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "events", "skipped") + elif event.is_action("game_highlight"): + # Deny all input, like walking around with the highlights open + vm.accept_input = vm.acceptable_inputs.INPUT_NONE + get_tree().call_group_flags(SceneTree.GROUP_CALL_REALTIME, "player", "halt") + if vm.tooltip: + vm.tooltip.force_tooltip_visible(false) + get_tree().call_group_flags(SceneTree.GROUP_CALL_REALTIME, "highlight_tooltip", "show_highlight") + + if event is InputEventKey and event.pressed: + match(event.scancode): + KEY_F12: + OS.print_all_textures_by_size() + + KEY_ESCAPE: + get_tree().quit() + + KEY_F2: + vm.load_file(ProjectSettings.get_setting("escoria/platform/game_start_script")) + vm.run_game() + + KEY_F9: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + + KEY_F10: + Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) + + KEY_F11: + OS.window_fullscreen = !OS.window_fullscreen + + if menu_stack.size() > 0: + menu_stack[menu_stack.size() - 1].input(event) + elif current != null: + if current.has_node("game"): + current.get_node("game").scene_input(event) + +func load_telon(): + var tpath = ProjectSettings.get_setting("escoria/platform/telon") + var tres = vm.res_cache.get_resource(tpath) + + get_node("layers/telon/telon").replace_by_instance(tres) + telon = get_node("layers/telon/telon") + +func _ready(): + printt("main ready") + # get_node("/root").set_render_target_clear_on_new_frame(true) + + game_size = get_viewport().size + + wait_timer = get_node("layers/wait_timer") + if wait_timer != null: + wait_timer.connect("timeout", self, "wait_finished") + add_to_group("game") + menu_layer = get_node("layers/menu") + set_process_input(true) + set_process(true) + + call_deferred("load_telon") + diff --git a/device/globals/main.tscn b/device/globals/main.tscn new file mode 100644 index 0000000..22a1a79 --- /dev/null +++ b/device/globals/main.tscn @@ -0,0 +1,55 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/main.gd" type="Script" id=1] +[ext_resource path="res://globals/bg_music.tscn" type="PackedScene" id=2] +[ext_resource path="res://globals/bg_snd.tscn" type="PackedScene" id=3] +[ext_resource path="res://ui/dd_player.tscn" type="PackedScene" id=4] + +[node name="main" type="Node"] + +pause_mode = 2 +script = ExtResource( 1 ) + +[node name="layers" type="Node" parent="." index="0"] + +[node name="telon" type="CanvasLayer" parent="layers" index="0"] + +layer = 127 +offset = Vector2( 0, 0 ) +rotation = 0.0 +scale = Vector2( 1, 1 ) +transform = Transform2D( 1, 0, 0, 1, 0, 0 ) + +[node name="bg_music" parent="layers/telon" index="0" instance=ExtResource( 2 )] + +[node name="telon" parent="layers/telon" index="1" instance_placeholder="res://globals/telon.tscn"] + +[node name="bg_snd" parent="layers/telon" index="2" instance=ExtResource( 3 )] + +[node name="dialog" type="CanvasLayer" parent="layers" index="1"] + +layer = 128 +offset = Vector2( 0, 0 ) +rotation = 0.0 +scale = Vector2( 1, 1 ) +transform = Transform2D( 1, 0, 0, 1, 0, 0 ) + +[node name="dd_player" parent="layers/dialog" index="0" instance=ExtResource( 4 )] + +[node name="menu" type="CanvasLayer" parent="layers" index="2"] + +layer = 2 +offset = Vector2( 0, 0 ) +rotation = 0.0 +scale = Vector2( 1, 1 ) +transform = Transform2D( 1, 0, 0, 1, 0, 0 ) + +[node name="wait_timer" type="Timer" parent="layers" index="3"] + +editor/display_folded = true +process_mode = 1 +wait_time = 1.0 +one_shot = false +autostart = false + + diff --git a/device/globals/mask_light2d.gd b/device/globals/mask_light2d.gd new file mode 100644 index 0000000..7800d64 --- /dev/null +++ b/device/globals/mask_light2d.gd @@ -0,0 +1,33 @@ +tool + +export(Vector2) var front_pos +var pos2DZ + +func init_mask(): + _update_terrain() + update() + +func get_front_position(): + if (has_node("front_pos")): + front_pos = get_node("front_pos").get_global_position() + return front_pos + + +func _update_terrain(): + var pos = get_front_position() + set_z_index(pos.y) + set_z_range_min( 1 ) + set_z_range_max( pos.y ) + +func debug_print_z(): + printt("MASKS node Z : ", get_z_index()) + printt("node", "name", "Z", "Z_range_min", "Z_range_max") + printt("node", get_name(), get_z_index(), get_z_range_min(), get_z_range_max()) + print("\n") + +func _ready(): + init_mask() + #debug_print_z() + pass + + diff --git a/device/globals/nav_poly_instance.gd b/device/globals/nav_poly_instance.gd new file mode 100644 index 0000000..86dfb20 --- /dev/null +++ b/device/globals/nav_poly_instance.gd @@ -0,0 +1,15 @@ +extends NavigationPolygonInstance + +export(String) var global_id + +func set_active(p_active): + enabled = p_active + + if enabled: + show() + else: + hide() + +func _ready(): + vm.register_object(global_id, self) + diff --git a/device/globals/npc.gd b/device/globals/npc.gd new file mode 100644 index 0000000..60af7b9 --- /dev/null +++ b/device/globals/npc.gd @@ -0,0 +1,757 @@ +tool + +extends KinematicBody2D + +signal left_click_on_npc +signal left_dblclick_on_npc +signal right_click_on_npc +signal mouse_enter_npc +signal mouse_exit_npc + +#warning-ignore:unused_class_variable +export var global_id = "" # API property +#warning-ignore:unused_class_variable +export(String, FILE, ".esc") var events_path = "" # API property +export var active = true setget set_active,get_active + +export var tooltip = "" +export var action = "" + +export(NodePath) var interact_position = null +#warning-ignore:unused_class_variable +export var use_combine = false # game.gd +#warning-ignore:unused_class_variable +export var use_action_menu = true # game.gd + +#warning-ignore:unused_class_variable +export(int, -1, 360) var interact_angle = -1 +#warning-ignore:unused_class_variable +export(Color) var dialog_color = null +export(Script) var animations +export var talk_animation = "talk" +export var placeholders = {} +export var dynamic_z_index = true +export var speed = 300 +export var scale_on_map = false +export var light_on_map = false setget set_light_on_map + +var check_maps # set by lightmap_area.gd + +var vm # A tool script cannot refer to singletons in Godot + +var area + +var orig_speed + +var anim_notify = null +var anim_scale_override = null +var ui_anim = null + +var clicked = false +var interact_pos +var camera_pos +var terrain +var terrain_is_scalenodes +var walk_path +var walk_context +var moved +var last_scale = Vector2(1, 1) +var last_deg = null +var last_dir = 0 +var animation +var state = "" +var walk_destination +var path_ofs +var pose_scale = 1 +var task +var sprites = [] +var audio + +# Godot doesn't do doubleclicks so we must +var last_lmb_dt = 0 +var waiting_dblclick = false + +# This'll contain a highlight tooltip if `tooltip_pos` is set as a child +var highlight_tooltip + +var event_table = {} + +var width = float(ProjectSettings.get("display/window/size/width")) +var height = float(ProjectSettings.get("display/window/size/height")) + +func run_event(p_ev): + vm.emit_signal("run_event", p_ev) + +func activate(p_action, p_param = null): + if p_param != null: + p_action = p_action + " " + p_param.global_id + + if p_action in event_table: + run_event(event_table[p_action]) + else: + return false + return true + +func set_active(p_active): + active = p_active + if p_active: + show() + else: + hide() + +func set_interactive(p_interactive): + self.area.visible = p_interactive + +func get_active(): + return active + +func get_dialog_pos(): + if has_node("dialog_pos"): + return $"dialog_pos".global_position + + return global_position + +func get_interact_pos(): + if interact_pos: + return interact_pos.global_position + + return global_position + +func get_camera_pos(): + if camera_pos: + return camera_pos.global_position + + return global_position + +#warning-ignore:unused_argument +func anim_finished(anim_name): + # TODO use parameter here? + if anim_notify != null: + vm.finished(anim_notify) + anim_notify = null + + if anim_scale_override != null: + set_scale(get_scale() * anim_scale_override) + anim_scale_override = null + + # Although states are permanent until changed, the underlying animations are not, + # so we must re-set the state for the appearance of permanence + set_state(state, true) + + if animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + + + #return is_visible() + +func get_action(): + return action + +func mouse_enter(): + _check_focus(true, false) + emit_signal("mouse_enter_npc", self) + +func mouse_exit(): + _check_focus(false, false) + emit_signal("mouse_exit_npc", self) + +func area_input(_viewport, event, _shape_idx): + # TODO: Expand this for other input events than mouse + if event is InputEventMouseButton || event.is_action("ui_accept"): + if event.is_pressed(): + clicked = vm.hover_stack.front() == self + + var ev_pos = get_global_mouse_position() + if event.is_action("game_general"): + if last_lmb_dt <= vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_dblclick_on_npc", self, ev_pos, event) + last_lmb_dt = 0 + waiting_dblclick = null + else: + last_lmb_dt = 0 + waiting_dblclick = [ev_pos, event] + + elif event.is_action("game_rmb"): + emit_signal("right_click_on_npc", self, ev_pos, event) + _check_focus(true, true) + +func _check_focus(focus, pressed): + if has_node("_focus_in"): + if focus: + get_node("_focus_in").show() + else: + get_node("_focus_in").hide() + + if has_node("_pressed"): + if pressed: + get_node("_pressed").show() + else: + get_node("_pressed").hide() + +func get_tooltip(hint=null): + if not tooltip: + return null + + # if `development_lang` matches `text_lang`, don't translate + if TranslationServer.get_locale() == ProjectSettings.get_setting("escoria/platform/development_lang"): + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("npc", ["Missing global_id in npc with tooltip '" + tooltip + "'"]) + return tooltip + + # Otherwise try to return the translated tooltip + var tooltip_identifier = global_id + ".tooltip" + if hint: + tooltip_identifier += "." + hint + + var translated = tr(tooltip_identifier) + + # Try again if there's no translation for this hint + if translated == "\"": + tooltip_identifier = global_id + ".tooltip" + translated = tr(tooltip_identifier) + + # But if translation isn't found, ensure it can be translated and return placeholder + if translated == tooltip_identifier: + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("npc", ["Missing global_id in npc with tooltip '" + tooltip + "'"]) + + return tooltip_identifier + + return translated + +func show_highlight(): + if not self.visible: + return + + if not self.area.visible: + return + + if not has_node("tooltip_pos"): + return + + highlight_tooltip = vm.tooltip.duplicate() + assert(highlight_tooltip != vm.tooltip) + + var tt_pos = $"tooltip_pos".global_position + var tt_text = get_tooltip() + + highlight_tooltip.highlight_only = true + highlight_tooltip.follow_mouse = false + highlight_tooltip.text = tt_text + + tt_pos = vm.camera.zoom_transform.xform(tt_pos) + + # Bail out if we're hopelessly out-of-view + if tt_pos.x < 0 or tt_pos.x > width or tt_pos.y < 0 or tt_pos.y > height: + highlight_tooltip.free() + highlight_tooltip = null + return + + vm.tooltip.get_parent().add_child(highlight_tooltip) + + highlight_tooltip.set_position(tt_pos) + + highlight_tooltip.show() + +func hide_highlight(): + if not highlight_tooltip: + return + + assert(highlight_tooltip.visible) + + highlight_tooltip.hide() + highlight_tooltip.free() + highlight_tooltip = null + +func global_changed(name): + var ev = "global_changed "+name + if ev in event_table: + run_event(event_table[ev]) + elif "global_changed" in event_table: + run_event(event_table.global_changed) + +func anim_get_ph_paths(p_anim): + if !(p_anim in placeholders): + return null + + var ret = [] + for p in placeholders[p_anim]: + var n = get_node(p) + if !(n is InstancePlaceholder): + continue + ret.push_back(n.get_instance_path()) + return ret + +func play_anim(p_anim, p_notify = null, p_reverse = false, p_flip = null): + if p_notify == null and (!animation or !animation.has_animation(p_anim)): + print("skipping cut scene '", p_anim, "'") + vm.finished(p_notify) + #_debug_states() + return + + if p_anim in placeholders: + for npath in placeholders[p_anim]: + var node = get_node(npath) + if !node is InstancePlaceholder: + continue + var path = node.get_instance_path() + var res = vm.res_cache.get_resource(path) + node.replace_by_instance(res) + _find_sprites(get_node(npath)) + if p_flip != null: + var s = get_scale() + set_scale(s * p_flip) + anim_scale_override = p_flip + else: + anim_scale_override = null + + if p_reverse: + animation.play(p_anim, -1, -1, true) + else: + animation.play(p_anim) + anim_notify = p_notify + + #_debug_states() + +func play_snd(p_snd, p_loop=false): + if !audio: + vm.report_errors("npc", ["play_snd called with no audio node"]) + return + + var resource = load(p_snd) + if !resource: + vm.report_errors("npc", ["play_snd resource not found " + p_snd]) + return + + audio.stream = resource + audio.stream.set_loop(p_loop) + audio.play() + + #_debug_states() + +func set_speaking(p_speaking): + printt("npc set speaking! ", global_id, p_speaking, state) + #print_stack() + if !animation: + return + if talk_animation == "": + return + if p_speaking: + if animation.has_animation(talk_animation): + animation.play(talk_animation) + animation.seek(0, true) + else: + set_state(state, true) + if animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func set_state(p_state, p_force = false): + if state == p_state && !p_force: + return + + # printt("set state ", "global_id: ", global_id, "state: ", state, "p_state: ", p_state, "p_force: ", p_force) + + state = p_state + + if animation != null: + # Though calling `.play()` probably stops the animation, be safe. + animation.stop() + if animation.has_animation(p_state): + animation.play(p_state) + +func teleport(obj, angle=null): + set_position(obj.global_position) + if angle: + set_angle(angle) + moved = true + _update_terrain(true) + +func teleport_pos(x, y, angle=null): + set_position(Vector2(x, y)) + if angle: + set_angle(angle) + moved = true + _update_terrain(true) + +func _update_terrain(need_z_update=false): + # Objects in the scene tree will issue errors unless this is conditional + if not terrain: + return + + var pos = get_position() + + if dynamic_z_index and need_z_update: + z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX + + if !scale_on_map && !light_on_map: + return + + var scale_range + if terrain_is_scalenodes: + scale_range = terrain.get_terrain(pos) + elif check_maps: + var color = terrain.get_terrain(pos) + scale_range = terrain.get_scale_range(color.b) + + if scale_on_map and scale_range != get_scale(): + # Check if `interact_pos` is a child of ours, and if so, + # take a backup of the global position, because it will be affected by scaling. + var interact_global_position + if has_node("interact_pos"): + interact_global_position = interact_pos.get_global_position() + + # Same for camera_pos + var camera_global_position + if has_node("camera_pos"): + camera_global_position = camera_pos.get_global_position() + + self.scale = scale_range + + # If `interact_pos` is a child, it was affected by scaling, so reset it + # to the expected location. + if interact_global_position: + interact_pos.global_position = interact_global_position + + # And camera position + if camera_global_position: + camera_pos.global_position = camera_global_position + + if light_on_map and check_maps: + var c = terrain.get_light(pos) + if c: + modulate(c) + + # The npc's - as the player's - `animations` define the direction + # as 1 or -1. This is stored as `pose_scale` and the easiest way + # to flip a node is multiply its x-axis scale. + if pose_scale == -1 and $"sprite".scale.x > 0: + $"sprite".scale.x *= pose_scale + $"area".scale.x *= pose_scale + elif pose_scale == 1 and $"sprite".scale.x < 0: + $"sprite".scale.x *= -1 + $"area".scale.x *= -1 + +func _check_bounds(): + printt("_check_bounds", terrain) + #printt("checking bouds for pos ", get_position(), terrain.is_solid(get_position())) + if !scale_on_map: + return + if !Engine.is_editor_hint(): + return + if !terrain: + return + if terrain.is_solid(get_position()): + if has_node("terrain_icon"): + get_node("terrain_icon").hide() + else: + if !has_node("terrain_icon"): + var node = Sprite.new() + var tex = load("res://globals/terrain.png") + node.set_texture(tex) + add_child(node) + node.set_name("terrain_icon") + get_node("terrain_icon").show() + +func _notification(what): + if !is_inside_tree() || !Engine.is_editor_hint(): + return + if what == CanvasItem.NOTIFICATION_TRANSFORM_CHANGED: + call_deferred("_editor_transform_changed") + +func _editor_transform_changed(): + _update_terrain() + _check_bounds() + +func hint_request(): + if !get_active(): + return + if !vm.can_interact(): + return + + if ui_anim == null: + return + + if ui_anim.is_playing(): + return + + ui_anim.play("hint") + +func setup_ui_anim(): + if has_node("ui_anims"): + ui_anim = get_node("ui_anims") + + for bg in get_tree().get_nodes_in_group("background"): + bg.connect("right_click_on_bg", self, "hint_request") + + var conn_err = vm.connect("global_changed", self, "global_changed") + if conn_err: + vm.report_errors("npc", ["global_changed -> global_changed error: " + String(conn_err)]) + +func set_light_on_map(p_light): + light_on_map = p_light + if light_on_map: + _update_terrain() + else: + modulate(Color(1, 1, 1, 1)) + +func slide_to(pos, context = null): + # Assume a straight line, and leverage some walk functionality + walk_path = [get_position(), pos] + walk_context = context + if walk_path.size() == 0: + walk_stop(get_position()) + set_process(false) + task = null + return + + moved = true + + walk_destination = walk_path[walk_path.size()-1] + + path_ofs = 0.0 + task = "slide" + set_process(true) + +func slide(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + slide_to(pos, context) + +func walk_stop(pos): + set_position(pos) + walk_path = [] + + if orig_speed: + speed = orig_speed + orig_speed = null + + # Walking is not a state, but we must re-set our previous state to reset the animation + set_state(state) + + if task == "walk": + if "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain(true) + + task = null + + if walk_context != null: + vm.finished(walk_context) + walk_context = null + +func walk_to(pos, context = null): + walk_path = terrain.get_terrain_path(get_position(), pos) + walk_context = context + if walk_path.size() == 0: + walk_stop(get_position()) + set_process(false) + task = null + return + moved = true + walk_destination = walk_path[walk_path.size()-1] + if terrain.is_solid(pos): + walk_destination = walk_path[walk_path.size()-1] + path_ofs = 0.0 + task = "walk" + set_process(true) + +func walk(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + walk_to(pos, context) + +func modulate(color): + for s in sprites: + s.set_modulate(color) + +func _physics_process(dt): + last_lmb_dt += dt + + if waiting_dblclick and last_lmb_dt > vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_click_on_npc", self, waiting_dblclick[0], waiting_dblclick[1]) + last_lmb_dt = 0 + waiting_dblclick = null + +func _process(time): + if task == "walk" or task == "slide": + var to_walk = speed * last_scale.x * time + var pos = get_position() + var old_pos = pos + if walk_path.size() > 0: + while to_walk > 0: + var next + if walk_path.size() > 1: + next = walk_path[path_ofs + 1] + else: + next = walk_path[path_ofs] + + var dist = pos.distance_to(next) + + if dist > to_walk: + var n = (next - pos).normalized() + pos = pos + n * to_walk + break + pos = next + to_walk -= dist + path_ofs += 1 + if path_ofs >= walk_path.size() - 1: + walk_stop(walk_destination) + set_process(false) + return + + var angle = (old_pos.angle_to_point(pos)) * -1 + + set_position(pos) + + if task == "walk": + last_deg = vm._get_deg_from_rad(angle) + last_dir = vm._get_dir_deg(last_deg, animations) + + if animation: + if animation.get_current_animation() != animations.directions[last_dir]: + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir+1] + + # If a z-indexed npc is moved, forcibly update its z index + _update_terrain(true) + +func turn_to(deg): + if deg < 0 or deg > 360: + vm.report_errors("interactive", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + if animation and animations and "directions" in animations: + if !animation.get_current_animation() or animation.get_current_animation() != animations.directions[last_dir]: + # XXX: This requires manually scripting a wait + # and setting the correct idle animation + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir + 1] + _update_terrain() + +func set_angle(deg): + if deg < 0 or deg > 360: + # Compensate for savegame files during a broken version of Escoria + if vm.loading_game: + vm.report_warnings("interactive", ["Reset invalid degree " + str(deg)]) + deg = 0 + else: + vm.report_errors("interactive", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + if animation and animations and "idles" in animations: + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func _find_sprites(p = null): + if p is CanvasItem: + sprites.push_back(p) + for i in range(0, p.get_child_count()): + _find_sprites(p.get_child(i)) + +func _ready(): + add_to_group("npc") + add_to_group("highlight_tooltip") + + if has_node("../terrain"): + terrain = $"../terrain" + terrain_is_scalenodes = terrain is preload("terrain_scalenodes.gd") + + if interact_position: + interact_pos = get_node(interact_position) + elif has_node("interact_pos"): + interact_pos = $"interact_pos" + + if has_node("camera_pos"): + camera_pos = $"camera_pos" + + _find_sprites(self) + + # Initialize Node2D items' terrain status like z-index. + # Stationary items will be set up correctly and + # if an npc moves, it will handle this in its _process() loop + _update_terrain(true) + + if Engine.is_editor_hint(): + return + + vm = $"/root/vm" + + var conn_err + + # {{{ Check for interaction area and connect signals only if the npc is interactive + area = get_node("area") + + conn_err = area.connect("input_event", self, "area_input") + if conn_err: + vm.report_errors("npc", ["area.input_event -> area_input error: " + String(conn_err)]) + + conn_err = area.connect("mouse_entered", self, "mouse_enter") + if conn_err: + vm.report_errors("npc", ["mouse_entered -> mouse_enter error: " + String(conn_err)]) + + conn_err = area.connect("mouse_exited", self, "mouse_exit") + if conn_err: + vm.report_errors("npc", ["mouse_exited -> mouse_exit error: " + String(conn_err)]) + + conn_err = connect("left_click_on_npc", $"/root/scene/game", "ev_left_click_on_npc") + if conn_err: + vm.report_errors("npc", ["left_click_on_npc -> ev_left_click_on_npc error: " + String(conn_err)]) + + conn_err = connect("left_dblclick_on_npc", $"/root/scene/game", "ev_left_dblclick_on_npc") + if conn_err: + vm.report_errors("npc", ["left_dblclick_on_npc -> ev_left_dblclick_on_npc error: " + String(conn_err)]) + + conn_err = connect("right_click_on_npc", $"/root/scene/game", "ev_right_click_on_npc") + if conn_err: + vm.report_errors("npc", ["right_click_on_npc -> ev_right_click_on_npc error: " + String(conn_err)]) + + conn_err = connect("mouse_enter_npc", $"/root/scene/game", "ev_mouse_enter_npc") + if conn_err: + vm.report_errors("npc", ["mouse_enter_npc -> ev_mouse_enter_npc error: " + String(conn_err)]) + + conn_err = connect("mouse_exit_npc", $"/root/scene/game", "ev_mouse_exit_npc") + if conn_err: + vm.report_errors("npc", ["mouse_exit_npc -> ev_mouse_exit_npc error: " + String(conn_err)]) + # }}} + + if events_path != "": + event_table = vm.compile(events_path) + + # Forbit pipe because it's used to separate flags from actions, like in `:use item | TK`. And space for good measure. + for c in ["|", " "]: + if c in global_id: + vm.report_errors("npc", ["Forbidden character '" + c + "' in global_id: " + global_id]) + + if not has_node("sprite"): + vm.report_errors("npc", ["No sprite node set"]) + + if not has_node("collision"): + vm.report_errors("npc", ["No collision node set"]) + + if has_node("animation"): + animation = $"animation" + conn_err = animation.connect("animation_finished", self, "anim_finished") + if conn_err: + vm.report_errors("npc", ["animation_finished -> anim_finished error: " + String(conn_err)]) + + if has_node("audio"): + audio = $"audio" + audio.set_bus("SFX") + + _check_focus(false, false) + + vm.register_object(global_id, self) + + diff --git a/device/globals/player.gd b/device/globals/player.gd new file mode 100644 index 0000000..ab95ba2 --- /dev/null +++ b/device/globals/player.gd @@ -0,0 +1,542 @@ +tool + +extends KinematicBody2D + +var task +var walk_destination +var animation +var vm # A tool script cannot refer to singletons in Godot +var terrain +var terrain_is_scalenodes +var walk_path +var walk_context +var moved +var path_ofs +export var speed = 300 +export var v_speed_damp = 1.0 +export(Script) var animations +#warning-ignore:unused_class_variable +export(Color) var dialog_color = null +var last_deg = null +var last_dir = 0 +var last_scale +var pose_scale = 1 +var params_queue + +export var telekinetic = false + +var check_maps # set by lightmap_area.gd + +var orig_speed + +var anim_notify = null +var anim_scale_override = null +var sprites = [] +export var placeholders = {} + +# Use `interact_status` to prevent player from moving past target when interacting. +# This appears to happen because Godot doesn't discern a click from a double click +# until the second click happens, by which time the player is already underway to +# an invalid position. +var interact_status = 0 +enum interact_statuses {INTERACT_NONE, INTERACT_STARTED, INTERACT_WALKING} + +func get_camera_pos(): + if has_node("camera_pos"): + return $"camera_pos".global_position + + return global_position + +func get_dialog_pos(): + if has_node("dialog_pos"): + return $"dialog_pos".global_position + + return global_position + +func resolve_angle_to(obj): + # Set `last_deg` and `last_dir` as they are globals + var angle = self.position.angle_to_point(obj.position) * -1 + last_deg = vm._get_deg_from_rad(angle) + # printt("Resolve angle from ", self.position, " to ", obj.position, ":", last_deg) + last_dir = vm._get_dir_deg(last_deg, animations) + +func set_active(p_active): + if p_active: + show() + else: + hide() + +func walk_to(pos, context = null): + if not terrain: + return walk_stop(get_position()) + + if interact_status == interact_statuses.INTERACT_WALKING: + return + if interact_status == interact_statuses.INTERACT_STARTED: + interact_status = interact_statuses.INTERACT_WALKING + walk_path = terrain.get_terrain_path(get_position(), pos) + walk_context = context + if walk_path.size() == 0: + task = null + walk_stop(get_position()) + set_process(false) + return + moved = true + walk_destination = walk_path[walk_path.size()-1] + if terrain.is_solid(pos): + walk_destination = walk_path[walk_path.size()-1] + path_ofs = 0.0 + task = "walk" + set_process(true) + +func walk(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + walk_to(pos, context) + +func anim_finished(anim_name): + # Ignore the signal if the previous animation finished while we're actually playing another one + if animation.get_current_animation() and anim_name != animation.get_current_animation(): + # printt("player: anim_finished: ignore", anim_name, animation.get_current_animation()) + return + + if anim_notify != null: + vm.finished(anim_notify) + anim_notify = null + + if anim_scale_override != null: + .set_scale(.get_scale() * anim_scale_override) + anim_scale_override = null + + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func set_speaking(p_speaking): + if p_speaking: + animation.play(animations.speaks[last_dir]) + pose_scale = animations.speaks[last_dir + 1] + else: + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + +func _find(p_val, p_array, p_flip): + var i = 0 + for v in p_array: + if typeof(v) == typeof(p_val) && v == p_val: + if p_flip == null: + return i + else: + if p_array[i+1] == p_flip: + return i + + i += 1 + return -1 + +func anim_get_ph_paths(p_anim): + if !(p_anim in placeholders): + return null + + var ret = [] + for p in placeholders[p_anim]: + var n = get_node(p) + if !(n is InstancePlaceholder): + continue + ret.push_back(n.get_instance_path()) + return ret + +func play_anim(p_anim, p_notify = null, p_reverse = false, p_flip = null): + if not animation: + vm.report_errors("player", ["Animation not found for play_anim"]) + if not animation.has_animation(p_anim): + vm.report_errors("player", [animation.name + " does not contain " + p_anim]) + + if p_notify != null && (!animation || !animation.has_animation(p_anim)): + vm.finished(p_notify) + return + + if p_anim in placeholders: + for npath in placeholders[p_anim]: + var node = get_node(npath) + if !(node is InstancePlaceholder): + continue + var path = node.get_instance_path() + var res = vm.res_cache.get_resource(path) + node.replace_by_instance(res) + _find_sprites(get_node(npath)) + + + pose_scale = 1 + _update_terrain() + if p_flip != null: + var s = .get_scale() + .set_scale(s * p_flip) + anim_scale_override = p_flip + else: + anim_scale_override = null + + if p_reverse: + animation.play(p_anim, -1, -1, true) + else: + animation.play(p_anim) + + anim_notify = p_notify + var dir = _find(p_anim, animations.directions, p_flip.x) + if dir == -1: + dir = _find(p_anim, animations.idles, p_flip.x) + if dir != -1: + last_dir = dir + + set_process(false) + +func interact(p_params): + interact_status = interact_statuses.INTERACT_STARTED + var obj = p_params[0] + var action = p_params[1] + var pos + if obj.has_method("get_interact_pos"): + pos = obj.get_interact_pos() + else: + pos = obj.get_global_position() + + # Check if we are using an item with another, lest we fall back to fallbacks' :use + # so this reworks action to match `:use inv_ice_cream` + if p_params.size() > 2 and p_params[2]: + var target = p_params[2].global_id + action += " " + target + + # It's safe to assume false, because it most likely gets reset + # or we pass control over to fallbacks in game.interact() + var do_walk = false + var telekinetic_action = false + if action in obj.event_table: + var ev_flags = obj.event_table[action]["ev_flags"] + + # Triggering a telekinetic default action by double-clicking will set + # the player walking, so stop as immediately as possible. It would be + # better if Godot waited a while to see if the first click of a double-click + # is a click or should it be handled as a double-click. + if not "TK" in ev_flags: + do_walk = true + else: + telekinetic_action = true + if walk_path: + walk_stop(walk_path[0]) + else: + walk_stop(get_position()) + + if not walk_context: + walk_context = {"fast": true} + else: + walk_context["fast"] = true + do_walk = true + + if (not telekinetic and do_walk) and get_global_position().distance_to(pos) > 10: + # It's important to set the queue before walking, so it + # is in effect until walk_stop() has to reset the queue. + params_queue = p_params + walk_to(pos, walk_context) + else: + if animations.dir_angles.size() > 0: + if not telekinetic_action and obj.interact_angle != -1: + last_dir = vm._get_dir_deg(obj.interact_angle, animations) + else: + resolve_angle_to(obj) + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", p_params) + +func slide_to(pos, context = null): + # Assume a straight line, and leverage some walk functionality + walk_path = [get_position(), pos] + walk_context = context + if walk_path.size() == 0: + walk_stop(get_position()) + set_process(false) + task = null + return + + moved = true + + walk_destination = walk_path[walk_path.size()-1] + + path_ofs = 0.0 + task = "slide" + set_process(true) + +func slide(pos, p_speed, context = null): + if p_speed: + orig_speed = speed + speed = p_speed + slide_to(pos, context) + +func halt(): + return walk_stop(self.global_position) + +func walk_stop(pos): + set_position(pos) + interact_status = interact_statuses.INTERACT_NONE + walk_path = [] + + if orig_speed: + speed = orig_speed + orig_speed = null + + task = null + moved = false + set_process(false) + if params_queue != null: + if animations.dir_angles.size() > 0: + if params_queue[0].interact_angle == -1: + resolve_angle_to(params_queue[0]) + else: + last_dir = vm._get_dir_deg(params_queue[0].interact_angle, animations) + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + else: + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "game", "interact", params_queue) + # Clear params queue to prevent the same action from being triggered again + params_queue = null + else: + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + _update_terrain() + if walk_context != null: + vm.finished(walk_context) + walk_context = null + + +func _notification(what): + if !is_inside_tree() || !Engine.is_editor_hint(): + return + + if what == CanvasItem.NOTIFICATION_TRANSFORM_CHANGED: + call_deferred("_editor_transform_changed") + +func _editor_transform_changed(): + _update_terrain() + _check_bounds() + +func _check_bounds(): + if !terrain: + return + + #printt("checking bouds for pos ", get_position(), terrain.is_solid(get_position())) + if terrain.is_solid(get_position()): + if has_node("terrain_icon"): + get_node("terrain_icon").hide() + else: + if !has_node("terrain_icon"): + var node = Sprite.new() + var tex = load("res://globals/terrain.png") + node.set_texture(tex) + add_child(node) + node.set_name("terrain_icon") + get_node("terrain_icon").show() + +func _update_terrain(): + if !terrain: + return + + var pos = get_position() + z_index = pos.y if pos.y <= VisualServer.CANVAS_ITEM_Z_MAX else VisualServer.CANVAS_ITEM_Z_MAX + + var color + if terrain_is_scalenodes: + last_scale = terrain.get_terrain(pos) + self.scale = last_scale + elif check_maps: + color = terrain.get_terrain(pos) + var scal = terrain.get_scale_range(color.b) + if scal != get_scale(): + last_scale = scal + self.scale = last_scale + + # Do not flip the entire player character, because that would conflict + # with shadows that expect to be siblings of $"sprite" + if pose_scale == -1 and $"sprite".scale.x > 0: + $"sprite".scale.x *= pose_scale + $"collision".scale.x *= pose_scale + elif pose_scale == 1 and $"sprite".scale.x < 0: + $"sprite".scale.x *= -1 + $"collision".scale.x *= -1 + + if check_maps: + color = terrain.get_light(pos) + + if color: + for s in sprites: + s.set_modulate(color) + +func _process(time): + if task == "walk" or task == "slide": + var pos = get_position() + var old_pos = pos + var next + if walk_path.size() > 1: + next = walk_path[path_ofs + 1] + else: + next = walk_path[path_ofs] + + var dist = speed * time * pow(last_scale.x, 2) * terrain.player_speed_multiplier + if walk_context and "fast" in walk_context and walk_context.fast: + dist *= terrain.player_doubleclick_speed_multiplier + var dir = (next - pos).normalized() + + # assume that x^2 + y^2 == 1, apply v_speed_damp the y axis + #printt("dir before", dir) + dir = dir * (dir.x * dir.x + dir.y * dir.y * v_speed_damp) + #printt("dir after", dir, dist) + + var new_pos + if pos.distance_to(next) < dist: + new_pos = next + path_ofs += 1 + else: + new_pos = pos + dir * dist + + if path_ofs >= walk_path.size() - 1: + walk_stop(walk_destination) + return + + pos = new_pos + + var angle = (old_pos.angle_to_point(pos)) * -1 + set_position(pos) + + if task == "walk": + last_deg = vm._get_deg_from_rad(angle) + last_dir = vm._get_dir_deg(last_deg, animations) + + if animation.get_current_animation() != animations.directions[last_dir]: + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir+1] + + _update_terrain() + else: + moved = false + + +func teleport(obj, angle=null): + if animations.dir_angles.size() > 0 and not angle: + if "interact_angle" in obj and obj.interact_angle != -1: + last_deg = obj.interact_angle + last_dir = vm._get_dir_deg(obj.interact_angle, animations) + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir + 1] + elif angle: + set_angle(angle) + + var pos + if obj.has_method("get_interact_pos"): + pos = obj.get_interact_pos() + else: + pos = obj.get_global_position() + + set_position(pos) + moved = true + _update_terrain() + +func set_state(costume): + # Set a costume-state by changing the AnimationPlayer. + if has_node(costume): + animation = get_node(costume) + else: + vm.report_errors("player", ["Costume AnimationPlayer '" + costume + "' not found"]) + + animation.play(animations.idles[last_dir]) + +func teleport_pos(x, y, angle=null): + set_position(Vector2(x, y)) + if angle: + set_angle(angle) + moved = true + _update_terrain() + +func turn_to(deg): + if deg < 0 or deg > 360: + vm.report_errors("player", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + if animation.get_current_animation() != animations.directions[last_dir]: + animation.play(animations.directions[last_dir]) + pose_scale = animations.directions[last_dir+1] + _update_terrain() + +func set_angle(deg): + if deg < 0 or deg > 360: + # Compensate for savegame files during a broken version of Escoria + if vm.loading_game: + vm.report_warnings("player", ["Reset invalid degree " + str(deg)]) + deg = 0 + else: + vm.report_errors("player", ["Invalid degree to turn to " + str(deg)]) + + moved = true + + last_deg = deg + last_dir = vm._get_dir_deg(deg, animations) + + # The player may have a state animation from before, which would be + # resumed, so we immediately force the correct idle animation + if animation.get_current_animation() != animations.idles[last_dir]: + animation.play(animations.idles[last_dir]) + pose_scale = animations.idles[last_dir+1] + _update_terrain() + + +func _find_sprites(p = null): + if p is CanvasItem: + sprites.push_back(p) + for i in range(0, p.get_child_count()): + _find_sprites(p.get_child(i)) + +func _ready(): + add_to_group("player") + + if has_node("../terrain"): + terrain = $"../terrain" + terrain_is_scalenodes = terrain is preload("terrain_scalenodes.gd") + + _find_sprites(self) + + # Set the player up for z-index, scale, light etc, update later when moving + _update_terrain() + + if Engine.is_editor_hint(): + return + + vm = $"/root/vm" + + if not has_node("sprite"): + vm.report_errors("player", ["No sprite node set"]) + + if not has_node("collision"): + vm.report_errors("player", ["No collision node set"]) + + if has_node("default"): + animation = $"default" + + if not animations: + vm.report_errors("player", ["Animations not set for player."]) + else: + vm.report_errors("player", ["No default AnimationPlayer found"]) + + for child in get_children(): + if child is AnimationPlayer: + child.connect("animation_finished", self, "anim_finished") + + vm.register_object("player", self) + + last_scale = .get_scale() + set_process(true) diff --git a/device/globals/resource_queue.gd b/device/globals/resource_queue.gd new file mode 100644 index 0000000..f10aaa0 --- /dev/null +++ b/device/globals/resource_queue.gd @@ -0,0 +1,190 @@ +var thread +var mutex +var sem + +#warning-ignore:unused_class_variable +var time_max = 100 # msec + +var queue = [] +var pending = {} + +#warning-ignore:unused_argument +func _lock(caller): + mutex.lock() + +#warning-ignore:unused_argument +func _unlock(caller): + mutex.unlock() + +#warning-ignore:unused_argument +func _post(caller): + sem.post() + +#warning-ignore:unused_argument +func _wait(caller): + sem.wait() + + +func queue_resource(path, p_in_front = false, p_permanent = false): + _lock("queue_resource") + if path in pending: + _unlock("queue_resource") + return + + elif ResourceLoader.has(path): + var res = ResourceLoader.load(path) + pending[path] = { "res": res, "permanent": p_permanent } + _unlock("queue_resource") + return + else: + var res = ResourceLoader.load_interactive(path) + res.set_meta("path", path) + if p_in_front: + queue.insert(0, res) + else: + queue.push_back(res) + pending[path] = { "res": res, "permanent": p_permanent } + _post("queue_resource") + _unlock("queue_resource") + return + +func cancel_resource(path): + _lock("cancel_resource") + if path in pending: + if pending[path].res is ResourceInteractiveLoader: + queue.erase(pending[path].res) + pending.erase(path) + _unlock("cancel_resource") + +func clear(): + _lock("clear") + + for p in pending.keys(): + if pending[p].permanent: + continue + cancel_resource(p) + #queue = [] + #pending = {} + + _unlock("clear") + + +func get_progress(path): + _lock("get_progress") + var ret = -1 + if path in pending: + if pending[path].res is ResourceInteractiveLoader: + ret = float(pending[path].res.get_stage()) / float(pending[path].res.get_stage_count()) + else: + ret = 1.0 + emit_signal("resource_loading_done", path) + emit_signal("resource_loading_progress", path, ret) + _unlock("get_progress") + + return ret + +func is_ready(path): + var ret + _lock("is_ready") + if path in pending: + ret = !(pending[path].res is ResourceInteractiveLoader) + else: + ret = false + + _unlock("is_ready") + + return ret + +func _wait_for_resource(res, path): + _unlock("wait_for_resource") + while true: + #VisualServer.call("sync") # workaround because sync is a keyword + VisualServer.force_sync() + OS.delay_usec(16000) # wait 1 frame + _lock("wait_for_resource") + if queue.size() == 0 || queue[0] != res: + return pending[path].res + _unlock("wait_for_resource") + + +func get_resource(path): + _lock("get_resource") + if path in pending: + if pending[path].res is ResourceInteractiveLoader: + var res = pending[path].res + if res != queue[0]: + var pos = queue.find(res) + queue.remove(pos) + queue.insert(0, res) + + res = _wait_for_resource(res, path) + + if !pending[path].permanent: + pending.erase(path) + _unlock("return") + return res + + else: + var res = pending[path].res + if !pending[path].permanent: + pending.erase(path) + _unlock("return") + return res + else: + _unlock("return") + return ResourceLoader.load(path) + +func thread_process(): + _wait("thread_process") + + _lock("process") + + while queue.size() > 0: + var res = queue[0] + + _unlock("process_poll") + var ret = res.poll() + _lock("process_check_queue") + + var path = res.get_meta("path") + if ret == ERR_FILE_EOF || ret != OK: + printt("finished loading ", path) + if path in pending: # else it was already retrieved + pending[res.get_meta("path")].res = res.get_resource() + + queue.erase(res) # something might have been put at the front of the queue while we polled, so use erase instead of remove + emit_signal("resource_queue_progress", queue.size()) + + get_progress(path) + + _unlock("process") + +#warning-ignore:unused_argument +func thread_func(u): + while true: + thread_process() + +func print_progress(p_path, p_progress): + printt(p_path, "loading", round(p_progress * 100), "%") + +func res_loaded(p_path): + printt("loaded resource", p_path) + +func print_queue_progress(p_queue_size): + printt("queue size:", p_queue_size) + +func start(): + mutex = Mutex.new() + sem = Semaphore.new() + thread = Thread.new() + thread.start(self, "thread_func", 0) + + add_user_signal("resource_loading_progress", ["path", "progress"]) + add_user_signal("resource_loading_done", ["path"]) + add_user_signal("resource_queue_progress", ["queue_size"]) + + ## Uncomment these for debug, or wait for someone to implement log levels + # connect("resource_loading_progress", self, "print_progress") + # connect("resource_loading_done", self, "res_loaded") + # connect("resource_queue_progress", self, "print_queue_progress") + diff --git a/device/globals/save_data.gd b/device/globals/save_data.gd new file mode 100644 index 0000000..ca76a15 --- /dev/null +++ b/device/globals/save_data.gd @@ -0,0 +1,202 @@ + +const DATA_STRING = 0 +const DATA_STRING_ARRAY = 1 +const DATA_VARIANT = 2 + +var base = "user://esc_saves" +var slots = {} +var max_slots = 3 +var settings + +func save_settings(p_data, p_callback): + var f = File.new() + f.open("user://settings.bin", File.WRITE) + f.store_var(p_data) + f.close() + + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], OK) + + return OK + +func load_settings(p_callback): + var f = File.new() + f.open("user://settings.bin", File.READ) + if !f.is_open(): + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], null) + return FAILED + + settings = f.get_var() + f.close() + + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], settings) + + return OK + +func _get_fname(p_slot): + + var date = OS.get_date() + var time = OS.get_time() + + var day = str(date.day) + if date.day < 10: + day = "0"+day + + + var hour = str(time.hour) + if time.hour < 10: + hour = "0"+hour + + var minute = str(time.minute) + if time.minute < 10: + minute = "0"+minute + + var second = str(time.second) + if time.second < 10: + second = "0"+second + + var fname = str(p_slot) + "-" + fname = fname + day + "-" + str(date.month) + "-" + str(date.year) + " " + hour+"."+minute+"."+second+".esc" + + return fname + + +func save_game(p_data, p_slot, p_callback): + + if p_slot < 0 || p_slot >= max_slots: + return FAILED + + var fname = _get_fname(p_slot) + var ret = _do_save(base + "/" + fname, p_data) + if ret != OK: + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], FAILED) + return FAILED + + if p_slot in slots: + var old_fname = slots[p_slot].fname + var d = Directory.new() + d.open(base) + d.remove(old_fname) + + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], OK) + return OK + +func _do_save(fname, p_data): + var f = File.new() + var ret = f.open(fname, File.WRITE) + if ret or not f.is_open(): + print("Unable to open file for save ", fname) + return FAILED + + if typeof(p_data) == typeof([]): + for s in p_data: + f.store_string(s) + else: + f.store_string(p_data) + + f.close() + printt("Saved game to " + fname) + + return OK + +func load_slot(p_slot, p_callback): + if p_callback == null: + return FAILED + + if !(p_slot in slots): + return FAILED + + var data = _do_load(slots[p_slot].fname) + if !data: + return FAILED + + p_callback[0].call_deferred(p_callback[1], data) + + return OK + +func load_autosave(p_callback): + if p_callback == null: + return FAILED + + var data = _do_load("user://quick_save.esc") + if data == null: + return FAILED + + p_callback[0].call_deferred(p_callback[1], data) + + return OK + + +func _do_load(fname): + + var f = File.new() + if !f.file_exists(fname): + return null + + f.open(fname, File.READ) + var data = f.get_as_text() + f.close() + + return data + + +func autosave(p_data, p_callback): + var err = _do_save("user://quick_save.esc", p_data) + + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], err) + + return err + +func get_slots_available(p_callback): + + if p_callback == null: + return FAILED + + var d = Directory.new() + d.open("user://") + if !d.dir_exists(base): + d.make_dir(base) + d.open(base) + + + d.list_dir_begin() + var f = d.get_next() + while f != "": + + if f.find(".esc") < 0 || f.find("-") < 0: + f = d.get_next() + continue + + var sep = f.find("-") + var n = int(f.substr(0, sep)) + if n >= max_slots: + f = d.get_next() + continue + + var t = f.replace(".esc", "") + t = t.substr(2, t.length()-2) + var l = t.split(" ") + var h = l[1] + var date = l[0] + + slots[n] = { "n": n, "fname": base + "/" + f, "date": date, "hour": h } + + f = d.get_next() + + d.list_dir_end() + + p_callback[0].call_deferred(p_callback[1], slots) + + return OK + +func autosave_available(): + var f = File.new() + return f.file_exists("user://quick_save.esc") + +func start(): + pass diff --git a/device/globals/save_data_icloud.gd b/device/globals/save_data_icloud.gd new file mode 100644 index 0000000..4fab46a --- /dev/null +++ b/device/globals/save_data_icloud.gd @@ -0,0 +1,173 @@ +var ICloud = null + +const DATA_STRING = 0 +const DATA_STRING_ARRAY = 1 +const DATA_VARIANT = 2 + +var base = "user://dmpb_saves" +var slots = {} +var max_slots = 3 +var settings + +func _get_fname(p_slot): + + var date = OS.get_date() + var time = OS.get_time() + + var day = str(date.day) + if date.day < 10: + day = "0"+day + + + var hour = str(time.hour) + if time.hour < 10: + hour = "0"+hour + + var minute = str(time.minute) + if time.minute < 10: + minute = "0"+minute + + var second = str(time.second) + if time.second < 10: + second = "0"+second + + var fname = str(p_slot) + "-" + fname = fname + day + "-" + str(date.month) + "-" + str(date.year) + " " + hour+"."+minute+"."+second+".esc" + + return fname + + +func save_settings(p_data, p_callback): + + ICloud.set_key_values({"settings": p_data}) + + if p_callback != null: + p_callback[0].call_deferred(p_callback[1], OK) + + return OK + +func load_settings(p_callback): + + var settings = ICloud.get_key_value("settings") + if settings == null: + settings = {} + + p_callback[0].call_deferred(p_callback[1], settings) + + return OK + +func _get_as_string(p_arr): + # find a way to do this faster + var ret = "" + for s in p_arr: + ret += s + + return ret + +func save_game(p_data, p_slot, p_callback): + + if p_slot < 0 || p_slot >= max_slots: + return FAILED + + if typeof(p_data) != typeof(""): + p_data = _get_as_string(p_data) + + var fname = _get_fname(p_slot) + + var skey = "slot_"+str(p_slot) + var path = str(base) + "/" + str(fname) + var vals = {} + vals[skey] = { "fname": path, "data": p_data } + var ret = ICloud.set_key_values(vals) + + if typeof(p_callback) != typeof(null): + p_callback[0].call_deferred(p_callback[1], OK) + return OK + +func autosave(p_data, p_callback): + var data = _get_as_string(p_data) + var ret = ICloud.set_key_values({"autosave": data}) + + var err = OK + if ret.size() > 0: + err = FAILED + + if p_callback != null: + p_callback[0].call_deferred(p_callback[1], err) + + return err + + +func load_slot(p_slot, p_callback): + + if p_slot < 0 || p_slot >= max_slots: + return FAILED + + if p_callback == null: + return FAILED + + var skey = "slot_"+str(p_slot) + var data = ICloud.get_key_value(skey) + + if data == null: + return FAILED + + p_callback[0].call_deferred(p_callback[1], data.data) + + return OK + +func load_autosave(p_callback): + if p_callback == null: + return FAILED + + var data = ICloud.get_key_value("autosave") + + if data == null: + return FAILED + + p_callback[0].call_deferred(p_callback[1], data) + + return OK + +func get_slots_available(p_callback): + + if p_callback == null: + return FAILED + + var skeys = ["slot_0", "slot_1", "slot_2"] + + for key in skeys: + + var slot = ICloud.get_key_value(key) + if slot == null: + continue + + var f = slot.fname + f = f.get_file() + var sep = f.find("-") + var n = int(f.substr(0, sep)) + if n >= max_slots: + continue + + var t = f.replace(".esc", "") + t = t.substr(2, t.length()-2) + var l = t.split(" ") + if l.size() < 2: + printt("invalid file for slot", n, slot.fname) + continue + var h = l[1] + var date = l[0] + + slots[n] = { "n": n, "fname": base + "/" + f, "date": date, "hour": h } + + p_callback[0].call_deferred(p_callback[1], slots) + + return OK + +func autosave_available(): + var data = ICloud.get_key_value("autosave") + return data != null + +func start(): + ICloud = ProjectSettings.get_setting("ICloud") + pass diff --git a/device/globals/scalenode.gd b/device/globals/scalenode.gd new file mode 100644 index 0000000..6e28300 --- /dev/null +++ b/device/globals/scalenode.gd @@ -0,0 +1,5 @@ +extends Position2D + +#warning-ignore:unused_class_variable +export(Vector2) var target_scale = Vector2(1.0, 1.0) + diff --git a/device/globals/scene.gd b/device/globals/scene.gd new file mode 100644 index 0000000..233714c --- /dev/null +++ b/device/globals/scene.gd @@ -0,0 +1,4 @@ +extends "res://globals/scene_base.gd" + +func _ready(): + pass diff --git a/device/globals/scene_base.gd b/device/globals/scene_base.gd new file mode 100644 index 0000000..bdf51db --- /dev/null +++ b/device/globals/scene_base.gd @@ -0,0 +1,41 @@ +extends Node + +export(String, FILE, ".esc") var events_path = "" +#warning-ignore:unused_class_variable +export(float) var default_zoom = 1.0 + +var event_table + +func _ready(): + # Call `set_current_scene` only when not loading the game, because doing so + # when loading the game causes glitches in the events when `set_scene` also + # calls `set_current_scene`. + var run_events = not vm.loading_game + if run_events: + main.call_deferred("set_current_scene", self, run_events) + + if events_path: + event_table = vm.compile(events_path) + +func _input(event): + if event is InputEventKey and event.pressed: + match(event.scancode): + KEY_F12: + OS.print_all_textures_by_size() + + KEY_ESCAPE: + get_tree().quit() + + KEY_F2: + vm.load_file(ProjectSettings.get_setting("escoria/platform/game_start_script")) + vm.run_game() + + KEY_F9: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + + KEY_F10: + Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) + + KEY_F11: + OS.window_fullscreen = !OS.window_fullscreen + diff --git a/device/globals/scene_main.gd b/device/globals/scene_main.gd new file mode 100644 index 0000000..a84b0e3 --- /dev/null +++ b/device/globals/scene_main.gd @@ -0,0 +1,14 @@ +extends "res://globals/scene_base.gd" + +func show_credits(end=false): + var credits + + if not end: + credits = ProjectSettings.get_setting("escoria/ui/credits") + else: + credits = ProjectSettings.get_setting("escoria/ui/end_credits") + + main.load_menu(credits) + +func _ready(): + main.load_menu(ProjectSettings.get_setting("escoria/ui/main_menu")) diff --git a/device/globals/scene_main.tscn b/device/globals/scene_main.tscn new file mode 100644 index 0000000..b279ab9 --- /dev/null +++ b/device/globals/scene_main.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://globals/scene_main.gd" type="Script" id=1] + +[node name="scene_main" type="Control"] +margin_right = 40.0 +margin_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/device/globals/scenes_cache.gd b/device/globals/scenes_cache.gd new file mode 100644 index 0000000..b040415 --- /dev/null +++ b/device/globals/scenes_cache.gd @@ -0,0 +1,7 @@ +const scenes = [ + "res://globals/game.tscn", + "res://actors/dragon/dragon.tscn", + "res://actors/god/god.tscn", + "res://rooms/garden/garden.tscn", + "res://rooms/end_credits/end_credits.tscn", +] diff --git a/device/globals/shadow.gd b/device/globals/shadow.gd new file mode 100644 index 0000000..b526f33 --- /dev/null +++ b/device/globals/shadow.gd @@ -0,0 +1,84 @@ +extends Node2D + +var light = null +var caster = null +var polygon = null + +var in_caster +var light_floor_pos +var vector_to +var alpha +var alpha_calculated + +onready var space = get_world_2d().get_direct_space_state() +onready var perspective_scale = $"perspective_scale" +onready var rotation_node = perspective_scale.get_node("rotation") +onready var shadow = rotation_node.get_node("shadow") + +export (float)var fixed_rotation = 0.0 +export var rotating = true +export var scaling = true + +func start(p_caster, p_polygon): + caster = p_caster + polygon = p_polygon + + light = p_caster.get_parent() + + self.visible = true + + set_process(true) + +func stop(): + self.queue_free() + +func shadow_in_caster(): + for shape in space.intersect_point(self.global_position): + if shape["collider"] == caster: + return true + + return false + +func _process(_delta): + in_caster = shadow_in_caster() + if not in_caster: + self.visible = false + return + elif in_caster and not self.visible: + self.visible = true + + light_floor_pos = light.global_position + Vector2(0, caster.light_y_offset) + vector_to = shadow.global_position - light_floor_pos + + # Recalculate shadow's alpha. + # The closer the light source, the bigger the alpha value + alpha = (caster.max_dist_visible / vector_to.length()) * caster.alpha_coefficient + # TODO: Should have some code to grade off the shadow when close to the polygon's edge + alpha_calculated = clamp(alpha, 0.0, caster.alpha_max) + # printt("ALPHA", alpha_calculated) + shadow.get_material().set_shader_param("alpha_value", alpha_calculated) + if alpha_calculated < 0.1: + shadow.get_material().set_shader_param("alpha_value", 0.0) + return + + # Rotate the shadow so it faces the light + if rotating: + rotation_node.look_at(shadow.global_position - vector_to) + rotation_node.rotate(PI) + else: + rotation_node.rotation_degrees = self.fixed_rotation + 90 + + if get_parent().scale.x < 0: + rotation_node.rotation_degrees -= 180 + 2 * (self.fixed_rotation + 90) + + # Scale the shadow according to its distance to light source + # The closer the light, the shorter its height (down to the minimum size) + if scaling: + shadow.scale.x = pow(vector_to.length(), caster.scale_power) / caster.scale_divide + caster.scale_extra + +func _ready(): + # We are always the "template" for shadows, so we ourselves can't be visible + self.visible = false + + set_process(false) + diff --git a/device/globals/shadow_caster.gd b/device/globals/shadow_caster.gd new file mode 100644 index 0000000..3cd6f03 --- /dev/null +++ b/device/globals/shadow_caster.gd @@ -0,0 +1,106 @@ +extends Area2D + +var light +var shadows = {} +var polygon + +var bkp_mask + +export (bool)var force_light_mask = true + +## These are used in shadow.gd +#warning-ignore:unused_class_variable +export (int)var light_y_offset = 0 +#warning-ignore:unused_class_variable +export (float)var max_dist_visible = 50 +#warning-ignore:unused_class_variable +export (float)var alpha_coefficient = 2.0 +#warning-ignore:unused_class_variable +export (float)var alpha_max = 0.65 +#warning-ignore:unused_class_variable +export (float)var scale_power = 1.2 +#warning-ignore:unused_class_variable +export (float)var scale_divide = 1000.0 +#warning-ignore:unused_class_variable +export (float)var scale_extra = 0.15 + +func change_light_mask(body, cull_mask): + body.light_mask = cull_mask + for child in body.get_children(): + if "light_mask" in child: + change_light_mask(child, cull_mask) + +func get_id(body): + var body_id + + if not "global_id" in body: + assert body is esc_type.PLAYER + body_id = "player" + else: + body_id = body.global_id + + return body_id + +func body_entered(body): + if not light.visible: + return + + if not self.visible: + return + + printt(body.name, " entered ", get_parent().name) + + if force_light_mask and body.light_mask != light.range_item_cull_mask: + bkp_mask = body.light_mask + change_light_mask(body, light.range_item_cull_mask) + + if body.has_node("shadow"): + var body_id = get_id(body) + + shadows[body_id] = body.get_node("shadow").duplicate() + body.add_child(shadows[body_id]) + shadows[body_id].start(self, polygon) + +func body_exited(body): + if not light.visible: + return + + if not self.visible: + return + + var body_id = get_id(body) + + if not body_id in shadows: + vm.report_errors("shadow_caster", [body.name + " leaving " + get_parent().name + " with no shadow"]) + + printt(body.name, " exited ", get_parent().name) + + if force_light_mask and bkp_mask != null: + change_light_mask(body, bkp_mask) + bkp_mask = null + + shadows[body_id].stop() + shadows.erase(body_id) + +func _ready(): + var conn_err + + light = get_parent() + + if not light is Light2D: + vm.report_errors("shadow_caster", ["parent is not Light2D"]) + + for child in get_children(): + if child is CollisionPolygon2D: + polygon = child + break + + conn_err = connect("body_entered", self, "body_entered") + if conn_err: + vm.report_errors("shadow_caster", ["body_entered -> body_entered error: " + String(conn_err)]) + + conn_err = connect("body_exited", self, "body_exited") + if conn_err: + vm.report_errors("item", ["body_exited -> body_exited error: " + String(conn_err)]) + + diff --git a/device/globals/static_shadow_sprite.gd b/device/globals/static_shadow_sprite.gd new file mode 100644 index 0000000..5c5427e --- /dev/null +++ b/device/globals/static_shadow_sprite.gd @@ -0,0 +1,15 @@ +extends Sprite + +onready var character = get_parent() + +var prev_scale_x + +func _process(_delta): + # Compensate for the character flipping + if character.scale.x < 0 and character.scale.x != prev_scale_x: + self.scale.x *= -1 + prev_scale_x = character.scale.x + elif self.scale.x < 0 and character.scale.x > 0: + self.scale.x *= -1 + prev_scale_x = character.scale.x + diff --git a/device/globals/target.gd b/device/globals/target.gd new file mode 100644 index 0000000..e3c354f --- /dev/null +++ b/device/globals/target.gd @@ -0,0 +1,40 @@ +extends Position2D + +export(String) var global_id + +var audio +var camera_pos + +func get_interact_pos(): + return get_global_position() + +func get_camera_pos(): + if camera_pos: + return camera_pos.get_global_position() + + return get_global_position() + +func play_snd(p_snd, p_loop=false): + if !audio: + vm.report_errors("item", ["play_snd called with no audio node"]) + return + + var resource = load(p_snd) + if !resource: + vm.report_errors("item", ["play_snd resource not found " + p_snd]) + return + + audio.stream = resource + audio.stream.set_loop(p_loop) + audio.play() + +func _ready(): + if has_node("camera_pos"): + camera_pos = $"camera_pos" + + if has_node("audio"): + audio = $"audio" + audio.set_bus("SFX") + + vm.register_object(global_id, self) + diff --git a/device/globals/telon.gd b/device/globals/telon.gd new file mode 100644 index 0000000..07c6897 --- /dev/null +++ b/device/globals/telon.gd @@ -0,0 +1,95 @@ +extends Control + +onready var white = $"white" + +var animation +var anim_notify + +export var global_id = "telon" + +func set_input_disabled(p_input_disabled): + $"/root".set_disable_input(p_input_disabled) + vm.set_global("save_disabled", str(p_input_disabled)) + vm.report_warnings("telon", ["Deprecated interface, use 'accept_input NONE' instead"]) + +func game_cleared(): + self.disconnect("tree_exited", vm, "object_exit_scene") + vm.register_object(global_id, self) + +#warning-ignore:unused_argument +func anim_finished(anim_name): + if anim_notify != null: + vm.finished(anim_notify) + anim_notify = null + +func saved(): + ## XXX: This should be implemented somewhere in the game data tree! + #get_node("indicators_anim").play("saved") + pass + +func ui_blocked(): + ## XXX: This should be implemented somewhere in the game data tree! + #get_node("indicators_anim").play("ui_blocked") + pass + +func setup_vm(): + printt("vm on telon is ", vm) + var conn_err = vm.connect("saved", self, "saved") + if conn_err: + vm.report_errors("telon", ["saved -> saved error: " + String(conn_err)]) + printt("connected") + +func rand_seek(p_node = null): + if p_node == null: + p_node = "music" + + var node = get_node(p_node) + + var length = node.get_length() + printt("length is ", length) + if length == 0: + return + + var r = randf() + var pos = length * r + printt("seek to ", pos, r) + + node.seek_pos(pos) + if !node.is_playing(): + node.play() + +func telon_play_anim(p_anim): + # Play animations like `get_tree().call_group("game", "telon_play_anim", "fade_in")` + animation.play(p_anim) + +## Have the unused arguments because API +#warning-ignore:unused_argument +#warning-ignore:unused_argument +func play_anim(p_anim, p_notify = null, p_reverse = false, p_flip = null): + # A simple wrapper that implements the `cut_scene`/`anim` API + anim_notify = p_notify + return telon_play_anim(p_anim) + +func cut_to_black(): + white.visible = true + white.self_modulate = "000000" + white.modulate = "ffffff" + +func cut_to_scene(): + white.visible = false + +func _ready(): + var conn_err + + animation = $"animation" + + vm.register_object(global_id, self) + + conn_err = animation.connect("animation_finished", self, "anim_finished") + if conn_err: + vm.report_errors("telon", ["animation_finished -> anim_finished error: " + String(conn_err)]) + + add_to_group("game") + + call_deferred("setup_vm") + diff --git a/device/globals/telon.tscn b/device/globals/telon.tscn new file mode 100644 index 0000000..7c3fd08 --- /dev/null +++ b/device/globals/telon.tscn @@ -0,0 +1,273 @@ +[gd_scene load_steps=11 format=2] + +[ext_resource path="res://globals/telon.gd" type="Script" id=1] +[ext_resource path="res://globals/white.png" type="Texture" id=2] + +[sub_resource type="Animation" id=1] +resource_name = "catch_input" +length = 0.01 +tracks/0/type = "method" +tracks/0/path = NodePath(".") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"values": [ { +"args": [ true ], +"method": "set_input_catch" +} ] +} + +[sub_resource type="Animation" id=2] +resource_name = "cut_to_black" +length = 0.01 +tracks/0/type = "value" +tracks/0/path = NodePath("white:self_modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 0, +"values": [ Color( 0, 0, 0, 1 ) ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("white:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} +tracks/2/type = "value" +tracks/2/path = NodePath("white:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/keys = { +"times": PoolRealArray( 0, 0.01 ), +"transitions": PoolRealArray( 1, 1 ), +"update": 0, +"values": [ Color( 1, 1, 1, 0 ), Color( 1, 1, 1, 1 ) ] +} + +[sub_resource type="Animation" id=3] +resource_name = "cut_to_scene" +length = 0.01 +tracks/0/type = "value" +tracks/0/path = NodePath("white:self_modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 0, +"values": [ Color( 0, 0, 0, 1 ) ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("white:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} +tracks/2/type = "value" +tracks/2/path = NodePath("white:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/keys = { +"times": PoolRealArray( 0, 0.01 ), +"transitions": PoolRealArray( 1, 1 ), +"update": 0, +"values": [ Color( 1, 1, 1, 1 ), Color( 1, 1, 1, 0 ) ] +} + +[sub_resource type="Animation" id=4] +length = 0.01 +tracks/0/type = "method" +tracks/0/path = NodePath(".") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"values": [ { +"args": [ true ], +"method": "set_input_disabled" +} ] +} + +[sub_resource type="Animation" id=5] +length = 0.01 +tracks/0/type = "method" +tracks/0/path = NodePath(".") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"values": [ { +"args": [ false ], +"method": "set_input_disabled" +} ] +} + +[sub_resource type="Animation" id=6] +tracks/0/type = "value" +tracks/0/path = NodePath("white:self_modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 0, +"values": [ Color( 0, 0, 0, 1 ) ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("white:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} +tracks/2/type = "value" +tracks/2/path = NodePath("white:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/keys = { +"times": PoolRealArray( 0, 1 ), +"transitions": PoolRealArray( 1, 1 ), +"update": 0, +"values": [ Color( 1, 1, 1, 1 ), Color( 1, 1, 1, 0 ) ] +} + +[sub_resource type="Animation" id=7] +tracks/0/type = "value" +tracks/0/path = NodePath("white:self_modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 0, +"values": [ Color( 0, 0, 0, 1 ) ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("white:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} +tracks/2/type = "value" +tracks/2/path = NodePath("white:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/keys = { +"times": PoolRealArray( 0, 1 ), +"transitions": PoolRealArray( 1, 1 ), +"update": 0, +"values": [ Color( 1, 1, 1, 0 ), Color( 1, 1, 1, 1 ) ] +} + +[sub_resource type="Animation" id=8] +length = 0.01 +tracks/0/type = "method" +tracks/0/path = NodePath(".") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"values": [ { +"args": [ false ], +"method": "set_input_catch" +} ] +} + +[node name="telon" type="Control"] +margin_right = 40.0 +margin_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"__editor_plugin_screen__": "2D", +"_edit_use_anchors_": false +} + +[node name="input_catch" type="Control" parent="."] +margin_right = 3860.0 +margin_bottom = 2190.0 +mouse_filter = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="white" type="TextureRect" parent="."] +modulate = Color( 1, 1, 1, 0 ) +self_modulate = Color( 0, 0, 0, 1 ) +margin_left = -37.0 +margin_top = -46.0 +margin_right = 3863.0 +margin_bottom = 2194.0 +mouse_filter = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 +texture = ExtResource( 2 ) +expand = true +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="animation" type="AnimationPlayer" parent="."] +anims/catch_input = SubResource( 1 ) +anims/cut_to_black = SubResource( 2 ) +anims/cut_to_scene = SubResource( 3 ) +anims/disable_input = SubResource( 4 ) +anims/enable_input = SubResource( 5 ) +anims/fade_in = SubResource( 6 ) +anims/fade_out = SubResource( 7 ) +anims/release_input = SubResource( 8 ) diff --git a/device/globals/terrain.gd b/device/globals/terrain.gd new file mode 100644 index 0000000..12a30ff --- /dev/null +++ b/device/globals/terrain.gd @@ -0,0 +1,44 @@ +tool + +extends "terrain_base.gd" + + +export var scale_min = 0.3 +export var scale_max = 1.0 + +func get_scale_range(r): + r = scale_min + (scale_max - scale_min) * r + return Vector2(r, r) + +func get_terrain(pos): + if scales == null || scales.get_data().is_empty(): + return Color(1, 1, 1, 1) + return get_pixel(pos, scales.get_data()) + +func get_pixel(pos, p_image): + if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0: + return Color(1.0, 0.0, 0.0) + + # `get_pixel()` is slow; this is accurate enough + # without interpolating neighboring pixels and accounting for fractions + return p_image.get_pixel(pos.x, pos.y) + +func _draw(): + if typeof(texture) == typeof(null): + return + if debug_mode == 0: + return + #if !get_tree().is_editor_hint(): + # printt("*********no editor hint") + # return + var scale_vect = bitmaps_scale + + var src = Rect2(0, 0, texture.get_width(), texture.get_height()) + var dst = Rect2(0, 0, texture.get_width() * scale_vect.x, texture.get_height() * scale_vect.y) + + draw_texture_rect_region(texture, dst, src) + #draw_texture(texture, Vector2(0, 0)) + +func _ready(): + #path = ImagePathFinder.new() + _update_texture() diff --git a/device/globals/terrain.png b/device/globals/terrain.png new file mode 100644 index 0000000..075e02e Binary files /dev/null and b/device/globals/terrain.png differ diff --git a/device/globals/terrain.png.import b/device/globals/terrain.png.import new file mode 100644 index 0000000..847fb7c --- /dev/null +++ b/device/globals/terrain.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/terrain.png-ee46c6210af3620499ae8a161b974f92.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://globals/terrain.png" +dest_files=[ "res://.import/terrain.png-ee46c6210af3620499ae8a161b974f92.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=false +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/globals/terrain_base.gd b/device/globals/terrain_base.gd new file mode 100644 index 0000000..8bf0296 --- /dev/null +++ b/device/globals/terrain_base.gd @@ -0,0 +1,178 @@ +tool + +extends Navigation2D + +export(Texture) var lightmap setget set_lightmap,get_lightmap +var lightmap_data +export(Texture) var scales setget set_scales,get_scales +export var bitmaps_scale = Vector2(1,1) setget set_bm_scale,get_bm_scale + +#warning-ignore:unused_class_variable +export var player_speed_multiplier = 1.0 # Override player speed in current scene +#warning-ignore:unused_class_variable +export var player_doubleclick_speed_multiplier = 1.5 # Make the player move faster when doubleclicked +export var lightmap_modulate = Color(1, 1, 1, 1) +export(int, "None", "Scales", "Lightmap") var debug_mode = 1 setget debug_mode_updated + +var texture +var img_area +var _texture_dirty = false + +func set_bm_scale(p_scale): + bitmaps_scale = p_scale + _update_texture() + +func get_bm_scale(): + return bitmaps_scale + +func set_lightmap(p_lightmap): + var need_init = (lightmap != p_lightmap) or (lightmap and not lightmap_data) + + lightmap = p_lightmap + + # It's bad enough a new copy is created when reading a pixel, we don't + # also need to get the data for every read to make yet another copy + if need_init: + if lightmap_data: + lightmap_data.unlock() + lightmap_data = lightmap.get_data() + lightmap_data.lock() + + _update_texture() + +func get_lightmap(): + return lightmap + +func set_scales(p_scales): + scales = p_scales + _update_texture() + +func get_scales(): + return scales + +func debug_mode_updated(p_mode): + debug_mode = p_mode + _update_texture() + +func _update_texture(): + if _texture_dirty: + return + + _texture_dirty = true + call_deferred("_do_update_texture") + +func _do_update_texture(): + _texture_dirty = false + if !is_inside_tree(): + return + if !Engine.is_editor_hint(): + return + + if debug_mode == 0: + update() + return + + texture = ImageTexture.new() + if debug_mode == 1: + if scales != null: + #texture.create_from_image(scales) + texture = scales + else: + if lightmap != null: + #texture.create_from_image(lightmap) + texture = lightmap + + update() + + + +func make_local(pos): + pos = pos - get_position() + pos = pos * 1.0 / get_scale() + pos = get_closest_point(pos) + return pos + +func make_global(pos): + pos = pos * get_scale() + pos = pos + get_position() + return pos + +func get_terrain_path(p_src, p_dest): + # printt("get path ", p_src, p_dest) + p_src = make_local(p_src) + p_dest = make_local(p_dest) + + var r_path = get_simple_path(p_src, p_dest, true) + r_path = Array(r_path) + for i in range(0, r_path.size()): + r_path[i] = make_global(r_path[i]) + return r_path + +func is_solid(pos): + pos = pos - get_position() + pos = pos * 1.0 / get_scale() + + var closest = get_closest_point(pos) + return pos == closest + +func _color_mul(a, b): + var c = Color() + c.r = a.r * b.r + c.g = a.g * b.g + c.b = a.b * b.b + c.a = a.a * b.a + return c + +func get_light(pos): + if not lightmap or lightmap.get_data().is_empty(): + return + + return _color_mul(get_pixel(pos, lightmap_data), lightmap_modulate) + +func get_pixel(pos, p_image): + p_image.lock() + + pos = make_local(pos) + pos = pos * 1.0 / bitmaps_scale + + if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0: + return Color() + + var ll = p_image.get_pixel(pos.x, pos.y) + var ndif = Vector2() + ndif.x = pos.x - floor(pos.x) + ndif.y = pos.y - floor(pos.y) + var ur + + img_area = Rect2(0, 0, p_image.get_width(), p_image.get_height()) + + var lr = ll + if ndif.x > 0 && img_area.has_point(Vector2(pos.x+1, pos.y)): + lr = p_image.get_pixel(pos.x+1, pos.y) + #if lr.a < 128: + # lr = ll + ur = lr + + var ul = ll + if ndif.y > 0 && img_area.has_point(Vector2(pos.x, pos.y+1)): + ul = p_image.get_pixel(pos.x, pos.y+1) + #if ul.a < 128: + # ul = ll + ur = ul + + if ndif.x > 0 && ndif.y > 0 && img_area.has_point(Vector2(pos.x+1, pos.y+1)): + var pix = p_image.get_pixel(pos.x+1, pos.y+1) + #if pix.a > 128: + ur = pix + + var bottom = ll.linear_interpolate(lr, ndif.x) + var top + if ur != null: + top = ul.linear_interpolate(ur, ndif.x) + else: + top = ul + + var final = bottom.linear_interpolate(top, ndif.y) + + p_image.unlock() + return final diff --git a/device/globals/terrain_scalenodes.gd b/device/globals/terrain_scalenodes.gd new file mode 100644 index 0000000..1c6b5ba --- /dev/null +++ b/device/globals/terrain_scalenodes.gd @@ -0,0 +1,92 @@ +tool + +extends "terrain_base.gd" + +const DIST_EPSILON = 0.000001 + +var scale_nodes = [] + +onready var scale_min = $"scale_min" +onready var scale_max = $"scale_max" + +func debug_mode_updated(p_mode): + debug_mode = p_mode + + ._update_texture() + +func _do_update_texture(): + _texture_dirty = false + if !is_inside_tree(): + return + if !Engine.is_editor_hint(): + return + + if debug_mode == 0: + update() + return + + texture = ImageTexture.new() + + if lightmap != null: + #texture.create_from_image(lightmap) + texture = lightmap + + update() + +static func sort_by_y(a, b): + return a.global_position.y < b.global_position.y + +# Return a "scale range" immediately based on the interpolated scale size +func get_terrain(pos): + # printt("Called", pos) + var prev + var next + var prev_target + var node_target + for i in range(1, scale_nodes.size()): + prev = scale_nodes[i - 1] + next = scale_nodes[i] + + if prev.global_position.y < pos.y and pos.y < next.global_position.y: + # printt("1:", prev.global_position.y, " < ", pos.y, " and ", pos.y, " < ", next.global_position.y) + prev_target = prev.target_scale.y + node_target = next.target_scale.y + break + + var nodes_dist = next.global_position.y - prev.global_position.y + if nodes_dist < DIST_EPSILON: + nodes_dist = DIST_EPSILON + var interp_dist = (pos.y - prev.global_position.y) / nodes_dist + + var y_1 = Vector2(0, prev_target) + var y_2 = Vector2(0, node_target) + + var interp = y_1.linear_interpolate(y_2, interp_dist) + + return Vector2(interp.y, interp.y) + +func get_pixel(pos, p_image): + if pos.x + 1 >= p_image.get_width() || pos.y + 1 >= p_image.get_height() || pos.x < 0 || pos.y < 0: + return Color(1.0, 0.0, 0.0) + + # `get_pixel()` is slow; this is accurate enough + # without interpolating neighboring pixels and accounting for fractions + return p_image.get_pixel(pos.x, pos.y) + +func _draw(): + if not texture: + return + + if debug_mode == 0: + return + + draw_texture(texture, Vector2(0, 0)) + +func _ready(): + for c in get_children(): + if c is preload("scalenode.gd"): + scale_nodes.push_back(c) + + scale_nodes.sort_custom(self, "sort_by_y") + scale_nodes.push_front(scale_min) + scale_nodes.push_back(scale_max) diff --git a/device/globals/tooltip.gd b/device/globals/tooltip.gd new file mode 100644 index 0000000..d70b363 --- /dev/null +++ b/device/globals/tooltip.gd @@ -0,0 +1,151 @@ +extends Label + +var follow_mouse = ProjectSettings.get_setting("escoria/ui/tooltip_follows_mouse") +var width = float(ProjectSettings.get("display/window/size/width")) +var height = float(ProjectSettings.get("display/window/size/height")) + +var highlight_only = false # Is this tooltip used only for highlighting? +var force_hide_tooltip = false # Used by `set_tooltip_visible` to never show + +var orig_size + +func show(): + set_process_input(true) + + if force_hide_tooltip: + return + + if not self.text: + var errors = ["Trying to show empty tooltip"] + + if vm.hover_object: + errors.push_back("Hovered object: " + vm.hover_object.global_id) + + vm.report_errors("tooltip", errors) + + set_tooltip_visible(true) + +func hide(): + # Although we try to hide implicit magick, there's no point in having text pending there + self.text = "" + + set_tooltip_visible(false) + + set_process_input(false) + +func update(): + if vm.action_menu and vm.action_menu.visible: + return + + var text = "" + + if vm.hover_object: + var tt + var is_actionable = vm.hover_object is esc_type.ITEM or vm.hover_object is esc_type.NPC + + if vm.current_action and vm.current_tool and vm.current_tool != vm.hover_object and is_actionable: + if not "inventory" in vm.hover_object or not vm.hover_object.inventory: + if not vm.inventory or not vm.inventory.blocks_tooltip(): + text = tr(vm.current_action + ".combine_id") + text = text.replace("%2", tr(vm.hover_object.get_tooltip("object2"))) + text = text.replace("%1", tr(vm.current_tool.get_tooltip("object1"))) + else: + text = tr("use.id") + text = text.replace("%1", tr(vm.current_tool.get_tooltip("object1"))) + else: + text = tr(vm.current_action + ".combine_id") + text = text.replace("%2", tr(vm.hover_object.get_tooltip("object2"))) + text = text.replace("%1", tr(vm.current_tool.get_tooltip("object1"))) + elif vm.current_action == "use" and vm.current_tool: + text = tr("use.id") + text = text.replace("%1", tr(vm.current_tool.get_tooltip("object1"))) + else: + tt = vm.hover_object.get_tooltip() + if not "inventory" in vm.hover_object or not vm.hover_object.inventory: + if not vm.inventory or not vm.inventory.blocks_tooltip(): + text = tr(tt) + else: + text = tr(tt) + else: + if vm.current_action and vm.current_tool: + if not vm.hover_object: + text = tr(vm.current_action + ".id") + text = text.replace("%1", tr(vm.current_tool.get_tooltip("object1"))) + + set_tooltip(text) + if text: + show() + if follow_mouse: + var mouse_pos = get_tree().get_root().get_viewport().get_mouse_position() + set_position( mouse_pos) + else: + hide() + +func set_tooltip(text): + if not typeof(text) == TYPE_STRING: + vm.report_errors("tooltip", ["Trying to set tooltip of type: " + str(typeof(text))]) + + if force_hide_tooltip: + if self.visible: + vm.report_errors("tooltip", ["Forcibly hidden tooltip visible while trying to set text: " + text]) + return + + self.text = text + self.rect_size = orig_size + +func set_tooltip_visible(p_visible): + self.visible = p_visible and self.text and not force_hide_tooltip + +func force_tooltip_visible(p_force_hide_tooltip): + # Not visible (false) means it's hidden + force_hide_tooltip = not p_force_hide_tooltip + printt("force-hide tooltip:", force_hide_tooltip) + if force_hide_tooltip: + self.hide() + +func set_visible(p_visible): + visible = p_visible + +func _clamp(tt_pos): + var tt_size = self.get_size() + var center_offset = tt_size.x / 2 + + # We want to have the center of the tooltip above where the cursor is, compensate first + tt_pos.x -= center_offset # Shift it half-way to the left + tt_pos.y -= tt_size.y # Shift it one size up + + var dist_from_right = width - (tt_pos.x + tt_size.x) # Check if the right edge, not eg. center, is overflowing + var dist_from_left = tt_pos.x + var dist_from_bottom = height - (tt_pos.y + tt_size.y) + var dist_from_top = tt_pos.y + + ## XXX: Godot has serious issues with the width of the text, so tooltips need + ## to be wide at a fixed size, which makes clamping a bit weird. + ## The code is left here in case someone fixes Godot. + if dist_from_right < 0: + tt_pos.x += dist_from_right + if dist_from_left < 0: + tt_pos.x -= dist_from_left + if dist_from_bottom < 0: + tt_pos.y += dist_from_bottom + if dist_from_top < 0: + tt_pos.y -= dist_from_top + + return tt_pos + +func set_position(pos, keep_margins=false): + rect_global_position= _clamp(pos) + +func _input(ev): + if follow_mouse: + # Must verify `position` is there, key inputs do not have it + if "position" in ev: + self.set_position(ev.position) + +func _ready(): + orig_size = self.rect_size + + if not highlight_only: + vm.register_tooltip(self) + + set_process_input(false) diff --git a/device/globals/trigger.gd b/device/globals/trigger.gd new file mode 100644 index 0000000..91418f9 --- /dev/null +++ b/device/globals/trigger.gd @@ -0,0 +1,136 @@ +extends "res://globals/interactive.gd" + +signal left_click_on_trigger +signal left_dblclick_on_trigger +signal right_click_on_trigger +signal mouse_enter_trigger +signal mouse_exit_trigger + +export var tooltip = "" +export var dblclick_teleport = true + +# Superior doubleclick +var last_lmb_dt = 0 +var waiting_dblclick = false + +func get_tooltip(hint=null): + if TranslationServer.get_locale() == ProjectSettings.get_setting("escoria/platform/development_lang"): + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("exit", ["Missing global_id in exit with tooltip '" + tooltip + "'"]) + return tooltip + + var tooltip_identifier = global_id + ".tooltip" + if hint: + tooltip_identifier += "." + hint + + var translated = tr(tooltip_identifier) + + # Try again if there's no translation for this hint + if translated == "\"": + tooltip_identifier = global_id + ".tooltip" + translated = tr(tooltip_identifier) + + if translated == tooltip_identifier: + if not global_id and ProjectSettings.get_setting("escoria/platform/force_tooltip_global_id"): + vm.report_errors("exit", ["Missing global_id in exit with tooltip '" + tooltip + "'"]) + return tooltip_identifier + + return translated + +func mouse_enter(): + emit_signal("mouse_enter_trigger", self) + +func mouse_exit(): + emit_signal("mouse_exit_trigger", self) + +func area_input(_viewport, event, _shape_idx): + if !(event is InputEventMouseButton and event.is_pressed()): + return + + var ev_pos = get_global_mouse_position() + if event.is_action("game_general"): + if last_lmb_dt <= vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_dblclick_on_trigger", self, ev_pos, event) + last_lmb_dt = 0 + waiting_dblclick = null + else: + last_lmb_dt = 0 + waiting_dblclick = [ev_pos, event] + elif event.is_action("game_rmb"): + emit_signal("right_click_on_trigger", self, ev_pos, event) + +func body_entered(body): + if body is esc_type.PLAYER: + if self.visible: + run_event("enter") + +func body_exited(body): + if body is esc_type.PLAYER: + if self.visible: + run_event("exit") + +func run_event(event): + if event in event_table: + vm.run_event(event_table[event]) + +func set_active(p_active): + self.visible = p_active + +func _physics_process(dt): + last_lmb_dt += dt + + if waiting_dblclick and last_lmb_dt > vm.DOUBLECLICK_TIMEOUT: + emit_signal("left_click_on_trigger", self, waiting_dblclick[0], waiting_dblclick[1]) + last_lmb_dt = 0 + waiting_dblclick = null + +func _ready(): + var conn_err + + assert(self is Area2D) + + conn_err = connect("input_event", self, "area_input") + if conn_err: + vm.report_errors("trigger", ["trigger.input_event -> area_input error: " + String(conn_err)]) + + if events_path: + var f = File.new() + if f.file_exists(events_path): + event_table = vm.compile(events_path) + + conn_err = connect("left_click_on_trigger", $"/root/scene/game", "ev_left_click_on_trigger") + if conn_err: + vm.report_errors("trigger", ["left_click_on_trigger -> ev_left_click_on_trigger error: " + String(conn_err)]) + + conn_err = connect("left_dblclick_on_trigger", $"/root/scene/game", "ev_left_dblclick_on_trigger") + if conn_err: + vm.report_errors("trigger", ["left_dblclick_on_trigger -> ev_left_dblclick_on_trigger error: " + String(conn_err)]) + + conn_err = connect("mouse_entered", self, "mouse_enter") + if conn_err: + vm.report_errors("trigger", ["trigger.mouse_entered -> mouse_enter error: " + String(conn_err)]) + + conn_err = connect("mouse_exited", self, "mouse_exit") + if conn_err: + vm.report_errors("trigger", ["trigger.mouse_exited -> mouse_exit error: " + String(conn_err)]) + + conn_err = connect("body_entered", self, "body_entered") + if conn_err: + vm.report_errors("trigger", ["trigger.body_entered -> body_entered error: " + String(conn_err)]) + + conn_err = connect("body_exited", self, "body_exited") + if conn_err: + vm.report_errors("trigger", ["trigger.body_exited -> body_exited error: " + String(conn_err)]) + + conn_err = connect("mouse_enter_trigger", $"/root/scene/game", "ev_mouse_enter_trigger") + if conn_err: + vm.report_errors("trigger", ["mouse_enter_trigger -> ev_mouse_enter_trigger error: " + String(conn_err)]) + + conn_err = connect("mouse_exit_trigger", $"/root/scene/game", "ev_mouse_exit_trigger") + if conn_err: + vm.report_errors("trigger", ["mouse_exit_trigger -> ev_mouse_exit_trigger error: " + String(conn_err)]) + + vm.register_object(global_id, self) + + add_to_group("triggers") + diff --git a/device/globals/vm_level.gd b/device/globals/vm_level.gd new file mode 100644 index 0000000..5ea44fa --- /dev/null +++ b/device/globals/vm_level.gd @@ -0,0 +1,422 @@ +var current_context + +func check_obj(name, cmd): + var obj = vm.get_object(name) + if obj == null: + vm.report_errors("", ["Global id "+name+" not found for " + cmd]) + return false + return true + +func _slide(params, block): + if !check_obj(params[0], "slide"): + return vm.state_return + if !check_obj(params[1], "slide"): + return vm.state_return + var tpos = vm.get_object(params[1]).get_interact_pos() + var speed = 0 + if params.size() > 2: + speed = int(params[2]) + if block: + current_context.waiting = true + vm.get_object(params[0]).slide(tpos, speed, current_context) + return vm.state_yield + else: + vm.get_object(params[0]).slide(tpos, speed) + return vm.state_return + +func _walk(params, block): + if !check_obj(params[0], "walk"): + return vm.state_return + if !check_obj(params[1], "walk"): + return vm.state_return + var tpos = vm.get_object(params[1]).get_interact_pos() + var speed = 0 + if params.size() > 2: + speed = int(params[2]) + if block: + current_context.waiting = true + vm.get_object(params[0]).walk(tpos, speed, current_context) + return vm.state_yield + else: + vm.get_object(params[0]).walk(tpos, speed) + return vm.state_return + +### commands + +func set_global(params): + vm.set_global(params[0], params[1]) + return vm.state_return + +func dec_global(params): + vm.dec_global(params[0], params[1]) + return vm.state_return + +func inc_global(params): + vm.inc_global(params[0], params[1]) + return vm.state_return + +func debug(params): + for p in params: + printt(p) + return vm.state_return + +func anim(params): + if !check_obj(params[0], "anim"): + return vm.state_return + var obj = vm.get_object(params[0]) + var anim_id = params[1] + var reverse = false + if params.size() > 2: + reverse = params[2] + var flip = Vector2(1, 1) + if params.size() > 3 && params[3]: + flip.x = -1 + if params.size() > 4 && params[4]: + flip.y = -1 + obj.play_anim(anim_id, null, reverse, flip) + return vm.state_return + +func play_snd(params): + if !check_obj(params[0], "play_snd"): + return vm.state_return + var obj = vm.get_object(params[0]) + var snd_id = params[1] + var loop = false + if params.size() == 3 and params[2]: + loop = true + obj.play_snd(snd_id, loop) + return vm.state_return + +func set_state(params): + var obj = vm.get_object(params[0]) + if obj != null: + obj.set_state(params[1]) + vm.set_state(params[0], params[1]) + return vm.state_return + +func set_hud_visible(params): + vm.set_hud_visible(params[0]) + +func say(params): + if !check_obj(params[0], "say"): + return vm.state_return + current_context.waiting = true + vm.say(params, current_context) + return vm.state_yield + +func dialog(params): + current_context.waiting = true + current_context.in_dialog = true + vm.dialog(params, current_context) + return vm.state_yield + +func end_dialog(params): + current_context.in_dialog = false + vm.end_dialog(params) + +func cut_scene(params): + if !check_obj(params[0], "cut_scene"): + return vm.state_return + var obj = vm.get_object(params[0]) + var anim_id = params[1] + var reverse = false + if params.size() > 2: + reverse = params[2] + var flip = Vector2(1, 1) + if params.size() > 3 && params[3]: + flip.x = -1 + if params.size() > 4 && params[4]: + flip.y = -1 + current_context.waiting = true + obj.play_anim(anim_id, current_context, reverse, flip) + return vm.state_yield + +func branch(params): + var branch_ev = vm.compiler.EscoriaEvent.new("branch", params, []) + + return vm.add_level(branch_ev, false) + +func inventory_add(params): + vm.inventory_set(params[0], true) + return vm.state_return + +func inventory_remove(params): + vm.inventory_set(params[0], false) + return vm.state_return + +func inventory_open(params): + vm.emit_signal("open_inventory", params[0]) + +func set_active(params): + var obj = vm.get_object(params[0]) + if obj != null: + obj.set_active(params[1]) + vm.set_active(params[0], params[1]) + return vm.state_return + +#warning-ignore:unused_argument +func stop(params): + return vm.state_break + +#warning-ignore:unused_argument +func repeat(params): + return vm.state_repeat + +#warning-ignore:unused_argument +func wait(params): + return vm.wait(params, current_context) + +func set_interactive(params): + var obj = vm.get_object(params[0]) + if obj: + obj.set_interactive(params[1]) + vm.set_interactive(params[0], params[1]) + return vm.state_return + +func set_speed(params): + var obj = vm.get_object(params[0]) + vm.set_speed(obj, params[1]) + +func teleport(params): + if !check_obj(params[0], "teleport"): + return vm.state_return + if !check_obj(params[1], "teleport"): + return vm.state_return + + var angle + if params.size() > 2: + angle = int(params[2]) + + vm.get_object(params[0]).teleport(vm.get_object(params[1]), angle) + return vm.state_return + +func teleport_pos(params): + if !check_obj(params[0], "teleport_pos"): + return vm.state_return + + var angle + if params.size() > 3: + angle = int(params[3]) + + vm.get_object(params[0]).teleport_pos(int(params[1]), int(params[2]), angle) + return vm.state_return + + +func slide(params): + return _slide(params, false) + +func slide_block(params): + return _slide(params, true) + +func walk(params): + return _walk(params, false) + +func walk_block(params): + return _walk(params, true) + +func turn_to(params): + var obj = vm.get_object(params[0]) + obj.turn_to(int(params[1])) + return vm.state_return + +func set_angle(params): + var obj = vm.get_object(params[0]) + obj.set_angle(int(params[1])) + return vm.state_return + +func change_scene(params): + # Savegames must have events disabled, so saving the game adds a false to params + var run_events = true + if params.size() == 2: + run_events = bool(params[1]) + + # looking for localized string format in scene. this should be somewhere else + var sep = params[0].find(":\"") + if sep >= 0: + var path = params[0].substr(sep + 2, params[0].length() - (sep + 2)) + vm.call_deferred("change_scene", [path], current_context, run_events) + else: + vm.call_deferred("change_scene", params, current_context, run_events) + + current_context.waiting = true + return vm.state_yield + +func spawn(params): + return vm.spawn(params) + +func jump(params): + vm.jump(params[0]) + return vm.state_jump + +func dialog_config(params): + vm.dialog_config(params) + return vm.state_return + +func sched_event(params): + var time = params[0] + var obj = params[1] + var event + if params.size() == 3: + event = params[2] + else: + # This should be easier in Godot 3.1 with array slicing + for i in range(2, params.size()): + var word = params[i] + if not event: + event = word + else: + event += " %s" % word + + if !check_obj(obj, "sched_event"): + return + var o = vm.get_object(obj) + if !(event in o.event_table): + vm.report_errors("", ["Event "+event+" not found on object " + obj + " for sched_event."]) + return + vm.sched_event(time, obj, event) + +func custom(params): + var obj = vm.get_object(params[0]) + # Do not error out because `obj` may not be present in every room of the game, + # making it (probably) safe to ignore it being missing. + if obj == null: + return + + if not obj.has_node("custom"): + vm.report_errors("custom", ["Node 'custom' not found for " + params[0]]) + + obj.get_node("custom").call(params[1], params) + +func camera_set_target(params): + # Pass strings in so vm can resolve what to do with them + var speed = params[0] + if params.size() > 2: + var targets = [] + for i in range(1, params.size()): + targets.push_back(params[i]) + vm.camera_set_target(speed, targets) + else: + vm.camera_set_target(speed, params[1]) + +func camera_set_drag_margin_enabled(params): + var dm_h_enabled = params[0] + var dm_v_enabled = params[0] + vm.camera_set_drag_margin_enabled(dm_h_enabled, dm_v_enabled) + +func camera_set_pos(params): + var speed = params[0] + var pos = Vector2(params[1], params[2]) + vm.camera_set_target(speed, pos) + +func camera_set_zoom(params): + var magnitude = params[0] + var time = params[1] if params.size() > 1 else 0 + vm.camera_set_zoom(magnitude, float(time)) + +func camera_set_zoom_height(params): + var magnitude = params[0] / vm.game_size.y + var time = params[1] if params.size() > 1 else 0 + vm.camera_set_zoom(magnitude, float(time)) + +func camera_push(params): + var target = params[0] + var time = params[1] if params.size() > 1 else 1 + var type = params[2] if params.size() > 2 else "QUAD" + + vm.camera_push(target, time, type) + +func camera_shift(params): + var x = params[0] + var y = params[1] + var time = params[2] if params.size() > 2 else 1 + var type = params[3] if params.size() > 3 else "QUAD" + + vm.camera_shift(x, y, time, type) + +func set_globals(params): + var pat = params[0] + var val = params[1] + vm.set_globals(pat, val) + +func accept_input(params): + var p_input = params[0] + var input = vm.acceptable_inputs["INPUT_" + p_input] + vm.set_accept_input(input) + +func autosave(params): + vm.request_autosave() + +func queue_resource(params): + var path = params[0] + var in_front = false + if params.size() > 1: + in_front = params[1] + + vm.res_cache.queue_resource(path, in_front) + +func queue_animation(params): + var obj = params[0] + var anim = params[1] + var in_front = false + if params.size() > 2: + in_front = params[2] + + if !check_obj(obj, "queue_animation"): + return vm.state_return + + obj = vm.get_object(obj) + var paths = obj.anim_get_ph_paths(anim) + for p in paths: + vm.res_cache.queue_resource(p, in_front) + + return vm.state_return + +func game_over(params): + var continue_enabled = params[0] + var show_credits = false + if params.size() > 1: + show_credits = params[1] + vm.call_deferred("game_over", continue_enabled, show_credits, current_context) + current_context.waiting = true + return vm.state_yield + +### end command + +func run(context): + var cmd = context.instructions[context.ip] + if cmd.name == "label": + return vm.state_return + if !vm.test(cmd): + return vm.state_return + #print("name is ", cmd.name) + #if !(cmd.name in self): + # vm.report_errors("", ["Unexisting command "+cmd.name]) + + return call(cmd.name, cmd.params) + +func resume(context): + current_context = context + if context.waiting: + return vm.state_yield + var count = context.instructions.size() + while context.ip < count: + var top = vm.stack.size() + var ret = run(context) + context.ip += 1 + if top < vm.stack.size(): + return vm.state_call + if ret == vm.state_yield: + return vm.state_yield + if ret == vm.state_call: + return vm.state_call + if ret == vm.state_break: + if context.break_stop: + break + else: + return vm.state_break + if ret == vm.state_repeat: + context.ip = 0 + if ret == vm.state_jump: + return vm.state_jump + context.ip = 0 + return vm.state_return diff --git a/device/globals/white.png b/device/globals/white.png new file mode 100644 index 0000000..573faa3 Binary files /dev/null and b/device/globals/white.png differ diff --git a/device/globals/white.png.import b/device/globals/white.png.import new file mode 100644 index 0000000..ea2a8f5 --- /dev/null +++ b/device/globals/white.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/white.png-b4741e2da7162adae83f97b1151f21cc.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://globals/white.png" +dest_files=[ "res://.import/white.png-b4741e2da7162adae83f97b1151f21cc.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=false +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/project.godot b/device/project.godot new file mode 100644 index 0000000..edbf67f --- /dev/null +++ b/device/project.godot @@ -0,0 +1,180 @@ +; 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=4 + +_global_script_classes=[ { +"base": "", +"class": "GitAPI", +"language": "NativeScript", +"path": "res://git_api.gdns" +} ] +_global_script_class_icons={ +"GitAPI": "" +} + +[application] + +config/name="Le Dieu du Fond du Jardin" +run/main_scene="res://globals/scene_main.tscn" +config/icon="res://globals/assets/icon.png" + +[audio] + +mix_rate=96000 + +[autoload] + +main="*res://globals/main.tscn" +vm="*res://globals/global_vm.gd" +esc_type="*res://globals/escoria_types.gd" + +[display] + +window/size/width=3840 +window/size/height=2160 +window/size/test_width=3840 +window/size/test_height=2160 +window/dpi/allow_hidpi=true +window/stretch/mode="2d" +window/stretch/aspect="keep" +window/stretch/shrink="1" + +[escoria] + +application/max_voice_volume=10 +application/save_data="res://globals/save_data.gd" +application/speech_suffix=".ogg" +application/text_lang="fr" +application/voice_lang="fr" +application/speech_path="res://globals/assets/speech/" +application/binaural_speech=false +application/speech_enabled=true +application/speech_locales_path="res://globals/assets/speech_locales.gd" +application/text_timeout_seconds=5 +application/dialog_minimum_visible_seconds=1 +application/dialog_damp_music_by_db=9 +debug/tag_untranslated_strings=true +debug/terminate_on_errors=false +platform/skip_cache=false +platform/terminate_on_errors=false +platform/action_menu_scale=2 +platform/dialog_option_height=1 +platform/dialog_type_suffix="" +platform/dialog_force_centered=false +platform/development_lang="fr" +platform/game_start_script="res://game.esc" +platform/force_text_ids=false +platform/force_disable_typewriter_text=false +platform/force_quit=true +platform/force_tooltip_global_id=false +platform/tag_untranslated_strings=true +platform/default_object_action="" +platform/object_action_requires_doubleclick=false +platform/exit_button=true +platform/screen_resizable=true +platform/telon="res://globals/telon.tscn" +platform/window_title_height=32 +ui/hud="res://ui/hud.tscn" +ui/instructions="" +ui/credits="res://rooms/end_credits/end_credits.tscn" +ui/end_credits="res://rooms/end_credits/end_credits.tscn" +ui/in_game_menu="" +ui/confirm_popup="res://ui/confirm_popup.tscn" +ui/savegames="" +ui/tooltip_follows_mouse=false +ui/right_mouse_button_action_menu=false +ui/action_menu="res://ui/action_menu.tscn" +ui/main_menu="res://ui/main_menu.tscn" + +[gdnative] + +singletons=[ "res://git_api.gdnlib" ] + +[input] + +inventory_toggle={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":5,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":7,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777218,"unicode":0,"echo":false,"script":null) + ] +} +look={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) + ] +} +menu_request={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777217,"unicode":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) + ] +} +punch_out_dodge={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":90,"unicode":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null) + ] +} +punch_out_punch={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":88,"unicode":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null) + ] +} +quick_save={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777247,"unicode":0,"echo":false,"script":null) + ] +} +start={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) + ] +} +talk={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) + ] +} +use={ +"deadzone": 0.5, +"events": [ Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null) + ] +} +game_general={ +"deadzone": 0.5, +"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null) + ] +} +game_rmb={ +"deadzone": 0.5, +"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":2,"pressed":false,"doubleclick":false,"script":null) + ] +} +game_highlight={ +"deadzone": 0.5, +"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":3,"pressed":false,"doubleclick":false,"script":null) + ] +} + +[locale] + +fallback="fr" +translations=PoolStringArray( ) +locale_filter=[ 0, [ "fr" ] ] + +[physics] + +2d/thread_model=2 + +[rendering] + +threads/thread_model=2 diff --git a/device/quick_save.esc b/device/quick_save.esc new file mode 100644 index 0000000..ea4e77b --- /dev/null +++ b/device/quick_save.esc @@ -0,0 +1,16 @@ +:load + +cut_scene telon fade_out + +## Global flags + + +## Objects + + +## Player + +#change_scene res://_____/rooms/test/test (copie 1).tscn +#teleport_pos player 840.961853 427.349304 + +cut_scene telon fade_in diff --git a/device/rooms/end_credits/Background.tscn b/device/rooms/end_credits/Background.tscn new file mode 100644 index 0000000..8546c9a --- /dev/null +++ b/device/rooms/end_credits/Background.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://globals/item.gd" type="Script" id=1] +[ext_resource path="res://rooms/garden/assets/sky.png" type="Texture" id=2] + +[node name="Background" type="Node2D"] +position = Vector2( -3820, 0 ) +script = ExtResource( 1 ) +global_id = "Background" + +[node name="Area2D" type="Area2D" parent="."] + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Area2D"] +polygon = PoolVector2Array( 27.7197, 2156.32, 19.939, -45.8096, 7703.59, 25.4802, 7691.86, 2207.11 ) + +[node name="Sprite" type="Sprite" parent="Area2D"] +texture = ExtResource( 2 ) +centered = false diff --git a/device/rooms/end_credits/Credits.tscn b/device/rooms/end_credits/Credits.tscn new file mode 100644 index 0000000..3381ef4 --- /dev/null +++ b/device/rooms/end_credits/Credits.tscn @@ -0,0 +1,451 @@ +[gd_scene load_steps=19 format=2] + +[ext_resource path="res://globals/item.gd" type="Script" id=1] +[ext_resource path="res://globals/assets/fonts/lavi_credit_title.tres" type="DynamicFont" id=2] +[ext_resource path="res://ui/dialog_credits_title.tres" type="Theme" id=3] +[ext_resource path="res://globals/assets/fonts/lavi_credit.tres" type="DynamicFont" id=4] +[ext_resource path="res://ui/dialog_credits.tres" type="Theme" id=5] + +[sub_resource type="Animation" id=1] +resource_name = "credit01" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Texte" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Kujiu" ] +} + +[sub_resource type="Animation" id=2] +resource_name = "credit02" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Graphisme" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Springouille" ] +} + +[sub_resource type="Animation" id=3] +resource_name = "credit03" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Voix, SFX et programmation" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Kujiu" ] +} + +[sub_resource type="Animation" id=4] +resource_name = "credit04" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Musique" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Dreaming Under the Stars +par Darren Curtis +Licence Creative Commons CC-BY +https://www.darrencurtismusic.com" ] +} + +[sub_resource type="Animation" id=5] +resource_name = "credit05" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Musique" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Robot Lover Song +par Neku (feat. Aline) +Licence Creative Commons" ] +} + +[sub_resource type="Animation" id=6] +resource_name = "credit06" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Caractères" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Lavi font +par Ruben Holthuijsen +Licence GPLv3" ] +} + +[sub_resource type="Animation" id=7] +resource_name = "credit07" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Caractères" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Rendu avec FreeType +The FreeType Project. All rights reserved. +http://www.freetype.org" ] +} + +[sub_resource type="Animation" id=8] +resource_name = "credit08" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Moteur" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Godot Engine +par Juan Linietsky, Ariel Manzur et contributeurs +Licence MIT +https://www.godotengine.org" ] +} + +[sub_resource type="Animation" id=9] +resource_name = "credit09" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Moteur" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Escoria +par Juan Linietsky, Ariel Manzur et contributeurs +Licence MIT +https://www.github.com/godotengine/escoria" ] +} + +[sub_resource type="Animation" id=10] +resource_name = "credit10" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Moteur" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "ENet +par Lee Salzman +Licence MIT-like" ] +} + +[sub_resource type="Animation" id=11] +resource_name = "credit11" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Moteur" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "MBedTLS +Licence Apache version 2.0" ] +} + +[sub_resource type="Animation" id=12] +resource_name = "credit12" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Production" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Une Narration Expérimentale de Réalité Vectographiée +Nerv Project" ] +} + +[sub_resource type="Animation" id=13] +resource_name = "credit13" +length = 12.0 +tracks/0/type = "value" +tracks/0/path = NodePath("Area2D/Title:text") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Licence" ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("Area2D/CreditsLabel:text") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ "Distribué sous licence Creative Commons +CC-BY-SA 4.0" ] +} + +[node name="Credits" type="Node2D"] +script = ExtResource( 1 ) + +[node name="Area2D" type="Area2D" parent="."] + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Area2D"] +position = Vector2( -12.9961, 363.898 ) +polygon = PoolVector2Array( 275.518, 119.954, 285.326, 345.547, 123.487, 463.248, 148.009, 1409.76, 3693.75, 1395.05, 3683.94, 556.428, 3497.58, 419.111, 3517.2, 70.9116, 1015.6, 108.758 ) + +[node name="Title" type="RichTextLabel" parent="Area2D"] +margin_left = 692.0 +margin_top = 558.0 +margin_right = 3530.0 +margin_bottom = 755.0 +rect_clip_content = false +theme = ExtResource( 3 ) +custom_fonts/mono_font = ExtResource( 2 ) +custom_fonts/bold_italics_font = ExtResource( 2 ) +custom_fonts/italics_font = ExtResource( 2 ) +custom_fonts/bold_font = ExtResource( 2 ) +custom_fonts/normal_font = ExtResource( 2 ) +custom_colors/default_color = Color( 0.0117647, 0.960784, 0.984314, 1 ) +scroll_active = false +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="CreditsLabel" type="RichTextLabel" parent="Area2D"] +margin_left = 943.0 +margin_top = 838.0 +margin_right = 3514.0 +margin_bottom = 1673.0 +rect_clip_content = false +theme = ExtResource( 5 ) +custom_fonts/mono_font = ExtResource( 4 ) +custom_fonts/bold_italics_font = ExtResource( 4 ) +custom_fonts/italics_font = ExtResource( 4 ) +custom_fonts/bold_font = ExtResource( 4 ) +custom_fonts/normal_font = ExtResource( 4 ) +custom_colors/default_color = Color( 0.933333, 1, 0.164706, 1 ) +scroll_active = false +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="animation" type="AnimationPlayer" parent="."] +anims/credit01 = SubResource( 1 ) +anims/credit02 = SubResource( 2 ) +anims/credit03 = SubResource( 3 ) +anims/credit04 = SubResource( 4 ) +anims/credit05 = SubResource( 5 ) +anims/credit06 = SubResource( 6 ) +anims/credit07 = SubResource( 7 ) +anims/credit08 = SubResource( 8 ) +anims/credit09 = SubResource( 9 ) +anims/credit10 = SubResource( 10 ) +anims/credit11 = SubResource( 11 ) +anims/credit12 = SubResource( 12 ) +anims/credit13 = SubResource( 13 ) diff --git a/device/rooms/end_credits/bg_music.tscn b/device/rooms/end_credits/bg_music.tscn new file mode 100644 index 0000000..7042090 --- /dev/null +++ b/device/rooms/end_credits/bg_music.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://globals/assets/musics/end_credits.ogg" type="AudioStream" id=2] + +[node name="background_music" type="Control"] +margin_right = 40.0 +margin_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="stream" type="AudioStreamPlayer" parent="."] +stream = ExtResource( 2 ) +autoplay = true +mix_target = 1 +bus = "Music" diff --git a/device/rooms/end_credits/end_credits.esc b/device/rooms/end_credits/end_credits.esc new file mode 100644 index 0000000..38c9ba9 --- /dev/null +++ b/device/rooms/end_credits/end_credits.esc @@ -0,0 +1,34 @@ +:start +set_active God true +cut_scene telon fade_in +slide Background Narrator 20 +set_active Credits true +cut_scene Credits credit01 +wait 1 +cut_scene Credits credit02 +wait 1 +cut_scene Credits credit03 +wait 1 +cut_scene Credits credit04 +wait 1 +cut_scene Credits credit05 +wait 1 +cut_scene Credits credit06 +wait 1 +cut_scene Credits credit07 +wait 1 +cut_scene Credits credit08 +wait 1 +cut_scene Credits credit09 +wait 1 +cut_scene Credits credit10 +wait 1 +cut_scene Credits credit11 +wait 1 +cut_scene Credits credit12 +wait 1 +cut_scene Credits credit13 +set_active Credits false +queue_resource "res://ui/main_menu.tscn" false +cut_scene telon fade_out +change_scene "res://ui/main_menu.tscn" diff --git a/device/rooms/end_credits/end_credits.gd b/device/rooms/end_credits/end_credits.gd new file mode 100644 index 0000000..120af82 --- /dev/null +++ b/device/rooms/end_credits/end_credits.gd @@ -0,0 +1,16 @@ +extends "res://globals/scene.gd" + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + ._ready() + get_node("Narrator").activate("start", null) + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/device/rooms/end_credits/end_credits.tscn b/device/rooms/end_credits/end_credits.tscn new file mode 100644 index 0000000..d0fd50c --- /dev/null +++ b/device/rooms/end_credits/end_credits.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=9 format=2] + +[ext_resource path="res://rooms/end_credits/end_credits.gd" type="Script" id=1] +[ext_resource path="res://actors/narrator/narrator.tscn" type="PackedScene" id=2] +[ext_resource path="res://actors/god/god.tscn" type="PackedScene" id=3] +[ext_resource path="res://rooms/garden/assets/foreground.png" type="Texture" id=4] +[ext_resource path="res://globals/game.tscn" type="PackedScene" id=5] +[ext_resource path="res://rooms/end_credits/Background.tscn" type="PackedScene" id=6] +[ext_resource path="res://rooms/end_credits/Credits.tscn" type="PackedScene" id=7] +[ext_resource path="res://rooms/end_credits/bg_music.tscn" type="PackedScene" id=8] + +[node name="scene" type="Node2D"] +script = ExtResource( 1 ) + +[node name="background_music" parent="." instance=ExtResource( 8 )] + +[node name="Background" parent="." instance=ExtResource( 6 )] + +[node name="Credits" parent="." instance=ExtResource( 7 )] +global_id = "Credits" + +[node name="Narrator" parent="." instance=ExtResource( 2 )] +events_path = "res://rooms/end_credits/end_credits.esc" + +[node name="God" parent="." instance=ExtResource( 3 )] +position = Vector2( 3103.93, 1767.52 ) +rotation = -0.824012 + +[node name="foreground" type="TextureRect" parent="."] +margin_right = 40.0 +margin_bottom = 40.0 +texture = ExtResource( 4 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="game" parent="." instance=ExtResource( 5 )] +camera_limits = Rect2( 0, 0, 3840, 2160 ) diff --git a/device/rooms/garden/assets/foreground.png b/device/rooms/garden/assets/foreground.png new file mode 100644 index 0000000..f68c640 Binary files /dev/null and b/device/rooms/garden/assets/foreground.png differ diff --git a/device/rooms/garden/assets/foreground.png.import b/device/rooms/garden/assets/foreground.png.import new file mode 100644 index 0000000..f17a0ae --- /dev/null +++ b/device/rooms/garden/assets/foreground.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/foreground.png-90c97ce6c1415394dfdc8c6c3f7fdca0.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://rooms/garden/assets/foreground.png" +dest_files=[ "res://.import/foreground.png-90c97ce6c1415394dfdc8c6c3f7fdca0.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=1 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/device/rooms/garden/assets/ground.png b/device/rooms/garden/assets/ground.png new file mode 100644 index 0000000..03e9d8e Binary files /dev/null and b/device/rooms/garden/assets/ground.png differ diff --git a/device/rooms/garden/assets/ground.png.import b/device/rooms/garden/assets/ground.png.import new file mode 100644 index 0000000..93d5499 --- /dev/null +++ b/device/rooms/garden/assets/ground.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/ground.png-c7e0a15ae8e4ee8aca9f85191b900f79.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://rooms/garden/assets/ground.png" +dest_files=[ "res://.import/ground.png-c7e0a15ae8e4ee8aca9f85191b900f79.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/device/rooms/garden/assets/plouf.ogg b/device/rooms/garden/assets/plouf.ogg new file mode 100644 index 0000000..1ea86d5 Binary files /dev/null and b/device/rooms/garden/assets/plouf.ogg differ diff --git a/device/rooms/garden/assets/plouf.ogg.import b/device/rooms/garden/assets/plouf.ogg.import new file mode 100644 index 0000000..9e0546b --- /dev/null +++ b/device/rooms/garden/assets/plouf.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/plouf.ogg-77bd996df3fb7498b7641349a713c276.oggstr" + +[deps] + +source_file="res://rooms/garden/assets/plouf.ogg" +dest_files=[ "res://.import/plouf.ogg-77bd996df3fb7498b7641349a713c276.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/rooms/garden/assets/rain.ogg b/device/rooms/garden/assets/rain.ogg new file mode 100644 index 0000000..a0f3293 Binary files /dev/null and b/device/rooms/garden/assets/rain.ogg differ diff --git a/device/rooms/garden/assets/rain.ogg.import b/device/rooms/garden/assets/rain.ogg.import new file mode 100644 index 0000000..0a56859 --- /dev/null +++ b/device/rooms/garden/assets/rain.ogg.import @@ -0,0 +1,15 @@ +[remap] + +importer="ogg_vorbis" +type="AudioStreamOGGVorbis" +path="res://.import/rain.ogg-d38a1a14c762d73f19a1fa8173fd4826.oggstr" + +[deps] + +source_file="res://rooms/garden/assets/rain.ogg" +dest_files=[ "res://.import/rain.ogg-d38a1a14c762d73f19a1fa8173fd4826.oggstr" ] + +[params] + +loop=true +loop_offset=0 diff --git a/device/rooms/garden/assets/sky.png b/device/rooms/garden/assets/sky.png new file mode 100644 index 0000000..e5c3cf5 Binary files /dev/null and b/device/rooms/garden/assets/sky.png differ diff --git a/device/rooms/garden/assets/sky.png.import b/device/rooms/garden/assets/sky.png.import new file mode 100644 index 0000000..9673dbb --- /dev/null +++ b/device/rooms/garden/assets/sky.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/sky.png-036974f3d76e2a229b9392643c2e8313.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://rooms/garden/assets/sky.png" +dest_files=[ "res://.import/sky.png-036974f3d76e2a229b9392643c2e8313.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/device/rooms/garden/background.tscn b/device/rooms/garden/background.tscn new file mode 100644 index 0000000..97eb85a --- /dev/null +++ b/device/rooms/garden/background.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://rooms/garden/assets/sky.png" type="Texture" id=1] + +[node name="root_background" type="Node2D"] + +[node name="ParallaxBackground" type="ParallaxBackground" parent="."] + +[node name="Sky" type="ParallaxLayer" parent="ParallaxBackground"] +motion_scale = Vector2( 0.5, 1 ) +motion_offset = Vector2( -1000, 0 ) + +[node name="Sprite" type="Sprite" parent="ParallaxBackground/Sky"] +texture = ExtResource( 1 ) +centered = false diff --git a/device/rooms/garden/bg_music.tscn b/device/rooms/garden/bg_music.tscn new file mode 100644 index 0000000..edcb873 --- /dev/null +++ b/device/rooms/garden/bg_music.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://globals/bg_music.gd" type="Script" id=1] +[ext_resource path="res://globals/assets/musics/dreaming_under_the_stars.ogg" type="AudioStream" id=2] + +[node name="bg_music" type="Control"] +margin_right = 40.0 +margin_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="stream" type="AudioStreamPlayer" parent="."] +stream = ExtResource( 2 ) +volume_db = -25.0 +autoplay = true +mix_target = 1 +bus = "Music" diff --git a/device/rooms/garden/foreground.tscn b/device/rooms/garden/foreground.tscn new file mode 100644 index 0000000..89e840b --- /dev/null +++ b/device/rooms/garden/foreground.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://rooms/garden/assets/foreground.png" type="Texture" id=1] + +[node name="root_foreground" type="Node2D"] + +[node name="ParallaxBackground" type="ParallaxBackground" parent="."] +layer = 0 + +[node name="ParallaxLayer" type="ParallaxLayer" parent="ParallaxBackground"] +motion_scale = Vector2( 2.5, 1 ) +motion_mirroring = Vector2( 5900, 0 ) + +[node name="Sprite" type="TextureRect" parent="ParallaxBackground/ParallaxLayer"] +margin_left = -1500.0 +margin_right = 4408.0 +margin_bottom = 2160.0 +grow_horizontal = 2 +size_flags_horizontal = 3 +texture = ExtResource( 1 ) +stretch_mode = 2 +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/device/rooms/garden/garden.esc b/device/rooms/garden/garden.esc new file mode 100644 index 0000000..f020e8f --- /dev/null +++ b/device/rooms/garden/garden.esc @@ -0,0 +1,77 @@ +:start +cut_scene telon fade_in + +set_active God false +set_active Dragon false + +wait 3 + +say Narrator player_speak_1_1:"Le petit jardin était toujours paisible. Les humains de la maison s'en occupaient durant la journée. Ils désherbaient, ils coupaient, ils retournaient la terre et rajoutaient de nouvelles plantations." + +wait 2 + +say Narrator player_speak_1_2:"Ils volaient également les précieuses graines et ils empoisonnaient même certaines plantes. Et cela ne Lui plaisait guère." + +camera_set_pos 300 5500 0 +wait 2 + +say Narrator player_speak_2_1:"À la lune levée, quelques pierres tremblaient au fond du jardin. Elles se rapprochaient petit à petit les unes des autres." + +wait 2 + +say Narrator player_speak_2_2:"Deux points bleus supplémentaires scintillaient à la surface du petit étang, ils passaient inaperçus dans le reflet de la voûte céleste. Il arrivait, le petit Dieu du fond du jardin." + +wait 2 + +say Narrator player_speak_3_1:"Les pierres tremblantes formaient un petit amas lorsqu'une pierre creuse, remplie d'eau, jaillissait de l'étang. Elle retombait systématiquement sur le petit squelette rocheux. La structure prenait vie sous forme d'un petit bonhomme en cailloux." + +set_active God true + +wait 2 + +say Narrator player_speak_4_1:"Ce petit Dieu avançait gauchement, se balançant de droite à gauche, déversant quelques gouttes de sa tête. Ses yeux brillaient comme des étoiles et il constatait le désastre engendré par les grands humains." + +wait 2 + +say Narrator player_speak_4_2:"Comment de telles créatures pouvaient à la fois détruire et le vénérer, Lui, qu'ils nommaient Main Verte ou encore Suzuriishi ? Et pourtant, Il n'avait rien de vert si ce n'est quelques plaques de vase et de lichen." + +camera_set_pos 300 3840 0 +wait 2 + +say Narrator player_speak_5_1:"Le petit Dieu gagnait un promontoire de terre, surplombant l'étang. Il prenait alors une petite tige de la pelouse de ses doigts pierreux et commandait aux éléments avec sa baguette magique." + +wait 2 + +say Narrator player_speak_5_2:"Il gesticulait vivement en chavirant sa tête clapotante. Il dirigeait le vent et l'eau ; il formait des figures au-dessus de l'étendue d'eau." + +set_active Dragon true + +wait 2 + +anim Dragon rain + +say Narrator player_speak_6_1:"Un dragon arrosait allègrement la terre pour la nettoyer de toutes ses souillures. Le petit Dieu marmonnait une incantation pour faire pousser toute plante et tout champignon." + +wait 2 + +say Narrator player_speak_6_2:"Il préparait les murmures du vent du jour afin de guider chaque être, chaque fourmi, chaque puceron. Il en appelait à la Nature pour protéger son territoire." + +set_active Dragon false +camera_set_pos 300 5500 0 +wait 2 + + +say Narrator player_speak_7_1:"Le petit Dieu avait perdu toute son eau. Il s'écroulait du haut de son promontoire à la tombée de la lune. Les cailloux s'éparpillaient et sa tête rejoignait une fois de plus les profondeurs de l'étang." + +wait 2 + +anim God plouf +set_active God false +queue_resource "res://rooms/end_credits/end_credits.tscn" false + +say Narrator player_speak_7_2:"Une nouvelle journée commençait, Il était parti en laissant pour seul indice une rosée vivifiante." + +wait 2 +cut_scene telon fade_out + +change_scene "res://rooms/end_credits/end_credits.tscn" diff --git a/device/rooms/garden/garden.gd b/device/rooms/garden/garden.gd new file mode 100644 index 0000000..038af4e --- /dev/null +++ b/device/rooms/garden/garden.gd @@ -0,0 +1,17 @@ +extends "res://globals/scene.gd" + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + ._ready() + get_node("Narrator").activate("start", null) # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/device/rooms/garden/garden.tscn b/device/rooms/garden/garden.tscn new file mode 100644 index 0000000..8fbf9fa --- /dev/null +++ b/device/rooms/garden/garden.tscn @@ -0,0 +1,45 @@ +[gd_scene load_steps=12 format=2] + +[ext_resource path="res://rooms/garden/garden.gd" type="Script" id=1] +[ext_resource path="res://globals/background.gd" type="Script" id=2] +[ext_resource path="res://rooms/garden/terrain.tscn" type="PackedScene" id=3] +[ext_resource path="res://rooms/garden/assets/ground.png" type="Texture" id=4] +[ext_resource path="res://actors/god/god.tscn" type="PackedScene" id=5] +[ext_resource path="res://actors/dragon/dragon.tscn" type="PackedScene" id=6] +[ext_resource path="res://rooms/garden/bg_music.tscn" type="PackedScene" id=7] +[ext_resource path="res://globals/game.tscn" type="PackedScene" id=8] +[ext_resource path="res://rooms/garden/background.tscn" type="PackedScene" id=9] +[ext_resource path="res://rooms/garden/foreground.tscn" type="PackedScene" id=10] +[ext_resource path="res://actors/narrator/narrator.tscn" type="PackedScene" id=11] + +[node name="scene" type="Node2D"] +script = ExtResource( 1 ) + +[node name="root_background" parent="." instance=ExtResource( 9 )] + +[node name="background" type="Sprite" parent="."] +texture = ExtResource( 4 ) +centered = false +script = ExtResource( 2 ) + +[node name="terrain" parent="." instance=ExtResource( 3 )] + +[node name="Narrator" parent="." instance=ExtResource( 11 )] +events_path = "res://rooms/garden/garden.esc" + +[node name="Dragon" parent="." instance=ExtResource( 6 )] +position = Vector2( 2544.46, 229.112 ) + +[node name="God" parent="." instance=ExtResource( 5 )] +position = Vector2( 5100.39, 584.421 ) +active = false + +[node name="root_foreground" parent="." instance=ExtResource( 10 )] + +[node name="bg_music" parent="." instance=ExtResource( 7 )] +margin_left = 933.764 +margin_top = 38.9067 +margin_right = 973.764 +margin_bottom = 78.9067 + +[node name="game" parent="." instance=ExtResource( 8 )] diff --git a/device/rooms/garden/terrain.tscn b/device/rooms/garden/terrain.tscn new file mode 100644 index 0000000..2394a48 --- /dev/null +++ b/device/rooms/garden/terrain.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://globals/terrain.gd" type="Script" id=1] +[ext_resource path="res://rooms/garden/assets/ground.png" type="Texture" id=2] + +[sub_resource type="NavigationPolygon" id=1] +vertices = PoolVector2Array( 832.858, 2088.26, 341.568, 1778.43, 510.342, 1791.71, 837.869, 1800.56, 1572.01, 2088.26, 6853.12, 1694.34, 7264.74, 1747.45, 7025.73, 1933.34, 5331.33, 2141.37, 651.975, 1654.5, 532.472, 1512.87, 1112.28, 1490.74, 829.017, 1694.34, 4654.14, 1605.82, 4578.14, 1415.5, 4653.38, 1335.83, 4768.46, 1424.35, 5008.23, 1650.08, 6994.75, 2136.94, 6507.89, 1535, 6100.69, 1601.39, 5463.34, 1530.57, 5892.67, 1415.5, 5211.82, 2066.13, 5083.47, 1875.81, 5118.88, 1716.47, 5144.67, 1459.76, 4228.48, 1450.91, 4454.21, 1353.53, 4338.28, 1650.08, 4183.37, 1565.98, 3900.95, 1459.76, 4095.7, 1349.11, 3935.51, 1641.23, 3603.56, 1517.3, 1563.74, 1455.33, 3187.51, 1570.41, 2886.54, 1796.14, 2660.81, 1747.45, 1391.12, 1419.92, 2333.29, 2141.37 ) +polygons = [ PoolIntArray( 0, 1, 2 ), PoolIntArray( 0, 2, 3, 4 ), PoolIntArray( 5, 6, 7, 8 ), PoolIntArray( 9, 10, 11, 12 ), PoolIntArray( 13, 14, 15, 16, 17 ), PoolIntArray( 7, 18, 8 ), PoolIntArray( 19, 5, 8, 20 ), PoolIntArray( 21, 22, 20, 8, 23, 24, 25 ), PoolIntArray( 26, 21, 25, 17, 16 ), PoolIntArray( 27, 28, 14, 13, 29, 30 ), PoolIntArray( 31, 32, 27, 30, 33 ), PoolIntArray( 31, 33, 34 ), PoolIntArray( 35, 31, 34, 36 ), PoolIntArray( 35, 36, 37, 38 ), PoolIntArray( 11, 39, 35, 38, 40, 4, 3, 12 ) ] +outlines = [ PoolVector2Array( 341.568, 1778.43, 832.858, 2088.26, 1572.01, 2088.26, 2333.29, 2141.37, 2660.81, 1747.45, 2886.54, 1796.14, 3187.51, 1570.41, 3603.56, 1517.3, 3935.51, 1641.23, 4183.37, 1565.98, 4338.28, 1650.08, 4654.14, 1605.82, 5008.23, 1650.08, 5118.88, 1716.47, 5083.47, 1875.81, 5211.82, 2066.13, 5331.33, 2141.37, 6994.75, 2136.94, 7025.73, 1933.34, 7264.74, 1747.45, 6853.12, 1694.34, 6507.89, 1535, 6100.69, 1601.39, 5892.67, 1415.5, 5463.34, 1530.57, 5144.67, 1459.76, 4768.46, 1424.35, 4653.38, 1335.83, 4578.14, 1415.5, 4454.21, 1353.53, 4228.48, 1450.91, 4095.7, 1349.11, 3900.95, 1459.76, 1563.74, 1455.33, 1391.12, 1419.92, 1112.28, 1490.74, 532.472, 1512.87, 651.975, 1654.5, 829.017, 1694.34, 837.869, 1800.56, 510.342, 1791.71 ) ] + +[node name="terrain" type="Navigation2D"] +script = ExtResource( 1 ) + +[node name="TextureRect" type="TextureRect" parent="."] +visible = false +margin_right = 7680.0 +margin_bottom = 2160.0 +texture = ExtResource( 2 ) +__meta__ = { +"_edit_lock_": true, +"_edit_use_anchors_": false +} + +[node name="NavigationPolygonInstance" type="NavigationPolygonInstance" parent="."] +navpoly = SubResource( 1 ) diff --git a/device/ui/action_menu.tscn b/device/ui/action_menu.tscn new file mode 100644 index 0000000..38da0ac --- /dev/null +++ b/device/ui/action_menu.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://globals/action_menu.gd" type="Script" id=1] + +[node name="action_menu" type="Container"] +z_index = 4096 + +script = ExtResource( 1 ) + +[node name="actions" type="Node2D" parent="."] + + +[node name="use" type="Button" parent="actions"] + +focus_ignore_mouse = false +focus_stop_mouse = true +size_flags_horizontal = 2 +size_flags_vertical = 2 +margin_left = 11.0 +margin_top = -31.0 +margin_right = 41.0 +margin_bottom = -11.0 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +text = "use" +flat = false diff --git a/device/ui/assets/Nerv-onDark.svg b/device/ui/assets/Nerv-onDark.svg new file mode 100644 index 0000000..4fb2912 --- /dev/null +++ b/device/ui/assets/Nerv-onDark.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2019 + + + Timothée Giet + + + + + + + + + + + + + diff --git a/device/ui/assets/Nerv-onDark.svg.import b/device/ui/assets/Nerv-onDark.svg.import new file mode 100644 index 0000000..d0a89ca --- /dev/null +++ b/device/ui/assets/Nerv-onDark.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/Nerv-onDark.svg-81334c112f344362405ffbb8a745efdd.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/Nerv-onDark.svg" +dest_files=[ "res://.import/Nerv-onDark.svg-81334c112f344362405ffbb8a745efdd.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=10.0 diff --git a/device/ui/assets/dialogue.png b/device/ui/assets/dialogue.png new file mode 100644 index 0000000..83eee76 Binary files /dev/null and b/device/ui/assets/dialogue.png differ diff --git a/device/ui/assets/dialogue.png.import b/device/ui/assets/dialogue.png.import new file mode 100644 index 0000000..04b6e71 --- /dev/null +++ b/device/ui/assets/dialogue.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/dialogue.png-a3136f6e6d2ce9c8401ac1d2c83587e6.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/dialogue.png" +dest_files=[ "res://.import/dialogue.png-a3136f6e6d2ce9c8401ac1d2c83587e6.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/device/ui/confirm_popup.gd b/device/ui/confirm_popup.gd new file mode 100644 index 0000000..fe9e678 --- /dev/null +++ b/device/ui/confirm_popup.gd @@ -0,0 +1,91 @@ +extends Control + +var lang_node +var target = null +var slot = "" +var confirm +var anim + +func start(p_node, p_target, p_slot): + target = p_target + slot = p_slot + + # This allows having TextureRects by the name of eg UI_CONFIRM_NEW_GAME or UI_CONFIRM_QUIT + var content_node = lang_node.get_node(p_node) + + if content_node is preload("res://ui/translated_label.gd") or content_node is preload("res://ui/translated_rtlabel.gd"): + content_node.language_changed() + + content_node.show() + + if anim: + anim.play("open") + + main.menu_open(self) + show() + +func button_pressed(p_confirm): + # This must be global or else we might see an animated confirmation menu + # overlaid onto the scene when starting a new game. See `anim_finished`. + confirm = p_confirm + + printt("button pressed!", slot, target.name, confirm) + if anim and anim.is_playing(): + printt("anim is playing!", anim.current_animation) + return + + # `anim_finished` calls target, but call it now if there are no animations + if not anim: + target.call_deferred(slot, confirm) + + close() + +func menu_collapsed(): + close() + +func close(): + main.menu_close(self) + if anim and anim.is_playing(): + printt("close: anim is playing!", anim.current_animation) + if anim.current_animation == "close": + return + + elif anim: + # Once this is finished, `anim_finished` will call `queue_free()` + # and whatever is defined in the target + anim.play("close") + else: + queue_free() + +func anim_finished(anim_name): + # print("Finished anim ", anim_name) + if anim_name == "close": + target.call_deferred(slot, confirm) + queue_free() + +func input(event): + if anim and anim.is_playing(): + return + if event.is_action("menu_request") && event.is_pressed() && !event.is_echo(): + button_pressed(false) + +func _ready(): + var lang = TranslationServer.get_locale() + + if not has_node(lang) and "_" in lang: + lang = lang.split("_")[0] + + lang_node = get_node(lang) + if not lang_node: + vm.report_errors("confirm_popup", + ["No language node " + TranslationServer.get_locale() + " in confirmation popup"]) + + lang_node.visible = true + + lang_node.get_node("yes").connect("pressed", self, "button_pressed", [true]) + lang_node.get_node("no").connect("pressed", self, "button_pressed", [false]) + + if has_node("animation"): + anim = $"animation" + anim.connect("animation_finished", self, "anim_finished") + diff --git a/device/ui/confirm_popup.tscn b/device/ui/confirm_popup.tscn new file mode 100644 index 0000000..5d8fbae --- /dev/null +++ b/device/ui/confirm_popup.tscn @@ -0,0 +1,212 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://ui/confirm_popup.gd" type="Script" id=1] + +[sub_resource type="Animation" id=1] + +resource_name = "close" +length = 0.1 +loop = false +step = 0.1 + +[sub_resource type="Animation" id=2] + +resource_name = "open" +length = 0.1 +loop = false +step = 0.1 + +[node name="confirm_popup" type="Control" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 1949.0 +margin_bottom = 1079.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) + +[node name="Panel" type="TextureButton" parent="." index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 667.0 +margin_top = 312.0 +margin_right = 1384.0 +margin_bottom = 759.0 +rect_scale = Vector2( 0.8, 0.8 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null + +[node name="en" type="Control" parent="." index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 667.0 +margin_top = 312.0 +margin_right = 707.0 +margin_bottom = 352.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 + +[node name="UI_QUIT_CONFIRM" type="Label" parent="en" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 7.5 +margin_top = 8.75 +margin_right = 439.5 +margin_bottom = 183.75 +rect_scale = Vector2( 1.3, 1.3 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_colors/font_color = Color( 0.807843, 0.156863, 0.219608, 1 ) +custom_colors/font_color_shadow = Color( 0.443137, 0.266667, 0.00784314, 1 ) +text = "Do you want +to quit the game ?" +align = 1 +valign = 1 +autowrap = true +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 + +[node name="yes" type="TextureButton" parent="en" index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 110.0 +margin_top = 296.25 +margin_right = 319.0 +margin_bottom = 391.25 +rect_scale = Vector2( 0.8, 0.8 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null + +[node name="UI_YES" type="Label" parent="en" index="2"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 169.0 +margin_top = 309.25 +margin_right = 211.0 +margin_bottom = 340.25 +rect_scale = Vector2( 2.5, 2.5 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_colors/font_color = Color( 0.988235, 0.913725, 0.309804, 1 ) +custom_colors/font_color_shadow = Color( 0.443137, 0.266667, 0.00784314, 1 ) +text = "UI_YES" +align = 1 +autowrap = true +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 + +[node name="no" type="TextureButton" parent="en" index="3"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 398.75 +margin_top = 296.25 +margin_right = 607.75 +margin_bottom = 390.25 +rect_scale = Vector2( 0.8, 0.8 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null + +[node name="UI_NO" type="Label" parent="en" index="4"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 458.75 +margin_top = 310.25 +margin_right = 503.75 +margin_bottom = 340.25 +rect_scale = Vector2( 2.5, 2.5 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_colors/font_color = Color( 0.988235, 0.913725, 0.309804, 1 ) +custom_colors/font_color_shadow = Color( 0.443137, 0.266667, 0.00784314, 1 ) +text = "UI_NO" +align = 1 +autowrap = true +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 + +[node name="animation" type="AnimationPlayer" parent="." index="2"] + +root_node = NodePath("..") +autoplay = "" +playback_process_mode = 1 +playback_default_blend_time = 0.0 +playback_speed = 1.0 +anims/close = SubResource( 1 ) +anims/open = SubResource( 2 ) +blend_times = [ ] + + diff --git a/device/ui/dd_default.tscn b/device/ui/dd_default.tscn new file mode 100644 index 0000000..b603127 --- /dev/null +++ b/device/ui/dd_default.tscn @@ -0,0 +1,135 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://globals/dialog_dialog.gd" type="Script" id=1] + +[sub_resource type="Animation" id=1] +length = 0.75 +step = 0.05 +tracks/0/type = "value" +tracks/0/path = NodePath("wrapper:visible") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0, 0.3 ), +"transitions": PoolRealArray( 1, 1 ), +"update": 1, +"values": [ true, false ] +} +tracks/1/type = "value" +tracks/1/path = NodePath("wrapper:scale") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0, 0.1, 0.15, 0.3 ), +"transitions": PoolRealArray( 1, 1, 1, 1 ), +"update": 0, +"values": [ Vector2( 1, 1 ), Vector2( 1, 1.1 ), Vector2( 1, 1.1 ), Vector2( 1, 0 ) ] +} + +[sub_resource type="Animation" id=2] +length = 0.75 +step = 0.05 +tracks/0/type = "value" +tracks/0/path = NodePath("wrapper:scale") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/keys = { +"times": PoolRealArray( 0, 0.15, 0.2, 0.35 ), +"transitions": PoolRealArray( 1, 1, 1, 1 ), +"update": 0, +"values": [ Vector2( 1, 0 ), Vector2( 1, 1.1 ), Vector2( 1, 1.1 ), Vector2( 1, 1 ) ] +} +tracks/1/type = "value" +tracks/1/path = NodePath(".:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} +tracks/2/type = "value" +tracks/2/path = NodePath("wrapper:visible") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/keys = { +"times": PoolRealArray( 0 ), +"transitions": PoolRealArray( 1 ), +"update": 1, +"values": [ true ] +} + +[node name="dialog" type="Control"] +anchor_left = 0.5 +anchor_right = 0.5 +margin_left = -450.0 +margin_top = 150.0 +margin_right = -450.0 +margin_bottom = 150.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} +mouse_enter_color = Color( 1, 1, 0.3, 1 ) + +[node name="wrapper" type="Node2D" parent="."] + +[node name="margin" type="MarginContainer" parent="wrapper"] +margin_right = 900.0 +margin_bottom = 400.0 +size_flags_vertical = 3 + +[node name="scroll" type="ScrollContainer" parent="wrapper/margin"] +margin_right = 900.0 +margin_bottom = 400.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="container" type="VBoxContainer" parent="wrapper/margin/scroll"] +margin_right = 900.0 +margin_bottom = 400.0 +mouse_filter = 0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +custom_constants/separation = 10 + +[node name="avatars" type="Node2D" parent="wrapper"] +position = Vector2( -323, -33 ) + +[node name="default" type="Sprite" parent="wrapper/avatars"] +position = Vector2( -472.18, -234.678 ) + +[node name="item" type="Control" parent="."] +rect_min_size = Vector2( 800, 30 ) + +[node name="button" type="TextureButton" parent="item"] +margin_right = 800.0 +margin_bottom = 30.0 +rect_min_size = Vector2( 800, 30 ) +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="label" type="Label" parent="item/button"] +margin_bottom = 14.0 +rect_min_size = Vector2( 800, 30 ) +mouse_filter = 1 +size_flags_horizontal = 2 +size_flags_vertical = 0 +text = "I should pick option one" + +[node name="animation" type="AnimationPlayer" parent="."] +anims/hide = SubResource( 1 ) +anims/show = SubResource( 2 ) diff --git a/device/ui/dd_player.tscn b/device/ui/dd_player.tscn new file mode 100644 index 0000000..acfa3f0 --- /dev/null +++ b/device/ui/dd_player.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/dd_default.tscn" type="PackedScene" id=1] +[ext_resource path="res://globals/dd_player.gd" type="Script" id=2] + +[node name="dd_player" type="ResourcePreloader"] +resources = [ PoolStringArray( "default" ), [ ExtResource( 1 ) ] ] +script = ExtResource( 2 ) +__meta__ = { +"__editor_plugin_screen__": "3D" +} diff --git a/device/ui/dialog_bottom.tscn b/device/ui/dialog_bottom.tscn new file mode 100644 index 0000000..4b51e5b --- /dev/null +++ b/device/ui/dialog_bottom.tscn @@ -0,0 +1,50 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/dialog_instance.gd" type="Script" id=1] +[ext_resource path="res://globals/assets/fonts/lavi.tres" type="DynamicFont" id=2] +[ext_resource path="res://ui/assets/dialogue.png" type="Texture" id=3] +[ext_resource path="res://ui/dialog_theme.tres" type="Theme" id=4] + +[node name="dialog_instance" type="Node2D"] +script = ExtResource( 1 ) +__meta__ = { +"__editor_plugin_screen__": "2D" +} +characters_per_second = 25.0 +fixed_pos = true + +[node name="anchor" type="Node2D" parent="."] +z_index = 200 + +[node name="bg" type="TextureRect" parent="anchor"] +margin_left = 748.537 +margin_top = 1603.29 +margin_right = 3248.54 +margin_bottom = 2053.29 +texture = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="text" type="RichTextLabel" parent="anchor"] +modulate = Color( 0, 0, 0, 1 ) +margin_left = 971.0 +margin_top = 1645.0 +margin_right = 3157.0 +margin_bottom = 2000.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +theme = ExtResource( 4 ) +custom_fonts/mono_font = ExtResource( 2 ) +custom_fonts/bold_italics_font = ExtResource( 2 ) +custom_fonts/italics_font = ExtResource( 2 ) +custom_fonts/bold_font = ExtResource( 2 ) +custom_fonts/normal_font = ExtResource( 2 ) +bbcode_enabled = true +scroll_active = false +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="avatars" type="Node2D" parent="anchor"] +position = Vector2( -0.880569, 1.82724 ) diff --git a/device/ui/dialog_credits.tres b/device/ui/dialog_credits.tres new file mode 100644 index 0000000..1f27726 --- /dev/null +++ b/device/ui/dialog_credits.tres @@ -0,0 +1,6 @@ +[gd_resource type="Theme" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/lavi_credit.tres" type="DynamicFont" id=1] + +[resource] +default_font = ExtResource( 1 ) diff --git a/device/ui/dialog_credits_title.tres b/device/ui/dialog_credits_title.tres new file mode 100644 index 0000000..7284e94 --- /dev/null +++ b/device/ui/dialog_credits_title.tres @@ -0,0 +1,6 @@ +[gd_resource type="Theme" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/lavi_credit_title.tres" type="DynamicFont" id=1] + +[resource] +default_font = ExtResource( 1 ) diff --git a/device/ui/dialog_default.tscn b/device/ui/dialog_default.tscn new file mode 100644 index 0000000..4b51e5b --- /dev/null +++ b/device/ui/dialog_default.tscn @@ -0,0 +1,50 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/dialog_instance.gd" type="Script" id=1] +[ext_resource path="res://globals/assets/fonts/lavi.tres" type="DynamicFont" id=2] +[ext_resource path="res://ui/assets/dialogue.png" type="Texture" id=3] +[ext_resource path="res://ui/dialog_theme.tres" type="Theme" id=4] + +[node name="dialog_instance" type="Node2D"] +script = ExtResource( 1 ) +__meta__ = { +"__editor_plugin_screen__": "2D" +} +characters_per_second = 25.0 +fixed_pos = true + +[node name="anchor" type="Node2D" parent="."] +z_index = 200 + +[node name="bg" type="TextureRect" parent="anchor"] +margin_left = 748.537 +margin_top = 1603.29 +margin_right = 3248.54 +margin_bottom = 2053.29 +texture = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="text" type="RichTextLabel" parent="anchor"] +modulate = Color( 0, 0, 0, 1 ) +margin_left = 971.0 +margin_top = 1645.0 +margin_right = 3157.0 +margin_bottom = 2000.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +theme = ExtResource( 4 ) +custom_fonts/mono_font = ExtResource( 2 ) +custom_fonts/bold_italics_font = ExtResource( 2 ) +custom_fonts/italics_font = ExtResource( 2 ) +custom_fonts/bold_font = ExtResource( 2 ) +custom_fonts/normal_font = ExtResource( 2 ) +bbcode_enabled = true +scroll_active = false +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="avatars" type="Node2D" parent="anchor"] +position = Vector2( -0.880569, 1.82724 ) diff --git a/device/ui/dialog_player.tscn b/device/ui/dialog_player.tscn new file mode 100644 index 0000000..b6300b5 --- /dev/null +++ b/device/ui/dialog_player.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://ui/dialog_default.tscn" type="PackedScene" id=1] +[ext_resource path="res://ui/dialog_bottom.tscn" type="PackedScene" id=2] +[ext_resource path="res://globals/dialog_player.gd" type="Script" id=3] + +[node name="dialog_player" type="ResourcePreloader"] +resources = [ PoolStringArray( "bottom", "default" ), [ ExtResource( 2 ), ExtResource( 1 ) ] ] +script = ExtResource( 3 ) +__meta__ = { +"__editor_plugin_screen__": "2D" +} diff --git a/device/ui/dialog_theme.tres b/device/ui/dialog_theme.tres new file mode 100644 index 0000000..222d454 --- /dev/null +++ b/device/ui/dialog_theme.tres @@ -0,0 +1,6 @@ +[gd_resource type="Theme" load_steps=2 format=2] + +[ext_resource path="res://globals/assets/fonts/lavi.tres" type="DynamicFont" id=1] + +[resource] +default_font = ExtResource( 1 ) diff --git a/device/ui/hud.tscn b/device/ui/hud.tscn new file mode 100644 index 0000000..d64e12d --- /dev/null +++ b/device/ui/hud.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://globals/hud.gd" type="Script" id=1] +[ext_resource path="res://globals/assets/fonts/Lavi.ttf" type="DynamicFontData" id=4] +[ext_resource path="res://globals/tooltip.gd" type="Script" id=5] + +[sub_resource type="DynamicFont" id=1] +size = 30 +font_data = ExtResource( 4 ) + +[node name="hud" type="Control"] +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tooltip" type="Label" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -541.986 +margin_top = -265.689 +margin_right = 447.014 +margin_bottom = -208.689 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_fonts/font = SubResource( 1 ) +custom_colors/font_color = Color( 1, 1, 1, 1 ) +align = 1 +valign = 1 +script = ExtResource( 5 ) diff --git a/device/ui/hud_minimal.tscn b/device/ui/hud_minimal.tscn new file mode 100644 index 0000000..6dec402 --- /dev/null +++ b/device/ui/hud_minimal.tscn @@ -0,0 +1,46 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://globals/hud.gd" type="Script" id=1] +[ext_resource path="res://globals/tooltip.gd" type="Script" id=2] + +[node name="hud" type="Control"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 40.0 +margin_bottom = 40.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) + +[node name="tooltip" type="Label" parent="." index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 160.0 +margin_top = 58.0 +margin_right = 1149.0 +margin_bottom = 115.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_colors/font_color = Color( 0, 0, 0, 1 ) +align = 1 +valign = 1 +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 +script = ExtResource( 2 ) + + diff --git a/device/ui/in_game_menu.tscn b/device/ui/in_game_menu.tscn new file mode 100644 index 0000000..277c6e1 --- /dev/null +++ b/device/ui/in_game_menu.tscn @@ -0,0 +1,104 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/main_menu.gd" type="Script" id=1] +[ext_resource path="res://ui/menu_button.gd" type="Script" id=2] + +[node name="main_menu" type="Control" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 1930.0 +margin_bottom = 1101.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +bg_sound = null + +[node name="continue" type="Button" parent="." index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 163.0 +margin_top = 140.0 +margin_right = 733.0 +margin_bottom = 229.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "UI_CONTINUE" +flat = false +align = 1 +script = ExtResource( 2 ) +translation_id = "UI_CONTINUE" + +[node name="new_game" type="Button" parent="." index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 159.0 +margin_top = 284.0 +margin_right = 729.0 +margin_bottom = 373.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "UI_NEW_GAME" +flat = false +align = 1 +script = ExtResource( 2 ) +translation_id = "UI_NEW_GAME" + +[node name="exit" type="Button" parent="." index="2"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 158.0 +margin_top = 438.0 +margin_right = 728.0 +margin_bottom = 527.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "UI_QUIT" +flat = false +align = 1 +script = ExtResource( 2 ) +translation_id = "UI_QUIT" + + diff --git a/device/ui/inventory.tscn b/device/ui/inventory.tscn new file mode 100644 index 0000000..2063632 --- /dev/null +++ b/device/ui/inventory.tscn @@ -0,0 +1,60 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://globals/white.png" type="Texture" id=1] +[ext_resource path="res://globals/inventory.gd" type="Script" id=2] +[ext_resource path="res://ui/inventory_items.tscn" type="PackedScene" id=3] + +[node name="inventory" type="TextureRect"] +self_modulate = Color( 0, 0, 0, 0.196078 ) +margin_right = 600.0 +margin_bottom = 280.0 +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +texture = ExtResource( 1 ) +expand = true +script = ExtResource( 2 ) + +[node name="slots" type="Control" parent="."] +margin_right = 40.0 +margin_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="Position2D" type="Position2D" parent="slots"] +position = Vector2( 120.477, 85.9731 ) + +[node name="Position2D1" type="Position2D" parent="slots"] +position = Vector2( 305.352, 82.4256 ) +scale = Vector2( 0.999999, 1 ) + +[node name="Position2D2" type="Position2D" parent="slots"] +position = Vector2( 484.329, 86.664 ) + +[node name="Position2D3" type="Position2D" parent="slots"] +position = Vector2( 120.782, 187.581 ) + +[node name="Position2D4" type="Position2D" parent="slots"] +position = Vector2( 303.731, 191.537 ) + +[node name="Position2D5" type="Position2D" parent="slots"] +position = Vector2( 484.393, 187.155 ) + +[node name="arrow_prev" type="TextureButton" parent="."] +margin_left = 466.0 +margin_top = 68.0 +margin_right = 506.0 +margin_bottom = 108.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="arrow_next" type="TextureButton" parent="."] +margin_left = 461.0 +margin_top = 167.0 +margin_right = 501.0 +margin_bottom = 207.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="items" parent="." instance=ExtResource( 3 )] +visible = false diff --git a/device/ui/inventory_items.tscn b/device/ui/inventory_items.tscn new file mode 100644 index 0000000..fc73c3a --- /dev/null +++ b/device/ui/inventory_items.tscn @@ -0,0 +1,3 @@ +[gd_scene format=2] + +[node name="items" type="Node2D"] diff --git a/device/ui/lang_button.gd b/device/ui/lang_button.gd new file mode 100644 index 0000000..60bbc6e --- /dev/null +++ b/device/ui/lang_button.gd @@ -0,0 +1,10 @@ +extends BaseButton + +# Emits a signal for the given language. Name the buttons after the locale! + +func pressed(): + emit_signal("language_selected", self.name) + +func _ready(): + add_user_signal("language_selected", ["lang"]) + diff --git a/device/ui/main_menu.gd b/device/ui/main_menu.gd new file mode 100644 index 0000000..e08eba4 --- /dev/null +++ b/device/ui/main_menu.gd @@ -0,0 +1,160 @@ +extends Control + +export(String, FILE) var bg_sound + +var continue_button +var confirm_popup = null +var labels = [] + +func load_autosave(): + vm.load_autosave() + +func can_continue(): + if not main.get_current_scene(): + return false + + return (main.get_current_scene() is esc_type.SCENE) || vm.save_data.autosave_available() + +func button_clicked(): + # play a clicking sound here? + pass + +func new_game_pressed(): + button_clicked() + if main.get_current_scene() is esc_type.SCENE: + confirm_popup = main.load_menu(ProjectSettings.get_setting("escoria/ui/confirm_popup")) + confirm_popup.start("UI_NEW_GAME_CONFIRM",self,"start_new_game") + else: + start_new_game(true) + +func start_new_game(p_confirm): + if !p_confirm: + return + vm.load_file(ProjectSettings.get_setting("escoria/platform/game_start_script")) + vm.run_game() + +func continue_pressed(): + button_clicked() + if main.get_current_scene() is esc_type.SCENE: + main.menu_collapse() + else: + if vm.continue_enabled: + load_autosave() + +func save_pressed(): + button_clicked() + main.load_menu(ProjectSettings.get_setting("escoria/ui/savegames")) + +func settings_pressed(): + button_clicked() + main.load_menu(ProjectSettings.get_setting("escoria/ui/settings")) + +func credits_pressed(): + button_clicked() + main.load_menu(ProjectSettings.get_setting("escoria/ui/credits")) + +func instructions_pressed(): + button_clicked() + main.load_menu(ProjectSettings.get_setting("escoria/ui/instructions")) + +func close(): + main.menu_close(self) + queue_free() + +func input(event): + if event.is_pressed() && !event.is_echo() && event.is_action("menu_request"): + if main.get_current_scene() is esc_type.SCENE: + close() + +func menu_collapsed(): + close() + +func exit_pressed(): + button_clicked() + confirm_popup = main.load_menu(ProjectSettings.get_setting("escoria/ui/confirm_popup")) + confirm_popup.start("UI_QUIT_CONFIRM",self,"_quit_game") + +func _quit_game(p_confirm): + if !p_confirm: + return + get_tree().quit() + +func language_changed(): + for l in labels: + l.set_text(l.get_name()) + +func _find_labels(p = null): + if p == null: + p = self + if p is Label: + labels.push_back(p) + for i in range(0, p.get_child_count()): + _find_labels(p.get_child(i)) + +func set_continue_button(): + if not continue_button: + return + + if vm.continue_enabled and can_continue(): + continue_button.set_disabled(false) + else: + continue_button.set_disabled(true) + +func set_bg_sound(): + var stream = $"stream" + + var resource = load(bg_sound) + stream.stream = resource + resource.set_loop(true) + stream.volume_db = 1 # TODO: Should have all this in ProjectSettings + stream.play() + +func _find_lang_buttons(node=self): + for c in node.get_children(): + if c is preload("res://ui/lang_button.gd"): + c.connect("language_selected", self, "_on_language_selected") + else: + _find_lang_buttons(c) + +func _on_language_selected(lang): + vm.settings.text_lang=lang + TranslationServer.set_locale(vm.settings.text_lang) + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "ui", "language_changed") + vm.save_settings() + +func _find_menu_buttons(node=self): + for c in node.get_children(): + if c is preload("res://ui/menu_button.gd") or c is preload("res://ui/menu_texturebutton.gd"): + var sighandler_name = c.name + "_pressed" + + c.connect("pressed", self, sighandler_name) + + if c.name == "continue": + continue_button = c + else: + _find_menu_buttons(c) + +func _ready(): + set_process_input(true) + + if has_node("stream") and bg_sound: + set_bg_sound() + + # Make sure menu buttons have the correct language when the menu is opened + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "ui", "language_changed") + + main.menu_open(self) + + _find_labels() + + _find_menu_buttons() + + _find_lang_buttons() + + add_to_group("ui") + + call_deferred("set_continue_button") + + if !ProjectSettings.get_setting("escoria/platform/exit_button"): + get_node("exit").hide() + diff --git a/device/ui/main_menu.tscn b/device/ui/main_menu.tscn new file mode 100644 index 0000000..f8dee19 --- /dev/null +++ b/device/ui/main_menu.tscn @@ -0,0 +1,68 @@ +[gd_scene load_steps=9 format=2] + +[ext_resource path="res://ui/main_menu.gd" type="Script" id=1] +[ext_resource path="res://ui/menu_button.gd" type="Script" id=2] +[ext_resource path="res://ui/assets/Nerv-onDark.svg" type="Texture" id=3] +[ext_resource path="res://globals/assets/fonts/Lavi.ttf" type="DynamicFontData" id=4] + +[sub_resource type="CanvasItemMaterial" id=1] + +[sub_resource type="OpenSimplexNoise" id=2] +seed = 500 +octaves = 8 +period = 3.0 +persistence = 0.2 +lacunarity = 4.0 + +[sub_resource type="NoiseTexture" id=3] +width = 3840 +height = 2160 +noise = SubResource( 2 ) + +[sub_resource type="DynamicFont" id=4] +size = 80 +font_data = ExtResource( 4 ) + +[node name="main_menu" type="Control"] +margin_right = 1930.0 +margin_bottom = 1101.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="TextureRect" type="TextureRect" parent="."] +material = SubResource( 1 ) +margin_right = 3862.0 +margin_bottom = 2191.0 +texture = SubResource( 3 ) + +[node name="exit" type="Button" parent="."] +margin_left = -2.0 +margin_top = 40.0 +margin_right = 3858.0 +margin_bottom = 2140.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = SubResource( 4 ) +flat = true +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="new_game" type="Button" parent="."] +margin_left = 1279.0 +margin_right = 2647.0 +margin_bottom = 2140.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +icon = ExtResource( 3 ) +flat = true +expand_icon = true +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/device/ui/menu_button.gd b/device/ui/menu_button.gd new file mode 100644 index 0000000..a04b06b --- /dev/null +++ b/device/ui/menu_button.gd @@ -0,0 +1,15 @@ +extends Button + +export(String) var translation_id + +func language_changed(): + if translation_id: + var ptext = TranslationServer.translate(translation_id) + self.text = ptext + +func _ready(): + if translation_id: + var ptext = TranslationServer.translate(translation_id) + self.text = ptext + + add_to_group("ui") diff --git a/device/ui/menu_texturebutton.gd b/device/ui/menu_texturebutton.gd new file mode 100644 index 0000000..23b97c8 --- /dev/null +++ b/device/ui/menu_texturebutton.gd @@ -0,0 +1,5 @@ +extends TextureButton + +func _ready(): + add_to_group("ui") + diff --git a/device/ui/translated_choice_container.gd b/device/ui/translated_choice_container.gd new file mode 100644 index 0000000..65cc501 --- /dev/null +++ b/device/ui/translated_choice_container.gd @@ -0,0 +1,17 @@ +extends Container + +func language_changed(): + var lang = TranslationServer.get_locale() + + if not has_node(lang): + vm.report_errors("translated_choice_container", ["Locale node not found: " + lang]) + + for child in get_children(): + if child.name == lang: + child.visible = true + else: + child.visible = false + +func _ready(): + add_to_group("ui") + diff --git a/device/ui/translated_label.gd b/device/ui/translated_label.gd new file mode 100644 index 0000000..8500c3a --- /dev/null +++ b/device/ui/translated_label.gd @@ -0,0 +1,12 @@ +extends Label + +export(String) var translation_id + +func language_changed(): + if translation_id: + var ptext = TranslationServer.translate(translation_id) + self.text = ptext + +func _ready(): + add_to_group("ui") + diff --git a/device/ui/translated_rtlabel.gd b/device/ui/translated_rtlabel.gd new file mode 100644 index 0000000..943d81f --- /dev/null +++ b/device/ui/translated_rtlabel.gd @@ -0,0 +1,15 @@ +extends RichTextLabel + +export(String) var translation_id + +func language_changed(): + if translation_id: + var ptext = TranslationServer.translate(translation_id) + if self.bbcode_enabled: + self.bbcode_text = ptext + else: + self.text = ptext + +func _ready(): + add_to_group("ui") + diff --git a/device/ui/verb_menu.gd b/device/ui/verb_menu.gd new file mode 100644 index 0000000..83a3045 --- /dev/null +++ b/device/ui/verb_menu.gd @@ -0,0 +1,21 @@ +extends Control + +var act_buttons = [] + +func action_changed(action): + vm.set_current_action(action) + + for b in act_buttons: + b.set_pressed(b.get_name() == action) + +func _ready(): + + var acts = get_node("actions") + for i in range(acts.get_child_count()): + var c = acts.get_child(i) + if !(c is BaseButton): + continue + + c.connect("pressed", self, "action_changed", [c.get_name()]) + act_buttons.push_back(c) + diff --git a/device/ui/verb_menu.tscn b/device/ui/verb_menu.tscn new file mode 100644 index 0000000..15207d2 --- /dev/null +++ b/device/ui/verb_menu.tscn @@ -0,0 +1,142 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/verb_menu.gd" type="Script" id=1] + +[node name="verb_menu" type="Control"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 40.0 +margin_bottom = 40.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) + +[node name="actions" type="Control" parent="."] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="use" type="Button" parent="actions"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 146.0 +margin_top = 7.0 +margin_right = 262.0 +margin_bottom = 83.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = true +enabled_focus_mode = 2 +shortcut = null +group = null +text = "use" +flat = false + +[node name="look" type="Button" parent="actions"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 14.0 +margin_top = 8.0 +margin_right = 135.0 +margin_bottom = 84.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = true +enabled_focus_mode = 2 +shortcut = null +group = null +text = "look" +flat = false + +[node name="talk" type="Button" parent="actions"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 17.0 +margin_top = 89.0 +margin_right = 132.0 +margin_bottom = 167.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = true +enabled_focus_mode = 2 +shortcut = null +group = null +text = "talk" +flat = false + +[node name="open" type="Button" parent="actions"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 144.0 +margin_top = 87.0 +margin_right = 259.0 +margin_bottom = 165.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = true +enabled_focus_mode = 2 +shortcut = null +group = null +text = "open" +flat = false + +[node name="pick_up" type="Button" parent="actions"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 267.0 +margin_top = 6.0 +margin_right = 382.0 +margin_bottom = 84.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +toggle_mode = true +enabled_focus_mode = 2 +shortcut = null +group = null +text = "pick up" +flat = false + + diff --git a/docs/background.md b/docs/background.md new file mode 100644 index 0000000..11e9a74 --- /dev/null +++ b/docs/background.md @@ -0,0 +1,19 @@ +# Background + +The background node acts as a catch-all for input events. Any input event that isn't captured by another interactive element is processed by this node. + +To add a background to your scene, add a `Sprite` node as the first child of the `scene` node, name it "background", make sure the `Offset/Centered` property is not checked and add the `globals/background.gd` script. + +This enables you to use `Area2D` nodes for your items, which lets you draw one or more `CollisionPolygon2D` child nodes to capture input, using only editor tools for the drawing. You can also add `Sprite` nodes as children of the `Area2D` item node if your items are not part of the background image. + +*Important note:* Having children on the background is possible, but discouraged. Usually there is a way of having them parallel to the background in the node tree, and not much testing has been put into children of the background. Please open an issue on GitHub if you find an insurmountable problem! + +### Parallax + +To create an illusion of perspective in a 2D game, a technique called parallax scrolling effect can be used. This effect is achieved by adding one or more additional background layers, which are moved at a different speed than the foreground when the camera pans. + +To implement a [ParallaxBackground](http://docs.godotengine.org/en/3.0/classes/class_parallaxbackground.html) in Escoria, a `Sprite` with the `globals/background.gd` script must be used for the foreground, as described in the previous section. Since the camera limits are calculated from the size of the `Sprite` texture, the image used must cover the visible space of the room, with transparent regions where the parallax layers will be visible. + +Each layer must have a `Sprite` child node with the `Offset/Centered` property unchecked, and the parallax image must either be larger than the foreground or be repeated using the `Motion/Mirroring` property of the `ParallaxLayer` node to counter the `Motion/Scale` property, which is used to control how fast the layer scrolls. Remember to adjust the `Transform/Position` property of the `ParallaxLayer` to make sure that the layer covers the left and upper edges of the viewport when the scene is played. + +Note: When using `camera_set_zoom` or `camera_set_zoom_height` in combination with parallax backgrounds, the `ParallaxBackground`'s `Scroll/Ignore Camera Zoom` property should be turned off so that the parallax layers do not move when zooming. diff --git a/docs/custom_signals.md b/docs/custom_signals.md new file mode 100644 index 0000000..49d70db --- /dev/null +++ b/docs/custom_signals.md @@ -0,0 +1,65 @@ +# Custom signals + +(Formerly known as `Right-mouse-button behavior`) + +Escoria provides sane defaults for the right mouse button. + +Right-clicking on an inventory item looks at it. + +You can also configure `escoria/ui/right_mouse_button_action_menu` +and follow the instructions in the [HUD](hud.md) document. You get an action menu that +opens on a right-click. + +What if you want more out of the button? Say you want to highlight items or disable the +`"Use Ratchet with Cow"` action. + +## Node structure + +`game` must still be the lowermost node in your scene tree. However, it may have a child +of type `Node`. + +This child is named `signal_script` because it only hosts the script where you implement your +behavior. You do not want to attempt scripting signals without a `Node` to bind to; +see the [connect()](http://docs.godotengine.org/en/latest/classes/class_object.html?highlight=connect#class-object-connect) +documentation. The `target` can not be `self` in a static script and things get real messy +if you try to `preload()` something. + +## Scripting + +You may name your script however you want, like `ui/rmb_hints.gd` or `ui/rmb_hide_tool.gd`. + +It requires only the `_ready():` function, becase it will be the last added to the +scene tree when loading. This means all the nodes and groups will be automagically +available for your convenience. + +Attach this script to `$"game/signal_script"`. + +Note that you do not have to attach it to the main `game` scene itself, if you're using +a custom one. This is because you may want to have mini-games with different behavior. +However, it is possible to attach it to your main `game` as well. + +## Examples + +You have an example in `device/demo/ui/rmb_hints.gd`. + +Another could look like this: + +``` +# Create a file like this in your game to define right-mouse-button +# behavior. + +extends Node + +func hide_current_tool(): + var game = get_parent() + + if game.current_action == "use" and game.current_tool: + vm.clear_action() + get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFAULT, "hud", "set_tooltip", "") + +func _ready(): + for bg in get_tree().get_nodes_in_group("background"): + bg.connect("right_click_on_bg", self, "hide_current_tool") + +``` + diff --git a/docs/designing_dialogs.md b/docs/designing_dialogs.md new file mode 100644 index 0000000..b354bf5 --- /dev/null +++ b/docs/designing_dialogs.md @@ -0,0 +1,41 @@ +# Designing dialogs + +The story is usually the cornerstone of an adventure game, and in visual media, stories are often told through interactions between characters. To make it easy to add dialog to your game, Escoria supports a flexible syntax for writing dialogs, backed by a heavily custimizible visual interface. This document is intended to help customize the appearence of such dialogs in your game. For a reference on scripting the dialogs, see the [ESC reference](esc_reference.md). + +### Dialog players + +To make your dialogs available from esc scripts, they must be added to one of the dialog players. When playing dialogs, the `type` parameter will be used to look up the correct dialog scene by name in the `ResourcePreloader` in either `device/ui/dialog_player.tscn` for the `say` command or `device/ui/dd_player.tscn` for the `?` command. If no `type` parameter is provided or the given type does not exist in the player, the "default" dialog scene will be used. + +If a `type` named "bottom" is registered, and the game's inventory is collapsible and open, this instance will be used with a fixed position to play the dialog. + +### Speech dialog + +The default speech dialog scene is located in `device/ui/dialog_default.tscn`. If you wish to customize its appearance, make a copy like `device/game/ui/dialog_default.tscn`, and replace the "default" scene in `device/ui/dialog_player.tscn` with your own, using the bottom-pane file picker. Remember that the name in the `ResourcePreloader` refers to the type of dialog UI to use, and that "default" will be used if no `type` parameter is provided. + +You may experience difficulties when trying to adjust the `text` node to your liking. Godot isn't very intuitive when it comes to customizing the `RichTextLabel`, but hopefully this will behave more like a simple `Label` in the future. In the meantime, Escoria relies heavily on the current type, as it should, to enable italic and bold font. + +The project setting `escoria/platform/dialog_force_centered` will center your dialog text if your game is like SCUMM games, with the dialog above the characters. If you use avatars and big boxes for dialog, you will probably want to leave this disabled. + +The dialog players are unlikely to change much upstream, so editing and adding multiple dialog scenes should rarely cause merge conficts. However, if you have multiple game directories within the framework, you are adviced to name your dialog resources sensibly and use the `type` parameter to choose which one to use with the `say` command. + +For further reading, see the Floss Manuals section on [speech text](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/dialog-text/). + +### Branching dialog + +To create unique looking dialog trees, custom made for your game, you can duplicate `device/demo/ui/dd_default.tscn`, and adjust these nodes to your liking: + +- `dialog` - This is the root of the branching dialog scene, and you should adjust the `Anchor` and `Margin` properties of it to make dialogs appear where you want within the viewport. As an example, to center position dialog horizontally, you will want to set the left+right anchors to `0.5` (center) and set the left+right margins to minus half the size of your dialog width. This will make sure the dialog is centered, no matter the screen width. +- `bg` - This node can be used as a backdrop for dialog by loading an image into the `Texture` property. You may optionally want to set the `Rect/Size`, `Expand` and `Visibility/Modulate` properties to use a repeating, semi transparent background. +- `scroll` - This node defines the area of the dialog scene that will be used to display text options. Use the `Rect/Position` and `Rect/Size` properties to set position and size. +- `avatars` - This node defines the area where avatars will be displayed, if set with the optional `type` parameter. To use, add one or more `Sprite`s as children of this node, and use the `Rect/Position` and `Rect/Size` properties to set position and size. +- `item` - This node is the template for text options and is instanced into the dialog tree as needed. Use the `Rect/Min Size/y` property to define the vertical space used to display each line of dialog, and use the exported colors on the `dialog` node to set font color. Once set, you can use the `escoria/platform/dialog_option_height` project setting if you want to quickly change the height of all your dialog tree scenes. + +You should not change the size or position of any of the other nodes, since that might make it difficult to make the dialogs appear the way you want. + +Additionally, you can tweak the "show" and "hide" animations of the `animation` node however you want. + +For further reading, see the Floss Manuals sections on the [dialog interface](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/how-to-polish-the-game/) and [dialog trees](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/dialog-trees/). + +### Font + +If you want to change the font of one of your dialogs, use the `Custom Fonts` section on your `Label` or `RichTextLabel`. To use a vector font, select the `New DynamicFont` option from the drop-down, then select `Edit`, and load your font file into the `Font/Font Data` property. If you want to re-use this font in multiple dialogs, you may want to save the font as a resource file (`.tres` file), and use the `Theme` property to load it into your `Label`s and `RichTextLabel`s. diff --git a/docs/esc_reference.md b/docs/esc_reference.md new file mode 100644 index 0000000..1997465 --- /dev/null +++ b/docs/esc_reference.md @@ -0,0 +1,358 @@ +General +------- + +- Events: + + When a scene is loaded, it may have events. We will not cover `:load` as it is used only internally for save games, nor will we cover [:start](starting-a-game.md) here. + + To initialize a room properly, you may want to use `:setup` like this: + +``` +:setup +teleport player door1 [eq last_exit door1] +teleport player door2 [eq last_exit door2] +``` + + It covers the fact that your `player` can be set anywhere in the room and be visible for just a moment before the `teleport` happens. + + It's not mandatory, nor mutually exclusive with `:setup` to have a `:ready` event. + +``` +:ready +say player player_wants_out:"I want out! To live my life and to be free!" [want_out] +``` + + When the player interacts with an object in the game, the object receives an even. Each event executes a series of commands. + Events start with the character ":" in the Events file. Example: + +``` +:use +``` + + Especially useful for fallbacks, you can give flags to events: + +``` +:use | TK +say player player_does_not_wanna:"I don't wanna." +``` + + These flags are implemented: + + * `TK` stands for "telekinetic". It means the player won't walk over to the item to say the line. + * `NO_TT` stands for "No tooltip". It hides the tooltip for the duration of the event. Probably not very useful, because events having multiple `say` commands in them are automatically hidden. + * `NO_HUD` stands for "No HUD". It hides the HUD for the duration of the event. Useful when you want something to look like a cut scene but not disable input for skipping dialog. + * `NO_SAVE` disables saving. Use this in cut scenes and anywhere a badly-timed autosave would leave your game in a messed-up state. + * `CUT_BLACK` applies only to `:setup`. It makes the screen go black during the setup phase. You will probably see a quick black flash, so use it only if you prefer it over the standard cut. + * `LEAVE_BLACK` applies only to `:setup`. In case your `:ready` starts with `cut_scene telon fade_in`, you must apply this flag or you will see a flash of your new scene before going black again for the `fade_in`. + +- Groups + Commands can be grouped using the character ">" to start a group, and incrementing the indentation of the commands that belong to the group. Example: +``` +> + set_global door_open true + animation player pick_up +# end of group +``` + +- Global flags + Global flags define the state of the game, and can have a value of true, false or an integer. All commands or groups can be conditioned to the value of a global flag. + +- Conditions + In order to run a command conditionally dependin on the value of a flag, use [] with a list of conditions. All conditions in the list must be true. The character "!" before a flag can be used to negate it. + Example: + +``` +# runs the command only if the door_open flag is true +say player "The door is open" [door_open] +``` + +``` +# runs the group only if door_open is false and i/key is true +> [!door_open,i/key] + say player "The door is close, maybe I can try this key in my inventory" +``` + + Additionally, there's a set of comparison operators for use with global integers: `eq`, `gt` and `lt`, all of which can be negated. + Example: + +``` +# runs the command only if the value of pieces_of_eight is greater than 5 +set_state inv_pieces_of_eight money_bag [gt pieces_of_eight 5] +``` + +- Commands + Commands consist of one word followed by parameters. Parameters can be one word, or strings in quotes. A string can also be preceeded by an ID for localization and the ":" character. Example: + +``` +# one parameter "player", another parameter "hello world", with id "dialog_hello" +say player dialog_hello:"hello world" +``` + +- Global IDs + All objects in the game have a global ID, which is used to identify them in commands. The ID is configured in the object's scene. + +- States + Each object can have a "state". The state is stored in the global state of the game, and the savegame, and it's set on the object when the scene is instanced. States can also be animations with the same name in the object's scene, in that case the animation is run when the state is set. + +- Active + Objects can be active or not active. Non-active objects are hidden and non clickable. + +- Blocking + Some commands will block execution of the event until they finish, others won't. See the command's reference for details on which commands block. + +Game global state +----------------- + +The following values are saved in the global game state and savegames: + + +- Global flags +- Object's "state" values +- Object's "active" values +- Object's potisions if moved + + +Inventory +--------- + +The inventory is handled as a special case of global flags. All flags with a name starting with "i/" are considered an inventory object, with the object's global id following. Example: + +``` +# adds the object "key" to the inventory +set_global i/key true +``` + +``` +# removes the object "key" to the inventory +set_global i/key false +``` + +Is item active? +--------------- + +Item activity is also handled as a special case of global flags. If the check starts with "a/", like "a/elaine", you're checking if "elaine" is active. + +``` +:ready +> [!a/elaine] + say player player_no_elaine_yet:"It would appear Elaine hasn't arrived yet." +``` + +Caveat: This works only when `set_active` has been called. Therefore if "elaine" is not set `active` in the editor but `set_active elaine true` is +called later, everything works as expected. If you have an item that can be picked up, even if it's `active` in the editor, but its state *may* toggle, +you must use this pattern: + +``` +:setup +set_active broom true [!i/inv_broom] +``` + +Now it has been explicitly set and it will work. The underlying technical rationale is way beyond the scope of this reference; just trust us +that it's the best way to go. + +Is item interactive? +-------------------- + +If you have something that only blocks the terrain, something you can move behind, you probably don't want to hassle with interaction areas +and tooltip texts. In this case, just set `is_interactive` to `false` and the item will not be checked for areas and its mouse events will +not be connected. + +Command list +------------ + +- `debug string [string2 ...]` + Takes 1 or more strings, prints them to the console. + +- `set_global name value` + Changes the value of the global "name" with the value. Value can be "true", "false" or an integer. + +- `dec_global name value` + Subtracts the value from global with given "name". Value and global must both be integers. + +- `inc_global name value` + Adds the value to global with given "name". Value and global must both be integers. + +- `set_globals pattern value` + Changes the value of multiple globals using a wildcard pattern. Example: + +``` +# clears the inventory +set_globals i/* false +``` + +- `accept_input [ALL|NONE|SKIP]` + What type of input does the game accept. `ALL` is the default, `SKIP` allows skipping of dialog but nothing else, `NONE` denies all input. Including opening the menu etc. + `SKIP` and `NONE` also disable autosaves. + Note that `SKIP` gets reset to `ALL` when the event is done, but `NONE` persists. This allows you to create cut scenes with `SKIP` where the dialog can be skipped, but also + initiate locked-down cutscenes with `accept_input NONE` in `:setup` and `accept_input ALL` later in `:ready`. + +- `set_state object state` + Changes the state of an object, and executes the state animation if present. The command can be used to change the appearance of an item or a player character. + +When used on a player object, the command is used to dress the player in a costume identified by the state parameter. An `AnimationPlayer` with the given parameter should be a child of the player node, although one named "animation" is always the fallback when trying set a missing costume. + +Items can also change state by playing animations from an `AnimationPlayer` named "animation". The `AnimationPlayer` is typically used to change the texture of a `Sprite` node, but it's also possible to add additional tracks for changing the tooltip and other properties of the item scene. By using keyframes and looping, any given state can also use multiple textures to bring more life to the item. + +- `set_hud_visible visible` + If you have a cut-scene-like sequence where the player doesn't have control, and you also have HUD elements visible, use this to hide the HUD. You want to do that because it explicitly signals the player that there is no control over the game at the moment. "visible" is true or false. + +- `say object text [type] [avatar]` + Runs the specified string as a dialog said by the object. Blocks execution until the dialog finishes playing. Optional parameters: + - "type" determines the type of dialog UI to use. Default value is "default" + - "avatar" determines the avatar to use for the dialog. Default value is "default" + +- `anim object name [reverse] [flip_x] [flip_y]` + Executes the animation specificed with the "name" parameter on the object, without blocking. The next command in the event will be executed immediately after. Optional parameters: + - reverse plays the animation in reverse when true + - flip_x flips the x axis of the object's sprites when true (object's root node needs to be Node2D) + - flip_y flips the y axis of the object's sprites when true (object's root node needs to be Node2D) + +- `cut_scene object name [reverse] [flip_x] [flip_y]` + Executes the animation specificed with the "name" parameter on the object, blocking. The next command in the event will be executed when the animation is finished playing. Optional parameters: + - reverse plays the animation in reverse when true + - flip_x flips the x axis of the object's sprites when true (object's root node needs to be Node2D) + - flip_y flips the y axis of the object's sprites when true (object's root node needs to be Node2D) + +- `play_snd object file [loop]` + Plays the sound specificed with the "file" parameter on the object, without blocking. You can play background sounds, eg. during scene changes, with `play_snd bg_snd res://...` + +- `set_active object value` + Changes the "active" state of the object, value can be true or false. Inactive objects are hidden in the scene. + +- `set_interactive object value` + Sets whether or not an action menu should be used, and a tooltip shown, on object. It must use the `item.gd` script. Value can be true or false. Useful for "soft-disabling" objects without removing them by `set_active`. + +- `wait seconds` + Blocks execution of the current script for a number of seconds specified by the "seconds" parameter. + +- `change_scene path run_events` + Loads a new scene, specified by "path". The `run_events` variable is a boolean (default true) which you never want to set manually! It's there only to benefit save games, so they don't conflict with the scene's events. + +- `set_speed object speed` + Sets how fast object moves. It must use the `interactive.gd` script or something extended from it. Value is an integer. + +- `teleport object1 object2 [angle]` + Sets the position of object1 to the position of object2. By default object2's `interact_angle` is used to turn `object1`, but `angle` will override this. + Useful for doors and such with an `interact_angle` you don't always want to adhere to when re-entering a room. + +- `slide object1 object2 [speed]` + Moves object1 towards the position of object2, at the speed determined by object1's "speed" property, unless overridden. This command is non-blocking. It does not respect the room's navigation polygons, so you can move items where the player can't walk. + +- `slide_block object1 object2 [speed]` + Moves object1 towards the position of object2, at the speed determined by object1's "speed" propert, unless overriddeny. This command is blocking. It does not respect the room's navigation polygons, so you can move items where the player can't walk. + +- `walk object1 object2 [speed]` + Walks, using the walk animation, object1 towards the position of object2, at the speed determined by object1's "speed" property, unless overridden. This command is non-blocking. + +- `walk_block object1 object2 [speed]` + Walks, using the walk animation, object1 towards the position of object2, at the speed determined by object1's "speed" property, unless overridden. This command is blocking. + +- `turn_to object degrees` + Turns `object` to a `degrees` angle with a `directions` animation. + 0 sets `object` facing forward, 90 sets it 90 degrees clockwise ("east") etc. When turning to the destination angle, animations are played if they're defined in `animations`. + `object` must be player or interactive. + `degrees` must be between [0, 360] or an error is reported. + +- `set_angle object degrees` + Turns `object` to a `degrees` angle without animations. + 0 sets `object` facing forward, 90 sets it 90 degrees clockwise ("east") etc. When turning to the destination angle, animations are played if they're defined in `animations`. + `object` must be player or interactive. + `degrees` must be between [0, 360] or an error is reported. + +- `spawn path [object2]` + Instances a scene determined by "path", and places in the position of object2 (object2 is optional) + +- `stop` + Stops the event's execution. + +- `repeat` + Restarts the execution of the current scope at the start. A scope can be a group or an event. + +- `sched_event time object event` + Schedules the execution of an "event" found in "object" in a time in seconds. If another event is running at the time, execution starts when the running event ends. + `event` can consist of multiple words like in `sched_event 0 tow_hook use inv_rope` + +- `custom obj func_name [params]` + If `obj` has a `(Node2D) custom` node, `func_name` will be searched for in its script and called with `params`. See device/contrib/custom/spine.gd for an example. + +- `camera_set_drag_margin_enabled h v` + "h" and "v" are booleans for whether or not horizontal and vertical drag margins are enabled. You will likely want to set them `false` for advanced camera motions and `true` for regular gameplay and/or tracking NPCs. + +- `camera_set_pos speed x y` + Moves the camera to a position defined by "x" and "y", at the speed defined by "speed" in pixels per second. If speed is 0, camera is teleported to the position. + +- `camera_set_target speed object [object2 object3 ...]` + Configures the camera to follow 1 or more objects, using "speed" as speed limit. This is the default behavior (default follow object is "player"). If there's more than 1 object, the camera follows the average position of all the objects specified. + +-`camera_set_zoom magnitude [time]` + Zooms the camera in/out to the desired magnitude. Values larger than 1 zooms the camera out, and smaller values zooms in, relative to the default value of 1. An optional time in seconds controls how long it takes for the camera to zoom into position. + +-`camera_set_zoom_height pixels [time]` + Similar to the command above, but uses pixel height instead of magnitude to zoom. + +-`camera_push target [time] [type]` + Push camera to target. Target must have `camera_pos` set. If it's of type `Camera2D`, its zoom will be used as well as position. + `type` is any of the `Tween.TransitionType` values without the prefix, eg. `LINEAR`, `QUART` or `CIRC`; defaults to `QUART`. + A `time` value of 0 will set the camera immediately. + +-`camera_shift x y [time] [type]` + Shift camera by `x` and `y` pixels over `time` seconds. + `type` is any of the `Tween.TransitionType` values without the prefix, eg. `LINEAR`, `QUART` or `CIRC`; defaults to `QUART`. + +- `queue_resource path front_of_queue` + Queues the load of a resource in a background thread. The path must be a full path inside your game, for example "res://scenes/next_scene.tscn". The "front_of_queue" parameter is optional (default value false), to put the resource in the front of the queue. Queued resources are cleared when a change scene happens (but after the scene is loaded, meaning you can queue resources that belong to the next scene). + +- `queue_animation object animation` + Similar to queue_resource, queues the resources necessary to have an animation loaded on an item. The resource paths are taken from the item placeholders. + +- `game_over continue_enabled show_credits` + Ends the game. Use the "continue_enabled" parameter to enable or disable the continue button in the main menu afterwards. The "show_credits" parameter loads the `ui/end_credits` scene if true. You can configure it to your regular credits scene if you want. + +Dialogs +------- + +To start a dialog, use the "?" character, with some parameters, followed by a list of dialog options. This hides the HUD. Each option starts with the "-" character, followed by a parameter with the text to display in the dialog interface. Inside the option, a group of commands is specified using indentation. Use "!" to signify the dialog is over and the HUD may be restored. The HUD will not be restored if the running event is flagged NO_HUD. Either way the Escoria virtual machine will know if the game is in a dialog context. + +Example: + +``` +# character's "talk" event +:talk +? type avatar timeout timeout_option + - "I'd like to buy a map." [!player_has_map] + say player "I'd like to buy a map" + say map_vendor "Do you know the secret code?" + ? + - "Uncle Sven sends regards." + say player "Uncle Sven sends regards." + + > [player_has_money] + say map_vendor "Here you go." + say player "Thanks!" + inventory_add map + set_global player_has_map true + stop + + > [!player_has_money] + say map_vendor "You can't afford it" + say player "I'll be back" + ! + stop + + - "Nevermind" + say player "Nevermind" + ! + stop + - "Nevermind" + say player "Nevermind" + ! + stop +repeat +``` + +All parameters are options: + - type: (default value "default") the type of dialog menu to use. All types are in the "dd_player" scene. + - avatar: (default value "default") the avatar to use in the dialog ui. + - timeout: (default value 0) timeout to select an option. After the time has passed, the "timeout_option" will be selected automatically. If the value is 0, there's no timeout. + - timeout_option: (default value 0) option selected when timeout is reached. + diff --git a/docs/export.md b/docs/export.md new file mode 100644 index 0000000..f25dfd5 --- /dev/null +++ b/docs/export.md @@ -0,0 +1,43 @@ +# Exporting your game + +When you have finished making your game, you will want to make exports so that others can play it. Escoria currently support GNU/Linux, macOS and Windows, but we will hopefully support the same platforms that Godot does in the future. + +### Preparations + +The first thing you need to do is to download export templates for the platforms you want to support. Select `Editor > Manage Export Templates` from the menu and click the `Download` button. Select a mirror and wait for the download to finish. + +If you want to compile your own export templates, in case you modified the engine in some way, eg. by adding Spine support or cherry-picking commits that haven't hit stable yet, these are the preferred configurations: + + * Linux: `scons platform=x11 tools=no target=release bits=64 builtin_libpng=yes builtin_openssl=yes builtin_zlib=yes debug_symbols=no use_static_cpp=yes use_lto=yes` + * Windows: `scons platform=windows tools=no` + * macOS: `scons platform=x11 tools=no` + +The Linux variant ensures maximum future-proofness to the extent Godot provides it. Windows is known for good backward compatibility so it doesn't matter. macOS is a bizarre platform to develop for so who knows what the future brings anyway. + +Do add `-j` for parallel building with your core count as the value. `use_lto` requires a recent GCC, but this should not present a problem on any modern distribution. + +If you use OGG videos for animation sequences, please cherry-pick https://github.com/godotengine/godot/commit/6dc20adadd721bfc31a6b761eb6224975938dbf4 and compile your own export templates. If you don't, the video will be looked for outside the exported `.pck` file and you will have to structure your export manually. It is not fun, especially not when exporting a DMG on macOS. This commit should be in Godot 3.1 when it's released. In the meantime, cherry-picking is the way to go. + +### Configuring for export + +During development Escoria defaults to having a non-resizable windowed game in FullHD. This is based on the assumption games are not developed for or targeted at lower resolutions. Many setups provide resolutions higher than FullHD and the development experience is much better anyway if previewing the game doesn't occupy all your screen real estate. + +When exporting, these preferences must change. It's recommended to set the following conditions in `project.godot`: + + * `window/size/fullscreen=true` + * `window/size/resizable=true` + +This is a hard requirement on macOS and works for everything else as well. The window will not be resized properly for fullscreen unless resizable is also true. + +It is further recommended to set the following conditions to avoid showing Escoria's "debug chatter": + + * `run/disable_stdout=true` + * `run/disable_stderr=true` + +### Exporting + +When you are ready to make your exports, select `Project > Export` from the menu, and add as many platform presets as you wish. In the `Resources` tab, make sure to add `*.esc` as a filter to export non-resource files, click `Export Project`, choose a path and file name, and click `Save`. Repeat the procedure for the remaining platforms. + +You may also want to add files like `*.ogv` if you use OGG videos for animation sequences. + +For more details, see the Floss Manuals section on [Exporting projects](http://docs.godotengine.org/en/3.0/getting_started/workflow/export/exporting_projects.html). diff --git a/docs/git_submodule.md b/docs/git_submodule.md new file mode 100644 index 0000000..931ed2f --- /dev/null +++ b/docs/git_submodule.md @@ -0,0 +1,68 @@ +# Using git submodules to develop a game + +Caveat: This is not necessarily the one true way. The framework and its practices +are constantly being developed, but this document contains some pointers. + +Submodules are a bit advanced and specific to your project, and therefore the commands +you may use are as well. You can get into Git [here](https://git-scm.com/book/en/v1/Getting-Started). + +This is the [chapter on submodules](https://git-scm.com/book/en/v1/Git-Tools-Submodules). + +## Why? + +Git's strength is in how well it handles remotes and branches. This may go beyond +Git 101, but it's a very efficient way to work once you get into it. + +Having the framework separate from your game also helps to identify changes to the +code you may have to make. The goal of a framework is to be completely separate from +whatever it's used for, so please open issues on GitHub if you find yourself making +changes to the framework that aren't bugfixes or new features. + +If possible, please make a PR that alters Escoria's behavior, eg. through `ProjectSettings`, +so that the change doesn't happen in Escoria's upstream code. + +You may read more about this in the ../CONTRIBUTING.md document. + +## How? + +Create a git repository and add the Escoria GitHub URL as a remote, eg. `gh-escoria`. +This document will refer to your repository as `origin`. + +Reset your `master` to match whatever `gh-escoria/master` is at the time. + +Escoria is not (at the time of writing this) at the stage where you never have to touch +the framework code. Whether you contribute or not, or whatever workflow you want to use, +this document will refer to your work branch as `devel`. This is checked off of `master`. + +The project root is `device/`. It contains your project settings. When submitting branches +for merging, please do not include your game's settings. Any new features should be disabled +by default, to not mess with the tutorial [*Creating Point and Click Games with Escoria*](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/). + +## Actual work + +Create a new repository for the submodule. + +Add it as `device/game/`. + +You can also create subdirectories like `rooms`, `actors` and `items`. The best practices are +not clearly defined yet, but such a naming scheme correlates with tradition. As your game comes +along, you will logically create a subdirectory for every item or actor and so on. + +Now you're ready to launch Godot! + +## Misc notes + +Whatever is parallel to `game/` can be copied under `game/` and the project structure changed +accordingly. This is your last resort if you can't expose a setting. + +As another use case, if you want to use the `demo/ui/action_menu.tscn` for a template, you could +copy it under `game/ui/` - your submodule - and maintain it there. + +In case you want to edit the "heads-up display", `ui/hud.tscn`, you will have to do it in-tree +at least for the time being. Take care not to include it in any PRs upstream. + +If you want to contribute to Escoria upstream, please do not submit a branch that is based on +`devel`. It may feel like branch acrobatics to have one taken off `master` and rebased onto +`devel` for live work, maybe even cherry-pick-squashing changes from the `devel` version onto +the `master` version, but it is necessary to keep the Escoria code base as clean as possible. + diff --git a/docs/hud.md b/docs/hud.md new file mode 100644 index 0000000..3f07ce7 --- /dev/null +++ b/docs/hud.md @@ -0,0 +1,58 @@ +# HUD + +The HUD layer contains the tooltip, the verb or action menu and the inventory. + +By default Escoria is configured to use an "old-school SCUMM" layout, with a verb +menu to the bottom-left side and the inventory to the bottom-right. + +An alternative HUD is provided as well, `ui/hud_minimal.tscn`. + +This documentation exists because of some limitations in how Godot's scenes work. + +Caveat: verb and action menus are incompatible. You will experience problems if you +use a verb menu and configure an action menu in your settings. You have been warned! + +## Customizing the verb menu + +First you'll have to make a copy of `device/globals/game.tscn` to `game/game.tscn` +so you can replace the placeholder HUD. Remember that the HUD contains the verb menu. + +Then you create a verb menu to your liking. You may copy `device/demo/ui/verb_menu.tscn` +to `game/ui/verb_menu.tscn` and use it as your base. Hook this up in `game/game.tscn` + +Use your `game/game.tscn` as your bottom-most node in the scene tree. + +Last you'll have to copy `device/ui/hud.tscn` to `game/ui/hud.tscn`. Alter it to +use your new verb menu. + +Adapting the steps above, you may also replace the inventory in your HUD. + +You can configure which HUD to use in the project settings. The path is +`Escoria -> Ui -> Hud`. + +If you want something completely unique to your needs, you may create a completely +new HUD scene in your `game/ui/` directory and configure the settings accordingly. +This is in case you don't want an inventory at all or want something new in your +HUD. + +From there on you may also create a unique-to-your-needs copy of the `hud.gd` +script and use it in your HUD scene. + +## Making games with an action menu + +By action menu we mean a "new-school SCUMM" menu, also known as a "verb coin". + +Since Escoria uses the "old-school SCUMM" UI layout, with a verb menu, it is +visible as a placeholder even when you don't want it. Let's address that. + +The first step is to add `game_am.tscn` instead of `game.tscn` as your lowermost +node in the scene tree. This does not contain the verb-menu placeholder. + +Second you'll want to configure `Escoria -> Ui -> Hud` use `res://ui/hud_minimal.tscn`. + +Your action menu is a scene like any other. Create it as `device/game/ui/action_menu.tscn` +and configure it into `Escoria -> Ui -> Action Menu`. + +The inventory is also a scene. You may take example from `device/demo/ui/inventory.tscn` +and place it in `device/game/ui/inventory.tscn`. Configure it in `Escoria -> Ui -> Inventory`. + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..76ebea4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,57 @@ +Escoria official documentation +============================== + +This page lists all existing documentation pages for the use of Escoria framework for Godot Engine. + +Installation +------------ + +[Git submodules](git_submodule.md) + +Reference +--------- + +[How a game is started](starting-a-game.md) + +[Background](background.md) + +[Terrain](terrain.md) + +[Designing dialogs](designing_dialogs.md) + +[HUD](hud.md) + +[Items](items.md) + +[Languages, tooltips and dialogs](lang_tooltip_dialog.md) + +[Music](music.md) + +[Right-mouse-button behavior](right_mouse_button_action_menu.md) + +[Triggers and exits](triggers-and-exits.md) + +[Shadows](shadows.md) + +Scripting +--------- + +[ESC reference](esc_reference.md) + +[Setting states](setting_states.md) + +[Tool scripts](tool_scripts.md) + + +Tools +----- + +[Tool scripts](tool_scripts.md) + +[Git submodules](git_submodule.md) + + +Miscellaneous +------------- + +[Exporting your game](export.md) diff --git a/docs/items.md b/docs/items.md new file mode 100644 index 0000000..dda9e79 --- /dev/null +++ b/docs/items.md @@ -0,0 +1,62 @@ +# Items + +Items are objects that have several attributes specifically designed to make the player character interact with them, using actions defined in their attached .esc script. There are a few different ways to design an item in the editor, but an item saved as a separate scene an instanced into the room typically has the following anatomy: + +* [item] - a `Node2D` which an `item.gd` script attached to, and to which the properties you want to use for this item. This can be an `Area2D` or a `Sprite`, in most cases. + * "area" - an `Area2D`, in case your item root is `Sprite`. + * "CollisionShape2D" or "CollisionPolygon2D" - The name of this does not matter, it's simply what `Area2D` expects to find. + * "iteract_pos" - a `Position2D` which determines the position the player character will walk to before interacting with the item + * "animation" - an `AnimationPlayer` used to change properties of the item by using the `set_state` .esc command. + * "_focus_in" - a `Sprite` to show when the item has focus + * "_focus_out" - a `Sprite` to show when the item does not have focus + * "_pressed" - a `Sprite` to show when the item is clicked + +Often you will find that you don't need to add all of these child nodes. + +## Interaction + +The `item.gd` script exports the `action` variable. This is the default action for this object. If you wish, you may also configure a global default action in `escoria/platform/default_object_action`. Now you can leave this empty in the editor and have the player always perform this action by default. + +It may be that you want these default actions to require double-clicks. This is configured in `escoria/platform/default_object_action_requires_doubleclick`. It applies to both the global and local default actions. + +An item-local default action overrides a global default action, as you'd expect. + +### Interact angle + +Items have an `interact_angle` variable. This defines the angle to which the player turns to when interacting. + +The unit is counter-clockwise degrees: 0 faces the camera, 90 the right, etc. + +You can leave it at `-1` to have Escoria resolve the interaction angle for you. This is usually the easiest way, but sometimes +item positions can be in slightly unintuitive coordinates, so if your game doesn't feel right, you can always set a value here. + +## Z-index + +By default every room in the game is considered to have faux "depth". This is done by setting the z-index based on the y-coordinate. + +If you want things to be flat or manually controlled, you can use Godot's z-indexes by unchecking the `Dynamic Z Index` checkbox. +At least for certain inventory configurations, this is something you must do. If your items don't show up on the HUD, uncheck! + +The system isn't perfect, so sometimes you may have to use an `AnimationPlayer` to tweak the z-index given by Godot. Likewise +if your item consists of many nodes, they will have different positions, so some of them may need manual tweaking. + +## Background items + +There are also times when you want to enable the player to interact with items which are not separate scenes, but drawn directly on the background. For such use cases, you will typically use an `Area2D` to frame the item, with an optional click mask, and no "area" child node. + +* [item] - an `Area2D` with an `item.gd` script attached and the properties you want to use for this item + * `CollisionPolygon2D` which defines the shape of the item + * ... + +## Parallax items + +Items can also be added to a parallax layer, but this comes with a caveat. You can't interact with them at the moment. This is because we can't +mix `Control` (UI) and `Node2D` (game) elements anymore. Godot 3.1 will bring a stricter type system. The rules for how priorities are dealt +with when mixing UI elements with game elemnts are complex. There are probably more reasons, at least from a design-philosophy point of view. + +This is what the text used to say. It's left as a guide in case it would be helpful in reimplementing the interactability with parallax-background +objects without the use of `Control` nodes. + +~First of all, you need to use an item with the shape defined by a `Control` node, since the mouse cursor cannot interact with an `Area2D` through the foreground layer. Secondly, since these items will be visible to the cursor through the foreground, any items meant to be obstructed by foreground elements will need to be masked by a `Control` node. These masks don't need to be items, but can be `TextureRects` with click masks for more fine grained control of their shape. Do take care not to cover any items in the foreground with these masks though, as that will make them impossible to interact with.~ + +~Because parallax layers are offset from the background, interacting with them might lead to surprising results. It is therefore recommended that you add `Position2D` node in the foreground layer where you want the player character to stand when interacting with parallax items. This can be achieved by setting the `Interact Position` property on the item and assigning the correct `Position2D` node in the scene.~ diff --git a/docs/lang_tooltip_dialog.md b/docs/lang_tooltip_dialog.md new file mode 100644 index 0000000..21fbf31 --- /dev/null +++ b/docs/lang_tooltip_dialog.md @@ -0,0 +1,56 @@ +# Languages, tooltips and dialog text + +Or how I learned to stop worrying and love internationalization. + +## Language configuration / Text locales + +Escoria supports separate languages for text and voice. Text includes speech and tooltips. + +These are configured in your project settings as defaults, but whoever plays your game can +alter them, save them and load them, if you provide more than one language option. + +### Development language and translations + +Set the language code used during development in `escoria/platform/development_lang`. This means the language you use for tooltips and writing dialog. + +This is required so Escoria can skip the translation code if the game is +in the same language as during development. + +If you're translating for a language that conjugates use/combine tooltips, you can add new +identifiers for tooltips that end in `.object1` and `.object2` with the proper words. + +In Finnish, for example, you would have `key.tooltip` say "Avain", `key.tooltip.object1` say "avainta" +and `key.tooltip.object2` say "avaimeen". + +Protip: this same functionality can be used to lowercase yor tooltips in use/combine +situations where your language requires it. German is exempt, as German nouns are always capitalized. + +Translating the texts are documented further in [The Escoria book](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/i18n/) + +### Changing locales + +Add buttons (inheriting from `BaseButton`) to your menu, which is any menu using `main_menu.gd`. +Name the buttons after the locale, eg. `fr` or `de`. Now you are able to change languages by clicking +the buttons. + +### Text timeout + +In case you don't have audio to determine how long your dialog text is visible, you can +change the `escoria/application/text_timeout_seconds` to your liking. This is how quickly +the dialog text disappears on its own. + +## Speech locales + +You must create a file like + +``` +#warning-ignore:unused_class_variable +var speech_locales = ["en", "de", "fr"] +``` + +in your game directory and configure `escoria/application/speech_locales_path` to point to it. + +This is the list of accepted and supported locales in your game. + +The game will crash if you define a locale that's not accepted, so don't add unsupported +locales to your game code or define an unsupported default ;) diff --git a/docs/menus.md b/docs/menus.md new file mode 100644 index 0000000..0728e58 --- /dev/null +++ b/docs/menus.md @@ -0,0 +1,114 @@ +# Menus + +There are two specific types of menus in Escoria. One is the "main menu" +and the other is the "in-game menu". + +These are defined in `escoria/ui/in_game_menu` and `escoria/ui/main_menu`. + +The menu system is somewhat intricate and certainly not final or stable +at the time of writing this. For example localization is quite lacking +when textures are used for buttons. + +## Prior documentation + +The documentation [at flossmanuals](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/game-menues/) is fairly well up to date, +so you may use that as a reference as well. + +## Required nodes + +Your base node must be `Control`. + +You are free to use `Container` nodes to lay out your menu. + +Attach `ui/menu_button.gd` or `ui/menu_texturebutton.gd` to your menu buttons and `ui/lang_button.gd` +to your language-changing buttons. + +The buttons' signal handlers are resolved dynamically. This means that you +should name the buttons like + + * new_game + * continue + * exit + * credits + * instructions + * save + +The use of `TextureButton` is possible if you don't intend to switch locales. +Sometime in the future we would like to support localized `TextureButton` buttons. + +## Optional nodes + +If you want background music, add an `AudioStreamPlayer` by the name +of `stream` and set a file in the `bg_sound` variable. It will play +and loop automatically + +## Spawning menus + +By default a menu is requested by hitting the escape button. You can +configure this in the `Input Map` tab in your project settings. + +The menu that opens up is the in-game menu. + +The main menu is used only when starting the game. + +## Confirm popup + +The structure similar to a regular menu. You have a base `Control`. +Then one `Control` named after each locale; create one and call it "en" in doubt. + +In these you can have eg. `TextureRect`s called + + * UI_QUIT_CONFIRM + * UI_NEW_GAME_CONFIRM + +Optionally you can use the `ui/translated_label.gd` or `ui/translated_rtlabel.gd` +scripts to pull text from localization. + +Which are hidden by default and shown on demand. + +You must also have the following + + * yes + * no + +They must be visible, because they are your yes/no buttons. + +The types on any of these don't matter, but *their names do*. + +If you have multiple locales, hide the top locale-named nodes, all of +them, or the code might pass the click to the wrong button and ignore it. + +Any means of passing in messages to the confirmation popups has been +deprecated as not being frameworky enough; that would require altering +Escoria code for a scene that's created by the developer anyway. + +## Credits + +You may configure a credits screen in `escoria/ui/credits`, and an end +credits screen in `escoria/ui/end_credits`. + +This is pretty free-form, but the simplest form is + + * credits (`Control`) + * background (`TextureRect`) + * menu (`TextureRect`) + +Then you attach the `globals/credits.gd` script to the `credits` node +and everything should work as expected. + +You can use the same script for the end credits, but if you want eg. +music there, you have to make a copy and edit that script. Or make +it configurable and submit a pull request with changes and removal +of this statement. + +Protip: if you end your game on a fade out, you will not be able to +see the end credits, as `telon` will have made it all black. + +You will then make a copy of `credits.gd` for your game, call it +`end_credits.gd` and attach that to `credits`. Then you'll add the +following line to `_ready()`: + +``` +get_tree().call_group("game", "telon_play_anim", "fade_in") +``` + diff --git a/docs/music-and-sound.md b/docs/music-and-sound.md new file mode 100644 index 0000000..1146372 --- /dev/null +++ b/docs/music-and-sound.md @@ -0,0 +1,45 @@ +# Music and Sound + +## Music + +Make your game come alive with some music. + +The background music can be controlled from .esc scripts using the `set_state bg_music` command. To play music when a scene loads, create a new .esc file, load it from the `Events Path` property of the `scene` node and add the following contents: + +``` +:ready +set_state bg_music "res://demo/audio/music/demo_melody.ogg" +``` + +Assuming `demo_melody.ogg` is a music file which exists under that resource path in your project. + +### Controlling music streams + +Similarly, you can also control music by triggering actions on items in your scene, for instance by using custom `turn_on` and `turn_off` actions on a radio item in your game with the following .esc script added to its `Events Path` property: + +``` +:turn_on +set_state bg_music "res://demo/audio/music/demo_melody.ogg" + +:turn_off +set_state bg_music off +``` + +### Sound + +You play sound files with `play_snd`. + +You can add an `AudioStreamPlayer2D` to any item, calling it `audio`. Please see [the reference](esc_reference.md) for more info. + +For idle animations, their idle sounds, it's preferred you use the `animation` node to control the audio stream. + +## Controlling volume + +Currently there exists no API for controlling music through Escoria. + +There is a setting `escoria/application/dialog_damp_music_by_db` +which allows you to dampen the background music while someone's +speaking. + +See [Decibel scale](http://docs.godotengine.org/en/3.0/tutorials/audio/audio_buses.html) for details. + diff --git a/docs/setting_states.md b/docs/setting_states.md new file mode 100644 index 0000000..4d35d54 --- /dev/null +++ b/docs/setting_states.md @@ -0,0 +1,42 @@ +# Setting states + +States are a versatile feature of Escoria. They provide a way to use the +animation interfaces to have the player change costumes and items to +appear different or transition from static to animated. + +## Animated items + +The animations are documented in the flossmanuals book, eg. +[Animations](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/animations/) +and [Main Player](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/main-player/) for most of your needs. + +The big difference is that your default animation player must be named `default` now. + +## Static items + +For static items, ones that don't move, you still need to use the `AnimationPlayer` +interface. You will be able to change between static states, from static to animated +and from animated back to static. + +Select the animation editor at the bottom of your screen. + +First you'll hit "Create new animation in player". + +Select your "sprite" node. See the key icon next to the texture? Click it to create a +track called "sprite:texture" in your new animation (track). + +Down in the lower-right corner of the animation editor is a button "Enable editing of +individual keys by clicking them". Hit it. + +The white blob in the timeline signifies a [Key Frame](https://en.wikipedia.org/wiki/Key_frame). +As you click it, you'll see your texture there as a starting point. Because we're not learning +to animate but how to change states, you can just replace it with something else. + +Now your state shares the name of your new animation! + +You can use `set_state your_item animation_name` in ESC scripts from now on. + +Do bear in mind that if you want to toggle between states or otherwise track changes, you +will also need `set_global` calls next to the `set_state` calls. This is because ESC can +only set states, not query them. + diff --git a/docs/shadows.md b/docs/shadows.md new file mode 100644 index 0000000..42ead39 --- /dev/null +++ b/docs/shadows.md @@ -0,0 +1,44 @@ +# Directional shadows + +Your player needs to have a `shadow` child. You can use/extend the one provided in `contrib/scenes/shadow/shadow.tscn`. + +To actually cast a shadow, you must have one or more `Light2D` instances, with an `Area2D` child (named eg. `shadow_caster`). +Attach the `globals/shadow_caster.gd` script to it and create the `CollisionPolygon2D` or `CollisionShape2D`. + +The `force_light_mask` toggle will reset your player's light mask to match the `Light2D`'s _range cull mask_ so the player +will always be lit up by the light. This is so you can stand in front of a light, outside `shadow_caster`, and not have it +affect you - simply by having different masks. If you don't want this behavior, uncheck the box or set matching masks. + +Now when your player enters the `shadow_caster`(s), there'll be shadows! + +## Tunables + +There are more than a few tunables here. + + * (int)var light_y_offset = 0 + * Affect the shadow direction by moving the light higher than it is + * (float)var max_dist_visible = 50 + * (float)var alpha_coefficient = 2.0 + * These two affect how far from the light the shadow is visible, within the collision polygon + * (float)var alpha_max = 0.65 + * The darkest the shadow can get + * (float)var scale_power = 1.2 + * (float)var scale_divide = 1000.0 + * (float)var scale_extra = 0.15 + * Magic tunables to affect the shadow's length + +## Shadow configuration + +All the scaling tunables above are ignored if you uncheck `scaling`. + +If you don't want your shadow to rotate, uncheck `rotating` and set a `fixed_rotation`. +This value is ignored when `rotating` is checked. The unit is degrees, starting directly +toward the camera and moving clockwise. + +# Static shadows + +Add a `Sprite` child to your character, you can call it `static_shadow` +if you want, and attach `globals/static_shadow_sprite.gd` to it. + +You may have to unset `centered`, and set the relevant offsets and scale. + diff --git a/docs/spine.md b/docs/spine.md new file mode 100644 index 0000000..b2803d9 --- /dev/null +++ b/docs/spine.md @@ -0,0 +1,56 @@ +# Using Spine + +[Spine](http://esotericsoftware.com/spine-in-depth) is an animation program that is not +officially supported by Godot, but there exists a [Spine module](https://github.com/GodotExplorer/spine/) +that can be added to the `modules/` directory in your Godot source tree and built +with eg. `schedtool -D -e ccache scons platform=x11 tools=yes target=release_debug` +or whatever you like. + +For compatibility with Godot, run `spine-to-godot-scene.py` ("the script"), found in the [Spine to Godot](https://github.com/mjtorn/spine-to-godot/) project, from your project root (typically the `device/` directory). + +Do note the script requires `python3-docopt`, so please install it using `pip3` or whatever +you prefer. You may also want to read its documentation; it may expand to cover things not in the scope of Escoria! + +## Caveats + +Do note that Spine animations cannot be played in reverse by design. + +## Prerequisities + +If you have different skins for your Spine skeletons, and multiple files for eg. moving in different directions, make sure they're named exactly the same. The script will do its best to make this easy for you to use. + +Do not use whitespaces anywhere; Escoria will not understand whitespaces, even if Godot would. + +## Creating Spine scenes + +Copy your Spine data under eg. `items/foo/`. This can be the player or an NPC or even just an item. + +Use the script to generate a scene in the same `items/foo/`. + +The script generates a scene with a `Spine` node and animations. These animations are autogenerated and have horrible names, but they're there. + +## Using Spine scenes + +To enable interoperability between Spine and eg. shadows, or other nodes that don't want to be flipped when the character is flipped, +create a `Node2D` node called `sprite` and put the `Spine` node(s) under it. + +To avoid editing and using the autogenerated animations, go to the animation editor and for every relevant animation, make a copy and rename it. + +Looking at the Spine node, you'll have to select the different animations to get their durations. Unfortunately there is no other way now to get them. + +Update your copied animation with the relevant information (like duration), add tracks, or whatever your project requires. + +## Examples + +### Changing skins + +Make a copy of the `spine_character__skin__blue` style animations to call it `skin_blue`. + +Now you must run the animation in your game: `cut_scene player skin_blue`. It's not really a cut scene, but this is how we guarantee it finishes before anything else happens. To make your player idle after that, run `set_state player idle`. + +### Moving around + +Because your movement directions will likely be in different files, you will get multiple visible Spine nodes when moving. + +The solution is to copy an animation like `spine_character_front__move_down` and call it `move_down`. Then you add tracks to it that hide the other Spine nodes for the duration of the animation. + diff --git a/docs/starting-a-game.md b/docs/starting-a-game.md new file mode 100644 index 0000000..294cdc9 --- /dev/null +++ b/docs/starting-a-game.md @@ -0,0 +1,19 @@ +# Starting a game + +Escoria looks for a start script for your game. + +You can configure its path in `platform/game_start_script`. + +It should look like this + +``` +:start + change_scene res://game-dir/rooms/your-first-room/scene.tscn +``` + +Note that the `:start` event is not used anywhere else. The start script +does not use `:load`, because loading save games has to disable events. So +if `:load` was used, your legitimate events at the start of your game would +not work. And not checking for game loads would completely break your game +every time you load a save! + diff --git a/docs/telon.md b/docs/telon.md new file mode 100644 index 0000000..06b3ef8 --- /dev/null +++ b/docs/telon.md @@ -0,0 +1,24 @@ +# Telon + +Telon is responsible for fading in and out, and dealing with input. + +You will interact with it through the `AnimationPlayer` interface using `cut_scene`. + +## Fading + + * fade_out + * fade_in + +## Input + +These animations control all input + + * disable_input + * enable_input + +Use `disable_input` to disable player input during eg. cut-scene-like sequences where skipping dialog would cause problems. You can also look into `set_hud_visible` when making these kinds of sequences. + +Note that `disable_input` disables autosaves, because if an autosave would happen in this type of a sequence, loading it back would put the game in an undefined state. + +Because the variables are global, you're encouraged to use these tightly, so you don't cause a bug where your game disables input forever! + diff --git a/docs/terrain.md b/docs/terrain.md new file mode 100644 index 0000000..112eab7 --- /dev/null +++ b/docs/terrain.md @@ -0,0 +1,57 @@ +# Terrain + +Your player and NPCs walk along a defined terrain. You'll need a `NavigationPolygon2D` +for that, and attaching a script for special features like scaling and lighting. + +Note that lighting on the terrain is not the same as lighting by `Light2D`, it is modulating +the player and items based on a separate texture. You'll make it white and color in the areas +where you want something to change. + +A terrain is not strictly necessary. In rooms like that, all interactions are handled +as if the player had already walked to the target position. + +## Scaling + +### Scalenodes + +The easiest way to make your player and NPCs scale is to use the `terrain_scalenodes.gd` +script. Then you create `Position2D` nodes with `scalenode.gd` attached to them. + +The names of these nodes don't matter, except you need `scale_min` and `scale_max`. +Those two should be placed above and below the navpoly. + +Anything which is set to scale from the map will linear-interpolate between the `target_scale` +values. + +Caveat: although these are `tool` scripts, you can't refresh the scalenode list at the +moment so you will need to refresh the scene when making bigger changes. + +### Gradient + +The harder, though more versatile way, is to create a separate scalemap texture. + +This is further documented in the [Flossmanuals booklight](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/lightening/). + +Attach `terrain.gd`. + +You'll define a scale range for how much to scale by. The terrain will look for the blue +value of a pixel and scale accordingly. More blue is a bigger scale. + +Caveat: for some reason this method is currently very slow, so having many NPCs move +around will probably kill performance. + +## Lighting by lightmap + +Reading pixels is, for whatever reason, slow. It is necessary for lightmaps, and scalemaps +if you use them. To mitigate the performance problems, there's a script `lightmap_area.gd` +which you would attach to an `Area2D` and draw its polygon or shape around the part of +your lightmap that actually contains information. + +## Dynamic navigation + +You can attach `nav_poly_instance.gd` to multiple `NavigationPolygonInstance`s, give +them `global_id`s and use `set_active` to toggle where you're allowed to move. + +Note that you should use pixel snapping or somesuch to share vertices between the +polygons or they are not considered the same terrain. + diff --git a/docs/tool_scripts.md b/docs/tool_scripts.md new file mode 100644 index 0000000..2f1ef55 --- /dev/null +++ b/docs/tool_scripts.md @@ -0,0 +1,9 @@ +# Tool scripts + +Tool scripts are command line GDScript files which are not exported with the game, but rather serves a purpose during development. These scripts are located in the `tools/` directory, and can be run by invoking Godot from the command line with the option `-s` /`--script` and a valid path to the script. If Godot is in your `$PATH`, the `lockit.gd` script may be run by issuing the following command from the `device/` directory: + + `godot -s ../tools/lockit.gd` + +### Internationalization + +The `lockit.gd` script is made to help with translation of the game by making sure all strings used in dialogue has a translation ID, which can be used with Godot's `TranslationServer`. When run, the script creates .esc.lockit files for every .esc file in your project, with translation IDs prefixed within each file. Additionally, a .csv file is generated, which carries some information about the context of each string, as well as any conflicting translation IDs found. This .csv file can also be used to create a .csv file which can be loaded into the game from the project settings, as described in the Floss Manuals section on [Internationalization](https://fr.flossmanuals.net/creating-point-and-click-games-with-escoria/i18n/). diff --git a/docs/triggers-and-exits.md b/docs/triggers-and-exits.md new file mode 100644 index 0000000..be60d9e --- /dev/null +++ b/docs/triggers-and-exits.md @@ -0,0 +1,63 @@ +# Triggers and exits + +Triggers in Escoria are thought of as "things a character can walk on", either +like opening a door in Prince of Persia by stepping on a plate or a more abstract +"To street" exit trigger. + +Inert items, things that are "triggers or hotspots for the mouse" but not to be walked on, should +be implemented using `item.gd`. + +Although triggers and exits can be attached to any type of `Node`, +not every case works properly. + +The working cases are described here. + +Exit is extended from trigger. To make gameplay quicker and "more modern", trigger +implements double-clicking, which therefore works in exit as well. + +Sometimes you may want a hidden `trigger`, by having an empty tooltip. As the player +will figure out where they are by the events triggered, it would feel broken if a +double-click teleported the player. Therefore an empty tooltip disables double-clicking. + +## Concerning the player + +Your player character must be a `KinematicBody2D` in order to work with triggers +and exits. + +If you refer to an old documentation that claims otherwise, and don't make it a +`KinematicBody2D`, Godot will complain. So before you start, remember this. + +Also bear in mind that your player will need a `CollisionShape2D` or such to +collide with the triggers and exits. + +## Triggers + +Triggers are `Area2D`, with the required `CollisionPolygon2D` child and optionally also +a `Sprite`) child. You may name them freely. + +The `trigger.gd` script must be attached to the `Area2D`. + +The triggers react only on the player character, and the player character's position being +within the Area2D. + +Note that you will have to create an ESC script with `:enter` and `:exit` +which are triggered when the player enters or exits the `Area2D`. + +## Exits + +Exits can have the same node structure as a trigger or be `TextureRect`s. Be warned that +`TextureRect` support may be dropped in the near future. + +The `exit.gd` script must be attached to either the `Area2D` or `TextureRect`. + +Because exits are meant for changing scenes, the event keyword is `:exit_scene` to avoid +confusion with triggers. + +An example of this would be + +``` +:exit_scene +set_global last_exit bridge +change_scene res://game/rooms/office/main.tscn +``` + diff --git a/templates/base_scene.tscn b/templates/base_scene.tscn new file mode 100644 index 0000000..f435312 --- /dev/null +++ b/templates/base_scene.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://globals/scene.gd" type="Script" id=1] +[ext_resource path="res://globals/game.tscn" type="PackedScene" id=2] + +[node name="scene" type="Node2D"] + +script = ExtResource( 1 ) + +[node name="game" parent="." instance=ExtResource( 2 )] + diff --git a/tools/item_placeholders.gd b/tools/item_placeholders.gd new file mode 100644 index 0000000..b4ea096 --- /dev/null +++ b/tools/item_placeholders.gd @@ -0,0 +1,38 @@ +tool + +extends EditorScript + +func _find_placeholders(scene, p_anim = null): + + if p_anim == null: + _find_placeholders(scene, "animation") + _find_placeholders(scene, "states") + return + + var anim = scene.get_node(p_anim) + if anim == null: + return + + var list = anim.get_animation_list() + for a in list: + var res = anim.get_animation(a) + var count = res.get_track_count() + for i in range(count): + var tpath = res.track_get_path(i) + var npath = str(tpath).split(":")[0] + var node = scene.get_node(npath) + if node != null && ((node extends InstancePlaceholder) || node.get_scene_instance_load_placeholder()): + if !(a in scene.placeholders): + scene.placeholders[a] = [] + if npath in scene.placeholders[a]: + continue + printt("******* adding placeholder ", a, npath, scene.placeholders[a]) + scene.placeholders[a].push_back(npath) + + +func _run(): + var scene = get_scene() + + scene.placeholders = {} + + _find_placeholders(scene) diff --git a/tools/lockit.gd b/tools/lockit.gd new file mode 100644 index 0000000..1b9d051 --- /dev/null +++ b/tools/lockit.gd @@ -0,0 +1,344 @@ +extends MainLoop + +func _iteration(time): + return true + +func get_input_files(argv): + + var list = [] + + if "-esc" in argv: + for i in range(argv.size()): + if argv[i] == "-esc": + list.push_back(argv[i+1]) + return list + + find_esc("", list) + + return list + +func find_esc(path, list): + + var loc = path + if loc == "": + loc = "." + + var d = Directory.new() + d.open(loc) + d.list_dir_begin() + var f = d.get_next() + + while f != "": + if f == "." || f == "..": + f = d.get_next() + continue + if d.current_is_dir(): + find_esc(path + f + "/", list) + else: + if f.find(".esc") == f.length() - 4: + list.push_back(path + f) + f = d.get_next() + + d.list_dir_end() + +func get_token(line, p_from): + while p_from < line.length(): + if line[p_from] == " " || line[p_from] == "\t": + p_from += 1 + else: + break + if p_from >= line.length(): + return -1 + + var tk_end + if line[p_from] == "\"": + tk_end = line.find("\"", p_from+1) + if tk_end == -1: + tk_end = line.length()-1 + tk_end += 1 + else: + tk_end = p_from + while tk_end < line.length(): + if line[tk_end] == ":": + var ntk = get_token(line, tk_end+1) + tk_end = ntk + break + if line[tk_end] == " " || line[tk_end] == "\t": + break + tk_end += 1 + return tk_end + +func process_file(path, ids): + + printt("process file ", path) + + var f = File.new() + f.open(path, File.READ) + if !f.is_open(): + print(" ** Failed opening file ", path) + return + + var fo = File.new() + var tmp_dst = "esc_tmp_" + str(OS.get_process_id()) + fo.open(tmp_dst, File.WRITE) + if !fo.is_open(): + print(" ** Failed opening temp file ", tmp_dst, " for ", path) + f.close() + return + + var section = "" + var section_count = 0 + var line_count = 0 + var base = path.get_file().get_basename() + + ids._found.push_back(["file", base]) + + var section_ids = [] + + while !f.eof_reached(): + var line = f.get_line() + line_count += 1 + if line == "": + continue + + if line[0] == "#": + continue + + if line[0] == ":": + var end = line.find(" ") + if end == -1: + end = line.length() + section = line.substr(1, end-1) + + var pos = f.get_position() + section_ids = find_section_ids(f) + f.seek(pos) + + ids._found.push_back(["section", section]) + + else: + var from = 0 + var first = null + var args = [] + while true: + var tk_end + if args.size() == 1 && args[0] == "*": + var cond = line.find("[") + if cond >= 0: + tk_end = cond - 1 + else: + tk_end = line.length() + else: + tk_end = get_token(line, from) + if tk_end == -1: + break + + var next = line.substr(from, tk_end - from) + next = next.strip_edges() + + if args.size() == 1 && args[0] == "*": + next = "\""+next+"\"" + + if next.find(":\"") != -1: + section_count += 1 + # already has an id + var sep = next.find(":") + var id = next.substr(0, sep) + var text = next.right(sep+1) + text = text.substr(1, text.length()-2) + + add_to_ids(ids, id, text, path, line_count, args) + + elif next[0] == "\"": # a string + section_count += 1 + var text = next.substr(1, next.length()-2) + var has = has_id(text, ids, args) + var id + if has != null: + id = has + else: + id = get_new_id(base, section, section_count, section_ids, args) + section_ids.push_back(id) + + add_to_ids(ids, id, text, path, line_count, args) + + var line_new = line.substr(0, from+1) + next = id + ":\"" + text + "\"" + line_new = line_new + next + var tend = line_new.length() + line_new = line_new + line.right(tk_end) + tk_end = tend + + line = line_new + elif next == "*": + var line_new = line.substr(0, tk_end - 1) + line = line_new + "-" + line.right(tk_end) + elif next[0] == "#": + next += " " + line.right(tk_end) + # Comments may contain quotes. Double quote in accordance with RFC-4180. + next = next.replace("\"", "\"\"") + tk_end = line.length() + + args.push_back(next) + + from = tk_end + + fo.store_line(line) + + f.close() + fo.close() + + var d = Directory.new() + d.open(".") + d.rename(tmp_dst, path+".lockit") + +func has_id(text, ids, args): + if !(text in ids._texts): + return null + + if args[0] == "say": + if args[1] in ids._texts[text]: + return ids._texts[text][args[1]] + else: # match any + var key = ids._texts[text].keys()[0] + return ids._texts[text][key] + + return null + +func add_to_ids(ids, id, text, path, line_count, args): + + ids._found.push_back(["text", id]) + + if id in ids: + ids[id].files.push_back(path + ":" + str(line_count)) + if text != ids[id].text: + ids[id].conflicts.push_back([path + ":" + str(line_count), text]) + + else: + ids[id] = { "text": text, "files": [ path + ":" + str(line_count) ], "conflicts": [], "args": args } + + if !(text in ids._texts): + ids._texts[text] = {} + + if args[0] == "say": + if !(args[1] in ids._texts[text]): + ids._texts[text][args[1]] = id + else: + ids._texts[text]["dialog"] = id + +func get_new_id(base, section, section_count, section_ids, args): + var r = base + "_" + section + "_" + str(section_count) + var count = 0 + var ret = r + while ret in section_ids: + count += 1 + ret = r + "." + str(count) + + return ret + +func find_section_ids(f): + var sec_ids = [] + + while !f.eof_reached(): + var line = f.get_line() + if line == "": + continue + + if line[0] == "#": + continue + + if line[0] == ":": + break + + var from = 0 + while true: + var tk_end = get_token(line, from) + if tk_end == -1: + break + var next = line.substr(from, tk_end - from) + next = next.strip_edges() + if next.find(":\"") != -1: + var sep = next.find(":") + var id = next.substr(0, sep) + sec_ids.push_back(id) + + from = tk_end + + return sec_ids + +func write_csv(out, ids): + var f = File.new() + f.open(out, File.WRITE) + + var last_type = "" + + for id in ids._found: + + if id[0] == "file": + f.store_string("\n#,") + f.store_string("\"" + id[1] + "\",") + + elif id[0] == "section": + if last_type != "file": + f.store_string("\n") + f.store_string("##,") + f.store_string("\"" + id[1] + "\",\n") + + elif id[0] == "text": + + var t = ids[id[1]] + + f.store_string("\"" + id[1] + "\",") + f.store_string("\"" + t.text + "\",") + + if t.args[0] == "say": + f.store_string("\""+t.args[1]+"\",") + elif t.args[0] == "*": + f.store_string("\"dialog\",") + else: + f.store_string("\""+t.args[0]+"\",") + + f.store_string("\"") + var tfiles = PoolStringArray(t.files) + f.store_string(tfiles.join(", ")) + f.store_string("\",") + + f.store_string("\"") + var tconflicts = PoolStringArray([]) + for tf in t.conflicts: + tconflicts.append(tf[0] + ":" + tf[1]) + f.store_string(tconflicts.join(", ")) + f.store_string("\"") + + if t.args.back()[0] == "#": + var comment = t.args.back().right(1).strip_edges() + f.store_string(",\"" + comment + "\"") + + f.store_string("\n") + + last_type = id[0] + + f.close() + +func _initialize(): + + var argv = Array(OS.get_cmdline_args()) + while argv.size(): + var p = argv[0] + argv.remove(0) + if p in ["-s", "--script"]: + break + + var out = "strings.csv" + for i in range(argv.size()): + if argv[i] == "-out": + out = argv[i+1] + break + + var ids = { "_found": [], "_texts": {} } + var input_files = get_input_files(argv) + + for f in input_files: + process_file(f, ids) + + write_csv(out, ids)