Skip to content

Commit 5c715a3

Browse files
committed
[Feat] Add positional data towards monsters
1 parent 48fc71b commit 5c715a3

File tree

9 files changed

+155
-32
lines changed

9 files changed

+155
-32
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/logs
1111
/logs_dps.json
1212
dist/logs/
13-
information_log.txt
13+
information_log.txt*
1414

1515
# Build output
1616
/dist

algo/packet.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,23 @@ class PacketProcessor {
291291
} else if (isTargetMonster) {
292292
this.#processEnemyAttrs(targetUuid.toNumber(), attrCollection.Attrs);
293293
}
294+
295+
for (const attr of attrCollection.Attrs) {
296+
if ((attr.Id === 52 || attr.Id === 53) && attr.RawData) {
297+
try {
298+
const position = pb.Position.decode(attr.RawData);
299+
const x = position.X ?? 0;
300+
const y = position.Y ?? 0;
301+
const z = position.Z ?? 0;
302+
303+
if (isTargetMonster) {
304+
this.userDataManager.enemyCache.position.set(targetUuid.toNumber(), { x, y, z });
305+
} else if (isTargetPlayer && targetUuid.toNumber() === this.userDataManager.localPlayerUid) {
306+
this.userDataManager.setLocalPlayerPosition({ x, y, z });
307+
}
308+
} catch (e) {}
309+
}
310+
}
294311
}
295312

296313
const skillEffect = aoiSyncDelta.SkillEffects;
@@ -376,13 +393,12 @@ class PacketProcessor {
376393
}
377394

