Merge branch 'permadeath' into nut_arulir_mountain

This commit is contained in:
Gonk
2019-10-30 22:47:13 +01:00
18 changed files with 503 additions and 72 deletions

View File

@@ -80,6 +80,38 @@
android:layout_height="wrap_content"
android:inputType="textPersonName" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:background="?attr/ui_theme_stdframe_bitmap"
android:clickable="true"
android:gravity="bottom|start"
android:orientation="vertical" >
<TextView
android:id="@+id/startscreen_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/startscreen_game_mode" />
</LinearLayout>
<LinearLayout
style="@style/AndorsTrail_Style_StdFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:orientation="horizontal" >
<Button
android:id="@+id/startscreen_mode_selector_button"
style="@style/AndorsTrail_Style_SpinnerEmulator"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="left"
android:text="" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"

View File

@@ -8,6 +8,16 @@
<item>@string/questlog_includecompleted_onlycompleted</item>
</string-array>
<!-- Order is hardcoded within StartScreenActivity_NewGame-->
<string-array name="startscreen_mode_selector">
<item>Unlimited saves and lives</item>
<item>Unlimited lives</item>
<item>50 lives</item>
<item>10 lives</item>
<item>3 lives</item>
<item>1 life (hardcore mode)</item>
</string-array>
<!-- Order is hardcoded within HeroInfoActivity_inventory-->
<string-array name="inventorylist_category_filters">
<item>@string/inventory_category_all</item>

View File

