Spaces:
Sleeping
Sleeping
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> | |
| <title>O Pilar dos Deuses</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap'); | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#0a0806;--bg2:#110e0a;--bg3:#1a150f; | |
| --border:#2a1e0e;--gold:#c9a84c;--gold2:#e8c97a; | |
| --red:#c0392b;--text:#d4c5a0;--text2:#a09070;--text3:#6b5a3e;--white:#f0e8d0; | |
| } | |
| body{background:var(--bg);color:var(--text);font-family:'EB Garamond',serif;height:100vh;display:flex;flex-direction:column;overflow:hidden;font-size:15px;} | |
| ::-webkit-scrollbar{width:3px}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px} | |
| /* MODAL */ | |
| #modal{position:fixed;inset:0;background:rgba(0,0,0,0.92);display:flex;align-items:center;justify-content:center;z-index:100} | |
| .modal-box{background:var(--bg2);border:1px solid var(--gold);border-radius:10px;padding:28px 22px;width:92%;max-width:400px;box-shadow:0 0 60px rgba(201,168,76,0.15)} | |
| .modal-title{text-align:center;margin-bottom:20px} | |
| .modal-title .icon{font-size:40px;margin-bottom:8px} | |
| .modal-title h1{font-family:'Cinzel',serif;color:var(--gold2);font-size:20px;letter-spacing:0.05em} | |
| .modal-title p{color:var(--text3);font-size:10px;letter-spacing:0.2em;margin-top:6px} | |
| .field-label{color:var(--text3);font-size:10px;letter-spacing:0.18em;text-transform:uppercase;margin-bottom:5px;margin-top:12px;display:block} | |
| .field{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:9px 12px;color:var(--white);font-family:'EB Garamond',serif;font-size:14px;outline:none} | |
| .field:focus{border-color:rgba(201,168,76,0.5)} | |
| select.field option{background:var(--bg3)} | |
| .start-btn{width:100%;background:linear-gradient(135deg,#8b1a00,#c0390b);border:1px solid var(--red);color:var(--white);font-family:'Cinzel',serif;font-size:14px;padding:13px;border-radius:6px;cursor:pointer;margin-top:16px;letter-spacing:0.06em} | |
| /* HEADER */ | |
| #header{flex-shrink:0;background:var(--bg2);border-bottom:1px solid var(--border)} | |
| .header-title{text-align:center;padding:8px 16px 6px;border-bottom:1px solid var(--border);font-family:'Cinzel',serif;color:var(--gold2);font-size:15px;letter-spacing:0.07em;text-shadow:0 0 20px rgba(201,168,76,0.3)} | |
| .status-bar{display:flex;overflow-x:auto} | |
| .stat-cell{padding:5px 10px;border-right:1px solid var(--border);min-width:fit-content;flex-shrink:0} | |
| .stat-cell .sl{font-size:8px;color:var(--text3);letter-spacing:0.1em} | |
| .stat-cell .sv{font-size:12px;font-weight:bold;white-space:nowrap;font-family:'Cinzel',serif} | |
| .hp-bar{height:3px;background:#120e07} | |
| .hp-fill{height:100%;background:linear-gradient(to right,#6b0000,#e05050);transition:width .6s} | |
| /* CHAT */ | |
| #chat{flex:1;overflow-y:auto;padding:14px 12px;display:flex;flex-direction:column;gap:12px} | |
| .empty{text-align:center;color:var(--text3);padding:40px 20px;font-style:italic;font-size:13px} | |
| .empty .e-icon{font-size:34px;opacity:.25;margin-bottom:10px} | |
| .msg-player{display:flex;justify-content:flex-end;animation:fi .3s ease} | |
| .msg-player .bubble{background:rgba(201,168,76,0.08);border:1px solid rgba(201,168,76,0.2);border-radius:8px 0 8px 8px;padding:10px 12px;line-height:1.7;max-width:83%} | |
| .msg-player .name{font-family:'Cinzel',serif;font-size:10px;color:var(--gold);text-align:right;margin-bottom:3px;letter-spacing:0.1em} | |
| .msg-mj{display:flex;gap:9px;animation:fi .3s ease} | |
| .mj-avatar{width:28px;height:28px;border-radius:50%;background:rgba(139,26,26,0.35);border:1px solid var(--red);display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0;margin-top:2px} | |
| .mj-content{flex:1} | |
| .mj-name{font-family:'Cinzel',serif;font-size:9px;color:var(--red);letter-spacing:0.14em;margin-bottom:4px} | |
| .dice-badge{display:inline-flex;align-items:center;gap:6px;background:#140f08;border:1px solid var(--border);border-radius:4px;padding:3px 10px;margin-bottom:7px;font-size:11px} | |
| .dice-num{font-size:20px;font-weight:bold;font-family:'Cinzel',serif} | |
| .dice-label{color:var(--text3)} | |
| .mj-bubble{background:#160f09;border:1px solid var(--border);border-radius:0 8px 8px 8px;padding:11px 13px;line-height:1.75;white-space:pre-wrap} | |
| .msg-sys{animation:fi .3s ease} | |
| .sys-bubble{background:rgba(46,109,164,0.08);border:1px solid rgba(46,109,164,0.25);border-radius:6px;padding:9px 12px;font-size:12px;color:#7aabcc;font-family:'Cinzel',serif} | |
| .typing{display:flex;gap:9px} | |
| .typing-dots{background:#160f09;border:1px solid var(--border);border-radius:0 8px 8px 8px;padding:14px 16px;display:flex;gap:6px;align-items:center} | |
| .dot{width:8px;height:8px;border-radius:50%;background:var(--red);animation:bl 1.2s infinite} | |
| /* INPUT */ | |
| #quick{flex-shrink:0;display:flex;gap:5px;padding:6px 10px 5px;overflow-x:auto;border-top:1px solid var(--border);background:var(--bg2)} | |
| .qa{background:var(--bg3);border:1px solid var(--border);color:var(--text2);font-family:'Cinzel',serif;font-size:11px;padding:5px 9px;border-radius:3px;cursor:pointer;white-space:nowrap;flex-shrink:0} | |
| .qa:disabled{opacity:.4} | |
| #input-row{flex-shrink:0;display:flex;gap:8px;padding:7px 10px 11px;background:var(--bg2)} | |
| #inp{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:9px 12px;color:var(--white);font-family:'EB Garamond',serif;font-size:15px;resize:none;outline:none;min-height:42px} | |
| #inp:focus{border-color:rgba(201,168,76,0.4)} | |
| #send-btn{background:linear-gradient(135deg,#8b1a00,#c0390b);border:1px solid var(--red);color:var(--white);font-family:'Cinzel',serif;font-size:13px;padding:9px 16px;border-radius:5px;cursor:pointer;white-space:nowrap} | |
| #send-btn:disabled{opacity:.4;cursor:not-allowed} | |
| #attrs{flex-shrink:0;display:flex;border-top:1px solid var(--border);background:#090706} | |
| .attr-cell{flex:1;padding:4px 4px;text-align:center;border-right:1px solid var(--border)} | |
| .attr-cell .al{font-size:8px;color:var(--text3)} | |
| .attr-cell .av{font-size:11px;color:var(--text);font-weight:bold;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Cinzel',serif} | |
| @keyframes fi{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes bl{0%,80%,100%{opacity:.15}40%{opacity:1}} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- MODAL --> | |
| <div id="modal"> | |
| <div class="modal-box"> | |
| <div class="modal-title"> | |
| <div class="icon">🔱</div> | |
| <h1>O Pilar dos Deuses</h1> | |
| <p>DEFINA SEU DESTINO</p> | |
| </div> | |
| <label class="field-label">Nome do Personagem</label> | |
| <input class="field" id="f-name" type="text" placeholder="Como você é chamado..." maxlength="40"> | |
| <label class="field-label">Origem</label> | |
| <select class="field" id="f-origin"> | |
| <option>Boreas — Norte de Ferro e Gelo</option> | |
| <option>Austros — Sul de Areia e Luz</option> | |
| <option>Fronteira do Pilar</option> | |
| </select> | |
| <label class="field-label">Linhagem</label> | |
| <select class="field" id="f-lineage"> | |
| <option>Meio-Demônio (linhagem adormecida)</option> | |
| <option>Humano comum</option> | |
| <option>Tiefling exilado</option> | |
| <option>Meio-Elfo errante</option> | |
| <option>Aasimar renegado</option> | |
| <option>Orc sem clã</option> | |
| </select> | |
| <label class="field-label">Vocação</label> | |
| <select class="field" id="f-voc"> | |
| <option>Guerreiro Sem Nome</option> | |
| <option>Ladrão de Informações</option> | |
| <option>Mago Aprendiz</option> | |
| <option>Mercenário</option> | |
| <option>Nobre Empobrecido</option> | |
| <option>Caçador de Recompensas</option> | |
| </select> | |
| <button class="start-btn" onclick="startGame()">✦ Despertar no Mundo ✦</button> | |
| </div> | |
| </div> | |
| <!-- HEADER --> | |
| <div id="header"> | |
| <div class="header-title">⚔ O Pilar dos Deuses ⚔</div> | |
| <div class="status-bar" id="status-bar"> | |
| <div class="stat-cell"><div class="sl">👤 JOGADOR</div><div class="sv" id="s-name" style="color:#e8c97a">—</div></div> | |
| <div class="stat-cell"><div class="sl">❤️ HP</div><div class="sv" id="s-hp" style="color:#7ab87a">100/100</div></div> | |
| <div class="stat-cell"><div class="sl">⭐ RANK</div><div class="sv" id="s-rank" style="color:#888">E</div></div> | |
| <div class="stat-cell"><div class="sl">💰 OURO</div><div class="sv" id="s-gold" style="color:#c9a84c">50g</div></div> | |
| <div class="stat-cell"><div class="sl">📅 DIA</div><div class="sv" id="s-dia">Dia 1</div></div> | |
| <div class="stat-cell"><div class="sl">🔄 CICLO</div><div class="sv" id="s-ciclo">Despertar</div></div> | |
| <div class="stat-cell"><div class="sl">📍 LOCAL</div><div class="sv" id="s-local" style="color:#e08020">—</div></div> | |
| </div> | |
| <div class="hp-bar"><div class="hp-fill" id="hp-fill" style="width:100%"></div></div> | |
| </div> | |
| <!-- CHAT --> | |
| <div id="chat"> | |
| <div class="empty"><div class="e-icon">🔱</div><div>O mundo aguarda sua chegada...</div></div> | |
| </div> | |
| <!-- QUICK ACTIONS --> | |
| <div id="quick"> | |
| <button class="qa" onclick="qa('Examino o ambiente ao redor com cuidado.')">🔍 Examinar</button> | |
| <button class="qa" onclick="qa('Procuro rumores e informações com os locais.')">🗣 Rumores</button> | |
| <button class="qa" onclick="qa('Descanso para recuperar forças e mana.')">😴 Descansar</button> | |
| <button class="qa" onclick="qa('Verifico o calendário e eventos importantes.')">📅 Agenda</button> | |
| <button class="qa" onclick="qa('Analiso as relações diplomáticas entre os reinos.')">⚔️ Política</button> | |
| <button class="qa" onclick="qa('Verifico meu inventário e status completo.')">🎒 Status</button> | |
| </div> | |
| <!-- INPUT --> | |
| <div id="input-row"> | |
| <textarea id="inp" placeholder="O que você faz?" rows="1" | |
| onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}" | |
| oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,100)+'px'"></textarea> | |
| <button id="send-btn" onclick="sendMsg()">Agir →</button> | |
| </div> | |
| <!-- ATTRS --> | |
| <div id="attrs"> | |
| <div class="attr-cell"><div class="al">FOR</div><div class="av" id="a-for">10</div></div> | |
| <div class="attr-cell"><div class="al">AGI</div><div class="av" id="a-agi">10</div></div> | |
| <div class="attr-cell"><div class="al">MAN</div><div class="av" id="a-mana">5</div></div> | |
| <div class="attr-cell"><div class="al">CAR</div><div class="av" id="a-car">8</div></div> | |
| <div class="attr-cell"><div class="al">MEN</div><div class="av" id="a-men">10</div></div> | |
| <div class="attr-cell"><div class="al">⏰</div><div class="av" id="a-hora">06:00</div></div> | |
| <div class="attr-cell"><div class="al">🌤</div><div class="av" id="a-clima">—</div></div> | |
| </div> | |
| <script> | |
| const RANK_COLORS = {E:"#888",D:"#a07040",C:"#60a060",B:"#5599cc",A:"#b080e0",S:"#e0b020","SS":"#e04080","SS+":"#c060ff"}; | |
| let gs = {hp:100,maxHp:100,gold:50,rank:"E",local:"",hora:"06:00",dia:"1",ciclo:"Despertar",clima:"—",for:10,agi:10,mana:5,car:8,men:10,name:"",lineage:""}; | |
| let messages = []; | |
| let busy = false; | |
| function parseStatus(text) { | |
| const m = text.match(/STATUS\|(.+)/); | |
| if (!m) return; | |
| m[1].split("|").forEach(p => { | |
| const idx = p.indexOf("="); | |
| if (idx < 0) return; | |
| const k = p.slice(0,idx).trim(), v = p.slice(idx+1).trim(); | |
| gs[k] = isNaN(v) ? v : Number(v); | |
| }); | |
| } | |
| function getDice(text) { | |
| const m = text.match(/\[DADO:(\d+)\]/i); | |
| return m ? parseInt(m[1]) : null; | |
| } | |
| function cleanText(text) { | |
| return text.replace(/\[DADO:\d+\]\s*/i,"").replace(/STATUS\|.*/g,"").trim(); | |
| } | |
| function updateUI() { | |
| document.getElementById("s-name").textContent = gs.name || "—"; | |
| const hpPct = Math.max(0,(gs.hp/gs.maxHp)*100); | |
| document.getElementById("s-hp").textContent = gs.hp+"/"+gs.maxHp; | |
| document.getElementById("s-hp").style.color = hpPct<30?"#c0392b":hpPct<60?"#e08020":"#7ab87a"; | |
| document.getElementById("hp-fill").style.width = hpPct+"%"; | |
| document.getElementById("hp-fill").style.background = hpPct<30?"linear-gradient(to right,#6b0000,#c0392b)":"linear-gradient(to right,#6b0000,#e05050)"; | |
| document.getElementById("s-rank").textContent = gs.rank; | |
| document.getElementById("s-rank").style.color = RANK_COLORS[gs.rank]||"#888"; | |
| document.getElementById("s-gold").textContent = gs.gold+"g"; | |
| document.getElementById("s-dia").textContent = "Dia "+gs.dia; | |
| document.getElementById("s-ciclo").textContent = gs.ciclo||"—"; | |
| document.getElementById("s-local").textContent = gs.local||"—"; | |
| document.getElementById("a-for").textContent = gs.for; | |
| document.getElementById("a-agi").textContent = gs.agi; | |
| document.getElementById("a-mana").textContent = gs.mana; | |
| document.getElementById("a-car").textContent = gs.car; | |
| document.getElementById("a-men").textContent = gs.men; | |
| document.getElementById("a-hora").textContent = gs.hora; | |
| document.getElementById("a-clima").textContent = gs.clima||"—"; | |
| } | |
| function addMsg(role, text) { | |
| const chat = document.getElementById("chat"); | |
| const empty = chat.querySelector(".empty"); | |
| if (empty) empty.remove(); | |
| const div = document.createElement("div"); | |
| if (role === "player") { | |
| div.className = "msg-player"; | |
| div.innerHTML = `<div><div class="name">${gs.name} ⚔️</div><div class="bubble">${esc(text)}</div></div>`; | |
| } else if (role === "sys") { | |
| div.className = "msg-sys"; | |
| div.innerHTML = `<div class="sys-bubble">📜 ${esc(text)}</div>`; | |
| } else { | |
| const dice = getDice(text); | |
| const body = cleanText(text); | |
| let diceHtml = ""; | |
| if (dice !== null) { | |
| const dc = dice===20?"#7af07a":dice===1?"#c0392b":"#e8c97a"; | |
| const dl = dice===20?"⚡ CRÍTICO!":dice===1?"💀 FALHA CATASTRÓFICA":dice>=15?"✓ Sucesso":dice>=8?"〜 Parcial":"✗ Falha"; | |
| diceHtml = `<div class="dice-badge"><span class="dice-num" style="color:${dc}">${dice}</span><span class="dice-label">/20 — ${dl}</span></div>`; | |
| } | |
| div.className = "msg-mj"; | |
| div.innerHTML = ` | |
| <div class="mj-avatar">👁</div> | |
| <div class="mj-content"> | |
| <div class="mj-name">MESTRE DE JOGO</div> | |
| ${diceHtml} | |
| <div class="mj-bubble">${esc(body)}</div> | |
| </div>`; | |
| } | |
| chat.appendChild(div); | |
| setTimeout(()=>div.scrollIntoView({behavior:"smooth",block:"end"}),50); | |
| } | |
| function addTyping() { | |
| const chat = document.getElementById("chat"); | |
| const div = document.createElement("div"); | |
| div.id = "typing"; | |
| div.className = "typing"; | |
| div.innerHTML = `<div class="mj-avatar">👁</div><div class="typing-dots"><span class="dot" style="animation-delay:0s"></span><span class="dot" style="animation-delay:.2s"></span><span class="dot" style="animation-delay:.4s"></span></div>`; | |
| chat.appendChild(div); | |
| div.scrollIntoView({behavior:"smooth"}); | |
| } | |
| function removeTyping() { | |
| const t = document.getElementById("typing"); | |
| if (t) t.remove(); | |
| } | |
| function setDisabled(v) { | |
| busy = v; | |
| document.getElementById("send-btn").disabled = v; | |
| document.querySelectorAll(".qa").forEach(b=>b.disabled=v); | |
| } | |
| function esc(t) { | |
| return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/\n/g,"<br>"); | |
| } | |
| async function callAPI(userText) { | |
| messages.push({role:"user", content: userText}); | |
| const res = await fetch("/api/chat", { | |
| method:"POST", | |
| headers:{"Content-Type":"application/json"}, | |
| body: JSON.stringify({ messages, char: gs }) | |
| }); | |
| const data = await res.json(); | |
| if (data.error) throw new Error(data.error); | |
| messages.push({role:"assistant", content: data.reply}); | |
| return data.reply; | |
| } | |
| async function startGame() { | |
| const name = document.getElementById("f-name").value.trim(); | |
| if (!name) { alert("Digite seu nome!"); return; } | |
| gs.name = name; | |
| gs.lineage = document.getElementById("f-lineage").value; | |
| gs.local = document.getElementById("f-origin").value.split("—")[0].trim(); | |
| document.getElementById("modal").style.display = "none"; | |
| updateUI(); | |
| setDisabled(true); | |
| addTyping(); | |
| try { | |
| const intro = `Inicie o RPG. Sou ${name}, ${gs.lineage}, vocação ${document.getElementById("f-voc").value}, de ${gs.local}. Crie cena de abertura épica e preencha STATUS com atributos iniciais.`; | |
| const reply = await callAPI(intro); | |
| parseStatus(reply); | |
| updateUI(); | |
| removeTyping(); | |
| addMsg("mj", reply); | |
| } catch(e) { | |
| removeTyping(); | |
| addMsg("sys", "Erro ao iniciar: "+e.message); | |
| } | |
| setDisabled(false); | |
| } | |
| async function sendMsg() { | |
| const inp = document.getElementById("inp"); | |
| const text = inp.value.trim(); | |
| if (!text || busy) return; | |
| inp.value = ""; | |
| inp.style.height = "auto"; | |
| addMsg("player", text); | |
| setDisabled(true); | |
| addTyping(); | |
| try { | |
| const reply = await callAPI(text); | |
| parseStatus(reply); | |
| updateUI(); | |
| removeTyping(); | |
| addMsg("mj", reply); | |
| } catch(e) { | |
| removeTyping(); | |
| addMsg("sys", "Erro: "+e.message); | |
| } | |
| setDisabled(false); | |
| inp.focus(); | |
| } | |
| function qa(text) { | |
| if (busy) return; | |
| document.getElementById("inp").value = text; | |
| sendMsg(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |