Skip to content

Commit

Permalink
zzre: Add fairy NPCs (#251)
Browse files Browse the repository at this point in the history
* zzre: Refactor HoverOffset out of HoverBehind component

* zzre: Add KeepLastHover system

* ease debugging

* Fix NPCLookAtPlayer for fairy npcs

* Fix fairy npc selection

* Fix marker position for flying npcs

* Fix dialog face and name for flying npcs
  • Loading branch information
Helco authored Oct 8, 2023
1 parent 76578bf commit b621e30
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 55 deletions.
7 changes: 6 additions & 1 deletion zzre/game/Game.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DefaultEcs.System;
Expand Down Expand Up @@ -91,7 +92,9 @@ public Game(ITagContainer diContainer, Savegame savegame)
new systems.TriggerActivation(this),
new systems.PlayerTriggers(this),
new systems.OverworldFairySpawner(this),
new systems.FairyHoverOffset(this),
new systems.FairyHoverBehind(this),
new systems.FairyKeepLastHover(this),
new systems.FairyAnimation(this),
new systems.DialogScript(this),
new systems.DialogDelay(this),
Expand Down Expand Up @@ -172,6 +175,8 @@ public void Render(CommandList cl)

public void LoadScene(string sceneName)
{
Console.WriteLine("Load " + sceneName);

if (ecsWorld.Has<WorldBuffers>())
ecsWorld.Get<WorldBuffers>().Dispose();
worldRenderer?.Dispose();
Expand Down
2 changes: 1 addition & 1 deletion zzre/game/Zanzarah.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public Zanzarah(ITagContainer diContainer, IZanzarahContainer zanzarahContainer)
tagContainer.AddTag(UI = new UI(this));
this.zanzarahContainer = zanzarahContainer;

var savegame = new zzio.Savegame() { sceneId = 3202 };
var savegame = new zzio.Savegame() { sceneId = 3200 };
/*using (var fileStream = new System.IO.FileStream(@"C:\dev\zanzarah\Save\_0004.dat", System.IO.FileMode.Open, System.IO.FileAccess.Read))
using (var reader = new System.IO.BinaryReader(fileStream))
savegame = zzio.Savegame.ReadNew(reader);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
namespace zzre.game.components;
using System.Numerics;

namespace zzre.game.components;

public enum FairyHoverState
{
Behind = 2,
KeepLastHover = 10
}

public readonly record struct FairyHoverOffset(Vector3 Value);

public readonly record struct FairyHoverStart(Vector3 Value);

public struct FairyHoverBehind
{
Expand All @@ -14,7 +26,6 @@ public enum Mode

public Mode CurMode;
public float TimeLeft;
public System.Numerics.Vector3 HoverOffset;

public float MaxDuration { get; init; }
public float Distance { get; init; }
Expand Down
6 changes: 6 additions & 0 deletions zzre/game/systems/dialog/DialogTalk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ private void CreateTalkLabel(DefaultEcs.Entity parent, UID dialogUID, Rect bgRec
private const string BaseFacePath = "resources/bitmaps/faces/";
private float? TryCreateFace(DefaultEcs.Entity parent, DefaultEcs.Entity npcEntity, Rect bgRect)
{
if (!npcEntity.Has<components.ActorParts>())
return null;

var npcBodyEntity = npcEntity.Get<components.ActorParts>().Body;
var npcModelName = npcBodyEntity.Get<resources.ClumpInfo>().Name
.Replace(".dff", "", StringComparison.OrdinalIgnoreCase);
Expand All @@ -113,6 +116,9 @@ private void CreateTalkLabel(DefaultEcs.Entity parent, UID dialogUID, Rect bgRec
private void CreateNameLabel(DefaultEcs.Entity parent, DefaultEcs.Entity npcEntity, Rect bgRect, float? faceWidth)
{
var npcName = npcEntity.Get<NpcRow>().Name;
if (string.IsNullOrWhiteSpace(npcName) || npcName == "-")
return;

var entity = preload.CreateLabel(parent)
.WithText(npcName)
.With(preload.Fnt001)
Expand Down
39 changes: 6 additions & 33 deletions zzre/game/systems/fairy/FairyHoverBehind.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,24 @@
namespace zzre.game.systems;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using DefaultEcs.System;
using Mode = zzre.game.components.FairyHoverBehind.Mode;

public partial class FairyHoverBehind : AEntitySetSystem<float>
{
private readonly GameTime time;
private readonly IDisposable playerEnteredSubscription;
private readonly IDisposable addedSubscription;
private WorldCollider? worldCollider;

public FairyHoverBehind(ITagContainer diContainer) : base(diContainer.GetTag<DefaultEcs.World>(), CreateEntityContainer, useBuffer: false)
{
time = diContainer.GetTag<GameTime>();
playerEnteredSubscription = World.Subscribe<messages.PlayerEntered>(HandlePlayerEntered);
addedSubscription = World.SubscribeEntityComponentAdded<components.FairyHoverBehind>(HandleAddedComponent);
Set.EntityAdded += ResetPosition;
}

public override void Dispose()
{
base.Dispose();
playerEnteredSubscription.Dispose();
addedSubscription.Dispose();
}

private void HandlePlayerEntered(in messages.PlayerEntered _)
Expand All @@ -33,22 +27,23 @@ private void HandlePlayerEntered(in messages.PlayerEntered _)
ResetPosition(entity);
}

private void HandleAddedComponent(in DefaultEcs.Entity entity, in components.FairyHoverBehind value) =>
ResetPosition(entity);

private void ResetPosition(in DefaultEcs.Entity entity)
{
var parent = entity.Get<components.Parent>().Entity;
entity.Get<Location>().LocalPosition = parent.Get<Location>().LocalPosition;
}

[WithPredicate]
private bool IsInHoverState(in components.FairyHoverState state) => state == components.FairyHoverState.Behind;

[Update]
private void Update(
float elapsedTime,
DefaultEcs.Entity entity,
Location location,
in components.Parent parent,
ref components.FairyHoverBehind hoverBehind,
in components.FairyHoverOffset hoverOffset,
ref components.Velocity velocity)
{
var random = Random.Shared;
Expand Down Expand Up @@ -84,7 +79,7 @@ private void Update(
var targetPosition = parentPos + targetOffset;
var move = targetPosition - location.LocalPosition;
move -= move * MathF.Pow(0.1f, elapsedTime);
location.LocalPosition += move + GetHoverOffset(ref hoverBehind, elapsedTime);
location.LocalPosition += move + hoverOffset.Value;
var minYPos = parentPos.Y - 0.5f;
if (location.LocalPosition.Y < minYPos)
location.LocalPosition = location.LocalPosition with { Y = minYPos };
Expand All @@ -97,26 +92,4 @@ private void Update(
hoverBehind.CurMode = Mode.CenterHigh;
}
}

private Vector3 GetHoverOffset(ref components.FairyHoverBehind hoverBehind, float elapsedTime)
{
var random = Random.Shared;
var offset = hoverBehind.HoverOffset;
if (offset == Vector3.Zero)
{
offset = new(
random.InLine() * 10f,
0f,
random.InLine() * 10f);
}

float sinDelta = MathF.Sin(elapsedTime), cosDelta = MathF.Cos(elapsedTime);
offset.X = cosDelta * offset.X - sinDelta * offset.Z;
offset.Y = 0.01f;
offset.Z = sinDelta * offset.X + cosDelta * offset.Z;
offset = Vector3.Normalize(offset) * elapsedTime * 0.4f; // yes, there is basically no non-vertical movement
offset.Y = MathF.Cos(time.TotalElapsed / 100f) * (elapsedTime * 0.3f); // TODO: Fix framerate-dependent vertical fairy hovering

return hoverBehind.HoverOffset = offset;
}
}
42 changes: 42 additions & 0 deletions zzre/game/systems/fairy/FairyHoverOffset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace zzre.game.systems;
using System;
using System.Numerics;
using DefaultEcs.System;

public partial class FairyHoverOffset : AEntitySetSystem<float>
{
private readonly Random random = Random.Shared;
private readonly GameTime time;

public FairyHoverOffset(ITagContainer diContainer) : base(diContainer.GetTag<DefaultEcs.World>(), CreateEntityContainer, useBuffer: false)
{
time = diContainer.GetTag<GameTime>();
}

[WithPredicate]
private bool IsInRelevantStates(in components.FairyHoverState state) => state == components.FairyHoverState.Behind;

[Update]
private void Update(float elapsedTime, ref components.FairyHoverOffset component)
{
var offset = component.Value;
if (offset == Vector3.Zero)
{
offset = new(
random.InLine() * 10f,
0f,
random.InLine() * 10f);
}

float sinDelta = MathF.Sin(elapsedTime), cosDelta = MathF.Cos(elapsedTime);
var oldOffsetX = offset.X;
offset.X = cosDelta * oldOffsetX - sinDelta * offset.Z;
offset.Y = 0.01f;
offset.Z = sinDelta * oldOffsetX + cosDelta * offset.Z;
offset = Vector3.Normalize(offset) * elapsedTime * 0.4f; // yes, there is basically no non-vertical movement
offset.Y = MathF.Cos(time.TotalElapsed / 100f) * (elapsedTime * 0.3f); // TODO: Fix framerate-dependent vertical fairy hovering
// ^ this is framerate-dependent as an absolute value depends on a single frame delta

component = new(offset);
}
}
59 changes: 59 additions & 0 deletions zzre/game/systems/fairy/FairyKeepLastHover.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace zzre.game.systems;
using System;
using System.Numerics;
using DefaultEcs.System;

public partial class FairyKeepLastHover : AEntitySetSystem<float>
{
private const float HoverDistance = -0.7f;
private const float HoverRadius = 0.2f;
private const float BounceMagnitude = 0.05f;
private readonly GameTime time;

public FairyKeepLastHover(ITagContainer diContainer)
: base(diContainer.GetTag<DefaultEcs.World>(), CreateEntityContainer, useBuffer: false)
{
time = diContainer.GetTag<GameTime>();
Set.EntityAdded += HandleAdded;
}

private void HandleAdded(in DefaultEcs.Entity entity)
{
var location = entity.Get<Location>();
entity.Set(new components.Velocity(new(0f, 0.01f, 0f)));
entity.Set(new components.FairyHoverStart(location.LocalPosition));
// ^ this is not modifying the set as fairies should already have this component
}

[WithPredicate]
private bool IsInState(in components.FairyHoverState state) => state == components.FairyHoverState.KeepLastHover;

[Update]
private void Update(
float elapsedTime,
in components.Parent parent,
in components.FairyHoverStart hoverStart,
ref components.FairyHoverOffset hoverOffset,
Location location)
{
// similar but not equal to the FairyHoverOffset update
float sinDelta = MathF.Sin(elapsedTime), cosDelta = MathF.Cos(elapsedTime);
var offset = hoverOffset.Value;
var oldOffsetX = offset.X;
offset.X = cosDelta * oldOffsetX - sinDelta * offset.Z;
offset.Y = 0f;
offset.Z = cosDelta * offset.Z + sinDelta * oldOffsetX;
if (MathEx.CmpZero(offset.X))
offset.X = 0.1f;
if (MathEx.CmpZero(offset.Z))
offset.Z = 0.1f;
offset = Vector3.Normalize(offset) * HoverRadius;
offset.Y = MathF.Cos(time.TotalElapsed) * BounceMagnitude;
location.LocalPosition = hoverStart.Value + offset;
hoverOffset = new(offset);

var parentDirection = parent.Entity.Get<Location>().InnerForward;
location.LookIn(parentDirection with { Y = 0f });
location.LocalPosition += location.InnerForward * HoverDistance;
}
}
4 changes: 4 additions & 0 deletions zzre/game/systems/fairy/OverworldFairySpawner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public OverworldFairySpawner(ITagContainer diContainer) : base(diContainer.GetTa
public override void Dispose()
{
base.Dispose();
sceneChangingSubscription.Dispose();
inventoryAddedSubscription.Dispose();
}

Expand Down Expand Up @@ -72,7 +73,10 @@ private void SpawnFairy(DefaultEcs.Entity parent, zzio.InventoryFairy invFairy)
fairy.Set(invFairy);
fairy.Set(dbRow);
fairy.Set(ManagedResource<zzio.ActorExDescription>.Create(dbRow.Mesh));
fairy.Set<components.FairyHoverStart>();
fairy.Set<components.FairyHoverOffset>();
fairy.Set(components.FairyHoverBehind.Normal);
fairy.Set(components.FairyHoverState.Behind);
fairy.Set<components.Velocity>();
fairy.Set(new components.FairyAnimation()
{
Expand Down
5 changes: 2 additions & 3 deletions zzre/game/systems/npc/NPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ namespace zzre.game.systems;
public partial class NPC : AEntitySetSystem<float>
{
private const uint MaxEnabledII2 = 1000;
private const float GroundFromOffset = 1f;
private const float GroundToOffset = -7f;

private readonly IDisposable sceneChangingSubscription;
private readonly IDisposable sceneLoadSubscription;
Expand All @@ -28,6 +26,7 @@ public NPC(ITagContainer diContainer) : base(diContainer.GetTag<DefaultEcs.World
public override void Dispose()
{
base.Dispose();
sceneChangingSubscription.Dispose();
sceneLoadSubscription.Dispose();
setNpcModifierSubscription.Dispose();
}
Expand Down Expand Up @@ -69,6 +68,7 @@ private void HandleSceneLoaded(in messages.SceneLoaded message)

if (dbRow.InitScript.Length > 0)
entity.Set(new components.ScriptExecution(dbRow.InitScript));
entity.Set<components.NPCType>(); // default is Biped
entity.Set(components.NPCState.Script);
entity.Set(components.NPCMovement.Default);
entity.Set<components.NPCIdle>();
Expand Down Expand Up @@ -99,7 +99,6 @@ private void HandleSceneLoaded(in messages.SceneLoaded message)
if (npcType != components.NPCType.Flying && entity.Has<Sphere>())
World.Publish(new messages.CreaturePlaceToGround(entity));

// TODO: Add SelectableNPC for Biped, Item, Flying
// TODO: Add Pixie behaviour
// TODO: Add PlantBlocker behaviour
// TODO: Add Biped behaviour (HeadIK, open doors)
Expand Down
6 changes: 3 additions & 3 deletions zzre/game/systems/npc/NPCActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ private static bool IsActivatableNPC(in components.NPCType type) =>
[Update]
private void Update(
in DefaultEcs.Entity entity,
Location npcLocation,
ref components.NonFairyAnimation animation)
Location npcLocation)
{
var currentDistanceSqr = playerLocation.DistanceSquared(npcLocation);
var isInComfortZone = currentDistanceSqr < NPCComfortZoneSqr;
animation.CanUseAlternativeIdles = !isInComfortZone;
if (entity.Has<components.NonFairyAnimation>())
entity.Get<components.NonFairyAnimation>().CanUseAlternativeIdles = !isInComfortZone;

DefaultEcs.Entity? otherNPC = null;
float otherDistanceSqr = float.PositiveInfinity;
Expand Down
29 changes: 22 additions & 7 deletions zzre/game/systems/npc/NPCScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ private void Update(in DefaultEcs.Entity entity, ref components.ScriptExecution

private void SetModel(DefaultEcs.Entity entity, string name)
{
if (entity.Has<components.ActorParts>()) // not required by Zanzarah and a bit too much effort
// not required by Zanzarah and a bit too much effort to clean up old actor parts&resources
if (entity.Has<components.ActorParts>())
throw new InvalidOperationException("NPC already has a model");
entity.Set(ManagedResource<zzio.ActorExDescription>.Create(name));

Expand Down Expand Up @@ -97,7 +98,17 @@ private void Wizform(DefaultEcs.Entity entity, int atIndex, int fairyId, int lev
inventory.AddXP(invFairy, xp);
inventory.SetSlot(invFairy, atIndex);

// TODO: Handle wizform command for fairy NPCs
if (entity.Get<components.NPCType>() == components.NPCType.Flying)
{
entity.Remove<Inventory>();
entity.Set(inventory); // this forces the added fairy to spawn
var fairyEntity = entity.Get<components.SpawnedFairy>().Entity;
fairyEntity.Set(components.FairyHoverState.KeepLastHover);

// this command acts as SetModel for flying NPC types
entity.Set(new Sphere(Vector3.Zero, FlyingColliderSize));
entity.Set<components.PuppetActorMovement>();
}
}

private void Spell(DefaultEcs.Entity entity, int fairyI, int slotI, int spellId)
Expand All @@ -124,11 +135,15 @@ private void LookAtPlayer(DefaultEcs.Entity entity, int intDuration, components.
entity.Set(new components.NPCLookAtPlayer(mode, actualDuration));
entity.Set(components.NPCState.LookAtPlayer);

ref var anim = ref entity.Get<components.NonFairyAnimation>();
if (anim.Next != zzio.AnimationType.Idle0 &&
anim.Next != zzio.AnimationType.Idle1 &&
anim.Next != zzio.AnimationType.Idle2)
anim.Next = zzio.AnimationType.Idle0;
if (entity.Has<components.NonFairyAnimation>())
{
ref var anim = ref entity.Get<components.NonFairyAnimation>();
if (anim.Next != zzio.AnimationType.Idle0 &&
anim.Next != zzio.AnimationType.Idle1 &&
anim.Next != zzio.AnimationType.Idle2)
anim.Next = zzio.AnimationType.Idle0;
}
// the alternative (e.g. fairy animation just does not need this Idle0 switch)
}

private void RemoveNPC(DefaultEcs.Entity entity)
Expand Down
Loading

0 comments on commit b621e30

Please sign in to comment.