@@ -19,11 +19,16 @@
<string name="dialog_loading_failed_title">Load Failed</string>
<string name="dialog_loading_failed_message">Andor\'s Trail was unable to load the savegame file.\n\n:(\n\nThe file may be damaged or incomplete.</string>
<string name="dialog_loading_failed_incorrectversion">Andor\'s Trail was unable to load the savegame file. This savegame file is created with a newer version than what is currently running.</string>
<string name="dialog_loading_failed_cheat">Andor\'s Trail was unable to load the savegame file. This savegame file has already been continued.</string>
<string name="dialog_recenter">Recenter</string>
<string name="dialog_close">Close</string>
<string name="dialog_more">More</string>
<string name="dialog_game_over_title">Game over</string>
<string name="dialog_game_over_text">You take your last breath and die.</string>
<string name="rip_startscreen">(RIP)</string>
<string name="dialog_monsterencounter_title">Encounter</string>
<string name="dialog_monsterencounter_message">Do you want to attack?\nDifficulty: %1$s</string>
<string name="dialog_monsterencounter_info">Info</string>
@@ -50,6 +55,13 @@
<string name="heroinfo_experiencepoints">Experience points (XP):</string>
<string name="heroinfo_actionpoints">Action points (AP):</string>
<string name="heroinfo_quests">Quests</string>
<string name="heroinfo_unlimited_lives_and_saves">unlimited lives and saves</string>
<string name="heroinfo_unlimited_lives">unlimited lives</string>
<string name="heroinfo_hardcore_mode">1 life (hardcore mode)</string>
<string name="heroinfo_lives_left">%1$d/%2$d lives left</string>
<string name="heroinfo_unlimited_saves">, unlimited saves</string>
<string name="combat_attack">Attack (%1$d AP)</string>
<string name="combat_move">Move (%1$d AP)</string>
@@ -143,6 +155,13 @@
<string name="startscreen_selectherosprite">Choose your hero</string>
<string name="startscreen_enterheroname">Enter hero name</string>
<string name="startscreen_load">Load</string>
<string name="startscreen_game_mode">Mode</string>
<string name="startscreen_load_game">Load game</string>
<string name="startscreen_load_game_confirm">The current game is unsaved and you will loose your character.</string>
<string name="startscreen_error_loading_game">Error loading game</string>
<string name="startscreen_error_loading_empty_slot">Can\'t load from an empty slot.</string>
<string name="startscreen_attention_slot_gets_delete_on_load">Attention</string>
<string name="startscreen_attention_message_slot_gets_delete_on_load">Loading this game deletes its save slot. You will have to save again before switching to another game.</string>
<!-- <string name="conversation_title">%1$s says</string> -->
<string name="conversation_rewardexp"> [You gained %1$d experience]</string>
@@ -448,10 +467,10 @@
<string name="skill_prerequisite_other_skill">To level up this skill, you need at least level %1$d of the %2$s skill.</string>
<string name="skill_prerequisite_level">To level up this skill, you need at least experience level %1$d.</string>
<string name="skill_prerequisite_stat">To level up this skill, you need at least %1$d %2$s (base stats).</string>
<string name="skill_number_of_increases_one">You may select one skill to improve.</string>
<string name="skill_number_of_increases_several">You may select %1$d skills to improve.</string>
<string name="levelup_adds_new_skillpoint">This level also gives you a new skill point to spend!</string>
<string name="skill_number_of_increases_one">You may select one skill to improve.</string>
<string name="skill_number_of_increases_several">You may select %1$d skills to improve.</string>
<string name="levelup_adds_new_skillpoint">This level also gives you a new skill point to spend!</string>
<string name="loadsave_save_to_new_slot">Create new savegame slot</string>
<string name="loadsave_save_overwrite_confirmation_title">Overwrite savegame?</string>
<string name="loadsave_save_overwrite_confirmation">This savegame contains a different player name (%1$s) than your current player name (%2$s). Are you sure you want to overwrite this savegame?</string>
@@ -470,7 +489,9 @@
<string name="traitsinfo_base_max_hp">Max HP:</string>
<string name="traitsinfo_base_max_ap">Max AP:</string>
<string name="menu_save_saving_not_allowed_in_combat">Cannot save the game while in combat.</string>
<string name="menu_save_saving_not_allowed_in_combat">Cannot save the game while in combat.</string>
<string name="menu_save_switch_character_title">Switch character</string>
<string name="menu_save_switch_character">Saving allows you to switch to another character and later continue the current game. Do you want to save and exit the current game?</string>
<string name="preferences_optimized_drawing_title">Optimized drawing</string>
<string name="preferences_optimized_drawing">Disable this if you see graphical artifacts. Enabling this option will make the game only redraw changed parts of the screen every frame.</string>
@@ -548,6 +569,8 @@
<string name="loadsave_save_overwrite_confirmation_all">Are you sure you want to overwrite this savegame?</string>
<string name="loadsave_save_overwrite_confirmation_slot">(slot %1$d)</string>
<string name="loadsave_empty_slot">%1$d.&lt;empty&gt;</string>
<string name="preferences_display_overwrite_savegame_entries_always_confirm">Always show confirmation dialog box</string>
<string name="preferences_display_overwrite_savegame_entries_confirm_overwrite">Only show when overwriting a different playername</string>
<string name="preferences_display_overwrite_savegame_entries_never_confirm">Never display confirmation dialog box</string>
@@ -604,7 +627,7 @@
<string name="skill_longdescription_weapon_prof_1hsword">For each skill level, increases attack chance of rapiers, longswords and broadswords by %1$d %% of the item\'s base attack chance, increases block chance by %2$d %% of the item\'s base block chance, and increases critical skill by %3$d %% of the item\'s base critical skill.</string>
<string name="skill_longdescription_weapon_prof_2hsword">For each skill level, increases attack chance of two-handed swords by %1$d %% of the item\'s base attack chance, increases block chance by %2$d %% of the item\'s base block chance, and increases critical skill by %3$d %% of the item\'s base critical skill.</string>
<string name="skill_longdescription_weapon_prof_axe">For each skill level, increases attack chance of axes and greataxes by %1$d %% of the item\'s base attack chance, increases block chance by %2$d %% of the item\'s base block chance, and increases critical skill by %3$d %% of the item\'s base critical skill.</string>
<string name="skill_longdescription_weapon_prof_blunt">For each skill level, increases attack chance of bludgeoning weapons by %1$d %% of the item\'s base attack chance, increases block chance by %2$d %% of the item\'s base block chance, and increases critical skill by %3$d %% of the item\'s base critical skill. This includes clubs, quarterstaves, maces, scepters, war hammers and giant hammers.</string>
<string name="skill_longdescription_weapon_prof_blunt">For each skill level, increases attack chance of bludgeoning weapons by %1$d %% of the item\'s base attack chance, increases block chance by %2$d %% of the item\'s base block chance, and increases critical skill by %3$d %% of the item\'s base critical skill. This includes clubs, quarterstaves, maces, scepters, war hammers and giant hammers.</string>
<string name="skill_longdescription_weapon_prof_unarmed">When fighting without a weapon and shield, gain %1$d attack chance, %2$d damage potential and %3$d block chance per skill level.</string>
<string name="skill_longdescription_armor_prof_shield">Increase damage resistance by %1$d per skill level while having a shield equipped.</string>
<string name="skill_longdescription_armor_prof_unarmored">While fighting without having any piece of armor equipped, gain %1$d block chance per skill level. Items made of cloth are not considered as being armor.</string>

View File

