Nitro Type - Racing Mini Map

Displays a mini map of the Race Track.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Nitro Type - Racing Mini Map
// @version      0.1.0
// @description  Displays a mini map of the Race Track.
// @author       Toonidy
// @match        *://*.nitrotype.com/race
// @match        *://*.nitrotype.com/race/*
// @icon         https://i.ibb.co/YRs06pc/toonidy-userscript.png
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/pixi.js/6.5.4/browser/pixi.min.js
// @require      https://greasyfork.org/scripts/443718-nitro-type-userscript-utils/code/Nitro%20Type%20Userscript%20Utils.js?version=1042360
// @license      MIT
// @namespace    https://greasyfork.org/users/858426
// ==/UserScript==
 
/* globals PIXI findReact createLogger */
 
const config = {
     colors: {
        me: 0xf2887f,
        opponentPlayer: 0x5097e3,
        opponentBot: 0xbbbbbb,
        opponentWampus: 0xbc530d,
        nitro: 0xef9e18,
        background: 0x20222e,
        raceLane: 0x555555,
        startLine: 0x929292,
        finishLine: 0x929292,
    },
    trackLocally: true, // whether to track your player's position without server data
    moveDestination: {
        enabled: true,
        alpha: 0.5, // range 0.0 - 1.0 (the lower the value, the more transparent)
    },
}
 
const logging = createLogger("Nitro Type Racing Mini Map"),
      raceContainer = document.querySelector("#raceContainer"),
      raceObj = raceContainer ? findReact(raceContainer) : null
if (!raceContainer || !raceObj) {
    logging.error("Init")("Could not find the race track")
    return
}
if (!raceObj.props.user.loggedIn) {
    logging.error("Init")("This userscript is not available for Guest Racing")
    return
}
 
PIXI.utils.skipHello()
 
const style = document.createElement("style")
style.appendChild(
        document.createTextNode(`
.nt-racing-mini-map-root canvas {
    display: block;
}`))
document.head.appendChild(style)
 
const racingMiniMap = new PIXI.Application({ width: 1024, height: 100, backgroundColor: config.colors.background }),
      container = document.createElement("div");
 
container.className = "nt-racing-mini-map-root"
 
///////////////////////
//  Prepare Objects  //
///////////////////////
 
const RACER_WIDTH = 28,
      CROSSING_LINE_WIDTH = 32,
      PADDING = 2,
      racers = Array(5).fill(null),
      server = raceObj.server,
      currentUserID = raceObj.props.user.userID
 
// Draw mini racetrack
const raceTrackBG = new PIXI.TilingSprite.from("/dist/site/images/backgrounds/bg-noise.png", { width: racingMiniMap.renderer.width, height: racingMiniMap.renderer.height} ),
      startLine = PIXI.Sprite.from(PIXI.Texture.WHITE),
      finishLine = PIXI.Sprite.from(PIXI.Texture.WHITE)
 
startLine.x = CROSSING_LINE_WIDTH
startLine.y = 0
startLine.width = 1
startLine.height = racingMiniMap.renderer.height
startLine.tint = config.colors.startLine
 
finishLine.x = racingMiniMap.renderer.width - CROSSING_LINE_WIDTH - 1
finishLine.y = 0
finishLine.width = 1
finishLine.height = racingMiniMap.renderer.height
finishLine.tint = config.colors.finishLine
 
raceTrackBG.addChild(startLine, finishLine)
 
for (let i = 1; i < 5; i++) {
    const lane = PIXI.Sprite.from(PIXI.Texture.WHITE)
    lane.x = 0
    lane.y = i * (racingMiniMap.renderer.height / 5)
    lane.width = racingMiniMap.renderer.width
    lane.height = 1
    lane.tint = config.colors.raceLane
    raceTrackBG.addChild(lane)
}
 
racingMiniMap.stage.addChild(raceTrackBG)
 
