Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ Per-item detail lives in the dated work log below. **Since 2026-07 versions are

---

### ★ EVA asteroid mining plays by the rules: drill tiers, hardness hits, no lost ore (#685, 2026-08-03, branch fix/eva-asteroid-mining-hardness)
During a spacewalk any asteroid block popped with a SINGLE bare-hand click — no drill, no hardness,
titanium included — while the same block on a planet takes a tier-2 drill and several timed hits. And
with a full backpack the ore silently vanished (the cell was cleared before the drops were banked).
Now the EVA path mirrors the planet pipeline for asteroids: `ToolCanMine` gates by drill kind + tier,
hardness accumulates per hit (`progress += MiningPower`), and a new `StructureMiningProgress` message
(NetCodec 188) lets the aim marker warm from cyan to orange as the ore works toward breaking. The
client mines hold-to-repeat at the on-foot drill pace (0.28 s) instead of per click. Drops are
capacity-checked BEFORE the cell clears (mirrors #600) — a full inventory refuses the break
losslessly and the banked progress finishes the job once there is room. Own-ship and station editing
stays instant by design (construction UX, not resource gathering; still 3 ship-hull mining tests
lock that in). Soft materials keep the light feel by their own hardness — ice (0.8) still breaks in
one hit, so the planned icy mini-asteroids stay easy water harvests while titanium keeps its gate.

### ★ Star systems & planets got real names: registries, Roman numerals, landmark proper names (#678, 2026-08-03, branch feat/system-planet-naming)
Every system used to follow one pattern ("Veyra-42") with numbered planets ("Veyra-42 1"). Systems
now roll one of several naming registries — coined proper names ("Tharion", ~55 %), catalog
Expand Down
47 changes: 44 additions & 3 deletions client/Assets/BlocksBeyondTheStars/Scripts/SpaceView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ private enum Phase { Launch, Cruise, Landing, Boarding }
private bool _evaHasAim;
private string _evaAimStructId; // id of the structure currently aimed at (ship or asteroid)
private GameObject _aimHighlight; // marker on the aimed cell
private Material _aimHighlightMat; // marker material — tinted by mining progress (#685)
private string _evaMineProgressStructId; // structure of the last server mining-progress report (#685)
private Vector3Int _evaMineProgressCell; // its structure-local cell
private float _evaMineProgressFraction; // 0..1 of the way to breaking
private float _evaMineProgressAt; // Time.time of the report (stale reports are ignored)
private float _nextEvaMine; // hold-to-mine cadence, matches the on-foot drill pace (#685)
private const float EvaMinePace = 0.28f; // seconds between mining hits while LMB is held (#685)
private const float EvaReach = 6f; // how far the suit can build/mine
private const float SuitRadius = 0.45f; // suit collision radius vs the voxel hull
private const float FarStructUnload = 95f; // S5: drop a voxel body's mesh beyond this (data kept)
Expand Down Expand Up @@ -324,6 +331,7 @@ private void Update()
if (Game.Network != null && !_structSubscribed)
{
Game.Network.StructureBlockChangedReceived += OnStructureBlockChanged;
Game.Network.StructureMiningProgressReceived += OnStructureMiningProgress; // #685
Game.Network.SpaceShipDesignReceived += OnStructureDesign;
Game.Network.SpaceEntityDestroyed += OnStructEntityDestroyed;
_structSubscribed = true;
Expand Down Expand Up @@ -1953,9 +1961,15 @@ private void UpdateEvaBuild()
return;
}

if (InputMap.PrimaryDown())
if (InputMap.PrimaryHeld())
{
Game.Network?.SendStructureEdit(_evaAimStructId, _evaAimHit.x, _evaAimHit.y, _evaAimHit.z, mine: true);
// #685: mining is hold-to-repeat at the on-foot drill pace — the server accumulates hardness
// progress per hit on asteroid ore, so a single click no longer pops a block instantly.
if (Time.time >= _nextEvaMine)
{
_nextEvaMine = Time.time + EvaMinePace;
Game.Network?.SendStructureEdit(_evaAimStructId, _evaAimHit.x, _evaAimHit.y, _evaAimHit.z, mine: true);
}
}
else if (InputMap.SecondaryDown())
{
Expand Down Expand Up @@ -1986,14 +2000,40 @@ private void UpdateAimHighlight()

if (_aimHighlight == null)
{
_aimHighlight = Cube("AimMarker", _root.transform, Vector3.zero, Vector3.one * 0.22f, Unlit(new Color(0.3f, 0.95f, 1f)));
_aimHighlightMat = Unlit(new Color(0.3f, 0.95f, 1f));
_aimHighlight = Cube("AimMarker", _root.transform, Vector3.zero, Vector3.one * 0.22f, _aimHighlightMat);
}

// #685: warm the marker toward orange as the server reports mining progress on the aimed cell, so
// multi-hit ore visibly works toward breaking. A report older than a second is stale (the aim moved
// on, or the cell broke) and falls back to the idle cyan.
float frac = 0f;
if (_evaMineProgressStructId == _evaAimStructId && _evaMineProgressCell == _evaAimHit
&& Time.time - _evaMineProgressAt < 1f)
{
frac = _evaMineProgressFraction;
}

if (_aimHighlightMat != null)
{
_aimHighlightMat.color = Color.Lerp(new Color(0.3f, 0.95f, 1f), new Color(1f, 0.55f, 0.15f), frac);
}

_aimHighlight.SetActive(true);
var cellCentre = new Vector3(_evaAimHit.x + 0.5f, _evaAimHit.y + 0.5f, _evaAimHit.z + 0.5f);
_aimHighlight.transform.localPosition = FromDesign(t, centre, cellCentre);
}

/// <summary>#685: remembers the server's mining progress on a structure cell (EVA asteroid mining), so
/// the aim marker can show how close the aimed ore block is to breaking.</summary>
private void OnStructureMiningProgress(BlocksBeyondTheStars.Networking.Messages.StructureMiningProgress m)
{
_evaMineProgressStructId = m.StructureId;
_evaMineProgressCell = new Vector3Int(m.X, m.Y, m.Z);
_evaMineProgressFraction = Mathf.Clamp01(m.Fraction);
_evaMineProgressAt = Time.time;
}

/// <summary>Applies a server structure edit to the local voxel grid (ship or asteroid) + re-meshes.</summary>
private void OnStructureBlockChanged(BlocksBeyondTheStars.Networking.Messages.StructureBlockChanged m)
{
Expand Down Expand Up @@ -2361,6 +2401,7 @@ private void Exit()
_shipCells = null; // ship voxel grid was under the destroyed _ship
_shipVox = null;
_aimHighlight = null; // marker lived under _root (destroyed)
_aimHighlightMat = null; // its material dies with it (#685)
_ship = null;
_exhaust = null;
_thruster = null; // particle thruster lived under the destroyed _ship
Expand Down
2 changes: 2 additions & 0 deletions src/BlocksBeyondTheStars.Client.Core/NetworkClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public sealed class NetworkClient : IDisposable
public event Action<SpaceState>? SpaceStateReceived;
public event Action<SpaceShipDesign>? SpaceShipDesignReceived; // item 20 S1: own ship as a voxel structure
public event Action<StructureBlockChanged>? StructureBlockChangedReceived; // item 20 S2: a structure cell changed
public event Action<StructureMiningProgress>? StructureMiningProgressReceived; // #685: EVA asteroid mining progress on a structure cell
public event Action<LandedShipState>? LandedShipReceived; // ship-as-object: a parked ship placed/replaced/removed
public event Action<SpaceEntityDestroyed>? SpaceEntityDestroyed;
public event Action<SpaceClosed>? SpaceClosed;
Expand Down Expand Up @@ -553,6 +554,7 @@ private void OnPayload(byte[] payload)
case SpaceState m: SpaceStateReceived?.Invoke(m); break;
case SpaceShipDesign m: SpaceShipDesignReceived?.Invoke(m); break;
case StructureBlockChanged m: StructureBlockChangedReceived?.Invoke(m); break;
case StructureMiningProgress m: StructureMiningProgressReceived?.Invoke(m); break;
case LandedShipState m: LandedShipReceived?.Invoke(m); break;
case SpaceEntityDestroyed m: SpaceEntityDestroyed?.Invoke(m); break;
case SpaceClosed m: SpaceClosed?.Invoke(m); break;
Expand Down
72 changes: 68 additions & 4 deletions src/BlocksBeyondTheStars.GameServer/GameServerSpaceStructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ public sealed partial class GameServer
/// a coarse anti-grief range so you can't edit a body across the flight zone.</summary>
private const float StructureEditRange = 40f;

/// <summary>#685: per-cell EVA mining progress on voxel structures (asteroid ore), keyed by structure id +
/// structure-local cell — the structure-space analogue of <c>_miningProgress</c>. An entry clears when its
/// cell breaks or its structure is removed; a changed block id restarts from zero (same guard as the world
/// path, B52).</summary>
private readonly Dictionary<(string Id, Vector3i Cell), (ushort Block, float Progress)> _structureMiningProgress = new();

/// <summary>Builds the served player's ship as a voxel <see cref="SpaceStructure"/> from its editor design
/// (item 20, S1). Mirrors the block mapping the ground stamp uses (<see cref="StampShipLayout"/>) but writes
/// into a standalone sparse grid instead of the planet world. Hatch/door cells render as holes. Ships with
Expand Down Expand Up @@ -455,6 +461,58 @@ private void HandleStructureEdit(PlayerSession session, StructureEditIntent inte
return;
}

var def = _content.BlockById(existing);

// #685: asteroid ore obeys the same rules as planet mining — tool gating + hardness accumulated
// over several hits (a single click used to pop ANY block bare-handed, titanium included). Own-ship
// and station editing stays instant by design: that is construction UX, not resource gathering.
if (isAsteroid)
{
if (def is null || !def.Mineable)
{
Reject(session, "structure", "Block cannot be mined.");
return;
}

var tool = ActiveTool(p);
if (!ToolCanMine(tool, def))
{
Reject(session, "structure", "Your current tool cannot mine this block.");
return;
}

float hardness = System.Math.Max(0.2f, def.Hardness);
float power = tool.MiningPower > 0f ? tool.MiningPower : 1f;
float prior = _structureMiningProgress.TryGetValue((s.Id, pos), out var prev) && prev.Block == existing.Value
? prev.Progress
: 0f;
float progress = prior + power;
if (progress + 0.0001f < hardness)
{
_structureMiningProgress[(s.Id, pos)] = (existing.Value, progress);
Send(session, new StructureMiningProgress
{
StructureId = s.Id,
X = pos.X,
Y = pos.Y,
Z = pos.Z,
Fraction = progress / hardness,
});
return;
}
}

// #685 (mirrors #600): make sure the drops fit BEFORE clearing the cell — the cell used to be
// cleared first, so a full inventory silently ate the ore. Refusing the break is the lossless
// option; accumulated progress stays banked, so the block falls on the next hit once there is room.
var pool = new MaterialPool(_content, p, _ship);
if (def is { Drops.Count: > 0 } && !pool.CanFit(def.Drops))
{
Reject(session, "structure", "@inventory_full");
return;
}

_structureMiningProgress.Remove((s.Id, pos));
s.Set(pos, BlockId.Air);

// item 20 S4 durable save: a hull cell the owner mined out persists as a per-cell delta (only
Expand All @@ -464,11 +522,10 @@ private void HandleStructureEdit(PlayerSession session, StructureEditIntent inte
_repo.SetStructureBlock(s.Id, pos, BlockId.AirValue);
}

// Bank the mined block's drops (ore from asteroids; rebuild materials from a ship hull). The cell is
// already cleared at this point, so a full inventory can still lose the drop — BankLoot says so.
if (_content.BlockById(existing) is { } def && def.Drops.Count > 0)
// Bank the mined block's drops (ore from asteroids; rebuild materials from a ship hull) — the
// capacity was checked above, so nothing is lost here.
if (def is { Drops.Count: > 0 })
{
var pool = new MaterialPool(_content, p, _ship);
BankLoot(session, pool, def.Drops);
SendInventory(session);
}
Expand Down Expand Up @@ -918,6 +975,13 @@ private void RemoveAsteroidStructure(SpaceInstance instance, string id)
{
if (instance.Structures.Remove(id))
{
// #685: drop any banked mining progress with the rock, so a respawned structure that happens to
// reuse the id never inherits half-mined cells.
foreach (var key in _structureMiningProgress.Keys.Where(k => k.Id == id).ToList())
{
_structureMiningProgress.Remove(key);
}

BroadcastToInstance(instance, new SpaceEntityDestroyed { Id = id });
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/BlocksBeyondTheStars.Networking/Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,19 @@ public sealed class StructureBlockChanged
public int Shape { get; set; }
}

/// <summary>Server → client: mining progress on one cell of a voxel structure (#685) — the structure-space
/// analogue of <see cref="MiningProgress"/>. EVA asteroid mining obeys block hardness like planet mining, so
/// a hard ore block needs several drill hits; Fraction is 0..1 of the way to breaking. The cell stays until
/// it breaks (<see cref="StructureBlockChanged"/>). Coordinates are structure-local cells.</summary>
public sealed class StructureMiningProgress
{
public string StructureId { get; set; } = string.Empty;
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
public float Fraction { get; set; }
}

/// <summary>A remote player's presence in a space instance: their ship (or floating EVA suit) position +
/// heading, for the flight view to render. Server → client, inside <see cref="SpaceState"/>.</summary>
public sealed class NetSpacePlayer
Expand Down
3 changes: 3 additions & 0 deletions src/BlocksBeyondTheStars.Networking/NetCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ static NetCodec()
Register(185, typeof(AchievementList)); // Server -> Client (join + whenever progress moves)
Register(186, typeof(AchievementUnlocked)); // Server -> Client (celebrate this one)
Register(187, typeof(AchievementRewardDeferred));// Server -> Client (earned, but make room for the reward)

// #685: EVA asteroid mining obeys hardness — per-cell progress on a voxel structure.
Register(188, typeof(StructureMiningProgress)); // Server -> Client
}

private static void Register(byte tag, Type type)
Expand Down
Loading