treat lvls > max_level as max_level, when dropping
[minetest-xpmod:xp.git] / init.lua
1 -- Copyright 2012 Andrew Engelbrecht.
2 -- This file is licensed under the WTFPL.
3
4 ----------------------------------
5
6 -- Some variables you can change:
7 ----------------------------------
8
9 -- How often (in seconds) xplevels file saves
10 local save_delta = 10
11 -- Max level a player can become; 0 means max level of 0.
12 local max_level = 32
13
14 ----------------------------------
15
16 -- some mod variables:
17 ----------------------------------
18
19 local xpmodver = "0.0.3"
20 local ffver = 1
21 local ffversupport = {1}
22 local file_name = "xplevels"
23
24 local xplevels_file = minetest.get_worldpath() .. "/" .. file_name
25 local xplevels = {}
26 local playert = {}
27 -- dynamically updated to note changes since last save to file:
28 local changed = false
29 -- automatically populated with list of xp needed to gain next level
30 local lptable = nil
31 -- create a global random seed
32 local prand = PseudoRandom(os.time())
33
34 ----------------------------------
35
36 -- function definitions:
37 ----------------------------------
38
39 -- prints out some default error text, the text passed as a parameter.
40 -- then the function kills the server.
41 local function parseerr(text)
42     print("")
43     print("*** xp module error:")
44     print("*** error parsing /"..file_name.." file!")
45     print("***")
46     print("*** "..text)
47     print("***")
48     print("")
49     os.exit()
50 end
51
52 -- extracts xp/level info per skill type for each user
53 -- pass it a line from the xplevels_file.
54 -- it returns the user name and a table of their stats
55 local function parselinev1(line)
56     local s,e,ign,spaceonly,laste,name,statstr,stkind,stlevel,stxp,badskl,test
57     local stats = {}
58
59     -- grabs any space or or an empty string on a blank line
60     ign,ign,spaceonly = string.find(line, "^(%s*)$")
61     if spaceonly ~= nil then
62         -- this is an empty line
63         return nil, nil
64     end
65
66     -- grabs text before the space
67     s,e,name = string.find(line, "^([^%s]+)%s")
68     if name == nil then
69         parseerr("missing space after user name:\n"..line)
70     end
71
72     laste = e
73     repeat
74         -- grabs the next set of text in parenthesis:
75         s,e,statstr = string.find(line, "^%s-%((.-)%)", e + 1)
76
77         if statstr ~= nil then
78             laste = e
79             -- grabs all the data in the parenthesis:
80             ign,ign,stkind,stlevel,stxp = string.find(statstr, "^([^%s]+): level: (%d+) xp: (%d+)$")
81
82             if stxp == nil then
83                 parseerr("bad text between parenthesis:\n"..statstr.."\n*** in this bad line:\n"..line)
84             end
85
86             stats[stkind] = {xp = tonumber(stxp), level = tonumber(stlevel)}
87         end
88     until statstr == nil
89
90     -- there should only be white space at the end of the line:
91     ign,ign,test = string.find(line, "^%s*(.-)$", laste + 1)
92     if test ~= "" then
93         -- finds the first string of text without parenthesis
94         ign,ign,badskl = string.find(line, "^%s*([^%s]+).*$", laste + 1)
95         parseerr("skill\n"..badskl.."\n*** lacks parenthesis in this line:\n"..line)
96     end
97
98     return name, stats
99 end
100
101 -- checks the version and format version string.
102 -- these signify the version of the xp mod that made the file,
103 -- and the formating style that was used.
104 -- pass it the first line of the xplevels_file
105 -- returns a pointer to the appropriate parser function
106 local function checkfver(line)
107     local s,e,ver,fmt,i,v
108
109     -- grabs the version, and the format version
110     s,e,ver,fmt = string.find(line, "-- Version: ([%d%a%.%-]+) File Format: (%d+)%s-$")
111
112     if ver == nil or fmt == nil then
113         print("*** xp module error: malformed Version string in /"..file_name.." line 1:")
114         print(line)
115         os.exit()
116     end
117
118     if tonumber(fmt) == 1 then
119         return parselinev1
120     else
121         print("*** xp module error: unsupported file format in /"..file_name.." file: "..fmt)
122         print("*** the version of this mod is: "..xpmodver)
123         print("*** this version only supports these format versions: ")
124         for i,v in ipairs(ffversupport) do
125             print(v)
126         end
127         print("***")
128         os.exit()
129     end
130 end
131
132 -- read in the xp and levels for each player listed in the xplevels_file
133 -- store the stats in xplevels, the table.
134 local function loadxp()
135     local input, line, parsel, name, stats
136
137     input = io.open(xplevels_file, "r")
138
139     if input ~= nil then
140         line = input:read("*l")
141
142         if line ~= nil then
143             parsel = checkfver(line)
144
145             line = input:read("*l")
146             while line ~= nil do
147
148                 name, stats = parsel(line)
149                 if name ~= nil then
150                     xplevels[name] = stats
151                 end
152
153                 line = input:read("*l")
154             end
155         end
156         io.close(input)
157     end
158 end
159
160 local function get_time()
161     return os.time()
162 end
163
164 -- returns true if player has ever logged into the server
165 -- it's different than minetest.is_player() since it accepts the name as a string,
166 -- and because you don't need to run a function that returns a player obj only for *online* players.
167 local function is_player(name)
168     local file
169
170     if playert[name] == nil then
171         file = io.open(minetest.get_worldpath().."/players/"..name, "r")
172         if file ~= nil then
173             playert[name] = true
174             io.close(file)
175         else
176             playert[name] = false
177         end
178     end
179
180     return playert[name]
181 end
182
183 -- returns the number of xp required to gain the next level.
184 -- pass it the player's current level.
185 local function lastpoint(level)
186     local i
187
188     if lptable == nil then
189         lptable = {}
190         for i = 0,max_level - 1 do
191             lptable[i] = (i + 1)^2 * 10
192         end
193     end
194     return lptable[level]
195 end
196
197 -- initializes a player's experience points and level for a given skill
198 local function initpxp(name, skill)
199     if xplevels[name] == nil then
200         xplevels[name] = {}
201     end
202     if xplevels[name][skill] == nil then
203         xplevels[name][skill] = {xp = 0, level = 0}
204     end
205 end
206
207 -- gives npoints additional xp points for the player in the specified skillset
208 local function gainxp(name, skill, npoints)
209     local stat, appnd, gainedlevel
210
211     if npoints < 0 then
212         return "negative"
213     end
214
215     if is_player(name) == false then
216         return "non-player"
217     end
218
219     initpxp(name, skill)
220     stat = xplevels[name][skill]
221
222     if stat.level >= max_level then
223         return "level-max"
224     end
225
226     -- the file needs to be updated in the future:
227     changed = true
228
229     gainedlevel = false
230     while npoints > 0 do
231
232         if stat.xp + npoints < lastpoint(stat.level) then
233             stat.xp = stat.xp + npoints
234             if gainedlevel == true then
235                 break
236             else
237                 return "gained-xp"
238             end
239         else
240             npoints = npoints - (lastpoint(stat.level) - stat.xp)
241
242             stat.level = stat.level + 1
243             stat.xp = 0
244
245             if stat.level >= max_level then
246                 appnd = " (max)"
247                 break
248             else
249                 appnd = ""
250             end
251
252             gainedlevel = true
253         end
254     end
255     minetest.chat_send_player(name, "Welcome to level "..stat.level.." in "..skill.."!"..appnd)
256     minetest.log("action", "xp mod: "..name.." gained level "..stat.level.." in "..skill..appnd..".")
257
258     return "gained-level"
259 end
260
261 -- prints the level and xp for a player in the specified skillset.
262 -- if no skillset is selected, then stats for every skillset are printed.
263 -- output goes to the player's chat console.
264 local function printxp(name, skill)
265     local i,v,stat,appnd
266
267     if xplevels[name] == nil then
268         minetest.chat_send_player(name, "You have no experience in anything.")
269         return
270     end
271
272     if skill == "" then
273         for i,v in pairs(xplevels[name]) do
274             if i ~= "" then
275                 printxp(name, i)
276             end
277         end
278     else
279         stat = xplevels[name][skill]
280         if stat == nil then
281             minetest.chat_send_player(name, "You don't have experience in '"..skill.."'. (check your spelling.)")
282         else
283             initpxp(name, skill)
284
285             if stat.level >= max_level then
286                 appnd = ". (max)"
287             else
288                 appnd = ", plus "..stat.xp.."/"..lastpoint(stat.level).." experience points."
289             end
290             minetest.chat_send_player(name, "You are level "..stat.level.." in "..skill..appnd)
291         end
292     end
293 end
294
295 -- save user xp and levels to xplevels_file
296 local function savetofile()
297     local line, output, i, v, j, w
298
299     output = io.open(xplevels_file, "w")
300
301     output:write("-- Version: "..xpmodver.." File Format: "..ffver.."\n")
302
303     for i, v in pairs(xplevels) do
304         line = ""
305         for j, w in pairs(v) do
306             line = line.." ("..j..": level: "..w.level.." xp: "..w.xp..")"
307         end
308         output:write(i..line.."\n")
309     end
310
311     io.close(output)
312 end
313
314 -- returns the probability of getting a drop, based on a player's level
315 -- if level_max is set to the default of 32, then:
316 -- at level 0, the prob. is about 1/100,000.
317 -- at level_max, it's about 1/1,000.
318 local function gotdrop(level)
319     local prob, randn
320
321     if level > max_level then
322         level = max_level
323     end
324
325     prob = (level + 3)^2 / (max_level + 3)^2 / 1000
326     randn = prand:next(0, 32767) / 32767
327
328     if randn < prob then
329         return true
330     else
331         return false
332     end
333 end
334
335 -- gives a mese block to player named 'name'.
336 -- if thier inventory is full, then it falls on the ground.
337 local function givemese(name)
338     local player, pos
339     player = minetest.env:get_player_by_name(name)
340
341     initpxp(name, 'digging')
342     if xplevels[name].digging.level >= 2 then
343         inv = player:get_inventory()
344
345         if inv:room_for_item('main', 'default:mese') then
346             inv:add_item('main', 'default:mese')
347         else
348             pos = player:getpos()
349             pos.y = pos.y + 1.5
350             minetest.spawn_item(pos, 'default:mese')
351         end
352     end
353 end
354
355 ----------------------------------
356
357 -- callback registration:
358 ----------------------------------
359
360 -- minetest.register_privilege("givexp", "Can give xp to others with /givexp"
361
362 -- it raises the xp of a player by the specified number of points.
363 minetest.register_chatcommand("givexp", {
364     params = "<username> <skill> <num-xp>",
365     description = "Give a player experience points",
366     -- privs = {givexp=true},
367     privs = {},
368     func = function(caller, param)
369         local ign, name, skill, numxp, result
370
371         -- matches two words: the name and the skill
372         ign,ign,name,skill,numxp = string.find(param, "^([^%s]+)%s+([^%s]+)%s+(%d+)%s*$")
373
374         if numxp == nil then
375             minetest.chat_send_player(caller, "Incorrect usage. See: /help givexp")
376         else
377             result = gainxp(name,skill,tonumber(numxp))
378             if result == "non-player" then
379                 minetest.chat_send_player(caller, name.." isn't a player.")
380             elseif result == "level-max" then
381                 minetest.chat_send_player(caller, name.."'s "..skill.." level is already maxed-out.")
382             elseif result == "gained-xp" or result == "gained-level" then
383                 minetest.chat_send_player(caller, name.." is now level "..xplevels[name][skill].level.." plus "..xplevels[name][skill].xp.." xp in "..skill..".")
384                 minetest.log("action", "xp mod: ("..caller.." gave "..name.." "..numxp.." xp in "..skill..".)")
385             end
386         end
387     end,
388 })
389
390 -- shows the xp for the calling player, for a given skill, or if none is listed, then all of them.
391 minetest.register_chatcommand("showxp", {
392     params = "<skill (opt)>",
393     description = "Show your experience points",
394     -- privs = {xp=true},
395     privs = {},
396     func = printxp,
397 })
398
399 -- every time a player places an object, they get one extra xp for this skill
400 minetest.register_on_placenode(function(pos, newnode, placer, oldnode)
401     local name = placer:get_player_name()
402     gainxp(name, "construction", 1)
403 end)
404
405 -- every time a player digs an object, they get one extra xp for this skill
406 -- also, for now, the player gets mese somewhat randomly, based on level.
407 minetest.register_on_dignode(function(pos, node, player)
408     local name = player:get_player_name()
409     gainxp(name, "digging", 1)
410     if gotdrop(xplevels[name].digging.level) then
411         givemese(name)
412     end
413 end)
414
415 -- every time a player joins the game, add their name to the list of existing players.
416 -- if their player file wasn't readable, this works around the issue. also, if this is a new player,
417 -- then they will be added to the table even if they were previously marked as non-existent.
418 minetest.register_on_joinplayer(function(player)
419     local name = player:get_player_name()
420     playert[name] = true
421 end)
422
423 local delta = 0
424 -- every save_delta seconds, save the xp and levels, if they have changed for one or more players
425 minetest.register_globalstep(function(dtime)
426     delta = delta + dtime
427     -- save it every <save_delta> seconds
428     if delta > save_delta then
429         delta = delta - save_delta
430         if changed then
431             savetofile()
432             changed = false
433         end
434     end
435 end)
436
437 ----------------------------------
438
439 -- execute at the start of the game:
440 ----------------------------------
441
442 -- before this file is done being read, this function must be called
443 -- to load player xp and levels for each skill
444 loadxp()
445