Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion agent_docs/combat.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ templates from `Mob.nx` (`maxHP`, `exp`, `PADamage`, `bodyAttack`,
(`field_mobs::mob_capacity`): one per 128,000 square pixels of its
footholds' bounds (`Footholds::bounds`, the client's: 30 px in from the
sides, 300 px above the top, 10 px below the bottom; the server adds 10
px a side), counted as at least 800 wide and 600 high after taking 450
px a side). On a map with `info/VRLimit` (only Orbis Tower<B2>) the
view range pulls those bounds in as the client does
(`Footholds::bounds_within`). The area is counted as at least 800 wide and 600 high after taking 450
off the height, times `info/mobRate`, from 1 to 40. With more than half
that many users on the field it grows, up to twice as many at twice as
many users.
Expand Down
45 changes: 45 additions & 0 deletions game-data/src/footholds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,34 @@ impl Footholds {
))
}

/// The map's bounds (`bounds`) inside its view range, for a map with
/// `info/VRLimit` (`FieldTemplate::view_limit`), as the v83 client
/// narrows them (r25 0xa44c3a): the left edge to 20 px inside the view's,
/// the right to 20 px inside, the top to 65 px below and the bottom to
/// the view's bottom, each only inward and only when the view's edge is
/// not 0.
pub fn bounds_within(
&self,
view: Option<(i32, i32, i32, i32)>,
) -> Option<(i32, i32, i32, i32)> {
let (mut left, mut top, mut right, mut bottom) = self.bounds()?;
if let Some((view_left, view_top, view_right, view_bottom)) = view {
if view_left != 0 {
left = left.max(view_left + 20);
}
if view_top != 0 {
top = top.max(view_top + 65);
}
if view_right != 0 {
right = right.min(view_right - 20);
}
if view_bottom != 0 {
bottom = bottom.min(view_bottom);
}
}
Some((left, top, right, bottom))
}

