Files
zfxaction25/DonkeysAndDroids.Godot/HexBoard3D.cs
2026-04-19 00:43:27 +02:00

768 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Godot;
using Godot.Collections;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
public partial class HexBoard3D : Node3D
{
public override void _Ready()
{
if (m_Board == null)
{
var random = new SRandom(241);
var board = Board.Generate(ref random, EDifficulty.Easy);
Configure(board);
}
}
public void Configure(Cell cell)
{
m_CellByHex[cell.Hex] = cell;
RebuildCellVisual(cell);
}
public void ModifyCell(Cell cell, EModifierId modifier, Tween tween)
{
m_CellByHex[cell.Hex] = cell;
tween.TweenCallback(Callable.From(() => RebuildCellVisual(cell)));
tween.Parallel();
SpawnCellUpdateVfx(cell, tween);
tween.Parallel().TweenCallback(Callable.From(() => Main.Instance.Music.Play(modifier)));
tween.TweenInterval(TotalAnimationDuration);
}
public void ModifyPoi(Cell cell, Poi poi, Tween tween)
{
m_CellByHex[cell.Hex] = cell;
tween.TweenCallback(Callable.From(() => RebuildCellVisual(cell)));
tween.Parallel();
if (poi != null)
{
tween.Parallel().TweenCallback(Callable.From(() => { Main.Instance.Music.Play(poi); }));
tween.TweenInterval(poi is Donkey ? 2.0 : TotalAnimationDuration);
}
SpawnCellUpdateVfx(cell, tween);
}
public void Configure(Board board)
{
var boardChanged = !ReferenceEquals(m_Board, board);
m_Board = board;
if (m_Board == null)
{
ClearBoard();
return;
}
if (boardChanged)
BuildBoard();
}
private void BuildBoard()
{
ClearBoard();
if (m_Board == null || m_Board.Cells.IsDefaultOrEmpty)
{
BoardBounds = new();
return;
}
m_CellByHex.Clear();
m_CellNodes.Clear();
var minX = float.MaxValue;
var maxX = float.MinValue;
var minZ = float.MaxValue;
var maxZ = float.MinValue;
var hasAnyCell = false;
foreach (var cell in m_Board.Cells)
{
m_CellByHex[cell.Hex] = cell;
var world = cell.Hex.ToWorld();
BuildCellNode(cell);
var x = world.X;
var z = world.Y;
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
if (z < minZ)
minZ = z;
if (z > maxZ)
maxZ = z;
hasAnyCell = true;
}
if (hasAnyCell)
BoardBounds = new(new(minX, minZ), new(maxX - minX, maxZ - minZ));
else
BoardBounds = new();
CreateHighlightMesh();
CreateSelectedMesh();
}
private void ClearBoard()
{
foreach (var child in GetChildren())
child.QueueFree();
m_PoiSprites.Clear();
m_AvatarSprites.Clear();
m_DonkeySprites.Clear();
m_CellByHex.Clear();
m_CellNodes.Clear();
m_HighlightInstance = null;
m_SelectedInstance = null;
HighlightedCell = null;
SelectedCell = null;
}
public override void _Process(double delta)
{
var camera = GetViewport()?.GetCamera3D();
if (camera == null)
return;
var camPos = camera.GlobalPosition;
foreach (var sprite in m_PoiSprites)
{
if (!IsInstanceValid(sprite))
continue;
var pos = sprite.GlobalPosition;
var target = new Vector3(camPos.X, pos.Y, camPos.Z);
sprite.LookAtFromPosition(sprite.GlobalPosition, target, Vector3.Up, true);
}
if (!m_UseMouseHighlight)
UpdateHighlight(camera);
}
public override void _UnhandledInput(InputEvent @event)
{
if (Input.IsActionJustPressed("cell_select") && HighlightedCell != null)
SelectCell(HighlightedCell);
switch (@event)
{
case InputEventMouseMotion:
case InputEventMouseButton:
{
m_UseMouseHighlight = true;
var camera = GetViewport()?.GetCamera3D();
if (camera != null)
UpdateHighlight(camera);
break;
}
case InputEventJoypadMotion:
case InputEventJoypadButton:
{
m_UseMouseHighlight = false;
break;
}
}
}
private void CreateHighlightMesh()
{
if (m_HighlightInstance != null)
return;
m_HighlightInstance = new()
{
Mesh = m_HexMesh,
Name = "HighlightHex",
Visible = false
};
if (HighlightMaterial != null)
m_HighlightInstance.MaterialOverride = HighlightMaterial;
else
{
var mat = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
AlbedoColor = new(1f, 1f, 1f, 0.4f),
NoDepthTest = true
};
m_HighlightInstance.MaterialOverride = mat;
}
AddChild(m_HighlightInstance);
}
[MemberNotNull(nameof(m_SelectedInstance))]
private void CreateSelectedMesh()
{
if (m_SelectedInstance != null)
return;
m_SelectedInstance = new()
{
Mesh = m_HexMesh,
Name = "SelectedHex",
Visible = false
};
if (SelectedMaterial != null)
m_SelectedInstance.MaterialOverride = SelectedMaterial;
else
{
var mat = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
AlbedoColor = new(1f, 1f, 1f, 0.6f),
NoDepthTest = true
};
m_SelectedInstance.MaterialOverride = mat;
}
AddChild(m_SelectedInstance);
}
private void UpdateHighlight(Camera3D camera)
{
if (m_Board == null || m_Board.Cells.IsDefaultOrEmpty)
return;
var viewport = GetViewport();
if (viewport == null)
return;
Vector2 screenPos;
if (m_UseMouseHighlight)
screenPos = viewport.GetMousePosition();
else
{
var rect = viewport.GetVisibleRect();
screenPos = rect.Size * 0.5f;
}
var origin = camera.ProjectRayOrigin(screenPos);
var dir = camera.ProjectRayNormal(screenPos);
if (Mathf.Abs(dir.Y) < 0.0001f)
{
HideHighlight();
return;
}
var t = -origin.Y / dir.Y;
if (t < 0)
{
HideHighlight();
return;
}
var hit = origin + dir * t;
var hex = Hex.FromWorld(new(hit.X, hit.Z));
if (!m_CellByHex.TryGetValue(hex, out var cell))
{
HideHighlight();
return;
}
if (HighlightedCell != null && HighlightedCell.Hex.Equals(hex))
return;
HighlightedCell = cell;
if (m_HighlightInstance != null)
{
var world = cell.Hex.ToWorld();
var pos = new Vector3(world.X, HighlightHeight, world.Y);
m_HighlightInstance.Transform = new(Basis.Identity, pos);
m_HighlightInstance.Visible = true;
}
}
private void SelectCell(Cell cell)
{
if (cell == null)
return;
SelectedCell = cell;
if (m_SelectedInstance == null)
CreateSelectedMesh();
var world = cell.Hex.ToWorld();
var pos = new Vector3(world.X, SelectedHeight, world.Y);
m_SelectedInstance.Transform = new(Basis.Identity, pos);
m_SelectedInstance.Visible = true;
Tooltip.Instance?.Describe(cell);
}
private void HideHighlight()
{
HighlightedCell = null;
if (m_HighlightInstance != null)
m_HighlightInstance.Visible = false;
}
private void BuildCellNode(Cell cell)
{
var hex = cell.Hex;
var cellNode = new Node3D { Name = $"Cell_{hex}" };
cellNode.Transform = BuildCellTransform(cell);
var meshInstance = new MeshInstance3D
{
Mesh = m_HexMesh,
Name = "HexMesh"
};
var mat = GetMaterialForType(cell.Type);
if (mat != null)
meshInstance.MaterialOverride = mat;
cellNode.AddChild(meshInstance);
if (DebuffScene != null)
{
foreach (var modifier in cell.Modifiers)
{
if (modifier is not UnreliableModifierBase && modifier is not CorruptModifierBase)
continue;
if (modifier.DebuffSources.Count > 0)
continue;
var area = DebuffScene.Instantiate<CorruptionArea>();
area.Color = modifier is UnreliableModifierBase ? InterferenceColor : CorruptColor;
area.Position = new(0f, 0.1f, 0f);
cellNode.AddChild(area);
}
}
if (cell.Poi is not null)
{
var (tex, pivot) = GetTextureAndPivotForPoi(cell.Poi);
if (tex != null)
{
var sprite = new Sprite3D
{
Texture = tex,
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled,
Centered = true,
NoDepthTest = true,
PixelSize = 0.01f,
Name = $"{cell.Poi.GetType().Name}_Sprite"
};
ApplyPivot(sprite, pivot);
sprite.Position = new(0f, PoiHeight, 0f);
cellNode.AddChild(sprite);
m_PoiSprites.Add(sprite);
if (cell.Poi is Avatar avatar)
m_AvatarSprites[avatar] = sprite;
if (cell.Poi is Donkey donkey)
m_DonkeySprites[donkey] = sprite;
}
var infoNode = BuildPoiInfoNode(cell);
if (infoNode != null)
{
infoNode.Position = new(0f, PoiInfoHeight, 0f);
cellNode.AddChild(infoNode);
}
}
AddChild(cellNode);
m_CellNodes[hex] = cellNode;
}
private void RebuildCellVisual(Cell cell)
{
if (m_CellNodes.TryGetValue(cell.Hex, out var oldNode) && IsInstanceValid(oldNode))
oldNode.QueueFree();
BuildCellNode(cell);
}
private Transform3D BuildCellTransform(Cell cell)
{
var world = cell.Hex.ToWorld();
var position = new Vector3(world.X, 0f, world.Y);
var rotation = Basis.Rotated(new(0, 1, 0), MathF.PI * (60 * m_CameraRotationIndex) / 180f);
return new(rotation, position);
}
private void UpdateCellTransforms()
{
if (m_Board == null || m_Board.Cells.IsDefaultOrEmpty)
return;
foreach (var cell in m_Board.Cells)
{
if (!m_CellNodes.TryGetValue(cell.Hex, out var node) || !IsInstanceValid(node))
continue;
node.Transform = BuildCellTransform(cell);
}
}
private Material GetMaterialForType(ECellType type)
{
if (CellTypeMaterials != null && CellTypeMaterials.Count > (int)type && CellTypeMaterials[(int)type] != null)
return CellTypeMaterials[(int)type];
var mat = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.PerPixel,
};
return mat;
}
private (Texture2D texture, Vector2 pivot) GetTextureAndPivotForPoi(Poi poi)
{
return poi switch
{
Crate => (CrateTexture, CratePivot),
Shed => (ShedTexture, ShedPivot),
Tower => (TowerTexture, TowerPivot),
Donkey d => (GetDonkeyTexture(d.Direction), DonkeyPivot),
Avatar a => (GetAvatarTexture(a.Direction), AvatarPivot),
_ => (null, default)
};
}
private Node3D BuildPoiInfoNode(Cell cell)
{
if (cell.Poi is null)
return null;
switch (cell.Poi)
{
case Crate crate:
{
if (crate.Amount > 0)
{
var label = new Label3D
{
Text = crate.Amount.ToString(),
FontSize = 72,
Name = "Crate_Amount",
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled
};
return label;
}
var check = CreateCheckmarkSprite();
if (check != null)
check.Name = "Crate_Checkmark";
return check;
}
case Shed shed:
{
var remaining = shed.Remaining;
if (remaining > 0)
{
var label = new Label3D
{
Text = $"-{remaining}",
FontSize = 72,
Name = "Shed_Remaining",
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled
};
return label;
}
var check = CreateCheckmarkSprite();
if (check != null)
check.Name = "Shed_Checkmark";
return check;
}
}
return null;
}
private Sprite3D CreateCheckmarkSprite()
{
if (CheckmarkTexture == null)
return null;
var sprite = new Sprite3D
{
Texture = CheckmarkTexture,
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled,
Centered = true,
NoDepthTest = true,
PixelSize = 0.01f,
Name = "CheckmarkSprite"
};
return sprite;
}
private void ApplyPivot(Sprite3D sprite, Vector2 pivotNormalized)
{
if (sprite.Texture is not { } tex)
return;
var sizeI = tex.GetSize();
var size = new Vector2(sizeI.X, sizeI.Y);
var pivotFromCenter = pivotNormalized - new Vector2(0.5f, 0.5f);
var offset = pivotFromCenter * size;
sprite.Offset = offset;
}
public void SetCameraRotationIndex(int rotationIndex)
{
rotationIndex = Mathf.PosMod(rotationIndex, 6);
if (m_CameraRotationIndex == rotationIndex)
return;
m_CameraRotationIndex = rotationIndex;
UpdateCellTransforms();
foreach (var kvp in m_AvatarSprites)
{
var avatar = kvp.Key;
var sprite = kvp.Value;
if (!IsInstanceValid(sprite))
continue;
sprite.Texture = GetAvatarTexture(avatar.Direction);
ApplyPivot(sprite, AvatarPivot);
}
foreach (var kvp in m_DonkeySprites)
{
var donkey = kvp.Key;
var sprite = kvp.Value;
if (!IsInstanceValid(sprite))
continue;
sprite.Texture = GetDonkeyTexture(donkey.Direction);
ApplyPivot(sprite, DonkeyPivot);
}
}
private Texture2D GetAvatarTexture(EDirection worldDirection)
{
var dirIndex = (int)worldDirection;
var relative = Mathf.PosMod(dirIndex - m_CameraRotationIndex, 6);
return (EDirection)relative switch
{
EDirection.Right => AvatarRightTexture,
EDirection.TopRight => AvatarTopRightTexture,
EDirection.TopLeft => AvatarTopLeftTexture,
EDirection.Left => AvatarLeftTexture,
EDirection.BottomLeft => AvatarBottomLeftTexture,
EDirection.BottomRight => AvatarBottomRightTexture,
_ => throw new UnreachableException()
};
}
private Texture2D GetDonkeyTexture(EDirection worldDirection)
{
var dirIndex = (int)worldDirection;
var relative = Mathf.PosMod(dirIndex - m_CameraRotationIndex, 6);
return (EDirection)relative switch
{
EDirection.Right => DonkeyRightTexture,
EDirection.TopRight => DonkeyTopRightTexture,
EDirection.TopLeft => DonkeyTopLeftTexture,
EDirection.Left => DonkeyLeftTexture,
EDirection.BottomLeft => DonkeyBottomLeftTexture,
EDirection.BottomRight => DonkeyBottomRightTexture,
_ => throw new UnreachableException()
};
}
private void SpawnCellUpdateVfx(Cell cell, Tween tween)
{
var mesh1 = new MeshInstance3D() { Mesh = m_HexMesh, Visible = false };
var mesh2 = new MeshInstance3D() { Mesh = m_HexMesh, Visible = false };
var mat1 = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
AlbedoColor = new(1f, 1f, 1f, 0.4f),
NoDepthTest = true
};
var mat2 = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
AlbedoColor = new(1f, 1f, 1f, 0.4f),
NoDepthTest = true
};
mesh1.MaterialOverride = mat1;
mesh2.MaterialOverride = mat2;
var world = cell.Hex.ToWorld();
var position = new Vector3(world.X, 0.1f, world.Y);
mesh1.Position = position;
mesh2.Position = position;
tween.TweenCallback(Callable.From(() =>
{
AddChild(mesh1);
AddChild(mesh2);
}));
tween.TweenProperty(mesh1, "scale", new Vector3(2, 1, 2), TotalAnimationDuration);
tween.Parallel().TweenProperty(mesh2, "position", position + new Vector3(0, 1, 0), TotalAnimationDuration);
tween.Parallel().TweenProperty(mat1, "albedo_color", new Color(1, 1, 1, 0.0f), TotalAnimationDuration);
tween.Parallel().TweenProperty(mat2, "albedo_color", new Color(1, 1, 1, 0.0f), TotalAnimationDuration);
tween.TweenCallback(Callable.From(() =>
{
mesh1.QueueFree();
mesh2.QueueFree();
}));
}
[ExportGroup("POI Pivots (normalized, 01)")]
[Export]
public Vector2 CratePivot { get; set; } = new(0.5f, 1.0f);
[Export]
public Vector2 ShedPivot { get; set; } = new(0.5f, 1.0f);
[Export]
public Vector2 TowerPivot { get; set; } = new(0.5f, 1.0f);
[Export]
public Vector2 DonkeyPivot { get; set; } = new(0.5f, 1.0f);
[Export]
public Vector2 AvatarPivot { get; set; } = new(0.5f, 1.0f);
[Export]
public Array<Material> CellTypeMaterials { get; set; } = new();
[ExportGroup("POI Textures")]
[Export]
public Texture2D CrateTexture { get; set; }
[Export]
public Texture2D ShedTexture { get; set; }
[Export]
public Texture2D TowerTexture { get; set; }
[Export]
public Texture2D AvatarRightTexture { get; set; }
[Export]
public Texture2D AvatarTopRightTexture { get; set; }
[Export]
public Texture2D AvatarTopLeftTexture { get; set; }
[Export]
public Texture2D AvatarLeftTexture { get; set; }
[Export]
public Texture2D AvatarBottomLeftTexture { get; set; }
[Export]
public Texture2D AvatarBottomRightTexture { get; set; }
[Export]
public Texture2D DonkeyRightTexture { get; set; }
[Export]
public Texture2D DonkeyTopRightTexture { get; set; }
[Export]
public Texture2D DonkeyTopLeftTexture { get; set; }
[Export]
public Texture2D DonkeyLeftTexture { get; set; }
[Export]
public Texture2D DonkeyBottomLeftTexture { get; set; }
[Export]
public Texture2D DonkeyBottomRightTexture { get; set; }
[Export]
public Texture2D CheckmarkTexture { get; set; }
[ExportGroup("Cell effects")]
[Export]
public PackedScene DebuffScene { get; set; }
[Export]
public Color CorruptColor { get; set; } = new(0.6f, 0.1f, 0.8f, 0.45f);
[Export]
public Color InterferenceColor { get; set; } = new(0.1f, 0.8f, 0.6f, 0.45f);
[ExportGroup("Highlight")]
[Export]
public Material HighlightMaterial { get; set; }
[Export]
public float HighlightHeight { get; set; } = 0.02f;
[Export]
public Material SelectedMaterial { get; set; }
[Export]
public float SelectedHeight { get; set; } = 0.03f;
public float TotalAnimationDuration => 0.375f / SettingsManager.Instance.GameSpeed;
public Cell HighlightedCell { get; private set; }
public Cell SelectedCell { get; private set; }
public Rect2 BoardBounds { get; private set; }
private const float PoiHeight = 0.15f;
private const float PoiInfoHeight = 0.25f;
private readonly System.Collections.Generic.Dictionary<Avatar, Sprite3D> m_AvatarSprites = new();
private readonly System.Collections.Generic.Dictionary<Hex, Cell> m_CellByHex = new();
private readonly System.Collections.Generic.Dictionary<Hex, Node3D> m_CellNodes = new();
private readonly System.Collections.Generic.Dictionary<Donkey, Sprite3D> m_DonkeySprites = new();
private readonly ArrayMesh m_HexMesh = HexMeshBuilder.CreateFlatHexMesh();
private readonly List<Sprite3D> m_PoiSprites = new();
private Board m_Board;
private int m_CameraRotationIndex;
private MeshInstance3D m_HighlightInstance;
private MeshInstance3D m_SelectedInstance;
private bool m_UseMouseHighlight = true;
}