Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
pokemonbox.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2020 Twilight
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15*/
16
23#include "pokemonbox.h"
24#include "../../qmlownership.h"
25#include "../savefileexpanded.h"
26#include "../player/player.h"
28#include "../../savefile.h"
31#include <pse-db/pokemon.h>
32#include <pse-db/moves.h>
33#include <pse-db/names.h>
35#include <pse-db/types.h>
36#include <pse-common/random.h>
37//#include "../../../../../ui/window/mainwindow.h"
38
39#include <QtMath>
40#include <QQmlEngine>
41#include <QDebug>
42
44{
45 // Own this move slot in C++ so QML's GC can never free it. (Realises the old
46 // @TODO; QQmlEngine::setObjectOwnership is static and needs no engine.)
47 // parentMon is stored as a plain member, NOT a QObject parent, so this object
48 // is parentless and would otherwise default to JavaScriptOwnership the moment
49 // it's handed to QML (e.g. via movesAt()). See qmlownership.h / qt6-patterns.md.
50 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
51
52 this->parentMon = parentMon;
53
59
60 this->moveID = move;
61 this->pp = pp;
62 this->ppUp = ppUp;
63
64 if(move == 0) {
65 randomize();
66 // A freshly-created empty move slot starts with 0 PP-Ups. randomize() (above)
67 // assigns a RANDOM 0-3 ppUp; this resets it so a brand-new move is clean. The
68 // original code wrote `ppUp = 0`, which assigned the constructor PARAMETER
69 // (shadowing the member) -- a no-op, so new moves silently kept a random ppUp.
70 // Now writes the member. (clang-analyzer-deadcode.DeadStores; confirmed intended
71 // with Twilight 2026-06-22.) Set directly (no ppUpChanged()) -- we're in the
72 // ctor, matching the plain member writes above; nothing is connected yet.
73 this->ppUp = 0;
74 }
75}
76
78{
79 return MovesDB::inst()->getIndAt(QString::number(moveID));
80}
81
83{
84 var8 moveListSize = MovesDB::inst()->getStoreSize();
85
86 for(var8 i = 0; i < 4; i++) {
87 MoveDBEntry* moveData;
88
89 do
90 moveData = const_cast<MoveDBEntry*>(MovesDB::inst()->getIndAt(
91 QString::number(Random::inst()->rangeExclusive(0, moveListSize))));
92 while(moveData == nullptr || moveData->glitch == true);
93
94 moveID = moveData->ind;
96
99
100 pp = getMaxPP();
101 ppChanged();
102 }
103}
104
106{
107 ppUp = 3;
108 ppUpChanged();
109}
110
112{
113 if(ppUp < 3)
114 ppUp++;
115 ppUpChanged();
116}
117
119{
120 if(ppUp > 0)
121 ppUp--;
122 ppUpChanged();
123}
124
126{
127 ppUp = 0;
128 ppUpChanged();
129}
130
132{
133 var8 maxPP = getMaxPP();
134 if(maxPP == 0)
135 return false;
136
137 return pp >= maxPP;
138}
139
141{
142 auto moveData = toMove();
143 if(moveData == nullptr || !moveData->pp)
144 return 0;
145
146 var8 basePP = *moveData->pp;
147 var8 ppUps = ppUp;
148 var8 ppUpSteps = basePP / 5;
149
150 return basePP + (ppUpSteps * ppUps);
151}
152
154{
155 return ppUp >= 3;
156}
157
159{
160 return moveID == 0 || toMove() == nullptr || toMove()->glitch;
161}
162
164{
165 if(moveID == 0)
166 return "";
167 else if(isInvalid() || toMove()->type == "")
168 return "Glitch";
169 else
170 return toMove()->toType->readable;
171}
172
174{
175 if(!isInvalid() && pp > getMaxPP()) {
176 pp = getMaxPP();
177 ppChanged();
178 }
179}
180
182{
183 QVector<int> ret;
184
185 if(!parentMon->isValidBool())
186 return ret;
187
188 auto monData = parentMon->toData();
189
190 for(auto el : monData->toInitial) {
191 if(!ret.contains(el->ind))
192 ret.append(el->ind);
193 }
194
195 for(auto el : monData->moves) {
196 if(el->toMove != nullptr && !ret.contains(el->toMove->ind))
197 ret.append(el->toMove->ind);
198 }
199
200 for(auto el : monData->toTmHmMove) {
201 if(!ret.contains(el->ind))
202 ret.append(el->ind);
203 }
204
205 return ret;
206}
207
209{
210 QVector<int> ret = allValidMoves();
211
212 if(!parentMon->isValidBool())
213 return ret;
214
215 for(int i = 0 ; i < 4; i++) {
216 if(parentMon->moves[i]->moveID > 0)
217 ret.removeAll(parentMon->moves[i]->moveID);
218 }
219
220 return ret;
221}
222
224{
225 int count = 0;
226
227 for(int i = 0 ; i < 4; i++) {
228 if(parentMon->moves[i]->moveID == this->moveID)
229 count++;
230 }
231
232 return count > 1;
233}
234
236{
237 var8 maxPP = getMaxPP();
238 if(maxPP == 0)
239 return;
240
241 pp = maxPP;
242 ppChanged();
243}
244
245void PokemonMove::changeMove(int move, int pp, int ppUp)
246{
247 this->moveID = move;
249
250 this->pp = pp;
251 ppChanged();
252
253 this->ppUp = ppUp;
254 ppUpChanged();
255}
256
258{
259 // Skip if pokemon is a glitch mon
260 if(!parentMon->isValidBool())
261 return;
262
263 // Count number of non-zero move rows
264 int rowCount = 0;
265
266 for(int i = 0; i < 4; i++) {
267 if(parentMon->moves[i]->moveID > 0)
268 rowCount++;
269 }
270
271 // Stop here if this move is zero and there are other moves which aren't
272 if(rowCount >= 1 && moveID <= 0)
273 return;
274
275 auto validMoves = allValidMoves();
276
277 if(!validMoves.contains(moveID) || isDuplicateMove()) {
278 if(rowCount <= 1) {
279 auto validMovesLeftList = validMovesLeft();
280 moveID = validMovesLeftList.at(0);
282 return;
283 }
284
285 moveID = 0;
287 }
288}
289
291 var16 startOffset,
292 var16 nicknameStartOffset,
293 var16 otNameStartOffset,
294 var8 index,
295 var8 recordSize)
296{
297 // Own this mon in C++ for its container's lifetime so QML's GC can never free
298 // it. (Realises the old @TODO: it wanted MainWindow::engine, but
299 // QQmlEngine::setObjectOwnership is static and needs no engine.) Every mon --
300 // loaded from a save OR created fresh via this same default-arg ctor
301 // (new PokemonBox() in newPokemon()) -- is now self-protecting from birth.
302 // This closes the QML-GC use-after-free at the source instead of relying only
303 // on qmlCppOwned() at each accessor, which protects a mon only once it's been
304 // handed out and misses any other exposure path (symptom: open a stored or
305 // freshly-created mon's editor, back out, re-open -> intermittent crash in
306 // PokemonStorageModel::hasChecked()/data() reading a GC'd mon). The container
307 // still owns lifetime and frees mons via deleteLater(), so CppOwnership here
308 // introduces no leak/double-free. See qmlownership.h / qt6-patterns.md.
309 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
310
311 for(int i = 0; i < 4; i++) {
312 moves[i] = new PokemonMove(this);
313
314 //connect(moves[i], &PokemonMove::moveIDChanged, this, &PokemonBox::movesChanged);
316 }
317
320
322 connect(this, &PokemonBox::hpChanged, this, &PokemonBox::statChanged);
329 connect(this, &PokemonBox::dvChanged, this, &PokemonBox::statChanged);
330
333
336
337 connect(this, &PokemonBox::hpExpChanged, this, &PokemonBox::evChanged);
338 connect(this, &PokemonBox::atkExpChanged, this, &PokemonBox::evChanged);
339 connect(this, &PokemonBox::defExpChanged, this, &PokemonBox::evChanged);
340 connect(this, &PokemonBox::spdExpChanged, this, &PokemonBox::evChanged);
341 connect(this, &PokemonBox::spExpChanged, this, &PokemonBox::evChanged);
342
344
345 load(saveFile,
346 startOffset,
347 nicknameStartOffset,
348 otNameStartOffset,
349 index,
350 recordSize);
351}
352
354 for(int i = 0; i < 4; i++) {
355 moves[i]->deleteLater();
356 }
357}
358
360{
361 PokemonDBEntry* pkmnData;
362
363 if(list == PokemonRandom::Random_All) {
364 auto listSize = PokemonDB::inst()->getStoreSize();
365 var8 ind = Random::inst()->rangeExclusive(0, listSize);
366 pkmnData = PokemonDB::inst()->getStoreAt(ind);
367 }
368 else if(list == PokemonRandom::Random_Pokedex) {
369 // The "dexN" keys are 0-based (dex0 = Bulbasaur .. dex150 = Mew; there is no
370 // dex151). rangeExclusive(0, 151) -> [0,150] covers all 151 species. Was
371 // rangeExclusive(1, ...), which silently made Bulbasaur (dex0) unreachable.
373 pkmnData = PokemonDB::inst()->getIndAt("dex" + QString::number(dex));
374 }
375 else if(list == PokemonRandom::Random_Starters)
377 else
379
380 return newPokemon(pkmnData, basics);
381}
382
384{
385 auto pkmn = new PokemonBox();
386
387 pkmn->species = pkmnData->ind;
388 pkmn->level = Random::inst()->rangeInclusive(5, 8);
389 pkmn->reRollDVs();
390
391 // Randomly give a nickanme or not
392 bool noNick = Random::inst()->flipCoin();
393 pkmn->changeName(noNick);
394
395 // 10% chance of it being a traded pokemon
396 bool isTrade = Random::inst()->chanceSuccess(10);
397
398 if(basics != nullptr && !isTrade)
399 pkmn->changeTrade(true, basics);
400 else
401 pkmn->changeTrade();
402
403 pkmn->resetPokemon();
404 pkmn->update(true, true, true, true);
405
406 return pkmn;
407}
408
410 var16 startOffset,
411 var16 nicknameStartOffset,
412 var16 otNameStartOffset,
413 var8 index,
414 var8 recordSize)
415{
416 reset();
417
418 if(saveFile == nullptr) {
419 return nullptr;
420 }
421
422 // Calculate record offset
423 var16 offset = (recordSize * index) + startOffset;
424
425 auto toolset = saveFile->toolset;
426 auto it = saveFile->iterator()->offsetTo(offset);
427
428 species = it->getByte();
430
431 hp = it->getWord();
432 hpChanged();
433
434 level = it->getByte();
435 levelChanged();
436
437 status = it->getByte();
439
440 type1 = it->getByte();
441 type1Changed();
442
443 type2 = it->getByte();
444 type2Changed();
445
446 // Don't duplicate type 1 to type 2, fill type 2 only if it's different
447 // Also mark if it was explicitly marked no in-game
448 if (type2 == 0xFF) {
449 type2Explicit = true;
451 } else if (type1 == type2) {
452 type2 = 0xFF;
453 type2Changed();
454 }
455
456 catchRate = it->getByte();
458
459 // Save offset to restore later
460 it->push();
461
462 // Temporarily save moves for later
463 // PP data which is important to moves has to be be gotten later
464 QVector<var8> moveIDList;
465 for (var8 i = 0; i < 4; i++) {
466 var8 moveID = it->getByte();
467 if(moveID == 0)
468 break;
469
470 moveIDList.append(moveID);
471 }
472
473 // Restore offset to start of moves and move past the moves
474 it->pop()->offsetBy(0x4);
475
476 otID = it->getWord();
477 otIDChanged();
478
479 // Exp is 3 bytes so it's a bit tricky
480 auto expRaw = it->getRange(3);
481 exp = expRaw[0];
482 exp <<= 8;
483 exp |= expRaw[1];
484 exp <<= 8;
485 exp |= expRaw[2];
486 expChanged();
487
488 hpExp = it->getWord();
489 hpExpChanged();
490
491 atkExp = it->getWord();
493
494 defExp = it->getWord();
496
497 spdExp = it->getWord();
499
500 spExp = it->getWord();
501 spExpChanged();
502
503 var16 dvTotal = it->getWord();
504 dv[(var8)PokemonStats::Attack] = (dvTotal & 0xF000) >> 12;
505 dv[(var8)PokemonStats::Defense] = (dvTotal & 0x0F00) >> 8;
506 dv[(var8)PokemonStats::Speed] = (dvTotal & 0x00F0) >> 4;
507 dv[(var8)PokemonStats::Special] = dvTotal & 0x000F;
508 dvChanged();
509
510 it->push();
511
512 // Next gather PP
513 QVector<var8> ppList;
514 for (var8 i = 0; i < moveIDList.size(); i++) {
515 var8 ppListEntry = it->getByte();
516 ppList.append(ppListEntry);
517 }
518
519 // Combine together in moves from earlier
520 for (var8 i = 0; i < moveIDList.size(); i++) {
521 var8 moveID = moveIDList.at(i);
522 var8 pp = ppList[i];
523 moves[i]->moveID = moveID;
524 moves[i]->moveIDChanged();
525
526 moves[i]->pp = pp & 0b00111111;
527 moves[i]->ppChanged();
528
529 moves[i]->ppUp = (pp & 0b11000000) >> 6;
530 moves[i]->ppUpChanged();
531 }
532 movesChanged();
533
534 // Restore back to before PP and move past PP
535 it->pop()->offsetBy(0x4);
536
537 // Now we must gather the OT names and Pokemon names whihc were poorly
538 // implemented in sometimes arbitrary spots outside of the data sructure
539 var16 otNameOffset = (index * 0xB) + otNameStartOffset;
540 otName = toolset->getStr(otNameOffset, 0xB, 7+1);
542
543 var16 nicknameOffset = (index * 0xB) + nicknameStartOffset;
544 nickname = toolset->getStr(nicknameOffset, 0xB, 10);
546
547 // Save the iterator to be picked up by sub-class if present
548 return it;
549}
550
552 var16 startOffset,
553 svar32 speciesStartOffset,
554 var16 nicknameStartOffset,
555 var16 otNameStartOffset,
556 var8 index,
557 var8 recordSize)
558{
559 auto toolset = saveFile->toolset;
560
561 // Retrieve stored internals
562 var16 offset = (recordSize * index) + startOffset;
563 auto it = saveFile->iterator()->offsetTo(offset);
564 var16 otNameOffset = (index * 0xB) + otNameStartOffset;
565 var16 nicknameOffset = (index * 0xB) + nicknameStartOffset;
566
567 // Add species to species list if exists
568 if(speciesStartOffset > 0) {
569 var16 speciesOffset = index + speciesStartOffset;
570 toolset->setByte(speciesOffset, species);
571 }
572
573 // Re-save back
574 it->setByte(species);
575 it->setWord(hp);
576
577 // Don't save level to BoxData if this is in the party
578 // This honors the global don't touch policy
579 // which is don't touch any bits that don't need to be changed
580 if (recordSize == 0x21) {
581 it->setByte(level);
582 } else {
583 it->inc();
584 }
585
586 it->setByte(status);
587 it->setByte(type1);
588
589 // If type 2 explicit no then just write what's in type 2
590 if (type2Explicit) {
591 it->setByte(type2);
592
593 // If type 2 is not explicitly no but implicitly no (Type 1 and 2 were marked the same)
594 // save it as type 1
595 } else if (type2 == 0xFF) {
596 it->setByte(type1);
597
598 // Else just save type 2
599 } else {
600 it->setByte(type2);
601 }
602
603 it->setByte(catchRate);
604
605 it->push();
606 for(var8 i = 0; i < 4; i++) {
607 it->setByte(moves[i]->moveID);
608 }
609 it->pop()->offsetBy(4);
610
611 it->setWord(otID);
612
613 it->setByte((exp & 0xFF0000) >> 16);
614 it->setByte((exp & 0x00FF00) >> 8);
615 it->setByte(exp & 0x0000FF);
616
617 it->setWord(hpExp);
618 it->setWord(atkExp);
619 it->setWord(defExp);
620 it->setWord(spdExp);
621 it->setWord(spExp);
622
623 var16 dvTmp = 0;
624 dvTmp |= (dv[(var8)PokemonStats::Attack] << 12);
625 dvTmp |= (dv[(var8)PokemonStats::Defense] << 8);
626 dvTmp |= (dv[(var8)PokemonStats::Speed] << 4);
627 dvTmp |= dv[(var8)PokemonStats::Special];
628 it->setWord(dvTmp);
629
630 it->push();
631 for (var8 i = 0; i < 4; i++) {
632 var8 ppCombined = (moves[i]->ppUp << 6) | moves[i]->pp;
633 it->setByte(ppCombined);
634 }
635 it->pop()->offsetBy(4);
636
637 toolset->setStr(otNameOffset, 0xB, 10+1, otName);
638 toolset->setStr(nicknameOffset, 0xB, 10+1, nickname);
639
640 return it;
641}
642
644{
645 species = 0;
647
648 hp = 0;
649 hpChanged();
650
651 level = 0;
652 levelChanged();
653
654 status = 0;
656
657 type1 = 0;
658 type1Changed();
659
660 type2 = 0;
661 type2Changed();
662
663 catchRate = 0;
665
666 otID = 0;
667 otIDChanged();
668
669 exp = 0;
670 expChanged();
671
672 hpExp = 0;
673 hpExpChanged();
674
675 atkExp = 0;
677
678 defExp = 0;
680
681 spdExp = 0;
683
684 spExp = 0;
685 spExpChanged();
686
687 otName = "";
689
690 nickname = "";
692
693 dv[0] = 0;
694 dv[1] = 0;
695 dv[2] = 0;
696 dv[3] = 0;
697 dvChanged();
698
699 clearMoves();
700
701 type2Explicit = false;
703}
704
706{
707 reset();
708
709 // Generate a random level 5 Pokemon from the pokedex
710 // Bump it's level up to a random value
711 // Give it a random name, otName, and otID
712 // Then fix all of it's types, exp, stats, etc.. to be game accurate
714 copyFrom(pkmn);
715 pkmn->deleteLater();
716
718 levelChanged();
719
720 atkExp = Random::inst()->rangeInclusive(0, 0xFFFF);
722
723 defExp = Random::inst()->rangeInclusive(0, 0xFFFF);
725
726 spdExp = Random::inst()->rangeInclusive(0, 0xFFFF);
728
729 spExp = Random::inst()->rangeInclusive(0, 0xFFFF);
730 spExpChanged();
731
732 hpExp = Random::inst()->rangeInclusive(0, 0xFFFF);
733 hpExpChanged();
734
735 // Delete it's moves and re-create 4 new non-glitch random moves
737
738 // Make the pokemon a non-trade pokemon
739 changeTrade(true, basics);
740
741 // 50/50 chance of not having a nickname
742 // If true, removes nickname
743 // If false, assigns random nickname
744 bool noNick = Random::inst()->flipCoin();
745 changeName(noNick);
746
747 // This is where we make the Pokemon completely game accurate
748 update(true, true, true, true);
749
750 // Heal the Pokemon
751 heal();
752
753 // This is where we give the Pokemon whacky types for fun
754 // We have to do this after all the code above otherwise it'll be re-corrected
755 // to be game accurate
756 auto type1 = TypesDB::inst()->getStoreAt(Random::inst()->rangeExclusive(0, TypesDB::inst()->getStoreSize()));
757 TypeDBEntry* type2 = nullptr;
758
759 // 25% chance of type 2
760 bool hasType2 = Random::inst()->chanceSuccess(25);
761 if(hasType2) {
762 type2 = TypesDB::inst()->getStoreAt(Random::inst()->rangeExclusive(0, TypesDB::inst()->getStoreSize()));
763
764 if(type1->ind == type2->ind)
765 type2 = nullptr;
766 }
767
768 this->type1 = type1->ind;
769 type1Changed();
770
771 if(type2 != nullptr)
772 this->type2 = type2->ind;
773 else
774 this->type2 = 0xFF;
775 type2Changed();
776}
777
779{
780 for(int i = 0; i < 4; i++) {
781 moves[i]->moveID = 0;
782 moves[i]->moveIDChanged();
783
784 moves[i]->pp = 0;
785 moves[i]->ppChanged();
786
787 moves[i]->ppUp = 0;
788 moves[i]->ppUpChanged();
789 }
790
791 movesChanged();
792}
793
794// Is this a valid Pokemon? (Is it even in the Pokedex?)
795// If not returns false, otherwise returns Pokemon Record
797{
798 // Get Pokemon Record
799 // The Pokemon Array is organized by species ID with 1 top entry missing
800 // thus offset by 1 accordingly
801 auto record = PokemonDB::inst()->getIndAt(QString::number(species));
802
803 // Check it's a valid Pokemon (not glitch)
804 if(record == nullptr || record->glitch || !(record->pokedex))
805 return nullptr;
806
807 return record;
808}
809
811{
812 return isValid() != nullptr;
813}
814
816{
817 auto record = isValid();
818 double exp = 0;
819
820 if(level < 0)
821 level = this->level;
822
823 // Proceed only if it's valid
824 if(record == nullptr)
825 return exp;
826
827 // Obtain it's growth rate and calculate accordingly it's exp for the given level
828 var8 gr = *record->growthRate;
829
830 // Growth Rate 0: Medium Fast
831 if(gr == 0)
832 exp = qPow(level, 3);
833
834 // Growth Rate 3: Medium Slow
835 else if(gr == 3)
836 exp = (1.2 * qPow(level, 3)) - (15 * qPow(level, 2)) + (100*level) - 140;
837
838 // Growth Rate 4: Fast
839 else if(gr == 4)
840 exp = (4 * qPow(level, 3)) / 5;
841
842 // Growth Rate 5: Slow
843 else if(gr == 5)
844 exp = (5 * qPow(level, 3)) / 4;
845
846 // Return EXP
847 return qFloor(exp);
848}
849
851{
852 if(isValid() == nullptr)
853 return this->exp;
854
855 return levelToExp((level < 100) ? level : 100);
856}
857
859{
860 if(isValid() == nullptr)
861 return exp;
862
863 return levelToExp((level < 100) ? level + 1 : 100) - 1;
864}
865
867{
868 if(this->isValid() == nullptr)
869 return 0;
870
871 if(level >= 100)
872 return 1;
873
874 var32 curExp = exp - expLevelRangeStart();
876
877 // Return percentage. Both operands are var32, so the previous `curExp / expEnd`
878 // was an INTEGER division truncated to 0 (or 1 at the very top of the level)
879 // before being widened to the float return -- the fractional percent was always
880 // lost. Divide in floating point, and guard a zero-width range (degenerate exp
881 // data) against divide-by-zero. Found by clang-tidy (bugprone-integer-division +
882 // clang-analyzer-core.DivideZero); see notes/reference/fix-patterns.md.
883 if(expEnd == 0)
884 return 0;
885 return static_cast<float>(curExp) / static_cast<float>(expEnd);
886}
887
889{
890 if(isValid() == nullptr)
891 return;
892
894 expChanged();
895}
896
898{
899 var8 hpDv = 0;
900
901 if((dv[(var8)PokemonStats::Attack] % 2) != 0)
902 hpDv |= 8;
903
904 if((dv[(var8)PokemonStats::Defense] % 2) != 0)
905 hpDv |= 4;
906
907 if((dv[(var8)PokemonStats::Speed] % 2) != 0)
908 hpDv |= 2;
909
910 if((dv[(var8)PokemonStats::Special] % 2) != 0)
911 hpDv |= 1;
912
913 return hpDv;
914}
915
917{
918 auto record = isValid();
919
920 // Proceed only if it's valid
921 if(record == nullptr || !(record->baseHp))
922 return 1;
923
924 return qFloor((((*record->baseHp + hpDV())*2+qFloor(qFloor(qSqrt(hpExp))/4))*level)/100) + level + 10;
925}
926
928{
929 auto record = isValid();
930
931 // Proceed only if it's valid
932 if(record == nullptr)
933 return 0;
934
935 int baseStat = 0;
936 int dvLocal = 0;
937 int evLocal = 0;
938
939 if(stat == PokemonStats::Attack) {
940 baseStat = *record->baseAttack;
941 dvLocal = dv[PokemonStats::Attack];
942 evLocal = atkExp;
943 }
944 else if(stat == PokemonStats::Defense) {
945 baseStat = *record->baseDefense;
946 dvLocal = dv[PokemonStats::Defense];
947 evLocal = defExp;
948 }
949 else if(stat == PokemonStats::Speed) {
950 baseStat = *record->baseSpeed;
951 dvLocal = dv[PokemonStats::Speed];
952 evLocal = spdExp;
953 }
954 else if(stat == PokemonStats::Special) {
955 baseStat = *record->baseSpecial;
956 dvLocal = dv[PokemonStats::Special];
957 evLocal = spExp;
958 }
959
960 return qFloor((((baseStat+dvLocal)*2+qFloor(qFloor(qSqrt(evLocal))/4))*level)/100) + 5;
961}
962
963void PokemonBox::update(bool resetHp,
964 bool resetExp,
965 bool resetType,
966 bool resetCatchRate,
967 bool correctMoves)
968{
969 auto record = isValid();
970 if(record == nullptr)
971 return;
972
973 if(resetHp) {
974 hp = hpStat();
975 hpChanged();
976 }
977
978 if(resetType && record->toType1) {
979 type1 = (*record).toType1->ind;
980 type1Changed();
981 }
982
983 // Only (re)derive type2 when explicitly asked. The previous code's bare `else`
984 // ran on EVERY update() called with resetType=false and overwrote type2 with
985 // type1 -- silently dropping a dual-type mon's second type (reachable via
986 // maxLevel()/maxEVs()/resetEVs()/reRollEVs()/manualLevelChanged()). Now type2
987 // is left untouched unless resetType is set.
988 if(resetType) {
989 if(record->toType2)
990 type2 = (*record).toType2->ind;
991 else if(record->toType1) // guard toType1 (matches the resetType-&&-toType1
992 type2 = (*record).toType1->ind; // check above) -- avoid a null deref on a
993 // record with neither type resolved.
994 // Found by clang-analyzer-core.NullDereference.
995
996 // A single type (no distinct second type) is stored internally as 0xFF.
997 if(type1 == type2)
998 type2 = 0xFF;
999
1000 type2Changed();
1001 }
1002
1003 if(resetCatchRate && record->catchRate) {
1004 catchRate = *record->catchRate;
1006 }
1007
1008 if(resetExp) {
1009 exp = levelToExp();
1010 expChanged();
1011 }
1012
1013 if(correctMoves) {
1014 this->correctMoves();
1015 cleanupMoves();
1016 }
1017}
1018
1019// Reset the typing to the species' DB defaults. Mirrors update()'s resetType block
1020// but standalone, so QML can call it without the unreliable multi-bool update() call
1021// (see the header note). No-ops on a glitch species (no DB record / unlinked type).
1023{
1024 auto record = isValid();
1025 if(record == nullptr || !record->toType1)
1026 return;
1027
1028 type1 = record->toType1->ind;
1029 type1Changed();
1030
1031 if(record->toType2)
1032 type2 = record->toType2->ind;
1033 else
1034 type2 = record->toType1->ind;
1035
1036 // A single type (no distinct second type) is stored internally as 0xFF.
1037 if(type1 == type2)
1038 type2 = 0xFF;
1039
1040 type2Changed();
1041}
1042
1044{
1045 return isMaxHp() && !isAfflicted() && isMaxPP();
1046}
1047
1049{
1050 return status > 0;
1051}
1052
1054{
1055 if(!isValid())
1056 return false;
1057
1058 return hp == hpStat();
1059}
1060
1062{
1063 hp = hpStat();
1064 hpChanged();
1065
1066 status = 0;
1067 statusChanged();
1068
1069 for(int i = 0; i < 4; i++)
1070 moves[i]->restorePP();
1071}
1072
1074{
1075 auto record = isValid();
1076
1077 if(record == nullptr)
1078 return false;
1079
1080 return record->name != nickname;
1081}
1082
1084{
1085 return basics->playerName != otName || basics->playerID != otID;
1086}
1087
1088void PokemonBox::changeName(bool removeNickname)
1089{
1090 if(!removeNickname)
1092 else if(removeNickname)
1093 nickname = toData()->name;
1094
1096}
1097
1098void PokemonBox::changeOtData(bool removeOtData, PlayerBasics* basics)
1099{
1100 // Randomize OT (give it "traded" status): always a real change, always emit.
1101 if(!removeOtData) {
1103 otID = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1104 otNameChanged();
1105 otIDChanged();
1106 return;
1107 }
1108
1109 // Adopt the player's OT (remove "traded" status). Need the player's data.
1110 if(basics == nullptr)
1111 return;
1112
1113 // Idempotent: only touch a field (and emit) if it actually differs. Keeps the
1114 // owned-mon OT sync from firing a storm of no-op change signals, and keeps us
1115 // from rewriting OT bytes that didn't need to change.
1116 if(otName != basics->playerName) {
1117 otName = basics->playerName;
1118 otNameChanged();
1119 }
1120
1121 if(otID != basics->playerID) {
1122 otID = basics->playerID;
1123 otIDChanged();
1124 }
1125}
1126
1127void PokemonBox::changeTrade(bool removeTradeStatus, PlayerBasics* basics)
1128{
1129 changeName(removeTradeStatus);
1130 changeOtData(removeTradeStatus, basics);
1131}
1132
1134{
1135 auto record = isValid();
1136
1137 if(record == nullptr)
1138 return false;
1139
1140 if(record->evolution.size() == 0)
1141 return false;
1142
1143 return true;
1144}
1145
1147{
1148 auto record = isValid();
1149
1150 if(record == nullptr)
1151 return false;
1152
1153 if(record->toDeEvolution == nullptr)
1154 return false;
1155
1156 return true;
1157}
1158
1160{
1161 auto record = isValid();
1162
1163 if(!hasEvolution())
1164 return;
1165
1166 // Does it have a nickname before evolution
1167 bool nickStatus = hasNickname();
1168
1169 // For Eevee evolutions, randomly pick one
1170 if(record->evolution.size() > 1) {
1171 var8 ind = Random::inst()->rangeExclusive(0, record->evolution.size());
1172 species = record->evolution.at(ind)->toEvolution->ind;
1173 }
1174 else
1175 species = record->evolution.at(0)->toEvolution->ind;
1176
1178
1179 // Update name if no nickname
1180 if(!nickStatus)
1181 changeName(true);
1182
1183 // Update all stats, make everything else game accurate
1184 update(true, true, true, true);
1185}
1186
1188{
1189 auto record = isValid();
1190
1191 if(!hasDeEvolution())
1192 return;
1193
1194 // Does it have a nickname before de-evolution
1195 bool nickStatus = hasNickname();
1196
1197 species = record->toDeEvolution->ind;
1199
1200 // Update name if no nickname
1201 if(!nickStatus)
1202 changeName(true);
1203
1204 // Update all stats, make everything game accurate
1205 update(true, true, true, true);
1206
1207 // As for moves, given this is made-up territory, I'm going with evolution
1208 // rules and saying the Pokemon can keep the evolved moves because it's the
1209 // same Pokemon that's reverted to a younger self and has the same memory.
1210}
1211
1213{
1214 return level >= 100;
1215}
1216
1218{
1219 bool ret = true;
1220
1221 // Empty slots (moveID 0) hold no move and have no PP, so they must NOT count as
1222 // "not max PP" -- otherwise any mon with fewer than 4 moves can never read as
1223 // max-PP, and therefore never as isHealed() (a user-facing wrong result on the
1224 // heal indicator). Mirrors isMaxedOut()'s existing moveID>0 guard. (2026-06-08.)
1225 for(int i = 0; i < 4; i++)
1226 if(moves[i]->moveID > 0 && !moves[i]->isMaxPP())
1227 ret = false;
1228
1229 return ret;
1230}
1231
1233{
1234 bool ret = true;
1235
1236 // Same empty-slot guard as isMaxPP(): an empty slot has no PP-Ups to max.
1237 for(int i = 0; i < 4; i++)
1238 if(moves[i]->moveID > 0 && !moves[i]->isMaxPpUps())
1239 ret = false;
1240
1241 return ret;
1242}
1243
1245{
1246 return atkExp == 0xFFFF &&
1247 defExp == 0xFFFF &&
1248 spdExp == 0xFFFF &&
1249 spExp == 0xFFFF &&
1250 hpExp == 0xFFFF;
1251}
1252
1254{
1255 // "Minimum EVs" means ALL five stat-exp are zero (symmetric with isMaxEVs()'s
1256 // all-0xFFFF). Was `||` (true if ANY one was 0), which wrongly disabled the
1257 // "Reset EVs" UI action whenever a single stat-exp happened to be 0. (Fixed
1258 // 2026-06-08, Twilight-confirmed.)
1259 return atkExp == 0 &&
1260 defExp == 0 &&
1261 spdExp == 0 &&
1262 spExp == 0 &&
1263 hpExp == 0;
1264}
1265
1267{
1268 bool ret = true;
1269
1270 for(var8 i = 0; i < 4; i++)
1271 if(dv[i] < 15) ret = false;
1272
1273 return ret;
1274}
1275
1277{
1278 bool ret = true;
1279
1280 for(var8 i = 0; i < 4; i++)
1281 if(dv[i] > 0) ret = false;
1282
1283 return ret;
1284}
1285
1287{
1288 level = 100;
1289 levelChanged();
1290
1291 update(true, true);
1292}
1293
1295{
1296 for(int i = 0; i < 4; i++)
1297 moves[i]->maxPpUp();
1298}
1299
1301{
1302 for(var8 i = 0; i < 4; i++)
1303 dv[i] = 15;
1304
1305 dvChanged();
1306}
1307
1309{
1310 for(var8 i = 0; i < 4; i++)
1311 dv[i] = Random::inst()->rangeInclusive(0, 15);
1312
1313 dvChanged();
1314}
1315
1317{
1318 for(var8 i = 0; i < 4; i++)
1319 dv[i] = 0;
1320
1321 dvChanged();
1322}
1323
1325{
1326 hpExp = 0xFFFF;
1327 hpExpChanged();
1328
1329 atkExp = 0xFFFF;
1330 atkExpChanged();
1331
1332 defExp = 0xFFFF;
1333 defExpChanged();
1334
1335 spdExp = 0xFFFF;
1336 spdExpChanged();
1337
1338 spExp = 0xFFFF;
1339 spExpChanged();
1340
1341 update(true);
1342}
1343
1345{
1346 hpExp = 0;
1347 hpExpChanged();
1348
1349 atkExp = 0;
1350 atkExpChanged();
1351
1352 defExp = 0;
1353 defExpChanged();
1354
1355 spdExp = 0;
1356 spdExpChanged();
1357
1358 spExp = 0;
1359 spExpChanged();
1360
1361 update(true);
1362}
1363
1365{
1366 hpExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1367 hpExpChanged();
1368
1369 atkExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1370 atkExpChanged();
1371
1372 defExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1373 defExpChanged();
1374
1375 spdExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1376 spdExpChanged();
1377
1378 spExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1379 spExpChanged();
1380
1381 update(true);
1382}
1383
1385{
1386 maxLevel();
1387 maxPpUps();
1388 maxEVs();
1389 maxDVs();
1390 heal();
1391
1392 update(true, true);
1393}
1394
1396{
1397 clearMoves();
1398
1399 for(var8 i = 0; i < 4; i++) {
1400 moves[i]->randomize();
1401 }
1402
1403 movesChanged();
1404}
1405
1407{
1408 auto record = isValid();
1409
1410 if(record == nullptr)
1411 return false;
1412
1413 bool movesReset = true;
1414
1415 // A reset mon (see resetPokemon()) carries exactly the species' initial moves,
1416 // each at base PP with 0 PP-Ups, and empty slots beyond them. The old loop
1417 // checked all four slots against toInitial.at(i) (out-of-range for species with
1418 // <4 initial moves -- saved only by the toMove()==null early-out, which also
1419 // forced "not reset"), and required isMaxPpUps() (3) when a reset mon actually
1420 // has 0 PP-Ups. Iterate only the real initial moves; require empty slots after.
1421 int initialCount = record->toInitial.size();
1422 if(initialCount > 4)
1423 initialCount = 4;
1424
1425 for(int i = 0; i < 4; i++) {
1426 auto move = moves[i];
1427
1428 // Slots past the species' initial-move list must be empty.
1429 if(i >= initialCount) {
1430 if(move->moveID != 0)
1431 movesReset = false;
1432 if(!movesReset)
1433 break;
1434 continue;
1435 }
1436
1437 if(move->toMove() == nullptr)
1438 movesReset = false;
1439 if(!movesReset)
1440 break;
1441
1442 if(move->moveID != record->toInitial.at(i)->ind)
1443 movesReset = false;
1444 if(move->ppUp != 0) // resetPokemon leaves PP-Ups at 0
1445 movesReset = false;
1446
1447 if(!movesReset)
1448 break;
1449 }
1450
1451 // isHealed() (full HP + no status + max PP) is now correct for any move count
1452 // because isMaxPP() skips empty slots; PP/HP/status are covered there, so here
1453 // we only need the level, the initial-move match, and zeroed EVs.
1454 return level == 5 && movesReset && isMinEvs() && isHealed();
1455}
1456
1458{
1459 if(level < 100)
1460 return false;
1461
1462 for(int i = 0; i < 4; i++) {
1463 if(moves[i]->moveID > 0 && !moves[i]->isInvalid() && (!moves[i]->isMaxPP() || !moves[i]->isMaxPpUps()))
1464 return false;
1465 }
1466
1467 if(atkExp < 0xFFFF || defExp < 0xFFFF || spdExp < 0xFFFF || spExp < 0xFFFF || hpExp < 0xFFFF)
1468 return false;
1469
1470 for(int i = 0; i < 4; i++) {
1471 if(dv[i] < 15)
1472 return false;
1473 }
1474
1475 // Stop here if pokemon is invalid and we got this far
1476 if(!isValidBool())
1477 return true;
1478
1479 if(hp < hpStat())
1480 return false;
1481
1482 if(exp < expLevelRangeEnd())
1483 return false;
1484
1485 return true;
1486}
1487
1489{
1490 auto record = isValid();
1491 if(record == nullptr)
1492 return true;
1493
1494 if(hpStat() != hp)
1495 return false;
1496
1497 if(record->toType1 != nullptr) {
1498 if(record->toType1->ind != type1)
1499 return false;
1500 }
1501
1502 // A mon is genuinely dual-type only when the record's second type really
1503 // differs from its first. The DB inconsistently stores single-type mons with
1504 // toType2 either null OR a duplicate of toType1; update() collapses a single
1505 // type to 0xFF, so for the single-type case accept either 0xFF or type1 here.
1506 //
1507 // TRACKED TEMPORARY EXCEPTION (see notes/plans/next-steps.md "type2 single
1508 // truth"): accepting BOTH 0xFF and the duplicate form is the OFFICIAL, intended
1509 // behaviour on the load/expanded side -- real saves disagree on how a single
1510 // type is stored, and the editor must read back exactly what it loaded (byte
1511 // fidelity: the type it reads is the type it writes, changed only when asked).
1512 // What is NOT decided is the single canonical form the editor should WRITE when
1513 // IT generates/corrects a single type (0xFF vs duplicate). Until Twilight makes
1514 // that call this tolerant check is a deliberate dirty patch -- do not leave it
1515 // indefinitely.
1516 bool dualType = record->toType2 != nullptr &&
1517 record->toType1 != nullptr &&
1518 record->toType2->ind != record->toType1->ind;
1519
1520 if(dualType) {
1521 if(record->toType2->ind != type2)
1522 return false;
1523 }
1524 else if(type2 != 0xFF && type2 != type1)
1525 return false;
1526
1527 if(record->catchRate) {
1528 if(*record->catchRate != catchRate)
1529 return false;
1530 }
1531
1532 if(levelToExp() != exp)
1533 return false;
1534
1535 return true;
1536}
1537
1539{
1540 auto record = isValid();
1541 if(record == nullptr)
1542 return -1;
1543
1544 return *record->pokedex;
1545}
1546
1548{
1549 auto record = isValid();
1550 if(record == nullptr)
1551 return "";
1552
1553 if(record->readable == "")
1554 return record->name;
1555 else
1556 return record->readable;
1557}
1558
1560{
1561 bool atkChk = dv[PokemonStats::Attack] & 2;
1562 bool defChk = dv[PokemonStats::Defense] == 0b1010;
1563 bool spdChk = dv[PokemonStats::Speed] == 0b1010;
1564 bool spChk = dv[PokemonStats::Special] == 0b1010;
1565
1566 return atkChk && defChk && spdChk && spChk;
1567}
1568
1570{
1571 return exp % 25;
1572}
1573
1575{
1576 // Get current value
1577 var8 cur = exp % 25;
1578
1579 // Get Level Ranges
1580 // We want to keep Pokemon in same level range if possible
1581 var32 min = expLevelRangeStart();
1582 var32 max = expLevelRangeEnd();
1583
1584 // Stop here if this is the nature
1585 if(cur == nature)
1586 return;
1587
1588 // Get offset to apply
1589 var8 offset = qAbs(nature - cur);
1590
1591 // Add or subtract
1592 if(cur > nature)
1593 exp -= offset;
1594 else
1595 exp += offset;
1596
1597 // Notify of change
1598 expChanged();
1599
1600 // Stop here if invalid Pokemon
1601 if(!isValidBool())
1602 return;
1603
1604 // Otherwise lets ensure the Pokemon is within the correct level range
1605 // If it's fallen below or risen above the max, offset by 25 to bring back
1606 // within level
1607 if(exp <= min) {
1608 exp += 25;
1609 expChanged();
1610 }
1611 else if(exp >= max) {
1612 exp -= 25;
1613 expChanged();
1614 }
1615}
1616
1618{
1619 QVector<PokemonMove*> movesNew;
1620
1621 // First gather actual moves
1622 for(int i = 0; i < 4; i++) {
1623 if(moves[i]->moveID <= 0)
1624 continue;
1625
1626 auto newMoveEl = new PokemonMove(
1627 this,
1628 moves[i]->moveID,
1629 moves[i]->pp,
1630 moves[i]->ppUp
1631 );
1632
1633 movesNew.append(newMoveEl);
1634 }
1635
1636 // Then clear out moves
1637 for(int i = 0; i < 4; i++) {
1638 moves[i]->moveID = 0;
1639 moves[i]->pp = 0;
1640 moves[i]->ppUp = 0;
1641 }
1642
1643 // Then re-insert moves
1644 for(int i = 0; i < movesNew.size(); i++) {
1645 moves[i]->moveID = movesNew.at(i)->moveID;
1646 moves[i]->pp = movesNew.at(i)->pp;
1647 moves[i]->ppUp = movesNew.at(i)->ppUp;
1648 }
1649
1650 // Then aknowledge changes
1651 for(int i = 0; i < 4; i++) {
1652 moves[i]->moveIDChanged();
1653 moves[i]->ppChanged();
1654 moves[i]->ppUpChanged();
1655 }
1656}
1657
1659{
1660 for(int i = 0; i < 4; i++)
1661 moves[i]->correctMove();
1662}
1663
1665{
1666 dv[PokemonStats::Defense] = 0b1010;
1667 dvChanged();
1668
1669 dv[PokemonStats::Speed] = 0b1010;
1670 dvChanged();
1671
1672 dv[PokemonStats::Special] = 0b1010;
1673 dvChanged();
1674
1677 dvChanged();
1678}
1679
1681{
1682 reRollDVs();
1683
1684 dv[PokemonStats::Attack] &= ~2;
1685 dvChanged();
1686}
1687
1689{
1690 // Since shinies have such specific DV's, it's easier just to roll a shiny
1691 // and set it's attack dv to the same attack as before only or'd with 2
1692 var8 tmpAtkDV = dv[PokemonStats::Attack];
1693 rollShiny();
1694
1695 dv[PokemonStats::Attack] = tmpAtkDV | 2;
1696 dvChanged();
1697}
1698
1700{
1701 // Just remove bit #1, the most minimum way of eliminating it as a shiny
1702 dv[PokemonStats::Attack] &= ~2;
1703 dvChanged();
1704}
1705
1707{
1708 return true;
1709}
1710
1711void PokemonBox::changeMove(int ind, int moveID, int pp, int ppUp)
1712{
1713 moves[ind]->changeMove(moveID, pp, ppUp);
1714}
1715
1717{
1718 if(ind < 0 || ind >= maxMoves)
1719 return;
1720
1721 // Clear the slot, then compact (cleanupMoves slides the later moves up and
1722 // emits each slot's per-field signals so the UI refreshes).
1723 moves[ind]->moveID = 0;
1724 moves[ind]->pp = 0;
1725 moves[ind]->ppUp = 0;
1726 cleanupMoves();
1727
1728 movesChanged();
1729}
1730
1732{
1733 // Compact first so the surviving move is genuinely the list's first slot, then
1734 // clear the rest.
1735 cleanupMoves();
1736
1737 for(int i = 1; i < maxMoves; i++) {
1738 moves[i]->moveID = 0;
1739 moves[i]->moveIDChanged();
1740
1741 moves[i]->pp = 0;
1742 moves[i]->ppChanged();
1743
1744 moves[i]->ppUp = 0;
1745 moves[i]->ppUpChanged();
1746 }
1747
1748 movesChanged();
1749}
1750
1752{
1753 if(ind < 0 || ind >= maxMoves)
1754 return;
1755
1756 // correctMove() may clear an invalid/duplicate move (leaving a gap); compact so
1757 // the later moves slide up and there is no hole.
1758 moves[ind]->correctMove();
1759 cleanupMoves();
1760
1761 movesChanged();
1762}
1763
1764void PokemonBox::reorderMove(int from, int to)
1765{
1766 // The (id, pp, ppUp) triple that travels together when a move is reordered, so
1767 // a move keeps its current/max PP as it changes slots.
1768 struct MoveVals { int id; int pp; int ppUp; };
1769
1770 // Collect the filled move slots (the compact prefix -- the first empty slot
1771 // ends the move list in game logic). Only filled moves reorder; empties stay
1772 // parked at the bottom.
1773 QVector<MoveVals> vec;
1774 for(int i = 0; i < maxMoves; i++) {
1775 if(moves[i]->moveID <= 0)
1776 break;
1777 vec.append({moves[i]->moveID, moves[i]->pp, moves[i]->ppUp});
1778 }
1779
1780 if(from < 0 || from >= vec.size())
1781 return;
1782
1783 MoveVals moved = vec.at(from);
1784
1785 // Anchor = the first move at/after the drop slot that ISN'T the one being moved;
1786 // the move is re-inserted directly before it, or appended when there is none
1787 // (dropping past the last move). Mirrors the storage drag-reorder convention.
1788 int anchorIdx = -1;
1789 for(int i = qBound(0, to, vec.size()); i < vec.size(); i++) {
1790 if(i != from) {
1791 anchorIdx = i;
1792 break;
1793 }
1794 }
1795 int anchorShift = (anchorIdx > from) ? 1 : 0; // removing 'from' shifts the anchor left
1796
1797 vec.removeAt(from);
1798 if(anchorIdx < 0)
1799 vec.append(moved);
1800 else
1801 vec.insert(anchorIdx - anchorShift, moved);
1802
1803 // Write the reordered values back into the fixed slot objects (the slot QObjects
1804 // themselves stay put -- only their values move -- so QML's movesAt() pointers
1805 // remain valid). changeMove() emits the per-field signals each row binds to.
1806 for(int i = 0; i < maxMoves; i++) {
1807 if(i < vec.size())
1808 moves[i]->changeMove(vec.at(i).id, vec.at(i).pp, vec.at(i).ppUp);
1809 else
1810 moves[i]->changeMove(0, 0, 0);
1811 }
1812
1813 movesChanged();
1814}
1815
1817{
1818 level = 5;
1819 levelChanged();
1820
1821 auto record = isValid();
1822 if(record == nullptr)
1823 return;
1824
1825 clearMoves();
1826
1827 for(int i = 0; i < 4 && i < record->toInitial.size(); i++) {
1828 auto moveData = record->toInitial.at(i);
1829 moves[i]->moveID = moveData->ind;
1830 moves[i]->moveIDChanged();
1831
1832 moves[i]->pp = *moveData->pp;
1833 moves[i]->ppChanged();
1834
1835 moves[i]->ppUp = 0;
1836 moves[i]->ppUpChanged();
1837 }
1838
1839 movesChanged();
1840
1841 resetEVs();
1842 heal();
1843 update(true, true, true, true);
1844}
1845
1847{
1848 species = pkmn->species;
1850
1851 hp = pkmn->hp;
1852 hpChanged();
1853
1854 level = pkmn->level;
1855 levelChanged();
1856
1857 status = pkmn->status;
1858 statusChanged();
1859
1860 type1 = pkmn->type1;
1861 type1Changed();
1862
1863 type2 = pkmn->type2;
1864 type2Changed();
1865
1866 catchRate = pkmn->catchRate;
1868
1869 otID = pkmn->otID;
1870 otIDChanged();
1871
1872 exp = pkmn->exp;
1873 expChanged();
1874
1875 hpExp = pkmn->hpExp;
1876 hpExpChanged();
1877
1878 atkExp = pkmn->atkExp;
1879 atkExpChanged();
1880
1881 defExp = pkmn->defExp;
1882 defExpChanged();
1883
1884 spdExp = pkmn->spExp;
1885 spdExpChanged();
1886
1887 spExp = pkmn->spExp;
1888 spExpChanged();
1889
1890 otName = pkmn->otName;
1891 otNameChanged();
1892
1893 nickname = pkmn->nickname;
1895
1896 dv[0] = pkmn->dv[0];
1897 dv[1] = pkmn->dv[1];
1898 dv[2] = pkmn->dv[2];
1899 dv[3] = pkmn->dv[3];
1900 dvChanged();
1901
1902 clearMoves();
1903
1904 for(int i = 0; i < 4; i++) {
1905 moves[i]->moveID = pkmn->moves[i]->moveID;
1906 moves[i]->moveIDChanged();
1907
1908 moves[i]->pp = pkmn->moves[i]->pp;
1909 moves[i]->ppChanged();
1910
1911 moves[i]->ppUp = pkmn->moves[i]->ppUp;
1912 moves[i]->ppUpChanged();
1913 }
1914
1915 movesChanged();
1916
1917 type2Explicit = false;
1919}
1920
1922{
1923 return PokemonDB::inst()->getIndAt(QString::number(species));
1924}
1925
1927{
1928 int ret = 0;
1929
1930 // Follows game logic
1931 // The first move with 0 ends move lookup
1932 for(int i = 0; i < 4; i++) {
1933 if(moves[i]->moveID <= 0)
1934 break;
1935
1936 ret++;
1937 }
1938
1939 return ret;
1940}
1941
1943{
1944 return maxMoves;
1945}
1946
1948{
1949 return qmlCppOwned(moves[ind]);
1950}
1951
1953{
1954 return maxDV;
1955}
1956
1958{
1959 return dv[ind];
1960}
1961
1962void PokemonBox::dvSet(int ind, int val)
1963{
1964 dv[ind] = val;
1965 dvChanged();
1966}
1967
1969{
1970 update(true, true, true, true);
1971}
1972
1974{
1975 update(true, true);
1976}
1977
1982
1987
1989{
1991}
1992
QString randomExample()
A random string from the list.
int getStoreSize() const
Move count.
Definition moves.cpp:80
MoveDBEntry * getIndAt(const QString &key) const
Move by name key (for QML).
Definition moves.cpp:88
static MovesDB * inst()
< Number of moves.
Definition moves.cpp:72
NamesPlayer * player() const
The player-name source (backs player).
Definition names.cpp:35
NamesPokemon * pokemon() const
The Pokemon-name source (backs pokemon).
Definition names.cpp:40
static Names * inst()
< Random player-name source.
Definition names.cpp:29
The trainer's headline values: name, ID, money, coins, badges, starter.
int playerID
Trainer ID (backs the property).
QString playerName
Trainer name (backs the property).
A single Pokemon record – the most property-rich object in the tree.
Definition pokemonbox.h:213
PokemonMove * movesAt(int ind)
Move slot ind (GC-protected return).
bool isMaxPpUps()
All moves at max PP-Ups.
PokemonMove * moves[4]
The four move slots.
Definition pokemonbox.h:519
void rollShiny()
Randomize DVs until shiny.
void rollNonShiny()
Randomize DVs until not shiny.
void hpExpChanged()
void resetDVs()
Zero all DVs.
void type2ExplicitChanged()
void levelChanged()
void expChanged()
void heal()
Pokecenter heal: full HP, clear status.
bool isValidBool()
Convenience bool form of isValid().
int dvAt(int ind)
DV value at ind.
void reRollDVs()
Randomize DVs.
void movesChanged()
void manualLevelChanged()
UI hook: level edited directly.
virtual void randomize(PlayerBasics *basics=nullptr)
Randomize this Pokemon (constrained).
void dvSet(int ind, int val)
Set DV ind.
void makeShiny()
Force DVs to a shiny combination.
bool isHealed()
Fully healed (HP + status). (heal() performs a Pokecenter heal.).
bool isMinEvs()
All stat-exp zero.
bool hasEvolution()
Species can evolve.
void expRangeChanged()
void reorderMove(int from, int to)
Reorder the filled move slots: take the move at from and re-insert it before slot to (drop-slot conve...
void resetEVs()
Zero all stat-exp.
int dvCount()
Number of stored DVs (maxDV).
void healedChanged()
bool hasDeEvolution()
Species has a pre-evolution.
int spdStat()
Computed Speed stat.
unsigned int expLevelRangeStart()
EXP at the start of the current level.
void maxLevel()
Set to level 100.
unsigned int exp
Definition pokemonbox.h:509
void otIDChanged()
void type2Changed()
void defExpChanged()
void evolve()
Evolve to the next species.
virtual ~PokemonBox()
void cleanupMoves()
Remove invalid/duplicate moves.
bool hasTradeStatus(PlayerBasics *basics=nullptr)
Counts as traded relative to basics.
bool isMaxHp()
HP equals computed max.
bool isMaxedOut()
Level/EV/DV/PP all maxed.
void evChanged()
protected::void speciesChanged()
int spStat()
Computed Special stat.
virtual void update(bool resetHp=false, bool resetExp=false, bool resetType=false, bool resetCatchRate=false, bool correctMoves=false)
Recompute derived stats.
void changeMove(int ind, int moveID=0, int pp=0, int ppUp=0)
Set move slot ind.
QString nickname
Definition pokemonbox.h:517
void pokemonResetChanged()
QString otName
Definition pokemonbox.h:516
virtual SaveFileIterator * save(SaveFile *saveFile=nullptr, var16 startOffset=0, svar32 speciesStartOffset=0, var16 nicknameStartOffset=0, var16 otNameStartOffset=0, var8 index=0, var8 recordSize=0x21)
Flatten one Pokemon back to the save.
unsigned int levelToExp(int level=-1)
EXP needed for level (current level if -1).
void resetExp()
Reset EXP to the current level's baseline.
void randomizeMoves()
Randomize the move set.
int dexNum()
Pokedex number.
void correctTypes()
Reset type1/type2 to this species' DB-default type(s) (e.g.
void maxOut()
Max level/EV/DV/PP at once.
void catchRateChanged()
void manualSpeciesChanged()
UI hook: species edited directly.
void changeName(bool removeNickname=false)
Randomize or (if true) remove the nickname.
void hpChanged()
void correctMoves()
Repair move/PP inconsistencies.
void spExpChanged()
void statChanged()
void clearMoves()
Empty all move slots.
void nicknameChanged()
void changeTrade(bool removeTradeStatus=false, PlayerBasics *basics=nullptr)
Toggle traded status.
void otNameChanged()
void unmakeShiny()
Force DVs to a non-shiny combination.
int movesCount()
Number of non-empty move slots.
void hasNicknameChanged()
void reRollEVs()
Randomize stat-exp.
void clearMovesButFirst()
Remove every move except the first one (slots 1..3 cleared).
bool isPokemonReset()
Matches the reset baseline.
void statusChanged()
virtual SaveFileIterator * load(SaveFile *saveFile=nullptr, var16 startOffset=0, var16 nicknameStartOffset=0, var16 otNameStartOffset=0, var8 index=0, var8 recordSize=0x21)
Expand one Pokemon from the save.
PokemonDBEntry * toData()
The species' DB entry for this mon.
PokemonDBEntry * isValid()
The species' DB entry, or null if not a real Pokedex species.
bool type2Explicit
Definition pokemonbox.h:530
unsigned int expLevelRangeEnd()
EXP at the next level.
virtual void copyFrom(PokemonBox *pkmn)
Deep-copy another mon's values into this one.
void correctMoveAt(int ind)
Make the move in slot ind valid (PokemonMove::correctMove) THEN compact: correctMove can clear an inv...
static PokemonBox * newPokemon(PokemonRandom::PokemonRandom_ list=PokemonRandom::Random_Starters, PlayerBasics *basics=nullptr)
void maxEVs()
Max all stat-exp.
bool isMaxLevel()
Level 100.
bool hasNickname()
Carries a real nickname.
void resetPokemon()
Reset to the baseline state.
void atkExpChanged()
bool isShiny()
Shiny per the VC-era DV formula (see disclaimer above).
void changeOtData(bool removeOtData=false, PlayerBasics *basics=nullptr)
Randomize or remove OT data.
void setNature(int nature)
QString speciesName()
Species display name.
int atkStat()
Computed Attack stat.
void deleteMoveAt(int ind)
Delete the move in slot ind, then compact so there is no gap in the move list (the slots after it sli...
void deEvolve()
Revert to the prior species.
float expLevelRangePercent()
Fractional progress through the level.
var8 dv[maxDV]
Stored DVs (Atk/Def/Spd/Spc); HP DV is derived.
Definition pokemonbox.h:515
void spdExpChanged()
void maxDVs()
Max all DVs.
void dvChanged()
virtual bool isBoxMon()
True for a pure box mon; PokemonParty overrides to false.
void maxPpUps()
Max every move's PP-Ups.
int defStat()
Computed Defense stat.
bool isMaxDVs()
All DVs maxed.
int nonHpStat(PokemonStats::PokemonStats_ stat)
PokemonBox(SaveFile *saveFile=nullptr, var16 startOffset=0, var16 nicknameStartOffset=0, var16 otNameStartOffset=0, var8 index=0, var8 recordSize=0x21)
< Species id (raw save value).
bool isCorrected()
Values internally consistent (see correct* slots).
bool isMaxEVs()
All stat-exp maxed.
void type1Changed()
virtual void reset()
Blank this Pokemon.
int movesMax()
Move-slot capacity (maxMoves).
bool isAfflicted()
Has any status condition.
bool isMinDVs()
All DVs zero.
bool isMaxPP()
All moves at max PP.
static PokemonDB * inst()
< Number of species.
Definition pokemon.cpp:183
PokemonDBEntry * getStoreAt(int idx) const
Species by store index (for QML).
Definition pokemon.cpp:193
int getStoreSize() const
Species count.
Definition pokemon.cpp:191
PokemonDBEntry * getIndAt(const QString &key) const
Species by name key (for QML).
Definition pokemon.cpp:199
One of a Pokemon's four move slots: move id, PP, and PP-Ups.
Definition pokemonbox.h:133
int pp
Current PP (backs property).
Definition pokemonbox.h:186
void ppChanged()
void ppCapChanged()
protected::void moveIDChanged()
void raisePpUp()
+1 PP-Up.
PokemonBox * parentMon
Owning Pokemon (for cross-slot validation).
Definition pokemonbox.h:188
PokemonMove(PokemonBox *parentMon, var8 move=0, var8 pp=0, var8 ppUp=0)
< Move id (indexes the moves DB).
MoveDBEntry * toMove()
Resolve moveID to its DB entry.
bool isMaxPpUps()
Are PP-Ups at max?
void ppUpChanged()
void lowerPpUp()
-1 PP-Up.
QString moveType()
The move's elemental type name.
int moveID
Move id (backs property).
Definition pokemonbox.h:185
QVector< int > validMovesLeft()
Legal moves not already used by the mon.
int getMaxPP()
PP cap for this move given PP-Ups.
void resetPpUp()
PP-Ups to 0.
bool isInvalid()
Is the move id out of range / not a real move?
void randomize()
Pick a random valid move.
void maxPpUp()
Set PP-Ups to max.
bool isDuplicateMove()
Is this move a duplicate within the mon's set?
void onMoveIdChanged()
Recompute derived state after the move id changes.
int ppUp
PP-Ups (backs property).
Definition pokemonbox.h:187
QVector< int > allValidMoves()
Every legal move id for this slot.
void restorePP()
Refill PP to the cap.
bool isMaxPP()
Is current PP at the cap?
void correctMove()
Clamp/repair inconsistent values.
void changeMove(int move=0, int pp=0, int ppUp=0)
Replace this slot's values.
bool chanceSuccess(const int percent) const
Did a percent chance succeed?
Definition random.cpp:73
bool flipCoin() const
50/50 coin flip via the integer path (chanceSuccess(50)).
Definition random.cpp:84
int rangeInclusive(const int start, const int end) const
Random integer in the closed interval [start, end].
Definition random.cpp:42
static Random * inst()
< Convenience 50% coin flip (integer path), readable from QML.
Definition random.cpp:31
int rangeExclusive(const int start, const int end) const
Random integer in the half-open interval [start, end).
Definition random.cpp:53
A moving cursor over a SaveFile, layering auto-advancing reads/writes on top of SaveFileToolset.
SaveFileIterator * offsetTo(var16 val)
Move the cursor to an absolute offset. Returns this for chaining.
One loaded save: the raw 32 KB bytes, their expanded object tree, and the tools that move between the...
Definition savefile.h:46
SaveFileToolset * toolset
Tools to operate directly on the raw sav file data.
Definition savefile.h:117
SaveFileIterator * iterator()
Returns a unique iterator that's setup to iterate over the raw sav file data.
Definition savefile.cpp:53
PokemonDBEntry * random3Starter() const
A random one of the 3 canonical starters.
static StarterPokemonDB * inst()
< Number of starter choices.
PokemonDBEntry * randomAnyStarter() const
A random "startery" species.
static TypesDB * inst()
< Number of types.
Definition types.cpp:37
TypeDBEntry * getStoreAt(int idx) const
Type by store index (for QML).
Definition types.cpp:47
svar32e svar32
Signed, exactly 32-bit (shorthand for svar32e).
Definition types.h:111
var8e var8
Everyday 8-bit alias. Exact (not "fastest") to dodge the pointer-width bug noted above.
Definition types.h:124
var16e var16
Everyday 16-bit alias. Exact width to avoid the "fastest" widening bug.
Definition types.h:125
var32e var32
Everyday 32-bit alias. Exact width to avoid the "fastest" widening bug.
Definition types.h:126
constexpr var8 pokemonDexCount
Number of species.
Definition pokemon.h:28
constexpr var8 pokemonLevelMax
Maximum level.
Definition pokemon.h:29
constexpr var8 maxMoves
Move slots per Pokemon.
Definition pokemonbox.h:191
constexpr var8 maxDV
DV entries stored (Atk/Def/Spd/Spc; HP DV is derived).
Definition pokemonbox.h:192
qmlCppOwned() – protect Q_INVOKABLE QObject returns from QML's GC.
static T * qmlCppOwned(T *obj)
Hand QML CppOwnership of a C++-owned QObject returned from a Q_INVOKABLE.
One move's static data (type, power, accuracy, PP, TM/HM), with links.
Definition moves.h:46
TypeDBEntry * toType
Resolved type entry (deepLink).
Definition moves.h:63
var8 ind
Move index/id.
Definition moves.h:52
bool glitch
Whether this is a glitch move.
Definition moves.h:53
One species' complete static data – the richest entry in the db layer.
Definition pokemon.h:98
QString name
Internal species name (key).
Definition pokemon.h:103
var8 ind
Internal species index.
Definition pokemon.h:104
@ Random_Starters
A "startery"-feeling Pokemon (non-legendary base evo).
Definition pokemonbox.h:115
@ Random_All
Any species at all, including MissingNo / glitch mons.
Definition pokemonbox.h:117
@ Random_Pokedex
Any Pokedex species.
Definition pokemonbox.h:116
@ Special
Special (single stat in Gen 1).
Definition pokemonbox.h:53
@ Defense
Physical defense.
Definition pokemonbox.h:51
@ Attack
Physical attack.
Definition pokemonbox.h:50
@ Speed
Speed.
Definition pokemonbox.h:52
One elemental type: its name plus the moves and Pokemon of that type.
Definition types.h:39
QString readable
Human-readable type name.
Definition types.h:45