/* * Copyright 2015 Daniel Dittmar * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package dan.dit.whatsthat.testsubject; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Color; import android.support.annotation.NonNull; import android.util.Log; import android.view.Gravity; import android.view.View; import com.github.johnpersano.supertoasts.SuperToast; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import dan.dit.whatsthat.BuildConfig; import dan.dit.whatsthat.R; import dan.dit.whatsthat.achievement.Achievement; import dan.dit.whatsthat.achievement.AchievementManager; import dan.dit.whatsthat.achievement.AchievementProperties; import dan.dit.whatsthat.riddle.RiddleInitializer; import dan.dit.whatsthat.riddle.achievement.holders.MiscAchievementHolder; import dan.dit.whatsthat.riddle.achievement.holders.TestSubjectAchievementHolder; import dan.dit.whatsthat.riddle.achievement.holders.TypeAchievementHolder; import dan.dit.whatsthat.riddle.types.PracticalRiddleType; import dan.dit.whatsthat.testsubject.intro.Intro; import dan.dit.whatsthat.testsubject.shopping.ShopArticleHolder; import dan.dit.whatsthat.testsubject.shopping.sortiment.ShopArticleRiddleHints; import dan.dit.whatsthat.testsubject.shopping.sortiment.SortimentHolder; import dan.dit.whatsthat.util.dependencies.Dependable; import dan.dit.whatsthat.util.dependencies.Dependency; import dan.dit.whatsthat.util.dependencies.MinValueDependency; import dan.dit.whatsthat.util.general.DelayedQueueProcessor; import dan.dit.whatsthat.util.wallet.Wallet; import dan.dit.whatsthat.util.wallet.WalletEntry; /** * Created by daniel on 11.04.15. */ public class TestSubject { private static final TestSubject INSTANCE = new TestSubject(); public static final String EMAIL_ON_ERROR = "whatsthat.contact@gmail.com"; public static final String EMAIL_FEEDBACK = "whatsthat.feedback@gmail.com"; private static final String TEST_SUBJECT_PREFERENCES_FILE = "dan.dit.whatsthat.testsubject_preferences"; private static final String TEST_SUBJECT_PREF_GENDER = "key_testsubject_gender"; public static final int DEFAULT_SKIPABLE_GAMES = 5; public static final int GENDER_NOT_CHOSEN = -1; // used for array indices public static final int GENDER_MALE = 0; // genders as int, not boolean .. you never know, male = 0 chosen by fair dice role, take that feminists public static final int GENDER_FEMALE = 1; public static final int GENDER_WHATEVER = 2; public static final int GENDER_ALIEN = 3; // :o public static final int GENDERS_COUNT = 4; // amount of valid genders public static final String SHW_KEY_MAX_AVAILABLE_RIDDLE_TYPES = "shw_key_can_choose_new_riddle"; private static final int AVAILABLE_RIDDLES_AT_GAME_START = 1; // one additional riddle type is granted per level up (also on level 0) private boolean mInitialized; private SharedPreferences mPreferences; private int mGender = GENDER_NOT_CHOSEN; private Context mApplicationContext; private TestSubjectAchievementHolder mAchievementHolder; private Purse mPurse; private DelayedQueueProcessor<TestSubjectToast> mGeneralToastProcessor; private DelayedQueueProcessor<Achievement> mAchievementToastProcessor; private ShopArticleHolder mShopArticleHolder; private int mCurrLevel; private TestSubjectLevel[] mLevels; private TestSubjectRiddleTypeController mTypesController; private TestSubject() { } public static synchronized TestSubject initialize(Context context) { if (isInitialized()) { return INSTANCE; } INSTANCE.mApplicationContext = context.getApplicationContext(); INSTANCE.initAchievementManager(); INSTANCE.initPreferences(); INSTANCE.initLevels(); INSTANCE.mInitialized = true; INSTANCE.mAchievementHolder = new TestSubjectAchievementHolder(AchievementManager.getInstance()); INSTANCE.mAchievementHolder.addDependencies(); INSTANCE.mShopArticleHolder.makeDependencies(); INSTANCE.mAchievementHolder.initAchievements(); return INSTANCE; } public int getGender() { return mGender; } public void setGender(int gender) { mGender = gender; mPreferences.edit().putInt(TEST_SUBJECT_PREF_GENDER, mGender).apply(); } @SuppressLint("CommitPrefEdits") public void saveIntro(Intro intro) { if (intro == null) { return; } intro.save(mPreferences.edit(), mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_TESTSUBJECT_LEVEL)); } public Intro makeIntro(View introView) { int level = mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_TESTSUBJECT_LEVEL); TestSubjectLevel currLevel = mLevels[level]; Intro intro = Intro.makeIntro(introView, currLevel); intro.load(mPreferences, level); return currLevel.makeIntro(intro); } public int getCurrentLevel() { return mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_TESTSUBJECT_LEVEL, TestSubjectLevel.LEVEL_NONE); } public int getMaximumLevel() { return mLevels.length - 1; // since level numbers start with zero } public Dependable getLevelDependency() { return mPurse.mShopWallet.assureEntry(Purse.SHW_KEY_TESTSUBJECT_LEVEL); } public void initToasts() { mGeneralToastProcessor = new DelayedQueueProcessor<>(new DelayedQueueProcessor.Callback<TestSubjectToast>() { @Override public long process(@NonNull TestSubjectToast toProcess) { showToast(toProcess); return toProcess.mDuration; } }); mAchievementToastProcessor = new DelayedQueueProcessor<>(new DelayedQueueProcessor.Callback<Achievement>() { @Override public long process(@NonNull Achievement toProcess) { TestSubjectToast achievementToast = makeAchievementToast(toProcess); showToast(achievementToast); return achievementToast == null ? 0L : achievementToast.mDuration; } }); } private void showToast(TestSubjectToast toast) { if (toast != null) { SuperToast superToast = toast.makeSuperToast(mApplicationContext); if (superToast != null) { superToast.show(); } } } private TestSubjectToast makeAchievementToast(Achievement achievement) { if (mApplicationContext == null) { return null; } TestSubjectToast toast = new TestSubjectToast(Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 5, achievement.getIconResId(), 0, SuperToast.Duration.LONG); toast.mIconPosition = SuperToast.IconPosition.LEFT; toast.mAnimations = SuperToast.Animations.FLYIN; toast.mBackground = R.drawable.achieved_background; toast.mTextColor = Color.BLACK; Resources res = mApplicationContext.getResources(); toast.mText = res.getString(R.string.achievement_achieved) + " " + achievement.getName(res); return toast; } public static boolean isInitialized() { return INSTANCE.mInitialized; } private void initAchievementManager() { AchievementManager.initialize(mApplicationContext); AchievementManager.getInstance().addAchievementChangedListener(new AchievementManager.OnAchievementChangedListener() { @Override public void onDataEvent(AchievementManager.AchievementChangeEvent changeEvent) { switch (changeEvent.getChangedHint()) { case AchievementManager.CHANGED_TO_ACHIEVED_AND_UNCLAIMED: TestSubject.this.postAchievementAchieved(changeEvent.getAchievement()); break; case AchievementManager.CHANGED_GOT_CLAIMED: TestSubject.this.addAchievementScore(changeEvent.getAchievement() .getScoreReward()); break; } } }); } private void initPreferences() { mPreferences = mApplicationContext.getSharedPreferences(TEST_SUBJECT_PREFERENCES_FILE, Context.MODE_PRIVATE); mPurse = new Purse(mApplicationContext); mShopArticleHolder = new SortimentHolder(mApplicationContext, new ForeignPurse(mPurse)); mGender = mPreferences.getInt(TEST_SUBJECT_PREF_GENDER, GENDER_NOT_CHOSEN); mTypesController = new TestSubjectRiddleTypeController(mPreferences); } public int getNextLevelUpCost() { if (mCurrLevel >= mLevels.length -1) { return -1; // already at max level } double fraction = mLevels[mCurrLevel + 1].getLevelUpAchievementScoreFraction(); int maxAchievementScore = mAchievementHolder.getExpectableTestSubjectScore(mCurrLevel); int alreadySpent = mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_SPENT_SCORE_ON_LEVEL_UP); int cost = (int) (maxAchievementScore * fraction) - alreadySpent; if (cost < 0) { return 0; } // now round to some decent value final int roundingPrecision = 5; return cost + roundingPrecision - (cost % roundingPrecision); } public synchronized boolean purchaseLevelUp() { return isLevelUpAvailable() && mPurse.purchaseLevelUp(getNextLevelUpCost()) && levelUp(); } public synchronized boolean isLevelUpAvailable() { if (mCurrLevel >= mLevels.length -1) { return false; // already at max level } return mCurrLevel == TestSubjectLevel.LEVEL_NONE || !canChooseNewRiddle(); } /** * Increases the TestSubject level by one. * @return This is almost always true. An exception is if there is no more level available. */ public boolean levelUp() { // every condition that will imply this method to return false also needs to imply that // isLevelUpAvailable also returns false. This method is only public to make available // for cheats. if (mCurrLevel >= mLevels.length -1) { return false; // already at max level } mPurse.mShopWallet.editEntry(SHW_KEY_MAX_AVAILABLE_RIDDLE_TYPES, AVAILABLE_RIDDLES_AT_GAME_START).add(1); mPurse.mShopWallet.editEntry(Purse.SHW_KEY_TESTSUBJECT_LEVEL).add(1); mCurrLevel = mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_TESTSUBJECT_LEVEL); TestSubjectLevel currLevel = mLevels[mCurrLevel]; currLevel.onLeveledUp(); currLevel.applyLevel(mApplicationContext.getResources()); return true; } private void initLevels() { mLevels = TestSubjectLevel.makeAll(this); WalletEntry levelEntry = mPurse.mShopWallet.assureEntry(Purse.SHW_KEY_TESTSUBJECT_LEVEL, TestSubjectLevel.LEVEL_NONE); mCurrLevel = levelEntry.getValue(); if (mCurrLevel == TestSubjectLevel.LEVEL_NONE) { levelUp(); } else { mLevels[mCurrLevel].applyLevel(mApplicationContext.getResources()); } } public static TestSubject getInstance() { if (!INSTANCE.mInitialized) { throw new IllegalStateException("Subject not initialized!"); } return INSTANCE; } public void postToast(TestSubjectToast toast, long delay) { if (mGeneralToastProcessor == null || toast == null) { if (mGeneralToastProcessor == null) { Log.e("HomeStuff", "Trying to post toast. No handler initialized for testsubject."); } return; } mGeneralToastProcessor.append(toast, delay); mGeneralToastProcessor.start(); } public void postAchievementAchieved(Achievement achievement) { mAchievementToastProcessor.append(achievement, 200L); mAchievementToastProcessor.start(); mAchievementHolder.getMiscData().updateMappedValue(MiscAchievementHolder.KEY_ACHIEVEMENTS_EARNED_COUNT, achievement.getId()); } public boolean addNewType(PracticalRiddleType type) { if (!mTypesController.addNewType(type)) { return false; } mPurse.setAvailableRiddleHintsAtStartCount(type); mTypesController.saveTypes(); return true; } /** * Returns a copy of the list of all currently available TestSubjectRiddleTypes. * @return A new list containing the riddle types. */ public List<TestSubjectRiddleType> getAvailableTypes() { return new ArrayList<>(mTypesController.getAll()); } public boolean canSkip() { return mPurse.mShopWallet.assureEntry(Purse.SHW_KEY_SKIPABLE_GAMES, DEFAULT_SKIPABLE_GAMES).getValue() > RiddleInitializer.INSTANCE.getRiddleManager().getUnsolvedRiddleCount(); } public void ensureSkipableGames(int amount) { int current = mPurse.mShopWallet.getEntryValue(Purse.SHW_KEY_SKIPABLE_GAMES, DEFAULT_SKIPABLE_GAMES); if (current < amount) { mPurse.mShopWallet.editEntry(Purse.SHW_KEY_SKIPABLE_GAMES, DEFAULT_SKIPABLE_GAMES) .set(amount); } } public boolean canChooseNewRiddle() { return mPurse.mShopWallet.getEntryValue(SHW_KEY_MAX_AVAILABLE_RIDDLE_TYPES, AVAILABLE_RIDDLES_AT_GAME_START) > mTypesController.getCount() && mTypesController.getCount() < PracticalRiddleType.ALL_PLAYABLE_TYPES.size(); } public synchronized boolean chooseNewRiddle(PracticalRiddleType type) { if (mTypesController.isTypeAvailable(type) || !canChooseNewRiddle()) { return false; } if (addNewType(type)) { Log.d("Riddle", "Added and saved new type: " + type); return true; } return false; } public void addSolvedRiddleScore(int score) { mPurse.mScoreWallet.editEntry(Purse.SW_KEY_SOLVED_RIDDLE_SCORE).add(score); if (BuildConfig.DEBUG) { WalletEntry entry = mPurse.mScoreWallet.assureEntry(Purse.SW_KEY_SOLVED_RIDDLE_SCORE); Log.d("HomeStuff", "Adding " + score + " to wallet, new riddle score: " + entry.getValue()); } } public void addAchievementScore(int score) { mPurse.mScoreWallet.editEntry(Purse.SW_KEY_ACHIEVEMENT_SCORE).add(score); Log.d("HomeStuff", "Adding " + score + " to wallet, new achievement score: " + mPurse.mScoreWallet.assureEntry(Purse.SW_KEY_ACHIEVEMENT_SCORE).getValue()); } public TestSubjectAchievementHolder getAchievementHolder() { return mAchievementHolder; } public int getAchievementScore() { return mPurse.getAchievementScore(); } public Dependency makeAchievementDependency(PracticalRiddleType type, int number) { TypeAchievementHolder typeAchievements = mAchievementHolder.getTypeAchievementHolder(type); if (typeAchievements != null) { Achievement dep = typeAchievements.getByNumber(number); if (dep != null) { return new MinValueDependency(dep, dep.getMaxValue()); } } return null; } public Dependency makeClaimedAchievementDependency(PracticalRiddleType type, int number) { TypeAchievementHolder typeAchievementHolder = mAchievementHolder.getTypeAchievementHolder(type); if (typeAchievementHolder != null) { Achievement dep = typeAchievementHolder.getByNumber(number); if (dep != null) { return new DependencyHolder.ClaimedAchievementDependency(dep); } } return null; } public int getCurrentRiddleHintNumber(PracticalRiddleType type) { return mPurse.getCurrentRiddleHintNumber(type); } public void increaseRiddleHintsDisplayed(PracticalRiddleType type) { int newNumber = mPurse.increaseCurrentRiddleHintNumber(type); mAchievementHolder.getMiscData().putValue(ShopArticleRiddleHints.makeKey(type), (long) (newNumber - 1), AchievementProperties.UPDATE_POLICY_ALWAYS); } public boolean hasAvailableHint(PracticalRiddleType type) { return hasAvailableHint(type, mPurse.getCurrentRiddleHintNumber(type)); } public boolean hasAvailableHint(PracticalRiddleType type, int hintNumber) { return hintNumber < mPurse.getAvailableRiddleHintsCount(type); } public boolean hasFeature(String featureKey) { return mPurse.mShopWallet.getEntryValue(featureKey) != WalletEntry.FALSE; } public boolean hasToggleableFeature(String featureKey) { return mPurse.hasToggleableFeature(featureKey); } public Dependency makeProductPurchasedDependency(String articleKey, int productIndex) { return new DependencyHolder.ProductPurchasedDependency(mShopArticleHolder.getArticle (articleKey), productIndex); } public Dependency makeFeatureAvailableDependency(String articleKey, int requiredFeatureValue) { return new DependencyHolder.FeatureAvailableDependency(mShopArticleHolder.getArticle (articleKey), requiredFeatureValue); } public static void sortTypes(List<PracticalRiddleType> types) { Collections.sort(types, TestSubjectRiddleTypeController.TYPE_COMPARATOR); } public boolean purchaseNextHintForFree(PracticalRiddleType type) { ForeignPurse purse = mShopArticleHolder.getPurse(); return purse.purchaseHint(type, 0); } public int getRiddleSolvedResIds() { return mLevels[mCurrLevel].mRiddleSolvedCandy; } public int getImageResId() { return mLevels[mCurrLevel].getImageResourceId(); } public int getCurrentScore() { return mPurse.getCurrentScore(); } public void registerScoreChangedListener(Wallet.OnEntryChangedListener listener) { mPurse.mScoreWallet.addChangedListener(listener); } public void removeScoreChangedListener(Wallet.OnEntryChangedListener listener) { mPurse.mScoreWallet.removeChangedListener(listener); } public int getShopValue(String key) { return mPurse.mShopWallet.getEntryValue(key); } public TestSubjectRiddleTypeController getTypesController() { return mTypesController; } private final Dependable mTestSubjectRiddleTypeCountDependable = new Dependable() { @Override public CharSequence getName(Resources res) { return res.getString(R.string.dependable_testsubject_riddle_type_count); } @Override public int getValue() { return mTypesController.getCount(); } }; public Dependable getTestSubjectRiddleTypeCountDependency() { return mTestSubjectRiddleTypeCountDependable; } private Map<PracticalRiddleType, Dependency> mTypeDependencies = new HashMap<>(); public Dependency getRiddleTypeDependency(PracticalRiddleType type) { Dependency dep = mTypeDependencies.get(type); if (dep == null) { dep = new DependencyHolder.RiddleTypeDependency(type, this); mTypeDependencies.put(type, dep); } return dep; } public ShopArticleHolder getShopSortiment() { return mShopArticleHolder; } }