/* Mini Map movement animation update. */
function animateRacerTicker() {
    const r = this
    const lapse = Date.now() - r.lastUpdated
    if (r.sprite.x < r.toX) {
        const distance = r.toX - r.fromX
        r.sprite.x = r.fromX + Math.min(distance, distance * (lapse / r.moveMS))
        if (r.ghostSprite && r.sprite.x === r.ghostSprite.x) {
            r.ghostSprite.renderable = false
        }
    }
    if (r.skipped > 0) {
        const nitroTargetWidth = r.nitroToX - r.nitroFromX
        if (r.nitroSprite.width < nitroTargetWidth) {
            r.nitroSprite.width = Math.min(nitroTargetWidth, r.sprite.x - r.nitroFromX)
        } else if (r.nitroSprite.width === nitroTargetWidth && r.nitroSprite.alpha > 0 && !r.nitroDisableFade) {
            if (r.nitroSprite.alpha === 1) {
                r.nitroStartFadeStamp = Date.now() - 1
            }
            r.nitroSprite.alpha = Math.max(0, 1 - ((Date.now() - r.nitroStartFadeStamp) / 1e3))
        }
    }
    if (r.completeStamp !== null && r.sprite.x === r.toX && r.nitroSprite.alpha === 0) {
        racingMiniMap.ticker.remove(animateRacerTicker, this)
    }
}
 
/* Handle adding in players on the mini map. */
server.on("joined", (e) => {
    const { lane, userID } = e
 
    let color = config.colors.opponentBot
    if (userID === currentUserID) {
        color = config.colors.me
    } else if (!e.robot) {
        color = config.colors.opponentPlayer
    } else if (e.profile.specialRobot === "wampus") {
        color = config.colors.opponentWampus
    }
 
    if (racers[lane]) {
        racers[lane].ghostSprite.tint = color
        racers[lane].sprite.tint = color
        racers[lane].sprite.x = 0 - RACER_WIDTH + PADDING
        racers[lane].lastUpdated = Date.now()
        racers[lane].fromX = racers[lane].sprite.x
        racers[lane].toX = PADDING
        racers[lane].sprite.renderable = true
        return
    }
 
    const r = PIXI.Sprite.from(PIXI.Texture.WHITE)
    r.x = 0 - RACER_WIDTH + PADDING
    r.y = PADDING + (lane > 0 ? 1 : 0) + (lane * (racingMiniMap.renderer.height / 5))
    r.tint = color
    r.width = RACER_WIDTH
    r.height = 16 - (lane > 0 ? 1 : 0)
 
    const n = PIXI.Sprite.from(PIXI.Texture.WHITE)
    n.y = r.y + ((16 - (lane > 0 ? 1 : 0)) / 2) - 1
    n.renderable = false
    n.tint = config.colors.nitro
    n.width = 1
    n.height = 2
 
    racers[lane] = {
        lane,
        sprite: r,
        userID: userID,
        ghostSprite: null,
        nitroSprite: n,
        lastUpdated: Date.now(),
        fromX: r.x,
        toX: PADDING,
        skipped: 0,
        nitroStartFadeStamp: null,
        nitroFromX: null,
        nitroToX: null,
        nitroDisableFade: false,
        moveMS: 500,
        completeStamp: null,
    }
 
    if (config.moveDestination.enabled) {
        const g = PIXI.Sprite.from(PIXI.Texture.WHITE)
        g.x = PADDING
        g.y = PADDING + (lane > 0 ? 1 : 0) + (lane * (racingMiniMap.renderer.height / 5))
        g.tint = color
        g.alpha = config.moveDestination.alpha
        g.width = RACER_WIDTH
        g.height = 16 - (lane > 0 ? 1 : 0)
        g.renderable = false
 
        racers[lane].ghostSprite = g
        racingMiniMap.stage.addChild(g)
    }
 
    racingMiniMap.stage.addChild(n)
    racingMiniMap.stage.addChild(r)
 
    racingMiniMap.ticker.add(animateRacerTicker, racers[lane])
})
 
/* Handle any players leaving the race track. */
server.on("left", (e) => {
    const lane = racers.findIndex((r) => r?.userID === e)
    if (racers[lane]) {
        racers[lane].sprite.renderable = false
        racers[lane].ghostSprite.renderable = false
        racers[lane].nitroSprite.renderable = false
    }
})
 
