client/code/game/game.resources.js
'use strict'
/* global ss, $, $game, d3 */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
resources.js
- In-game resources that NPCs give to the player
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
var _ = require('underscore')
var TANGRAMS = require('/data.tangrams.js')
// TODO: Resource object.
function Resource (data, skinSuit) {
// Copy contents of resource data to this object
for (var i in data) {
this[i] = data[i]
}
this.playerAnswers = []
// TODO: See where this is being used. Should the skinsuit
// be a property of the NPC, the resource, or either?
if (skinSuit) {
this.skinSuit = skinSuit
}
}
Resource.prototype.getNumResponses = function () {
return this.playerAnswers.length
}
Resource.prototype.addPlayerResponse = function (data) {
return this.playerAnswers.push(data)
}
// Remove a public player response from the resource's list of player responses
Resource.prototype.removePlayerResponse = function (data) {
this.playerAnswers = _.reject(this.playerAnswers, function (answer) {
return answer.playerId === data.playerId
})
}
// Tangram pieces as object prototypes.
var _tangrams = []
function Tangram (data) {
this.id = data.id // Numerical unique id for each tangram piece
this.name = data.name // "correctX" or "wrongX" - not a unique identifier. Used by resource to identify which piece to use, currently.
this.level = data.level // Pieces are associated with a level. Each level has a group of tangrams that go into a puzzle.
this.path = data.path // SVG shape path
this.fill = data.fill // Fill color, a string. Use method .getCSSColor to get a CSS/Canvas-compatible color string.
}
Tangram.prototype.getCSSColor = function () {
var fills = {
orange: 'rgb(236,113,41)',
lightOrange: 'rgb(237,173,135)',
blue: 'rgb(14,152,212)',
lightBlue: 'rgb(109,195,233)',
green: 'rgb(76,212,206)',
lightGreen: 'rgb(164,238,235)'
}
return fills[this.fill] || 'rgb(0,0,0)' // Fallback to black
}
var $resources = module.exports = {
ready: false,
inventorySlotsPerLevel: {},
totalResources: {},
// load in all the resources and the corresponding answers
init: function (callback) {
// Get all resources
var npcs = $game.$npc.getNpcData()
$.each(npcs, function (key, npc) {
if (npc.isHolding) {
_resources.data[npc.resource.id] = new Resource(npc.resource, npc.skinSuit)
// Set up resource counts per level - used for inventory counts
// This only counts resources held by NPCs that will be issued as
// inventory pieces (as in, there is a shape)
// It does not count resources that do not have shapes (like those that are
// attached to a resume question)
var resource = _resources.data[npc.resource.id]
if (resource.shape !== '') {
this.inventorySlotsPerLevel[npc.level] = this.inventorySlotsPerLevel[npc.level] + 1 || 1
}
}
}.bind(this))
// HACK
// Update the URL of the Master NPC resource, if
// the player has obtained one of the community resources
if ($game.$player.getResource(4000)) {
var communityIDs = [4001, 4002, 4003]
for (var i = 0; i < communityIDs.length; i++) {
var checkResource = $game.$player.getResource(communityIDs[i])
if (checkResource && checkResource.rewarded === true && checkResource.attempts > 0) {
$game.$resources.get(4000).url = communityIDs[i].toString()
break
}
}
}
// Create all tangram pieces - construct an array of all tangram pieces as object prototypes.
_tangrams = TANGRAMS.map(function (tangram) {
return new Tangram(tangram)
})
// Create array of ALL player responses
ss.rpc('game.npc.getResponses', $game.$player.instanceName, function (response) {
var allPlayerResponses = response[0].resourceResponses
$.each(allPlayerResponses, function (key, answer) {
if (answer.madePublic === true) {
$resources.get(answer.resourceId).addPlayerResponse(answer)
}
})
$resources.ready = true
callback()
})
},
resetInit: function () {
$resources.ready = false
},
get: function (id) {
return _resources.data[id]
},
getAll: function () {
return _resources.data
},
// Returns the total number of obtainable resources
getTotalResourceCount: function () {
return _resources.data.reduce(function (a) { return a + 1 }, 0)
},
debug: function () { // TODO: REMOVE
console.log(_resources.data)
},
// Decide how to display resource on screen depending on state of player
showResource: function (id) {
var el = document.getElementById('resource-area')
var resource = _resources.data[id]
// Load resource content, then display.
$game.flags.set('visible-resource-overlay')
_resources.loadArticle(resource, function () {
$game.$audio.playTriggerFx('windowShow')
$game.$audio.fadeLow()
_resources.addContent(id, 1)
$(el).fadeIn(300)
})
},
// Called when player views a resource from inventory
examineResource: function (id) {
// If an id is called without a resource.url, then bail
if (_resources.data[id].url === '') return
// HIDES (not closes) the inventory, then show resource
// Set a flag that remembers we were in the inventory
$game.flags.set('viewing-inventory')
$game.inventory.hide(function () {
$resources.showResource(id)
})
},
// Hide the resource area
hideResource: function (callback) {
var el = document.getElementById('resource-area')
$(el).fadeOut(300, function () {
// Reset all resource slides and buttons to a hidden & clean state.
_resources.resetSlides()
// Clean up background globals & game state flags
_resources.temporaryAnswer = ''
$game.flags.unset('visible-resource-overlay')
// Clear resource stage
_resources.unloadArticle()
// Restore sound level
$game.$audio.fadeHi()
// If inventory was showing previously, re-open the inventory
if ($game.flags.check('viewing-inventory') === true) $game.inventory.show()
if (typeof callback === 'function') callback()
})
},
// Activated when clicking on something that is specific to viewing answers
examineResponses: function (id) {
var overlay = document.getElementById('resource-area')
var el = overlay.querySelector('.resource-responses')
_resources.addContent(id, 4)
// Display rules
_resources.hideContent()
el.style.display = 'block'
if ($(overlay).is(':hidden')) {
$game.flags.set('visible-resource-overlay')
$(overlay).fadeIn(300)
}
},
// Display messages on checking user input
showCheckMessage: function (message, callback) {
var $check = $('#resource-area .check')
var $el = $check.find('.message-feedback')
var $confirmBtn = $el.find('.btn-primary')
$check.find('.check-dialog').hide()
$check.show()
$el.find('.feedback').text(message)
$confirmBtn.on('click', function () {
$resources.hideCheckMessage(callback)
}).show()
$el.fadeIn(200)
$confirmBtn.focus()
},
hideCheckMessage: function (callback) {
var $el = $('#resource-area .check')
if ($el.is(':visible')) {
$el.fadeOut(200, callback)
}
},
// Get the Tangram for a given resource id
getTangram: function (id) {
var resource = $resources.get(id)
return _.findWhere(_tangrams, { level: $game.$player.currentLevel, name: resource.shape })
},
getTangrams: function () {
return _tangrams
}
}
/**
*
* PRIVATE FUNCTIONS
*
**/
var _resources = {
data: [],
temporaryAnswer: '',
resetSlides: function () {
var overlay = document.getElementById('resource-area')
var article = overlay.querySelector('.resource-article')
this.hideContent()
// Clear article content to prevent it from affecting the rest of the game, e.g. stopping videos that are still playing
// This is equivalent to, but faster than & less prone to memory leaks than innerHTML = ''
while (article.firstChild) article.removeChild(article.firstChild)
// When slides are reset, always reset all buttons
this.resetButtons()
},
hideContent: function () {
var overlay = document.getElementById('resource-area')
var listOfContentElementSelectors = [
'.resource-content',
'.resource-article',
'.resource-question',
'.resource-responses',
'.resource-custom-content'
]
for (var i in listOfContentElementSelectors) {
var el = overlay.querySelector(listOfContentElementSelectors[i])
el.style.display = 'none'
}
},
resetButtons: function () {
var buttons = document.getElementById('resource-area').querySelector('.buttons')
// Reset event listeners by cloning and hide all buttons
_.each(buttons.querySelectorAll('button'), function (button) {
var clone = button.cloneNode(true)
button = button.parentNode.replaceChild(clone, button)
clone.style.display = 'none'
})
},
// Preloads the resource article into the staging area
loadArticle: function (resource, callback) {
// Continue if there is no resource article.
if (!resource.url) {
callback()
return
}
// Otherwise, go get that resource article and pre-load it!
ss.rpc('game.resource.get', resource.url, function (html) {
$('#resource-stage').empty().html(html)
callback()
})
},
// Clears staging area
unloadArticle: function () {
var el = document.getElementById('resource-stage')
// This is equivalent to, but faster than & less prone to memory leaks than innerHTML = ''
while (el.firstChild) el.removeChild(el.firstChild)
},
unloadTangram: function () {
var artboard = document.getElementById('resource-area').querySelector('.tangram')
artboard.innerHMTL = ''
d3.select('#resource-area .tangram svg').remove()
},
// Loads the tangram piece and adds it into DOM
loadTangram: function (resource) {
// Loads the SVG version of the tangram.
var artboard = document.getElementById('resource-area').querySelector('.tangram')
var artboardX = artboard.offsetWidth
var artboardY = artboard.offsetHeight
var shape = $resources.getTangram(resource.id)
var fill = shape.getCSSColor()
// Clear previous SVG if any
this.unloadTangram()
var svg = d3.select('#resource-area .tangram').append('svg').attr('class', 'tangram-svg')
var path = svg.append('path')
.attr('d', shape.path)
.attr('fill', fill)
.attr('stroke', 'rgb(255,255,255)')
.attr('stroke-width', 0)
var pathCentroid = _getCentroid(path)
var displayX = (artboardX / 2) - pathCentroid[0]
var displayY = (artboardY / 2) - pathCentroid[1]
// Set the tangram to display in the middle of the area
path.attr('transform', 'translate(' + displayX + ',' + displayY + ')')
function _getCentroid (selection) {
var bbox = selection.node().getBBox()
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2]
}
},
// Preloads question information into the DOM
loadQuestion: function (resource) {
var overlay = document.getElementById('resource-area')
var el = overlay.querySelector('.resource-question')
var form = el.querySelector('form')
var formHTML = ''
// Fill in the question
el.querySelector('.question').textContent = resource.question
// Create the answer form
switch (resource.questionType) {
case 'multiple':
for (var i = 0; i < resource.possibleAnswers.length; i++) {
formHTML += '<input name="resourceMultipleChoice" type ="radio" id="answer_' + i + '" value="' + resource.possibleAnswers[i] + '"><label for="answer_' + i + '">' + resource.possibleAnswers[i] + '</label><br>'
}
break
case 'open':
formHTML = '<textarea class="open-response" placeholder="Type your answer here..." maxlength="5000" autofocus>' + _resources.temporaryAnswer + '</textarea></form><p class="privacy-message">Your answer will be private by default. You can later choose to make it public to earn special seeds.</p>'
break
case 'truefalse':
formHTML = '<input name="resourceMultipleChoice" type="radio" id="true" value="true"><label for="true">true</label>' +
'<br><input name="resourceMultipleChoice" type="radio" id="false" value="false"><label for="false">false</label>'
break
case 'yesno':
formHTML = '<input name="resourceMultipleChoice" type="radio" id="yes" value="yes"><label for="yes">yes</label>' +
'<br><input name="resourceMultipleChoice" type="radio" id="no" value="no"><label for="no">no</label>'
break
case 'resume':
formHTML = this.makeResumeFormHTML(resource)
break
default:
formHTML = 'Whoops! The game tried to set up a type of question that doesn’t exist!'
break
}
form.innerHTML = formHTML
// Autofocus on input box, if any
if (resource.questionType === 'resume') {
// Add scrollable to form
form.classList.add('scrollable', 'resume-form')
// Wait for DOM render
setTimeout(function () {
// Find focus
$('.resume-text-input').filter(':first').focus()
// Bind event to form submit action
$('.resume-form').submit(function (e) {
// Prevent actual submittal
e.preventDefault()
// Submit form contents as a string to rpc
// Why does this happen twice?
// TODO: fix this
// Temporarily (?) hijacked by getAnswer() to save to answer field
var formContents = $(this).serialize()
console.log(formContents)
ss.rpc('game.player.saveResumeAnswer', {
id: $game.$player.id,
form: formContents
})
// Find the answer button and trigger click action on it
$('#resource-area .buttons .answer-button').click()
})
}, 0)
}
},
// TODO: put resume HTML templates elsewhere
makeResumeFormHTML: function (resource) {
var html = ''
// Notes on questions and desired functionality from Sam's Google Doc
switch (resource.answer) {
// Purpose
// "What is your passion, and how do you want to work toward it?"
case 'purpose':
// One short text box
html = '<input class="resume-text-input" name="resume-purpose" type="text" value="" placeholder="" maxlength=255>'
break
// Education
// What is your field of study at college?
case 'education':
// One short text box
html = '<input class="resume-text-input" name="resume-education" type="text" value="" placeholder="" maxlength=140>'
break
// Experience
// What is your previous experience with civic engagement?
// List each organization you've worked with in the past, and
// (briefly) what you did there.
case 'experience':
// Three text boxes (potentially with the ability to add another?)
html = '<div class="resume-experience-section">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-organization-01">Organization #1</label>\
</div><div class="col-sm-10">\
<input id="resume-experience-organization-01" class="resume-text-input" name="resume-experience-organization-01" type="text" value="" placeholder="" maxlength=50>\
</div>\
</div class="row">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-summary-01">Summary</label>\
</div><div class="col-sm-10">\
<textarea id="resume-experience-summary-01" class="resume-textarea" name="resume-experience-summary-01"></textarea>\
</div>\
</div class="row">\
</div>\
<div class="resume-experience-section">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-organization-02">Organization #2</label>\
</div><div class="col-sm-10">\
<input id="resume-experience-organization-02" class="resume-text-input" name="resume-experience-organization-02" type="text" value="" placeholder="" maxlength=50>\
</div>\
</div class="row">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-summary-02">Summary</label>\
</div><div class="col-sm-10">\
<textarea id="resume-experience-summary-02" class="resume-textarea" name="resume-experience-summary-02"></textarea>\
</div>\
</div class="row">\
</div>\
<div class="resume-experience-section">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-organization-03">Organization #3</label>\
</div><div class="col-sm-10">\
<input id="resume-experience-organization-03" class="resume-text-input" name="resume-experience-03" type="text" value="" placeholder="" maxlength=50>\
</div>\
</div class="row">\
<div class="row">\
<div class="col-sm-2">\
<label class="resume-label" for="resume-experience-summary-03">Summary</label>\
</div><div class="col-sm-10">\
<textarea id="resume-experience-summary-03" class="resume-textarea" name="resume-experience-summary-03"></textarea>\
</div>\
</div>\
</div>\
<p><strong>If you need more than three, you can add more later!</p>'
break
// Skills
// What skills would you bring to a civic engagement opportunity? Check all that apply!
case 'skills':
// Checkboxes
// Hard code all of this for now...
var skills = [
'Social Media Content Development',
'Social Media Back-end Management',
'Salesforce / Raiser’s Edge / Constant Contact / CRM',
'SPSS / STATA',
'Microsoft Office Suite',
'Access / Database Development and Reporting',
'Data Cleaning / Data Management',
'Graphic Design - Marketing / Promotion',
'Graphic Design – Infographics / Data Visualization',
'Multimedia Production',
'Quantitative Modelling / Statistical Analysis / Excel',
'Administration and Organizational',
'Project Design',
'Group Facilitation',
'Manage Teams',
'Working across Difference',
'Child Development',
'Early Literacy',
'Languages (you can add specifics later)',
'ESL Instruction',
'Nutrition / Food Preparation / Food Security Support',
'Qualitative Data Collection – Interview / Focus Groups',
'Qualitative Analysis – Coding / NVivo',
'Event Planning / Logistics / Event Support',
'Public Speaking',
'Writing – Marketing / Promotion',
'Writing – Reports / Expository',
'Copyediting / Proofreading',
'Fundraising',
'Environmental Stewardship / Environmental Program Operations',
'Recycling Program Operations',
'Public Health',
'Performing Arts',
'Community Outreach / Recruitment',
'Working with Underserved Populations',
'Client Support (you can add specifics later)',
'CPR',
'Human Subjects / CITI',
'Familiar with Public Transport',
'Other Skill (you can add specifics later)'
]
for (var i = 0, j = skills.length; i < j; i++) {
html += '<input name="resume-skills" type="checkbox" id="resume-skills-' + i + '" value="' + skills[i] + '"> <label for="resume-skills-' + i + '">' + skills[i] + '</label> <br>'
}
break
// Tagline
// In just a few words, describe yourself (in a civic engagement context). Example: "Skilled artist, big heart"
case 'tagline':
// One short text box with a tight character limit
html = '<input class="resume-text-input" name="resume-tagline" type="text" value="" placeholder="" maxlength=50>'
break
// Catch all for typos etc
default:
html = '<strong class="color-red">Error:</strong> The game attempted to ask a resume question type that does not exist. Resume type provided: ' + resource.answer
break
}
return html
},
// Adds content for the screen if the Player answered the resource question correctly
loadRewards: function (resource) {
var el = document.getElementById('resource-area').querySelector('.resource-content')
var input = el.querySelector('.tagline-input input')
var npc = $game.$npc.findNpcByResourceId(resource.id)
var playerLevel = $game.$player.currentLevel
var feedback = (resource.feedbackRight.length < 1) ? 'Thanks for sharing.' : resource.feedbackRight
var dialogue = ''
var skin = $game.$skins.getSkin(resource.skinSuit)
var seedsRewarded = $game.$player.getResource(resource.id).seedsRewarded
// Legacy stuff saved here, never used.
// _rightOpenRandom = ['Very interesting. I\'ve never looked at it like that before.', 'That says a lot about you!', 'Thanks for sharing. Now get out there and spread some color!'],
if (npc.level < playerLevel || resource.questionType === 'resume') {
// This can happen because not all tangram pieces need to be obtained to
// solve the botanist's puzzle. The player only needs the "correct" ones.
// As a result, if a player goes back to talk to an NPC they haven't talked
// to, they can still complete it, but will no longer obtain the tangram piece
// since it is no longer necessary.
// ALSO: No tangram piece is obtained if player is answering a resume question.
// The player will still obtain seeds and skin rewards.
dialogue = feedback + ' Here, take ' + seedsRewarded + ' seeds!'
// Hide all the tangram related stuff
_resources.unloadTangram()
el.querySelector('.tagline-input').style.display = 'none'
} else {
// Load the tangram as an SVG path
_resources.loadTangram(resource)
// Reset and focus tagline input
el.querySelector('.tagline-input').style.display = 'block'
input.value = ''
input.focus()
// Bind an event to the submit action of the tagline input form
$('.tagline-input').submit(function (e) {
e.preventDefault()
$('#resource-area .save-button').trigger('click')
})
// Bind a check event listener to the standard close button on the upper right
$('#resource-area .close-overlay').on('click.onCloseCheck', function (e) {
e.stopImmediatePropagation()
e.preventDefault()
// Basically, do the same thing as the save button in this case.
$('#resource-area .save-button').trigger('click')
})
dialogue = feedback + ' Here, take this puzzle piece, and ' + seedsRewarded + ' seeds!'
}
// give them the skinsuit regardless if in prev level or not
if (skin) {
dialogue += ' You unlocked the ' + skin.name + ' suit! Try it on or browse your other suits by clicking the changing room button below.'
}
$game.$audio.playTriggerFx('resourceRight')
el.querySelector('.speaker').textContent = npc.name
el.querySelector('.message').textContent = dialogue
},
// A variation on loadRewards() to display 'Master NPC' content
loadMasterNPCContent: function (resource) {
var el = document.getElementById('resource-area').querySelector('.resource-custom-content')
var npc = $game.$npc.findNpcByResourceId(resource.id)
_resources.hideContent()
el.style.display = 'block'
el.querySelector('.speaker').textContent = npc.name
},
// Load other players answers and your own
loadResponses: function (resource) {
var el = document.getElementById('resource-area').querySelector('.resource-responses')
var playerResource = $game.$player.getAnswer(resource.id)
var playerPublic = false
var playerHTML = ''
var responsesHTML = ''
var npc = $game.$npc.findNpcByResourceId(resource.id)
var dialogue = ''
// Process public responses (we do not have access to non-public responses here)
for (var i = 0; i < resource.playerAnswers.length; i++) {
var thisAnswer = resource.playerAnswers[i]
if (thisAnswer.playerId === $game.$player.id) {
// If yours is public, remember this for later
playerPublic = true
} else {
// Create HTML snippet of all other players' public responses
responsesHTML += '<li class="response"><p><span>' + thisAnswer.name + ': </span>' + thisAnswer.answer + '</p><div class="pledge-button"><button class="btn btn-success" data-resource="' + resource.id + '" data-player="' + thisAnswer.playerId + '">Seed It!</button></div></li>'
}
}
// Determine what NPC says for status
if (responsesHTML !== '') {
dialogue = 'Here are some recent answers by your peers.'
} else {
if (!playerPublic) {
dialogue = 'There are no public answers. If you make your answer public, other players can give you more seeds!'
} else {
dialogue = 'Your answer is shown below, but other players have not made their answers public.'
}
responsesHTML = '<li class="response response-notice"><p>More answers from your peers will appear shortly. Be sure to check back.</p></li>'
}
// add in the player's answer with the appropriate button
// TODO: Make a better templating system for all of this
playerHTML += '<li class="response your-response"><p><span>' + 'You said' + ': </span>' + playerResource.answers[playerResource.answers.length - 1] + '</p>'
if (!playerPublic) {
playerHTML += '<div class="public-button"><button class="btn btn-info" data-resource="' + resource.id + '">Make Public</button> <i class="fa fa-lock fa-lg"></i></div>'
} else {
playerHTML += '<div class="private-button"><button class="btn btn-info" data-resource="' + resource.id + '">Make Private</button> <i class="fa fa-unlock-alt fa-lg"></i></div>'
}
playerHTML += '</li>'
el.querySelector('.question').innerHTML = 'Q: ' + resource.question
el.querySelector('.content-box ul').innerHTML = playerHTML + responsesHTML
el.querySelector('.speaker').textContent = npc.name
el.querySelector('.message').textContent = dialogue
// Bind a check event listener to the standard close button on the upper right
$('#resource-area .close-overlay').on('click.onCloseResource', function (e) {
e.stopImmediatePropagation()
e.preventDefault()
// Basically, do the same thing as the close button in this case.
$('#resource-area .close-button').trigger('click')
})
},
// Clear the display and decide what to show on screen
addContent: function (resourceId, section, slide) {
var overlay = document.getElementById('resource-area')
var answer = $game.$player.getAnswer(resourceId)
var isAnswered = (answer && answer.result) ? true : false
var isRevisit = $game.flags.check('viewing-inventory')
var resource = _resources.data[resourceId]
var $article = $('#resource-stage > section')
var slides = $article.length
// Reset all resource slides and buttons to a hidden & clean state.
_resources.resetSlides()
// Skip section 1 if the NPC's resource does not have URL field
// It means there is no article content to display, so go straight to question
// Resume question types are like this, so are some open-ended questions.
if (section === 1 && resource.url === '') {
section = 2
}
// HACK - LOGIC ROUTING FOR MASTER NPC
// TODO: Need logic to handle state
if (resource.id === 4000) {
// If the player has not received correct resources for any of the
// community NPCs, go to the special Master NPC screen
if (!($game.$player.checkForCompletedResource(4001) === true ||
$game.$player.checkForCompletedResource(4002) === true ||
$game.$player.checkForCompletedResource(4003) === true)) {
section = 4000
}
}
// Determine what content to add.
switch (section) {
// [SECTION 01] ARTICLE.
case 1:
if (!slide) slide = 0
if (slide < 0 || slide === slides) this.addContent('next', 4) // Exit out if bad slide
// Load and show article content. Assuming already preloaded!
var page = $article.get(slide).innerHTML
$('.resource-article').html(page).show()
// Look for links and attach a handler to open them in a new window.
// This method is necessary for links on the same domain otherwise
// Davis will attempt to capture it for routing purposes, which we don't want
$('.resource-article').find('a').on('click', function (e) {
e.preventDefault()
e.stopImmediatePropagation()
window.open(e.target.href, '_blank')
})
// Logic for adding buttons
// Always add a next button if there is more article to show
if (slide < slides - 1) _addButton('next', 1, slide + 1)
// On the last article slide, we must test for certain conditions
else if (slide === slides - 1) {
// If this resource is being reviewed later:
if (isRevisit || isAnswered) {
// If open-ended question, go to responses next
if (resource.questionType === 'open') _addButton('next', 4)
// If question was answered correctly for any other question type, close resource window
else _addButton('close', null, null, _checkBotanistCallback)
} else {
// If question was not answered correctly, go to next slide (question screen)
_addButton('next', 2)
}
}
// Add a back button if it's not the first slide.
if (slide > 0) _addButton('back', 1, slide - 1)
break
// [SECTION 02] QUESTION.
// The next slide after the article is the Question screen, which displays if the
// player has NOT answered this question correctly.
case 2:
// Load and show question.
_resources.loadQuestion(resource)
overlay.querySelector('.resource-question').style.display = 'block'
// Add buttons
_addButton('answer')
// No back button for questions without articles to view
if (resource.url !== '') {
_addButton('back', 1, slides - 1, function () {
// If they were answering an open question, store their answer if the player goes back
if (resource.questionType === 'open') {
_resources.temporaryAnswer = overlay.querySelector('.open-response').value
}
})
}
// After submitting an answer, if incorrect, the player is kicked back out to the game.
// If correct, the player goes to section [3].
// If answered, the player skips to section [4].
break
// [SECTION 03] REWARD.
// Only shown immediately after section [2] if it is answered correctly.
case 3:
overlay.querySelector('.resource-content').style.display = 'block'
// Load resource details and draw tangram - note that this needs to happen after
// the visibility is set to 'block' because we calculate div width/height in this function.
_resources.loadRewards(resource)
if (resource.questionType === 'open') {
_addButton('save', 4)
} else if (resource.questionType === 'resume') {
_addButton('close')
} else {
_addButton('save', null, null, _checkBotanistCallback)
}
break
// [4] RESPONSES.
// Shown immediately after slide [3] if the player gets it correct, -OR-
// immediately after [1] if question was answered correctly and player is revisiting.
case 4:
_resources.loadResponses(resource)
overlay.querySelector('.resource-responses').style.display = 'block'
_addButton('close', null, null, _checkBotanistCallback)
// If an article was preloaded onto the stage, display the 'back' button.
if (document.getElementById('resource-stage').innerHTML !== '') _addButton('back', 1, slides - 1)
break
// [4000] MASTER NPC.
// Kind of a hack, but this is a screen to show for the Master NPC's
// introductory thing. It is only activated through activating Master NPC
// for the first time, and will display until a player receives one of the
// community NPCs' resources.
// Behind the scenes, upon reading this message, the player obtains a
// placeholder resource from the Master NPC that "unlocks" the community NPCs.
// It will be refreshed later with the Q&A content.
case 4000:
_resources.loadMasterNPCContent(resource)
_addButton('close')
// Momentarily save dummy data for viewing this thing
var data = {
id: resource.id,
answer: '',
attempts: 0,
npcLevel: null,
questionType: null,
skinSuit: null,
correct: false,
tagline: null
}
$game.$player.saveResourceLocally(data)
break
// Generic error for debugging.
default:
$resources.hideResource(function callback () {
$game.debug('Error Code 4992 dump!')
console.log(resourceId, resource, section, slide)
$game.$npc.showSpeechBubble('Error Code 4992', ['The game failed to provide a slide to display, or tried to display a slide that doesn’t exist. See console for log details.'])
})
break
}
// Private add button function. Displays the button each slide asks for and binds actions to them.
function _addButton (button, section, slide, callback) {
var buttons = overlay.querySelector('.buttons')
var back = buttons.querySelector('.back-button')
var next = buttons.querySelector('.next-button')
var answer = buttons.querySelector('.answer-button')
var save = buttons.querySelector('.save-button')
var close = buttons.querySelector('.close-button')
// Note on EventListeners. Removal is possible within the function itself (the easiest
// way is to name the function so you can remove it) but there is no good way to remove
// event listeners on *other* buttons, which is necessary because there is usually a
// binary choice on these (e.g. previous and next buttons co-existing at the same time.)
// As a result, event listeners are cleared on the resetButtons() function by programatically
// cloning every button.
switch (button) {
case 'next':
next.style.display = 'inline-block'
next.addEventListener('click', function () {
if (typeof callback === 'function') callback()
_resources.addContent(resourceId, section, slide)
})
break
case 'back':
back.style.display = 'inline-block'
back.addEventListener('click', function () {
if (typeof callback === 'function') callback()
_resources.addContent(resourceId, section, slide)
})
break
case 'answer':
answer.style.display = 'inline-block'
answer.addEventListener('click', function () {
if (typeof callback === 'function') callback()
// This is kind of a dumb place to put it, but it's the best place for it to
// work right now. If it's an open-ended question, check to make sure that the
// response is sufficient. If not, exit prematurely and preserve the state of the form.
// It doesn't seem possible to put these returns inside another callback function
// because it only returns out of the callback, not the listener function.
if (resource.questionType === 'open') {
if (_resources.validateOpenResponse(resource) !== true) return
}
// Here is where the answer gets checked. If it's correct, save the answer and move
// to the next slide. If not, we'll record that the answer was wrong, and quit.
if (_resources.checkAnswer(resource) === true) {
_resources.submitAnswer(resource, true)
// Go to reward screen.
_resources.addContent(resourceId, 3)
} else {
_resources.submitAnswer(resource, false)
// Quit
_resources.showFeedbackWrong(resource)
}
})
break
case 'save':
save.style.display = 'inline-block'
save.addEventListener('click', function _saveButton () {
// For all question types except resume,
// check the tagline
if (resource.questionType !== 'resume') {
var input = _resources.readTaglineInput()
var tagline = _resources.sanitizeTagline(input)
// If the tagline is not validated, exit
if (_resources.validateTagline(tagline) !== true) {
return false
} else {
// Else, set tagline and save the resource
$game.$player.setTagline(resource, tagline)
}
}
// Proceed to next screen or close resource window
// depending on the situation
if (section) {
_resources.addContent(resourceId, section)
} else {
// If correct, then unlock suit, add seeds, add
// tangram to inventory, and save answer to DB
if (!isRevisit) {
$game.$player.saveResource(resource)
}
$resources.hideResource(callback)
}
// Cleanup: remove tagline check event
$('#resource-area .close-overlay').off('click.onCloseCheck')
})
break
case 'close':
close.style.display = 'inline-block'
close.addEventListener('click', function _closeButton () {
// If correct, then unlock suit, add seeds, add
// tangram to inventory, and save answer to DB
if (!isRevisit) {
$game.$player.saveResource(resource)
}
// Cleanup: remove close button event listener
$('#resource-area .close-overlay').off('click.onCloseResource')
$resources.hideResource(callback)
})
break
default:
// Nothing.
$game.debug('Warning: the game attempted to add a button that does not exist.')
break
}
return true
}
function _checkBotanistCallback () {
// A callback function. If a resource was just collected, check to see if player shoud be automatically teleported to the botanist.
if (!isRevisit) {
$game.$botanist.checkState()
}
}
},
readTaglineInput: function () {
return document.getElementById('resource-area').querySelector('.tagline-input input').value
},
sanitizeTagline: function (tagline) {
return tagline.trim()
},
validateTagline: function (tagline) {
// This is a callback function to focus on the input box after closing the message
var _focusInput = function () {
document.getElementById('resource-area').querySelector('.tagline-input input').focus()
}
if (tagline.length === 0) {
$resources.showCheckMessage('You should summarize what you learned. You’ll need this later!', _focusInput)
return false
} else {
return true
}
},
// Called by check answer to validate whether an open-ended response is sufficient
validateOpenResponse: function (resource) {
var response = this.getAnswer(resource)
function _focusInput () {
document.querySelector('.open-response').focus()
}
if (response.length === 0) {
$resources.showCheckMessage('Please answer the question!', _focusInput)
return false
} else if (resource.requiredLength && response.length < resource.requiredLength) {
_resources.popupCheck(resource, _focusInput)
return false
} else {
return true
}
},
// Trigger a popup if answer was too short
popupCheck: function (resource, callback) {
var $el = $('#resource-area .check')
$el.find('.check-dialog').hide()
$el.find('.confirm-skimpy').show()
// Bind actions to buttons.
// [1] Acknowledge prompt that your answer is skimpy and submit anyway
$el.find('.sure-button').on('click', function () {
$resources.hideCheckMessage()
_resources.submitAnswer(resource, true)
_resources.addContent(resource.id, 3)
}).show()
// [2] Else, close and retry
$el.find('.retry-button').on('click', function () {
$resources.hideCheckMessage(callback)
}).show()
$el.fadeIn(200)
$el.find('.retry-button').focus()
},
// Check whether the Player made the correct response
checkAnswer: function (resource) {
var response = this.getAnswer(resource)
if (resource.questionType === 'open' || resource.questionType === 'resume') {
// Open-ended and resume questions are never false
return true
} else {
// Compare given answer with the resource's correct answer
return (response === resource.answer) ? true : false
}
},
// Retrieve Player's answers from the question form
getAnswer: function (resource) {
if (resource.questionType === 'open') {
return document.getElementById('resource-area').querySelector('.open-response').value.trim()
} else if (resource.questionType === 'resume') {
return $('.resume-form').serialize()
} else {
return $('input[name=resourceMultipleChoice]:checked').val()
}
},
submitAnswer: function (resource, isCorrect) {
var response = this.getAnswer(resource)
var npc = $game.$npc.findNpcByResourceId(resource.id)
var data = {
id: resource.id,
answer: response,
npcLevel: npc.level,
questionType: resource.questionType,
skinSuit: resource.skinSuit,
correct: (isCorrect === true) ? true : false
}
$game.$player.saveResourceLocally(data)
},
// Called after submitAnswer(..., false) because the answer is wrong, and we're done
showFeedbackWrong: function (resource) {
var npc = $game.$npc.findNpcByResourceId(resource.id)
var message = resource.feedbackWrong
$resources.hideResource(function callback () {
$game.$audio.playTriggerFx('resourceWrong')
$game.$npc.showSpeechBubble(npc.name, message)
})
}
}