Merge branch 'master' into controller-support

controller-support
HumanoidSandvichDispenser 2023-09-06 22:54:42 -07:00
commit 6c5bc4edac
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
39 changed files with 571 additions and 287 deletions

View File

@ -1,4 +1,4 @@
[gd_resource type="ParticleProcessMaterial" load_steps=5 format=3 uid="uid://cbfaqolx1ydvv"]
[gd_resource type="ParticleProcessMaterial" load_steps=7 format=3 uid="uid://cbfaqolx1ydvv"]
[sub_resource type="Gradient" id="Gradient_44upg"]
offsets = PackedFloat32Array(0, 0.5)
@ -7,16 +7,27 @@ colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_droiy"]
gradient = SubResource("Gradient_44upg")
[sub_resource type="Curve" id="Curve_4kf4j"]
_data = [Vector2(0, 0.5), 0.0, 0.0, 0, 0, Vector2(0.1, 1), 0.0, 0.0, 0, 0, Vector2(1, 0.5), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Curve" id="Curve_mx7gl"]
_data = [Vector2(0, 0.3), 0.0, 0.0, 0, 1, Vector2(1, 0.3), 0.0, 0.0, 1, 0]
point_count = 2
[sub_resource type="CurveTexture" id="CurveTexture_qqrjb"]
curve = SubResource("Curve_4kf4j")
[sub_resource type="Curve" id="Curve_lfa60"]
_data = [Vector2(0.1, 1), 0.0, -1.75, 0, 1, Vector2(0.5, 0.3), -1.75, 0.0, 1, 0]
point_count = 2
[sub_resource type="Curve" id="Curve_3iug1"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 1, Vector2(1, 1), 0.0, 0.0, 1, 0]
point_count = 2
[sub_resource type="CurveXYZTexture" id="CurveXYZTexture_xryvh"]
curve_x = SubResource("Curve_mx7gl")
curve_y = SubResource("Curve_lfa60")
curve_z = SubResource("Curve_3iug1")
[resource]
emission_shape = 1
emission_sphere_radius = 4.0
particle_flag_align_y = true
particle_flag_disable_z = true
spread = 180.0
gravity = Vector3(0, 0, 0)
@ -26,7 +37,7 @@ orbit_velocity_min = 0.0
orbit_velocity_max = 0.0
linear_accel_min = -512.0
linear_accel_max = -512.0
scale_min = 2.0
scale_max = 2.0
scale_curve = SubResource("CurveTexture_qqrjb")
scale_min = 0.1
scale_max = 0.1
scale_curve = SubResource("CurveXYZTexture_xryvh")
color_ramp = SubResource("GradientTexture1D_droiy")

View File

@ -35,7 +35,6 @@ public partial class ConnectorBox : Area2D
{
BodyEntered += (Node2D body) =>
{
GD.Print(body.Name + " entered");
if (body is Player && InteractionTrigger is null)
{
OnInteraction();

View File

@ -11,6 +11,7 @@ public partial class Hurtbox : BoundingBox, IFaction
float damage,
Character inflictor,
float knockback,
Items.Weapon weapon = null,
Vector2 knockbackDir = default);
/// <summary>
@ -30,7 +31,6 @@ public partial class Hurtbox : BoundingBox, IFaction
{
InvincibilityTimer.Timeout += () =>
{
GD.Print("invincibility off");
Monitorable = true;
};
}
@ -40,6 +40,7 @@ public partial class Hurtbox : BoundingBox, IFaction
float damage,
Character inflictor,
float knockback,
Items.Weapon weapon = default,
Vector2 knockbackOrigin = default,
Vector2 knockbackVector = default)
{
@ -77,7 +78,6 @@ public partial class Hurtbox : BoundingBox, IFaction
InvincibilityTimer.Start();
//Monitorable = false;
SetDeferred("monitorable", false);
GD.Print("invincible");
}
EmitSignal(
@ -85,6 +85,7 @@ public partial class Hurtbox : BoundingBox, IFaction
damage,
inflictor,
knockback,
weapon,
knockbackDir);
}
}

View File

