diff --git a/README.md b/README.md index ae988e0..2d62231 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Kute is a high-performance Krunker client designed to enhance your gaming experi - [x] Autoupdater with a download progress window, plus changelogs - [x] Rebindable kute hotkeys - [x] Spotify overlay - by [@Liamoulee](https://github.com/Liamoulee) +- [x] CSS Store - by [@Liamoulee](https://github.com/Liamoulee) - [ ] BetterKDR™️ (coming soon) - [x] and more... diff --git a/server/public/index.html b/server/public/index.html index 5b232d3..b614042 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -385,6 +385,7 @@
Replaces game files with your own. The manager shows what the game loads, drop a file onto the one it replaces and it lands in the right place. CSS and other text files can be written and edited right there, and CSS swaps apply instantly.
Keep as many CSS themes as you like: write them in the client with syntax highlighting and a live preview, paste them or import .css files, and switch between them with one click. They always load after Krunker's styles, so your rules win.
Browse CSS made by the community right in the CSS Manager, with previews, and download any of them with one click.
One switch gives the whole client Kute's look: menus, settings, every window and popup, the social window and the HUD, with a few cats. Colors that mean something, like KR, rarities and health, stay as they are, and your own CSS still wins.
A slight blur while you turn the camera, with adjustable strength. The HUD stays sharp, and nothing runs while you stand still.
Your own sky on every map: starfield, aurora, synthwave and more built in, a color gradient, or any image you drop into the skies folder.
A high-performance Krunker client with enhanced features - made by [cute]
A high-performance Krunker client with enhanced features - made by [cute]
Input test: ${s.withInput} of ${s.readings} readings in the match had mouse input from Kute (${s.hostSteps} steps sent, ${s.pageEvents} seen by the game).
`;let e=Object.entries(s.ended).filter(([o])=>o!=="done"&&o!=="stopped").map(([o,n])=>`${o}: ${n}x`).join(", "),t="the game did not hold the mouse when the readings started";return s.asked>0&&(t=e||`Kute sent ${s.hostSteps} steps, the game saw ${s.pageEvents} mouse events`),`The input test did not reach the game (${t}). Mouse wait and settings that only cost in a fight were not measured.
`}function ut(s,e){let t=s.filter(r=>typeof r=="number").map(Math.round);if(t.length===0)return"-";let[o,n]=[Math.min(...t),Math.max(...t)];return o===n?`${o} ${e}`:`${o} to ${n} ${e}`}function e0(s){let e=[[`As you had it (${Dt(s.beforeCap)})`,s.played??[]],["no limit, first readings",s.unlimited??[]],...s.caps.filter(u=>u.where==="test match").map(u=>[Dt(u.cap),u.readings??[]])],{render:t=null,display:o=null}=s.adapters??{},n=t!==null&&o!==null&&t!==o,r=u=>{let h=(u?.nvidia??[]).map(p=>p.clockMhz).filter(p=>typeof p=="number");return h.length>0?Math.max(...h):null},i=u=>{let h=(u?.nvidia??[]).reduce((f,m)=>f|(m.heldBack??0),0),p=[[1,"heat"],[2,"power limit"],[4,"battery"],[16,"weak power supply"]].filter(([f])=>(h&Number(f))!==0).map(([,f])=>f);return p.length>0?p.join(", "):h===0?"no":`yes (${h})`},a=[...s.played??[],...s.unlimited??[]].some(u=>(u.load?.nvidia??[]).length>0),l=e.filter(([,u])=>u.some(h=>h.load)).map(([u,h])=>{let p=h.map(m=>m.load),f=(m,g)=>ut(p.map(b=>m===null?null:b?.gpu?.[m]?.[g]),"%");return`Input test: ${s.withInput} of ${s.readings} readings in the match had mouse input from Kute (${s.hostSteps} steps sent, ${s.pageEvents} seen by the game).
`;let e=Object.entries(s.ended).filter(([o])=>o!=="done"&&o!=="stopped").map(([o,n])=>`${o}: ${n}x`).join(", "),t="the game did not hold the mouse when the readings started";return s.asked>0&&(t=e||`Kute sent ${s.hostSteps} steps, the game saw ${s.pageEvents} mouse events`),`The input test did not reach the game (${t}). Mouse wait and settings that only cost in a fight were not measured.
`}function ut(s,e){let t=s.filter(r=>typeof r=="number").map(Math.round);if(t.length===0)return"-";let[o,n]=[Math.min(...t),Math.max(...t)];return o===n?`${o} ${e}`:`${o} to ${n} ${e}`}function n0(s){let e=[[`As you had it (${Dt(s.beforeCap)})`,s.played??[]],["no limit, first readings",s.unlimited??[]],...s.caps.filter(u=>u.where==="test match").map(u=>[Dt(u.cap),u.readings??[]])],{render:t=null,display:o=null}=s.adapters??{},n=t!==null&&o!==null&&t!==o,r=u=>{let h=(u?.nvidia??[]).map(p=>p.clockMhz).filter(p=>typeof p=="number");return h.length>0?Math.max(...h):null},i=u=>{let h=(u?.nvidia??[]).reduce((f,m)=>f|(m.heldBack??0),0),p=[[1,"heat"],[2,"power limit"],[4,"battery"],[16,"weak power supply"]].filter(([f])=>(h&Number(f))!==0).map(([,f])=>f);return p.length>0?p.join(", "):h===0?"no":`yes (${h})`},a=[...s.played??[],...s.unlimited??[]].some(u=>(u.load?.nvidia??[]).length>0),l=e.filter(([,u])=>u.some(h=>h.load)).map(([u,h])=>{let p=h.map(m=>m.load),f=(m,g)=>ut(p.map(b=>m===null?null:b?.gpu?.[m]?.[g]),"%");return`Processor speed is its share of the nominal clock: above 100 % with turbo, far below while the PC throttles it. A heat limit under 100 % means the firmware slows the PC down. A graphics clock that drops while the frame rate drops means the graphics driver is saving power.
- ${c.length>0?`| Time | FPS | Processor speed | Graphics card busy | Graphics clock | Driver holds it back |
|---|
| Time | FPS | Processor speed | Graphics card busy | Graphics clock | Driver holds it back |
|---|
${y.charAt(0).toUpperCase()}${y.slice(1)}, kept as they were: ${C.join(", ")}.
`),h=(o.length>0?Dn('Without an FPS limit this PC ran ${Math.round(s.capacity)} FPS in the test match. Kute aims for at least ${s.plan.target} FPS +
${y.charAt(0).toUpperCase()}${y.slice(1)}, kept as they were: ${C.join(", ")}.
`),h=(o.length>0?Dn('Without an FPS limit this PC ran ${Math.round(s.capacity)} FPS in the test match. Kute aims for at least ${s.plan.target} FPS (${3}x your ${s.hz} Hz screen), and this PC needs ${Math.round(s.plan.needed)} to keep that in a fight: samples of the same settings differ by ${Math.round(s.noise*100)} %, and it ended the run at ${Math.round(s.drift*100)} % of its starting speed.
`;if(s.pushed){let y=(s.unlimited??[]).map(H=>xt(H.fps,0)).join(", "),{limit:C,steadyUpTo:I}=s.pushed,_=`No limit ran steadily, not even your screen's ${s.hz} FPS, so there was no limit to offer.`;C!==null&&I!==null&&(_=`Up to ${I} FPS it ran steadily in the empty test match. A real match is heavier than that, so the limit with room is the highest multiple of your ${s.hz} Hz that has ${I>C?"one more steady step above it":"run steadily"}: ${C} FPS. A limit of your own that runs steadily is kept, a lower one would make the mouse wait longer.`),m=`This PC does not run steadily without an FPS limit: it stood still ${Math.round(s.pushed.stoodStill*100)} % of the time (readings without a limit: ${y} FPS). That happens when a PC throttles itself under full load, common on laptops.
-${_} Game settings were not measured, their gain is read without a limit.
`}let g=s.played??[],b=g.some(ws)?`The game drew more frames than reached the screen: one every ${zp(jn(g.map(y=>y.p50)))}, shown one every ${zp(jn(g.map(y=>y.presentMs)))}.
`:"",v=e0(s),k=s.pushed?"On a PC that does not run steadily without a limit, the limits are not compared with each other: each one only has to run steadily.":"A limit is taken when the game reacts sooner or the mouse waits less with it than with yours (gain above zero), and never when anything measures worse. A lower limit's longer frames are in the mouse wait. Slowest 1 % counts beyond the frame time.",E=i.map((y,C)=>Ho(`${y.where}: FPS limits`,`${y.html}${C===i.length-1?`${k}
`:""}`,{unit:"times in ms"}));return`${s.gpu}
${s.cpu}${s.laptop?" (laptop)":""}, ${s.hz} Hz${s.powerOverlay?`, Windows power mode: ${s.powerOverlay}`:""}.${p}
${s.rolledBack}
`:""}${s.pipelineRefused?`${s.pipelineRefused}
`:""}${Qv(s.input)}${b}`),Ho("Before and after",`${Dn(`Your setup stayed as it was, so there is no after to measure.
'}`,{unit:"times in ms"}),Ho("Client test: setup",n.length>0?Dn(`The client test did not run, the client's own setup was left alone.
",{unit:"times in ms"}),...E,Ho("Test match: game settings",`${l}
${h} +${_} Game settings were not measured, their gain is read without a limit.
`}let g=s.played??[],b=g.some(ws)?`The game drew more frames than reached the screen: one every ${Hp(jn(g.map(y=>y.p50)))}, shown one every ${Hp(jn(g.map(y=>y.presentMs)))}.
`:"",v=n0(s),k=s.pushed?"On a PC that does not run steadily without a limit, the limits are not compared with each other: each one only has to run steadily.":"A limit is taken when the game reacts sooner or the mouse waits less with it than with yours (gain above zero), and never when anything measures worse. A lower limit's longer frames are in the mouse wait. Slowest 1 % counts beyond the frame time.",E=i.map((y,C)=>Ho(`${y.where}: FPS limits`,`${y.html}${C===i.length-1?`${k}
`:""}`,{unit:"times in ms"}));return`${s.gpu}
${s.cpu}${s.laptop?" (laptop)":""}, ${s.hz} Hz${s.powerOverlay?`, Windows power mode: ${s.powerOverlay}`:""}.${p}
${s.rolledBack}
`:""}${s.pipelineRefused?`${s.pipelineRefused}
`:""}${s0(s.input)}${b}`),Ho("Before and after",`${Dn(`Your setup stayed as it was, so there is no after to measure.
'}`,{unit:"times in ms"}),Ho("Client test: setup",n.length>0?Dn(`The client test did not run, the client's own setup was left alone.
",{unit:"times in ms"}),...E,Ho("Test match: game settings",`${l}
${h}Settings that need a reload cannot be measured in one test match, they were not changed.
`),v?Ho("What the PC did",v,{wide:!0}):""].join("")}${Ke(s,!0)}`}br(s){return"An error occurred:
"+Ke(t.message+"",!0)+"";return e?Promise.resolve(o):o}if(e)return Promise.reject(t);throw t}}},po=new $w;ie.options=ie.setOptions=function(s){return po.setOptions(s),ie.defaults=po.defaults,mh(ie.defaults),ie};ie.getDefaults=xa;ie.defaults=ho;ie.use=Cw;ie.walkTokens=function(s,e){return po.walkTokens(s,e)};ie.parseInline=po.parseInline;ie.Parser=tt;ie.parser=tt.parse;ie.Renderer=Jn;ie.TextRenderer=Aa;ie.Lexer=et;ie.lexer=et.lex;ie.Tokenizer=Xn;ie.Hooks=Is;ie.parse=ie;jS=ie.options,US=ie.setOptions,GS=ie.walkTokens,WS=ie.parseInline,VS=tt.parse,YS=et.lex});var Eh=we((o3,Mw)=>{Mw.exports=`
${Ke(s,!0)}`}br(s){return"An error occurred:
"+Ke(t.message+"",!0)+"";return e?Promise.resolve(o):o}if(e)return Promise.reject(t);throw t}}},po=new Aw;ie.options=ie.setOptions=function(s){return po.setOptions(s),ie.defaults=po.defaults,fh(ie.defaults),ie};ie.getDefaults=Sa;ie.defaults=ho;ie.use=Lw;ie.walkTokens=function(s,e){return po.walkTokens(s,e)};ie.parseInline=po.parseInline;ie.Parser=ot;ie.parser=ot.parse;ie.Renderer=Jn;ie.TextRenderer=La;ie.Lexer=tt;ie.lexer=tt.lex;ie.Tokenizer=Xn;ie.Hooks=Is;ie.parse=ie;YS=ie.options,XS=ie.setOptions,JS=ie.walkTokens,ZS=ie.parseInline,QS=ot.parse,e3=tt.lex});var $h=we((a3,Rw)=>{Rw.exports=`
An error display: flex; justify-content: center; align-items: center; - `,document.body.append(a);let l=document.createElement("div");l.id="changelogPopupHost",a.append(l);let c=l.attachShadow({mode:"open"}),u=document.createElement("div");for(u.innerHTML=r.default;u.firstChild;)c.append(u.firstChild);let h=_=>c.getElementById(_),p=h("changelogLogo");p&&(p.src=i.default);let f=h("changelogContent");f&&(f.textContent="Loading release notes...");let m=h("changelogVersion");m&&(m.textContent=n);let g=new AbortController,b=()=>{g.abort(),a.remove()};for(let _ of["changelogClose","changelogDone"]){let H=h(_);H&&(H.onclick=b)}a.addEventListener("mousedown",_=>{_.target===a&&b()});let v=1,k=0,E=()=>{let _=mo??1,H=h("changelogPosition");H&&(H.textContent=`${v} / ${_}`),h("changelogNewer")?.classList.toggle("changelogDisabled",v<=1),h("changelogOlder")?.classList.toggle("changelogDisabled",v>=_);let G=h("changelogNav");G&&(G.style.display=_>1?"":"none")},L=async _=>{v=Math.min(Math.max(_,1),mo??1);let H=++k;E();let G=await Ra(v);if(H!==k||!f)return;if(!G){f.innerHTML="Could not load the release notes. View them on GitHub instead.";let be=h("changelogGithub");be&&(be.onclick=()=>window.chrome.webview.postMessage(`open-url, https://github.com/NullDev/Kute/releases/tag/${n}`));return}let J=`https://github.com/NullDev/Kute/releases/tag/${G.tag}`;m&&(m.textContent=G.tag);let B=h("changelogEyebrow");B&&(B.textContent=G.tag===n?"[ WHAT'S NEW ]":`[ RELEASED ${G.date} ]`);let re=h("changelogGithub");re&&(re.onclick=()=>window.chrome.webview.postMessage(`open-url, ${J}`));let je=await ie.parse(G.body||"No release notes found.",{breaks:!0,async:!0});if(H===k){f.innerHTML=je,h("changelogContentWrapper")?.scrollTo(0,0);for(let be of f.querySelectorAll("a"))be.getAttribute("href")?.startsWith("#")&&be.setAttribute("href",J+be.getAttribute("href")),be.setAttribute("target","_blank"),be.setAttribute("rel","noopener")}},y=h("changelogOlder");y&&(y.onclick=()=>L(v+1));let C=h("changelogNewer");C&&(C.onclick=()=>L(v-1)),document.addEventListener("keydown",_=>{if(_.key==="Escape")b();else if(_.key==="ArrowLeft")L(v-1);else if(_.key==="ArrowRight")L(v+1);else return;_.stopPropagation()},{signal:g.signal,capture:!0});let I=await Aw(n);g.signal.aborted||await L(I)}let e=d?.version,t=window.localStorage.getItem("kute_lastSeenVersion"),o=t!==null&&$h(e,t)>0;window.localStorage.setItem("kute_lastSeenVersion",e),d?.settings.data?.showChangelog&&t&&e&&o&&await s(e),d.showChangelogPopup=s})()});var Na={};A(Na,{default:()=>Lw});var Ia,Lw,_a=w(()=>{"use strict";Ia=class{tracked=new WeakSet;draft=null;observer=null;pending=null;sentAt=0;constructor(){document.addEventListener("focusin",e=>{let t=e.target;t instanceof HTMLTextAreaElement&&t.classList.contains("conversation-input")&&this.track(t)})}friendOf(e){return e.closest(".conversation-root")?.querySelector(".conversation-header-name")?.textContent??""}track(e){this.tracked.has(e)||(this.tracked.add(e),e.addEventListener("input",()=>this.save(e)),e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&!t.isComposing&&(this.sentAt=performance.now())}),e.closest(".conversation-compose")?.querySelector(".conversation-send-button")?.addEventListener("click",()=>{this.sentAt=performance.now()}),e.value&&this.save(e))}save(e){if(!e.value){this.stop();return}if(this.draft={input:e,friend:this.friendOf(e),text:e.value,start:e.selectionStart,end:e.selectionEnd},this.observer)return;let t=e.closest(".conversation-root");t&&(this.observer=new MutationObserver(()=>this.schedule()),this.observer.observe(t,{childList:!0,subtree:!0,characterData:!0}))}schedule(){this.pending===null&&(this.pending=setTimeout(()=>this.check(),0))}check(){this.pending=null;let{draft:e}=this;if(!e)return;let{input:t}=e;if(!t.isConnected||this.friendOf(t)!==e.friend){this.stop();return}if(t.value){this.save(t);return}if(performance.now()-this.sentAt<250){this.stop();return}t.value=e.text,document.activeElement===t&&t.setSelectionRange(e.start,e.end),t.dispatchEvent(new Event("input",{bubbles:!0}))}stop(){this.draft=null,this.observer?.disconnect(),this.observer=null}},Lw=new Ia});var Th,Mh=w(()=>{Th='
Collecting...
+ `,document.body.append(a);let l=document.createElement("div");l.id="changelogPopupHost",a.append(l);let c=l.attachShadow({mode:"open"}),u=document.createElement("div");for(u.innerHTML=r.default;u.firstChild;)c.append(u.firstChild);let h=_=>c.getElementById(_),p=h("changelogLogo");p&&(p.src=i.default);let f=h("changelogContent");f&&(f.textContent="Loading release notes...");let m=h("changelogVersion");m&&(m.textContent=n);let g=new AbortController,b=()=>{g.abort(),a.remove()};for(let _ of["changelogClose","changelogDone"]){let H=h(_);H&&(H.onclick=b)}a.addEventListener("mousedown",_=>{_.target===a&&b()});let v=1,k=0,E=()=>{let _=mo??1,H=h("changelogPosition");H&&(H.textContent=`${v} / ${_}`),h("changelogNewer")?.classList.toggle("changelogDisabled",v<=1),h("changelogOlder")?.classList.toggle("changelogDisabled",v>=_);let G=h("changelogNav");G&&(G.style.display=_>1?"":"none")},L=async _=>{v=Math.min(Math.max(_,1),mo??1);let H=++k;E();let G=await Pa(v);if(H!==k||!f)return;if(!G){f.innerHTML="Could not load the release notes. View them on GitHub instead.";let be=h("changelogGithub");be&&(be.onclick=()=>window.chrome.webview.postMessage(`open-url, https://github.com/NullDev/Kute/releases/tag/${n}`));return}let J=`https://github.com/NullDev/Kute/releases/tag/${G.tag}`;m&&(m.textContent=G.tag);let B=h("changelogEyebrow");B&&(B.textContent=G.tag===n?"[ WHAT'S NEW ]":`[ RELEASED ${G.date} ]`);let re=h("changelogGithub");re&&(re.onclick=()=>window.chrome.webview.postMessage(`open-url, ${J}`));let je=await ie.parse(G.body||"No release notes found.",{breaks:!0,async:!0});if(H===k){f.innerHTML=je,h("changelogContentWrapper")?.scrollTo(0,0);for(let be of f.querySelectorAll("a"))be.getAttribute("href")?.startsWith("#")&&be.setAttribute("href",J+be.getAttribute("href")),be.setAttribute("target","_blank"),be.setAttribute("rel","noopener")}},y=h("changelogOlder");y&&(y.onclick=()=>L(v+1));let C=h("changelogNewer");C&&(C.onclick=()=>L(v-1)),document.addEventListener("keydown",_=>{if(_.key==="Escape")b();else if(_.key==="ArrowLeft")L(v-1);else if(_.key==="ArrowRight")L(v+1);else return;_.stopPropagation()},{signal:g.signal,capture:!0});let I=await Iw(n);g.signal.aborted||await L(I)}let e=d?.version,t=window.localStorage.getItem("kute_lastSeenVersion"),o=t!==null&&Ch(e,t)>0;window.localStorage.setItem("kute_lastSeenVersion",e),d?.settings.data?.showChangelog&&t&&e&&o&&await s(e),d.showChangelogPopup=s})()});var _a={};A(_a,{default:()=>Nw});var Na,Nw,Ba=w(()=>{"use strict";Na=class{tracked=new WeakSet;draft=null;observer=null;pending=null;sentAt=0;constructor(){document.addEventListener("focusin",e=>{let t=e.target;t instanceof HTMLTextAreaElement&&t.classList.contains("conversation-input")&&this.track(t)})}friendOf(e){return e.closest(".conversation-root")?.querySelector(".conversation-header-name")?.textContent??""}track(e){this.tracked.has(e)||(this.tracked.add(e),e.addEventListener("input",()=>this.save(e)),e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&!t.isComposing&&(this.sentAt=performance.now())}),e.closest(".conversation-compose")?.querySelector(".conversation-send-button")?.addEventListener("click",()=>{this.sentAt=performance.now()}),e.value&&this.save(e))}save(e){if(!e.value){this.stop();return}if(this.draft={input:e,friend:this.friendOf(e),text:e.value,start:e.selectionStart,end:e.selectionEnd},this.observer)return;let t=e.closest(".conversation-root");t&&(this.observer=new MutationObserver(()=>this.schedule()),this.observer.observe(t,{childList:!0,subtree:!0,characterData:!0}))}schedule(){this.pending===null&&(this.pending=setTimeout(()=>this.check(),0))}check(){this.pending=null;let{draft:e}=this;if(!e)return;let{input:t}=e;if(!t.isConnected||this.friendOf(t)!==e.friend){this.stop();return}if(t.value){this.save(t);return}if(performance.now()-this.sentAt<250){this.stop();return}t.value=e.text,document.activeElement===t&&t.setSelectionRange(e.start,e.end),t.dispatchEvent(new Event("input",{bubbles:!0}))}stop(){this.draft=null,this.observer?.disconnect(),this.observer=null}},Nw=new Na});var Ah,Th=w(()=>{Ah=' Collecting...
!important rules still need !important here. Files live in Documents\\kute\\css.!important rules still need !important here. Files live in Documents\\kute\\css.krunker.io/docs/terms.txt. Drop .js files onto a group, or create one with New script.krunker.io/docs/terms.txt. Drop .js files onto a group, or create one with New script.swapper/textures/pistol.png replaces krunker.io/textures/pistol.png. Easiest way: drop your file onto the game file it should replace (right side), it gets the right folder and name. Folder names do not care about case. Text files like CSS can be written right here: New file, or the edit button on a game file to start from a copy.${S(e)}
${t}swapper/textures/pistol.png replaces krunker.io/textures/pistol.png. Easiest way: drop your file onto the game file it should replace (right side), it gets the right folder and name. Folder names do not care about case. Text files like CSS can be written right here: New file, or the edit button on a game file to start from a copy.${S(e)}
${t}${S(o)}
`;else{let i=!e.give.length&&!e.giveKr||!e.get.length&&!e.getKr,a=[...t.give.items,...t.get.items].some(m=>m.unknown),l=Gs(t.verdict),c=i?"Add items on both sides to compare":Gt[l].detail,u=Gt[l].label;a&&(u="Incomplete comparison"),i&&(u="Add both sides"),r+=`${c}`;let h=(m,g)=>{let b=m.items.reduce((k,E)=>k+E.count,0),v=m.items.some(k=>k.unknown);return`This server does not yet provide the detailed metrics for this item.
| Ask ceiling | Listings | Total asks / KR |
|---|---|---|
| ${T(a.price)} KR${a.premium?`Within +${Math.round(a.premium*100)}% of lowest`:"At the lowest ask"} | ${a.count} | ${T(a.kr)} |
The next successful book observation will populate the ask ladder.
';return`Completed daily means through ${S(e.throughDay)} UTC. ${e.observedDays30} of 30 days have observed sale prices.
This book needs a fresh observation. The ladder describes the stored sample.
')+i+`Cumulative counts in ${t.sampled} sampled listings; ${e.reportedListed} reported listed. These rows overlap. ${t.belowEstimate} sampled asks are below the Kute estimate.
All technical indicators use completed daily average sale prices, not exchange closing prices or individual transactions. Moving averages require every day in their window. RSI resets at a missing price day; a flat sequence is shown as 50. A high or low RSI describes past momentum and does not establish a buy or sell signal.
Volatility is the sample standard deviation of log returns between adjacent observed days. Drawdown is the largest observed peak-to-trough decline. The sales-weighted benchmark weights each daily mean by its reported sale count. Missing days are excluded, never treated as zero-price trades.
The ladder contains asking prices only. We have no bid book, so it cannot measure bid/ask spread, execution slippage, or buyer demand. Supply ratios require a supply observation within 24 hours. These metrics do not enable unvalidated forecasts.
| Category | Eligible | 7D median | Rising / falling | 30D KR turnover | Turnover share |
|---|---|---|---|---|---|
| ${S($t[o.type]??"Other")} | ${o.items} | ${ae(o.change)} | ${o.rising??"?"} / ${o.falling??"?"} | ${T(o.volume)} | ${Yt(e.eligibleVolume>0?o.volume/e.eligibleVolume:null)} |
Coverage: ${t.eligible} of ${T(t.total)} tradable items. Requires a sales-based estimate updated within 24 hours, at least 5 sales in 7 days, and a measured 7D trend. Turnover shares describe this eligible set, not the entire market. Price changes use Kute estimates.
${S(o)}
`;else{let i=!e.give.length&&!e.giveKr||!e.get.length&&!e.getKr,a=[...t.give.items,...t.get.items].some(m=>m.unknown),l=Gs(t.verdict),c=i?"Add items on both sides to compare":Gt[l].detail,u=Gt[l].label;a&&(u="Incomplete comparison"),i&&(u="Add both sides"),r+=`${c}`;let h=(m,g)=>{let b=m.items.reduce((k,E)=>k+E.count,0),v=m.items.some(k=>k.unknown);return`This server does not yet provide the detailed metrics for this item.
| Ask ceiling | Listings | Total asks / KR |
|---|---|---|
| ${T(a.price)} KR${a.premium?`Within +${Math.round(a.premium*100)}% of lowest`:"At the lowest ask"} | ${a.count} | ${T(a.kr)} |
The next successful book observation will populate the ask ladder.
';return`Completed daily means through ${S(e.throughDay)} UTC. ${e.observedDays30} of 30 days have observed sale prices.
This book needs a fresh observation. The ladder describes the stored sample.
')+i+`Cumulative counts in ${t.sampled} sampled listings; ${e.reportedListed} reported listed. These rows overlap. ${t.belowEstimate} sampled asks are below the Kute estimate.
All technical indicators use completed daily average sale prices, not exchange closing prices or individual transactions. Moving averages require every day in their window. RSI resets at a missing price day; a flat sequence is shown as 50. A high or low RSI describes past momentum and does not establish a buy or sell signal.
Volatility is the sample standard deviation of log returns between adjacent observed days. Drawdown is the largest observed peak-to-trough decline. The sales-weighted benchmark weights each daily mean by its reported sale count. Missing days are excluded, never treated as zero-price trades.
The ladder contains asking prices only. We have no bid book, so it cannot measure bid/ask spread, execution slippage, or buyer demand. Supply ratios require a supply observation within 24 hours. These metrics do not enable unvalidated forecasts.
| Category | Eligible | 7D median | Rising / falling | 30D KR turnover | Turnover share |
|---|---|---|---|---|---|
| ${S($t[o.type]??"Other")} | ${o.items} | ${ae(o.change)} | ${o.rising??"?"} / ${o.falling??"?"} | ${T(o.volume)} | ${Yt(e.eligibleVolume>0?o.volume/e.eligibleVolume:null)} |
Coverage: ${t.eligible} of ${T(t.total)} tradable items. Requires a sales-based estimate updated within 24 hours, at least 5 sales in 7 days, and a measured 7D trend. Turnover shares describe this eligible set, not the entire market. Price changes use Kute estimates.
${S(r.detail)}
-No observation to highlight.
'}${n}`}function wc(s,e,t,o){let{model:n}=s,r=n?.historical,i=n?.forward,a=r?.skill??null,l=[["","All signals"],["activity","Activity"],["supply","Supply"],["relative","Relative value"],["risk","Risks"],["availability","Availability"]],c=[...s.items].sort((h,p)=>Number(o.includes(p.skin))-Number(o.includes(h.skin))),u=h=>`No observation to highlight.
'}${n}`}function kc(s,e,t,o){let{model:n}=s,r=n?.historical,i=n?.forward,a=r?.skill??null,l=[["","All signals"],["activity","Activity"],["supply","Supply"],["relative","Relative value"],["risk","Risks"],["availability","Availability"]],c=[...s.items].sort((h,p)=>Number(o.includes(p.skin))-Number(o.includes(h.skin))),u=h=>`Lower log error is better. Error reduction compares with the better of unchanged last price and the existing valuation baseline. Historical mean range width: ${yr(r.width)} of the predicted price. Forward publication requires 200 outcomes, 20 items, four issue weeks, at least 5% lower error, 85-95% range coverage and mean range width no greater than the predicted price.
`:""}Sales history: ${S(s.coverage.firstDay??"Pending")} to ${S(s.coverage.lastDay??"Pending")}. Supply observations since ${s.coverage.supplySince?S(new Date(s.coverage.supplySince).toISOString().slice(0,16).replace("T"," ")):"collection began"}. ${n?`Model trained ${S(Ge(Date.now()-n.trainedAt))} ago.`:""}
${s.sources.length?`News and catalog feeds have not been collected yet.
'}`+(s.news.length?`News and catalog feeds have not been collected yet.
'}`+(s.news.length?`${h.url?`
${T(s.trades30d)} reported private trades involved this item. ${T(s.matchedSales30d)} of ${T(s.sales30d)} contributed market sales match independently sampled listings and fit official daily totals.
@@ -499,7 +499,7 @@ ${l}`));for(let l of n)this.seen.delete(l);for(;this.sent.size>cf;)this.sent.del Comparisons require multiple reports, source sessions and dates; source sessions do not identify unique players. These descriptive samples exclude bundles and reported serial, wear, style and paint attributes. The valuation model separately uses supported bundles with KR anchors; its contribution is explained above. -${T(t.reports)} eligible exchanges inform the ${T(s.fair)} KR Kute estimate. @@ -507,13 +507,13 @@ ${l}`));for(let l of n)this.seen.delete(l);for(;this.sent.size>cf;)this.sent.del ${S(t?.reason??"These reports establish activity; their currencies or instance attributes do not yet support a base-item KR estimate.")}
`} ${s.privateTrading?`${T(s.privateTrading.trades7d)} private trades in seven days. Latest reported trade date: ${S(s.privateTrading.lastTradeDay)} UTC.
`:""} ${e&&o?`Client reports can be incomplete or inaccurate. A verdict needs sufficient combined public and private evidence. Private trading does not establish cash-sale liquidity. The letter grade still describes public-market activity.
-- ${S(Jl(a))} + ${S(Zl(a))} · ${S(Ws(a))}
@@ -572,13 +572,13 @@ ${l}`));for(let l of n)this.seen.delete(l);for(;this.sent.size>cf;)this.sent.del KR `,a.spread!==null?ae(a.spread)+` vs estimate${I?" · quote needs updating":""}`:"No available quote")}${ne("Highest observed ask",H,G)}${ne("Sales in the last 7 days",String(a.sales7d),`${a.onS} currently listed · ${T(a.inC)} in circulation`)}${r?k:""} Dates and times are UTC.
| ${me(n)}${bc(n)} | +${me(n)}${vc(n)} | ${T(n.price)} | ${T(n.fair)} | ${Math.round(n.discount*100)}% | @@ -970,7 +970,7 @@ ${l}`));for(let l of n)this.seen.delete(l);for(;this.sent.size>cf;)this.sent.del recent lower prices, unstable prices, supply changes and concentrated listings. Quotes older than three hours are excluded; availability can still change. - `:'
${S(c)} Historical prices applied to what you own today; this is not personal profit. Marks expire after @@ -1079,7 +1079,7 @@ ${l}`));for(let l of n)this.seen.delete(l);for(;this.sent.size>cf;)this.sent.del