/* Handle race map progress position updates. */
server.on("update", (e) => {
    let moveFinishMS = 500
 
    const payloadUpdateRacers = e.racers.slice().sort((a, b) => {
        if (a.progress.completeStamp === b.progress.completeStamp) {
            return 0
        }
        if (a.progress.completeStamp === null) {
            return 1
        }
        return a.progress.completeStamp > 0 && b.progress.completeStamp > 0 && a.progress.completeStamp > b.progress.completeStamp ? 1 : -1
    })
 
    for (let i = 0; i < payloadUpdateRacers.length; i++) {
        const r = payloadUpdateRacers[i],
              { completeStamp, skipped } = r.progress,
              racerObj = racers[r.lane]
        if (!racerObj || racerObj.completeStamp > 0 || (r.userID === currentUserID && completeStamp <= 0 && config.trackLocally)) {
            continue
        }
 
        if (r.disqualified) {
            racingMiniMap.ticker.remove(animateRacerTicker, racerObj)
            racingMiniMap.stage.removeChild(racerObj.sprite, racerObj.nitroSprite)
            if (racerObj.ghostSprite) {
                racingMiniMap.stage.removeChild(racerObj.ghostSprite)
            }
            racerObj.sprite.destroy()
            racerObj.ghostSprite.destroy()
            racerObj.nitroSprite.destroy()
 
            racers[r.lane] = null
            continue
        }
 
        racerObj.lastUpdated = Date.now()
        racerObj.fromX = racerObj.sprite.x
 
        if (racerObj.completeStamp === null && completeStamp > 0) {
            racerObj.completeStamp = completeStamp
            racerObj.toX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
            racerObj.moveMS = moveFinishMS
 
            if (racerObj.nitroDisableFade) {
                racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
                racerObj.nitroDisableFade = false
            }
        } else {
            racerObj.moveMS = 1e3
            racerObj.toX = r.progress.percentageFinished * (racingMiniMap.renderer.width - RACER_WIDTH - CROSSING_LINE_WIDTH - PADDING - 1)
            racerObj.sprite.x = racerObj.fromX
        }
 
        if (racerObj.ghostSprite) {
            racerObj.ghostSprite.x = racerObj.toX
            racerObj.ghostSprite.renderable = true
        }
 
        if (skipped !== racerObj.skipped) {
            if (racerObj.skipped === 0) {
                racerObj.nitroFromX = racerObj.fromX
                racerObj.nitroSprite.x = racerObj.fromX
                racerObj.nitroSprite.renderable = true
            }
            racerObj.skipped = skipped // because infinite nitros exist? :/
            racerObj.nitroToX = racerObj.toX
            racerObj.nitroSprite.alpha = 1
            if (racerObj.completeStamp !== null) {
                racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
            }
        }
 
        if (completeStamp > 0 && i + 1 < payloadUpdateRacers.length) {
            const nextRacer = payloadUpdateRacers[i + 1],
                  nextRacerObj = racers[nextRacer?.lane]
            if (nextRacerObj && nextRacerObj.completeStamp === null && nextRacer.progress.completeStamp > 0 && nextRacer.progress.completeStamp > completeStamp) {
                moveFinishMS += 250
            }
        }
    }
})
 
if (config.trackLocally) {
    let lessonLength = 0
    server.on("status", (e) => {
        if (e.status === "countdown") {
            lessonLength = e.lessonLength
        }
    })
 
    const originalSendPlayerUpdate = server.sendPlayerUpdate
    server.sendPlayerUpdate = (data) => {
        originalSendPlayerUpdate(data)
        const racerObj = racers.find((r) => r?.userID === currentUserID)
        if (!racerObj) {
            return
        }
 
        const percentageFinished = (data.t / (lessonLength || 1))
        racerObj.lastUpdated = Date.now()
        racerObj.fromX = racerObj.sprite.x
        racerObj.moveMS = 500
        racerObj.toX = percentageFinished * (racingMiniMap.renderer.width - RACER_WIDTH - CROSSING_LINE_WIDTH - PADDING - 1)
        racerObj.sprite.x = racerObj.fromX
 
        if (racerObj.ghostSprite) {
            racerObj.ghostSprite.x = racerObj.toX
            racerObj.ghostSprite.renderable = true
        }
 
        if (data.s) {
            if (racerObj.skipped === 0) {
                racerObj.nitroFromX = racerObj.fromX
                racerObj.nitroSprite.x = racerObj.fromX
                racerObj.nitroSprite.renderable = true
            }
            racerObj.skipped = data.s // because infinite nitros exist? but I'm not going to test that! :/
            racerObj.nitroToX = racerObj.toX
            racerObj.nitroSprite.alpha = 1
            racerObj.nitroDisableFade = percentageFinished === 1
 
            if (racerObj.completeStamp !== null) {
                racerObj.nitroToX = racingMiniMap.renderer.width - RACER_WIDTH - PADDING
            }
        }
    }
}
 
/////////////
//  Final  //
/////////////
 
container.append(racingMiniMap.view)
raceContainer.before(container)