distinct bullet hit entities

This commit is contained in:
jacob 2026-03-19 19:49:14 -05:00
parent a76cfc5dfd
commit 18ad1fb69e
6 changed files with 635 additions and 411 deletions

View File

@ -145,65 +145,76 @@ f64 CR_FloatFromString(String str)
} }
} }
// Find decimal place String whole_str = STRING(str.len - whole_start_idx, &str.text[whole_start_idx]);
u64 frac_start_idx = whole_start_idx; if (MatchString(whole_str, Lit("inf")))
for (; ok && frac_start_idx < str.len; ++frac_start_idx)
{ {
u8 c = str.text[frac_start_idx]; result = Inf * sign;
if (c == '.')
{
break;
}
} }
else if (MatchString(whole_str, Lit("NaN")))
// Parse whole part
u64 whole_part = 0;
for (u64 char_idx = whole_start_idx; ok && char_idx < frac_start_idx; ++char_idx)
{ {
u8 c = str.text[char_idx]; result = Nan;
if (c >= '0' && c <= '9')
{
u8 digit = c - '0';
whole_part += digit * PowU64(10, frac_start_idx - (char_idx + 1));
}
else
{
ok = 0;
}
}
// Parse frac part
u64 frac_part = 0;
for (u64 char_idx = frac_start_idx + 1; ok && char_idx < str.len; ++char_idx)
{
u8 c = str.text[char_idx];
if (c >= '0' && c <= '9')
{
u8 digit = c - '0';
frac_part += digit * PowU64(10, str.len - (char_idx + 1));
}
else
{
ok = 0;
}
}
if (ok)
{
if (frac_part != 0)
{
result = ((f64)whole_part + ((f64)frac_part / PowU64(10, str.len - (frac_start_idx + 1)))) * sign;
}
else
{
result = (f64)whole_part * sign;
}
} }
else else
{ {
result = 0; // Find decimal place
} u64 frac_start_idx = whole_start_idx;
for (; ok && frac_start_idx < str.len; ++frac_start_idx)
{
u8 c = str.text[frac_start_idx];
if (c == '.')
{
break;
}
}
// Parse whole part
u64 whole_part = 0;
for (u64 char_idx = whole_start_idx; ok && char_idx < frac_start_idx; ++char_idx)
{
u8 c = str.text[char_idx];
if (c >= '0' && c <= '9')
{
u8 digit = c - '0';
whole_part += digit * PowU64(10, frac_start_idx - (char_idx + 1));
}
else
{
ok = 0;
}
}
// Parse frac part
u64 frac_part = 0;
for (u64 char_idx = frac_start_idx + 1; ok && char_idx < str.len; ++char_idx)
{
u8 c = str.text[char_idx];
if (c >= '0' && c <= '9')
{
u8 digit = c - '0';
frac_part += digit * PowU64(10, str.len - (char_idx + 1));
}
else
{
ok = 0;
}
}
if (ok)
{
if (frac_part != 0)
{
result = ((f64)whole_part + ((f64)frac_part / PowU64(10, str.len - (frac_start_idx + 1)))) * sign;
}
else
{
result = (f64)whole_part * sign;
}
}
else
{
result = 0;
}
}
return result; return result;
} }

View File

