2026-09-29 19:10:03 +09:00
|
|
|
const MODE = Object.freeze({
|
|
|
|
|
// Hirt et al. time-dependent maximum-speed form:
|
|
|
|
|
// v = a * M^b * (1 - exp(-h * M^i)), v in km/h, M in kg.
|
|
|
|
|
// Flying, running and swimming coefficients are the published Supplementary
|
|
|
|
|
// Table 4 fits for the time-dependent maximum-speed model.
|
2026-09-29 23:45:12 +09:00
|
|
|
running:{a:25.5,b:.26,h:22,i:-.60,routineFraction:.34,calibration:'Hirt-2017'},
|
|
|
|
|
swimming:{a:11.2,b:.36,h:19.5,i:-.56,routineFraction:.30,calibration:'Hirt-2017'},
|
|
|
|
|
flying:{a:142.8,b:.24,h:2.4,i:-.72,routineFraction:.42,calibration:'Hirt-2017'},
|
2026-09-29 19:10:03 +09:00
|
|
|
});
|
|
|
|
|
export const locomotionModes=Object.freeze(Object.keys(MODE));
|
|
|
|
|
export const normalizeLocomotionMode=mode=>MODE[mode]?mode:'running';
|
|
|
|
|
export const locomotionCalibration=mode=>({...MODE[normalizeLocomotionMode(mode)]});
|
|
|
|
|
export function maximumSpeedMPerDay(massKg,mode='running'){
|
|
|
|
|
const c=MODE[normalizeLocomotionMode(mode)],m=Math.max(massKg,1e-6),kmh=c.a*Math.pow(m,c.b)*(1-Math.exp(-c.h*Math.pow(m,c.i)));
|
|
|
|
|
return Math.max(1,kmh*24000); // km/h -> m/day
|
|
|
|
|
}
|
|
|
|
|
export const maximumSpeedMultiplier=massKg=>maximumSpeedMPerDay(massKg,'running')/maximumSpeedMPerDay(3,'running');
|
|
|
|
|
export function routineTravelSpeedMPerDay(massKg,dailyMovementBudgetM,mode='running'){
|
|
|
|
|
const c=MODE[normalizeLocomotionMode(mode)],budgetSpeed=Math.max(0,dailyMovementBudgetM)*c.routineFraction;
|
|
|
|
|
return Math.min(maximumSpeedMPerDay(massKg,mode)*.22,Math.max(1,budgetSpeed));
|
|
|
|
|
}
|
|
|
|
|
export function foragingSpeedMPerDay(massKg,dailyMovementBudgetM,mode='running'){
|
|
|
|
|
return Math.min(maximumSpeedMPerDay(massKg,mode)*.42,routineTravelSpeedMPerDay(massKg,dailyMovementBudgetM,mode)*1.35);
|
|
|
|
|
}
|
|
|
|
|
export function escapeSpeedMPerDay(massKg,dailyMovementBudgetM,mode='running'){
|
|
|
|
|
return Math.min(maximumSpeedMPerDay(massKg,mode),routineTravelSpeedMPerDay(massKg,dailyMovementBudgetM,mode)*2.4);
|
|
|
|
|
}
|
2026-09-29 23:45:12 +09:00
|
|
|
export function dailyMovementBudgetM(massKg,geneBudgetM){
|
|
|
|
|
// Daily travel capacity is an independent movement trait.
|
|
|
|
|
return Math.max(25,geneBudgetM)*(0.75+0.25*Math.pow(Math.max(massKg,.01)/3,.08));
|
2026-09-29 19:10:03 +09:00
|
|
|
}
|
2026-09-29 23:45:12 +09:00
|
|
|
export function sampleRoamingDistanceM(random,dailyBudgetM){
|
|
|
|
|
// Persistent free-roaming waypoint: typically ~0.35-1.5 days of travel,
|
|
|
|
|
// capped to avoid asking a local navigator to cross the entire world at once.
|
|
|
|
|
const budget=Math.max(80,dailyBudgetM),scale=Math.max(120,budget*.72),draw=-Math.log(Math.max(1e-9,1-random()))*scale;
|
|
|
|
|
return Math.min(Math.max(90,draw),Math.min(1800,budget*1.8));
|
2026-09-29 19:10:03 +09:00
|
|
|
}
|
2026-09-29 23:45:12 +09:00
|
|
|
export function limitSpeedForArrival(speedMPerDay,distanceM,arrivalRadiusM,dtDays){
|
|
|
|
|
const available=Math.max(0,Math.max(0,distanceM)-Math.max(0,arrivalRadiusM));
|
|
|
|
|
return Math.min(Math.max(0,speedMPerDay),dtDays>0?available/dtDays:0);
|
2026-09-29 19:10:03 +09:00
|
|
|
}
|