@ -26,9 +26,6 @@ public partial class Character : CharacterBody2D, IFaction
}
}
[Export]
public float Stealth { get; protected set; } = 0;
[Signal]
public delegate void HealthChangedEventHandler(Events.HealthChangedArgs args);
@ -168,6 +165,9 @@ public partial class Character : CharacterBody2D, IFaction
}
}
/// <summary>
/// Handles the <c>Character</c>'s death.
/// </summary>
public virtual void Die()
{
if (HurtAnimation.HasAnimation("death"))
@ -190,11 +190,20 @@ public partial class Character : CharacterBody2D, IFaction
NetImpulse += impulse / Mass;
}
/// <summary>
/// Stuns the <c>Chararacter</c> for an amount of time. If
/// <paramref name="time"/> is less than the <c>Character</c>'s current
/// stun time left, it will have no effect.
/// </summary>
public virtual void Stun(float time)
{
StunTime = Mathf.Max(time, StunTime);
}
/// <summary>
/// Draws the character so that its sprite and inventory items face the
/// character's direction.
/// </summary>
protected virtual void DrawTarget()
{
Vector2 target = Target;
@ -213,6 +222,10 @@ public partial class Character : CharacterBody2D, IFaction
Inventory.Rotation = angle;
}
/// <summary>
/// Use the current item the character is using. Prefer to call this over
/// <c>Item.Use</c> as it will check if the character is stunned or alive.
/// </summary>
public void UseCurrentItem()
{
if (StunTime > 0 || !IsAlive)
@ -264,6 +277,9 @@ public partial class Character : CharacterBody2D, IFaction
}
}
/// <summary>
/// Override this method to modify the damage the character takes.
/// </summary>
protected virtual float ReceiveDamage(
float damage,
Character inflictor,
@ -285,10 +301,14 @@ public partial class Character : CharacterBody2D, IFaction
_curDamageText.ShowText();
}
/// <summary>
/// Handles the character taking damage.
/// </summary>
protected virtual void OnReceivedDamage(
float damage,
Character inflictor,
float knockback,
Weapon weapon = null,
Vector2 knockbackDir = default)
{
if (Health <= 0)
@ -325,7 +345,6 @@ public partial class Character : CharacterBody2D, IFaction
if (this.GetNode("Effects/HurtSound") is AudioStreamPlayer2D sound)
{
// very small pitch deviation
GD.Print("hurt sound");
sound.At(GlobalPosition).WithPitchDeviation(0.125f).PlayOneShot();
}
@ -334,11 +353,17 @@ public partial class Character : CharacterBody2D, IFaction
Attacker = inflictor,
OldHealth = oldHealth,
NewHealth = Health,
Weapon = weapon,
Damage = damage,
};
EmitSignal(SignalName.Hurt, args);
if (inflictor is Player)
{
EmitPlayerHitSignal(args);
}
if (Health <= 0)
{
EmitSignal(SignalName.Death, args);
@ -348,6 +373,26 @@ public partial class Character : CharacterBody2D, IFaction
}
}
/// <summary>
/// Converts a HurtArgs to HitArgs if the attacker was a player and emits
/// the <c>EventBus.PlayerHit</c> signal.
/// </summary>
private void EmitPlayerHitSignal(Events.HurtArgs args)
{
var newArgs = new Events.HitArgs
{
OldHealth = args.OldHealth,
NewHealth = args.NewHealth,
Damage = args.Damage,
Weapon = args.Weapon,
Victim = this,
};
var bus = Events.EventBus.Instance;
bus.EmitSignal(Events.EventBus.SignalName.PlayerHit, newArgs);
}
#if DEBUG
/// <summary>
/// For debugging purposes
/// </summary>
@ -355,7 +400,12 @@ public partial class Character : CharacterBody2D, IFaction
{
OnReceivedDamage(damage, null, 0);
}
#endif
/// <summary>
/// Plays a footstep sound. This should be called through an
/// <c>AnimationPlayer</c> to sync sounds with animations.
/// </summary>
public virtual void Footstep()
{
if (GetNode("Effects/Footstep") is AudioStreamPlayer2D player)
@ -364,6 +414,15 @@ public partial class Character : CharacterBody2D, IFaction
}
}
/// <summary>
/// Returns whether the <c>Character</c> has line of sight with
/// <paramref name="character"/>.
/// </summary>
/// <param name="character">The character to check for LOS</param>
/// <param name="excludeClip">
/// Determines whether the raycast should pass through world clips (physics
/// layer 5)
/// </param>
public bool HasLineOfSight(Character character, bool excludeClip = false)
{
var exclude = new Godot.Collections.Array<Godot.Rid>();

View File

@ -96,29 +96,9 @@ public partial class NPC : Character
};
}
public override void _Draw()
{
#if DEBUG
for (int i = 0; i < 16; i++)
{
Vector2 vec = _weightDirs[i] * _weights[i] * 32;
Color c = Colors.Green;
if (_bestWeightIdx == i)
{
c = Colors.Blue;
}
else if (_weights[i] < 0)
{
c = Colors.Red;
vec = -vec;
}
DrawLine(Vector2.Zero, vec, c);
}
#endif
base._Draw();
}
/// <summary>
/// Finds the NPC's best character to target.
/// </summary>
public virtual Character FindBestTarget()
{
float bestScore = float.MaxValue;
@ -136,21 +116,9 @@ public partial class NPC : Character
float score = 0;
score += Position.DistanceTo(character.Position);
score *= (character.Stealth + 1);
// if the character has enough stealth, the dot product of the
// enemy's current direction and to the character will affect
// the score
// TODO: implement
if (score < bestScore)
{
// if the character has enough stealth, they won't be
// targeted if the NPC is not able to see
if (!HasLineOfSight(character) && character.Stealth >= 1)
{
continue;
}
bestScore = score;
bestChar = character;
}
@ -170,132 +138,4 @@ public partial class NPC : Character
ThinkerStateMachine.PhysicsProcess(delta);
base._PhysicsProcess(delta);
}
public void ThinkProcess(double delta)
{
if ((_thinkTimeElapsed += delta) > ThinkTime)
{
_thinkTimeElapsed = 0;
Think();
#if DEBUG_NPC
QueueRedraw();
#endif
}
if (!ShouldMove || (!ShouldMoveWhenUsingItem && Inventory.IsUsingItem))
{
Direction = Vector2.Zero;
}
else
{
Direction = _weightDirs[_bestWeightIdx];
}
}
public void UpdateWeights(Vector2 pos)
{
// FIXME: TODO: remove all the spaghetti
Vector2 dir = Target.Normalized();
float distSq = GlobalPosition.DistanceSquaredTo(pos);
var spaceState = GetWorld2D().DirectSpaceState;
var exclude = new Godot.Collections.Array<Godot.Rid>();
exclude.Add(this.GetRid());
// calculate weights based on distance
for (int i = 0; i < 16; i++)
{
float directDot = _weightDirs[i].Dot(dir);
// clamp dot from [-1, 1] to [0, 1]
directDot = (directDot + 1) / 2;
float strafeDot = Math.Abs(_weightDirs[i].Dot(dir.Clockwise90()));
float currDirDot = (_weightDirs[i].Dot(Direction) + 1) / 16;
strafeDot = Mathf.Pow((strafeDot + 1) / 2, 2) + currDirDot;
// favor strafing when getting closer
if (distSq > _preferredWeightDistanceSq)
{
_weights[i] = directDot;
}
else if (distSq > _maxWeightDistanceSq)
{
float dDotWeight = Mathf.Sqrt(distSq / 4096);
float sDotWeight = 1 - dDotWeight;
_weights[i] = (dDotWeight * directDot) +
(sDotWeight * strafeDot);
}
else
{
_weights[i] = strafeDot;
}
}
// subtract weights that collide
for (int i = 0; i < 16; i++)
{
var rayParams = new PhysicsRayQueryParameters2D
{
Exclude = exclude,
CollideWithBodies = true,
From = GlobalPosition,
To = GlobalPosition + (_weightDirs[i] * 24),
CollisionMask = 1 + 2 + 16
};
var result = spaceState.IntersectRay(rayParams);
// if we hit something
if (result.Count > 0)
{
// then we subtract the value of this from the other weights
float oldWeight = _weights[i];
for (int j = 0; j < 16; j++)
{
if (i == j)
{
_weights[i] = 0;
}
else
{
float dot = _weightDirs[i].Dot(_weightDirs[j]);
_weights[j] -= _weights[j] * dot;
}
}
}
}
float bestWeight = 0;
for (int i = 0; i < 16; i++)
{
if (_weights[i] > bestWeight)
{
_bestWeightIdx = i;
bestWeight = _weights[i];
}
}
}
protected virtual void Think()
{
// TODO: the entity should wander if it doesn't find a best target
Character bestTarget = FindBestTarget();
if (bestTarget is not null)
{
Vector2 pos = FindBestTarget().GlobalPosition;
Target = pos - GlobalPosition;
Vector2 dir = Target;
float dist = GlobalPosition.DistanceSquaredTo(pos);
UpdateWeights(pos);
if (dist < 1600 && CanAttack)
{
if (Inventory.SelectedItem is Weapon weapon)
{
UseCurrentItem();
}
}
}
}
}

