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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
bin
obj
.vs

35
AGENTS.MD Normal file
View File

@@ -0,0 +1,35 @@
# AGENTS
## Context
- Game genre: roguelite strategy deckbuilder.
- Game fantasy: program a robot courier on a hex farmland delivering crate goods to sheds while glitches mutate instructions and patches improve the deck/program.
- Use this file to keep new work aligned with the existing design/implementation; see code under RobotAndDonkey.Game and tests under RobotAndDonkey.Tests.
## Design document
- Detailed system design exists in the design.md file.
- UX design for a card row exists in the cardrow.md file.
## Tech Design
- Deterministic core: GameState.GameState.CreateNew seeds Utils.SRandom; CoreLoop owns Board.Generate, glitch deck, robot, decks, and metrics (energy wasted, overspill, path length, visits). Balancing tunables live in Data/Balancing.cs.
- Deterministic finite automaton: CoreLoop.RunPhase (ERunPhase) feeds Requests (Execution/Requests/*) and Commands (Execution/Commands/*) via GameRuntime; EnterPreview seeds the first tape/hand; loop is DrawGlitch -> Improve -> (Gamble/BufferOverflow) -> ExecuteProgram/Scoring -> back to DrawGlitch.
- Entities/modifiers: cards/robots/cells derive from Modifiers.Entity holding modifiers. Modifier (Kind/Id/Duration) exposes OnAdded/Before/After/OnRemoved; DebuffSources disables effects. ModifierStack pushes robot -> cell -> card layers before each card; immunity/debuff rules are honored when executing intents.
- Intent pipeline: cards override Card.CreateIntents to enqueue intents (Intents/*). ModifierStack.Execute runs modifier Before hooks, validates Intent.IsValid (energy + debuffs), runs intents (mutate board/currency/add intents), then After hooks. Results records mirror deltas for UI sync.
- Board model: Board.Board stores ImmutableArray<Cell> and TargetDeliveryAmount; Board.Generate builds axial hex maps sized by difficulty, sprinkling terrain/POIs with Balancing probabilities while preserving connectivity and POI reachability. Towers seed permanent Unreliable to neighbors; corrupt cells added by spread. FindCellIndex is currently O(n).
- Robots: Robots.* records define starting deck/program count; base Robot exposes Currency (energy/max carry/preview/tape length/hand size) derived from difficulty. Derived robots add modifiers (e.g., AnalystModifier) and deck tweaks.
- Cards/modifiers: Cards/Patches and Cards/Glitches are records; PatchCard/GlitchCard derive from Card with tooltip/modifier metadata. ModifyCardBase handles temporary modifier application; OccupiedSpace respects Efficient. Corruption-aware indexing uses Utils.CardExtensions.NextIndexConsideringCorruption.
- Frontend: HexBoard3D builds a 3D board with HexMeshBuilder multimeshes per cell type, billboarded POI sprites cached in dictionaries, highlight/selection meshes, camera-facing pivoted textures, optional debuff VFX from cell modifiers. Exported textures/materials/pivots enable art hookup.
## Code Style & Patterns
- Indentation is done with 4 spaces per level.
- C# records and expression-bodied members; prefer immutability (ImmutableArray, init props) and explicit DeepClone including modifiers/debuff state.
- No modifier duplicates; always use AddModifier/InsertModifier/RemoveModifier so hooks fire. Respect DebuffSources when checking if effects apply.
- Intents/results are the only way to mutate state for UI; emit CurrencyResult/HandResult/TapeResult/PoiResult/etc. with a cloned state whenever state changes.
- Thread deterministic RNG through CoreLoop.Random (ref SRandom) instead of creating new Random instances; avoid hidden randomness.
- Balancing.Instance is the source of truth for costs/probabilities; avoid magic numbers in cards/intents/board generation.
- Tests live in RobotAndDonkey.Tests with builders (BoardBuilder, CoreLoopBuilder, TestRuntime, CardTestBase) and card-specific suites; mirror patterns for new content to keep coverage and determinism.
- Hex math uses axial coords (Utils.Hex) and EDirection; derive relative directions via helpers instead of manual math to respect corruption/orientation rules. Rendering caches meshes/sprites and small helper methods over monoliths.
- Variable names are full words and are not abbreviated (e.g. exception instead of ex, image instead of img). Structs have an S prefix, enums have an E prefix, fields have a m_ prefix, statics have a s_ prefix, members use UpperCamelCase, locals and arguments use lowerCamelCase.
- Inside of a class: statics appear first, then methods, then properties, and finally all other members.
- Always include method visibility modifiers, especially private.
- Don't use regions.
- Curly braces each belong in their own line.

View File

@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
DonkeysAndDroids.Godot/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
DonkeysAndDroids.Godot/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/

View File

@@ -0,0 +1,29 @@
using Godot;
public partial class Background : Control
{
public override void _Ready()
{
var rect = GetNode<ColorRect>("SwirlRect");
m_SwirlMaterial = rect.Material as ShaderMaterial;
m_Particles = GetNode<GpuParticles2D>("DustParticles");
m_ParticlesProcess = m_Particles.ProcessMaterial as ParticleProcessMaterial;
}
public override void _Process(double delta)
{
m_Particles.Position = Size / 2;
((ParticleProcessMaterial)m_Particles.ProcessMaterial).EmissionBoxExtents = new(Size.X / 2, Size.Y / 2, 1);
Refresh();
}
public void Refresh()
{
}
private GpuParticles2D m_Particles;
private ParticleProcessMaterial m_ParticlesProcess;
private ShaderMaterial m_SwirlMaterial;
}

View File

@@ -0,0 +1 @@
uid://bqii5k4sdtlnq

View File

@@ -0,0 +1,258 @@
using Godot;
public partial class BoardCameraController : Camera3D
{
public override void _Ready()
{
InitBoardBounds();
InitOrbit();
}
private void InitBoardBounds()
{
if (HexBoard != null && HexBoard.BoardBounds.Size != Vector2.Zero)
{
m_BoardRect = HexBoard.BoardBounds;
var center2D = m_BoardRect.Position + m_BoardRect.Size * 0.5f;
m_TargetPosition = new(center2D.X - 1, 0f, center2D.Y + 3);
}
else
{
m_BoardRect = new(-10, -10, 20, 20);
m_TargetPosition = new(-1, 0f, 3);
}
var radius = Mathf.Max(m_BoardRect.Size.X, m_BoardRect.Size.Y) * 0.5f;
m_MinDistance = MinZoomDistance;
m_MaxDistance = Mathf.Max(m_MinDistance * 2f, radius * MaxZoomOutFactor);
}
private void InitOrbit()
{
var startYawRad = GlobalTransform.Basis.GetEuler().Y;
var startYawDeg = Mathf.RadToDeg(startYawRad);
m_RotationIndex = Mathf.PosMod(Mathf.RoundToInt(startYawDeg / 60f), 6);
m_YawDegrees = m_RotationIndex * 60f;
m_Distance = (m_MaxDistance - m_MinDistance) * 0.75f + m_MinDistance;
UpdateTransformFromOrbit();
if (HexBoard != null)
HexBoard.SetCameraRotationIndex(m_RotationIndex);
}
private void UpdateTransformFromOrbit()
{
var pitchRad = Mathf.DegToRad(PitchDegrees);
var yawRad = Mathf.DegToRad(m_YawDegrees);
var horizontal = m_Distance * Mathf.Cos(pitchRad);
var height = m_Distance * Mathf.Sin(pitchRad);
var offset = new Vector3(Mathf.Sin(yawRad) * horizontal, height, Mathf.Cos(yawRad) * horizontal);
GlobalPosition = m_TargetPosition + offset;
LookAt(m_TargetPosition, Vector3.Up);
}
public override void _Process(double delta)
{
var dt = (float)delta;
HandleRotationInput(dt);
HandleMovementInput(dt);
HandleZoomInput(dt);
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseButton mb && mb.Pressed)
{
if (mb.ButtonIndex == MouseButton.WheelUp)
m_PendingWheelSteps -= 1f;
else if (mb.ButtonIndex == MouseButton.WheelDown)
m_PendingWheelSteps += 1f;
}
}
private void HandleMovementInput(float delta)
{
var input = Vector2.Zero;
input.Y -= Input.GetActionStrength("camera_move_forward");
input.Y += Input.GetActionStrength("camera_move_back");
input.X -= Input.GetActionStrength("camera_move_left");
input.X += Input.GetActionStrength("camera_move_right");
if (input.LengthSquared() < 0.0001f)
return;
var yawRad = Mathf.DegToRad(m_YawDegrees);
var forward = new Vector2(Mathf.Sin(yawRad), Mathf.Cos(yawRad));
var right = new Vector2(forward.Y, -forward.X);
var worldDir = right * input.X + forward * input.Y;
var deltaPos = worldDir * MoveSpeed * delta;
m_TargetPosition.X += deltaPos.X;
m_TargetPosition.Z += deltaPos.Y;
ClampTargetToBoard();
UpdateTransformFromOrbit();
}
private void HandleZoomInput(float delta)
{
var zoomDelta = 0f;
var axis = Input.GetActionStrength("camera_zoom_out") - Input.GetActionStrength("camera_zoom_in");
if (Mathf.Abs(axis) > 0.001f)
zoomDelta += axis * PadZoomSpeed * delta;
if (Mathf.Abs(m_PendingWheelSteps) > 0.001f)
{
zoomDelta += m_PendingWheelSteps * WheelZoomStep;
m_PendingWheelSteps = 0f;
}
if (Mathf.Abs(zoomDelta) < 0.0001f)
return;
m_Distance = Mathf.Clamp(m_Distance + zoomDelta, m_MinDistance, m_MaxDistance);
UpdateTransformFromOrbit();
}
private void HandleRotationInput(float delta)
{
if (Input.IsActionJustPressed("camera_rotate_left"))
QueueRotate(-1);
if (Input.IsActionJustPressed("camera_rotate_right"))
QueueRotate(+1);
var axis = Input.GetActionStrength("camera_rotate_right") - Input.GetActionStrength("camera_rotate_left");
var axisDir = 0;
if (axis > GamepadDeadZone)
axisDir = +1;
else if (axis < -GamepadDeadZone)
axisDir = -1;
if (axisDir != 0)
{
if (m_RotateAxisDir != axisDir)
{
m_RotateAxisDir = axisDir;
m_RotateAxisTimer = 0f;
QueueRotate(axisDir);
}
else
{
m_RotateAxisTimer += delta;
if (m_RotateAxisTimer >= GamepadRotateRepeatDelay)
{
m_RotateAxisTimer = 0f;
QueueRotate(axisDir);
}
}
}
else
{
m_RotateAxisDir = 0;
m_RotateAxisTimer = 0f;
}
}
private void QueueRotate(int steps)
{
if (steps == 0 || m_RotationTween != null)
return;
m_RotationIndex = Mathf.PosMod(m_RotationIndex + steps, 6);
var targetYaw = m_RotationIndex * 60f;
var current = m_YawDegrees;
var deltaYaw = Mathf.Wrap(targetYaw - current, -180f, 180f);
targetYaw = current + deltaYaw;
HexBoard?.SetCameraRotationIndex(m_RotationIndex);
m_RotationTween = CreateTween();
m_RotationTween.SetTrans(Tween.TransitionType.Back);
m_RotationTween.SetEase(Tween.EaseType.Out);
m_RotationTween.TweenProperty(this, nameof(YawDegrees), targetYaw, RotationDuration);
m_RotationTween.TweenCallback(Callable.From(() =>
{
YawDegrees = m_RotationIndex * 60f;
m_RotationTween = null;
}));
}
private void ClampTargetToBoard()
{
if (m_BoardRect.Size == Vector2.Zero)
return;
var minX = m_BoardRect.Position.X;
var maxX = m_BoardRect.Position.X + m_BoardRect.Size.X;
var minZ = m_BoardRect.Position.Y;
var maxZ = m_BoardRect.Position.Y + m_BoardRect.Size.Y;
m_TargetPosition.X = Mathf.Clamp(m_TargetPosition.X, minX, maxX);
m_TargetPosition.Z = Mathf.Clamp(m_TargetPosition.Z, minZ, maxZ);
}
public float YawDegrees
{
get => m_YawDegrees;
set
{
m_YawDegrees = value;
UpdateTransformFromOrbit();
}
}
[Export]
public HexBoard3D HexBoard { get; set; }
[Export]
public float GamepadDeadZone = 0.4f;
[Export]
public float GamepadRotateRepeatDelay = 1.0f;
[Export]
public float MaxZoomOutFactor = 2.2f;
[ExportCategory("Zoom")]
[Export]
public float MinZoomDistance = 8f;
[ExportCategory("Movement")]
[Export]
public float MoveSpeed = 10f;
[Export]
public float PadZoomSpeed = 40f;
[ExportCategory("Rotation")]
[Export]
public float PitchDegrees = 60f;
[Export]
public float RotationDuration = 0.35f;
[Export]
public float WheelZoomStep = 3f;
private Rect2 m_BoardRect;
private float m_Distance;
private float m_MaxDistance;
private float m_MinDistance;
private float m_PendingWheelSteps;
private int m_RotateAxisDir;
private float m_RotateAxisTimer;
private int m_RotationIndex;
private Tween m_RotationTween;
private Vector3 m_TargetPosition = new(-1, 0f, 3);
private float m_YawDegrees;
}

View File

@@ -0,0 +1 @@
uid://cs6idwp164gov

View File

@@ -0,0 +1,50 @@
using Godot;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.Pois;
using RobotAndDonkey.Game.Utils;
using System;
public partial class BoardNode : Node3D
{
public override void _Ready()
{
m_HexBoard3D = GetNode<HexBoard3D>("HexBoard3D");
}
public override void _Process(double delta)
{
}
public void Configure(Board board)
{
m_HexBoard3D.Configure(board);
}
public void Configure(Result result, Tween tween)
{
switch (result)
{
case CellTypeResult cellResult:
{
var cell = cellResult.Board.Cells[cellResult.Board.FindCellIndex(cellResult.Hex)];
tween.TweenCallback(Callable.From(() => m_HexBoard3D.Configure(cell)));
break;
}
case ModifyCellResult cellResult:
{
var cell = cellResult.Board.Cells[cellResult.Board.FindCellIndex(cellResult.Hex)];
m_HexBoard3D.ModifyCell(cell, cellResult.Modifier, tween);
break;
}
case PoiResult cellResult:
{
var cell = cellResult.Board.Cells[cellResult.Board.FindCellIndex(cellResult.Hex)];
m_HexBoard3D.ModifyPoi(cell, cellResult.Poi, tween);
break;
}
}
}
private HexBoard3D m_HexBoard3D;
}

View File

@@ -0,0 +1 @@
uid://b5ehrdnu2ovli

View File

@@ -0,0 +1,91 @@
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Results;
public partial class BufferOverflow : Control, IScreen
{
public override void _Ready()
{
m_Hand = GetNode<CardRow>("%Hand");
m_Remove = GetNode<Button>("%Remove");
m_Remove.Pressed += OnRemovePressed;
m_Skip = GetNode<Button>("%Skip");
m_Skip.Pressed += OnSkipPressed;
m_Hand.Connect(CardRow.SignalName.SelectionChanged, new(this, nameof(OnSelectionChanged)));
}
private void OnSelectionChanged(CardControl[] selection)
{
if (selection.Length != 1)
{
m_DestroyCardCommand = null;
return;
}
m_DestroyCardCommand = new(Main.Instance.CurrentRequest.RequestId, m_Hand.OrderedCards.IndexOf(selection[0].Card));
}
private void OnRemovePressed()
{
Main.Instance.Execute(m_DestroyCardCommand);
}
private void OnSkipPressed()
{
Main.Instance.Execute(m_StopBufferOverflowCommand);
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_Remove.Disabled = false;
m_Skip.Disabled = false;
}
public void DisableInputs()
{
m_Remove.Disabled = true;
m_Skip.Disabled = true;
}
public void Activate()
{
}
public bool HandleResult(Result result, Tween tween)
{
switch (result)
{
case HandResult handResult:
{
m_Hand.Configure(handResult.Hand, tween, true, false);
return true;
}
}
return false;
}
public override void _Process(double delta)
{
m_Skip.Disabled = m_StopBufferOverflowCommand == null || !m_StopBufferOverflowCommand.IsValid(Main.Instance.CoreLoop, out _);
m_Remove.Disabled = m_DestroyCardCommand == null || !m_DestroyCardCommand.IsValid(Main.Instance.CoreLoop, out _);
}
public void Configure(Tween tween)
{
m_Hand.Configure(Main.Instance.CoreLoop.Hand, tween, false, false);
m_StopBufferOverflowCommand = new(Main.Instance.CurrentRequest.RequestId);
}
private DestroyCardCommand m_DestroyCardCommand;
private CardRow m_Hand;
private Button m_Remove;
private Button m_Skip;
private StopBufferOverflowCommand m_StopBufferOverflowCommand;
}

View File

@@ -0,0 +1 @@
uid://b05rg3st8wwtp

View File

@@ -0,0 +1,597 @@
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Cards.Patches;
using RobotAndDonkey.Game.Modifiers;
using System;
using System.Collections.Generic;
using System.Linq;
using RobotAndDonkey.Game;
public partial class CardControl : Control
{
public override void _Ready()
{
m_ArtTextureRect = GetNode<TextureRect>("%ArtTextureRect");
m_ModifiersContainer = GetNode<HBoxContainer>("%ModifiersContainer");
m_BorderPanel = GetNode<Panel>("%Frame");
m_Background = GetNode<Panel>("%Background");
m_Label = GetNode<Label>("%Label");
MouseEntered += OnMouseEntered;
MouseExited += OnMouseExited;
PivotOffset = Size / 2;
Scale = new(1, 1);
Modulate = new(1, 1, 1);
m_DesiredPosition = Position;
UpdateState();
SnapToDesired();
if (ModifierIconScene == null)
GD.PushWarning($"{nameof(CardControl)} on node '{Name}' has no ModifierIconScene assigned. No modifier icons will be shown.");
if (m_Card == null)
{
m_Card = new DetoxiumPrime();
m_Card.AddModifier(new UnreliableCardModifier(EModifierDuration.Permanent), []);
m_Card.AddModifier(new EffectiveModifier(EModifierDuration.Permanent), []);
m_Card.AddModifier(new RaceConditionModifier(EModifierDuration.Permanent), []);
m_Card.AddModifier(new OptimizedModifier(EModifierDuration.Permanent), []);
}
Refresh();
}
public override void _Process(double delta)
{
UpdateAnimatedState(delta);
}
private void OnMouseEntered()
{
if (m_Dragging || GetViewport().GuiIsDragging())
return;
if (!CanDrag && !CanSelect || Disabled)
return;
if (ShouldHoverSelect())
Selected = true;
m_Hover = true;
ZIndex = 1;
UpdateState();
}
private bool ShouldHoverSelect()
{
if (!CanSelect || Disabled || m_CardRow == null)
return false;
if (!m_CardRow.SupportsMultiSelect)
return false;
if (!Input.IsMouseButtonPressed(MouseButton.Left))
return false;
if (CardRow.IsCardPressActive)
return false;
return true;
}
private void OnMouseExited()
{
m_PressedLeftDown = false;
if (m_Dragging)
return;
if (!CanDrag && !CanSelect || Disabled)
return;
m_DropBefore = null;
m_Hover = false;
ZIndex = 0;
UpdateState();
}
private void UpdateState()
{
Vector2 newPivot = Size / 2;
Vector2 newScale = new Vector2(1, 1);
Color newModulate = new Color(1, 1, 1, 1);
if (Selected)
{
newPivot.Y = Size.Y;
newScale = new(1.25f, 1.25f);
}
if (m_Hover)
{
newScale = new(1.25f, 1.25f);
newModulate = new(1.25f, 1.25f, 1.25f);
}
if (m_DropBefore == false)
{
newPivot.X = Size.X;
newScale = new(1.25f, 1.25f);
newModulate.A = 0.5f;
}
else if (m_DropBefore == true)
{
newPivot.X = 0;
newScale = new(1.25f, 1.25f);
newModulate.A = 0.5f;
}
if (m_Dragging)
{
newModulate.A = 0.25f;
}
if (m_DesiredModulate == newModulate && m_DesiredScale == newScale && m_DesiredPivotOffset == newPivot)
return;
m_DesiredPivotOffset = newPivot;
m_DesiredScale = newScale;
m_DesiredModulate = newModulate;
EmitSignal(SignalName.CardStateChanged, this);
}
public void Configure(Card card, CardRow cardRow)
{
m_Card = card;
m_CardRow = cardRow;
Refresh();
UpdateState();
}
public void Refresh()
{
RefreshRarity();
RefreshArtTexture();
SetDescription(m_Card.Name, m_Card.ToolTip);
SetModifiers(m_Card.Modifiers);
}
private void RefreshArtTexture()
{
SetArtTexture(GD.Load<Texture2D>(m_Card.Id switch
{
ECard.Bitflip => "res://images/bitflip.png",
ECard.ShortCircuit => "res://images/short-circuit.png",
ECard.Slipstream => "res://images/slipstream.png",
ECard.Latency => "res://images/latency.png",
ECard.Rain => "res://images/rain.png",
ECard.Drought => "res://images/drought.png",
ECard.Pest => "res://images/pest.png",
ECard.Gravity => "res://images/gravity.png",
ECard.HeatWave => "res://images/heat-wave.png",
ECard.SolarFlare => "res://images/solar-flare.png",
ECard.LightningStorm => "res://images/lightning-storm.png",
ECard.MeteorStorm => "res://images/meteor-storm.png",
ECard.WindStorm => "res://images/wind-storm.png",
ECard.Move => "res://images/move.png",
ECard.TurnLeft => "res://images/turn-left.png",
ECard.TurnRight => "res://images/turn-right.png",
ECard.Interact => "res://images/interact.png",
ECard.NoOp => "res://images/no-op.png",
ECard.Potentiate => "res://images/potentiate.png",
ECard.Optimize => "res://images/optimize.png",
ECard.Streamline => "res://images/streamline.png",
ECard.Persist => "res://images/persist.png",
ECard.Remember => "res://images/remember.png",
ECard.Reason => "res://images/reason.png",
ECard.DetoxiumPrime => "res://images/detoxium-prime.png",
ECard.FlyingDisk => "res://images/flying-disk.png",
ECard.AluminumHat => "res://images/aluminum-hat.png",
ECard.EMField => "res://images/em-field.png",
ECard.AtomicClock => "res://images/atomic-clock.png",
ECard.Jump => "res://images/jump.png",
ECard.Repeat => "res://images/repeat.png",
ECard.Rest => "res://images/rest.png",
ECard.Stabilize => "res://images/stabilize.png",
_ => throw new ArgumentOutOfRangeException()
}));
}
private void RefreshRarity()
{
var color = m_Card.Rarity switch
{
ERarity.Common => new(1, 1, 1),
ERarity.Magic => new(0.3f, 1, 0.3f),
ERarity.Uncommon => new(0.4f, 0.6f, 1.5f),
ERarity.Rare => new(0.8f, 0.3f, 1),
ERarity.Legendary => new Color(1, 0.5f, 0.2f),
_ => throw new ArgumentOutOfRangeException()
};
SetRarity(color);
}
public void SetRarity(Color color)
{
if (m_BorderPanel == null || m_Background == null)
return;
m_BorderPanel.Modulate = color;
m_Background.Modulate = color;
}
public void SetArtTexture(Texture2D texture)
{
if (m_ArtTextureRect == null)
return;
m_ArtTextureRect.Texture = texture;
}
public void SetDescription(string displayName, string description)
{
if (m_Label != null)
m_Label.Text = displayName;
var header = string.IsNullOrEmpty(displayName) ? string.Empty : displayName.Trim();
var body = string.IsNullOrEmpty(description) ? string.Empty : description.Trim();
if (!string.IsNullOrEmpty(header) && !string.IsNullOrEmpty(body))
TooltipText = $"{header}\n{body}";
else if (!string.IsNullOrEmpty(header))
TooltipText = header;
else
TooltipText = body;
}
public void SetModifiers(IReadOnlyList<Modifier> modifiers)
{
if (m_ModifiersContainer == null)
return;
foreach (var child in m_ModifiersContainer.GetChildren())
{
child.QueueFree();
}
foreach (var modifier in modifiers)
{
if (modifier.DebuffSources.Count > 0)
continue;
var icon = ModifierIconScene.Instantiate<ModifierIcon>();
icon.Configure(modifier);
m_ModifiersContainer.AddChild(icon);
}
}
public void AnimateCard(Tween tween)
{
}
public void SetDesiredPosition(Vector2 desiredPosition)
{
m_DesiredPosition = desiredPosition;
}
public void SnapToDesired()
{
Position = m_DesiredPosition;
PivotOffset = m_DesiredPivotOffset;
Scale = m_DesiredScale;
Modulate = m_DesiredModulate;
}
private void UpdateAnimatedState(double delta)
{
var weight = GetBlendWeight(delta);
if (weight >= 1f)
{
SnapToDesired();
return;
}
Position = Position.Lerp(m_DesiredPosition, weight);
PivotOffset = PivotOffset.Lerp(m_DesiredPivotOffset, weight);
Scale = Scale.Lerp(m_DesiredScale, weight);
Modulate = Modulate.Lerp(m_DesiredModulate, weight);
}
private float GetBlendWeight(double delta)
{
var duration = GetAnimationDuration() * 0.05f;
if (duration <= 0f)
return 1f;
var weight = 1f - Mathf.Exp(-(float)delta / duration);
return Mathf.Clamp(weight, 0f, 1f);
}
private float GetAnimationDuration()
{
if (m_CardRow != null)
return m_CardRow.AnimationDuration;
return 0.15f;
}
public override void _GuiInput(InputEvent @event)
{
if (@event is not InputEventMouseButton mouseButton || mouseButton.ButtonIndex != MouseButton.Left)
return;
AcceptEvent();
if (Disabled)
return;
if (mouseButton.Pressed)
{
m_PressedLeftDown = true;
CardRow.NotifyCardPress(true);
}
else
{
if (m_PressedLeftDown && CanSelect)
Selected = !Selected;
m_PressedLeftDown = false;
CardRow.NotifyCardPress(false);
}
}
public override Variant _GetDragData(Vector2 atPosition)
{
if (!CanDrag || Disabled)
return default;
m_Dragging = true;
UpdateState();
UpdateDragPreviewForDropTarget(null);
return this;
}
public override void _Notification(int what)
{
switch ((long)what)
{
case NotificationDragBegin:
m_DropBefore = null;
UpdateState();
break;
case NotificationDragEnd:
m_Dragging = false;
m_Hover = false;
m_DropBefore = null;
m_DragPreviewRoot = null;
m_DragPreviewCards = null;
UpdateState();
break;
}
}
public override bool _CanDropData(Vector2 atPosition, Variant data)
{
if (!CanDrag || Disabled)
return false;
var dataObject = data.AsGodotObject();
if (dataObject is not CardControl sourceControl || sourceControl == this)
return false;
if (GetParent<CardRow>() is { } cardRow)
{
if (!cardRow._CanDropData(atPosition, data))
return false;
}
sourceControl.UpdateDragPreviewForDropTarget(this);
var size = Size.X / 2;
var before = atPosition.X < size;
if (before != m_DropBefore)
{
GD.Print($"Can drop before={before} {m_Card.Id}");
m_DropBefore = before;
UpdateState();
}
return true;
}
public void UpdateDragPreviewForDropTarget(CardControl targetControl)
{
var draggedCards = GetDraggedCardsForPreview();
if (ShouldForceSinglePreview(targetControl, draggedCards))
SetDragPreviewCards([this]);
else
SetDragPreviewCards(draggedCards);
}
private IReadOnlyList<CardControl> GetDraggedCardsForPreview()
{
if (m_CardRow == null || !m_CardRow.SupportsMultiDrag)
return [this];
var selectedCards = m_CardRow.SelectedCards;
if (selectedCards.Count <= 1)
return [this];
foreach (var selectedCard in selectedCards)
{
if (selectedCard.CardId == Card.CardId)
return selectedCards.Select(c => m_CardRow.CardToControl[c.CardId]).ToList();
}
return [this];
}
private bool ShouldForceSinglePreview(CardControl targetControl, IReadOnlyList<CardControl> draggedCards)
{
if (targetControl == null)
return false;
if (draggedCards.Count <= 1)
return false;
if (!targetControl.Selected)
return false;
foreach (var draggedCard in draggedCards)
{
if (draggedCard == targetControl)
return true;
}
return false;
}
private void SetDragPreviewCards(IReadOnlyList<CardControl> cards)
{
if (m_DragPreviewCards != null && m_DragPreviewCards.SequenceEqual(cards))
return;
m_DragPreviewCards = cards.ToList();
foreach (var card in m_CardRow.CardToControl.Values)
{
card.m_Dragging = false;
card.UpdateState();
}
if (m_DragPreviewRoot != null)
{
foreach (var child in m_DragPreviewRoot.GetChildren())
child.QueueFree();
}
if (cards.Count == 0)
return;
if (cards.Count == 1)
{
m_DragPreviewRoot?.QueueFree();
m_DragPreviewRoot = null;
var cardControl = cards[0];
var previewCard = (CardControl)cardControl.Duplicate();
previewCard.SetAnchorsPreset(LayoutPreset.TopLeft);
previewCard.Configure(cardControl.Card, null);
previewCard.Selected = cardControl.Selected;
previewCard.UpdateState();
previewCard.SetDesiredPosition(previewCard.Position);
previewCard.SnapToDesired();
SetDragPreview(previewCard);
return;
}
if (m_DragPreviewRoot == null)
{
m_DragPreviewRoot = new();
m_DragPreviewRoot.MouseFilter = MouseFilterEnum.Ignore;
}
var spacing = m_CardRow?.CardSpacing ?? 0f;
var offset = Mathf.Max(6f, spacing * 2f);
var offsetVector = new Vector2(offset, offset);
for (var index = 0; index < cards.Count; index++)
{
var cardControl = cards[index];
cardControl.m_Dragging = true;
cardControl.UpdateState();
var previewCard = (CardControl)cardControl.Duplicate();
previewCard.SetAnchorsPreset(LayoutPreset.TopLeft);
previewCard.Configure(cardControl.Card, null);
previewCard.Position = new(offsetVector.X * index, offsetVector.Y * index);
previewCard.Size = Size;
previewCard.UpdateState();
previewCard.SetDesiredPosition(previewCard.Position);
previewCard.SnapToDesired();
previewCard.ZIndex = index + 1;
m_DragPreviewRoot.AddChild(previewCard);
}
m_DragPreviewRoot.Size = Size + offsetVector * Math.Max(0, cards.Count - 1);
SetDragPreview(m_DragPreviewRoot);
}
public override void _DropData(Vector2 atPosition, Variant data)
{
if (!CanDrag)
return;
var obj = data.AsGodotObject();
if (obj is CardControl source)
{
var size = Size.X / 2;
var before = atPosition.X < size;
EmitSignal(SignalName.CardDroppedOn, source, this, before);
}
}
[Export]
public bool CanDrag { get; set; }
[Export]
public bool CanSelect { get; set; }
[Export]
public bool Disabled { get; set; }
[Export]
public bool Selected
{
get => m_Selected;
set
{
if (m_Selected == value)
return;
m_Selected = value;
EmitSignal(SignalName.CardSelectionChanged, this, m_Selected);
UpdateState();
if (m_Selected && m_CardRow != null)
{
Tooltip.Instance.Describe(Card);
CardRow.SelectionContext = m_CardRow;
}
}
}
public CardRow CardRow => m_CardRow;
public Card Card => m_Card;
public Vector2 DesiredScale => m_DesiredScale;
public Vector2 DesiredPivotOffset => m_DesiredPivotOffset;
[Signal]
public delegate void CardDroppedOnEventHandler(CardControl source, CardControl target, bool before);
[Signal]
public delegate void CardSelectionChangedEventHandler(CardControl card, bool selected);
[Signal]
public delegate void CardStateChangedEventHandler(CardControl card);
[Export]
public PackedScene ModifierIconScene { get; set; }
private TextureRect m_ArtTextureRect;
private Panel m_Background;
private Panel m_BorderPanel;
private HBoxContainer m_ModifiersContainer;
private Card m_Card;
private bool m_PressedLeftDown;
private bool m_Dragging;
private bool m_Selected;
private bool m_Hover;
private bool? m_DropBefore;
private CardRow m_CardRow;
private Label m_Label;
private HBoxContainer m_DragPreviewRoot;
private IReadOnlyList<CardControl> m_DragPreviewCards;
private Vector2 m_DesiredPosition;
private Vector2 m_DesiredPivotOffset;
private Vector2 m_DesiredScale;
private Color m_DesiredModulate;
}

View File

@@ -0,0 +1 @@
uid://oiles788elk5

View File

@@ -0,0 +1,756 @@
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Modifiers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using RobotAndDonkey.Game;
public partial class CardRow : Control
{
public override void _Ready()
{
CacheBackgroundPanels();
UpdateBackgroundVisibility();
}
public override void _Process(double delta)
{
if (s_CardPressActive && !Input.IsMouseButtonPressed(MouseButton.Left))
s_CardPressActive = false;
if (OrderedCards.Count == 0)
{
m_LayoutDirty = false;
return;
}
var needsLayout = m_LayoutDirty || GlobalPosition != m_LastPosition || Size != m_LastSize;
if (!needsLayout)
return;
m_LayoutDirty = false;
LayoutCards();
if (GetParent() is not Container)
SetAnchorsPreset(LayoutPreset.Center);
}
public override void _GuiInput(InputEvent @event)
{
if (@event is InputEventMouseButton mouseButton && mouseButton.ButtonIndex == MouseButton.Left && mouseButton.Pressed)
ClearSelection();
}
public void Configure(IReadOnlyList<Card> cards, Tween tween, bool celebrateModifiers, bool drag)
{
Debug.WriteLine($"Configure {cards.Count} cards, celebrate={celebrateModifiers}, drag={drag}");
if (CardScene == null)
{
GD.PushWarning($"{nameof(CardRow)} has no CardScene assigned. No cards will be shown.");
return;
}
if (cards.SequenceEqual(OrderedCards))
return;
var newSet = new HashSet<Guid>(cards.Select(c => c.CardId));
var toRemove = OrderedCards.Where(c => !newSet.Contains(c.CardId)).ToList();
foreach (var card in toRemove)
{
if (!CardToControl.Remove(card.CardId, out var control))
continue;
control.QueueFree();
}
OrderedCards.Clear();
OrderedCards.AddRange(cards);
var newControls = new List<CardControl>();
for (var index = 0; index < cards.Count; index++)
{
var card = cards[index];
if (!CardToControl.TryGetValue(card.CardId, out var control))
{
Debug.WriteLine($"Add new card {card.Id}");
control = CreateCardControl(card);
CardToControl.Add(card.CardId, control);
AddChild(control);
newControls.Add(control);
}
else
{
control.Configure(card, this);
}
}
LayoutCards();
foreach (var control in newControls)
control.SnapToDesired();
if (celebrateModifiers)
{
foreach (var card in cards)
{
foreach (var modifier in card.Modifiers)
{
ModifyCard(card, modifier.Id, tween);
}
}
}
}
private void GetLayoutMetrics(Card card, out float visualWidth, out float leftOffset, out float topOffset, out float bottomOffset)
{
var size = CardSize;
var desiredScale = Vector2.One;
var desiredPivot = size / 2f;
if (CardToControl.TryGetValue(card.CardId, out var control))
{
size = control.Size;
desiredScale = control.DesiredScale;
desiredPivot = control.DesiredPivotOffset;
}
visualWidth = size.X * desiredScale.X;
leftOffset = (1f - desiredScale.X) * desiredPivot.X;
topOffset = (1f - desiredScale.Y) * desiredPivot.Y;
bottomOffset = topOffset + size.Y * desiredScale.Y;
}
private void CalculateCardPositions(IReadOnlyList<Card> cards)
{
m_CardPositions.Clear();
var count = cards.Count;
if (count == 0)
return;
var cardHeight = CardSize.Y;
var cardIds = new Guid[count];
var visualWidths = new float[count];
var leftOffsets = new float[count];
var topOffsets = new float[count];
var bottomOffsets = new float[count];
var totalVisualWidth = 0f;
for (var index = 0; index < count; index++)
{
var card = cards[index];
cardIds[index] = card.CardId;
GetLayoutMetrics(card, out var visualWidth, out var leftOffset, out var topOffset, out var bottomOffset);
visualWidths[index] = visualWidth;
leftOffsets[index] = leftOffset;
topOffsets[index] = topOffset;
bottomOffsets[index] = bottomOffset;
totalVisualWidth += visualWidth;
}
var availableWidth = Size.X > 0 ? Size.X : CardSize.X;
var availableHeight = Size.Y > 0 ? Size.Y : CardSize.Y;
if (availableWidth <= 0)
availableWidth = totalVisualWidth + (count - 1) * CardSpacing;
if (availableHeight <= 0)
availableHeight = cardHeight;
if (WrapCards)
{
var rowSpacing = CardSpacing;
var rowTopOffsets = new List<float>();
var rowBottomOffsets = new List<float>();
var rowIndices = new int[count];
var rowWidth = 0f;
var rowIndex = 0;
var rowHasCards = false;
var rowTopOffset = 0f;
var rowBottomOffset = 0f;
for (var index = 0; index < count; index++)
{
var cardWidthForLayout = visualWidths[index];
var spacing = rowWidth > 0f ? CardSpacing : 0f;
if (rowWidth > 0f && rowWidth + spacing + cardWidthForLayout > availableWidth)
{
rowTopOffsets.Add(rowTopOffset);
rowBottomOffsets.Add(rowBottomOffset);
rowIndex += 1;
rowWidth = 0f;
rowHasCards = false;
spacing = 0f;
}
if (!rowHasCards)
{
rowTopOffset = topOffsets[index];
rowBottomOffset = bottomOffsets[index];
rowHasCards = true;
}
else
{
rowTopOffset = Mathf.Min(rowTopOffset, topOffsets[index]);
rowBottomOffset = Mathf.Max(rowBottomOffset, bottomOffsets[index]);
}
rowIndices[index] = rowIndex;
rowWidth += spacing + cardWidthForLayout;
}
if (rowHasCards)
{
rowTopOffsets.Add(rowTopOffset);
rowBottomOffsets.Add(rowBottomOffset);
}
var rowTops = new float[rowTopOffsets.Count];
if (rowTops.Length > 0)
{
rowTops[0] = 0f;
for (var index = 1; index < rowTopOffsets.Count; index++)
{
var previousBottom = rowTops[index - 1] + rowBottomOffsets[index - 1];
rowTops[index] = previousBottom + rowSpacing - rowTopOffsets[index];
}
}
var currentRow = 0;
var currentVisualLeft = 0f;
for (var index = 0; index < count; index++)
{
var cardRowIndex = rowIndices[index];
if (cardRowIndex != currentRow)
{
currentRow = cardRowIndex;
currentVisualLeft = 0f;
}
if (currentVisualLeft > 0f)
currentVisualLeft += CardSpacing;
var positionX = currentVisualLeft - leftOffsets[index];
var positionY = rowTops[cardRowIndex];
m_CardPositions[cardIds[index]] = new Vector2(positionX, positionY);
currentVisualLeft += visualWidths[index];
}
if (rowTopOffsets.Count > 0)
{
var totalHeight = rowTops[^1] + rowBottomOffsets[^1];
CustomMinimumSize = new(0, totalHeight);
}
}
else
{
var spacing = CardSpacing;
var widthWithBaseSpacing = totalVisualWidth + (count - 1) * CardSpacing;
if (count > 1)
{
if (widthWithBaseSpacing > availableWidth)
{
spacing = (availableWidth - totalVisualWidth) / (count - 1);
}
}
var startVisualLeft = (availableWidth - widthWithBaseSpacing) * 0.5f;
var y = (availableHeight - cardHeight) * 0.5f;
var currentVisualLeft = startVisualLeft;
for (var index = 0; index < count; index++)
{
var positionX = currentVisualLeft - leftOffsets[index];
m_CardPositions[cardIds[index]] = new Vector2(positionX, y);
currentVisualLeft += visualWidths[index] + spacing;
}
}
}
public void RunCard(Card card, Tween tween)
{
if (!CardToControl.Remove(card.CardId, out var control))
return;
Main.Instance.Music.Play(MusicManager.ESound.Card);
control.QueueFree();
OrderedCards.Remove(card);
LayoutCards();
}
public void ModifyCard(Card card, EModifierId modifier, Tween tween)
{
if (!CardToControl.TryGetValue(card.CardId, out var control))
{
Debug.WriteLine($"Failed to modify {card.Id}");
return;
}
var hasModifier = card.Modifiers.Any(m => m.Id == modifier && m.DebuffSources.Count == 0);
Debug.WriteLine($"Modify {card.Id}");
control.Configure(card, this);
if (hasModifier)
Main.Instance.Music.Play(modifier);
}
private CardControl CreateCardControl(Card card)
{
var node = CardScene.Instantiate<CardControl>();
node.Name = $"Card_{card.Id}";
node.SetAnchorsPreset(LayoutPreset.TopLeft);
node.Size = CardSize;
node.PivotOffset = CardSize / 2f;
node.CanDrag = CardsCanDrag;
node.CanSelect = CardsCanSelect;
node.Configure(card, this);
node.Connect(CardControl.SignalName.CardDroppedOn, new(this, nameof(OnCardDroppedOn)));
node.Connect(CardControl.SignalName.CardSelectionChanged, new(this, nameof(OnCardSelectionChanged)));
node.Connect(CardControl.SignalName.CardStateChanged, new(this, nameof(OnCardStateChanged)));
return node;
}
private void OnCardStateChanged(CardControl card)
{
m_LayoutDirty = true;
}
private void OnCardSelectionChanged(CardControl card, bool selected)
{
if (!m_SuppressSelectionValidation && CanChangeSelection != null && !CanChangeSelection(card, selected))
{
m_SuppressSelectionValidation = true;
card.Selected = !selected;
m_SuppressSelectionValidation = false;
return;
}
if (m_SuppressSelectionSignals)
return;
if (selected)
SelectionContext = this;
if (selected && !SupportsMultiSelect)
{
m_SuppressSelectionValidation = true;
foreach (var other in CardToControl.Values)
{
if (other != card && other.Selected)
other.Selected = false;
}
m_SuppressSelectionValidation = false;
}
EmitSignal(SignalName.SelectionChanged, CardToControl.Values.Where(c => c.Selected).ToArray());
}
private void OnCardDroppedOn(CardControl source, CardControl target, bool before)
{
if (source == target)
return;
var sourceRow = source.CardRow;
var targetRow = target.CardRow;
var fromCardId = sourceRow.CardToControl.FirstOrDefault(kv => kv.Value == source).Key;
var toCardId = targetRow.CardToControl.FirstOrDefault(kv => kv.Value == target).Key;
if (fromCardId == Guid.Empty || toCardId == Guid.Empty)
return;
var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId);
var toCard = OrderedCards.Single(c => c.CardId == toCardId);
var draggedCards = GetDraggedCards(sourceRow, fromCard);
if (draggedCards.Contains(toCard))
{
if (SelectedCards.Contains(toCard))
{
draggedCards = [fromCard];
}
else
{
return;
}
}
var targetCards = targetRow.OrderedCards;
var toIndex = targetCards.IndexOf(toCard);
if (toIndex == -1)
return;
var insertIndex = before ? toIndex : toIndex + 1;
MoveCards(sourceRow, targetRow, draggedCards, insertIndex);
}
public override bool _CanDropData(Vector2 atPosition, Variant data)
{
if (Disabled || !CardsCanDrag)
return false;
if (data.Obj is not CardControl sourceControl)
return false;
var sourceRow = sourceControl.CardRow;
var fromCardId = sourceRow.CardToControl.FirstOrDefault(kv => kv.Value == sourceControl).Key;
if (fromCardId == Guid.Empty)
return false;
var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId);
var draggedCards = GetDraggedCards(sourceRow, fromCard);
if (draggedCards.Count == 0)
return false;
if (sourceRow != this && OccupiedSpace + draggedCards.Select(c => c.OccupiedSpace).Sum() > MaxCards)
return false;
sourceControl.UpdateDragPreviewForDropTarget(null);
return true;
}
public override void _DropData(Vector2 atPosition, Variant data)
{
if (Disabled || !CardsCanDrag)
return;
if (data.Obj is not CardControl sourceControl)
return;
var sourceRow = sourceControl.CardRow;
var fromCardId = sourceRow?.CardToControl.FirstOrDefault(kv => kv.Value == sourceControl).Key;
if (fromCardId == Guid.Empty)
return;
var fromCard = sourceRow.OrderedCards.Single(c => c.CardId == fromCardId);
var draggedCards = GetDraggedCards(sourceRow, fromCard);
if (draggedCards.Count == 0)
return;
if (OccupiedSpace + draggedCards.Select(c => c.OccupiedSpace).Sum() > MaxCards)
return;
var insertIndex = GetDropIndex(atPosition);
MoveCards(sourceRow, this, draggedCards, insertIndex);
}
private List<Card> GetDraggedCards(CardRow sourceRow, Card fromCard)
{
if (fromCard == null)
return [];
if (!sourceRow.SupportsMultiDrag)
return [fromCard];
var selected = sourceRow.SelectedCardIndices.Select(i => sourceRow.OrderedCards[i]).ToList();
if (selected.Count > 1 && selected.Contains(fromCard))
{
var originalOrder = sourceRow.OrderedCards;
return selected.OrderBy(c => originalOrder.IndexOf(c)).ToList();
}
return [fromCard];
}
private void MoveCards(CardRow sourceRow, CardRow targetRow, List<Card> draggedCards, int rawInsertIndex)
{
if (draggedCards == null || draggedCards.Count == 0)
return;
var draggedSet = new HashSet<Card>(draggedCards);
if (sourceRow == targetRow)
{
var original = sourceRow.OrderedCards.ToList();
if (original.Count == 0)
return;
var insertIndex = Mathf.Clamp(rawInsertIndex, 0, original.Count);
var removedBefore = 0;
foreach (var card in draggedCards)
{
var idx = original.IndexOf(card);
if (idx >= 0 && idx < insertIndex)
removedBefore++;
}
var adjustedIndex = insertIndex - removedBefore;
var newOrder = original.Where(c => !draggedSet.Contains(c)).ToList();
adjustedIndex = Mathf.Clamp(adjustedIndex, 0, newOrder.Count);
newOrder.InsertRange(adjustedIndex, draggedCards);
sourceRow.Configure(newOrder, null, false, true);
sourceRow.EmitSignal(SignalName.OrderChanged);
}
else
{
var sourceList = sourceRow.OrderedCards.ToList();
var targetList = targetRow.OrderedCards.ToList();
foreach (var card in draggedCards)
{
sourceList.Remove(card);
targetList.Remove(card);
}
var insertIndex = Mathf.Clamp(rawInsertIndex, 0, targetList.Count);
targetList.InsertRange(insertIndex, draggedCards);
sourceRow.Configure(sourceList, null, false, true);
targetRow.Configure(targetList, null, false, true);
targetRow.EmitSignal(SignalName.OrderChanged);
}
}
private int GetDropIndex(Vector2 localPosition)
{
var count = OrderedCards.Count;
if (count == 0)
return 0;
var orderedControls = OrderedCards.Select(card => CardToControl.GetValueOrDefault(card.CardId)).Where(control => control != null).ToList();
if (orderedControls.Count == 0)
return 0;
var mouseX = localPosition.X;
if (mouseX <= orderedControls[0].Position.X)
return 0;
var last = orderedControls[^1];
if (mouseX >= last.Position.X + CardSize.X)
return orderedControls.Count;
for (var i = 0; i < orderedControls.Count; i++)
{
var control = orderedControls[i];
if (mouseX < control.Position.X)
return i;
}
return orderedControls.Count;
}
private void LayoutCards()
{
Debug.WriteLine("Layout");
var count = OrderedCards.Count;
if (count == 0)
return;
m_LastPosition = GlobalPosition;
m_LastSize = Size;
CalculateCardPositions(OrderedCards);
foreach (var card in OrderedCards)
{
if (!CardToControl.TryGetValue(card.CardId, out var control))
continue;
if (m_CardPositions.TryGetValue(card.CardId, out var targetPos))
control.SetDesiredPosition(targetPos);
}
}
private void CacheBackgroundPanels()
{
m_Backgrounds.Clear();
foreach (var child in GetChildren())
{
if (child is Control control && control.Name.ToString().StartsWith("Background"))
{
control.ZIndex = 0;
control.MouseFilter = MouseFilterEnum.Ignore;
m_Backgrounds.Add(control);
}
}
}
private void UpdateBackgroundVisibility()
{
if (m_Backgrounds.Count == 0)
return;
var clamped = Mathf.Clamp(BackgroundVariant, 0, m_Backgrounds.Count - 1);
for (var i = 0; i < m_Backgrounds.Count; i++)
m_Backgrounds[i].Visible = i == clamped;
}
public void ApplySelection(IReadOnlyCollection<Guid> selectedCardIds, bool suppressSignals)
{
m_SuppressSelectionValidation = true;
m_SuppressSelectionSignals = suppressSignals;
foreach (var kvp in CardToControl)
{
kvp.Value.Selected = selectedCardIds.Contains(kvp.Key);
}
m_SuppressSelectionSignals = false;
m_SuppressSelectionValidation = false;
}
public void ClearSelection()
{
ApplySelection([], true);
EmitSignal(SignalName.SelectionChanged, []);
}
public static List<Card> GetSortedCards(IEnumerable<Card> cards)
{
return cards.Order(Comparer<Card>.Create((a, b) =>
{
var cmp = a.Rarity.CompareTo(b.Rarity);
if (cmp == 0)
cmp = a.Id.CompareTo(b.Id);
if (cmp == 0)
cmp = a.Modifiers.Count.CompareTo(b.Modifiers.Count);
if (cmp == 0)
cmp = a.CardId.CompareTo(b.CardId);
return cmp;
})).ToList();
}
[Export]
public PackedScene CardScene { get; set; }
[Export]
public Vector2 CardSize { get; set; } = new(96, 96);
[Export]
public float CardSpacing { get; set; } = 8f;
[Export]
public float AnimationDuration { get; set; } = 0.15f;
[Export]
public bool CardsCanDrag { get; set; } = true;
[Export]
public bool CardsCanSelect { get; set; } = false;
[Export]
public bool SupportsMultiSelect { get; set; } = false;
[Export]
public bool SupportsMultiDrag { get; set; } = false;
[Export]
public int BackgroundVariant
{
get => m_BackgroundVariant;
set
{
if (m_BackgroundVariant == value)
return;
m_BackgroundVariant = value;
UpdateBackgroundVisibility();
}
}
public Func<CardControl, bool, bool> CanChangeSelection { get; set; }
public static bool IsCardPressActive => s_CardPressActive;
public static void NotifyCardPress(bool pressed)
{
s_CardPressActive = pressed;
}
public static CardRow SelectionContext
{
get => s_SelectionContext;
set
{
if (s_SelectionContext == value)
return;
if (s_SelectionContext != null)
{
foreach (var card in s_SelectionContext.CardToControl)
card.Value.Selected = false;
}
s_SelectionContext = value;
}
}
[Export]
public bool Disabled
{
get => m_Disabled;
set
{
if (m_Disabled == value)
return;
m_Disabled = value;
foreach (var card in CardToControl)
card.Value.Disabled = m_Disabled;
}
}
[Export]
public int MaxCards { get; set; } = int.MaxValue;
[Export]
public bool WrapCards { get; set; } = false;
public int OccupiedSpace => OrderedCards.Select(c => c.OccupiedSpace).Sum();
public IEnumerable<int> SelectedCardIndices =>
OrderedCards.Select((c, i) => (Card: CardToControl[c.CardId], Index: i)).Where(pair => pair.Card.Selected).Select(pair => pair.Index);
public IReadOnlyList<Guid> SelectedCardIds =>
OrderedCards.Where(card => CardToControl.TryGetValue(card.CardId, out var control) && control.Selected)
.Select(card => card.CardId)
.ToList();
public IReadOnlyList<Card> SelectedCards =>
OrderedCards.Where(card => CardToControl.TryGetValue(card.CardId, out var control) && control.Selected)
.ToList();
public List<Card> OrderedCards { get; } = [];
public Dictionary<Guid, CardControl> CardToControl => m_CardToControl;
[Signal]
public delegate void OrderChangedEventHandler();
[Signal]
public delegate void SelectionChangedEventHandler(CardControl[] selection);
private static CardRow s_SelectionContext;
private static bool s_CardPressActive;
private readonly List<Control> m_Backgrounds = [];
private readonly Dictionary<Guid, CardControl> m_CardToControl = new();
private readonly Dictionary<Guid, Vector2> m_CardPositions = [];
private int m_BackgroundVariant;
private bool m_Disabled;
private bool m_SuppressSelectionValidation;
private bool m_SuppressSelectionSignals;
private bool m_LayoutDirty;
private Vector2 m_LastPosition;
private Vector2 m_LastSize;
}

View File

@@ -0,0 +1 @@
uid://breqe5ccn40sg

View File

@@ -0,0 +1,213 @@
using System;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Execution.Results;
using System.Collections.Generic;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Pois;
public partial class CoreLoopScreen : Control, IScreen
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
m_DrawGlitch = GetNode<DrawGlitch>("%Draw Glitch");
m_Improve = GetNode<Improve>("%Improve");
m_CosmicRays = GetNode<Gamble>("%Cosmic Rays");
m_BufferOverflow = GetNode<BufferOverflow>("%Buffer Overflow");
m_ProgramScreen = GetNode<ProgramScreen>("%Program");
m_BoardNode = GetNode<BoardNode>("%BoardNode");
m_CurrencyBar = GetNode<CurrencyBar>("%CurrencyBar");
m_OptionsButton = GetNode<Button>("%OptionsButton");
m_SeedButton = GetNode<Button>("%SeedButton");
m_SeedButton.Pressed += OnSeedButtonPressed;
m_OptionsButton.Pressed += OnOptionsButtonPressed;
}
private void OnSeedButtonPressed()
{
DisplayServer.ClipboardSet(Main.Instance.StringSeed);
//Main.Instance.StartCoreLoop(Random.Shared.Next(), EDifficulty.Easy, ERobotType.Vintage);
}
public bool TransitionScreen<T>(T newScreen, Tween tween) where T : Control, IScreen
{
newScreen.EnableInputs();
if (m_CurrentScreen == newScreen)
return false;
var parent = newScreen.GetParent();
parent.MoveChild(newScreen, 1);
var oldControl = (Control)m_CurrentScreen;
var oldScreen = m_CurrentScreen;
m_CurrentScreen = newScreen;
newScreen.Modulate = new(1, 1, 1);
newScreen.Visible = true;
newScreen.Activate();
if (oldScreen == null)
return true;
tween.TweenProperty(oldControl, "modulate", new Color(0, 0, 0, 0), 0.25);
tween.TweenCallback(Callable.From(() =>
{
oldScreen.Deactivate();
oldControl.Visible = false;
oldControl.Modulate = new(1, 1, 1);
}));
return true;
}
public void EnableInputs()
{
m_SeedButton.Text = Main.Instance.StringSeed;
m_CurrentScreen?.EnableInputs();
m_BoardNode.Configure(Main.Instance.CoreLoop.Board);
m_CurrencyBar.Configure(Main.Instance.CoreLoop, Main.Instance.CoreLoop.ProgramCount, Main.Instance.CoreLoop.Currency, null);
}
public void Activate()
{
}
public void Deactivate()
{
if (m_CurrentScreen is Control control)
{
control.Visible = false;
control.Modulate = new(1, 1, 1);
m_CurrentScreen.Deactivate();
m_CurrentScreen = null;
}
}
public void DisableInputs()
{
m_CurrentScreen?.DisableInputs();
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouse mouseEvent)
{
var container = GetNode<Control>("%BoardViewPortContainer");
if (!container.GetGlobalRect().HasPoint(mouseEvent.Position))
return;
var viewPort = GetNode<SubViewport>("%BoardViewPort");
viewPort.PushInput(@event);
if (viewPort.IsInputHandled())
GetViewport().SetInputAsHandled();
}
}
private void OnOptionsButtonPressed()
{
Main.Instance.OptionsMenu.ShowMenu();
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event.IsActionPressed("options") && m_CurrentScreen != null)
{
Main.Instance.OptionsMenu.ShowMenu();
GetViewport().SetInputAsHandled();
}
}
public void DrawGlitch(Card card, Tween tween)
{
TransitionScreen(m_DrawGlitch, tween);
m_DrawGlitch.Configure(card, tween);
}
public void Improve(Tween tween)
{
TransitionScreen(m_Improve, tween);
m_Improve.Configure(Main.Instance.CoreLoop.Shop, Main.Instance.CoreLoop.PatchDeck, tween);
}
public bool HandleResult(Result result, Tween tween)
{
if (m_CurrentScreen.HandleResult(result, tween))
return true;
switch (result)
{
case CellTypeResult cellResult:
{
m_BoardNode.Configure(cellResult, tween);
return true;
}
case ModifyCellResult cellResult:
{
m_BoardNode.Configure(cellResult, tween);
return true;
}
case PoiResult cellResult:
{
m_BoardNode.Configure(cellResult, tween);
return true;
}
case CurrencyResult currencyResult:
{
m_CurrencyBar.Configure(Main.Instance.CoreLoop, Main.Instance.CoreLoop.ProgramCount, currencyResult.NewCurrency, tween);
return true;
}
case ProgramResult programResult:
{
m_CurrencyBar.Configure(Main.Instance.CoreLoop, programResult.NewProgram, Main.Instance.CoreLoop.Currency, tween);
return true;
}
case ModifyCardResult modifyCardResult:
{
m_Improve.Deck.ModifyCard(modifyCardResult.Card, modifyCardResult.Modifier, tween);
return true;
}
case DeckResult deckResult:
{
m_Improve.Deck.Configure(deckResult.Deck, tween, false, false);
return true;
}
}
GD.Print($"Unhandled result {result.GetType().Name} in screen {m_CurrentScreen.GetType().Name}");
return false;
}
public void CosmicRays(List<Card> gambleCards, Tween tween)
{
TransitionScreen(m_CosmicRays, tween);
m_CosmicRays.Configure(gambleCards, tween);
}
public void BufferOverflow(Tween tween)
{
TransitionScreen(m_BufferOverflow, tween);
m_BufferOverflow.Configure(tween);
}
public void ExecuteProgram(Tween tween)
{
var becomeActive = TransitionScreen(m_ProgramScreen, tween);
m_ProgramScreen.Configure(becomeActive ? tween : null);
}
private ProgramScreen m_ProgramScreen;
private DrawGlitch m_DrawGlitch;
private Improve m_Improve;
private IScreen m_CurrentScreen;
private Gamble m_CosmicRays;
private BufferOverflow m_BufferOverflow;
private BoardNode m_BoardNode;
private CurrencyBar m_CurrencyBar;
private Button m_SeedButton;
private Button m_OptionsButton;
}

View File

@@ -0,0 +1 @@
uid://b8aed65amla7p

View File

@@ -0,0 +1,68 @@
using Godot;
public partial class CorruptionArea : Node3D
{
public override void _Ready()
{
_disc = GetNode<MeshInstance3D>("Disc");
if (_disc == null)
{
GD.PushWarning("CorruptionArea: Could not find child MeshInstance3D named 'Disc'.");
return;
}
_baseScale = _disc.Scale;
ApplyRadius();
_shaderMaterial = (ShaderMaterial)_disc.GetActiveMaterial(0).Duplicate();
_disc.MaterialOverride = _shaderMaterial;
}
public override void _Process(double delta)
{
if (_shaderMaterial == null)
return;
var t = Time.GetTicksMsec() / 1000.0f;
var pulse = 1.0f + Mathf.Sin(t * PulseSpeed) * PulseAmount;
_shaderMaterial.SetShaderParameter("u_pulse", pulse);
_shaderMaterial.SetShaderParameter("base_color", Color);
}
/// <summary>
/// Call this if you change Radius at runtime.
/// </summary>
public void ApplyRadius()
{
if (_disc == null)
return;
var r = Mathf.Max(Radius, 0.01f);
// PlaneMesh is 2x2 units, so scale XZ by radius to get the desired world radius.
_disc.Scale = new(_baseScale.X * r, _baseScale.Y, _baseScale.Z * r);
}
public void SetRadius(float radius)
{
Radius = radius;
ApplyRadius();
}
[Export]
public float Radius { get; set; } = 1.0f; // World-space radius in XZ units.
[Export]
public float PulseSpeed { get; set; } = 1.5f;
[Export]
public float PulseAmount { get; set; } = 0.25f;
[Export]
public Color Color { get; set; } = new(0.6f, 0.1f, 0.8f, 0.45f);
private Vector3 _baseScale = Vector3.One;
private MeshInstance3D _disc;
private ShaderMaterial _shaderMaterial;
}

View File

@@ -0,0 +1 @@
uid://gdp1jvwnkerk

View File

@@ -0,0 +1,132 @@
using System;
using Godot;
using RobotAndDonkey.Game.GameState;
public partial class CurrencyBar : Control
{
public override void _Ready()
{
m_EnergyContainer = GetNode<Control>("%Energy");
m_CarryContainer = GetNode<Control>("%Carry");
m_DeliveryContainer = GetNode<Control>("%Delivery");
m_TapeContainer = GetNode<Control>("%Tape");
m_ProgramContainer = GetNode<Control>("%Program");
m_HandContainer = GetNode<Control>("%Hand");
m_EnergyLabel = GetNode<Label>("%Energy/Label");
m_CarryLabel = GetNode<Label>("%Carry/Label");
m_DeliveryLabel = GetNode<Label>("%Delivery/Label");
m_TapeLabel = GetNode<Label>("%Tape/Label");
m_ProgramLabel = GetNode<Label>("%Program/Label");
m_HandLabel = GetNode<Label>("%Hand/Label");
ResetVisuals();
}
private void ResetVisuals()
{
m_EnergyContainer.Scale = Vector2.One;
m_CarryContainer.Scale = Vector2.One;
m_DeliveryContainer.Scale = Vector2.One;
m_TapeContainer.Scale = Vector2.One;
m_ProgramContainer.Scale = Vector2.One;
m_HandContainer.Scale = Vector2.One;
m_EnergyLabel.Modulate = s_BaseColor;
m_CarryLabel.Modulate = s_BaseColor;
m_DeliveryLabel.Modulate = s_BaseColor;
m_TapeLabel.Modulate = s_BaseColor;
m_ProgramLabel.Modulate = s_BaseColor;
m_HandLabel.Modulate = s_BaseColor;
}
public void Configure(CoreLoop coreLoop, int programCount, Currency currency, Tween tween)
{
if (!m_HasCurrency)
{
m_CurrentCurrency = currency;
m_CurrentProgramCount = programCount;
m_HasCurrency = true;
SetAllTexts(coreLoop, programCount, currency);
ResetVisuals();
return;
}
AnimateStat(m_EnergyContainer, m_EnergyLabel, m_CurrentCurrency.Energy, currency.Energy, tween, currency.Energy > m_CurrentCurrency.Energy ? MusicManager.ESound.Rest : MusicManager.ESound.Energy, value => value.ToString());
var oldCarryCombined = m_CurrentCurrency.Carry + m_CurrentCurrency.MaxCarry * 10000;
var newCarryCombined = currency.Carry + currency.MaxCarry * 10000;
AnimateStat(m_CarryContainer, m_CarryLabel, oldCarryCombined, newCarryCombined, tween, MusicManager.ESound.Carry, _ => $"{currency.Carry}/{currency.MaxCarry}");
AnimateStat(m_DeliveryContainer, m_DeliveryLabel, m_CurrentCurrency.Delivery, currency.Delivery, tween, MusicManager.ESound.Delivery, value => $"{value}/{coreLoop.Board.TargetDeliveryAmount}");
AnimateStat(m_TapeContainer, m_TapeLabel, m_CurrentCurrency.TapeLength, currency.TapeLength, tween, MusicManager.ESound.TapeLength, value => value.ToString());
AnimateStat(m_HandContainer, m_HandLabel, m_CurrentCurrency.HandSize, currency.HandSize, tween, MusicManager.ESound.Hand, value => value.ToString());
AnimateStat(m_ProgramContainer, m_ProgramLabel, m_CurrentProgramCount, programCount, tween, MusicManager.ESound.Program, value => $"Program {(value == 0 ? 0 : 1 + coreLoop.Robot.ProgramCount - value)}/{coreLoop.Robot.ProgramCount}");
m_CurrentCurrency = currency;
m_CurrentProgramCount = programCount;
}
private void SetAllTexts(CoreLoop coreLoop, int programCount, Currency currency)
{
m_EnergyLabel.Text = currency.Energy.ToString();
m_CarryLabel.Text = $"{currency.Carry}/{currency.MaxCarry}";
m_DeliveryLabel.Text = $"{currency.Delivery}/{coreLoop.Board.TargetDeliveryAmount}";
m_TapeLabel.Text = currency.TapeLength.ToString();
m_HandLabel.Text = currency.HandSize.ToString();
var programNumber = (programCount == 0 ? 0 : 1 + coreLoop.Robot.ProgramCount - programCount);
m_ProgramLabel.Text = $"Program {programNumber}/{coreLoop.Robot.ProgramCount}";
}
private void AnimateStat(Control container, Label label, int oldValue, int newValue, Tween tween, MusicManager.ESound sound, Func<int, string> formatter)
{
if (oldValue == newValue || formatter == null)
{
label.Text = formatter?.Invoke(newValue) ?? label.Text;
return;
}
if (tween != null)
{
var increased = newValue > oldValue;
label.Modulate = increased ? s_GoodColor : s_BadColor;
tween.SetParallel();
tween.TweenCallback(Callable.From(() =>
{
container.Scale = new(2, 2);
label.Text = formatter(newValue);
Main.Instance.Music.Play(sound);
}));
tween.TweenProperty(container, "scale", Vector2.One, 0.25f).SetTrans(Tween.TransitionType.Back).SetEase(Tween.EaseType.Out);
tween.TweenProperty(label, "modulate", s_BaseColor, 0.25f).SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
tween.SetParallel(false);
}
else
{
label.Text = formatter(newValue);
}
}
private static readonly Color s_BadColor = new(0.94f, 0.60f, 0.60f);
private static readonly Color s_BaseColor = new(1f, 1f, 1f);
private static readonly Color s_GoodColor = new(0.65f, 0.84f, 0.65f);
private Control m_CarryContainer;
private Label m_CarryLabel;
private Currency m_CurrentCurrency;
private int m_CurrentProgramCount;
private Control m_DeliveryContainer;
private Label m_DeliveryLabel;
private Control m_EnergyContainer;
private Label m_EnergyLabel;
private bool m_HasCurrency;
private Control m_ProgramContainer;
private Label m_ProgramLabel;
private Control m_TapeContainer;
private Label m_TapeLabel;
private Control m_HandContainer;
private Label m_HandLabel;
}

View File

@@ -0,0 +1 @@
uid://6cx86f16c8ml

View File

@@ -0,0 +1,10 @@
<Project Sdk="Godot.NET.Sdk/4.5.1">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RobotAndDonkey.Game\RobotAndDonkey.Game.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Godot (debug)</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,36 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DonkeysAndDroids", "DonkeysAndDroids.csproj", "{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotAndDonkey.Game", "..\RobotAndDonkey.Game\RobotAndDonkey.Game.csproj", "{C905453E-CBCA-6B8A-4CAD-932E551DBC72}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{0B4F8DC2-61FA-4BA2-A464-C79B25F82807}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.ExportDebug|Any CPU.ActiveCfg = Release|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.ExportDebug|Any CPU.Build.0 = Release|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.ExportRelease|Any CPU.ActiveCfg = Release|Any CPU
{C905453E-CBCA-6B8A-4CAD-932E551DBC72}.ExportRelease|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BE8B6832-9C61-4C03-AA94-1899E276011D}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,114 @@
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Results;
public partial class DrawGlitch : Control, IScreen
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
m_CardControl = GetNode<CardControl>("%Card");
m_Background = GetNode<Control>("%Background");
m_Deck = GetNode<CardRow>("%Deck");
m_AcceptButton = GetNode<Button>("VBoxContainer/MarginContainer/HBoxContainer/AcceptButton");
m_DeferButton = GetNode<Button>("VBoxContainer/MarginContainer/HBoxContainer/DeferButton");
m_AcceptButton.Pressed += OnAcceptPressed;
m_DeferButton.Pressed += OnDeferPressed;
}
private void OnDeferPressed()
{
Main.Instance.Execute(new DeferCardCommand(Main.Instance.CurrentRequest.RequestId));
}
private void OnAcceptPressed()
{
Main.Instance.Execute(new AcceptCardCommand(Main.Instance.CurrentRequest.RequestId));
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_Background.Modulate = new(1, 1, 1);
m_CardControl.Modulate = new(1, 1, 1);
m_CardControl.Disabled = false;
m_AcceptButton.Disabled = false;
m_DeferButton.Disabled = false;
m_Deck.Disabled = false;
}
public void DisableInputs()
{
m_CardControl.Disabled = true;
m_AcceptButton.Disabled = true;
m_DeferButton.Disabled = true;
m_Deck.Disabled = true;
}
public void Activate()
{
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
public void Configure(Card card, Tween tween)
{
m_CardControl.Configure(card, null);
var energy = new DeferCardCommand(Main.Instance.CurrentRequest.RequestId).EstimateEnergyCost(Main.Instance.CoreLoop);
m_DeferButton.Text = $"Defer ({energy} energy)";
Deck.Configure([], null, false, false);
Deck.Configure(CardRow.GetSortedCards(Main.Instance.CoreLoop.PatchDeck), tween, false, false);
Tooltip.Instance.Describe(card);
}
public bool HandleResult(Result result, Tween tween)
{
switch (result)
{
case RunCardResult:
{
tween.TweenCallback(Callable.From(() =>
{
m_CardControl.Disabled = true;
m_AcceptButton.Disabled = true;
m_DeferButton.Disabled = true;
m_Deck.Disabled = true;
}));
tween.TweenProperty(m_Background, "modulate", new Color(1, 1, 1, 0), 0.5f);
tween.TweenCallback(Callable.From(() => Main.Instance.Music.Play(MusicManager.ESound.Card)));
m_CardControl.AnimateCard(tween);
tween.TweenProperty(m_CardControl, "modulate", new Color(1, 1, 1, 0), 0.5f);
return true;
}
case DeckResult deckResult:
{
Deck.Configure(CardRow.GetSortedCards(deckResult.Deck), tween, false, false);
return true;
}
case ModifyCardResult modifyCardResult:
{
Deck.ModifyCard(modifyCardResult.Card, modifyCardResult.Modifier, tween);
return true;
}
}
return false;
}
public CardRow Deck => m_Deck;
private CardRow m_Deck;
private CardControl m_CardControl;
private Button m_AcceptButton;
private Button m_DeferButton;
private Control m_Background;
}

View File

@@ -0,0 +1 @@
uid://bohqea762phhu

View File

@@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Godot;
/// <summary>
/// Godot's global functions.
/// </summary>
public static class GD
{
/// <summary>
/// Decodes a byte array back to a <see cref="Variant"/> value, without decoding objects.
/// Note: If you need object deserialization, see <see cref="BytesToVarWithObjects"/>.
/// </summary>
/// <param name="bytes">Byte array that will be decoded to a <see cref="Variant"/>.</param>
/// <returns>The decoded <see cref="Variant"/>.</returns>
public static Variant BytesToVar(Span<byte> bytes) => Godot.GD.BytesToVar(bytes);
/// <summary>
/// Decodes a byte array back to a <see cref="Variant"/> value. Decoding objects is allowed.
/// Warning: Deserialized object can contain code which gets executed. Do not use this
/// option if the serialized object comes from untrusted sources to avoid potential security
/// threats (remote code execution).
/// </summary>
/// <param name="bytes">Byte array that will be decoded to a <see cref="Variant"/>.</param>
/// <returns>The decoded <see cref="Variant"/>.</returns>
public static Variant BytesToVarWithObjects(Span<byte> bytes) => Godot.GD.BytesToVarWithObjects(bytes);
/// <summary>
/// Converts <paramref name="what"/> to <paramref name="type"/> in the best way possible.
/// The <paramref name="type"/> parameter uses the <see cref="Variant.Type"/> values.
/// </summary>
/// <example>
/// <code>
/// Variant a = new Godot.Collections.Array { 4, 2.5, 1.2 };
/// GD.Print(a.VariantType == Variant.Type.Array); // Prints true
///
/// var b = GD.Convert(a, Variant.Type.PackedByteArray);
/// GD.Print(b); // Prints [4, 2, 1]
/// GD.Print(b.VariantType == Variant.Type.Array); // Prints false
/// </code>
/// </example>
/// <returns>The <c>Variant</c> converted to the given <paramref name="type"/>.</returns>
public static Variant Convert(Variant what, Variant.Type type) => Godot.GD.Convert(what, type);
/// <summary>
/// Returns the integer hash of the passed <paramref name="var"/>.
/// </summary>
/// <example>
/// <code>
/// GD.Print(GD.Hash("a")); // Prints 177670
/// </code>
/// </example>
/// <param name="var">Variable that will be hashed.</param>
/// <returns>Hash of the variable passed.</returns>
public static int Hash(Variant var) => Godot.GD.Hash(var);
/// <summary>
/// Loads a resource from the filesystem located at <paramref name="path"/>.
/// The resource is loaded on the method call (unless it's referenced already
/// elsewhere, e.g. in another script or in the scene), which might cause slight delay,
/// especially when loading scenes. To avoid unnecessary delays when loading something
/// multiple times, either store the resource in a variable.
///
/// Note: Resource paths can be obtained by right-clicking on a resource in the FileSystem
/// dock and choosing "Copy Path" or by dragging the file from the FileSystem dock into the script.
///
/// Important: The path must be absolute, a local path will just return <see langword="null"/>.
/// This method is a simplified version of <see cref="ResourceLoader.Load"/>, which can be used
/// for more advanced scenarios.
/// </summary>
/// <example>
/// <code>
/// // Load a scene called main located in the root of the project directory and cache it in a variable.
/// var main = GD.Load("res://main.tscn"); // main will contain a PackedScene resource.
/// </code>
/// </example>
/// <param name="path">Path of the <see cref="Resource"/> to load.</param>
/// <returns>The loaded <see cref="Resource"/>.</returns>
public static Resource Load(string path) => Godot.GD.Load(path);
/// <summary>
/// Loads a resource from the filesystem located at <paramref name="path"/>.
/// The resource is loaded on the method call (unless it's referenced already
/// elsewhere, e.g. in another script or in the scene), which might cause slight delay,
/// especially when loading scenes. To avoid unnecessary delays when loading something
/// multiple times, either store the resource in a variable.
///
/// Note: Resource paths can be obtained by right-clicking on a resource in the FileSystem
/// dock and choosing "Copy Path" or by dragging the file from the FileSystem dock into the script.
///
/// Important: The path must be absolute, a local path will just return <see langword="null"/>.
/// This method is a simplified version of <see cref="ResourceLoader.Load"/>, which can be used
/// for more advanced scenarios.
/// </summary>
/// <example>
/// <code>
/// // Load a scene called main located in the root of the project directory and cache it in a variable.
/// var main = GD.Load&lt;PackedScene&gt;("res://main.tscn"); // main will contain a PackedScene resource.
/// </code>
/// </example>
/// <param name="path">Path of the <see cref="Resource"/> to load.</param>
/// <typeparam name="T">The type to cast to. Should be a descendant of <see cref="Resource"/>.</typeparam>
public static T Load<T>(string path) where T : class => Godot.GD.Load<T>(path);
private static string AppendPrintParams(object[] parameters)
{
if (parameters == null)
{
return "null";
}
var sb = new StringBuilder();
for (int i = 0; i < parameters.Length; i++)
{
sb.Append(parameters[i]?.ToString() ?? "null");
}
return sb.ToString();
}
private static string AppendPrintParams(char separator, object[] parameters)
{
if (parameters == null)
{
return "null";
}
var sb = new StringBuilder();
for (int i = 0; i < parameters.Length; i++)
{
if (i != 0)
sb.Append(separator);
sb.Append(parameters[i]?.ToString() ?? "null");
}
return sb.ToString();
}
/// <summary>
/// Prints a message to the console.
///
/// Note: Consider using <see cref="PushError(string)"/> and <see cref="PushWarning(string)"/>
/// to print error and warning messages instead of <see cref="Print(string)"/>.
/// This distinguishes them from print messages used for debugging purposes,
/// while also displaying a stack trace when an error or warning is printed.
/// </summary>
/// <param name="what">Message that will be printed.</param>
public static void Print(string what)
{
Debug.WriteLine("Info: " + what);
Godot.GD.Print(what);
}
/// <summary>
/// Converts one or more arguments of any type to string in the best way possible
/// and prints them to the console.
///
/// Note: Consider using <see cref="PushError(object[])"/> and <see cref="PushWarning(object[])"/>
/// to print error and warning messages instead of <see cref="Print(object[])"/>.
/// This distinguishes them from print messages used for debugging purposes,
/// while also displaying a stack trace when an error or warning is printed.
/// </summary>
/// <example>
/// <code>
/// var a = new Godot.Collections.Array { 1, 2, 3 };
/// GD.Print("a", "b", a); // Prints ab[1, 2, 3]
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void Print(params object[] what)
{
Print(AppendPrintParams(what));
}
/// <summary>
/// Prints a message to the console.
/// The following BBCode tags are supported: b, i, u, s, indent, code, url, center,
/// right, color, bgcolor, fgcolor.
/// Color tags only support named colors such as <c>red</c>, not hexadecimal color codes.
/// Unsupported tags will be left as-is in standard output.
/// When printing to standard output, the supported subset of BBCode is converted to
/// ANSI escape codes for the terminal emulator to display. Displaying ANSI escape codes
/// is currently only supported on Linux and macOS. Support for ANSI escape codes may vary
/// across terminal emulators, especially for italic and strikethrough.
///
/// Note: Consider using <see cref="PushError(string)"/> and <see cref="PushWarning(string)"/>
/// to print error and warning messages instead of <see cref="Print(string)"/> or
/// <see cref="PrintRich(string)"/>.
/// This distinguishes them from print messages used for debugging purposes,
/// while also displaying a stack trace when an error or warning is printed.
/// </summary>
/// <param name="what">Message that will be printed.</param>
public static void PrintRich(string what)
{
Debug.WriteLine("Info: " + what);
Godot.GD.PrintRich(what);
}
/// <summary>
/// Converts one or more arguments of any type to string in the best way possible
/// and prints them to the console.
/// The following BBCode tags are supported: b, i, u, s, indent, code, url, center,
/// right, color, bgcolor, fgcolor.
/// Color tags only support named colors such as <c>red</c>, not hexadecimal color codes.
/// Unsupported tags will be left as-is in standard output.
/// When printing to standard output, the supported subset of BBCode is converted to
/// ANSI escape codes for the terminal emulator to display. Displaying ANSI escape codes
/// is currently only supported on Linux and macOS. Support for ANSI escape codes may vary
/// across terminal emulators, especially for italic and strikethrough.
///
/// Note: Consider using <see cref="PushError(object[])"/> and <see cref="PushWarning(object[])"/>
/// to print error and warning messages instead of <see cref="Print(object[])"/> or
/// <see cref="PrintRich(object[])"/>.
/// This distinguishes them from print messages used for debugging purposes,
/// while also displaying a stack trace when an error or warning is printed.
/// </summary>
/// <example>
/// <code>
/// GD.PrintRich("[code][b]Hello world![/b][/code]"); // Prints out: [b]Hello world![/b]
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void PrintRich(params object[] what)
{
PrintRich(AppendPrintParams(what));
}
/// <summary>
/// Prints a message to standard error line.
/// </summary>
/// <param name="what">Message that will be printed.</param>
public static void PrintErr(string what)
{
Debug.WriteLine("Error: " + what);
Godot.GD.PrintErr(what);
}
/// <summary>
/// Prints one or more arguments to strings in the best way possible to standard error line.
/// </summary>
/// <example>
/// <code>
/// GD.PrintErr("prints to stderr");
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void PrintErr(params object[] what)
{
PrintErr(AppendPrintParams(what));
}
/// <summary>
/// Prints a message to the OS terminal.
/// Unlike <see cref="Print(string)"/>, no newline is added at the end.
/// </summary>
/// <param name="what">Message that will be printed.</param>
public static void PrintRaw(string what)
{
Debug.WriteLine(what);
Godot.GD.PrintRaw(what);
}
/// <summary>
/// Prints one or more arguments to strings in the best way possible to the OS terminal.
/// Unlike <see cref="Print(object[])"/>, no newline is added at the end.
/// </summary>
/// <example>
/// <code>
/// GD.PrintRaw("A");
/// GD.PrintRaw("B");
/// GD.PrintRaw("C");
/// // Prints ABC to terminal
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void PrintRaw(params object[] what)
{
PrintRaw(AppendPrintParams(what));
}
/// <summary>
/// Prints one or more arguments to the console with a space between each argument.
/// </summary>
/// <example>
/// <code>
/// GD.PrintS("A", "B", "C"); // Prints A B C
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void PrintS(params object[] what)
{
string message = AppendPrintParams(' ', what);
PrintErr(message);
Godot.GD.PrintS(what);
}
/// <summary>
/// Prints one or more arguments to the console with a tab between each argument.
/// </summary>
/// <example>
/// <code>
/// GD.PrintT("A", "B", "C"); // Prints A B C
/// </code>
/// </example>
/// <param name="what">Arguments that will be printed.</param>
public static void PrintT(params object[] what)
{
string message = AppendPrintParams('\t', what);
PrintErr(message);
Godot.GD.PrintT(what);
}
/// <summary>
/// Pushes an error message to Godot's built-in debugger and to the OS terminal.
///
/// Note: Errors printed this way will not pause project execution.
/// </summary>
/// <example>
/// <code>
/// GD.PushError("test error"); // Prints "test error" to debugger and terminal as error call
/// </code>
/// </example>
/// <param name="message">Error message.</param>
public static void PushError(string message)
{
Debug.WriteLine("Error: " + message);
Godot.GD.PushError(message);
}
/// <summary>
/// Pushes an error message to Godot's built-in debugger and to the OS terminal.
///
/// Note: Errors printed this way will not pause project execution.
/// </summary>
/// <example>
/// <code>
/// GD.PushError("test_error"); // Prints "test error" to debugger and terminal as error call
/// </code>
/// </example>
/// <param name="what">Arguments that form the error message.</param>
public static void PushError(params object[] what)
{
PushError(AppendPrintParams(what));
}
/// <summary>
/// Pushes a warning message to Godot's built-in debugger and to the OS terminal.
/// </summary>
/// <example>
/// <code>
/// GD.PushWarning("test warning"); // Prints "test warning" to debugger and terminal as warning call
/// </code>
/// </example>
/// <param name="message">Warning message.</param>
public static void PushWarning(string message)
{
Debug.WriteLine("Warning: " + message);
Godot.GD.PushWarning(message);
}
/// <summary>
/// Pushes a warning message to Godot's built-in debugger and to the OS terminal.
/// </summary>
/// <example>
/// <code>
/// GD.PushWarning("test warning"); // Prints "test warning" to debugger and terminal as warning call
/// </code>
/// </example>
/// <param name="what">Arguments that form the warning message.</param>
public static void PushWarning(params object[] what)
{
PushWarning(AppendPrintParams(what));
}
/// <summary>
/// Returns a random floating point value between <c>0.0</c> and <c>1.0</c> (inclusive).
/// </summary>
/// <example>
/// <code>
/// GD.Randf(); // Returns e.g. 0.375671
/// </code>
/// </example>
/// <returns>A random <see langword="float"/> number.</returns>
public static float Randf() => Godot.GD.Randf();
/// <summary>
/// Returns a normally-distributed pseudo-random floating point value
/// using Box-Muller transform with the specified <pararmref name="mean"/>
/// and a standard <paramref name="deviation"/>.
/// This is also called Gaussian distribution.
/// </summary>
/// <returns>A random normally-distributed <see langword="float"/> number.</returns>
public static double Randfn(double mean, double deviation) => Godot.GD.Randfn(mean, deviation);
/// <summary>
/// Returns a random unsigned 32-bit integer.
/// Use remainder to obtain a random value in the interval <c>[0, N - 1]</c>
/// (where N is smaller than 2^32).
/// </summary>
/// <example>
/// <code>
/// GD.Randi(); // Returns random integer between 0 and 2^32 - 1
/// GD.Randi() % 20; // Returns random integer between 0 and 19
/// GD.Randi() % 100; // Returns random integer between 0 and 99
/// GD.Randi() % 100 + 1; // Returns random integer between 1 and 100
/// </code>
/// </example>
/// <returns>A random <see langword="uint"/> number.</returns>
public static uint Randi() => Godot.GD.Randi();
/// <summary>
/// Randomizes the seed (or the internal state) of the random number generator.
/// The current implementation uses a number based on the device's time.
///
/// Note: This method is called automatically when the project is run.
/// If you need to fix the seed to have consistent, reproducible results,
/// use <see cref="Seed(ulong)"/> to initialize the random number generator.
/// </summary>
public static void Randomize() => Godot.GD.Randomize();
/// <summary>
/// Returns a random floating point value between <paramref name="from"/>
/// and <paramref name="to"/> (inclusive).
/// </summary>
/// <example>
/// <code>
/// GD.RandRange(0.0, 20.5); // Returns e.g. 7.45315
/// GD.RandRange(-10.0, 10.0); // Returns e.g. -3.844535
/// </code>
/// </example>
/// <returns>A random <see langword="double"/> number inside the given range.</returns>
public static double RandRange(double from, double to) => Godot.GD.RandRange(from, to);
/// <summary>
/// Returns a random signed 32-bit integer between <paramref name="from"/>
/// and <paramref name="to"/> (inclusive). If <paramref name="to"/> is lesser than
/// <paramref name="from"/>, they are swapped.
/// </summary>
/// <example>
/// <code>
/// GD.RandRange(0, 1); // Returns either 0 or 1
/// GD.RandRange(-10, 1000); // Returns random integer between -10 and 1000
/// </code>
/// </example>
/// <returns>A random <see langword="int"/> number inside the given range.</returns>
public static int RandRange(int from, int to) => Godot.GD.RandRange(from, to);
/// <summary>
/// Given a <paramref name="seed"/>, returns a randomized <see langword="uint"/>
/// value. The <paramref name="seed"/> may be modified.
/// Passing the same <paramref name="seed"/> consistently returns the same value.
///
/// Note: "Seed" here refers to the internal state of the pseudo random number
/// generator, currently implemented as a 64 bit integer.
/// </summary>
/// <example>
/// <code>
/// var a = GD.RandFromSeed(4);
/// </code>
/// </example>
/// <param name="seed">
/// Seed to use to generate the random number.
/// If a different seed is used, its value will be modified.
/// </param>
/// <returns>A random <see langword="uint"/> number.</returns>
public static uint RandFromSeed(ref ulong seed) => Godot.GD.RandFromSeed(ref seed);
/// <summary>
/// Returns a <see cref="IEnumerable{T}"/> that iterates from
/// <c>0</c> (inclusive) to <paramref name="end"/> (exclusive)
/// in steps of <c>1</c>.
/// </summary>
/// <param name="end">The last index.</param>
public static IEnumerable<int> Range(int end) => Godot.GD.Range(end);
/// <summary>
/// Returns a <see cref="IEnumerable{T}"/> that iterates from
/// <paramref name="start"/> (inclusive) to <paramref name="end"/> (exclusive)
/// in steps of <c>1</c>.
/// </summary>
/// <param name="start">The first index.</param>
/// <param name="end">The last index.</param>
public static IEnumerable<int> Range(int start, int end) => Godot.GD.Range(start, end);
/// <summary>
/// Returns a <see cref="IEnumerable{T}"/> that iterates from
/// <paramref name="start"/> (inclusive) to <paramref name="end"/> (exclusive)
/// in steps of <paramref name="step"/>.
/// The argument <paramref name="step"/> can be negative, but not <c>0</c>.
/// </summary>
/// <exception cref="ArgumentException">
/// <paramref name="step"/> is 0.
/// </exception>
/// <param name="start">The first index.</param>
/// <param name="end">The last index.</param>
/// <param name="step">The amount by which to increment the index on each iteration.</param>
public static IEnumerable<int> Range(int start, int end, int step) => Godot.GD.Range(start, end, step);
/// <summary>
/// Sets seed for the random number generator to <paramref name="seed"/>.
/// Setting the seed manually can ensure consistent, repeatable results for
/// most random functions.
/// </summary>
/// <example>
/// <code>
/// ulong mySeed = (ulong)GD.Hash("Godot Rocks");
/// GD.Seed(mySeed);
/// var a = GD.Randf() + GD.Randi();
/// GD.Seed(mySeed);
/// var b = GD.Randf() + GD.Randi();
/// // a and b are now identical
/// </code>
/// </example>
/// <param name="seed">Seed that will be used.</param>
public static void Seed(ulong seed) => Godot.GD.Seed(seed);
/// <summary>
/// Converts a formatted string that was returned by <see cref="VarToStr(Variant)"/>
/// to the original value.
/// </summary>
/// <example>
/// <code>
/// string a = "{ \"a\": 1, \"b\": 2 }"; // a is a string
/// var b = GD.StrToVar(a).AsGodotDictionary(); // b is a Dictionary
/// GD.Print(b["a"]); // Prints 1
/// </code>
/// </example>
/// <param name="str">String that will be converted to Variant.</param>
/// <returns>The decoded <c>Variant</c>.</returns>
public static Variant StrToVar(string str) => Godot.GD.StrToVar(str);
/// <summary>
/// Encodes a <see cref="Variant"/> value to a byte array, without encoding objects.
/// Deserialization can be done with <see cref="BytesToVar"/>.
/// Note: If you need object serialization, see <see cref="VarToBytesWithObjects"/>.
/// </summary>
/// <param name="var"><see cref="Variant"/> that will be encoded.</param>
/// <returns>The <see cref="Variant"/> encoded as an array of bytes.</returns>
public static byte[] VarToBytes(Variant var) => Godot.GD.VarToBytes(var);
/// <summary>
/// Encodes a <see cref="Variant"/>. Encoding objects is allowed (and can potentially
/// include executable code). Deserialization can be done with <see cref="BytesToVarWithObjects"/>.
/// </summary>
/// <param name="var"><see cref="Variant"/> that will be encoded.</param>
/// <returns>The <see cref="Variant"/> encoded as an array of bytes.</returns>
public static byte[] VarToBytesWithObjects(Variant var) => Godot.GD.VarToBytesWithObjects(var);
/// <summary>
/// Converts a <see cref="Variant"/> <paramref name="var"/> to a formatted string that
/// can later be parsed using <see cref="StrToVar(string)"/>.
/// </summary>
/// <example>
/// <code>
/// var a = new Godot.Collections.Dictionary { ["a"] = 1, ["b"] = 2 };
/// GD.Print(GD.VarToStr(a));
/// // Prints:
/// // {
/// // "a": 1,
/// // "b": 2
/// // }
/// </code>
/// </example>
/// <param name="var">Variant that will be converted to string.</param>
/// <returns>The <see cref="Variant"/> encoded as a string.</returns>
public static string VarToStr(Variant var) => Godot.GD.VarToStr(var);
/// <summary>
/// Get the <see cref="Variant.Type"/> that corresponds for the given <see cref="Type"/>.
/// </summary>
/// <returns>The <see cref="Variant.Type"/> for the given <paramref name="type"/>.</returns>
public static Variant.Type TypeToVariantType(Type type) => Godot.GD.TypeToVariantType(type);
}

View File

@@ -0,0 +1 @@
uid://djouyufexm7yv

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Utils;
public partial class Gamble : Control, IScreen
{
public override void _Ready()
{
m_Patches = GetNode<CardRow>("%Patches");
m_Add = GetNode<Button>("%Add");
m_Add.Pressed += OnAddPressed;
m_Skip = GetNode<Button>("%Skip");
m_Skip.Pressed += OnSkipPressed;
m_Patches.Connect(CardRow.SignalName.SelectionChanged, new(this, nameof(OnSelectionChanged)));
}
private void OnSelectionChanged(CardControl[] selection)
{
if (selection.Length != 1)
{
m_BuyCardsCommand = null;
return;
}
m_BuyCardsCommand = new(Main.Instance.CurrentRequest.RequestId, selection.Select(c => m_Patches.OrderedCards.IndexOf(c.Card)).ToArray());
}
private void OnAddPressed()
{
Main.Instance.Execute(m_BuyCardsCommand);
}
private void OnSkipPressed()
{
Main.Instance.Execute(m_StopGamblingCommand);
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_Add.Disabled = false;
m_Skip.Disabled = false;
}
public void DisableInputs()
{
m_Add.Disabled = true;
m_Skip.Disabled = true;
}
public void Activate()
{
}
public bool HandleResult(Result result, Tween tween)
{
switch (result)
{
case ShopResult shopResult:
{
m_Patches.Configure(shopResult.Shop, tween, false, false);
return true;
}
}
return false;
}
public override void _Process(double delta)
{
m_Skip.Disabled = m_StopGamblingCommand == null || !m_StopGamblingCommand.IsValid(Main.Instance.CoreLoop, out _);
m_Add.Disabled = m_BuyCardsCommand == null || !m_BuyCardsCommand.IsValid(Main.Instance.CoreLoop, out _);
}
public void Configure(List<Card> hand, Tween tween)
{
m_Patches.Configure(hand, tween, true, false);
m_StopGamblingCommand = new(Main.Instance.CurrentRequest.RequestId);
}
private BuyCardsCommand m_BuyCardsCommand;
private CardRow m_Patches;
private Button m_Add;
private Button m_Skip;
private StopGamblingCommand m_StopGamblingCommand;
}

View File

@@ -0,0 +1 @@
uid://b7slub0th4nva

View File

@@ -0,0 +1 @@
uid://vct2p2n8n7iq

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;
}

View File

@@ -0,0 +1 @@
uid://c3b2l0t2bjhy5

View File

@@ -0,0 +1,75 @@
using System;
using Godot;
using Array = Godot.Collections.Array;
public static class HexMeshBuilder
{
public static ArrayMesh CreateFlatHexMesh()
{
var uvs = new Vector2[19];
var vertices = new Vector3[19];
var normals = new Vector3[19];
const float height = 0.02f;
vertices[0] = new(0f, height, 0f);
uvs[0] = new(0.5f, 0.5f);
normals[0] = Vector3.Up;
for (var i = 0; i < 18; i++)
{
var angleDeg = 60f * i - 30f;
var angleRad = Mathf.DegToRad(angleDeg);
var x = MathF.Cos(angleRad);
var z = MathF.Sin(angleRad);
var y = i >= 12 ? height : 0.0f;
var s = 1.0f - y;
vertices[i + 1] = new(x * s, y, z * s);
uvs[i + 1] =
i < 6 ? new((i % 6), 0.5f) :
i >= 12 ? new Vector2(x * 0.5f + 0.5f, z * 0.5f + 0.5f) :
new((i % 6), 0.5f + height);
normals[i + 1] =
i < 6 ? Vector3.Up :
i >= 12 ? new Vector3(x, s, z).Normalized() :
new Vector3(x, 0.75f, z).Normalized();
}
var indices = new int[18 * 3];
var idx = 0;
for (var i = 0; i < 6; i++)
{
var curr = i + 1;
var next = (i + 1) % 6 + 1;
indices[idx++] = 0;
indices[idx++] = curr + 12;
indices[idx++] = next + 12;
}
for (var j = 0; j < 6; j++)
{
var curr = j + 7;
var next = (j + 1) % 6 + 7;
indices[idx++] = curr;
indices[idx++] = next;
indices[idx++] = next + 6;
indices[idx++] = curr;
indices[idx++] = next + 6;
indices[idx++] = curr + 6;
}
var arrays = new Array();
arrays.Resize((int)Mesh.ArrayType.Max);
arrays[(int)Mesh.ArrayType.Vertex] = vertices;
arrays[(int)Mesh.ArrayType.Normal] = normals;
arrays[(int)Mesh.ArrayType.Index] = indices;
arrays[(int)Mesh.ArrayType.TexUV] = uvs;
var mesh = new ArrayMesh();
mesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, arrays);
return mesh;
}
}

View File

@@ -0,0 +1 @@
uid://bofj561u323iw

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Godot;
using RobotAndDonkey.Game.Execution.Results;
namespace DonkeysAndDroids
{
public interface IScreen
{
void Deactivate();
void EnableInputs();
void DisableInputs();
void Activate();
bool HandleResult(Result result, Tween tween);
}
}

View File

@@ -0,0 +1 @@
uid://v20uerfcj7lx

View File

@@ -0,0 +1,150 @@
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Execution.Commands;
using System.Collections.Generic;
using System.Linq;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution.Results;
public partial class Improve : Control, IScreen
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
m_ShopCards = GetNode<CardRow>("%Shop");
m_Deck = GetNode<CardRow>("%Deck");
m_BuyButton = GetNode<Button>("%BuyButton");
m_CosmicRaysButton = GetNode<Button>("%CosmicRaysButton");
m_RerollButton = GetNode<Button>("%RerollButton");
m_BufferOverflowButton = GetNode<Button>("%BufferOverflowButton");
m_SkipButton = GetNode<Button>("%SkipButton");
m_BuyButton.Disabled = true;
m_BuyButton.Pressed += OnBuyButtonPressed;
m_CosmicRaysButton.Pressed += OnCosmicRaysButtonPressed;
m_RerollButton.Pressed += OnRerollButtonPressed;
m_BufferOverflowButton.Pressed += OnBufferOverflowButtonPressed;
m_SkipButton.Pressed += OnSkipButtonPressed;
m_ShopCards.Connect(CardRow.SignalName.SelectionChanged, new(this, nameof(OnPatchSelectionChanged)));
}
private void UpdateButtonTexts()
{
m_CosmicRaysButton.Text = $"Cosmic Rays\n({Balancing.Instance.GambleEnergyCost} energy)";
m_RerollButton.Text = $"Reroll ({Balancing.Instance.GetRerollEnergyCost(Main.Instance.CoreLoop.RerollCount)} energy)";
m_BufferOverflowButton.Text = $"Buffer Overflow\n({Balancing.Instance.BufferOverflowEnergyCost} energy)";
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_ShopCards.Disabled = false;
Deck.Disabled = false;
m_BuyButton.Disabled = false;
m_CosmicRaysButton.Disabled = !Main.Instance.CoreLoop.CanGamble;
m_RerollButton.Disabled = false;
m_BufferOverflowButton.Disabled = !Main.Instance.CoreLoop.CanBufferOverflow;
m_SkipButton.Disabled = false;
UpdateButtonTexts();
}
public void DisableInputs()
{
m_ShopCards.Disabled = true;
Deck.Disabled = true;
m_BuyButton.Disabled = true;
m_CosmicRaysButton.Disabled = true;
m_RerollButton.Disabled = true;
m_BufferOverflowButton.Disabled = true;
m_SkipButton.Disabled = true;
}
public void Activate()
{
}
private void OnPatchSelectionChanged(CardControl[] selection)
{
var cost = selection.Sum(c => c.Card.ShopCost);
m_BuyButton.Disabled = cost == 0 || cost > Main.Instance.CoreLoop.Currency.Energy;
m_BuyButton.Text = $"Buy ({cost} energy)";
}
private void OnBuyButtonPressed()
{
var cards = m_ShopCards.SelectedCardIndices.ToArray();
Main.Instance.Execute(new BuyCardsCommand(Main.Instance.CurrentRequest.RequestId, cards));
m_BuyButton.Text = "Buy";
m_BuyButton.Disabled = true;
}
private void OnCosmicRaysButtonPressed()
{
Main.Instance.Execute(new StartGamblingCommand(Main.Instance.CurrentRequest.RequestId));
}
private void OnRerollButtonPressed()
{
Main.Instance.Execute(new RerollCommand(Main.Instance.CurrentRequest.RequestId));
}
private void OnBufferOverflowButtonPressed()
{
Main.Instance.Execute(new StartBufferOverflowCommand(Main.Instance.CurrentRequest.RequestId));
}
private void OnSkipButtonPressed()
{
Main.Instance.Execute(new PreviewProgramCommand(Main.Instance.CurrentRequest.RequestId));
}
public void Configure(IReadOnlyList<Card> shop, IReadOnlyList<Card> patches, Tween tween)
{
m_ShopCards.Configure(shop, tween, false, false);
Deck.Configure(CardRow.GetSortedCards(patches), tween, false, false);
}
public bool HandleResult(Result result, Tween tween)
{
switch (result)
{
case ShopResult shopResult:
{
m_ShopCards.Configure(shopResult.Shop, tween, false, false);
return true;
}
case DeckResult deckResult:
{
Deck.Configure(CardRow.GetSortedCards(deckResult.Deck), tween, false, false);
return true;
}
case ModifyCardResult modifyCardResult:
{
Deck.ModifyCard(modifyCardResult.Card, modifyCardResult.Modifier, tween);
return true;
}
}
return false;
}
public override void _Process(double delta)
{
}
public CardRow Deck => m_Deck;
private CardRow m_Deck;
private CardRow m_ShopCards;
private Button m_BuyButton;
private Button m_CosmicRaysButton;
private Button m_RerollButton;
private Button m_BufferOverflowButton;
private Button m_SkipButton;
}

View File

@@ -0,0 +1 @@
uid://c58lu37yfgtsw

View File

@@ -0,0 +1,47 @@
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Execution.Results;
public partial class LogoScreen : Control, IScreen
{
public override void _Ready()
{
StartButton.Pressed += OnStartButtonPressed;
}
public override void _Process(double delta)
{
}
public void OnStartButtonPressed()
{
GD.Print("Started!");
Main.Instance.StartMetaGame();
}
public void Deactivate()
{
}
public void EnableInputs()
{
StartButton.Disabled = false;
}
public void DisableInputs()
{
StartButton.Disabled = true;
}
public void Activate()
{
}
public bool HandleResult(Result result, Tween tween)
{
return false;
}
[ExportGroup("Buttons")] [Export]
private Button StartButton;
}

View File

@@ -0,0 +1 @@
uid://bj0r4vjvf85dc

View File

@@ -0,0 +1,232 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Execution;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Requests;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Utils;
public partial class Main : CanvasLayer
{
public enum EScreenId
{
Logo,
MetaGame,
CoreLoop,
Victory
}
public override void _Ready()
{
#if DEBUG
if (!Trace.Listeners.OfType<DefaultTraceListener>().Any())
Trace.Listeners.Add(new DefaultTraceListener());
#endif
Instance = this;
m_Runtime = new();
m_Runtime.Published += (_, e) => OnGameEvent(e);
m_Background = GetNode<Background>("Background");
m_LogoScreen = GetNode<LogoScreen>("LogoScreen");
m_MetaGameScreen = GetNode<MetaGameScreen>("MetaGameScreen");
m_CoreLoopScreen = GetNode<CoreLoopScreen>("CoreLoopScreen");
m_VictoryScreen = GetNode<VictoryScreen>("VictoryScreen");
OptionsMenu = GetNode<OptionsMenu>("OptionsMenu");
m_CurrentScreen = m_LogoScreen;
Music.Init(this);
Music.PlaySong(MusicManager.ESong.MetaGame);
}
public void StartMetaGame()
{
Music.PlaySong(MusicManager.ESong.MetaGame);
var tween = GetTree().CreateTween();
TransitionScreen(m_MetaGameScreen, tween);
}
public void TransitionScreen<T>(T newScreen, Tween tween) where T : Control, IScreen
{
newScreen.EnableInputs();
if (m_CurrentScreen == newScreen)
return;
MoveChild(newScreen, 1);
var oldControl = (Control)m_CurrentScreen;
var oldScreen = m_CurrentScreen;
m_CurrentScreen = newScreen;
newScreen.Modulate = new(1, 1, 1);
newScreen.Visible = true;
newScreen.Activate();
tween.TweenProperty(oldControl, "modulate", new Color(0, 0, 0, 0), 0.25);
tween.TweenCallback(Callable.From(() =>
{
oldScreen.Deactivate();
oldControl.Visible = false;
oldControl.Modulate = new(1, 1, 1);
}));
}
public void StartCoreLoop(int seed, EDifficulty difficulty, ERobotType selectedRobot)
{
Seed = seed;
CoreLoop = new(new((ulong)seed), new(selectedRobot, difficulty, seed));
m_Runtime.Start(CoreLoop);
}
private void OnGameEvent(GameEvent gameEvent)
{
switch (gameEvent.Payload)
{
case NextStepIssued { Step: { } request }:
{
m_NextRequest = request;
if (m_Tween != null)
break;
ProcessNextRequest();
break;
}
case StepApplied { Results: var results }:
{
GD.Print("Results available");
foreach (var result in results)
{
Results.Enqueue(result);
}
break;
}
case StateChanged:
{
GD.Print("State changed");
m_Tween = GetTree().CreateTween();
while (Results.TryDequeue(out var result))
{
GD.Print(result.ToString());
if (m_CurrentScreen.HandleResult(result, m_Tween))
m_Tween.SetParallel(false);
else
GD.Print("> Unhandled result!");
}
m_Tween.TweenCallback(Callable.From(() =>
{
m_Tween = null;
ProcessNextRequest();
}));
break;
}
case StepRejected r:
{
GD.Print($"Invalid action: {r.Reason}");
break;
}
case GameOver:
{
GD.Print("Game over");
break;
}
}
}
private void ProcessNextRequest()
{
m_CurrentScreen.DisableInputs();
CurrentRequest = m_NextRequest;
m_NextRequest = null;
GD.Print($"Next step: {CurrentRequest}");
m_Tween = GetTree().CreateTween();
var tween = m_Tween;
switch (CurrentRequest)
{
case DrawGlitchRequest drawCardRequest:
{
Music.PlaySong(MusicManager.ESong.Shop);
TransitionScreen(m_CoreLoopScreen, tween);
m_CoreLoopScreen.DrawGlitch(drawCardRequest.Card, tween);
break;
}
case ImproveRequest:
{
TransitionScreen(m_CoreLoopScreen, tween);
m_CoreLoopScreen.Improve(tween);
break;
}
case GambleRequest:
{
TransitionScreen(m_CoreLoopScreen, tween);
m_CoreLoopScreen.CosmicRays(CoreLoop.BoosterPack, tween);
break;
}
case BufferOverflowRequest:
{
TransitionScreen(m_CoreLoopScreen, tween);
m_CoreLoopScreen.BufferOverflow(tween);
break;
}
case ExecuteProgramRequest:
{
Music.PlaySong(MusicManager.ESong.CoreLoop);
TransitionScreen(m_CoreLoopScreen, tween);
m_CoreLoopScreen.ExecuteProgram(tween);
break;
}
case ScoringRequest:
{
Music.PlaySong(MusicManager.ESong.Scoring);
TransitionScreen(m_VictoryScreen, tween);
m_VictoryScreen.Configure(CoreLoop, tween);
break;
}
}
}
public void Execute(Command command)
{
if (command == null)
return;
GD.Print($"Execute {command}");
m_Runtime.Submit(command);
}
public static Main Instance { get; set; }
public Request CurrentRequest { get; private set; } = Request.s_Empty;
public Queue<Result> Results { get; } = [];
public CoreLoop CoreLoop { get; private set; }
public int Seed { get; private set; }
public string StringSeed => SeedString.ToString(Seed);
public MusicManager Music { get; } = new();
public OptionsMenu OptionsMenu { get; private set; }
private Background m_Background;
private CoreLoopScreen m_CoreLoopScreen;
private IScreen m_CurrentScreen;
private LogoScreen m_LogoScreen;
private MetaGameScreen m_MetaGameScreen;
private Request m_NextRequest;
private GameRuntime m_Runtime;
private Tween m_Tween;
private VictoryScreen m_VictoryScreen;
}

View File

@@ -0,0 +1 @@
uid://b362eqi3w8p1n

View File

@@ -0,0 +1,182 @@
using System;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.Utils;
public partial class MetaGameScreen : Control, IScreen
{
public override void _Ready()
{
m_SeedLineEdit = GetNode<LineEdit>("MarginContainer/MarginContainer/MainVBox/SeedRow/SeedLineEdit");
m_RandomizeButton = GetNode<Button>("MarginContainer/MarginContainer/MainVBox/SeedRow/RandomizeButton");
m_DifficultyOptionButton = GetNode<OptionButton>("MarginContainer/MarginContainer/MainVBox/DifficultyRow/DifficultyOptionButton");
m_StartButton = GetNode<Button>("%StartButton");
m_OptionsButton = GetNode<Button>("%OptionsButton");
m_RobotContainer = GetNode<HBoxContainer>("MarginContainer/MarginContainer/MainVBox/RobotsSection/RobotsContainer");
foreach (var robot in Enum.GetValues<ERobotType>())
{
var currentRobot = robot;
var robotInstance = (Robot)RobotScene.Instantiate<Button>();
robotInstance.Pressed += () => OnRobotButtonPressed(currentRobot);
robotInstance.Name = robot.ToString();
robotInstance.Texture = robot switch
{
ERobotType.Vintage => ResourceLoader.Load<Texture2D>("uid://b51acya8abb3p"),
ERobotType.Courier => ResourceLoader.Load<Texture2D>("uid://c6h1eqa6n2ca8"),
ERobotType.Analyst => ResourceLoader.Load<Texture2D>("uid://c6h1eqa6n2ca8"),
ERobotType.Ranger => ResourceLoader.Load<Texture2D>("uid://c6h1eqa6n2ca8"),
_ => throw new ArgumentOutOfRangeException()
};
m_RobotContainer.AddChild(robotInstance);
}
m_RobotButtons = new Button[m_RobotContainer.GetChildren().Count];
for (var i = 0; i < m_RobotButtons.Length; i++)
{
m_RobotButtons[i] = (Button)m_RobotContainer.GetChildren()[i];
// JAM
if (i > 0)
m_RobotButtons[i].Disabled = true;
}
m_DifficultyOptionButton.Clear();
foreach (var difficulty in Enum.GetValues<EDifficulty>())
m_DifficultyOptionButton.AddItem(difficulty.ToString(), (int)difficulty);
m_DifficultyOptionButton.Selected = 0;
m_RandomizeButton.Pressed += OnRandomizePressed;
m_StartButton.Pressed += OnStartPressed;
m_OptionsButton.Pressed += OnOptionsPressed;
m_SeedLineEdit.TextChanged += OnSeedTextChanged;
m_StartButton.Disabled = true;
OnRandomizePressed();
}
private void OnOptionsPressed()
{
Main.Instance.OptionsMenu.ShowMenu();
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event.IsActionPressed("options") && !m_StartButton.Disabled)
{
Main.Instance.OptionsMenu.ShowMenu();
GetViewport().SetInputAsHandled();
}
}
private void OnRandomizePressed()
{
m_Seed = m_Random.Next(int.MaxValue);
m_SeedLineEdit.Text = SeedString.ToString(m_Seed);
}
private void OnSeedTextChanged(string newText)
{
if (!SeedString.TryParse(newText, out m_Seed, true))
m_Seed = 0;
UpdateStartButtonState();
}
private void OnRobotButtonPressed(ERobotType robot)
{
m_SelectedRobot = robot;
UpdateRobotButtonsVisual();
UpdateStartButtonState();
}
private void UpdateRobotButtonsVisual()
{
var index = 0;
foreach (var node in m_RobotContainer.GetChildren())
{
var robot = (ERobotType)index++;
if (node is not Button button)
continue;
button.ButtonPressed = robot == m_SelectedRobot;
}
}
private void UpdateStartButtonState()
{
var hasSeed = m_Seed > 0;
var hasRobot = m_SelectedRobot >= 0;
m_StartButton.Disabled = !(hasSeed && hasRobot);
}
private void OnStartPressed()
{
if (m_StartButton.Disabled)
return;
var seed = m_SeedLineEdit.Text.Trim();
var difficulty = (EDifficulty)m_DifficultyOptionButton.Selected;
GD.Print($"Starting game with Seed={seed}, Difficulty={difficulty}, Robot={m_SelectedRobot}");
Main.Instance.StartCoreLoop(m_Seed, difficulty, m_SelectedRobot);
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_DifficultyOptionButton.Disabled = false;
m_RandomizeButton.Disabled = false;
m_StartButton.Disabled = false;
// JAM
m_RobotButtons[0].Disabled = false;
//foreach (var button in m_RobotButtons)
// button.Disabled = false;
m_SeedLineEdit.Editable = true;
}
public void DisableInputs()
{
m_DifficultyOptionButton.Disabled = true;
m_RandomizeButton.Disabled = true;
m_StartButton.Disabled = true;
foreach (var button in m_RobotButtons)
button.Disabled = true;
m_SeedLineEdit.Editable = false;
}
public void Activate()
{
}
public bool HandleResult(Result result, Tween tween)
{
return false;
}
[Export]
public PackedScene RobotScene { get; set; }
public string StringSeed => SeedString.ToString(m_Seed);
private OptionButton m_DifficultyOptionButton;
private Button m_RandomizeButton;
private Button m_OptionsButton;
private Button m_StartButton;
private Button[] m_RobotButtons;
private HBoxContainer m_RobotContainer;
private LineEdit m_SeedLineEdit;
private SRandom m_Random = new((ulong)DateTime.UtcNow.ToFileTimeUtc());
private int m_Seed;
private ERobotType m_SelectedRobot = ERobotType.Vintage;
}

View File

@@ -0,0 +1 @@
uid://bmxs8bmhtxvxw

View File

@@ -0,0 +1,64 @@
using System;
using Godot;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Modifiers;
public partial class ModifierIcon : TextureRect
{
public override void _Ready()
{
UpdateTooltip();
}
public void Configure(Modifier modifier)
{
Name = $"Modifier_{modifier.Id}";
m_Modifier = modifier;
Refresh();
}
public void Refresh()
{
if (m_Modifier == null)
return;
Texture = GD.Load<Texture2D>(m_Modifier.Id switch
{
EModifierId.Corrupt => "res://images/corrupt.png",
EModifierId.Unreliable => "res://images/unreliable.png",
EModifierId.RaceCondition => "res://images/race-condition.png",
EModifierId.Throttled => "res://images/throttled.png",
EModifierId.Effective => "res://images/effective.png",
EModifierId.Optimized => "res://images/optimized.png",
EModifierId.Efficient => "res://images/efficient.png",
EModifierId.Persistent => "res://images/persistent.png",
EModifierId.Analytic => string.Empty, //"res://images/analytic.png",
EModifierId.Rain => "res://images/rain.png",
EModifierId.Drought => "res://images/drought.png",
EModifierId.Pest => "res://images/pest.png",
EModifierId.Gravity => "res://images/gravity.png",
EModifierId.HeatWave => "res://images/heat-wave.png",
EModifierId.CourierOverspill => string.Empty, //"res://images/courier-overspill.png",
EModifierId.RangerFertileRest => string.Empty, //"res://images/ranger-fertile-rest.png",
EModifierId.GlobalImmunity => string.Empty, //"res://images/global-immunity.png",
_ => throw new ArgumentOutOfRangeException()
});
Description = $"{m_Modifier.Id}\n{m_Modifier.ToolTip}";
UpdateTooltip();
}
private void UpdateTooltip()
{
if (!string.IsNullOrEmpty(Description))
TooltipText = Description;
else
TooltipText = m_Modifier.Id.ToString();
}
[Export(PropertyHint.MultilineText)]
public string Description { get; set; } = string.Empty;
public Modifier Modifier => m_Modifier;
private Modifier m_Modifier;
}

View File

@@ -0,0 +1 @@
uid://bl8mg6iej6u01

View File

@@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3 uid="uid://yo336l0oucfq"]
[ext_resource type="Texture2D" uid="uid://c7wwcxywr2nh7" path="res://images/efficient.png" id="1_icon"]
[ext_resource type="Script" uid="uid://bl8mg6iej6u01" path="res://ModifierIcon.cs" id="2_script"]
[node name="ModifierIcon" type="TextureRect"]
texture_filter = 1
custom_minimum_size = Vector2(32, 32)
offset_right = 32.0
offset_bottom = 32.0
size_flags_vertical = 0
texture = ExtResource("1_icon")
expand_mode = 5
stretch_mode = 5
script = ExtResource("2_script")

View File

@@ -0,0 +1,170 @@
using Godot;
using RobotAndDonkey.Game;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
using System;
using System.Diagnostics;
public class MusicManager
{
public enum ESong
{
MetaGame,
Shop,
CoreLoop,
Scoring
}
public enum ESound
{
Energy,
Carry,
Delivery,
TapeLength,
Hand,
Program,
Card,
Rest
}
public void Init(Main main)
{
m_Player1 = new() { Bus = "Music" };
main.AddChild(m_Player1);
m_Player2 = new() { Bus = "Music" };
main.AddChild(m_Player2);
m_SfxPlayer = new() { Bus = "Sfx" };
main.AddChild(m_SfxPlayer);
m_Tree = main.GetTree();
}
public void PlaySong(ESong song)
{
if (m_Song == song)
return;
var tween = m_Tree.CreateTween();
tween.SetParallel();
m_Song = song;
if (m_CurrentPlayer)
CrossFade(m_Player1, m_Player2, song, tween);
else
CrossFade(m_Player2, m_Player1, song, tween);
m_CurrentPlayer = !m_CurrentPlayer;
}
private static void EnableLooping(AudioStream stream)
{
if (stream is AudioStreamOggVorbis ogg)
{
ogg.Loop = true;
}
else if (stream is AudioStreamMP3 mp3)
{
mp3.Loop = true;
}
else if (stream is AudioStreamWav wav)
{
wav.LoopMode = AudioStreamWav.LoopModeEnum.Forward;
}
}
public void Play(EModifierId modifier)
{
m_SfxPlayer.Stream = ResourceLoader.Load<AudioStream>(modifier switch
{
EModifierId.Corrupt => "uid://dnc8ksbbt8rxg",
EModifierId.Unreliable => "uid://b8cgqw2meefkw",
EModifierId.RaceCondition => "uid://c5061hog183xe",
EModifierId.Throttled => "uid://djkcjn5niyhje",
EModifierId.Effective => "uid://7toyoaj4xv5t",
EModifierId.Optimized => "uid://cij77w63o5ahu",
EModifierId.Efficient => "uid://iccli4sklikc",
EModifierId.Persistent => "uid://bmkqvi2y64wxk",
_ => null
});
if (m_SfxPlayer.Stream != null)
m_SfxPlayer.Play();
}
public void Play(ESound sound)
{
m_SfxPlayer.Stream = ResourceLoader.Load<AudioStream>(sound switch
{
ESound.Energy => "uid://prjkw6o6m53s",
ESound.Carry => "uid://taabst8vqf07",
ESound.Delivery => "uid://cr7ajk4lac6e0",
ESound.TapeLength => "uid://bvt2nl4geonil",
ESound.Hand => "uid://bu2tncw11cgqt",
ESound.Program => "uid://6cqu6nualhm2",
ESound.Card => "uid://b7508mnlvtm26",
ESound.Rest => "uid://bp1dme3dnek76",
_ => throw new ArgumentOutOfRangeException(nameof(sound), sound, null)
});
m_SfxPlayer.Play();
}
public void Play(ECellType cellType)
{
m_SfxPlayer.Stream = ResourceLoader.Load<AudioStream>(cellType switch
{
ECellType.Grass => "uid://xc27unw7jisd",
ECellType.Dry => "uid://b1licxrv5a5kx",
ECellType.Fertile => "uid://ufefxts5f0op",
ECellType.Mud => "uid://bkr83d0ylrgoh",
ECellType.Blocked => "uid://fvia4q78bwmj",
ECellType.Rocky => "uid://c22k1uvleut6w",
_ => throw new ArgumentOutOfRangeException(nameof(cellType), cellType, null)
});
m_SfxPlayer.Play();
}
public void Play(Poi poi)
{
m_SfxPlayer.Stream = ResourceLoader.Load<AudioStream>(poi switch
{
Crate => "uid://2g4nuqy03if5",
Shed => "uid://bv03vbh5us6ym",
Tower => "uid://dqolgkxab6gfy",
Donkey => "uid://bg8eu2auayu2k",
Avatar => "uid://nw8iofm1vaj8",
_ => throw new NotImplementedException()
});
m_SfxPlayer.Play();
}
private void CrossFade(AudioStreamPlayer player1, AudioStreamPlayer player2, ESong song, Tween tween)
{
var stream = GetStream(song);
EnableLooping(stream);
player2.Stream = stream;
player2.Play();
tween.TweenProperty(player1, "volume_db", -80.0f, 1.0).SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(player2, "volume_db", 0.0f, 1.0).SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Expo);
}
private AudioStream GetStream(ESong song)
{
return ResourceLoader.Load<AudioStream>(song switch
{
ESong.MetaGame => "uid://bnrm544axjxoi",
ESong.Shop => "uid://62fyj028yf7w",
ESong.CoreLoop => "uid://5w305h2two2l",
ESong.Scoring => "uid://b4qv7oicdvqg6",
_ => throw new NotImplementedException()
});
}
private bool m_CurrentPlayer;
private AudioStreamPlayer m_Player1;
private AudioStreamPlayer m_Player2;
private AudioStreamPlayer m_SfxPlayer;
private ESong m_Song = (ESong)(-1);
private SceneTree m_Tree;
}

View File

@@ -0,0 +1 @@
uid://b62jg2i30qsu7

View File

@@ -0,0 +1,104 @@
using Godot;
using RobotAndDonkey.Game.Pois;
public partial class OptionsMenu : Control
{
public override void _Ready()
{
m_MasterSlider = GetNode<HSlider>("%MasterSlider");
m_MusicSlider = GetNode<HSlider>("%MusicSlider");
m_SfxSlider = GetNode<HSlider>("%SfxSlider");
m_GameSpeedSlider = GetNode<HSlider>("%GameSpeedSlider");
m_TutorialScreen = GetNode<TutorialScreen>("%TutorialScreen");
m_ContinueButton = GetNode<Button>("%ContinueButton");
m_ExitToMainButton = GetNode<Button>("%ExitToMainButton");
m_ExitGameButton = GetNode<Button>("%ExitGameButton");
m_TutorialButton = GetNode<Button>("%TutorialButton");
m_MasterSlider.ValueChanged += OnMasterSliderChanged;
m_MusicSlider.ValueChanged += OnMusicSliderChanged;
m_SfxSlider.ValueChanged += OnSfxSliderChanged;
m_GameSpeedSlider.ValueChanged += OnGameSpeedSliderChanged;
m_ContinueButton.Pressed += OnContinuePressed;
m_ExitToMainButton.Pressed += OnExitToMainPressed;
m_ExitGameButton.Pressed += OnExitGamePressed;
m_TutorialButton.Pressed += OnTutorialPressed;
HideMenu();
}
public void ShowMenu()
{
Visible = true;
var settings = SettingsManager.Instance;
if (settings != null)
{
m_MasterSlider.Value = settings.MasterVolumeDb;
m_MusicSlider.Value = settings.MusicVolumeDb;
m_SfxSlider.Value = settings.SfxVolumeDb;
m_GameSpeedSlider.Value = settings.GameSpeed;
}
m_MasterSlider.GrabFocus();
}
public void HideMenu()
{
Visible = false;
}
private void OnMasterSliderChanged(double value)
{
SettingsManager.Instance?.SetMasterVolume((float)value);
}
private void OnMusicSliderChanged(double value)
{
SettingsManager.Instance?.SetMusicVolume((float)value);
}
private void OnSfxSliderChanged(double value)
{
SettingsManager.Instance?.SetSfxVolume((float)value);
Main.Instance.Music.Play(new Donkey());
}
private void OnGameSpeedSliderChanged(double value)
{
SettingsManager.Instance?.SetGameSpeed((float)value);
}
private void OnContinuePressed()
{
HideMenu();
}
private void OnExitToMainPressed()
{
Main.Instance.StartMetaGame();
HideMenu();
}
private void OnExitGamePressed()
{
GetTree().Quit();
}
private void OnTutorialPressed()
{
m_TutorialScreen.Restart();
m_TutorialScreen.Visible = true;
}
private TutorialScreen m_TutorialScreen;
private Button m_ContinueButton;
private Button m_ExitGameButton;
private Button m_ExitToMainButton;
private Button m_TutorialButton;
private HSlider m_MasterSlider;
private HSlider m_MusicSlider;
private HSlider m_SfxSlider;
private HSlider m_GameSpeedSlider;
}

View File

@@ -0,0 +1 @@
uid://1fyo1un7vvcl

View File

@@ -0,0 +1,243 @@
using System.Collections.Generic;
using System.Linq;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Execution;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.Utils;
public partial class ProgramScreen : Control, IScreen
{
public override void _Ready()
{
m_ProgramRow = GetNode<CardRow>("%ProgramRow");
m_Execute = GetNode<Button>("%Execute");
m_Discard = GetNode<Button>("%Discard");
m_Sort = GetNode<Button>("%Sort");
m_TapeLabel = GetNode<Label>("%TapeLabel");
m_HandLabel = GetNode<Label>("%HandLabel");
m_FlameBackground = GetNode<Node2D>("%FlameBackground");
m_Execute.Pressed += OnExecutePressed;
m_Discard.Pressed += OnDiscardPressed;
m_Sort.Pressed += OnSortPressed;
m_ProgramRow.Connect(CardRow.SignalName.SelectionChanged, new(this, nameof(OnSelectionChanged)));
m_ProgramRow.Connect(CardRow.SignalName.OrderChanged, new(this, nameof(OnOrderChanged)));
m_ProgramRow.CanChangeSelection = CanChangeSelection;
}
private void OnOrderChanged()
{
UpdateLabels();
SendMoveCommand();
}
private void SendMoveCommand()
{
if (Main.Instance.CurrentRequest == null)
return;
var ordered = m_ProgramRow.OrderedCards.Select(card => card.CardId).ToArray();
var selected = m_ProgramRow.SelectedCardIds.ToArray();
Main.Instance.Execute(new MoveCardsCommand(Main.Instance.CurrentRequest.RequestId, ordered, selected));
}
private void UpdateLabels()
{
var selectedIds = m_ProgramRow.SelectedCardIds.ToHashSet();
var tapeSpace = 0;
var handSpace = 0;
foreach (var card in m_ProgramRow.OrderedCards)
{
if (selectedIds.Contains(card.CardId))
tapeSpace += card.OccupiedSpace;
else
handSpace += card.OccupiedSpace;
}
m_TapeLabel.Text = $"{tapeSpace} / {Main.Instance.CoreLoop.Currency.TapeLength}";
m_HandLabel.Text = $"{handSpace} / {Main.Instance.CoreLoop.Currency.HandSize}";
m_Discard.Text = Main.Instance.CoreLoop.PatchDeck.Count.ToString();
m_Discard.TooltipText = $"Discard {selectedIds.Count} instructions. Costs {Balancing.Instance.DiscardEnergyCost} each.";
}
private void RefreshDiscardCommand()
{
if (Main.Instance.CurrentRequest == null)
{
m_DiscardCommand = null;
return;
}
m_DiscardCommand = new(Main.Instance.CurrentRequest.RequestId, m_ProgramRow.SelectedCardIds.ToArray());
if (m_Discard != null)
m_Discard.Disabled = m_DiscardCommand == null || !m_DiscardCommand.IsValid(Main.Instance.CoreLoop, out _);
}
private void OnSelectionChanged(CardControl[] cardControls)
{
RefreshDiscardCommand();
UpdateLabels();
SendMoveCommand();
}
private bool CanChangeSelection(CardControl cardControl, bool selected)
{
var selectedIds = m_ProgramRow.SelectedCardIds.ToHashSet();
if (selected)
selectedIds.Add(cardControl.Card.CardId);
else
selectedIds.Remove(cardControl.Card.CardId);
var tapeSpace = 0;
var handSpace = 0;
foreach (var card in m_ProgramRow.OrderedCards)
{
if (selectedIds.Contains(card.CardId))
tapeSpace += card.OccupiedSpace;
else
handSpace += card.OccupiedSpace;
}
return tapeSpace <= Main.Instance.CoreLoop.Currency.TapeLength && handSpace <= Main.Instance.CoreLoop.Currency.HandSize;
}
private void OnExecutePressed()
{
Main.Instance.Execute(m_RunProgramCommand);
}
private void OnDiscardPressed()
{
Main.Instance.Execute(m_DiscardCommand);
}
private void OnSortPressed()
{
var selectedIds = m_ProgramRow.SelectedCardIds.ToHashSet();
var unselected = m_ProgramRow.OrderedCards.Where(card => !selectedIds.Contains(card.CardId)).ToList();
var sortedUnselected = CardExtensions.SortForHand(unselected);
var ordered = new List<Card>(m_ProgramRow.OrderedCards.Count);
var unselectedIndex = 0;
foreach (var card in m_ProgramRow.OrderedCards)
{
if (selectedIds.Contains(card.CardId))
{
ordered.Add(card);
continue;
}
ordered.Add(sortedUnselected[unselectedIndex]);
unselectedIndex += 1;
}
m_ProgramRow.Configure(ordered, GetTree().CreateTween(), false, false);
m_ProgramRow.ApplySelection(selectedIds, true);
OnOrderChanged();
}
public void Deactivate()
{
}
public void EnableInputs()
{
m_RunProgramCommand ??= new(Main.Instance.CurrentRequest.RequestId);
RefreshDiscardCommand();
m_Sort.Disabled = false;
}
public void DisableInputs()
{
m_RunProgramCommand = null;
m_DiscardCommand = null;
m_Sort.Disabled = true;
}
public void Activate()
{
}
public bool HandleResult(Result result, Tween tween)
{
switch (result)
{
case RunCardResult runCardResult:
{
m_ProgramRow.RunCard(runCardResult.Card, tween);
RefreshDiscardCommand();
UpdateLabels();
return true;
}
case ProgramRowResult programRowResult:
{
m_ProgramRow.Configure(programRowResult.OrderedCards, tween, false, false);
m_ProgramRow.ApplySelection(programRowResult.TapeCardIds, true);
RefreshDiscardCommand();
UpdateLabels();
return true;
}
case ModifyCardResult modifyCardResult:
{
m_ProgramRow.ModifyCard(modifyCardResult.Card, modifyCardResult.Modifier, tween);
UpdateLabels();
return true;
}
case HandResult:
case TapeResult:
{
UpdateLabels();
return true;
}
}
return false;
}
public override void _Process(double delta)
{
if (m_Execute != null)
{
m_Execute.Disabled = m_RunProgramCommand == null;
var lastRun = m_RunProgramCommand?.Preview(Main.Instance.CoreLoop).Any(r => r is RunPhaseResult { NewRunPhase: ERunPhase.Scoring }) ?? false;
if (lastRun)
{
m_FlameBackground.Visible = true;
m_Execute.TooltipText = "Execute last program";
}
else
{
m_FlameBackground.Visible = false;
m_Execute.TooltipText = "Execute";
}
}
}
public void Configure(Tween tween)
{
var coreLoop = Main.Instance.CoreLoop;
var ordered = coreLoop.ProgramRow.ToList();
m_ProgramRow.Configure(ordered, tween, false, false);
m_ProgramRow.ApplySelection(coreLoop.TapeCardIds.ToArray(), true);
m_RunProgramCommand = new(Main.Instance.CurrentRequest.RequestId);
RefreshDiscardCommand();
UpdateLabels();
}
private RunProgramCommand m_RunProgramCommand;
private DiscardCommand m_DiscardCommand;
private CardRow m_ProgramRow;
private Button m_Execute;
private Button m_Discard;
private Button m_Sort;
private Label m_TapeLabel;
private Label m_HandLabel;
private Node2D m_FlameBackground;
}

View File

@@ -0,0 +1 @@
uid://vdiphjbwoqh1

View File

@@ -0,0 +1,16 @@
{
"profiles": {
"Godot (console)": {
"commandName": "Executable",
"executablePath": "D:\\Code\\Godot_v4.5.1-stable_mono_win64\\Godot_v4.5.1-stable_mono_win64_console.exe",
"workingDirectory": ".",
"nativeDebugging": true
},
"Godot (debug)": {
"commandName": "Executable",
"executablePath": "D:\\Code\\Godot_v4.5.1-stable_mono_win64\\Godot_v4.5.1-stable_mono_win64.exe",
"workingDirectory": ".",
"nativeDebugging": false
}
}
}

View File

@@ -0,0 +1,37 @@
using Godot;
public partial class Robot : Button
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
m_TextureRect = GetNode<TextureRect>("%TextureRect");
m_Label = GetNode<Label>("%Label");
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (m_TextureRect != null)
m_TextureRect.Texture = Texture;
if (m_Label != null)
m_Label.Text = Name;
}
[Export]
public Texture2D Texture
{
get;
set;
}
[Export]
public string RobotName
{
get;
set;
} = "Robot";
private Label m_Label;
private TextureRect m_TextureRect;
}

View File

@@ -0,0 +1 @@
uid://cxg6ixmxgaxtw

View File

@@ -0,0 +1,117 @@
using Godot;
public partial class SettingsManager : Node
{
public override void _EnterTree()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
}
public override void _Ready()
{
LoadSettings();
ApplyVolumes();
}
private void LoadSettings()
{
var config = new ConfigFile();
var err = config.Load(c_ConfigPath);
if (err == Error.Ok)
{
MasterVolumeDb = (float)config.GetValue(c_SectionAudio, "master_db", c_DefaultMasterVolume);
MusicVolumeDb = (float)config.GetValue(c_SectionAudio, "music_db", c_DefaultMusicVolume);
SfxVolumeDb = (float)config.GetValue(c_SectionAudio, "sfx_db", c_DefaultSfxVolume);
GameSpeed = (float)config.GetValue(c_SectionGame, "speed", c_DefaultGameSpeed);
}
else
{
MasterVolumeDb = c_DefaultMasterVolume;
MusicVolumeDb = c_DefaultMusicVolume;
SfxVolumeDb = c_DefaultSfxVolume;
GameSpeed = c_DefaultGameSpeed;
SaveSettings();
}
}
private void SaveSettings()
{
var config = new ConfigFile();
config.SetValue(c_SectionAudio, "master_db", MasterVolumeDb);
config.SetValue(c_SectionAudio, "music_db", MusicVolumeDb);
config.SetValue(c_SectionAudio, "sfx_db", SfxVolumeDb);
config.SetValue(c_SectionGame, "speed", GameSpeed);
config.Save(c_ConfigPath);
}
public void SetMasterVolume(float db)
{
MasterVolumeDb = db;
ApplyVolumes();
SaveSettings();
}
public void SetMusicVolume(float db)
{
MusicVolumeDb = db;
ApplyVolumes();
SaveSettings();
}
public void SetSfxVolume(float db)
{
SfxVolumeDb = db;
ApplyVolumes();
SaveSettings();
}
public void SetGameSpeed(float gameSpeed)
{
GameSpeed = gameSpeed;
SaveSettings();
}
private void ApplyVolumes()
{
var masterBus = AudioServer.GetBusIndex("Master");
var musicBus = AudioServer.GetBusIndex("Music");
var sfxBus = AudioServer.GetBusIndex("Sfx");
if (masterBus != -1)
AudioServer.SetBusVolumeDb(masterBus, MasterVolumeDb);
if (musicBus != -1)
AudioServer.SetBusVolumeDb(musicBus, MusicVolumeDb);
if (sfxBus != -1)
AudioServer.SetBusVolumeDb(sfxBus, SfxVolumeDb);
}
public static SettingsManager Instance { get; private set; }
public float MasterVolumeDb { get; private set; }
public float MusicVolumeDb { get; private set; }
public float SfxVolumeDb { get; private set; }
public float GameSpeed { get; private set; }
private const float c_DefaultMusicVolume = -16.0f;
private const float c_DefaultSfxVolume = 0.0f;
private const float c_DefaultMasterVolume = 0.0f;
private const float c_DefaultGameSpeed = 1.0f;
private const string c_ConfigPath = "user://settings.cfg";
private const string c_SectionAudio = "audio";
private const string c_SectionGame = "game";
}

View File

@@ -0,0 +1 @@
uid://bkhsu7257yto6

View File

@@ -0,0 +1,349 @@
using System;
using System.Text;
using Godot;
using RobotAndDonkey.Game.Board;
using RobotAndDonkey.Game.Cards;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.GameState;
using RobotAndDonkey.Game.Modifiers;
using RobotAndDonkey.Game.Pois;
public partial class Tooltip : Control
{
public override void _Ready()
{
Instance = this;
m_Label = GetNode<RichTextLabel>("RichText");
m_Label.BbcodeEnabled = true;
m_Label.AutowrapMode = TextServer.AutowrapMode.Word;
m_Label.ScrollActive = true;
}
public override void _Process(double delta)
{
UpdateGamepadScroll((float)delta);
}
private void UpdateGamepadScroll(float delta)
{
if (m_Label == null || !Visible)
return;
var axis = Input.GetActionStrength("tooltip_scroll_down") - Input.GetActionStrength("tooltip_scroll_up");
if (Mathf.Abs(axis) < 0.1f)
return;
var vbar = m_Label.GetVScrollBar();
if (vbar == null)
return;
vbar.Value += axis * GamepadScrollSpeed * delta;
}
public void Describe(Cell cell)
{
Begin();
AppendCell(cell);
Commit();
}
public void Describe(Card card)
{
Begin();
AppendCard(card);
Commit();
}
private void Begin()
{
m_Builder.Clear();
}
private void Commit()
{
if (m_Label == null)
return;
m_Label.Text = m_Builder.ToString();
m_Label.ScrollToLine(0);
}
private static string WrapColor(string text, string colorHex)
{
return $"[color={colorHex}]{text}[/color]";
}
private void AppendTitle(string title, string subtitle = null)
{
m_Builder.Append(WrapColor($"[b]{title}[/b]", ColorTitle));
if (!string.IsNullOrEmpty(subtitle))
{
m_Builder.Append(' ');
m_Builder.Append(WrapColor(subtitle, ColorSubtitle));
}
m_Builder.Append('\n');
}
private void AppendSectionHeader(string title)
{
m_Builder.Append('\n');
m_Builder.Append(WrapColor($"[b]{title}[/b]", ColorHeader));
m_Builder.Append('\n');
}
private void AppendKeyValue(string key, string value)
{
m_Builder.Append(WrapColor($"{key}: ", ColorLabel)).Append(WrapColor(value, ColorText)).Append('\n');
}
private void AppendBlankLine()
{
m_Builder.Append('\n');
}
private void AppendCell(Cell cell)
{
var subtitle = cell.Hex.ToString();
AppendTitle("Cell", subtitle);
var surfaceColor = GetCellColor(cell.Type);
AppendKeyValue("Surface", WrapColor(cell.Type.ToString(), surfaceColor));
m_Builder.Append(WrapColor(GetCellDescription(cell.Type), ColorText));
m_Builder.Append('\n');
if (cell.Poi != null)
{
Main.Instance.Music.Play(cell.Poi);
AppendPoi(cell.Poi);
}
else
{
Main.Instance.Music.Play(cell.Type);
}
AppendModifiers("Cell", cell);
}
public static string GetCellDescription(ECellType cellType)
{
return cellType switch
{
ECellType.Grass => "Plain and simple terrain, suitable for most tasks.",
ECellType.Dry => $"Resting replenishes {WrapColor($"{Balancing.Instance.DryRestEnergyMalus} less energy", ColorBad)} and interacting costs {WrapColor($"{Balancing.Instance.DryInteractEnergyMalus} more energy", ColorBad)}.",
ECellType.Fertile => $"Replenish {WrapColor($"{Balancing.Instance.FertileRestEnergyReplenish} more energy", ColorGood)} while resting here.",
ECellType.Mud => $"Movement is very difficult and costs {WrapColor($"{Balancing.Instance.MudMoveEnergyCost} more energy", ColorBad)}.",
ECellType.Blocked => "Can't walk here, try jumping or flying.",
ECellType.Rocky => "When entering, programs have a high risk of race conditions.",
_ => throw new ArgumentOutOfRangeException(nameof(cellType), cellType, null)
};
}
private string GetCellColor(ECellType type)
{
return type switch
{
ECellType.Grass => "#9ccc65",
ECellType.Dry => "#ffcc80",
ECellType.Fertile => "#a5d6a7",
ECellType.Mud => "#8d6e63",
ECellType.Blocked => "#b0bec5",
ECellType.Rocky => "#b39ddb",
_ => ColorText
};
}
private void AppendPoi(Poi poi)
{
switch (poi)
{
case Avatar:
AppendRobot(Main.Instance.CoreLoop.Robot);
break;
case Shed shed:
{
var summary = $"Requested {shed.Requested}\n" + $"Received {shed.Received}\n" + $"Remaining {shed.Remaining}";
AppendPoiBlock("Shed", summary, poi);
break;
}
case Crate crate:
AppendPoiBlock("Crate", $"Contains {crate.Amount} goods", poi);
break;
case Tower tower:
AppendPoiBlock("Tower", tower.Active ? WrapColor("Active", ColorBad) : WrapColor("Disabled", ColorGood), poi);
break;
case Donkey:
AppendPoiBlock("Donkey", string.Empty, poi);
break;
default:
AppendPoiBlock(poi.GetType().Name, string.Empty, poi);
break;
}
}
private void AppendPoiBlock(string title, string body, Poi poi)
{
AppendSectionHeader(title);
m_Builder.Append("[indent]\n");
if (!string.IsNullOrWhiteSpace(body))
{
m_Builder.Append(WrapColor(body, ColorText));
m_Builder.Append("\n\n");
}
if (!string.IsNullOrWhiteSpace(poi.Tooltip))
{
m_Builder.Append(WrapColor(poi.Tooltip, ColorText));
m_Builder.Append('\n');
var tooltipModifiers = poi.TooltipModifiers;
if (tooltipModifiers.Length > 0)
{
AppendSectionHeader("Related:");
foreach (var modifier in tooltipModifiers)
AppendModifier(modifier);
}
}
m_Builder.Append("[/indent]\n");
}
private void AppendRobot(RobotAndDonkey.Game.Robots.Robot robot)
{
AppendSectionHeader(Main.Instance.CoreLoop.Robot.Type.ToString());
m_Builder.Append("[indent]\n");
AppendKeyValue("Programs", robot.ProgramCount.ToString());
AppendCurrency(robot.Currency);
AppendModifiers("Robot", robot);
m_Builder.Append("[/indent]\n");
}
private void AppendCurrency(Currency currency)
{
AppendKeyValue("Remaining energy", currency.Energy.ToString());
AppendKeyValue("Good carried", $"{currency.Carry}/{currency.MaxCarry}");
AppendKeyValue("Completed deliveries", currency.Delivery.ToString());
AppendKeyValue("Tape length", currency.TapeLength.ToString());
AppendKeyValue("Hand size", currency.HandSize.ToString());
}
private void AppendCard(Card card)
{
var rarityColor = GetRarityColor(card.Rarity);
var rarityText = WrapColor(card.Rarity.ToString(), rarityColor);
var subtitle = $"\n{rarityText} {card.CardType}";
AppendTitle(card.Name, subtitle);
AppendSectionHeader("Costs");
m_Builder.Append("[indent]\n");
AppendKeyValue("Shop", card.ShopCost.ToString());
AppendKeyValue("Play", card.PlayCost.ToString());
m_Builder.Append("[/indent]\n");
if (!string.IsNullOrWhiteSpace(card.ToolTip))
{
AppendSectionHeader("Effect");
m_Builder.Append("[indent]\n");
m_Builder.Append(WrapColor(card.ToolTip, ColorText));
m_Builder.Append('\n');
var tooltipModifiers = card.TooltipModifiers;
if (tooltipModifiers.Length > 0)
{
AppendSectionHeader("Related:");
foreach (var modifier in tooltipModifiers)
AppendModifier(modifier);
}
m_Builder.Append("[/indent]\n");
}
AppendModifiers("Card", card);
}
private string GetRarityColor(ERarity rarity)
{
return rarity switch
{
ERarity.Common => "#FFFFFF",
ERarity.Magic => "#4CFF4C",
ERarity.Uncommon => "#4C99FF",
ERarity.Rare => "#CC4CFF",
ERarity.Legendary => "#FF8033",
_ => ColorText
};
}
private void AppendModifiers(string entityName, Entity entity)
{
if (entity.Modifiers.Count == 0)
return;
AppendSectionHeader($"{entityName} modifiers");
m_Builder.Append("[indent]\n");
foreach (var modifier in entity.Modifiers)
AppendModifier(modifier);
m_Builder.Append("[/indent]\n");
}
private void AppendModifier(Modifier modifier)
{
var debuffed = modifier.DebuffSources.Count > 0;
var header = $"{modifier.Id} [{modifier.Duration}]";
if (debuffed)
{
m_Builder.Append(WrapColor($"[s]{header}[/s]", ColorMuted));
}
else
{
m_Builder.Append(WrapColor(header, ColorModifier));
}
m_Builder.Append('\n');
if (!string.IsNullOrWhiteSpace(modifier.ToolTip))
{
m_Builder.Append(WrapColor(modifier.ToolTip, ColorText));
m_Builder.Append('\n');
}
if (debuffed)
{
m_Builder.Append(WrapColor(" (Debuffed)", ColorMuted));
m_Builder.Append('\n');
}
}
public static Tooltip Instance { get; private set; }
private const string ColorTitle = "#ffd65c";
private const string ColorSubtitle = "#aaaaaa";
private const string ColorHeader = "#a5d6ff";
private const string ColorLabel = "#cccccc";
private const string ColorText = "#ffffff";
private const string ColorGood = "#a5d6a7";
private const string ColorBad = "#ef9a9a";
private const string ColorMuted = "#9e9e9e";
private const string ColorModifier = "#ffcc80";
private readonly StringBuilder m_Builder = new();
[Export]
public float GamepadScrollSpeed = 800f;
private RichTextLabel m_Label;
}

View File

@@ -0,0 +1 @@
uid://b1g3ewcbtqagh

View File

@@ -0,0 +1,98 @@
using System;
using System.Linq;
using Godot;
using RobotAndDonkey.Game.Board;
public partial class TutorialScreen : Control
{
public override void _Ready()
{
m_CounterLabel = GetNode<Label>("%CounterLabel");
m_Text = GetNode<RichTextLabel>("%Text");
m_Text.BbcodeEnabled = true;
m_Text.AutowrapMode = TextServer.AutowrapMode.Word;
m_Text.ScrollActive = true;
m_NextButton = GetNode<Button>("%NextButton");
m_NextButton.Pressed += OnNextButtonPressed;
m_CloseButton = GetNode<Button>("%CloseButton");
m_CloseButton.Pressed += OnCloseButtonPressed;
if (m_Text != null)
{
m_Text.Text = Text;
Configure(0);
}
}
private void OnCloseButtonPressed()
{
Visible = false;
}
private void OnNextButtonPressed()
{
var tween = GetTree().CreateTween();
tween.TweenProperty(m_Text, "modulate", new Color(1, 1, 1, 0), 0.25f);
tween.TweenCallback(Callable.From(() =>
{
m_CurrentHint += 1;
Configure(m_CurrentHint);
}));
tween.TweenProperty(m_Text, "modulate", new Color(1, 1, 1), 0.25f);
}
public void Restart()
{
m_Text.Modulate = new(1, 1, 1);
m_CurrentHint = 0;
Configure(m_CurrentHint);
}
public void Configure(int hint)
{
if (hint >= s_Hints.Length)
return;
if (hint == s_Hints.Length - 1)
{
m_NextButton.Visible = false;
}
else
{
m_NextButton.Visible = true;
}
Text = s_Hints[hint];
if (m_Text != null)
m_Text.Text = Text;
if (m_CounterLabel != null)
m_CounterLabel.Text = $"{hint + 1}/{s_Hints.Length}";
}
public string Text { get; private set; }
private static readonly string[] s_Hints =
[
$"Your robot does not take direct orders it runs {c_On}programs{c_Off}.\nFor each assignment, your goal is to {c_On}deliver enough cargo{c_Off} to meet the target before you run out of {c_Energy}Energy{c_Off}.\nMove around the hex map, use {c_On}Interact{c_Off} to work with sheds, crates and towers, and try to do it in {c_On}as few instructions{c_Off} as possible. Efficient routes and short programs are rewarded in the final score.",
$"The board consists of hexagonal shapes.\n\n - You can move the camera around with the {c_On}WASD{c_Off} keys.\n - You can rotate the view by 60 degrees using the {c_On}Q{c_Off} and {c_On}E{c_Off} keys.\n - Zoom in and out using the {c_On}R{c_Off} and {c_On}F{c_Off} keys or the {c_On}mouse wheel{c_Off}\n - Interact with the board and the cards with a {c_On}left mouse button click{c_Off}",
$"Each program is a single pass through your {c_On}command tape{c_Off}.\n\n - Draw cards into your {c_On}hand{c_Off}.\n - Drag cards onto the tape up to its maximum {c_On}length{c_Off}.\n - Reorder, remove, or discard cards as needed (discarding costs {c_Energy}Energy{c_Off}).",
$"When you press {c_On}Run{c_Off}, the tape executes left to right:\n\n - Most instructions spend {c_Energy}Energy{c_Off} to move, turn or interact.\n - Some cards are marked {c_On}Persistent{c_Off} and return to your deck after use. Others are discarded.\n - Zone and glitch effects may change how the remaining instructions behave.\n\nAfter execution, you return to planning the next program with whatever {c_Energy}Energy{c_Off} and map state you have left. Every program is a chance to refine your route.",
$"Different hexes change how your robot behaves:\n\n{string.Join("\n", Enum.GetValues<ECellType>().Select(c => $" - {c_On}{c}{c_Off}: {Tooltip.GetCellDescription(c)}"))}\nPlan your path so you rest on helpful tiles and cross bad terrain only when its worth the cost.",
$"Points of interest are where the real work happens:\n\n - {c_On}Sheds{c_Off}: load and deliver cargo using {c_On}Interact{c_Off}.\n - {c_On}Crates{c_Off}: adjust your {c_On}Carry{c_Off} by picking up or dropping off contents.\n - {c_On}Donkeys{c_Off}: increase your {c_On}Max Carry{c_Off} so each trip is more productive.\n - {c_On}Interference towers{c_Off}: entering them applies special glitches to the {c_On}remaining{c_Off} tape.\n\nShort, high-value delivery loops between these points are usually better than wandering everywhere.",
$"At the end of an assignment youre judged on more than just success or failure. The game tracks:\n\n - Instructions and programs used (fewer is better).\n - Path length and tiles visited.\n - {c_Energy}Energy{c_Off} wasted by moving while carrying nothing.\n - Overspill from delivering more than requested..",
$"In the {c_On}Improve{c_Off} step you upgrade your deck using {c_Energy}Energy{c_Off} as currency.\n\n - Buy {c_On}patch cards{c_Off} to add new instructions or upgrades.\n - {c_On}Reroll{c_Off} to see a new set of patches, at an increasing {c_Energy}Energy{c_Off} cost.\n - Use {c_On}Cosmic Rays{c_Off} to add a fancy new random card to your deck.\n - Use {c_On}Buffer Overflow{c_Off} to permanently destroy a card.\n\nStronger cards are great, but a bloated deck is harder to program with. Trimming bad cards can be as powerful as buying new ones.",
$"At the end of every round you draw a {c_On}Glitch{c_Off} card.\nYou must decide:\n\n - {c_On}Accept{c_Off}: apply the effect immediately (it may change the map or your instructions).\n - {c_On}Defer{c_Off}: put it back on the stack. The next time you defer a glitch in this run, it costs more {c_Energy}Energy{c_Off}.\n\nGlitches can be temporary map-wide storms or permanent defects on a single instruction. Sometimes taking a small hit now is better than paying {c_Energy}Energy{c_Off} to delay it."
];
private const string c_Energy = "[color=#a5d6a7]";
private const string c_On = "[color=#ffcc80]";
private const string c_Off = "[/color]";
private int m_CurrentHint;
private Button m_NextButton;
private Button m_CloseButton;
private RichTextLabel m_Text;
private Label m_CounterLabel;
}

View File

@@ -0,0 +1 @@
uid://biagw2yiy1bs5

View File

@@ -0,0 +1,220 @@
using System.Linq;
using DonkeysAndDroids;
using Godot;
using RobotAndDonkey.Game.Execution.Commands;
using RobotAndDonkey.Game.Execution.Results;
using RobotAndDonkey.Game.GameState;
using System.Text;
using RobotAndDonkey.Game.Data;
using RobotAndDonkey.Game.Pois;
public partial class VictoryScreen : Control, IScreen
{
public override void _Ready()
{
ContinueButton.Pressed += OnContinueButtonPressed;
if (StatsLabel != null)
{
StatsLabel.BbcodeEnabled = true;
StatsLabel.AutowrapMode = TextServer.AutowrapMode.Word;
StatsLabel.Clear();
}
}
public override void _Process(double delta)
{
UpdateGamepadScroll((float)delta);
}
private void UpdateGamepadScroll(float delta)
{
if (StatsLabel == null || !Visible)
return;
var axis = Input.GetActionStrength("tooltip_scroll_down") - Input.GetActionStrength("tooltip_scroll_up");
if (Mathf.Abs(axis) < 0.1f)
return;
var verticalScrollBar = StatsLabel.GetVScrollBar();
if (verticalScrollBar == null)
return;
verticalScrollBar.Value += axis * GamepadScrollSpeed * delta;
}
public void OnContinueButtonPressed()
{
if (m_Victory)
{
Main.Instance.StartMetaGame();
}
else
{
Main.Instance.Execute(new NextAssignmentCommand(Main.Instance.CurrentRequest.RequestId));
}
}
public void Deactivate()
{
}
public void EnableInputs()
{
ContinueButton.Disabled = false;
}
public void DisableInputs()
{
ContinueButton.Disabled = true;
}
public void Activate()
{
}
public void Configure(CoreLoop coreLoop, Tween tween)
{
if (StatsLabel == null || tween == null)
return;
DisableInputs();
BuildStats(coreLoop);
StatsLabel.Modulate = new(1f, 1f, 1f, 0f);
ContinueButton.Modulate = new(1f, 1f, 1f, 0f);
tween.TweenProperty(StatsLabel, "modulate:a", 1.0f, 0.6f).SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
tween.TweenInterval(0.2f);
tween.TweenProperty(ContinueButton, "modulate:a", 1.0f, 0.4f).SetTrans(Tween.TransitionType.Cubic).SetEase(Tween.EaseType.Out);
tween.TweenCallback(Callable.From(EnableInputs));
}
public bool HandleResult(Result result, Tween tween)
{
return false;
}
private void BuildStats(CoreLoop coreLoop)
{
var sheds = coreLoop.Board.Cells.Select(c => c.Poi as Shed).Where(s => s != null).ToArray();
m_Victory = sheds.Sum(s => s.Remaining) == 0;
ContinueButton.Text = m_Victory ? "New assignment" : "Continue";
m_Builder.Clear();
AppendTitle("Execution report", GetAssignmentSubtitle(coreLoop));
AppendSectionHeader("Programs");
AppendKeyValue("Programs executed", coreLoop.ProgramsExecuted.ToString());
AppendKeyValue("Programs remaining", coreLoop.ProgramCount.ToString());
AppendKeyValue("Instructions used", coreLoop.InstructionsUsed.ToString());
AppendKeyValue("Instructions remaining", coreLoop.PatchDeck.Count.ToString());
AppendSectionHeader("Stats");
var deliveries = coreLoop.Board.Cells.Select(c => c.Poi as Shed).Where(s => s != null).Sum(s => s!.Remaining);
var deliveryColor = deliveries == 0 ? ColorGood : ColorBad;
AppendKeyValue("Deliveries pending", WrapColor(deliveries.ToString(), deliveryColor));
var patchColor = coreLoop.PatchDeck.Count == 0 ? ColorBad : ColorGood;
AppendKeyValue("Cards not drawn", WrapColor(coreLoop.PatchDeck.Count.ToString(), patchColor));
var energyLeft = coreLoop.Currency.Energy - Balancing.Instance.EndOfProgramEnergyReplenish;
var energyLeftColor = energyLeft == 0 ? ColorBad : ColorGood;
AppendKeyValue("Remaining energy", WrapColor(energyLeft.ToString(), energyLeftColor));
AppendSectionHeader("Movement");
AppendKeyValue("Path length", coreLoop.PathLength.ToString());
AppendKeyValue("Tiles visited", coreLoop.CellsVisited.Count.ToString());
AppendSectionHeader("Efficiency");
var energyColor = coreLoop.EnergyWasted == 0 ? ColorGood : ColorBad;
AppendKeyValue("Energy wasted", WrapColor(coreLoop.EnergyWasted.ToString(), energyColor));
var overspillText = coreLoop.Overspill > 0 ? WrapColor(coreLoop.Overspill.ToString(), ColorBad) : WrapColor(coreLoop.Overspill.ToString(), ColorGood);
AppendKeyValue("Overspill", overspillText);
// Assignment / deliveries
AppendSectionHeader("Assignment");
AppendKeyValue("Total delivery", coreLoop.Currency.Delivery.ToString());
AppendKeyValue("Glitches deferred", coreLoop.DeferGlitchCount.ToString());
AppendKeyValue("Shop rerolls", coreLoop.RerollCount.ToString());
StatsLabel.Text = m_Builder.ToString();
StatsLabel.ScrollToLine(0);
}
private string GetAssignmentSubtitle(CoreLoop coreLoop)
{
if (m_Victory)
return WrapColor("Victory! All goods delivered", ColorGood);
if (coreLoop.Currency.Delivery <= 0)
return WrapColor("No deliveries completed", ColorBad);
if (coreLoop.EnergyWasted == 0 && coreLoop.Overspill == 0)
return WrapColor("Perfectly efficient run", ColorGood);
if (coreLoop.EnergyWasted <= 3 && coreLoop.Overspill <= 1)
return WrapColor("Very efficient run", ColorGood);
return WrapColor("Lots of room for optimization", ColorSubtitle);
}
// ----------------------------------------------------------------------
// Small helpers, mirroring Tooltip.cs style
// ----------------------------------------------------------------------
private static string WrapColor(string text, string colorHex)
{
return $"[color={colorHex}]{text}[/color]";
}
private void AppendTitle(string title, string subtitle = null)
{
m_Builder.Append(WrapColor($"[b]{title}[/b]", ColorTitle));
if (!string.IsNullOrEmpty(subtitle))
{
m_Builder.Append('\n');
m_Builder.Append(subtitle);
}
m_Builder.Append('\n');
}
private void AppendSectionHeader(string title)
{
m_Builder.Append('\n');
m_Builder.Append(WrapColor($"[b]{title}[/b]", ColorHeader));
m_Builder.Append('\n');
}
private void AppendKeyValue(string key, string value)
{
m_Builder.Append(WrapColor($"{key}: ", ColorLabel)).Append(WrapColor(value, ColorText)).Append('\n');
}
private const string ColorTitle = "#ffd65c";
private const string ColorSubtitle = "#aaaaaa";
private const string ColorHeader = "#a5d6ff";
private const string ColorLabel = "#cccccc";
private const string ColorText = "#ffffff";
private const string ColorGood = "#a5d6a7";
private const string ColorBad = "#ef9a9a";
private const string ColorMuted = "#9e9e9e";
private readonly StringBuilder m_Builder = new();
[Export]
public float GamepadScrollSpeed = 800f;
[ExportGroup("Buttons")] [Export]
private Button ContinueButton;
[ExportGroup("Stats")] [Export]
private RichTextLabel StatsLabel;
private bool m_Victory;
}

View File

@@ -0,0 +1 @@
uid://bdkn652vxxxjc

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cv3yontw8gqm6"
path="res://.godot/imported/at01.ttf-69bf5315ce67bdb110359b105000e1d3.fontdata"
[deps]
source_file="res://at01.ttf"
dest_files=["res://.godot/imported/at01.ttf-69bf5315ce67bdb110359b105000e1d3.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://nw8iofm1vaj8"
path="res://.godot/imported/avatar.mp3-82fc7beafe069bc85893f17912be83ed.mp3str"
[deps]
source_file="res://audio/avatar.mp3"
dest_files=["res://.godot/imported/avatar.mp3-82fc7beafe069bc85893f17912be83ed.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://fvia4q78bwmj"
path="res://.godot/imported/blocked.mp3-4922bb74916eb5379179e0f2b2fa6b2f.mp3str"
[deps]
source_file="res://audio/blocked.mp3"
dest_files=["res://.godot/imported/blocked.mp3-4922bb74916eb5379179e0f2b2fa6b2f.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bl82gfd0kls8q"
path="res://.godot/imported/button.mp3-60d245a4565b2782b55f77cfd0a0e536.mp3str"
[deps]
source_file="res://audio/button.mp3"
dest_files=["res://.godot/imported/button.mp3-60d245a4565b2782b55f77cfd0a0e536.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b7508mnlvtm26"
path="res://.godot/imported/card.mp3-ee897fd8303b9bb06cb94b3ffd71f454.mp3str"
[deps]
source_file="res://audio/card.mp3"
dest_files=["res://.godot/imported/card.mp3-ee897fd8303b9bb06cb94b3ffd71f454.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://taabst8vqf07"
path="res://.godot/imported/carry.mp3-86982d8575e24723d16442839f60bd1a.mp3str"
[deps]
source_file="res://audio/carry.mp3"
dest_files=["res://.godot/imported/carry.mp3-86982d8575e24723d16442839f60bd1a.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://5w305h2two2l"
path="res://.godot/imported/coreloop.mp3-3141325ff71f7640aa9a3b111c62b467.mp3str"
[deps]
source_file="res://audio/coreloop.mp3"
dest_files=["res://.godot/imported/coreloop.mp3-3141325ff71f7640aa9a3b111c62b467.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dnc8ksbbt8rxg"
path="res://.godot/imported/corrupt.mp3-99ab57b242f1168d4e91c8d185f8f82e.mp3str"
[deps]
source_file="res://audio/corrupt.mp3"
dest_files=["res://.godot/imported/corrupt.mp3-99ab57b242f1168d4e91c8d185f8f82e.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://2g4nuqy03if5"
path="res://.godot/imported/crate.mp3-a58fe30d4bf1a98178c288fd12e1febd.mp3str"
[deps]
source_file="res://audio/crate.mp3"
dest_files=["res://.godot/imported/crate.mp3-a58fe30d4bf1a98178c288fd12e1febd.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://cr7ajk4lac6e0"
path="res://.godot/imported/delivery.mp3-43c96710f31d1291fb88afa80fbaecda.mp3str"
[deps]
source_file="res://audio/delivery.mp3"
dest_files=["res://.godot/imported/delivery.mp3-43c96710f31d1291fb88afa80fbaecda.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bg8eu2auayu2k"
path="res://.godot/imported/donkey.mp3-fd68a08fc18189af6fc8435c1c6b79a4.mp3str"
[deps]
source_file="res://audio/donkey.mp3"
dest_files=["res://.godot/imported/donkey.mp3-fd68a08fc18189af6fc8435c1c6b79a4.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b1licxrv5a5kx"
path="res://.godot/imported/dry.mp3-5d8bfb5ebda62c34969013d3be6bae84.mp3str"
[deps]
source_file="res://audio/dry.mp3"
dest_files=["res://.godot/imported/dry.mp3-5d8bfb5ebda62c34969013d3be6bae84.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://7toyoaj4xv5t"
path="res://.godot/imported/effective.mp3-d738386a1cb0076d1c87831691994f50.mp3str"
[deps]
source_file="res://audio/effective.mp3"
dest_files=["res://.godot/imported/effective.mp3-d738386a1cb0076d1c87831691994f50.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://iccli4sklikc"
path="res://.godot/imported/efficient.mp3-429684dcebfdf9b4498d780235f8bd3c.mp3str"
[deps]
source_file="res://audio/efficient.mp3"
dest_files=["res://.godot/imported/efficient.mp3-429684dcebfdf9b4498d780235f8bd3c.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://prjkw6o6m53s"
path="res://.godot/imported/energy.mp3-f443ba3bb7ca91c2cfd01561cdefc91d.mp3str"
[deps]
source_file="res://audio/energy.mp3"
dest_files=["res://.godot/imported/energy.mp3-f443ba3bb7ca91c2cfd01561cdefc91d.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://ufefxts5f0op"
path="res://.godot/imported/fertile.mp3-604529cfe31a2de59cda44e683bb80bf.mp3str"
[deps]
source_file="res://audio/fertile.mp3"
dest_files=["res://.godot/imported/fertile.mp3-604529cfe31a2de59cda44e683bb80bf.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Some files were not shown because too many files have changed in this diff Show More