ludic/packages/ludic.aim/pick.ludic
Orkuncakilkaya 56e4b6d688 feat(senses): walkers slide round still things, and lines of sight are the world's to answer
- ludic.wildlife: WildlifeWorld.push / pushed_x / pushed_z (defaults change nothing); a ground
  walker's step is slid clear of a still thing with a body from its species' size and scale, and
  a step that gets nowhere turns away as the water does. Birds are not pushed; no dice added.
- ludic.npc: NpcWorld.push / pushed_x / pushed_z (a person 0.35 m round) in npc_walk, and
  NpcWorld.clear (default true), which npc_line_ok asks at 1 m so a detour does not cut a trunk.
- ludic.aim: AimView.clear replaces AimView.ground and the ground march; the pick asks the line
  a body's width short of where the ray enters the thing (aim_clear_at(t, back)), so a solid
  thing does not hide itself. probe.ludic (the ground-agreement probe) and AimView.dry are gone.
- ludic.photo: PhotoLens.clear replaces PhotoLens.ground, PhotoLens.trunk and the march.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:59:39 +03:00

57 lines
2.6 KiB
Text

# pick.ludic - one target a frame: the game offers every candidate, and the nearest one the ray enters
# that nothing hides is the pick. Near things occlude far ones for free, which is what a
# player means by "I am looking at that one".
export const AIM_NONE: int = 0
export const AIM_REACH_MIN: float = 3.0 # about as far as a hand goes with a step into it
export const AIM_USE_MAX: float = 10.0 # past this the crosshair is for looking, not doing
export const AIM_NEVER: float = -1.0 # a reach that is never in reach (an animal without a camera)
export state AimState {
am_kind: int = 0
am_id: int = -1
am_t: float = 10000.0
am_dist: float = 0.0
am_reach: bool = false
am_off: float = 1.0
}
# The crosshair decides WHICH; the distance decides only WHETHER. Every reach is lifted to a floor of
# AIM_REACH_MIN, because a per-kind reach chosen against its model was never chosen against standing
# between two props and looking at one.
export function aim_reach_of(reach: float) -> float { return Math.max(reach, AIM_REACH_MIN) }
export function aim_begin(aim_st: mut AimState) -> void {
aim_st.am_kind = AIM_NONE
aim_st.am_id = -1
aim_st.am_t = 10000.0
aim_st.am_dist = 0.0
aim_st.am_reach = false
aim_st.am_off = 1.0
}
# A candidate of `kind` (the game's own, not AIM_NONE) and `id`: an upright cylinder at (x, z) from y0
# to y1 of radius r, within `far` of the body, in reach within `reach` (floored; AIM_NEVER never).
# True when it is now the pick.
export function aim_offer(aim_st: mut AimState, kind: int, id: int, x: float, y0: float, y1: float, z: float, r: float, reach: float, far: float) -> bool {
let d = aim_range(x, z)
if d > far { return false }
let rr = r + aim_slack_m(d)
let hit = aim_ray_cyl(x, y0, y1, z, rr)
if hit < 0.0 or not (hit < aim_st.am_t) { return false }
if not aim_clear_at(hit, rr) { return false }
aim_st.am_t = hit
aim_st.am_off = aim_miss(x, z, rr)
aim_st.am_kind = kind
aim_st.am_id = id
aim_st.am_dist = d
aim_st.am_reach = reach >= 0.0 and d < aim_reach_of(reach)
return true
}
export function aim_kind(aim_st: AimState) -> int { return aim_st.am_kind }
export function aim_id(aim_st: AimState) -> int { return aim_st.am_id }
export function aim_dist(aim_st: AimState) -> float { return aim_st.am_dist }
export function aim_in_reach(aim_st: AimState) -> bool { return aim_st.am_reach }
export function aim_t(aim_st: AimState) -> float { return aim_st.am_t }
# how well centred the pick is: 0 dead centre, 1 at its (forgiven) edge
export function aim_off_centre(aim_st: AimState) -> float { return aim_st.am_off }