View File

@ -11,6 +11,8 @@ public sealed partial class Player : Character
{
private string _spriteAnim;
public Vector2 DesiredTarget { get; set; }
[Export]
public PlayerCamera Camera { get; set; }
@ -25,11 +27,6 @@ public sealed partial class Player : Character
public override void _Ready()
{
InteractionRay = GetNode<InteractionRay>("Direction2D/InteractionRay");
Death += async (Events.HurtArgs args) =>
{
HurtAnimation.Play("death");
await ToSignal(HurtAnimation, "animation_finished");
};
base._Ready();
@ -51,10 +48,6 @@ public sealed partial class Player : Character
public override void _Process(double delta)
{
base._Process(delta);
var mod = Sprite.SelfModulate;
mod.A = 1 - (Stealth / 2);
Sprite.SelfModulate = mod;
}
public override void _Input(InputEvent @event)
@ -65,6 +58,9 @@ public sealed partial class Player : Character
}
}
/// <summary>
/// Respawns the player with full health and plays spawn animation
/// </summary>
public void Spawn()
{
Health = 100;
@ -87,6 +83,7 @@ public sealed partial class Player : Character
float damage,
Character inflictor,
float knockback,
Items.Weapon weapon = null,
Vector2 knockbackDir = default)
{
if (damage >= 10 && IsAlive)
@ -97,21 +94,22 @@ public sealed partial class Player : Character
GetNode<GpuParticles2D>("Effects/HurtParticles")
.SetDirection(knockbackDir);
base.OnReceivedDamage(damage, inflictor, knockback, knockbackDir);
base.OnReceivedDamage(damage,
inflictor,
knockback,
weapon,
knockbackDir);
}
public override void Die()
{
GD.Print("died");
//base.Die();
HurtAnimation.Play("death");
}
protected override void DrawTarget()
{
base.DrawTarget();
DirectionMarker.GlobalRotation = DirectionMarker.GlobalPosition
.DirectionTo(GetGlobalMousePosition())
.Angle();
DirectionMarker.GlobalRotation = DesiredTarget.Angle();
}
public override void Footstep()

View File

@ -1,12 +1,15 @@
[gd_scene load_steps=63 format=3 uid="uid://b2254pup8k161"]
[gd_scene load_steps=66 format=3 uid="uid://b2254pup8k161"]
[ext_resource type="Script" path="res://Characters/Player.cs" id="1_flygr"]
[ext_resource type="Shader" path="res://Shaders/Flash.gdshader" id="2_ngsgt"]
[ext_resource type="Texture2D" uid="uid://dpepm54hjuyga" path="res://Assets/Sprites/Characters/forsen-hand.png" id="3_3dqh7"]
[ext_resource type="Texture2D" uid="uid://bej8thq7ruyty" path="res://Assets/Sprites/Characters/forsen2.png" id="4_5vird"]
[ext_resource type="Script" path="res://Utils/PlayerStats.cs" id="4_06oya"]
[ext_resource type="PackedScene" uid="uid://cl56eadpklnbo" path="res://Utils/PlayerCamera.tscn" id="4_ym125"]
[ext_resource type="Script" path="res://State/Character/CharacterStateMachine.cs" id="5_rgckv"]
[ext_resource type="Script" path="res://Utils/Values/DoubleValue.cs" id="5_txl0r"]
[ext_resource type="Script" path="res://State/Character/CharacterDashState.cs" id="6_rft7p"]
[ext_resource type="Script" path="res://Utils/Values/IntValue.cs" id="6_sunc5"]
[ext_resource type="Script" path="res://State/Character/PlayerIdleState.cs" id="6_wkfdm"]
[ext_resource type="PackedScene" uid="uid://dvqap2uhcah63" path="res://Items/Weapons/Sword.tscn" id="7_4rxuv"]
[ext_resource type="Script" path="res://State/Character/PlayerMoveState.cs" id="7_dfqd8"]
@ -331,6 +334,15 @@ StateMachine = NodePath("StateMachine")
Hurtbox = NodePath("Hurtbox")
Faction = 1
[node name="Stats" type="Node" parent="."]
script = ExtResource("4_06oya")
[node name="XP" type="Node" parent="Stats"]
script = ExtResource("5_txl0r")
[node name="Level" type="Node" parent="Stats"]
script = ExtResource("6_sunc5")
[node name="StateMachine" type="Node" parent="." node_paths=PackedStringArray("InitialState", "Character")]
script = ExtResource("5_rgckv")
InitialState = NodePath("Idle")

