Mining 4a: blasts wear the rocks their disc covers, through Detonate
Detonate's chunk pass shares the disc by area over rocks (lens over pi r^2), publishes one AbsorbEvent per worn rock on Zone.ChunkCapabilities, then Zone.Wear. Publish takes a CapabilityEvents so entities and rocks share it. One largest-rock bound (Zone.LargestChunkRadius) serves the index and the blast query. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c47a34115c
commit
cbe33a0f1d
4 changed files with 324 additions and 15 deletions
|
|
@ -1221,7 +1221,7 @@ public static class FireControl
|
|||
|
||||
if (shot.Outcome.Shielded)
|
||||
{
|
||||
Publish(shot.Target, AbsorbLayer.Shield, ImpactPoint(shot.Target, shot.Outcome.Cell),
|
||||
Publish(shot.Target.Capabilities, AbsorbLayer.Shield, ImpactPoint(shot.Target, shot.Outcome.Cell),
|
||||
float3(shot.TravelDirection.x, 0, shot.TravelDirection.y), shot.Damage, shot.DamageType);
|
||||
shot.Target.Shield.TakeHit(shot.DamageType, shot.Damage);
|
||||
return;
|
||||
|
|
@ -1266,7 +1266,7 @@ public static class FireControl
|
|||
|
||||
for (var li = 0; li < laneCount; li++) ArrayPool<LaneCell>.Shared.Return(buffers[li]);
|
||||
|
||||
Publish(target, AbsorbLayer.Hull, ImpactPoint(target, shot.Outcome.Cell),
|
||||
Publish(target.Capabilities, AbsorbLayer.Hull, ImpactPoint(target, shot.Outcome.Cell),
|
||||
float3(shot.TravelDirection.x, 0, shot.TravelDirection.y), shot.Damage, shot.DamageType);
|
||||
target.DamageHull(hullDamage);
|
||||
}
|
||||
|
|
@ -1278,17 +1278,18 @@ public static class FireControl
|
|||
return float3(world.x, target.Position.y, world.y);
|
||||
}
|
||||
|
||||
// The one publisher of an absorbed impact: once per receiver, on its Capabilities, before the damage it reports.
|
||||
private static void Publish(Entity receiver, AbsorbLayer layer, float3 position, float3 direction, float magnitude, DamageType damageType) =>
|
||||
receiver.Capabilities.PublishAbsorb(new AbsorbEvent(position, direction, magnitude, damageType, layer));
|
||||
// The one publisher of an absorbed impact: once per receiver, on its CapabilityEvents (an entity's, or the zone's
|
||||
// for a rock), before the damage it reports.
|
||||
private static void Publish(CapabilityEvents receiver, AbsorbLayer layer, float3 position, float3 direction, float magnitude, DamageType damageType) =>
|
||||
receiver.PublishAbsorb(new AbsorbEvent(position, direction, magnitude, damageType, layer));
|
||||
|
||||
// A blast's impact on one entity: at the blast point (on the entity's height), travelling from the blast out through
|
||||
// the entity's centre, or zero when they coincide.
|
||||
private static void PublishBlast(Entity entity, float2 blast, AbsorbLayer layer, float magnitude, DamageType damageType)
|
||||
// A blast's impact on one receiver: at the blast point (at the receiver's height), travelling from the blast out
|
||||
// through the receiver's planar centre, or zero when they coincide. An entity and a rock go through it alike.
|
||||
private static void PublishBlast(CapabilityEvents receiver, float2 centre, float height, float2 blast, AbsorbLayer layer, float magnitude, DamageType damageType)
|
||||
{
|
||||
var away = entity.Position.xz - blast;
|
||||
var away = centre - blast;
|
||||
var direction = lengthsq(away) > 0f ? normalize(away) : float2.zero;
|
||||
Publish(entity, layer, float3(blast.x, entity.Position.y, blast.y), float3(direction.x, 0, direction.y), magnitude, damageType);
|
||||
Publish(receiver, layer, float3(blast.x, height, blast.y), float3(direction.x, 0, direction.y), magnitude, damageType);
|
||||
}
|
||||
|
||||
// Cut 12.4(b) (docs/fire-control-cut.md, "Where P is"): a contact or delayed blast needs a committed hit --
|
||||
|
|
@ -1607,7 +1608,13 @@ public static class FireControl
|
|||
|
||||
// Cut 12.4(b) (docs/fire-control-cut.md, "Area, per entity"): the one owner of blast damage -- a blast is
|
||||
// an area, not a bundle of rays (Q12-7 dissolved the ray model's remainder question along with the rays
|
||||
// themselves). Splash's signature with the point made planar (R7). Callers: Apply's three fuse branches. Nothing draws: every candidate entity in radius takes its share, unconditionally.
|
||||
// themselves). Splash's signature with the point made planar (R7). Callers: Apply's three fuse branches and
|
||||
// Zone's mine step. Nothing draws: every candidate entity in radius takes its share, unconditionally.
|
||||
//
|
||||
// Mining cut 4a (docs/mining-cut-refresh.md, Q10 = A): this is also the one owner of blast area over rocks.
|
||||
// After the entity pass, every unbroken rock whose disc the blast disc overlaps takes its lens share of the
|
||||
// damage -- damage * lens / (pi r^2), the cells' own normaliser -- through Zone.Wear, and publishes one absorb
|
||||
// on Zone.ChunkCapabilities first. Overlapping bodies each take their own overlap; shares off every body are lost.
|
||||
//
|
||||
// For each candidate (an entity whose centre lies within radius plus half its own hull diagonal --
|
||||
// CandidatesIncludeHullsWhoseCentreIsOutsideTheRadius, which Splash's centre-distance cull could not pass):
|
||||
|
|
@ -1673,7 +1680,7 @@ public static class FireControl
|
|||
{
|
||||
if (shield.CanTakeHit(damageType, totalShare))
|
||||
{
|
||||
PublishBlast(entity, worldPlanar, AbsorbLayer.Shield, totalShare, damageType);
|
||||
PublishBlast(entity.Capabilities, entity.Position.xz, entity.Position.y, worldPlanar, AbsorbLayer.Shield, totalShare, damageType);
|
||||
shield.TakeHit(damageType, totalShare);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1703,11 +1710,37 @@ public static class FireControl
|
|||
for (var i = 0; i < amounts.Length; i++) hullTotal += amounts[i];
|
||||
}
|
||||
|
||||
PublishBlast(entity, worldPlanar, AbsorbLayer.Hull, totalShare, damageType);
|
||||
PublishBlast(entity.Capabilities, entity.Position.xz, entity.Position.y, worldPlanar, AbsorbLayer.Hull, totalShare, damageType);
|
||||
entity.DamageHull(hullTotal);
|
||||
}
|
||||
|
||||
if (!(radius > 0f) || zone.AsteroidBelts.Count == 0) return; // no area, or no rocks to cover
|
||||
// Rocks, through the targeting index (no observer: entities stay out of it). Reach is exact on a rock's
|
||||
// centre, so it is the blast radius plus the widest any rock can be. Walked in (field, index) order so the
|
||||
// order of wear -- and any later dice on it -- is fixed.
|
||||
var rocks = _rockScratch ??= new List<TargetCandidate>();
|
||||
zone.Targets.Within(new TargetSearch(worldPlanar, radius + Zone.LargestChunkRadius(zone.Settings)), rocks);
|
||||
rocks.RemoveAll(c => !c.Target.Chunk.HasValue);
|
||||
rocks.Sort((a, b) =>
|
||||
{
|
||||
var byField = string.CompareOrdinal(a.Target.Chunk.Value.Field.Value, b.Target.Chunk.Value.Field.Value);
|
||||
return byField != 0 ? byField : a.Target.Chunk.Value.Index.CompareTo(b.Target.Chunk.Value.Index);
|
||||
});
|
||||
var discArea = PI * radius * radius;
|
||||
foreach (var rock in rocks)
|
||||
{
|
||||
var chunk = rock.Target.Chunk.Value;
|
||||
if (!zone.ChunkExists(chunk)) continue;
|
||||
var centre = zone.ChunkPose(chunk.Field, chunk.Index).xy;
|
||||
var share = damage * CircleCircleOverlap(length(centre - worldPlanar), radius, zone.ChunkRadius(chunk)) / discArea;
|
||||
if (!(share > 0f)) continue;
|
||||
PublishBlast(zone.ChunkCapabilities, centre, 0f, worldPlanar, AbsorbLayer.Hull, share, damageType);
|
||||
zone.Wear(chunk, share);
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic] private static List<TargetCandidate> _rockScratch;
|
||||
|
||||
// Whether the disc reaches any occupied hull cell of the entity: Detonate's own coverage test (the same
|
||||
// ToSchematicPoint, SchematicCellSize and CircleSquareOverlap), so what triggers a mine and what its blast
|
||||
// reaches are one geometry.
|
||||
|
|
@ -1721,6 +1754,19 @@ public static class FireControl
|
|||
return false;
|
||||
}
|
||||
|
||||
// Mining cut 4a: the exact area shared by two discs at centre distance d -- the lens. Exactly zero when they are
|
||||
// disjoint or tangent, and the smaller disc's whole area when one contains the other.
|
||||
public static float CircleCircleOverlap(float d, float r1, float r2)
|
||||
{
|
||||
if (r1 <= 0f || r2 <= 0f || d >= r1 + r2) return 0f;
|
||||
var small = min(r1, r2);
|
||||
if (d <= abs(r1 - r2)) return PI * small * small;
|
||||
var a = (d * d + r1 * r1 - r2 * r2) / (2f * d * r1);
|
||||
var b = (d * d + r2 * r2 - r1 * r1) / (2f * d * r2);
|
||||
var k = (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2);
|
||||
return r1 * r1 * acos(clamp(a, -1f, 1f)) + r2 * r2 * acos(clamp(b, -1f, 1f)) - .5f * sqrt(max(0f, k));
|
||||
}
|
||||
|
||||
// Cut 12.4(b) (docs/fire-control-cut.md, "Exact overlap, in float"): the exact area of a unit square,
|
||||
// centred on `cell` (the unit square centred on its integer coordinate, the same convention every other
|
||||
// frame rule uses), that lies inside a disc of radius `r` centred at `diskCentre` -- both in the same
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ public sealed class BeltTargets : ITargetProvider
|
|||
_zone = zone;
|
||||
_field = field;
|
||||
_data = data;
|
||||
var size = settings.AsteroidSize;
|
||||
_largestRadius = size.Exponent > 0 ? max(abs(size.Minimum), abs(size.Maximum)) : float.PositiveInfinity;
|
||||
_largestRadius = Zone.LargestChunkRadius(settings);
|
||||
|
||||
var count = data.Asteroids.Length;
|
||||
_periods = new double[count];
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ public class Zone
|
|||
// the entities, and one provider per belt. The reticle, cycling and any range query over rocks go through it;
|
||||
// nothing else scans a belt.
|
||||
public readonly TargetingIndex Targets = new TargetingIndex();
|
||||
// Mining cut 4a: the receiver of every rock impact FireControl.Detonate publishes. Notification only: a presenter
|
||||
// subscribes once per zone, and nothing in ServerShared reads it.
|
||||
public readonly CapabilityEvents ChunkCapabilities = new CapabilityEvents();
|
||||
private readonly List<Sun> _suns = new List<Sun>();
|
||||
|
||||
private HashSet<CultRecordKey> _updatedOrbits = new HashSet<CultRecordKey>();
|
||||
|
|
@ -674,6 +677,14 @@ public class Zone
|
|||
return belt.DamagedSize(chunk.Index, wear.Damage, Settings);
|
||||
}
|
||||
|
||||
// No rock of any belt, whole or worn, is wider than this (AsteroidSize's bound over every argument, so over the
|
||||
// Asteroid.Size range the generator writes). The index's visibility bound and a blast's query reach both use it.
|
||||
public static float LargestChunkRadius(PlanetSettings settings)
|
||||
{
|
||||
var size = settings.AsteroidSize;
|
||||
return size.Exponent > 0 ? max(abs(size.Minimum), abs(size.Maximum)) : float.PositiveInfinity;
|
||||
}
|
||||
|
||||
// Cut 2: the only writer of chunk wear. Returns whether this hit broke the chunk. The break threshold is
|
||||
// strictly greater-than, matching the deleted per-tick miner's own comparison against accumulated damage:
|
||||
// damage exactly equal to hitpoints does not yet break the chunk, only damage that exceeds them does.
|
||||
|
|
|
|||
Loading…
Reference in a new issue