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