-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexpTrackerSystem.lua
More file actions
408 lines (357 loc) · 15.8 KB
/
expTrackerSystem.lua
File metadata and controls
408 lines (357 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
dofile('tools/stateManager')
function ExpTrackerSystem()
local system = {
configFile = '/exp_tracker/config.json';
isInit = false;
trackExpEvent = false;
cleanOldDataEvent = false;
currentMode = 'adjusted';
stateManager = StateManager({ staminaEnabled = true, expStagesEnabled = true, expStages = {} });
-- Time intervals for exp tracking (in seconds)
timeIntervals = {60, 180, 300, 600, 900, 1800, 3600, 7200, 10800, 21600, 43200, 86400, 172800};
-- Experience data storage
expData = {
history = {}, -- Stores exp gains with timestamps
deaths = {}, -- Stores death events
lastExp = 0,
currentLevel = 0,
currentExp = 0,
playerMap = {}, -- Maps player names to unique IDs
nextPlayerId = 1,
playerEvents = {}, -- List of player visibility events: {id, appear, disappear}
uniquePlayers = {} -- Maps player ID to last update timestamp for unique count optimization
};
-- Module initialization
init = function(self)
if not self.isInit then
self:loadConfig()
self:connect()
self.trackExpEvent = cycleEvent(function() self:trackExp() end, 1000)
self.cleanOldDataEvent = cycleEvent(function() self:cleanOldData() end, 60000)
-- Listen for changes to save config
self.stateManager:onStateChange('staminaEnabled', function() self:saveConfig() end)
self.stateManager:onStateChange('expStagesEnabled', function() self:saveConfig() end)
self.isInit = true
end
end;
-- Module termination
terminate = function(self)
if self.isInit then
self:disconnect()
if self.trackExpEvent ~= false then
removeEvent(self.trackExpEvent)
self.trackExpEvent = false
end
if self.cleanOldDataEvent ~= false then
removeEvent(self.cleanOldDataEvent)
self.cleanOldDataEvent = false
end
self.isInit = false
end
end;
connect = function(self)
connect(g_game, { onDeath = self.onDeath })
connect(LocalPlayer, { onPositionChange = self.onLocalPlayerPositionChange })
connect(Player, {
onAppear = self.onPlayerAppear,
onDisappear = self.onPlayerDisappear,
onPositionChange = self.onPlayerPositionChange
})
end;
disconnect = function(self)
disconnect(g_game, { onDeath = self.onDeath })
disconnect(LocalPlayer, { onPositionChange = self.onLocalPlayerPositionChange })
disconnect(Player, {
onAppear = self.onPlayerAppear,
onDisappear = self.onPlayerDisappear,
onPositionChange = self.onPlayerPositionChange
})
end;
-- Load configuration
loadConfig = function(self)
if g_resources.fileExists(self.configFile) then
local parsed = JSON.decode(g_resources.readFileContents(self.configFile))
self.stateManager:setState({
staminaEnabled = parsed.staminaEnabled == nil and true or parsed.staminaEnabled,
expStagesEnabled = parsed.expStagesEnabled == nil and true or parsed.expStagesEnabled,
expStages = parsed.stages or {}
})
else
self.stateManager:setState({ staminaEnabled = true, expStagesEnabled = true, expStages = {} })
end
end;
getModuleOrMods = function(self)
local splitPath = string.split(g_resources.getRealDir(), '/')
return splitPath[#splitPath]
end;
writeFile = function(self, path, text)
local file = io.open(g_resources.getWorkDir() .. self:getModuleOrMods() .. path, "w")
if file then
file:write(text)
file:close()
return true
else
return false
end
end;
-- Save configuration
saveConfig = function(self)
local config = {
stages = self.stateManager:get('expStages'),
staminaEnabled = self.stateManager:get('staminaEnabled'),
expStagesEnabled = self.stateManager:get('expStagesEnabled')
}
self:writeFile(self.configFile, JSON.encode(config))
end;
-- Get experience multiplier based on level
getExpMultiplier = function(self, level)
if not self.stateManager:get('expStagesEnabled') then
return 1.0 -- Default multiplier when exp stages are disabled
end
for _, stage in ipairs(self.stateManager:get('expStages')) do
if level >= stage.levelMin and (stage.levelMax == 0 or level <= stage.levelMax) then
return stage.multiplier
end
end
return 1.0 -- Default multiplier if no stage matches
end;
-- Calculate stamina bonus multiplier (150% for 42-40 hours)
getStaminaBonusMultiplier = function(self)
if not self.stateManager:get('staminaEnabled') then return 1.0 end
local stamina = g_game.getLocalPlayer():getStamina() / 60 -- Convert to hours
return stamina >= 40 and stamina <= 42 and 1.5 or 1.0
end;
-- Calculate stamina penalty multiplier (50% below 14 hours)
getStaminaPenaltyMultiplier = function(self)
local stamina = g_game.getLocalPlayer():getStamina() / 60 -- Convert to hours
return stamina < 14 and 0.5 or 1.0
end;
-- Calculate experience needed for next level
getExpForLevel = function(self, level)
return math.floor(((50 * level * level * level) - (150 * level * level) + (400 * level)) / 3)
end;
-- Get current number of visible players
getCurrentPlayersCount = function(self)
local count = 0
for _, event in ipairs(self.expData.playerEvents) do
if event.disappear == nil then
count = count + 1
end
end
return count
end;
-- Track experience gain
trackExp = function(self)
local player = g_game.getLocalPlayer()
if not player then return end
local currentExp = player:getExperience()
local level = player:getLevel()
if self.expData.lastExp > 0 and currentExp > self.expData.lastExp then
local expGain = currentExp - self.expData.lastExp
local multiplier = self:getExpMultiplier(level) * self:getStaminaBonusMultiplier() * self:getStaminaPenaltyMultiplier()
local adjustedExp = math.floor(expGain / multiplier)
table.insert(self.expData.history, {
timestamp = os.time(),
adjusted = adjustedExp,
raw = expGain,
level = level,
players = self:getCurrentPlayersCount()
})
end
self.expData.lastExp = currentExp
self.expData.currentLevel = level
self.expData.currentExp = currentExp
end;
-- Clean old data
cleanOldData = function(self)
local now = os.time()
local maxInterval = self.timeIntervals[#self.timeIntervals]
for i = #self.expData.history, 1, -1 do
if now - self.expData.history[i].timestamp > maxInterval then
table.remove(self.expData.history, i)
end
end
for i = #self.expData.playerEvents, 1, -1 do
local event = self.expData.playerEvents[i]
if event.disappear and now - event.disappear > maxInterval then
table.remove(self.expData.playerEvents, i)
end
end
for id, ts in pairs(self.expData.uniquePlayers) do
if now - ts > maxInterval then
self.expData.uniquePlayers[id] = nil
end
end
end;
-- Handle player death
onDeathAction = function(self)
local player = g_game.getLocalPlayer()
if not player then return end
local expLoss = self.expData.currentExp - player:getExperience()
table.insert(self.expData.deaths, {
timestamp = os.time(),
expLost = expLoss,
previousExp = self.expData.currentExp,
level = self.expData.currentLevel
})
end;
onPlayerAppearAction = function(self, player)
if not player:isLocalPlayer() and player:getPosition().z == g_game.getLocalPlayer():getPosition().z then
local name = player:getName()
if not self.expData.playerMap[name] then
self.expData.playerMap[name] = self.expData.nextPlayerId
self.expData.nextPlayerId = self.expData.nextPlayerId + 1
end
local id = self.expData.playerMap[name]
table.insert(self.expData.playerEvents, {id = id, appear = os.time(), disappear = nil})
self.expData.uniquePlayers[id] = os.time()
end
end;
onPlayerDisappearAction = function(self, player)
if g_game.isOnline() and not player:isLocalPlayer() and player:getPosition().z == g_game.getLocalPlayer():getPosition().z then
local name = player:getName()
local id = self.expData.playerMap[name]
if id then
for i = #self.expData.playerEvents, 1, -1 do
local event = self.expData.playerEvents[i]
if event.id == id and event.disappear == nil then
event.disappear = os.time()
break
end
end
self.expData.uniquePlayers[id] = os.time()
end
end
end;
onLocalPlayerFloorChangeAction = function(self, localPlayer, newPos, oldPos)
if oldPos and newPos.z ~= oldPos.z then
-- Get spectators: multiFloor -> true
local spectators = g_map.getSpectators(localPlayer:getPosition(), true)
for _, creature in ipairs(spectators) do
if creature:isPlayer() and not creature:isLocalPlayer() then
if oldPos.z == creature:getPosition().z then
self:onPlayerDisappearAction(creature)
elseif newPos.z == creature:getPosition().z then
self:onPlayerAppearAction(creature)
end
end
end
end
end;
onPlayerFloorChangeAction = function(self, player, newPos, oldPos)
if not player:isLocalPlayer() and newPos and oldPos then
if oldPos.z == g_game.getLocalPlayer():getPosition().z and oldPos.z ~= newPos.z then
self:onPlayerDisappearAction(player)
elseif newPos.z == g_game.getLocalPlayer():getPosition().z and oldPos.z ~= newPos.z then
self:onPlayerAppearAction(player)
end
end
end;
-- Calculate exp gain for interval
getExpForInterval = function(self, seconds, mode)
mode = mode or self.currentMode
local field = (mode == 'adjusted') and 'adjusted' or 'raw'
local now = os.time()
local totalExp = 0
for _, entry in ipairs(self.expData.history) do
if now - entry.timestamp <= seconds then
totalExp = totalExp + entry[field]
end
end
return totalExp
end;
-- Calculate unique players for interval
getUniquePlayersForInterval = function(self, seconds)
local now = os.time()
local start = now - seconds
local count = 0
for _, ts in pairs(self.expData.uniquePlayers) do
if ts >= start then
count = count + 1
end
end
return count
end;
-- Calculate time to next level
getTimeToNextLevel = function(self)
local player = g_game.getLocalPlayer()
if not player then return 0 end
local level = player:getLevel()
local currentExp = player:getExperience()
local expNeeded = self:getExpForLevel(level + 1) - currentExp
local expPerSecond = self:getExpForInterval(3600, 'raw') / 3600 -- Exp per second based on last hour
return expPerSecond > 0 and math.floor(expNeeded / expPerSecond) or 0
end;
-- Calculate level at stamina end
getLevelAtStaminaEnd = function(self)
local player = g_game.getLocalPlayer()
if not player then return 0 end
local stamina = player:getStamina() / 60 -- In hours
if stamina > 40 then stamina = 40 end -- Cap at 40 hours (stamina limit)
local expPerSecond = self:getExpForInterval(3600, 'raw') / 3600
local totalExp = expPerSecond * stamina * 3600
local level = player:getLevel()
local currentExp = player:getExperience()
while totalExp > 0 do
local expToNext = self:getExpForLevel(level + 1) - currentExp
if totalExp >= expToNext then
totalExp = totalExp - expToNext
currentExp = self:getExpForLevel(level + 1)
level = level + 1
else
break
end
end
return level
end;
-- Get death history
getDeathHistory = function(self)
return self.expData.deaths
end;
-- Get exp stages
getExpStages = function(self)
return self.stateManager:get('expStages')
end;
-- Add new exp stage
addExpStage = function(self, levelMin, levelMax, multiplier)
local expStages = self.stateManager:get('expStages')
table.insert(expStages, {
levelMin = levelMin,
levelMax = levelMax,
multiplier = multiplier
})
table.sort(expStages, function(a, b) return a.levelMin < b.levelMin end)
self.stateManager:set('expStages', expStages)
self:saveConfig()
end;
-- Remove exp stage by index
removeExpStage = function(self, index)
local expStages = self.stateManager:get('expStages')
if expStages[index] then
table.remove(expStages, index)
self.stateManager:set('expStages', expStages)
self:saveConfig()
end
end;
-- Get StateManager instance
getStateManager = function(self)
return self.stateManager
end;
}
system.onDeath = function()
system:onDeathAction()
end
system.onPlayerAppear = function(player)
system:onPlayerAppearAction(player)
end
system.onPlayerDisappear = function(player)
system:onPlayerDisappearAction(player)
end
system.onLocalPlayerPositionChange = function(localPlayer, newPos, oldPos)
system:onLocalPlayerFloorChangeAction(localPlayer, newPos, oldPos)
end
system.onPlayerPositionChange = function(player, newPos, oldPos)
system:onPlayerFloorChangeAction(player, newPos, oldPos)
end
return system
end