Untitled

 avatar
unknown
plain_text
10 months ago
22 kB
12
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