fix: correct Forza gear mapping - 0=N, 1-10=gears, 11=R, 21=P; update gear indicator and composite dashboard

This commit is contained in:
2026-07-25 16:38:19 +08:00
parent 09ac4f8e11
commit 7226d58a7a
4 changed files with 193 additions and 142 deletions
+1 -1
View File
@@ -223,7 +223,7 @@ let GAME_ID='';
speedU.textContent='KM/H';
// Gear
const gv=gear===0?'R':String(gear);
const gv=gear===0?'N':gear===11?'R':gear===21?'P':String(gear);
if(gv!==gearV.textContent){
gearV.textContent=gv;
gearBox.style.animation='none';gearBox.offsetHeight;gearBox.style.animation='';
+86 -137
View File
@@ -15,28 +15,27 @@ html,body{width:100%;height:100%;overflow:hidden;background:#000;
#box.fill{width:100vw;height:100vh}
#box.center{width:auto;height:auto}
#content{width:100%;height:100%;display:flex;align-items:center;justify-content:center;
background:linear-gradient(135deg,#0a0a14,#1a1a30);flex-direction:column;gap:min(3vh,20px)}
.gear-circle{position:relative;width:min(30vmin,130px);height:min(30vmin,130px);
border-radius:50%;background:rgba(52,130,255,.1);border:3px solid rgba(52,130,255,.35);
background:linear-gradient(135deg,#0a0a14,#1a1a30);flex-direction:column;gap:min(2.5vh,16px)}
.gear-circle{position:relative;width:min(28vmin,120px);height:min(28vmin,120px);
border-radius:50%;background:rgba(52,130,255,.08);border:3px solid rgba(52,130,255,.3);
display:flex;align-items:center;justify-content:center;
box-shadow:0 0 40px rgba(52,130,255,.12);transition:border-color .2s,border-width .2s}
.gear-circle span{font-size:min(14vmin,56px);font-weight:900;color:#fff;transition:.12s ease}
.gear-circle.active{border-color:rgba(52,130,255,.7);border-width:4px;
box-shadow:0 0 50px rgba(52,130,255,.25)}
.gear-circle.r-active{border-color:rgba(233,70,52,.6);box-shadow:0 0 50px rgba(233,70,52,.25)}
.gear-circle.n-active{border-color:rgba(255,255,255,.3);box-shadow:0 0 40px rgba(255,255,255,.10)}
.bar-row{display:flex;gap:min(1.5vw,8px);align-items:flex-end;padding:0 10px}
.bar-col{display:flex;flex-direction:column;align-items:center;gap:4px;min-width:min(8vw,36px)}
.bar-label{font-size:min(2.5vmin,13px);font-weight:600;color:rgba(255,255,255,.25);
transition:color .2s}
.bar-fill{width:min(6vw,28px);height:min(6vw,28px);border-radius:6px;background:rgba(255,255,255,.06);
transition:background .2s,transform .15s}
.bar-col.active .bar-fill{background:var(--accent,#3482FF);transform:scaleY(1.2)}
box-shadow:0 0 40px rgba(52,130,255,.1);transition:border-color .2s,border-width .2s}
.gear-circle span{font-size:min(13vmin,50px);font-weight:900;color:#fff;transition:.12s ease}
.gear-circle.r-active{border-color:rgba(233,70,52,.55);box-shadow:0 0 50px rgba(233,70,52,.2)}
.gear-circle.n-active{border-color:rgba(255,255,255,.25)}
.gear-circle.p-active{border-color:rgba(52,199,89,.3)}
.bar-row{display:flex;gap:min(1.2vw,6px);align-items:flex-end;padding:0 6px;flex-wrap:wrap;justify-content:center}
.bar-col{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:min(7vw,32px)}
.bar-label{font-size:min(2.2vmin,12px);font-weight:600;color:rgba(255,255,255,.2);transition:color .2s}
.bar-fill{width:min(5vw,24px);height:min(5vw,24px);border-radius:5px;background:rgba(255,255,255,.05);transition:background .2s,transform .15s}
.bar-col.active .bar-fill{background:var(--accent,#3482FF);transform:scaleY(1.25)}
.bar-col.active .bar-label{color:#fff}
.bar-col.r .bar-fill{background:rgba(233,70,52,.3)}
.bar-col.r .bar-fill{background:rgba(233,70,52,.25)}
.bar-col.r.active .bar-fill{background:#E94634}
.bar-col.n .bar-fill{background:rgba(255,255,255,.1)}
.bar-col.n.active .bar-fill{background:rgba(255,255,255,.25)}
.bar-col.n .bar-fill{background:rgba(255,255,255,.08)}
.bar-col.n.active .bar-fill{background:rgba(255,255,255,.2)}
.bar-col.p .bar-fill{background:rgba(52,199,89,.15)}
.bar-col.p.active .bar-fill{background:rgba(52,199,89,.5)}
</style>
</head>
<body>
@@ -45,142 +44,92 @@ html,body{width:100%;height:100%;overflow:hidden;background:#000;
<div class="bar-row" id="bar-row"></div>
</div></div></div>
<script>
const M={aspect_ratio:"1:1",render_mode:"contain",gear_style:"auto"};
const M={aspect_ratio:"1:1",render_mode:"contain"};
let GAME_ID='';
// Forza: 0=R, 1-8=1-8 (no neutral in UDP)
// AC/F1: -1=R, 0=N, 1-8=1-8
// iRacing: 0=N, 1=R, 2=1st... (offset +2)
function gearLabel(g){
if(!g&&g!==0)return'-';
const style=M.gear_style||'auto';
const isForza=GAME_ID&&(GAME_ID.includes('forza'));
const isIRacing=GAME_ID&&GAME_ID.includes('iracing');
const AC_STYLE_LABELS=['R','N','1','2','3','4','5','6','7','8'];
const FORZA_LABELS=['N','1','2','3','4','5','6','7','8','9','10','R','P'];
if(style==='forza'||isForza){
if(g===0)return'R';
if(g>=1&&g<=8)return String(g);
return String(g);
}
if(style==='ac'||(!isForza&&!isIRacing)){
if(g===-1)return'R';
if(g===0)return'N';
if(g>=1&&g<=8)return String(g);
return String(g);
}
if(isIRacing){
if(g===0)return'N';
if(g===1)return'R';
if(g>=2&&g<=9)return String(g-1);
return String(g);
}
// fallback
if(g===0)return'N';
if(g>0)return String(g);
return String(g);
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
function buildBars(){
const isForza=GAME_ID&&GAME_ID.includes('forza');
const labels=isForza?FORZA_LABELS:AC_STYLE_LABELS;
barRow.innerHTML=labels.map((l,i)=>{
let cls='bar-col';
if(l==='R')cls+=' r';
if(l==='N')cls+=' n';
if(l==='P')cls+=' p';
return `<div class="${cls} g-${i}"><div class="bar-fill"></div><div class="bar-label">${l}</div></div>`;
}).join('');
}
function gearBarIndex(g){
if(!g&&g!==0)return-1;
// Forza: 0=N, 1-10=1-10, 11=R, 21=P
// AC/F1: -1=R, 0=N, 1-8=1-8
// iRacing: 0=N, 1=R, 2=1, 3=2...
function gearInfo(g){
const isForza=GAME_ID&&GAME_ID.includes('forza');
const isIRacing=GAME_ID&&GAME_ID.includes('iracing');
const isIracing=GAME_ID&&GAME_ID.includes('iracing');
if(isForza){
if(g===0)return 0; // R at index 0
if(g>=1&&g<=8)return g+1; // 1->2
if(g===0)return{label:'N',cls:'n',idx:0};
if(g===11)return{label:'R',cls:'r',idx:11};
if(g===21)return{label:'P',cls:'p',idx:12};
if(g>=1&&g<=10)return{label:String(g),cls:'',idx:g};
return{label:String(g),cls:'',idx:-1};
}
if(isIRacing){
if(g===0)return 1; // N at index 1
if(g===1)return 0; // R at index 0
if(g>=2&&g<=9)return g; // 2->2(index for '1')
if(isIracing){
if(g===0)return{label:'N',cls:'n',idx:0};
if(g===1)return{label:'R',cls:'r',idx:1};
if(g>=2&&g<=9)return{label:String(g-1),cls:'',idx:g};
return{label:String(g),cls:'',idx:-1};
}
// AC/F1/default
if(g===-1)return 0; // R
if(g===0)return 1; // N
if(g>=1&&g<=8)return g+1; // 1->2
return -1;
// default AC/F1 style
if(g===-1)return{label:'R',cls:'r',idx:0};
if(g===0)return{label:'N',cls:'n',idx:1};
if(g>=1&&g<=8)return{label:String(g),cls:'',idx:g+1};
return{label:String(g),cls:'',idx:-1};
}
(function(){
const box=document.getElementById('box'),gearTxt=document.getElementById('gear-txt'),
gearEl=document.getElementById('gear-el'),barRow=document.getElementById('bar-row');
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
let ws,rt,lastGear=-999;
const LABELS=['R','N','1','2','3','4','5','6','7','8'];
let lastGear=-999;
function updateGear(g){
if(g===lastGear)return;lastGear=g;
const info=gearInfo(g);
gearTxt.textContent=info.label;
gearEl.className='gear-circle active';
if(info.cls==='r')gearEl.classList.add('r-active');
else if(info.cls==='n')gearEl.classList.add('n-active');
else if(info.cls==='p')gearEl.classList.add('p-active');
barRow.querySelectorAll('.bar-col').forEach((el,i)=>{el.classList.toggle('active',i===info.idx)});
}
function parseRatio(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h;}return null;}
function buildBars(){
barRow.innerHTML=LABELS.map((l,i)=>{
let cls='bar-col';
if(l==='R')cls+=' r';
if(l==='N')cls+=' n';
cls+=` g-${i}`;
return `<div class="${cls}"><div class="bar-fill"></div><div class="bar-label">${l}</div></div>`;
}).join('');
function apply(){
const ratio=parseRatio(M.aspect_ratio||'1:1'),mode=M.render_mode||'contain';
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
switch(mode){
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
case'center':cw=Math.min(vw,800);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,800);cw=ch*ratio};box.className='center';break;
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
}
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
}
// gear=-1 -> R, gear=0 -> N, gear=1-8 -> 1-8 (AC/F1 style)
// gear=0 -> R, gear=1-8 -> 1-8 (Forza style, no N)
function gearToIndex(g){
const style=M.gear_style||'auto';
if(style==='ac')return g+2; // -1->1(R), 0->2(N), 1->3(1), ...
if(style==='forza')return g===0?0:g+1; // 0->0(R), 1->2(1), 2->3(2) ...
// auto: if gear can be -1, use AC style; otherwise Forza
if(g===-1)return 1; // R (AC style)
if(g===0)return 0; // R (Forza style, since no AC games have been seen)
if(g>0)return g+1; // 1->2, 2->3 ... wait this is wrong
// Better auto logic:
// Forza: 0=R(0), 1=1(2), 2=2(3)... mapping: g==0?0:g+1
// AC: -1=R(0), 0=N(1), 1=1(2)... mapping: g+1 (but then -1 maps to 0=R ✓)
// Actually: AC: -1->0(R), 0->1(N), 1->2(1st)... So: g+1 maps to index
// Forza: 0->0(R), 1->2(1st), 2->3(2nd)... So: g==0?0:g+1
// Auto-detect: if we've seen negative gear, use AC mapping
// Since auto mode doesn't track history, default to Forza mapping
// But if game changes, the server might send negative gear
return g===0?0:g+1;
}
function updateGear(g){
if(g===lastGear)return;lastGear=g;
const label=gearLabel(g);
const activeIndex=gearBarIndex(g);
gearTxt.textContent=label;
gearEl.className='gear-circle active';
if(label==='R')gearEl.classList.add('r-active');
else if(label==='N')gearEl.classList.add('n-active');
barRow.querySelectorAll('.bar-col').forEach((el,i)=>{
el.classList.toggle('active',i===activeIndex);
});
}
function apply(){
const ratio=parseRatio(M.aspect_ratio||'1:1'),mode=M.render_mode||'contain';
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return;}
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
switch(mode){
case'cover':if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover';break;
case'fill':box.style.width='100vw';box.style.height='100vh';box.className='fill';return;
case'center':cw=Math.min(vw,800);ch=cw/ratio;if(ch>vh){ch=Math.min(vh,800);cw=ch*ratio};box.className='center';break;
default:if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio};box.className='contain';
}
if(mode!=='fill'){box.style.width=cw+'px';box.style.height=ch+'px';}
}
function connect(){
function connect(){
ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);
if(m.type==='connected'){GAME_ID=m.selected_game_id||'';console.log('[Gear] game:',GAME_ID)}
if(m.type==='telemetry')updateGear(m.data.gear||0)}catch(e){}};
ws.onclose=()=>{rt=setTimeout(connect,2000)};
}
if(m.type==='connected'){GAME_ID=m.selected_game_id||'';buildBars()}
if(m.type==='telemetry')updateGear(m.data.gear||0)
}catch(e){}};
ws.onclose=()=>{rt=setTimeout(connect,2000)};
}
const box=document.getElementById('box'),gearTxt=document.getElementById('gear-txt'),
gearEl=document.getElementById('gear-el'),barRow=document.getElementById('bar-row');
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
let ws,rt;
buildBars();apply();window.addEventListener('resize',apply);connect();
})();
buildBars();apply();window.addEventListener('resize',apply);connect();
</script>
</body>
</html>
+3 -4
View File
@@ -2,14 +2,13 @@
"id": "gear_indicator",
"name": "档位指示器",
"category": "basic",
"description": "自适应档位显示,支持R/N/1-8 高亮提示",
"description": "自适应档位显示,支持P/R/N/D/1-10,根据游戏自动匹配编码",
"author": "Yei.J. (AskaEth)",
"version": "1.0.0",
"version": "1.1.0",
"supported_games": "all",
"config": {
"icon": "⚙️",
"aspect_ratio": "1:1",
"render_mode": "contain",
"gear_style": "auto"
"render_mode": "contain"
}
}