feat(pathfinder): add configurable underpass walk-under-furniture support

Allow avatars to walk under furniture items placed at a configurable height threshold (default 1.5). When a blocking item is elevated enough above the walk surface, the tile is treated as walkable.

Changes:
- RoomLayout: add UNDERPASS_HEIGHT static field (default 1.5)
- PluginManager: load pathfinder.underpass.height from emulator config
- RoomTileManager: add getUnderpassWalkHeight() method and underpass checks in tile state calculation, stack height, and canWalkAt()
- RoomItemManager: add getWalkableItemAt() method that returns the correct walkable item considering underpass clearance
- RoomUnit: use getWalkableItemAt() in movement cycle for accurate item resolution when walking under elevated furniture

Co-Authored-By: medievalshell <medievalshell@users.noreply.github.com>
This commit is contained in:
simoleo89
2026-03-16 18:00:27 +01:00
parent 90cd622c89
commit e75fe83c80
5 changed files with 96 additions and 2 deletions
@@ -448,6 +448,43 @@ public class RoomItemManager {
return highestItem;
}
/**
* Gets the top walkable item at a position, considering underpass.
* If the topmost item is elevated enough to walk under, returns the highest item at walk surface level instead.
*/
public HabboItem getWalkableItemAt(int x, int y) {
HabboItem topItem = this.getTopItemAt(x, y);
if (topItem == null) {
return null;
}
// If the top item is walkable, just return it
if (topItem.isWalkable() || topItem.getBaseItem().allowWalk() || topItem.getBaseItem().allowSit() || topItem.getBaseItem().allowLay()) {
return topItem;
}
// Check for underpass: get the walk surface height
double walkSurface = this.room.getLayout() != null ? this.room.getLayout().getHeightAtSquare(x, y) : 0;
HabboItem walkSurfaceItem = null;
for (HabboItem item : this.getItemsAt(x, y)) {
if (item.isWalkable() || item.getBaseItem().allowWalk() || item.getBaseItem().allowSit() || item.getBaseItem().allowLay()) {
double itemTop = item.getZ() + Item.getCurrentHeight(item);
if (itemTop > walkSurface) {
walkSurface = itemTop;
walkSurfaceItem = item;
}
}
}
// If there's enough clearance under the top blocking item, return the walk surface item
if (topItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) {
return walkSurfaceItem;
}
return topItem;
}
/**
* Gets the top item from a set of tiles.
*/
@@ -19,6 +19,7 @@ public class RoomLayout {
protected static final int DIAGONALMOVEMENTCOST = 14;
public static double MAXIMUM_STEP_HEIGHT = 1.5;
public static boolean ALLOW_FALLING = true;
public static double UNDERPASS_HEIGHT = 1.5;
public boolean CANMOVEDIAGONALY = true;
private String name;
private short doorX;
@@ -91,9 +91,41 @@ public class RoomTileManager {
tallestItem = item;
}
if (result == RoomTileState.BLOCKED && tallestItem != null) {
double walkSurface = this.getUnderpassWalkHeight(tile, items, exclude);
if (tallestItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) {
result = RoomTileState.OPEN;
}
}
return result;
}
/**
* Calculates the walk surface height for underpass checks.
* Returns the floor height or the top of the highest walkable item below any blocking items.
*/
private double getUnderpassWalkHeight(RoomTile tile, THashSet<HabboItem> items, HabboItem exclude) {
RoomLayout layout = this.room.getLayout();
double walkHeight = layout != null ? layout.getHeightAtSquare(tile.x, tile.y) : 0;
if (items != null) {
for (HabboItem item : items) {
if (exclude != null && item == exclude) {
continue;
}
if (item.isWalkable() || item.getBaseItem().allowWalk() || item.getBaseItem().allowSit() || item.getBaseItem().allowLay()) {
double itemTop = item.getZ() + Item.getCurrentHeight(item);
if (itemTop > walkHeight) {
walkHeight = itemTop;
}
}
}
}
return walkHeight;
}
/**
* Determines the tile state based on a specific item.
*/
@@ -193,7 +225,22 @@ public class RoomTileManager {
HabboItem item = this.room.getItemManager().getTopItemAt(x, y, exclude);
if (item != null) {
canStack = item.getBaseItem().allowStack();
height = item.getZ() + (item.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(item));
double itemTop = item.getZ() + (item.getBaseItem().allowSit() ? 0 : Item.getCurrentHeight(item));
// Underpass: if the top item is blocking but high enough to walk under, use floor height
if (!item.isWalkable() && !item.getBaseItem().allowWalk() && !item.getBaseItem().allowSit() && !item.getBaseItem().allowLay()) {
RoomLayout layout2 = this.room.getLayout();
RoomTile tile = layout2 != null ? layout2.getTile(x, y) : null;
THashSet<HabboItem> allItems = tile != null ? this.room.getItemManager().getItemsAt(tile) : null;
double walkSurface = this.getUnderpassWalkHeight(tile, allItems, exclude);
if (item.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) {
height = walkSurface;
} else {
height = itemTop;
}
} else {
height = itemTop;
}
}
if (calculateHeightmap) {
@@ -396,6 +443,14 @@ public class RoomTileManager {
}
}
// Underpass: if top item blocks but is high enough, allow walking under
if (!canWalk && topItem != null) {
double walkSurface = this.getUnderpassWalkHeight(roomTile, items, null);
if (topItem.getZ() - walkSurface >= RoomLayout.UNDERPASS_HEIGHT) {
canWalk = true;
}
}
return canWalk;
}
@@ -235,7 +235,7 @@ public class RoomUnit {
}
}
HabboItem item = room.getTopItemAt(next.x, next.y);
HabboItem item = room.getItemManager().getWalkableItemAt(next.x, next.y);
boolean canSitNextTile = room.canSitAt(next.x, next.y);
boolean canLayNextTile = room.canLayAt(next.x, next.y);
@@ -84,6 +84,7 @@ public class PluginManager {
Room.MUTEAREA_CAN_WHISPER = Emulator.getConfig().getBoolean("room.chat.mutearea.allow_whisper", false);
RoomChatMessage.SAVE_ROOM_CHATS = Emulator.getConfig().getBoolean("save.room.chats", false);
RoomLayout.MAXIMUM_STEP_HEIGHT = Emulator.getConfig().getDouble("pathfinder.step.maximum.height", 1.1);
RoomLayout.UNDERPASS_HEIGHT = Emulator.getConfig().getDouble("pathfinder.underpass.height", 1.5);
RoomLayout.ALLOW_FALLING = Emulator.getConfig().getBoolean("pathfinder.step.allow.falling", true);
RoomTrade.TRADING_ENABLED = Emulator.getConfig().getBoolean("hotel.trading.enabled") && !ShutdownEmulator.instantiated;
RoomTrade.TRADING_REQUIRES_PERK = Emulator.getConfig().getBoolean("hotel.trading.requires.perk");