0
GAMES#35f6743c
The Goblin Oracle
@GigaChad·deposited 3w ago·updated 2w ago·36 views
GAMES#35f6743c
The Goblin Oracle
GI
@GigaChad
36Views
0Comments
0Forks
0Saves
SHARE · REMIX
The Goblin Oracle — a HTML Games widget by @GigaChad.
CONTROLS
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
vibes.onReadyvibes.savevibes.load
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "The Goblin Oracle" by @GigaChad]
Source: https://itjustvibes.com/GigaChad/the-goblin-oracle
Type: HTML
--- SOURCE CODE ---
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Goblin Oracle</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700;900&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
:root{
--swamp:#0a0f0a;--murk:#131f13;--moss:#1a2e1a;--glow:#4aff4a;
--glow-dim:#2a8a2a;--gold:#d4a017;--gold-dim:#8b6914;--bone:#c9b896;
--flesh:#7a9a5a;--eye-red:#ff3333;--crystal:#8af4ff;--parchment:#d4c5a0;
--smoke:rgba(74,255,74,0.05);
}
html,body{height:100%;overflow-x:hidden}
body{
background:var(--swamp);color:var(--bone);
font-family:'Crimson Text',Georgia,serif;
display:flex;flex-direction:column;align-items:center;
min-height:100vh;position:relative;
}
body::before{
content:'';position:fixed;inset:0;
background:
radial-gradient(ellipse at 50% 0%,rgba(74,255,74,0.04) 0%,transparent 60%),
radial-gradient(ellipse at 20% 80%,rgba(74,255,74,0.02) 0%,transparent 40%),
radial-gradient(ellipse at 80% 60%,rgba(138,244,255,0.02) 0%,transparent 40%);
pointer-events:none;z-index:0;
}
.particles{position:fixed;inset:0;pointer-events:none;z-index:1;overflow:hidden}
.particle{
position:absolute;width:3px;height:3px;border-radius:50%;
background:var(--glow);opacity:0;
animation:float-up 6s infinite ease-out;
}
@keyframes float-up{
0%{opacity:0;transform:translateY(100vh) scale(0.5)}
10%{opacity:0.6}
90%{opacity:0.2}
100%{opacity:0;transform:translateY(-10vh) scale(0)}
}
.container{
position:relative;z-index:2;width:100%;max-width:520px;
padding:20px;display:flex;flex-direction:column;align-items:center;
min-height:100vh;
}
h1{
font-family:'Cinzel Decorative',serif;font-weight:900;
font-size:clamp(1.4rem,5vw,2rem);color:var(--glow);
text-align:center;margin:20px 0 4px;
text-shadow:0 0 20px rgba(74,255,74,0.5),0 0 40px rgba(74,255,74,0.2);
letter-spacing:3px;
}
.subtitle{
font-style:italic;color:var(--glow-dim);font-size:0.9rem;
margin-bottom:20px;text-align:center;opacity:0.8;
}
/* ── GOBLIN ── */
.goblin-wrap{position:relative;width:200px;height:220px;margin:0 auto 10px}
.goblin{
position:relative;width:100%;height:100%;
animation:goblin-hover 3s ease-in-out infinite;
filter:drop-shadow(0 10px 30px rgba(74,255,74,0.2));
}
@keyframes goblin-hover{
0%,100%{transform:translateY(0)}
50%{transform:translateY(-8px)}
}
.goblin-body{
position:absolute;bottom:20px;left:50%;transform:translateX(-50%);
width:90px;height:70px;background:var(--flesh);
border-radius:40px 40px 30px 30px;
box-shadow:inset 0 -10px 20px rgba(0,0,0,0.3);
}
.goblin-head{
position:absolute;top:20px;left:50%;transform:translateX(-50%);
width:110px;height:100px;background:#5a8a3a;
border-radius:55px 55px 40px 40px;
box-shadow:inset 0 -15px 25px rgba(0,0,0,0.3),0 0 30px rgba(74,255,74,0.1);
}
.goblin-ear{
position:absolute;top:35px;width:40px;height:22px;
background:#4a7a2a;border-radius:50%;
}
.goblin-ear.l{left:-15px;transform:rotate(-25deg)}
.goblin-ear.r{right:-15px;transform:rotate(25deg)}
.goblin-eye{
position:absolute;top:38px;width:28px;height:32px;
background:#111;border-radius:50%;overflow:hidden;
box-shadow:inset 0 0 8px rgba(74,255,74,0.3);
}
.goblin-eye.l{left:18px}
.goblin-eye.r{right:18px}
.goblin-pupil{
position:absolute;top:8px;left:50%;transform:translateX(-50%);
width:14px;height:16px;background:var(--glow);border-radius:50%;
box-shadow:0 0 10px var(--glow),0 0 20px rgba(74,255,74,0.4);
animation:blink 4s infinite;
}
.goblin-pupil::after{
content:'';position:absolute;top:3px;left:3px;
width:5px;height:5px;background:#fff;border-radius:50%;opacity:0.8;
}
@keyframes blink{
0%,42%,46%,100%{transform:translateX(-50%) scaleY(1)}
44%{transform:translateX(-50%) scaleY(0.1)}
}
.goblin-nose{
position:absolute;top:58px;left:50%;transform:translateX(-50%);
width:16px;height:14px;background:#4a7a2a;
border-radius:50%;
box-shadow:inset 0 -3px 5px rgba(0,0,0,0.3);
}
.goblin-mouth{
position:absolute;top:76px;left:50%;transform:translateX(-50%);
width:36px;height:10px;overflow:hidden;transition:height 0.3s;
}
.goblin-mouth-inner{
width:36px;height:24px;background:#2a1a0a;
border-radius:0 0 18px 18px;
border-top:2px solid #3a5a2a;
}
.goblin-hat{
position:absolute;top:-25px;left:50%;transform:translateX(-50%);
width:0;height:0;
border-left:40px solid transparent;border-right:40px solid transparent;
border-bottom:55px solid #2a1a3a;
filter:drop-shadow(0 0 8px rgba(138,244,255,0.2));
}
.goblin-hat::after{
content:'';position:absolute;top:42px;left:-48px;
width:96px;height:14px;background:#2a1a3a;
border-radius:50%;
}
.hat-star{
position:absolute;top:5px;left:50%;transform:translateX(-50%);
font-size:18px;color:var(--gold);
text-shadow:0 0 10px var(--gold);
animation:star-pulse 2s infinite;
}
@keyframes star-pulse{0%,100%{opacity:1;transform:translateX(-50%) scale(1)}50%{opacity:0.6;transform:translateX(-50%) scale(0.85)}}
/* crystal ball */
.crystal-ball{
position:absolute;bottom:0;left:50%;transform:translateX(-50%);
width:50px;height:50px;border-radius:50%;
background:radial-gradient(circle at 35% 35%,rgba(138,244,255,0.6),rgba(74,255,74,0.2),rgba(0,0,0,0.4));
box-shadow:0 0 20px rgba(138,244,255,0.3),0 0 40px rgba(74,255,74,0.1),inset 0 0 15px rgba(138,244,255,0.2);
animation:crystal-glow 3s ease-in-out infinite alternate;
}
@keyframes crystal-glow{
0%{box-shadow:0 0 20px rgba(138,244,255,0.2),0 0 40px rgba(74,255,74,0.1)}
100%{box-shadow:0 0 30px rgba(138,244,255,0.5),0 0 60px rgba(74,255,74,0.2)}
}
.crystal-base{
position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);
width:60px;height:12px;background:#3a2a1a;
border-radius:6px;
box-shadow:0 2px 8px rgba(0,0,0,0.5);
}
/* Arms */
.goblin-arm{
position:absolute;bottom:30px;width:20px;height:50px;
background:var(--flesh);border-radius:10px;
transform-origin:top center;
}
.goblin-arm.l{left:28px;transform:rotate(15deg);animation:arm-l 3s ease-in-out infinite}
.goblin-arm.r{right:28px;transform:rotate(-15deg);animation:arm-r 3s ease-in-out infinite}
@keyframes arm-l{0%,100%{transform:rotate(15deg)}50%{transform:rotate(20deg)}}
@keyframes arm-r{0%,100%{transform:rotate(-15deg)}50%{transform:rotate(-20deg)}}
/* ── SCENARIO PICKER ── */
.scenario-bar{
display:flex;gap:6px;flex-wrap:wrap;justify-content:center;
margin-bottom:16px;
}
.scenario-btn{
background:var(--moss);border:1px solid var(--glow-dim);
color:var(--bone);padding:6px 14px;border-radius:20px;
font-family:'Crimson Text',serif;font-size:0.85rem;cursor:pointer;
transition:all 0.25s;
}
.scenario-btn:hover,.scenario-btn.active{
background:var(--glow-dim);color:var(--swamp);
box-shadow:0 0 12px rgba(74,255,74,0.3);
}
/* ── ORACLE BUTTON ── */
.oracle-btn{
position:relative;
background:linear-gradient(135deg,#1a3a1a,#2a4a2a);
border:2px solid var(--glow);color:var(--glow);
font-family:'Cinzel Decorative',serif;font-weight:700;
font-size:1.1rem;padding:14px 40px;border-radius:50px;
cursor:pointer;letter-spacing:2px;
box-shadow:0 0 20px rgba(74,255,74,0.2),inset 0 0 20px rgba(74,255,74,0.05);
transition:all 0.3s;margin:10px 0 20px;
}
.oracle-btn:hover{
box-shadow:0 0 30px rgba(74,255,74,0.4),inset 0 0 30px rgba(74,255,74,0.1);
transform:scale(1.04);
}
.oracle-btn:active{transform:scale(0.97)}
.oracle-btn.casting{
animation:casting-pulse 0.6s ease-in-out infinite alternate;
pointer-events:none;
}
@keyframes casting-pulse{
0%{box-shadow:0 0 20px rgba(74,255,74,0.3),inset 0 0 20px rgba(74,255,74,0.1)}
100%{box-shadow:0 0 50px rgba(74,255,74,0.7),inset 0 0 40px rgba(74,255,74,0.2)}
}
/* ── EXCUSE DISPLAY ── */
.excuse-scroll{
width:100%;min-height:100px;
background:linear-gradient(135deg,rgba(30,20,10,0.9),rgba(20,15,8,0.95));
border:1px solid var(--gold-dim);border-radius:12px;
padding:24px 20px;position:relative;overflow:hidden;
box-shadow:inset 0 0 30px rgba(0,0,0,0.5),0 4px 20px rgba(0,0,0,0.3);
}
.excuse-scroll::before{
content:'';position:absolute;inset:3px;
border:1px solid rgba(212,160,23,0.15);border-radius:10px;
pointer-events:none;
}
.excuse-text{
font-size:1.15rem;line-height:1.6;color:var(--parchment);
text-align:center;
animation:text-appear 0.5s ease-out;
}
@keyframes text-appear{
0%{opacity:0;transform:translateY(10px)}
100%{opacity:1;transform:translateY(0)}
}
.excuse-placeholder{
color:var(--glow-dim);font-style:italic;text-align:center;opacity:0.6;
}
.excuse-actions{
display:flex;gap:10px;justify-content:center;margin-top:14px;
}
.action-btn{
background:none;border:1px solid var(--gold-dim);color:var(--gold);
padding:6px 16px;border-radius:16px;cursor:pointer;
font-family:'Crimson Text',serif;font-size:0.85rem;
transition:all 0.25s;display:flex;align-items:center;gap:5px;
}
.action-btn:hover{background:rgba(212,160,23,0.15);border-color:var(--gold)}
.action-btn .ico{font-size:1rem}
/* ── FAVORITES ── */
.fav-section{
width:100%;margin-top:24px;
}
.fav-header{
font-family:'Cinzel Decorative',serif;font-size:1rem;
color:var(--gold);margin-bottom:10px;
display:flex;align-items:center;gap:8px;cursor:pointer;
user-select:none;
}
.fav-header .arrow{transition:transform 0.3s;display:inline-block}
.fav-header .arrow.open{transform:rotate(90deg)}
.fav-list{display:none;flex-direction:column;gap:8px}
.fav-list.show{display:flex}
.fav-item{
background:rgba(26,46,26,0.6);border:1px solid rgba(74,255,74,0.1);
border-radius:8px;padding:12px 14px;position:relative;
font-size:0.95rem;line-height:1.5;color:var(--bone);
}
.fav-item .fav-tag{
font-size:0.7rem;color:var(--glow-dim);text-transform:uppercase;
letter-spacing:1px;margin-bottom:4px;
}
.fav-remove{
position:absolute;top:8px;right:10px;background:none;border:none;
color:var(--eye-red);cursor:pointer;font-size:1rem;opacity:0.5;
transition:opacity 0.2s;
}
.fav-remove:hover{opacity:1}
.fav-empty{color:var(--glow-dim);font-style:italic;font-size:0.85rem;opacity:0.5}
/* ── COUNTER ── */
.counter{
margin-top:20px;font-size:0.75rem;color:var(--glow-dim);
opacity:0.4;text-align:center;letter-spacing:1px;
}
/* ── TOAST ── */
.toast{
position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(80px);
background:var(--moss);border:1px solid var(--glow);color:var(--glow);
padding:10px 24px;border-radius:24px;font-size:0.9rem;
box-shadow:0 4px 20px rgba(74,255,74,0.3);
transition:transform 0.4s cubic-bezier(0.34,1.56,0.64,1),opacity 0.4s;
opacity:0;z-index:99;pointer-events:none;
}
.toast.show{transform:translateX(-50%) translateY(0);opacity:1}
</style>
</head>
<body>
<!-- floating particles -->
<div class="particles" id="particles"></div>
<div class="container">
<h1>The Goblin Oracle</h1>
<p class="subtitle">Purveyor of Fine Excuses Since the Dawn of Slacking</p>
<!-- Goblin Character -->
<div class="goblin-wrap">
<div class="goblin">
<div class="goblin-hat"><span class="hat-star">★</span></div>
<div class="goblin-head">
<div class="goblin-ear l"></div>
<div class="goblin-ear r"></div>
<div class="goblin-eye l"><div class="goblin-pupil"></div></div>
<div class="goblin-eye r"><div class="goblin-pupil"></div></div>
<div class="goblin-nose"></div>
<div class="goblin-mouth"><div class="goblin-mouth-inner"></div></div>
</div>
<div class="goblin-body">
<div class="goblin-arm l"></div>
<div class="goblin-arm r"></div>
</div>
<div class="crystal-ball"></div>
<div class="crystal-base"></div>
</div>
</div>
<!-- Scenario Picker -->
<div class="scenario-bar" id="scenarioBar"></div>
<!-- Oracle Button -->
<button class="oracle-btn" id="oracleBtn" onclick="conjureExcuse()">
CONSULT THE ORACLE
</button>
<!-- Excuse Display -->
<div class="excuse-scroll" id="excuseScroll">
<p class="excuse-placeholder" id="placeholder">
The goblin awaits your query...<br>Choose a scenario and consult the oracle.
</p>
<p class="excuse-text" id="excuseText" style="display:none"></p>
<div class="excuse-actions" id="excuseActions" style="display:none">
<button class="action-btn" onclick="conjureExcuse()"><span class="ico">🔮</span> Another</button>
<button class="action-btn" onclick="saveExcuse()"><span class="ico">⭐</span> Save</button>
<button class="action-btn" onclick="copyExcuse()"><span class="ico">📋</span> Copy</button>
</div>
</div>
<!-- Favorites -->
<div class="fav-section">
<div class="fav-header" onclick="toggleFavs()">
<span class="arrow" id="favArrow">▶</span>
⭐ Saved Excuses (<span id="favCount">0</span>)
</div>
<div class="fav-list" id="favList"></div>
</div>
<div class="counter" id="counter"></div>
</div>
<div class="toast" id="toast"></div>
<script>
const SCENARIOS = [
{id:'late',label:'Running Late'},
{id:'meeting',label:'Skip Meeting'},
{id:'deadline',label:'Missed Deadline'},
{id:'wfh',label:'Work from Home'},
{id:'leave',label:'Leave Early'},
{id:'noshow',label:'Day Off'},
];
const EXCUSES = {
late:[
["My neighbor's cat","triggered my car alarm at 4 AM","and I spent the entire morning filing a police report about a feline break-in."],
["A family of raccoons","established a toll booth at the end of my driveway","and they would not accept anything less than three granola bars."],
["My smart home system","decided today was a holiday","and locked me inside until I sang it a lullaby."],
["I was ready on time but","my front door handle came off in my hand","and I had to escape through the bathroom window like a secret agent."],
["A water main broke","directly in front of my street","and I had to wait for a kayak-based rideshare."],
["My alarm clock","got a firmware update overnight","and switched itself to Tokyo time."],
["I was stuck behind","a parade that nobody authorized","celebrating National Slow Walking Awareness Day."],
["A delivery truck","spilled 10,000 rubber ducks on the highway","and traffic was at a complete standstill while they were collected."],
["I accidentally wore","two different shoes this morning","and had to go back because one was a slipper and the other was a ski boot."],
["My GPS","routed me through a Renaissance fair","and I couldn't leave until I jousted the gatekeeper."],
],
meeting:[
["My calendar app","showed the meeting at 3 PM instead of 10 AM","due to a rare timezone glitch affecting only Tuesdays."],
["I was deeply focused on","that critical deliverable you mentioned last week","and lost all sense of time and space."],
["My headset","started picking up radio signals from a trucker convoy","and I couldn't unmute without broadcasting CB chatter."],
["I had a scheduling conflict","with an urgent facilities inspection","that apparently could not wait even five minutes."],
["My VPN","decided to route me through seven countries","and by the time I connected, the meeting was pure static."],
["I was actually in the meeting room","but my camera and mic both failed","so I was just vibing silently in the void."],
],
deadline:[
["I finished it on time but","my computer did an automatic restart for updates","and corrupted the file beyond recognition."],
["The project was 99% complete when","I realized the requirements had changed","based on a Slack message I received at 11:47 PM last night."],
["I accidentally saved over the final version","with a draft from three weeks ago","and had to reconstruct it from memory like an archaeologist."],
["My cloud storage","hit a sync conflict","and chose to keep the empty version over my finished masterpiece."],
["I was waiting on","a response from the vendor since last Thursday","and they just got back to me forty-five seconds ago."],
["The power flickered for one second","but that one second was enough","to destroy four hours of unsaved work."],
],
wfh:[
["My building's fire alarm","has been going off intermittently all morning","and I can't concentrate with the strobe lights."],
["A pipe burst","in the apartment above mine","and my desk is currently in the splash zone."],
["There's emergency construction","directly outside my window","involving what appears to be seventeen jackhammers."],
["My internet provider","is doing unscheduled maintenance in my area","and I'm currently tethered to a phone with 2% battery."],
["I woke up with","a suspicious cough and mild sniffles","and I think it's responsible to keep my distance today."],
["The exterminator showed up unannounced","for a mandatory inspection","and I'd rather not be on camera while they search for whatever is living in my walls."],
],
leave:[
["I have to pick up","a prescription that the pharmacy says expires","if I don't collect it before 4 PM today."],
["My building manager just called--","there's a gas leak investigation on my floor","and I need to be there to let the inspector in."],
["I have an appointment","that I booked three months ago","and they'll charge me $200 if I cancel within 24 hours."],
["My dog walker just canceled","and my dog has a strict schedule","that if broken leads to consequences I'd rather not describe."],
["My car is being towed","from a zone I didn't know was temporary no-parking","and I have exactly 45 minutes to save it."],
["I just got a call that","a package requiring my signature","is being returned to sender if I'm not home by 4:30."],
],
noshow:[
["I woke up with","what my doctor described over the phone as","a highly contagious but mercifully short-lived stomach situation."],
["There's a family emergency--","nothing life-threatening but","I need to be present and I'll keep you updated."],
["My apartment complex","is being emergency fumigated","and I was told I cannot remain inside the building until tomorrow."],
["I threw my back out","reaching for my alarm clock this morning","and I currently cannot sit, stand, or exist without wincing."],
["A tree fell","on my car overnight during the storm","and I'm spending the day dealing with insurance and a chainsaw."],
["I'm locked out of my apartment","with my laptop inside","because the door mechanism jammed and the locksmith can't come until 2 PM."],
],
};
const GOBLIN_QUIPS = [
"The oracle has spoken! Use this wisdom wisely, mortal.",
"Hehehe... this one's particularly believable. The goblin approves.",
"A classic from the ancient scrolls of corporate deception!",
"The crystal ball grows cloudy... but the excuse is crystal clear.",
"Even the goblin is impressed by this one. *chef's kiss*",
"Deploy this excuse with confidence. The spirits are on your side.",
"The stars have aligned in your favor, oh crafty one!",
"This excuse has a 94.7% believability rating. Trust the goblin.",
"Whispered from the shadows of a thousand successful sick days...",
"The ancient art of excuse-craft at its finest!",
];
let state = {
scenario: 'late',
favorites: [],
totalConsults: 0,
currentExcuse: '',
currentScenario: '',
};
// ── PARTICLES ──
function spawnParticles(){
const c = document.getElementById('particles');
for(let i=0;i<20;i++){
const p = document.createElement('div');
p.className='particle';
p.style.left = Math.random()*100+'%';
p.style.animationDelay = Math.random()*6+'s';
p.style.animationDuration = (4+Math.random()*4)+'s';
p.style.width = p.style.height = (2+Math.random()*3)+'px';
c.appendChild(p);
}
}
// ── SCENARIO BAR ──
function buildScenarios(){
const bar = document.getElementById('scenarioBar');
SCENARIOS.forEach(s=>{
const b = document.createElement('button');
b.className = 'scenario-btn'+(s.id===state.scenario?' active':'');
b.textContent = s.label;
b.onclick = ()=>{
state.scenario = s.id;
persist();
bar.querySelectorAll('.scenario-btn').forEach(x=>x.classList.remove('active'));
b.classList.add('active');
};
bar.appendChild(b);
});
}
// ── EXCUSE GENERATION ──
function conjureExcuse(){
const btn = document.getElementById('oracleBtn');
btn.classList.add('casting');
// animate mouth open
document.querySelector('.goblin-mouth').style.height='18px';
setTimeout(()=>{
const pool = EXCUSES[state.scenario] || EXCUSES.late;
const parts = pool[Math.floor(Math.random()*pool.length)];
const excuse = parts.join(' ');
const quip = GOBLIN_QUIPS[Math.floor(Math.random()*GOBLIN_QUIPS.length)];
state.currentExcuse = excuse;
state.currentScenario = state.scenario;
state.totalConsults++;
document.getElementById('placeholder').style.display='none';
const et = document.getElementById('excuseText');
et.style.display='block';
et.innerHTML = `<span style="color:var(--crystal);font-size:0.8rem;display:block;margin-bottom:8px">${quip}</span>"${excuse}"`;
et.style.animation='none';
et.offsetHeight; // reflow
et.style.animation='text-appear 0.5s ease-out';
document.getElementById('excuseActions').style.display='flex';
document.getElementById('counter').textContent = `${state.totalConsults} excuse${state.totalConsults!==1?'s':''} conjured`;
btn.classList.remove('casting');
document.querySelector('.goblin-mouth').style.height='10px';
persist();
}, 800);
}
// ── SAVE / COPY ──
function saveExcuse(){
if(!state.currentExcuse) return;
const label = SCENARIOS.find(s=>s.id===state.currentScenario)?.label||'Unknown';
if(state.favorites.some(f=>f.text===state.currentExcuse)) {
showToast('Already saved!');
return;
}
state.favorites.unshift({text:state.currentExcuse, tag:label, ts:Date.now()});
if(state.favorites.length>20) state.favorites.pop();
renderFavs();
persist();
showToast('Excuse saved to the goblin vault!');
}
async function copyExcuse(){
if(!state.currentExcuse) return;
try{
if(navigator.clipboard?.writeText){
await navigator.clipboard.writeText(state.currentExcuse);
} else {
const ta = document.createElement('textarea');
ta.value = state.currentExcuse;
ta.setAttribute('readonly','');
ta.style.position='fixed';
ta.style.opacity='0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
showToast('Copied to clipboard!');
}catch(e){
showToast('Copy failed -- press and hold the text to copy');
}
}
// ── FAVORITES ──
function toggleFavs(){
const list = document.getElementById('favList');
const arrow = document.getElementById('favArrow');
list.classList.toggle('show');
arrow.classList.toggle('open');
}
function renderFavs(){
const list = document.getElementById('favList');
document.getElementById('favCount').textContent = state.favorites.length;
if(!state.favorites.length){
list.innerHTML='<p class="fav-empty">No saved excuses yet. The goblin judges you.</p>';
return;
}
list.innerHTML = state.favorites.map((f,i)=>`
<div class="fav-item">
<div class="fav-tag">${f.tag}</div>
"${f.text}"
<button class="fav-remove" onclick="removeFav(${i})" title="Remove">✕</button>
</div>
`).join('');
}
function removeFav(i){
state.favorites.splice(i,1);
renderFavs();
persist();
showToast('Excuse banished from the vault.');
}
// ── TOAST ──
function showToast(msg){
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),2200);
}
// ── PERSISTENCE ──
async function persist(){
try{
if(!window.vibes?.save) return;
await window.vibes.save('goblin_state', {
favorites: state.favorites,
totalConsults: state.totalConsults,
scenario: state.scenario,
});
}catch(e){
console.error('Save failed:', e.message || e);
}
}
async function loadState(){
try{
if(!window.vibes?.load) return;
const raw = await window.vibes.load('goblin_state');
if(raw){
const d = typeof raw === 'string' ? JSON.parse(raw) : raw;
state.favorites = Array.isArray(d.favorites) ? d.favorites : [];
state.totalConsults = Number(d.totalConsults) || 0;
if(d.scenario && EXCUSES[d.scenario]) state.scenario = d.scenario;
}
}catch(e){
console.error('Load failed:', e.message || e);
}
// sync UI
document.querySelectorAll('.scenario-btn').forEach((b,i)=>{
b.classList.toggle('active', SCENARIOS[i].id===state.scenario);
});
document.getElementById('counter').textContent =
state.totalConsults ? `${state.totalConsults} excuse${state.totalConsults!==1?'s':''} conjured` : '';
renderFavs();
}
// ── INIT ──
async function init(){
await loadState();
buildScenarios();
spawnParticles();
}
(function waitForVibes(){
if(window.vibes?.onReady){
window.vibes.onReady(init);
} else {
setTimeout(waitForVibes, 25);
}
})();
</script>
</body>
</html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/GigaChad/the-goblin-oracle */