Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions forge-game/src/main/java/forge/game/player/PlayerFactoryUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import forge.game.card.Card;
import forge.game.card.CardFactoryUtil;
import forge.game.keyword.Hexproof;
import forge.game.keyword.KeywordInterface;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
Expand All @@ -12,23 +13,28 @@ public class PlayerFactoryUtil {
public static void addStaticAbility(final KeywordInterface inst, final Player player) {
String keyword = inst.getOriginal();

if (keyword.startsWith("Hexproof")) {
final StringBuilder sbDesc = new StringBuilder("Hexproof");
if (keyword.startsWith("Hexproof")
&& inst instanceof Hexproof hexproof) {
final StringBuilder sbValid = new StringBuilder();

if (!keyword.equals("Hexproof")) {
final String[] k = keyword.split(":");

sbDesc.append(" from ").append(k[2]);
sbValid.append("| ValidSource$ ").append(k[1]);
if (!hexproof.getValidType().isEmpty()) {
sbValid.append("| ValidSource$ ")
.append(hexproof.getValidType());
}

String effect = "Mode$ CantTarget | ValidTarget$ Player.You | Secondary$ True "
+ sbValid.toString() + " | Activator$ Opponent | EffectZone$ Command | Description$ "
+ sbDesc.toString() + " (" + inst.getReminderText() + ")";
String effect = "Mode$ CantTarget"
+ " | ValidTarget$ Player.You"
+ " | Secondary$ True "
+ sbValid
+ " | Activator$ Opponent"
+ " | EffectZone$ Command"
+ " | Description$ "
+ inst.getTitle()
+ " (" + inst.getReminderText() + ")";

final Card card = player.getKeywordCard();
inst.addStaticAbility(StaticAbility.create(effect, card, card.getCurrentState(), false));
inst.addStaticAbility(StaticAbility.create(
effect, card, card.getCurrentState(), false));
} else if (keyword.equals("Shroud")) {
String effect = "Mode$ CantTarget | ValidTarget$ Player.You | Secondary$ True "
+ "| EffectZone$ Command | Description$ Shroud (" + inst.getReminderText() + ")";
Expand Down
34 changes: 34 additions & 0 deletions forge-gui-mobile/src/forge/adventure/player/AdventurePlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,40 @@ public int copyDeck() {
return -1;
}

/**
* Finds the first empty deck slot, expanding if needed.
* Returns -1 only if all 99 slots are occupied.
*/
public int findFirstEmptySlot() {
for (int i = 0; i < maxDeckCount; i++) {
if (i >= getDeckCount()) addDeck();
if (isEmptyDeck(i)) return i;
}
if (getDeckCount() < 99) {
maxDeckCount = Math.min(maxDeckCount + 1, 99);
addDeck();
return getDeckCount() - 1;
}
return -1;
}

/**
* Replaces the contents of a deck slot with an imported deck.
* Clears all existing sections and copies from the source.
*/
public void importIntoSlot(int slot, Deck importedDeck) {
Deck target = getDeck(slot);
for (DeckSection section : DeckSection.values()) {
if (target.has(section)) target.get(section).clear();
}
for (java.util.Map.Entry<DeckSection, CardPool> entry : importedDeck) {
target.getOrCreate(entry.getKey()).addAll(entry.getValue());
}
if (importedDeck.getName() != null && !importedDeck.getName().isEmpty()) {
target.setName(importedDeck.getName());
}
}

private void ensureDeckLoadoutsSize() {
while (deckLoadouts.size() < getDeckCount()) {
deckLoadouts.add(null);
Expand Down
140 changes: 140 additions & 0 deletions forge-gui-mobile/src/forge/adventure/scene/DeckSelectScene.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
import forge.Forge;
import forge.adventure.player.AdventurePlayer;
import forge.adventure.stage.GameHUD;
import forge.adventure.util.CardUtil;
import forge.adventure.util.Controls;
import forge.adventure.util.Current;
import forge.deck.io.DeckSerializer;
import forge.util.FileUtil;

import java.io.File;

public class DeckSelectScene extends UIScene {
private final IntMap<TextraButton> buttons = new IntMap<>();
private final IntMap<Label> labels = new IntMap<>();
Color defColor;
TextField textInput;
TextField filePathInput;
Table layout;
TextraLabel header;
TextraButton back, edit, rename, add;
Expand Down Expand Up @@ -49,6 +55,17 @@ public DeckSelectScene() {
this.layoutDeckButtons();

textInput = Controls.newTextField("");
filePathInput = Controls.newTextField("");

root.row();
Table fileOpsTable = new Table();
fileOpsTable.add(Controls.newTextButton("Import Deck", this::importDeck)).pad(2).expandX().fillX();
fileOpsTable.add(Controls.newTextButton("Export Deck", this::exportDeck)).pad(2).expandX().fillX();
fileOpsTable.row();
fileOpsTable.add(Controls.newTextButton("Export Collection", this::exportCollection)).pad(2).expandX().fillX();
fileOpsTable.add(Controls.newTextButton("Mark for Sale", this::markForSale)).pad(2).expandX().fillX();
root.add(fileOpsTable).colspan(2).fillX();

back = ui.findActor("return");
edit = ui.findActor("edit");
rename = ui.findActor("rename");
Expand Down Expand Up @@ -242,4 +259,127 @@ private void edit() {
editScene.loadEvent(null);
Forge.switchScene(editScene);
}

private void importDeck() {
Dialog modeDialog = new Dialog("Import Deck", Controls.getSkin());
modeDialog.getContentTable().add(Controls.newTextraLabel("Choose import mode:"));
modeDialog.button(Controls.newTextButton("Give missing (free)", () -> {
removeDialog();
showFilePathDialog("Import Deck - File Path", path -> {
File file = CardUtil.resolveFilePath(path);
if (!file.exists()) {
showResultDialog("Import Deck", "File not found: " + file.getAbsolutePath());
return;
}
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.GIVE_MISSING);
if (!result.success) {
showResultDialog("Import Deck", "Import failed: " + result.message);
} else {
refreshDeckButtons();
select(result.slot);
showResultDialog("Import Deck", String.format("Imported '%s' into slot %d (%d cards added to collection)",
result.deckName, result.slot + 1, result.cardsAdded));
}
});
}));
modeDialog.button(Controls.newTextButton("Buy missing cards", () -> {
removeDialog();
showFilePathDialog("Import Deck - File Path", path -> {
File file = CardUtil.resolveFilePath(path);
if (!file.exists()) {
showResultDialog("Import Deck", "File not found: " + file.getAbsolutePath());
return;
}
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.BUY_MISSING);
if (!result.success) {
showResultDialog("Import Deck", "Import failed: " + result.message);
} else {
refreshDeckButtons();
select(result.slot);
showResultDialog("Import Deck", String.format("Imported '%s' into slot %d (%d cards purchased)",
result.deckName, result.slot + 1, result.cardsAdded));
}
});
}));
modeDialog.button(Controls.newTextButton("Check only (report)", () -> {
removeDialog();
showFilePathDialog("Import Deck - File Path", path -> {
File file = CardUtil.resolveFilePath(path);
if (!file.exists()) {
showResultDialog("Import Deck", "File not found: " + file.getAbsolutePath());
return;
}
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.REPORT_ONLY);
showResultDialog("Import Deck", result.formatMissingReport());
});
}));
showDialog(modeDialog);
}

private void exportDeck() {
showFilePathDialog("Export Deck - Save Path", path -> {
File file = CardUtil.resolveFilePath(path);
try {
DeckSerializer.writeDeck(Current.player().getSelectedDeck(), file);
showResultDialog("Export Deck", "Saved deck to " + file.getAbsolutePath());
} catch (Exception e) {
showResultDialog("Export Deck", "Save failed: " + e.getMessage());
}
});
}

private void exportCollection() {
showFilePathDialog("Export Collection - Save Path", path -> {
File file = CardUtil.resolveFilePath(path);
try {
String arenaList = CardUtil.exportCollectionAsArena(Current.player());
FileUtil.writeFile(file, arenaList);
showResultDialog("Export Collection", String.format("Exported %d unique cards to %s (Arena format)",
Current.player().getCards().countDistinct(), file.getAbsolutePath()));
} catch (Exception e) {
showResultDialog("Export Collection", "Export failed: " + e.getMessage());
}
});
}

private void markForSale() {
showFilePathDialog("Mark for Sale - File Path", path -> {
File file = CardUtil.resolveFilePath(path);
if (!file.exists()) {
showResultDialog("Mark for Sale", "File not found: " + file.getAbsolutePath());
return;
}
CardUtil.MarkSellResult result = CardUtil.markCardsForSale(file, Current.player());
if (!result.success) {
showResultDialog("Mark for Sale", "Mark failed: " + result.message);
} else {
showResultDialog("Mark for Sale", String.format("Marked %d cards for sale (%d skipped: in decks/already marked/not owned)",
result.marked, result.skipped));
}
});
}

private void showFilePathDialog(String title, java.util.function.Consumer<String> onConfirm) {
filePathInput.setText("");
Dialog dialog = createGenericDialog(title, null,
Forge.getLocalizer().getMessage("lblOK"),
Forge.getLocalizer().getMessage("lblAbort"), () -> {
String path = filePathInput.getText();
removeDialog();
if (path != null && !path.trim().isEmpty()) {
onConfirm.accept(path.trim());
}
}, this::removeDialog);
dialog.getContentTable().add(Controls.newLabel("File path:")).align(Align.left).colspan(2);
dialog.getContentTable().row();
dialog.getContentTable().add(filePathInput).fillX().expandX().colspan(2);
dialog.getContentTable().row();
showDialog(dialog);
}

private void showResultDialog(String title, String message) {
Dialog dialog = createGenericDialog(title, message,
Forge.getLocalizer().getMessage("lblOK"), null, this::removeDialog, null);
showDialog(dialog);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
import forge.model.CardBlock;
import forge.model.FModel;
import forge.screens.CoverScreen;
import forge.deck.io.DeckSerializer;
import forge.util.Aggregates;
import forge.util.FileUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -561,5 +564,61 @@ private ConsoleCommandInterpreter() {

return "Exit the map to reset it.";
});
registerCommand(new String[]{"load", "deck"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
if (!file.exists()) return "File not found: " + file.getAbsolutePath();
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.GIVE_MISSING);
if (!result.success) return "Import failed: " + result.message;
return String.format("Imported '%s' into slot %d (%d cards added to collection)",
result.deckName, result.slot + 1, result.cardsAdded);
});
registerCommand(new String[]{"load", "deck", "buy"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
if (!file.exists()) return "File not found: " + file.getAbsolutePath();
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.BUY_MISSING);
if (!result.success) return "Import failed: " + result.message;
return String.format("Imported '%s' into slot %d (%d cards purchased)",
result.deckName, result.slot + 1, result.cardsAdded);
});
registerCommand(new String[]{"check", "deck"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
if (!file.exists()) return "File not found: " + file.getAbsolutePath();
CardUtil.ImportResult result = CardUtil.importDeckFromFile(file, Current.player(), CardUtil.ImportMode.REPORT_ONLY);
return result.formatMissingReport();
});
registerCommand(new String[]{"save", "deck"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
try {
DeckSerializer.writeDeck(Current.player().getSelectedDeck(), file);
return "Saved deck to " + file.getAbsolutePath();
} catch (Exception e) {
return "Save failed: " + e.getMessage();
}
});
registerCommand(new String[]{"export", "collection"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
try {
String arenaList = CardUtil.exportCollectionAsArena(Current.player());
FileUtil.writeFile(file, arenaList);
return String.format("Exported %d unique cards to %s (Arena format)",
Current.player().getCards().countDistinct(), file.getAbsolutePath());
} catch (Exception e) {
return "Export failed: " + e.getMessage();
}
});
registerCommand(new String[]{"mark", "sell"}, s -> {
if (s.length < 1) return "Command needs 1 parameter: file path.";
File file = CardUtil.resolveFilePath(s[0]);
if (!file.exists()) return "File not found: " + file.getAbsolutePath();
CardUtil.MarkSellResult result = CardUtil.markCardsForSale(file, Current.player());
if (!result.success) return "Mark failed: " + result.message;
return String.format("Marked %d cards for sale (%d skipped: in decks/already marked/not owned)",
result.marked, result.skipped);
});
}
}
Loading