/* * 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.achievement; import android.content.SharedPreferences; import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.List; import dan.dit.whatsthat.R; import dan.dit.whatsthat.util.dependencies.Dependable; import dan.dit.whatsthat.util.dependencies.Dependency; import dan.dit.whatsthat.util.general.PercentProgressListener; /** * Base of all achievements. An achievement has a name, a description and an icon and is * managed by an AchievementManager, which is responsible for saving and loading the current state * to and from permanent memory. * An achievement can be discovered or undiscovered which only has effect on the display of its description * (or others). An achievement starts with a value of zero and is achieved as soon as the value is equal to or greater * than the set max value. An achievement will get discovered before being achieved. * Each achievement has a reward associated which is any score value greater than or equal to zero. The reward * needs to be claimed after the achievement is achieved. * * Finally an achievement is Dependable and can therefore be a Dependency, e.g. for other achievements. An * achievement should only be achieved if the dependencies are fulfilled, though this is up to the achievement * as the time to check can vary for example for progressive achievements. * Created by daniel on 12.05.15. */ public abstract class Achievement implements AchievementDataEventListener, Dependable { protected static final String SEPARATOR = "_"; private static final String KEY_DISCOVERED = "discovered"; private static final String KEY_VALUE = "value"; private static final String KEY_ACHIEVED_TIMESTAMP = "achievedtime"; private static final String KEY_REWARD_CLAIMED = "rewardclaimed"; private static final int DEFAULT_VALUE = 0; private static final int DEFAULT_MAX_VALUE = 1; protected final String mId; protected boolean mDiscovered; protected final AchievementManager mManager; private int mValue; private int mMaxValue; protected final int mLevel; private final int mScoreReward; private boolean mRewardClaimed; private final int mNameResId; protected final int mDescrResId; private final int mRewardResId; protected final List<Dependency> mDependencies; protected long mAchievedTimestamp; private AchievementManager.AchievementChangeEvent mStateChangeEvent; /** * Creates a new achievement. The id string needs to be unique for all achievements. The given name, description * and reward are by default read from the given resources. * @param id The unique id identifying the achievement. * @param nameResId The name. * @param descrResId The description. * @param rewardResId The reward description, if 0 it displays a default message. * @param manager The achievement manager required. * @param level The level of the achievement. Can be used to create an additional dependency or changing the displaying. * @param scoreReward The score reward that is to be claimed after achievement is achieved. * @param maxValue The maximum value to reach to achieve the achievement. Must be positive. * @param discovered If the achievement is discovered by default. No effect if achievement was already saved. */ protected Achievement(String id, int nameResId, int descrResId, int rewardResId, AchievementManager manager, int level, int scoreReward, int maxValue, boolean discovered) { mId = id; mValue = DEFAULT_VALUE; mDiscovered = discovered; mMaxValue = Math.max(DEFAULT_MAX_VALUE, maxValue); mNameResId = nameResId; mDescrResId = descrResId; mRewardResId = rewardResId; mDependencies = new ArrayList<>(); mManager = manager; mLevel = level; mStateChangeEvent = new AchievementManager.AchievementChangeEvent(this); mScoreReward = Math.max(scoreReward, 0); if (TextUtils.isEmpty(mId)) { throw new IllegalArgumentException("Null id given."); } if (manager == null) { throw new IllegalArgumentException("Null manager given."); } loadData(manager.getSharedPreferences()); } @Override public int getValue() { return mValue; } /** * Returns the icon resource id. This is the default icon for the achievement used in general * places like when it is achieved or shown in some generic place. * @return The icon resource id. */ public abstract int getIconResId(); /** * Returns the icon resource id depending on the state of the achievement. (like discovered, * achieved, claimed,...) * @return The icon resource id dependending on current state. */ public abstract int getIconResIdByState(); /** * Returns this achievement's name. This is the id if no name resource given * or else the string read of the resources. * @param res Resources used to retrieve the string. * @return A name for the achievement. */ public CharSequence getName(Resources res) { return mNameResId == 0 || res == null ? mId : res.getString(mNameResId); } /** * Returns this achievement's description. This is empty if no description resource * given or else the string read of the resources. * @param res Resources used to retrieve the string. * @return A description of the achievement. */ public CharSequence getDescription(Resources res) { return mDescrResId == 0 || res == null ? "": res.getString(mDescrResId); } /** * Returns this achievement's reward description. This is "(+score reward)" for the set * score reward if no reward resource given or else the string read of the resources parameterized by the * score reward. * @param res Resources used to retrieve the string. * @return A description of the achievement's reward. */ public CharSequence getRewardDescription(Resources res) { return mRewardResId == 0 ? ("+" + getScoreReward()) : res.getString(mRewardResId, getScoreReward()); } /** * Checks if all dependencies are fulfilled. If any dependency is not fulfilled * this immediately returns false. * @return If all dependencies are fulfilled. */ public boolean areDependenciesFulfilled() { for (int i = 0; i < mDependencies.size(); i++) { if (!mDependencies.get(i).isFulfilled()) return false; } return true; } /** * Achieves this achievement if all dependencies are fulfilled. Does nothing * if already achieved. * @return true only if the dependencies are fulfilled. */ protected boolean achieveAfterDependencyCheck() { if (areDependenciesFulfilled()) { achieve(); return true; } return false; } /** * Resets any progress of this achievement. This also works if the achievement is already * achieved, even if is already claimed. Will notify the manager that it changed * CHANGED_TO_RESET.<br> * The value will be equal to the default value afterwards. */ protected void resetAnyProgress() { Log.d("Achievement", "Resetting progress " + mId + " was unclaimed: " + isRewardClaimable()); mValue = DEFAULT_VALUE; mRewardClaimed = false; mManager.onChanged(this, AchievementManager.CHANGED_TO_RESET); } /** * Achieves a percentage of the set max value. This has no effect if achievement * already achieved. Will achieve the achievement for progress equal to 100. * Does NO dependency checks. * @param progress A percent value which is cut between 0 to 100 inclusive. */ protected void achieveProgressPercent(int progress) { if (isAchieved()) { return; } if (progress > 100) { progress = 100; } else if (progress < 0) { progress = 0; } int oldValue = mValue; mValue = Math.round(progress * mMaxValue / 100.f); if (mValue >= mMaxValue || progress == 100) { achieveUnchecked(); } else if (mValue != oldValue) { mManager.onChanged(this, AchievementManager.CHANGED_PROGRESS); } } /** * Achieves the given delta which can be any value. Does nothing if already achieved. * Does NO dependency check. * @param delta The delta to achieve. */ protected void achieveDelta(int delta) { if (isAchieved()) { return; } mValue += delta; if (mValue >= mMaxValue) { achieveUnchecked(); } else if (delta != 0) { mManager.onChanged(this, AchievementManager.CHANGED_PROGRESS); } } /** * Achieves the given delta if this does not achieve this achievement. Does nothing * if already achieved. Does NO dependency check. * @param delta The delta to add. Can be any value. * @return True if the given delta was achieved, this is when the achievement was not yet * achieved and the delta added on the current value is lower than the maximum value. */ protected boolean addDeltaIfNotAchieved(int delta) { if (isAchieved() || mValue + delta >= mMaxValue) { return false; } achieveDelta(delta); return true; } /** * Returns the set level. * @return The level. */ public int getLevel() { return mLevel; } /** * Returns the set score reward. * @return The score reward. */ public int getScoreReward() { return mScoreReward; } /** * The maximum possible score reward. By default equal to getScoreReward(). * @return The maximum possible reward. */ public int getMaxScoreReward() { return getScoreReward(); } /** * Checks if the reward is claimable. This is when the achievement is * achieved and the reward was not yet claimed. * @return If this reward is claimable. */ public boolean isRewardClaimable() { return !mRewardClaimed && isAchieved(); } /** * Claims the reward. Does nothing if reward not claimable. */ public void claimReward() { if (isRewardClaimable()) { mRewardClaimed = true; mManager.onChanged(this, AchievementManager.CHANGED_GOT_CLAIMED); } } @Override public int hashCode() { return mId.hashCode(); } @Override public boolean equals(Object other) { if (other instanceof Achievement) { return mId.equals(((Achievement) other).mId); } else { return super.equals(other); } } /** * Discovers the achievement if not yet discovered. This will invoke * onDiscovered() before notifying the manager. */ protected synchronized void discover() { if (mDiscovered) { return; // already discovered } mDiscovered = true; onDiscovered(); mManager.onChanged(this, AchievementManager.CHANGED_TO_DISCOVERED); } /** * Covers the achievement if not yet covered. */ protected synchronized final void cover() { if (!mDiscovered) { return; } mDiscovered = false; mManager.onChanged(this, AchievementManager.CHANGED_TO_COVERED); } /** * Invoked when the achievement is discovered after previously being covered. */ protected abstract void onDiscovered(); /** * Checks if this achievement is achieved. * @return If the achievement is achieved. */ public boolean isAchieved() { return mValue >= mMaxValue; } /** * No checks at all. * Achieves this achievement. If the achievement previously is not discovered * it will be discovered before being achieved. */ private void achieveUnchecked() { if (!mDiscovered) { discover(); } mValue = mMaxValue; mAchievedTimestamp = System.currentTimeMillis(); onAchieved(); mManager.onChanged(this, AchievementManager.CHANGED_TO_ACHIEVED_AND_UNCLAIMED); } /** * Achieve this achievement. Does nothing if already achieved. Does NO dependency check. */ protected synchronized final void achieve() { if (isAchieved()) { return; } achieveUnchecked(); } /** * Invoked when the achievement is being achieved. */ protected abstract void onAchieved(); /** * Adds this data that is required to be stored permanently. * @param editor The editor to put data into. */ protected void addData(SharedPreferences.Editor editor) { editor .putBoolean(mId + SEPARATOR + KEY_DISCOVERED, mDiscovered) .putInt(mId + SEPARATOR + KEY_VALUE, mValue) .putLong(mId + SEPARATOR + KEY_ACHIEVED_TIMESTAMP, mAchievedTimestamp) .putBoolean(mId + SEPARATOR + KEY_REWARD_CLAIMED, mRewardClaimed); } /** * Loads data out of the given shared preferences. * @param prefs The preferences to load data from. */ protected void loadData(SharedPreferences prefs) { mDiscovered = prefs.getBoolean(mId + SEPARATOR + KEY_DISCOVERED, mDiscovered); mValue = prefs.getInt(mId + SEPARATOR + KEY_VALUE, mValue); mAchievedTimestamp = prefs.getLong(mId + SEPARATOR + KEY_ACHIEVED_TIMESTAMP, 0L); mRewardClaimed = prefs.getBoolean(mId + SEPARATOR + KEY_REWARD_CLAIMED, false); //Log.d("Achievement", "Loaded achievement data : " + mDiscovered + " " + mValue + " " + mMaxValue + " " + mAchievedTimestamp + " " + mRewardClaimed); } /** * Checks if this achievement is discovered. * @return If the achievement is discovered. */ public final boolean isDiscovered() { return mDiscovered; } /** * Returns the progress in percent. A progress of 100 is equal to the achievement being achieved. * @return The progress. */ public final int getProgress() { return (int) (PercentProgressListener.PROGRESS_COMPLETE * mValue / (double) mMaxValue); } /** * Returns the set maximum value. * @return The max value. */ public final int getMaxValue() { return mMaxValue; } /** * Returns the dependencies of this achievement. List is backed by the achievement. * @return All dependencies of this achievement. */ public List<Dependency> getDependencies() { return mDependencies; } /** * Builds the text for this achievement's dependencies. By default this is the names of all * not fulfilled dependencies appended separated by commata. * @param res The resources used to load the dependencies' name. * @return The text describing all not fulfilled achievements. */ public CharSequence buildDependenciesText(Resources res) { StringBuilder builder = new StringBuilder(); builder.append(res.getString(R.string.dependency_required)); builder.append(' '); boolean addSeparator = false; for (int i = 0; i < mDependencies.size(); i++) { Dependency dep = mDependencies.get(i); if (!dep.isFulfilled()) { if (addSeparator) { builder.append(", "); } builder.append(mDependencies.get(i).getName(res)); addSeparator = true; } } return builder.toString(); } /** * Returns the unique id of this Achievement. * @return The unique id for managing this achievement. */ public String getId() { return mId; } /** * Returns the expected score of this achievement for a client at a certain level. This can * be any non zero value. By default this is zero if the achievement level is higher than the * given level and else this is getMaxScoreReward(). * @param forLevel The level to check the expected score for. * @return The expected score. Should be non negative. */ public int getExpectedScore(int forLevel) { if (forLevel >= mLevel) { return getMaxScoreReward(); } return 0; } AchievementManager.AchievementChangeEvent getStateChangeEvent() { return mStateChangeEvent; } }