View File

@ -71,7 +71,6 @@ public partial class DynamicDoor : StaticBody2D
{
if (anim.TrackGetPath(i) == nodePath)
{
GD.Print($"Disabled anim for {nodePath}");
anim.TrackSetEnabled(i, isEnabled);
}
}

View File

@ -43,8 +43,11 @@ public partial class Projectile : RigidBody2D
[Export]
public double Delay { get; set; } = 0;
[System.Obsolete]
public Character Character { get; set; }
public Items.Weapon Weapon { get; set; }
public bool IsDead { get; set; }
public override void _Ready()
@ -87,8 +90,9 @@ public partial class Projectile : RigidBody2D
{
hurtbox.InflictDamage(
Hitbox.Damage,
Character,
Hitbox.Inflictor,
Hitbox.Knockback,
weapon: Weapon,
knockbackVector: Direction
);
EmitSignal(SignalName.Hit, box);

View File

@ -63,6 +63,7 @@ public partial class ShungiteSpike : Projectile
float damage,
Characters.Character inflictor,
float knockback,
Items.Weapon weapon,
Vector2 knockbackDir)
{
// if we were hit by the player before the spike freezes,

View File

@ -4,6 +4,8 @@ namespace SupaLidlGame.Events;
public partial class EventBus : Node
{
public static EventBus Instance { get; private set; }
[Signal]
public delegate void RequestMoveToAreaEventHandler(RequestAreaArgs args);
@ -16,6 +18,15 @@ public partial class EventBus : Node
[Signal]
public delegate void PlayerHurtEventHandler(HurtArgs args);
[Signal]
public delegate void PlayerHitEventHandler(HitArgs args);
[Signal]
public delegate void PlayerXPChangedEventHandler(double xp);
[Signal]
public delegate void PlayerLevelChangedEventHandler(int level);
[Signal]
public delegate void PlayerHealthChangedEventHandler(HealthChangedArgs args);
@ -37,5 +48,10 @@ public partial class EventBus : Node
public override void _Ready()
{
ProcessMode = ProcessModeEnum.Always;
if (Instance is not null)
{
throw new MultipleSingletonsException();
}
Instance = this;
}
}

View File

@ -0,0 +1,6 @@
namespace SupaLidlGame.Events;
public partial class HitArgs : HurtArgs
{
public Characters.Character Victim { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace SupaLidlGame;
public class MultipleSingletonsException : System.Exception
{
}

View File

@ -64,7 +64,6 @@ public partial class Inventory : Node2D
{
if (child is Item item)
{
GD.Print("Adding item " + item.Name);
AddItem(item);
}
}

View File

@ -23,6 +23,12 @@ public abstract partial class Item : Node2D
public Character CharacterOwner { get; set; }
/// <summary>
/// Determines if the item is being used. This property determines if
/// a character can use another item or not.
/// See <see cref="Character.UseCurrentItem"/>
/// </summary>
///
public virtual bool IsUsing => false;
/// <summary>

View File

@ -60,6 +60,9 @@ public abstract partial class Weapon : Item
[Export]
public float MaxDistanceHint { get; set; }
[Export]
public float PlayerLevelGain { get; set; }
[Export]
public Sprite2D HandAnchor { get; set; }

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=21 format=3 uid="uid://p7oijq6dbvvk"]
[gd_scene load_steps=22 format=3 uid="uid://p7oijq6dbvvk"]
[ext_resource type="Script" path="res://Items/Weapons/Sword.cs" id="1_1oyma"]
[ext_resource type="Script" path="res://State/Weapon/WeaponStateMachine.cs" id="2_c41ov"]
@ -6,6 +6,7 @@
[ext_resource type="Script" path="res://State/Weapon/SwordAnticipateState.cs" id="4_t7af2"]
[ext_resource type="Script" path="res://State/Weapon/SwordAttackState.cs" id="5_i5v42"]
[ext_resource type="Texture2D" uid="uid://o7enu13gvji5" path="res://Assets/Sprites/doc-lance.png" id="6_7t87o"]
[ext_resource type="Texture2D" uid="uid://d75jkoev5v3w" path="res://Assets/Sprites/Particles/circle-64.png" id="8_gufhv"]
[ext_resource type="Material" uid="uid://cbfaqolx1ydvv" path="res://Assets/Sprites/Particles/ParryParticles.tres" id="8_y2qyn"]
[ext_resource type="PackedScene" uid="uid://du5vhccg75nrq" path="res://BoundingBoxes/Hitbox.tscn" id="9_buajm"]
[ext_resource type="Texture2D" uid="uid://cmvh6pc71ir1m" path="res://Assets/Sprites/sword-swing-large.png" id="11_46l1i"]
@ -34,7 +35,7 @@ orbit_velocity_max = 0.0
scale_min = 2.0
scale_max = 2.0
scale_curve = SubResource("CurveTexture_383y7")
color = Color(1, 0, 1, 1)
color = Color(0.560784, 0.145098, 0.180392, 1)
[sub_resource type="Animation" id="Animation_b7327"]
length = 0.001
@ -239,17 +240,15 @@ texture = ExtResource("6_7t87o")
[node name="ParryParticles" type="GPUParticles2D" parent="Anchor/Node2D/Sprite2D"]
modulate = Color(1.2, 1.2, 1.2, 1)
position = Vector2(-0.221825, -3.12132)
position = Vector2(0, -3)
rotation = 0.785398
emitting = false
amount = 16
process_material = ExtResource("8_y2qyn")
texture = ExtResource("8_gufhv")
lifetime = 2.0
one_shot = true
explosiveness = 1.0
trail_enabled = true
trail_lifetime = 0.1
trail_sections = 4
[node name="GPUParticles2D" type="GPUParticles2D" parent="Anchor/Node2D/Sprite2D"]
position = Vector2(-2.28882e-05, -6)

View File

@ -49,6 +49,9 @@ public partial class ProjectileSpawner : Ranged
}
}
projectile.Hitbox.Inflictor = Character;
projectile.Weapon = this;
if (projectile is Utils.ITarget target)
{
if (Character is Characters.NPC npc)
@ -92,7 +95,6 @@ public partial class ProjectileSpawner : Ranged
for (int i = 0; i < ProjectileCount; i++)
{
float curDeviation = -i + maxAngleDeviations;
GD.Print(curDeviation);
SpawnProjectile(map, target.Rotated(curDeviation * theta));
}
}

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=28 format=3 uid="uid://5y1acxl4j4n7"]
[gd_scene load_steps=29 format=3 uid="uid://5y1acxl4j4n7"]
[ext_resource type="Script" path="res://Items/Weapons/Sword.cs" id="1_mai31"]
[ext_resource type="Script" path="res://State/Weapon/WeaponStateMachine.cs" id="2_5ramr"]
@ -8,6 +8,7 @@
[ext_resource type="Texture2D" uid="uid://dfpe74vxvuwal" path="res://Assets/Sprites/Items/pugio.png" id="6_d28k5"]
[ext_resource type="Script" path="res://State/Weapon/SwordBlockState.cs" id="6_yvm8x"]
[ext_resource type="Material" uid="uid://cbfaqolx1ydvv" path="res://Assets/Sprites/Particles/ParryParticles.tres" id="8_we1sv"]
[ext_resource type="Texture2D" uid="uid://d75jkoev5v3w" path="res://Assets/Sprites/Particles/circle-64.png" id="9_3p5s2"]
[ext_resource type="AudioStream" uid="uid://m1sbk3c4eask" path="res://Assets/Sounds/metal-bash2.wav" id="9_b6yro"]
[ext_resource type="PackedScene" uid="uid://du5vhccg75nrq" path="res://BoundingBoxes/Hitbox.tscn" id="9_qimey"]
[ext_resource type="AudioStream" uid="uid://kao8wbfaum27" path="res://Assets/Sounds/metal-bash3.wav" id="10_istfq"]
@ -386,6 +387,7 @@ Damage = 20.0
UseTime = 0.55
UseAltTime = 1.5
Knockback = 64.0
PlayerLevelGain = 1.0
HandAnchor = NodePath("Anchor/Node2D/Sprite2D/Hand")
[node name="State" type="Node" parent="." node_paths=PackedStringArray("InitialState")]
@ -435,17 +437,14 @@ texture = ExtResource("6_d28k5")
[node name="ParryParticles" type="GPUParticles2D" parent="Anchor/Node2D/Sprite2D"]
modulate = Color(1.2, 1.2, 1.2, 1)
position = Vector2(-0.221825, -3.12132)
position = Vector2(0, -3)
rotation = 0.785398
emitting = false
amount = 16
process_material = ExtResource("8_we1sv")
texture = ExtResource("9_3p5s2")
lifetime = 2.0
one_shot = true
explosiveness = 1.0
trail_enabled = true
trail_lifetime = 0.1
trail_sections = 4
[node name="Hand" type="Sprite2D" parent="Anchor/Node2D/Sprite2D"]
position = Vector2(-2.52724e-05, 7)