@@ -25,10 +25,10 @@ public final class AndorsTrailApplication extends Application {
public static final boolean DEVELOPMENT_FORCE_CONTINUEGAME = false;
public static final boolean DEVELOPMENT_DEBUGBUTTONS = true;
public static final boolean DEVELOPMENT_FASTSPEED = false;
public static final boolean DEVELOPMENT_VALIDATEDATA = false;
public static final boolean DEVELOPMENT_DEBUGMESSAGES = false;
public static final boolean DEVELOPMENT_VALIDATEDATA = true;
public static final boolean DEVELOPMENT_DEBUGMESSAGES = true;
public static final boolean DEVELOPMENT_INCOMPATIBLE_SAVEGAMES = DEVELOPMENT_DEBUGRESOURCES || DEVELOPMENT_DEBUGBUTTONS || DEVELOPMENT_FASTSPEED;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? 999 : 47;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? 999 : 48;
public static final String CURRENT_VERSION_DISPLAY = "0.7.7";
public static final boolean IS_RELEASE_VERSION = !CURRENT_VERSION_DISPLAY.matches(".*[a-d].*");

View File

@@ -244,6 +244,25 @@ public final class Dialogs {
});
}
public static void showHeroDied(final MainActivity mainActivity, final ControllerContext controllers) {
final Dialog d = CustomDialogFactory.createDialog(mainActivity,
mainActivity.getResources().getString(R.string.dialog_game_over_title),
mainActivity.getResources().getDrawable(R.drawable.ui_icon_combat),
mainActivity.getResources().getString(R.string.dialog_game_over_text),
null,
true);
CustomDialogFactory.addDismissButton(d, android.R.string.ok);
showDialogAndPause(d, controllers, new OnDismissListener() {
@Override
public void onDismiss(DialogInterface arg0) {
mainActivity.finish();
}
});
}
public static Intent getIntentForItemInfo(final Context ctx, String itemTypeID, ItemInfoActivity.ItemInfoAction actionType, String buttonText, boolean buttonEnabled, Inventory.WearSlot inventorySlot) {
Intent intent = new Intent(ctx, ItemInfoActivity.class);
intent.putExtra("buttonText", buttonText);
@@ -324,11 +343,33 @@ public final class Dialogs {
Toast.makeText(mainActivity, R.string.menu_save_saving_not_allowed_in_combat, Toast.LENGTH_SHORT).show();
return false;
}
controllerContext.gameRoundController.pause();
Intent intent = new Intent(mainActivity, LoadSaveActivity.class);
intent.setData(Uri.parse("content://com.gpl.rpg.AndorsTrail/save"));
mainActivity.startActivityForResult(intent, MainActivity.INTENTREQUEST_SAVEGAME);
return true;
if (!world.model.statistics.hasUnlimitedSaves()) {
final Dialog d = CustomDialogFactory.createDialog(mainActivity,
mainActivity.getResources().getString(R.string.menu_save_switch_character_title),
null,
mainActivity.getResources().getString(R.string.menu_save_switch_character),
null,
true);
CustomDialogFactory.addButton(d, android.R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View v) {
controllerContext.gameRoundController.pause();
Intent intent = new Intent(mainActivity, LoadSaveActivity.class);
intent.setData(Uri.parse("content://com.gpl.rpg.AndorsTrail/save"));
mainActivity.startActivityForResult(intent, MainActivity.INTENTREQUEST_SAVEGAME);
}
});
CustomDialogFactory.addDismissButton(d, android.R.string.cancel);
CustomDialogFactory.show(d);
return false;
} else {
controllerContext.gameRoundController.pause();
Intent intent = new Intent(mainActivity, LoadSaveActivity.class);
intent.setData(Uri.parse("content://com.gpl.rpg.AndorsTrail/save"));
mainActivity.startActivityForResult(intent, MainActivity.INTENTREQUEST_SAVEGAME);
return true;
}
}
public static void showLoad(final Activity currentActivity) {

View File

@@ -28,6 +28,8 @@ public final class WorldSetup {
public boolean isSceneReady = false;
public String newHeroName;
public int newHeroIcon;
public int newHeroStartLives;
public boolean newHeroUnlimitedSaves;
private Savegames.LoadSavegameResult loadResult;
public WorldSetup(WorldContext world, ControllerContext controllers, Context androidContext) {
@@ -149,7 +151,7 @@ public final class WorldSetup {
private void createNewWorld() {
Context ctx = androidContext.get();
world.model = new ModelContainer();
world.model = new ModelContainer(newHeroStartLives, newHeroUnlimitedSaves);
world.model.player.initializeNewPlayer(world.dropLists, newHeroName, newHeroIcon);
controllers.actorStatsController.recalculatePlayerStats(world.model.player);

View File

@@ -108,10 +108,23 @@ public final class LoadSaveActivity extends Activity implements OnClickListener
}
private void addSavegameSlotButtons(ViewGroup parent, LayoutParams params, List<Integer> usedSavegameSlots) {
int unused = 1;
for (int slot : usedSavegameSlots) {
final FileHeader header = Savegames.quickload(this, slot);
if (header == null) continue;
while (unused < slot){
Button b = new Button(this);
b.setLayoutParams(params);
b.setTag(unused);
b.setOnClickListener(this);
b.setText(getString(R.string.loadsave_empty_slot, unused));
tileManager.setImageViewTileForPlayer(getResources(), b, header.iconID);
parent.addView(b, params);
unused++;
}
unused++;
Button b = new Button(this);
b.setLayoutParams(params);
b.setTag(slot);
@@ -139,6 +152,7 @@ public final class LoadSaveActivity extends Activity implements OnClickListener
private String getConfirmOverwriteQuestion(int slot) {
if (isLoading) return null;
if (slot == SLOT_NUMBER_CREATE_NEW_SLOT) return null; // if we're creating a new slot
if (!Savegames.getSlotFile(slot).exists()) return null;
if (preferences.displayOverwriteSavegame == AndorsTrailPreferences.CONFIRM_OVERWRITE_SAVEGAME_ALWAYS) {
return getString(R.string.loadsave_save_overwrite_confirmation_all);
@@ -160,42 +174,73 @@ public final class LoadSaveActivity extends Activity implements OnClickListener
@Override
public void onClick(View view) {
final int slot = (Integer) view.getTag();
final String message = getConfirmOverwriteQuestion(slot);
if (message != null) {
final String title =
getString(R.string.loadsave_save_overwrite_confirmation_title) + ' '
+ getString(R.string.loadsave_save_overwrite_confirmation_slot, slot);
// new AlertDialog.Builder(this)
// .setIcon(android.R.drawable.ic_dialog_alert)
// .setTitle(title)
// .setMessage(message)
// .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
// @Override
// public void onClick(DialogInterface dialog, int which) {
// loadsave(slot);
// }
// })
// .setNegativeButton(android.R.string.no, null)
// .show();
final Dialog d = CustomDialogFactory.createDialog(this,
title,
getResources().getDrawable(android.R.drawable.ic_dialog_alert),
message,
null,
true);
CustomDialogFactory.addButton(d, android.R.string.yes, new View.OnClickListener() {
@Override
public void onClick(View v) {
loadsave(slot);
}
});
CustomDialogFactory.addDismissButton(d, android.R.string.no);
CustomDialogFactory.show(d);
if (isLoading) {
if(!Savegames.getSlotFile(slot).exists()) {
showErrorLoadingEmptySlot();
} else {
final FileHeader header = Savegames.quickload(this, slot);
if (header != null && !header.hasUnlimitedSaves) {
showSlotGetsDeletedOnLoadWarning(slot);
} else {
loadsave(slot);
}
}
} else {
loadsave(slot);
final String message = getConfirmOverwriteQuestion(slot);
if (message != null) {
showConfirmoverwriteQuestion(slot, message);
} else {
loadsave(slot);
}
}
}
private void showErrorLoadingEmptySlot() {
final Dialog d = CustomDialogFactory.createDialog(this,
getString(R.string.startscreen_error_loading_game),
getResources().getDrawable(android.R.drawable.ic_dialog_alert),
getString(R.string.startscreen_error_loading_empty_slot),
null,
true);
CustomDialogFactory.addDismissButton(d, android.R.string.ok);
CustomDialogFactory.show(d);
}
private void showSlotGetsDeletedOnLoadWarning(int slot) {
final Dialog d = CustomDialogFactory.createDialog(this,
getString(R.string.startscreen_attention_slot_gets_delete_on_load),
getResources().getDrawable(android.R.drawable.ic_dialog_alert),
getString(R.string.startscreen_attention_message_slot_gets_delete_on_load),
null,
true);
CustomDialogFactory.addButton(d, android.R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View v) {
loadsave(slot);
}
});
CustomDialogFactory.show(d);
}
private void showConfirmoverwriteQuestion(int slot, String message) {
final String title =
getString(R.string.loadsave_save_overwrite_confirmation_title) + ' '
+ getString(R.string.loadsave_save_overwrite_confirmation_slot, slot);
final Dialog d = CustomDialogFactory.createDialog(this,
title,
getResources().getDrawable(android.R.drawable.ic_dialog_alert),
message,
null,
true);
CustomDialogFactory.addButton(d, android.R.string.yes, new View.OnClickListener() {
@Override
public void onClick(View v) {
loadsave(slot);
}
});
CustomDialogFactory.addDismissButton(d, android.R.string.no);
CustomDialogFactory.show(d);
}
}

View File

@@ -28,7 +28,7 @@ public final class LoadingActivity extends Activity implements OnResourcesLoaded
private Dialog progressDialog;
private CloudsAnimatorView clouds_back, clouds_mid, clouds_front;
boolean loaded = false;
private Object semaphore = new Object();
@Override
@@ -158,6 +158,8 @@ public final class LoadingActivity extends Activity implements OnResourcesLoaded
}
if (loadResult == Savegames.LoadSavegameResult.savegameIsFromAFutureVersion) {
showLoadingFailedDialog(R.string.dialog_loading_failed_incorrectversion);
} else if (loadResult == Savegames.LoadSavegameResult.cheatingDetected) {
showLoadingFailedDialog(R.string.dialog_loading_failed_cheat);
} else {
showLoadingFailedDialog(R.string.dialog_loading_failed_message);
}

View File

@@ -149,6 +149,9 @@ public final class MainActivity
final int slot = data.getIntExtra("slot", 1);
if (save(slot)) {
Toast.makeText(this, getResources().getString(R.string.menu_save_gamesaved, slot), Toast.LENGTH_SHORT).show();
if (!world.model.statistics.hasUnlimitedSaves()) {
finish();
}
} else {
Toast.makeText(this, R.string.menu_save_failed, Toast.LENGTH_LONG).show();
}
@@ -188,9 +191,11 @@ public final class MainActivity
super.onResume();
if (!AndorsTrailApplication.getApplicationFromActivity(this).getWorldSetup().isSceneReady) return;
controllers.gameRoundController.resume();
updateStatus();
if (world.model.statistics.isDead()) this.finish();
else {
controllers.gameRoundController.resume();
updateStatus();
}
}
private void unsubscribeFromModel() {
@@ -452,7 +457,11 @@ public final class MainActivity
@Override
public void onPlayerDied(int lostExp) {
message(getString(R.string.combat_hero_dies, lostExp));
if (!world.model.statistics.isDead()) {
message(getString(R.string.combat_hero_dies, lostExp));
} else {
Dialogs.showHeroDied(this, controllers);
}
}
@Override

View File

@@ -19,6 +19,7 @@ import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.Dialogs;
import com.gpl.rpg.AndorsTrail.R;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.GameStatistics;
import com.gpl.rpg.AndorsTrail.model.actor.HeroCollection;
import com.gpl.rpg.AndorsTrail.model.actor.Player;
import com.gpl.rpg.AndorsTrail.model.item.Inventory;
@@ -35,6 +36,7 @@ public final class HeroinfoActivity_Stats extends Fragment {
private WorldContext world;
private Player player;
private GameStatistics statistics;
private View view;
private Button levelUpButton;
@@ -53,6 +55,7 @@ public final class HeroinfoActivity_Stats extends Fragment {
private TableLayout heroinfo_basestats_table;
private ViewGroup heroinfo_container;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -60,6 +63,7 @@ public final class HeroinfoActivity_Stats extends Fragment {
if (!app.isInitialized()) return;
this.world = app.getWorld();
this.player = world.model.player;
this.statistics = world.model.statistics;
}
@Override
@@ -72,7 +76,23 @@ public final class HeroinfoActivity_Stats extends Fragment {
TextView tv = (TextView) v.findViewById(R.id.heroinfo_title);
if (tv != null) {
tv.setText(player.getName());
String description = player.getName() + "\n";
final Resources res = getResources();
if (statistics.hasUnlimitedLives() && statistics.hasUnlimitedSaves()) {
description += res.getString(R.string.heroinfo_unlimited_lives_and_saves);
} else if (statistics.hasUnlimitedLives()) {
description += res.getString(R.string.heroinfo_unlimited_lives);
} else {
if (statistics.getStartLives() == 1 && !statistics.hasUnlimitedSaves()) {
description += res.getString(R.string.heroinfo_hardcore_mode);
} else {
description += res.getString(R.string.heroinfo_lives_left, statistics.getLivesLeft(), statistics.getStartLives());
if (statistics.hasUnlimitedSaves()) {
description += res.getString(R.string.heroinfo_unlimited_saves);
}
}
}
tv.setText(description);
tv.setCompoundDrawablesWithIntrinsicBounds(HeroCollection.getHeroLargeSprite(player.iconID), 0, 0, 0);
}
heroinfo_container = (ViewGroup) v.findViewById(R.id.heroinfo_container);

View File

@@ -106,7 +106,27 @@ public class StartScreenActivity_MainMenu extends Fragment {
startscreen_load.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
Dialogs.showLoad(StartScreenActivity_MainMenu.this);
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(getActivity());
if (hasExistingGame && app != null && app.getWorld() != null && app.getWorld().model != null
&& app.getWorld().model.statistics != null && !app.getWorld().model.statistics.hasUnlimitedSaves()) {
final Dialog d = CustomDialogFactory.createDialog(getActivity(),
getString(R.string.startscreen_load_game),
getResources().getDrawable(android.R.drawable.ic_delete),
getString(R.string.startscreen_load_game_confirm),
null,
true);
CustomDialogFactory.addButton(d, android.R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View v) {
Dialogs.showLoad(StartScreenActivity_MainMenu.this);
}
});
CustomDialogFactory.addDismissButton(d, android.R.string.cancel);
CustomDialogFactory.show(d);
} else {
Dialogs.showLoad(StartScreenActivity_MainMenu.this);
}
}
});
@@ -131,12 +151,14 @@ public class StartScreenActivity_MainMenu extends Fragment {
String playerName;
String displayInfo = null;
int iconID = TileManager.CHAR_HERO;
boolean isDead = false;
FileHeader header = Savegames.quickload(getActivity(), Savegames.SLOT_QUICKSAVE);
if (header != null && header.playerName != null) {
playerName = header.playerName;
displayInfo = header.displayInfo;
iconID = header.iconID;
isDead = header.isDead;
} else {
// Before fileversion 14 (v0.6.7), quicksave was stored in Shared preferences
SharedPreferences p = getActivity().getSharedPreferences("quicksave", Activity.MODE_PRIVATE);
@@ -146,7 +168,7 @@ public class StartScreenActivity_MainMenu extends Fragment {
}
}
hasExistingGame = (playerName != null);
setButtonState(playerName, displayInfo, iconID);
setButtonState(playerName, displayInfo, iconID, isDead);
if (isNewVersion()) {
Dialogs.showNewVersion(getActivity());
@@ -168,13 +190,13 @@ public class StartScreenActivity_MainMenu extends Fragment {
listener = null;
}
private void setButtonState(final String playerName, final String displayInfo, int iconID) {
startscreen_continue.setEnabled(hasExistingGame);
private void setButtonState(final String playerName, final String displayInfo, int iconID, boolean isDead) {
startscreen_continue.setEnabled(hasExistingGame && !isDead);
startscreen_newgame.setEnabled(true);
if (hasExistingGame) {
TileManager tm = AndorsTrailApplication.getApplicationFromActivity(getActivity()).getWorld().tileManager;
tm.setImageViewTileForPlayer(getResources(), save_preview_hero_icon, iconID);
save_preview_hero_desc.setText(playerName + ", " + displayInfo);
save_preview_hero_desc.setText((isDead ? getString(R.string.rip_startscreen) : "") + playerName + ", " + displayInfo);
save_preview_holder.setVisibility(View.VISIBLE);
} else {
save_preview_holder.setVisibility(View.GONE);

View File

@@ -20,12 +20,15 @@ import com.gpl.rpg.AndorsTrail.R;
import com.gpl.rpg.AndorsTrail.WorldSetup;
import com.gpl.rpg.AndorsTrail.activity.LoadingActivity;
import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager;
import com.gpl.rpg.AndorsTrail.view.SpinnerEmulator;
public class StartScreenActivity_NewGame extends Fragment {
private TextView startscreen_enterheroname;
private int selectedIconID = TileManager.CHAR_HERO;
private int startLives = -1;
private boolean unlimitedSaves = true;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -39,7 +42,40 @@ public class StartScreenActivity_NewGame extends Fragment {
startscreen_enterheroname = (TextView) root.findViewById(R.id.startscreen_enterheroname);
new SpinnerEmulator(root, R.id.startscreen_mode_selector_button, R.array.startscreen_mode_selector, R.string.startscreen_game_mode) {
@Override
public void setValue(int value) {
if (value == 0) {
startLives = -1;
unlimitedSaves = true;
} else {
unlimitedSaves = false;
if (value == 1) {
startLives = -1;
} else if (value == 2) {
startLives = 50;
} else if (value == 3) {
startLives = 10;
} else if (value == 4) {
startLives = 3;
} else {
startLives = 1;
}
}
}
@Override
public void selectionChanged(int value) {
}
@Override
public int getValue() {
return 0;
}
};
final RadioGroup group = (RadioGroup) root.findViewById(R.id.newgame_spritegroup);
group.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@@ -114,6 +150,8 @@ public class StartScreenActivity_NewGame extends Fragment {
setup.loadFromSlot = loadFromSlot;
setup.newHeroName = name;
setup.newHeroIcon = selectedIconID;
setup.newHeroStartLives = startLives;
setup.newHeroUnlimitedSaves = unlimitedSaves;
gameCreationOver();
startActivity(new Intent(getActivity(), LoadingActivity.class));
}

View File

@@ -47,6 +47,7 @@ public final class Constants {
public static final String FILENAME_WORLDMAP_HTMLFILE_SUFFIX = ".html";
public static final String FILENAME_SAVEGAME_FILENAME_PREFIX = "savegame";
public static final String PLACEHOLDER_PLAYERNAME = "$playername";
public static final String CHEAT_DETECTION_FOLDER = "dEAGyGE3YojqXjI3x4x7";
public static final Random rnd = new Random();
public static int rollValue(final ConstRange r) { return rollValue(r.max, r.current); }

View File

@@ -129,8 +129,11 @@ public final class MapController {
if (lostExp < 0) lostExp = 0;
controllers.actorStatsController.addExperience(-lostExp);
world.model.statistics.addPlayerDeath(lostExp);
controllers.movementController.respawnPlayerAsync();
lotsOfTimePassed();
if (!world.model.statistics.isDead()) {
controllers.movementController.respawnPlayerAsync();
lotsOfTimePassed();
}
worldEventListeners.onPlayerDied(lostExp);
}

View File

@@ -25,8 +25,14 @@ public final class GameStatistics {
private final HashMap<String, Integer> killedMonsters = new HashMap<String, Integer>();
private final HashMap<String, Integer> usedItems = new HashMap<String, Integer>();
private int spentGold = 0;
private boolean unlimitedSaves = true;
private int startLives = -1; // -1 --> unlimited
public GameStatistics(boolean unlimitedSaves, int startLives) {
this.unlimitedSaves = unlimitedSaves;
this.startLives = startLives;
}
public GameStatistics() { }
public void addMonsterKill(String monsterTypeID) {
if (!killedMonsters.containsKey(monsterTypeID)) killedMonsters.put(monsterTypeID, 1);
else killedMonsters.put(monsterTypeID, killedMonsters.get(monsterTypeID) + 1);
@@ -51,6 +57,16 @@ public final class GameStatistics {
return spentGold;
}
public boolean hasUnlimitedSaves() { return unlimitedSaves; }
public boolean hasUnlimitedLives() { return startLives == -1; }
public int getStartLives() { return startLives; }
public int getLivesLeft() { return hasUnlimitedLives() ? -1 : startLives - deaths; }
public boolean isDead() { return !hasUnlimitedLives() && getLivesLeft() < 1; }
public int getNumberOfKillsForMonsterType(String monsterTypeID) {
Integer v = killedMonsters.get(monsterTypeID);
if (v == null) return 0;
@@ -167,6 +183,11 @@ public final class GameStatistics {
this.usedItems.put(name, value);
}
this.spentGold = src.readInt();
if (fileversion < 48) return;
this.startLives = src.readInt();
this.unlimitedSaves = src.readBoolean();
}
public void writeToParcel(DataOutputStream dest) throws IOException {
@@ -184,5 +205,7 @@ public final class GameStatistics {
dest.writeInt(e.getValue());
}
dest.writeInt(spentGold);
dest.writeInt(startLives);
dest.writeBoolean(unlimitedSaves);
}
}

View File

@@ -20,10 +20,10 @@ public final class ModelContainer {
public PredefinedMap currentMap;
public LayeredTileMap currentTileMap;
public ModelContainer() {
public ModelContainer(int startLives, boolean unlimitedSaves) {
player = new Player();
uiSelections = new InterfaceData();
statistics = new GameStatistics();
statistics = new GameStatistics(unlimitedSaves, startLives);
worldData = new WorldData();
}

View File

@@ -6,6 +6,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.UUID;
import android.util.SparseIntArray;
@@ -44,6 +45,8 @@ public final class Player extends Actor {
private String spawnMap;
private String spawnPlace;
private final HashMap<String, Integer> alignments = new HashMap<String, Integer>();
public String id = UUID.randomUUID().toString();
public long savedVersion = 1; // the version get's increased for cheat detection everytime a player with limited saves is saved
// Unequipped stats
public static final class PlayerBaseTraits {
@@ -365,6 +368,11 @@ public final class Player extends Actor {
this.alignments.put(faction, alignment);
}
}
if (fileversion >= 48) {
this.id = src.readUTF();
this.savedVersion = src.readLong();
}
}
public void writeToParcel(DataOutputStream dest) throws IOException {
@@ -421,6 +429,8 @@ public final class Player extends Actor {
dest.writeUTF(e.getKey());
dest.writeInt(e.getValue());
}
dest.writeUTF(id);
dest.writeLong(savedVersion);
}
}

View File

@@ -5,6 +5,7 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
@@ -21,6 +22,7 @@ import java.util.regex.Pattern;
import android.content.Context;
import android.content.res.Resources;
import android.os.Environment;
import android.os.SystemClock;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
@@ -32,15 +34,25 @@ import com.gpl.rpg.AndorsTrail.util.L;
public final class Savegames {
public static final int SLOT_QUICKSAVE = 0;
public static final long DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED = -1;
private static long lastBackup = 0;
public static enum LoadSavegameResult {
success
, unknownError
, savegameIsFromAFutureVersion
, cheatingDetected
}
public static boolean saveWorld(WorldContext world, Context androidContext, int slot, String displayInfo) {
try {
if (slot != SLOT_QUICKSAVE && !world.model.statistics.hasUnlimitedSaves()) {
world.model.player.savedVersion++;
}
String id = world.model.player.id;
long savedVersion = world.model.player.savedVersion;
// Create the savegame in a temporary memorystream first to ensure that the savegame can
// be created correctly. We don't want to trash the user's file unneccessarily if there is an error.
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -51,18 +63,51 @@ public final class Savegames {
FileOutputStream fos = getOutputFile(androidContext, slot);
fos.write(savegame);
fos.close();
if (!world.model.statistics.hasUnlimitedSaves()) {
if (slot != SLOT_QUICKSAVE) {
androidContext.deleteFile(Constants.FILENAME_SAVEGAME_QUICKSAVE);
writeCheatCheck(androidContext, savedVersion, id);
} else if (SystemClock.uptimeMillis() > lastBackup + 120000) {
writeBackup(savegame, id);
lastBackup = SystemClock.uptimeMillis();
}
}
return true;
} catch (IOException e) {
L.log("Error saving world: " + e.toString());
return false;
}
}
private static void writeBackup(byte[] savegame, String playerId) throws IOException {
File root = Environment.getExternalStorageDirectory();
File cheatDetectionFolder = new File(root, Constants.CHEAT_DETECTION_FOLDER);
if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir();
File backupFile = new File(cheatDetectionFolder, playerId + "X");
FileOutputStream fileOutputStream = new FileOutputStream(backupFile);
fileOutputStream.write(savegame);
fileOutputStream.close();
}
public static LoadSavegameResult loadWorld(WorldContext world, ControllerContext controllers, Context androidContext, int slot) {
try {
FileHeader fh = quickload(androidContext, slot);
if(fh == null) {
return LoadSavegameResult.unknownError;
}
if (!fh.hasUnlimitedSaves && slot != SLOT_QUICKSAVE && triedToCheat(androidContext, fh)) {
return LoadSavegameResult.cheatingDetected;
}
FileInputStream fos = getInputFile(androidContext, slot);
LoadSavegameResult result = loadWorld(androidContext.getResources(), world, controllers, fos, fh);
fos.close();
if (result == LoadSavegameResult.success && slot != SLOT_QUICKSAVE && !world.model.statistics.hasUnlimitedSaves()) {
getSlotFile(slot).delete();
writeCheatCheck(androidContext, DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED, fh.playerId);
}
return result;
} catch (IOException e) {
if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) {
@@ -76,6 +121,47 @@ public final class Savegames {
}
}
private static boolean triedToCheat(Context androidContext, FileHeader fh) throws IOException {
long savedVersionToCheck = 0;
File root = Environment.getExternalStorageDirectory();
File cheatDetectionFolder = new File(root, Constants.CHEAT_DETECTION_FOLDER);
if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir();
File cheatDetectionFile = new File(cheatDetectionFolder, fh.playerId);
if (cheatDetectionFile.exists()) {
FileInputStream fileInputStream = new FileInputStream(cheatDetectionFile);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
savedVersionToCheck = cheatDetection.savedVersion;
dataInputStream.close();
fileInputStream.close();
}
if (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) {
return true;
}
if (androidContext.getFileStreamPath(fh.playerId).exists()) {
FileInputStream fileInputStream = androidContext.openFileInput(fh.playerId);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
if (cheatDetection.savedVersion == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) {
savedVersionToCheck = DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED;
}
else if (cheatDetection.savedVersion > savedVersionToCheck) {
savedVersionToCheck = cheatDetection.savedVersion;
}
if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) {
L.log("Internal cheatcheck file savedVersion: " + cheatDetection.savedVersion);
}
dataInputStream.close();
fileInputStream.close();
}
return (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED || fh.savedVersion < savedVersionToCheck);
}
private static FileOutputStream getOutputFile(Context androidContext, int slot) throws IOException {
if (slot == SLOT_QUICKSAVE) {
return androidContext.openFileOutput(Constants.FILENAME_SAVEGAME_QUICKSAVE, Context.MODE_PRIVATE);
@@ -96,7 +182,8 @@ public final class Savegames {
return new FileInputStream(getSlotFile(slot));
}
}
private static File getSlotFile(int slot) {
public static File getSlotFile(int slot) {
File root = getSavegameDirectory();
return new File(root, Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + slot);
}
@@ -108,7 +195,12 @@ public final class Savegames {
public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException {
DataOutputStream dest = new DataOutputStream(outStream);
FileHeader.writeToParcel(dest, world.model.player.getName(), displayInfo, world.model.player.iconID);
FileHeader.writeToParcel(dest, world.model.player.getName(),
displayInfo, world.model.player.iconID,
world.model.statistics.isDead(),
world.model.statistics.hasUnlimitedSaves(),
world.model.player.id,
world.model.player.savedVersion);
world.maps.writeToParcel(dest, world);
world.model.writeToParcel(dest);
dest.close();
@@ -156,6 +248,24 @@ public final class Savegames {
}
}
private static void writeCheatCheck(Context androidContext, long savedVersion, String playerId) throws IOException {
File root = Environment.getExternalStorageDirectory();
File cheatDetectionFolder = new File(root, Constants.CHEAT_DETECTION_FOLDER);
if (!cheatDetectionFolder.exists()) cheatDetectionFolder.mkdir();
File cheatDetectionFile = new File(cheatDetectionFolder, playerId);
FileOutputStream fileOutputStream = new FileOutputStream(cheatDetectionFile);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
CheatDetection.writeToParcel(dataOutputStream, savedVersion);
dataOutputStream.close();
fileOutputStream.close();
fileOutputStream = androidContext.openFileOutput(playerId, Context.MODE_PRIVATE);
dataOutputStream = new DataOutputStream(fileOutputStream);
CheatDetection.writeToParcel(dataOutputStream, savedVersion);
dataOutputStream.close();
fileOutputStream.close();
}
private static final Pattern savegameFilenamePattern = Pattern.compile(Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + "(\\d+)");
public static List<Integer> getUsedSavegameSlots() {
try {
@@ -178,13 +288,36 @@ public final class Savegames {
}
}
private static final class CheatDetection {
public final int fileversion;
public final long savedVersion;
// ====== PARCELABLE ===================================================================
public CheatDetection(DataInputStream src) throws IOException {
this.fileversion = src.readInt();
this.savedVersion = src.readLong();
}
public static void writeToParcel(DataOutputStream dest, long savedVersion) throws IOException {
dest.writeInt(AndorsTrailApplication.CURRENT_VERSION);
dest.writeLong(savedVersion);
}
}
public static final class FileHeader {
public final int fileversion;
public final String playerName;
public final String displayInfo;
public final int iconID;
public boolean skipIcon = false;
public final boolean isDead;
public final boolean hasUnlimitedSaves;
public final String playerId;
public final long savedVersion;
public String describe() {
return playerName + ", " + displayInfo;
}
@@ -203,9 +336,10 @@ public final class Savegames {
this.playerName = null;
this.displayInfo = null;
}
if (fileversion >= 43 && !skipIcon) {
if (fileversion >= 43) {
int id = src.readInt();
if (id > TileManager.LAST_HERO) {
if (skipIcon || id > TileManager.LAST_HERO) {
this.iconID = TileManager.CHAR_HERO_0;
this.skipIcon = true;
} else {
@@ -214,13 +348,29 @@ public final class Savegames {
} else {
this.iconID = TileManager.CHAR_HERO_0;
}
if (fileversion >= 48) {
this.isDead = src.readBoolean();
this.hasUnlimitedSaves = src.readBoolean();
this.playerId = src.readUTF();
this.savedVersion = src.readLong();
} else {
this.isDead = false;
this.hasUnlimitedSaves = true;
this.playerId = "";
this.savedVersion = 0;
}
}
public static void writeToParcel(DataOutputStream dest, String playerName, String displayInfo, int iconID) throws IOException {
public static void writeToParcel(DataOutputStream dest, String playerName, String displayInfo, int iconID, boolean isDead, boolean hasUnlimitedSaves, String playerId, long savedVersion) throws IOException {
dest.writeInt(AndorsTrailApplication.CURRENT_VERSION);
dest.writeUTF(playerName);
dest.writeUTF(displayInfo);
dest.writeInt(iconID);
dest.writeBoolean(isDead);
dest.writeBoolean(hasUnlimitedSaves);
dest.writeUTF(playerId);
dest.writeLong(savedVersion);
}
}
}