Skip to content

Instantly share code, notes, and snippets.

@turlockmike
Last active March 17, 2026 01:56
Show Gist options
  • Select an option

  • Save turlockmike/2dea69238ea5ed127c9038e38acb9414 to your computer and use it in GitHub Desktop.

Select an option

Save turlockmike/2dea69238ea5ed127c9038e38acb9414 to your computer and use it in GitHub Desktop.
Eevee's Berry Farm - A peaceful 3D farming game with all eeveelutions
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eevee's Berry Farm 3D</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Patrick+Hand&display=swap');
:root{--eevee:#c4956a;--card:rgba(255,255,255,0.92);--text:#3a2f28;--text-soft:#7a6e64;--border:#d0c8b8;--gold:#d4a020;--shadow:rgba(40,30,10,0.12);}
*{margin:0;padding:0;box-sizing:border-box;}
body{font-family:'Fredoka',sans-serif;background:#000;color:var(--text);overflow:hidden;height:100vh;}
#gameCanvas{position:fixed;inset:0;z-index:0;}
/* HUD */
.hud{position:fixed;z-index:20;pointer-events:none;}
.hud>*{pointer-events:auto;}
.hud-top{top:10px;left:10px;right:10px;display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:8px;}
.player-card{background:var(--card);backdrop-filter:blur(12px);border:2px solid var(--border);border-radius:16px;padding:10px 16px;display:flex;align-items:center;gap:12px;box-shadow:0 4px 20px var(--shadow);}
.pc-avatar{width:48px;height:48px;border-radius:50%;font-size:1.8rem;display:flex;align-items:center;justify-content:center;border:3px solid var(--eevee);background:linear-gradient(135deg,#fdf6ee,#f0e4d0);flex-shrink:0;}
.pc-name{font-weight:700;font-size:0.95rem;}
.pc-type{font-size:0.68rem;color:var(--text-soft);}
.xp-wrap{height:6px;background:#e0d8cc;border-radius:3px;margin-top:3px;overflow:hidden;width:120px;}
.xp-fill{height:100%;border-radius:3px;background:linear-gradient(90deg,var(--eevee),#d4a848);transition:width 0.5s;}
.stats-bar{display:flex;gap:8px;flex-wrap:wrap;}
.stat{background:var(--card);backdrop-filter:blur(12px);border:2px solid var(--border);border-radius:12px;padding:5px 12px;font-weight:600;font-size:0.8rem;box-shadow:0 3px 12px var(--shadow);display:flex;align-items:center;gap:4px;}
/* Toolbar */
.hud-bottom{bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:6px;flex-wrap:wrap;justify-content:center;}
.tool-btn{background:var(--card);backdrop-filter:blur(12px);border:2.5px solid var(--border);border-radius:12px;padding:8px 14px;font-family:'Fredoka';font-weight:600;font-size:0.82rem;cursor:pointer;transition:all 0.15s;display:flex;align-items:center;gap:4px;box-shadow:0 3px 12px var(--shadow);}
.tool-btn:hover{transform:translateY(-2px);box-shadow:0 6px 16px var(--shadow);}
.tool-btn.active{border-color:var(--gold);background:#fff8e0;box-shadow:0 0 0 3px rgba(212,160,32,0.25);}
.tool-icon{font-size:1.1rem;}
/* Seed bar */
.hud-seeds{bottom:60px;left:50%;transform:translateX(-50%);display:flex;gap:4px;flex-wrap:wrap;justify-content:center;}
.seed-btn{background:var(--card);backdrop-filter:blur(10px);border:2px solid var(--border);border-radius:9px;padding:4px 9px;font-family:'Fredoka';font-size:0.72rem;font-weight:600;cursor:pointer;transition:all 0.12s;display:flex;align-items:center;gap:3px;box-shadow:0 2px 8px var(--shadow);}
.seed-btn:hover{transform:translateY(-1px);}
.seed-btn.active{border-color:var(--gold);background:#fff8e0;}
/* Side panels */
.side-panel{position:fixed;top:0;right:-400px;width:380px;max-width:90vw;height:100vh;background:var(--card);backdrop-filter:blur(16px);border-left:2px solid var(--border);z-index:50;transition:right 0.3s cubic-bezier(.34,1.2,.64,1);overflow-y:auto;padding:20px;box-shadow:-8px 0 32px var(--shadow);}
.side-panel.open{right:0;}
.panel-close{position:absolute;top:12px;right:14px;background:none;border:none;font-size:1.4rem;cursor:pointer;color:var(--text-soft);z-index:2;}
.panel-close:hover{color:var(--text);}
.panel-title{font-family:'Patrick Hand',cursive;font-size:1.6rem;margin-bottom:14px;padding-right:30px;}
/* Side buttons */
.hud-right{top:80px;right:10px;display:flex;flex-direction:column;gap:6px;}
.side-btn{background:var(--card);backdrop-filter:blur(12px);border:2px solid var(--border);border-radius:12px;padding:8px 14px;font-family:'Fredoka';font-weight:600;font-size:0.82rem;cursor:pointer;transition:all 0.15s;box-shadow:0 3px 12px var(--shadow);display:flex;align-items:center;gap:5px;}
.side-btn:hover{transform:translateX(-3px);box-shadow:0 6px 16px var(--shadow);}
/* Village */
.village-grid{display:grid;grid-template-columns:1fr;gap:10px;}
.v-card{background:#f8f4ee;border:2px solid var(--border);border-radius:14px;padding:12px;display:flex;align-items:center;gap:12px;cursor:pointer;transition:all 0.2s;}
.v-card:hover{transform:translateX(4px);border-color:var(--gold);box-shadow:0 4px 16px var(--shadow);}
.v-emoji{font-size:2rem;width:48px;height:48px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.v-info{flex:1;}.v-name{font-weight:700;font-size:0.88rem;}.v-role{font-size:0.68rem;color:var(--text-soft);}
.v-hearts{font-size:0.72rem;margin-top:2px;}
/* Shop */
.shop-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.shop-item{background:#f8f4ee;border:2px solid var(--border);border-radius:12px;padding:10px;text-align:center;cursor:pointer;transition:all 0.2s;}
.shop-item:hover{border-color:var(--gold);transform:translateY(-2px);}
.shop-icon{font-size:1.6rem;}.shop-name{font-weight:700;font-size:0.78rem;margin:2px 0;}
.shop-price{color:var(--gold);font-weight:700;font-size:0.78rem;}.shop-desc{color:var(--text-soft);font-size:0.65rem;}
/* Affinity */
.aff-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.aff-card{background:#f8f4ee;border:2px solid var(--border);border-radius:10px;padding:8px;text-align:center;}
.aff-card.lead{border-color:var(--gold);background:#fff8e0;}
.aff-icon{font-size:1.3rem;}.aff-name{font-weight:700;font-size:0.72rem;}
.aff-bar-w{height:5px;background:#e0d8cc;border-radius:3px;margin-top:3px;overflow:hidden;}
.aff-bar{height:100%;border-radius:3px;transition:width 0.5s;}
.aff-pts{font-size:0.65rem;color:var(--text-soft);margin-top:2px;}
.evolve-btn{display:block;margin:14px auto 0;padding:11px 26px;background:linear-gradient(135deg,#f0c848,#e8a030,#f0c848);background-size:200% 200%;animation:shimmer 2s ease infinite;color:#5a3e10;border:3px solid #d4a020;border-radius:14px;font-family:'Fredoka';font-weight:700;font-size:0.95rem;cursor:pointer;transition:all 0.2s;box-shadow:0 4px 20px rgba(212,160,32,0.3);}
@keyframes shimmer{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
.evolve-btn:hover{transform:translateY(-2px) scale(1.03);}
.evolve-btn:disabled{opacity:0.5;cursor:default;transform:none !important;}
/* Dialog */
.dialog-overlay{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,0.5);backdrop-filter:blur(5px);display:none;align-items:center;justify-content:center;padding:20px;}
.dialog-overlay.open{display:flex;}
.dialog{background:white;border-radius:20px;padding:22px;max-width:440px;width:100%;box-shadow:0 16px 56px rgba(0,0,0,0.25);animation:dIn 0.3s cubic-bezier(.34,1.56,.64,1);position:relative;max-height:85vh;overflow-y:auto;}
@keyframes dIn{0%{transform:scale(0.85) translateY(20px);opacity:0}100%{transform:scale(1);opacity:1}}
.d-close{position:absolute;top:10px;right:14px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--text-soft);}
.d-avatar{width:64px;height:64px;border-radius:50%;font-size:2.4rem;display:flex;align-items:center;justify-content:center;margin:0 auto 8px;}
.d-name{text-align:center;font-weight:700;font-size:1.1rem;}
.d-type{text-align:center;font-size:0.72rem;color:var(--text-soft);margin-bottom:8px;}
.d-text{background:#f6f2ea;border-radius:12px;padding:10px 14px;font-family:'Patrick Hand',cursive;font-size:1rem;line-height:1.4;margin-bottom:8px;}
.d-gift-result{text-align:center;padding:7px;background:#f0ffe0;border-radius:10px;font-weight:600;font-size:0.82rem;margin-bottom:8px;}
.d-section{font-weight:700;font-size:0.78rem;margin:8px 0 5px;color:var(--text-soft);}
.gift-grid{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;margin-bottom:8px;}
.gift-btn{background:#f6f2ea;border:2px solid var(--border);border-radius:9px;padding:5px 9px;font-family:'Fredoka';font-size:0.72rem;font-weight:600;cursor:pointer;transition:all 0.12s;display:flex;align-items:center;gap:3px;}
.gift-btn:hover{transform:translateY(-1px);border-color:var(--gold);background:#fff8e0;}
.gift-btn:disabled{opacity:0.3;cursor:default;transform:none !important;}
.d-btn{display:block;width:100%;margin-top:8px;background:linear-gradient(135deg,var(--eevee),#b08050);color:white;border:none;border-radius:12px;padding:9px;font-family:'Fredoka';font-weight:700;font-size:0.88rem;cursor:pointer;}
/* Controls hint */
.controls{position:fixed;bottom:100px;left:10px;z-index:20;background:var(--card);backdrop-filter:blur(12px);border:2px solid var(--border);border-radius:12px;padding:8px 12px;font-size:0.7rem;color:var(--text-soft);box-shadow:0 3px 12px var(--shadow);line-height:1.6;}
.controls kbd{background:#e8e4da;border:1px solid var(--border);border-radius:3px;padding:0 4px;font-family:'Fredoka';font-size:0.68rem;}
/* Messages */
.msg-area{position:fixed;top:80px;right:10px;z-index:30;display:flex;flex-direction:column;gap:5px;pointer-events:none;}
.msg{background:rgba(255,255,255,0.95);backdrop-filter:blur(10px);border:2px solid var(--border);border-radius:12px;padding:7px 14px;font-weight:600;font-size:0.8rem;box-shadow:0 4px 16px var(--shadow);max-width:260px;animation:msgIn 0.3s,msgOut 0.3s 2.2s forwards;}
@keyframes msgIn{from{opacity:0;transform:translateX(20px)}}
@keyframes msgOut{to{opacity:0;transform:translateY(-8px)}}
/* Ceremony */
.ceremony{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,0.9);display:none;align-items:center;justify-content:center;flex-direction:column;}
.ceremony.active{display:flex;}
.evo-big{font-size:5rem;margin-bottom:12px;animation:evoPulse 0.5s ease-out;}
@keyframes evoPulse{0%{transform:scale(0.5);opacity:0}100%{transform:scale(1);opacity:1}}
.evo-msg{color:white;font-family:'Patrick Hand',cursive;font-size:1.6rem;margin-bottom:6px;}
.evo-sub{color:rgba(255,255,255,0.6);font-size:0.9rem;margin-bottom:16px;}
.evo-ok{background:rgba(255,255,255,0.15);border:2px solid rgba(255,255,255,0.3);color:white;border-radius:12px;padding:10px 24px;font-family:'Fredoka';font-weight:700;cursor:pointer;}
.sparkle-layer{position:absolute;inset:0;pointer-events:none;overflow:hidden;}
.sparkle{position:absolute;font-size:1.3rem;animation:sparkAnim 1.5s ease-out infinite;}
@keyframes sparkAnim{0%{transform:translateY(0) scale(0);opacity:0}30%{opacity:1;transform:translateY(-25px) scale(1)}100%{opacity:0;transform:translateY(-100px) scale(0.5) rotate(180deg)}}
/* Sleep */
.sleep-ov{position:fixed;inset:0;z-index:150;background:black;display:none;align-items:center;justify-content:center;flex-direction:column;}
.sleep-ov.active{display:flex;animation:sleepAnim 2.2s ease-in-out;}
@keyframes sleepAnim{0%{opacity:0}15%{opacity:1}85%{opacity:1}100%{opacity:0}}
.sleep-txt{color:white;font-family:'Patrick Hand';font-size:1.8rem;}
.sleep-stars{color:#f0e070;font-size:1.3rem;margin-top:6px;}
/* Mobile dpad */
.dpad{position:fixed;bottom:110px;right:10px;z-index:20;display:none;}
.dpad-grid{display:grid;grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);}
.dpad-btn{background:var(--card);border:2px solid var(--border);border-radius:10px;font-size:1.1rem;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none;box-shadow:0 2px 8px var(--shadow);}
.dpad-btn:active{background:#fff8e0;transform:scale(0.9);}
.dpad-blank{visibility:hidden;}
.dpad-center{font-size:0.6rem;font-weight:700;color:var(--text-soft);}
@media(max-width:768px){.dpad{display:block;}.controls{display:none;}}
@media(pointer:coarse){.dpad{display:block;}.controls{display:none;}}
/* NPC label */
.npc-label{position:fixed;z-index:15;pointer-events:none;background:rgba(255,255,255,0.9);padding:2px 8px;border-radius:8px;font-size:0.65rem;font-weight:700;white-space:nowrap;box-shadow:0 2px 6px rgba(0,0,0,0.1);transform:translate(-50%,-100%);transition:opacity 0.2s;}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<!-- HUD TOP -->
<div class="hud hud-top">
<div class="player-card">
<div class="pc-avatar" id="pcAvatar">🦊</div>
<div>
<div class="pc-name" id="pcName">Eevee</div>
<div class="pc-type" id="pcType">Normal Type</div>
<div class="xp-wrap"><div class="xp-fill" id="xpFill" style="width:0%"></div></div>
</div>
</div>
<div class="stats-bar">
<div class="stat">📅 Day <span id="sDay">1</span></div>
<div class="stat">💰 <span id="sGold">80</span></div>
<div class="stat">⭐ Lv.<span id="sLvl">1</span></div>
</div>
</div>
<!-- HUD BOTTOM - Tools -->
<div class="hud hud-seeds" id="hudSeeds"></div>
<div class="hud hud-bottom" id="hudTools"></div>
<!-- HUD RIGHT - Panels -->
<div class="hud hud-right">
<button class="side-btn" onclick="togglePanel('village')">🏘️ Village</button>
<button class="side-btn" onclick="togglePanel('shop')">🛒 Shop</button>
<button class="side-btn" onclick="togglePanel('evo')">✨ Evolve</button>
<button class="side-btn" onclick="doSleep()">🌙 Sleep</button>
</div>
<!-- Controls -->
<div class="controls">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> Move<br>
<kbd>Space</kbd> Use tool / Talk<br>
<kbd>1</kbd>-<kbd>4</kbd> Switch tools<br>
<kbd>Q</kbd><kbd>E</kbd> Rotate camera
</div>
<!-- Mobile dpad -->
<div class="dpad" id="dpad">
<div class="dpad-grid">
<div class="dpad-blank"></div>
<div class="dpad-btn" data-dir="up">⬆️</div>
<div class="dpad-blank"></div>
<div class="dpad-btn" data-dir="left">⬅️</div>
<div class="dpad-btn dpad-center" onclick="doAction()">USE</div>
<div class="dpad-btn" data-dir="right">➡️</div>
<div class="dpad-blank"></div>
<div class="dpad-btn" data-dir="down">⬇️</div>
<div class="dpad-blank"></div>
</div>
</div>
<!-- Side Panels -->
<div class="side-panel" id="panelVillage">
<button class="panel-close" onclick="closeAllPanels()">✕</button>
<div class="panel-title">🏘️ Eeveelution Village</div>
<div class="village-grid" id="villageGrid"></div>
</div>
<div class="side-panel" id="panelShop">
<button class="panel-close" onclick="closeAllPanels()">✕</button>
<div class="panel-title">🛒 Berry Shop</div>
<div class="shop-grid" id="shopGrid"></div>
</div>
<div class="side-panel" id="panelEvo">
<button class="panel-close" onclick="closeAllPanels()">✕</button>
<div class="panel-title">✨ Evolution Path</div>
<p style="font-size:0.78rem;color:var(--text-soft);margin-bottom:12px;">Grow berries & give gifts to build affinity. Level 8 + 100 affinity to evolve!</p>
<div class="aff-grid" id="affGrid"></div>
<button class="evolve-btn" id="evoBtn" disabled onclick="startEvo()">✨ Evolve! ✨</button>
<p id="evoStatus" style="text-align:center;font-size:0.72rem;color:var(--text-soft);margin-top:8px;"></p>
</div>
<div class="msg-area" id="msgArea"></div>
<!-- Dialog -->
<div class="dialog-overlay" id="dialogOverlay">
<div class="dialog">
<button class="d-close" onclick="closeDialog()">✕</button>
<div class="d-avatar" id="dAvatar"></div>
<div class="d-name" id="dName"></div>
<div class="d-type" id="dType"></div>
<div class="d-text" id="dText"></div>
<div class="d-gift-result" id="dGiftResult" style="display:none"></div>
<div class="d-section">🎁 Give a Gift</div>
<div class="gift-grid" id="giftGrid"></div>
<div class="d-section" id="dDailyLabel"></div>
<div class="d-gift-result" id="dDaily" style="display:none"></div>
<button class="d-btn" onclick="closeDialog()">See you later! 💕</button>
</div>
</div>
<div class="sleep-ov" id="sleepOv"><div class="sleep-txt">💤 Zzz...</div><div class="sleep-stars">⭐ 🌙 ⭐</div></div>
<div class="ceremony" id="ceremony">
<div class="sparkle-layer" id="sparkles"></div>
<div class="evo-stage" id="evoStage"></div>
</div>
<script>
// ======================== DATA ========================
const EVOS={
eevee:{name:'Eevee',emoji:'🦊',type:'Normal',color:0xc4956a,hexColor:'#c4956a'},
vaporeon:{name:'Vaporeon',emoji:'🐳',type:'Water',color:0x6bb5e0,hexColor:'#6bb5e0',berry:'oran',role:'Berry Washer',fav:'oran',
lines:["The river's so peaceful today...","Water berries grow best near streams!","Your farm looks wonderful!"],
giftLove:["Oh! An Oran Berry! My favorite!","You know me so well~"],giftLike:["A berry for me? Sweet!"],giftReact:["A gift! Thanks~"]},
jolteon:{name:'Jolteon',emoji:'⚡',type:'Electric',color:0xf5d94e,hexColor:'#f5d94e',berry:'cheri',role:'Delivery Runner',fav:'cheri',
lines:["Zoom zoom!","Cheri Berries are the best!","Can't stop won't stop!"],
giftLove:["CHERI BERRIES! *zaps*","YES! My fav!"],giftLike:["Ooh! *vibrates*"],giftReact:["For ME?! *sparks*"]},
flareon:{name:'Flareon',emoji:'🔥',type:'Fire',color:0xe87040,hexColor:'#e87040',berry:'rawst',role:'Berry Dryer',fav:'rawst',
lines:["Berry jam by the fire~","Rawst Berries are amazing!","My fluff keeps everything warm!"],
giftLove:["Rawst Berry!! Perfect~","My favorite! So thoughtful!"],giftLike:["I'll toast it gently~"],giftReact:["*tail wags warmly*"]},
espeon:{name:'Espeon',emoji:'🔮',type:'Psychic',color:0xd4a0d0,hexColor:'#d4a0d0',berry:'pecha',role:'Fortune Teller',fav:'pecha',
lines:["I sense a great harvest!","Pecha Berries have a calming aura~","Beautiful visions today."],
giftLove:["A Pecha Berry! I sensed it~","The aura is strong!"],giftLike:["I foresaw your kindness~"],giftReact:["*gem glows softly*"]},
umbreon:{name:'Umbreon',emoji:'🌙',type:'Dark',color:0x3a3a5c,hexColor:'#3a3a5c',berry:'wiki',role:'Night Guard',fav:'wiki',
lines:["All clear tonight.","Wiki Berries bloom under moonlight...","I've got your farm covered."],
giftLove:["A Wiki Berry... Thanks.","*rings glow brighter*"],giftLike:["*quiet smile* Thanks."],giftReact:["*rings pulse softly*"]},
leafeon:{name:'Leafeon',emoji:'🍃',type:'Grass',color:0x7db858,hexColor:'#7db858',berry:'sitrus',role:'Garden Expert',fav:'sitrus',
lines:["Your soil is excellent!","Sitrus Berries are miracles!","Photosynthesis feels great~"],
giftLove:["Sitrus Berry! Nature's gift!","I can feel it growing!"],giftLike:["From your garden! Beautiful~"],giftReact:["*leaves rustle*"]},
glaceon:{name:'Glaceon',emoji:'❄️',type:'Ice',color:0x8ad0d8,hexColor:'#8ad0d8',berry:'aspear',role:'Berry Preserver',fav:'aspear',
lines:["Froze berries for storage!","Aspear = mountain breeze~","My ice crystals are pretty!"],
giftLove:["Aspear Berry! Cool perfection!","I'll preserve it in crystal~"],giftLike:["How refreshing!"],giftReact:["*tiny snowflake*"]},
sylveon:{name:'Sylveon',emoji:'🎀',type:'Fairy',color:0xf0a0b8,hexColor:'#f0a0b8',berry:'mago',role:'Berry Baker',fav:'mago',
lines:["Berry pies for everyone!","Mago = perfect fairy cakes!","Berry picnic soon!"],
giftLove:["Mago Berry! For my recipe!","I'll bake you something!"],giftLike:["How adorable~"],giftReact:["*ribbons dance*"]}
};
const BERRIES={
oran:{name:'Oran Berry',emoji:'🫐',color:0x5588cc,type:'Water',grow:3,sell:8,cost:3,xp:5,aff:'vaporeon'},
cheri:{name:'Cheri Berry',emoji:'🍒',color:0xe05050,type:'Electric',grow:2,sell:6,cost:2,xp:4,aff:'jolteon'},
rawst:{name:'Rawst Berry',emoji:'🍊',color:0xe87040,type:'Fire',grow:4,sell:12,cost:5,xp:7,aff:'flareon'},
pecha:{name:'Pecha Berry',emoji:'🍑',color:0xd4a0d0,type:'Psychic',grow:3,sell:10,cost:4,xp:6,aff:'espeon'},
wiki:{name:'Wiki Berry',emoji:'🍇',color:0x6060a0,type:'Dark',grow:5,sell:16,cost:7,xp:10,aff:'umbreon'},
sitrus:{name:'Sitrus Berry',emoji:'🍋',color:0xa0c040,type:'Grass',grow:3,sell:9,cost:3,xp:5,aff:'leafeon'},
aspear:{name:'Aspear Berry',emoji:'🍐',color:0x80c8d0,type:'Ice',grow:4,sell:11,cost:4,xp:6,aff:'glaceon'},
mago:{name:'Mago Berry',emoji:'🍓',color:0xf0a0b8,type:'Fairy',grow:3,sell:10,cost:4,xp:6,aff:'sylveon'}
};
const COLS=18,ROWS=14;
// Map: F=farm, P=path, W=water, T=tree
const MAP=[
'TTTTTTPPPPPPTTTTTT',
'TTTPPPFFFFFFPPTTTT',
'TTPFFFFFFFFFFFFFTT',
'TPPFFFFFFFFFFFFFTT',
'TPFFFFFFFFFPPPTTTT',
'TPFFFFFFFFFPTWWTTT',
'TPFFFFFFFFFPTWWWTT',
'TPFFFFFFFFFPPWWWTT',
'TPPFFFFFFFFPTWWTTT',
'TTPFFFFFFFFPTTTTTT',
'TTPPPFFFFFPPTTTTTT',
'TTTPPPPPPPPTTTTTTT',
'TTTTTPPPPPTTTTTTTT',
'TTTTTTTTTTTTTTTTTT',
];
// ======================== STATE ========================
const G={
day:1,gold:80,xp:0,level:1,xpNext:15,tool:'hoe',seed:'oran',
evolved:false,evolvedTo:null,
px:7,pz:12, // player grid pos
plots:{},
seeds:{oran:5,cheri:3,rawst:0,pecha:0,wiki:0,sitrus:0,aspear:0,mago:0},
harvest:{oran:0,cheri:0,rawst:0,pecha:0,wiki:0,sitrus:0,aspear:0,mago:0},
affinity:{vaporeon:0,jolteon:0,flareon:0,espeon:0,umbreon:0,leafeon:0,glaceon:0,sylveon:0},
friendship:{vaporeon:0,jolteon:0,flareon:0,espeon:0,umbreon:0,leafeon:0,glaceon:0,sylveon:0},
talkedToday:{},giftedToday:{},
camAngle:Math.PI*0.25
};
for(let z=0;z<ROWS;z++) for(let x=0;x<COLS;x++){
if(MAP[z][x]==='F') G.plots[x+','+z]={s:'grass',crop:null,g:0,w:false};
}
const NPC_DATA=[
{key:'vaporeon',x:11,z:5},{key:'jolteon',x:4,z:11},{key:'flareon',x:1,z:4},
{key:'espeon',x:13,z:1},{key:'umbreon',x:9,z:11},{key:'leafeon',x:6,z:11},
{key:'glaceon',x:12,z:7},{key:'sylveon',x:8,z:0}
];
const npcs=NPC_DATA.map(n=>({...n,homeX:n.x,homeZ:n.z,mesh:null,labelEl:null}));
// ======================== THREE.JS SETUP ========================
let scene,camera,renderer,clock;
let playerMesh,playerGroup;
let tileMeshes={},cropMeshes={};
let npcMeshes=[];
let sunLight,ambientLight;
const keys={};
let moveTimer=0;
function initThree(){
scene=new THREE.Scene();
scene.background=new THREE.Color(0x88c8e8);
scene.fog=new THREE.Fog(0x88c8e8,40,80);
camera=new THREE.PerspectiveCamera(45,window.innerWidth/window.innerHeight,0.1,100);
renderer=new THREE.WebGLRenderer({canvas:document.getElementById('gameCanvas'),antialias:true});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
clock=new THREE.Clock();
// Lighting
ambientLight=new THREE.AmbientLight(0xffffff,0.5);
scene.add(ambientLight);
sunLight=new THREE.DirectionalLight(0xfff0d0,0.9);
sunLight.position.set(8,12,4);
sunLight.castShadow=true;
sunLight.shadow.mapSize.set(2048,2048);
sunLight.shadow.camera.left=-20;sunLight.shadow.camera.right=20;
sunLight.shadow.camera.top=20;sunLight.shadow.camera.bottom=-20;
sunLight.shadow.camera.near=0.1;sunLight.shadow.camera.far=60;
scene.add(sunLight);
const hemiLight=new THREE.HemisphereLight(0x88bbff,0x445522,0.3);
scene.add(hemiLight);
buildWorld();
buildPlayer();
buildNPCs();
updateCamera();
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
}
// ======================== WORLD BUILD ========================
function buildWorld(){
// Base ground
const groundGeo=new THREE.PlaneGeometry(80,80);
const groundMat=new THREE.MeshStandardMaterial({color:0x5a8a30});
const ground=new THREE.Mesh(groundGeo,groundMat);
ground.rotation.x=-Math.PI/2;ground.position.y=-0.01;ground.receiveShadow=true;
scene.add(ground);
// Build tiles
for(let z=0;z<ROWS;z++) for(let x=0;x<COLS;x++){
const c=MAP[z][x];
const px=x-COLS/2+0.5, pz=z-ROWS/2+0.5;
if(c==='F'){
const geo=new THREE.BoxGeometry(0.95,0.3,0.95);
const mat=new THREE.MeshStandardMaterial({color:0x6aaa38});
const mesh=new THREE.Mesh(geo,mat);
mesh.position.set(px,0.15,pz);
mesh.castShadow=true;mesh.receiveShadow=true;
mesh.userData={type:'farm',gx:x,gz:z};
scene.add(mesh);
tileMeshes[x+','+z]=mesh;
} else if(c==='P'){
const geo=new THREE.BoxGeometry(0.95,0.15,0.95);
const mat=new THREE.MeshStandardMaterial({color:0xc0a878});
const mesh=new THREE.Mesh(geo,mat);
mesh.position.set(px,0.075,pz);mesh.receiveShadow=true;
scene.add(mesh);
} else if(c==='W'){
const geo=new THREE.BoxGeometry(0.95,0.1,0.95);
const mat=new THREE.MeshStandardMaterial({color:0x4499cc,transparent:true,opacity:0.8});
const mesh=new THREE.Mesh(geo,mat);
mesh.position.set(px,0.05,pz);mesh.receiveShadow=true;
mesh.userData={water:true};
scene.add(mesh);
} else if(c==='T'){
// Tree
const trunk=new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.12,0.6,6),new THREE.MeshStandardMaterial({color:0x6b4420}));
trunk.position.set(px,0.3,pz);trunk.castShadow=true;
scene.add(trunk);
const leaves=new THREE.Mesh(new THREE.SphereGeometry(0.35,8,6),new THREE.MeshStandardMaterial({color:0x2d8030+Math.floor(Math.random()*0x202020)}));
leaves.position.set(px,0.75+Math.random()*0.1,pz);leaves.castShadow=true;
leaves.scale.y=0.8+Math.random()*0.4;
scene.add(leaves);
// Base
const base=new THREE.Mesh(new THREE.BoxGeometry(0.95,0.15,0.95),new THREE.MeshStandardMaterial({color:0x4a7a28}));
base.position.set(px,0.075,pz);base.receiveShadow=true;
scene.add(base);
}
}
}
function updateTile(x,z){
const key=x+','+z;
const plot=G.plots[key];
const mesh=tileMeshes[key];
if(!mesh||!plot)return;
// Update color
if(plot.crop&&plot.w) mesh.material.color.setHex(0x4a3420);
else if(plot.crop||plot.s==='tilled') mesh.material.color.setHex(0x7a5a3a);
else mesh.material.color.setHex(0x6aaa38);
// Update crop visual
const px=x-COLS/2+0.5, pz=z-ROWS/2+0.5;
if(cropMeshes[key]){cropMeshes[key].forEach(m=>scene.remove(m));delete cropMeshes[key];}
if(plot.crop){
const b=BERRIES[plot.crop];
const done=plot.g>=b.grow;
const progress=Math.min(plot.g/b.grow,1);
const parts=[];
// Stem
const stemH=0.15+progress*0.35;
const stem=new THREE.Mesh(
new THREE.CylinderGeometry(0.03,0.04,stemH,5),
new THREE.MeshStandardMaterial({color:0x44aa44})
);
stem.position.set(px,0.3+stemH/2,pz);stem.castShadow=true;
scene.add(stem);parts.push(stem);
// Leaves at mid-growth
if(progress>0.3){
const leaf=new THREE.Mesh(
new THREE.SphereGeometry(0.1+progress*0.08,6,4),
new THREE.MeshStandardMaterial({color:0x55cc55})
);
leaf.position.set(px,0.3+stemH*0.6,pz);
leaf.scale.y=0.5;leaf.castShadow=true;
scene.add(leaf);parts.push(leaf);
}
// Berry at full growth
if(done){
const berry=new THREE.Mesh(
new THREE.SphereGeometry(0.12,8,6),
new THREE.MeshStandardMaterial({color:b.color,emissive:b.color,emissiveIntensity:0.15})
);
berry.position.set(px,0.3+stemH+0.08,pz);berry.castShadow=true;
scene.add(berry);parts.push(berry);
// Glow ring
const ring=new THREE.Mesh(
new THREE.RingGeometry(0.2,0.35,16),
new THREE.MeshBasicMaterial({color:0xffffaa,transparent:true,opacity:0.3,side:THREE.DoubleSide})
);
ring.rotation.x=-Math.PI/2;
ring.position.set(px,0.32,pz);
scene.add(ring);parts.push(ring);
}
cropMeshes[key]=parts;
}
}
function refreshAllTiles(){
Object.keys(G.plots).forEach(k=>{
const[x,z]=k.split(',').map(Number);
updateTile(x,z);
});
}
// ======================== PLAYER ========================
function buildPlayer(){
playerGroup=new THREE.Group();
// Body
const body=new THREE.Mesh(
new THREE.SphereGeometry(0.22,10,8),
new THREE.MeshStandardMaterial({color:0xc4956a})
);
body.scale.set(1,0.85,1.1);body.position.y=0.22;body.castShadow=true;
playerGroup.add(body);
// Head
const head=new THREE.Mesh(
new THREE.SphereGeometry(0.18,10,8),
new THREE.MeshStandardMaterial({color:0xd4a878})
);
head.position.set(0,0.45,0.05);head.castShadow=true;
playerGroup.add(head);
// Ears
[-1,1].forEach(side=>{
const ear=new THREE.Mesh(
new THREE.ConeGeometry(0.07,0.18,4),
new THREE.MeshStandardMaterial({color:0x8b6040})
);
ear.position.set(side*0.12,0.6,0.02);
ear.rotation.z=side*0.3;ear.castShadow=true;
playerGroup.add(ear);
});
// Eyes
[-1,1].forEach(side=>{
const eye=new THREE.Mesh(
new THREE.SphereGeometry(0.035,6,4),
new THREE.MeshStandardMaterial({color:0x2a1a10})
);
eye.position.set(side*0.08,0.47,0.16);
playerGroup.add(eye);
});
// Tail
const tail=new THREE.Mesh(
new THREE.SphereGeometry(0.12,6,5),
new THREE.MeshStandardMaterial({color:0xd4a060})
);
tail.position.set(0,0.3,-0.28);tail.scale.set(0.8,0.8,1.2);tail.castShadow=true;
playerGroup.add(tail);
// Collar/mane
const mane=new THREE.Mesh(
new THREE.TorusGeometry(0.16,0.06,6,12),
new THREE.MeshStandardMaterial({color:0xf0e0c8})
);
mane.position.set(0,0.36,0.06);mane.rotation.x=Math.PI/2;
playerGroup.add(mane);
const gx=G.px-COLS/2+0.5, gz=G.pz-ROWS/2+0.5;
playerGroup.position.set(gx,0,gz);
scene.add(playerGroup);
playerMesh=playerGroup;
}
function updatePlayerColor(hexColor){
const c=new THREE.Color(hexColor);
playerGroup.children[0].material.color.copy(c); // body
playerGroup.children[1].material.color.copy(c.clone().offsetHSL(0,0,0.1)); // head
}
// ======================== NPCs ========================
function buildNPCs(){
npcs.forEach(n=>{
const e=EVOS[n.key];
const g=new THREE.Group();
const body=new THREE.Mesh(
new THREE.SphereGeometry(0.18,8,6),
new THREE.MeshStandardMaterial({color:e.color})
);
body.scale.set(1,0.85,1);body.position.y=0.18;body.castShadow=true;
g.add(body);
const head=new THREE.Mesh(
new THREE.SphereGeometry(0.14,8,6),
new THREE.MeshStandardMaterial({color:e.color})
);
head.position.set(0,0.36,0.03);head.castShadow=true;
g.add(head);
// Eyes
[-1,1].forEach(side=>{
const eye=new THREE.Mesh(
new THREE.SphereGeometry(0.025,5,4),
new THREE.MeshStandardMaterial({color:0x1a1a1a})
);
eye.position.set(side*0.06,0.38,0.13);
g.add(eye);
});
// Ears
[-1,1].forEach(side=>{
const ear=new THREE.Mesh(
new THREE.ConeGeometry(0.05,0.14,4),
new THREE.MeshStandardMaterial({color:e.color})
);
ear.position.set(side*0.09,0.5,0);
ear.rotation.z=side*0.3;
g.add(ear);
});
const px=n.x-COLS/2+0.5, pz=n.z-ROWS/2+0.5;
g.position.set(px,0,pz);
scene.add(g);
n.mesh=g;
// Label
const label=document.createElement('div');
label.className='npc-label';
label.textContent=e.name;
label.style.color=e.hexColor;
document.body.appendChild(label);
n.labelEl=label;
});
}
function updateNPCLabels(){
npcs.forEach(n=>{
if(!n.mesh||!n.labelEl)return;
const pos=n.mesh.position.clone();
pos.y+=0.7;
const v=pos.project(camera);
const x=(v.x*0.5+0.5)*window.innerWidth;
const y=(-v.y*0.5+0.5)*window.innerHeight;
n.labelEl.style.left=x+'px';
n.labelEl.style.top=y+'px';
n.labelEl.style.opacity=v.z<1?'1':'0';
});
}
// ======================== CAMERA ========================
function updateCamera(){
const dist=11;
const height=8;
const tx=playerGroup.position.x+Math.sin(G.camAngle)*dist;
const tz=playerGroup.position.z+Math.cos(G.camAngle)*dist;
camera.position.set(tx,height,tz);
camera.lookAt(playerGroup.position.x,0.5,playerGroup.position.z);
}
// ======================== MOVEMENT ========================
function canWalk(x,z){
if(x<0||x>=COLS||z<0||z>=ROWS)return false;
const c=MAP[z][x];
if(c==='T'||c==='W'||c==='R')return false;
if(npcs.some(n=>n.x===x&&n.z===z))return false;
return true;
}
function movePlayer(dx,dz){
// Adjust movement based on camera angle
const angle=G.camAngle;
const cos=Math.cos(angle),sin=Math.sin(angle);
let mx=Math.round(dx*cos-dz*sin);
let mz=Math.round(dx*sin+dz*cos);
// Clamp to cardinal
if(Math.abs(mx)>Math.abs(mz)){mz=0;mx=mx>0?1:-1;}
else{mx=0;mz=mz>0?1:-1;}
const nx=G.px+mx,nz=G.pz+mz;
if(!canWalk(nx,nz))return;
G.px=nx;G.pz=nz;
const target=new THREE.Vector3(nx-COLS/2+0.5,0,nz-ROWS/2+0.5);
playerGroup.position.lerp(target,1);
// Face direction
if(mx!==0||mz!==0){
playerGroup.rotation.y=Math.atan2(mx,mz);
}
}
function doAction(){
const key=G.px+','+G.pz;
if(G.plots[key]) useTool(G.px,G.pz);
// Adjacent NPC?
npcs.forEach(n=>{
const d=Math.abs(G.px-n.x)+Math.abs(G.pz-n.z);
if(d<=1) openDialog(n.key);
});
}
// ======================== TOOL USE ========================
function useTool(x,z){
const key=x+','+z;
const p=G.plots[key];
if(!p)return;
switch(G.tool){
case 'hoe':
if(p.s==='grass'&&!p.crop){p.s='tilled';msg('Tilled! ⛏️');}
break;
case 'water':
if(p.s!=='grass'&&p.crop&&!p.w){p.w=true;msg('Watered! 💧');}
else if(p.w)msg('Already watered!');
break;
case 'plant':
if(p.s==='tilled'&&!p.crop){
if(G.seeds[G.seed]>0){p.crop=G.seed;p.g=0;G.seeds[G.seed]--;msg(`Planted ${BERRIES[G.seed].name}!`);renderSeeds();}
else msg('No seeds!');
} else if(p.s==='grass')msg('Till first!');
break;
case 'harvest':
if(p.crop&&p.g>=BERRIES[p.crop].grow){
const b=BERRIES[p.crop];
G.gold+=b.sell;G.xp+=b.xp;G.harvest[p.crop]++;
G.affinity[b.aff]=(G.affinity[b.aff]||0)+b.xp;
checkLvl();msg(`Harvested ${b.name}! +${b.sell}g +${b.xp}xp`);
p.crop=null;p.g=0;p.w=false;p.s='tilled';
renderSeeds();renderAff();
} else if(p.crop){const r=BERRIES[p.crop].grow-p.g;msg(`${r} day${r>1?'s':''} left`);}
break;
}
updateTile(x,z);updateUI();
}
// ======================== INPUT ========================
document.addEventListener('keydown',e=>{
keys[e.key.toLowerCase()]=true;
if(document.querySelector('.dialog-overlay.open')||document.querySelector('.side-panel.open'))return;
switch(e.key){
case ' ':e.preventDefault();doAction();break;
case '1':G.tool='hoe';renderTools();break;
case '2':G.tool='water';renderTools();break;
case '3':G.tool='plant';renderTools();break;
case '4':G.tool='harvest';renderTools();break;
case 'q':case 'Q':G.camAngle-=Math.PI/4;break;
case 'e':case 'E':G.camAngle+=Math.PI/4;break;
}
});
document.addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false;});
// Dpad
document.querySelectorAll('.dpad-btn[data-dir]').forEach(btn=>{
const dir=btn.dataset.dir;
let iv=null;
const doMove=()=>{
switch(dir){
case 'up':movePlayer(0,-1);break;
case 'down':movePlayer(0,1);break;
case 'left':movePlayer(-1,0);break;
case 'right':movePlayer(1,0);break;
}
};
btn.addEventListener('pointerdown',e=>{e.preventDefault();doMove();iv=setInterval(doMove,200);});
btn.addEventListener('pointerup',()=>clearInterval(iv));
btn.addEventListener('pointerleave',()=>clearInterval(iv));
});
// ======================== NPC WANDER ========================
setInterval(()=>{
npcs.forEach(n=>{
if(Math.random()>0.35)return;
const dirs=[[0,-1],[0,1],[-1,0],[1,0]];
const[dx,dz]=dirs[Math.floor(Math.random()*dirs.length)];
const nx=n.x+dx,nz=n.z+dz;
if(nx<0||nx>=COLS||nz<0||nz>=ROWS)return;
const c=MAP[nz][nx];
if(c==='T'||c==='W')return;
if(nx===G.px&&nz===G.pz)return;
if(npcs.some(o=>o!==n&&o.x===nx&&o.z===nz))return;
if(Math.abs(nx-n.homeX)+Math.abs(nz-n.homeZ)>5)return;
n.x=nx;n.z=nz;
});
},2500);
// ======================== GAME LOOP ========================
function animate(){
requestAnimationFrame(animate);
const dt=clock.getDelta();
moveTimer+=dt;
// Keyboard movement
if(moveTimer>0.18){
if(keys['w']||keys['arrowup']){movePlayer(0,-1);moveTimer=0;}
else if(keys['s']||keys['arrowdown']){movePlayer(0,1);moveTimer=0;}
else if(keys['a']||keys['arrowleft']){movePlayer(-1,0);moveTimer=0;}
else if(keys['d']||keys['arrowright']){movePlayer(1,0);moveTimer=0;}
}
// Player bob
const t=clock.elapsedTime;
playerGroup.position.y=Math.sin(t*3)*0.04;
// Smooth player position
const targetX=G.px-COLS/2+0.5, targetZ=G.pz-ROWS/2+0.5;
playerGroup.position.x+=(targetX-playerGroup.position.x)*0.15;
playerGroup.position.z+=(targetZ-playerGroup.position.z)*0.15;
// NPC smooth movement and bob
npcs.forEach((n,i)=>{
if(!n.mesh)return;
const tx=n.x-COLS/2+0.5,tz=n.z-ROWS/2+0.5;
n.mesh.position.x+=(tx-n.mesh.position.x)*0.05;
n.mesh.position.z+=(tz-n.mesh.position.z)*0.05;
n.mesh.position.y=Math.sin(t*2+i*1.5)*0.03;
// Face movement direction
const dx=tx-n.mesh.position.x,dz=tz-n.mesh.position.z;
if(Math.abs(dx)>0.01||Math.abs(dz)>0.01){
n.mesh.rotation.y=Math.atan2(dx,dz);
}
});
// Water animation
scene.children.forEach(c=>{
if(c.userData&&c.userData.water){
c.position.y=0.05+Math.sin(t*2+c.position.x)*0.02;
c.material.opacity=0.7+Math.sin(t*3)*0.1;
}
});
// Crop glow rings pulse
Object.values(cropMeshes).forEach(parts=>{
parts.forEach(p=>{
if(p.geometry&&p.geometry.type==='RingGeometry'){
p.material.opacity=0.2+Math.sin(t*3)*0.15;
p.rotation.z=t*0.5;
}
});
});
updateCamera();
updateNPCLabels();
renderer.render(scene,camera);
}
// ======================== HUD ========================
function renderTools(){
const el=document.getElementById('hudTools');el.innerHTML='';
[{id:'hoe',icon:'⛏️',label:'Till'},{id:'water',icon:'💧',label:'Water'},
{id:'plant',icon:'🌱',label:'Plant'},{id:'harvest',icon:'🧺',label:'Harvest'}
].forEach(t=>{
const b=document.createElement('button');
b.className='tool-btn'+(t.id===G.tool?' active':'');
b.innerHTML=`<span class="tool-icon">${t.icon}</span> ${t.label}`;
b.onclick=()=>{G.tool=t.id;renderTools();};
el.appendChild(b);
});
}
function renderSeeds(){
const el=document.getElementById('hudSeeds');el.innerHTML='';
Object.entries(BERRIES).forEach(([k,b])=>{
const btn=document.createElement('button');
btn.className='seed-btn'+(k===G.seed?' active':'');
btn.innerHTML=`${b.emoji} ${b.name.split(' ')[0]} ×${G.seeds[k]}`;
btn.onclick=()=>{G.seed=k;renderSeeds();};
if(G.seeds[k]<=0)btn.style.opacity='0.35';
el.appendChild(btn);
});
}
function updateUI(){
document.getElementById('sDay').textContent=G.day;
document.getElementById('sGold').textContent=G.gold;
document.getElementById('sLvl').textContent=G.level;
document.getElementById('xpFill').style.width=(G.xp/G.xpNext*100)+'%';
}
// ======================== PANELS ========================
function togglePanel(name){
const panels={village:'panelVillage',shop:'panelShop',evo:'panelEvo'};
const panel=document.getElementById(panels[name]);
const isOpen=panel.classList.contains('open');
closeAllPanels();
if(!isOpen){
panel.classList.add('open');
if(name==='village')renderVillage();
if(name==='shop')renderShop();
if(name==='evo')renderAff();
}
}
function closeAllPanels(){document.querySelectorAll('.side-panel').forEach(p=>p.classList.remove('open'));}
// ======================== VILLAGE ========================
function renderVillage(){
const grid=document.getElementById('villageGrid');grid.innerHTML='';
Object.entries(EVOS).forEach(([k,e])=>{
if(k==='eevee')return;
const card=document.createElement('div');card.className='v-card';
const h=G.friendship[k]||0;
const hearts=h>=15?'💕💕💕💕':h>=10?'💕💕💕':h>=5?'💕💕':h>=1?'💕':'🤍';
card.innerHTML=`<div class="v-emoji" style="background:${e.hexColor}20;border:2px solid ${e.hexColor}">${e.emoji}</div>
<div class="v-info"><div class="v-name">${e.name}</div><div class="v-role">${e.type} — ${e.role}</div><div class="v-hearts">${hearts} (${h})</div></div>`;
card.onclick=()=>openDialog(k);
grid.appendChild(card);
});
}
// ======================== DIALOG ========================
function openDialog(k){
const e=EVOS[k];
document.getElementById('dAvatar').textContent=e.emoji;
document.getElementById('dAvatar').style.background=`${e.hexColor}30`;
document.getElementById('dAvatar').style.border=`3px solid ${e.hexColor}`;
document.getElementById('dName').textContent=e.name;
document.getElementById('dType').textContent=`${e.type} Type — ${e.role}`;
document.getElementById('dText').textContent=`"${e.lines[Math.floor(Math.random()*e.lines.length)]}"`;
const dailyEl=document.getElementById('dDaily'),dailyLabel=document.getElementById('dDailyLabel');
if(!G.talkedToday[k]){
G.talkedToday[k]=true;
G.friendship[k]=(G.friendship[k]||0)+1;
const gifts=[
{t:`Gave you 2 ${BERRIES[e.berry].name} seeds!`,fn:()=>{G.seeds[e.berry]+=2;renderSeeds();}},
{t:`Gave you 5 gold!`,fn:()=>{G.gold+=5;updateUI();}},
{t:`+2 ${e.name} affinity!`,fn:()=>{G.affinity[k]+=2;renderAff();}}
];
const g=gifts[Math.floor(Math.random()*gifts.length)];
dailyLabel.textContent='📬 Daily Visit Bonus';dailyEl.textContent='🎁 '+g.t;dailyEl.style.display='block';g.fn();
} else {dailyLabel.textContent='📬 Already visited today!';dailyEl.style.display='none';}
document.getElementById('dGiftResult').style.display='none';
renderGiftBtns(k);
document.getElementById('dialogOverlay').classList.add('open');
}
function renderGiftBtns(k){
const grid=document.getElementById('giftGrid');grid.innerHTML='';
const gifted=G.giftedToday[k]||false;
Object.entries(BERRIES).forEach(([bk,b])=>{
const btn=document.createElement('button');btn.className='gift-btn';
btn.innerHTML=`${b.emoji} ${b.name.split(' ')[0]} (${G.harvest[bk]})`;
btn.disabled=G.harvest[bk]<=0||gifted;
btn.onclick=()=>giveGift(k,bk);
grid.appendChild(btn);
});
}
function giveGift(vk,bk){
if(G.harvest[bk]<=0||G.giftedToday[vk])return;
G.harvest[bk]--;G.giftedToday[vk]=true;
const e=EVOS[vk],b=BERRIES[bk];
const isFav=e.fav===bk,isType=b.aff===vk;
let reaction,affGain,friendGain;
if(isFav){reaction=e.giftLove[Math.floor(Math.random()*e.giftLove.length)];affGain=15;friendGain=3;}
else if(isType){reaction=e.giftLike[Math.floor(Math.random()*e.giftLike.length)];affGain=10;friendGain=2;}
else{reaction=e.giftReact[Math.floor(Math.random()*e.giftReact.length)];affGain=5;friendGain=1;}
G.affinity[vk]=(G.affinity[vk]||0)+affGain;
G.friendship[vk]=(G.friendship[vk]||0)+friendGain;
const r=document.getElementById('dGiftResult');
r.innerHTML=`"${reaction}"<br>💕 +${friendGain} friendship · ✨ +${affGain} affinity`;
r.style.display='block';r.style.background=isFav?'#fff0e0':isType?'#f0ffe0':'#f0f0ff';
msg(`Gave ${b.name} to ${e.name}! ${isFav?'💕💕':'💕'}`);
renderGiftBtns(vk);renderSeeds();renderAff();updateUI();
}
function closeDialog(){document.getElementById('dialogOverlay').classList.remove('open');}
// ======================== SHOP ========================
function renderShop(){
const grid=document.getElementById('shopGrid');grid.innerHTML='';
Object.entries(BERRIES).forEach(([k,b])=>{
const item=document.createElement('div');item.className='shop-item';
item.innerHTML=`<div class="shop-icon">${b.emoji}</div><div class="shop-name">${b.name} Seed</div><div class="shop-price">💰 ${b.cost}g</div><div class="shop-desc">${b.grow}d · Sells ${b.sell}g</div>`;
item.onclick=()=>{
if(G.gold>=b.cost){G.gold-=b.cost;G.seeds[k]++;msg(`Bought ${b.name} seed!`);renderSeeds();updateUI();renderShop();}
else msg('Not enough gold!');
};
grid.appendChild(item);
});
}
// ======================== AFFINITY / EVO ========================
function renderAff(){
const grid=document.getElementById('affGrid');grid.innerHTML='';
let maxK=null,maxV=0;
Object.entries(G.affinity).forEach(([k,v])=>{if(v>maxV){maxV=v;maxK=k;}});
Object.entries(EVOS).forEach(([k,e])=>{
if(k==='eevee')return;
const pts=G.affinity[k]||0;const pct=Math.min(pts,100);
const card=document.createElement('div');card.className='aff-card'+(k===maxK&&pts>0?' lead':'');
card.innerHTML=`<div class="aff-icon">${e.emoji}</div><div class="aff-name">${e.name}</div><div class="aff-bar-w"><div class="aff-bar" style="width:${pct}%;background:${e.hexColor}"></div></div><div class="aff-pts">${pts}/100</div>`;
grid.appendChild(card);
});
const btn=document.getElementById('evoBtn'),status=document.getElementById('evoStatus');
if(G.evolved){btn.disabled=true;btn.textContent=`Evolved to ${EVOS[G.evolvedTo].name}!`;status.textContent='';}
else if(maxV>=100&&G.level>=8){btn.disabled=false;status.textContent=`Ready to evolve into ${EVOS[maxK].name}!`;}
else{btn.disabled=true;const n=[];if(G.level<8)n.push(`Lv8 (now ${G.level})`);if(maxV<100)n.push(`100 aff (best:${maxV})`);status.textContent='Need: '+n.join(' & ');}
}
function startEvo(){
let maxK=null,maxV=0;
Object.entries(G.affinity).forEach(([k,v])=>{if(v>maxV){maxV=v;maxK=k;}});
if(!maxK||maxV<100||G.level<8)return;
G.evolved=true;G.evolvedTo=maxK;
const evo=EVOS[maxK];
const cer=document.getElementById('ceremony'),stage=document.getElementById('evoStage'),sp=document.getElementById('sparkles');
sp.innerHTML='';
['✨','⭐','💫','🌟','❇️'].forEach((e,i)=>{for(let j=0;j<5;j++){
const s=document.createElement('span');s.className='sparkle';s.textContent=e;
s.style.left=(5+Math.random()*90)+'%';s.style.top=(30+Math.random()*60)+'%';
s.style.animationDelay=(Math.random()*2)+'s';sp.appendChild(s);}});
cer.classList.add('active');
stage.innerHTML=`<div class="evo-big">🦊</div><div class="evo-msg">What's happening...?</div><div class="evo-sub">Eevee is glowing!</div>`;
setTimeout(()=>{stage.innerHTML=`<div class="evo-big" style="filter:brightness(2.5)">🦊</div><div class="evo-msg" style="color:${evo.hexColor}">Eevee is evolving!</div><div class="evo-sub">The power of ${evo.type}...</div>`;},2200);
setTimeout(()=>{stage.innerHTML=`<div class="evo-big">${evo.emoji}</div><div class="evo-msg" style="color:${evo.hexColor}">You evolved into ${evo.name}!</div><div class="evo-sub">${evo.type} Type — ${evo.role}</div><button class="evo-ok" onclick="finishEvo()">Continue! →</button>`;},5000);
}
function finishEvo(){
document.getElementById('ceremony').classList.remove('active');
const evo=EVOS[G.evolvedTo];
document.getElementById('pcAvatar').textContent=evo.emoji;
document.getElementById('pcName').textContent=evo.name;
document.getElementById('pcType').textContent=evo.type+' Type — Master Farmer';
updatePlayerColor(evo.hexColor);
renderAff();msg(`🎉 You're now ${evo.name}!`);
}
// ======================== SLEEP ========================
function doSleep(){
const ov=document.getElementById('sleepOv');ov.classList.add('active');
setTimeout(()=>{
G.day++;G.talkedToday={};G.giftedToday={};
let grew=0;
Object.entries(G.plots).forEach(([k,p])=>{if(p.crop&&p.w){p.g++;grew++;}p.w=false;});
refreshAllTiles();renderSeeds();updateUI();
msg(`☀️ Day ${G.day}!${grew?` ${grew} crops grew!`:''}`);
},1100);
setTimeout(()=>ov.classList.remove('active'),2200);
}
function checkLvl(){while(G.xp>=G.xpNext){G.xp-=G.xpNext;G.level++;G.xpNext=Math.floor(G.xpNext*1.5);G.gold+=G.level*3;msg(`🎉 Level ${G.level}!`);}}
function msg(t){const a=document.getElementById('msgArea');const m=document.createElement('div');m.className='msg';m.textContent=t;a.appendChild(m);setTimeout(()=>m.remove(),2600);}
// ======================== START ========================
initThree();
renderTools();renderSeeds();refreshAllTiles();updateUI();
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment