//============================================================================= // 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); }; })();