diff --git a/js/html.js b/js/html.js index 6c46fbb..8c63617 100644 --- a/js/html.js +++ b/js/html.js @@ -540,7 +540,7 @@ function render_monster(monster) if(def.immune) html+=info_line({line:"IMMUNE",color:"#AEAEAE"}); if(def.peaceful) html+=info_line({line:"PEACEFUL",color:"#54B25F"}); if(def.supporter) html+=info_line({line:"SUPPORTER",color:"#CA5931"}); - if(def.spawns) html+=info_line({line:"SPAWNS",color:"#AEAEAE"}); + //if (def.spawns) html += info_line({ line: "SPAWNS", color: "#AEAEAE" }); this is just adding an extra line to the mob UI if(def.abilities) { for(var id in def.abilities) @@ -549,11 +549,76 @@ function render_monster(monster) html+=info_line({name:def.abilities[id].aura&&"AURA"||"ABILITY",color:"#FC5F39",value:G.skills[id].name.toUpperCase(),onclick:"dialogs_target=xtarget||ctarget; render_skill('#topleftcornerdialog','"+id+"')"}); } } - if(def.spawns) - { - def.spawns.forEach(function(s){ - html+=info_line({name:"SPAWNS",color:"#237B2A",value:G.monsters[s[1]].name.toUpperCase(),onclick:"render_monster_info('"+s[1]+"')"}); - }) + if (def.spawns) { + // Collect info + const spawnInfo = {}; + + def.spawns.forEach(function (s) { + const condition = s[0]; + const name = s[1]; + const count = s[2] || 1; + + if (!spawnInfo[name]) { + spawnInfo[name] = { timers: [], thresholds: [] }; + } + + if (typeof condition === "number") { + spawnInfo[name].timers.push({ interval: condition, count }); + } else if (typeof condition === "string" && condition.startsWith("hp:")) { + const threshold = parseFloat(condition.split(":")[1]); + spawnInfo[name].thresholds.push({ threshold, count }); + } + }); + + // Build lines + Object.keys(spawnInfo).forEach((name) => { + const info = spawnInfo[name]; + let lines = []; + + // Timers + info.timers.forEach(t => { + lines.push(`${t.count} ${G.monsters[name].name}${t.count > 1 ? "s" : ""} every ${t.interval}ms`); + }); + + // HP thresholds + if (info.thresholds.length === 1) { + const t = info.thresholds[0]; + lines.push(`${t.count} ${G.monsters[name].name}${t.count > 1 ? "s" : ""} at ${t.threshold * 100}% hp`); + } else if (info.thresholds.length > 1) { + // Sort thresholds high → low + info.thresholds.sort((a, b) => b.threshold - a.threshold); + + const percents = info.thresholds.map(t => t.threshold * 100); + const counts = [...new Set(info.thresholds.map(t => t.count))]; + + if (counts.length === 1) { + // Check if evenly spaced + const diffs = percents.map((p, i) => i > 0 ? percents[i - 1] - p : null).slice(1); + const allEqual = diffs.every(d => d === diffs[0]); + + if (allEqual) { + // Summarized + lines.push(`${counts[0]} ${G.monsters[name].name}${counts[0] > 1 ? "s" : ""} every ${diffs[0]}% hp`); + } else { + // Generic + lines.push(`Spawns ${G.monsters[name].name} at hp thresholds`); + } + } else { + // Generic if counts differ + lines.push(`Spawns ${G.monsters[name].name} at hp thresholds`); + } + } + + // Add to tooltip + lines.forEach(line => { + html += info_line({ + name: "SPAWNS", + color: "#237B2A", + value: line, + onclick: "render_monster_info('" + name + "')" + }); + }); + }); } if(monster.target) html+=info_line({name:"TRG",color:"orange",value:monster.target}); if(monster.pet) diff --git a/node/server.js b/node/server.js index cc856b3..96aba65 100644 --- a/node/server.js +++ b/node/server.js @@ -12012,35 +12012,67 @@ function update_instance(instance) { set_ghash(aggressives, monster, 32); } } - if (monster.target && monster.spawns && get_player(monster.target) && !is_disabled(monster)) { - monster.spawns.forEach(function (spi) { - var interval = spi[0]; - var name = spi[1]; - if (!monster.last[name] || mssince(monster.last[name]) > interval) { - var pname = random_one(Object.keys(monster.points)); - var player = get_player(pname); - if (!player || player.npc || distance(monster, player) > 400) { - return; - } - if (!is_same(player, get_player(monster.target), true)) { - return; - } - monster.last[name] = new Date(); - var spot = safe_xy_nearby(player.map, player.x + Math.random() * 20 - 10, player.y + Math.random() * 20 - 10); - if (!spot) { - return; - } - new_monster(instance.name, { - type: name, - stype: "spawn", - x: spot.x, - y: spot.y, - target: player.name, - master: monster.id, - }); - } - }); - } + if (monster.target && monster.spawns && get_player(monster.target) && !is_disabled(monster)) { + monster.spawns.forEach((spi) => { + const condition = spi[0]; // interval or "hp:0.75" + const name = spi[1]; // monster type + const count = spi[2] || 1; // default to 1 + + // --- Timed spawns (existing) --- + if (typeof condition === "number") { + if (!monster.last[name] || mssince(monster.last[name]) > condition) { + for (let i = 0; i < count; i++) { + const pname = random_one(Object.keys(monster.points)); + const player = get_player(pname); + if (!player || player.npc || distance(monster, player) > 400) return; + if (!is_same(player, get_player(monster.target), true)) return; + + monster.last[name] = new Date(); + const spot = safe_xy_nearby(player.map, player.x + Math.random() * 20 - 10, player.y + Math.random() * 20 - 10); + if (!spot) return; + + new_monster(instance.name, { + type: name, + stype: "spawn", + x: spot.x, + y: spot.y, + target: player.name, + master: monster.id, + }); + } + } + } + + // --- HP threshold spawns (new) --- + if (typeof condition === "string" && condition.startsWith("hp:")) { + const threshold = parseFloat(condition.split(":")[1]); // e.g. 0.75 + const currentHpRatio = monster.hp / monster.max_hp; + const key = name + "_hp_" + threshold; + + if (currentHpRatio <= threshold && !monster.last[key]) { // trigger once per fight + for (let i = 0; i < count; i++) { + const pname = random_one(Object.keys(monster.points)); + const player = get_player(pname); + if (!player || player.npc || distance(monster, player) > 400) return; + if (!is_same(player, get_player(monster.target), true)) return; + + const spot = safe_xy_nearby(player.map, player.x + Math.random() * 20 - 10, player.y + Math.random() * 20 - 10); + if (!spot) return; + + new_monster(instance.name, { + type: name, + stype: "spawn", + x: spot.x, + y: spot.y, + target: player.name, + master: monster.id, + }); + } + monster.last[key] = true; // mark this threshold as triggered + } + } + }); + } function attack_target_or_move() { var player = players[name_to_id[monster.target]]; if (player && ssince(monster.last.attacked) > 20 && Math.random() > monster.rage * 0.99) {