您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Change how skins and names look in CellCraft (local only) + S E N S E menu
// ==UserScript== // @name CellCraft Visual Mod: Skins & Name Colors (S E N S E) // @namespace http://tampermonkey.net/ // @version 1.0 // @description Change how skins and names look in CellCraft (local only) + S E N S E menu // @author S E N S E // @license S E N S E // @match *://cellcraft.io/* // @grant none // ==/UserScript== (function(){ 'use strict'; /* ------------------------- Helper: localStorage helpers -------------------------*/ const KEY = 'sense_cell_mod_v1'; const defaults = { mySkinUrl: '', myNameColor: '#00ff00', othersSkinUrl: '', // a default skin to replace others with othersNameColor: '#ff00ff', replacements: {} // mapping exact names -> {skinUrl, color} }; function loadState(){ try { const raw = localStorage.getItem(KEY); if(!raw) return {...defaults}; return Object.assign({}, defaults, JSON.parse(raw)); } catch(e){ return {...defaults}; } } function saveState(s){ localStorage.setItem(KEY, JSON.stringify(s)); } let state = loadState(); /* ------------------------- UI: Collapsible S E N S E menu (right side) -------------------------*/ function makeMenu(){ const wrapper = document.createElement('div'); wrapper.id = 'sense-menu'; Object.assign(wrapper.style, { position: 'fixed', top: '12vh', right: '12px', zIndex: 999999, fontFamily: 'Arial, sans-serif', userSelect: 'none' }); // collapsed button const button = document.createElement('button'); button.innerText = 'S E N S E'; Object.assign(button.style, { width: '48px', height: '48px', borderRadius: '8px', border: 'none', background: 'linear-gradient(90deg,#0f172a,#0ea5a0)', color: '#fff', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.4)' }); // panel const panel = document.createElement('div'); Object.assign(panel.style, { width: '260px', padding: '10px', background: 'rgba(0,0,0,0.75)', color: '#fff', borderRadius: '8px', marginTop: '8px', display: 'none', fontSize: '13px', boxShadow: '0 6px 18px rgba(0,0,0,0.5)' }); panel.innerHTML = ` <div style="font-weight:700; margin-bottom:6px">CellCraft Visual Mod</div> <div style="font-size:12px; opacity:0.85; margin-bottom:6px">By S E N S E — local visual changes only</div> <div style="margin-bottom:8px"> <div style="font-weight:600">My Skin (image URL)</div> <input id="sense-my-skin" placeholder="https://image.png" style="width:100%; margin-top:4px; padding:6px; border-radius:4px; border:none"/> <button id="sense-my-skin-set" style="margin-top:6px;padding:6px;border-radius:4px;border:none;cursor:pointer">Apply My Skin</button> </div> <div style="margin-bottom:8px"> <div style="font-weight:600">My Name Color</div> <input id="sense-my-color" type="color" style="width:44px;height:32px;margin-top:4px;border:0;padding:0"/> <button id="sense-my-color-set" style="margin-left:8px;padding:6px;border-radius:4px;border:none;cursor:pointer">Set</button> </div> <hr style="border:none;border-top:1px solid rgba(255,255,255,0.06); margin:8px 0"/> <div style="margin-bottom:8px"> <div style="font-weight:600">Replace Others' Skin (URL)</div> <input id="sense-others-skin" placeholder="https://image.png" style="width:100%; margin-top:4px; padding:6px; border-radius:4px; border:none"/> <button id="sense-others-skin-set" style="margin-top:6px;padding:6px;border-radius:4px;border:none;cursor:pointer">Apply to Others</button> </div> <div style="margin-bottom:8px"> <div style="font-weight:600">Replace Others' Name Color</div> <input id="sense-others-color" type="color" style="width:44px;height:32px;margin-top:4px;border:0;padding:0"/> <button id="sense-others-color-set" style="margin-left:8px;padding:6px;border-radius:4px;border:none;cursor:pointer">Set</button> </div> <hr style="border:none;border-top:1px solid rgba(255,255,255,0.06); margin:8px 0"/> <div style="margin-bottom:8px"> <div style="font-weight:600">Per-player Overrides</div> <div style="font-size:12px; opacity:0.9; margin-bottom:6px">Exact name match</div> <input id="sense-name-key" placeholder="PlayerName" style="width:48%; padding:6px; border-radius:4px; border:none;"/> <input id="sense-name-skin" placeholder="skin URL" style="width:48%; padding:6px; border-radius:4px; border:none; float:right"/> <input id="sense-name-color" type="color" style="margin-top:6px;"/> <button id="sense-add-repl" style="display:block;margin-top:6px;padding:6px;border-radius:4px;border:none;cursor:pointer">Add / Save Override</button> <div id="sense-repl-list" style="margin-top:8px; max-height:90px; overflow:auto; font-size:12px"></div> </div> <div style="text-align:right; margin-top:8px"> <button id="sense-reset" style="padding:6px;border-radius:6px;border:none;cursor:pointer">Reset</button> </div> `; wrapper.appendChild(button); wrapper.appendChild(panel); document.body.appendChild(wrapper); // restore inputs const inMySkin = panel.querySelector('#sense-my-skin'); const inMyColor = panel.querySelector('#sense-my-color'); const inOthersSkin = panel.querySelector('#sense-others-skin'); const inOthersColor = panel.querySelector('#sense-others-color'); inMySkin.value = state.mySkinUrl || ''; inMyColor.value = state.myNameColor || '#00ff00'; inOthersSkin.value = state.othersSkinUrl || ''; inOthersColor.value = state.othersNameColor || '#ff00ff'; // Toggle panel button.addEventListener('click', () => { panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); // Buttons panel.querySelector('#sense-my-skin-set').addEventListener('click', () => { state.mySkinUrl = inMySkin.value.trim(); saveState(state); flash('Applied: my skin'); }); panel.querySelector('#sense-my-color-set').addEventListener('click', () => { state.myNameColor = inMyColor.value; saveState(state); flash('Applied: my name color'); }); panel.querySelector('#sense-others-skin-set').addEventListener('click', () => { state.othersSkinUrl = inOthersSkin.value.trim(); saveState(state); flash('Applied: others skin'); }); panel.querySelector('#sense-others-color-set').addEventListener('click', () => { state.othersNameColor = inOthersColor.value; saveState(state); flash('Applied: others name color'); }); // per-player overrides const nameKey = panel.querySelector('#sense-name-key'); const nameSkin = panel.querySelector('#sense-name-skin'); const nameColor = panel.querySelector('#sense-name-color'); const addBtn = panel.querySelector('#sense-add-repl'); const replList = panel.querySelector('#sense-repl-list'); function renderReplList(){ replList.innerHTML = ''; const keys = Object.keys(state.replacements || {}); if(keys.length === 0){ replList.innerHTML = '<i style="opacity:0.7">No overrides</i>'; return; } keys.forEach(k => { const v = state.replacements[k] || {}; const div = document.createElement('div'); div.style.padding = '6px'; div.style.borderBottom = '1px solid rgba(255,255,255,0.03)'; div.innerHTML = `<b>${escapeHtml(k)}</b> — color: <span style="color:${v.color||'#fff'}">${v.color||'—'}</span> — skin: ${v.skin ? '<a href="'+escapeHtml(v.skin)+'" target="_blank">link</a>' : '—'} <button data-name="${escapeHtml(k)}" style="float:right">Remove</button>`; replList.appendChild(div); }); replList.querySelectorAll('button').forEach(btn=>{ btn.addEventListener('click', (ev)=>{ const key = ev.target.getAttribute('data-name'); delete state.replacements[key]; saveState(state); renderReplList(); flash('Removed override'); }); }); } addBtn.addEventListener('click', ()=>{ const k = nameKey.value.trim(); if(!k){ flash('Enter exact player name'); return; } state.replacements[k] = { skin: nameSkin.value.trim() || null, color: nameColor.value || null }; saveState(state); renderReplList(); flash('Saved override for: ' + k); nameKey.value = ''; nameSkin.value = ''; }); panel.querySelector('#sense-reset').addEventListener('click', ()=>{ if(confirm('Reset all S E N S E visual settings?')){ state = {...defaults}; saveState(state); inMySkin.value = ''; inMyColor.value = '#00ff00'; inOthersSkin.value = ''; inOthersColor.value = '#ff00ff'; renderReplList(); flash('Reset done'); } }); renderReplList(); } function escapeHtml(s){ return String(s).replace(/[&<>"'`]/g, (c)=>({ '&':'&','<':'<','>':'>','"':'"',"'":''','`':'`' }[c])); } function flash(msg){ const b = document.createElement('div'); b.innerText = msg; Object.assign(b.style, { position:'fixed', left:'50%', transform:'translateX(-50%)', bottom:'20px', padding:'8px 12px', background:'rgba(0,0,0,0.8)', color:'#fff', borderRadius:'8px', zIndex:999999 }); document.body.appendChild(b); setTimeout(()=>b.remove(), 1800); } /* ------------------------- Intercept drawing: drawImage + fillText We'll attempt to replace skin images and name colors on-the-fly. This is heuristic-based: it changes images/text as they're drawn. -------------------------*/ // small cache for replacement images const replacementImageCache = new Map(); function getReplacementImage(url){ if(!url) return null; if(replacementImageCache.has(url)) return replacementImageCache.get(url); const img = new Image(); img.crossOrigin = 'anonymous'; img.src = url; replacementImageCache.set(url, img); return img; } // Hook drawImage to swap skin images with user-provided images. const origDrawImage = CanvasRenderingContext2D.prototype.drawImage; CanvasRenderingContext2D.prototype.drawImage = function(...args){ try { // args[0] can be an Image, Canvas, Video, or HTMLImageElement const source = args[0]; if(source && source.src && typeof source.src === 'string'){ const src = source.src; // Heuristics: skins/players images often have 'skin' or 'player' in url or are small sprites. // We'll apply replacement for: // - if this image is likely a player-sprite by size (e.g., small) OR user set replacements. // Replace logic: // 1) if there is exact override for a name — we can't know name here, skip. // 2) if source corresponds to your own skin (we can't know either), use state.mySkinUrl for any matching sprite pattern. // 3) if state.othersSkinUrl exists, and src looks like a skin sprite, swap to that. const low = src.toLowerCase(); const looksLikeSkin = /skin|player|cell|blob|sprite|avatar|face|body/.test(low) || (/\.png$|\.jpg$|\.jpeg$/.test(low) && (source.naturalWidth <= 256 && source.naturalHeight <= 256)); if(looksLikeSkin){ // Prefer user-specific URLs if present const replUrl = state.othersSkinUrl || state.mySkinUrl; if(replUrl){ const replImg = getReplacementImage(replUrl); if(replImg && replImg.complete){ // draw replacement image instead of original but keep drawing args (position + size) args[0] = replImg; } else { // Not yet loaded: attempt to draw and let browser fetch; still safe to call original replImg && replImg.addEventListener('load', ()=>{ /* will be used next draws */ }); } } } } } catch(e){ /* fail silently */ } return origDrawImage.apply(this, args); }; // We'll override fillStyle temporarily when drawing names. const origFillText = CanvasRenderingContext2D.prototype.fillText; CanvasRenderingContext2D.prototype.fillText = function(text, x, y, maxWidth){ try { if(typeof text === 'string' && text.length > 0 && text.length < 60){ const t = text.trim(); // exact override if(state.replacements && state.replacements[t]){ const color = state.replacements[t].color || state.othersNameColor || state.myNameColor; if(color){ const prev = this.fillStyle; this.fillStyle = color; const res = origFillText.apply(this, arguments); this.fillStyle = prev; return res; } } // if text equals "You" or your own name heuristic: use myNameColor // We attempt to detect your name by reading DOM username if present (fallback). const myName = detectMyName(); if(myName && t === myName){ const prev = this.fillStyle; this.fillStyle = state.myNameColor || prev; const res = origFillText.apply(this, arguments); this.fillStyle = prev; return res; } // Otherwise, if othersNameColor set, color other players if(state.othersNameColor){ // Avoid coloring UI texts by simple heuristics: names are usually short (<24) and not contain colons or spaces-only strings. if(t.length <= 24 && !/[:\[\]\(\)]/.test(t)){ const prev = this.fillStyle; this.fillStyle = state.othersNameColor; const res = origFillText.apply(this, arguments); this.fillStyle = prev; return res; } } } } catch(e){ /* ignore */ } return origFillText.apply(this, arguments); }; // Attempt to discover player's name from page (some games have an input or label) function detectMyName(){ // Many .io games have an input#nick or input[name=nick] — try a few guesses const selectors = ['input[name="nick"]','input#nick','input#name','input[name="name"]','input[type="text"]']; for(const s of selectors){ const el = document.querySelector(s); if(el && el.value && el.value.trim().length>0) return el.value.trim(); } // fallback: look for elements containing "Nickname" or "Name" — but avoid heavy querying return null; } /* ------------------------- Small periodic refresh to pick up new state and ensure everything loads -------------------------*/ function refreshState(){ state = loadState(); } setInterval(refreshState, 2000); /* ------------------------- Create UI after page load -------------------------*/ window.addEventListener('load', () => { try { makeMenu(); } catch(e){ console.error('SENSE UI failed', e); } }); // quick apply if script injected after load setTimeout(()=>{ try { makeMenu(); } catch(e){} }, 1500); })();