// Mario Bookmarklet Script - V6.5 (Comprehensive Comments) (function() { // Prevent multiple instances of Mario on the page if (document.getElementById('pageMarioContainer')) { const existingMarioContainer = document.getElementById('pageMarioContainer'); // If an instance exists but is hidden, show it. Otherwise, do nothing. if (existingMarioContainer.style.display === 'none') { existingMarioContainer.style.display = 'block'; } return; } // --- CONSTANTS --- const SCALE_FACTOR = 5; // Visual and physics scaling factor. Mario appears 2x larger. /** * Parses the 5-digit hexadecimal physics values from "A Complete Guide to SMB's Physics Engine". * Format: BSSs.sss (Blocks, Pixels, Subpixels, SubSubpixels, SubSubSubpixels) * where each unit is 1/16th of the previous, starting from Pixels for sub-units. * Blocks are 16 pixels. * This function converts the guide's values into a single "guide pixel" unit for our physics. * @param {string} hexStr - The 5-digit hexadecimal string (e.g., "01900"). * @returns {number} The value in "guide pixel" units. */ function parsePhysicsValue(hexStr) { const b = parseInt(hexStr.charAt(0), 16); // Blocks (guide refers to this as 0 for vel/accel) const p = parseInt(hexStr.charAt(1), 16); // Pixels (s.-pixels) const s = parseInt(hexStr.charAt(2), 16); // Subpixels (ss.-pixels, 1/16th of a pixel) const ss = parseInt(hexStr.charAt(3), 16); // SubSubpixels (1/256th of a pixel) const sss = parseInt(hexStr.charAt(4), 16);// SubSubSubpixels (1/4096th of a pixel) // The guide's "Blocks" digit (first char) for velocities/accelerations is usually 0. // Our interpretation: 1 Block = 16 Pixels. return (b * 16) + p + (s / 16.0) + (ss / 256.0) + (sss / 4096.0); } // Physics parameters, converted from the guide's hex values. // These represent values like player speed (Player_X_Speed = $57, Player_Y_Speed = $9f), // acceleration/friction (FrictionAdderLow = $0702), and jump forces (VerticalForce = $0709) // as described abstractly in SMBDIS.ASM. const PHYSICS = { maxWalkSpeed: parsePhysicsValue("01900"), // Approx 1.56 guide pixels/frame walkingAcceleration: parsePhysicsValue("00098"), // Approx 0.037 guide pixels/frame^2 maxRunSpeed: parsePhysicsValue("02900"), // Approx 2.56 guide pixels/frame runningAcceleration: parsePhysicsValue("000E4"), // Approx 0.056 guide pixels/frame^2 releaseDeceleration: parsePhysicsValue("000D0"), // Approx 0.05 guide pixels/frame^2 (friction) skiddingDeceleration: parsePhysicsValue("001A0"),// Approx 0.1 guide pixels/frame^2 // Jump velocities are negative because positive Y is downwards. jumpInitialVelocitySlow: -parsePhysicsValue("04000"), // For horizontal speeds (absVx) < speedThresholdFast (02500) jumpInitialVelocityFast: -parsePhysicsValue("05000"), // For absVx >= speedThresholdFast (02500) // Gravity values (positive Y is downwards) based on speed category at jump initiation. // Corresponds to "Holding 'A' Gravity" and "Falling Gravity" in the guide. gravityA_Held_Slow: parsePhysicsValue("00200"), // absVx < speedThresholdMedium ("Less than 01000") gravityA_Held_Medium: parsePhysicsValue("001E0"), // speedThresholdMedium <= absVx < speedThresholdFast ("01000 - 024FF") gravityA_Held_Fast: parsePhysicsValue("00280"), // absVx >= speedThresholdFast ("02500 or greater") gravityFalling_Slow: parsePhysicsValue("00700"), gravityFalling_Medium: parsePhysicsValue("00600"), gravityFalling_Fast: parsePhysicsValue("00900"), terminalVelocity: parsePhysicsValue("04800"), // Max falling speed, approx 4.5 guide pixels/frame // Speed thresholds from the guide for jump/gravity categorization. speedThresholdMedium: parsePhysicsValue("01000"), speedThresholdFast: parsePhysicsValue("02500") }; // Sprite URLs (base64 encoded images) const SPRITES = { standing: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAATxJREFUSImlVbuNwzAMfRYyQ2rBG7i4LhPktnB78AyBZzCuzRbJBOlSaAPBdYY4XRFIICXSlp1XERI/jx9RDRTcOhvys283N/nZocYQAL4GA/Tluak2VsAoaQ4kxHRYCvfTnORL1wIARueTDADH3rOgidvPgHDpWqYcMTqf5Ne1Da9rGwoHmpHmNDoxMfpiwhkLiqKNIPnnhlT+nd4NYClIUST6FKyiMZU1I9qJYjRvnQ3S4IzOJ9oURQ3upxmYLJsJADg/LDDMIXfCQtV0I9cx2kWtk+o50JwU1To/rGok3Zk90SkLcRLXmDAG0qE0B9pSaejTBIDn9LcYMXek76pKmHzDbMGx9w1jsEZf0knRtyxUkKXaLBlHpaV7tYj0F5J+pIiPu3CIEYqRdlwx3w/iTtzFYO9jUp/zVvwDv6eCnFqRE4sAAAAASUVORK5CYII=)', crouch: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAR9JREFUSIntVLENwjAQvESZgdrKBi7SMYHZIm3kGRAzRLTZAiZIR5ENotQMgSmQLf/7baCj4KqP3/d/Z38M/PHHL6AqJS9aOb52WDbCaT4lAkBna6Cna/VXZAGJhVwBCYdlqxIL1/0W4qNuAQCnZQ0xAOz6NTQmugYLd9Qt2exxWtYQ36fW3afWEQuDhYu7Sp35N3K3IFmQlJxHVMGCmRXMrMgGXkhCFcv3ha77rUjy2PVregtcKj+DuAkANHH3UqEc5PEqwMwqxIOFEwvk/HtyPGziGdzGBwyUlCJkSBb4hnc5oiBI1y8VMTpbo0M6WLX39cm980ZmVi8LnMy7S2uEc9HK+T+sNBeDRdjn343kb/Q4j/SxyeXfTmJJDQA8AfHeg3BDDfSlAAAAAElFTkSuQmCC)', jump: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAURJREFUSImlVbuRgzAQfdJQA7GGDggucwVcF6QeamBcg0apuzAVkF1ABwzxFXFc4uVWq4+F70Vi2fd2pX0IhZP4vjc7ret+Vfod8m1Zj+cqlfxozZ56RyJj20CdIX4MYcOewKuq02X7n0BMrIoFAWBsG4DtlUAHSDg2dR2wj23jJccgc5JjpEopQYoX+0C2TlB4tp+qliICgLPwnXhb1oAQE+Uxb4y5TmJdOQv10ok02m42gZGchap4ZQCY8JfUzSZKJDIAaE6W4MTpsqGbTZBTNEZJpOqeQIkLY11oiFPna55Ma/lJ61RVTiZSNxt82Z+wgxwZQEAKBOp+VSXJEtcBu677VdFleZaMd24kaSqde1mCijuR2zbmuqiADBCxVCj5YyHi57IpZM4nKSBBQlj8ePZjOkgZHB04C+VwfgrI3Qcl+AX6GpjZUk8DmAAAAABJRU5ErkJggg==)', skid: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAWdJREFUSImdVbGRgzAQ3NO4ho8ZOlDgjAqeLpx6qMHzNTCk7gIq+MwBHXgUuwjrA8/hlZAE/o0Eul3tnY5DkMFoK5/ba2cnuj7o4tzBA8D3b5Xj4diZ1+L0fmeYvAdfp7vwswDA41p7APiZ7wCAi62hzzlHmoZRMhNj28fOYGrcsgbVyOy1zg7b2Yk6ELZ/sTVu/fNdLHqfq4fhID3h1j+XICbrvuJxrX3gQAs2NS4gx0QWDa5ktJVXchyYEhp6iDCZT8+dyPkHDvj0VN7cDxo39JADIsSn6c3EZMWuPpgal0wNcRFBbc2Ii5d0MNrKj7byardESjqIv3/txi2hrAAjlz9SjfSpyKqIPFxSs4CFgk7MiZTE2tnJaiZ+iqATSx/P1LjAhQ6UVSunBFmIRzpSRSz1w9Cv4+XcwevG3p8JI/gvtLOT1H3nyIuD3KYWrSiAjStM5b1KYSuohIAYO9kjHEyk/zj5A99XxHiqsqoTAAAAAElFTkSuQmCC)', walk: [ 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAUJJREFUSInFVblxwzAQXGBcg2MOOkCgzBWAXSjloAaNatA4ZRd2BczRAYcxm4AD+TCHH1bijUBSt3d7uyKB/4ZoPfzSk0/vze6Iat5GCwHgYiVwje/JPxUXkEmoEZQwu0NkEr4/jnC+aQUAuLs9nAHg/bqHxtFci4W/aRX9mHB3ezifq/LnqnxGUCuqkZ6r8oFgsehq51MQijaC6U8L+fnzASH5Ra1LaXxCMYm0zBbIiYxgsfBmm8I1DxC3jxDtgIp5FvB4XpPEFE0b8Rsss01Vl8IELRuJxGh4kkRyIglce4kEAC54LreZxBZRiiwHPaQ5qS5xdIqoa+1dwG012xS91qr/hdFpujloTQMAIvW/ZyU9JxlDEqhribxLwLsShpc4u0PAAUbXY54RpF8eFBYHVyAYTWIK+UrxYuHJPflqZ8IPpxyJWPji/hAAAAAASUVORK5CYII=)', 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAATpJREFUSImlVLENwjAQPCxmoI6yQYp0TBC2oEWZAWWGiDZbwATpKLJBlJohCAWydbb/HSOuchz//b3v38Cf2Gk/7lWxhnunaYnO73MCAaBuDXCO9012sAJPkkYgwZbjlfA4Lm59rUoAQDfNbg0Ah/PsJXXaLi3Wa1V6hy26aXbr11Cur6FcIwItSCO1JMZmTxYcqGBENoLqDwN5feu/Bhj+kLJI8hnejdpStoLYiag1w16wTRTapypoxsL1QzMWAPWHLZVhtGAOtEQS9CYXINmdTaCp2CR4HBfUrVEncpOALZXsdQSpi2KENkcKcoiYRCxBI3n272hPHCYm6TAnSZ0C6cVlcIPx2eQscBCDW1q1MSdYVMAqQpLkMEnZm7HwLk+aBdUFzn7rsdPezUiSdFCS/hNBikhlzlXyAfB+icjL2GvBAAAAAElFTkSuQmCC)', 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAU5JREFUSImllLGRgzAQRb801EDMqAMCZ66A64LUQw0eamCc0sVdBWQO6EDjmCKsC3zLrKSVhH0/Ehrt213tRwoJfbeNC/e+1ocK96ojgQBwGjTQx/v6cHBCXkkpgCRqx2vh5/zY19fWAADG1e5rAKh76yXda7sMcNfWeIdJ42r39TYbt83GRYBUUApKEE3Zsw0HVXBFYwTrPwzk69v0GoDXgpRFKp/Lu1FqpRTEJxFZE8GdECwcXxIQmom80S2N9y9ss3F1b5XYAqlbGhEi+kAaJQ9MnTvsAwJyGHJOzInGPa42DwizhZDbBKVT5fPg06D3b3obyIkqdfM58XFWketa4D49ixDSR5dIugxwEeBodmpdgT0OJQB/8kgafz9K3Vt1n57ioZwiK3dL8xakknzAIaWxFn3AZy4liwAQ/v1sBSGZLHpU/zIS+JP2aRW/G1GlQvb7go8AAAAASUVORK5CYII=)' ] }; // Sound effect URLs and player const SOUND_BASE_URL = 'https://raw.githubusercontent.com/mroyale/assets/refs/heads/legacy/audio/sfx/'; const SOUNDS = { jumpBig: SOUND_BASE_URL + 'jump1.mp3', jumpSmall: SOUND_BASE_URL + 'jump0.mp3', // Will be used if mario.isBig is false bump: SOUND_BASE_URL + 'bump.mp3', loaded: SOUND_BASE_URL + 'item.mp3', // Sound for Mario appearing on the page // Sounds for future implementation die: SOUND_BASE_URL + 'dead.mp3', powerupCollect: SOUND_BASE_URL + 'powerup.mp3' }; /** * Plays a sound effect. * @param {string} soundKey - The key for the sound in the SOUNDS object. * @param {number} [volume=0.05] - Volume level (0.0 to 1.0). */ function playSound(soundKey, volume = 0.05) { // Adjusted default volume if (SOUNDS[soundKey]) { const audio = new Audio(SOUNDS[soundKey]); audio.volume = volume; // Set volume BEFORE playing audio.play().catch(e => console.warn("Sound play failed for " + soundKey + ":", e.message)); } } // Durations for each frame of the walk animation based on speed const ANIM_DURATIONS = { run_max: 2, // When at/near max run speed run_accel: 3, // When accelerating to run or running at medium speed walk_max: 4, // When at/near max walk speed walk_accel: 7 // When walking slowly or accelerating to walk }; // Mario's state object const mario = { el: null, // The DOM element for Mario // x, y are bottom-center of Mario in physics units (viewport-relative) x: (window.innerWidth / SCALE_FACTOR) / 2, y: (window.innerHeight / SCALE_FACTOR) - 5, previousY: 0, // Mario's y position at the start of the previous frame (for platform landing) vx: 0, vy: 0, // Velocities in physics units per frame baseWidth: 16, // Physics width standingBaseHeight: 32, // Physics hitbox height when standing (Big Mario) crouchingHitboxHeight: 16, // Physics hitbox height when crouching (Big Mario) currentHitboxHeight: 32, // Current physics hitbox height, changes with crouching visualHeight: 32, // Visual height of the div (fixed for Big Mario, matches standing) onGround: false, // True if Mario is on a surface (floor or platform) facingRight: true, // Direction Mario is visually facing isRunning: false, // True if the run key ('Z') is held isBig: true, // True if Mario is in his "Big" state (always true for now) isCrouching: false, // True if Mario is currently crouching isSkidding: false, // True if Mario is in a skidding state jumpCategory: 'slow', // Determines jump physics ('slow', 'medium', 'fast') based on speed at takeoff jumpStartedCrouched: false, // True if a jump was initiated while crouching jumpKeyPressedLastFrame: false, // Tracks if jump key was pressed in the previous frame to prevent auto-jump // Animation state currentSpriteKey: 'standing', // Key for SPRITES object (e.g., 'standing', 'walk', 'jump') animationFrameIndex: 0, // Current frame index for multi-frame animations (like walk) animationTimer: 0 // Timer to control duration of the current animation frame }; mario.currentHitboxHeight = mario.standingBaseHeight; mario.visualHeight = mario.standingBaseHeight; mario.previousY = mario.y; // Input handling state const keys = {}; let lastScrollY = window.scrollY; // Tracks last vertical page scroll position let lastScrollX = window.scrollX; // Tracks last horizontal page scroll position /** * Handles key down events. * Sets the corresponding key state and prevents default browser action for game keys. */ function handleKeyDown(e) { keys[e.code] = true; // Prevent default browser behavior (like page scrolling) for game control keys if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Space", "KeyX", "KeyZ"].includes(e.code)) { e.preventDefault(); } } /** * Handles key up events. */ function handleKeyUp(e) { keys[e.code] = false; } /** * Initializes Mario, sets up the DOM element, and starts the game loop. */ function init() { // Create a container for Mario to live in (helps with z-index and positioning context) const container = document.createElement('div'); container.id = 'pageMarioContainer'; container.style.cssText = 'position:fixed; left:0; top:0; width:100%; height:100%; pointer-events:none; z-index:99999;'; // Create Mario's DOM element mario.el = document.createElement('div'); mario.el.id = 'pageMario'; mario.el.style.position = 'absolute'; mario.el.style.backgroundImage = SPRITES.standing; // Set initial sprite mario.el.style.backgroundRepeat = 'no-repeat'; mario.el.style.backgroundSize = '100% 100%'; // Stretch the 16x32 sprite to fill the div mario.el.style.imageRendering = 'pixelated'; // For crisp pixel art // CSS hack to promote to its own compositing layer, can help prevent visual artifacts/smearing mario.el.style.transform = 'translateZ(0px)'; // Set visual dimensions based on base size and scale factor mario.el.style.width = (mario.baseWidth * SCALE_FACTOR) + 'px'; mario.el.style.height = (mario.visualHeight * SCALE_FACTOR) + 'px'; // Visual height is Big Mario's standing height container.appendChild(mario.el); document.body.appendChild(container); // Add event listeners for keyboard input document.addEventListener('keydown', handleKeyDown); document.addEventListener('keyup', handleKeyUp); // Initialize scroll tracking and Mario's starting position (viewport-relative) lastScrollY = window.scrollY; lastScrollX = window.scrollX; mario.y = (window.innerHeight / SCALE_FACTOR) - 5; // Bottom edge 5 physics units above viewport bottom mario.x = (window.innerWidth / SCALE_FACTOR) / 2; // Horizontal center of viewport mario.previousY = mario.y; playSound('loaded'); // Play sound when Mario is initialized gameLoop(); // Start the main game loop } let gameLoopId = null; // ID for canceling the animation frame let platformQueryCounter = 0; // Counter to limit frequency of DOM queries for platforms const PLATFORM_QUERY_INTERVAL = 5; // Query for platforms every 5 frames let currentPlatforms = []; // Cached list of potential platforms /** * Main game loop, calls update and render. */ function gameLoop() { update(); render(); gameLoopId = requestAnimationFrame(gameLoop); } /** * Updates Mario's physics, state, and handles collisions. */ function update() { // --- Handle Page Scroll --- // Adjust Mario's viewport-relative physics position to compensate for page scrolling. // This makes him appear to stay in place relative to the page content. const currentScrollY = window.scrollY; const scrollDeltaY = currentScrollY - lastScrollY; if (scrollDeltaY !== 0) { const scaledScrollDelta = scrollDeltaY / SCALE_FACTOR; mario.y -= scaledScrollDelta; } lastScrollY = currentScrollY; const currentScrollX = window.scrollX; const scrollDeltaX = currentScrollX - lastScrollX; if (scrollDeltaX !== 0) { const scaledScrollDelta = scrollDeltaX / SCALE_FACTOR; mario.x -= scaledScrollDelta; } lastScrollX = currentScrollX; // Store Mario's y position from *before* this frame's physics are applied (but *after* scroll compensation). // Crucial for one-way platform collision detection. mario.previousY = mario.y; // --- Read Inputs --- const onLeft = keys['ArrowLeft']; const onRight = keys['ArrowRight']; const onDown = keys['ArrowDown']; const onJumpKey = keys['KeyX']; const onRun = keys['KeyZ']; mario.isRunning = onRun; mario.isSkidding = false; // Reset skid state at the beginning of each frame // --- Crouching Logic (Big Mario only) --- // SMBDIS.ASM: CrouchingFlag = $0714, PlayerSize = $0754 if (mario.isBig) { if (mario.onGround) { // Can only change crouch state if on the ground if (onDown && !mario.isCrouching) { // Pressing Down: Start crouching mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight; } else if (!onDown && mario.isCrouching && !mario.jumpStartedCrouched) { // Releasing Down: Stop crouching // TODO: Implement ceiling check here to prevent standing up into a block mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; } } // Handle landing from a crouch-jump if (mario.onGround && mario.jumpStartedCrouched) { mario.jumpStartedCrouched = false; // Reset flag // Re-evaluate crouch state based on whether Down is still held if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;} else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; } } } // --- Horizontal Movement --- // Inspired by assembly logic for acceleration, max speed, and friction. // SMBDIS.ASM: Player_X_Speed = $57, RunningSpeed = $0703, FrictionAdderLow = $0702 let targetVx = 0; let currentMaxSpeed = mario.isRunning ? PHYSICS.maxRunSpeed : PHYSICS.maxWalkSpeed; let currentAcceleration = mario.isRunning ? PHYSICS.runningAcceleration : PHYSICS.walkingAcceleration; // Cannot initiate new horizontal movement if crouching on the ground const canMoveHorizontally = !(mario.isCrouching && mario.onGround); if (canMoveHorizontally) { if (onLeft) { targetVx = -currentMaxSpeed; } if (onRight) { targetVx = currentMaxSpeed; } if (onLeft && onRight) { targetVx = mario.facingRight ? currentMaxSpeed : -currentMaxSpeed; } // If both, maintain current direction // Mid-Air Direction Lock: Only change facing direction if on ground and a new direction is input. if (mario.onGround) { if (onLeft && !onRight) mario.facingRight = false; if (onRight && !onLeft) mario.facingRight = true; } } if (targetVx !== 0 && canMoveHorizontally) { // Skidding: If trying to move opposite to current velocity and not stationary. if (Math.sign(targetVx) !== Math.sign(mario.vx) && mario.vx !== 0) { mario.vx += Math.sign(targetVx) * PHYSICS.skiddingDeceleration; mario.isSkidding = true; // During skid, if on ground, face the new direction of movement intention. if (mario.onGround) { if (onLeft) mario.facingRight = false; if (onRight) mario.facingRight = true; } } else { // Normal acceleration mario.vx += Math.sign(targetVx) * currentAcceleration; } // Cap speed if (targetVx > 0) mario.vx = Math.min(mario.vx, targetVx); else mario.vx = Math.max(mario.vx, targetVx); } else { // Decelerating (friction) const decel = PHYSICS.releaseDeceleration; if (Math.abs(mario.vx) > decel) mario.vx -= Math.sign(mario.vx) * decel; else mario.vx = 0; } // --- Vertical Movement (Jumping) --- // SMBDIS.ASM: VerticalForce = $0709 (initial jump), VerticalForceDown = $070a (gravity) // Jump only if key is newly pressed (transition from not pressed to pressed) if (mario.onGround && onJumpKey && !mario.jumpKeyPressedLastFrame) { const currentAbsSpeed = Math.abs(mario.vx); // Used to determine jump height category // Select initial jump velocity based on current horizontal speed (from guide) if (currentAbsSpeed >= PHYSICS.speedThresholdFast) { mario.vy = PHYSICS.jumpInitialVelocityFast; mario.jumpCategory = 'fast'; } else { mario.vy = PHYSICS.jumpInitialVelocitySlow; // For both slow and medium initial jump height mario.jumpCategory = (currentAbsSpeed >= PHYSICS.speedThresholdMedium) ? 'medium' : 'slow'; } if (mario.isCrouching) mario.jumpStartedCrouched = true; // Flag if jump started from crouch mario.onGround = false; // Mario is now in the air playSound(mario.isBig ? 'jumpBig' : 'jumpSmall'); // Play appropriate jump sound } mario.jumpKeyPressedLastFrame = onJumpKey; // Store jump key state for next frame // If crouch-jumping, ensure hitbox and crouching state remain small/active if (mario.jumpStartedCrouched) { mario.currentHitboxHeight = mario.crouchingHitboxHeight; mario.isCrouching = true; } // --- Apply Gravity --- if (!mario.onGround) { let appliedGravity, heldGravity; // Select gravity values based on speed category at the *start* of the jump if (mario.jumpCategory === 'fast') { heldGravity = PHYSICS.gravityA_Held_Fast; appliedGravity = PHYSICS.gravityFalling_Fast; } else if (mario.jumpCategory === 'medium') { heldGravity = PHYSICS.gravityA_Held_Medium; appliedGravity = PHYSICS.gravityFalling_Medium; } else { heldGravity = PHYSICS.gravityA_Held_Slow; appliedGravity = PHYSICS.gravityFalling_Slow; } // Apply less gravity if jump key is held and Mario is rising (variable jump height) if (onJumpKey && mario.vy < 0) mario.vy += heldGravity; else mario.vy += appliedGravity; // Apply normal falling gravity } // Apply terminal velocity if (mario.vy > PHYSICS.terminalVelocity) mario.vy = PHYSICS.terminalVelocity; // --- Update Position from Velocities --- mario.x += mario.vx; mario.y += mario.vy; // Assume Mario is in the air until a collision with ground/platform sets onGround = true mario.onGround = false; // --- Platform Collision Detection --- platformQueryCounter++; if (platformQueryCounter >= PLATFORM_QUERY_INTERVAL) { platformQueryCounter = 0; const allElements = document.querySelectorAll('div, p, span, h1, h2, h3, h4, h5, h6, li, a, button, img, article, section, header, footer, nav, main, td, th, label, input, textarea'); currentPlatforms = []; for (const el of allElements) { const rect = el.getBoundingClientRect(); // Viewport-relative screen pixels // Filter elements to be potential platforms if (rect.width > 10 && rect.height > 2 && // Minimum dimensions rect.width < window.innerWidth * 1.5 && rect.height < window.innerHeight * 0.75 && // Avoid huge layout blocks el.offsetParent !== null) { // Element is rendered const style = window.getComputedStyle(el); if (style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0.1) { currentPlatforms.push({ // Store platform coords in physics units top: (rect.top / SCALE_FACTOR), bottom: (rect.bottom / SCALE_FACTOR), left: (rect.left / SCALE_FACTOR), right: (rect.right / SCALE_FACTOR), el: el }); } } } currentPlatforms.sort((a, b) => a.top - b.top); // Sort by top edge } let landedOnPlatformThisFrame = false; for (const platform of currentPlatforms) { const marioLeft = mario.x - (mario.baseWidth / 2); const marioRight = mario.x + (mario.baseWidth / 2); // mario.y is current bottom, mario.previousY is previous bottom (both viewport-relative physics units) // platform.top is also viewport-relative physics units const landingTolerance = Math.max(5, Math.abs(mario.vy) * 1.1); // Tolerance for landing if falling fast // One-way platform collision conditions: if (mario.vy >= 0 && // Moving down or stationary mario.previousY <= platform.top && // Previous bottom was at or above platform's top mario.y >= platform.top && // Current bottom is at or below platform's top mario.y <= platform.top + landingTolerance && // Current bottom is not too far below (passed through) marioRight > platform.left && // Horizontal overlap marioLeft < platform.right) { mario.y = platform.top; // Snap to platform top mario.vy = 0; mario.onGround = true; landedOnPlatformThisFrame = true; // Handle crouch state on landing if (mario.isBig && mario.jumpStartedCrouched) { mario.jumpStartedCrouched = false; if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;} else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; } } break; // Stop checking other platforms for this frame } } // --- Viewport Floor Collision --- const worldFloor = (window.innerHeight / SCALE_FACTOR); // Viewport bottom in physics units if (!landedOnPlatformThisFrame && mario.y >= worldFloor) { mario.y = worldFloor; mario.vy = 0; mario.onGround = true; // Handle crouch state on landing if (mario.isBig && mario.jumpStartedCrouched) { mario.jumpStartedCrouched = false; if (onDown) { mario.isCrouching = true; mario.currentHitboxHeight = mario.crouchingHitboxHeight;} else { mario.isCrouching = false; mario.currentHitboxHeight = mario.standingBaseHeight; } } } // --- Skid on Landing Logic --- // If just landed (onGround is true now) and wasn't already skidding from input, // and horizontal velocity is opposite to facing direction. if (mario.onGround && !mario.isSkidding && mario.vx !== 0 && ((mario.facingRight && mario.vx < 0) || (!mario.facingRight && mario.vx > 0)) ) { mario.isSkidding = true; } // --- Viewport Edge Collisions (Ceiling, Left/Right Walls) --- const worldRightEdge = (window.innerWidth / SCALE_FACTOR); const worldLeftEdge = 0; const worldCeiling = 0; // Viewport top in physics units // Ceiling collision (uses current hitbox height) if (mario.y - mario.currentHitboxHeight < worldCeiling) { mario.y = worldCeiling + mario.currentHitboxHeight; // Correct position if (mario.vy < 0) { // If was moving up mario.vy = 0; // Bonk playSound('bump'); } } // Side wall collisions if (mario.x - (mario.baseWidth / 2) < worldLeftEdge) { mario.x = worldLeftEdge + (mario.baseWidth / 2); mario.vx = 0; } if (mario.x + (mario.baseWidth / 2) > worldRightEdge) { mario.x = worldRightEdge - (mario.baseWidth / 2); mario.vx = 0; } // --- Animation State Logic --- // SMBDIS.ASM: PlayerAnimTimer = $0781, PlayerAnimCtrl = $070d if (mario.isCrouching || mario.jumpStartedCrouched) { mario.currentSpriteKey = 'crouch'; } else if (!mario.onGround) { mario.currentSpriteKey = 'jump'; } else if (mario.isSkidding) { mario.currentSpriteKey = 'skid'; } else if (mario.vx !== 0) { // Walking or Running mario.currentSpriteKey = 'walk'; mario.animationTimer--; if (mario.animationTimer <= 0) { mario.animationFrameIndex = (mario.animationFrameIndex + 1) % 3; // Cycle Walk1, Walk2, Walk3 (0, 1, 2) const absVx = Math.abs(mario.vx); // Set animation frame duration based on current speed if (absVx >= PHYSICS.maxRunSpeed * 0.85) { // Approx max run speed mario.animationTimer = ANIM_DURATIONS.run_max; } else if (absVx > PHYSICS.maxWalkSpeed * 0.90) { // Running or fast walk (threshold between walk and run_accel) mario.animationTimer = ANIM_DURATIONS.run_accel; } else if (absVx > PHYSICS.maxWalkSpeed * 0.50) { // Medium to max walk speed mario.animationTimer = ANIM_DURATIONS.walk_max; } else if (absVx > 0.01) { // Slow walk / just starting mario.animationTimer = ANIM_DURATIONS.walk_accel; } // If vx becomes 0, next state will be 'standing', resetting animTimer explicitly isn't critical here } } else { // Idle mario.currentSpriteKey = 'standing'; mario.animationFrameIndex = 0; // Reset walk frame index when idle } } /** * Renders Mario on the screen based on his current state. */ function render() { if (!mario.el) return; // Div visual height is always Big Mario's standing height const renderDivHeight = mario.visualHeight * SCALE_FACTOR; const renderDivWidth = mario.baseWidth * SCALE_FACTOR; // Calculate CSS top-left from Mario's physics bottom-center coordinates const renderX = (mario.x - mario.baseWidth / 2) * SCALE_FACTOR; // Mario's y is bottom, visualHeight is the full height of the sprite/div const renderY = (mario.y - mario.visualHeight) * SCALE_FACTOR; mario.el.style.left = renderX + 'px'; mario.el.style.top = renderY + 'px'; mario.el.style.width = renderDivWidth + 'px'; mario.el.style.height = renderDivHeight + 'px'; // Flip sprite based on direction, and include translateZ for layer promotion const scaleXValue = mario.facingRight ? '1' : '-1'; mario.el.style.transform = `scaleX(${scaleXValue}) translateZ(0px)`; // Select current sprite image let currentSpriteImage = SPRITES.standing; // Default if (mario.currentSpriteKey === 'walk') { currentSpriteImage = SPRITES.walk[mario.animationFrameIndex]; } else if (SPRITES[mario.currentSpriteKey]) { // For single-frame states like jump, crouch, skid currentSpriteImage = SPRITES[mario.currentSpriteKey]; } mario.el.style.backgroundImage = currentSpriteImage; // Each sprite is 16x32. The div will be sized to show this. // '100% 100%' makes the sprite image fill the div. mario.el.style.backgroundSize = '100% 100%'; } init(); // Start Page Mario! })();