Use a bounded read-only pre-destruction occupancy sample; preserve conservative damage for unresolved geometry and do not defer or repeat damage on replies. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -141,6 +141,16 @@ impl Comps {
|
||||
/// Health points an object loses at the centre of a blast.
|
||||
const OBJECT_HIT: [f64; 5] = [2.0, 6.0, 14.0, 26.0, 40.0];
|
||||
|
||||
/// Three cheap game samples across a hull. Known cover leaves residual splash;
|
||||
/// unknown geometry keeps the previous unattenuated damage (not a claim of air).
|
||||
/// Zero async wait freezes the pre-destruction cache and avoids late double damage.
|
||||
fn blast_exposure(ground: &Ground, from: DVec3, centre: DVec3, size: f64, now: u64) -> f64 {
|
||||
let across = horizontal(centre - from).cross(DVec3::Y).normalize_or(DVec3::X) * size;
|
||||
[centre, centre - across, centre + across].into_iter().map(|to| {
|
||||
if ground.world.blast_cover(from, to, now) == Some(true) { 0.15 } else { 1.0 }
|
||||
}).sum::<f64>() / 3.0
|
||||
}
|
||||
|
||||
impl MissileKind {
|
||||
/// The explosive a missile left in a burning launcher adds (TNT).
|
||||
fn charge(self) -> f64 {
|
||||
@@ -240,7 +250,7 @@ impl Sim {
|
||||
}
|
||||
|
||||
/// A blast of `power` (0 to 4) at `pos`: the systems and objects round it take damage.
|
||||
pub(super) fn blast_damage(&mut self, pos: DVec3, power: usize, out: &mut Out) {
|
||||
pub(super) fn blast_damage(&mut self, pos: DVec3, power: usize, ground: &Ground, out: &mut Out) {
|
||||
let (reach, hit) = (REACH[power], HIT[power]);
|
||||
let mut destroyed = Vec::new();
|
||||
for (i, sam) in self.sams.iter_mut().enumerate() {
|
||||
@@ -253,8 +263,9 @@ impl Sim {
|
||||
continue;
|
||||
}
|
||||
let k = 1.0 - d / reach;
|
||||
let cover = blast_exposure(ground, pos, sam.pos + DVec3::Y * (1.5 * sam.kind.spec().scale), sam.kind.spec().scale, self.tick);
|
||||
let before = sam.health;
|
||||
sam.health -= hit * k * k * if d < 1.0 { 1.5 } else { 1.0 };
|
||||
sam.health -= cover * hit * k * k * if d < 1.0 { 1.5 } else { 1.0 };
|
||||
if sam.health <= 0.0 {
|
||||
destroyed.push(i);
|
||||
continue;
|
||||
@@ -279,7 +290,7 @@ impl Sim {
|
||||
let kp = 1.0 - dp / reach;
|
||||
let roll = dice(self.seed, self.tick, sam.id as u64, part as u64);
|
||||
// The parts are softer than the hull: a blast the hull survives can still put one out.
|
||||
let dmg = hit * kp * kp * (0.7 + roll) * 1.5 * if dp < 1.0 { 1.4 } else { 1.0 };
|
||||
let dmg = cover * hit * kp * kp * (0.7 + roll) * 1.5 * if dp < 1.0 { 1.4 } else { 1.0 };
|
||||
let was = sam.comps.state(part);
|
||||
let v = &mut sam.comps.0[part as usize];
|
||||
*v = (*v - dmg).max(0.0);
|
||||
@@ -300,7 +311,8 @@ impl Sim {
|
||||
if d < r {
|
||||
if let Some(h) = self.target_health.get_mut(i) {
|
||||
let before = *h;
|
||||
*h = (*h - OBJECT_HIT[power] * (1.0 - d / r)).max(0.0);
|
||||
let cover = blast_exposure(ground, pos, *t + DVec3::Y * 0.8, 0.75, self.tick);
|
||||
*h = (*h - cover * OBJECT_HIT[power] * (1.0 - d / r)).max(0.0);
|
||||
if before > 0.0 && *h <= 0.0 {
|
||||
out.messages.push(format!("ОБЪЕКТ №{} УНИЧТОЖЕН", i + 1));
|
||||
}
|
||||
@@ -523,3 +535,99 @@ pub(super) fn draw_wreck_of(sam: &Sam, out: &mut Out) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cover_tests {
|
||||
use super::*;
|
||||
|
||||
struct CoverWorld { wall: bool, opening: bool }
|
||||
impl world::WorldSource for CoverWorld {
|
||||
fn solid(&self, x: i32, _y: i32, z: i32) -> Option<bool> {
|
||||
Some(self.wall && x == 8 && !(self.opening && z == 8))
|
||||
}
|
||||
}
|
||||
|
||||
fn cache(src: &CoverWorld) -> Ground {
|
||||
let mut ground = Ground::default();
|
||||
let mut answers = Vec::new();
|
||||
for x in -1..=1 { for z in -1..=1 {
|
||||
let sec = (x, 4, z);
|
||||
answers.push(world::SectionAnswer { sec, fill: world::section_of(src, sec) });
|
||||
}}
|
||||
ground.world.answer(answers, 10);
|
||||
ground
|
||||
}
|
||||
|
||||
fn victim() -> Sim {
|
||||
let mut sim = Sim::new(7);
|
||||
sim.tick = 10;
|
||||
let id = sim.id();
|
||||
sim.sams.push(Sam::new(id, SamKind::S400, DVec3::new(10.5, 70.0, 8.5), 0.0));
|
||||
sim.targets.push(DVec3::new(10.5, 71.0, 8.5));
|
||||
sim.target_health.push(100.0);
|
||||
sim
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_wall_and_opening_attenuate_hull_parts_and_objects() {
|
||||
let from = DVec3::new(0.5, 71.5, 8.5);
|
||||
let mut results = Vec::new();
|
||||
for src in [CoverWorld { wall: false, opening: false }, CoverWorld { wall: true, opening: false }, CoverWorld { wall: true, opening: true }] {
|
||||
let mut sim = victim();
|
||||
sim.blast_damage(from, 2, &cache(&src), &mut Out::default());
|
||||
results.push((1.0 - sim.sams[0].health, 1.0 - sim.sams[0].comps.get(Part::Tracks), 100.0 - sim.target_health[0]));
|
||||
}
|
||||
for i in 0..3 {
|
||||
let open = [results[0].0, results[0].1, results[0].2][i];
|
||||
let wall = [results[1].0, results[1].1, results[1].2][i];
|
||||
let gap = [results[2].0, results[2].1, results[2].2][i];
|
||||
assert!(open > 0.0);
|
||||
assert!((wall / open - 0.15).abs() < 1e-9);
|
||||
assert!(wall < gap && gap < open, "sample {i}: wall {wall}, gap {gap}, open {open}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_cover_has_bounded_zero_wait_and_no_late_damage() {
|
||||
let from = DVec3::new(0.5, 71.5, 8.5);
|
||||
let mut unknown = Ground::default();
|
||||
assert_eq!(unknown.world.blast_cover(from, DVec3::new(10.5, 71.5, 8.5), 10), None);
|
||||
let mut sim = victim();
|
||||
let mut open = victim();
|
||||
sim.blast_damage(from, 2, &unknown, &mut Out::default());
|
||||
open.blast_damage(from, 2, &cache(&CoverWorld { wall: false, opening: false }), &mut Out::default());
|
||||
assert_eq!(sim.sams[0].health, open.sams[0].health);
|
||||
let health = sim.sams[0].health;
|
||||
let hit_points = sim.target_health[0];
|
||||
assert!(unknown.world.take_wanted(10).is_empty());
|
||||
assert!(unknown.world.take_rays(10).is_empty());
|
||||
// A reply, even one proving cover, is not a second combat event.
|
||||
unknown.world.answer(vec![world::SectionAnswer { sec: (0, 4, 0), fill: world::Fill::Full }], 11);
|
||||
assert_eq!(sim.sams[0].health, health);
|
||||
assert_eq!(sim.target_health[0], hit_points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_contact_is_not_weakened_by_cover_and_blast_does_not_mutate_world() {
|
||||
let mut sim = victim();
|
||||
let ground = cache(&CoverWorld { wall: true, opening: false });
|
||||
let from = DVec3::new(0.5, 71.5, 8.5);
|
||||
let end = DVec3::new(10.5, 71.5, 8.5);
|
||||
assert_eq!(ground.world.blast_cover(from, end, 10), Some(true));
|
||||
sim.blast_damage(from, 2, &ground, &mut Out::default());
|
||||
assert_eq!(ground.world.blast_cover(from, end, 10), Some(true));
|
||||
let at = sim.sams[0].pos + DVec3::Y;
|
||||
sim.direct_hit(at, DVec3::X * 2.0, 1, &mut Out::default());
|
||||
assert!(!sim.sams[0].alive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_cover_is_unresolved_and_source_voxel_does_not_shield_everything() {
|
||||
let ground = cache(&CoverWorld { wall: true, opening: false });
|
||||
let from = DVec3::new(0.5, 71.5, 8.5);
|
||||
let end = DVec3::new(10.5, 71.5, 8.5);
|
||||
assert_eq!(ground.world.blast_cover(from, end, 1210), None);
|
||||
let source = DVec3::new(8.5, 71.5, 8.5);
|
||||
assert_eq!(ground.world.blast_cover(source, end, 10), Some(false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2386,7 +2386,7 @@ impl Sim {
|
||||
out.jolt((1.0 - d / reach) as f32 * strength, [1.0, 0.8, 1.6, 2.2, 3.0][power]);
|
||||
}
|
||||
}
|
||||
self.blast_damage(pos, power, out);
|
||||
self.blast_damage(pos, power, ground, out);
|
||||
if self.damage && tnt > 0 {
|
||||
for i in 0..tnt {
|
||||
out.commands.push(format!(
|
||||
@@ -2416,17 +2416,17 @@ impl Sim {
|
||||
}
|
||||
|
||||
/// A drone hit by the guns of system `by`.
|
||||
fn shoot_down(&mut self, idx: usize, by: u32, missile: Option<u32>, out: &mut Out) -> Option<Outcome> {
|
||||
fn shoot_down(&mut self, idx: usize, by: u32, missile: Option<u32>, ground: &Ground, out: &mut Out) -> Option<Outcome> {
|
||||
let kind = self.sams.iter().find(|s| s.id == by).map_or(SamKind::Gepard, |s| s.kind);
|
||||
let shell = kind.spec().gun.as_ref().map_or(5.0, |g| g.shell);
|
||||
let at = self.drones[idx].pos;
|
||||
self.shoot_down_by(idx, by, missile, Impact::gun(kind, at, shell), out)
|
||||
self.shoot_down_by(idx, by, missile, Impact::gun(kind, at, shell), ground, out)
|
||||
}
|
||||
|
||||
/// A drone brought down by `imp` from system `by`: how it ends is rolled once (combat randomness, so a
|
||||
/// detonation really means no ground impact), recorded with the kill, and drawn from the visual randomness
|
||||
/// only. The kill is counted here and nowhere else.
|
||||
fn shoot_down_by(&mut self, idx: usize, by: u32, missile: Option<u32>, imp: Impact, out: &mut Out) -> Option<Outcome> {
|
||||
fn shoot_down_by(&mut self, idx: usize, by: u32, missile: Option<u32>, imp: Impact, ground: &Ground, out: &mut Out) -> Option<Outcome> {
|
||||
if !self.drones[idx].alive() {
|
||||
return None;
|
||||
}
|
||||
@@ -2489,7 +2489,7 @@ impl Sim {
|
||||
out.particles("minecraft:firework", pos, 0.6, DVec3::ZERO, 0.5, 50);
|
||||
// The shock arrives after the flash (`boom` sounds travel); the warhead's blast hurts what is near.
|
||||
out.sound("airdef:boom.sonic", pos, 0.8, 1.2);
|
||||
self.blast_damage(pos, 0, out);
|
||||
self.blast_damage(pos, 0, ground, out);
|
||||
}
|
||||
}
|
||||
out.sound(sound, pos, gain, pitch);
|
||||
@@ -2927,7 +2927,7 @@ impl Sim {
|
||||
}
|
||||
for (t, by) in gun_hits {
|
||||
if let Some(i) = self.drones.iter().position(|d| d.id == t) {
|
||||
let _ = self.shoot_down(i, by, None, out);
|
||||
let _ = self.shoot_down(i, by, None, ground, out);
|
||||
}
|
||||
}
|
||||
for i in 0..self.sams.len() {
|
||||
@@ -3133,7 +3133,7 @@ impl Sim {
|
||||
self.drones[t].engaged = self.drones[t].engaged.saturating_sub(1);
|
||||
if kill {
|
||||
let (at, closing) = (self.drones[t].pos, (m.vel - self.drones[t].vel).length());
|
||||
let _ = self.shoot_down_by(t, sam, Some(mid), Impact::missile(m.kind, at, closing), out);
|
||||
let _ = self.shoot_down_by(t, sam, Some(mid), Impact::missile(m.kind, at, closing), ground, out);
|
||||
}
|
||||
}
|
||||
// A missile that lost its target frees it for others.
|
||||
|
||||
@@ -247,7 +247,7 @@ mod tests {
|
||||
sim.sams.push(Sam::new(sam, SamKind::Patriot, DVec3::new(0.0, 70.0, 0.0), 0.0));
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, 300.0), DVec3::X, DVec3::new(900.0, 70.0, 0.0));
|
||||
let at = sim.drones[0].pos;
|
||||
let o = sim.shoot_down_by(0, sam, None, imp(at), &mut out).unwrap();
|
||||
let o = sim.shoot_down_by(0, sam, None, imp(at), &Ground::default(), &mut out).unwrap();
|
||||
(o, sim.drones.len(), sim.rng.state())
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
sim.sams.push(Sam::new(sam, SamKind::Patriot, DVec3::new(0.0, 70.0, 0.0), 0.0));
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, 300.0), DVec3::X, DVec3::new(900.0, 70.0, 0.0));
|
||||
let at = sim.drones[0].pos;
|
||||
let o = sim.shoot_down_by(0, sam, Some(5), Impact::missile(MissileKind::M77n6, at, 6.0), &mut out).unwrap();
|
||||
let o = sim.shoot_down_by(0, sam, Some(5), Impact::missile(MissileKind::M77n6, at, 6.0), &Ground::default(), &mut out).unwrap();
|
||||
if o != Outcome::AirDetonation {
|
||||
continue;
|
||||
}
|
||||
@@ -311,7 +311,7 @@ mod tests {
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(f64::from(i) * 500.0, 400.0, 300.0), DVec3::X, DVec3::new(9e5, 70.0, 0.0));
|
||||
let at = sim.drones.last().unwrap().pos;
|
||||
let idx = sim.drones.len() - 1;
|
||||
let _ = sim.shoot_down_by(idx, sam, None, Impact::missile(MissileKind::M77n6, at, 6.0), &mut out);
|
||||
let _ = sim.shoot_down_by(idx, sam, None, Impact::missile(MissileKind::M77n6, at, 6.0), &Ground::default(), &mut out);
|
||||
}
|
||||
let pieces = |s: &Sim| s.fx.iter().filter(|(_, f)| matches!(f, Fx::Piece { .. })).count();
|
||||
let n = pieces(&sim);
|
||||
|
||||
@@ -1376,7 +1376,7 @@ mod tests {
|
||||
sim.sams.push(Sam::new(sam, SamKind::Patriot, DVec3::new(0.0, 70.0, 0.0), 0.0));
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, 300.0), DVec3::X, DVec3::new(900.0, 70.0, 0.0));
|
||||
let at = sim.drones[0].pos;
|
||||
let live = sim.shoot_down_by(0, sam, None, Impact::missile(MissileKind::M77n6, at, 6.0), &mut Out::default()).unwrap();
|
||||
let live = sim.shoot_down_by(0, sam, None, Impact::missile(MissileKind::M77n6, at, 6.0), &Ground::default(), &mut Out::default()).unwrap();
|
||||
let rec = sim.rec.take().unwrap();
|
||||
let recorded = rec.events.iter().find_map(|e| if let EventKind::Kill { outcome, .. } = e.kind { Some(outcome) } else { None });
|
||||
assert_eq!(recorded, Some(live), "the live outcome is what was recorded");
|
||||
|
||||
@@ -64,7 +64,7 @@ fn network_launcher_must_contribute_to_route_risk() {
|
||||
fn repair_must_cancel_pending_magazine_explosions() {
|
||||
let mut sim=Sim::new(1);
|
||||
let id=sim.id();sim.sams.push(Sam::new(id,SamKind::Patriot,DVec3::new(0.,70.,0.),0.));
|
||||
sim.blast_damage(DVec3::new(0.,70.,0.),4,&mut Out::default());
|
||||
sim.blast_damage(DVec3::new(0.,70.,0.),4,&Ground::default(),&mut Out::default());
|
||||
assert!(!sim.later.is_empty());sim.repair();
|
||||
sim.tick=3; sim.step_later(&mut Ground::default(),None,&mut Out::default());
|
||||
assert!(sim.sams[0].alive(),"a fully repaired launcher was destroyed again by its own pre-repair cook-off");
|
||||
@@ -1479,7 +1479,7 @@ fn graphics_settings_do_not_change_combat_randomness_or_ids() {
|
||||
sim.fireball(DVec3::new(f64::from(i), 80.0, 0.0), 4.0, 11);
|
||||
}
|
||||
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 80.0, 0.0), DVec3::X, DVec3::new(900.0, 80.0, 0.0));
|
||||
sim.shoot_down(0, 0, None, &mut out);
|
||||
sim.shoot_down(0, 0, None, &Ground::default(), &mut out);
|
||||
let id = sim.id();
|
||||
(sim.drones[0].id, id, sim.aim_error(DroneKind::Shahed))
|
||||
};
|
||||
|
||||
@@ -147,7 +147,7 @@ fn a_blast_from_behind_hurts_the_link_a_blast_from_the_front_the_drive() {
|
||||
let id = sim.id();
|
||||
sim.sams.push(Sam::new(id, SamKind::Patriot, at, 0.0));
|
||||
let mut out = Out::default();
|
||||
sim.blast_damage(at + DVec3::new(0.0, 3.0, dz), 2, &mut out);
|
||||
sim.blast_damage(at + DVec3::new(0.0, 3.0, dz), 2, &Ground::default(), &mut out);
|
||||
assert!(sim.sams[0].alive());
|
||||
let c = sim.sams[0].comps;
|
||||
(1.0 - c.get(Part::Comms), 1.0 - c.get(Part::Mount), 1.0 - c.get(Part::Sensor))
|
||||
@@ -168,7 +168,7 @@ fn a_close_blast_can_leave_a_firing_but_immobile_system() {
|
||||
sim.sams.push(Sam::new(id, SamKind::Pantsir, DVec3::new(0.0, 70.0, 0.0), 0.0));
|
||||
let mut out = Out::default();
|
||||
sim.tick = seed;
|
||||
sim.blast_damage(DVec3::new(0.0, 69.0, 6.0 + seed as f64 * 0.06), 3, &mut out);
|
||||
sim.blast_damage(DVec3::new(0.0, 69.0, 6.0 + seed as f64 * 0.06), 3, &Ground::default(), &mut out);
|
||||
let s = &sim.sams[0];
|
||||
if s.alive() && s.part(Part::Tracks) <= 0.0 && s.part(Part::Launcher) > 0.0 && s.part(Part::Gun) > 0.0 {
|
||||
assert!(!sim.order_move(0, DVec3::new(300.0, 70.0, 0.0)), "failed running gear must refuse a march");
|
||||
|
||||
@@ -799,6 +799,55 @@ pub fn section_of(source: &dyn WorldSource, s: Sec) -> Fill {
|
||||
}
|
||||
|
||||
impl WorldCache {
|
||||
/// Read-only occupancy at the instant of a blast. Some(true) is known cover,
|
||||
/// Some(false) a fully known open segment, None unresolved geometry. No query,
|
||||
/// chunk load or later reply can change damage already committed for this blast.
|
||||
/// This deliberately treats partial collision blocks as occupied cubes.
|
||||
pub fn blast_cover(&self, a: DVec3, b: DVec3, now: u64) -> Option<bool> {
|
||||
let d = b - a;
|
||||
let mut p = [a.x.floor() as i32, a.y.floor() as i32, a.z.floor() as i32];
|
||||
let av = [a.x, a.y, a.z];
|
||||
let dv = [d.x, d.y, d.z];
|
||||
let step = dv.map(|v| if v > 0.0 { 1 } else { -1 });
|
||||
let mut next = [f64::INFINITY; 3];
|
||||
let mut delta = [f64::INFINITY; 3];
|
||||
for i in 0..3 {
|
||||
if dv[i] != 0.0 {
|
||||
let edge = f64::from(p[i]) + if dv[i] > 0.0 { 1.0 } else { 0.0 };
|
||||
next[i] = (edge - av[i]) / dv[i];
|
||||
delta[i] = (1.0 / dv[i]).abs();
|
||||
}
|
||||
}
|
||||
let mut unknown = false;
|
||||
for n in 0..256 {
|
||||
let fill = self.secs.get(&sec_of(p[0], p[1], p[2]))
|
||||
.filter(|e| !e.stale && now.saturating_sub(e.since) < RECHECK)
|
||||
.map(|e| &e.fill);
|
||||
let solid = match fill {
|
||||
Some(Fill::Empty) => Some(false),
|
||||
Some(Fill::Full) => Some(true),
|
||||
Some(Fill::Mixed(bits)) => {
|
||||
let i = ((p[1] & 15) << 8 | (p[2] & 15) << 4 | (p[0] & 15)) as usize;
|
||||
Some(bits[i >> 3] >> (i & 7) & 1 != 0)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match solid {
|
||||
// Ignore the struck/origin voxel, not the next wall behind it.
|
||||
Some(true) if n > 0 => return Some(true),
|
||||
None => unknown = true,
|
||||
_ => {}
|
||||
}
|
||||
let axis = if next[0] < next[1] {
|
||||
if next[0] < next[2] { 0 } else { 2 }
|
||||
} else if next[1] < next[2] { 1 } else { 2 };
|
||||
if next[axis] > 1.0 { return if unknown { None } else { Some(false) }; }
|
||||
next[axis] += delta[axis];
|
||||
p[axis] += step[axis];
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Answers the exact rays of the last tick from a synthetic world, as the game does one tick later.
|
||||
pub fn serve_rays(&mut self, source: &dyn WorldSource, rays: Vec<(u32, DVec3, DVec3)>) {
|
||||
let replies: Vec<RayReply> = rays
|
||||
|
||||
@@ -71,6 +71,26 @@ Metal-проверки не подтверждают Windows/Linux, HDR всех
|
||||
|
||||
## Важные ограничения для следующего агента
|
||||
|
||||
### #119: непрямой взрыв и укрытия
|
||||
|
||||
`WorldCache::blast_cover` читает текущую occupancy без запросов мира и без загрузки чанков.
|
||||
Три луча к репрезентативным точкам корпуса дают общий множитель для корпуса и узлов;
|
||||
для отмеченного объекта считается своя тройка. Известное укрытие оставляет 15% игрового
|
||||
воздействия по перекрытому лучу, открытый — 100%; края/проёмы дают промежуточное значение.
|
||||
Все лучи этого вызова читаются до отправки разрушения в движок. Исходный воксель пропускается,
|
||||
чтобы контакт на поверхности стены не закрывал взрыв сразу во все стороны.
|
||||
|
||||
Unknown, stale и слишком старые секции считаются **неразрешёнными**. Ожидание равно нулю:
|
||||
для них сохраняется прежний неослабленный урон как консервативная игровая политика,
|
||||
а не утверждение, что там воздух. После ответа мира этот взрыв не пересчитывается.
|
||||
Нет очереди отложенного урона и повторного применения на world reply. Частичные блоки
|
||||
на этом этапе считаются занятым кубом; тонкая collision shape для blast отдельно не запрашивается.
|
||||
Прямой вооружённый контакт не ослабляется этим множителем.
|
||||
|
||||
Проверки `sim::damage::cover_tests`: стена/проём для корпуса, узлов и объекта;
|
||||
Unknown и поздний ответ без повторного урона/запросов; прямое попадание; устаревшая геометрия;
|
||||
пропуск исходного блока. Все четыре прошли. Объединённый полный прогон будет указан ниже.
|
||||
|
||||
- Не закрывать issue даже после исправления; сообщать проверенный объём и остаток в комментарии.
|
||||
- Не переписывать gitlink на плавающую ветку. Изменения движка сначала публикуются отдельно.
|
||||
- Не путать наблюдённый трек с настоящим положением цели и `own_tick` с relay.
|
||||
|
||||
Reference in New Issue
Block a user