/// The highest ground at `x` that is at or below `y`.
pub fn ground_below(&self, x: i16, y: i16) -> Option<i16> {
self.lines
Expand Down Expand Up @@ -172,6 +200,23 @@ mod tests {
assert_eq!(Footholds::default().bounds(), None);
}

#[test]
fn a_view_range_only_pulls_the_bounds_in() {
let cliff = cliff();
assert_eq!(cliff.bounds_within(None), cliff.bounds());
// Left and top move in; the right and bottom view edges lie outside.
assert_eq!(
cliff.bounds_within(Some((-200, -300, 400, 500))),
Some((-180, -200, 270, 210))
);
// Right and bottom move in; an edge of 0 is left alone.
assert_eq!(
cliff.bounds_within(Some((0, 0, 200, 150))),
Some((-270, -200, 180, 150))
);
assert_eq!(Footholds::default().bounds_within(Some((1, 1, 1, 1))), None);
}

#[test]
fn drops_land_on_the_ground_below_their_column() {
let footholds = cliff();
Expand Down
40 changes: 40 additions & 0 deletions game-data/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ pub struct FieldTemplate {
pub mob_spawns: Vec<MapMobTemplate>,
pub reactors: Vec<MapReactorTemplate>,
pub mob_rate: f64,
/// `info/VRLeft`, `VRTop`, `VRRight` and `VRBottom` when `info/VRLimit`
/// is set (only Orbis Tower<B2>, 200082300): the view range that narrows
/// the map's bounds (`Footholds::bounds_within`).
pub view_limit: Option<(i32, i32, i32, i32)>,
/// `info/recovery`: how much faster players recover HP and MP at rest,
/// e.g. 2 in the Sleepywood sauna.
pub recovery: f64,
Expand Down Expand Up @@ -580,6 +584,19 @@ fn build_field_template(
.transpose()?
.flatten()
.unwrap_or(1.0);
let view_limit = match nx.child_by_name(map_node_idx, "info")? {
Some(info) if nx.int_child(info, "VRLimit")?.unwrap_or(0) != 0 => {
let edge =
|key| -> Result<i32, GameDataError> { Ok(nx.int_child(info, key)?.unwrap_or(0)) };
Some((
edge("VRLeft")?,
edge("VRTop")?,
edge("VRRight")?,
edge("VRBottom")?,
))
}
_ => None,
};
let time_limit = nx
.child_by_name(map_node_idx, "info")?
.map(|info| nx.int_child(info, "timeLimit"))
Expand Down Expand Up @@ -682,6 +699,7 @@ fn build_field_template(
mob_spawns,
reactors,
mob_rate,
view_limit,
recovery,
town,
field_limit,
Expand Down Expand Up @@ -1296,6 +1314,7 @@ mod tests {
mob_spawns: Vec::new(),
reactors: Vec::new(),
mob_rate: 1.0,
view_limit: None,
recovery: 1.0,
town: false,
field_limit: 0,
Expand Down Expand Up @@ -1371,6 +1390,7 @@ mod tests {
map_npcs: Vec::new(),
mob_spawns: Vec::new(),
mob_rate: 1.0,
view_limit: None,
recovery: 1.0,
town: false,
field_limit: 0,
Expand Down Expand Up @@ -1418,6 +1438,26 @@ mod tests {
assert!(henesys.may_use(2000000) && henesys.may_drop());
}

#[test]
fn orbis_towers_view_range_narrows_its_bounds() {
let Some(path) = test_assets::path("Map.nx") else {
return;
};
let game_data = GameData::load_from_nx_map(&path).expect("load map nx");
let tower = game_data.field(200082300).expect("Orbis Tower<B2>");
assert_eq!(tower.view_limit, Some((-400, -2239, 400, 285)));
// Only the top moves in, to 65 px below the view's.
assert_eq!(tower.footholds.bounds(), Some((-335, -2610, 335, 160)));
assert_eq!(
tower.footholds.bounds_within(tower.view_limit),
Some((-335, -2174, 335, 160))
);
assert_eq!(
game_data.field(100000000).expect("Henesys").view_limit,
None
);
}

#[test]
fn loads_map_npcs_from_assets_map_nx() {
let Some(path) = test_assets::path("Map.nx") else {
Expand Down
8 changes: 6 additions & 2 deletions runtime/src/actor/field_mobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,8 +1318,9 @@ fn near(mob: (i16, i16), at: (i16, i16)) -> bool {
/// How many mobs a field holds with few users on it (CField's mob
/// capacity): one per 128,000 square pixels of the map, at least 800 wide
/// and 600 high after taking off 450, times its `mobRate`, from 1 to 40.
/// `bounds` are its footholds' bounds (`Footholds::bounds`), which the
/// server grows by 10 px on every side.
/// `bounds` are its footholds' bounds inside any view range
/// (`Footholds::bounds_within`), which the server grows by 10 px on every
/// side.
pub fn mob_capacity(bounds: Option<(i32, i32, i32, i32)>, mob_rate: f64) -> usize {
let (width, height) = bounds.map_or((0, 0), |(left, top, right, bottom)| {
(right - left + 20, bottom - top + 20)
Expand Down Expand Up @@ -2240,6 +2241,9 @@ mod tests {
assert_eq!(mob_capacity(Some((0, 0, 1580, 1030)), 1.0), 7);
assert_eq!(mob_capacity(Some((0, 0, 9000, 9000)), 1.0), 40);
assert_eq!(mob_capacity(None, 0.0), 1);
// Orbis Tower<B2>, by its footholds and then inside its view range.
assert_eq!(mob_capacity(Some((-335, -2610, 335, 160)), 1.0), 14);
assert_eq!(mob_capacity(Some((-335, -2174, 335, 160)), 1.0), 11);
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion runtime/src/actor/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ fn load_field_mobs(field_key: FieldKey) -> MobSet {
}
}
}))
.with_capacity(mob_capacity(field.footholds.bounds(), field.mob_rate));
.with_capacity(mob_capacity(
field.footholds.bounds_within(field.view_limit),
field.mob_rate,
));
if full_spawn() {
mobs.filling_every_point()
} else {
Expand Down
Loading