feat: XP/level system, mob loot drops, level-up popup
This commit is contained in:
parent
edb08094db
commit
2ebb457fc5
166
game.js
166
game.js
|
|
@ -190,6 +190,13 @@ function customConfirm(msg, onYes) {
|
|||
|
||||
// Показываем в UI
|
||||
worldIdEl.textContent = worldId;
|
||||
// XP/Level display
|
||||
const lvXpNext = xpForLevel(player.level + 1);
|
||||
const lvXpCur = xpForLevel(player.level);
|
||||
const xpInLevel = player.xp - lvXpCur;
|
||||
const xpNeeded = lvXpNext - lvXpCur;
|
||||
document.getElementById('xplevel').textContent = player.level;
|
||||
document.getElementById('xpbar').textContent = xpInLevel + '/' + xpNeeded;
|
||||
multiplayerStatus.style.display = 'block';
|
||||
});
|
||||
|
||||
|
|
@ -378,11 +385,8 @@ function customConfirm(msg, onYes) {
|
|||
if (sm && data.killer === mySocketId) {
|
||||
// Give loot to the killer
|
||||
if (sm.kind === 'chicken') playSound('hurt_chicken');
|
||||
inv.meat += (sm.kind==='chicken' ? 1 : 2);
|
||||
if (sm.kind === 'skeleton') {
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if (Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
spawnDrops(sm.x, sm.y, sm.kind);
|
||||
grantXP(getMobXP(sm.kind));
|
||||
rebuildHotbar();
|
||||
}
|
||||
serverMobs.delete(data.id);
|
||||
|
|
@ -657,6 +661,10 @@ function customConfirm(msg, onYes) {
|
|||
meat: { n:'Сырое мясо', icon:'🥩', food:15 },
|
||||
cooked: { n:'Жареное мясо', icon:'🍖', food:45 },
|
||||
arrow: { n:'Стрела', icon:'➡️', stack:64 },
|
||||
chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 },
|
||||
feather: { n:'Перо', icon:'🪶', stack:64 },
|
||||
bone: { n:'Кость', icon:'🦴', stack:64 },
|
||||
gunpowder: { n:'Порох', icon:'💥', stack:64 }
|
||||
};
|
||||
|
||||
// Seed мира для детерминированной генерации
|
||||
|
|
@ -1836,11 +1844,130 @@ function customConfirm(msg, onYes) {
|
|||
sleeping: false,
|
||||
inBoat: false,
|
||||
armor: 0, // Текущий уровень защиты (0 = без брони, 0.5 = железная броня)
|
||||
equippedArmor: null // Тип надетой брони
|
||||
equippedArmor: null, // Тип надетой брони
|
||||
xp: 0,
|
||||
level: 1
|
||||
};
|
||||
|
||||
// Сохраняем начальную позицию для возрождения
|
||||
const spawnPoint = { x: 6*TILE, y: 0*TILE };
|
||||
|
||||
// Система дропов с мобов
|
||||
const drops = []; // {x, y, vy, item, qty, age}
|
||||
let levelUpPopup = null; // {text, timer}
|
||||
|
||||
function xpForLevel(lv) {
|
||||
if (lv <= 1) return 0;
|
||||
const thresholds = [0, 50, 150, 300, 500, 800, 1200, 1700, 2300, 3000];
|
||||
if (lv - 1 < thresholds.length) return thresholds[lv - 1];
|
||||
return Math.floor(3000 + (lv - 10) * (lv - 10) * 50 + (lv - 10) * 200);
|
||||
}
|
||||
|
||||
function getMobLoot(kind) {
|
||||
const table = {
|
||||
chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}],
|
||||
pig: [{item:'meat',min:1,max:2,chance:1}],
|
||||
zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}],
|
||||
skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}],
|
||||
creeper: [{item:'gunpowder',min:1,max:2,chance:1}]
|
||||
};
|
||||
return table[kind] || [];
|
||||
}
|
||||
|
||||
function getMobXP(kind) {
|
||||
const xpTable = { chicken:3, pig:5, zombie:10, skeleton:12, creeper:15 };
|
||||
return xpTable[kind] || 0;
|
||||
}
|
||||
|
||||
function spawnDrops(mx, my, kind) {
|
||||
const loot = getMobLoot(kind);
|
||||
for (const entry of loot) {
|
||||
if (Math.random() > entry.chance) continue;
|
||||
const qty = entry.min + Math.floor(Math.random() * (entry.max - entry.min + 1));
|
||||
if (qty <= 0) continue;
|
||||
drops.push({
|
||||
x: mx + (Math.random() - 0.5) * 20,
|
||||
y: my + (Math.random() - 0.5) * 10,
|
||||
vy: -1 - Math.random() * 2,
|
||||
item: entry.item,
|
||||
qty: qty,
|
||||
age: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function grantXP(amount) {
|
||||
player.xp += amount;
|
||||
while (player.xp >= xpForLevel(player.level + 1)) {
|
||||
player.level++;
|
||||
levelUpPopup = { text: '⭐ Уровень ' + player.level + '!', timer: 180 };
|
||||
}
|
||||
}
|
||||
|
||||
function pickupDrops() {
|
||||
for (let i = drops.length - 1; i >= 0; i--) {
|
||||
const d = drops[i];
|
||||
const dx = player.x + player.w/2 - d.x;
|
||||
const dy = player.y + player.h/2 - d.y;
|
||||
if (dx*dx + dy*dy < 30*30) {
|
||||
if (!inv[d.item]) inv[d.item] = 0;
|
||||
inv[d.item] += d.qty;
|
||||
drops.splice(i, 1);
|
||||
rebuildHotbar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawDrops(ctx) {
|
||||
for (let i = drops.length - 1; i >= 0; i--) {
|
||||
const d = drops[i];
|
||||
d.age++;
|
||||
if (d.age > 3600) { drops.splice(i, 1); continue; } // 60 sec at 60fps
|
||||
// Bounce animation
|
||||
const bounce = Math.abs(Math.sin(d.age * 0.05)) * 6;
|
||||
const dy = d.y - bounce;
|
||||
const sx = d.x - camX;
|
||||
const sy = dy - camY;
|
||||
// Skip if off screen
|
||||
if (sx < -40 || sx > W + 40 || sy < -40 || sy > H + 40) continue;
|
||||
// Glow effect
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, 14, 0, Math.PI*2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
// Item icon
|
||||
ctx.save();
|
||||
ctx.font = '16px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const itemDef = ITEMS[d.item];
|
||||
const icon = itemDef ? itemDef.icon : '🎁';
|
||||
const label = d.qty > 1 ? icon + '×' + d.qty : icon;
|
||||
ctx.fillText(label, sx, sy);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawLevelUpPopup(ctx) {
|
||||
if (!levelUpPopup) return;
|
||||
levelUpPopup.timer--;
|
||||
if (levelUpPopup.timer <= 0) { levelUpPopup = null; return; }
|
||||
const alpha = Math.min(1, levelUpPopup.timer / 60);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.font = 'bold 36px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeText(levelUpPopup.text, W/2, H/2 - 60);
|
||||
ctx.fillText(levelUpPopup.text, W/2, H/2 - 60);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Система сохранения игры (localStorage + in-memory fallback)
|
||||
const SAVE_KEY = 'minegrechka_save';
|
||||
|
|
@ -1870,7 +1997,9 @@ function customConfirm(msg, onYes) {
|
|||
y: player.y,
|
||||
hp: player.hp,
|
||||
hunger: player.hunger,
|
||||
o2: player.o2
|
||||
o2: player.o2,
|
||||
xp: player.xp,
|
||||
level: player.level
|
||||
},
|
||||
inventory: inv,
|
||||
time: worldTime,
|
||||
|
|
@ -1968,6 +2097,8 @@ function customConfirm(msg, onYes) {
|
|||
player.y = saveData.player.y;
|
||||
player.hunger = saveData.player.hunger;
|
||||
player.o2 = saveData.player.o2;
|
||||
player.xp = saveData.player.xp || 0;
|
||||
player.level = saveData.player.level || 1;
|
||||
|
||||
// Обновляем spawnPoint на позицию из сохранения
|
||||
spawnPoint.x = player.x;
|
||||
|
|
@ -2459,11 +2590,8 @@ function customConfirm(msg, onYes) {
|
|||
}
|
||||
if(m.hp<=0){
|
||||
if(m.kind === 'chicken') playSound('hurt_chicken');
|
||||
inv.meat += (m.kind==='chicken' ? 1 : 2);
|
||||
if(m.kind === 'skeleton'){
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if(Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind);
|
||||
grantXP(getMobXP(m.kind));
|
||||
// Remove from the correct array
|
||||
if(m.id !== undefined){
|
||||
serverMobs.delete(m.id);
|
||||
|
|
@ -3267,11 +3395,8 @@ function customConfirm(msg, onYes) {
|
|||
}
|
||||
}
|
||||
if(m.hp <= 0){
|
||||
inv.meat += (m.kind==='chicken' ? 1 : 2);
|
||||
if(m.kind === 'skeleton'){
|
||||
inv.arrow += 2 + Math.floor(Math.random()*3);
|
||||
if(Math.random() < 0.15) inv.bow = (inv.bow||0) + 1;
|
||||
}
|
||||
spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind);
|
||||
grantXP(getMobXP(m.kind));
|
||||
// Remove from the correct array
|
||||
if(m.id !== undefined){
|
||||
serverMobs.delete(m.id);
|
||||
|
|
@ -3757,6 +3882,13 @@ function customConfirm(msg, onYes) {
|
|||
ctx.fillText('Нажмите на кровать чтобы проснуться', W/2, H/2 + 40);
|
||||
}
|
||||
|
||||
// Рисуем дропы
|
||||
drawDrops(ctx);
|
||||
// Пикап дропов
|
||||
pickupDrops();
|
||||
// Popup уровня
|
||||
drawLevelUpPopup(ctx);
|
||||
|
||||
// Миникарта (обновляем раз в ~4 кадра для оптимизации)
|
||||
if(minimapOpen && Math.random() < 0.25){
|
||||
renderMinimap();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -20,6 +20,7 @@
|
|||
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
|
||||
<div class="row">🕒 <span id="tod">День</span></div>
|
||||
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
|
||||
<div class="row">⭐ Lv.<span id="xplevel">1</span> | XP: <span id="xpbar">0/50</span></div>
|
||||
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,6 +93,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="game.js?v=18"></script>
|
||||
<script src="game.js?v=19"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue