using System;
using System.Linq;
namespace Ashfall.Core
{
public sealed partial class GameSimulation
{
public bool CanControlGroup(PlayerState p, VehicleState v, int group)
{
if (p == null || !p.connected || p.stationId == "") return false;
var seat = v.stations.Find(s => s.id == p.stationId && s.playerId == p.id);
if (seat == null) return false;
if (seat.role == StationRole.Gunner) return seat.weaponGroup == group;
return !v.stations.Any(s => s.role == StationRole.Gunner && s.weaponGroup == group && s.playerId != "");
}
private void TickCombat(float dt)
{
foreach (var p in World.players.Where(x => x.connected && x.transitionId == ""))
{
if (p.stationId != "")
{
var v = StationVehicle(p.stationId);
if (v == null || v.power <= 0 || v.energy <= 0 || v.flightMode == FlightMode.Cruise) continue;
foreach (var hp in v.hardpoints)
{
if (!CanControlGroup(p, v, hp.group)) continue;
var module = Content.Module(hp.moduleId); if (module == null || hp.health <= 0) continue;
V2 direction = (p.input.aim - v.position).Normalized;
hp.aimHeading = MathEx.Angle(direction);
bool trigger = hp.group == 0 ? p.input.fire : p.input.secondary;
if (!trigger || hp.readyAt > World.time || Math.Abs(MathEx.DeltaAngle(v.heading + hp.arcCenter, hp.aimHeading)) > hp.arc / 2) continue;
if (!v.cargo.Take(module.ammoId, 1)) continue;
hp.readyAt = World.time + module.interval;
FireRay(p, "world", 0, v.position + direction * (v.width * .55f), direction, module.range, module.damage, v.family, v.Altitude, v.id);
}
continue;
}
if (!p.input.fire || p.shotReadyAt > World.time || p.burrowUntil > World.time) continue;
float bonus = 1 + (p.Level - 1) * .16f;
if (p.IsWorm)
{
p.shotReadyAt = World.time + .45;
var direction = (p.input.aim - p.location.position).Normalized;
foreach (var e in World.enemies.Where(e => e.location.SameFloor(p.location) && e.health > 0 && V2.Distance(e.location.position, p.location.position) < 1.8f))
{
if (V2.Dot(direction, (e.location.position - p.location.position).Normalized) < -.1f) continue;
if (Blocked(p.location, e.location.position)) continue;
Damage(e, 45 * bonus, p);
}
AddShot(p.location.spaceId, p.location.floor, p.location.position, p.location.position + direction * 1.7f, Family.Biological, 0);
}
else
{
bool human = p.species == Species.Human;
if (human && !p.inventory.Take("ammo", 1)) continue;
if (!human && p.energy < 1) continue;
if (!human) p.energy -= 1;
p.shotReadyAt = World.time + (p.buffUntil > World.time ? .09 : .18);
FireRay(p, p.location.spaceId, p.location.floor, p.location.position,
(p.input.aim - p.location.position).Normalized, 24, (human ? 32 : 38) * bonus,
human ? Family.Mechanical : Family.Biological, p.location.height);
}
}
foreach (var e in World.enemies.Where(e => e.health > 0))
{
var target = World.players.Where(p => p.connected && p.stationId == "" && p.location.SameFloor(e.location)
&& p.burrowUntil <= World.time && p.location.height < 2).OrderBy(p => V2.Distance(p.location.position, e.location.position)).FirstOrDefault();
if (target != null && V2.Distance(target.location.position, e.location.position) < (e.boss ? 30 : 16))
{
float distance = V2.Distance(target.location.position, e.location.position);
if (distance > 1)
{
var next = e.location.Copy(); next.position += (target.location.position - e.location.position).Normalized * dt * (e.boss ? 2.2f : 2.5f);
if (CanStand(next, e.boss ? .8f : .35f)) e.location.position = next.position;
}
else if (e.attackReadyAt <= World.time && !Blocked(e.location, target.location.position))
{ e.attackReadyAt = World.time + .9; target.health -= e.boss ? 55 : 12; }
}
else
{
var vehicle = World.vehicles.Where(v => v.flightMode == FlightMode.Ground && v.dockedTo == "" && v.hull > 0)
.OrderBy(v => V2.Distance(v.position, e.location.position)).FirstOrDefault();
if (vehicle != null && V2.Distance(vehicle.position, e.location.position) < vehicle.width * .5f + 1.3f && e.attackReadyAt <= World.time)
{ e.attackReadyAt = World.time + 1; vehicle.hull = Math.Max(0, vehicle.hull - (e.boss ? 70 : 12)); vehicle.engine = Math.Max(0, vehicle.engine - 1); }
}
}
foreach (var piece in World.pieces.Where(b => b.kind == PieceKind.Turret))
{
if (World.tick % 15 != 0) continue;
var target = World.enemies.Find(e => e.health > 0 && e.location.SameFloor(piece.location) && V2.Distance(e.location.position, piece.location.position) < 18 && !Blocked(piece.location, e.location.position));
if (target != null && World.warehouse.Take(piece.family == Family.Mechanical ? "ammo" : "biomass", 1))
{ target.health -= 28; AddShot(piece.location.spaceId, piece.location.floor, piece.location.position, target.location.position, piece.family, 0); }
}
World.enemies.RemoveAll(e => e.health <= 0);
}
public bool Blocked(Location from, V2 target, string ignoreVehicle = "")
{
var floor = World.Floor(from); if (floor == null) return true;
if (from.spaceId == "world")
{
foreach (var building in World.spaces.Where(s => s.kind == SpaceKind.Building))
if (from.height < 8 && new Box2(building.worldAnchor, building.id == "base" ? new V2(18, 18) : new V2(16, 14)).BlocksSegment(from.position, target)) return true;
foreach (var v in World.vehicles.Where(v => v.id != ignoreVehicle && v.dockedTo == ""))
if (Math.Abs(from.height - v.Altitude) < 4 && new Box2(new V2(), new V2(v.width, v.length)).BlocksSegment(
Projection.WorldToInterior(from.position, v.position, v.heading), Projection.WorldToInterior(target, v.position, v.heading))) return true;
}
return floor.walls.Any(w => !w.open && w.height > from.height && w.bounds.BlocksSegment(from.position, target))
|| World.pieces.Any(b => b.location.SameFloor(from) && b.kind == PieceKind.Wall
&& new Box2(b.location.position, b.rotation % 2 == 0 ? new V2(1, .18f) : new V2(.18f, 1)).BlocksSegment(from.position, target));
}
private void FireRay(PlayerState player, string space, int floor, V2 origin, V2 direction, float range, float damage, Family family, float height, string ignoreVehicle = "")
{
if (direction.Length < .1f) return;
V2 end = origin + direction * range;
var start = WorldFactory.At(space, floor, origin); start.height = height;
// Short fixed sweep also truncates the visible tracer at walls.
for (float d = .25f; d <= range; d += .25f)
if (Blocked(start, origin + direction * d, ignoreVehicle)) { end = origin + direction * Math.Max(0, d - .25f); break; }
float nearest = V2.Distance(origin, end); EnemyState hit = null;
foreach (var e in World.enemies)
{
if (e.health <= 0 || e.location.spaceId != space || e.location.floor != floor) continue;
V2 diff = e.location.position - origin; float along = V2.Dot(diff, direction);
if (along < 0 || along > nearest || (diff - direction * along).Length > (e.boss ? 1.1f : .6f)) continue;
// Ground guns cannot accidentally hit a target on a different height plane.
if (height > 12 || e.location.height > height + 2) continue;
nearest = along; hit = e;
}
if (hit != null) { end = origin + direction * nearest; Damage(hit, damage, player); }
AddShot(space, floor, origin, end, family, height);
}
private void Damage(EnemyState e, float damage, PlayerState p)
{
e.health -= damage;
if (e.health > 0) return;
p.kills++; p.experience += e.boss ? 600 : 25;
while (p.experience >= 100 && p.Level < 60)
{ p.experience -= 100; if (p.species == Species.Human) p.humanLevel++; else p.broodLevel++; RefreshHealth(p, false); }
World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = "biomass", amount = e.boss ? 50 : 8, location = e.location.Copy() });
if (e.boss || p.kills % 3 == 0)
World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = "sample", amount = e.boss ? 5 : 1,
location = WorldFactory.At(e.location.spaceId, e.location.floor, e.location.position + new V2(.7f, 0)) });
if (e.boss) { World.bossDefeated = true; Notice?.Invoke("地区首领击败:远征技术样机验证完成"); CriticalChange?.Invoke(); }
}
private void AddShot(string space, int floor, V2 from, V2 to, Family family, float height)
{ World.shots.Add(new ShotEvent { id = World.nextId++, spaceId = space, floor = floor, from = from, to = to, family = family, height = height, expiresAt = World.time + .12 }); }
}
}
using System;
using System.Linq;
namespace Ashfall.Core
{
public sealed partial class GameSimulation
{
public bool CanControlGroup(PlayerState p, VehicleState v, int group)
{
if (p == null || !p.connected || p.stationId == "") return false;
var seat = v.stations.Find(s => s.id == p.stationId && s.playerId == p.id);
if (seat == null) return false;
if (seat.role == StationRole.Gunner) return seat.weaponGroup == group;
return !v.stations.Any(s => s.role == StationRole.Gunner && s.weaponGroup == group && s.playerId != "");
}
private void TickCombat(float dt)
{
foreach (var p in World.players.Where(x => x.connected && x.transitionId == ""))
{
if (p.stationId != "")
{
var v = StationVehicle(p.stationId);
if (v == null || v.power <= 0 || v.energy <= 0 || v.flightMode == FlightMode.Cruise) continue;
foreach (var hp in v.hardpoints)
{
if (!CanControlGroup(p, v, hp.group)) continue;
var module = Content.Module(hp.moduleId); if (module == null || hp.health <= 0) continue;
V2 direction = (p.input.aim - v.position).Normalized;
hp.aimHeading = MathEx.Angle(direction);
bool trigger = hp.group == 0 ? p.input.fire : p.input.secondary;
if (!trigger || hp.readyAt > World.time || Math.Abs(MathEx.DeltaAngle(v.heading + hp.arcCenter, hp.aimHeading)) > hp.arc / 2) continue;
if (!v.cargo.Take(module.ammoId, 1)) continue;
hp.readyAt = World.time + module.interval;
FireRay(p, "world", 0, v.position + direction * (v.width * .55f), direction, module.range, module.damage, v.family, v.Altitude, v.id);
}
continue;
}
if (!p.input.fire || p.shotReadyAt > World.time || p.burrowUntil > World.time) continue;
float bonus = 1 + (p.Level - 1) * .16f;
if (p.IsWorm)
{
p.shotReadyAt = World.time + .45;
var direction = (p.input.aim - p.location.position).Normalized;
foreach (var e in World.enemies.Where(e => e.location.SameFloor(p.location) && e.health > 0 && V2.Distance(e.location.position, p.location.position) < 1.8f))
{
if (V2.Dot(direction, (e.location.position - p.location.position).Normalized) < -.1f) continue;
if (Blocked(p.location, e.location.position)) continue;
Damage(e, 45 * bonus, p);
}
AddShot(p.location.spaceId, p.location.floor, p.location.position, p.location.position + direction * 1.7f, Family.Biological, 0);
}
else
{
bool human = p.species == Species.Human;
if (human && !p.inventory.Take("ammo", 1)) continue;
if (!human && p.energy < 1) continue;
if (!human) p.energy -= 1;
p.shotReadyAt = World.time + (p.buffUntil > World.time ? .09 : .18);
FireRay(p, p.location.spaceId, p.location.floor, p.location.position,
(p.input.aim - p.location.position).Normalized, 24, (human ? 32 : 38) * bonus,
human ? Family.Mechanical : Family.Biological, p.location.height);
}
}
foreach (var e in World.enemies.Where(e => e.health > 0))
{
var target = World.players.Where(p => p.connected && p.stationId == "" && p.location.SameFloor(e.location)
&& p.burrowUntil <= World.time && p.location.height < 2).OrderBy(p => V2.Distance(p.location.position, e.location.position)).FirstOrDefault();
if (target != null && V2.Distance(target.location.position, e.location.position) < (e.boss ? 30 : 16))
{
float distance = V2.Distance(target.location.position, e.location.position);
if (distance > 1)
{
var next = e.location.Copy(); next.position += (target.location.position - e.location.position).Normalized * dt * (e.boss ? 2.2f : 2.5f);
if (CanStand(next, e.boss ? .8f : .35f)) e.location.position = next.position;
}
else if (e.attackReadyAt <= World.time && !Blocked(e.location, target.location.position))
{ e.attackReadyAt = World.time + .9; target.health -= e.boss ? 55 : 12; }
}
else
{
var vehicle = World.vehicles.Where(v => v.flightMode == FlightMode.Ground && v.dockedTo == "" && v.hull > 0)
.OrderBy(v => V2.Distance(v.position, e.location.position)).FirstOrDefault();
if (vehicle != null && V2.Distance(vehicle.position, e.location.position) < vehicle.width * .5f + 1.3f && e.attackReadyAt <= World.time)
{ e.attackReadyAt = World.time + 1; vehicle.hull = Math.Max(0, vehicle.hull - (e.boss ? 70 : 12)); vehicle.engine = Math.Max(0, vehicle.engine - 1); }
}
}
foreach (var piece in World.pieces.Where(b => b.kind == PieceKind.Turret))
{
if (World.tick % 15 != 0) continue;
var target = World.enemies.Find(e => e.health > 0 && e.location.SameFloor(piece.location) && V2.Distance(e.location.position, piece.location.position) < 18 && !Blocked(piece.location, e.location.position));
if (target != null && World.warehouse.Take(piece.family == Family.Mechanical ? "ammo" : "biomass", 1))
{ target.health -= 28; AddShot(piece.location.spaceId, piece.location.floor, piece.location.position, target.location.position, piece.family, 0); }
}
World.enemies.RemoveAll(e => e.health <= 0);
}
public bool Blocked(Location from, V2 target, string ignoreVehicle = "")
{
var floor = World.Floor(from); if (floor == null) return true;
if (from.spaceId == "world")
{
foreach (var building in World.spaces.Where(s => s.kind == SpaceKind.Building))
if (from.height < 8 && new Box2(building.worldAnchor, building.id == "base" ? new V2(18, 18) : new V2(16, 14)).BlocksSegment(from.position, target)) return true;
foreach (var v in World.vehicles.Where(v => v.id != ignoreVehicle && v.dockedTo == ""))
if (Math.Abs(from.height - v.Altitude) < 4 && new Box2(new V2(), new V2(v.width, v.length)).BlocksSegment(
Projection.WorldToInterior(from.position, v.position, v.heading), Projection.WorldToInterior(target, v.position, v.heading))) return true;
}
return floor.walls.Any(w => !w.open && w.height > from.height && w.bounds.BlocksSegment(from.position, target))
|| World.pieces.Any(b => b.location.SameFloor(from) && b.kind == PieceKind.Wall
&& new Box2(b.location.position, b.rotation % 2 == 0 ? new V2(1, .18f) : new V2(.18f, 1)).BlocksSegment(from.position, target));
}
private void FireRay(PlayerState player, string space, int floor, V2 origin, V2 direction, float range, float damage, Family family, float height, string ignoreVehicle = "")
{
if (direction.Length < .1f) return;
V2 end = origin + direction * range;
var start = WorldFactory.At(space, floor, origin); start.height = height;
// Short fixed sweep also truncates the visible tracer at walls.
for (float d = .25f; d <= range; d += .25f)
if (Blocked(start, origin + direction * d, ignoreVehicle)) { end = origin + direction * Math.Max(0, d - .25f); break; }
float nearest = V2.Distance(origin, end); EnemyState hit = null;
foreach (var e in World.enemies)
{
if (e.health <= 0 || e.location.spaceId != space || e.location.floor != floor) continue;
V2 diff = e.location.position - origin; float along = V2.Dot(diff, direction);
if (along < 0 || along > nearest || (diff - direction * along).Length > (e.boss ? 1.1f : .6f)) continue;
// Ground guns cannot accidentally hit a target on a different height plane.
if (height > 12 || e.location.height > height + 2) continue;
nearest = along; hit = e;
}
if (hit != null) { end = origin + direction * nearest; Damage(hit, damage, player); }
AddShot(space, floor, origin, end, family, height);
}
private void Damage(EnemyState e, float damage, PlayerState p)
{
e.health -= damage;
if (e.health > 0) return;
p.kills++; p.experience += e.boss ? 600 : 25;
while (p.experience >= 100 && p.Level < 60)
{ p.experience -= 100; if (p.species == Species.Human) p.humanLevel++; else p.broodLevel++; RefreshHealth(p, false); }
World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = "biomass", amount = e.boss ? 50 : 8, location = e.location.Copy() });
if (e.boss || p.kills % 3 == 0)
World.resources.Add(new ResourceState { id = World.NewId("drop"), itemId = "sample", amount = e.boss ? 5 : 1,
location = WorldFactory.At(e.location.spaceId, e.location.floor, e.location.position + new V2(.7f, 0)) });
if (e.boss) { World.bossDefeated = true; Notice?.Invoke("地区首领击败:远征技术样机验证完成"); CriticalChange?.Invoke(); }
}
private void AddShot(string space, int floor, V2 from, V2 to, Family family, float height)
{ World.shots.Add(new ShotEvent { id = World.nextId++, spaceId = space, floor = floor, from = from, to = to, family = family, height = height, expiresAt = World.time + .12 }); }
}
}