@ -3184,137 +3184,140 @@ void P_StepFrame(P_Frame *frame)
// TODO: Separate 'hits' from bullets, so that bullets can have multiple hits // TODO: Separate 'hits' from bullets, so that bullets can have multiple hits
for (P_Ent *bullet = P_FirstEnt(frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
{ {
if (bullet->is_bullet) P_EntList hits_to_spawn = Zi;
for (P_Ent *bullet = P_FirstEnt(frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
{ {
P_Ent *weapon = P_EntFromKey(frame, bullet->source); if (bullet->is_bullet)
P_Ent *firer = P_EntFromKey(frame, weapon->source);
P_Ent *damager = P_EntFromKey(frame, bullet->damage_attribution);
b32 is_first_bullet_tick = bullet->created_at_tick == frame->tick;
//////////////////////////////
//- Bullet properties
f32 spread = Tau * 0.05;
// f32 spread = Tau * 0.01;
// f32 spread = 0;
b32 should_ricochet = 0;
f32 initial_speed = 1;
f32 speed_falloff = 0;
if (weapon->is_uzi)
{ {
initial_speed = TweakFloat("Bullet speed", 100, 1, 100); P_Ent *weapon = P_EntFromKey(frame, bullet->source);
} P_Ent *firer = P_EntFromKey(frame, weapon->source);
else if (weapon->is_launcher) P_Ent *damager = P_EntFromKey(frame, bullet->damage_attribution);
{
should_ricochet = 1;
initial_speed = 50;
// initial_speed = 100;
speed_falloff = 5;
}
////////////////////////////// b32 is_first_bullet_tick = bullet->created_at_tick == frame->tick;
//- Initialize
// if (bullet->created_at_tick != frame->tick) //////////////////////////////
// { //- Bullet properties
// Vec2 start = bullet->bullet_start;
// Vec2 end = bullet->bullet_end;
// Vec2 vel = SubVec2(end, start);
// bullet->bullet_start = end;
// bullet->bullet_end = AddVec2(end, vel);
// bullet->xf.t = bullet->bullet_start;
// bullet->xf.r = NormVec2(vel);
// }
Struct(BulletPath) f32 spread = Tau * 0.05;
{ // f32 spread = Tau * 0.01;
BulletPath *next; // f32 spread = 0;
Vec2 start;
Vec2 end;
};
BulletPath *first_bullet_path = 0;
BulletPath *last_bullet_path = 0;
if (is_first_bullet_tick) b32 should_ricochet = 0;
{
Vec2 fire_pos = Zi; f32 initial_speed = 1;
Vec2 fire_dir = Zi; f32 speed_falloff = 0;
Vec2 fire_base0 = Zi; if (weapon->is_uzi)
Vec2 fire_base1 = Zi;
{ {
Vec2 look = firer->control.look; initial_speed = TweakFloat("Bullet speed", 100, 1, 100);
P_Anim anim = P_AnimFromEnt(frame, firer); }
SPR_Sprite body = SPR_SpriteFromSheet(anim.sheet, anim.span, anim.frame_seq); else if (weapon->is_launcher)
SPR_Sprite wep = SPR_SpriteFromSheet(anim.wep_sheet, anim.span, anim.frame_seq); {
should_ricochet = 1;
initial_speed = 50;
// initial_speed = 100;
speed_falloff = 5;
}
//- Compute sprite transforms //////////////////////////////
Affine ent_to_world_af = MulAffineXform(AffineIdentity, firer->xf); //- Initialize
Affine body_pix_to_world_af = AffineIdentity;
Affine wep_pix_to_world_af = AffineIdentity; // if (bullet->created_at_tick != frame->tick)
// {
// Vec2 start = bullet->bullet_start;
// Vec2 end = bullet->bullet_end;
// Vec2 vel = SubVec2(end, start);
// bullet->bullet_start = end;
// bullet->bullet_end = AddVec2(end, vel);
// bullet->xf.t = bullet->bullet_start;
// bullet->xf.r = NormVec2(vel);
// }
Struct(BulletPath)
{
BulletPath *next;
Vec2 start;
Vec2 end;
};
BulletPath *first_bullet_path = 0;
BulletPath *last_bullet_path = 0;
if (is_first_bullet_tick)
{
Vec2 fire_pos = Zi;
Vec2 fire_dir = Zi;
Vec2 fire_base0 = Zi;
Vec2 fire_base1 = Zi;
{ {
Vec2 pix_scale = VEC2(1.0 / P_CellsPerMeter, 1.0 / P_CellsPerMeter); Vec2 look = firer->control.look;
P_Anim anim = P_AnimFromEnt(frame, firer);
SPR_Sprite body = SPR_SpriteFromSheet(anim.sheet, anim.span, anim.frame_seq);
SPR_Sprite wep = SPR_SpriteFromSheet(anim.wep_sheet, anim.span, anim.frame_seq);
//- Compute body transform //- Compute sprite transforms
Affine body_pix_to_ent_af = AffineIdentity; Affine ent_to_world_af = MulAffineXform(AffineIdentity, firer->xf);
Affine body_pix_to_world_af = AffineIdentity;
Affine wep_pix_to_world_af = AffineIdentity;
{ {
body_pix_to_ent_af = ScaleAffine(body_pix_to_ent_af, pix_scale); Vec2 pix_scale = VEC2(1.0 / P_CellsPerMeter, 1.0 / P_CellsPerMeter);
SPR_Ray anchor_ray = body.rays[SPR_RayKind_Anchor]; //- Compute body transform
body_pix_to_ent_af = RotateAffine(body_pix_to_ent_af, InvertRot(anchor_ray.dir)); Affine body_pix_to_ent_af = AffineIdentity;
body_pix_to_ent_af = TranslateAffine(body_pix_to_ent_af, NegVec2(anchor_ray.pos)); {
body_pix_to_ent_af = ScaleAffine(body_pix_to_ent_af, pix_scale);
SPR_Ray anchor_ray = body.rays[SPR_RayKind_Anchor];
body_pix_to_ent_af = RotateAffine(body_pix_to_ent_af, InvertRot(anchor_ray.dir));
body_pix_to_ent_af = TranslateAffine(body_pix_to_ent_af, NegVec2(anchor_ray.pos));
}
//- Compute weapon transform
Affine wep_pix_to_ent_af = AffineIdentity;
{
wep_pix_to_ent_af = ScaleAffine(wep_pix_to_ent_af, pix_scale);
SPR_Ray body_anchor_ray = body.rays[SPR_RayKind_Anchor];
SPR_Ray body_ap_ray = body.rays[SPR_RayKind_Ap];
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, InvertRot(body_anchor_ray.dir));
wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, SubVec2(body_ap_ray.pos, body_anchor_ray.pos));
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, NegVec2(body_ap_ray.dir));
SPR_Ray anchor_ray = wep.rays[SPR_RayKind_Anchor];
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, anchor_ray.dir);
wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, NegVec2(anchor_ray.pos));
}
body_pix_to_world_af = MulAffine(ent_to_world_af, body_pix_to_ent_af);
wep_pix_to_world_af = MulAffine(ent_to_world_af, wep_pix_to_ent_af);
} }
//- Compute weapon transform SPR_Ray muzzle_ray = wep.rays[SPR_RayKind_Ap];
Affine wep_pix_to_ent_af = AffineIdentity; fire_pos = MulAffineVec2(wep_pix_to_world_af, muzzle_ray.pos);
{ fire_dir = NormVec2(MulAffineBasisVec2(wep_pix_to_world_af, muzzle_ray.dir));
wep_pix_to_ent_af = ScaleAffine(wep_pix_to_ent_af, pix_scale);
SPR_Ray body_anchor_ray = body.rays[SPR_RayKind_Anchor]; fire_base0 = MulVec2(PerpVec2(NegVec2(firer->xf.r)), WedgeVec2(firer->xf.r, SubVec2(firer->xf.t, fire_pos)));
SPR_Ray body_ap_ray = body.rays[SPR_RayKind_Ap]; fire_base0 = AddVec2(fire_base0, firer->xf.t);
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, InvertRot(body_anchor_ray.dir));
wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, SubVec2(body_ap_ray.pos, body_anchor_ray.pos));
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, NegVec2(body_ap_ray.dir));
SPR_Ray anchor_ray = wep.rays[SPR_RayKind_Anchor]; Vec2 chamber_pos = MulAffineVec2(body_pix_to_world_af, body.rays[SPR_RayKind_Ap].pos);
wep_pix_to_ent_af = RotateAffine(wep_pix_to_ent_af, anchor_ray.dir);
wep_pix_to_ent_af = TranslateAffine(wep_pix_to_ent_af, NegVec2(anchor_ray.pos));
}
body_pix_to_world_af = MulAffine(ent_to_world_af, body_pix_to_ent_af); fire_base1 = MulVec2(PerpVec2(fire_dir), WedgeVec2(fire_dir, SubVec2(fire_pos, chamber_pos)));
wep_pix_to_world_af = MulAffine(ent_to_world_af, wep_pix_to_ent_af); fire_base1 = AddVec2(fire_base1, chamber_pos);
P_DebugDrawLine(fire_base0, fire_base1, Color_Yellow);
P_DebugDrawLine(fire_base1, fire_pos, Color_Green);
P_DebugDrawPoint(fire_base0, Color_Yellow);
P_DebugDrawPoint(fire_base1, Color_Green);
P_DebugDrawPoint(fire_pos, Color_Red);
Vec2 bullet_dir = RotateVec2Angle(fire_dir, spread * (Norm24(MixU64s(bullet->key.v, P_BulletSpreadBasis)) - 0.5));
bullet->xf.t = fire_pos;
bullet->xf.r = NormVec2(bullet_dir);
bullet->v = MulVec2(NormVec2(bullet_dir), initial_speed);
bullet->v = AddVec2(bullet->v, firer->v);
} }
SPR_Ray muzzle_ray = wep.rays[SPR_RayKind_Ap];
fire_pos = MulAffineVec2(wep_pix_to_world_af, muzzle_ray.pos);
fire_dir = NormVec2(MulAffineBasisVec2(wep_pix_to_world_af, muzzle_ray.dir));
fire_base0 = MulVec2(PerpVec2(NegVec2(firer->xf.r)), WedgeVec2(firer->xf.r, SubVec2(firer->xf.t, fire_pos)));
fire_base0 = AddVec2(fire_base0, firer->xf.t);
Vec2 chamber_pos = MulAffineVec2(body_pix_to_world_af, body.rays[SPR_RayKind_Ap].pos);
fire_base1 = MulVec2(PerpVec2(fire_dir), WedgeVec2(fire_dir, SubVec2(fire_pos, chamber_pos)));
fire_base1 = AddVec2(fire_base1, chamber_pos);
P_DebugDrawLine(fire_base0, fire_base1, Color_Yellow);
P_DebugDrawLine(fire_base1, fire_pos, Color_Green);
P_DebugDrawPoint(fire_base0, Color_Yellow);
P_DebugDrawPoint(fire_base1, Color_Green);
P_DebugDrawPoint(fire_pos, Color_Red);
Vec2 bullet_dir = RotateVec2Angle(fire_dir, spread * (Norm24(MixU64s(bullet->key.v, P_BulletSpreadBasis)) - 0.5));
bullet->xf.t = fire_pos;
bullet->xf.r = NormVec2(bullet_dir);
bullet->v = MulVec2(NormVec2(bullet_dir), initial_speed);
bullet->v = AddVec2(bullet->v, firer->v);
}
@ -3322,168 +3325,171 @@ void P_StepFrame(P_Frame *frame)
// On bullet's first tick, we want to ensure that the firer/weapon
// On bullet's first tick, we want to ensure that the firer/weapon // wasn't obstructed (e.g. to prevent shooting through walls), so we
// wasn't obstructed (e.g. to prevent shooting through walls), so we // insert a path from the bullet's base to its starting position before
// insert a path from the bullet's base to its starting position before // its actual firing path
// its actual firing path
{
// Firer origin -> weapon chamber path
BulletPath *path = PushStruct(scratch.arena, BulletPath);
SllQueuePush(first_bullet_path, last_bullet_path, path);
path->start = fire_base0;
path->end = fire_base1;
}
{
// Weapon chamber -> bullet start path
BulletPath *path = PushStruct(scratch.arena, BulletPath);
SllQueuePush(first_bullet_path, last_bullet_path, path);
path->start = fire_base1;
path->end = fire_pos;
}
}
Vec2 dir = MulVec2(bullet->v, sim_dt);
Vec2 p0 = bullet->xf.t;
Vec2 p1 = p0;
// Cur pos -> next pos path
if (!is_first_bullet_tick)
{
p1 = AddVec2(p0, dir);
BulletPath *path = PushStruct(scratch.arena, BulletPath);
SllQueuePush(first_bullet_path, last_bullet_path, path);
path->start = p0;
path->end = p1;
}
P_EntKey victim_key = Zi;
P_RaycastResult victim_raycast = Zi;
b32 hit = 0;
{
for (BulletPath *path = first_bullet_path; path && !hit; path = path->next)
{
Vec2 path_dir = SubVec2(path->end, path->start);
P_Space *cast_spaces[] = {
&world->walls_space,
&post_solve_ents_space,
};
P_SpaceEntryList cast_entries = Zi;
P_UniqueSpaceEntriesFromRay(
scratch.arena,
&cast_entries,
countof(cast_spaces),
cast_spaces,
path->start,
path->end
);
f32 closest_len_sq = Inf;
for (P_SpaceEntryNode *entry_node = cast_entries.first; entry_node; entry_node = entry_node->next)
{ {
P_SpaceEntry *entry = &entry_node->entry; // Firer origin -> weapon chamber path
P_EntKey potential_victim_key = (P_EntKey) { .v = entry->shape_id }; BulletPath *path = PushStruct(scratch.arena, BulletPath);
P_Ent *potential_victim = P_EntFromKey(frame, potential_victim_key); SllQueuePush(first_bullet_path, last_bullet_path, path);
b32 can_hit = ( path->start = fire_base0;
P_IsEntNil(potential_victim) || path->end = fire_base1;
(potential_victim->is_guy && (!P_MatchEntKey(potential_victim->key, firer->key) || P_IsEntKeyNil(firer->key))) }
{
// Weapon chamber -> bullet start path
BulletPath *path = PushStruct(scratch.arena, BulletPath);
SllQueuePush(first_bullet_path, last_bullet_path, path);
path->start = fire_base1;
path->end = fire_pos;
}
}
Vec2 dir = MulVec2(bullet->v, sim_dt);
Vec2 p0 = bullet->xf.t;
Vec2 p1 = p0;
// Cur pos -> next pos path
if (!is_first_bullet_tick)
{
p1 = AddVec2(p0, dir);
BulletPath *path = PushStruct(scratch.arena, BulletPath);
SllQueuePush(first_bullet_path, last_bullet_path, path);
path->start = p0;
path->end = p1;
}
P_EntKey victim_key = Zi;
P_RaycastResult victim_raycast = Zi;
b32 has_hit = 0;
{
for (BulletPath *path = first_bullet_path; path && !has_hit; path = path->next)
{
Vec2 path_dir = SubVec2(path->end, path->start);
P_Space *cast_spaces[] = {
&world->walls_space,
&post_solve_ents_space,
};
P_SpaceEntryList cast_entries = Zi;
P_UniqueSpaceEntriesFromRay(
scratch.arena,
&cast_entries,
countof(cast_spaces),
cast_spaces,
path->start,
path->end
); );
if (can_hit) f32 closest_len_sq = Inf;
for (P_SpaceEntryNode *entry_node = cast_entries.first; entry_node; entry_node = entry_node->next)
{ {
P_Shape potential_victim_shape = entry->shape; P_SpaceEntry *entry = &entry_node->entry;
P_RaycastResult entrance_raycast = P_RaycastShape(potential_victim_shape, path->start, path_dir); P_EntKey potential_victim_key = (P_EntKey) { .v = entry->shape_id };
Vec2 entrance = entrance_raycast.p; P_Ent *potential_victim = P_EntFromKey(frame, potential_victim_key);
if (entrance_raycast.is_intersecting) b32 can_hit = (
P_IsEntNil(potential_victim) ||
(potential_victim->is_guy && (!P_MatchEntKey(potential_victim->key, firer->key) || P_IsEntKeyNil(firer->key)))
);
if (can_hit)
{ {
P_RaycastResult exit_raycast = P_RaycastShape(potential_victim_shape, path->start, NegVec2(path_dir)); P_Shape potential_victim_shape = entry->shape;
Vec2 exit = exit_raycast.p; P_RaycastResult entrance_raycast = P_RaycastShape(potential_victim_shape, path->start, path_dir);
f32 da = DotVec2(path_dir, SubVec2(entrance, path->start)); Vec2 entrance = entrance_raycast.p;
f32 db = DotVec2(path_dir, SubVec2(exit, path->start)); if (entrance_raycast.is_intersecting)
if (db > 0 && (da <= Vec2LenSq(path_dir) || da <= 0))
{ {
f32 len_sq = Vec2LenSq(SubVec2(entrance_raycast.p, path->start)); P_RaycastResult exit_raycast = P_RaycastShape(potential_victim_shape, path->start, NegVec2(path_dir));
if (len_sq < closest_len_sq) Vec2 exit = exit_raycast.p;
f32 da = DotVec2(path_dir, SubVec2(entrance, path->start));
f32 db = DotVec2(path_dir, SubVec2(exit, path->start));
if (db > 0 && (da <= Vec2LenSq(path_dir) || da <= 0))
{ {
closest_len_sq = len_sq; f32 len_sq = Vec2LenSq(SubVec2(entrance_raycast.p, path->start));
victim_key = potential_victim_key; if (len_sq < closest_len_sq)
victim_raycast = entrance_raycast; {
hit = 1; closest_len_sq = len_sq;
victim_key = potential_victim_key;
victim_raycast = entrance_raycast;
has_hit = 1;
}
} }
} }
} }
} }
} }
} }
} P_Ent *victim = P_EntFromKey(frame, victim_key);
P_Ent *victim = P_EntFromKey(frame, victim_key);
bullet->has_hit = 0; // Create hit
Vec2 final_pos = p1;
Vec2 final_pos = p1; if (has_hit)
if (hit)
{
Vec2 normal = victim_raycast.normal;
bullet->has_hit = 1;
bullet->hit_entry = victim_raycast.p;
bullet->hit_entry_normal = normal;
// FIXME: Use relative velocity at collision point
bullet->hit_entry_velocity = bullet->v;
// bullet->bullet_end = bullet->hit_entry;
if (victim->is_guy)
{ {
bullet->hit_material = P_MaterialKind_Flesh; Vec2 normal = victim_raycast.normal;
} bullet->bullet_hits_count += 1;
else
{ P_Ent *hit = P_PushTempEnt(scratch.arena, &hits_to_spawn);
bullet->hit_material = P_MaterialKind_Wall; {
hit->key = P_EntKeyFromU64(MixU64s(bullet->key.v, P_BulletHitBasis + bullet->bullet_hits_count));
hit->is_hit = 1;
hit->hit_entry = victim_raycast.p;
hit->hit_entry_normal = normal;
hit->lifetime_seconds = P_ObservationDurationSeconds;
// FIXME: Use relative velocity at collision point
hit->hit_entry_velocity = bullet->v;
if (victim->is_guy)
{
hit->hit_material = P_MaterialKind_Flesh;
}
else
{
hit->hit_material = P_MaterialKind_Wall;
}
}
// Reflect velocity along normal
if (should_ricochet)
{
bullet->v = SubVec2(bullet->v, MulVec2(normal, 2 * DotVec2(bullet->v, normal)));
bullet->v = MulVec2(bullet->v, 0.5);
}
else
{
bullet->exists = 0;
}
f32 collision_offset = 0.01; // Tiny offset along normal to prevent collision with the victim during ricochets
final_pos = AddVec2(hit->hit_entry, MulVec2(hit->hit_entry_normal, collision_offset));
} }
// Reflect velocity along normal bullet->xf.t = final_pos;
if (should_ricochet) if (Vec2LenSq(dir) > (0.001 * 0.001))
{ {
bullet->v = SubVec2(bullet->v, MulVec2(normal, 2 * DotVec2(bullet->v, normal))); bullet->xf.r = NormVec2(dir);
bullet->v = MulVec2(bullet->v, 0.5);
} }
else bullet->v = MulVec2(bullet->v, 1.0 - SaturateF32(speed_falloff * sim_dt));
// TODO: Remove this
if (!is_client && !P_IsEntNil(victim))
{
if (damager->is_player)
{
victim->damage_attribution = damager->key;
}
victim->health -= 0.25;
}
// Prune out of bounds bullet
Rng2 bounds = Zi;
bounds.p0 = VEC2(-P_WorldPitch / 2, -P_WorldPitch / 2);
bounds.p1 = VEC2(P_WorldPitch / 2, P_WorldPitch / 2);
if (
bullet->xf.t.x < bounds.p0.x || bullet->xf.t.y < bounds.p0.y ||
bullet->xf.t.x > bounds.p1.x || bullet->xf.t.y > bounds.p1.y
)
{ {
bullet->exists = 0; bullet->exists = 0;
} }
f32 collision_offset = 0.01; // Tiny offset along normal to prevent collision with the victim during ricochets
final_pos = AddVec2(bullet->hit_entry, MulVec2(bullet->hit_entry_normal, collision_offset));
}
bullet->xf.t = final_pos;
if (Vec2LenSq(dir) > (0.001 * 0.001))
{
bullet->xf.r = NormVec2(dir);
}
bullet->v = MulVec2(bullet->v, 1.0 - SaturateF32(speed_falloff * sim_dt));
// TODO: Remove this
if (!is_client && !P_IsEntNil(victim))
{
if (damager->is_player)
{
victim->damage_attribution = damager->key;
}
victim->health -= 0.25;
}
// Prune out of bounds bullet
Rng2 bounds = Zi;
bounds.p0 = VEC2(-P_WorldPitch / 2, -P_WorldPitch / 2);
bounds.p1 = VEC2(P_WorldPitch / 2, P_WorldPitch / 2);
if (
bullet->xf.t.x < bounds.p0.x || bullet->xf.t.y < bounds.p0.y ||
bullet->xf.t.x > bounds.p1.x || bullet->xf.t.y > bounds.p1.y
)
{
bullet->exists = 0;
} }
} }
P_SpawnEntsFromList(frame, hits_to_spawn);
} }

