# senses.ludic - what an animal makes of one player: sight (a clear line, closer is worse, the dark # helps), scent (downwind, further than sight), noise; the rise is NET of how fast it forgets, so # an alert settles where the distance puts it instead of climbing to the top and sticking # the nearest player's distance; `wl_near_i` says who function wl_near(wildlife_st: mut WildlifeState, x: float, z: float) -> float { var best = 999999999999.0 wildlife_st.wl_near_i = 0 for p in 0 .. WildlifeWorld.players() { if not WildlifeWorld.player_on(p) { continue } let d = wl_pdist2(p, x, z) if d < best { best = d wildlife_st.wl_near_i = p } } return Math.sqrt(best) } function wl_pdist2(p: int, x: float, z: float) -> float { let dx = WildlifeWorld.player_x(p) - x let dz = WildlifeWorld.player_z(p) - z return dx * dx + dz * dz } function wl_pdist(p: int, a: WildAnimal) -> float { return Math.sqrt(wl_pdist2(p, a.x, a.z)) } # -1 (it is upwind of player p: it cannot smell them) .. 1 (straight downwind, it gets everything) export function wildlife_downwind(a: WildAnimal, p: int) -> float { let dx = a.x - WildlifeWorld.player_x(p) let dz = a.z - WildlifeWorld.player_z(p) let d = Math.max(Math.sqrt(dx * dx + dz * dz), 0.01) let dir = WildlifeWorld.wind_dir() return dx / d * -Math.sin(dir) + dz / d * -Math.cos(dir) } export function wildlife_can_see(a: WildAnimal, p: int) -> bool { if WildlifeWorld.hidden(p) { return false } return WildlifeWorld.sight(a.x, a.y + 0.9, a.z, WildlifeWorld.player_x(p), WildlifeWorld.player_y(p) + 0.9, WildlifeWorld.player_z(p)) } # 1 at the animal, 0 at `reach` and beyond, squared: the far half of a range is almost free function wl_near_sq(d: float, reach: float) -> float { let n = Math.max(1.0 - d / reach, 0.0) return n * n } # alert points a second from player p at `dh` metres; 0 when nothing of them reaches it export function wildlife_alert_rate(wildlife_st: WildlifeState, a: WildAnimal, p: int, dh: float) -> float { let s = wl_sp(wildlife_st, a.sp) if s.tame or a.calm > 0.0 { return 0.0 } if (a.state == WILD_APPROACH or a.state == WILD_EAT) and a.lure != 0 and a.lure_kind == WILD_FEED and dh > 5.0 and WildlifeWorld.taught() { return 0.0 } var reach = s.flee * WildlifeWorld.wariness() if a.spook > 0.0 { reach = reach * 1.25 } var rate = 0.0 if dh < reach * 1.9 and wildlife_can_see(a, p) { rate = rate + 55.0 * wl_near_sq(dh, reach * 1.9) * WildlifeWorld.light() } let wind = wildlife_downwind(a, p) let ws = WildlifeWorld.wind() if wind > 0.0 and ws > 0.08 and dh < reach * 3.2 { rate = rate + 30.0 * wl_near_sq(dh, reach * 3.2) * wind * (WildlifeWorld.scent(p) * Math.min(1.0, 0.4 + ws)) } let noise = WildlifeWorld.noise(p) if noise > 0.12 and dh < reach * 1.5 * noise { rate = rate + 40.0 * wl_near_sq(dh, reach * 1.5 * noise) * noise } return rate } # how fast it forgets player p: standing still buys it, sitting down buys more export function wildlife_alert_decay(wildlife_st: WildlifeState, a: WildAnimal, p: int) -> float { var d = wl_sp(wildlife_st, a.sp).forget let still = WildlifeWorld.stillness(p) if still == WILD_SITTING { d = d * 2.5 } else if still == WILD_STILL { d = d * 1.6 } if a.spook > 0.0 { d = d * 0.7 } return d } # the alert rises and falls with the player who nets it the most, and it watches them function wl_alert(wildlife_st: WildlifeState, a: WildAnimal, np: int, dh: float, dt: float) -> void { var rise = wildlife_alert_rate(wildlife_st, a, np, dh) var fall = wildlife_alert_decay(wildlife_st, a, np) var who = np for p in 0 .. WildlifeWorld.players() { if p == np or not WildlifeWorld.player_on(p) { continue } let r2 = wildlife_alert_rate(wildlife_st, a, p, wl_pdist(p, a)) let f2 = wildlife_alert_decay(wildlife_st, a, p) if r2 - f2 > rise - fall { rise = r2 fall = f2 who = p } } if rise > 0.05 { a.sus_x = WildlifeWorld.player_x(who) a.sus_z = WildlifeWorld.player_z(who) } a.alert = Math.clamp(a.alert + (rise - fall) * dt, 0.0, 100.0) }