Skip to content
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 // Normalise type 2 to the internal single-type sentinel (0xFF = "None") while
447 // preserving on-disk fidelity (see the "single truth" note in save()):
448 // * a literal 0xFF (only from a hacked/glitch save -- the game never writes
449 // it) is kept as-is AND flagged type2Explicit so save() writes 0xFF back;
450 // * the game's real single-type form (type2 == type1, a duplicate) collapses
451 // to the 0xFF sentinel with type2Explicit left false, so save() writes the
452 // duplicate back. A genuine dual type is stored verbatim.
453 if (type2 == 0xFF) {
454 type2Explicit = true;
456 } else if (type1 == type2) {
457 type2 = 0xFF;
458 type2Changed();
459 }
460
461 catchRate = it->getByte();
463
464 // Save offset to restore later
465 it->push();
466
467 // Temporarily save moves for later
468 // PP data which is important to moves has to be be gotten later
469 QVector<var8> moveIDList;
470 for (var8 i = 0; i < 4; i++) {
471 var8 moveID = it->getByte();
472 if(moveID == 0)
473 break;
474
475 moveIDList.append(moveID);
476 }
477
478 // Restore offset to start of moves and move past the moves
479 it->pop()->offsetBy(0x4);
480
481 otID = it->getWord();
482 otIDChanged();
483
484 // Exp is 3 bytes so it's a bit tricky
485 auto expRaw = it->getRange(3);
486 exp = expRaw[0];
487 exp <<= 8;
488 exp |= expRaw[1];
489 exp <<= 8;
490 exp |= expRaw[2];
491 expChanged();
492
493 hpExp = it->getWord();
494 hpExpChanged();
495
496 atkExp = it->getWord();
498
499 defExp = it->getWord();
501
502 spdExp = it->getWord();
504
505 spExp = it->getWord();
506 spExpChanged();
507
508 var16 dvTotal = it->getWord();
509 dv[(var8)PokemonStats::Attack] = (dvTotal & 0xF000) >> 12;
510 dv[(var8)PokemonStats::Defense] = (dvTotal & 0x0F00) >> 8;
511 dv[(var8)PokemonStats::Speed] = (dvTotal & 0x00F0) >> 4;
512 dv[(var8)PokemonStats::Special] = dvTotal & 0x000F;
513 dvChanged();
514
515 it->push();
516
517 // Next gather PP
518 QVector<var8> ppList;
519 for (var8 i = 0; i < moveIDList.size(); i++) {
520 var8 ppListEntry = it->getByte();
521 ppList.append(ppListEntry);
522 }
523
524 // Combine together in moves from earlier
525 for (var8 i = 0; i < moveIDList.size(); i++) {
526 var8 moveID = moveIDList.at(i);
527 var8 pp = ppList[i];
528 moves[i]->moveID = moveID;
529 moves[i]->moveIDChanged();
530
531 moves[i]->pp = pp & 0b00111111;
532 moves[i]->ppChanged();
533
534 moves[i]->ppUp = (pp & 0b11000000) >> 6;
535 moves[i]->ppUpChanged();
536 }
537 movesChanged();
538
539 // Restore back to before PP and move past PP
540 it->pop()->offsetBy(0x4);
541
542 // Now we must gather the OT names and Pokemon names whihc were poorly
543 // implemented in sometimes arbitrary spots outside of the data sructure
544 var16 otNameOffset = (index * 0xB) + otNameStartOffset;
545 otName = toolset->getStr(otNameOffset, 0xB, 7+1);
547
548 var16 nicknameOffset = (index * 0xB) + nicknameStartOffset;
549 nickname = toolset->getStr(nicknameOffset, 0xB, 10);
551
552 // Save the iterator to be picked up by sub-class if present
553 return it;
554}
555
557 var16 startOffset,
558 svar32 speciesStartOffset,
559 var16 nicknameStartOffset,
560 var16 otNameStartOffset,
561 var8 index,
562 var8 recordSize)
563{
564 auto toolset = saveFile->toolset;
565
566 // Retrieve stored internals
567 var16 offset = (recordSize * index) + startOffset;
568 auto it = saveFile->iterator()->offsetTo(offset);
569 var16 otNameOffset = (index * 0xB) + otNameStartOffset;
570 var16 nicknameOffset = (index * 0xB) + nicknameStartOffset;
571
572 // Add species to species list if exists
573 if(speciesStartOffset > 0) {
574 var16 speciesOffset = index + speciesStartOffset;
575 toolset->setByte(speciesOffset, species);
576 }
577
578 // Re-save back
579 it->setByte(species);
580 it->setWord(hp);
581
582 // Don't save level to BoxData if this is in the party
583 // This honors the global don't touch policy
584 // which is don't touch any bits that don't need to be changed
585 if (recordSize == 0x21) {
586 it->setByte(level);
587 } else {
588 it->inc();
589 }
590
591 it->setByte(status);
592 it->setByte(type1);
593
594 // Type 2 storage -- the resolved "single truth" (grounded in the pokered
595 // disassembly; see notes/reference/gen1-knowledge.md "Single-type storage"):
596 // * the game stores a SINGLE type as a duplicate of type 1 (base_stats/*.asm,
597 // e.g. Charmander `db FIRE, FIRE`), and 0xFF is not a valid type at all
598 // (type_constants.asm runs $00..$1A). So the CANONICAL written form of a
599 // single type is duplicate-of-type1 -- that is what the editor writes.
600 // * Byte fidelity still rules for LOADED saves: a mon read with a literal
601 // 0xFF (only ever from a hacked/glitch save) is written back as 0xFF
602 // unchanged (type2Explicit); a mon read as a duplicate is written back as
603 // the duplicate. A type byte changes only when explicitly edited.
604 // Internally a single type is held as type2 == 0xFF ("None"); type2Explicit
605 // marks that the 0xFF was literal-on-disk (preserve it) vs. editor-implied
606 // (write the duplicate). Any editor (re)generation clears type2Explicit, so a
607 // freshly generated/corrected single type takes the canonical duplicate form.
608 if (type2Explicit) {
609 it->setByte(type2); // preserve a literally-loaded 0xFF (byte fidelity)
610 } else if (type2 == 0xFF) {
611 it->setByte(type1); // canonical single type -> duplicate-of-type1
612 } else {
613 it->setByte(type2); // genuine dual type
614 }
615
616 it->setByte(catchRate);
617
618 it->push();
619 for(var8 i = 0; i < 4; i++) {
620 it->setByte(moves[i]->moveID);
621 }
622 it->pop()->offsetBy(4);
623
624 it->setWord(otID);
625
626 it->setByte((exp & 0xFF0000) >> 16);
627 it->setByte((exp & 0x00FF00) >> 8);
628 it->setByte(exp & 0x0000FF);
629
630 it->setWord(hpExp);
631 it->setWord(atkExp);
632 it->setWord(defExp);
633 it->setWord(spdExp);
634 it->setWord(spExp);
635
636 var16 dvTmp = 0;
637 dvTmp |= (dv[(var8)PokemonStats::Attack] << 12);
638 dvTmp |= (dv[(var8)PokemonStats::Defense] << 8);
639 dvTmp |= (dv[(var8)PokemonStats::Speed] << 4);
640 dvTmp |= dv[(var8)PokemonStats::Special];
641 it->setWord(dvTmp);
642
643 it->push();
644 for (var8 i = 0; i < 4; i++) {
645 var8 ppCombined = (moves[i]->ppUp << 6) | moves[i]->pp;
646 it->setByte(ppCombined);
647 }
648 it->pop()->offsetBy(4);
649
650 toolset->setStr(otNameOffset, 0xB, 10+1, otName);
651 toolset->setStr(nicknameOffset, 0xB, 10+1, nickname);
652
653 return it;
654}
655
657{
658 species = 0;
660
661 hp = 0;
662 hpChanged();
663
664 level = 0;
665 levelChanged();
666
667 status = 0;
669
670 type1 = 0;
671 type1Changed();
672
673 type2 = 0;
674 type2Changed();
675
676 catchRate = 0;
678
679 otID = 0;
680 otIDChanged();
681
682 exp = 0;
683 expChanged();
684
685 hpExp = 0;
686 hpExpChanged();
687
688 atkExp = 0;
690
691 defExp = 0;
693
694 spdExp = 0;
696
697 spExp = 0;
698 spExpChanged();
699
700 otName = "";
702
703 nickname = "";
705
706 dv[0] = 0;
707 dv[1] = 0;
708 dv[2] = 0;
709 dv[3] = 0;
710 dvChanged();
711
712 clearMoves();
713
714 type2Explicit = false;
716}
717
719{
720 reset();
721
722 // Generate a random level 5 Pokemon from the pokedex
723 // Bump it's level up to a random value
724 // Give it a random name, otName, and otID
725 // Then fix all of it's types, exp, stats, etc.. to be game accurate
727 copyFrom(pkmn);
728 pkmn->deleteLater();
729
731 levelChanged();
732
733 atkExp = Random::inst()->rangeInclusive(0, 0xFFFF);
735
736 defExp = Random::inst()->rangeInclusive(0, 0xFFFF);
738
739 spdExp = Random::inst()->rangeInclusive(0, 0xFFFF);
741
742 spExp = Random::inst()->rangeInclusive(0, 0xFFFF);
743 spExpChanged();
744
745 hpExp = Random::inst()->rangeInclusive(0, 0xFFFF);
746 hpExpChanged();
747
748 // Delete it's moves and re-create 4 new non-glitch random moves
750
751 // Make the pokemon a non-trade pokemon
752 changeTrade(true, basics);
753
754 // 50/50 chance of not having a nickname
755 // If true, removes nickname
756 // If false, assigns random nickname
757 bool noNick = Random::inst()->flipCoin();
758 changeName(noNick);
759
760 // This is where we make the Pokemon completely game accurate
761 update(true, true, true, true);
762
763 // Heal the Pokemon
764 heal();
765
766 // This is where we give the Pokemon whacky types for fun
767 // We have to do this after all the code above otherwise it'll be re-corrected
768 // to be game accurate
769 auto type1 = TypesDB::inst()->getStoreAt(Random::inst()->rangeExclusive(0, TypesDB::inst()->getStoreSize()));
770 TypeDBEntry* type2 = nullptr;
771
772 // 25% chance of type 2
773 bool hasType2 = Random::inst()->chanceSuccess(25);
774 if(hasType2) {
775 type2 = TypesDB::inst()->getStoreAt(Random::inst()->rangeExclusive(0, TypesDB::inst()->getStoreSize()));
776
777 if(type1->ind == type2->ind)
778 type2 = nullptr;
779 }
780
781 this->type1 = type1->ind;
782 type1Changed();
783
784 if(type2 != nullptr)
785 this->type2 = type2->ind;
786 else
787 this->type2 = 0xFF;
788 type2Changed();
789
790 // Editor-generated typing is canonical (not preserved load bytes): a single
791 // type must serialise as duplicate-of-type1, the game's own form (see the note
792 // in save() and notes/reference/gen1-knowledge.md). Clear the load-fidelity
793 // flag so save() writes the duplicate, never a stray 0xFF. (randomize() also
794 // reset()s at the top, but keep the invariant local + explicit here.)
795 type2Explicit = false;
797}
798
800{
801 for(int i = 0; i < 4; i++) {
802 moves[i]->moveID = 0;
803 moves[i]->moveIDChanged();
804
805 moves[i]->pp = 0;
806 moves[i]->ppChanged();
807
808 moves[i]->ppUp = 0;
809 moves[i]->ppUpChanged();
810 }
811
812 movesChanged();
813}
814
815// Is this a valid Pokemon? (Is it even in the Pokedex?)
816// If not returns false, otherwise returns Pokemon Record
818{
819 // Get Pokemon Record
820 // The Pokemon Array is organized by species ID with 1 top entry missing
821 // thus offset by 1 accordingly
822 auto record = PokemonDB::inst()->getIndAt(QString::number(species));
823
824 // Check it's a valid Pokemon (not glitch)
825 if(record == nullptr || record->glitch || !(record->pokedex))
826 return nullptr;
827
828 return record;
829}
830
832{
833 return isValid() != nullptr;
834}
835
837{
838 auto record = isValid();
839 double exp = 0;
840
841 if(level < 0)
842 level = this->level;
843
844 // Proceed only if it's valid
845 if(record == nullptr)
846 return exp;
847
848 // Obtain it's growth rate and calculate accordingly it's exp for the given level
849 var8 gr = *record->growthRate;
850
851 // Growth Rate 0: Medium Fast
852 if(gr == 0)
853 exp = qPow(level, 3);
854
855 // Growth Rate 3: Medium Slow
856 else if(gr == 3)
857 exp = (1.2 * qPow(level, 3)) - (15 * qPow(level, 2)) + (100*level) - 140;
858
859 // Growth Rate 4: Fast
860 else if(gr == 4)
861 exp = (4 * qPow(level, 3)) / 5;
862
863 // Growth Rate 5: Slow
864 else if(gr == 5)
865 exp = (5 * qPow(level, 3)) / 4;
866
867 // Return EXP
868 return qFloor(exp);
869}
870
872{
873 if(isValid() == nullptr)
874 return this->exp;
875
876 return levelToExp((level < 100) ? level : 100);
877}
878
880{
881 if(isValid() == nullptr)
882 return exp;
883
884 return levelToExp((level < 100) ? level + 1 : 100) - 1;
885}
886
888{
889 if(this->isValid() == nullptr)
890 return 0;
891
892 if(level >= 100)
893 return 1;
894
895 var32 curExp = exp - expLevelRangeStart();
897
898 // Return percentage. Both operands are var32, so the previous `curExp / expEnd`
899 // was an INTEGER division truncated to 0 (or 1 at the very top of the level)
900 // before being widened to the float return -- the fractional percent was always
901 // lost. Divide in floating point, and guard a zero-width range (degenerate exp
902 // data) against divide-by-zero. Found by clang-tidy (bugprone-integer-division +
903 // clang-analyzer-core.DivideZero); see notes/reference/fix-patterns.md.
904 if(expEnd == 0)
905 return 0;
906 return static_cast<float>(curExp) / static_cast<float>(expEnd);
907}
908
910{
911 if(isValid() == nullptr)
912 return;
913
915 expChanged();
916}
917
919{
920 var8 hpDv = 0;
921
922 if((dv[(var8)PokemonStats::Attack] % 2) != 0)
923 hpDv |= 8;
924
925 if((dv[(var8)PokemonStats::Defense] % 2) != 0)
926 hpDv |= 4;
927
928 if((dv[(var8)PokemonStats::Speed] % 2) != 0)
929 hpDv |= 2;
930
931 if((dv[(var8)PokemonStats::Special] % 2) != 0)
932 hpDv |= 1;
933
934 return hpDv;
935}
936
938{
939 auto record = isValid();
940
941 // Proceed only if it's valid
942 if(record == nullptr || !(record->baseHp))
943 return 1;
944
945 return qFloor((((*record->baseHp + hpDV())*2+qFloor(qFloor(qSqrt(hpExp))/4))*level)/100) + level + 10;
946}
947
949{
950 auto record = isValid();
951
952 // Proceed only if it's valid
953 if(record == nullptr)
954 return 0;
955
956 int baseStat = 0;
957 int dvLocal = 0;
958 int evLocal = 0;
959
960 if(stat == PokemonStats::Attack) {
961 baseStat = *record->baseAttack;
962 dvLocal = dv[PokemonStats::Attack];
963 evLocal = atkExp;
964 }
965 else if(stat == PokemonStats::Defense) {
966 baseStat = *record->baseDefense;
967 dvLocal = dv[PokemonStats::Defense];
968 evLocal = defExp;
969 }
970 else if(stat == PokemonStats::Speed) {
971 baseStat = *record->baseSpeed;
972 dvLocal = dv[PokemonStats::Speed];
973 evLocal = spdExp;
974 }
975 else if(stat == PokemonStats::Special) {
976 baseStat = *record->baseSpecial;
977 dvLocal = dv[PokemonStats::Special];
978 evLocal = spExp;
979 }
980
981 return qFloor((((baseStat+dvLocal)*2+qFloor(qFloor(qSqrt(evLocal))/4))*level)/100) + 5;
982}
983
984void PokemonBox::update(bool resetHp,
985 bool resetExp,
986 bool resetType,
987 bool resetCatchRate,
988 bool correctMoves)
989{
990 auto record = isValid();
991 if(record == nullptr)
992 return;
993
994 if(resetHp) {
995 hp = hpStat();
996 hpChanged();
997 }
998
999 if(resetType && record->toType1) {
1000 type1 = (*record).toType1->ind;
1001 type1Changed();
1002 }
1003
1004 // Only (re)derive type2 when explicitly asked. The previous code's bare `else`
1005 // ran on EVERY update() called with resetType=false and overwrote type2 with
1006 // type1 -- silently dropping a dual-type mon's second type (reachable via
1007 // maxLevel()/maxEVs()/resetEVs()/reRollEVs()/manualLevelChanged()). Now type2
1008 // is left untouched unless resetType is set.
1009 if(resetType) {
1010 if(record->toType2)
1011 type2 = (*record).toType2->ind;
1012 else if(record->toType1) // guard toType1 (matches the resetType-&&-toType1
1013 type2 = (*record).toType1->ind; // check above) -- avoid a null deref on a
1014 // record with neither type resolved.
1015 // Found by clang-analyzer-core.NullDereference.
1016
1017 // A single type (no distinct second type) is stored internally as 0xFF.
1018 if(type1 == type2)
1019 type2 = 0xFF;
1020
1021 type2Changed();
1022
1023 // A DB-derived (re)typing is canonical, so drop any preserved load-fidelity
1024 // 0xFF: a single type now serialises as duplicate-of-type1 (the game's form;
1025 // see the note in save()). Stops a mon that was loaded with a literal 0xFF
1026 // from re-writing 0xFF after its species/typing was reset here.
1027 type2Explicit = false;
1029 }
1030
1031 if(resetCatchRate && record->catchRate) {
1032 catchRate = *record->catchRate;
1034 }
1035
1036 if(resetExp) {
1037 exp = levelToExp();
1038 expChanged();
1039 }
1040
1041 if(correctMoves) {
1042 this->correctMoves();
1043 cleanupMoves();
1044 }
1045}
1046
1047// Reset the typing to the species' DB defaults. Mirrors update()'s resetType block
1048// but standalone, so QML can call it without the unreliable multi-bool update() call
1049// (see the header note). No-ops on a glitch species (no DB record / unlinked type).
1051{
1052 auto record = isValid();
1053 if(record == nullptr || !record->toType1)
1054 return;
1055
1056 type1 = record->toType1->ind;
1057 type1Changed();
1058
1059 if(record->toType2)
1060 type2 = record->toType2->ind;
1061 else
1062 type2 = record->toType1->ind;
1063
1064 // A single type (no distinct second type) is stored internally as 0xFF.
1065 if(type1 == type2)
1066 type2 = 0xFF;
1067
1068 type2Changed();
1069
1070 // This is an editor-driven correction, so the result is canonical, not loaded
1071 // bytes: clear the load-fidelity flag so a single type serialises as
1072 // duplicate-of-type1 (the game's form; see the note in save()) rather than a
1073 // stale 0xFF carried over from a hacked save that literally stored 0xFF.
1074 type2Explicit = false;
1076}
1077
1079{
1080 return isMaxHp() && !isAfflicted() && isMaxPP();
1081}
1082
1084{
1085 return status > 0;
1086}
1087
1089{
1090 if(!isValid())
1091 return false;
1092
1093 return hp == hpStat();
1094}
1095
1097{
1098 hp = hpStat();
1099 hpChanged();
1100
1101 status = 0;
1102 statusChanged();
1103
1104 for(int i = 0; i < 4; i++)
1105 moves[i]->restorePP();
1106}
1107
1109{
1110 auto record = isValid();
1111
1112 if(record == nullptr)
1113 return false;
1114
1115 return record->name != nickname;
1116}
1117
1119{
1120 return basics->playerName != otName || basics->playerID != otID;
1121}
1122
1123void PokemonBox::changeName(bool removeNickname)
1124{
1125 if(!removeNickname)
1127 else if(removeNickname)
1128 nickname = toData()->name;
1129
1131}
1132
1133void PokemonBox::changeOtData(bool removeOtData, PlayerBasics* basics)
1134{
1135 // Randomize OT (give it "traded" status): always a real change, always emit.
1136 if(!removeOtData) {
1138 otID = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1139 otNameChanged();
1140 otIDChanged();
1141 return;
1142 }
1143
1144 // Adopt the player's OT (remove "traded" status). Need the player's data.
1145 if(basics == nullptr)
1146 return;
1147
1148 // Idempotent: only touch a field (and emit) if it actually differs. Keeps the
1149 // owned-mon OT sync from firing a storm of no-op change signals, and keeps us
1150 // from rewriting OT bytes that didn't need to change.
1151 if(otName != basics->playerName) {
1152 otName = basics->playerName;
1153 otNameChanged();
1154 }
1155
1156 if(otID != basics->playerID) {
1157 otID = basics->playerID;
1158 otIDChanged();
1159 }
1160}
1161
1162void PokemonBox::changeTrade(bool removeTradeStatus, PlayerBasics* basics)
1163{
1164 changeName(removeTradeStatus);
1165 changeOtData(removeTradeStatus, basics);
1166}
1167
1169{
1170 auto record = isValid();
1171
1172 if(record == nullptr)
1173 return false;
1174
1175 if(record->evolution.size() == 0)
1176 return false;
1177
1178 return true;
1179}
1180
1182{
1183 auto record = isValid();
1184
1185 if(record == nullptr)
1186 return false;
1187
1188 if(record->toDeEvolution == nullptr)
1189 return false;
1190
1191 return true;
1192}
1193
1195{
1196 auto record = isValid();
1197
1198 if(!hasEvolution())
1199 return;
1200
1201 // Does it have a nickname before evolution
1202 bool nickStatus = hasNickname();
1203
1204 // For Eevee evolutions, randomly pick one
1205 if(record->evolution.size() > 1) {
1206 var8 ind = Random::inst()->rangeExclusive(0, record->evolution.size());
1207 species = record->evolution.at(ind)->toEvolution->ind;
1208 }
1209 else
1210 species = record->evolution.at(0)->toEvolution->ind;
1211
1213
1214 // Update name if no nickname
1215 if(!nickStatus)
1216 changeName(true);
1217
1218 // Update all stats, make everything else game accurate
1219 update(true, true, true, true);
1220}
1221
1223{
1224 auto record = isValid();
1225
1226 if(!hasDeEvolution())
1227 return;
1228
1229 // Does it have a nickname before de-evolution
1230 bool nickStatus = hasNickname();
1231
1232 species = record->toDeEvolution->ind;
1234
1235 // Update name if no nickname
1236 if(!nickStatus)
1237 changeName(true);
1238
1239 // Update all stats, make everything game accurate
1240 update(true, true, true, true);
1241
1242 // As for moves, given this is made-up territory, I'm going with evolution
1243 // rules and saying the Pokemon can keep the evolved moves because it's the
1244 // same Pokemon that's reverted to a younger self and has the same memory.
1245}
1246
1248{
1249 return level >= 100;
1250}
1251
1253{
1254 bool ret = true;
1255
1256 // Empty slots (moveID 0) hold no move and have no PP, so they must NOT count as
1257 // "not max PP" -- otherwise any mon with fewer than 4 moves can never read as
1258 // max-PP, and therefore never as isHealed() (a user-facing wrong result on the
1259 // heal indicator). Mirrors isMaxedOut()'s existing moveID>0 guard. (2026-06-08.)
1260 for(int i = 0; i < 4; i++)
1261 if(moves[i]->moveID > 0 && !moves[i]->isMaxPP())
1262 ret = false;
1263
1264 return ret;
1265}
1266
1268{
1269 bool ret = true;
1270
1271 // Same empty-slot guard as isMaxPP(): an empty slot has no PP-Ups to max.
1272 for(int i = 0; i < 4; i++)
1273 if(moves[i]->moveID > 0 && !moves[i]->isMaxPpUps())
1274 ret = false;
1275
1276 return ret;
1277}
1278
1280{
1281 return atkExp == 0xFFFF &&
1282 defExp == 0xFFFF &&
1283 spdExp == 0xFFFF &&
1284 spExp == 0xFFFF &&
1285 hpExp == 0xFFFF;
1286}
1287
1289{
1290 // "Minimum EVs" means ALL five stat-exp are zero (symmetric with isMaxEVs()'s
1291 // all-0xFFFF). Was `||` (true if ANY one was 0), which wrongly disabled the
1292 // "Reset EVs" UI action whenever a single stat-exp happened to be 0. (Fixed
1293 // 2026-06-08, Twilight-confirmed.)
1294 return atkExp == 0 &&
1295 defExp == 0 &&
1296 spdExp == 0 &&
1297 spExp == 0 &&
1298 hpExp == 0;
1299}
1300
1302{
1303 bool ret = true;
1304
1305 for(var8 i = 0; i < 4; i++)
1306 if(dv[i] < 15) ret = false;
1307
1308 return ret;
1309}
1310
1312{
1313 bool ret = true;
1314
1315 for(var8 i = 0; i < 4; i++)
1316 if(dv[i] > 0) ret = false;
1317
1318 return ret;
1319}
1320
1322{
1323 level = 100;
1324 levelChanged();
1325
1326 update(true, true);
1327}
1328
1330{
1331 for(int i = 0; i < 4; i++)
1332 moves[i]->maxPpUp();
1333}
1334
1336{
1337 for(var8 i = 0; i < 4; i++)
1338 dv[i] = 15;
1339
1340 dvChanged();
1341}
1342
1344{
1345 for(var8 i = 0; i < 4; i++)
1346 dv[i] = Random::inst()->rangeInclusive(0, 15);
1347
1348 dvChanged();
1349}
1350
1352{
1353 for(var8 i = 0; i < 4; i++)
1354 dv[i] = 0;
1355
1356 dvChanged();
1357}
1358
1360{
1361 hpExp = 0xFFFF;
1362 hpExpChanged();
1363
1364 atkExp = 0xFFFF;
1365 atkExpChanged();
1366
1367 defExp = 0xFFFF;
1368 defExpChanged();
1369
1370 spdExp = 0xFFFF;
1371 spdExpChanged();
1372
1373 spExp = 0xFFFF;
1374 spExpChanged();
1375
1376 update(true);
1377}
1378
1380{
1381 hpExp = 0;
1382 hpExpChanged();
1383
1384 atkExp = 0;
1385 atkExpChanged();
1386
1387 defExp = 0;
1388 defExpChanged();
1389
1390 spdExp = 0;
1391 spdExpChanged();
1392
1393 spExp = 0;
1394 spExpChanged();
1395
1396 update(true);
1397}
1398
1400{
1401 hpExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1402 hpExpChanged();
1403
1404 atkExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1405 atkExpChanged();
1406
1407 defExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1408 defExpChanged();
1409
1410 spdExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1411 spdExpChanged();
1412
1413 spExp = Random::inst()->rangeInclusive(0x0000, 0xFFFF);
1414 spExpChanged();
1415
1416 update(true);
1417}
1418
1420{
1421 maxLevel();
1422 maxPpUps();
1423 maxEVs();
1424 maxDVs();
1425 heal();
1426
1427 update(true, true);
1428}
1429
1431{
1432 clearMoves();
1433
1434 for(var8 i = 0; i < 4; i++) {
1435 moves[i]->randomize();
1436 }
1437
1438 movesChanged();
1439}
1440
1442{
1443 auto record = isValid();
1444
1445 if(record == nullptr)
1446 return false;
1447
1448 bool movesReset = true;
1449
1450 // A reset mon (see resetPokemon()) carries exactly the species' initial moves,
1451 // each at base PP with 0 PP-Ups, and empty slots beyond them. The old loop
1452 // checked all four slots against toInitial.at(i) (out-of-range for species with
1453 // <4 initial moves -- saved only by the toMove()==null early-out, which also
1454 // forced "not reset"), and required isMaxPpUps() (3) when a reset mon actually
1455 // has 0 PP-Ups. Iterate only the real initial moves; require empty slots after.
1456 int initialCount = record->toInitial.size();
1457 if(initialCount > 4)
1458 initialCount = 4;
1459
1460 for(int i = 0; i < 4; i++) {
1461 auto move = moves[i];
1462
1463 // Slots past the species' initial-move list must be empty.
1464 if(i >= initialCount) {
1465 if(move->moveID != 0)
1466 movesReset = false;
1467 if(!movesReset)
1468 break;
1469 continue;
1470 }
1471
1472 if(move->toMove() == nullptr)
1473 movesReset = false;
1474 if(!movesReset)
1475 break;
1476
1477 if(move->moveID != record->toInitial.at(i)->ind)
1478 movesReset = false;
1479 if(move->ppUp != 0) // resetPokemon leaves PP-Ups at 0
1480 movesReset = false;
1481
1482 if(!movesReset)
1483 break;
1484 }
1485
1486 // isHealed() (full HP + no status + max PP) is now correct for any move count
1487 // because isMaxPP() skips empty slots; PP/HP/status are covered there, so here
1488 // we only need the level, the initial-move match, and zeroed EVs.
1489 return level == 5 && movesReset && isMinEvs() && isHealed();
1490}
1491
1493{
1494 if(level < 100)
1495 return false;
1496
1497 for(int i = 0; i < 4; i++) {
1498 if(moves[i]->moveID > 0 && !moves[i]->isInvalid() && (!moves[i]->isMaxPP() || !moves[i]->isMaxPpUps()))
1499 return false;
1500 }
1501
1502 if(atkExp < 0xFFFF || defExp < 0xFFFF || spdExp < 0xFFFF || spExp < 0xFFFF || hpExp < 0xFFFF)
1503 return false;
1504
1505 for(int i = 0; i < 4; i++) {
1506 if(dv[i] < 15)
1507 return false;
1508 }
1509
1510 // Stop here if pokemon is invalid and we got this far
1511 if(!isValidBool())
1512 return true;
1513
1514 if(hp < hpStat())
1515 return false;
1516
1517 if(exp < expLevelRangeEnd())
1518 return false;
1519
1520 return true;
1521}
1522
1524{
1525 auto record = isValid();
1526 if(record == nullptr)
1527 return true;
1528
1529 if(hpStat() != hp)
1530 return false;
1531
1532 if(record->toType1 != nullptr) {
1533 if(record->toType1->ind != type1)
1534 return false;
1535 }
1536
1537 // A mon is genuinely dual-type only when the record's second type really
1538 // differs from its first. The DB inconsistently stores single-type mons with
1539 // toType2 either null OR a duplicate of toType1; load()/update() collapse a
1540 // single type to the internal 0xFF sentinel. Accept EITHER 0xFF or the
1541 // duplicate (type2 == type1) as "corrected" for a single-type species.
1542 //
1543 // RESOLVED "single truth" (2026-07-09, from the pokered disassembly -- see
1544 // notes/reference/gen1-knowledge.md "Single-type storage"): the game's single-
1545 // type form is duplicate-of-type1 and 0xFF is not a valid type, so BOTH the
1546 // 0xFF sentinel and the duplicate are legitimate representations of the same
1547 // single-type mon that serialise to the identical canonical bytes. Neither is
1548 // "wrong", so tolerating both here is the intended final behaviour, not a
1549 // temporary patch.
1550 bool dualType = record->toType2 != nullptr &&
1551 record->toType1 != nullptr &&
1552 record->toType2->ind != record->toType1->ind;
1553
1554 if(dualType) {
1555 if(record->toType2->ind != type2)
1556 return false;
1557 }
1558 else if(type2 != 0xFF && type2 != type1)
1559 return false;
1560
1561 if(record->catchRate) {
1562 if(*record->catchRate != catchRate)
1563 return false;
1564 }
1565
1566 if(levelToExp() != exp)
1567 return false;
1568
1569 return true;
1570}
1571
1573{
1574 auto record = isValid();
1575 if(record == nullptr)
1576 return -1;
1577
1578 return *record->pokedex;
1579}
1580
1582{
1583 auto record = isValid();
1584 if(record == nullptr)
1585 return "";
1586
1587 if(record->readable == "")
1588 return record->name;
1589 else
1590 return record->readable;
1591}
1592
1594{
1595 bool atkChk = dv[PokemonStats::Attack] & 2;
1596 bool defChk = dv[PokemonStats::Defense] == 0b1010;
1597 bool spdChk = dv[PokemonStats::Speed] == 0b1010;
1598 bool spChk = dv[PokemonStats::Special] == 0b1010;
1599
1600 return atkChk && defChk && spdChk && spChk;
1601}
1602
1604{
1605 return exp % 25;
1606}
1607
1609{
1610 // Get current value
1611 var8 cur = exp % 25;
1612
1613 // Get Level Ranges
1614 // We want to keep Pokemon in same level range if possible
1615 var32 min = expLevelRangeStart();
1616 var32 max = expLevelRangeEnd();
1617
1618 // Stop here if this is the nature
1619 if(cur == nature)
1620 return;
1621
1622 // Get offset to apply
1623 var8 offset = qAbs(nature - cur);
1624
1625 // Add or subtract
1626 if(cur > nature)
1627 exp -= offset;
1628 else
1629 exp += offset;
1630
1631 // Notify of change
1632 expChanged();
1633
1634 // Stop here if invalid Pokemon
1635 if(!isValidBool())
1636 return;
1637
1638 // Otherwise lets ensure the Pokemon is within the correct level range
1639 // If it's fallen below or risen above the max, offset by 25 to bring back
1640 // within level
1641 if(exp <= min) {
1642 exp += 25;
1643 expChanged();
1644 }
1645 else if(exp >= max) {
1646 exp -= 25;
1647 expChanged();
1648 }
1649}
1650
1652{
1653 QVector<PokemonMove*> movesNew;
1654
1655 // First gather actual moves
1656 for(int i = 0; i < 4; i++) {
1657 if(moves[i]->moveID <= 0)
1658 continue;
1659
1660 auto newMoveEl = new PokemonMove(
1661 this,
1662 moves[i]->moveID,
1663 moves[i]->pp,
1664 moves[i]->ppUp
1665 );
1666
1667 movesNew.append(newMoveEl);
1668 }
1669
1670 // Then clear out moves
1671 for(int i = 0; i < 4; i++) {
1672 moves[i]->moveID = 0;
1673 moves[i]->pp = 0;
1674 moves[i]->ppUp = 0;
1675 }
1676
1677 // Then re-insert moves
1678 for(int i = 0; i < movesNew.size(); i++) {
1679 moves[i]->moveID = movesNew.at(i)->moveID;
1680 moves[i]->pp = movesNew.at(i)->pp;
1681 moves[i]->ppUp = movesNew.at(i)->ppUp;
1682 }
1683
1684 // Then aknowledge changes
1685 for(int i = 0; i < 4; i++) {
1686 moves[i]->moveIDChanged();
1687 moves[i]->ppChanged();
1688 moves[i]->ppUpChanged();
1689 }
1690}
1691
1693{
1694 for(int i = 0; i < 4; i++)
1695 moves[i]->correctMove();
1696}
1697
1699{
1700 dv[PokemonStats::Defense] = 0b1010;
1701 dvChanged();
1702
1703 dv[PokemonStats::Speed] = 0b1010;
1704 dvChanged();
1705
1706 dv[PokemonStats::Special] = 0b1010;
1707 dvChanged();
1708
1711 dvChanged();
1712}
1713
1715{
1716 reRollDVs();
1717
1718 dv[PokemonStats::Attack] &= ~2;
1719 dvChanged();
1720}
1721
1723{
1724 // Since shinies have such specific DV's, it's easier just to roll a shiny
1725 // and set it's attack dv to the same attack as before only or'd with 2
1726 var8 tmpAtkDV = dv[PokemonStats::Attack];
1727 rollShiny();
1728
1729 dv[PokemonStats::Attack] = tmpAtkDV | 2;
1730 dvChanged();
1731}
1732
1734{
1735 // Just remove bit #1, the most minimum way of eliminating it as a shiny
1736 dv[PokemonStats::Attack] &= ~2;
1737 dvChanged();
1738}
1739
1741{
1742 return true;
1743}
1744
1745void PokemonBox::changeMove(int ind, int moveID, int pp, int ppUp)
1746{
1747 moves[ind]->changeMove(moveID, pp, ppUp);
1748}
1749
1751{
1752 if(ind < 0 || ind >= maxMoves)
1753 return;
1754
1755 // Clear the slot, then compact (cleanupMoves slides the later moves up and
1756 // emits each slot's per-field signals so the UI refreshes).
1757 moves[ind]->moveID = 0;
1758 moves[ind]->pp = 0;
1759 moves[ind]->ppUp = 0;
1760 cleanupMoves();
1761
1762 movesChanged();
1763}
1764
1766{
1767 // Compact first so the surviving move is genuinely the list's first slot, then
1768 // clear the rest.
1769 cleanupMoves();
1770
1771 for(int i = 1; i < maxMoves; i++) {
1772 moves[i]->moveID = 0;
1773 moves[i]->moveIDChanged();
1774
1775 moves[i]->pp = 0;
1776 moves[i]->ppChanged();
1777
1778 moves[i]->ppUp = 0;
1779 moves[i]->ppUpChanged();
1780 }
1781
1782 movesChanged();
1783}
1784
1786{
1787 if(ind < 0 || ind >= maxMoves)
1788 return;
1789
1790 // correctMove() may clear an invalid/duplicate move (leaving a gap); compact so
1791 // the later moves slide up and there is no hole.
1792 moves[ind]->correctMove();
1793 cleanupMoves();
1794
1795 movesChanged();
1796}
1797
1798void PokemonBox::reorderMove(int from, int to)
1799{
1800 // The (id, pp, ppUp) triple that travels together when a move is reordered, so
1801 // a move keeps its current/max PP as it changes slots.
1802 struct MoveVals { int id; int pp; int ppUp; };
1803
1804 // Collect the filled move slots (the compact prefix -- the first empty slot
1805 // ends the move list in game logic). Only filled moves reorder; empties stay
1806 // parked at the bottom.
1807 QVector<MoveVals> vec;
1808 for(int i = 0; i < maxMoves; i++) {
1809 if(moves[i]->moveID <= 0)
1810 break;
1811 vec.append({moves[i]->moveID, moves[i]->pp, moves[i]->ppUp});
1812 }
1813
1814 if(from < 0 || from >= vec.size())
1815 return;
1816
1817 MoveVals moved = vec.at(from);
1818
1819 // Anchor = the first move at/after the drop slot that ISN'T the one being moved;
1820 // the move is re-inserted directly before it, or appended when there is none
1821 // (dropping past the last move). Mirrors the storage drag-reorder convention.
1822 int anchorIdx = -1;
1823 for(int i = qBound(0, to, vec.size()); i < vec.size(); i++) {
1824 if(i != from) {
1825 anchorIdx = i;
1826 break;
1827 }
1828 }
1829 int anchorShift = (anchorIdx > from) ? 1 : 0; // removing 'from' shifts the anchor left
1830
1831 vec.removeAt(from);
1832 if(anchorIdx < 0)
1833 vec.append(moved);
1834 else
1835 vec.insert(anchorIdx - anchorShift, moved);
1836
1837 // Write the reordered values back into the fixed slot objects (the slot QObjects
1838 // themselves stay put -- only their values move -- so QML's movesAt() pointers
1839 // remain valid). changeMove() emits the per-field signals each row binds to.
1840 for(int i = 0; i < maxMoves; i++) {
1841 if(i < vec.size())
1842 moves[i]->changeMove(vec.at(i).id, vec.at(i).pp, vec.at(i).ppUp);
1843 else
1844 moves[i]->changeMove(0, 0, 0);
1845 }
1846
1847 movesChanged();
1848}
1849
1851{
1852 level = 5;
1853 levelChanged();
1854
1855 auto record = isValid();
1856 if(record == nullptr)
1857 return;
1858
1859 clearMoves();
1860
1861 for(int i = 0; i < 4 && i < record->toInitial.size(); i++) {
1862 auto moveData = record->toInitial.at(i);
1863 moves[i]->moveID = moveData->ind;
1864 moves[i]->moveIDChanged();
1865
1866 moves[i]->pp = *moveData->pp;
1867 moves[i]->ppChanged();
1868
1869 moves[i]->ppUp = 0;
1870 moves[i]->ppUpChanged();
1871 }
1872
1873 movesChanged();
1874
1875 resetEVs();
1876 heal();
1877 update(true, true, true, true);
1878}
1879
1881{
1882 species = pkmn->species;
1884
1885 hp = pkmn->hp;
1886 hpChanged();
1887
1888 level = pkmn->level;
1889 levelChanged();
1890
1891 status = pkmn->status;
1892 statusChanged();
1893
1894 type1 = pkmn->type1;
1895 type1Changed();
1896
1897 type2 = pkmn->type2;
1898 type2Changed();
1899
1900 catchRate = pkmn->catchRate;
1902
1903 otID = pkmn->otID;
1904 otIDChanged();
1905
1906 exp = pkmn->exp;
1907 expChanged();
1908
1909 hpExp = pkmn->hpExp;
1910 hpExpChanged();
1911
1912 atkExp = pkmn->atkExp;
1913 atkExpChanged();
1914
1915 defExp = pkmn->defExp;
1916 defExpChanged();
1917
1918 spdExp = pkmn->spdExp;
1919 spdExpChanged();
1920
1921 spExp = pkmn->spExp;
1922 spExpChanged();
1923
1924 otName = pkmn->otName;
1925 otNameChanged();
1926
1927 nickname = pkmn->nickname;
1929
1930 dv[0] = pkmn->dv[0];
1931 dv[1] = pkmn->dv[1];
1932 dv[2] = pkmn->dv[2];
1933 dv[3] = pkmn->dv[3];
1934 dvChanged();
1935
1936 clearMoves();
1937
1938 for(int i = 0; i < 4; i++) {
1939 moves[i]->moveID = pkmn->moves[i]->moveID;
1940 moves[i]->moveIDChanged();
1941
1942 moves[i]->pp = pkmn->moves[i]->pp;
1943 moves[i]->ppChanged();
1944
1945 moves[i]->ppUp = pkmn->moves[i]->ppUp;
1946 moves[i]->ppUpChanged();
1947 }
1948
1949 movesChanged();
1950
1951 type2Explicit = false;
1953}
1954
1956{
1957 return PokemonDB::inst()->getIndAt(QString::number(species));
1958}
1959
1961{
1962 int ret = 0;
1963
1964 // Follows game logic
1965 // The first move with 0 ends move lookup
1966 for(int i = 0; i < 4; i++) {
1967 if(moves[i]->moveID <= 0)
1968 break;
1969
1970 ret++;
1971 }
1972
1973 return ret;
1974}
1975
1977{
1978 return maxMoves;
1979}
1980
1982{
1983 return qmlCppOwned(moves[ind]);
1984}
1985
1987{
1988 return maxDV;
1989}
1990
1992{
1993 return dv[ind];
1994}
1995
1996void PokemonBox::dvSet(int ind, int val)
1997{
1998 dv[ind] = val;
1999 dvChanged();
2000}
2001
2003{
2004 update(true, true, true, true);
2005}
2006
2008{
2009 update(true, true);
2010}
2011
2016
2021
2023{
2025}
2026
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