ported from perforce

This commit is contained in:
2026-04-19 00:43:27 +02:00
commit 6c0c33f5d4
700 changed files with 19735 additions and 0 deletions

View File

@@ -0,0 +1,768 @@
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;
}