Pixel Movement
Freie, pixelgenaue Bewegung für RPG Maker MZ – ohne an Türrahmen hängenzubleiben
Plug & Play · keine Plugin-Befehle · läuft laut Hilfetext auch unter MV
Schluss mit dem starren Feld-für-Feld-Raster: Mit diesem Plugin gleitet der Spieler
pixelgenau und frei in 4 oder 8 Richtungen über die Map. Dabei bleibt alles, was du in deinen Maps eingestellt hast, gültig – Durchgangsregeln, Events und Fahrzeuge funktionieren wie gewohnt.
Was das Plugin kann
- Ecken-Gleiten: Läuft man knapp gegen eine Wandkante neben einer Öffnung – Tür, Gang oder Lücke zwischen zwei NPCs –, schiebt das Plugin den Spieler automatisch seitlich hinein. Kein frustrierendes Hängenbleiben mehr.
- Natürliche Laufanimation: Die Schritte richten sich nach der tatsächlich gelaufenen Strecke. Kein „Moonwalk“ gegen Wände, passendes Tempo beim Rennen, ein sofortiger erster Schritt und kein Flackern beim Stehenbleiben.
- Flüssige Gefolgsleute: Die Gruppe folgt dem Spieler sauber auf seinem Weg statt Feld für Feld hinterherzuspringen.
- Gleichmäßiges Tempo: Diagonal ist man auf Wunsch genauso schnell wie geradeaus – statt rund 41 % schneller.
- Volle Map-Kompatibilität: Richtungs-Durchgänge, Stern-Tiles, Leitern, Büsche, Schadensböden, Theken und endlos verbundene Maps werden berücksichtigt.
- Alles andere bleibt, wie es ist: Events, Fahrzeuge, Zufallskämpfe, Schrittzähler, Maus-/Touch-Steuerung und die Aktionstaste funktionieren unverändert. Events selbst bewegen sich weiterhin feldweise.
Einbau in drei Schritten1. Den Code oben als
PixelMovement.js im Ordner
js/plugins/ deines Projekts speichern.
2. Im
Plugin-Manager hinzufügen und einschalten – möglichst
unterhalb anderer Plugins, die die Spielerbewegung verändern.
3. Spiel starten und loslaufen. Plugin-Befehle sind nicht nötig.
Der Dateiname muss genau PixelMovement.js lauten, sonst findet das Plugin seine Einstellungen nicht und nutzt die Standardwerte.
Einstellungen
| Parameter | Standard | Wirkung |
|---|
| Hitbox Width / Height | 30 × 24 px | Größe der Kollisionsbox an den Füßen des Spielers (Breite höchstens eine Tile-Breite) |
| Hitbox Bottom Offset | 4 px | Abstand der Hitbox zur Unterkante des Tiles |
| Corner Slide Tolerance | 22 px | Wie weit der Spieler an Ecken automatisch seitlich geschoben wird – 0 schaltet das Ecken-Gleiten ab |
| Corner Slide Speed | 100 % | Tempo des seitlichen Schiebens, gemessen an der Laufgeschwindigkeit |
| 8-Direction Movement | an | 8 Richtungen statt 4 |
| Normalize Diagonal Speed | an | Diagonal genauso schnell wie geradeaus |
| Walk / Dash Stride | 24 / 34 px | Strecke pro Animationsbild beim Gehen bzw. Rennen – kleiner = schnellere Schritte |
| Stop Delay | 6 Frames | Wartezeit, bevor die Figur in die Standpose wechselt (verhindert Flackern) |
| Event Hitbox Margin | 4 px | Blockierende Events belegen ihr Feld abzüglich dieses Randes |
| Action Reach | 16 px | Wie weit die Aktionstaste vor dem Spieler nach Events sucht |
| Follower Distance | 40 px | Abstand zwischen den Gefolgsleuten |
| Disable Switch | keiner | Solange dieser Schalter AN ist, gilt wieder die normale Feldbewegung – ideal für Rätsel oder Zwischensequenzen |
Gut zu wissen
32-px-Tiles (MV oder ältere Ressourcen): Die Standardwerte sind auf 48-px-Tiles abgestimmt. Probiere für 32 px z. B. Hitbox Width 20, Height 16 und Offset 3.
- „Bewegungsroute festlegen“ für den Spieler, Sprünge und Fahrzeuge nutzen weiterhin die Feldbewegung. Der Spieler wird vorher sanft am nächsten Feld ausgerichtet – ohne sichtbaren Ruck.
- Für Abschnitte, die exakt auf dem Raster ablaufen sollen, einfach den Disable Switch einschalten.
//=============================================================================
// PixelMovement.js
//=============================================================================
/*:
* @target MZ
* @plugindesc v1.0.0 Pixel movement for the player with corner sliding (doorways), distance-based walk animation and smooth followers.
* @author cdqgrayde
*
* @param HitboxWidth
* @text Hitbox Width (px)
* @desc Width of the player's collision box in pixels. Must be <= tile width.
* @type number
* @min 1
* @default 30
*
* @param HitboxHeight
* @text Hitbox Height (px)
* @desc Height of the player's collision box (the "feet" area) in pixels.
* @type number
* @min 1
* @default 24
*
* @param HitboxOffsetY
* @text Hitbox Bottom Offset (px)
* @desc Distance between the bottom of the tile and the bottom of the hitbox.
* @type number
* @min 0
* @default 4
*
* @param SlideTolerance
* @text Corner Slide Tolerance (px)
* @desc How far (px) the player is automatically nudged sideways around corners / into openings. 0 = off.
* @type number
* @min 0
* @default 22
*
* @param SlideSpeed
* @text Corner Slide Speed (%)
* @desc Speed of the automatic sideways nudge in % of the current move speed.
* @type number
* @min 1
* @max 200
* @default 100
*
* @param Diagonal
* @text 8-Direction Movement
* @type boolean
* @on Yes
* @off No (4 directions)
* @default true
*
* @param NormalizeDiagonal
* @text Normalize Diagonal Speed
* @desc Diagonal movement is as fast as straight movement (instead of ~41% faster).
* @type boolean
* @default true
*
* @param StrideLength
* @text Walk Stride (px)
* @desc Distance walked per animation frame while walking. Smaller = faster feet.
* @type number
* @min 1
* @default 24
*
* @param DashStrideLength
* @text Dash Stride (px)
* @desc Distance per animation frame while dashing.
* @type number
* @min 1
* @default 34
*
* @param StopDelay
* @text Stop Delay (frames)
* @desc Frames without movement before the character returns to its standing pose. Prevents flicker.
* @type number
* @min 0
* @default 6
*
* @param EventMargin
* @text Event Hitbox Margin (px)
* @desc Blocking events use the full tile minus this margin on every side.
* @type number
* @min 0
* @default 4
*
* @param ActionReach
* @text Action Reach (px)
* @desc How far in front of the hitbox the action button looks for events.
* @type number
* @min 1
* @default 16
*
* @param FollowerDistance
* @text Follower Distance (px)
* @desc Distance between party members walking behind the player.
* @type number
* @min 1
* @default 40
*
* @param DisableSwitch
* @text Disable Switch
* @desc While this switch is ON, the player uses the normal tile movement. 0 = none.
* @type switch
* @default 0
*
* @help
* ============================================================================
* PixelMovement (RPG Maker MZ / MV)
* ============================================================================
* The player no longer moves tile by tile but pixel by pixel.
*
* Features
* - Free movement in 4 or 8 directions using the normal map passability
* (including directional passability, star tiles, ladders, bushes,
* damage floors, counters, looping maps).
* - Corner sliding: if you walk against the edge of a wall next to an
* opening (doorway, corridor, gap between NPCs), the player is
* automatically nudged sideways and walks through the opening.
* - Walk animation based on the distance actually walked: no "moonwalking"
* against walls, natural speed when dashing, an immediate first step and
* no flicker when stopping.
* - Followers walk smoothly along the player's path.
* - Events, vehicles, encounters, steps, touch/mouse movement and the action
* button keep working like before. Events themselves still move by tiles.
*
* Notes
* - Hitbox values are in pixels and tuned for 48px tiles (MZ). For 32px
* tiles (MV/older resource packs) try e.g. Width 20, Height 16, Offset 3.
* - "Set Movement Route" for the player, jumps and vehicles use tile movement.
* The player is smoothly aligned to the nearest tile first.
* - Place this plugin below other plugins that change player movement.
*
* No plugin commands. Use the Disable Switch to temporarily get tile movement.
*/
(() => {
"use strict";
const PLUGIN_NAME = (() => {
const script = document.currentScript;
const match = script && script.src.match(/([^/]+)\.js$/);
return match ? decodeURIComponent(match[1]) : "PixelMovement";
})();
const params = PluginManager.parameters(PLUGIN_NAME);
const num = (name, def) => {
const n = Number(params[name]);
return params[name] === undefined || params[name] === "" || isNaN(n) ? def : n;
};
const bool = (name, def) => (params[name] === undefined ? def : params[name] === "true");
const P = {
hitW: num("HitboxWidth", 30),
hitH: num("HitboxHeight", 24),
hitOffY: num("HitboxOffsetY", 4),
slideTol: num("SlideTolerance", 22),
slideSpeed: num("SlideSpeed", 100) / 100,
diagonal: bool("Diagonal", true),
normalizeDiag: bool("NormalizeDiagonal", true),
stride: num("StrideLength", 24),
dashStride: num("DashStrideLength", 34),
stopDelay: num("StopDelay", 6),
eventMargin: num("EventMargin", 4),
actionReach: num("ActionReach", 16),
followerGap: num("FollowerDistance", 40),
disableSwitch: num("DisableSwitch", 0)
};
const EPS = 1e-4;
const DIR_VECTORS = {
1: [-1, 1], 2: [0, 1], 3: [1, 1],
4: [-1, 0], 6: [1, 0],
7: [-1, -1], 8: [0, -1], 9: [1, -1]
};
const tileW = () => $gameMap.tileWidth();
const tileH = () => $gameMap.tileHeight();
const bushDepth = () => ($gameMap.bushDepth ? $gameMap.bushDepth() : 12);
//-------------------------------------------------------------------------
// Hitbox helpers (shared by player and followers)
//-------------------------------------------------------------------------
// Box in tile units. At an integer position the box lies inside its tile.
const hitbox = (px, py) => {
const tw = tileW();
const th = tileH();
const w = Math.min(P.hitW, tw) / tw;
const h = Math.min(P.hitH, th) / th;
const off = Math.min(P.hitOffY, th - Math.min(P.hitH, th)) / th;
const b = py + 1 - off;
return { l: px + 0.5 - w / 2, r: px + 0.5 + w / 2, t: b - h, b: b };
};
const tileXOf = (px, py) => {
const v = Math.floor(px + 0.5 + EPS);
return $dataMap ? $gameMap.roundX(v) : v;
};
const tileYOf = (px, py) => {
const box = hitbox(px, py);
const v = Math.floor((box.t + box.b) / 2 + EPS);
return $dataMap ? $gameMap.roundY(v) : v;
};
// x/y become the tile under the hitbox center; the raw position stays in
// _x/_y. MZ reads _x directly for terrain checks, so those use x/y here.
const defineTileGetters = proto => {
proto.isOnLadder = function() {
return $gameMap.isLadder(this.x, this.y);
};
proto.isOnBush = function() {
return $gameMap.isBush(this.x, this.y);
};
proto.terrainTag = function() {
return $gameMap.terrainTag(this.x, this.y);
};
proto.regionId = function() {
return $gameMap.regionId(this.x, this.y);
};
Object.defineProperty(proto, "x", {
get: function() {
return this.pmTileX();
},
configurable: true
});
Object.defineProperty(proto, "y", {
get: function() {
return this.pmTileY();
},
configurable: true
});
};
//-------------------------------------------------------------------------
// Game_CharacterBase - distance based walk animation
//-------------------------------------------------------------------------
Game_CharacterBase.prototype.pmUsesPixelAnim = function() {
return false;
};
Game_CharacterBase.prototype.pmStrideLength = function() {
return $gamePlayer.isDashing() ? P.dashStride : P.stride;
};
Game_CharacterBase.prototype.pmRefreshBush = function() {
if (
this.isNormalPriority() &&
!this.isObjectCharacter() &&
this.isOnBush() &&
!this.isJumping()
) {
this._bushDepth = bushDepth();
} else {
this._bushDepth = 0;
}
};
const _Game_CharacterBase_updateAnimation = Game_CharacterBase.prototype.updateAnimation;
Game_CharacterBase.prototype.updateAnimation = function() {
if (!this.pmUsesPixelAnim()) {
this._pmWalking = false;
_Game_CharacterBase_updateAnimation.call(this);
return;
}
const dist = this._pmFrameDist || 0;
if (dist > 0 && this.hasWalkAnime()) {
if (!this._pmWalking) {
// Start with a step immediately, alternating the leading foot.
this._pmWalking = true;
this._pmStrideCount = 0;
this._pmFoot = this._pmFoot === 2 ? 0 : 2;
this._pattern = this._pmFoot;
}
this._pmStrideCount += dist;
const stride = this.pmStrideLength();
while (this._pmStrideCount >= stride) {
this._pmStrideCount -= stride;
this._pattern = (this._pattern + 1) % this.maxPattern();
}
this._pmIdle = 0;
this._animationCount = 0;
} else {
this._pmIdle = (this._pmIdle || 0) + 1;
if (this._pmWalking && this._pmIdle >= P.stopDelay) {
this._pmWalking = false;
if (!this.hasStepAnime()) {
this.resetPattern();
}
}
if (!this._pmWalking) {
_Game_CharacterBase_updateAnimation.call(this);
}
}
};
//-------------------------------------------------------------------------
// Game_Player
//-------------------------------------------------------------------------
defineTileGetters(Game_Player.prototype);
Game_Player.prototype.pmTileX = function() {
return tileXOf(this._x, this._y);
};
Game_Player.prototype.pmTileY = function() {
return tileYOf(this._x, this._y);
};
Game_Player.prototype.pmActive = function() {
if (P.disableSwitch > 0 && $gameSwitches.value(P.disableSwitch)) {
return false;
}
return (
!this.isInVehicle() &&
!this._vehicleGettingOn &&
!this._vehicleGettingOff &&
!this.isMoveRouteForcing() &&
!this.isJumping()
);
};
Game_Player.prototype.pmUsesPixelAnim = function() {
return this.pmActive() && !this.isMoving();
};
Game_Player.prototype.pmIsOffGrid = function() {
return this._x !== this.x || this._y !== this.y;
};
// Align the logical position to the tile the player stands on.
// _realX/_realY stay where they are, so the player glides there.
Game_Player.prototype.pmSnapToTile = function() {
const x = this.x;
const y = this.y;
this._x = x;
this._y = y;
};
Game_Player.prototype.pmSyncTileMemory = function() {
this._pmTileX = this.x;
this._pmTileY = this.y;
};
// Player occupies every tile its hitbox overlaps (used by events).
const _Game_Player_pos = Game_Player.prototype.pos;
Game_Player.prototype.pos = function(x, y) {
if (this._x === Math.floor(this._x) && this._y === Math.floor(this._y) && !this.pmIsOffGrid()) {
return _Game_Player_pos.call(this, x, y);
}
const box = hitbox(this._x, this._y);
const c0 = Math.floor(box.l + EPS);
const c1 = Math.floor(box.r - EPS);
const r0 = Math.floor(box.t + EPS);
const r1 = Math.floor(box.b - EPS);
for (let c = c0; c <= c1; c++) {
for (let r = r0; r <= r1; r++) {
if ($gameMap.roundX(c) === x && $gameMap.roundY(r) === y) {
return true;
}
}
}
return false;
};
const _Game_Player_locate = Game_Player.prototype.locate;
Game_Player.prototype.locate = function(x, y) {
_Game_Player_locate.call(this, x, y);
this.pmSyncTileMemory();
this._pmStepAcc = 0;
};
const _Game_Player_update = Game_Player.prototype.update;
Game_Player.prototype.update = function(sceneActive) {
this._pmFrameDist = 0;
this._pmTileChanged = false;
this._pmStepped = false;
_Game_Player_update.call(this, sceneActive);
if (!this.pmActive() || this.isMoving()) {
this.pmSyncTileMemory();
}
};
const _Game_Player_moveByInput = Game_Player.prototype.moveByInput;
Game_Player.prototype.moveByInput = function() {
if (!this.pmActive()) {
_Game_Player_moveByInput.call(this);
return;
}
if (this.isMoving() || !this.canMove()) {
return;
}
const dir = P.diagonal ? Input.dir8 : Input.dir4;
if (dir > 0) {
$gameTemp.clearDestination();
const v = DIR_VECTORS[dir];
this.pmMoveInput(v[0], v[1], false);
} else if ($gameTemp.isDestinationValid()) {
this.pmMoveToDestination();
}
};
Game_Player.prototype.pmFaceInput = function(ix, iy) {
let d;
if (ix !== 0 && iy !== 0) {
const h = ix > 0 ? 6 : 4;
const v = iy > 0 ? 2 : 8;
const cur = this.direction();
d = cur === h || cur === v ? cur : h;
} else if (ix !== 0) {
d = ix > 0 ? 6 : 4;
} else {
d = iy > 0 ? 2 : 8;
}
this.setDirection(d);
};
// ix/iy: -1, 0 or 1. laneAssist centers the player on the path (touch).
Game_Player.prototype.pmMoveInput = function(ix, iy, laneAssist) {
this.pmFaceInput(ix, iy);
const speed = this.distancePerFrame();
let vx = ix * speed;
let vy = iy * speed;
if (ix !== 0 && iy !== 0 && P.normalizeDiag) {
vx *= Math.SQRT1_2;
vy *= Math.SQRT1_2;
}
const sx = this._x;
const sy = this._y;
const obstacles = this.pmObstacles();
if (vx !== 0) {
const moved = this.pmMoveAxis("x", vx, obstacles, true);
if (iy === 0 && Math.abs(moved) < Math.abs(vx) - EPS) {
this.pmCornerSlide("x", vx, obstacles);
}
}
if (vy !== 0) {
const moved = this.pmMoveAxis("y", vy, obstacles, true);
if (ix === 0 && Math.abs(moved) < Math.abs(vy) - EPS) {
this.pmCornerSlide("y", vy, obstacles);
}
}
if (laneAssist) {
if (iy === 0) {
const dy = $gameMap.deltaY(this.y, this._y).clamp(-speed, speed);
this.pmMoveAxis("y", dy, obstacles, false);
} else if (ix === 0) {
const dx = $gameMap.deltaX(this.x, this._x).clamp(-speed, speed);
this.pmMoveAxis("x", dx, obstacles, false);
}
}
// Diagonal input with one axis blocked: face the direction we really move.
if (ix !== 0 && iy !== 0) {
if (Math.abs(this._x - sx) < EPS && Math.abs(this._y - sy) > EPS) {
this.setDirection(iy > 0 ? 2 : 8);
} else if (Math.abs(this._y - sy) < EPS && Math.abs(this._x - sx) > EPS) {
this.setDirection(ix > 0 ? 6 : 4);
}
}
this.pmAfterMove(this._x - sx, this._y - sy);
};
Game_Player.prototype.pmMoveToDestination = function() {
const tx = $gameTemp.destinationX();
const ty = $gameTemp.destinationY();
if (this.x === tx && this.y === ty) {
// Final approach: walk to the exact tile position.
const dx = $gameMap.deltaX(tx, this._x);
const dy = $gameMap.deltaY(ty, this._y);
const len = Math.hypot(dx, dy);
const speed = this.distancePerFrame();
if (len < EPS) {
$gameTemp.clearDestination();
return;
}
const f = Math.min(1, speed / len);
const sx = this._x;
const sy = this._y;
const obstacles = this.pmObstacles();
this.pmMoveAxis("x", dx * f, obstacles, false);
this.pmMoveAxis("y", dy * f, obstacles, false);
this.pmAfterMove(this._x - sx, this._y - sy);
if (len <= speed) {
$gameTemp.clearDestination();
}
} else {
const d = this.findDirectionTo(tx, ty);
if (d > 0) {
const v = DIR_VECTORS[d];
this.pmMoveInput(v[0], v[1], true);
} else {
$gameTemp.clearDestination();
}
}
};
Game_Player.prototype.pmAfterMove = function(mx, my) {
if ($gameMap.isLoopHorizontal()) {
this._x = $gameMap.roundX(this._x);
}
if ($gameMap.isLoopVertical()) {
this._y = $gameMap.roundY(this._y);
}
this._realX = this._x;
this._realY = this._y;
const dist = Math.hypot(mx * tileW(), my * tileH());
this._pmFrameDist = dist > 0.001 ? dist : 0;
if (this._pmFrameDist > 0) {
this.resetStopCount();
this._pmStepAcc = (this._pmStepAcc || 0) + dist / tileW();
if (this._pmStepAcc >= 1) {
this._pmStepAcc -= 1;
this.increaseSteps();
this._pmStepped = true;
}
const tx = this.x;
const ty = this.y;
if (tx !== this._pmTileX || ty !== this._pmTileY) {
this._pmTileX = tx;
this._pmTileY = ty;
this._pmTileChanged = true;
}
}
if (this.isOnLadder()) {
this.setDirection(8);
}
this.pmRefreshBush();
};
//-------------------------------------------------------------------------
// Collision
//-------------------------------------------------------------------------
Game_Player.prototype.pmCanIgnoreCollision = function() {
return this.isThrough() || this.isDebugThrough();
};
// Can the player leave tile (x,y) in direction d?
Game_Player.prototype.pmTilePass = function(x, y, d) {
const x1 = $gameMap.roundX(x);
const y1 = $gameMap.roundY(y);
if (!$gameMap.isValid(x1, y1)) {
return false;
}
const x2 = $gameMap.roundXWithDirection(x1, d);
const y2 = $gameMap.roundYWithDirection(y1, d);
if (!$gameMap.isValid(x2, y2)) {
return false;
}
return this.isMapPassable(x1, y1, d);
};
// Blocking events and vehicles near the player, as boxes in tile units.
Game_Player.prototype.pmObstacles = function() {
const list = [];
if (this.pmCanIgnoreCollision()) {
return list;
}
const mx = P.eventMargin / tileW();
const my = P.eventMargin / tileH();
const add = (cx, cy, owner) => {
const ex = this._x + $gameMap.deltaX(cx, this._x);
const ey = this._y + $gameMap.deltaY(cy, this._y);
if (Math.abs(ex - this._x) > 3 || Math.abs(ey - this._y) > 3) {
return;
}
list.push({ l: ex + mx, r: ex + 1 - mx, t: ey + my, b: ey + 1 - my, owner: owner });
};
for (const event of $gameMap.events()) {
if (event && event.isNormalPriority() && !event.isThrough()) {
add(event.x, event.y, event);
}
}
for (const vehicle of [$gameMap.boat(), $gameMap.ship()]) {
if (vehicle && vehicle._mapId === $gameMap.mapId() && !vehicle.isThrough()) {
add(vehicle.x, vehicle.y, null);
}
}
return list;
};
// How far can the hitbox at (px,py) move along one axis? -> { move, owner }
Game_Player.prototype.pmTestAxis = function(px, py, axis, delta, obstacles) {
const result = { move: delta, owner: null };
if (delta === 0 || this.pmCanIgnoreCollision()) {
return result;
}
const box = hitbox(px, py);
const horz = axis === "x";
const lo = horz ? box.l : box.t;
const hi = horz ? box.r : box.b;
const side0 = horz ? box.t : box.l;
const side1 = horz ? box.b : box.r;
const s0 = Math.floor(side0 + EPS);
const s1 = Math.floor(side1 - EPS);
const pass = (along, across, d) =>
horz ? this.pmTilePass(along, across, d) : this.pmTilePass(across, along, d);
// Tiles: at most one tile boundary is crossed per frame.
if (delta > 0) {
const c = Math.floor(hi - EPS);
if (Math.floor(hi + delta - EPS) > c) {
const d = horz ? 6 : 2;
for (let s = s0; s <= s1; s++) {
if (!pass(c, s, d)) {
result.move = Math.max(0, c + 1 - hi);
break;
}
}
}
} else {
const c = Math.floor(lo + EPS);
if (Math.floor(lo + delta + EPS) < c) {
const d = horz ? 4 : 8;
for (let s = s0; s <= s1; s++) {
if (!pass(c, s, d)) {
result.move = Math.min(0, c - lo);
break;
}
}
}
}
// Events / vehicles (ignore ones we already overlap, so we can't get stuck).
for (const o of obstacles) {
const oLo = horz ? o.l : o.t;
const oHi = horz ? o.r : o.b;
const oS0 = horz ? o.t : o.l;
const oS1 = horz ? o.b : o.r;
if (side0 >= oS1 - EPS || side1 <= oS0 + EPS) {
continue;
}
if (delta > 0 && hi <= oLo + EPS && hi + result.move > oLo) {
result.move = Math.max(0, oLo - hi);
result.owner = o.owner;
} else if (delta < 0 && lo >= oHi - EPS && lo + result.move < oHi) {
result.move = Math.min(0, oHi - lo);
result.owner = o.owner;
}
}
return result;
};
Game_Player.prototype.pmMoveAxis = function(axis, delta, obstacles, canTouch) {
if (Math.abs(delta) < EPS * 0.01) {
return 0;
}
const res = this.pmTestAxis(this._x, this._y, axis, delta, obstacles);
if (axis === "x") {
this._x += res.move;
} else {
this._y += res.move;
}
if (canTouch && res.owner && Math.abs(res.move) < Math.abs(delta) - EPS) {
this.pmTouchEvent(res.owner);
}
return res.move;
};
Game_Player.prototype.pmTouchEvent = function(event) {
if (event instanceof Game_Event && !$gameMap.isEventRunning()) {
this.checkEventTriggerTouch(event.x, event.y);
}
};
// Blocked on `axis`: look for an opening within the tolerance on the
// other axis and nudge the player towards it. Returns the nudge distance.
Game_Player.prototype.pmCornerSlide = function(axis, delta, obstacles) {
if (P.slideTol <= 0) {
return 0;
}
const other = axis === "x" ? "y" : "x";
const unit = other === "x" ? 1 / tileW() : 1 / tileH();
const maxStep = Math.abs(delta) * P.slideSpeed;
for (let k = 1; k <= P.slideTol; k++) {
for (const sign of [-1, 1]) {
const off = sign * k * unit;
const shift = this.pmTestAxis(this._x, this._y, other, off, obstacles);
if (Math.abs(shift.move - off) > EPS) {
continue;
}
const nx = other === "x" ? this._x + off : this._x;
const ny = other === "y" ? this._y + off : this._y;
const ahead = this.pmTestAxis(nx, ny, axis, delta, obstacles);
if (Math.abs(ahead.move) < Math.abs(delta) - EPS) {
continue;
}
const step = sign * Math.min(maxStep, k * unit);
return this.pmMoveAxis(other, step, obstacles, false);
}
}
return 0;
};
//-------------------------------------------------------------------------
// Events / triggers
//-------------------------------------------------------------------------
const _Game_Player_updateNonmoving = Game_Player.prototype.updateNonmoving;
Game_Player.prototype.updateNonmoving = function(wasMoving, sceneActive) {
if (!this.pmActive()) {
_Game_Player_updateNonmoving.call(this, wasMoving, sceneActive);
return;
}
if ($gameMap.isEventRunning()) {
return;
}
if (this._pmStepped) {
$gameParty.onPlayerWalk();
}
if (this._pmTileChanged) {
this.checkEventTriggerHere([1, 2]);
if ($gameMap.setupStartingEvent()) {
return;
}
}
if (sceneActive && this.triggerAction()) {
return;
}
if (this._pmStepped) {
this.updateEncounterCount();
} else if (!this._pmFrameDist) {
$gameTemp.clearDestination();
}
};
// Tile in front of the hitbox (not just in front of the center tile).
Game_Player.prototype.pmFrontTile = function(d) {
const box = hitbox(this._x, this._y);
const cx = (box.l + box.r) / 2;
const cy = (box.t + box.b) / 2;
const v = DIR_VECTORS[d] || [0, 0];
const reachX = (box.r - box.l) / 2 + P.actionReach / tileW();
const reachY = (box.b - box.t) / 2 + P.actionReach / tileH();
return {
x: $gameMap.roundX(Math.floor(cx + v[0] * reachX)),
y: $gameMap.roundY(Math.floor(cy + v[1] * reachY))
};
};
const _Game_Player_checkEventTriggerThere = Game_Player.prototype.checkEventTriggerThere;
Game_Player.prototype.checkEventTriggerThere = function(triggers) {
if (this.pmActive() && this.canStartLocalEvents()) {
const d = this.direction();
const f = this.pmFrontTile(d);
this.startMapEvent(f.x, f.y, triggers, true);
if (!$gameMap.isAnyEventStarting() && $gameMap.isCounter(f.x, f.y)) {
const x3 = $gameMap.roundXWithDirection(f.x, d);
const y3 = $gameMap.roundYWithDirection(f.y, d);
this.startMapEvent(x3, y3, triggers, true);
}
if ($gameMap.isAnyEventStarting()) {
return;
}
}
_Game_Player_checkEventTriggerThere.call(this, triggers);
};
//-------------------------------------------------------------------------
// Switching to tile movement (move routes, jumps, vehicles)
//-------------------------------------------------------------------------
const snapWrapper = original =>
function(...args) {
if (!this.pmIsOffGrid()) {
return original.apply(this, args);
}
const rx = this._realX;
const ry = this._realY;
this.pmSnapToTile();
const result = original.apply(this, args);
// Glide from the current pixel position instead of jumping.
if (!this.isJumping()) {
this._realX = rx;
this._realY = ry;
}
return result;
};
Game_Player.prototype.moveStraight = snapWrapper(Game_Player.prototype.moveStraight);
Game_Player.prototype.moveDiagonally = snapWrapper(Game_Player.prototype.moveDiagonally);
Game_Player.prototype.jump = snapWrapper(Game_Player.prototype.jump);
//-------------------------------------------------------------------------
// Game_Follower - trails the player's exact path
//-------------------------------------------------------------------------
defineTileGetters(Game_Follower.prototype);
Game_Follower.prototype.pmTileX = function() {
return tileXOf(this._x, this._y);
};
Game_Follower.prototype.pmTileY = function() {
return tileYOf(this._x, this._y);
};
Game_Follower.prototype.pos = function(x, y) {
return this.x === x && this.y === y;
};
Game_Follower.prototype.pmUsesPixelAnim = function() {
return true;
};
Game_Follower.prototype.isMoving = function() {
return (this._pmFrameDist || 0) > 0 || Game_Character.prototype.isMoving.call(this);
};
Game_Follower.prototype.isGathered = function() {
return this.x === $gamePlayer.x && this.y === $gamePlayer.y;
};
Game_Follower.prototype.pmSetPosition = function(x, y) {
const dx = x - this._realX;
const dy = y - this._realY;
const dist = Math.hypot(dx * tileW(), dy * tileH());
if (dist > 0.001 && dist < tileW() * 2) {
this._pmFrameDist = dist;
this.pmFaceDelta(dx, dy);
} else {
this._pmFrameDist = 0;
}
this._x = this._realX = x;
this._y = this._realY = y;
if (this.isOnLadder()) {
this.setDirection(8);
}
this.pmRefreshBush();
};
// Turn towards the movement, with some hysteresis for diagonal paths.
Game_Follower.prototype.pmFaceDelta = function(dx, dy) {
const ax = Math.abs(dx);
const ay = Math.abs(dy);
const d = this.direction();
if (((d === 4 && dx < 0) || (d === 6 && dx > 0)) && ax >= ay * 0.8) {
return;
}
if (((d === 8 && dy < 0) || (d === 2 && dy > 0)) && ay >= ax * 0.8) {
return;
}
if (ax > ay) {
this.setDirection(dx > 0 ? 6 : 4);
} else {
this.setDirection(dy > 0 ? 2 : 8);
}
};
//-------------------------------------------------------------------------
// Game_Followers
//-------------------------------------------------------------------------
const followerGap = () => P.followerGap / tileW();
Game_Followers.prototype.pmResetTrail = function(x, y) {
this._pmTrail = [{ x: x, y: y }];
this._pmShrink = 0;
};
Game_Followers.prototype.pmUpdateTrail = function() {
if (!this._pmTrail || this._pmTrail.length === 0) {
this.pmResetTrail($gamePlayer._realX, $gamePlayer._realY);
}
const trail = this._pmTrail;
const px = $gamePlayer._realX;
const py = $gamePlayer._realY;
let last = trail[trail.length - 1];
// Keep the trail continuous when the player wraps around a looping map.
const w = $gameMap.width();
const h = $gameMap.height();
const shiftX = $gameMap.isLoopHorizontal() ? Math.round((px - last.x) / w) * w : 0;
const shiftY = $gameMap.isLoopVertical() ? Math.round((py - last.y) / h) * h : 0;
if (shiftX !== 0 || shiftY !== 0) {
for (const p of trail) {
p.x += shiftX;
p.y += shiftY;
}
for (const f of this._data) {
f._x += shiftX;
f._realX += shiftX;
f._y += shiftY;
f._realY += shiftY;
}
last = trail[trail.length - 1];
}
if (Math.abs(px - last.x) > EPS || Math.abs(py - last.y) > EPS) {
trail.push({ x: px, y: py });
}
// Drop points nobody needs anymore.
const maxLen = (this._data.length + 1) * followerGap() + 1;
let acc = 0;
for (let i = trail.length - 1; i > 0; i--) {
acc += Math.hypot(trail[i].x - trail[i - 1].x, trail[i].y - trail[i - 1].y);
if (acc > maxLen) {
trail.splice(0, i - 1);
break;
}
}
};
Game_Followers.prototype.pmTrailPoint = function(distance) {
const trail = this._pmTrail;
let acc = 0;
for (let i = trail.length - 1; i > 0; i--) {
const a = trail[i];
const b = trail[i - 1];
const seg = Math.hypot(a.x - b.x, a.y - b.y);
if (acc + seg >= distance) {
const t = seg > 0 ? (distance - acc) / seg : 0;
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}
acc += seg;
}
return trail[0];
};
Game_Followers.prototype.update = function() {
this.pmUpdateTrail();
if (this._gathering) {
this._pmShrink = (this._pmShrink || 0) + $gamePlayer.distancePerFrame();
}
const gap = followerGap();
const shrink = this._gathering ? this._pmShrink : 0;
this._data.forEach((follower, i) => {
const pt = this.pmTrailPoint(Math.max(0, (i + 1) * gap - shrink));
follower.pmSetPosition(pt.x, pt.y);
});
if (this._gathering && this.areGathered()) {
this._gathering = false;
this.pmResetTrail($gamePlayer._realX, $gamePlayer._realY);
}
for (const follower of this._data) {
follower.update();
}
};
Game_Followers.prototype.updateMove = function() {
// Followers are positioned along the trail in update().
};
Game_Followers.prototype.areGathered = function() {
const count = this.visibleFollowers().length;
return (this._pmShrink || 0) >= count * followerGap() - EPS;
};
const _Game_Followers_synchronize = Game_Followers.prototype.synchronize;
Game_Followers.prototype.synchronize = function(x, y, d) {
_Game_Followers_synchronize.call(this, x, y, d);
this.pmResetTrail(x, y);
};
})();