Last active
March 17, 2026 01:56
-
-
Save turlockmike/2dea69238ea5ed127c9038e38acb9414 to your computer and use it in GitHub Desktop.
Eevee's Berry Farm - A peaceful 3D farming game with all eeveelutions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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