Idea

pingu2k4 avatar image
pingu2k4 suggested

Sample cloudscript to validate user can claim quest completion

I recently completed writing some cloudscript to ensure that players are unable to claim rewards from a quest more often than possible (due to the release schedule for quests) and thought it might be useful for others to change it into their own thing.

I am using this with google play game services quest system, when a user claims a quest. It should be fairly easy to adapt this however to protect users claiming other things more often than they should... Daily bonuses, periodic free gifts etc. I changed the Quest ID's away from my actual ones for obvious reasons, but kept in "believable" values to show how it works.

(It's not necessarily the absolute best way to do this, but wanted to share in case it saves someone else some time / hassle. :) )

function AddCurrency(ActualCoins)
{
    var AddUserVirtualCurrencyRequest =
           {
               "PlayFabId": currentPlayerId,
               "VirtualCurrency": "CO",
               "Amount": ActualCoins
           };
    var AddUserVirtualCurrencyResult = server.AddUserVirtualCurrency(AddUserVirtualCurrencyRequest);
}



///////////////////////////////
// Handling Quest Completion //
///////////////////////////////

//A dictionary of Quest ID's and their numerical rewards.
var QuestDictionary = {
    qqqweedfsfsdfsdfas: 25,
    qqqghsfdswaerfhytf: 25,
    qqqasdfgsdrfgrdrgf: 25,
    qqqgthfdtgyhytrhty: 25,
    qqqdtghghfghdfghfd: 25,
    qqqjhkkklihjkhjkjh: 25,
    qqqgfhdgfhdghfghhh: 25,
    qqqkjhdfshdlkclxkj: 100,
    qqqdfgsfgdfgdfgdfg: 250,
    qqqfkhjghgfnhghnhn: 250,
    qqqdfghdfghdsdfdfd: 250,
    qqqfdghfgdhfghfghh: 250
};

//args.QuestID should be the quest ID they are claiming to have completed. 
handlers.CompleteQuest = function (args)
{
    try
    {
        //Check that QuestID is provided.
        if (args == null || typeof args.QuestID === undefined || args.QuestID === "")
        {
            throw "Failed - Incorrect Args.";
        }

        //Check that QuestID matches one of the recognised quest ID's
        if(!QuestDictionary.hasOwnProperty(args.QuestID))
        {
            throw "Unknown Quest ID.";
        }

        //Retrieve the users CompletedQuests key so we can see which quests they have previously completed and when.
        var GetUserInternalDataRequest =
        {
            "PlayFabId": currentPlayerId,
            "Keys": ["CompletedQuests"]
        };
        var GetUserInternalDataResult = server.GetUserInternalData(GetUserInternalDataRequest);

        var QuestCompletion = {};
        var QuestID = args.QuestID;
        var retObj = {};

        //If the user has never completed a quest, the key will be missing. Go ahead and create it. Else we must check validity...
        if(!GetUserInternalDataResult.Data.hasOwnProperty("CompletedQuests"))
        {
            QuestCompletion[QuestID] = new Date().toUTCString();
            retObj = ProcessCompletedQuest(QuestID, QuestCompletion);
        }
        else
        {
            QuestCompletion = JSON.parse(GetUserInternalDataResult.Data["CompletedQuests"].Value);

            //If the user has never completed this specific quest before, then go ahead and give the reward. Else, check its validity...
            if(!QuestCompletion.hasOwnProperty(QuestID))
            {
                QuestCompletion[QuestID] = new Date().toUTCString();
                retObj = ProcessCompletedQuest(QuestID, QuestCompletion);
            }
            else
            {
                var LastTimeCompleted = Date.parse(QuestCompletion[QuestID]);
                var DaysSinceLastCompletion = CalculateDayDifference(LastTimeCompleted, Date.now());

                //Quests with different rewards have different minimum lengths inbetween. Whichever the case, check they are valid.
                switch (QuestDictionary[QuestID])
                {
                    case 25:
                        if(DaysSinceLastCompletion < 7)
                        {
                            throw "ANTIHACK. Quest: " + QuestID + ". Days Since Last Completion: " + DaysSinceLastCompletion + ".";
                        }
                    case 100:
                        var Day = Date.now().getDay();
                        if (Day == 0) {
                            Day = 7;
                        }
                        if (DaysSinceLastCompletion < Day) {
                            throw "ANTIHACK. Quest: " + QuestID + ". Days Since Last Completion: " + DaysSinceLastCompletion + ". Current Day: " + Day + ".";
                        }
                        break;
                    case 250:
                        if (DaysSinceLastCompletion < 23) {
                            throw "ANTIHACK. Quest: " + QuestID + ". Days Since Last Completion: " + DaysSinceLastCompletion + ".";
                        }
                        break;
                    default:
                        throw "Unknown Reward.";
                        break;
                }

                QuestCompletion[QuestID] = new Date().toUTCString();
                retObj = ProcessCompletedQuest(QuestID, QuestCompletion);
            }
        }

        return retObj;
    }
    catch (e)
    {
        var retObj = {};
        retObj["errorDetails"] = "Error: " + e;
        return retObj;
    }
}

function ProcessCompletedQuest(QuestID, QuestCompletion)
{
    //All validity checks were satisfied - Push the new QuestCompletion data back onto user.
    var UpdateUserInternalDataRequest =
    {
        "PlayFabId": currentPlayerId,
        "Data": {}
    };

    UpdateUserInternalDataRequest.Data["CompletedQuests"] = JSON.stringify(QuestCompletion);
    var UpdateUserInternalDataResult = server.UpdateUserInternalData(UpdateUserInternalDataRequest);

    //Add the correct amount of currency.
    AddCurrency(QuestDictionary[QuestID]);

    var retObj = {};
    retObj["Success"] = true;
    return retObj;
}

function CalculateDayDifference(First, Second)
{
    var FirstDate = new Date(First);
    var SecondDate = new Date(Second);
    return Math.abs((Date.UTC(FirstDate.getFullYear(), FirstDate.getMonth(), FirstDate.getDate()) - Date.UTC(SecondDate.getFullYear(), SecondDate.getMonth(), SecondDate.getDate())) / 86400000);
}

For those interested, I have 3 types of quest. The first, I have 7 different dailies. One which repeats on Monday, one on Tuesday etc. (they all award 25 coins). The check for those checks that at least 7 days (When counting absolute days) have passed since the last time that quest was claimed.

The second type is a weekly. It awards 100 coins, and repeats itself every week. Because of this, someone could complete it on a sunday, and then the next monday making it 1 day apart. So I added in a little extra to this check using Date.now().getDay().

Finally, the other 4 repeat weekly, but one of them begins on the first of the month (lasting a week), the next begins on the 8th of the month etc. I just checked that in the worst case scenario (Completing a quest in the last day on February on a non leap year then in the first day in March) that would satisfy the results. Technically, someone could call this a coupld days early most the year round but the effort : reward ratio is so small that leaving it like this makes sense.

Open to any feedback, and hope this helps someone. :)

CloudScript
10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

1 Comment

·
jrmcdona avatar image
jrmcdona commented
@pingu2k4

where do you define your Quests and the quest details? Can you post an example Quest JSON?

10 |1200

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Write a Comment

Up to 2 attachments (including images) can be used with a maximum of 512.0 KiB each and 1.0 MiB total.

Your Opinion Counts

Share your great idea, or help out by voting for other people's ideas.