ff9mapkit 1.0.0b3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""The Build & Deploy document for the Workspace (Phase 6b) -- the tkinter ff9_build_gui, folded in.
|
|
2
|
+
|
|
3
|
+
Pick a project file; its kind is auto-detected (:func:`..editor.jobs.detect_kind`) and the matching target
|
|
4
|
+
panel shows: a single field (test slot / install to game / build to a folder), a whole campaign
|
|
5
|
+
(deploy / build-only), a multi-campaign journey (dry-run playbook / one-shot deploy / re-apply links), or a
|
|
6
|
+
battle map (deploy + optional trigger field). **Check** validates in-process (structured Problems); **Build /
|
|
7
|
+
Deploy / Revert** stream through the shell's ``run_job`` into the Output panel. Only this view is Qt --
|
|
8
|
+
detection + argv are jobs.py, verdicts are editor.feedback, and the deploys are the same ``tools/deploy_*.py``
|
|
9
|
+
the CLI loop uses (the journey path = ``tools/deploy_journey.py``, the orchestrator above deploy_campaign).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from PySide6.QtWidgets import (
|
|
17
|
+
QButtonGroup, QCheckBox, QFileDialog, QFrame, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
|
|
18
|
+
QPushButton, QRadioButton, QScrollArea, QVBoxLayout, QWidget,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ..editor import feedback as fb
|
|
22
|
+
from ..editor import jobs
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BuildDoc(QWidget):
|
|
26
|
+
"""Build / deploy a field, campaign, journey, or battle map, as a Workspace document. ``run`` =
|
|
27
|
+
``shell.run_job`` (streams a subprocess to Output + posts a verdict); ``problems`` =
|
|
28
|
+
``shell._show_problems`` (the in-process Check verdict + problems list)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, pal, repo_root, *, run, problems):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.pal = pal
|
|
33
|
+
self.repo = Path(repo_root)
|
|
34
|
+
self.kit = self.repo / "ff9mapkit" # `-m ff9mapkit build` cwd (local pkg shadows)
|
|
35
|
+
self._run = run
|
|
36
|
+
self._problems = problems
|
|
37
|
+
self.kind = "field"
|
|
38
|
+
self.plan = None # the campaign plan when kind == "campaign"
|
|
39
|
+
self.manifest = None # the journey manifest when kind == "journey"
|
|
40
|
+
self.field_id = None
|
|
41
|
+
self.field_name = None
|
|
42
|
+
self.mod_folder, self.worktree_id = jobs.detect_deploy_target(self.repo)
|
|
43
|
+
self.game_mod = jobs.detect_game_mod()
|
|
44
|
+
self._build_ui()
|
|
45
|
+
self._render_kind()
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------ UI
|
|
48
|
+
def _build_ui(self):
|
|
49
|
+
# SCROLL the body: five target group boxes + the New-Game box stack tall, so a short window would
|
|
50
|
+
# cram them and inflate the central minimum height (blocking the bottom Output dock from growing).
|
|
51
|
+
outer = QVBoxLayout(self)
|
|
52
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
53
|
+
scroll = QScrollArea()
|
|
54
|
+
scroll.setWidgetResizable(True)
|
|
55
|
+
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
56
|
+
inner = QWidget()
|
|
57
|
+
v = QVBoxLayout(inner)
|
|
58
|
+
v.setContentsMargins(14, 14, 14, 14)
|
|
59
|
+
v.setSpacing(10)
|
|
60
|
+
row = QHBoxLayout()
|
|
61
|
+
row.addWidget(QLabel("Project file:"))
|
|
62
|
+
self.path = QLineEdit()
|
|
63
|
+
self.path.setPlaceholderText("a .field.toml, campaign.toml, journeys.toml, or battle.toml")
|
|
64
|
+
self.path.textChanged.connect(self._on_path)
|
|
65
|
+
browse = QPushButton("Browse…")
|
|
66
|
+
browse.clicked.connect(self.browse)
|
|
67
|
+
row.addWidget(self.path, 1)
|
|
68
|
+
row.addWidget(browse)
|
|
69
|
+
v.addLayout(row)
|
|
70
|
+
|
|
71
|
+
self.status = QLabel("Pick a field, campaign, journey, or battle file.")
|
|
72
|
+
self.status.setWordWrap(True)
|
|
73
|
+
self.status.setStyleSheet(f"color:{self.pal['muted']};")
|
|
74
|
+
v.addWidget(self.status)
|
|
75
|
+
|
|
76
|
+
v.addWidget(self._field_box())
|
|
77
|
+
v.addWidget(self._campaign_box())
|
|
78
|
+
v.addWidget(self._journey_box())
|
|
79
|
+
v.addWidget(self._battle_box())
|
|
80
|
+
v.addWidget(self._newgame_box())
|
|
81
|
+
|
|
82
|
+
btns = QHBoxLayout()
|
|
83
|
+
self.chk = QPushButton("Check logic")
|
|
84
|
+
self.chk.clicked.connect(self.on_check)
|
|
85
|
+
self.go = QPushButton("Build / Deploy")
|
|
86
|
+
self.go.clicked.connect(self.on_go)
|
|
87
|
+
self.rev = QPushButton("Revert test deploy")
|
|
88
|
+
self.rev.clicked.connect(self.on_revert)
|
|
89
|
+
btns.addWidget(self.chk)
|
|
90
|
+
btns.addWidget(self.go)
|
|
91
|
+
btns.addWidget(self.rev)
|
|
92
|
+
btns.addStretch(1)
|
|
93
|
+
v.addLayout(btns)
|
|
94
|
+
v.addStretch(1)
|
|
95
|
+
scroll.setWidget(inner)
|
|
96
|
+
outer.addWidget(scroll)
|
|
97
|
+
|
|
98
|
+
def _field_box(self):
|
|
99
|
+
box = QGroupBox("Build to (field)")
|
|
100
|
+
gv = QVBoxLayout(box)
|
|
101
|
+
self.tg = QButtonGroup(self)
|
|
102
|
+
tid = self.worktree_id or 4003
|
|
103
|
+
self.rb_test = QRadioButton(f"Test slot {tid} — quick + reversible; play via F6 → Warp"
|
|
104
|
+
+ (" (or New Game → hut door)" if tid == 4003 else ""))
|
|
105
|
+
self.rb_test.setChecked(True)
|
|
106
|
+
self.rb_game = QRadioButton(f"Install to game (shipping mod folder): {self.game_mod}"
|
|
107
|
+
if self.game_mod else "Install to game — (game install not found)")
|
|
108
|
+
if not self.game_mod:
|
|
109
|
+
self.rb_game.setEnabled(False)
|
|
110
|
+
of = QHBoxLayout()
|
|
111
|
+
self.rb_other = QRadioButton("Build only — to a folder:")
|
|
112
|
+
self.other = QLineEdit()
|
|
113
|
+
ob = QPushButton("Browse…")
|
|
114
|
+
ob.clicked.connect(self.browse_other)
|
|
115
|
+
of.addWidget(self.rb_other)
|
|
116
|
+
of.addWidget(self.other, 1)
|
|
117
|
+
of.addWidget(ob)
|
|
118
|
+
for rb in (self.rb_test, self.rb_game, self.rb_other):
|
|
119
|
+
self.tg.addButton(rb)
|
|
120
|
+
rb.toggled.connect(self._update_dest)
|
|
121
|
+
self.other.textChanged.connect(self._update_dest)
|
|
122
|
+
gv.addWidget(self.rb_test)
|
|
123
|
+
gv.addWidget(self.rb_game)
|
|
124
|
+
gv.addLayout(of)
|
|
125
|
+
self.dest = QLabel("")
|
|
126
|
+
self.dest.setWordWrap(True)
|
|
127
|
+
self.dest.setStyleSheet(f"color:{self.pal['accent']};")
|
|
128
|
+
gv.addWidget(self.dest)
|
|
129
|
+
self.field_box = box
|
|
130
|
+
return box
|
|
131
|
+
|
|
132
|
+
def _campaign_box(self):
|
|
133
|
+
box = QGroupBox("Deploy campaign")
|
|
134
|
+
cv = QVBoxLayout(box)
|
|
135
|
+
self.cg = QButtonGroup(self)
|
|
136
|
+
self.rb_camp_deploy = QRadioButton("Deploy to game (reversible)")
|
|
137
|
+
self.rb_camp_deploy.setChecked(True)
|
|
138
|
+
self.rb_camp_build = QRadioButton("Build only — compile every member to the campaign's dist/")
|
|
139
|
+
self.cg.addButton(self.rb_camp_deploy)
|
|
140
|
+
self.cg.addButton(self.rb_camp_build)
|
|
141
|
+
self.wire_newgame = QCheckBox("Wire New Game entry (experimental — off = reach the chain via F6 → Warp)")
|
|
142
|
+
cv.addWidget(self.rb_camp_deploy)
|
|
143
|
+
cv.addWidget(self.rb_camp_build)
|
|
144
|
+
cv.addWidget(self.wire_newgame)
|
|
145
|
+
self.campaign_box = box
|
|
146
|
+
return box
|
|
147
|
+
|
|
148
|
+
def _journey_box(self):
|
|
149
|
+
box = QGroupBox("Deploy journey")
|
|
150
|
+
jv = QVBoxLayout(box)
|
|
151
|
+
self.jg = QButtonGroup(self)
|
|
152
|
+
self.rb_jour_preview = QRadioButton("Preview deploy playbook (dry-run — no game files touched)")
|
|
153
|
+
self.rb_jour_preview.setChecked(True)
|
|
154
|
+
self.rb_jour_apply = QRadioButton("Deploy journey to game (one-shot: campaigns → links → hub, reversible)")
|
|
155
|
+
self.rb_jour_links = QRadioButton("Re-apply cross-campaign links only (after a campaign re-deploy)")
|
|
156
|
+
for rb in (self.rb_jour_preview, self.rb_jour_apply, self.rb_jour_links):
|
|
157
|
+
self.jg.addButton(rb)
|
|
158
|
+
rb.toggled.connect(self._update_journey_hint)
|
|
159
|
+
jv.addWidget(rb)
|
|
160
|
+
# Single mod folder: merge the whole journey into ONE FolderNames entry (one-shot deploy only)
|
|
161
|
+
self.cb_single_folder = QCheckBox("Single mod folder — merge the whole journey into ONE FolderNames "
|
|
162
|
+
"entry (instead of one folder per campaign)")
|
|
163
|
+
self.cb_single_folder.setToolTip("Cleaner one-folder install. Trade-off: re-deploying re-merges the "
|
|
164
|
+
"whole journey — you lose cheap per-campaign re-deploy.")
|
|
165
|
+
jv.addWidget(self.cb_single_folder)
|
|
166
|
+
# New-Game landing: meaningful only for the one-shot deploy (single-owner) -> disabled otherwise
|
|
167
|
+
self.ng_group = QGroupBox("New Game landing (one-shot deploy — single-owner)")
|
|
168
|
+
ngv = QVBoxLayout(self.ng_group)
|
|
169
|
+
self.ngg = QButtonGroup(self)
|
|
170
|
+
self.rb_ng_none = QRadioButton("Don't wire New Game — reach the hub via F6 → Warp")
|
|
171
|
+
self.rb_ng_none.setChecked(True)
|
|
172
|
+
self.rb_ng_hub = QRadioButton("Wire New Game → the hub menu (pick the journey at Mognet; seamless)")
|
|
173
|
+
self.rb_ng_entry = QRadioButton("Wire New Game → straight into the opening (no menu; keeps the real FMV)")
|
|
174
|
+
self.rb_ng_entry.setToolTip("Single-journey arc only — a multi-journey hub has no single opening to land in.")
|
|
175
|
+
for rb in (self.rb_ng_none, self.rb_ng_hub, self.rb_ng_entry):
|
|
176
|
+
self.ngg.addButton(rb)
|
|
177
|
+
ngv.addWidget(rb)
|
|
178
|
+
self.ng_group.setEnabled(False)
|
|
179
|
+
jv.addWidget(self.ng_group)
|
|
180
|
+
self.journey_hint = QLabel("")
|
|
181
|
+
self.journey_hint.setWordWrap(True)
|
|
182
|
+
self.journey_hint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
183
|
+
jv.addWidget(self.journey_hint)
|
|
184
|
+
self.journey_box = box
|
|
185
|
+
return box
|
|
186
|
+
|
|
187
|
+
def _newgame_box(self):
|
|
188
|
+
# always-visible: point New Game straight at a deployed field id (the hub-less single destination).
|
|
189
|
+
box = QGroupBox("New Game entry (skip the hub — land straight on a field)")
|
|
190
|
+
gv = QVBoxLayout(box)
|
|
191
|
+
row = QHBoxLayout()
|
|
192
|
+
row.addWidget(QLabel("Field id:"))
|
|
193
|
+
self.newgame_id = QLineEdit()
|
|
194
|
+
self.newgame_id.setFixedWidth(90)
|
|
195
|
+
self.newgame_id.setPlaceholderText("4100")
|
|
196
|
+
self.set_ng = QPushButton("Point New Game here")
|
|
197
|
+
self.set_ng.clicked.connect(self.on_set_newgame)
|
|
198
|
+
self.rev_ng = QPushButton("Revert New Game")
|
|
199
|
+
self.rev_ng.clicked.connect(self.on_revert_newgame)
|
|
200
|
+
row.addWidget(self.newgame_id)
|
|
201
|
+
row.addWidget(self.set_ng)
|
|
202
|
+
row.addWidget(self.rev_ng)
|
|
203
|
+
row.addStretch(1)
|
|
204
|
+
gv.addLayout(row)
|
|
205
|
+
hint = QLabel("Single-owner: CREATES the field-70 override from stock (opening FMV preserved) and "
|
|
206
|
+
"replaces the current New-Game landing (skips any World Hub) — works even on a clean "
|
|
207
|
+
"install or a fresh region fork. The field must already be DEPLOYED/registered. Relaunch "
|
|
208
|
+
"to test.")
|
|
209
|
+
hint.setWordWrap(True)
|
|
210
|
+
hint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
211
|
+
gv.addWidget(hint)
|
|
212
|
+
self.newgame_box = box
|
|
213
|
+
return box
|
|
214
|
+
|
|
215
|
+
def _battle_box(self):
|
|
216
|
+
box = QGroupBox("Deploy battle map")
|
|
217
|
+
bv = QVBoxLayout(box)
|
|
218
|
+
self.battle_dest = QLabel(f"This worktree's mod folder: {self.mod_folder}")
|
|
219
|
+
self.battle_dest.setStyleSheet(f"color:{self.pal['muted']};")
|
|
220
|
+
bv.addWidget(self.battle_dest)
|
|
221
|
+
tf = QHBoxLayout()
|
|
222
|
+
tf.addWidget(QLabel("Trigger field (optional):"))
|
|
223
|
+
self.trigger = QLineEdit()
|
|
224
|
+
self.trigger.setFixedWidth(90)
|
|
225
|
+
tf.addWidget(self.trigger)
|
|
226
|
+
self.trigger_hint = QLabel("repoint a deployed field's encounter at the minted scene (mint only).")
|
|
227
|
+
self.trigger_hint.setWordWrap(True)
|
|
228
|
+
self.trigger_hint.setStyleSheet(f"color:{self.pal['muted']};")
|
|
229
|
+
tf.addWidget(self.trigger_hint, 1)
|
|
230
|
+
bv.addLayout(tf)
|
|
231
|
+
self.battle_box = box
|
|
232
|
+
return box
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------------ kind detection + rendering
|
|
235
|
+
def crumb_label(self):
|
|
236
|
+
"""A short 'you are deploying X' label for the breadcrumb when the Build & Deploy tab is active --
|
|
237
|
+
the detected kind + the target file name (or a no-target hint)."""
|
|
238
|
+
p = self.path.text().strip().strip('"')
|
|
239
|
+
return f"{self.kind} · {Path(p).name}" if p else "no build target"
|
|
240
|
+
|
|
241
|
+
def set_target(self, path):
|
|
242
|
+
"""Point the doc at a project file (the shell calls this when a campaign/field opens, so Build &
|
|
243
|
+
Deploy is pre-aimed at what you're working on)."""
|
|
244
|
+
self.path.setText(str(path))
|
|
245
|
+
|
|
246
|
+
def _on_path(self, _text=None):
|
|
247
|
+
path = self.path.text().strip().strip('"')
|
|
248
|
+
kind, payload = ("field", None)
|
|
249
|
+
if path and Path(path).is_file():
|
|
250
|
+
kind, payload = jobs.detect_kind(path)
|
|
251
|
+
self.kind = kind
|
|
252
|
+
self.plan = payload if kind == "campaign" else None
|
|
253
|
+
self.manifest = payload if kind == "journey" else None
|
|
254
|
+
if kind == "field":
|
|
255
|
+
self.field_id, self.field_name = jobs.field_id_name(path) if path else (None, None)
|
|
256
|
+
if self.field_id is not None and not self.newgame_id.text().strip():
|
|
257
|
+
self.newgame_id.setText(str(self.field_id)) # convenience: prefill the New-Game target once
|
|
258
|
+
self._render_kind()
|
|
259
|
+
|
|
260
|
+
def _render_kind(self):
|
|
261
|
+
self.field_box.setVisible(self.kind == "field")
|
|
262
|
+
self.campaign_box.setVisible(self.kind == "campaign")
|
|
263
|
+
self.journey_box.setVisible(self.kind == "journey")
|
|
264
|
+
self.battle_box.setVisible(self.kind == "battle")
|
|
265
|
+
if self.kind == "campaign" and self.plan is not None:
|
|
266
|
+
ids = [m.new_id for m in self.plan.members]
|
|
267
|
+
rng = f"{min(ids)}-{max(ids)}" if ids else "?"
|
|
268
|
+
self.status.setText(f"Campaign '{self.plan.name}': {len(self.plan.members)} fields "
|
|
269
|
+
f"(ids {rng}) → {self.plan.mod_folder}")
|
|
270
|
+
self.go.setText("Build / Deploy campaign")
|
|
271
|
+
self.rev.setText("Revert campaign")
|
|
272
|
+
self.rb_camp_deploy.setText(f"Deploy to game (reversible) → {self.plan.mod_folder}")
|
|
273
|
+
elif self.kind == "journey" and self.manifest is not None:
|
|
274
|
+
m = self.manifest
|
|
275
|
+
hub_id = m.hub.get("id") if m.hub else None
|
|
276
|
+
name = (m.hub.get("name") if m.hub else None) or Path(self.path.text().strip()).stem
|
|
277
|
+
self.status.setText(f"Journey '{name}': {len(m.journeys)} journey(s), hub field {hub_id} "
|
|
278
|
+
"→ each campaign stacks into its own mod folder.")
|
|
279
|
+
self.go.setText("Build / Deploy journey")
|
|
280
|
+
self.rev.setText("Revert journey")
|
|
281
|
+
self._update_journey_hint()
|
|
282
|
+
elif self.kind == "battle":
|
|
283
|
+
deployed = jobs.detect_deployed_fields(self.mod_folder)
|
|
284
|
+
avail = ("deployed: " + ", ".join(f"{i} ({n})" for i, n in deployed) + " — ") if deployed \
|
|
285
|
+
else "no fields deployed here yet — "
|
|
286
|
+
self.trigger_hint.setText(avail + "repoint a deployed field's encounter at the minted scene "
|
|
287
|
+
"so you can fight it now (mint only; blank otherwise).")
|
|
288
|
+
self.status.setText(f"Battle map: {Path(self.path.text().strip()).name} → {self.mod_folder}")
|
|
289
|
+
self.go.setText("Build / Deploy battle")
|
|
290
|
+
self.rev.setText("Revert battle")
|
|
291
|
+
else:
|
|
292
|
+
self.go.setText("Build / Deploy")
|
|
293
|
+
self.rev.setText("Revert test deploy")
|
|
294
|
+
p = self.path.text().strip()
|
|
295
|
+
if p and self.field_id is not None:
|
|
296
|
+
self.status.setText(f"Field: {self.field_name or Path(p).stem} (its own id: {self.field_id})"
|
|
297
|
+
f" — {Path(p).name}")
|
|
298
|
+
elif p:
|
|
299
|
+
self.status.setText(f"Field project: {Path(p).name}")
|
|
300
|
+
else:
|
|
301
|
+
self.status.setText("Pick a field, campaign, journey, or battle file.")
|
|
302
|
+
self._update_dest()
|
|
303
|
+
|
|
304
|
+
def _update_dest(self, *_):
|
|
305
|
+
if self.kind != "field":
|
|
306
|
+
return
|
|
307
|
+
tid = self.worktree_id or 4003
|
|
308
|
+
own = self.field_id if self.field_id is not None else "?"
|
|
309
|
+
if self.rb_test.isChecked():
|
|
310
|
+
msg = (f"→ deploys to field {tid} in {self.mod_folder} (this worktree's test slot; reversible). "
|
|
311
|
+
f"Your field's own id ({own}) is overridden — reach it via F6 → Warp to {tid}.")
|
|
312
|
+
elif self.rb_game.isChecked():
|
|
313
|
+
where = self.game_mod or "(game install not found)"
|
|
314
|
+
msg = f"→ installs at field {own} (the field's OWN id) in {where} — overwrites any field {own} there."
|
|
315
|
+
else:
|
|
316
|
+
folder = self.other.text().strip() or "(pick a folder)"
|
|
317
|
+
msg = f"→ builds field {own} into {folder} — no game change."
|
|
318
|
+
self.dest.setText(msg)
|
|
319
|
+
|
|
320
|
+
def _journey_newgame_mode(self) -> str:
|
|
321
|
+
"""The selected New-Game landing for the one-shot deploy: ``"hub"`` / ``"entry"`` / ``"none"``."""
|
|
322
|
+
if self.rb_ng_hub.isChecked():
|
|
323
|
+
return "hub"
|
|
324
|
+
if self.rb_ng_entry.isChecked():
|
|
325
|
+
return "entry"
|
|
326
|
+
return "none"
|
|
327
|
+
|
|
328
|
+
def _update_journey_hint(self, *_):
|
|
329
|
+
if self.kind != "journey":
|
|
330
|
+
return
|
|
331
|
+
apply_on = self.rb_jour_apply.isChecked()
|
|
332
|
+
if self.rb_jour_preview.isChecked():
|
|
333
|
+
msg = ("→ lints the manifest + prints the ordered deploy playbook. No game files are touched — "
|
|
334
|
+
"safe to run anytime; review the steps, then switch to 'Deploy journey to game'.")
|
|
335
|
+
elif self.rb_jour_links.isChecked():
|
|
336
|
+
msg = ("→ re-applies ONLY the cross-campaign link .eb remaps (run after a campaign re-deploy "
|
|
337
|
+
"wholesale-replaces its folder and wipes the links). The campaigns must already be deployed.")
|
|
338
|
+
elif self.cb_single_folder.isChecked():
|
|
339
|
+
msg = ("→ one-shot, SINGLE FOLDER: build every campaign + the hub, MERGE them into one stacked mod "
|
|
340
|
+
"folder (one Memoria.ini entry), apply the cross-campaign links, optional New Game — one "
|
|
341
|
+
"unified revert. Cleaner install; re-deploying re-merges the whole journey.")
|
|
342
|
+
else:
|
|
343
|
+
msg = ("→ one-shot: each campaign → its own stacked folder, the cross-campaign links, then the hub "
|
|
344
|
+
"field — one unified revert. You then stack the folders in Memoria.ini and relaunch once.")
|
|
345
|
+
self.cb_single_folder.setEnabled(apply_on)
|
|
346
|
+
self.ng_group.setEnabled(apply_on)
|
|
347
|
+
# "straight into the opening" needs a single-journey manifest (a multi-journey hub has no single opening)
|
|
348
|
+
single = self.manifest is not None and len(self.manifest.journeys) == 1
|
|
349
|
+
self.rb_ng_entry.setEnabled(apply_on and single)
|
|
350
|
+
if not single and self.rb_ng_entry.isChecked():
|
|
351
|
+
self.rb_ng_none.setChecked(True)
|
|
352
|
+
self.journey_hint.setText(msg)
|
|
353
|
+
|
|
354
|
+
# ------------------------------------------------------------------ pickers
|
|
355
|
+
def browse(self):
|
|
356
|
+
f, _ = QFileDialog.getOpenFileName(
|
|
357
|
+
self, "Pick a field.toml, campaign.toml, journeys.toml, or battle.toml", self.path.text().strip(),
|
|
358
|
+
"Field / campaign / journey / battle (*.toml);;All files (*)")
|
|
359
|
+
if f:
|
|
360
|
+
self.path.setText(f)
|
|
361
|
+
|
|
362
|
+
def browse_other(self):
|
|
363
|
+
d = QFileDialog.getExistingDirectory(self, "Output folder")
|
|
364
|
+
if d:
|
|
365
|
+
self.other.setText(d)
|
|
366
|
+
self.rb_other.setChecked(True)
|
|
367
|
+
|
|
368
|
+
# ------------------------------------------------------------------ helpers
|
|
369
|
+
def _confirm(self, title, text):
|
|
370
|
+
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
|
|
371
|
+
|
|
372
|
+
def _warn(self, title, text):
|
|
373
|
+
QMessageBox.warning(self, title, text)
|
|
374
|
+
|
|
375
|
+
def _info(self, title, text):
|
|
376
|
+
QMessageBox.information(self, title, text)
|
|
377
|
+
|
|
378
|
+
def _picked(self):
|
|
379
|
+
f = self.path.text().strip().strip('"')
|
|
380
|
+
if not f or not Path(f).is_file():
|
|
381
|
+
self._warn("No file", "Pick a .field.toml, campaign.toml, journeys.toml, or battle.toml first.")
|
|
382
|
+
return None
|
|
383
|
+
return f
|
|
384
|
+
|
|
385
|
+
def _busy(self, b):
|
|
386
|
+
for w in (self.chk, self.go, self.rev, self.set_ng, self.rev_ng):
|
|
387
|
+
w.setEnabled(not b)
|
|
388
|
+
|
|
389
|
+
def _stream(self, argv, *, cwd, subject, ok_headline, ok_next=""):
|
|
390
|
+
self._busy(True)
|
|
391
|
+
if not self._run(argv, cwd=cwd, subject=subject, ok_headline=ok_headline, ok_next=ok_next,
|
|
392
|
+
on_finished=lambda _c: self._busy(False)):
|
|
393
|
+
self._busy(False) # a job was already running; nothing started
|
|
394
|
+
|
|
395
|
+
# ------------------------------------------------------------------ Check (in-process, structured)
|
|
396
|
+
def on_check(self):
|
|
397
|
+
f = self._picked()
|
|
398
|
+
if not f:
|
|
399
|
+
return
|
|
400
|
+
if self.kind == "campaign":
|
|
401
|
+
self._check_campaign(f)
|
|
402
|
+
elif self.kind == "journey":
|
|
403
|
+
self._check_journey(f)
|
|
404
|
+
elif self.kind == "battle":
|
|
405
|
+
self._check_battle(f)
|
|
406
|
+
else:
|
|
407
|
+
self._check_field(f)
|
|
408
|
+
|
|
409
|
+
def _verdict(self, errs, warns, *, subject, clean):
|
|
410
|
+
self._problems(fb.classify(errs, warns, subject=subject, clean_headline=clean),
|
|
411
|
+
fb.problems(errs, warns))
|
|
412
|
+
|
|
413
|
+
def _check_field(self, field):
|
|
414
|
+
try:
|
|
415
|
+
from ..build import FieldProject, lint_logic, validate
|
|
416
|
+
p = FieldProject.load(field)
|
|
417
|
+
self._verdict(validate(p), lint_logic(p), subject=f"Check {Path(field).name}",
|
|
418
|
+
clean=f"{Path(field).name} — no problems")
|
|
419
|
+
except Exception as e: # noqa: BLE001
|
|
420
|
+
self._verdict([f"{type(e).__name__}: {e}"], [], subject="Check", clean="")
|
|
421
|
+
|
|
422
|
+
def _check_campaign(self, path):
|
|
423
|
+
try:
|
|
424
|
+
from ..campaign import lint_campaign, load_campaign
|
|
425
|
+
plan = load_campaign(path)
|
|
426
|
+
errs, warns = lint_campaign(plan, Path(path).parent)
|
|
427
|
+
self._verdict(errs, warns, subject=f"Campaign lint ({plan.name})", clean=f"{plan.name} — no problems")
|
|
428
|
+
except Exception as e: # noqa: BLE001
|
|
429
|
+
self._verdict([f"{type(e).__name__}: {e}"], [], subject="Campaign lint", clean="")
|
|
430
|
+
|
|
431
|
+
def _check_journey(self, path):
|
|
432
|
+
try:
|
|
433
|
+
from ..journey import lint_manifest, load_journeys
|
|
434
|
+
m = load_journeys(path) # re-load from disk (the file may have changed)
|
|
435
|
+
errs, warns = lint_manifest(m)
|
|
436
|
+
name = (m.hub.get("name") if m.hub else None) or Path(path).stem
|
|
437
|
+
self._verdict(errs, warns, subject=f"Journey lint ({name})", clean=f"{name} — no problems")
|
|
438
|
+
except Exception as e: # noqa: BLE001
|
|
439
|
+
self._verdict([f"{type(e).__name__}: {e}"], [], subject="Journey lint", clean="")
|
|
440
|
+
|
|
441
|
+
def _check_battle(self, battle):
|
|
442
|
+
try:
|
|
443
|
+
from ..battle.build import BattleProject, validate_battle
|
|
444
|
+
p = BattleProject.load(battle)
|
|
445
|
+
self._verdict(validate_battle(p), [], subject=f"Check {Path(battle).name}",
|
|
446
|
+
clean=f"{Path(battle).name} — no problems")
|
|
447
|
+
except Exception as e: # noqa: BLE001
|
|
448
|
+
self._verdict([f"{type(e).__name__}: {e}"], [], subject="Battle check", clean="")
|
|
449
|
+
|
|
450
|
+
# ------------------------------------------------------------------ Build / Deploy
|
|
451
|
+
def on_go(self):
|
|
452
|
+
f = self._picked()
|
|
453
|
+
if not f:
|
|
454
|
+
return
|
|
455
|
+
if self.kind == "campaign":
|
|
456
|
+
self._go_campaign(f)
|
|
457
|
+
elif self.kind == "journey":
|
|
458
|
+
self._go_journey(f)
|
|
459
|
+
elif self.kind == "battle":
|
|
460
|
+
self._go_battle(f)
|
|
461
|
+
else:
|
|
462
|
+
self._go_field(f)
|
|
463
|
+
|
|
464
|
+
def _go_field(self, field):
|
|
465
|
+
if self.rb_test.isChecked():
|
|
466
|
+
tid = self.worktree_id or 4003
|
|
467
|
+
reach = ("New Game → walk to the hut door (or F6 → Warp)" if tid == 4003
|
|
468
|
+
else f"F6 → Warp to field {tid}")
|
|
469
|
+
if self._confirm(f"Deploy to test field {tid}",
|
|
470
|
+
f"Build and deploy this field to the test slot {tid} ({self.mod_folder})? "
|
|
471
|
+
"It replaces whatever is there now (reversible)."):
|
|
472
|
+
self._stream(jobs.deploy_field_argv(self.repo, field), cwd=self.repo,
|
|
473
|
+
subject=f"Deploy to test field {tid}",
|
|
474
|
+
ok_headline=f"Deployed to test field {tid} ({self.mod_folder})",
|
|
475
|
+
ok_next=f"In-game: {reach}.")
|
|
476
|
+
elif self.rb_game.isChecked():
|
|
477
|
+
if self._confirm("Install to game",
|
|
478
|
+
f"Build this field into the game mod folder?\n\n{self.game_mod}\n\n"
|
|
479
|
+
"Writes the field at its real id (may overwrite a field with the same id)."):
|
|
480
|
+
self._stream(jobs.build_argv(field, str(self.game_mod)), cwd=self.kit,
|
|
481
|
+
subject="Install to game", ok_headline=f"Built into {self.game_mod}")
|
|
482
|
+
else:
|
|
483
|
+
out = self.other.text().strip()
|
|
484
|
+
if not out:
|
|
485
|
+
return self._warn("No folder", "Pick an output folder.")
|
|
486
|
+
self._stream(jobs.build_argv(field, out), cwd=self.kit, subject="Build",
|
|
487
|
+
ok_headline=f"Built into {out}")
|
|
488
|
+
|
|
489
|
+
def _go_campaign(self, path):
|
|
490
|
+
if self.rb_camp_build.isChecked():
|
|
491
|
+
self._stream(jobs.build_campaign_argv(path), cwd=self.kit, subject="Build campaign",
|
|
492
|
+
ok_headline=f"Built campaign {self.plan.name}")
|
|
493
|
+
return
|
|
494
|
+
wire = self.wire_newgame.isChecked()
|
|
495
|
+
route = ("It also wires New Game to enter the chain (experimental)." if wire
|
|
496
|
+
else "Reach each screen in-game via F6 → Warp.")
|
|
497
|
+
if self._confirm("Deploy campaign",
|
|
498
|
+
f"Reversibly install campaign '{self.plan.name}' ({len(self.plan.members)} fields) "
|
|
499
|
+
f"into:\n\n{self.plan.mod_folder}\n\n{route}"):
|
|
500
|
+
ids = [m.new_id for m in self.plan.members]
|
|
501
|
+
entry = self.plan.members[0].new_id if self.plan.members else (min(ids) if ids else "?")
|
|
502
|
+
self._stream(jobs.deploy_campaign_argv(self.repo, path, wire_newgame=wire), cwd=self.repo,
|
|
503
|
+
subject="Deploy campaign",
|
|
504
|
+
ok_headline=f"Deployed campaign '{self.plan.name}' → {self.plan.mod_folder}",
|
|
505
|
+
ok_next=f"Relaunch once (new DictionaryPatch), then F6 → Warp → {entry} to walk the chain.")
|
|
506
|
+
|
|
507
|
+
def _go_journey(self, path):
|
|
508
|
+
if self.rb_jour_preview.isChecked(): # dry-run: print the playbook, no game writes -> no confirm
|
|
509
|
+
self._stream(jobs.deploy_journey_argv(self.repo, path), cwd=self.repo,
|
|
510
|
+
subject="Journey deploy playbook (dry-run)",
|
|
511
|
+
ok_headline="Printed the journey deploy playbook (no game files touched)",
|
|
512
|
+
ok_next="Review the ordered steps above, then choose 'Deploy journey to game' to run them.")
|
|
513
|
+
return
|
|
514
|
+
if self.rb_jour_links.isChecked():
|
|
515
|
+
if self._confirm("Re-apply cross-campaign links",
|
|
516
|
+
"Re-apply ONLY the cross-campaign link .eb rewrites?\n\nRun this after re-deploying "
|
|
517
|
+
"a campaign — deploy_campaign wholesale-replaces its folder, wiping the boundary "
|
|
518
|
+
"links. The campaigns must already be deployed."):
|
|
519
|
+
self._stream(jobs.deploy_journey_argv(self.repo, path, apply_links=True), cwd=self.repo,
|
|
520
|
+
subject="Re-apply journey links",
|
|
521
|
+
ok_headline="Re-applied the cross-campaign links",
|
|
522
|
+
ok_next="Relaunch and playtest the campaign boundary.")
|
|
523
|
+
return
|
|
524
|
+
mode = self._journey_newgame_mode()
|
|
525
|
+
single = self.cb_single_folder.isChecked()
|
|
526
|
+
name = (self.manifest.hub.get("name") if self.manifest and self.manifest.hub else None) or Path(path).stem
|
|
527
|
+
njourneys = len(self.manifest.journeys) if self.manifest else "?"
|
|
528
|
+
route = {"hub": "New Game will land on the hub MENU (single-owner — replaces the current New-Game target).",
|
|
529
|
+
"entry": "New Game will land STRAIGHT in the opening field, no menu (single-owner — replaces the "
|
|
530
|
+
"current target; keeps the real opening FMV).",
|
|
531
|
+
"none": "New Game is left UNCHANGED — reach the hub via F6 → Warp."}[mode]
|
|
532
|
+
layout = ("MERGED into ONE stacked mod folder (a single FolderNames entry)" if single
|
|
533
|
+
else "every campaign into its own stacked mod folder")
|
|
534
|
+
folders_note = ("Reversible via one unified revert. You then add the ONE merged folder to Memoria.ini "
|
|
535
|
+
"(remove the journey's old per-campaign folders) and relaunch once." if single else
|
|
536
|
+
"Reversible via one unified revert. You must then STACK the folders in Memoria.ini and "
|
|
537
|
+
"relaunch once.")
|
|
538
|
+
if self._confirm("Deploy journey",
|
|
539
|
+
f"Deploy journey '{name}' ({njourneys} journey(s)) in one shot — {layout}, the "
|
|
540
|
+
f"cross-campaign links, then the hub field?\n\n{route}\n\n{folders_note}"):
|
|
541
|
+
reach = {"hub": "New Game → the hub menu", "entry": "New Game → straight into the opening",
|
|
542
|
+
"none": "F6 → Warp to the hub"}[mode]
|
|
543
|
+
stackmsg = (f"Add the ONE merged folder to Memoria.ini [Mod] FolderNames (drop the old per-campaign "
|
|
544
|
+
f"ones), relaunch once, then {reach}. Playtest." if single else
|
|
545
|
+
f"Stack every campaign + hub folder in Memoria.ini [Mod] FolderNames, relaunch once, "
|
|
546
|
+
f"then {reach}. Playtest.")
|
|
547
|
+
self._stream(jobs.deploy_journey_argv(self.repo, path, apply=True, newgame=mode, single_folder=single),
|
|
548
|
+
cwd=self.repo, subject="Deploy journey",
|
|
549
|
+
ok_headline=f"Deployed journey '{name}'" + (" (single folder)" if single else ""),
|
|
550
|
+
ok_next=stackmsg)
|
|
551
|
+
|
|
552
|
+
def _go_battle(self, battle):
|
|
553
|
+
trig = self.trigger.text().strip()
|
|
554
|
+
if trig and not trig.isdigit():
|
|
555
|
+
return self._warn("Bad trigger field", "Trigger field must be a field id number (or blank).")
|
|
556
|
+
tmsg = (f"\n\nAlso repoint field {trig}'s encounter at the minted scene." if trig else "")
|
|
557
|
+
if self._confirm("Deploy battle map",
|
|
558
|
+
f"Build and deploy this battle map into:\n\n{self.mod_folder}\n\n"
|
|
559
|
+
"Replaces any prior deploy of the same map (reversible). A minted scene or a "
|
|
560
|
+
"BattlePatch line needs one relaunch." + tmsg):
|
|
561
|
+
self._stream(jobs.deploy_battle_argv(self.repo, battle, trigger=trig or None), cwd=self.repo,
|
|
562
|
+
subject="Deploy battle map",
|
|
563
|
+
ok_headline=f"Deployed battle map → {self.mod_folder}",
|
|
564
|
+
ok_next="A minted scene / BattlePatch line needs one relaunch; a texture/FBX override "
|
|
565
|
+
"loads on the next battle.")
|
|
566
|
+
|
|
567
|
+
# ------------------------------------------------------------------ New Game entry (hub-less)
|
|
568
|
+
def on_set_newgame(self):
|
|
569
|
+
fid = self.newgame_id.text().strip()
|
|
570
|
+
if not fid.isdigit():
|
|
571
|
+
return self._warn("Bad field id", "Enter the numeric field id New Game should land on "
|
|
572
|
+
"(e.g. a deployed slice's entry, 4100).")
|
|
573
|
+
if self._confirm("Point New Game here",
|
|
574
|
+
f"Point New Game straight at field {fid}?\n\nThis CREATES the field-70 override from "
|
|
575
|
+
"stock (the opening FMV is preserved) and REPLACES the current New-Game landing "
|
|
576
|
+
"(single-owner), skipping any World Hub. Works even on a clean install / a fresh fork. "
|
|
577
|
+
"The field must already be deployed/registered; relaunch the game to test."):
|
|
578
|
+
self._stream(jobs.newgame_from_stock_argv(self.repo, fid), cwd=self.repo, subject="Set New Game entry",
|
|
579
|
+
ok_headline=f"New Game now lands on field {fid}",
|
|
580
|
+
ok_next="Relaunch the game, then New Game. Undo with 'Revert New Game'.")
|
|
581
|
+
|
|
582
|
+
def on_revert_newgame(self):
|
|
583
|
+
argv = jobs.revert_newgame_argv(self.repo) # most-recent New-Game revert (from-stock OR retarget)
|
|
584
|
+
if argv is None or not Path(argv[-1]).exists():
|
|
585
|
+
return self._info("Nothing to revert", "No New-Game change to undo yet.")
|
|
586
|
+
if self._confirm("Revert New Game", "Restore the previous New-Game landing?"):
|
|
587
|
+
self._stream(argv, cwd=self.repo, subject="Revert New Game",
|
|
588
|
+
ok_headline="Reverted the New-Game retarget",
|
|
589
|
+
ok_next="Relaunch to load the restored New-Game landing.")
|
|
590
|
+
|
|
591
|
+
# ------------------------------------------------------------------ Revert
|
|
592
|
+
def on_revert(self):
|
|
593
|
+
if self.kind == "battle":
|
|
594
|
+
argv, what = jobs.revert_battle_argv(self.repo), "battle"
|
|
595
|
+
elif self.kind == "campaign":
|
|
596
|
+
argv, what = jobs.revert_campaign_argv(self.repo), "campaign"
|
|
597
|
+
elif self.kind == "journey":
|
|
598
|
+
argv = jobs.revert_journey_argv(self.repo) # the MOST RECENT journey revert (full or links-only)
|
|
599
|
+
what = ("journey links" if argv and Path(argv[-1]).name == "revert_journey_links.py" else "journey")
|
|
600
|
+
else:
|
|
601
|
+
argv, what = jobs.revert_field_argv(self.repo), "test field"
|
|
602
|
+
if argv is None or not Path(argv[-1]).exists():
|
|
603
|
+
return self._info("Nothing to revert", f"No {what} deploy to undo yet.")
|
|
604
|
+
if self._confirm(f"Revert {what}", f"Restore the game to before the last {what} deploy?"):
|
|
605
|
+
self._stream(argv, cwd=self.repo, subject=f"Revert {what}",
|
|
606
|
+
ok_headline=f"Reverted the last {what} deploy",
|
|
607
|
+
ok_next="Relaunch the game to load the restored state.")
|