View File

@ -8,6 +8,7 @@
#define P_WallShapeIDBasis 0x40d501b4cf6d4f0cull #define P_WallShapeIDBasis 0x40d501b4cf6d4f0cull
#define P_BulletSpreadBasis 0xc3b72fe38ca5a1d6ull #define P_BulletSpreadBasis 0xc3b72fe38ca5a1d6ull
#define P_BulletHitBasis 0xbc70fc783c1c507full
Struct(P_EntKey) Struct(P_EntKey)
{ {
@ -84,7 +85,7 @@ Struct(P_DebugDrawNode)
#define P_RollTimeNs NsFromSeconds(0.5) #define P_RollTimeNs NsFromSeconds(0.5)
#define P_RollTurnTimeNs (NsFromSeconds(0.1)) #define P_RollTurnTimeNs (NsFromSeconds(0.1))
#define P_RollTimeoutNs NsFromSeconds(0.5) #define P_RollTimeoutNs NsFromSeconds(0.5)
#define P_ObservationLifetimeNs NsFromSeconds(2) #define P_ObservationDurationSeconds 1
Struct(P_Control) Struct(P_Control)
{ {
@ -121,8 +122,9 @@ Struct(P_Ent)
//- Client data //- Client data
i64 observation_time_ns; i64 initial_observation_time_ns;
b32 has_observed; i64 last_observation_time_ns;
b32 is_first_observation;
//- Build data //- Build data
@ -137,11 +139,14 @@ Struct(P_Ent)
Xform xf; Xform xf;
//- Bullet / hit //- Bullet
b32 is_bullet; b32 is_bullet;
u32 bullet_hits_count;
b32 has_hit; //- Hit
b32 is_hit;
Vec2 hit_entry; Vec2 hit_entry;
Vec2 hit_entry_normal; Vec2 hit_entry_normal;
Vec2 hit_entry_velocity; Vec2 hit_entry_velocity;

View File

@ -23,7 +23,7 @@ String P_PackWorld(Arena *arena, P_World *src_world)
{ {
// TODO: Pack ignored ents // TODO: Pack ignored ents
b32 ignore = 0; b32 ignore = 0;
if (ent->is_weapon || ent->is_bullet || ent->is_bomb) if (ent->is_weapon || ent->is_bullet || ent->is_bomb || ent->is_hit)
{ {
ignore = 1; ignore = 1;
} }

View File

@ -1895,14 +1895,16 @@ void V_TickForever(WaveLaneCtx *lane)
////////////////////////////// //////////////////////////////
//- Observe entities //- Observe entities
for (P_Ent *ent = P_FirstEnt(local_frame); !P_IsEntNil(ent); ent = P_NextEnt(ent)) for (P_Ent *ent = P_FirstEnt(local_frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
{ {
P_EntKey key = ent->key; P_EntKey key = ent->key;
i64 expiration_ns = P_ObservationLifetimeNs; i64 expiration_ns = NsFromSeconds(P_ObservationDurationSeconds);
i64 observation_time_ns = 0; V_Observation *obs = 0;
{ {
V_ObservationBin *bin = &V.observation_bins[key.v % countof(V.observation_bins)]; V_ObservationBin *bin = &V.observation_bins[key.v % countof(V.observation_bins)];
V_Observation *obs = bin->first; obs = bin->first;
for (; obs; obs = obs->next) for (; obs; obs = obs->next)
{ {
if (obs->key == key.v) if (obs->key == key.v)
@ -1910,19 +1912,20 @@ void V_TickForever(WaveLaneCtx *lane)
break; break;
} }
} }
if (obs) if (obs)
{ {
if (obs->time_ns + expiration_ns > frame->time_ns) if (frame->time_ns > obs->last_observation_time_ns + expiration_ns)
{ {
// Re-observe matching expired observation // Observation expired, re-init
DllQueueRemove(V.first_observation, V.last_observation, obs); obs->initial_observation_time_ns = frame->time_ns;
DllQueuePush(V.first_observation, V.last_observation, obs);
obs->time_ns = frame->time_ns;
} }
DllQueueRemove(V.first_observation, V.last_observation, obs);
DllQueuePush(V.first_observation, V.last_observation, obs);
} }
else else
{ {
if (V.first_observation && V.first_observation->time_ns + expiration_ns <= frame->time_ns) if (V.first_observation && frame->time_ns > V.first_observation->last_observation_time_ns + expiration_ns)
{ {
// Remove expired observation for reuse // Remove expired observation for reuse
obs = V.first_observation; obs = V.first_observation;
@ -1935,14 +1938,14 @@ void V_TickForever(WaveLaneCtx *lane)
obs = PushStruct(perm, V_Observation); obs = PushStruct(perm, V_Observation);
} }
obs->key = key.v; obs->key = key.v;
obs->time_ns = frame->time_ns; obs->initial_observation_time_ns = frame->time_ns;
DllQueuePush(V.first_observation, V.last_observation, obs); DllQueuePush(V.first_observation, V.last_observation, obs);
DllQueuePushNP(bin->first, bin->last, obs, next_in_bin, prev_in_bin); DllQueuePushNP(bin->first, bin->last, obs, next_in_bin, prev_in_bin);
} }
observation_time_ns = obs->time_ns; obs->last_observation_time_ns = frame->time_ns;
} }
ent->observation_time_ns = observation_time_ns; ent->last_observation_time_ns = frame->time_ns;
ent->has_observed = observation_time_ns < frame->time_ns; ent->is_first_observation = obs->initial_observation_time_ns == frame->time_ns;
} }
@ -1950,6 +1953,61 @@ void V_TickForever(WaveLaneCtx *lane)
// for (P_Ent *ent = P_FirstEnt(local_frame); !P_IsEntNil(ent); ent = P_NextEnt(ent))
// {
// P_EntKey key = ent->key;
// i64 expiration_ns = P_ObservationLifetimeNs;
// i64 observation_time_ns = 0;
// {
// V_ObservationBin *bin = &V.observation_bins[key.v % countof(V.observation_bins)];
// V_Observation *obs = bin->first;
// for (; obs; obs = obs->next)
// {
// if (obs->key == key.v)
// {
// break;
// }
// }
// if (obs)
// {
// if (obs->time_ns + expiration_ns > frame->time_ns)
// {
// // Re-observe matching expired observation
// DllQueueRemove(V.first_observation, V.last_observation, obs);
// DllQueuePush(V.first_observation, V.last_observation, obs);
// obs->time_ns = frame->time_ns;
// }
// }
// else
// {
// if (V.first_observation && V.first_observation->time_ns + expiration_ns <= frame->time_ns)
// {
// // Remove expired observation for reuse
// obs = V.first_observation;
// V_ObservationBin *old_bin = &V.observation_bins[obs->key % countof(V.observation_bins)];
// DllQueueRemove(V.first_observation, V.last_observation, obs);
// DllQueueRemoveNP(old_bin->first, old_bin->last, obs, next_in_bin, prev_in_bin);
// }
// else
// {
// obs = PushStruct(perm, V_Observation);
// }
// obs->key = key.v;
// obs->time_ns = frame->time_ns;
// DllQueuePush(V.first_observation, V.last_observation, obs);
// DllQueuePushNP(bin->first, bin->last, obs, next_in_bin, prev_in_bin);
// }
// observation_time_ns = obs->time_ns;
// }
// ent->observation_time_ns = observation_time_ns;
// ent->has_observed = observation_time_ns < frame->time_ns;
// }
@ -2483,80 +2541,6 @@ void V_TickForever(WaveLaneCtx *lane)
// //////////////////////////////
// //- Push test bullet particles
// // TODO: Not like this
// for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
// {
// if (bullet->is_bullet)
// {
// // FIXME: Use 'last visible' pos
// P_Ent *old_bullet = P_EntFromKey(prev_local_frame, bullet->key);
// Vec2 start = old_bullet->xf.t;
// Vec2 end = bullet->xf.t;
// if (P_IsEntNil(old_bullet))
// {
// start = end;
// }
// b32 skip = 0;
// if (bullet->has_hit)
// {
// Vec2 hit_pos = bullet->hit_entry;
// if (DotVec2(SubVec2(hit_pos, start), SubVec2(end, start)) < 0)
// {
// skip = 1;
// }
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, start), Color_Red);
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, end), Color_Purple);
// end = hit_pos;
// }
// if (!skip)
// {
// {
// f32 trail_len = Vec2Len(SubVec2(end, start));
// f32 particles_count = trail_len * frame->dt * Kibi(8); // Particles per meter per second
// particles_count = MaxF32(particles_count, 1);
// f32 angle = AngleFromVec2(PerpVec2(SubVec2(end, start)));
// // f32 angle = AngleFromVec2(NegVec2(SubVec2(end, start)));
// V_Emitter emitter = Zi;
// {
// emitter.kind = V_ParticleKind_BulletTrail;
// emitter.count = particles_count;
// f32 angle_spread = Tau / 4.0;
// emitter.angle.min = angle - angle_spread / 2;
// emitter.angle.max = angle + angle_spread / 2;
// emitter.pos.p0 = start;
// emitter.pos.p1 = end;
// // emitter.color_lin = LinearFromSrgb(VEC4(0, 1, 0, 1));
// // emitter.color_lin = LinearFromSrgb(VEC4(0.8, 0.6, 0.2, 1));
// emitter.speed.min = -1;
// emitter.speed.max = 1;
// }
// V_PushParticles(emitter);
// }
// }
// }
// }
////////////////////////////// //////////////////////////////
//- Push test impact particles //- Push test impact particles
@ -2566,24 +2550,15 @@ void V_TickForever(WaveLaneCtx *lane)
// if (0) // if (0)
{ {
for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet)) for (P_Ent *hit = P_FirstEnt(local_frame); !P_IsEntNil(hit); hit = P_NextEnt(hit))
{ {
if (bullet->is_bullet && bullet->has_hit) if (hit->is_hit && hit->is_first_observation)
{ {
// FIXME: Use actual velocity P_MaterialKind material = hit->hit_material;
P_Ent *old_bullet = P_EntFromKey(prev_local_frame, bullet->key);
Vec2 start = old_bullet->xf.t;
Vec2 end = bullet->xf.t;
if (P_IsEntNil(old_bullet))
{
start = end;
}
P_MaterialKind material = bullet->hit_material; Vec2 hit_entry = hit->hit_entry;
Vec2 hit_entry_normal = hit->hit_entry_normal;
Vec2 hit_entry = bullet->hit_entry; Vec2 hit_entry_velocity = hit->hit_entry_velocity;
Vec2 hit_entry_normal = bullet->hit_entry_normal;
Vec2 hit_entry_velocity = bullet->hit_entry_velocity;
////////////////////////////// //////////////////////////////
//- Wall particles //- Wall particles
@ -2699,6 +2674,232 @@ void V_TickForever(WaveLaneCtx *lane)
// //////////////////////////////
// //- Push test bullet particles
// // TODO: Not like this
// for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
// {
// if (bullet->is_bullet)
// {
// // FIXME: Use 'last visible' pos
// P_Ent *old_bullet = P_EntFromKey(prev_local_frame, bullet->key);
// Vec2 start = old_bullet->xf.t;
// Vec2 end = bullet->xf.t;
// if (P_IsEntNil(old_bullet))
// {
// start = end;
// }
// b32 skip = 0;
// if (bullet->has_hit)
// {
// Vec2 hit_pos = bullet->hit_entry;
// if (DotVec2(SubVec2(hit_pos, start), SubVec2(end, start)) < 0)
// {
// skip = 1;
// }
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, start), Color_Red);
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, end), Color_Purple);
// end = hit_pos;
// }
// if (!skip)
// {
// {
// f32 trail_len = Vec2Len(SubVec2(end, start));
// f32 particles_count = trail_len * frame->dt * Kibi(8); // Particles per meter per second
// particles_count = MaxF32(particles_count, 1);
// f32 angle = AngleFromVec2(PerpVec2(SubVec2(end, start)));
// // f32 angle = AngleFromVec2(NegVec2(SubVec2(end, start)));
// V_Emitter emitter = Zi;
// {
// emitter.kind = V_ParticleKind_BulletTrail;
// emitter.count = particles_count;
// f32 angle_spread = Tau / 4.0;
// emitter.angle.min = angle - angle_spread / 2;
// emitter.angle.max = angle + angle_spread / 2;
// emitter.pos.p0 = start;
// emitter.pos.p1 = end;
// // emitter.color_lin = LinearFromSrgb(VEC4(0, 1, 0, 1));
// // emitter.color_lin = LinearFromSrgb(VEC4(0.8, 0.6, 0.2, 1));
// emitter.speed.min = -1;
// emitter.speed.max = 1;
// }
// V_PushParticles(emitter);
// }
// }
// }
// }
// //////////////////////////////
// //- Push test impact particles
// // TODO: Not like this
// // if (0)
// {
// for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
// {
// if (bullet->is_bullet && bullet->has_hit)
// {
// // FIXME: Use actual velocity
// P_Ent *old_bullet = P_EntFromKey(prev_local_frame, bullet->key);
// Vec2 start = old_bullet->xf.t;
// Vec2 end = bullet->xf.t;
// if (P_IsEntNil(old_bullet))
// {
// start = end;
// }
// P_MaterialKind material = bullet->hit_material;
// Vec2 hit_entry = bullet->hit_entry;
// Vec2 hit_entry_normal = bullet->hit_entry_normal;
// Vec2 hit_entry_velocity = bullet->hit_entry_velocity;
// //////////////////////////////
// //- Wall particles
// if (material != P_MaterialKind_Flesh)
// {
// //- Wall debris
// {
// V_Emitter emitter = Zi;
// {
// // emitter.flags |= V_ParticleFlag_PruneWhenStill;
// // emitter.flags |= V_ParticleFlag_StainOnPrune;
// emitter.kind = V_ParticleKind_Debris;
// emitter.count = 4;
// emitter.pos.p0 = emitter.pos.p1 = hit_entry;
// emitter.speed.min = 0;
// emitter.speed.max = 20;
// // emitter.velocity_falloff = 5;
// // emitter.velocity_falloff_spread = emitter.velocity_falloff_spread * 1.5;
// Vec2 dir = hit_entry_normal;
// f32 angle = AngleFromVec2(dir);
// f32 angle_spread = Tau * 0.5;
// emitter.angle.min = angle - angle_spread / 2;
// emitter.angle.max = angle + angle_spread / 2;
// // emitter.lifetime = 0.25;
// // emitter.lifetime = 0.05;
// // emitter.lifetime = 0.04;
// // emitter.lifetime_spread = emitter.lifetime * 2;
// }
// V_PushParticles(emitter);
// }
// //- Wall dust
// // {
// // V_Emitter emitter = Zi;
// // {
// // emitter.kind = V_ParticleKind_Smoke;
// // emitter.count = 128;
// // emitter.pos.p0 = emitter.pos.p1 = hit_entry;
// // // emitter.color_lin = LinearFromSrgb(VEC4(0.5, 0.5, 0.5, 0.75));
// // emitter.speed.min = 10;
// // emitter.speed.max = 20;
// // // emitter.velocity_falloff = 12;
// // // emitter.velocity_falloff_spread = emitter.velocity_falloff_spread * 1.5;
// // Vec2 dir = hit_entry_normal;
// // f32 angle = AngleFromVec2(dir);
// // f32 angle_spread = Tau * 0.1;
// // emitter.angle.min = angle - angle_spread / 2;
// // emitter.angle.max = angle + angle_spread / 2;
// // }
// // V_PushParticles(emitter);
// // }
// }
// //////////////////////////////
// //- Blood particles
// if (material == P_MaterialKind_Flesh)
// {
// {
// V_Emitter emitter = Zi;
// emitter.kind = V_ParticleKind_BloodTrail;
// // emitter.kind = V_ParticleKind_BloodDebris;
// Vec2 dir = NormVec2(NegVec2(hit_entry_velocity));
// f32 angle = AngleFromVec2(dir);
// // f32 angle = 0;
// // f32 angle_spread = Tau * 0.25;
// f32 angle_spread = TweakFloat("Emitter angle spread", 0.1, 0, 1) * Tau;
// // f32 angle_spread = Tau;
// // f32 angle_spread = 0;
// // f32 speed = 5;
// // f32 speed = 25;
// f32 speed = 50;
// // f32 speed = 100;
// f32 speed_spread = speed * 2;
// emitter.pos.p0 = emitter.pos.p1 = hit_entry;
// emitter.speed.min = speed - speed_spread * 0.5;
// emitter.speed.max = speed + speed_spread * 0.5;
// emitter.angle.min = angle - angle_spread * 0.5;
// emitter.angle.max = angle + angle_spread * 0.5;
// emitter.count = TweakFloat("Emitter count", 1, 0, 100) * (Kibi(1) * frame->dt);
// V_PushParticles(emitter);
// }
// }
// // V_DrawPoint(victim_raycast.p, Color_Green);
// // V_DrawLine(victim_raycast.p, AddVec2(victim_raycast.p, MulVec2(victim_raycast.normal, 0.5)), Color_White);
// }
// }
// }
////////////////////////////// //////////////////////////////
//- Push test emitter //- Push test emitter

View File

@ -239,7 +239,8 @@ Struct(V_Observation)
V_Observation *next_in_bin; V_Observation *next_in_bin;
V_Observation *prev_in_bin; V_Observation *prev_in_bin;
u64 key; u64 key;
i64 time_ns; i64 initial_observation_time_ns;
i64 last_observation_time_ns;
}; };
Struct(V_ObservationBin) Struct(V_ObservationBin)