Untitled
unknown
plain_text
10 months ago
22 kB
14
Indexable
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>The Histology Realms — Quest RPG</title>
<style>
:root{
--bg:#0f1724; --panel:#0b1220; --accent:#60a5fa; --muted:#9aa7b2;
--good:#7ee787; --bad:#ff7b7b; --card:#0c1622;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
*{box-sizing:border-box}
body{
margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
background: radial-gradient(1200px 600px at 10% 10%, rgba(96,165,250,0.06), transparent),
linear-gradient(180deg, #071024 0%, #081026 100%);
color:#e6eef6;
padding:24px;
}
.game {
width:960px; max-width:100%;
display:grid; grid-template-columns: 360px 1fr; gap:18px;
}
.panel {
background:linear-gradient(180deg,var(--panel) 0%, #041226 120%);
border-radius:12px; padding:18px; box-shadow: 0 6px 30px rgba(2,6,23,0.7);
border:1px solid rgba(255,255,255,0.03);
}
.left {display:flex; flex-direction:column; gap:12px; min-height:360px}
h1{font-size:20px; margin:0 0 8px 0}
.statbar{display:flex; gap:10px; align-items:center; justify-content:space-between}
.xpbar{height:12px; background:rgba(255,255,255,0.04); border-radius:8px; overflow:hidden; flex:1; margin-left:8px}
.xpfill{height:100%; background:linear-gradient(90deg,var(--accent), #7c3aed); width:0%; transition:width .6s ease}
.stat {font-size:13px; color:var(--muted)}
.inventory {display:flex; gap:8px; flex-wrap:wrap}
.item {background:rgba(255,255,255,0.02); padding:8px 10px; border-radius:8px; font-size:13px; color:var(--muted)}
.center {padding:18px;}
.story {min-height:180px; background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent); border-radius:10px; padding:12px; margin-bottom:12px}
.choices {display:flex; flex-direction:column; gap:10px}
.choice {background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); border-radius:10px; padding:12px; cursor:pointer; border:1px solid rgba(255,255,255,0.02); transition:transform .12s ease, box-shadow .12s ease}
.choice:hover{transform:translateY(-4px); box-shadow:0 8px 30px rgba(2,6,23,0.6)}
.choice.correct{outline: 2px solid rgba(126,231,135,0.18); box-shadow: 0 6px 18px rgba(34,197,94,0.06)}
.choice.wrong{outline: 2px solid rgba(255,123,123,0.12)}
.meta {font-size:13px; color:var(--muted); display:flex; justify-content:space-between; margin-top:8px}
.controls {display:flex; gap:8px; margin-top:10px}
button {background:var(--accent); color:#021025; border:none; padding:8px 12px; border-radius:8px; cursor:pointer; font-weight:600}
button.secondary{background:transparent; color:var(--muted); border:1px solid rgba(255,255,255,0.03)}
.log{font-size:13px; color:var(--muted); max-height:160px; overflow:auto; padding:8px; background:rgba(255,255,255,0.01); border-radius:8px}
.sigils{display:flex; gap:8px; margin-top:6px}
.sigil{padding:6px 8px; border-radius:8px; background:rgba(255,255,255,0.02); color:var(--muted); font-size:13px}
.center-right {display:flex; flex-direction:column; gap:12px}
.title-row{display:flex; gap:8px; align-items:center; justify-content:space-between}
.hint{font-size:13px; color:var(--muted)}
.footer{font-size:12px; color:var(--muted); text-align:right}
.flash {padding:8px; border-radius:8px; margin-bottom:10px; font-weight:700}
.good{background: linear-gradient(90deg, rgba(126,231,135,0.08), rgba(126,231,135,0.02)); color:var(--good)}
.bad{background: linear-gradient(90deg, rgba(255,123,123,0.06), rgba(255,123,123,0.02)); color:var(--bad)}
@media (max-width:880px){
.game{grid-template-columns:1fr}
.left{order:2}
.center-right{order:1}
}
</style>
</head>
<body>
<div class="game" role="application" aria-label="Histology Realms Game">
<div class="panel left" id="leftPanel">
<div>
<div class="title-row">
<h1>The Histology Realms</h1>
<div class="statbar" style="flex:0 0 auto;">
<div class="stat" id="level">Lvl 1</div>
</div>
</div>
<div style="display:flex; gap:10px; align-items:center; margin-top:10px">
<div style="min-width:110px">
<div class="stat">XP</div>
<div class="xpbar" title="XP progress">
<div class="xpfill" id="xpFill"></div>
</div>
</div>
<div style="flex:1">
<div class="stat">XP: <span id="xpText">0/10</span></div>
</div>
</div>
</div>
<div style="margin-top:12px;">
<div class="stat">Inventory</div>
<div class="inventory" id="inventoryArea">
<!-- items -->
</div>
<div style="margin-top:12px">
<div class="stat">Sigils</div>
<div class="sigils" id="sigilArea"></div>
</div>
</div>
<div style="margin-top:12px;">
<div class="stat">Quest Log</div>
<div class="log" id="logArea" aria-live="polite"></div>
</div>
<div style="margin-top:12px;">
<div class="stat">Controls</div>
<div class="controls">
<button id="saveBtn" title="Save progress">Save</button>
<button class="secondary" id="resetBtn" title="Reset progress">Reset</button>
<button class="secondary" id="helpBtn" title="How to play">How to play</button>
</div>
</div>
<div style="margin-top:12px;" class="footer">Progress auto-saves. Works offline.</div>
</div>
<div class="panel center-right" id="mainPanel">
<div class="center">
<div class="title-row">
<div>
<h1 id="sceneTitle">Welcome, Apprentice</h1>
<div class="hint" id="sceneHint">Begin your journey through Organia.</div>
</div>
<div style="text-align:right">
<div class="stat">XP per correct answer: <strong id="xpValue">3</strong></div>
<div style="height:8px"></div>
</div>
</div>
<div class="story" id="storyBox">
<div id="storyText">You arrive at the shimmering gates of Organia — a living world where tissue realms rule. Are you ready to begin the campaign?</div>
</div>
<div class="choices" id="choicesBox">
<!-- choice buttons injected -->
</div>
<div class="meta">
<div id="feedback" class="hint">Tip: Use intuition and memory — this is a learning quest!</div>
<div id="progressMeta" class="hint">Realms completed: <span id="realmsDone">0/4</span></div>
</div>
</div>
</div>
</div>
<script>
/* ===========================
Data and game configuration
=========================== */
const GAME_KEY = 'histology_realms_save_v1';
const CONFIG = {
xpPerCorrect: 3,
xpToLevel: (lvl) => 10 + (lvl-1)*10, // scaling xp requirement
};
/* Quest bank: each quest has id, title, scene, story, choices, correct index, xp reward, sigil to award */
const QUESTS = [
// Intro / gateway
{
id:'epithelia_intro',
realm: 'Epithelia',
title: 'Enter the Shielding City',
story: 'You walk through a glowing gateway into the Shielding City. A Gatekeeper asks: which epithelial guild is flat, thin, and allows rapid exchange (like lungs)?',
choices: ['Stratified squamous','Simple squamous','Cuboidal'],
correct:1,
explanation: 'Simple squamous epithelium is thin for rapid exchange (lungs, blood vessel lining).',
xp:3,
award:'Epithelial Sigil'
},
{
id:'epithelia_layers',
realm:'Epithelia',
title:'Gate of Layers',
story:'Which epithelial guild is layered and protects against abrasion (like skin)?',
choices:['Stratified squamous','Columnar','Cuboidal'],
correct:0,
explanation:'Stratified squamous epithelium provides protection where friction is common (skin, mouth, esophagus).',
xp:3
},
{
id:'epithelia_columnar',
realm:'Epithelia',
title:'Scribes of Absorption',
story:'Which guild is tall and good at absorption and secretion (like intestines)?',
choices:['Cuboidal','Columnar','Stratified squamous'],
correct:1,
explanation:'Columnar epithelia are tall cells adept at absorption and secretion (GI tract).',
xp:3
},
// Connective
{
id:'connect_dense',
realm:'Connectiva',
title:'The Fiber Loom',
story:'Which guild is like strong cords, forming tendons and ligaments?',
choices:['Loose connective','Dense connective','Cartilage'],
correct:1,
explanation:'Dense connective tissue has lots of collagen fibers for strength (tendons, ligaments).',
xp:3,
award:'Connective Sigil'
},
{
id:'connect_cartilage',
realm:'Connectiva',
title:'Cushion Keepers',
story:'Which guild is a smooth shock-absorber found at joints and ear tips?',
choices:['Cartilage','Bone','Loose connective'],
correct:0,
explanation:'Cartilage cushions joints and provides flexible support.',
xp:3
},
{
id:'connect_blood',
realm:'Connectiva',
title:'Rivers of Life',
story:'Which guild is fluid, carrying nutrients, wastes, and messengers?',
choices:['Cartilage','Blood','Bone'],
correct:1,
explanation:'Blood transports gases, nutrients, wastes, and immune cells.',
xp:3
},
// Muscular
{
id:'muscle_skeletal',
realm:'Musculora',
title:'Striated Knights',
story:'Which tribe is voluntary and striated, moving bones?',
choices:['Skeletal','Cardiac','Smooth'],
correct:0,
explanation:'Skeletal muscle is striated and under voluntary control.',
xp:3,
award:'Muscular Sigil'
},
{
id:'muscle_cardiac',
realm:'Musculora',
title:'Heartforge Paladins',
story:'Which tribe is striated but involuntary, found only in the heart?',
choices:['Skeletal','Cardiac','Smooth'],
correct:1,
explanation:'Cardiac muscle is striated and involuntary, located in the heart.',
xp:3
},
{
id:'muscle_smooth',
realm:'Musculora',
title:'Silent Blades',
story:'Which tribe is involuntary and not striated, lining hollow organs like the stomach?',
choices:['Skeletal','Cardiac','Smooth'],
correct:2,
explanation:'Smooth muscle is non-striated and involuntary in organs.',
xp:3
},
// Nervous
{
id:'neural_cells',
realm:'Neuralis',
title:'Lightning Mages',
story:'Which cells are the signal-senders carrying information as action potentials?',
choices:['Neurons','Neuroglia'],
correct:0,
explanation:'Neurons transmit electrical signals; neuroglia support them.',
xp:3,
award:'Nervous Sigil'
},
{
id:'neural_glia',
realm:'Neuralis',
title:'Keepers of the Tower',
story:'Which cells are the protectors and supporters, outnumbering the others ~10:1?',
choices:['Neurons','Neuroglia'],
correct:1,
explanation:'Neuroglia protect, nourish, and support neurons.',
xp:3
},
{
id:'neural_synapse',
realm:'Neuralis',
title:'Synapse Bridge',
story:'What is the gap where one neuron communicates with the next?',
choices:['Soma','Synapse','Axon'],
correct:1,
explanation:'The synapse is the junction (gap) where neurons communicate.',
xp:3
},
// Final integration boss (requires finishing earlier realms)
{
id:'final_integration',
realm:'Crown',
title:'The Crown of Integration',
story:'An organ is built from multiple tissues. Which pairing best explains how tissues combine in a typical organ (like the small intestine)?',
choices:[
'Epithelial lining + Connective support + Muscular layers + Nervous control',
'Bone + Cartilage + Blood only',
'Neurons only'
],
correct:0,
explanation:'Organs typically combine epithelial surfaces, connective scaffolding, smooth muscle layers, and nervous input to coordinate function.',
xp:6,
award:'Crown of Integration'
}
];
/* ===========================
Game state and persistence
=========================== */
let state = {
xp: 0,
level: 1,
inventory: [],
sigils: [],
log: [],
completedQuests: [],
currentQuestIndex: 0,
};
/* ===========================
Utility helpers
=========================== */
function saveState(){
localStorage.setItem(GAME_KEY, JSON.stringify(state));
flash('Progress saved', 'good');
}
function loadState(){
const raw = localStorage.getItem(GAME_KEY);
if(raw){
try {
const parsed = JSON.parse(raw);
Object.assign(state, parsed);
return true;
} catch(e){}
}
return false;
}
function resetState(){
if(!confirm('Reset progress? This will erase saved game.')) return;
localStorage.removeItem(GAME_KEY);
state = {
xp: 0, level:1, inventory:[], sigils:[], log:[], completedQuests:[], currentQuestIndex:0
};
renderAll();
flash('Progress reset', 'bad');
}
function addLog(text){
state.log.unshift(`[${new Date().toLocaleTimeString()}] ${text}`);
if(state.log.length>200) state.log.pop();
renderLog();
}
function flash(msg, kind='good'){
const fb = document.getElementById('feedback');
fb.textContent = msg;
fb.className = 'hint flash ' + (kind==='good' ? 'good' : 'bad');
setTimeout(()=> {
fb.textContent = 'Tip: Use intuition and memory — this is a learning quest!';
fb.className = 'hint';
}, 2500);
}
function xpRequiredForNext(level){
return CONFIG.xpToLevel(level);
}
function awardXP(amount){
state.xp += amount;
addLog(`Gained ${amount} XP.`);
checkLevelUp();
renderXP();
saveState();
}
function checkLevelUp(){
while(state.xp >= xpRequiredForNext(state.level)){
state.xp -= xpRequiredForNext(state.level);
state.level += 1;
addLog(`Leveled up! Now level ${state.level}.`);
flash(`LEVEL UP! You are now Level ${state.level}`, 'good');
}
}
function awardItem(name){
if(!state.inventory.includes(name)){
state.inventory.push(name);
addLog(`Received ${name}`);
renderInventory();
saveState();
}
}
function awardSigil(name){
if(!state.sigils.includes(name)){
state.sigils.push(name);
addLog(`Earned ${name}`);
renderSigils();
saveState();
}
}
/* ===========================
Rendering functions
=========================== */
function renderXP(){
const req = xpRequiredForNext(state.level);
const percent = Math.round((state.xp / req) * 100);
document.getElementById('xpFill').style.width = Math.min(100, Math.max(0, percent)) + '%';
document.getElementById('xpText').textContent = `${state.xp}/${req}`;
document.getElementById('level').textContent = `Lvl ${state.level}`;
document.getElementById('xpValue').textContent = CONFIG.xpPerCorrect;
}
function renderInventory(){
const area = document.getElementById('inventoryArea');
area.innerHTML = '';
if(state.inventory.length === 0){
area.innerHTML = '<div class="item" style="opacity:.5">Empty satchel</div>';
return;
}
state.inventory.forEach(i => {
const d = document.createElement('div');
d.className='item';
d.textContent = i;
area.appendChild(d);
});
}
function renderSigils(){
const area = document.getElementById('sigilArea');
area.innerHTML = '';
if(state.sigils.length === 0) {
area.innerHTML = '<div class="sigil" style="opacity:.5">No sigils yet</div>';
return;
}
state.sigils.forEach(s => {
const d = document.createElement('div');
d.className='sigil';
d.textContent = s;
area.appendChild(d);
});
}
function renderLog(){
const area = document.getElementById('logArea');
area.innerHTML = '';
if(state.log.length === 0) area.innerHTML = '<div style="opacity:.6">Your quest log is empty — begin exploring.</div>';
state.log.slice(0,80).forEach(line => {
const p = document.createElement('div');
p.textContent = line;
area.appendChild(p);
});
}
function renderScene(){
// choose the next incomplete quest by default. If all done, show finale available if not completed.
const doneRealms = new Set(QUESTS.filter(q => state.completedQuests.includes(q.id)).map(q=>q.realm));
document.getElementById('realmsDone').textContent = `${doneRealms.size}/4`;
// find next quest not completed; but for final, require at least 3 realms completed
let q = QUESTS.find(q => !state.completedQuests.includes(q.id));
// special: ensure final appears only if > = 3 realms finished
const realmsFinished = ['Epithelia','Connectiva','Musculora','Neuralis'].filter(r => Array.from(doneRealms).includes(r)).length;
const finalIndex = QUESTS.findIndex(x=>x.id==='final_integration');
if(finalIndex >=0){
const final = QUESTS[finalIndex];
if(realmsFinished >= 3 && !state.completedQuests.includes(final.id)){
q = final;
} else if(state.completedQuests.includes(final.id)){
// everything complete
q = null;
} else {
// ensure final does not appear prematurely
if(q && q.id === 'final_integration') {
q = QUESTS.find(q2 => !state.completedQuests.includes(q2.id) && q2.id !== 'final_integration');
}
}
}
const title = document.getElementById('sceneTitle');
const hint = document.getElementById('sceneHint');
const storyBox = document.getElementById('storyText');
const choicesBox = document.getElementById('choicesBox');
choicesBox.innerHTML = '';
if(!q){
title.textContent = 'Master of Organia';
hint.textContent = 'You have united the realms.';
storyBox.innerHTML = `
You stand before the Crown of Integration. You mastered the Realms and can now explain how tissues cooperate to build organs.
<div style="margin-top:12px" class="meta">Want to replay quests or reset to practice more?</div>`;
const replay = document.createElement('button');
replay.textContent = 'Replay Quests';
replay.onclick = () => {
state.completedQuests = [];
state.inventory = [];
state.sigils = [];
state.xp = 0;
state.level = 1;
state.log = [];
renderAll();
flash('Replay started — good luck!', 'good');
saveState();
};
choicesBox.appendChild(replay);
return;
}
title.textContent = q.title;
hint.textContent = `${q.realm} — Quest`;
storyBox.textContent = q.story;
q.choices.forEach((opt, idx) => {
const c = document.createElement('div');
c.className = 'choice';
c.tabIndex = 0;
c.role = 'button';
c.setAttribute('aria-pressed','false');
c.innerHTML = `<div style="font-weight:700">${String.fromCharCode(65+idx)}.</div> <div style="margin-top:6px; color:var(--muted)">${opt}</div>`;
c.onclick = () => onSelect(q, idx, c);
c.onkeydown = (ev) => { if(ev.key === 'Enter' || ev.key === ' ') c.click(); };
choicesBox.appendChild(c);
});
}
/* ===========================
Gameplay interactions
=========================== */
function onSelect(quest, choiceIndex, btnElement){
// prevent double answers
if(state.completedQuests.includes(quest.id)) {
flash('Quest already completed', 'bad');
return;
}
// mark choices visually
const allChoices = document.querySelectorAll('.choice');
allChoices.forEach((el, i) => {
el.classList.remove('correct','wrong');
el.style.pointerEvents = 'none';
});
const correct = choiceIndex === quest.correct;
const chosen = allChoices[choiceIndex];
if(correct){
chosen.classList.add('correct');
awardXP(quest.xp);
addLog(`Quest "${quest.title}" completed. +${quest.xp} XP.`);
flash(`Correct! +${quest.xp} XP`, 'good');
// award item or sigil if present
if(quest.award){
// award crown or sigils differently
if(quest.award.includes('Sigil')) awardSigil(quest.award);
else awardItem(quest.award);
}
// show explanation
setTimeout(()=> {
showExplanation(quest.explanation, true);
state.completedQuests.push(quest.id);
renderScene();
saveState();
}, 700);
} else {
chosen.classList.add('wrong');
// show correct answer highlight
const correctEl = allChoices[quest.correct];
correctEl.classList.add('correct');
addLog(`Quest "${quest.title}" failed (answer: ${String.fromCharCode(65+quest.correct)}).`);
flash('Not quite — read the explanation to learn', 'bad');
setTimeout(()=> {
showExplanation(quest.explanation, false);
// still mark quest as completed? No — allow retry. But give small XP for attempt? We'll not give XP on wrong.
// allow retry: re-enable choices except the wrong one
allChoices.forEach(el => el.style.pointerEvents = 'auto');
}, 700);
}
}
// show explanation overlay within story area
function showExplanation(text, success){
const storyBox = document.getElementById('storyText');
storyBox.innerHTML = `<div style="font-weight:700">${success ? 'Correct!' : 'Explanation'}</div>
<div style="margin-top:8px; color:var(--muted)">${text}</div>`;
// small hint to move on
const cont = document.createElement('div');
cont.style.marginTop = '12px';
const nextBtn = document.createElement('button');
nextBtn.textContent = 'Continue';
nextBtn.onclick = () => {
renderScene();
};
cont.appendChild(nextBtn);
storyBox.appendChild(cont);
}
/* ===========================
Controls
=========================== */
document.getElementById('saveBtn').onclick = saveState;
document.getElementById('resetBtn').onclick = resetState;
document.getElementById('helpBtn').onclick = () => {
alert(
`How to play:
- Read the story and choose the answer you think is correct.
- Correct answers award XP and may grant items or sigils.
- Gain XP to level up. Progress auto-saves to your browser.
- Finish the Realms to face the Crown of Integration (final challenge).
This is designed as a study tool — if you get something wrong, read the explanation and try again.`
);
};
/* ===========================
Startup / restore
=========================== */
function renderAll(){
renderXP();
renderInventory();
renderSigils();
renderLog();
renderScene();
}
if(loadState()){
addLog('Loaded saved progress.');
} else {
addLog('New journey begins.');
}
// initial rendering
renderAll();
</script>
</body>
</html>Editor is loading...
Leave a Comment