diff --git a/prefabs/enemies/watcher.tscn b/prefabs/enemies/watcher.tscn new file mode 100644 index 0000000..8584ab3 --- /dev/null +++ b/prefabs/enemies/watcher.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=7 format=3 uid="uid://ccg3n7sobsvdw"] + +[ext_resource type="Script" path="res://scripts/enemies/Watcher.cs" id="1_wfhbm"] +[ext_resource type="SpriteFrames" uid="uid://dlf2p3eragspn" path="res://sprites/enemies/watcher.tres" id="2_757xa"] +[ext_resource type="Texture2D" uid="uid://dlbl6d4yghvht" path="res://sprites/mask.png" id="3_nbgee"] +[ext_resource type="PackedScene" uid="uid://cf0wpahgwygxx" path="res://prefabs/light_sense.tscn" id="4_22lca"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_pcaas"] +radius = 100.0 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_n43vg"] +size = Vector2(14, 8) + +[node name="Watcher" type="Node2D"] +script = ExtResource("1_wfhbm") + +[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."] +clip_children = 2 +z_index = 1 +position = Vector2(0, 1) +sprite_frames = ExtResource("2_757xa") + +[node name="Sprite2D" type="Sprite2D" parent="AnimatedSprite2D"] +modulate = Color(0, 0, 0, 1) +position = Vector2(0, -2) +scale = Vector2(0.05, 0.05) +texture = ExtResource("3_nbgee") + +[node name="LightSense" parent="." instance=ExtResource("4_22lca")] +monitorable = false + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="LightSense"] +position = Vector2(0, 1) +polygon = PackedVector2Array(-4, -5, 4, -5, 7, -3, 7, 0, 4, 2, -4, 2, -7, 0, -7, -3) + +[node name="Activation" type="Area2D" parent="."] +collision_layer = 4 +collision_mask = 4 +input_pickable = false +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Activation"] +shape = SubResource("CircleShape2D_pcaas") + +[node name="PlayerCollision" type="Area2D" parent="."] +collision_layer = 4 +collision_mask = 4 +input_pickable = false +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerCollision"] +shape = SubResource("RectangleShape2D_n43vg") + +[connection signal="animation_finished" from="AnimatedSprite2D" to="." method="AnimationEnded"] +[connection signal="area_entered" from="LightSense" to="." method="LightEntered"] +[connection signal="body_entered" from="Activation" to="." method="PlayerActivated"] +[connection signal="body_entered" from="PlayerCollision" to="." method="PlayerEntered"] +[connection signal="body_exited" from="PlayerCollision" to="." method="PlayerLeft"] diff --git a/prefabs/light_sense.tscn b/prefabs/light_sense.tscn index fb834de..17c09d1 100644 --- a/prefabs/light_sense.tscn +++ b/prefabs/light_sense.tscn @@ -3,3 +3,4 @@ [node name="LightSense" type="Area2D"] collision_layer = 2 collision_mask = 2 +input_pickable = false diff --git a/prefabs/player.tscn b/prefabs/player.tscn index 781d9a3..a852cf2 100644 --- a/prefabs/player.tscn +++ b/prefabs/player.tscn @@ -8,6 +8,8 @@ size = Vector2(15, 15) [node name="Player" type="CharacterBody2D"] y_sort_enabled = true +collision_layer = 5 +collision_mask = 5 script = ExtResource("1_1vpun") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] diff --git a/project.godot b/project.godot index f7555ed..6ec3ddc 100644 --- a/project.godot +++ b/project.godot @@ -73,6 +73,7 @@ flashlight_charge={ 2d_render/layer_3="UI" 2d_physics/layer_1="World" 2d_physics/layer_2="Light" +2d_physics/layer_3="Player" [rendering] diff --git a/scenes/main_scene.tscn b/scenes/main_scene.tscn index d0a7101..3dfa823 100644 --- a/scenes/main_scene.tscn +++ b/scenes/main_scene.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=18 format=3 uid="uid://dhn7yt46fyac8"] +[gd_scene load_steps=19 format=3 uid="uid://dhn7yt46fyac8"] [ext_resource type="PackedScene" uid="uid://bhulqhxesd5gc" path="res://prefabs/player.tscn" id="1_65a7v"] [ext_resource type="AudioStream" uid="uid://bsy2d0bl3lgg0" path="res://sounds/crank.ogg" id="1_cweq4"] @@ -9,6 +9,7 @@ [ext_resource type="Shader" path="res://shaders/dithering.gdshader" id="5_64d71"] [ext_resource type="Script" path="res://scripts/GameCamera.cs" id="6_quua3"] [ext_resource type="Script" path="res://scripts/PointLight2DWorkaround.cs" id="6_slohe"] +[ext_resource type="PackedScene" uid="uid://ccg3n7sobsvdw" path="res://prefabs/enemies/watcher.tscn" id="10_fsiss"] [sub_resource type="Curve" id="Curve_o5byr"] _data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.0824742, 0.273684), -10.2105, 0.0, 0, 0, Vector2(0.242268, 0.494737), -5.10526, 0.0, 0, 0, Vector2(0.396907, 0.736842), -7.6579, 0.0, 0, 0, Vector2(0.737113, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] @@ -20,7 +21,7 @@ light_mode = 2 [sub_resource type="ShaderMaterial" id="ShaderMaterial_m680d"] shader = ExtResource("5_64d71") -[sub_resource type="ViewportTexture" id="ViewportTexture_nebfb"] +[sub_resource type="ViewportTexture" id="ViewportTexture_u0vqj"] viewport_path = NodePath("FlashlightViewport") [sub_resource type="CircleShape2D" id="CircleShape2D_prnh4"] @@ -49,9 +50,9 @@ Camera = NodePath("../PlayerCamera") Circle = NodePath("../FlashlightViewport/CanvasGroup/Circle") PlayerCircle = NodePath("../FlashlightViewport/PlayerCircle") Polygon = NodePath("../FlashlightViewport/CanvasGroup/Triangle") -CollisionCircle = NodePath("../PlayerCamera/Area2D/Circle") -CollisionPlayerCircle = NodePath("../PlayerCamera/Area2D/PlayerCircle") -CollisionPolygon = NodePath("../PlayerCamera/Area2D/CollisionPolygon2D") +CollisionCircle = NodePath("../PlayerCamera/FlashlightCollision/Circle") +CollisionPlayerCircle = NodePath("../PlayerCamera/FlashlightCollision/PlayerCircle") +CollisionPolygon = NodePath("../PlayerCamera/FlashlightCollision/CollisionPolygon2D") FlashlightGroup = NodePath("../FlashlightViewport/CanvasGroup") BrightnessCurve = SubResource("Curve_o5byr") CrankSoundPlayer = NodePath("../Sounds/CrankSound") @@ -73,6 +74,7 @@ position = Vector2(85, 71) texture = ExtResource("2_edqdh") [node name="FlashlightViewport" type="SubViewport" parent="."] +disable_3d = true transparent_bg = true snap_2d_transforms_to_pixel = true snap_2d_vertices_to_pixel = true @@ -102,22 +104,23 @@ CameraBounds = Vector2(30, 20) [node name="PointLight2D" type="PointLight2D" parent="PlayerCamera" node_paths=PackedStringArray("LightViewport")] blend_mode = 2 range_item_cull_mask = 2 -texture = SubResource("ViewportTexture_nebfb") +texture = SubResource("ViewportTexture_u0vqj") script = ExtResource("6_slohe") LightViewport = NodePath("../../FlashlightViewport") -[node name="Area2D" type="Area2D" parent="PlayerCamera"] +[node name="FlashlightCollision" type="Area2D" parent="PlayerCamera"] position = Vector2(-128, -96) collision_layer = 2 collision_mask = 2 +monitoring = false -[node name="PlayerCircle" type="CollisionShape2D" parent="PlayerCamera/Area2D"] +[node name="PlayerCircle" type="CollisionShape2D" parent="PlayerCamera/FlashlightCollision"] shape = SubResource("CircleShape2D_prnh4") -[node name="Circle" type="CollisionShape2D" parent="PlayerCamera/Area2D"] +[node name="Circle" type="CollisionShape2D" parent="PlayerCamera/FlashlightCollision"] shape = SubResource("CircleShape2D_qcayn") -[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="PlayerCamera/Area2D"] +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="PlayerCamera/FlashlightCollision"] polygon = PackedVector2Array(0, 0, 0, 0, 0, 0) [node name="CanvasLayer" type="CanvasLayer" parent="."] @@ -146,3 +149,6 @@ y_sort_enabled = true material = SubResource("CanvasItemMaterial_au1d0") position = Vector2(-83, 43) texture = ExtResource("3_8o315") + +[node name="Watcher" parent="." instance=ExtResource("10_fsiss")] +position = Vector2(116, -76) diff --git a/scripts/enemies/Watcher.cs b/scripts/enemies/Watcher.cs new file mode 100644 index 0000000..fb63c6a --- /dev/null +++ b/scripts/enemies/Watcher.cs @@ -0,0 +1,241 @@ +using Godot; +using System; + +public partial class Watcher : Node2D +{ + public enum State + { + Waiting, + Opening, + Looking, + PreparingToTeleport, + Teleporting, + LitUp + } + + [Export] public float EyeDotSpeed = 2f; + [Export] public Vector2 EyeDotDistance = new(4f, 3f); + [Export] public float EyeTeleportDistance = 48f; + + public State CurrentState + { + get => _state; + + private set + { + _state = value; + _timeSinceState = 0; + GD.Print($"watcher: new state is {_state}"); + + switch (_state) + { + case State.Waiting: + HideEye(); + break; + + case State.Opening: + _dotSprite.Position = _dotSpriteInitialPos; + ShowEye(); + break; + + case State.Looking: + break; + + case State.PreparingToTeleport: + _sprite.PlayBackwards("default"); + break; + + case State.Teleporting: + HideEye(); + break; + + case State.LitUp: + _isLitUp = false; + + var newPosition = (_player.Position - Position).Normalized(); + if (newPosition.IsZeroApprox()) + newPosition = new Vector2(0, -1); + Position += newPosition * Constants.HalfScreenSize.Length(); + + HideEye(); + break; + + default: + break; + } + } + } + + private Player _player; + private AnimatedSprite2D _sprite; + private Sprite2D _dotSprite; + private Vector2 _dotSpriteInitialPos; + private State _state; + private float _timeSinceState; + private bool _isPlayerInside; + private bool _isLitUp; + + public override void _Ready() + { + _sprite = (AnimatedSprite2D)FindChild("AnimatedSprite2D"); + + _dotSprite = (Sprite2D)FindChild("Sprite2D"); + _dotSpriteInitialPos = _dotSprite.Position; + + CurrentState = State.Waiting; + } + + public override void _PhysicsProcess(double delta) + { + _timeSinceState += (float)delta; + + switch (_state) + { + case State.Waiting: + break; + + case State.Opening: + break; + + case State.Looking: + { + var playerRelative = _player.Position - Position; + var normal = playerRelative.Normalized(); + _dotSprite.Position = _dotSprite.Position.Lerp(normal * EyeDotDistance, EyeDotSpeed * (float)delta); + + if (_timeSinceState > 5f) // 5 seconds to look at the player + CurrentState = State.PreparingToTeleport; + } + break; + + case State.PreparingToTeleport: + break; + + case State.Teleporting: + if (_timeSinceState > 1f) // 1 second to teleport + { + var newPosition = _player.Position - Position; + if (newPosition.Length() > EyeTeleportDistance) + newPosition = newPosition.Normalized() * EyeTeleportDistance; + + Position += newPosition; + CurrentState = State.Opening; + } + + break; + + case State.LitUp: + if (_timeSinceState > 2f) // 2 seconds of cooldown + { + CurrentState = State.Opening; + } + break; + + default: + break; + } + + CheckIfKilledPlayer(); + CheckIfLitUp(); + } + + private void AnimationEnded() + { + switch (CurrentState) + { + case State.Opening: + CurrentState = State.Looking; + break; + + case State.PreparingToTeleport: + CurrentState = State.Teleporting; + break; + + case State.LitUp: + //QueueFree(); + GD.Print("TODO: watcher: teleport"); + break; + + case State.Waiting: + case State.Looking: + case State.Teleporting: + default: + break; + } + } + + private void LightEntered(Area2D area) + { + if (CurrentState is State.Waiting or State.Opening or State.Teleporting or State.LitUp) + return; + + if (area.GetParentOrNull() is null) + return; // Not a flashlight + + _isLitUp = true; + GD.Print("watcher: light enter"); + } + + private void PlayerEntered(Node2D body) + { + if (body is not Player player) + return; + + _isPlayerInside = true; + } + + private void PlayerLeft(Node2D body) + { + if (body is not Player player) + return; + + _isPlayerInside = false; + } + + private void PlayerActivated(Node2D body) + { + GD.Print("watcher: player activated1"); + if (CurrentState != State.Waiting || body is not Player player) + return; + + // TODO: turn off the player activation field for better performance + + GD.Print("watcher: player activated"); + _player = player; + CurrentState = State.Opening; + } + + void HideEye() + { + Modulate = new Color(1, 1, 1, 0); + _sprite.Stop(); + } + + void ShowEye() + { + Modulate = new Color(1, 1, 1, 1); + _sprite.Play("default"); + } + + void CheckIfKilledPlayer() + { + if (_player is null || !_isPlayerInside) + return; + + if (CurrentState is State.LitUp or State.PreparingToTeleport or State.Teleporting or State.Waiting) + return; + + _player.Kill(this); + } + + void CheckIfLitUp() + { + if (!_isLitUp) + return; + + if (CurrentState is State.Waiting or State.Opening or State.Teleporting or State.LitUp) + return; + + CurrentState = State.LitUp; + } +} diff --git a/sprites/enemies/watcher.png b/sprites/enemies/watcher.png new file mode 100644 index 0000000..fbd69ef Binary files /dev/null and b/sprites/enemies/watcher.png differ diff --git a/sprites/enemies/watcher.png.import b/sprites/enemies/watcher.png.import new file mode 100644 index 0000000..705c5c9 --- /dev/null +++ b/sprites/enemies/watcher.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://crckuao681ak2" +path="res://.godot/imported/watcher.png-4a2cb80763cb0becf48095119d13abc7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://sprites/enemies/watcher.png" +dest_files=["res://.godot/imported/watcher.png-4a2cb80763cb0becf48095119d13abc7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/sprites/enemies/watcher.tres b/sprites/enemies/watcher.tres new file mode 100644 index 0000000..64a8bad --- /dev/null +++ b/sprites/enemies/watcher.tres @@ -0,0 +1,95 @@ +[gd_resource type="SpriteFrames" load_steps=14 format=3 uid="uid://dlf2p3eragspn"] + +[ext_resource type="Texture2D" uid="uid://crckuao681ak2" path="res://sprites/enemies/watcher.png" id="1_dujpi"] + +[sub_resource type="AtlasTexture" id="AtlasTexture_jjej5"] +atlas = ExtResource("1_dujpi") +region = Rect2(0, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_p187d"] +atlas = ExtResource("1_dujpi") +region = Rect2(16, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_raqyq"] +atlas = ExtResource("1_dujpi") +region = Rect2(32, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_pcud7"] +atlas = ExtResource("1_dujpi") +region = Rect2(48, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_qoohe"] +atlas = ExtResource("1_dujpi") +region = Rect2(64, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_idtew"] +atlas = ExtResource("1_dujpi") +region = Rect2(80, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_mdtd1"] +atlas = ExtResource("1_dujpi") +region = Rect2(96, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_hw0o0"] +atlas = ExtResource("1_dujpi") +region = Rect2(112, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_qw5lw"] +atlas = ExtResource("1_dujpi") +region = Rect2(128, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_084tv"] +atlas = ExtResource("1_dujpi") +region = Rect2(144, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_f1y8l"] +atlas = ExtResource("1_dujpi") +region = Rect2(160, 0, 16, 32) + +[sub_resource type="AtlasTexture" id="AtlasTexture_5r3b7"] +atlas = ExtResource("1_dujpi") +region = Rect2(176, 0, 16, 32) + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": SubResource("AtlasTexture_jjej5") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_p187d") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_raqyq") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_pcud7") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_qoohe") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_idtew") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_mdtd1") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_hw0o0") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_qw5lw") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_084tv") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_f1y8l") +}, { +"duration": 1.0, +"texture": SubResource("AtlasTexture_5r3b7") +}], +"loop": false, +"name": &"default", +"speed": 10.0 +}]