diff --git a/js/fuck-jr-central-oshi-tabi-speedtest/README.md b/js/fuck-jr-central-oshi-tabi-speedtest/README.md
new file mode 100644
index 0000000..b6a93ec
--- /dev/null
+++ b/js/fuck-jr-central-oshi-tabi-speedtest/README.md
@@ -0,0 +1,62 @@
+# fuck-jr-central-oshi-tabi-speedtest 去他妈的JR东海/东日本 "推し旅" 测速
+
+> 本项目修改自 [kiritoxkiriko/fuck-jr-central-oshi-tabi-speedtest](https://github.com/kiritoxkiriko/fuck-jr-central-oshi-tabi-speedtest)
+
+## 简介
+众所周知,JR 东海/东日本经常推出和热门 IP 联动的 "推し旅" 活动。
+这些活动往往需要乘坐新干线进行测速来领取特典,比如 [JR東海×「BanG Dream! 10th ANNIVERSARY」](https://recommend.jr-central.co.jp/oshi-tabi/bang-dream-10th/)、
+[ブルーアーカイブ × JR東海 推し旅 名古屋](https://oshi-tabi.voistock.com/2605bluearchive/)。
+本脚本用来快速绕过这个测速。
+
+
+
+
+
+## 原理
+活动页通过浏览器的 `navigator.geolocation` API 获取你的**速度**和**位置**,并校验位置是否落在一组预设的矩形区域(`area`)内。
+原项目针对的 `recommend.jr-central.co.jp` 旧活动是在**前端**直接判定的,所以只要 hook `navigator.geolocation.watchPosition`,
+让它返回一个**固定速度**和一个**范围内随机经纬度**即可绕过。
+(`oshi-tabi.voistock.com` 的活动则把采样点上传到**服务端**判定,详见下方「服务端测速」一节;但数据源同样是 `navigator.geolocation`,所以 hook 的思路通用。)
+
+不过 voistock 的 ブルアカ × 名古屋 活动除了速度,还会判定**方向**:
+它会比较连续位置点相对目标车站(名古屋 `[35.17099309, 136.8815566]`)的运动方向,
+入口 `名古屋行き` 要求逐步**靠近**名古屋(`towards`),`名古屋帰り` 要求逐步**远离**(`away`)。
+旧脚本返回的“东京矩形内随机点”没有稳定路线,连续采样的方向来回跳变,会触发:
+
+```
+進行方向が逆(反対方向)です。正しい方向の新幹線で再度お試しください。
+```
+
+因此本脚本改为**沿东海道新干线真实途经点的折线轨迹模拟移动**:
+
+- 内置东京→名古屋的途经点折线,按真实时间推进位置(`已行驶距离 = 经过秒数 × 速度`),
+ 保证上报的 `speed` 字段与相邻采样点算出的“距离/时间”速度自洽,且落在活动页要求的 `150–360 km/h` 区间内。
+- 根据相邻点的方位角填入 `heading`(不再是 `null`)。
+- 同时覆写 `watchPosition`、`getCurrentPosition`、`clearWatch`,覆盖页面可能用到的多种采样方式。
+- 右下角提供方向选择悬浮面板(`名古屋行き` / `名古屋帰り`),切换即从对应起点重新出发;
+ `towards` 时到名古屋距离单调递减,`away` 时单调递增。
+
+可调参数集中在脚本顶部(`SPEED_KMH`、`SAMPLE_INTERVAL_MS`、途经点 `ROUTE_TO_NAGOYA`、`ENABLE_NIGHT_BYPASS` 等),
+不同活动页如有差异可自行调整。
+
+## 服务端测速与接口营业时间
+`oshi-tabi.voistock.com` 上的 ブルアカ × 名古屋 活动(第 1、2 弾共用同一套 `scrt_measure.js`)的测速判定在**服务端**:
+浏览器只负责用 `navigator.geolocation` 采集 GPS 点、成批 `POST` 到 `/api/measure/start`、`/api/measure/point(s)`,
+最终由服务器返回通过/失败。
+(这跟原项目针对的 `recommend.jr-central.co.jp` 旧活动不是同一套实现,后者据原项目描述为纯前端判定。)
+
+好消息是 - 服务器仍然是**按你上传的 GPS 轨迹**判定的,并不需要真实乘车凭证或额外认证,
+所以本脚本的轨迹模拟**同样有效**,方向选对即可通过。
+
+需要注意的一个坑是 **接口营业时间**:测速 API 在**深夜会关闭**,此时请求会直接返回
+
+```json
+{ "status": "unavailable", "message": "このAPIは深夜の間はご利用いただけません。", "available_from": "05:55 JST" }
+```
+
+控制台对应 `[MeasureClient] Error: 503 error`。**这跟脚本无关**,是接口本身按时间关闭。处理方式:
+
+- 正常情况下,**等日本时间 05:55 之后再测**即可。
+- 夜间调试时,脚本顶部的 `ENABLE_NIGHT_BYPASS`(默认开启)会在 `document-start` 阶段写入
+ `localStorage.devMode = '1'`,让页面启用其内置的夜间测试 token 以绕开营业时间限制。
+ 这只绕开“营业时间”,token 由 JR 维护、随时可能失效;不需要时把它设为 `false` 即可。
diff --git a/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js b/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js
new file mode 100644
index 0000000..d4d0aa5
--- /dev/null
+++ b/js/fuck-jr-central-oshi-tabi-speedtest/fuck-jr-central-oshi-tabi-speedtest.js
@@ -0,0 +1,218 @@
+// ==UserScript==
+// @name fuck-jr-central-oshi-tabi-speedtest
+// @namespace https://recommend.jr-central.co.jp/oshi-tabi/
+// @version 1.0
+// @description 去他妈的JR东海/东日本 推し旅 活动测速(沿新干线轨迹模拟,支持方向判定)
+// @author Stardream
+// @match https://oshi-tabi.voistock.com/*
+// @match https://recommend.jr-central.co.jp/oshi-tabi/*
+// @grant none
+// @run-at document-start
+// @license MIT
+// ==/UserScript==
+
+(function () {
+ 'use strict';
+
+ // ===== 夜间测试开关 =====
+ // 测速接口在深夜会关闭(503 / available_from 05:55 JST)。
+ // 页面内联脚本会在 devMode 下设置 window.NIGHT_BYPASS_TOKEN 以绕开夜间窗口。
+ // 本脚本 @run-at document-start 先于该内联脚本执行,所以这里提前置位即可生效。
+ // 注意:这只是绕开“营业时间”,且 token 由 JR 维护,随时可能失效;白天测试无需此项。
+ const ENABLE_NIGHT_BYPASS = true;
+ if (ENABLE_NIGHT_BYPASS) {
+ try {
+ if (localStorage.getItem('devMode') !== '1') {
+ localStorage.setItem('devMode', '1');
+ }
+ } catch (e) { /* localStorage 不可用时忽略 */ }
+ }
+
+ // ===== 可配置项 =====
+ // 模拟速度,单位 km/h(活动页要求落在 150-360 之间)
+ const SPEED_KMH = 250;
+ // 成功回调的采样间隔(毫秒)
+ const SAMPLE_INTERVAL_MS = 1000;
+ // GPS 精度(米)
+ const ACCURACY_M = 10;
+ // 默认行进方向:'towards' = 名古屋行き(驶向名古屋),'away' = 名古屋帰り(驶离名古屋)
+ let DIRECTION = 'towards';
+
+ const SPEED_MPS = (SPEED_KMH * 1000) / 3600;
+
+ // ===== 东海道新干线 东京→名古屋 主要途经点(名古屋为终点)=====
+ // 沿真实线路排列,保证“驶向名古屋”时到名古屋的距离单调递减。
+ const ROUTE_TO_NAGOYA = [
+ { lat: 35.681236, lon: 139.767125 }, // 東京
+ { lat: 35.628471, lon: 139.738760 }, // 品川
+ { lat: 35.507871, lon: 139.617495 }, // 新横浜
+ { lat: 35.256293, lon: 139.155720 }, // 小田原
+ { lat: 35.103156, lon: 139.078000 }, // 熱海
+ { lat: 35.126363, lon: 138.911040 }, // 三島
+ { lat: 35.142395, lon: 138.663320 }, // 新富士
+ { lat: 34.971401, lon: 138.389500 }, // 静岡
+ { lat: 34.838074, lon: 138.130700 }, // 掛川付近
+ { lat: 34.703608, lon: 137.734900 }, // 浜松
+ { lat: 34.762960, lon: 137.381900 }, // 豊橋
+ { lat: 34.962145, lon: 137.081050 }, // 三河安城
+ { lat: 35.170993, lon: 136.881557 } // 名古屋(目标点)
+ ];
+
+ // ===== 几何工具 =====
+ const R = 6371000; // 地球半径(米)
+ const toRad = (d) => (d * Math.PI) / 180;
+ const toDeg = (r) => (r * 180) / Math.PI;
+
+ function haversine(a, b) {
+ const dLat = toRad(b.lat - a.lat);
+ const dLon = toRad(b.lon - a.lon);
+ const lat1 = toRad(a.lat);
+ const lat2 = toRad(b.lat);
+ const h =
+ Math.sin(dLat / 2) ** 2 +
+ Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
+ return 2 * R * Math.asin(Math.sqrt(h));
+ }
+
+ // 计算从 a 指向 b 的方位角(0-360,正北为0,顺时针)
+ function bearing(a, b) {
+ const lat1 = toRad(a.lat);
+ const lat2 = toRad(b.lat);
+ const dLon = toRad(b.lon - a.lon);
+ const y = Math.sin(dLon) * Math.cos(lat2);
+ const x =
+ Math.cos(lat1) * Math.sin(lat2) -
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
+ return (toDeg(Math.atan2(y, x)) + 360) % 360;
+ }
+
+ // 在两点之间按比例 t 线性插值(短距离下足够精确)
+ function lerp(a, b, t) {
+ return {
+ lat: a.lat + (b.lat - a.lat) * t,
+ lon: a.lon + (b.lon - a.lon) * t
+ };
+ }
+
+ // ===== 预计算某方向路线的累计距离 =====
+ function buildRoute(points) {
+ const cum = [0];
+ for (let i = 1; i < points.length; i++) {
+ cum.push(cum[i - 1] + haversine(points[i - 1], points[i]));
+ }
+ return { points, cum, total: cum[cum.length - 1] };
+ }
+
+ const routeTowards = buildRoute(ROUTE_TO_NAGOYA);
+ const routeAway = buildRoute([...ROUTE_TO_NAGOYA].reverse());
+
+ function activeRoute() {
+ return DIRECTION === 'away' ? routeAway : routeTowards;
+ }
+
+ // 给定沿路线已行驶的距离(米),返回当前坐标与航向
+ function pointAtDistance(route, dist) {
+ const { points, cum, total } = route;
+ const d = Math.max(0, Math.min(dist, total));
+ let i = 0;
+ while (i < cum.length - 2 && cum[i + 1] < d) i++;
+ const segLen = cum[i + 1] - cum[i] || 1;
+ const t = (d - cum[i]) / segLen;
+ const pos = lerp(points[i], points[i + 1], t);
+ const head = bearing(points[i], points[i + 1]);
+ return { pos, head };
+ }
+
+ // ===== 轨迹状态 =====
+ let startTime = null; // 本次测速的起始时间戳
+
+ function ensureStarted() {
+ if (startTime === null) startTime = Date.now();
+ }
+
+ function currentCoords() {
+ ensureStarted();
+ const elapsedSec = (Date.now() - startTime) / 1000;
+ const traveled = elapsedSec * SPEED_MPS;
+ const { pos, head } = pointAtDistance(activeRoute(), traveled);
+ return {
+ latitude: pos.lat,
+ longitude: pos.lon,
+ accuracy: ACCURACY_M,
+ altitude: null,
+ altitudeAccuracy: null,
+ heading: head,
+ speed: SPEED_MPS // 单位 m/s
+ };
+ }
+
+ function createPosition() {
+ return {
+ coords: currentCoords(),
+ timestamp: Date.now()
+ };
+ }
+
+ // ===== 覆写 geolocation API =====
+ const geo = navigator.geolocation;
+
+ geo.watchPosition = function (success, error, options) {
+ // 每次开始监听都重置轨迹,从线路起点出发
+ startTime = Date.now();
+ if (typeof success === 'function') success(createPosition());
+ const id = setInterval(() => {
+ if (typeof success === 'function') success(createPosition());
+ }, SAMPLE_INTERVAL_MS);
+ return id;
+ };
+
+ geo.clearWatch = function (watchId) {
+ clearInterval(watchId);
+ };
+
+ geo.getCurrentPosition = function (success, error, options) {
+ if (typeof success === 'function') success(createPosition());
+ };
+
+ // ===== 方向选择悬浮面板 =====
+ function injectPanel() {
+ if (document.getElementById('fjct-panel')) return;
+ const panel = document.createElement('div');
+ panel.id = 'fjct-panel';
+ panel.style.cssText = [
+ 'position:fixed', 'right:12px', 'bottom:12px', 'z-index:2147483647',
+ 'background:rgba(20,20,28,0.92)', 'color:#fff', 'font:12px/1.5 sans-serif',
+ 'padding:10px 12px', 'border-radius:10px', 'box-shadow:0 4px 16px rgba(0,0,0,.4)',
+ 'min-width:150px'
+ ].join(';');
+ panel.innerHTML =
+ '推し旅 测速模拟
' +
+ '' +
+ '' +
+ '';
+ const mount = () => {
+ (document.body || document.documentElement).appendChild(panel);
+ panel.querySelectorAll('input[name="fjct-dir"]').forEach((r) => {
+ r.addEventListener('change', (e) => {
+ DIRECTION = e.target.value;
+ startTime = Date.now(); // 切换方向后重新出发
+ updateStatus();
+ });
+ });
+ updateStatus();
+ };
+ if (document.body) mount();
+ else document.addEventListener('DOMContentLoaded', mount);
+ }
+
+ function updateStatus() {
+ const el = document.getElementById('fjct-status');
+ if (!el) return;
+ const dir = DIRECTION === 'away' ? '驶离名古屋' : '驶向名古屋';
+ el.textContent = `${dir} · ${SPEED_KMH}km/h`;
+ }
+
+ injectPanel();
+})();