· 8 years ago · Aug 02, 2018, 12:10 AM
1-- It is very important to consider all cases for arguments, as the client can send anything for arguments via exploit.
2-- Pretty much just return false for requests with invalid arguments.
3
4-- search "OVH" for overhaul notes
5-- search "PDL" and also look for other potential pokemon data leaks
6-- todo: search all instances of Pokemon:new on server, ensure PlayerData is included in call
7-- destroy Pokemon objects where appropriate
8
9-- OVH remove rc4 as much as possible on server side
10local _f = require(script.Parent)
11
12local PlayerData, PC
13local PlayerDataByPlayer = {}--setmetatable({}, {__mode = 'k'})
14local function onPlayerEnter(player)
15 if not player or not player:IsA('Player') or PlayerDataByPlayer[player] then return end
16 local pd = PlayerData:new(player)
17 PlayerDataByPlayer[player] = pd
18end
19
20local network = _f.Network
21local context = _f.Context
22
23local publicFns = {
24 getContinueScreenInfo = true,
25 continueGame = true,
26 startNewGame = true,
27 saveGame = true,
28 completeEvent = true,
29
30 getStarterData = true,
31 buyStarter = true,
32 buyAshGreninja = true,
33 roStatus = true,
34
35 getParty = true,
36 getPartyPokeBalls = true,
37 getPokemonSummary = true,
38 getCutter = true,
39 getDigger = true,
40 getHeadbutter = true,
41 getSmasher = true,
42 getClimber = true,
43 getHappiness = true,
44
45 getDex = true,
46 getCardInfo = true,
47
48 getBagPouch = true,
49 getTMs = true,
50 getBattleBag = true,
51 useItem = true,
52 giveItem = true,
53 takeItem = true,
54 tossItem = true,
55 teachTM = true,
56 obtainItem = true,
57
58 deleteMove = true,
59 remindMove = true,
60 getShop = true,
61 maxBuy = true,
62 buyItem = true,
63 bMaxBuy = true,
64 buyWithBP = true,
65 sellItem = true,
66
67 makeDecision = true,
68 openPC = true,
69 cPC = true,
70 closePC = true,
71
72 getDCPhrase = true,
73 takeEgg = true,
74 getDCInfo = true,
75 leaveDCPokemon = true,
76 takeDCPokemon = true,
77
78 countBatteries = true,
79 hasFossil = true,
80 reviveFossil = true,
81 dive = true,
82 nextDig = true,
83 finishDig = true,
84
85 nSpins = true,
86 spinForStamp = true,
87 stampInventory = true,
88 setStamps = true,
89
90 hasOKS = true,
91 hasSTP = true,
92 hasFlute = true,
93 hasRTM = true,
94 hasJKey = true,
95 getHoneyData = true,
96 getHoney = true,
97 isDinWM = true,
98 isTinD = true,
99 buySushi = true,
100 getGreenhouseState = true,
101 giveEkans = true,
102 motorize = true,
103
104 hover = true,
105 setHoverboard = true,
106 ownsHoverboard = true,
107 purchaseHoverboard = true,
108
109 getWtrOp = true, -- get water options (Surf/Old Rod/etc.)
110
111 -- debug
112 pdc = true
113}
114local publicEvents = {
115 chooseName = true,
116 completedEggCycle = true,
117 rearrangeParty = true,
118 keepEgg = true,
119 resetFishStreak = true,
120 slatherHoney = true,
121 purchaseRoPower = true,
122 unhover = true,
123}
124network:bindFunction('PDS', function(player, fnName, ...)
125 if not publicFns[fnName] then network.GenerateReport(player, 'attempted to call PDS function "'..tostring(fnName)..'"') return end
126 local pd = PlayerDataByPlayer[player]
127 if not pd then
128 -- uh, we should have created PlayerData for this player... what happened?
129 error(player.Name .. ' has no Player Data')
130 end
131 return pd[fnName](pd, ...)
132end)
133network:bindEvent('PDS', function(player, fnName, ...)
134 if not publicEvents[fnName] then network.GenerateReport(player, 'attempted to call PDS event "'..tostring(fnName)..'"') return end
135 local pd = PlayerDataByPlayer[player]
136 if not pd then
137 -- uh, we should have created PlayerData for this player... what happened?
138 error(player.Name .. ' has no Player Data')
139 end
140 pd[fnName](pd, ...)
141end)
142
143
144local storage = game:GetService('ServerStorage')
145local Utilities = _f.Utilities
146local BitBuffer = _f.BitBuffer--require(storage.Plugins.BitBuffer)
147local Region = require(storage.Plugins.Region)
148local Assets = require(storage.src.Assets) -- for game passes
149local UsableItemsClient = require(storage.src.UsableItemsClient)() -- note: nothing passed for _p
150local RoamingPokemon = require(storage.Data.Chunks).roamingEncounter
151
152local MAX_MONEY = 9999999
153local MAX_BP = 9999
154local RO_POWER_EFFECT_DURATION = 60 * 60
155
156local RUN_FULL_CHECK = false
157
158PlayerData = Utilities.class({
159 className = 'ServerPlayerData',
160 gameBegan = false,
161 trainerName = '',
162 pokedex = '',
163 money = 0,
164 bp = 0,
165 obtainedItems = '',
166 tms = '',
167 hms = '',
168 defeatedTrainers = '',
169 expShareOn = false,
170 lcht = 0,
171 lastDrifloonEncounterWeek = 0,
172 lastTrubbishEncounterWeek = 0,
173 lastHoneyGivenDay = 0,
174 fishingStreak = 0,
175 starterType = '',
176 stampSpins = 0,
177 currentHoverboard = '',
178
179}, function(player)
180 local self = {
181 player = player,
182 userId = player.UserId,
183 trainerName = player.Name, -- temporary/backup
184 pc = PC:new(),
185 party = {},
186 bag = {{},{},{},{},{}}, -- Items, Medicine, Poke Balls, Berries, Key Items
187 badges = {},
188 completedEvents = {},
189 daycare = {
190 depositedPokemon = {},
191 manHasEgg = false
192 },
193 ownedGamePassCache = {},
194 rtick = tick()%1,
195 roPowers = {
196 powerLevel = {0, 0, 0, 0, 0, 0, 0},
197 lastPurchasedAt = {0, 0, 0, 0, 0, 0, 0}
198 },
199 flags = {}, -- for indicating that the player is allowed to do certain tasks
200 lastCompletedEggCycle = tick(),
201-- eggCycleAbuseReports = 0, -- limit these per session
202 decision_data = {},
203 decision_count = 0,
204 starterProductStack = {},
205 ashGreninjaProductStack = {}, -- todo: unify this with the starter purchase system
206 hoverboardProductStack = {},
207 pbStamps = {},
208 ownedHoverboards = {},
209 }
210 setmetatable(self, PlayerData)
211 -- cache player save data as soon as possible
212 Utilities.fastSpawn(PlayerData.getSaveData, self)
213 -- cache owned game passes for quicker lookup
214 if self.userId > 0 then
215 Utilities.fastSpawn(function()
216 for _, passId in pairs(Assets.passId) do
217 self:ownsGamePass(passId)
218 end
219 end)
220 end
221 return self
222end)
223
224function PlayerData:random(x, y)
225 local r = (math.random()+self.rtick)%1
226 if x and y then
227 return math.floor(x + (y+1-x)*r)
228 elseif x then
229 return math.floor(1 + x*r)
230 end
231 return r
232end
233function PlayerData:random2(x, y)
234 local r = (math.random()-self.rtick+1)%1
235 if x and y then
236 return math.floor(x + (y+1-x)*r)
237 elseif x then
238 return math.floor(1 + x*r)
239 end
240 return r
241end
242
243
244function PlayerData:check() end -- OVH todo
245
246
247function PlayerData:isInBattle()
248 return _f.BattleEngine:getBattleSideForPlayer(self.player) ~= nil
249end
250
251function PlayerData:isInTrade()
252 return _f.TradeManager:playerIsInTrade(self.player)
253end
254
255function PlayerData:getParty(context)
256 -- check for open battles involving this player
257 -- party order may change, hp, etc.
258 local battleSide = _f.BattleEngine:getBattleSideForPlayer(self.player)
259 local battleParty
260 if battleSide then
261 battleParty = battleSide.pokemon
262 -- 2v2
263 if battleSide.isTwoPlayerSide and battleSide.battle.is2v2 then
264 local lp = battleSide.battle.listeningPlayers
265 local teamn = (lp[battleSide.id]==self.player) and 1 or 2
266-- local indexOffset = (teamn==2) and battleSide.nPokemonFromTeam1 or 0
267 local party = {}
268 for _, battlePokemon in pairs(battleSide.pokemon) do
269 if battlePokemon.teamn == teamn then
270 table.insert(party, self.party[battlePokemon.originalPartyIndex]:getPartyData(battlePokemon, context))
271 end
272 end
273 return party
274 end
275 --
276 end
277
278 local party = {0, 0, 0, 0, 0, 0} -- placeholders
279 for i, pokemon in ipairs(self.party) do
280 if battleParty then
281 local battlePokemon
282 for _, p in pairs(battleParty) do
283 if p.index == i then
284 battlePokemon = p
285 break
286 end
287 end
288 if battlePokemon then
289 party[battlePokemon.position] = pokemon:getPartyData(battlePokemon, context)
290 end
291 else
292 party[i] = pokemon:getPartyData({}, context)
293 end
294 end
295 for i = 6, 1, -1 do
296 if party[i] == 0 then
297 table.remove(party, i)
298 end
299 end
300 return party
301end
302
303function PlayerData:getPartyPokeBalls()
304 -- we also (discretely) heal here
305 self:heal()
306 local balls = {}
307 for _, p in pairs(self.party) do
308 if not p.egg then
309 table.insert(balls, p.pokeball or 1)
310 end
311 end
312 return balls
313end
314
315function PlayerData:getPokemonSummary(index)
316 local battleSide = _f.BattleEngine:getBattleSideForPlayer(self.player)
317 local pokemon, battlePokemon
318 if battleSide then
319 -- 2v2
320 if battleSide.isTwoPlayerSide and battleSide.battle.is2v2 then
321 local lp = battleSide.battle.listeningPlayers
322 local teamn = (lp[battleSide.id]==self.player) and 1 or 2
323-- local indexOffset = (teamn==2) and battleSide.nPokemonFromTeam1 or 0
324 for _, battlePokemon in pairs(battleSide.pokemon) do
325 if battlePokemon.teamn == teamn then
326 index = index - 1
327 if index == 0 then
328-- if battlePokemon.index == index then
329 return self.party[battlePokemon.originalPartyIndex]:getSummary(battlePokemon)
330 end
331 end
332 end
333 return nil
334 end
335 --
336 battlePokemon = battleSide.pokemon[index]
337 pokemon = self.party[battlePokemon.index]
338 else
339 pokemon = self.party[index]
340 end
341 if not pokemon then return end
342 return pokemon:getSummary(battlePokemon or {})
343end
344
345function PlayerData:getMoveUser(moveId)
346 for _, p in pairs(self.party) do
347 if not p.egg then
348 for _, m in pairs(p.moves) do
349 if m.id == moveId then
350 return p:getName()
351 end
352 end
353 end
354 end
355end
356function PlayerData:getCutter()
357 if not self.badges[1] then return end
358 return self:getMoveUser('cut')
359end
360function PlayerData:getDigger()
361 return self:getMoveUser('dig')
362end
363function PlayerData:getHeadbutter()
364 return self:getMoveUser('headbutt')
365end
366local rockSmashEncounter
367function PlayerData:getSmasher()
368 if not self.badges[5] then return end
369 local pName = self:getMoveUser('rocksmash')
370 if pName then
371 local model = storage.Models.BrokenRock:Clone()
372 model.Parent = self.player:WaitForChild('PlayerGui')
373 local enc
374 if self:random2(3) == 2 then
375 if not rockSmashEncounter then
376 rockSmashEncounter = require(storage.Data.Chunks).rockSmashEncounter
377 end
378 enc = rockSmashEncounter
379 end
380 return pName, model, enc
381 end
382end
383function PlayerData:getClimber()
384 if not self.badges[6] then return end
385 return self:getMoveUser('rockclimb')
386end
387
388function PlayerData:getHappiness()
389 local p = self:getFirstNonEgg()
390 if not p then return end
391 local h = p.happiness
392 local n = 'Your '..p.name..'...'
393 if h >= 255 then
394 return {n, 'It\'s extremely friendly toward you.', 'It couldn\'t possibly love you more.', 'It\'s a pleasure to see!'}
395 elseif h >= 200 then
396 return {n, 'It seems to be very happy.', 'It\'s obviously friendly toward you.'}
397 elseif h >= 150 then
398 return {n, 'It\'s quite friendly toward you.', 'It seems to want to be babied a little.'}
399 elseif h >= 100 then
400 return {n, 'It\'s getting used to you.', 'It seems to believe in you.'}
401 elseif h >= 50 then
402 return {n, 'It\'s not very used to you yet.', 'It neither loves nor hates you.'}
403 elseif h > 0 then
404 return {n, 'It\'s very wary.', 'It has a scary look in its eyes.', 'It doesn\'t like you much at all.'}
405 end
406 return {n, 'This is a little hard for me to say...', 'Your pokemon simply detests you.', 'Doesn\'t that make you uncomfortable?'}
407end
408
409function PlayerData:getDex()
410 return self.pokedex
411end
412
413function PlayerData:getCardInfo()
414 return {
415 name = self.trainerName,
416 dex = select(2, self:countSeenAndOwnedPokemon()),
417 badges = Utilities.map({1,2,3,4,5,6,7,8}, function(i) return self.badges[i] and 1 or 0 end),
418 money = self.money,
419 bp = self.bp
420 }
421end
422
423function PlayerData:chooseName(tName)
424 self.trainerName = tName
425end
426
427function PlayerData:getBattleTeam(ignoreHPState, teamPreviewOrder) -- todo: connect team preview
428 if ignoreHPState and teamPreviewOrder then
429 local team = {}
430 for teamIndex, partyIndex in pairs(teamPreviewOrder) do
431 team[teamIndex] = self.party[partyIndex]:getBattleData(true)
432 end
433 return team
434 end
435
436 local team = {}
437 local fainted = {}
438 for _, p in pairs(self.party) do
439 local d = p:getBattleData(ignoreHPState)
440 if (ignoreHPState or p.hp > 0) and not p.egg then
441 table.insert(team, d)
442 else
443 table.insert(fainted, d)
444 end
445 end
446 assert(#team > 0, 'No healthy Pokemon')
447 for _, d in pairs(fainted) do
448 table.insert(team, d)
449 end
450 return team
451end
452
453function PlayerData:newPokemon(data)
454 return _f.ServerPokemon:new(data, self)
455end
456
457function PlayerData:startNewGame()
458 if self.gameBegan then --[[ERROR]] return false end
459 self.gameBegan = true
460
461 self:onGameBegin()
462end
463
464function PlayerData:continueGame()
465 if self.gameBegan then --[[ERROR]] return false end
466 local data, pcData = self:getSaveData()
467 if not data then --[[ERROR]] return false end
468 self.gameBegan = true
469 self.loadedData = nil -- remove cached data
470
471 local etc = self:deserialize(data)
472 if pcData then
473 self:PC_deserialize(pcData)
474 end
475 self:onGameBegin()
476 return true, etc
477end
478
479function PlayerData:onGameBegin()
480 if self.gameBeganExtras then return end -- dispatch once
481 self.gameBeganExtras = true
482 -- cache game passes that may have been deleted (but the player has the key item for them still)
483 -- or, if they own the pass but not the key item, give them the key item
484 for _, passName in pairs({'ShinyCharm', 'AbilityCharm', 'OvalCharm'}) do
485 local itemId = passName:lower()
486 if self:getBagDataById(itemId, 5) then
487 self.ownedGamePassCache[Assets.passId[passName] ] = true
488 elseif self.ownedGamePassCache[Assets.passId[passName] ] then
489 self:addBagItems({id = itemId, quantity = 1})
490 end
491 end
492 -- the following passes have a special function to run when purchased, activate them
493 for _, passName in pairs({'ExpShare', 'MoreBoxes'}) do
494 local passId = Assets.passId[passName]
495 if self.ownedGamePassCache[passId] then
496 self:onAssetPurchased(passId)
497 end
498 end
499 -- let the player know what these initial values are
500 local firstNonEgg = self:getFirstNonEgg()
501 if firstNonEgg then
502 _f.Network:post('PDChanged', self.player,
503 'firstNonEggLevel', firstNonEgg.level,
504 'firstNonEggAbility', firstNonEgg:getAbilityName(),
505 'money', self.money,
506 'bp', self.bp)
507 end
508 -- etc.
509 self:checkForHatchables(true)
510 self:updatePlayerListEntry()
511end
512
513local shopProducts = {
514 [Assets.productId.MasterBall] = {id = 'masterball', icon = 87619102}
515}
516function PlayerData:onDevProductPurchased(id) -- todo: make processreceipt return a response based on this function's response
517 if not id then return end
518 local attemptAutosave = false
519 -- Starter Product
520 if id == Assets.productId.Starter then
521 local s = self.starterProductStack
522 if #s > 0 then
523 table.remove(s, #s)()
524 end
525 -- Ash-Greninja
526 elseif id == Assets.productId.AshGreninja then
527 local s = self.ashGreninjaProductStack
528 if #s > 0 then
529 table.remove(s, #s)()
530 end
531 -- Hoverboard
532 elseif id == Assets.productId.Hoverboard then
533 local s = self.hoverboardProductStack
534 if #s > 0 then
535 table.remove(s, #s)()
536 end
537 -- BP Products
538 elseif id == Assets.productId.TenBP then
539 self:addBP(10, true, true)
540 elseif id == Assets.productId.FiftyBP then
541 self:addBP(50, true, true)
542 -- UMV Batter Products
543 elseif id == Assets.productId.UMV1 then
544 self:addBagItems({id = 'umvbattery', quantity = 1})
545 elseif id == Assets.productId.UMV3 then
546 self:addBagItems({id = 'umvbattery', quantity = 3})
547 elseif id == Assets.productId.UMV6 then
548 self:addBagItems({id = 'umvbattery', quantity = 6})
549 -- Money Products
550 elseif id == Assets.productId._10kP then
551 self:addMoney(10000, true)
552 elseif id == Assets.productId._50kP then
553 self:addMoney(50000, true)
554 elseif id == Assets.productId._100kP then
555 self:addMoney(100000, true)
556 elseif id == Assets.productId._200kP then
557 self:addMoney(200000, true)
558 -- Stamp Spinner Products
559 elseif id == Assets.productId.PBSpins1 then
560 self.stampSpins = math.min(999, self.stampSpins + 1)
561 _f.Network:post('uPBSpins', self.player, self.stampSpins)
562 attemptAutosave = true
563 elseif id == Assets.productId.PBSpins5 then
564 self.stampSpins = math.min(999, self.stampSpins + 5)
565 _f.Network:post('uPBSpins', self.player, self.stampSpins)
566 attemptAutosave = true
567 elseif id == Assets.productId.PBSpins10 then
568 self.stampSpins = math.min(999, self.stampSpins + 10)
569 _f.Network:post('uPBSpins', self.player, self.stampSpins)
570 attemptAutosave = true
571 else
572 -- Shop Products
573 local shopItem = shopProducts[id]
574 if shopItem then
575 local item = _f.Database.ItemById[shopItem.id]
576 self:addBagItems({num = item.num, quantity = shopItem.qty or 1})
577 _f.Network:post('ItemProductPurchased', self.player, item.name, shopItem.icon)
578 else
579 -- RO-Power Products
580 for g, list in pairs(Assets.productId.RoPowers) do
581 for l, pId in pairs(list) do
582 if pId == id then
583-- print('RO POWER PURCHASED')
584 _f.Network:post('rpActivate', self.player, g, l, RO_POWER_EFFECT_DURATION)
585 self:ROPowers_setTimePurchasedAndLevelForPower(g, os.time(), l)
586-- -- auto-save just the ro-power data
587 self:ROPowers_save()
588-- local s = pcall(function()
589-- local buffer = BitBuffer.Create()
590-- for i = 1, 7 do
591-- buffer:WriteBool(self.roPowers.powerLevel[i] == 2)
592-- buffer:WriteFloat64(self.roPowers.lastPurchasedAt[i])
593-- end
594-- _f.DataPersistence.ROPowerSave(self.player, 'save', buffer:ToBase64())
595-- end)
596-- if not s then warn('RO-Power autosave failed') end
597 --
598 break
599 end
600 end
601 end
602 end
603 end
604 if attemptAutosave then
605 -- attempt an autosave of the received stamp & used spin
606 spawn(function()
607 if self.lastSaveEtc then
608 self:saveGame(self.lastSaveEtc)
609 end
610 end)
611 end
612end
613
614function PlayerData:onAssetPurchased(id) -- keep in mind this will be called at least once every session after the pass is purchased (protect it from multi-awarding)
615 if id == Assets.passId.ExpShare then
616 if not self:getBagDataById('expshare', 5) then
617 self:addBagItems({id = 'expshare', quantity = 1})
618 _f.Network:post('PDChanged', self.player, 'expShareOn', true) -- when initially given, automatically turn it on
619 end
620 elseif id == Assets.passId.MoreBoxes then
621 if self.pc.maxBoxes == 8 then
622 self.pc.maxBoxes = 50
623 _f.Network:post('PCPassPurchased', self.player)
624 end
625 end
626end
627
628function PlayerData:completeEvent(eventName, ...)
629 if self.completedEvents[eventName] then return false end
630 local event = _f.PlayerEvents[eventName]
631 if not event then return false end
632 local r = event
633 local pseudo = false -- pseudo-events do not store to PlayerData
634 if type(event) == 'function' then
635 r = event(self, ...)
636 elseif type(event) == 'table' then
637 if event.manual then return false end
638 if event.pseudo then pseudo = true end
639 if event.callback then
640 r = event.callback(self, ...)
641 end
642 -- todo: continue to fill cases
643 end
644 if r ~= false and not pseudo then
645 self.completedEvents[eventName] = true
646 end
647 return r
648end
649
650function PlayerData:completeEventServer(eventName, ...)
651 if self.completedEvents[eventName] then return false end
652 local event = _f.PlayerEvents[eventName]
653 if event == nil then return false end
654 local r = event
655 if type(event) == 'function' then
656 r = event(self, ...)
657 elseif type(event) == 'table' then
658 -- todo: other cases where server is concerned with the data in the table
659 if type(event.pseudo) == 'function' and event.pseudo(self) then return false end
660 if event.callback then
661 r = event.callback(self, ...)
662 end
663 elseif r == false then
664 r = nil
665 end
666 if r ~= false then
667 self.completedEvents[eventName] = true
668 _f.Network:post('eventCompleted', self.player, eventName) -- notify client
669 end
670 return r
671end
672
673function PlayerData:giveStoryAbsol(slot)
674 local hadSeenAbsol = self:hasSeenPokemon( 359)
675 local hadOwnedAbsol = self:hasOwnedPokemon(359)
676 local absol = self:newPokemon {
677 name = 'Absol',
678 level = 50,
679 shinyChance = 4096,
680 item = 534,-- Absolite
681 moves = {{id = 'nightslash'},{id = 'psychocut'},{id = 'megahorn'},{id = 'detect'}}
682 }
683 local box, position
684 if slot then
685 box, position = self:PC_sendToStore(table.remove(self.party, slot), true)
686 end
687 table.insert(self.party, 1, absol)
688 self:onOwnPokemon(359)
689 self.absolMeta = {
690 slot = slot, box = box, position = position,
691 seen = hadSeenAbsol,
692 owned = hadOwnedAbsol
693 }
694end
695
696function PlayerData:undoGiveStoryAbsol()
697 self:incrementBagItem('megakeystone', -1)
698 self.flags.gotAbsol = nil
699 if self.party[1].name == 'Absol' then
700 table.remove(self.party, 1)
701 end
702 local meta = self.absolMeta
703 if not meta then return end
704 self.absolMeta = nil
705 local slot, box, position = meta.slot, meta.box, meta.position
706 if slot and box and position then
707 table.insert(self.party, slot, _f.ServerPokemon:deserialize(self.pc.boxes[box][position][3], self))
708 self.pc.boxes[box][position] = nil
709 end
710 if not meta.seen then self:unseePokemon(359) end
711 if not meta.owned then self:unownPokemon(359) end
712end
713
714function PlayerData:getStarterData()
715 local starters = {} do
716 for i, v in pairs({ -- starters are listed in 3 places: here, PlayerData:buyStarter (just below), and PlayerEvents.ChooseFirstPokemon
717 'Bulbasaur', 'Charmander', 'Squirtle',
718 'Chikorita', 'Cyndaquil', 'Totodile',
719 'Treecko', 'Torchic', 'Mudkip',
720 'Turtwig', 'Chimchar', 'Piplup',
721 'Snivy', 'Tepig', 'Oshawott',
722 'Chespin', 'Fennekin', 'Froakie',
723 'Rowlet', 'Litten', 'Popplio',
724 }) do
725 starters[i] = {v, _f.Database.GifData._FRONT[v]}
726 end
727 end
728 return starters
729end
730
731function PlayerData:buyStarter(species)
732 local valid = {
733 Bulbasaur = true, Charmander = true, Squirtle = true,
734 Chikorita = true, Cyndaquil = true, Totodile = true,
735 Treecko = true, Torchic = true, Mudkip = true,
736 Turtwig = true, Chimchar = true, Piplup = true,
737 Snivy = true, Tepig = true, Oshawott = true,
738 Chespin = true, Fennekin = true, Froakie = true,
739 Rowlet = true, Litten = true, Popplio = true,
740 }
741 if not species or not valid[species] then return false end
742 local sendToPC = false
743 local processed = false
744 local pokemon
745 table.insert(self.starterProductStack, function()
746 if processed then return end
747 processed = true
748 pokemon = self:newPokemon {
749 name = species,
750 level = 5,
751 shinyChance = 4096,
752 }
753 if sendToPC then
754 self:PC_sendToStore(pokemon)
755 return
756 end
757 -- defer storage until after nickname
758 end)
759 game:GetService('MarketplaceService'):PromptProductPurchase(self.player, Assets.productId.Starter)
760 for i = 1, 40 do
761 wait(.5)
762 if processed then break end
763 end
764 if not processed then
765 -- timed out
766 sendToPC = true
767 return 'to'
768 end
769 if pokemon then
770 return {
771 d = self:createDecision {
772 callback = function(_, nickname)
773 if type(nickname) == 'string' then
774 pokemon:giveNickname(nickname)
775 end
776 local box = self:caughtPokemon(pokemon)
777 if box then
778 return pokemon:getName() .. ' has been transferred to Box ' .. box .. '!'
779 end
780 end
781 },
782 i = pokemon:getIcon(),
783 s = pokemon.shiny
784 }
785 end
786 -- is there a condition that reaches here?
787end
788
789function PlayerData:buyAshGreninja()
790 if #self.party > 5 then return 'fp' end
791 local sendToPC = false
792 local processed = false
793 local pokemon
794 table.insert(self.ashGreninjaProductStack, function()
795 if processed then return end
796 processed = true
797 pokemon = self:newPokemon {
798 name = 'Greninja',
799 forme = 'bb',
800 level = 36,
801 shinyChance = 2048,
802 ot = 12301,
803 moves = {
804 {id = 'watershuriken'},{id = 'aerialace'},
805 {id = 'doubleteam'}, {id = 'nightslash'}
806 }
807 }
808 if sendToPC then
809 -- processed after timeout, store without nicknaming
810 self:PC_sendToStore(pokemon)
811 return
812 end
813 -- defer storage until after nickname
814 end)
815 game:GetService('MarketplaceService'):PromptProductPurchase(self.player, Assets.productId.AshGreninja)
816 for i = 1, 40 do
817 wait(.5)
818 if processed then break end
819 end
820 if not processed then
821 -- timed out
822 sendToPC = true
823 return 'to'
824 end
825 if pokemon then
826 return {
827 d = self:createDecision {
828 callback = function(_, nickname)
829 if type(nickname) == 'string' then
830 pokemon:giveNickname(nickname)
831 end
832 local box = self:caughtPokemon(pokemon)
833 if box then
834 return pokemon:getName() .. ' has been transferred to Box ' .. box .. '!'
835 end
836 end
837 },
838 i = pokemon:getIcon(),
839 s = pokemon.shiny
840 }
841 end
842end
843
844function PlayerData:completedEggCycle()
845 -- my fastest egg step completion was approx. 39.4 sec
846 -- THIS MAY NO LONGER BE THE CASE WHEN WE RELEASE HOVERBOARDS
847 -- reject & report anything faster than 30 seconds
848 local now = tick()
849 local duration = tick()-self.lastCompletedEggCycle
850 local maxStepTime = (self.currentHoverboard~='' and self.hoverboardModel) and (self.currentHoverboard:sub(1,6)=='Basic ' and 20 or 15) or 30
851 if duration < maxStepTime then--30 then
852 -- TODO
853 return
854 end
855 self.lastCompletedEggCycle = now
856
857 local party = self.party
858 self:Daycare_tryBreed()
859 local reduceBy = 1
860 for _, p in pairs(party) do
861 local a = p:getAbilityName()
862 if not p.egg and (a == 'Flame Body' or a == 'Magma Armor') then
863 reduceBy = 2
864 break
865 end
866 end
867 reduceBy = reduceBy * (1 + self:ROPowers_getPowerLevel(2))
868 for _, p in pairs(party) do
869 if p.egg then
870 if not p.fossilEgg then
871 p.eggCycles = p.eggCycles - reduceBy
872 end
873 else
874 p:addHappiness(2, 2, 1)
875 end
876 end
877 self:checkForHatchables()
878 -- add 256 Exp. to Pokemon in the Day Care
879 for _, p in pairs(self.daycare.depositedPokemon) do
880 p.experience = p.experience + 256
881 end
882end
883
884function PlayerData:rearrangeParty(indices)
885 if self:isInBattle() then return end
886 local nParty = #self.party
887 if #indices ~= nParty then return end
888 local ii = {}
889 local vv = {}
890 for i, v in pairs(indices) do
891 if type(i) ~= 'number' or i > nParty or type(v) ~= 'number' or v > nParty then return end
892 if ii[i] or vv[v] then return end -- clone attempt
893 ii[i] = true
894 vv[v] = true
895 end
896 for i = 1, nParty do if not ii[i] or not vv[i] then return end end
897 local party = {}
898 for i = 1, nParty do
899 party[i] = self.party[indices[i]]
900 end
901 self.party = party
902 local firstNonEgg = self:getFirstNonEgg()
903 _f.Network:post('PDChanged', self.player, 'firstNonEggLevel', firstNonEgg.level,
904 'firstNonEggAbility', firstNonEgg:getAbilityName())
905end
906
907function PlayerData:getBattleBag()
908 if not self:isInBattle() then return end
909 local bags = {{},{},{}}
910 for n = 1, 4 do
911 for _, bd in pairs(self.bag[n]) do
912 local item = _f.Database.ItemByNumber[bd.num]
913 if item and item.battleCategory then
914 table.insert(bags[item.battleCategory], {
915 id = item.id,
916 name = item.name,
917 icon = item.icon or item.num,
918 qty = bd.quantity,
919 desc = item.desc,
920 bUse = item.isPokeball or type(item.onUse) == 'function',
921 bCat = item.battleCategory
922 })
923 end
924 end
925 end
926 return bags
927end
928
929function PlayerData:getBagDataForTransfer(item, bd, context) -- helper function
930 local itemId = item.id
931 local canUse
932 local usableItemClient = UsableItemsClient[itemId]
933 if not usableItemClient or not usableItemClient.canUse then
934 local usableItemServer = _f.UsableItems[itemId]
935 if usableItemServer then
936 local s_canUse = usableItemServer.canUse
937 if s_canUse then
938 if type(s_canUse) == 'function' then
939 canUse = {}
940 for i, p in pairs(self.party) do
941 canUse[tostring(i)] = s_canUse(p) -- stupid table limitations...
942 end
943 else
944 canUse = s_canUse
945 end
946 end
947 end
948 end
949 return {
950 id = itemId,
951 name = item.name,
952 icon = item.icon or item.num,
953 qty = (item.bagCategory~=5 or item.showsQuantity) and bd.quantity or nil,
954 desc = item.desc,
955 canUse = canUse, -- true or false or a table of true/false (1 for each pokemon in party)
956 -- ^ exists when UsableItemsServer has a canUse function but UsableItemsClient doesn't
957
958 sell = (context=='sell' and item.sellPrice or nil),
959 }
960end
961
962function PlayerData:getBagPouch(n, context)
963 local pouch = {}
964 local count = 0
965 for _, bd in pairs(self.bag[n]) do
966 local item = _f.Database.ItemByNumber[bd.num]
967 count = count + 1
968 pouch[count] = self:getBagDataForTransfer(item, bd, context)
969 end
970 return pouch
971end
972
973function PlayerData:getTMs()
974 local list = {}
975
976 local partyKnownMoves = {}
977 local partyLearnedMachines = {}
978 for i, p in pairs(self.party) do
979 local k = {}
980 local l = {}
981 if not p.egg then
982 for _, move in pairs(p:getMoves()) do
983 k[move.num] = true
984 end
985 pcall(function()
986 for _, num in pairs(p:getLearnedMoves().machine) do
987 l[num] = true
988 end
989 end)
990 end
991 partyKnownMoves[i] = k
992 partyLearnedMachines[i] = l
993 end
994
995 local buffer = BitBuffer.Create()
996 local function add(str, isHMs)
997 buffer:FromBase64(str)
998 local data = _f.Database.Machines[isHMs and 'hms' or 'tms']
999 for m = 1, str:len()*6 do
1000 if buffer:ReadBool() then
1001 local moveId = data[m]
1002 local move = _f.Database.MoveById[moveId]
1003 local moveNum = move.num
1004 local canLearn = {}
1005 for i, p in pairs(self.party) do
1006 canLearn[i] = (partyKnownMoves[i][moveNum] and 2) or (partyLearnedMachines[i][moveNum] and 1) or 0
1007 end
1008 list[#list+1] = {
1009 mName = move.name,
1010 num = m,
1011 hm = isHMs,
1012 type = move.type,
1013 desc = move.category..', '..move.type..'-type, '..(move.basePower or 0)..' Power,\n'..(move.accuracy==true and '--' or ((move.accuracy or 0)..'%'))..' Accuracy'..((move.desc and move.desc~='') and ('. Effect: '..move.desc) or ''),
1014 learn = canLearn
1015 }
1016 end
1017 end
1018 end
1019 add(self.tms)
1020 add(self.hms, true)
1021
1022 return list
1023end
1024
1025function PlayerData:teachTM(pokemonIndex, tmNum, isHM)
1026 -- verify arguments
1027 local moveId; pcall(function() moveId = _f.Database.Machines[isHM and 'hms' or 'tms'][tmNum] end)
1028 local pokemon; pcall(function() pokemon = self.party[pokemonIndex] end)
1029 if not moveId or not pokemon or pokemon.egg then return false end
1030 -- verify player owns TM/HM
1031 if not BitBuffer.GetBit(isHM and self.hms or self.tms, tmNum) then return false end
1032 -- verify pokemon can learn TM/HM
1033 local canLearn = false
1034 pcall(function()
1035 local moveNum = _f.Database.MoveById[moveId].num
1036 for _, num in pairs(pokemon:getLearnedMoves().machine) do
1037 if num == moveNum then
1038 canLearn = true
1039 break
1040 end
1041 end
1042 end)
1043 if not canLearn then return false end
1044 -- verify pokemon doesn't already know the move
1045 for _, move in pairs(pokemon.moves) do
1046 if move.id == moveId then
1047 return false
1048 end
1049 end
1050 -- learn immediately if there is space
1051 if #pokemon.moves < 4 then
1052 pokemon.moves[#pokemon.moves+1] = {id = moveId}
1053 return true
1054 end
1055 -- gather data about known moves and the move to learn
1056 local moves = {}
1057 local function add(move)
1058 moves[#moves+1] = {
1059 name = move.name,
1060 category = move.category,
1061 type = move.type,
1062 power = move.basePower,
1063 accuracy = move.accuracy,
1064 pp = move.pp,
1065 desc = move.desc
1066 }
1067 end
1068 for _, move in pairs(pokemon.moves) do
1069 if move.id == moveId then return false end -- make sure move is not already known
1070 add(_f.Database.MoveById[move.id])
1071 end
1072 add(_f.Database.MoveById[moveId])
1073 -- send data & new decision id to player
1074 return moves, self:createDecision {
1075 callback = function(_, moveSlot)
1076 if type(moveSlot) ~= 'number' or moveSlot < 1 or moveSlot > 4 then return end
1077 pokemon.moves[math.floor(moveSlot)] = {id = moveId}
1078 end
1079 }
1080end
1081
1082function PlayerData:useItem(itemId, targetIndex)
1083 if not itemId or type(itemId) ~= 'string' then return false end
1084 local usableItemServer = _f.UsableItems[itemId]
1085 local usableItemClient = UsableItemsClient[itemId]
1086 -- .noTarget and .nonConsumable are preferred to be placed on the client's usableItem (or else the client will be confused
1087 local hasTarget = not ((usableItemServer and usableItemServer.noTarget) or (usableItemClient and usableItemClient.noTarget))
1088 local consume = not ((usableItemServer and usableItemServer.nonConsumable) or (usableItemClient and usableItemClient.nonConsumable))
1089 if (targetIndex ~= nil) ~= (hasTarget and true or false) then return false end
1090 local target
1091 if hasTarget then
1092 target = self.party[targetIndex]
1093 if not target then return false end
1094 end
1095 local item = _f.Database.ItemById[itemId]
1096 if not item then return false end
1097 local bd = self:getBagDataByNum(item.num)
1098 if not bd or not bd.quantity or bd.quantity < 1 then return false end
1099 local used
1100 if usableItemServer and usableItemServer.onUse then
1101 used = usableItemServer.onUse(target)
1102 if used == false then return false end
1103 end
1104 if consume then
1105
1106 local _, bd = self:incrementBagItem(item.num, -1) -- qty verified above
1107 if itemId:match('repel$') then -- repels report whether there are any remaining
1108 return (bd and bd.quantity and bd.quantity > 0) and 1 or 0
1109 end
1110 end
1111 return used, (target and target:getPartyData({}))
1112end
1113
1114function PlayerData:giveItem(itemId, pokemonIndex)
1115 if not itemId or type(itemId) ~= 'string' or not pokemonIndex or type(pokemonIndex) ~= 'number' then return false end
1116 local item = _f.Database.ItemById[itemId]
1117 local pokemon = self.party[pokemonIndex]
1118 if not item or not pokemon or pokemon.egg then return false end
1119 if not item.bagCategory or item.bagCategory > 4 then return false end -- check whether it can even be held
1120 if not self:incrementBagItem(item.num, -1) then return false end
1121 local taking = pokemon:getHeldItem()
1122 local takenBD
1123 if taking.num then
1124 local s, r = self:incrementBagItem(taking.num, 1)
1125 if s then takenBD = r end
1126 end
1127 pokemon.item = item.num
1128 return true, (takenBD and self:getBagDataForTransfer(taking, takenBD)), (takenBD and taking.bagCategory)
1129end
1130
1131function PlayerData:takeItem(pokemonIndex)
1132 if not pokemonIndex or type(pokemonIndex) ~= 'number' then return false end
1133 local pokemon = self.party[pokemonIndex]
1134 if not pokemon or pokemon.egg then return false end
1135 local item = pokemon:getHeldItem()
1136 if not item.num then return false end
1137 local s, bd = self:incrementBagItem(item.num, 1)
1138 if not s then return false end
1139 pokemon.item = nil
1140 return true, self:getBagDataForTransfer(item, bd), item.bagCategory
1141end
1142
1143function PlayerData:tossItem(itemId, amount)
1144 if not itemId or type(itemId) ~= 'string' or not amount or type(amount) ~= 'number' or amount < 1 then return false end
1145 local item = _f.Database.ItemById[itemId]
1146 if not item or not item.bagCategory or item.bagCategory > 4 or itemId == 'masterball' then return false end -- check whether it can be tossed
1147 if not self:incrementBagItem(item.num, -amount) then return false end
1148 return true
1149end
1150
1151function PlayerData:deleteMove(pokemonIndex)
1152 if not pokemonIndex or not self.party[pokemonIndex] then return end
1153 local pokemon = self.party[pokemonIndex]
1154 if pokemon.egg then return 0, 'eg' end
1155 if #pokemon.moves == 0 then return 0, '0m' end
1156 if #pokemon.moves == 1 then return pokemon.name, '1m' end
1157 return pokemon.name, {
1158 moves = pokemon:getCurrentMovesData(),
1159 d = self:createDecision {
1160 callback = function(_, moveslot)
1161 if not moveslot or not pokemon.moves[moveslot] then return end
1162 table.remove(pokemon.moves, moveslot)
1163 end
1164 }
1165 }
1166end
1167
1168function PlayerData:remindMove()
1169 local heartscale = _f.Database.ItemById.heartscale
1170 local nHeartScales = 0
1171 pcall(function() nHeartScales = self:getBagDataByNum(heartscale.num, 1).quantity end)
1172 return {
1173 hsi = heartscale.icon or heartscale.num,
1174 nhs = nHeartScales,
1175 money = self.money,
1176 d = self:createDecision {
1177 callback = function(_, pokemonIndex)
1178 if not pokemonIndex or not self.party[pokemonIndex] then return end
1179 local pokemon = self.party[pokemonIndex]
1180 if pokemon.egg then return 0, 'eg' end
1181
1182 local learnedMoves
1183 pcall(function() learnedMoves = pokemon:getLearnedMoves().levelUp end)
1184 local moves = {}
1185 if learnedMoves then
1186 -- get moves by level (earliest learned to latest learned)
1187 local level = pokemon.level
1188 for _, d in pairs(learnedMoves) do
1189 if level < d[1] then break end
1190 for i = 2, #d do
1191 table.insert(moves, d[i])
1192 end
1193 end
1194 -- remove duplicate moves
1195 for i, move in pairs(moves) do
1196 for j = #moves, i+1, -1 do
1197 if move == moves[j] then
1198 table.remove(moves, j)
1199 end
1200 end
1201 end
1202 -- remove currently known moves
1203 for _, move in pairs(pokemon:getMoves()) do
1204 for j = #moves, 1, -1 do
1205 if move.num == moves[j] then
1206 table.remove(moves, j)
1207 break
1208 end
1209 end
1210 end
1211 end
1212 if #moves == 0 then return pokemon.name, 'nm' end
1213 local validMovesNumToId = {}
1214 for i, moveNum in pairs(moves) do
1215 local move = _f.Database.MoveByNumber[moveNum]
1216 moves[i] = {
1217 num = move.num,
1218 name = move.name,
1219 category = move.category,
1220 type = move.type,
1221 power = move.basePower,
1222 accuracy = move.accuracy,
1223 pp = move.pp,
1224 desc = move.desc
1225 }
1226 validMovesNumToId[moveNum] = move.id
1227 end
1228
1229 return pokemon.name, {
1230 nn = pokemon:getName(),
1231 known = pokemon:getCurrentMovesData(),
1232 moves = moves,
1233 d = self:createDecision {
1234 callback = function(_, paymentMethod, moveNum, moveSlot)
1235 if (paymentMethod ~= 1 and paymentMethod ~= 2)
1236 or (moveSlot ~= 1 and moveSlot ~= 2 and moveSlot ~= 3 and moveSlot ~= 4) then
1237 return
1238 end
1239 local moveId = validMovesNumToId[moveNum]
1240 if not moveId then return end
1241 if paymentMethod == 1 then
1242 if not (self:incrementBagItem(heartscale.num, -1)) then return end
1243 else
1244 if not (self:addMoney(-30000)) then return end
1245 end
1246 pokemon.moves[moveSlot] = {id = moveId}
1247 end
1248 }
1249 }
1250 end
1251 }
1252 }
1253end
1254
1255local getShop = require(script.GetShop)
1256function PlayerData:getShop(shopId)
1257 local items, other = getShop(self, shopId)
1258 if not items then return false end
1259 self.currentShop = items
1260 return items, other
1261end
1262
1263function PlayerData:maxBuyInternal(itemId)
1264 if not self.currentShop then return false end
1265 pcall(function() itemId = Utilities.rc4(itemId) end)
1266 if type(itemId) ~= 'string' then return false end
1267 local item = _f.Database.ItemById[itemId]
1268 if not item then return false end
1269 local price
1270 for _, l in pairs(self.currentShop) do
1271 if Utilities.rc4(l[1]) == itemId then
1272 price = l[2]
1273 break
1274 end
1275 end
1276 if not price then return false end
1277 local currentQty = 0
1278 local bd = self:getBagDataByNum(item.num)
1279 if bd then
1280 currentQty = bd.quantity or 0
1281 end
1282 if currentQty >= 99 then return 'fb' end -- full bag
1283 if self.money < price then return 'nm' end -- not enough money
1284 return math.min(99-currentQty, math.floor(self.money/price)), item, price
1285end
1286function PlayerData:maxBuy(itemId) -- rc4'd (from client)
1287 return (self:maxBuyInternal(itemId)) -- return single value to client
1288end
1289
1290function PlayerData:buyItem(itemId, qty) -- rc4'd
1291 local max, item, price = self:maxBuyInternal(itemId)
1292 if type(max) ~= 'number' or not item or not price or qty > max or qty < 1 then return false end
1293 qty = math.floor(qty)
1294 if not self:addMoney(-price*qty) then return false end
1295 self:addBagItems{num = item.num, quantity = qty}
1296 local givePremierBall = false
1297 if item.isPokeball and qty > 9 then
1298 self:addBagItems{id = 'premierball', quantity = 1}
1299 givePremierBall = true
1300 end
1301 return true, givePremierBall
1302end
1303
1304function PlayerData:bMaxBuyInternal(shopIndex)
1305 if not self.currentShop then return false end
1306 local itemIdPricePair = self.currentShop[shopIndex]
1307 if type(itemIdPricePair) ~= 'table' then return false end
1308 local itemId = itemIdPricePair[1]
1309 if type(itemId) ~= 'string' then return false end
1310 local price = itemIdPricePair[2]
1311 if type(price) ~= 'number' then return false end
1312 if itemId:sub(1, 2) == 'BP' then return false end -- assumption: no items sold here later will start with "BP"
1313 local tmNum = itemId:match('^TM(%d+)')
1314 if tmNum then
1315 tmNum = tonumber(tmNum)
1316 if BitBuffer.GetBit(self.tms, tmNum) then return 'ao' end -- already own
1317 if self.bp < price then return 'nm' end
1318 return 'tm', tonumber(tmNum), price
1319 end
1320 local item = _f.Database.ItemById[itemId]
1321 if not item then return false end
1322 local currentQty = 0
1323 local bd = self:getBagDataByNum(item.num)
1324 if bd then
1325 currentQty = bd.quantity or 0
1326 end
1327 if currentQty >= 99 then return 'fb' end -- full bag
1328 if self.bp < price then return 'nm' end -- not enough money
1329 return math.min(99-currentQty, math.floor(self.bp/price)), item, price
1330end
1331function PlayerData:bMaxBuy(shopIndex)
1332 return (self:bMaxBuyInternal(shopIndex))
1333end
1334
1335function PlayerData:buyWithBP(shopIndex, qty)
1336 local max, item, price = self:bMaxBuyInternal(shopIndex)
1337 if max == 'tm' then
1338 self:obtainTM(item)
1339 self.bp = self.bp - price
1340 return true, self.bp
1341 end
1342 if not item or type(max) ~= 'number' or type(qty) ~= 'number' or max < qty or qty < 1 then return false end
1343 qty = math.floor(qty)
1344 self.bp = self.bp - price*qty
1345 self:addBagItems{num = item.num, quantity = qty}
1346 return true, self.bp
1347end
1348
1349function PlayerData:sellItem(itemId, qty) -- NOT rc4'd
1350 if type(itemId) ~= 'string' or type(qty) ~= 'number' or qty < 1 then return false end
1351 local item = _f.Database.ItemById[itemId]
1352 if not item or not item.sellPrice then return false end
1353 local bd = self:getBagDataByNum(item.num)
1354 qty = math.floor(qty)
1355 if not bd or not bd.quantity or bd.quantity < 1 or qty > bd.quantity then return false end
1356 if not self:addMoney(qty*item.sellPrice) then return 'fw' end
1357 self:incrementBagItem(item.num, -qty)
1358 return self.money
1359end
1360
1361function PlayerData:obtainItem(id)
1362 if not self.currentObtainableItems then return end
1363 local item = self.currentObtainableItems[id]
1364 if not item then return end
1365 self.currentObtainableItems[id] = nil -- no repeat obtains
1366 if type(item) == 'number' then
1367 -- TM
1368 self:obtainTM(item)
1369 return 'TM'..(item<10 and '0' or '')..item
1370 elseif type(item) == 'table' then
1371 -- item
1372 local oin = item[2]
1373 item = _f.Database.ItemById[item[1] ]
1374 if not item then return end
1375 self.obtainedItems = BitBuffer.SetBit(self.obtainedItems, oin, true)
1376 self:addBagItems({num = item.num, quantity = 1})
1377 return item.name
1378 end
1379end
1380
1381function PlayerData:makeDecision(id, ...)
1382 if not id or type(id) ~= 'number' then return false end
1383 local data = self.decision_data[id]
1384 if not data then return false end
1385 local ret = {data.callback(data, ...)}
1386 if ret[1] == false then return false end
1387 self.decision_data[id] = false
1388 return unpack(ret)
1389end
1390
1391function PlayerData:openPC()
1392 if self.pcSession then
1393 self.pcSession:close()
1394 end
1395 if self:isInBattle() then return end
1396 local newSession = _f.PCService:new(self)
1397 self.pcSession = newSession
1398 return newSession:getStartPacket()
1399end
1400
1401function PlayerData:cPC(fn, ...)
1402 if type(fn) ~= 'string' then return end
1403 local pc = self.pcSession
1404 if not pc or not pc.public[fn] then return end
1405 return pc[fn](pc, ...)
1406end
1407
1408function PlayerData:closePC(id, ch)
1409 local pc = self.pcSession
1410 if not pc then return end
1411 if id and pc.id ~= id then return end
1412 local ret = pc:close(ch)
1413 self.pcSession = nil
1414 return ret
1415end
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428function PlayerData:createDecision(data)
1429 assert(data.callback ~= nil, 'decision must include a callback')
1430 local id = self.decision_count + 1
1431 self.decision_count = id
1432 self.decision_data[id] = data
1433 return id
1434end
1435
1436function PlayerData:checkForHatchables(forceClear)
1437 -- make sure that there isn't a queued hatch waiting
1438 for i, d in pairs(self.decision_data) do -- note that d can be `false`
1439 if d and d.hatch then
1440 if forceClear then
1441 self.decision_data[i] = false
1442 else
1443 return
1444 end
1445 end
1446 end
1447 -- check for hatchable egg in party
1448 for _, p in pairs(self.party) do
1449 if p.egg and not p.fossilEgg and p.eggCycles <= 0 then
1450 local id = self:createDecision {
1451 hatch = true,
1452 callback = function(data, nickname)
1453 -- hatch pokemon
1454 self:onOwnPokemon(p.num)
1455 p.egg = nil
1456 p.ot = self.userId
1457 if nickname and type(nickname) == 'string' then
1458 p:giveNickname(nickname)
1459 end
1460 -- check for another hatchable
1461 self:checkForHatchables(true)
1462 end
1463 }
1464 -- send event to player
1465 _f.Network:post('hatch', self.player, {
1466 d_id = id,
1467 eggIcon = p:getIcon(),
1468 pSprite = p:getSprite(true),
1469 pName = p.data.baseSpecies or p.data.species,
1470 pIcon = p:getIcon(true),
1471 pShiny = p.shiny and true or false
1472 })
1473 -- only allow one at a time
1474 return
1475 end
1476 end
1477end
1478
1479function PlayerData:resetFishStreak()
1480 self.fishingStreak = 0
1481end
1482
1483function PlayerData:getRegion()
1484 -- not perfect, just gives a best guess (can only be depended on when player is assumed to be outdoors)
1485 if not self.currentChunk then return end
1486 local chunkData = _f.Database.ChunkData[self.currentChunk]
1487 if chunkData then
1488 local onlyRegion
1489 for name in pairs(chunkData.regions) do
1490 if not onlyRegion then
1491 onlyRegion = name
1492 else
1493 onlyRegion = nil
1494 break
1495 end
1496 end
1497 if onlyRegion then return onlyRegion end
1498 end
1499 local map = storage.MapChunks:FindFirstChild(self.currentChunk)
1500 if not map then return end
1501 local regions = map:FindFirstChild('Regions')
1502 if not regions then return end
1503 local pos; pcall(function() pos = self.player.Character.HumanoidRootPart.Position end)
1504 if not pos then return end
1505 for _, part in pairs(regions:GetChildren()) do
1506 if part:IsA('BasePart') then
1507 if Region.FromPart(part):CastPoint(pos) then
1508 return part.Name
1509 end
1510 end
1511 end
1512end
1513
1514
1515function PlayerData:addMoney(amount)
1516 if amount < 0 and self.money+amount < 0 then return false end
1517 if amount > 0 and self.money > MAX_MONEY then return false end
1518 self.money = math.min(self.money + amount, MAX_MONEY)
1519 _f.Network:post('PDChanged', self.player, 'money', self.money)
1520 return true
1521end
1522
1523function PlayerData:addBP(amount, showGui)
1524 self.bp = math.min(self.bp + amount, MAX_BP)
1525 if showGui then
1526 _f.Network:post('bpAwarded', self.player, amount, self.bp)
1527 end
1528end
1529
1530function PlayerData:ownsGamePass(passId, mustReturnInstantly)
1531 if self.userId < 1 then return false end
1532 if type(passId) == 'string' then
1533 passId = Assets.passId[passId]
1534 end
1535 if self.ownedGamePassCache[passId] then return true end
1536 if mustReturnInstantly then -- the old PD model checked once when the player entered whether the game pass was owned, so this behavior is acceptable (it's an improvement)
1537 spawn(function() self:ownsGamePass(passId) end) -- attempt to cache
1538 return false -- return false for now
1539 end
1540 local marketplaceService = game:GetService('MarketplaceService')
1541 local s, r = pcall(function() return marketplaceService:PlayerOwnsAsset(self.player, passId) end)
1542 if s and r then
1543 self.ownedGamePassCache[passId] = true
1544 return true
1545 end
1546 return false
1547end
1548
1549function PlayerData:updatePlayerListEntry(awardDexBadges)
1550 -- the PlayerList displays Name, badge icon, and Pokedex (or Rank in PVP)
1551 -- Name never changes; only badges and Pokedex[/Rank]
1552 local badgeId, ownedPokemon = self:getPlayerListInfo()
1553 local player = self.player
1554 local changed = false
1555 if not player:FindFirstChild('BadgeId') then
1556 Instance.new('IntValue', player).Name = 'BadgeId'
1557 changed = true
1558 end
1559 if not player:FindFirstChild('OwnedPokemon') then
1560 Instance.new('IntValue', player).Name = 'OwnedPokemon'
1561 changed = true
1562 end
1563 changed = changed or (badgeId ~= player.BadgeId.Value) or (ownedPokemon ~= player.OwnedPokemon.Value)
1564 if not changed then return end
1565 player.BadgeId.Value = badgeId
1566 player.OwnedPokemon.Value = ownedPokemon
1567 network:postAll('UpdatePlayerlist', player.Name, badgeId, ownedPokemon)
1568 if _f.Context ~= 'battle' and awardDexBadges then
1569 for _, badgeData in pairs(Assets.badgeId.DexCompletion) do
1570 local reqOwnedPokemon, badgeId = unpack(badgeData)
1571 if ownedPokemon >= reqOwnedPokemon then
1572 pcall(function() game:GetService('BadgeService'):AwardBadge(self.userId, badgeId) end)
1573 else
1574 break
1575 end
1576 end
1577 end
1578 return player.Name, badgeId, ownedPokemon
1579end
1580
1581local BattleEloManager
1582function PlayerData:getPlayerListInfo()
1583 -- some players get special badge ids (why did I make this a thing...)
1584 local badgesByPlayerId = {
1585 [1084073] = 313110711, -- brad
1586 [1123551] = 313609630, -- lando
1587 [14838908] = 318864251, -- zommi
1588 [21632574] = 327766366, -- sixth
1589 [1281876] = 317091893, -- kres
1590 [71050628] = 685197115, -- pully
1591 [81637134] = 314917806, -- mei
1592 [747409] = 320173802, -- briest
1593 [5665260] = 324821165, -- undef
1594 [3651386] = 418935648, -- [ani] dave
1595 [28276317] = 383445099, -- dvd
1596 [2108078] = 527387805, -- reactron
1597 [13234608] = 335915729, -- haces
1598 [7568292] = 338919143, -- TT
1599 [23801047] = 343548985, -- ally
1600 [22121682] = 347056729, -- spec7
1601 [2839425] = 359868894, -- armyzack/zoism
1602 [40874521] = 421234847, -- naky
1603 [93618279] = 380897951, -- pyro
1604 [30575130] = 383445031, -- ran
1605 [92720144] = 391310637, -- xychiz
1606 [4189809] = 527386850, -- Our_Hero
1607
1608 [5730064] = 536016603, -- Kman
1609
1610 [19612377] = 435168760, -- kevincatssing
1611 [2168003] = 435169561, -- Shipooi
1612 [2117045] = 435169019, -- Roball1
1613 [27791223] = 435169423, -- RoseNight50
1614 [36422716] = 435168848, -- OldSchooldDude2
1615
1616 [13094490] = 467363815, -- Lilly_S
1617 [38979592] = 498610186, -- jc_cj
1618 [64461809] = 600684944, -- CoralSoul
1619
1620 [74783517] = 527383768, -- marolex
1621 }
1622 local badgeId = badgesByPlayerId[self.userId]
1623 if not badgeId then
1624 local latestBadge = 0
1625 for i, b in pairs(self.badges) do
1626 if b then
1627 latestBadge = math.max(latestBadge, i)
1628 end
1629 end
1630 badgeId = Assets.badgeImageId[latestBadge] or 0
1631 end
1632 local ownedPokemon
1633 -- if PVP, override pokedex with rank
1634 if _f.Context == 'battle' then
1635 if not BattleEloManager then
1636 BattleEloManager = require(script.Parent.BattleEngine.BattleEloManager)
1637 end
1638 ownedPokemon = BattleEloManager:getPlayerRank(self.player.UserId)
1639 else
1640 ownedPokemon = select(2, self:countSeenAndOwnedPokemon())
1641 end
1642 return badgeId, ownedPokemon
1643end
1644
1645local function concatenate(s, ...)
1646 -- this is weird, yes, but there was actually a period of time
1647 -- where the concatenation operation seemed to randomly return
1648 -- a partial version of what it should
1649 local function concatenateInner(a, b)
1650 local totalLen = a:len() + b:len()
1651 local c = a .. b
1652 local attempts = 0
1653 while c:len() ~= totalLen do
1654 attempts = attempts + 1
1655 if attempts > 5 then
1656 error('failed concatenation: failed too many times')
1657 end
1658 warn('failed concatenation: retrying')
1659 c = a .. b
1660 end
1661 return c
1662 end
1663 for _, o in pairs({...}) do
1664 s = concatenateInner(s, o)
1665 end
1666 return s
1667end
1668
1669
1670-- RO Powers
1671function PlayerData:purchaseRoPower(group, level)
1672 if self:ROPowers_getPowerLevel(group) > 0 then return end
1673 game:GetService('MarketplaceService'):PromptProductPurchase(self.player, Assets.productId.RoPowers[group][level])
1674end
1675
1676function PlayerData:ROPowers_getPowerLevel(g)
1677 local ro = self.roPowers
1678 local l = ro.powerLevel[g]
1679 if l > 0 then
1680 if os.time()-ro.lastPurchasedAt[g] > RO_POWER_EFFECT_DURATION then
1681 ro.powerLevel[g] = 0
1682 return 0
1683 end
1684 end
1685 return l
1686end
1687
1688function PlayerData:ROPowers_getTimePurchased(g)
1689 return self.roPowers.lastPurchasedAt[g]
1690end
1691
1692function PlayerData:ROPowers_setTimePurchasedAndLevelForPower(g, t, l)
1693 self.roPowers.lastPurchasedAt[g] = t
1694 self.roPowers.powerLevel[g] = l
1695end
1696
1697function PlayerData:ROPowers_save()
1698 local now = os.time()
1699 local buffer = BitBuffer.Create()
1700 local version = 0
1701 buffer:WriteUnsigned(6, version)
1702 for i = 1, 7 do
1703 local p = self:ROPowers_getPowerLevel(i)
1704 if p == 0 then
1705 buffer:WriteUnsigned(13, 0)
1706 else
1707 buffer:WriteBool(p == 2)
1708 local s = RO_POWER_EFFECT_DURATION - math.ceil(now - self.roPowers.lastPurchasedAt[i])
1709 buffer:WriteUnsigned(12, math.max(0, s))
1710 end
1711 end
1712 _f.DataPersistence.ROPowerSave(self.player, 'save', buffer:ToBase64())
1713end
1714
1715function PlayerData:ROPowers_restore()
1716 local data = _f.DataPersistence.ROPowerSave(self.player, 'load')
1717 local ro = self.roPowers
1718 if data then
1719 --[[
1720 OLD:
1721 ([3?,] 6, or 7) * 65
1722 potentially:
1723 [195?] -> 33 chars
1724 390 -> 65
1725 455 -> 76
1726 NEW:
1727 6 - version
1728 7 * 13 - seconds remaining (3600 max)
1729 total:
1730 97 -> 17 chars
1731 ]]
1732 local buffer = BitBuffer.Create()
1733 buffer:FromBase64(data)
1734 if data:len() > 20 then
1735 -- Assume OLD
1736 for i = 1, 7 do
1737 pcall(function()
1738 local isLv2 = buffer:ReadBool()
1739 local pTime = buffer:ReadFloat64()
1740 if pTime > ro.lastPurchasedAt[i] then
1741 ro.lastPurchasedAt[i] = pTime
1742 ro.powerLevel[i] = isLv2 and 2 or 1
1743 end
1744 end)
1745 end
1746 else
1747 -- NEW
1748 local now = os.time()
1749 local version = buffer:ReadUnsigned(6)
1750 for i = 1, 7 do
1751 local isLv2 = buffer:ReadBool()
1752 local s = buffer:ReadUnsigned(12) - 20 -- WE DEDUCT 20 SECONDS for the shiny soft-resetters
1753 if s > 0 then
1754 ro.lastPurchasedAt[i] = now - RO_POWER_EFFECT_DURATION + s
1755 ro.powerLevel[i] = isLv2 and 2 or 1
1756 end
1757 end
1758 end
1759 end
1760end
1761
1762function PlayerData:roStatus()
1763 local now = os.time()
1764 local r = {}
1765 for i = 1, 7 do
1766 local p = self:ROPowers_getPowerLevel(i)
1767 if p > 0 then
1768 r[tostring(i)] = {p, self.roPowers.lastPurchasedAt[i] + RO_POWER_EFFECT_DURATION - now}
1769 end
1770 end
1771 local icons = {}
1772 for eventName, pokemonList in pairs(RoamingPokemon) do
1773 if self.completedEvents[eventName] then
1774 for _, enc in pairs(pokemonList) do
1775-- print(enc[1])
1776 icons[#icons+1] = _f.Database.PokemonById[Utilities.toId(enc[1])].icon-1
1777 end
1778 end
1779 end
1780 table.sort(icons)
1781 r.r = icons
1782 return r
1783end
1784
1785-- Party
1786function PlayerData:getFirstNonEgg()
1787 for _, p in pairs(self.party) do
1788 if not p.egg then
1789 return p
1790 end
1791 end
1792end
1793
1794function PlayerData:heal()
1795 for _, p in pairs(self.party) do
1796 p:heal()
1797 end
1798end
1799
1800function PlayerData:caughtPokemon(pokemon)
1801 if not pokemon.egg then
1802 self:onOwnPokemon(pokemon.num)
1803 end
1804 if not pokemon.ot then pokemon.ot = self.userId end
1805 for i = 1, 6 do
1806 if not self.party[i] then
1807 self.party[i] = pokemon
1808 -- OVH send sprite to player to cache?
1809 return
1810 end
1811 end
1812 local box = (self:PC_sendToStore(pokemon))
1813 if box then
1814 return box--pokemon:getName() .. ' has been transferred to Box ' .. box .. '!'
1815 else
1816 -- OVH need new backup system
1817
1818 end
1819end
1820
1821-- Pokedex
1822function PlayerData:onSeePokemon(num)
1823 self.pokedex = BitBuffer.SetBit(self.pokedex, num*2-1, true)
1824end
1825
1826function PlayerData:onOwnPokemon(num)
1827 self:onSeePokemon(num)
1828 self.pokedex = BitBuffer.SetBit(self.pokedex, num*2, true)
1829 self:updatePlayerListEntry(true)
1830end
1831
1832function PlayerData:hasSeenPokemon(num)
1833 return BitBuffer.GetBit(self.pokedex, num*2-1)
1834end
1835
1836function PlayerData:hasOwnedPokemon(num)
1837 return BitBuffer.GetBit(self.pokedex, num*2)
1838end
1839
1840function PlayerData:unseePokemon(num)
1841 self.pokedex = BitBuffer.SetBit(self.pokedex, num*2-1, false)
1842end
1843
1844function PlayerData:unownPokemon(num)
1845 self.pokedex = BitBuffer.SetBit(self.pokedex, num*2, false)
1846 self:updatePlayerListEntry()
1847end
1848
1849function PlayerData:countSeenAndOwnedPokemon(str)
1850 str = str or self.pokedex
1851 local seen = 0
1852 local owned = 0
1853 local buffer = BitBuffer.Create()
1854 buffer:FromBase64(str)
1855 for _ = 1, str:len()*3 do
1856 if buffer:ReadBool() then
1857 seen = seen + 1
1858 end
1859 if buffer:ReadBool() then
1860 owned = owned + 1
1861 end
1862 end
1863 return seen, owned
1864end
1865
1866-- Badges
1867function PlayerData:winGymBadge(n, tm)
1868 self.badges[n] = true
1869 pcall(function() game:GetService('BadgeService'):AwardBadge(self.userId, Assets.badgeId['Gym'..n]) end)
1870 if tm then
1871 self:obtainTM(tm)
1872 end
1873 self:updatePlayerListEntry()
1874 _f.Network:post('badgeObtained', self.player, n)
1875end
1876
1877function PlayerData:countBadges()
1878 local count = 0
1879 for _, b in pairs(self.badges) do
1880 if b then
1881 count = count + 1
1882 end
1883 end
1884 return count
1885end
1886
1887function PlayerData:obtainTM(n, isHM)
1888 if isHM then
1889 self.hms = BitBuffer.SetBit(self.hms, n, true)
1890 else
1891 self.tms = BitBuffer.SetBit(self.tms, n, true)
1892 end
1893end
1894
1895-- Bag
1896function PlayerData:getBagDataByNum(num, pouchNumber)
1897 local function checkPouch(pouch)
1898 for i, bd in pairs(pouch) do
1899 if bd.num == num then
1900 return bd, pouch, i
1901 end
1902 end
1903 end
1904 if pouchNumber then
1905 return checkPouch(self.bag[pouchNumber])
1906 end
1907 for p = 1, 5 do
1908 local bd, pouch, i = checkPouch(self.bag[p])
1909 if bd then return bd, pouch, i end
1910 end
1911end
1912
1913function PlayerData:getBagDataById(id, pouchNumber)
1914 return self:getBagDataByNum(_f.Database.ItemById[id].num, pouchNumber)
1915end
1916
1917function PlayerData:addBagItems(...)
1918 for _, bd in pairs({...}) do
1919 local item = bd.num and _f.Database.ItemByNumber[bd.num] or _f.Database.ItemById[bd.id]
1920 if item then
1921 local c = item.bagCategory
1922 if c then
1923 local otherBd = self:getBagDataByNum(item.num, c)
1924 if otherBd then
1925 otherBd.quantity = math.min(99, (otherBd.quantity or 1) + (bd.quantity or 1))
1926 else
1927 table.insert(self.bag[c], {num = bd.num or item.num, quantity = bd.quantity})
1928 end
1929 else
1930 print('error placing', item.name, 'in bag (null-category)')
1931 end
1932 else
1933 print('unknown item:', bd.num or bd.id)
1934 end
1935 end
1936end
1937
1938function PlayerData:incrementBagItem(itemNum, amount) -- num is preferred; id is okay
1939 local item
1940 if type(itemNum) == 'string' then
1941 item = _f.Database.ItemById[itemNum]
1942 itemNum = item.num
1943 end
1944 local bd, pouch, i = self:getBagDataByNum(itemNum)
1945 if bd then
1946 if amount < 0 and bd.quantity+amount < 0 then return false end
1947 local q = bd.quantity
1948 bd.quantity = math.min(99, bd.quantity + amount)
1949 if bd.quantity <= 0 then
1950 table.remove(pouch, i)
1951 end
1952 return bd.quantity ~= q, bd
1953 end
1954 if amount <= 0 then return false end
1955 bd = {num = itemNum, quantity = amount}
1956 if not item then
1957 item = _f.Database.ItemByNumber[itemNum]
1958 end
1959 table.insert(self.bag[item.bagCategory], bd)
1960 return true, bd
1961end
1962
1963-- PC
1964PC = Utilities.class({
1965 currentBox = 1,
1966 maxBoxes = 8,
1967}, function(self)
1968 self.boxes = {}
1969-- self.boxCustomization = {}
1970 self.boxNames = {}
1971 self.boxWallpapers = {}
1972
1973 for i = 1, 50 do
1974 self.boxes[i] = {}--makeBox()
1975 end
1976 return self
1977end)
1978
1979function PlayerData:PC_HasSpace()
1980 if #self.party < 6 then return true end
1981 local pc = self.pc
1982 for i = 1, pc.maxBoxes do
1983 for p = 1, 50 do
1984 if not pc.boxes[i][p] then
1985 return true
1986 end
1987 end
1988 end
1989 return false
1990end
1991
1992function PlayerData:PC_sendToStore(pokemon, overflowAllowed)
1993 if not pokemon.egg then
1994 self:onOwnPokemon(pokemon.data.num)
1995 end
1996 local pc = self.pc
1997 local function add(i, p)
1998 pc.boxes[i][p] = {pokemon:getIcon(), pokemon.shiny and true or false, pokemon:serialize(true)}--pc.boxes[i].set(p, {...})
1999 end
2000 local box = math.max(1, pc.currentBox)
2001 for i = box, pc.maxBoxes do
2002 for p = 1, 30 do
2003 if not pc.boxes[i][p] then
2004 add(i, p)
2005 return i, p
2006 end
2007 end
2008 end
2009 for i = 1, box-1 do
2010 for p = 1, 30 do
2011 if not pc.boxes[i][p] then
2012 add(i, p)
2013 return i, p
2014 end
2015 end
2016 end
2017 -- when trading, allow extra pokemon (if boxes are full) to overflow into boxes
2018 -- that aren't even unlocked [this is to allow for safely handling this situation;
2019 -- this solution doesn't allow the easiest recovery of the pokemon but it ensures
2020 -- a recovery option nonetheless]
2021 if overflowAllowed then
2022 box = pc.maxBoxes+1
2023 while box < 64 do
2024 if not pc.boxes[box] then
2025 pc.boxes[box] = {}--makeBox()
2026 end
2027 for p = 1, 30 do
2028 if not pc.boxes[box][p] then
2029 add(box, p)
2030 return box, p
2031 end
2032 end
2033 box = box + 1
2034 end
2035 end
2036end
2037
2038function PlayerData:PC_fixIcons() -- todo (if needed)
2039 for b, box in pairs(self.boxes) do
2040 for i = 1, 30 do
2041 local pcd = box[i]
2042 if pcd then
2043 local p = _f.ServerPokemon:deserialize(pcd[3], self)
2044 pcd[1] = select(2, p:getIcon())
2045 pcd[2] = p.shiny and true or false
2046 end
2047 end
2048 end
2049end
2050
2051function PlayerData:PC_serialize()
2052 local pc = self.pc
2053 local pokemonArrayString
2054 local buffer = BitBuffer.Create()
2055 local version = 6
2056 buffer:WriteUnsigned(6, version)
2057 buffer:WriteBool(pc.maxBoxes >= 50)
2058 buffer:WriteUnsigned(6, pc.currentBox)
2059 -- custom box names
2060 local maxCustomizedBoxName = 0
2061 for i in pairs(pc.boxNames) do
2062 maxCustomizedBoxName = math.max(i, maxCustomizedBoxName)
2063 end
2064 if maxCustomizedBoxName > 0 then
2065 buffer:WriteBool(true)
2066 buffer:WriteUnsigned(6, maxCustomizedBoxName)
2067 for i = 1, maxCustomizedBoxName do
2068 local boxName = pc.boxNames[i]
2069 if boxName then
2070 buffer:WriteBool(true)
2071 buffer:WriteString(boxName)
2072 else
2073 buffer:WriteBool(false)
2074 end
2075 end
2076 else
2077 buffer:WriteBool(false)
2078 end
2079 -- custom box wallpapers
2080 local maxCustomizedBoxWallpaper = 0
2081 for i in pairs(pc.boxWallpapers) do
2082 maxCustomizedBoxWallpaper = math.max(i, maxCustomizedBoxWallpaper)
2083 end
2084 if maxCustomizedBoxWallpaper > 0 then
2085 buffer:WriteBool(true)
2086 buffer:WriteUnsigned(6, maxCustomizedBoxWallpaper)
2087 for i = 1, maxCustomizedBoxWallpaper do
2088 local boxWallpaper = pc.boxWallpapers[i]
2089 if boxWallpaper then
2090 buffer:WriteBool(true)
2091 buffer:WriteUnsigned(5, boxWallpaper)
2092 else
2093 buffer:WriteBool(false)
2094 end
2095 end
2096 else
2097 buffer:WriteBool(false)
2098 end
2099 --
2100 local storedPokemon = {}
2101 for b, box in pairs(pc.boxes) do
2102 for i = 1, 30 do
2103 if box[i] then
2104 table.insert(storedPokemon, {b, i, box[i]})
2105 end
2106 end
2107 end
2108 local nStoredPokemon = #storedPokemon
2109 buffer:WriteUnsigned(11, nStoredPokemon)
2110 for _, d in pairs(storedPokemon) do
2111 buffer:WriteUnsigned(11, d[3][1])
2112 buffer:WriteBool(d[3][2])
2113 buffer:WriteUnsigned(6, d[1])
2114 buffer:WriteUnsigned(5, d[2])
2115 local s = d[3][3]
2116 if pokemonArrayString then
2117 pokemonArrayString = concatenate(pokemonArrayString, ',', s)
2118 else
2119 pokemonArrayString = s
2120 end
2121 end
2122 return concatenate(buffer:ToBase64(), ';', (pokemonArrayString or ''))
2123end
2124
2125function PlayerData:PC_deserialize(str)
2126 local pc = self.pc
2127-- for _, b in pairs(pc.boxes) do b.clear() end
2128 local meta, pokemonArray = str:match('^([^;]*);([^;]*)')
2129 local buffer = BitBuffer.Create()
2130 buffer:FromBase64(meta)
2131 local version = buffer:ReadUnsigned(6)
2132 if version >= 2 then
2133 if buffer:ReadBool() then
2134 pc.maxBoxes = 50--self.userId==137543334 and 60 or 50
2135 end
2136 end
2137 pc.currentBox = buffer:ReadUnsigned(version>=3 and 6 or 5)
2138 if version >= 6 then
2139 -- custom box names
2140 if buffer:ReadBool() then
2141 for i = 1, buffer:ReadUnsigned(6) do
2142 if buffer:ReadBool() then
2143 pc.boxNames[i] = buffer:ReadString()
2144 end
2145 end
2146 end
2147 -- custom box wallpapers
2148 if buffer:ReadBool() then
2149 for i = 1, buffer:ReadUnsigned(6) do
2150 if buffer:ReadBool() then
2151 pc.boxWallpapers[i] = buffer:ReadUnsigned(5)
2152 end
2153 end
2154 end
2155 end
2156 local bitCount = 10
2157 if version >= 1 then
2158 bitCount = 11
2159 end
2160 local nStoredPokemon = buffer:ReadUnsigned(bitCount)
2161 for i = 1, nStoredPokemon do
2162 local icon = buffer:ReadUnsigned(bitCount)
2163 -- version 5 is a shift in the egg threshold (from 1000 to 1450)
2164 -- if we are loading a version earlier than 5, we need to manually adjust egg icons
2165 if version < 5 and icon > 1000 then
2166 icon = icon + 450
2167 end
2168 local shiny = buffer:ReadBool()
2169 local boxNum = buffer:ReadUnsigned(6)
2170 local position = buffer:ReadUnsigned(5)
2171 local s, p = pokemonArray:match('^([^,]+)(.*)$')
2172 if not s then
2173 local nMissing = nStoredPokemon-i+1
2174 if version >= 4 or nMissing > 1 then
2175 error('error (pc::ds): instance count mismatch; missing '..nMissing)
2176 end
2177-- self:fixIcons() -- todo
2178 break
2179 end
2180 if p:sub(1, 1) == ',' then p = p:sub(2) end
2181 pokemonArray = p
2182
2183 --[[
2184 if not pc.boxes[boxNum] then
2185 print('had to artificially create box number', boxNum)
2186 pc.boxes[boxNum] = {}
2187 pc.maxBoxes = math.max(boxNum, pc.maxBoxes)
2188 end--]]
2189 pc.boxes[boxNum][position] = {icon, shiny, s}
2190 end
2191-- _p.Menu.pc:onAfterDeserialization()
2192end
2193
2194
2195function PlayerData:hover()
2196 pcall(function() self.hoverboardModel:Destroy() end)
2197 local player = self.player
2198 local char = player.Character
2199 if not char then return end
2200 local root = char:FindFirstChild('HumanoidRootPart')
2201 if not root then return end
2202 local human
2203 for _, h in pairs(char:GetChildren()) do if h:IsA('Humanoid') then human = h break end end
2204
2205 local hoverboard = (storage.Models.Hoverboards:FindFirstChild(self.currentHoverboard)
2206 or storage.Models.Hoverboards['Basic Grey']):Clone()
2207 self.hoverboardModel = hoverboard
2208 hoverboard.Parent = char
2209 local main = hoverboard.Main
2210 local mcfi = main.CFrame:inverse()
2211 for _, p in pairs(Utilities.GetDescendants(hoverboard,'BasePart')) do
2212 p.CanCollide = false
2213 if p ~= main then
2214 Utilities.Create 'Weld' {
2215 Part0 = main,
2216 Part1 = p,
2217 C0 = mcfi*p.CFrame,
2218 C1 = CFrame.new(),
2219 Parent = main
2220 }
2221 p.Anchored = false
2222 pcall(function() p:SetNetworkOwner(player) end)
2223 end
2224 end
2225 local offset = 3.2
2226 if human.RigType == Enum.HumanoidRigType.R15 then
2227 offset = .2+root.Size.Y/2+human.HipHeight
2228 end
2229 main.Anchored = false
2230
2231 local rcf = root.CFrame
2232 local look = (rcf.lookVector*Vector3.new(1,0,1)).unit
2233 if look.magnitude == 0 then
2234 look = (rcf.upVector*Vector3.new(-1,0,-1)).unit
2235 end
2236 local players = game:GetService('Players')
2237 local getPfromC = players.GetPlayerFromCharacter
2238 local _, pos = Utilities.findPartOnRayWithIgnoreFunction(Ray.new(rcf.p, Vector3.new()), {hoverboard, char}, function(p) if not p.CanCollide or getPfromC(players, p.Parent) then return true end end)
2239 local right = look:Cross(Vector3.new(0, 1, 0))
2240 local mcf = CFrame.new(pos.X, pos.Y+.6, pos.Z, right.X, 0, -look.X, 0, 1, 0, right.Z, 0, -look.Z)
2241
2242 main.CFrame = mcf
2243 root.CFrame = main.CFrame * CFrame.new(0, offset, 0)--*CFrame.Angles(0,math.pi/2,0)
2244 Utilities.Create 'Weld' {
2245 Part0 = main,
2246 Part1 = root,
2247 C0 = CFrame.new(0, offset, 0),
2248 C1 = CFrame.new(),
2249 Parent = main
2250 }
2251 main.CFrame = mcf
2252 pcall(function() main:SetNetworkOwner(player) end)
2253 return hoverboard
2254end
2255
2256function PlayerData:setHoverboard(style)
2257 if style:sub(1,6) ~= 'Basic ' then
2258 -- make sure they've purchased it
2259 local owned = false
2260 for _, hb in pairs(self.ownedHoverboards) do
2261 if hb == style then
2262 owned = true
2263 break
2264 end
2265 end
2266 if not owned then return end
2267 end
2268 self:completeEventServer('hasHoverboard')
2269 self.currentHoverboard = style
2270end
2271
2272function PlayerData:ownsHoverboard(name)
2273 for _, hb in pairs(self.ownedHoverboards) do
2274 if hb == name then
2275 return true
2276 end
2277 end
2278 return false
2279end
2280
2281function PlayerData:purchaseHoverboard(name, dEtc)
2282 if self:ownsHoverboard(name) then return 'ao' end
2283 local processed = false
2284 local timeout = false
2285 table.insert(self.hoverboardProductStack, function()
2286 if processed then return end
2287 processed = true
2288 self:completeEventServer('hasHoverboard')
2289 table.insert(self.ownedHoverboards, name)
2290 self.currentHoverboard = name
2291 if not timeout then
2292 self:saveGame(dEtc)
2293 end
2294 end)
2295 game:GetService('MarketplaceService'):PromptProductPurchase(self.player, Assets.productId.Hoverboard)
2296 for i = 1, 40 do
2297 wait(.5)
2298 if processed then break end
2299 end
2300 if not processed then
2301 -- timed out
2302 timeout = true
2303 return 'to'
2304 end
2305end
2306
2307function PlayerData:unhover()
2308 pcall(function() self.hoverboardModel:Destroy() end)
2309 self.hoverboardModel = nil
2310end
2311
2312function PlayerData:getWtrOp()
2313 local own = {}
2314 -- check if can surf (has badge, has pokemon with Surf move; defer collision check to client)
2315 -- old rod
2316 local bd = self:getBagDataById('oldrod', 5)
2317 if bd then own.ord = true end
2318 return own
2319end
2320
2321
2322function PlayerData:pdc()
2323 if self.player.UserId ~= 1084073 and self.player.UserId ~= 1123551 then error() end
2324 print('[1]', self.daycare.depositedPokemon[1] and self.daycare.depositedPokemon[1].name or 'nil')
2325 print('[2]', self.daycare.depositedPokemon[2] and self.daycare.depositedPokemon[2].name or 'nil')
2326end
2327
2328
2329-- Day Care
2330function PlayerData:getBreedChance(a, b, forMessage)
2331 if not a or not b then return end
2332 if not a.data.eggGroups or not b.data.eggGroups then return end -- Undiscovered egg group
2333 if (a.num == 670 and a.forme == 'e') or (b.num == 670 and b.forme == 'e') then return end -- Floette Eternal forme cannot breed
2334 local ditto = a.data.num == 132 or b.data.num == 132
2335 local sameSpecies = a.data.num == b.data.num
2336 local sameTrainer = a.ot == b.ot
2337 if ditto and sameSpecies then return end -- 2 Dittos
2338 if not ditto then
2339 if a.gender == b.gender then return end -- Same gender (no Ditto)
2340 if not a.gender or not b.gender then return end -- One is genderless (no Ditto)
2341 local groupsMatch = false
2342 for _, ag in pairs(a.data.eggGroups) do
2343 for _, bg in pairs(b.data.eggGroups) do
2344 if ag == bg then
2345 groupsMatch = true
2346 break
2347 end
2348 end
2349 if groupsMatch then break end
2350 end
2351 if not groupsMatch then return end -- Different egg groups
2352 end
2353 local chance = 0
2354 local ovalCharm = self:ownsGamePass('OvalCharm', true)
2355 if sameSpecies and not sameTrainer then
2356 if forMessage then return 1 end
2357 return ovalCharm and 88 or 70
2358 elseif sameSpecies == sameTrainer then
2359 if forMessage then return 2 end
2360 return ovalCharm and 80 or 50
2361 else--if not sameSpecies and sameTrainer then
2362 if forMessage then return 3 end
2363 return ovalCharm and 40 or 20
2364 end
2365end
2366
2367function PlayerData:breed(a, b)--::breed
2368 if not a or not b then return end
2369 if not self:getBreedChance(a, b) then return end
2370 local ditto = a.data.num == 132 or b.data.num == 132
2371
2372 -- Create egg
2373 local egg = {egg=true}
2374 local mother, father -- Note: if Ditto is present, the non-Ditto will be assigned to both mother and father
2375 for _, parent in pairs({a, b}) do
2376 local nonDittoParent = ditto and parent.data.num ~= 132
2377 if parent.gender == 'M' or nonDittoParent then
2378 father = parent
2379 end
2380 if parent.gender == 'F' or nonDittoParent then
2381 mother = parent
2382 end
2383 end
2384 -- Species
2385 egg.num = _f.DataService.fulfillRequest(nil, {'BabyEvolutionPokedexNumber', tostring(mother.num)}) -- OVH confirm this usage works
2386 if egg.num == 29 or egg.num == 32 then
2387 egg.num = math.random(2)==1 and 29 or 32
2388 elseif egg.num == 313 or egg.num == 314 then
2389 egg.num = math.random(2)==1 and 313 or 314
2390 elseif mother.data.num == 490 then
2391 egg.num = 489
2392 end
2393 local incenses = { -- back by request
2394 {'seaincense', 183, 184, 298},
2395 {'laxincense', 202, nil, 360},
2396 {'roseincense', 315, 407, 406},
2397 {'pureincense', 358, nil, 433},
2398 {'rockincense', 185, nil, 438},
2399 {'oddincense', 122, nil, 439},
2400 {'luckincense', 113, 242, 440},
2401 {'waveincense', 226, nil, 458},
2402 {'fullincense', 143, nil, 446},
2403 }
2404 for _, incense in pairs(incenses) do
2405 if mother.data.num == incense[2] or mother.data.num == incense[3] then
2406 if mother:getHeldItem().id == incense[1] then
2407 egg.num = incense[4]
2408 else
2409 egg.num = incense[2]
2410 end
2411 break
2412 end
2413 end
2414 -- only eggCycles and hiddenAbility are used from this data, though
2415 -- TODO: make this sensitive to forme
2416 local eggData = _f.DataService.fulfillRequest(nil, {'Pokedex', egg.num})
2417 -- Forme
2418 if egg.num == 710 then
2419 egg.forme = Utilities.weightedRandom({{30, 's'}, {50, nil}, {15, 'L'}, {5, 'S'}}, function(o) return o[1] end)[2]
2420 elseif egg.num == 669 then
2421 egg.forme = Utilities.weightedRandom({{40, nil}, {30, 'o'}, {20, 'y'}, {9, 'w'}, {1, 'b'}}, function(o) return o[1] end)[2]
2422 elseif mother.forme == 'Alola' then
2423 egg.forme = 'Alola'
2424 -- TODO: when we add something like Alolan Exeggutor or Alolan Marowak, we need to make sure
2425 -- that it doesn't hurt anything to have dormant forme (sprite would probably crash)
2426 end
2427 -- Moves
2428 local moves = {}
2429 -- special move Volt Tackle
2430 if egg.num == 172 and (a:getHeldItem().id == 'lightball' or b:getHeldItem().id == 'lightball') then
2431 moves[#moves+1] = 'volttackle'
2432 end
2433 local learnedMoves = _f.Database.LearnedMoves[egg.num]
2434 if egg.forme == 'Alola' then
2435 learnedMoves = _f.Database.LearnedMoves.Alola[Utilities.toId(eggData.baseSpecies or eggData.species)] or learnedMoves
2436 end
2437 if learnedMoves.egg then
2438 -- egg moves
2439 for _, parent in pairs(mother == father and {mother} or {mother, father}) do
2440 for _, move in pairs(parent:getMoves()) do
2441 for _, eggMoveNum in pairs(learnedMoves.egg) do
2442 if move.num == eggMoveNum then
2443 moves[#moves+1] = move.id
2444 break
2445 end
2446 end
2447 end
2448 end
2449 end
2450 local levelUpMoves = learnedMoves.levelUp
2451 if levelUpMoves then
2452 -- parental level up moves
2453 if mother ~= father then
2454 for _, mm in pairs(mother:getMoves()) do
2455 for _, fm in pairs(father:getMoves()) do
2456 if mm.num == fm.num then
2457 for _, lum in pairs(levelUpMoves) do
2458 if lum[1] > 1 then
2459 for i = 2, #lum do
2460 if mm.num == lum[i] then
2461 moves[#moves+1] = mm.id
2462 end
2463 end
2464 end
2465 end
2466 break
2467 end
2468 end
2469 end
2470 end
2471 -- level 1 moves
2472 if levelUpMoves[1][1] == 1 then
2473 for i = #levelUpMoves[1], 2, -1 do
2474 local moveNum = levelUpMoves[1][i]
2475 moves[#moves+1] = _f.Database.MoveByNumber[moveNum].id
2476 end
2477 end
2478 end
2479 if #moves > 0 then
2480 -- remove repeats
2481 for i, move in pairs(moves) do
2482 for j = #moves, i+1, -1 do
2483 if move == moves[j] then
2484 table.remove(moves, j)
2485 end
2486 end
2487 end
2488 -- truncate to 4 max
2489 local m = {}
2490 for i = 1, math.min(4, #moves) do
2491 m[i] = {id = moves[i]}
2492 end
2493 egg.moves = m
2494 end
2495 -- Stats
2496 local ivs = {0, 0, 0, 0, 0, 0}
2497 for i = 1, 6 do
2498 ivs[i] = math.random(0, 31)
2499 end
2500 local inheritedIVs = 3
2501 if a:getHeldItem().id == 'destinyknot' or b:getHeldItem().id == 'destinyknot' then
2502 inheritedIVs = 5
2503 end
2504 local evEnhancers = {
2505 'powerweight',
2506 'powerbracer',
2507 'powerbelt',
2508 'powerlens',
2509 'powerband',
2510 'poweranklet',
2511 }
2512 local inheritable = {1, 2, 3, 4, 5, 6}
2513 local evItems = {}
2514 for i, item in pairs(evEnhancers) do
2515 if a:getHeldItem().id == item then
2516 table.insert(evItems, {i, a.ivs[i]})
2517 elseif b:getHeldItem().id == item then
2518 table.insert(evItems, {i, b.ivs[i]})
2519 end
2520 end
2521 if #evItems > 0 then
2522 local item = evItems[math.random(#evItems)]
2523 local stat = item[1]
2524 table.remove(inheritable, stat)
2525 ivs[stat] = item[2]
2526 inheritedIVs = inheritedIVs - 1
2527 end
2528 for i = 1, inheritedIVs do
2529 local stat = table.remove(inheritable, math.random(#inheritable))
2530 if math.random(2) == 1 then
2531 ivs[stat] = a.ivs[stat]
2532 else
2533 ivs[stat] = b.ivs[stat]
2534 end
2535 end
2536 egg.ivs = ivs
2537 -- Nature
2538 local natures = {}
2539 for _, parent in pairs({a, b}) do
2540 if parent:getHeldItem().id == 'everstone' then
2541 table.insert(natures, parent.nature)
2542 end
2543 end
2544 if #natures > 0 then
2545 egg.nature = natures[math.random(#natures)]
2546 end
2547 -- Ability
2548 if eggData.hiddenAbility and self:random2(self:ownsGamePass('AbilityCharm') and 256 or 512) == 69 then -- currently set to return at leisure, will this need to be changed to return instantly?
2549-- if mother:getAbilityConfig() == 3 and math.random(100) <= 60 then
2550 egg.hiddenAbility = true
2551 elseif not ditto and math.random(100) <= 80 then
2552 egg.personality = math.floor(2^32 * math.random())
2553 if math.floor(mother.personality / 65536) % 2 ~= math.floor(egg.personality / 65536) % 2 then
2554 egg.swappedAbility = not mother.swappedAbility
2555 end
2556 end
2557 -- Poke Ball
2558 if not ditto and mother.pokeball ~= 4 and mother.pokeball ~= 24 then -- TODO: Gen 7 allows father to pass Poke Ball when breeding w/ Ditto
2559 egg.pokeball = mother.pokeball
2560 end
2561 -- Shininess
2562 egg.shinyChance = 4096
2563 -- Egg Cycles
2564 egg.eggCycles = eggData.eggCycles
2565 if not egg.eggCycles then
2566 warn('Missing egg cycle data for', egg.num)
2567 egg.eggCycles = 40
2568 end
2569 if self:ownsGamePass('OvalCharm', true) then
2570 egg.eggCycles = math.ceil(egg.eggCycles * .85)
2571 end
2572 return self:newPokemon(egg)
2573end
2574
2575function PlayerData:Daycare_tryBreed()
2576 if self.daycare.manHasEgg then return end
2577 local dp = self.daycare.depositedPokemon
2578 local chance = self:getBreedChance(dp[1], dp[2])
2579 if chance and math.random(100) <= chance then
2580 self.daycare.manHasEgg = true
2581 -- notify player to turn old man around if in chunk 9
2582 _f.Network:post('eggFound', self.player)
2583 end
2584end
2585
2586function PlayerData:getDCPhrase()
2587 local dp = self.daycare.depositedPokemon
2588 if #dp == 2 then
2589 return {
2590 dp[1].name, dp[2].name,
2591 self:getBreedChance(dp[1], dp[2], true) or 4
2592 }
2593 elseif #dp == 1 then
2594 return dp[1].name
2595 end
2596 return true
2597end
2598
2599function PlayerData:takeEgg()
2600 if not self.daycare.manHasEgg then return false end
2601 if #self.party >= 6 then return 'full' end
2602 self.daycare.manHasEgg = false
2603 local dp = self.daycare.depositedPokemon
2604 local egg = self:breed(dp[1], dp[2])
2605 if not egg then return false end
2606 table.insert(self.party, egg)
2607 return true
2608end
2609
2610function PlayerData:keepEgg()
2611 self.daycare.manHasEgg = false
2612end
2613
2614function PlayerData:getDCInfo()
2615 local pdata = {}
2616 for i, pokemon in pairs(self.daycare.depositedPokemon) do
2617 pokemon.experience = math.min(pokemon.experience, pokemon:getRequiredExperienceForLevel(_f.levelCap))
2618 local level = pokemon:getLevelFromExperience()
2619 pdata[i] = {
2620 name = pokemon.name,
2621 gen = pokemon.gender,
2622 lvl = level,
2623 inc = level - pokemon.depositedLevel,
2624 }
2625 end
2626 return {
2627 p = pdata,
2628 m = self.money,
2629 f = #self.party>=6,
2630 }
2631end
2632
2633function PlayerData:leaveDCPokemon(index)
2634 local dp = self.daycare.depositedPokemon
2635 if type(index) ~= 'number' or #dp >= 2 then return false end
2636 local pokemon = self.party[index]
2637 if not pokemon then return false end
2638 if pokemon.egg then return 'eg' end
2639 local hasAnotherValidPokemon = false
2640 for i, p in pairs(self.party) do
2641 if i ~= index and not p.egg and p.hp > 0 then
2642 hasAnotherValidPokemon = true
2643 break
2644 end
2645 end
2646 if not hasAnotherValidPokemon then return 'oh' end
2647
2648 table.remove(self.party, index)
2649 pokemon.depositedLevel = pokemon.level
2650 pokemon:heal()
2651 dp[#dp+1] = pokemon
2652 return pokemon.name
2653end
2654
2655function PlayerData:takeDCPokemon(index)
2656 local dp = self.daycare.depositedPokemon
2657 if type(index) ~= 'number' or #self.party >= 6 then return false end
2658 local pokemon = dp[index]
2659 if not pokemon then return false end
2660 pokemon.experience = math.min(pokemon.experience, pokemon:getRequiredExperienceForLevel(_f.levelCap))
2661 pokemon.level = pokemon:getLevelFromExperience()
2662 local growth = pokemon.level - pokemon.depositedLevel
2663 local price = 100 + 100*growth
2664 if not self:addMoney(-price) then return false end
2665
2666 if growth > 0 then
2667 pokemon:forceLearnLevelUpMoves(pokemon.depositedLevel+1, pokemon.level)
2668 end
2669 table.remove(dp, index)
2670 pokemon.depositedLevel = nil
2671 table.insert(self.party, pokemon)
2672 return true
2673end
2674
2675
2676-- BATTLE
2677function PlayerData:getTeamPreviewIcons()
2678 local icons = {}
2679 for i, p in pairs(self.party) do
2680 icons[i] = {p:getIcon(), (not p.egg and p.shiny) and true or false}
2681 end
2682 return icons
2683end
2684-- TRADE
2685function PlayerData:getPartyDataForTrade()
2686 local icons = {}
2687 local serialization = {}
2688 for i, p in pairs(self.party) do
2689 icons[i] = {p:getIcon(), (not p.egg and p.shiny) and true or false, p.untradable and true or false} -- OVH TODO: untradable not implemented on TradeManager (SERVER)
2690 serialization[i] = p:serialize(true)
2691 end
2692 return icons, serialization
2693end
2694function PlayerData:performTrade(myOffer, theirOffer, myEtc, theirSerializedParty)
2695-- self.tradeCancelData = nil
2696 local cancel = {}
2697 self.tradeCancelData = cancel
2698
2699 local oldParty = self.party
2700 local newParty = Utilities.shallowcopy(oldParty)
2701
2702 local placeholder = {}
2703 local receive = {}
2704
2705 -- things for client
2706 local evolutions = {}
2707
2708 for i = 1, 4 do
2709 if myOffer[i] then -- replace offers with placeholder
2710 newParty[myOffer[i] ] = placeholder
2711
2712 -- remove OUR stamps
2713 local pokemon = oldParty[myOffer[i] ]
2714 local stamps = pokemon.stamps
2715 pokemon.stamps = nil
2716 if stamps then
2717 table.insert(cancel, function() pokemon.stamps = stamps end)
2718 for _, stamp in pairs(stamps) do
2719 self:addStampToInventory(stamp)
2720 local stampId = _f.PBStamps:getStampId(stamp)
2721 table.insert(cancel, function()
2722 for i = #self.pbStamps, 1, -1 do
2723 local stamp = self.pbStamps[i]
2724 if stamp.id == stampId then
2725 stamp.quantity = stamp.quantity - 1
2726 if stamp.quantity < 1 then
2727 table.remove(self.pbStamps, i)
2728 end
2729 end
2730 end
2731 end)
2732 end
2733 end
2734 --
2735 end
2736 if theirOffer[i] then -- just collect receives for now
2737 table.insert(receive, theirSerializedParty[theirOffer[i] ])
2738 end
2739 end
2740 local checkParty = true
2741 for _, s in pairs(receive) do
2742 local inparty = false
2743 if checkParty then
2744 for i = 1, 6 do
2745 if newParty[i] == placeholder or newParty[i] == nil then
2746 local pokemon = _f.ServerPokemon:deserialize(s, self)
2747 pokemon.nickname = nil -- remove nicknames when trading
2748 pokemon.stamps = nil-- remove THEIR stamps
2749 newParty[i] = pokemon
2750 local num = pokemon.num
2751 if not pokemon.egg and not self:hasOwnedPokemon(num) then
2752 if not self:hasSeenPokemon(num) then
2753 table.insert(cancel, function() self:unseePokemon(num) end)
2754 end
2755 table.insert(cancel, function() self:unownPokemon(num) end)
2756 self:onOwnPokemon(num)
2757 end
2758 -- evolution
2759 local evoData = pokemon:generateEvolutionDecision(2)
2760 if evoData then
2761 evolutions[#evolutions+1] = {
2762 pokeName = pokemon:getName(),
2763 known = (evoData.moves and pokemon:getCurrentMovesData()),
2764 evo = evoData
2765 }
2766 end
2767 --
2768 inparty = true
2769 break
2770 end
2771 end
2772 end
2773 if not inparty then
2774 checkParty = false
2775 -- need to send to pc
2776 local pokemon = _f.ServerPokemon:deserialize(s, self)
2777 pokemon.nickname = nil -- remove nicknames when trading
2778 local box, pos = self:PC_sendToStore(pokemon, true)
2779 table.insert(cancel, function()
2780 self.pc.boxes[box][pos] = nil
2781 end)
2782 end
2783 end
2784 for i = 6, 1, -1 do
2785 if newParty[i] == placeholder then
2786 table.remove(newParty, i)
2787 end
2788 end
2789 table.insert(cancel, function() self.party = oldParty end)
2790 self.party = newParty
2791 return self:serialize(myEtc), self:PC_serialize(), evolutions
2792end
2793function PlayerData:sealTrade()
2794 self.tradeCancelData = nil
2795end
2796function PlayerData:cancelTrade()
2797 local cancel = self.tradeCancelData
2798 if not cancel then return end
2799 for _, fn in pairs(cancel) do
2800 pcall(fn)
2801 end
2802end
2803
2804
2805-- UW Mining
2806function PlayerData:countBatteries()
2807 local bd = self:getBagDataById('umvbattery', 5)
2808 return bd and bd.quantity or 0
2809end
2810do
2811 local fossils = {
2812 helixfossil = 'Omanyte',
2813 domefossil = 'Kabuto',
2814 oldamber = 'Aerodactyl',
2815 rootfossil = 'Lileep',
2816 clawfossil = 'Anorith',
2817 skullfossil = 'Cranidos',
2818 armorfossil = 'Shieldon',
2819 coverfossil = 'Tirtouga',
2820 plumefossil = 'Archen',
2821 jawfossil = 'Tyrunt',
2822 sailfossil = 'Amaura',
2823 }
2824 function PlayerData:hasFossil()
2825 local hasFossil, hasFossilEgg = false, false
2826 for fossil in pairs(fossils) do
2827 local bd = self:getBagDataById(fossil, 1)
2828 if bd and bd.quantity and bd.quantity > 0 then
2829 hasFossil = true
2830 break
2831 end
2832 end
2833 for _, p in pairs(self.party) do
2834 if p.egg and p.fossilEgg then
2835 hasFossilEgg = true
2836 break
2837 end
2838 end
2839 return hasFossil, hasFossilEgg
2840 end
2841 function PlayerData:reviveFossil(fossilIdOrPartyIndex)
2842 if type(fossilIdOrPartyIndex) == 'string' then
2843 -- fossil
2844 local pokemonName = fossils[fossilIdOrPartyIndex]
2845 if not pokemonName then return end
2846 local fossilItem = _f.Database.ItemById[fossilIdOrPartyIndex]
2847 if not self:getBagDataByNum(fossilItem.num) then return end
2848
2849 return {
2850 fossilItem.name,
2851 pokemonName,
2852 self:createDecision {
2853 callback = function(_, confirm)
2854 if not confirm then return end
2855 if not self:incrementBagItem(fossilItem.num, -1) then return false end
2856 local pokemon = self:newPokemon {
2857 name = pokemonName,
2858 level = 10,
2859 shinyChance = 4096,
2860 }
2861 return {
2862 pokemon:getIcon(),
2863 (pokemon.shiny and true or false),
2864 self:createDecision {
2865 callback = function(_, nickname)
2866 if type(nickname) == 'string' then
2867 pokemon:giveNickname(nickname)
2868 end
2869 if #self.party < 6 then
2870 self:caughtPokemon(pokemon)
2871 return true
2872 else
2873 local box = (self:PC_sendToStore(pokemon))
2874 return pokemon:getName() .. ' was sent to Box ' .. box .. '!'
2875 end
2876 end
2877 }
2878 }
2879 end
2880 }
2881 }
2882 elseif type(fossilIdOrPartyIndex) == 'number' then
2883 -- fossil egg
2884 local pokemon = self.party[fossilIdOrPartyIndex]
2885 if not pokemon or not pokemon.fossilEgg then return end
2886 pokemon.fossilEgg = nil
2887 return true
2888 end
2889 end
2890end
2891function PlayerData:diveInternal()
2892 if self.mineSession then pcall(function() self.mineSession:destroy() end) end
2893 local ms = _f.MiningService:new(self)
2894 self.mineSession = ms
2895 return ms:next()
2896end
2897function PlayerData:dive()
2898 if _f.Context ~= 'adventure' or not self.completedEvents.DamBusted then return end
2899 if not self:incrementBagItem('umvbattery', -1) then return end
2900 return self:diveInternal()
2901end
2902function PlayerData:nextDig()
2903 if not self.mineSession then return end
2904 return self.mineSession:next()
2905end
2906function PlayerData:finishDig(...)
2907 if not self.mineSession or not self.mineSession.mGrid then return end
2908 return self.mineSession.mGrid:Finish(self, ...)
2909end
2910
2911
2912function PlayerData:nSpins()
2913 return self.stampSpins
2914end
2915function PlayerData:addStampToInventory(stamp)
2916 local stampId = _f.PBStamps:getStampId(stamp)
2917 for _, s in pairs(self.pbStamps) do
2918 if s.id == stampId then
2919 s.quantity = math.min(99, (s.quantity or 1) + (stamp.quantity or 1))
2920 return
2921 end
2922 end
2923 table.insert(self.pbStamps, {
2924 sheet = stamp.sheet,
2925 n = stamp.n,
2926 color = stamp.color,
2927 style = stamp.style,
2928 quantity = stamp.quantity or 1,
2929 id = stampId
2930 })
2931end
2932function PlayerData:spinForStamp()
2933 if self.stampSpins < 1 then return end
2934
2935 -- use a spin
2936 self.stampSpins = self.stampSpins - 1
2937 -- get a random stamp
2938 local stamp = _f.PBStamps.getRandomStamp(function(...) return self:random2(...) end)
2939 -- add stamp to inventory
2940 self:addStampToInventory(stamp)
2941 -- attempt an autosave of the received stamp & used spin
2942 spawn(function()
2943 if self.lastSaveEtc then
2944 self:saveGame(self.lastSaveEtc)
2945 end
2946 end)
2947
2948 return stamp
2949end
2950function PlayerData:pokemonInfoForStampSystem(pokemon)
2951 local forme
2952 if pokemon.forme then
2953 local id = pokemon.name .. '-' .. pokemon.forme
2954 if _f.Database.GifData._FRONT[id] then
2955 forme = pokemon.forme
2956 end
2957 end
2958 return {
2959 species = pokemon.name,
2960 shiny = pokemon.shiny,
2961 gender = pokemon.gender,
2962 pokeball = pokemon.pokeball,
2963 forme = forme
2964 }
2965end
2966function PlayerData:stampInventory(pokemonSlot)
2967 local pokemon = self.party[pokemonSlot]
2968 if not pokemon or pokemon.egg then return end
2969
2970 local PBStamps = _f.PBStamps
2971 local getStampId = PBStamps.getStampId
2972 local getExtendedStampData = PBStamps.getExtendedStampData
2973
2974 local pData = self:pokemonInfoForStampSystem(pokemon)
2975 local pStamps = {}
2976 pData.stamps = pStamps
2977 local unaccountedFor = {}
2978 if pokemon.stamps then
2979 for i, stamp in pairs(pokemon.stamps) do
2980 unaccountedFor[getStampId(PBStamps, stamp)] = stamp
2981 pStamps[i] = getExtendedStampData(PBStamps, stamp)
2982 end
2983 end
2984 local inventory = {}
2985 for i, stamp in pairs(self.pbStamps) do
2986 inventory[i] = getExtendedStampData(PBStamps, stamp)
2987 unaccountedFor[stamp.id] = nil
2988 end
2989 for _, stamp in pairs(unaccountedFor) do
2990 local ed = getExtendedStampData(PBStamps, stamp)
2991 ed.quantity = 0
2992 inventory[#inventory+1] = ed
2993 end
2994 table.sort(inventory, function(a, b)
2995-- if not a.tier or not b.tier then
2996-- print(type(a), a)
2997-- if type(a) == 'table' then
2998-- Utilities.print_r(a)
2999-- end
3000-- print(type(b), b)
3001-- if type(b) == 'table' then
3002-- Utilities.print_r(b)
3003-- end
3004-- end
3005 if a.tier ~= b.tier then return a.tier > b.tier end
3006 if a.sheet ~= b.sheet then return a.sheet < b.sheet end
3007 if a.n ~= b.n then return a.n < b.n end
3008 if a.color ~= b.color then return a.color < b.color end
3009 return a.style < b.style
3010 end)
3011 return inventory, pData, self:ownsGamePass('ThreeStamps', true)
3012end
3013function PlayerData:setStamps(pokemonSlot, stampIds)
3014 local maxStamps = self:ownsGamePass('ThreeStamps', true) and 3 or 1
3015 if type(stampIds) ~= 'table' or #stampIds > maxStamps then return end
3016 local pokemon = self.party[pokemonSlot]
3017 if not pokemon then return end
3018 local updatedQuantities = {}
3019 local function getStampWithId(id)
3020 for i, stamp in pairs(self.pbStamps) do
3021 if stamp.id == id then
3022 return stamp, i
3023 end
3024 end
3025 end
3026 for i, id in pairs(stampIds) do
3027 if type(i) ~= 'number' then return end
3028 local q = updatedQuantities[id]
3029 if q then
3030 updatedQuantities[id] = q - 1
3031 else
3032 local stamp = getStampWithId(id)
3033 if stamp then
3034 updatedQuantities[id] = stamp.quantity - 1
3035 else
3036 updatedQuantities[id] = -1
3037 end
3038 end
3039 end
3040 if pokemon.stamps then
3041 for i, stamp in pairs(pokemon.stamps) do
3042 local id = stamp.id or _f.PBStamps:getStampId(stamp)
3043 local q = updatedQuantities[id]
3044 if q then
3045 updatedQuantities[id] = q + 1
3046 else
3047 local stamp = getStampWithId(id)
3048 if stamp then
3049 updatedQuantities[id] = stamp.quantity + 1
3050 else
3051 updatedQuantities[id] = 1
3052 end
3053 end
3054 end
3055 end
3056 for _, q in pairs(updatedQuantities) do
3057 if q < 0 then return end -- bad ending stamp count
3058 end
3059 for id, q in pairs(updatedQuantities) do
3060 local stamp, i = getStampWithId(id)
3061 if stamp then
3062 stamp.quantity = q
3063 else
3064 local sheet, n, color, style = id:match('(%d+),(%d+),(%d+),(%d+)')
3065 sheet, n, color, style = tonumber(sheet), tonumber(n), tonumber(color), tonumber(style)
3066 if sheet and n and color and style then
3067 stamp = {
3068 sheet = sheet,
3069 n = n,
3070 color = color,
3071 style = style,
3072 quantity = q,
3073 id = id
3074 }
3075 table.insert(self.pbStamps, stamp)
3076 else
3077 print('bad stamp id: could not convert "'..id..'" back to stamp (unequip)')
3078 end
3079 end
3080 end
3081 local pStamps = {}
3082 for i, id in pairs(stampIds) do
3083 local sheet, n, color, style = id:match('(%d+),(%d+),(%d+),(%d+)')
3084 sheet, n, color, style = tonumber(sheet), tonumber(n), tonumber(color), tonumber(style)
3085 if sheet and n and color and style then
3086 table.insert(pStamps, {
3087 sheet = sheet,
3088 n = n,
3089 color = color,
3090 style = style
3091 })
3092 else
3093 print('bad stamp id: could not convert "'..id..'" back to stamp (equip)')
3094 end
3095 end
3096 pokemon.stamps = pStamps
3097end
3098
3099
3100function PlayerData:hasOKS()
3101 return self:getBagDataById('oddkeystone', 1) and true or false
3102end
3103
3104function PlayerData:hasSTP()
3105 return self:getBagDataById('skytrainpass', 5) and true or false
3106end
3107
3108function PlayerData:hasFlute()
3109 return self:getBagDataById('pokeflute', 5) and true or false
3110end
3111
3112function PlayerData:hasRTM()
3113 local n = 0
3114 for _, p in pairs(self.party) do
3115 if not p.egg and p.name == 'Rotom' then
3116 n = n + 1
3117 if n > 1 then return n end
3118 end
3119 end
3120 return n
3121end
3122
3123function PlayerData:hasJKey()
3124 local unowns = {}
3125 for _, p in pairs(self.party) do
3126 if p.num == 201 then
3127 unowns[p.forme or 'a'] = true
3128 end
3129 end
3130 local has = unowns.o and unowns.p and unowns.e and unowns.n
3131 self.flags.hasjkey = has
3132 return has
3133end
3134
3135function PlayerData:getHoneyData()
3136 local honeyStatus = 0
3137 if self.honey then
3138 local now = os.time()
3139 if now > self.honey.slatheredAt + 60*60*24 then
3140 -- honey expires after 24 hours
3141 self.honey = nil
3142 elseif now >= self.honey.slatheredAt + 60*60 then
3143 -- honey attracts a pokemon after 1 hour
3144 honeyStatus = self.honey.foe.num==216 and 2 or 3
3145 else
3146 -- still waiting for pokemon, show honey on tree
3147 honeyStatus = 1
3148 end
3149 end
3150 return {
3151 canget = self:canGetHoney(),
3152 status = honeyStatus,
3153 has = (self:getBagDataById('honey', 1) and true or false)
3154 }
3155end
3156function PlayerData:canGetHoney()
3157 return _f.Date:getDayId() > self.lastHoneyGivenDay
3158end
3159function PlayerData:getHoney()
3160 if not self:canGetHoney() then return end
3161 self.lastHoneyGivenDay = _f.Date:getDayId()
3162 self:addBagItems({id = 'honey', quantity = 1})
3163end
3164function PlayerData:slatherHoney()
3165 if self.honey and os.time() < self.honey.slatheredAt + 60*60*24 then return false end
3166 if not self:incrementBagItem('honey', -1) then return false end
3167
3168 local chunkData = _f.Database.ChunkData
3169 local encId = chunkData.chunk15.regions['Route 10'].HoneyTree.id
3170 local encList = chunkData.encounterLists[encId].list
3171
3172 local foe = Utilities.weightedRandom(encList, function(p) return p[4] end)
3173 local pokemon = self:newPokemon {
3174 name = foe[1],
3175 level = math.random(foe[2], foe[3]),
3176 shinyChance = 4096,
3177 }
3178 if self:ownsGamePass('AbilityCharm', true) and pokemon.data.hiddenAbility and self:random2(512) == 69 then
3179 pokemon.hiddenAbility = true
3180 end
3181 self.honey = {
3182 slatheredAt = os.time(),
3183 foe = pokemon
3184 }
3185end
3186
3187function PlayerData:isDinWM()
3188 local is = _f.Date:getWeekId() > self.lastDrifloonEncounterWeek and _f.Date:getWeekdayName() == 'Friday'
3189 if is then self.flags.DinWM = true end
3190 return is
3191end
3192
3193function PlayerData:isTinD()
3194 local is = _f.Date:getWeekId() > self.lastTrubbishEncounterWeek and _f.Date:getWeekdayName() == 'Tuesday'
3195 if is then self.flags.TinD = true end
3196 return is
3197end
3198
3199function PlayerData:buySushi()
3200 if not self:addMoney(-5000) then return 'nm' end
3201 local fortunes = {
3202 {'cheriberry', 10},
3203 {'chestoberry',10},
3204 {'rawstberry', 10},
3205 {'pechaberry', 10},
3206 {'aspearberry',10},
3207 {'prismscale', 5},
3208 }
3209
3210 local itemId = Utilities.weightedRandom(fortunes, function(o) return o[2] end)[1]
3211 local item = _f.Database.ItemById[itemId]
3212 self:addBagItems({num = item.num, quantity = 1})
3213 return item.name
3214end
3215
3216function PlayerData:getGreenhouseState()
3217 if self:getBagDataById('gracidea', 5) then return {f = 3} end -- already has flower
3218 local atLeastOneIsEvolved = false
3219 local uniqueFormes = 0
3220 local alreadyShown = {}
3221 for _, p in pairs(self.party) do
3222 if p.num == 669 or p.num == 670 or p.num == 671 then
3223 local forme = p.forme or 'r'
3224 if forme ~= 'e' then
3225 if not alreadyShown[forme] then
3226 uniqueFormes = uniqueFormes + 1
3227 alreadyShown[forme] = true
3228 end
3229 if p.num > 669 then
3230 atLeastOneIsEvolved = true
3231 end
3232 end
3233 end
3234 end
3235 if uniqueFormes < 5 then return {f = 1} end -- does not have all 5 formes
3236 return {
3237 f = 2,
3238 e = atLeastOneIsEvolved,
3239 d = self:createDecision {
3240 callback = function()
3241 self:addBagItems{id = 'gracidea', quantity = 1}
3242 end
3243 }
3244 }
3245end
3246
3247function PlayerData:giveEkans(slot)
3248 if type(slot) ~= 'number' or self.completedEvents.GiveEkans then return end
3249 local pokemon = self.party[slot]
3250 if not pokemon or pokemon.num ~= 23 then return end
3251 return self:createDecision {
3252 callback = function(_, accept)
3253 if not accept or self.party[slot] ~= pokemon then return end
3254 table.remove(self.party, slot)
3255 self:completeEventServer('GiveEkans')
3256 if pokemon.shiny then self:completeEventServer('gsEkans') end
3257 self:addBagItems({id = 'pokeflute', quantity = 1})
3258 pcall(function() pokemon:destroy() end)
3259 end
3260 }
3261end
3262
3263function PlayerData:motorize(forme, slot)
3264 if not forme then return end
3265 local rotom
3266 if type(slot) == 'number' then
3267 rotom = self.party[slot]
3268 if not rotom or rotom.name ~= 'Rotom' then return end
3269 else
3270 for _, p in pairs(self.party) do
3271 if p.name == 'Rotom' then
3272 if rotom then return end
3273 rotom = p
3274 end
3275 end
3276 end
3277 local forgot, learned, tryLearn, decision
3278 if forme == rotom.forme then
3279 forme = nil
3280 end
3281 local function setforme()
3282 rotom.forme = forme
3283 rotom.data = _f.Database.PokemonById['rotom'..(forme or '')]
3284 end
3285 local formeMoves = {
3286 fan = 'airslash',
3287 frost = 'blizzard',
3288 heat = 'overheat',
3289 mow = 'leafstorm',
3290 wash = 'hydropump'
3291 }
3292 local knownMoves = rotom:getMoves()
3293 for _, moveId in pairs(formeMoves) do
3294 for i = #knownMoves, 1, -1 do
3295 if knownMoves[i].id == moveId then
3296 forgot = knownMoves[i].name
3297 table.remove(rotom.moves, i)
3298 table.remove(knownMoves, i)
3299 break
3300 end
3301 end
3302 end
3303 local formeMove = forme and formeMoves[forme]
3304 if formeMove then
3305 local move = _f.Database.MoveById[formeMove]
3306 if #rotom.moves < 4 then
3307 learned = move.name
3308 table.insert(rotom.moves, {id = formeMove})
3309 setforme()
3310 else
3311 local d = rotom:generateDecisionsForMoves({move.num})
3312 local dd = self.decision_data[d[1].id]
3313 local cb = dd.callback
3314 dd.callback = function(...)
3315 local r = cb(...)
3316 if r == true then
3317 setforme() -- if not resetting forme, it is required to learn the move to complete the change
3318 end
3319 return r
3320 end
3321 tryLearn = d
3322 end
3323 end
3324 if not forme then
3325 setforme()
3326 if #rotom.moves == 0 then
3327 rotom.moves[1] = {id = 'thundershock'}
3328 end
3329 end
3330 return {
3331 f = forgot,
3332 l = learned,
3333 t = tryLearn,
3334 k = tryLearn and rotom:getCurrentMovesData() or nil,
3335 n = rotom:getName(),
3336 r = forme==nil and true or false,
3337 }
3338end
3339
3340
3341-- Save/Load Data
3342do
3343 local indexToEvent = { -- !!! ALWAYS add new keys to the END of the list !!!
3344 'MeetJake',
3345 'MeetParents',
3346 'ChooseFirstPokemon',
3347 'JakeBattle1',
3348 'PCPorygonEncountered',
3349 'ParentsKidnappedScene',
3350 'BronzeBrickStolen',
3351 'JakeTracksLinda',
3352 'BronzeBrickRecovered',
3353 'IntroducedToGym1', -- 10
3354 'GivenSawsbuckCoffee',
3355 'ReceivedRTD',
3356 'EeveeAwarded',
3357 'RunningShoesGiven',
3358 'GroudonScene',
3359 'JakeBattle2',
3360 'TalkToJakeAndSebastian',
3361 'IntroToUMV',
3362 'TestDriveUMV',
3363 'ReceivedBWEgg', -- 20
3364 'DamBusted',
3365 'JakeStartFollow',
3366 'JakeEndFollow',
3367 'GivenSnover',
3368 'KingsRockGiven',
3369 'RosecoveWelcome',
3370 'LighthouseScene',
3371 'ProfAfterGym3',
3372 'JakeAndTessDepart',
3373 'RotomBit0', -- 30
3374 'RotomBit1',
3375 'RotomBit2',
3376 'JTBattlesR9',
3377 'GivenLeftovers',
3378 'Jirachi',
3379 'MeetAbsol',
3380 'ReachCliffPC',
3381 'BlimpwJT',
3382 'MeetGerald',
3383 'G4FoundTape', -- 40
3384 'G4GaveTape',
3385 'G4FoundWrench',
3386 'G4GaveWrench',
3387 'G4FoundHammer',
3388 'G4GaveHammer',
3389 'SeeTEship',
3390 'GeraldKey',
3391 'TessStartFollow',
3392 'TessEndFollow',
3393 'DefeatTEinAC', -- 50
3394 'EnteredPast',
3395 'LearnAboutSanta',
3396 'BeatSanta',
3397 'NiceListReward',
3398 'G5Shovel',
3399 'G5Pickaxe',
3400 'Shaymin',
3401 'RJO', -- red jewel obtained
3402 'RJP', -- red jewel placed
3403 'GJO', -- etc. -- 60
3404 'GJP',
3405 'PJO',
3406 'PJP',
3407 'BJO',
3408 'BJP',
3409 'Victini',
3410 'TEinCastle',
3411 'Snorlax',
3412 'GiveEkans',
3413 'vAredia', -- 70
3414 'gsEkans',
3415 'RNatureForces',
3416 'Landorus',
3417 'Heatran',
3418 'OpenJDoor',
3419 'Diancie',
3420 'FluoDebriefing',
3421 'vFluoruma',
3422 'TERt14',
3423 'RBeastTrio', -- 80
3424 'PBSIntro',
3425 'hasHoverboard',
3426 'Eevee2Awarded',
3427 --#newevent
3428 -- 1023 max (overkill)
3429 }
3430 local div = ';'
3431 local div2 = '-'
3432 local pokemonDiv = ','
3433
3434 local CHAT = game:GetService('Chat')
3435
3436 function PlayerData:getContinueScreenInfo()
3437 local str = select(1, self:getSaveData())
3438 if not str then return false end
3439
3440 local ndiv = '([^'..div..']*)'
3441 local basic = str:match('^'..ndiv..div)
3442 local pokedex = ''
3443 local s = basic:find(div2, 1, true)
3444 if s then
3445 pokedex = basic:sub(s+1)
3446 basic = basic:sub(1, s-1)
3447 s = pokedex:find(div2, 1, true)
3448 if s then
3449 pokedex = pokedex:sub(1, s-1)
3450 end
3451 end
3452 local buffer = BitBuffer.Create()
3453 buffer:FromBase64(basic)
3454 local version = buffer:ReadUnsigned(6)
3455 local player = self.player
3456 local trainerName = buffer:ReadString()
3457 pcall(function() trainerName = CHAT:FilterStringAsync(trainerName, player, player) end)
3458 if trainerName == '' then trainerName = player.Name end
3459 local badges = 0
3460 for i = 1, 8 do
3461 if buffer:ReadBool() then
3462 badges = badges + 1
3463 end
3464 end
3465 local owned = 0
3466 buffer:FromBase64(pokedex)
3467 for _ = 1, pokedex:len()*3 do
3468 buffer:ReadBool()
3469 if buffer:ReadBool() then
3470 owned = owned + 1
3471 end
3472 end
3473 return true, trainerName, badges, owned
3474 end
3475
3476 function PlayerData:serialize(etc)
3477 if not self.gameBegan then error('attempt to save before game began') end
3478
3479 local saveString
3480 local buffer = BitBuffer.Create()
3481-- buffer:SetDebug(true)
3482
3483 -- basic data
3484 local version = 14
3485 buffer:WriteUnsigned(6, version)
3486 -- name
3487 buffer:WriteString(--[[etc.tName or]] self.trainerName)
3488 -- badges
3489 for i = 1, 8 do
3490 buffer:WriteBool(self.badges[i] and true or false)
3491 end
3492 -- money
3493 buffer:WriteUnsigned(24, math.min(self.money, MAX_MONEY))
3494 buffer:WriteUnsigned(14, math.min(self.bp, MAX_BP))
3495 -- completed events
3496 local maxEventIndex = 0
3497 for i = #indexToEvent, 1, -1 do
3498 if self.completedEvents[indexToEvent[i]] then
3499 maxEventIndex = i
3500 break
3501 end
3502 end
3503 buffer:WriteUnsigned(10, maxEventIndex)
3504 for i = 1, maxEventIndex do
3505 buffer:WriteBool(self.completedEvents[indexToEvent[i]] and true or false)
3506 end
3507 -- misc
3508 buffer:WriteBool(etc.expShareOn and true or false)
3509 buffer:WriteString(self.starterType or '')
3510 if etc.repel and etc.repel.steps and etc.repel.steps > 0 then
3511 buffer:WriteBool(true)
3512 buffer:WriteUnsigned(2, etc.repel.kind)
3513 buffer:WriteUnsigned(8, math.ceil(etc.repel.steps/2))
3514 else
3515 buffer:WriteBool(false)
3516 end
3517 buffer:WriteUnsigned(12, math.min(4095, self.lastDrifloonEncounterWeek))
3518 buffer:WriteUnsigned(15, math.min(32767, self.lastHoneyGivenDay))
3519 if self.honey then
3520 buffer:WriteBool(true)
3521 buffer:WriteFloat64(self.honey.slatheredAt)
3522 buffer:WriteString(self.honey.foe:serialize(true))
3523 else
3524 buffer:WriteBool(false)
3525 end
3526 -- day care
3527 buffer:WriteBool(self.daycare.manHasEgg and true or false)
3528 for i = 1, 2 do
3529 local poke = self.daycare.depositedPokemon[i]
3530 if poke then
3531 buffer:WriteBool(true)
3532 buffer:WriteString(poke:serialize(true))
3533 buffer:WriteUnsigned(7, poke.depositedLevel or poke.level)
3534 else
3535 buffer:WriteBool(false)
3536 break
3537 end
3538 end
3539 -- options
3540 buffer:WriteBool(etc.options.autosaveEnabled and true or false)
3541 buffer:WriteBool(etc.options.reduceGraphics and true or false)
3542 buffer:WriteFloat64(etc.options.lastUnstuckTick or 0.0)
3543 -- RO-Powers -- in v12 we remove RO-Powers from the regular save data
3544-- for g = 1, 6 do
3545-- local l = self:ROPowers_getPowerLevel(g)
3546-- if l > 0 then
3547-- buffer:WriteBool(true)
3548-- buffer:WriteBool(l == 2)
3549-- buffer:WriteFloat64(self:ROPowers_getTimePurchased(g))
3550-- else
3551-- buffer:WriteBool(false)
3552-- end
3553-- end
3554 buffer:WriteFloat64(self.lcht)
3555 buffer:WriteUnsigned(12, math.min(4095, self.lastTrubbishEncounterWeek))
3556 -- [[ Poke Ball Stamps
3557 buffer:WriteUnsigned(10, math.min(999, self.stampSpins))
3558 buffer:WriteUnsigned(10, #self.pbStamps)
3559 for _, stamp in pairs(self.pbStamps) do
3560 buffer:WriteUnsigned(4, stamp.sheet)
3561 buffer:WriteUnsigned(5, stamp.n)
3562 buffer:WriteUnsigned(5, stamp.color)
3563 buffer:WriteUnsigned(3, stamp.style)
3564 buffer:WriteUnsigned(7, math.min(99, stamp.quantity or 1))
3565 end--]]
3566 -- Hoverboards
3567 buffer:WriteString(self.currentHoverboard)
3568 buffer:WriteUnsigned(5, #self.ownedHoverboards)
3569 for _, h in ipairs(self.ownedHoverboards) do
3570 buffer:WriteString(h)
3571 end
3572 --
3573
3574
3575 saveString = buffer:ToBase64()
3576
3577 -- pokedex
3578 saveString = concatenate(saveString, div2, self.pokedex)
3579
3580 -- misc
3581 saveString = concatenate(saveString, div2, self.defeatedTrainers, div2, self.tms, div2, self.hms)
3582
3583 -- party
3584 saveString = concatenate(saveString, div)
3585 for i = 1, 6 do
3586 if self.party[i] then
3587 if i ~= 1 then saveString = concatenate(saveString, pokemonDiv) end
3588 saveString = concatenate(saveString, self.party[i]:serialize())
3589 end
3590 end
3591
3592 -- bag
3593 saveString = concatenate(saveString, div, self.obtainedItems, div2)
3594 buffer:Reset()
3595 local stuff = {}
3596 for i = 1, 5 do
3597 for _, bd in pairs(self.bag[i]) do
3598 if bd.quantity > 0 then
3599 table.insert(stuff, { bd.num, bd.quantity or 1 })
3600 end
3601 end
3602 end
3603 buffer:WriteUnsigned(10, #stuff)
3604 for _, item in pairs(stuff) do
3605 buffer:WriteUnsigned(10, item[1])
3606 buffer:WriteUnsigned(7, math.min(99, item[2]))
3607 end
3608 saveString = concatenate(saveString, buffer:ToBase64())
3609
3610 -- location
3611 saveString = concatenate(saveString, div)
3612 if context == 'adventure' then
3613 saveString = concatenate(saveString, etc.location)
3614 else
3615 saveString = concatenate(saveString, self.adventureLocationData)
3616 end
3617
3618-- if _p.debug then print(saveString:len(), ':', saveString) end
3619 return saveString
3620 end
3621
3622 function PlayerData:deserialize(str)
3623 if select(2, str:gsub(div, div)) ~= 3 then
3624 -- OVH report so that I am notified and can attempt a fix
3625 error('error (pd::ds): div count mismatch')
3626 end
3627 local etc = {}
3628 local ndiv = '([^'..div..']*)'
3629 local basic, party, bag, location = str:match('^'..string.rep(ndiv..div, 3)..ndiv)
3630 local s = basic:find(div2, 1, true)
3631 if s then
3632 self.pokedex = basic:sub(s+1)
3633 basic = basic:sub(1, s-1)
3634 s = self.pokedex:find(div2, 1, true)
3635 if s then
3636 self.defeatedTrainers = self.pokedex:sub(s+1)
3637 self.pokedex = self.pokedex:sub(1, s-1)
3638 s = self.defeatedTrainers:find(div2, 1, true)
3639 if s then
3640 self.tms = self.defeatedTrainers:sub(s+1)
3641 self.defeatedTrainers = self.defeatedTrainers:sub(1, s-1)
3642 s = self.tms:find(div2, 1, true)
3643 if s then
3644 self.hms = self.tms:sub(s+1)
3645 self.tms = self.tms:sub(1, s-1)
3646 end
3647 end
3648 end
3649 else
3650 print(basic, 'No pokedex data found')
3651 end
3652 etc.dTrainers = self.defeatedTrainers
3653-- if _p.debug then
3654-- print(str)
3655-- print('basic', basic)
3656-- print('pokedex', self.pokedex)
3657-- print('party', party)
3658-- print('bag', bag)
3659-- print('location', location)
3660-- end
3661 local buffer = BitBuffer.Create()
3662-- buffer:SetDebug(true)
3663
3664 -- basic data
3665 buffer:FromBase64(basic)
3666 local version = buffer:ReadUnsigned(6)
3667 -- name
3668 self.trainerName = buffer:ReadString()
3669 spawn(function()
3670 local player = self.player
3671 self.trainerName = CHAT:FilterStringAsync(self.trainerName, player, player)
3672 end)
3673 if self.trainerName == '' then self.trainerName = self.player.Name end
3674 etc.tName = self.trainerName
3675 -- badges
3676 local eb = {}
3677 for i = 1, 8 do
3678 if buffer:ReadBool() then
3679 self.badges[i] = true
3680 eb[tostring(i)] = true
3681 end
3682 end
3683 etc.badges = eb
3684 -- money
3685 self.money = buffer:ReadUnsigned(24)
3686 if version >= 3 then
3687 self.bp = buffer:ReadUnsigned(14)
3688 end
3689 -- completed events
3690 local maxEventIndex = buffer:ReadUnsigned(10)
3691 for i = 1, maxEventIndex do
3692 if buffer:ReadBool() then
3693 self.completedEvents[indexToEvent[i]] = true
3694 end
3695 end
3696 etc.completedEvents = Utilities.shallowcopy(self.completedEvents)
3697 -- misc
3698 if version >= 1 then
3699 etc.expShareOn = buffer:ReadBool()
3700 end
3701 if version >= 2 then
3702 self.starterType = buffer:ReadString()
3703 end
3704 if version >= 4 and buffer:ReadBool() then
3705 etc.repel = {}
3706 etc.repel.kind = buffer:ReadUnsigned(2)
3707 etc.repel.steps = buffer:ReadUnsigned(8) * 2
3708 local id = ({'repel', 'superrepel', 'maxrepel'})[etc.repel.kind]
3709 local more = self:getBagDataById(id, 1)
3710 if more and more.quantity and more.quantity > 0 then
3711 etc.repel.more = true
3712 end
3713 end
3714 if version >= 10 then
3715 self.lastDrifloonEncounterWeek = buffer:ReadUnsigned(12)
3716 self.lastHoneyGivenDay = buffer:ReadUnsigned(15)
3717 if buffer:ReadBool() then
3718 local honey = {}
3719 honey.slatheredAt = buffer:ReadFloat64()
3720 honey.foe = _f.ServerPokemon:deserialize(buffer:ReadString(), self)
3721 self.honey = honey
3722 end
3723 end
3724 -- day care
3725 if version >= 5 then
3726 self.daycare.manHasEgg = buffer:ReadBool()
3727 if self.daycare.manHasEgg then
3728 etc.dcEgg = true
3729 end
3730 for i = 1, 2 do
3731 if not buffer:ReadBool() then break end
3732 local poke = _f.ServerPokemon:deserialize(buffer:ReadString(), self)
3733 poke.depositedLevel = buffer:ReadUnsigned(7)
3734 self.daycare.depositedPokemon[i] = poke
3735 end
3736 end
3737 -- options
3738 if version >= 6 then
3739 etc.options = {}
3740 if buffer:ReadBool() then
3741 etc.options.autosaveEnabled = true
3742 end
3743 if buffer:ReadBool() then
3744 etc.options.reduceGraphics = true--_p.DataManager.useMobileGrass = true
3745-- _p.Menu.options:setLightingForReducedGraphics(true)
3746 end
3747 pcall(function()
3748 etc.options.lastUnstuckTick = buffer:ReadFloat64()
3749 end)
3750 end
3751 -- RO-Powers
3752 if version < 12 and version >= 7 then -- RO-Powers were added in v7, removed in v12
3753 for g = 1, (version>=9 and 6 or 3) do
3754 if buffer:ReadBool() then
3755 buffer:ReadUnsigned(65) -- skip past the data
3756-- local l = buffer:ReadBool() and 2 or 1
3757-- local t = math.min(buffer:ReadFloat64(), os.time()+RO_POWER_EFFECT_DURATION*2) -- in case they saved with an outrageously future purchase time (pre-OVH), limit it to two hours from load time
3758-- self:ROPowers_setTimePurchasedAndLevelForPower(g, t, l)
3759 end
3760 end
3761 end
3762 if version >= 8 then
3763 self.lcht = buffer:ReadFloat64()
3764 end
3765 if version >= 11 then
3766 self.lastTrubbishEncounterWeek = buffer:ReadUnsigned(12)
3767 end
3768 -- [[ Poke Ball Stamps
3769 if version >= 13 then
3770 self.stampSpins = buffer:ReadUnsigned(10)
3771 local pbStamps = {}
3772 for i = 1, buffer:ReadUnsigned(10) do
3773 local stamp = {}
3774 stamp.sheet = buffer:ReadUnsigned(4)
3775 stamp.n = buffer:ReadUnsigned(5)
3776 stamp.color = buffer:ReadUnsigned(5)
3777 stamp.style = buffer:ReadUnsigned(3)
3778 stamp.quantity = buffer:ReadUnsigned(7)
3779 stamp.id = _f.PBStamps:getStampId(stamp)
3780 pbStamps[i] = stamp
3781 end
3782 self.pbStamps = pbStamps
3783 end--]]
3784 -- Hoverboards
3785 if version >= 14 then
3786 self.currentHoverboard = buffer:ReadString()
3787 local oh = {}
3788 for i = 1, buffer:ReadUnsigned(5) do
3789 oh[i] = buffer:ReadString()
3790 end
3791 self.ownedHoverboards = oh
3792 end
3793 --
3794
3795 -- pokedex
3796 -- completed above
3797
3798 -- party
3799 local p = 1
3800 for s in party:gmatch('[^'..pokemonDiv..']+') do
3801 if s and s ~= '' then
3802 self.party[p] = _f.ServerPokemon:deserialize(s, self)
3803 p = p + 1
3804 end
3805 end
3806 if not self.party[1] then
3807 etc.newGameFlag = true -- indicates to hide Pokemon / Pokedex from the Menu
3808 end
3809
3810 -- bag
3811 if bag and bag ~= '' then
3812 local s = bag:find(div2, 1, true)
3813 if s then
3814 self.obtainedItems = bag:sub(1, s-1)
3815 bag = bag:sub(s+1)
3816 buffer:FromBase64(bag)
3817-- local items = {}
3818-- local toQuery = {}
3819 for _ = 1, buffer:ReadUnsigned(10) do
3820 local num = buffer:ReadUnsigned(10)
3821 local qty = buffer:ReadUnsigned(7)
3822 self:addBagItems({num = num, quantity = qty})
3823-- table.insert(items, {num, qty})
3824-- table.insert(toQuery, num)
3825 end
3826-- if #items > 0 then
3827-- _p.DataManager:getItemBundle(toQuery)
3828-- for _, i in pairs(items) do
3829-- local item = _f.DataService.fulfillRequest(nil, {'Items', i[1]})
3830-- self:addBagItems({ num = i[2], quantity = i[2] })
3831-- end
3832-- end
3833 end
3834 end
3835
3836 -- location
3837 if context == 'adventure' then
3838 etc.location = location
3839 else
3840 self.adventureLocationData = location
3841 end
3842 if #self.daycare.depositedPokemon > 0 then
3843 etc.daycareHasPokemon = true
3844 end
3845
3846 -- Misc
3847 -- Restore RO Powers
3848 self:ROPowers_restore()
3849
3850 -- Fix for Absol in Pokedex
3851 if self.completedEvents.EnteredPast then
3852 self:onOwnPokemon(359)
3853 end
3854
3855 -- Update Player Lists (and get dex count)
3856 self:updatePlayerListEntry(true)
3857
3858 -- Pseudo-events / Server-events
3859 if BitBuffer.GetBit(self.hms, 1) then
3860 etc.completedEvents.GetCut = true
3861 end
3862 if self:getBagDataById('oldrod', 5) then
3863 etc.completedEvents.GetOldRod = true
3864 end
3865 for k, v in pairs(_f.PlayerEvents) do
3866 if type(v) == 'table' and v.server then
3867 etc.completedEvents[k] = nil
3868 end
3869 end
3870 etc.rotom = self:getRotomEventLevel()
3871
3872 return etc
3873 end
3874end
3875
3876function PlayerData:getSaveData()
3877 if self.loadedData then
3878 return self.loadedData[1], self.loadedData[2]
3879 end
3880 local data, pcData
3881 while true do
3882 local s, d, p = _f.DataPersistence.LoadData(self.player)
3883 if s then
3884 data = d
3885 pcData = p
3886 break
3887 end
3888 wait(1.5)
3889 end
3890 self.loadedData = {data, pcData}
3891 return data, pcData
3892end
3893
3894function PlayerData:saveGame(etc)
3895 if not self.gameBegan or self.userId < 1 then return false end -- refuse to save guests' data
3896 -- todo: refuse during battle or trade?
3897 if not etc or type(etc) ~= 'table'
3898-- or type(etc.tName) ~= 'string'
3899 or type(etc.options) ~= 'table'
3900 or type(etc.options.lastUnstuckTick) ~= 'number'
3901 or (type(etc.location) ~= 'string' and _f.Context == 'adventure') -- location is not required in battle/trade contexts
3902 then
3903 print('BAD ETC FROM PLAYER '..self.player.Name)
3904 return false
3905 end
3906 local s, r = pcall(function() return self:serialize(etc) end)
3907 if not s then
3908 print(self.player.Name..' ENCOUNTERED ERROR DURING SERIALIZATION:')
3909 print(r)
3910 return false
3911 end
3912 local saveString = r
3913 s, r = pcall(function() return self:PC_serialize() end)
3914 if not s then
3915 print(self.player.Name..' ENCOUNTERED ERROR DURING PC SERIALIZATION:')
3916 print(r)
3917 return false
3918 end
3919 local pcString = r
3920 for _ = 1, 3 do
3921 s = _f.DataPersistence.SaveData(self.player, saveString, pcString)
3922 if s then
3923 self.lastSaveEtc = etc -- Use SPARINGLY and CAREFULLY. Currently used for autosaving items obtained during diving; also for PB Stamp Spinner.
3924 return true
3925 end
3926 wait(.1)
3927 end
3928 return false
3929end
3930
3931function PlayerData:getRotomEventLevel()
3932 local v = 0
3933 for i = 0, 2 do
3934 if self.completedEvents['RotomBit'..i] then
3935 v = v + 2^i
3936 end
3937 end
3938 return v
3939end
3940function PlayerData:setRotomEventLevel(v)
3941 for i = 2, 0, -1 do
3942 local p = 2^i
3943 if v >= p then
3944 v = v - p
3945 self.completedEvents['RotomBit'..i] = true
3946 else
3947 self.completedEvents['RotomBit'..i] = false
3948 end
3949 end
3950end
3951
3952
3953-- important for preventing data leaks
3954function PlayerData:destroy()
3955 for _, p in pairs(self.party) do
3956 p:destroy()
3957 end
3958 self.party = nil
3959 for _, p in pairs(self.daycare.depositedPokemon) do
3960 p:destroy()
3961 end
3962 self.daycare = nil
3963 if self.honey and self.hony.foe then
3964 self.honey.foe:destroy()
3965 end
3966 self.honey = nil
3967 pcall(function() self.pcSession:destroy() end)
3968 self.pcSession = nil
3969 pcall(function() self.mineSession:destroy() end)
3970 self.mineSession = nil
3971end
3972
3973
3974--// enter/leave connections //--
3975local players = game:GetService('Players')
3976players.ChildAdded:connect(onPlayerEnter)
3977for _, p in pairs(players:GetChildren()) do onPlayerEnter(p) end
3978players.ChildRemoved:connect(function()
3979 for player, data in pairs(PlayerDataByPlayer) do
3980 if not player or not player.Parent then
3981 PlayerDataByPlayer[player] = nil
3982 pcall(function() if data.gameBegan then data:ROPowers_save() end end)
3983 pcall(function() data:destroy() end)
3984 end
3985 end
3986end)
3987
3988
3989return PlayerDataByPlayer--PlayerData -- OVH is this what we want?