/
chewy
/
BDZ_GenAI
Обзор
Документация
Войти
/
chewy
/
BDZ_GenAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scripts/engine.js
370 строк
15 KB
pichugin.sa
Раскидал все по папкам, чтобы нормально выглядел репозиторий
16 ноя 2025, 21:55
16 ноя 2025, 21:55
bd854ee
Код
Авторство
О чём код?
// engine.js - core world, entities, physics, rendering primitives export const canvas = document.getElementById('game'); export const ctx = canvas.getContext('2d', {alpha:true}); export let W = 900, H = 600; export function resizeCanvas(){ W = canvas.width = canvas.clientWidth; H = canvas.height = canvas.clientHeight; } export const world = { width: 2000, height: 1200, }; // CONFIG: core numeric parameters for movement/scale/camera export const CONFIG = { baseSpeedNormal: 100, // px/s at Normal scale scaleMultipliers: {1:1.6,2:1.0,3:0.6}, // Tiny / Normal / Giant hitbox: {1:{w:18,h:28}, 2:{w:28,h:44}, 3:{w:36,h:60}}, // hitbox sizes by scale camera: { zoomChangeDuration: 0.4, lookahead: 96, clampPadding: 40 }, inertia: { lerpFactor: 0.12, damping: 0.82 }, // movement feel tuning staminaMax: 100 }; /* export player early so generator can reference it during module initialization */ export const player = { x:220, y:820, w:28, h:44, vx:0, vy:0, speed:100, // legacy alias (kept) baseSpeed: CONFIG.baseSpeedNormal, // preserve base for multipliers (uses CONFIG) health:100, teleCooldown:0, inventory: [], // {id, name, tags:[]} spriteFrame:0, // 0 = idle, 1 = move (simple two-frame chain) spriteTimer:0, // timer to flip sprite frames sneak: false, // legacy flag (kept for UI toggles) - note: Shift used for sprint in stamina system stamina: CONFIG.staminaMax, // new: stamina (0..100) staminaRecoverTimer:0, // for recovery pacing breathTimer:0, // visual breathing timer scale: 2, // current scale index (1..3) mode: 'normal', // 'normal'|'sneak' opacity: 1.0 }; // add day/night & weather state (exported) export let timeOfDay = 0; // seconds since cycle start export let dayLengthSeconds = 320 + Math.floor(Math.random()*60); // 5–6 minutes ~ 300-360s (randomized) export let dayPhase = 'day'; // 'morning','day','evening','night' export let weather = { kind: 'clear', timer: 0, duration: 0 }; // kind: clear/fog/rain export function initTimeAndWeather(seedRand){ // seedRand optional function returning 0..1; fallback to Math.random const rnd = seedRand || Math.random; dayLengthSeconds = 300 + Math.floor(rnd()*60); timeOfDay = Math.floor(rnd()*dayLengthSeconds); updateDayPhase(); // initial weather 60-90s random weather.kind = (rnd() < 0.7) ? 'clear' : (rnd() < 0.85 ? 'fog' : 'rain'); weather.duration = 60 + Math.floor(rnd()*31); weather.timer = weather.duration; } function updateDayPhase(){ const t = (timeOfDay / dayLengthSeconds); // 0..1 if(t < 0.18) dayPhase = 'morning'; else if(t < 0.52) dayPhase = 'day'; else if(t < 0.78) dayPhase = 'evening'; else dayPhase = 'night'; } export function advanceTime(dt){ timeOfDay = (timeOfDay + dt) % dayLengthSeconds; updateDayPhase(); // weather ticking if(weather.timer > 0){ weather.timer = Math.max(0, weather.timer - dt); } else { // pick new weather 60-90s const r = Math.random(); weather.kind = (r < 0.6) ? 'clear' : (r < 0.8 ? 'fog' : 'rain'); weather.duration = 60 + Math.floor(Math.random()*31); weather.timer = weather.duration; } } // replace static arrays with empty placeholders to be filled by generator export let platforms = []; export let trees = []; export let npcs = []; export let boxes = []; export let portals = []; export let items = []; export let mobs = []; export let smallMechanisms = []; export let locations = []; export let shrines = []; // new: shrine entities export let relics = []; // new: world relic pickups export let chests = []; // new: locked chests export let boards = []; // new: quest boards export let contracts = []; // new: active run contracts export let doors = []; // new: connections between rooms export let bridges = []; // new: short bridges across ravines export let tallGrass = []; // new: patches of tall grass export let stoneShelters = []; // new: stone cover objects that block vision & boost recovery export let templeTiles = []; // new: special tiles inside temple zones that enforce Silence rule export let seed = 0; export let pulses = []; // active attack pulses /* Procedural generation: grid-based rooms (6-10 rooms per zone), doors and randomized contents */ export function generateWorld(s){ // tombstone: moved world generation to worldgen.js // removed function generateWorld(s) { ...existing code moved to worldgen.js ... } console.warn('generateWorld: delegated to worldgen.js; please import and call worldgen.generateWorld(seed)'); } // initialize a generated world on module load // replaced direct call with tombstone: original generator moved to worldgen.js // generateWorld(Math.floor(Math.random()*1e9)); export function rectsOverlap(a,b){ return a.x < b.x+b.w && a.x+a.w > b.x && a.y < b.y+b.h && a.y+a.h > b.y; } export function roundedRect(ctx,x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); } /* emit a forward knowledge pulse from player */ export function emitPulse(){ // pulse travels forward in player's facing direction (use velocity or default right) const dir = (player.vx !== 0 || player.vy !== 0) ? {x: Math.sign(player.vx||1), y: Math.sign(player.vy||0)} : {x:1,y:0}; pulses.push({ x: player.x + player.w/2, y: player.y + player.h/2, vx: dir.x * 420 + (Math.random()*40-20), vy: dir.y * 420 + (Math.random()*40-20), r: 8, life: 0.9, timer: 0 }); } /* update combat pulses and mob interactions; dt in seconds */ export function updateCombat(dt){ // advance pulses for(let i = pulses.length - 1; i >= 0; i--){ const p = pulses[i]; p.timer += dt; p.x += p.vx * dt; p.y += p.vy * dt; p.r += 40 * dt; // expanding wave if(p.timer >= p.life){ pulses.splice(i,1); continue; } // interact with mobs for(let j = mobs.length - 1; j >= 0; j--){ const m = mobs[j]; const dx = (m.x + m.w/2) - p.x; const dy = (m.y + m.h/2) - p.y; const dist = Math.hypot(dx,dy); if(dist < p.r + Math.max(m.w,m.h)/2){ // apply damage based on type if(!m.hp) m.hp = (m.type === 'slime') ? 10 : (m.type === 'spirit' ? 8 : 12); m.hp -= 8; // pulse base damage // small knockback const nx = dx / (dist || 1), ny = dy / (dist || 1); m.x += nx * 8; m.y += ny * 8; // mark last hit for visual or AI reaction m._lastHitAt = performance.now(); if(m.hp <= 0){ // death behavior // award xp to player player.xp = (player.xp||0) + (m.type === 'slime' ? 6 : m.type === 'spirit' ? 10 : 8); // drop either item or knowledge fragment const drop = {id:'drop_'+(Math.random()*1e6|0), x: m.x, y: m.y, name: (Math.random()>0.5?'Fragment':'Loot'), tags: (Math.random()>0.5?['knowledge']:['relic'])}; items.push(drop); // slime splits into two smaller slimes sometimes if(m.type === 'slime' && Math.random() < 0.6){ for(let s=0;s<2;s++){ mobs.push({id: m.id+'_split_'+s, type:'slime', x: m.x + (s?12:-12), y: m.y, w: Math.max(8, m.w-4), h: Math.max(6, m.h-4), vx: (Math.random()*40-20), vy: -30}); } } // remove mob mobs.splice(j,1); } } } // pulses can also trigger small mechanisms if overlapping for(const mech of smallMechanisms){ if(!mech.active && mech.x < p.x + p.r && mech.x + mech.w > p.x - p.r && mech.y < p.y + p.r && mech.y + mech.h > p.y - p.r){ mech.active = true; } } } // simple mob AI updates per type (additional passive behaviour) for(const m of mobs){ if(m.type === 'beetle' || /beetle/i.test(m.type)){ // move toward player, but flee if player is giant (reduce speed) const dx = (player.x + player.w/2) - (m.x + m.w/2); const dy = (player.y + player.h/2) - (m.y + m.h/2); const d = Math.hypot(dx,dy) || 1; let speed = 46; if(window.__scaleSetting === 3 || (player.scale && player.scale === 3)) speed = 18; // frightened m.x += (dx/d) * speed * dt; m.y += (dy/d) * speed * dt; } else if(m.type === 'slime'){ // slow jumpy movement: small vx oscillation and periodic hop m.vy = (m.vy || 0) + 220 * dt * (Math.random()*0.2 - 0.1); m.x += (m.vx || 0) * dt; m.y += (m.vy || 0) * dt; if(Math.random() < 0.01) { m.vy = -80 - Math.random()*60; m.vx = (Math.random()*80-40); } // clamp to world ground if(m.y > world.height - 30) m.y = world.height - 30; } else if(m.type === 'spirit'){ // ranged attacker: occasionally spawn a small projectile toward player m._shootTimer = (m._shootTimer || 0) - dt; if(m._shootTimer <= 0){ m._shootTimer = 1.2 + Math.random()*1.2; // projectile as a short pulse-like object (reuse pulses array but tag) pulses.push({x: m.x + m.w/2, y: m.y + m.h/2, vx: ((player.x - m.x)/Math.max(1,Math.hypot(player.x-m.x,player.y-m.y))) * 220, vy: ((player.y - m.y)/Math.max(1,Math.hypot(player.x-m.x,player.y-m.y))) * 220, r: 6, life: 1.6, timer: 0, hostile:true}); } } } } /* utility to spawn a mob (used by other modules if needed) */ export function spawnMob(type,x,y){ if(!type) type='beetle'; let w=12,h=8; if(type==='slime'){ w=18; h=12; } if(type==='beetle'){ w=14; h=10; } if(type==='spirit'){ w=12; h=18; } mobs.push({id:'mob_'+(Math.random()*1e6|0), type, x:x||100, y:y||100, w,h, vx:0, vy:0}); } /* --- NPC helpers: perception, suspicion, update, render --- */ export const NPC = { // update single NPC perception & suspicion update(npc, dt){ // basic FOV params fallback npc.fovAngle = npc.fovAngle || 90; npc.viewDistance = npc.viewDistance || 180; npc.suspicion = npc.suspicion || 0; npc.state = npc.state || 'Calm'; // Calm/Aware/Alert // simple patrol step if(npc.route && npc.route.length){ const target = npc.route[npc.routeIndex % npc.route.length]; const dx = target.x - npc.x, dy = target.y - npc.y, d = Math.hypot(dx,dy) || 1; const sp = (npc.routeSpeed||28) * dt; if(d > 2){ npc.x += (dx/d)*sp; npc.y += (dy/d)*sp; } else npc.routeIndex = (npc.routeIndex+1)%npc.route.length; } // perception: check player relative const px = player.x + player.w/2, py = player.y + player.h/2; const nx = npc.x + npc.w/2, ny = npc.y + npc.h/2; const dx = px - nx, dy = py - ny; const dist = Math.hypot(dx,dy); let inCone = false; if(dist <= npc.viewDistance){ const facing = npc.facing || {x:1,y:0}; const dot = (dx*(facing.x) + dy*(facing.y)) / Math.max(0.0001, (Math.hypot(dx,dy)*Math.hypot(facing.x,facing.y))); const ang = Math.acos(Math.max(-1,Math.min(1,dot))) * (180/Math.PI); if(ang <= npc.fovAngle*0.5) inCone = true; } // visibility modifiers: tall grass & cover & player sneaking const inTall = (window.tallGrass && window.tallGrass.some(g=> rectsOverlap({x:g.x,y:g.y,w:g.w,h:g.h}, {x:player.x,y:player.y,w:player.w,h:player.h}))); const inCover = (window.stoneShelters && window.stoneShelters.some(s=> rectsOverlap({x:s.x,y:s.y,w:s.w,h:s.h},{x:player.x,y:player.y,w:player.w,h:player.h}))); const sneaking = (player.mode === 'sneak' || player.sneak); if(inCone && !inCover){ let baseRate = 18; // per second suspicion growth if(sneaking) baseRate *= 0.5; if(inTall) baseRate *= 0.33; npc.suspicion = Math.min(100, npc.suspicion + baseRate * dt); } else { // decay npc.suspicion = Math.max(0, npc.suspicion - 26 * dt); } // update state & reactions if(npc.suspicion >= 100 && npc.state !== 'Alert'){ npc.state = 'Alert'; // apply penalty & global effect window.__RuleEngine && window.__RuleEngine.changeRep && window.__RuleEngine.changeRep('MiniTribe', -10); try{ window.dispatchEvent(new CustomEvent('npc:alert',{detail:{id:npc.id}})); }catch(e){} } else if(npc.suspicion >= 66) npc.state = 'Aware'; else npc.state = 'Calm'; }, // render NPC FOV cone and suspicion emoji above head render(ctx, npc, cam){ // cone debug toggle if(window.__showNpcFOV){ ctx.save(); ctx.globalAlpha = 0.14; ctx.fillStyle = 'rgba(255,80,80,0.12)'; const angRad = (npc.fovAngle || 90) * Math.PI/180; const facing = npc.facing || {x:1,y:0}; // compute facing angle const fa = Math.atan2(facing.y, facing.x); ctx.translate((npc.x + npc.w/2 - cam.x)*cam.zoom, (npc.y + npc.h/2 - cam.y)*cam.zoom); ctx.beginPath(); ctx.moveTo(0,0); ctx.arc(0,0, (npc.viewDistance||180)*cam.zoom, fa - angRad/2, fa + angRad/2); ctx.closePath(); ctx.fill(); ctx.restore(); } // emoji above head based on suspicion let emoji = '😐'; if(npc.suspicion >= 67) emoji = '😡'; else if(npc.suspicion >= 34) emoji = '😮'; ctx.save(); ctx.font = (14 * cam.zoom) + 'px Noto Sans'; ctx.textAlign = 'center'; ctx.fillStyle = 'white'; ctx.fillText(emoji, (npc.x + npc.w/2 - cam.x) * cam.zoom, (npc.y - 6 - cam.y) * cam.zoom); ctx.restore(); } }; /* Shrine interact placeholder (gesture mini-game entry point) */ export function shrineInteract(shrine){ // open language mini-game as placeholder (if available) try{ if(window.__LanguageSystem){ // create a simple 3-symbol target then open UI (reuse language system) const fakeNpc = {id: 'shrine_'+(shrine.id||Math.random()*1e6|0), faction: 'MiniTribe'}; window.__LanguageSystem.openFor(fakeNpc); // register a one-off listener for success -> grant reward const onSuccess = (ev)=>{ // small reward: rep +3 or temporary buff (speed) if(Math.random() > 0.5){ window.__RuleEngine && window.__RuleEngine.changeRep && window.__RuleEngine.changeRep('MiniTribe', 3); window.__LanguageSystem && window.__LanguageSystem.learnedPhrases && window.__LanguageSystem.learnedPhrases.push('✦ ✿ ★'); } else { if(window.__engine && window.__engine.player){ window.__engine.player.effects = window.__engine.player.effects || []; window.__engine.player.effects.push({kind:'speed', mul:1.3, t:10}); } } try{ window.dispatchEvent(new CustomEvent('shrine:reward',{detail:{shrine}})); }catch(e){} document.removeEventListener('player:first:learn', onSuccess); }; document.addEventListener('player:first:learn', onSuccess); } else { // fallback immediate reward window.__RuleEngine && window.__RuleEngine.changeRep && window.__RuleEngine.changeRep('MiniTribe', 2); try{ window.dispatchEvent(new CustomEvent('shrine:reward',{detail:{shrine}})); }catch(e){} } }catch(e){} } // helpers used by UI for icon rendering export function playerInStoneShelter(){ const r = {x: player.x, y: player.y, w: player.w, h: player.h}; for(const s of stoneShelters){ if(rectsOverlap(r, s)) return true; } return false; } export function playerOnTempleTile(){ const r = {x: player.x, y: player.y, w: player.w, h: player.h}; for(const t of templeTiles){ if(rectsOverlap(r, t)) return true; } return false; } // ensure exports export { NPC as npc };