Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
pokemonoverviewmodel.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2026 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
21
22#include <algorithm>
23#include <QHash>
24#include <QSet>
25#include <QCollator>
26#include <QVariantList>
27
33
34namespace {
35// Render the species name markup the same way the Pokedex / storage grid do, so a
36// tooltip name reads "Nidoran ♂" rather than "NIDORAN<m>" (gender markers can show
37// up in either case; Mr.Mime gets its space). Presentation only -- a real custom
38// nickname won't match these patterns, so it's left untouched.
39QString fixSpeciesMarkup(QString s)
40{
41 s.replace(QStringLiteral("<f>"), QStringLiteral(" ♀"));
42 s.replace(QStringLiteral("<F>"), QStringLiteral(" ♀"));
43 s.replace(QStringLiteral("<m>"), QStringLiteral(" ♂"));
44 s.replace(QStringLiteral("<M>"), QStringLiteral(" ♂"));
45 s.replace(QStringLiteral("Mr.Mime"), QStringLiteral("Mr. Mime"));
46 return s;
47}
48}
49
51 : party(party),
52 storage(storage),
53 basics(basics)
54{
55 // Stay live: any add/remove/move on the party or any box re-aggregates. The box
56 // pointers are stable for the save's lifetime, so connect once here. (Species /
57 // nickname / OT edits happen in the detail editor and don't emit pokemonChanged
58 // on the box -- the QML pane rebuilds the table on open to cover those.)
59 if(this->party != nullptr)
61
62 if(this->storage != nullptr) {
63 for(int i = 0; i < storage->boxCount(); ++i) {
64 auto box = storage->boxAt(i);
65 if(box != nullptr)
67 }
68 }
69
70 rebuild();
71}
72
73int PokemonOverviewModel::rowCount(const QModelIndex& parent) const
74{
75 Q_UNUSED(parent)
76 return rows.size();
77}
78
80{
81 return colLabels;
82}
83
84QVariant PokemonOverviewModel::data(const QModelIndex& index, int role) const
85{
86 if(!index.isValid() || index.row() < 0 || index.row() >= rows.size())
87 return QVariant();
88
89 const Row& r = rows.at(index.row());
90
91 if(role == NameRole)
92 return r.name;
93
94 if(role == CountsRole) {
95 QVariantList out;
96 out.reserve(r.cells.size());
97 for(const Cell& c : r.cells)
98 out.append(c.count);
99 return out;
100 }
101
102 if(role == TooltipsRole) {
103 QVariantList out;
104 out.reserve(r.cells.size());
105 for(const Cell& c : r.cells)
106 out.append(c.tooltip);
107 return out;
108 }
109
110 return QVariant();
111}
112
113QHash<int, QByteArray> PokemonOverviewModel::roleNames() const
114{
115 QHash<int, QByteArray> roles;
116 roles[NameRole] = "speciesName";
117 roles[CountsRole] = "counts";
118 roles[TooltipsRole] = "tooltips";
119 return roles;
120}
121
122PokemonOverviewModel::Cell PokemonOverviewModel::buildCell(PokemonStorageBox* box, int speciesId) const
123{
124 Cell cell;
125 if(box == nullptr)
126 return cell;
127
128 // Gather every mon of this species in this box, tallying caught vs traded and
129 // collecting the distinct nicknames (mons whose nickname differs from the
130 // species name -- PokemonBox::hasNickname).
131 QStringList nicknames;
132 int others = 0;
133 int caught = 0;
134 int traded = 0;
135
136 for(auto mon : box->pokemon) {
137 if(mon == nullptr || mon->species != speciesId)
138 continue;
139
140 cell.count++;
141
142 if(mon->hasTradeStatus(basics))
143 traded++;
144 else
145 caught++;
146
147 if(mon->hasNickname())
148 nicknames.append(fixSpeciesMarkup(mon->nickname));
149 else
150 others++;
151 }
152
153 if(cell.count == 0)
154 return cell;
155
156 // ---- Build the tooltip -------------------------------------------------------
157 // Line 1 (names): the differing nicknames spelled out, then an "...and xN
158 // other(s)" tail for the un-nicknamed remainder. When NOTHING is nicknamed the
159 // line is omitted (the cell already shows the count, and line 2 splits it).
160 QString line1;
161 if(!nicknames.isEmpty()) {
162 line1 = nicknames.join(QStringLiteral(", "));
163 if(others > 0)
164 line1 += QStringLiteral(", and ×%1 %2")
165 .arg(others)
166 .arg(others == 1 ? QStringLiteral("other") : QStringLiteral("others"));
167 }
168
169 // Line 2 (ownership): the caught/traded split. Shown ONLY when at least one mon
170 // is traded -- if they're all caught (the common case) the split adds nothing, so
171 // it's omitted (Twilight). When shown, each side appears only if non-zero, so an
172 // all-traded cell reads "×N traded".
173 QString line2;
174 if(traded > 0) {
175 QStringList own;
176 if(caught > 0)
177 own.append(QStringLiteral("×%1 caught").arg(caught));
178 own.append(QStringLiteral("×%1 traded").arg(traded));
179 line2 = own.join(QStringLiteral(", "));
180 }
181
182 // No nicknames AND nothing traded -> nothing worth a tooltip; leave it empty so
183 // the view shows no tooltip at all on that cell.
184 if(line1.isEmpty())
185 cell.tooltip = line2;
186 else if(line2.isEmpty())
187 cell.tooltip = line1;
188 else
189 cell.tooltip = line1 + QStringLiteral("\n") + line2;
190
191 return cell;
192}
193
195{
196 beginResetModel();
197 rows.clear();
198 colBoxes.clear();
199 colLabels.clear();
200
201 // ---- Columns: the party first, then ONLY the boxes that hold Pokemon. --------
202 colBoxes.append(party);
203 colLabels.append(QStringLiteral("Party"));
204
205 if(storage != nullptr) {
206 for(int i = 0; i < storage->boxCount(); ++i) {
207 auto box = storage->boxAt(i);
208 if(box != nullptr && box->pokemonCount() > 0) {
209 colBoxes.append(box);
210 colLabels.append(QStringLiteral("Box %1").arg(i + 1));
211 }
212 }
213 }
214
215 // ---- Species universe: every distinct species id present in any column. ------
216 // Keyed by raw species id (so two ids that happen to share a display name stay
217 // distinct rows, like the items overview keys by index); name + dex resolved once.
218 QSet<int> speciesIds;
219 QHash<int, QString> names;
220 QHash<int, int> dexes;
221
222 for(auto box : colBoxes) {
223 if(box == nullptr)
224 continue;
225 for(auto mon : box->pokemon) {
226 if(mon == nullptr)
227 continue;
228 int id = mon->species;
229 speciesIds.insert(id);
230 if(!names.contains(id)) {
231 names[id] = mon->speciesName();
232 dexes[id] = mon->dexNum();
233 }
234 }
235 }
236
237 // ---- Rows: one per species, a cell per column. -------------------------------
238 for(int id : speciesIds) {
239 Row row;
240 row.name = names.value(id, QStringLiteral("???"));
241 row.dex = dexes.value(id, 0);
242 row.id = id;
243 row.cells.reserve(colBoxes.size());
244 for(auto box : colBoxes)
245 row.cells.append(buildCell(box, id));
246 rows.append(row);
247 }
248
249 applySort();
250
251 endResetModel();
253}
254
255void PokemonOverviewModel::applySort()
256{
257 // The Pokedex screen's sort orders (PokedexModel): dex number, alphabetical,
258 // internal id. Stable, deterministic ties (id) so the order never wobbles.
259 if(sortSelect == SortName) {
260 QCollator collator;
261 collator.setNumericMode(true);
262 collator.setIgnorePunctuation(true);
263 std::sort(rows.begin(), rows.end(), [&collator](const Row& a, const Row& b) {
264 int c = collator.compare(a.name, b.name);
265 return (c != 0) ? (c < 0) : (a.id < b.id);
266 });
267 }
268 else if(sortSelect == SortInternal) {
269 std::sort(rows.begin(), rows.end(), [](const Row& a, const Row& b) {
270 return a.id < b.id;
271 });
272 }
273 else { // SortDex (default fallback)
274 std::sort(rows.begin(), rows.end(), [](const Row& a, const Row& b) {
275 return (a.dex != b.dex) ? (a.dex < b.dex) : (a.id < b.id);
276 });
277 }
278}
279
281{
282 // Mirror PokedexModel::dexSortCycle -- advance through the three orders, wrapping
283 // past the sentinels.
284 sortSelect++;
285 if(sortSelect >= SortEnd)
286 sortSelect = SortBegin + 1;
287
288 beginResetModel();
289 applySort();
290 endResetModel();
291
293}
294
296{
297 switch(sortSelect) {
298 case SortDex: return QStringLiteral("Dex order");
299 case SortName: return QStringLiteral("Alphabetical");
300 case SortInternal: return QStringLiteral("Internal order");
301 default: return QString();
302 }
303}
304
306{
307 switch(sortSelect) {
308 case SortDex: return QStringLiteral("qrc:/assets/icons/sort/pokedex.png");
309 case SortName: return QStringLiteral("qrc:/assets/icons/sort/alphabetical.png");
310 case SortInternal: return QStringLiteral("qrc:/assets/icons/sort/internal.png");
311 default: return QString();
312 }
313}
The trainer's headline values: name, ID, money, coins, badges, starter.
void sortCycle()
Advance to the next species sort order (re-sorts in place).
@ SortInternal
Internal / creation (raw species id) order.
@ SortName
Alphabetical by species name.
@ SortDex
Pokedex number order.
virtual QHash< int, QByteArray > roleNames() const override
Role -> QML name.
protected::void columnsChanged()
@ CountsRole
Per-column counts (QVariantList<int>, aligned to columns).
@ TooltipsRole
Per-column hover tooltips (QVariantList<QString>; empty when count 0).
@ NameRole
Species display name.
virtual int rowCount(const QModelIndex &parent) const override
Row count.
virtual QVariant data(const QModelIndex &index, int role) const override
Row+role value.
PokemonOverviewModel(PokemonStorageBox *party, Storage *storage, PlayerBasics *basics)
QStringList columns() const
void rebuild()
Re-aggregate the party + boxes (full model reset). Wired to pokemonChanged.
Holds contents of a single Pokemon storage box.
void pokemonChanged()
Box contents changed.
QVector< PokemonBox * > pokemon
The stored mons.
int pokemonCount()
Number of mons present.
The PC: the item storage box and all 12 Pokemon boxes.
Definition storage.h:49