378395
if (isDead) {
379-
const enemyUid = `E${targetUuid.toNumber()}`;
396+
const enemyUid = `${targetUuid.toNumber()}`;
380397
const monsterId = this.userDataManager.enemyCache.monsterId.get(enemyUid);
381398
if (monsterId && TRACKED_MONSTER_IDS.has(String(monsterId))) {
382-
// Only reset tracking if BPTimer submission is enabled
383399
if (this.userDataManager.globalSettings.enableBPTimerSubmission !== false) {
384400
const line = this.userDataManager.getCurrentLineId();
385-
const bpTimer = initialize(this.logger);
401+
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
386402
setTimeout(() => {
387403
bpTimer.resetMonster(monsterId, line);
388404
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (isDead flag)`);
@@ -678,7 +694,8 @@ class PacketProcessor {
678694
for (const attr of attrs) {
679695
if (!attr.Id || !attr.RawData) continue;
680696
const reader = pbjs.Reader.create(attr.RawData);
681-
this.logger.debug(`Found attrId ${attr.Id} for E${enemyUid} ${attr.RawData.toString('base64')}`);
697+
const base64Data = attr.RawData.toString('base64');
698+
682699
switch (attr.Id) {
683700
case AttrType.AttrName:
684701
const enemyName = reader.string();
@@ -704,7 +721,6 @@ class PacketProcessor {
704721
this.userDataManager.enemyCache.lastSeen.set(enemyUid, Date.now());
705722
break;
706723
default:
707-
// this.logger.debug(`Found unknown attrId ${attr.Id} for E${enemyUid} ${attr.RawData.toString('base64')}`);
708724
break;
709725
}
710726
}
@@ -713,7 +729,7 @@ class PacketProcessor {
713729
#reportBossHpThreshold(enemyUid: string, currentHp: number) {
714730
try {
715731
// Check if BPTimer submission is enabled
716-
if (this.userDataManager.globalSettings.enableBPTimerSubmission === false) {
732+
if (this.userDataManager.globalSettings?.enableBPTimerSubmission === false) {
717733
return;
718734
}
719735

@@ -730,7 +746,7 @@ class PacketProcessor {
730746

731747
if (currentHp === 0 || currentHp <= maxHp * 0.001) {
732748
const line = this.userDataManager.getCurrentLineId();
733-
const bpTimer = initialize(this.logger);
749+
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
734750
setTimeout(() => {
735751
bpTimer.resetMonster(monsterId, line);
736752
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (HP reached 0)`);
@@ -742,7 +758,7 @@ class PacketProcessor {
742758

743759
const line = this.userDataManager.getCurrentLineId();
744760

745-
const bpTimer = initialize(this.logger);
761+
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
746762
bpTimer.createHpReport(monsterId, hpPercentage, line).catch(err => {
747763
this.logger.debug(`[BPTimer] Failed to report HP threshold: ${err.message}`);
748764
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bpsr-meter",
3-
"version": "0.3.4",
3+
"version": "0.3.5",
44
"description": "BPSR Meter",
55
"author": "Denoder",
66
"type": "module",

src/main/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,6 @@ function setupIpcHandlers() {
361361
Object.assign(currentSettings, settings);
362362
await fs.promises.writeFile(settingsPath, JSON.stringify(currentSettings, null, 4));
363363

364-
// Broadcast transparency changes to all windows
365364
if (settings.hasOwnProperty("disableTransparency")) {
366365
Object.values(windows).forEach((window) => {
367366
if (window && !window.isDestroyed()) {
@@ -370,7 +369,6 @@ function setupIpcHandlers() {
370369
});
371370
}
372371

373-
// Broadcast heightStep changes to main window
374372
if (settings.hasOwnProperty("heightStep")) {
375373
if (windows.main && !windows.main.isDestroyed()) {
376374
windows.main.webContents.send("height-step-changed", settings.heightStep);
@@ -386,19 +384,16 @@ function setupIpcHandlers() {
386384
const logsDir = path.join(userDataPath, "logs");
387385
const logPath = path.join(logsDir, logId);
388386

389-
// Security check: ensure the path is within the logs directory
390387
if (!logPath.startsWith(logsDir)) {
391388
return { success: false, error: "Invalid log path" };
392389
}
393390

394-
// Check if directory exists
395391
try {
396392
await fs.promises.access(logPath);
397393
} catch {
398394
return { success: false, error: "Log not found" };
399395
}
400396

401-
// Delete the directory recursively
402397
await fs.promises.rm(logPath, { recursive: true, force: true });
403398
logToFile(`Deleted history log: ${logId}`);
404399

src/renderer/src/monsters/App.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,26 @@ import { useTranslations } from "../shared/hooks/useTranslations";
55
import { useWindowControls, useSocket } from "../shared/hooks";
66
import MonstersHeader from "./MonstersHeader";
77
import SortDropdown from "./SortDropdown";
8+
import { TRACKED_MONSTER_IDS } from "../shared/constants";
89

910
interface MonsterEntry {
1011
name?: string | null;
1112
hp?: number | null;
1213
max_hp?: number | null;
1314
monster_id?: number | null;
1415
last_seen?: number | null;
16+
position?: { x: number; y: number; z: number } | null;
17+
distance?: number | null;
1518
}
1619

1720
export default function MonstersApp(): React.JSX.Element {
1821
const [monsters, setMonsters] = useState<Record<string, MonsterEntry>>({});
1922
const [isLoading, setIsLoading] = useState<boolean>(true);
2023
const [error, setError] = useState<string | null>(null);
21-
const [sortKey, setSortKey] = useState<"id" | "name" | "hp">("hp");
22-
const [sortDesc, setSortDesc] = useState<boolean>(true);
24+
const [sortKey, setSortKey] = useState<"id" | "name" | "hp" | "distance">("distance");
25+
const [sortDesc, setSortDesc] = useState<boolean>(false);
2326
const [zhNames, setZhNames] = useState<Record<number, string>>({});
27+
const [bossOnlyMode, setBossOnlyMode] = useState<boolean>(false);
2428

2529
const { scale, zoomIn, zoomOut, handleDragStart, handleClose, isDragging } = useWindowControls({
2630
baseWidth: 560,
@@ -149,7 +153,25 @@ export default function MonstersApp(): React.JSX.Element {
149153
) : (
150154
<div className="monsters-container">
151155
<div className="flex justify-between items-center mb-2 gap-2">
152-
<div className="text-sm font-semibold">Monsters ({Object.keys(monsters).length})</div>
156+
<div className="flex items-center gap-2">
157+
<div className="text-sm font-semibold">
158+
Monsters ({
159+
bossOnlyMode
160+
? Object.values(monsters).filter(m => m.monster_id && TRACKED_MONSTER_IDS.has(String(m.monster_id))).length
161+
: Object.keys(monsters).length
162+
})
163+
</div>
164+
<button
165+
onClick={() => setBossOnlyMode((prev) => !prev)}
166+
className="text-xs px-2 py-1 rounded"
167+
style={{
168+
background: bossOnlyMode ? "#3498db" : "rgba(255,255,255,0.1)",
169+
color: bossOnlyMode ? "#fff" : "rgba(255,255,255,0.7)"
170+
}}
171+
>
172+
{bossOnlyMode ? "Bosses Only" : "All Monsters"}
173+
</button>
174+
</div>
153175
<div className="flex items-center gap-2">
154176
<label className="text-xs">Sort:</label>
155177
<div className="min-w-[140px]">
@@ -162,16 +184,20 @@ export default function MonstersApp(): React.JSX.Element {
162184
<table className="monsters-table w-full border-collapse">
163185
<thead>
164186
<tr>
165-
<th className="text-left p-1">ID</th>
166187
<th className="text-left p-1">Name</th>
167188
<th className="text-right p-1">HP</th>
168189
<th className="text-right p-1">Max HP</th>
169190
<th className="text-right p-1">HP %</th>
191+
<th className="text-right p-1 pl-5">Distance</th>
170192
</tr>
171193
</thead>
172194
<tbody>
173195
{Object.entries(monsters)
174196
.map(([id, m]) => ({ id, ...m }))
197+
.filter((m) => {
198+
if (!bossOnlyMode) return true;
199+
return m.monster_id && TRACKED_MONSTER_IDS.has(String(m.monster_id));
200+
})
175201
.sort((a, b) => {
176202
if (sortKey === "hp") {
177203
const ah = a.hp ?? -Infinity;
@@ -183,17 +209,21 @@ export default function MonstersApp(): React.JSX.Element {
183209
const bn = (b.name || "").toString();
184210
return sortDesc ? bn.localeCompare(an) : an.localeCompare(bn);
185211
}
186-
// id
187-
const ai = Number(a.id);
188-
const bi = Number(b.id);
189-
return sortDesc ? bi - ai : ai - bi;
212+
if (sortKey === "distance") {
213+
const ad = a.distance ?? Infinity;
214+
const bd = b.distance ?? Infinity;
215+
return sortDesc ? bd - ad : ad - bd;
216+
}
217+
return 0;
190218
})
191219
.map((m) => {
192-
const displayName = `${t(`monsters.${m.monster_id}`, m?.name ?? "Unknown")} (${m.monster_id})`;
220+
const displayName = `${t(`monsters.${m.monster_id}`, m?.name ?? "Unknown")}`;
193221
const pct = m.max_hp && m.hp ? Math.max(0, Math.min(1, (m.hp / m.max_hp))) : null;
222+
const distanceText = m.distance !== null && m.distance !== undefined
223+
? `${m.distance.toFixed(1)}m`
224+
: "-";
194225
return (
195226
<tr key={m.id} style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
196-
<td className="p-2">{m.id}</td>
197227
<td className="p-2">{displayName}</td>
198228
<td className="p-2 text-right">{m?.hp ?? "-"}</td>
199229
<td className="p-2 text-right">{m?.max_hp ?? "-"}</td>
@@ -209,6 +239,7 @@ export default function MonstersApp(): React.JSX.Element {
209239
</div>
210240
)}
211241
</td>
242+
<td className="p-2 text-right text-xs font-mono">{distanceText}</td>
212243
</tr>
213244
);
214245
})}

src/renderer/src/monsters/SortDropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface Option {
88
const OPTIONS: Option[] = [
99
{ value: "hp", label: "HP" },
1010
{ value: "name", label: "Name" },
11-
{ value: "id", label: "ID" },
11+
{ value: "distance", label: "Distance" },
1212
];
1313

1414
export function SortDropdown({ value, onChange }: { value: string; onChange: (v: string) => void; }): React.JSX.Element {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Boss monster IDs that are tracked for BPTimer
2+
export const TRACKED_MONSTER_IDS = new Set([
3+
// Golden Juggernaut
4+
'80006', '10032',
5+
// Frost Ogre
6+
'108', '10009', '20100', '20127', '80002', '2000129', '2000140', '2004172', '3000006', '3000019',
7+
// Inferno Ogre
8+
'80004', '10018',
9+
// Phantom Arachnocrab
10+
'80008', '10069',
11+
// Brigand Leader
12+
'10056',
13+
// Venobzzar Incubator
14+
'80009', '10077', '3000025',
15+
// Muku Chief
16+
'80007', '10059', '3000022',
17+
// Iron Fang
18+
'8611', '80010', '10081', '3000024',
19+
// Storm Goblin King
20+
'80001', '10007', '61219', '61220', '61221',
21+
// Tempest Ogre
22+
'4301', '10010', '20024', '20072', '20092', '80003', '2000131', '2000137', '3000001', '3000020', '530354',
23+
// Celestial Flier
24+
'8614', '80011', '10082', '3000023',
25+
// Earth Ogre
26+
'80005', '10025',
27+
]);

src/server/dataManager.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,13 @@ interface EnemyCache {
495495
maxHp: Map<string, number>;
496496
monsterId: Map<string, number>;
497497
lastSeen: Map<string, number>;
498+
position: Map<string, { x: number; y: number; z: number; }>;
499+
}
500+
501+
interface PlayerPosition {
502+
x: number;
503+
y: number;
504+
z: number;
498505
}
499506

500507
interface SceneInfo {
@@ -515,6 +522,7 @@ export class UserDataManager {
515522
logDirExist: Set<string>;
516523
enemyCache: EnemyCache;
517524
localPlayerUid: number | null;
525+
localPlayerPosition: PlayerPosition | null;
518526
lastLogTime?: number;
519527
sceneData: Map<string, SceneInfo>;
520528

@@ -535,9 +543,11 @@ export class UserDataManager {
535543
maxHp: new Map(),
536544
monsterId: new Map(),
537545
lastSeen: new Map(),
546+
position: new Map(),
538547
};
539548
this.sceneData = new Map();
540549
this.localPlayerUid = null;
550+
this.localPlayerPosition = null;
541551
}
542552

543553
setLocalPlayerUid(uid: number): void {
@@ -546,6 +556,20 @@ export class UserDataManager {
546556
}
547557
}
548558

559+
setLocalPlayerPosition(position: { x: number; y: number; z: number }): void {
560+
this.localPlayerPosition = position;
561+
}
562+
563+
calculateDistance(enemyPosition: { x: number; y: number; z: number }): number | null {
564+
if (!this.localPlayerPosition) return null;
565+
566+
const dx = enemyPosition.x - this.localPlayerPosition.x;
567+
const dy = enemyPosition.y - this.localPlayerPosition.y;
568+
const dz = enemyPosition.z - this.localPlayerPosition.z;
569+
570+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
571+
}
572+
549573
async initialize(): Promise<void> { }
550574

551575
getUser(uid: number): UserData {
@@ -774,7 +798,16 @@ export class UserDataManager {
774798
]);
775799
const now = Date.now();
776800
const STALE_MS = 4500;
801+
// Exclude companions
802+
const EXCLUDED_MONSTER_IDS = new Set([3100000, 3100001, 3100002]);
803+
777804
enemyIds.forEach((id) => {
805+
const monsterId = this.enemyCache.monsterId.get(id);
806+
807+
if (monsterId && EXCLUDED_MONSTER_IDS.has(monsterId)) {
808+
return;
809+
}
810+
778811
const last = this.enemyCache.lastSeen.get(id) || 0;
779812
let hpVal = this.enemyCache.hp.get(id);
780813

@@ -786,12 +819,17 @@ export class UserDataManager {
786819
}
787820
}
788821

822+
const position = this.enemyCache.position.get(id);
823+
const distance = position ? this.calculateDistance(position) : null;
824+
789825
result[id] = {
790826
name: this.enemyCache.name.get(id),
791827
hp: hpVal,
792828
max_hp: this.enemyCache.maxHp.get(id),
793829
monster_id: this.enemyCache.monsterId.get(id) ?? null,
794830
last_seen: this.enemyCache.lastSeen.get(id) ?? null,
831+
position: position ? { x: position.x, y: position.y, z: position.z } : null,
832+
distance: distance,
795833
};
796834
});
797835
return result;
@@ -803,12 +841,13 @@ export class UserDataManager {
803841
this.enemyCache.maxHp.clear();
804842
this.enemyCache.monsterId.clear();
805843
this.enemyCache.lastSeen.clear();
844+
this.enemyCache.position.clear();
806845
}
807846

808847
async clearAll(isLineSwitch: boolean = false): Promise<void> {
809-
const shouldSave = this.users.size > 0 &&
810-
this.globalSettings.enableHistorySave &&
811-
(!isLineSwitch || this.globalSettings.saveOnLineSwitch !== false);
848+
const shouldSave = this.users.size > 0 &&
849+
this.globalSettings.enableHistorySave &&
850+
(!isLineSwitch || this.globalSettings.saveOnLineSwitch !== false);
812851

813852
if (shouldSave) {
814853
await this.saveAllUserData();

0 commit comments

Comments
 (0)