View File

@ -258,6 +258,7 @@ StateMachine = NodePath("State")
Damage = 12.0
UseTime = 1.5
InitialVelocity = 220.0
PlayerLevelGain = 0.5
[node name="State" type="Node" parent="." node_paths=PackedStringArray("InitialState")]
script = ExtResource("2_ag6rd")

View File

@ -7,6 +7,9 @@ using SupaLidlGame.State.Weapon;
namespace SupaLidlGame.Items.Weapons;
/// <summary>
/// A basic melee weapon.
/// </summary>
public partial class Sword : Weapon, IParryable
{
public bool IsAttacking { get; protected set; }
@ -77,6 +80,9 @@ public partial class Sword : Weapon, IParryable
EnableParry(Time.GetTicksMsec());
}
/// <summary>
/// Makes this melee weapon be able to parry and be parried.
/// </summary>
public void EnableParry(ulong parryTimeOrigin)
{
IsParried = false;
@ -84,6 +90,9 @@ public partial class Sword : Weapon, IParryable
ParryTimeOrigin = parryTimeOrigin;
}
/// <summary>
/// Makes this melee weapon be able to parry and be parried.
/// </summary>
public void DisableParry()
{
IsParryable = false;
@ -113,6 +122,10 @@ public partial class Sword : Weapon, IParryable
base.DeuseAlt();
}
/// <summary>
/// Enables the weapon's hitbox. Prefer to call this from a state machine
/// rather than managing state through the weapon script.
/// </summary>
public void Attack()
{
//RemainingAttackTime = AttackTime;
@ -120,6 +133,9 @@ public partial class Sword : Weapon, IParryable
Hitbox.IsDisabled = false;
}
/// <summary>
/// Disables the weapon's hitbox and processes all hurtboxes it hit.
/// </summary>
public void Deattack()
{
IsAttacking = false;
@ -161,6 +177,9 @@ public partial class Sword : Weapon, IParryable
base._Process(delta);
}
/// <summary>
/// Processes all hits and applies damages to hurtboxes.
/// </summary>
public void ProcessHits()
{
if (IsParried)
@ -172,7 +191,11 @@ public partial class Sword : Weapon, IParryable
{
if (box is Hurtbox hurtbox)
{
hurtbox.InflictDamage(Damage, Character, Knockback);
hurtbox.InflictDamage(
Damage,
Character,
Knockback,
this);
}
}
}
@ -199,6 +222,10 @@ public partial class Sword : Weapon, IParryable
}
}
/// <summary>
/// Stuns the wepaon holder. This is unique to swords and melee weapons
/// if they can parry.
/// </summary>
public void Stun()
{
IsParried = true;
@ -238,9 +265,4 @@ public partial class Sword : Weapon, IParryable
}
}
}
protected void SetAnimationCondition(string condition, bool value)
{
}
}

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=36 format=3 uid="uid://dvqap2uhcah63"]
[gd_scene load_steps=37 format=3 uid="uid://dvqap2uhcah63"]
[ext_resource type="Script" path="res://Items/Weapons/Sword.cs" id="1_mlo73"]
[ext_resource type="Script" path="res://State/Weapon/WeaponStateMachine.cs" id="2_vwirq"]
@ -10,6 +10,7 @@
[ext_resource type="Script" path="res://State/Weapon/SwordAttackState.cs" id="5_hmisb"]
[ext_resource type="AudioStream" uid="uid://c4n7ioxpukdwi" path="res://Assets/Sounds/parry.wav" id="6_8nxjm"]
[ext_resource type="Material" uid="uid://cbfaqolx1ydvv" path="res://Assets/Sprites/Particles/ParryParticles.tres" id="8_10gir"]
[ext_resource type="Texture2D" uid="uid://d75jkoev5v3w" path="res://Assets/Sprites/Particles/circle-64.png" id="9_o34ry"]
[ext_resource type="Shape2D" uid="uid://dw4e4r2yxwk1b" path="res://Items/Weapons/SwordCollisionShape.tres" id="9_wsprl"]
[ext_resource type="Texture2D" uid="uid://cmvh6pc71ir1m" path="res://Assets/Sprites/sword-swing-large.png" id="10_672jv"]
[ext_resource type="AudioStream" uid="uid://qvthq6tppp63" path="res://Assets/Sounds/whoosh.wav" id="10_mfnl7"]
@ -369,6 +370,7 @@ Damage = 20.0
UseTime = 0.55
Knockback = 64.0
ShouldHideIdle = true
PlayerLevelGain = 1.0
HandAnchor = NodePath("Anchor/Node2D/Sprite2D/Hand")
[node name="State" type="Node" parent="." node_paths=PackedStringArray("InitialState")]
@ -414,23 +416,22 @@ y_sort_enabled = true
position = Vector2(0, -8)
texture = ExtResource("3_r75ni")
[node name="Hand" type="Sprite2D" parent="Anchor/Node2D/Sprite2D"]
position = Vector2(-2.52724e-05, 7)
rotation = 1.5708
[node name="ParryParticles" type="GPUParticles2D" parent="Anchor/Node2D/Sprite2D"]
modulate = Color(1.2, 1.2, 1.2, 1)
position = Vector2(-0.221825, -3.12132)
position = Vector2(0, -3)
rotation = 0.785398
emitting = false
amount = 16
process_material = ExtResource("8_10gir")
texture = ExtResource("9_o34ry")
lifetime = 2.0
one_shot = true
explosiveness = 1.0
trail_enabled = true
trail_lifetime = 0.1
trail_sections = 4
[node name="Hand" type="Sprite2D" parent="Anchor/Node2D/Sprite2D"]
position = Vector2(-2.52724e-05, 7)
rotation = 1.5708
fixed_fps = 16
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
libraries = {

View File

@ -4,7 +4,22 @@ Forsen-related game
![](./Assets/Sprites/Characters/forsen2-portrait.png)
![](https://i.ibb.co/t367kD4/baj.gif)
## Building
This is currently being developed on a custom dev branch of Godot 4.1.1:
https://github.com/ryze312/godot/tree/x11_event_fix
Try to avoid using newer versions of Godot as they are unstable with C#.
## Notes
The tilde key (`~`) can open the developer console. This allows access to
singletons --- an instance of `Utils.World` can be accessed through `World`,
and the player character can be accessed through `World.CurrentPlayer`.
The default starting scene is `res://Scenes/ArenaExterior.tscn`, and running a
non-map scene may spill out errors from `SupaLidlGame.Utils.World`. Eventually
this will be fixed to allow main menu scenes.
## Attributions

View File

@ -1,8 +0,0 @@
#+TITLE: SupaLidlGame
Forsen-related game
#+attr_html: :style margin-left: auto; margin-right: auto;
[[./Assets/Sprites/Characters/forsen2-portrait.png]]
[[https://i.ibb.co/t367kD4/baj.gif]]

View File

@ -42,7 +42,6 @@ public abstract partial class PlayerState : CharacterState
if (@event.IsActionPressed("interact"))
{
// if looking at a trigger then interact with it
GD.Print("interacting");
player.InteractionRay.Trigger?.InvokeInteraction();
}
}

View File

@ -18,11 +18,28 @@ public abstract partial class StateMachine<T> : Node where T : Node, IState<T>
ChangeState(InitialState);
}
/// <summary>
/// Changes the state of the <c>StateMachine</c>.
/// </summary>
/// <param name="nextState">The next state to transition to.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="nextState" /> is a
/// valid state, otherwise <see langword="false" />
/// </returns>
public virtual bool ChangeState(T nextState)
{
return ChangeState(nextState, out Stack<T> _);
}
/// <summary>
/// Changes the state of the <c>StateMachine</c>.
/// </summary>
/// <param name="nextState">The next state to transition to.</param>
/// <param name="finalState">The actual state.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="nextState" /> is a
/// valid state, otherwise <see langword="false" />
/// </returns>
public bool ChangeState(T nextState, out T finalState)
{
var status = ChangeState(nextState, out Stack<T> states);
@ -30,6 +47,15 @@ public abstract partial class StateMachine<T> : Node where T : Node, IState<T>
return status;
}
/// <summary>
/// Changes the state of the <c>StateMachine</c>.
/// </summary>
/// <param name="nextState">The next state to transition to.</param>
/// <param name="states">Stack of all states that transitioned/proxied.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="nextState" /> is a
/// valid state, otherwise <see langword="false" />
/// </returns>
public bool ChangeState(T nextState, out Stack<T> states)
{
states = new Stack<T>();
@ -71,14 +97,35 @@ public abstract partial class StateMachine<T> : Node where T : Node, IState<T>
}
/// <summary>
/// Changes the current state to a state of type U which must inherit from T.
/// Changes the state of the <c>StateMachine</c> of type
/// <typeparamref name="U" /> which must inherit from
/// <typeparamref name="T" />.
/// </summary>
/// <typeparam name="U">The type of the state to transition to.</typeparam>
/// <param name="state">The resulting state to be transitioned to.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="nextState" /> is a
/// valid state, otherwise <see langword="false" />
/// </returns>
public bool ChangeState<U>(out U state) where U : T
{
state = this.FindChildOfType<U>();
return ChangeState(state);
}
/// <summary>
/// Changes the state of the <c>StateMachine</c> with node name
/// <paramref name="name" />.
/// </summary>
/// <typeparam name="U">The type of the state to transition to.</typeparam>
/// <param name="name">
/// The name of the <typeparamref name="T" /> node.
/// </param>
/// <param name="state">The resulting state to be transitioned to.</param>
/// <returns>
/// <see langword="true" /> if <paramref name="nextState" /> is a
/// valid state, otherwise <see langword="false" />
/// </returns>
public bool ChangeState(string name, out T state)
{
state = GetNode<T>(name);

View File

@ -1,9 +1,10 @@
[gd_scene load_steps=7 format=3 uid="uid://c271rdjhd1gfo"]
[gd_scene load_steps=8 format=3 uid="uid://c271rdjhd1gfo"]
[ext_resource type="PackedScene" uid="uid://73jm5qjy52vq" path="res://Dialogue/balloon.tscn" id="1_atjb1"]
[ext_resource type="Script" path="res://UI/UIController.cs" id="2_b4b6l"]
[ext_resource type="PackedScene" uid="uid://bxo553hblp6nf" path="res://UI/HealthBar.tscn" id="3_j1j6h"]
[ext_resource type="PackedScene" uid="uid://01d24ij5av1y" path="res://UI/BossBar.tscn" id="4_igi28"]
[ext_resource type="PackedScene" uid="uid://cr7tkxctmyags" path="res://UI/LevelBar.tscn" id="4_rcekd"]
[ext_resource type="PackedScene" uid="uid://c77754nvmckn" path="res://UI/LocationDisplay.tscn" id="5_cr6vo"]
[ext_resource type="PackedScene" uid="uid://d3q1yu3n7cqfj" path="res://UI/SceneTransition.tscn" id="6_j0nhv"]
@ -47,7 +48,7 @@ BossBar = NodePath("Bottom/BossBar")
layout_mode = 1
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 40.0
offset_bottom = 64.0
grow_horizontal = 2
[node name="Margin" type="MarginContainer" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top"]
@ -55,9 +56,21 @@ layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 16
[node name="HealthBar" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top/Margin" instance=ExtResource("3_j1j6h")]
[node name="VBoxContainer" type="VBoxContainer" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top/Margin"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 12
[node name="HealthBar" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top/Margin/VBoxContainer" instance=ExtResource("3_j1j6h")]
layout_mode = 2
[node name="LevelBar" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top/Margin/VBoxContainer" instance=ExtResource("4_rcekd")]
layout_mode = 2
[node name="Margin2" type="MarginContainer" parent="SubViewportContainer/UIViewport/MainUILayer/Main/Top/Margin/VBoxContainer"]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 16
[node name="Bottom" type="HBoxContainer" parent="SubViewportContainer/UIViewport/MainUILayer/Main"]
layout_mode = 1

36
UI/LevelBar.cs 100644
View File

@ -0,0 +1,36 @@
using Godot;
using SupaLidlGame.Events;
namespace SupaLidlGame.UI;
public partial class LevelBar : Control
{
public TextureProgressBar XPBar { get; set; }
private TextureProgressBar[] _levelBars = new TextureProgressBar[4];
public override void _Ready()
{
XPBar = GetNode<TextureProgressBar>("%XPBar");
for (int i = 0; i < 4; i++)
{
_levelBars[i] = GetNode<TextureProgressBar>($"%Level{i + 1}Bar");
}
EventBus.Instance.PlayerXPChanged += (xp) =>
{
XPBar.Value = xp;
};
EventBus.Instance.PlayerLevelChanged += (level) =>
{
for (int i = 0; i < _levelBars.Length; i++)
{
// level 0: 0 is not less than 0
// level 1: 0 is less than 1
_levelBars[i].Value = i < level ? 1 : 0;
}
};
}
}

94
UI/LevelBar.tscn 100644
View File

@ -0,0 +1,94 @@
[gd_scene load_steps=4 format=3 uid="uid://cr7tkxctmyags"]
[ext_resource type="Script" path="res://UI/LevelBar.cs" id="1_eqetx"]
[ext_resource type="Texture2D" uid="uid://b75oak1nd2q6x" path="res://Assets/Sprites/UI/over-under-bar.png" id="2_f332l"]
[ext_resource type="Texture2D" uid="uid://co7xm7i5f6n51" path="res://Assets/Sprites/UI/progress-bar.png" id="3_arvub"]
[node name="LevelBar" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 128.0
offset_bottom = 8.0
script = ExtResource("1_eqetx")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
offset_right = 128.0
offset_bottom = 8.0
[node name="XPBar" type="TextureProgressBar" parent="HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
size_flags_horizontal = 3
max_value = 4.0
step = 0.01
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
texture_under = ExtResource("2_f332l")
texture_progress = ExtResource("3_arvub")
[node name="Level1Bar" type="TextureProgressBar" parent="HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
max_value = 1.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
texture_under = ExtResource("2_f332l")
texture_progress = ExtResource("3_arvub")
[node name="Level2Bar" type="TextureProgressBar" parent="HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
max_value = 1.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
texture_under = ExtResource("2_f332l")
texture_progress = ExtResource("3_arvub")
[node name="Level3Bar" type="TextureProgressBar" parent="HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
max_value = 1.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
texture_under = ExtResource("2_f332l")
texture_progress = ExtResource("3_arvub")
[node name="Level4Bar" type="TextureProgressBar" parent="HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
max_value = 1.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
texture_under = ExtResource("2_f332l")
texture_progress = ExtResource("3_arvub")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 4

View File

@ -23,12 +23,9 @@ public static class Physics
var relVel = relIdle + relDir * speed;
var relSpeed = relVel.Length();
GD.Print("Relative velocity: " + relVel);
// get t = time to travel
// = (|s2| - |s1|)/(|v2| - |v1|)
time = position.DistanceTo(targetPosition) / relSpeed;
GD.Print("Time: " + time);
// predict target's position after time t
return targetPosition + targetVelocity * time;

View File

@ -0,0 +1,75 @@
using Godot;
using SupaLidlGame.Events;
namespace SupaLidlGame.Utils;
public partial class PlayerStats : Node
{
public const int MAX_XP_PER_LEVEL = 4;
public const int MAX_LEVELS = 4;
public DoubleValue XP { get; set; }
public IntValue Level { get; set; }
public double XPDecayVelocity { get; set; } = 1;
protected bool _shouldDecayXP = true;
protected Timer _xpDecayTimer;
public override void _Ready()
{
XP = GetNode<DoubleValue>("XP");
Level = GetNode<IntValue>("Level");
_xpDecayTimer = new Timer();
_xpDecayTimer.Timeout += () => _shouldDecayXP = true;
_xpDecayTimer.Stop();
AddChild(_xpDecayTimer);
var bus = EventBus.Instance;
XP.Changed += (oldValue, newValue) =>
{
bus.EmitSignal(EventBus.SignalName.PlayerXPChanged, newValue);
};
Level.Changed += (oldValue, newValue) =>
{
bus.EmitSignal(EventBus.SignalName.PlayerLevelChanged, newValue);
};
bus.PlayerHit += (args) =>
{
double xp = XP.Value;
xp += args.Weapon?.PlayerLevelGain ?? 0;
if (xp >= MAX_XP_PER_LEVEL)
{
int deltaLevel = (int)(xp / MAX_XP_PER_LEVEL);
xp %= MAX_XP_PER_LEVEL;
Level.Value = Mathf.Min(Level.Value + deltaLevel, MAX_LEVELS);
}
if (Level.Value == MAX_LEVELS)
{
// if max level, can only go up to 1 xp
xp = Mathf.Min(xp, 1);
}
XP.Value = xp;
_shouldDecayXP = false;
_xpDecayTimer.Start(1);
};
}
public override void _Process(double delta)
{
if (_shouldDecayXP)
{
XP.Value = Mathf.MoveToward(XP.Value, 1, XPDecayVelocity * delta);
}
}
}

View File

@ -0,0 +1,22 @@
using Godot;
namespace SupaLidlGame;
public partial class DoubleValue : Node, IValue<double>
{
[Signal]
public delegate void ChangedEventHandler(double oldValue, double newValue);
protected double _value = default;
[Export]
public double Value
{
get => _value;
set
{
EmitSignal(SignalName.Changed, _value, value);
_value = value;
}
}
}

View File

@ -0,0 +1,22 @@
using Godot;
namespace SupaLidlGame;
public partial class FloatValue : Node, IValue<float>
{
[Signal]
public delegate void ChangedEventHandler(float oldValue, float newValue);
protected float _value = default;
[Export]
public float Value
{
get => _value;
set
{
EmitSignal(SignalName.Changed, _value, value);
_value = value;
}
}
}

View File

@ -0,0 +1,8 @@
namespace SupaLidlGame;
public interface IValue<T>
{
public delegate void ChangedEventHandler(T oldValue, T newValue);
public T Value { get; set; }
}

View File

@ -0,0 +1,22 @@
using Godot;
namespace SupaLidlGame;
public partial class IntValue : Node, IValue<int>
{
[Signal]
public delegate void ChangedEventHandler(int oldValue, int newValue);
protected int _value = default;
[Export]
public int Value
{
get => _value;
set
{
EmitSignal(SignalName.Changed, _value, value);
_value = value;
}
}
}

View File

@ -327,8 +327,6 @@ public partial class World : Node
public void SpawnPlayer()
{
// TODO: add max health property
//CurrentPlayer.Health = 100;
//CurrentPlayer.Sprite.Visible = true;
if (CurrentMap.SceneFilePath != GlobalState.Stats.SaveMapKey)
{
LoadScene(GlobalState.Stats.SaveMapKey);

View File

@ -1,39 +0,0 @@
#+TITLE: SupaLidlGame To-do List
* STARTED Campfires
DEADLINE: <2022-12-03 Sat>
* DONE Enemy Spawning
* DONE Handle Character Death
DEADLINE: <2022-12-04 Sun>
* Doc Boss
** DONE Reset possible attacks after each cycle
CLOSED: [2023-07-21 Fri]
** DONE Attack animations
CLOSED: [2023-07-20 Thu]
** DONE Boss Music
CLOSED: [2023-07-24 Mon]
* TODO Boss cards
* DONE Dialog
CLOSED: [2023-07-25 Tue]
* TODO Short arena entrance
CLOSED: [2023-08-02 Wed]
* DONE Video demonstration
CLOSED: [2023-07-25 Tue]
* TODO Combo Attacks ("Level system")
** TODO Healing
** TODO Alt attack
** TODO Max level

View File

@ -206,4 +206,5 @@ locale/translations_pot_files=PackedStringArray("res://Assets/Dialog/doc.dialogu
[rendering]
textures/canvas_textures/default_texture_filter=0
renderer/rendering_method="gl_compatibility"
environment/defaults/default_clear_color=Color(0.301961, 0.301961, 0.301961, 1)

View File

@ -1,2 +0,0 @@
#+title: TODO LIST