Created
December 25, 2016 18:09
-
-
Save DolenzSong/1bcfb618effa46ce039cef2903fd9f69 to your computer and use it in GitHub Desktop.
The game "Double Choose" without custom images and sounds, so it can be run in anyone's Codea on any iPad. Of course without proper images it's nonsense; this version is only useful for code sharing.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# Main | |
-- Main | |
saveProjectInfo( "Description", "Two-choice Adventure Games by Rosie and Charlotte." ) | |
--[[ | |
This project creates two simple adventure games that my daughters made up. | |
The only project-specific code is in the Main and ImageSet tabs. | |
All other code can be repurposed without needing direct modification. | |
The tabs explained in order: | |
1. Main sets everything up--instantiating and configuring all the other classes. | |
2. ImageSets contains tables with the names of all the images to be used. | |
3. GameMC is a menu screen for launching different TwoChoiceAdventures. | |
4. TwoChoiceAdventure is a class that can be made into a simple adventure game. | |
5. Screen is a basic interactive screen, the building block of the previous two classes. | |
6. Inventory is a class for tracking, modifying, and displaying inventory. | |
7. StyleTable is a utility class for storing and applying style settings at will. | |
8. Utilities contains miscellaneous handy functions with self-explanatory names. | |
A note on my code conventions: I have attempted wherever possible to stick to the "golden path" | |
convention, which means sticking to only two indentations maximum. I have also attempted to keep | |
every statement to one line of text (as measured in Codea's portrait mode). And as a reaction to | |
the very compelling points made in "Programming Sucks" (http://stilldrinking.org/programming- | |
sucks), I aspired to live by the slogan "the only good code is obsessively commented code." | |
]] | |
--set basic display settings | |
supportedOrientations(LANDSCAPE_ANY) | |
--use this setting inside Codea (so that Codea control buttons are active and visible on screen) | |
displayMode(FULLSCREEN) | |
--use following setting when exporting to XCode (so Codea control buttons don't show on screen) | |
--displayMode(FULLSCREEN_NO_BUTTONS) | |
--a flag for controlling whether or not custom sounds play (flip to false when sharing code) | |
function testDoubleChoose() | |
testGame = TwoChoiceAdventure("test adventure") | |
firstScreen = {name = "firstScreen", background = "ccFirst", narration = "hi there boobly bear", choices = {{choiceText = "go to test screen 2", resultScreen = "testScreenTable2"}}} | |
testScreenTable2 = {name = "testScreenTable2", background = "theAdventureIs", narration = "bagonna hamonna vagolla", choices = {{choiceText = "go to first screen", resultScreen = "firstScreen"}}} | |
testGame:addScreens(firstScreen, testScreenTable2) | |
print("added test screens") | |
testGame:startGame() | |
print("started test game") | |
end | |
--SODA STUFF | |
--user inputs: | |
function keyboard(key) | |
Soda.keyboard(key) | |
end | |
function orientationChanged(ori) | |
Soda.orientationChanged(ori) | |
end | |
--measure performance: | |
profiler={} | |
function profiler.init(quiet) | |
profiler.del=0 | |
profiler.c=0 | |
profiler.fps=0 | |
profiler.mem=0 | |
if not quiet then | |
parameter.watch("profiler.fps") | |
parameter.watch("profiler.mem") | |
end | |
end | |
function profiler.draw() | |
profiler.del = profiler.del + DeltaTime | |
profiler.c = profiler.c + 1 | |
if profiler.c==10 then | |
profiler.fps=profiler.c/profiler.del | |
profiler.del=0 | |
profiler.c=0 | |
profiler.mem=collectgarbage("count", 2) | |
end | |
end | |
--END SODA STUFF | |
function setup() | |
print(WIDTH, HEIGHT) | |
--SODA: | |
profiler.init() | |
Soda.setup() | |
parameter.watch("Soda.focus.title") | |
--overview{} | |
--/SODA | |
prepareConstants() | |
if testingDoubleChoose then | |
testDoubleChoose() | |
return | |
end | |
simpleSetup() | |
end | |
function simpleSetup () | |
startBackgroundMusic() | |
createGames() | |
print("HERE") | |
createGameMC() | |
mainScreen = doubleMC | |
report("finished setup") | |
doneLoading = true | |
end | |
function report(...) | |
if printingReports then | |
local args = {...} | |
print(table.unpack(args)) | |
end | |
end | |
function prepareConstants() | |
testingDoubleChoose = false | |
customSounds = false | |
printingReports = false | |
doneLoading = false | |
shouldUseOnlyCodeaImages = true | |
infoButton = {} | |
defaultFontSize = WIDTH / 720 | |
local defaultSodaFillColor = color(97, 189, 195, 243) | |
local highlightTextColor = defaultSodaFillColor | |
local defaultStrokeColor = color(0, 241, 255, 255) | |
local highlightColor = color(50, 56, 57, 255) | |
local highlightColor = defaultStrokeColor | |
local defaultTextColor = color(255, 255, 255, 255) | |
defaultSodaStyle = { | |
shape = {fill = defaultSodaFillColor, stroke = defaultStrokeColor, strokeWidth = 4}, | |
text = {fontSize = defaultFontSize, font = "GillSans-Light", fill=defaultTextColor}, | |
highlight = { | |
text = {fontSize = defaultFontSize, font = "GillSans-Light", fill=highlightTextColor}, | |
shape = {fill = highlightColor} | |
} | |
} | |
report("finished prepareConstants") | |
end | |
function startBackgroundMusic() | |
if customSounds == true then | |
music("Documents:Choices_rumble-2", true) | |
end | |
report("finished startBackgroundMusic") | |
end | |
function createGames() | |
rosiesGame = makeRosiesGame() | |
charlottesGame = makeCharlottesGame() | |
gamesCreated = true | |
report("finished createGames") | |
end | |
function createGameMC() | |
loadImages(gameMCImages) | |
doubleMC = GameMC(defaultSodaStyle) | |
addInfoScreenToGameMC() | |
doubleMC:addGame("Rosie's Game", rosiesGame) | |
doubleMC:addGame("Charlotte's Game", charlottesGame) | |
customizeGameSelectionScreen() | |
customizeGameExitButtons() | |
print("HERE2") | |
report("finished createGameMC") | |
print("HERE3") | |
end | |
function customizeGameSelectionScreen() | |
--set background image | |
doubleMC.gameSelectionScreen.background = gameMCImages.doubleMenuBackground | |
--define the placement of custom GameMC buttons | |
local roX, roY = WIDTH / 3.4, HEIGHT / 2.5 | |
local chX, chY = WIDTH / 1.41, HEIGHT / 1.995 | |
local roW, roH = WIDTH * 0.482, HEIGHT * 0.537 | |
local chW, chH = WIDTH * 0.369, HEIGHT * 0.523 | |
local resizedRosie = Utilities:makeResizedImage(gameMCImages.doubleMenuRosie, roW, roH) | |
local resizedChar = Utilities:makeResizedImage(gameMCImages.doubleMenuCharlotte, chW, chH) | |
--use those settings to customize the GameMC's game buttons | |
doubleMC:customGameButton("Rosie's Game", resizedRosie, roX, roY) | |
doubleMC:customGameButton("Charlotte's Game", resizedChar, chX, chY) | |
--cutomize the tint color that shows the game buttons have been pressed | |
doubleMC.gameSelectionScreen.touchedTint = color(213, 207, 207, 245) | |
--make a custom action to play a sound when Charlotte's game starts | |
local charlotteButtonAction = function () | |
if customSounds == true then | |
Utilities:playSoundAndWaitUntilFinished("Documents:Charlotte_Game_intro-3") | |
end | |
infoButton:hide() | |
doubleMC:runGame("Charlotte's Game") | |
end | |
--make a custom action to play a sound when Charlotte's game starts | |
local rosieButtonAction = function () | |
if customSounds == true then | |
Utilities:playSoundAndWaitUntilFinished("Documents:Rosie_game_intro-2") | |
end | |
infoButton:hide() | |
doubleMC:runGame("Rosie's Game") | |
end | |
--add those actions to the gameMC | |
doubleMC:customGameButtonAction("Charlotte's Game", charlotteButtonAction) | |
doubleMC:customGameButtonAction("Rosie's Game", rosieButtonAction) | |
end | |
function customizeGameExitButtons() | |
--[[ --old way: | |
--set a fill and stroke style for the rect of a custom exit button | |
fill(87, 90, 109, 129) | |
strokeWidth(0) | |
--make a StyleTable (which captures the current style upon initializing) | |
local exitBoxStyle = StyleTable() | |
--set a style for the text of thecustom exit button | |
fill(255, 255, 255, 255) | |
]] | |
--leftover from non-Soda, but seem to work for calculating button size: | |
font("GillSans-Light") | |
fontSize(HEIGHT / 30) | |
--local exitTextStyle = StyleTable() | |
--define the exit label text | |
local exitText = "back to menu" | |
--get the size of the exit text | |
local textW, textH = textSize(exitText) | |
--define a height and width for the exit label | |
local exitW, exitH = textW + (textW / 1.75), textH + (textH / 1.25) | |
--use those styles to create the image for the button | |
--exitLabel = Utilities:label("back to menu", 200, 50, nil, nil, exitTextStyle, exitBoxStyle) | |
--exitLabel = Utilities:label(exitText, exitW, exitH, nil, nil, exitTextStyle, exitBoxStyle) | |
--replace the GameMC's stored exit button image and location with custom values | |
--doubleMC.exitButton.image = exitLabel | |
--local exitX = math.floor((exitLabel.width / 2) - 1) | |
--local exitY = math.floor(HEIGHT / 12) | |
-- doubleMC.exitButton.x = (exitLabel.width / 2) - 1 | |
-- doubleMC.exitButton.y = HEIGHT / 12 | |
doubleMC.exitButton.x = 0.145 | |
doubleMC.exitButton.y = 0.915 | |
doubleMC.exitButton.w = exitW | |
doubleMC.exitButton.h = exitH | |
doubleMC.exitButton.title = exitText | |
--change the button background color | |
doubleMC.exitButton.style.shape.fill = color(218, 218, 218, 66) | |
doubleMC.exitButton.style.text.fill = color(106, 106, 106, 107) | |
doubleMC.exitButton.style.shape.stroke = doubleMC.exitButton.style.text.fill | |
--modify the exit function to also show the info button | |
local existingExit = doubleMC.exitButtonAction | |
local newExitAction = function() | |
existingExit() | |
infoButton:show() | |
end | |
doubleMC.exitButtonAction = newExitAction | |
end | |
function loadImages(tableWithImageNamesAsKeys) | |
--go through the keys in the table | |
for key, value in pairs(tableWithImageNamesAsKeys) do | |
--if shouldNotUseCustomImages is true, use a Codea default | |
if shouldUseOnlyCodeaImages then | |
tableWithImageNamesAsKeys[key] = readImage("Planet Cute:Dirt Block") | |
else | |
--attempt to get image locally--if image doesn't exist, this will return nil | |
local imageGot = readImage("Project:"..key) | |
--if the local attempt does not come up nil, assign the image to the name given | |
if imageGot == nil then | |
return | |
end | |
tableWithImageNamesAsKeys[key] = imageGot | |
end | |
end | |
end | |
function draw() | |
--SODA | |
pushMatrix() | |
Soda.camera() | |
drawing() | |
popMatrix() | |
profiler.draw() | |
--/SODA | |
end | |
function drawing(breakPoint) | |
-- | |
if doneLoading then | |
mainScreen:draw() | |
--[[ | |
elseif testingDoubleChoose then | |
testGame:draw() | |
else | |
--]] | |
else | |
sprite("Project:doubleMenuBackground", WIDTH /2, HEIGHT/2, WIDTH, HEIGHT) | |
--sprite("Project:Loading Text",WIDTH /2, HEIGHT/2, WIDTH, HEIGHT) | |
end | |
--SODA | |
--in order for gaussian blur to work, do all your drawing here | |
--background(40, 40, 50) | |
--sprite("Cargo Bot:Game Area", WIDTH*0.5, HEIGHT*0.5, WIDTH, HEIGHT) | |
Soda.draw(breakPoint) | |
--/SODA | |
end | |
function touched(touch) | |
--SODA | |
if Soda.touched(touch) then return end | |
--/SODA | |
--if the games have not been created, return without processing touches | |
if doneLoading then | |
--send the touch to the main screen, which redirects it from there | |
mainScreen:touched(touch) | |
--note: mainScreen gets defined in createGames() | |
return | |
elseif testingDoubleChoose then | |
testGame:touched(touch) | |
end | |
end | |
function addInfoScreenToGameMC() | |
local infoText = [['Double Choose' is two small adventure games written entirely by my daughters Rosie and Charlotte. Tap on Rosie to play her game, and tap on Charlotte to play hers. | |
Rosie did the art for her game, and I did most of Charlotte's game under her firm art-direction. | |
You'll enjoy the games most by trying to find all the possible endings. Rosie's game has awesome emotionally-excruciating endings. Charlotte actually included a little adventure-game-style inventory management--impressive for a five-year-old! | |
We made it with the excellent iPad app Codea. We hope you enjoy our games, and we especially hope they inspire other kids to make games too. Try Codea, it rocks!]] | |
--define sizes for the info window | |
local insetX, insetY = math.floor(WIDTH * 0.04), math.floor(HEIGHT * 0.04) | |
local windowW, windowH = WIDTH - (insetX * 2), HEIGHT - (insetY * 2) | |
local textW, textH = windowW - (insetX * 2), windowH - (insetY * 2) | |
--make a function that shows the info window | |
local iFontName = "GillSans-Light" | |
--use a Utilities method to find the right fontSize for the current device's screen | |
local iFontSize = Utilities:pointSizeToFitTextToHeight(infoText, iFontName, windowW, windowH) | |
--adjust iFontSize because, I think, it uses different point values for font sizes | |
iFontSize = iFontSize * 0.045 | |
local infoColor = color(221, 120, 221, 255) | |
--determine the correct the size of the info circle | |
local iCircleSize = WIDTH * 0.065 | |
--define the placement of the info circle font("ArialRoundedMTBold") | |
local iCircleOffset = iCircleSize * 0.48 | |
local infoCircleFontSize = iFontSize * 1.9 | |
local iStrokeWidth = infoCircleFontSize * 0.85 | |
local iCircleColor = color(144, 125, 144, 255) | |
infoButton = Soda.Button{ | |
title = "i", | |
style = { | |
shape = {fill = color(0, 0, 0, 0), stroke = iCircleColor, strokeWidth = iStrokeWidth}, | |
text = {fontSize = infoCircleFontSize, font = "GillSans", fill=iCircleColor}, | |
highlight = { | |
text = {fontSize = infoCircleFontSize, font = "GillSans", fill=color(255, 255, 255, 255)}, | |
shape = {fill = iCircleColor} | |
} | |
}, | |
shape = Soda.ellipse, | |
x = 20, y = 20, w = iCircleSize, h = iCircleSize | |
} | |
local window = 0 --placeholder value to initialize variable but keep it local | |
local infoScreenNeedsMaking = true | |
local showInfo = function() | |
if infoScreenNeedsMaking then | |
window = Soda.Window { | |
title = infoText, | |
label = {x = 0.5, y = 0.5}, | |
blurred = true, | |
close = true, | |
doNotKill = true, | |
style = { | |
shape = {fill = color(37, 37, 37, 255), stroke = "midGrey", strokeWidth = iStrokeWidth}, text = {fontSize = iFontSize, font = iFontName, fill=infoColor, textAlign = CENTER}}, | |
x = insetX, y = insetY, w = windowW, h = windowH | |
} | |
--[[ probably deletable: | |
local subWindow = { | |
--parent = window, | |
title = infoText, | |
close = true, | |
blurred = true, | |
label = {x = 0.5, y = 0.5}, | |
style = { | |
shape = {fill = color(0, 0, 0, 0)}, | |
text = {fontSize = iFontSize, font = iFontName, fill=infoColor, textAlign = CENTER}, | |
}, | |
-- x = 0, y = -1 * windowH / 3.65, w = windowW, h = windowH | |
x = 0, y = 0, w = windowW, h = windowH | |
} | |
]]-- | |
infoScreenNeedsMaking = false | |
else | |
window:show() | |
end | |
end | |
infoButton.callback = showInfo | |
end | |
function makeRosiesGame() | |
--create the game object | |
local rosiesGame = TwoChoiceAdventure("Rosie's Game", defaultSodaStyle) | |
--flag game to only use Codea-supplied images, using setting from constant | |
rosiesGame:useOnlyCodeaImages(shouldUseOnlyCodeaImages) | |
--add an image set | |
rosiesGame:setImageSet(rcImages) | |
--create the individual screens (the format for these is explained in TwoChoiceAdventure) | |
rosiesGame:addScreens( | |
{name = "firstScreen", | |
background = "rcFirst", | |
images = {}, | |
narration = "Do you want to have an adventure?", | |
choices = { | |
{choiceText = "adventure", resultScreen = "theAdventureIs"}, | |
{choiceText = "stay in bed", resultScreen ="sleepForDays" } } | |
}, | |
{name = "theAdventureIs", | |
background = "theAdventureIs", | |
images = {}, | |
narration = "The adventure is: help everybody!", | |
choices = { | |
{choiceText = "first go see your boyfriend", resultScreen ="boyfriendAsks" } } | |
}, | |
{name = "sleepForDays", | |
background = "rcSleepForDays", | |
images = {}, | |
narration = "You sleep for days and days and\nend up hated by everybody.", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen"} } | |
}, | |
{name = "boyfriendAsks", | |
background = "rcBoyfriendAsks", | |
images = {}, | |
narration = | |
"Your boyfriend says,\n'Do you want to know my secret,\nor do you want to break up?'", | |
choices = { | |
{choiceText = "break up", resultScreen ="breakUp" }, | |
{choiceText = "secret", resultScreen = "boyfriendSecret"} } | |
}, | |
{name = "boyfriendSecret", | |
background = "rcBoyfriendWithNote", | |
images = {}, | |
narration = "He says 'My secret is this note\nI need to get to my sister somehow.'", | |
choices = { | |
{choiceText = "send note for him", resultScreen ="helpEverybody" } } | |
}, | |
{name = "breakUp", | |
background = "rcBreakup", | |
images = {}, | |
narration = "You break up and he gets a girlfriend\nprettier than you and you cry.", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen"} } | |
}, | |
{name = "helpEverybody", | |
background = "rcHelpEverybody", | |
images = {}, | |
narration = "First, you send your boyfriend's note.\n\rThen you help everybody!", | |
choices = { | |
{choiceText = "good job", resultScreen ="queenAsks" } } | |
}, | |
{name = "queenAsks", | |
background = "rcQueenAsksYouToDeliver", | |
images = {}, | |
narration = "For doing such a good job, the Queen\nsays you should be Queen, but only if\nyou deliver some birthday invitations\nfor her.", | |
choices = { | |
{choiceText = "don't deliver", resultScreen ="everybodyDisappointed" }, | |
{choiceText = "deliver", resultScreen = "deliverInvitations"} } | |
}, | |
{name = "deliverInvitations", | |
background = "rcDeliverAllInvitations", | |
images = {}, | |
narration = "You deliver all the invitations!", | |
choices = { | |
{choiceText = "go back to Queen", resultScreen ="doYouHaveASecret"} } | |
}, | |
{name = "everybodyDisappointed", | |
background = "rcEverybodysDisappointed", | |
images = {}, | |
narration = "You never get famous and everybody's\ndisappointed because they wanted you\nto be queen.", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen"} } | |
}, | |
{name = "doYouHaveASecret", | |
background = "rcSecretOrQueen", | |
images = {}, | |
narration = "The Queen asks:\n\r'Do you have a secret\nor do you want to be queen?'", | |
choices = { | |
{choiceText = "reveal your secret", resultScreen = "revealYourSecret"}, | |
{choiceText = "become Queen", resultScreen ="becomeQueen" } } | |
}, | |
{name = "revealYourSecret", | |
background = "rcTellYourSecret", | |
images = {}, | |
narration = "You tell your secret:\n\r'I didn't really want to help anybody.'\n\rThe people kick you out and you lose.", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen"} } | |
}, | |
{name = "becomeQueen", | |
background = "rcYoureQueen", | |
images = {}, | |
narration = "You're Queen!", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen"} } | |
} | |
) | |
report("finished makeRosiesGame") | |
return rosiesGame | |
end | |
function makeCharlottesGame() | |
--create the game object | |
local charlottesGame = TwoChoiceAdventure("Charlotte's Game", defaultSodaStyle) | |
--flag game to only use Codea-supplied images, using setting from constant | |
charlottesGame:useOnlyCodeaImages(shouldUseOnlyCodeaImages) | |
--add an image set | |
charlottesGame:setImageSet(ccImages) | |
--create the individual screens (the format for these is explained in TwoChoiceAdventure) | |
charlottesGame:addScreens( | |
{name = "firstScreen", | |
background = "ccFirst", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.23, HEIGHT * 0.576, heightRatio = 0.61197917}, | |
{"coffeeSmall", "ccCoffeeSmall", WIDTH * 0.457, HEIGHT * 0.57161, heightRatio = 0.0625} }, | |
narration = "You woke up.\n\rWhat do you want to\ndo with your coffee?", | |
choices = { | |
{choiceText = "drink it", resultScreen = "drankCoffee"}, | |
{choiceText = "save it for later", resultScreen ="savedCoffee", | |
inventoryAdd = {name = "coffee", icon = "ccCoffeeBig", heightRatio = 0.21875} } } | |
}, | |
{name = "drankCoffee", | |
background = "ccFirst", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.23, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "It tastes good.", | |
choices = { | |
{choiceText = "go outside", resultScreen = "boyfriendTellsAboutQueen"} }, | |
}, | |
{name = "savedCoffee", | |
background = "ccFirst", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.23, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "You keep it with you for later.", | |
choices = { | |
{choiceText = "go outside", resultScreen = "boyfriendTellsAboutQueen"} }, | |
}, | |
{name = "boyfriendTellsAboutQueen", | |
background = "ccGenericOutside", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.3115, HEIGHT * 0.576, heightRatio = 0.61197917}, | |
{"boyfriend", "ccBoyfriend", WIDTH * 0.54003906, HEIGHT * 0.5859375, heightRatio = 0.65885417} }, | |
narration = "Your boyfriend tells you the queen is bored.", | |
choices = { | |
{choiceText = "go to see the queen", resultScreen = "knightScreen"} } | |
}, | |
{name = "knightScreen", | |
background = "ccKnightScreen", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.2675, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "You see a knight at the gate.\n\rWhat do you say to him?", | |
choices = { | |
{choiceText = "Just let me in to the castle.", resultScreen ="knightScolds" }, | |
{choiceText = "Hi, having a nice day?", resultScreen = "knightGivesHeart", | |
inventoryAdd = {name = "heartBox", icon = "ccHeartBox", heightRatio = 0.22135417} } } | |
}, | |
{name = "knightGivesHeart", | |
background = "ccKnightScreen", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.2675, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "The knight likes you and gives you a heart box with chocolates in it.", | |
choices = { | |
{choiceText = "go in to castle", resultScreen ="boredQueen" } } | |
}, | |
{name = "knightScolds", | |
background = "ccKnightScreen", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.23, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "The knight tells you that you have a bad attitude.", | |
choices = { | |
{choiceText = "go home", resultScreen ="homeAfterKnight" } } | |
}, | |
{name = "homeAfterKnight", | |
background = "ccFirst", | |
images = { | |
{"heroineFlipped", "ccHeroineFlipped", WIDTH * 0.390625, HEIGHT * 0.56380208, heightRatio = 0.61197917} }, | |
narration = "You say to yourself, \"He doesn't like me anyway.\"", | |
choices = { | |
{choiceText = "start over", resultScreen ="firstScreen", | |
inventoryRemove = "allItems" } } | |
}, | |
{name = "boredQueen", | |
background = "ccQueenBored", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.2197, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "The queen tells you she's really bored.", | |
choices = { | |
{choiceText = "tell her you know a puppet show", resultScreen = "queenSaysShowMe" } } | |
}, | |
{name = "queenSaysShowMe", | |
background = "ccQueenBored", | |
images = { | |
{"heroine", "ccHeroine", WIDTH * 0.2246, HEIGHT * 0.576, heightRatio = 0.61197917} }, | |
narration = "You tell the queen you know how to put on a puppet show.\n\rShe says, \"Show me as soon as you can!\"", | |
choices = { | |
{choiceText = "leave", resultScreen = "grouchyPuppeteer" } } | |
}, | |
{name = "grouchyPuppeteer", | |
background = "ccGenericOutside", | |
images = { | |
{"heroineFlipped", "ccHeroineFlipped", WIDTH * 0.68359375, HEIGHT * 0.59635417, heightRatio = 0.61197917}, | |
{"puppeteerGrouchy", "ccPuppeteerGrumpy", WIDTH * 0.31738281, HEIGHT * 0.59635417, heightRatio = 0.67317708} }, | |
narration = "On your way home you see a grouchy puppeteer.", | |
choices = { | |
{onlyIfInInventory = "coffee", choiceText = "give him your coffee", | |
resultScreen = "happyPuppeteer", inventoryRemove = "coffee" }, | |
{choiceText = "go home and practice", resultScreen ="queenNotLike" } } | |
}, | |
{name = "happyPuppeteer", | |
background = "ccGenericOutside", | |
images = { | |
{"heroineFlipped", "ccHeroineFlipped", WIDTH * 0.67675781, HEIGHT * 0.59635417, heightRatio = 0.61197917}, | |
{"puppeteerHappy", "ccPuppeteerHappy", WIDTH * 0.31738281, HEIGHT * 0.59635417, heightRatio = 0.66145833} }, | |
narration = "He teaches you a new puppet show.", | |
choices = { | |
{choiceText = "go home and give up", resultScreen ="homeAndSleep" }, | |
{choiceText = "go show the queen", resultScreen = "queenLovesShow"} } | |
}, | |
{name = "queenNotLike", | |
background = "ccQueenNotLike", | |
images = { | |
{"heroineWithSockPuppets", "ccHeroineWithSockPuppets", WIDTH * 0.26464844, HEIGHT * 0.55989583, heightRatio = 0.63932292} }, | |
narration = "You practice, but the queen doesn't like your show, and you're embarrassed.", | |
choices = { | |
{choiceText = "start over", resultScreen ="firstScreen", | |
inventoryRemove = "allItems" } } | |
}, | |
{name = "homeAndSleep", | |
background = "ccHomeAndSleep", | |
narration = "You go back home and go to sleep.", | |
choices = { | |
{choiceText = "start over", resultScreen ="firstScreen", | |
inventoryRemove = "allItems" } } | |
}, | |
{name = "queenLovesShow", | |
background = "ccQueenLoves", | |
images = { | |
{"heroineWithMarionettes", "ccHeroineWithMarionettes", WIDTH * 0.26464844, HEIGHT * 0.57291667, heightRatio = 0.63932292} }, | |
narration = "The queen loves the show!\n\rShe tells you a secret. If you give the princess a heart box with chocolates in it, you will become the new queen.", | |
choices = { | |
{choiceText = "choose who to give the heart to", resultScreen ="chooseHeart" } } | |
}, | |
{name = "chooseHeart", | |
background = "ccGenericCastleInterior", | |
images = { | |
{"princess", "ccPrincess", WIDTH * 0.5, HEIGHT * 0.64453125, heightRatio = 0.72916667}, | |
{"boyfriend", "ccBoyfriend", WIDTH * 0.23730469, HEIGHT * 0.58854167, heightRatio = 0.65885417} }, | |
narration = "Do you give the heart to the princess or your boyfriend?", | |
choices = { | |
{choiceText = "princess", resultScreen = "youBecomeQueen", | |
inventoryRemove = "heartBox"}, | |
{choiceText = "boyfriend", resultScreen ="happyInForest", | |
inventoryRemove = "heartBox" } } | |
}, | |
{name = "youBecomeQueen", | |
background = "ccGenericCastleInterior", | |
images = { | |
{"heroineQueened", "ccHeroineQueened", WIDTH * 0.31152344, HEIGHT * 0.58854167, heightRatio = 0.64973958} }, | |
narration = "You become the queen!", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen", | |
inventoryRemove = "allItems" } } | |
}, | |
{name = "happyInForest", | |
background = "ccHappyInForest", | |
images = { | |
{"happyFamily", "ccHappyFamily", WIDTH * 0.31835938, HEIGHT * 0.42057292, heightRatio = 0.286} }, | |
narration = "You and your boyfriend end up living in a shack in the forest with your kids.\n\rAnd you are happy.", | |
choices = { | |
{choiceText = "The End", resultScreen ="firstScreen", | |
inventoryRemove = "allItems" } } | |
} | |
) | |
report("finished makeCharlottesGame") | |
return charlottesGame | |
end | |
--# ImageSets | |
--[[ | |
ImageSets is not a class, just a tab for holding tables that have image names as keys (and all values set to the placeholder value 0), as well as a function for setting the images directly. Main can either call that function, or set the images another way. | |
]] | |
--image keys for the game selection screen | |
gameMCImages = {} | |
gameMCImages.doubleMenuRosie = 0 | |
gameMCImages.doubleMenuCharlotte = 0 | |
gameMCImages.doubleMenuBackground = 0 | |
--image keys for Charlotte's game | |
ccImages = {} | |
ccImages.ccFirst = 0 | |
ccImages.ccHeroine = 0 | |
ccImages.ccHeroineFlipped = 0 | |
ccImages.ccCoffeeSmall = 0 | |
ccImages.ccCoffeeBig = 0 | |
ccImages.ccGenericOutside = 0 | |
ccImages.ccBoyfriend = 0 | |
ccImages.ccKnightScreen = 0 | |
ccImages.ccHeartBox = 0 | |
ccImages.ccQueenBored = 0 | |
ccImages.ccPuppeteerGrumpy = 0 | |
ccImages.ccPuppeteerHappy = 0 | |
ccImages.ccQueenNotLike = 0 | |
ccImages.ccHeroineWithSockPuppets = 0 | |
ccImages.ccHomeAndSleep = 0 | |
ccImages.ccQueenLoves = 0 | |
ccImages.ccHeroineWithMarionettes = 0 | |
ccImages.ccGenericCastleInterior = 0 | |
ccImages.ccPrincess = 0 | |
ccImages.ccHeroineQueened = 0 | |
ccImages.ccHappyInForest = 0 | |
ccImages.ccHappyFamily = 0 | |
--image keys for Rosie's game | |
rcImages = {} | |
rcImages.rcFirst = 0 | |
rcImages.theAdventureIs = 0 | |
rcImages.rosiePlaceholder = 0 | |
rcImages.rcSleepForDays = 0 | |
rcImages.rcBoyfriendAsks = 0 | |
rcImages.rcBoyfriendWithNote = 0 | |
rcImages.rcBreakup = 0 | |
rcImages.rcHelpEverybody = 0 | |
rcImages.rcQueenAsksYouToDeliver = 0 | |
rcImages.rcDeliverAllInvitations = 0 | |
rcImages.rcEverybodysDisappointed = 0 | |
rcImages.rcSecretOrQueen = 0 | |
rcImages.rcTellYourSecret = 0 | |
rcImages.rcYoureQueen = 0 | |
function assignIemageSetsAllAtOnce() | |
--image keys for the game selection screen | |
gameMCImages = {} | |
gameMCImages.doubleMenuRosie = readImage("Project:doubleMenuRosie") | |
gameMCImages.doubleMenuCharlotte = readImage("Project:doubleMenuCharlotte") | |
gameMCImages.doubleMenuBackground = readImage("Project:doubleMenuBackground") | |
--image keys for Charlotte's game | |
ccImages = {} | |
ccImages.ccFirst = readImage("Project:ccFirst") | |
ccImages.ccHeroine = readImage("Project:ccHeroine") | |
ccImages.ccHeroineFlipped = readImage("Project:ccHeroineFlipped") | |
ccImages.ccCoffeeSmall = readImage("Project:ccCoffeeSmall") | |
ccImages.ccCoffeeBig = readImage("Project:ccCoffeeBig") | |
ccImages.ccGenericOutside = readImage("Project:ccGenericOutside") | |
ccImages.ccBoyfriend = readImage("Project:ccBoyfriend") | |
ccImages.ccKnightScreen = readImage("Project:ccKnightScreen") | |
ccImages.ccHeartBox = readImage("Project:ccHeartBox") | |
ccImages.ccQueenBored = readImage("Project:ccQueenBored") | |
ccImages.ccPuppeteerGrumpy = readImage("Project:ccPuppeteerGrumpy") | |
ccImages.ccPuppeteerHappy = readImage("Project:ccPuppeteerHappy") | |
ccImages.ccQueenNotLike = readImage("Project:ccQueenNotLike") | |
ccImages.ccHeroineWithSockPuppets = readImage("Project:ccHeroineWithSockPuppets") | |
ccImages.ccHomeAndSleep = readImage("Project:ccHomeAndSleep") | |
ccImages.ccQueenLoves = readImage("Project:ccQueenLoves") | |
ccImages.ccHeroineWithMarionettes = readImage("Project:ccHeroineWithMarionettes") | |
ccImages.ccGenericCastleInterior = readImage("Project:ccGenericCastleInterior") | |
ccImages.ccPrincess = readImage("Project:ccPrincess") | |
ccImages.ccHeroineQueened = readImage("Project:ccHeroineQueened") | |
ccImages.ccHappyInForest = readImage("Project:ccHappyInForest") | |
ccImages.ccHappyFamily = readImage("Project:ccHappyFamily") | |
--image keys for Rosie's game | |
rcImages = {} | |
rcImages.rcFirst = readImage("Project:rcFirst") | |
rcImages.theAdventureIs = readImage("Project:theAdventureIs") | |
rcImages.rosiePlaceholder = readImage("Project:rosiePlaceholder") | |
rcImages.rcSleepForDays = readImage("Project:rcSleepForDays") | |
rcImages.rcBoyfriendAsks = readImage("Project:rcBoyfriendAsks") | |
rcImages.rcBoyfriendWithNote = readImage("Project:rcBoyfriendWithNote") | |
rcImages.rcBreakup = readImage("Project:rcBreakup") | |
rcImages.rcHelpEverybody = readImage("Project:rcHelpEverybody") | |
rcImages.rcQueenAsksYouToDeliver = readImage("Project:rcQueenAsksYouToDeliver") | |
rcImages.rcDeliverAllInvitations = readImage("Project:rcDeliverAllInvitations") | |
rcImages.rcEverybodysDisappointed = readImage("Project:rcEverybodysDisappointed") | |
rcImages.rcSecretOrQueen = readImage("Project:rcSecretOrQueen") | |
rcImages.rcTellYourSecret = readImage("Project:rcTellYourSecret") | |
rcImages.rcYoureQueen = readImage("Project:rcYoureQueen") | |
end | |
--# GameMC | |
GameMC = class(Screen) --making this a subclass is a new experiment | |
--[[ | |
GameMC creates a menu screen for launching TwoChoiceAdventure games. It will automatically generate menu buttons for every game added by the addGame method. The automatically-generated screen is hideous, though, so there are also a couple methods included for customizing it. This class also adds an "exit game" button to the first screen of every game it runs. | |
]] | |
function GameMC:init(sodaStyle) | |
--initialize the superclass | |
Screen.init(self, "GameMC") | |
--store the Soda style | |
self.style = Utilities:copyWholeTable(sodaStyle) | |
table.pack() | |
--make the games table | |
self.games = {} | |
--create an instance of the Screen class to become the selection screen | |
self.gameSelectionScreen = Screen() | |
--for setup purposes, instantiate a placeholder game | |
self.currentGame = TwoChoiceAdventure("placeholder game") | |
--add the placeholder to self, so an unconfigured selection menu can at least show one button | |
self:addGame("placeholder game", self.currentGame) | |
--make a boolean to track if a game is underway | |
self.gameUnderway = false | |
--create a table for the "exit game" button (overlaid on the first screen of any running game) | |
self.exitButton = {} | |
self.exitButton.image = Utilities:label("exit game", 200, 50) | |
--set the exit button location to be the middle of the screen (you'll want to customize that) | |
self.exitButton.x = WIDTH / 2 | |
self.exitButton.y = HEIGHT / 10 | |
--the action for the exit button to run when pressed | |
self.exitButtonAction = function () self:endGame() end | |
--instead: | |
--define the exit button as a Soda button table | |
self.exitButton = { | |
title = "exit game", | |
label = {x = 0.5, y = 0.5}, | |
--blurred = true, -- causes Soda crash | |
style = self.style, | |
shape = Soda.RoundedRectangle, | |
x = 20, y = 20, w = 200, h = 75, | |
} | |
end | |
function GameMC:draw() | |
--if a game is not underway, use the draw() method of the selection screen | |
if self.gameUnderway == false then | |
self.gameSelectionScreen:draw() | |
else | |
--otherwise use the current game's draw() method | |
self.currentGame:draw() | |
end | |
end | |
function GameMC:touched(touch) | |
--if a game is not underway, use the touched(touch) method of the selection screen | |
if self.gameUnderway == false then | |
self.gameSelectionScreen:touched(touch) | |
else | |
--otherwise use the current game's touched(touch) method | |
self.currentGame:touched(touch) | |
end | |
end | |
function GameMC:addGame(gameName, gameObject) | |
--if the placeholder game exists, remove it | |
if Utilities:doesTableHaveStringKey(self.games, "placeholder game") then | |
self:removeGame("placeholder game") | |
end | |
--add the new game under the given name | |
self.games[gameName] = gameObject | |
--a count of the current games for measuring button placement | |
local gameCount = Utilities:numberOfKeyValueElements(self.games) | |
--the height to use for each button | |
local buttonHeight = (HEIGHT / gameCount) | |
--create new buttons for each game | |
for key, value in pairs(self.games) do | |
--make a name for the current game button | |
local buttonName = key.." Button" | |
--erase any old one by that name | |
Utilities:removeValueByStringKey(self.gameSelectionScreen.touchables, buttonName) | |
--make a new button image | |
local buttonImage = Utilities:label("press to start "..key, WIDTH, buttonHeight) | |
--calculate the y value to use | |
local buttonY = (gameCount * buttonHeight) - (buttonHeight / 2) | |
--add it to the game selection screen | |
self.gameSelectionScreen:addTouchableAt(buttonName, buttonImage, WIDTH / 2, buttonY) | |
--decrement the game counter so the next label will be placed under the current one | |
gameCount = gameCount - 1 | |
--make an action for it | |
local buttonAction = function () self:runGame(key) end | |
--make a name for the action | |
local actionName = buttonName.." Action" | |
--assign the action to the button | |
self.gameSelectionScreen:assignActionToTouchable(actionName, buttonAction, buttonName) | |
end | |
end | |
function GameMC:customGameButton(gameName, newImage, x, y) | |
--exit if no such game | |
if self.games[gameName] == nil then | |
return false | |
end | |
--change the button of the given game to use the given image and given x, y coordinates | |
self.gameSelectionScreen.touchables[gameName.." Button"].image = newImage | |
self.gameSelectionScreen.touchables[gameName.." Button"].x = x | |
self.gameSelectionScreen.touchables[gameName.." Button"].y = y | |
return true | |
end | |
function GameMC:customGameButtonAction(gameName, action) | |
--a reference to the button name for the given game | |
local buttonName = gameName.." Button" | |
--a reference to that button's action's name | |
local actionName = buttonName.." Action" | |
--use that action name to assign the passed-in action to that button | |
self.gameSelectionScreen:assignActionToTouchable(actionName, action, buttonName) | |
--note: be sure to include runGame(gameName) in the new action! | |
end | |
function GameMC:removeGame(gameName) | |
--if the game to remove is the currentGame, set currentGame to nil to remove the reference | |
if self.currentGame == self.games[gameName] then | |
self.currentGame = nil | |
end | |
--use a utility function to remove the game from the games table | |
Utilities:removeValueByStringKey(self.games, gameName) | |
--lastly, remove the button and button action for the given game on the selection screen | |
self.gameSelectionScreen:removeTouchableNamed(gameName.." Button") | |
self.gameSelectionScreen:removeActionNamed(gameName.." Button Action") | |
end | |
function GameMC:runGame(gameName) | |
--set the internal flag that shows a game is underway | |
self.gameUnderway = true | |
--designate the given game as the current game | |
self.currentGame = self.games[gameName] | |
--make shorter references to the position where the exit button will be placed | |
--local buttonX = self.exitButton.x | |
--local buttonY = self.exitButton.y | |
--use them to create a table holding the arguments for adding the exit button | |
--local touchableArgs = {"exit button", self.exitButton.image, buttonX, buttonY} | |
--use that table to add the exit button to the first screen of the game about to be run | |
self.currentGame.firstScreen:addTouchableAt("exit button", self.exitButton.image, nil, nil, self.exitButton) | |
--add the exit button function to the given game as the action for the exit button | |
self.currentGame.firstScreen:assignActionToTouchable("exit", self.exitButtonAction, "exit button") | |
--start the game! | |
self.currentGame:startGame() | |
end | |
function GameMC:endGame() | |
--set the internal flag that shows that no game is underway | |
self.gameUnderway = false | |
--remove the exit button and its action from the first screen of the game | |
self.currentGame.firstScreen:removeTouchableNamed("exit button") | |
self.currentGame.firstScreen:removeActionNamed("exit") | |
--clear any Soda elements being displayed | |
self.currentGame.currentScreen:killSodaUIInstances() | |
end | |
--# Scratch | |
function addInfoScreen() | |
local infoText = [['Double Choose' is two small adventure games written entirely by my daughters Rosie and Charlotte. Tap on Rosie to play her game, and tap on Charlotte to play hers. | |
Rosie did the art for her game, and I did most of Charlotte's game under her firm art-direction. | |
You'll enjoy the games most by trying to find all the possible endings. Rosie's game has awesome emotionally-excruciating endings. Charlotte actually included a little adventure-game-style inventory management--impressive for a five-year-old! | |
We made it with the excellent iPad app Codea. We hope you enjoy our games, and we especially hope they inspire other kids to make games too. Try Codea, it rocks!]] | |
--define parameters for the info window | |
local insetX, insetY = math.floor(WIDTH * 0.04), math.floor(HEIGHT * 0.04) | |
local windowW, windowH = WIDTH - (insetX * 2), HEIGHT - (insetY * 2) | |
local textW, textH = windowW - (insetX * 2), windowH - (insetY * 2) | |
local iFontName = "GillSans-Light" | |
local iFontSize = 1.75 | |
local infoColor = color(198, 110, 198, 255) | |
local showInfo = function() | |
local window = Soda.Window { | |
title = "", | |
blurred = true, | |
close = true, | |
style = { | |
shape = {fill = color(37, 37, 37, 255), stroke = "midGrey", strokeWidth = iStrokeWidth}, text = {}}, | |
x = insetX, y = insetY, w = windowW, h = windowH | |
} | |
local subWindow = Soda.Window { | |
parent = window, | |
title = infoText, | |
style = { | |
shape = {fill = color(0, 0, 0, 0), stroke = "midGrey", strokeWidth = iStrokeWidth}, | |
text = {fontSize = iFontSize, font = iFontName, fill=infoColor, textAlign = LEFT}, | |
}, | |
x = 0, y = -1 * windowH / 3.65, w = windowW, h = windowH | |
} | |
end | |
--determine the correct the size of the info circle | |
local iCircleSize = WIDTH * 0.065 | |
--define the placement of the info circle | |
local iCircleOffset = iCircleSize * 0.48 | |
local infoCircleFontSize = iFontSize * 1.9 | |
local iStrokeWidth = infoCircleFontSize * 0.85 | |
local iCircleColor = color(144, 125, 144, 255) | |
infoButtonTable = Soda.Button{ | |
title = "i", | |
style = { | |
shape = {fill = color(0, 0, 0, 0), stroke = iCircleColor, strokeWidth = iStrokeWidth}, | |
text = {fontSize = infoCircleFontSize, font = "GillSans", fill=iCircleColor}, | |
highlight = { | |
text = {fontSize = infoCircleFontSize, font = "GillSans", fill=color(0, 0, 0, 0)}, | |
shape = {fill = iCircleColor} | |
} | |
}, | |
shape = Soda.ellipse, | |
x = 20, y = 20, w = iCircleSize, h = iCircleSize, | |
callback = showInfo | |
} | |
end | |
--# TwoChoiceAdventure | |
TwoChoiceAdventure = class() | |
--[[ | |
TwoChoiceAdventure is a class that can be made into a simple adventure game. The game consists of a series of screens with a small amount of narration and up to two choices of response. The choices can either change the current screen to a new screen or modify the inventory. Choices can be displayed or hidden dynamically based on inventory state. Screens are defined using the addScreen(...) method. The very first screen must be named "firstScreen". | |
]] | |
function TwoChoiceAdventure:init(name, defaultStyle) | |
--check if a name was given | |
if name == nil then | |
--if no name was given, print an error message | |
print("TwoChoiceAdventure: game didn't init with name. Initialization failed.") | |
--and exit without initializing | |
return | |
end | |
self.name = name | |
--a default first screen, in case one is never set | |
local first = Screen() | |
--a label for it | |
local firstLabel = Utilities:label(name..": blank screen", WIDTH / 2, HEIGHT / 2) | |
--add the label to the screen | |
first:addImageAt("default art", firstLabel, WIDTH / 2, HEIGHT / 2) | |
--declare the set of tables for saving screen definitions | |
self.screenTables = {} | |
--store the first screen as a special variable | |
self.firstScreen = first | |
--change the current screen to be the firstScreen | |
self:changeScreen("firstScreen") | |
--make an Inventory object for tracking inventory | |
self.inventory = Inventory() | |
--make a style table to preserve the desired style for drawn text | |
self.textStyle = StyleTable() | |
--Soda style defaults | |
self.defaultStyle = Utilities:copyWholeTable(defaultStyle) | |
--set the desired style settings | |
font("SourceSansPro-Regular") | |
fontSize(HEIGHT / 28) | |
textWrapWidth(WIDTH / 2.4) | |
textAlign(LEFT) | |
fill(251, 251, 251, 255) | |
--capture the current style settings the style table | |
self.textStyle:captureCurrentStyle() | |
--specify the size and location of the narration box | |
local narrationHeightAsPercent = 0.2941176 | |
self.narrationW = WIDTH / 2.15 | |
self.narrationH = HEIGHT * narrationHeightAsPercent | |
self.narrationX = 0.73326 | |
self.narrationY = 0.39483 | |
--specify the size of the choice buttons | |
local choiceHeightAsPercent = 0.07194245 | |
self.choiceW = self.narrationW | |
self.choiceH = HEIGHT * choiceHeightAsPercent | |
--align the horizontal position of the buttons with the narration box | |
self.choice1X = self.narrationX | |
self.choice2X = self.choice1X | |
--specify the gap between choice buttons as a screen percent | |
gapBetweenChoicesAsPercent = 0.0095 | |
--find the height of the narration panel as a screen percent | |
local bottomOfNarration = self.narrationY - (narrationHeightAsPercent / 2) | |
--set the vertical positions of the choice buttons | |
self.choice1Y = bottomOfNarration - (choiceHeightAsPercent / 2) - gapBetweenChoicesAsPercent | |
self.choice2Y = self.choice1Y - choiceHeightAsPercent - gapBetweenChoicesAsPercent | |
print(self.choice2Y, fromNarrationYToChoice1Y, "iiiii") | |
end | |
function TwoChoiceAdventure:useOnlyCodeaImages(flagValue) | |
--used to toggle custom images off | |
self.shouldUseOnlyCodeaImages = flagValue | |
self.inventory:useOnlyCodeaImages(flagValue) | |
end | |
function TwoChoiceAdventure:addScreens(...) | |
--[[ | |
This method can take any number of arguments, each of which must be a properly constructed screen table. The tables are stored in self.screenTables and used to assemble screens when needed. Screen tables are key-value tables, which use these keys: | |
name = screen name | |
background = background image name as defined in imageSet | |
images (optional) = a table of imageTables, which have the format: {name, image, x, y} | |
narration = narration text | |
choices = table of up to two choiceTables, key-value tables with these keys & values: | |
choiceText = choice text that appears on the choice button | |
resultScreen = name of screen shown after button is pressed | |
inventoryAdd (optional) = table with the keys & values needed for Inventory:addItem | |
inventoryRemove (optional) = name of inventory item to remove | |
onlyIfInInventory (optional) = name of item necessary for this choice to be displayed | |
]] | |
local arg = {...} | |
--iterate through the arguments | |
for index, screenTable in ipairs(arg) do | |
--add each argument to self.screenTables using its own "name" value | |
self.screenTables[screenTable.name] = screenTable | |
--if the screen is named "firstScreen", assemble it and keep it around | |
self:assembleIfNameIsFirstScreen(screenTable) | |
end | |
end | |
function TwoChoiceAdventure:assembleIfNameIsFirstScreen(screenTable) | |
--the screen named "firstScreen" has to be kept in memory, so check if this is it | |
if screenTable.name == "firstScreen" then | |
--if it is, make it, and assign it to the firstScreen instance variable | |
self.firstScreen = self:assembleScreen(screenTable) | |
report(self.name.." assembled first screen") | |
end | |
end | |
function TwoChoiceAdventure:assembleScreen(screenTable) | |
--create a Screen object to be configured | |
local newScreen = Screen() | |
--configure it using the values in the given screenTable | |
self:placeScreenImages(newScreen, screenTable) | |
self:placeScreenNarration(newScreen, screenTable) | |
self:createScreenChoices(newScreen, screenTable) | |
--return it | |
return newScreen | |
end | |
function TwoChoiceAdventure:changeScreen(newScreenName) | |
if self.currentScreen ~= nil then | |
--remove any Soda elements (which otherwise persist outside of the Screen's scope) | |
self.currentScreen:killSodaUIInstances() | |
--wipe out the current screen, hopefully preventing a crash from all screens being made | |
self.currentScreen = nil | |
end | |
--if returning to the firstScreen, which is kept in memory, nothing has to be assembled | |
if newScreenName == "firstScreen" then | |
--set the first screen to be the current screen | |
self.currentScreen = self.firstScreen | |
--exit | |
return | |
end | |
print("screen change done and good") | |
--if going to any other screen, first assemble it using the given name | |
local assembledScreen = self:assembleScreen(self.screenTables[newScreenName]) | |
--then check if anything needs to happen to it based on inventory state | |
if assembledScreen.inventoryCheck ~= nil then | |
--if so, run that function | |
assembledScreen.inventoryCheck() | |
end | |
--put in the new screen | |
self.currentScreen = assembledScreen | |
collectgarbage() | |
end | |
function TwoChoiceAdventure:placeScreenImages(screen, screenTable) | |
report("screen:", screen, "screenTable:", table.unpack(screenTable)) | |
--load the background image | |
if self.shouldUseOnlyCodeaImages then | |
screen:setBackground(readImage("Cargo Bot:Game Area")) | |
else | |
screen:setBackground(readImage("Project:"..screenTable.background)) | |
end | |
--if no images are defined, bail | |
if screenTable.images == nil then | |
return | |
end | |
--otherwise iterate through each of the image tables in the screenTable | |
for index, imageTable in ipairs(screenTable.images) do | |
local thisImage | |
if self.shouldUseOnlyCodeaImages then | |
thisImage = readImage("Planet Cute:Grass Block") | |
else | |
thisImage = readImage("Project:"..imageTable[2]) | |
end | |
--report it | |
report("placeScreenImages placing "..imageTable[1]) | |
--after checking for heightRatio, add each image by unpacking its image table directly into addImageAt | |
if imageTable.heightRatio == nil then | |
screen:addImageAt(imageTable[1], thisImage, imageTable[3], imageTable[4]) | |
else | |
report("placeScreenImages heightRatio detected") | |
screen:addImageAt(imageTable[1], thisImage, imageTable[3], imageTable[4], imageTable.heightRatio) | |
end | |
end | |
end | |
function TwoChoiceAdventure:placeScreenNarration(screen, screenTable) | |
--if no narration is present, bail | |
if screenTable.narration == nil then | |
return | |
end | |
--[[ | |
--create a style table for the narration box's fill and stroke | |
local boxStyle = StyleTable() | |
--set the desired fill and stroke (which is no stroke at all) | |
fill(13, 132, 209, 239) | |
strokeWidth(0) | |
--capture those settings in the style table | |
boxStyle:captureCurrentStyle() | |
--a shorter variable name for covenience | |
local narration = screenTable.narration | |
--for readability, make shorter local variables for the needed stored values | |
local w, h, x, y = self.narrationW, self.narrationH, self.narrationX, self.narrationY | |
--create an image using the narration settings | |
local narrationBox = Utilities:label(narration, w, h, nil, nil, self.textStyle, boxStyle) | |
--place the image on the given screen | |
screen:addImageAt("narration", narrationBox, x, y) | |
]] | |
--a shorter variable name for covenience | |
local narration = screenTable.narration | |
--for readability, make shorter local variables for the needed stored values | |
local w, h, x, y = self.narrationW, self.narrationH, self.narrationX, self.narrationY | |
local narrationTable = { | |
--blurred = true, --causes crash | |
title = narration, | |
x = x, y = y, w = w, h = h, | |
style = Utilities:copyWholeTable(self.defaultStyle) | |
} | |
--Soda.Button(narrationTable) | |
screen:addTextBox("narration", narrationTable) | |
--designate the narration as the last image to be drawn | |
--screen:setLastImageToDraw("narration") | |
end | |
function TwoChoiceAdventure:createScreenChoices(screen, screenTable) | |
print("screenTable.choices", table.unpack(screenTable.choices)) | |
--if no choices are present, bail | |
if screenTable.choices == nil then | |
report("no choices found") | |
return | |
end | |
--for readability, make shorter local variables for the needed stored values | |
local textStyle, height, width = self.textStyle, self.choiceH, self.choiceW | |
--make the style table for the choice button's colors | |
local buttonStyle = StyleTable() | |
--set the desired fill and stroke (which is no stroke at all) | |
fill(197, 17, 20, 239) | |
strokeWidth(0) | |
--capture those settings in the style table | |
buttonStyle:captureCurrentStyle() | |
--make buttons from the screenTable's stored choice settings | |
for index, choiceTable in ipairs(screenTable.choices) do | |
--shorter variable name for readability | |
local choiceText, width, height = choiceTable.choiceText, self.choiceW, self.choiceH | |
--a name for the current button based on which choice it is ("choice1", "choice2", etc) | |
local buttonName = "choice"..index | |
--capture the x and y at which to draw this button | |
local choiceX, choiceY = self[buttonName.."X"], self[buttonName.."Y"] | |
--make the button image for the current choice | |
local buttn = Utilities:label(choiceText, width, height, nil, nil, textStyle, buttonStyle) | |
local w, h, x, y = self.choiceW, self.choiceH, choiceX, choiceY | |
--make a SODA table | |
local sodaTable = { | |
title = choiceText, | |
x = x, y = y, w = w, h = h, | |
style = Utilities:copyWholeTable(self.defaultStyle) | |
} | |
--add that image as a touchable | |
screen:addTouchableAt(buttonName, nil, nil, nil, sodaTable) | |
--create the action function for this button | |
local actionFunction = function() | |
report("performing choice action") | |
self:performChoiceTableResults(choiceTable) | |
end | |
--make a name for the action by appending "action" to the button name | |
local actionName = buttonName.." action" | |
--assign the action to the button | |
screen:assignActionToTouchable(actionName, actionFunction, buttonName) | |
--if this button depends on an inventory state, make the action that checks for that | |
self:createInventoryCheckIfNeeded(screen, choiceTable, buttonName) | |
end | |
end | |
function TwoChoiceAdventure:performChoiceTableResults(choiceTable) | |
--play button-pushing sound | |
sound("Game Sounds One:Menu Back", 0.2) | |
--check if a result screen was specified | |
if choiceTable.resultScreen ~= nil then | |
--if so, make it the current screen | |
self:changeScreen(choiceTable.resultScreen) | |
end | |
--check if an inventory item should be added | |
if choiceTable.inventoryAdd ~= nil then | |
--if so, add it | |
local inventoryImage = readImage("Project:"..choiceTable.inventoryAdd.icon) | |
self.inventory:addItem(choiceTable.inventoryAdd.name, inventoryImage, choiceTable.inventoryAdd.heightRatio) | |
--play fanfare sound | |
sound("Game Sounds One:Bell 2", 0.2) | |
end | |
--check if an inventory item should be removed | |
if choiceTable.inventoryRemove ~= nil then | |
--if so, remove it (note: using the name "allItems" will remove all items) | |
self.inventory:removeItem(choiceTable.inventoryRemove) | |
--play fanfare sound | |
sound("Game Sounds One:Bell 2", 0.2) | |
end | |
end | |
function TwoChoiceAdventure:createInventoryCheckIfNeeded(screen, choiceTable, buttonName) | |
--check if the given choiceTable has a value for "onlyIfInInventory" | |
if choiceTable.onlyIfInInventory == nil then | |
--if not, return | |
return | |
end | |
--compose a function that checks for the item and hides or reveals the button appropriately | |
local inventoryCheck = function () | |
--store a boolean for whether or not the necessary inventory item is present | |
local itemPresent = self.inventory:checkForItem(choiceTable.onlyIfInInventory) | |
--store a boolean for whether or not the screen currently has a hidden button | |
local visibleButton = screen.hiddenButton == nil | |
--store a boolean that tells if the item and button are both showing | |
local correctShownState = itemPresent and visibleButton | |
--store a boolean that tells if neither the item nor the button are showing | |
local correctHiddenState = (not itemPresent) and (not visibleButton) | |
--if either both-showing or both-not-showing is true, things are the way they should be | |
if correctShownState or correctHiddenState then | |
return | |
--otherwise, check if the state is button-hidden-but-item-present | |
elseif not visibleButton then | |
--in which case, assume the inventory is in the correct state, and show the button | |
self:restoreHiddenButtonUsingName(screen, buttonName) | |
else | |
--and conversely, if item is not present, also obey inventory state, and hide button | |
self:hideButton(screen, buttonName) | |
end | |
end | |
--assign the function thus created to the inventoryCheck value of the screen given | |
screen.inventoryCheck = inventoryCheck | |
end | |
function TwoChoiceAdventure:hideButton(screen, buttonName) | |
--store the touchable with the given buttonName under the hiddenButton value | |
screen.hiddenButton = screen.touchables[buttonName] | |
--erase that touchable from the touchables table | |
screen.touchables[buttonName] = nil | |
--if the button that was hidden is choice 1 (the bottom button) we're all done, so return | |
if buttonName == "choice2" then | |
return | |
--if the button that was hidden is choice 1, move choice 2 up to take its place | |
elseif buttonName == "choice1" then | |
screen.touchables.choice1 = screen.touchables.choice2 | |
screen.touchables.choice1.x = self.choice1X | |
screen.touchables.choice1.y = self.choice1Y | |
end | |
end | |
function TwoChoiceAdventure:restoreHiddenButtonUsingName(screen, buttonName) | |
--if there is no hiddenButton key, return | |
if screen.hiddenButton == nil then | |
return | |
--if the hidden button is choice 2, then put it back on the screen as choice 2 | |
elseif buttonName == "choice2" then | |
screen.choice2 = screen.hiddenButton | |
--if the hidden button is choice 1, move the existing choice 1 down to choice 2 | |
elseif buttonName == "choice1" then | |
screen.touchables.choice2 = screen.touchables.choice1 | |
screen.touchables.choice2.x = self.choice2X | |
screen.touchables.choice2.y = self.choice2Y | |
--then return the hidden button to choice 1 | |
screen.touchables.choice1 = screen.hiddenButton | |
end | |
--erase anything that's still assigned to the hiddenButton key | |
screen.hiddenButton = nil | |
end | |
function TwoChoiceAdventure:setImageSet(imageSet) | |
self.imageSet = imageSet | |
end | |
function TwoChoiceAdventure:startGame() | |
self:changeScreen("firstScreen") | |
end | |
function TwoChoiceAdventure:draw() | |
--draw the current screen, being sure to draw the narration last | |
self.currentScreen:draw() | |
--draw the inventory (if any) | |
self.inventory:draw() | |
end | |
function TwoChoiceAdventure:touched(touch) | |
report("adventure touched") | |
--pass touches to the current screen | |
self.currentScreen:touched(touch) | |
end | |
--# Screen | |
Screen = class() | |
--[[ | |
Screen is a basic interactive screen, with methods for adding and removing images, touchable images ("touchables"), actions, and text. | |
]] | |
function Screen:init(name) | |
self.name = name | |
--default font specs | |
--note: the text functions of this class haven't been tested | |
self.screenFont = "SourceSansPro-Regular" | |
self.screenFontSize = 34 | |
self.screenFontColor = color(236, 211, 63, 255) | |
--blank background image & white background color: | |
self.background = image(WIDTH,HEIGHT) | |
self.backgroundColor = color(255, 255, 255, 255) | |
--empty table for images | |
self.images = {} | |
--empty table for things that can be touched | |
self.touchables = {} | |
--empty table for texts to print | |
self.texts = {} | |
--empty table for actions that can be triggered | |
self.actions = {} | |
--variable for designating a touchable that has been touched | |
self.touchedButton = "none" | |
--color to use for tinting a touched touchable | |
self.touchedTint = color(103, 108, 134, 255) | |
end | |
function Screen:setFontSpecs(font, size, color) | |
--define font specs to use for all text | |
--note: the text functions of this class haven't been tested | |
self.screenFont = font | |
self.screenFontSize = size | |
self.screenFontColor = color | |
end | |
function Screen:setBackground(thisImage) | |
--store background image | |
self.background = thisImage | |
end | |
function Screen:setBackgroundColor(thisColor) | |
--store background color | |
self.backgroundColor = thisColor | |
end | |
function Screen:addImageAt(imageName, imageToAdd, positionX, positionY, heightRatio) | |
--report image name | |
report("addImageAt adding "..imageName) | |
--if the name given is unique, use it as a key for a table holding the image and position | |
if Utilities:nameIsUniqueAndNotNil(imageName, self.images, "image") == true then | |
self.images[imageName] = {} | |
self.images[imageName].image = imageToAdd | |
self.images[imageName].x = positionX | |
self.images[imageName].y = positionY | |
if heightRatio ~= nil then | |
report("addImageAt detected heightRatio") | |
self.images[imageName].heightRatio = heightRatio | |
else | |
report("no ratio detected") | |
end | |
end | |
end | |
function Screen:setLastImageToDraw(imageName) | |
--define the last image to be drawn (not including touchables) | |
self.lastDrawnImage = self.images[imageName] | |
end | |
function Screen:addTouchableAt(name, touchableImage, positionX, positionY, sodaTable) | |
--if the name given is unique, use it as a key for a table holding the image and position | |
--sodaTable means handle this as a SODA button, using given table as init | |
--sodaTables override other settings | |
--returns a boolean indicating success or failure | |
--maybe add a soda table item thats a reference to the button itself, to use for verifying its existence? | |
--exit with error if name is used already | |
if Utilities:nameIsUniqueAndNotNil(name, self.touchables, "touchable") == false then | |
print("Touchable named '"..name.."' error: name already exists.") | |
return false | |
end | |
--if it's sodaTable, store by name, and add isShowing and any image | |
if sodaTable ~= nil then | |
self.touchables[name] = sodaTable | |
self.touchables[name].image = touchableImage | |
print("made soda: ", name, self.touchables[name], "x is:", self.touchables[name].x) | |
else | |
--if not, make table and set parameters for Screen's use | |
self.touchables[name] = {} | |
--flag the touchable as active, meaning it responds to touches | |
self.touchables[name].state = "active" | |
--store negative case so that when all non-sodae are removed there is no cruft left | |
self.touchables[name].isNotSoda = true | |
self.touchables[name].x = positionX | |
self.touchables[name].y = positionY | |
end | |
--set the values used whether Soda table or not | |
self.touchables[name].image = touchableImage | |
--indicate success | |
return true | |
end | |
function Screen:addTextBox(boxName, sodaTable) | |
--if the name given is unique, use it as a key for a table holding the text and position | |
--note: the text functions of this class haven't been tested | |
if Utilities:nameIsUniqueAndNotNil(boxName, self.texts, "text") then | |
self.texts[boxName] = sodaTable | |
--self.texts[boxName].inactive = true | |
self.texts[boxName].style.highlight = { | |
shape = sodaTable.style.shape, | |
text = sodaTable.style.text | |
} | |
end | |
end | |
function Screen:addAction(actionName, actionFunction) | |
--if the name given is unique, use it as a key for storing the action function | |
if Utilities:nameIsUniqueAndNotNil(actionName, self.images, "action") == true then | |
self.actions[actionName] = actionFunction | |
end | |
end | |
function Screen:assignActionToTouchable(actionName, actionFunction, touchableName) | |
--add function to the actions table under the given name | |
self:addAction(actionName, actionFunction) | |
--do the complicated thing as long as un-soda elements exist | |
if self.touchables[touchableName].isNotSoda then | |
--define the action of the given touchable using the given action name | |
self.touchables[touchableName].action = self.actions[actionName] | |
return | |
else | |
self.touchables[touchableName].callback = self.actions[actionName] | |
end | |
end | |
function Screen:draw() | |
--draw the preset background color and background image | |
background(self.backgroundColor) | |
sprite(self.background, WIDTH/2, HEIGHT/2, WIDTH, HEIGHT) | |
--draw all the imageTables unless they're the designated lastDrawnImage | |
for key, imageTable in pairs(self.images) do | |
self:drawUnlessLastImage(imageTable) | |
end | |
--draw the lastDrawnImage, if there is one | |
if self.lastDrawnImage ~= nil then | |
self:drawImageWithScaleIfAny(self.lastDrawnImage) | |
end | |
--draw all the touchables | |
for key, touchable in pairs(self.touchables) do | |
if touchable.isNotSoda then | |
self:drawTouchable(touchable) | |
else | |
if touchable.instance == nil then | |
touchable.instance = Soda.Button(touchable) | |
end | |
end | |
end | |
--draw the text | |
for key, textTable in pairs(self.texts) do | |
self:drawTextBox(textTable) | |
end | |
end | |
function Screen:drawImageWithScaleIfAny(imageTable) | |
--set width and height, using heightRatio if it exists | |
local drawnWidth, drawnHeight | |
if imageTable.heightRatio ~= nil then | |
drawnHeight = HEIGHT * imageTable.heightRatio | |
drawnWidth = drawnHeight * imageTable.image.width / imageTable.image.height | |
else | |
drawnHeight = imageTable.image.height | |
drawnWidth = imageTable.image.width | |
end | |
--go ahead and draw this thing | |
sprite(imageTable.image, imageTable.x, imageTable.y, drawnWidth, drawnHeight) | |
end | |
function Screen:drawTextBox(textTable) | |
--note: the text functions of this class haven't been tested well | |
if textTable.instance == nil then | |
textTable.instance = Soda.Button(textTable) | |
end | |
end | |
function Screen:drawUnlessLastImage(imageTable) | |
--if there is a lastDrawnImage and this is it, return without drawing anything | |
if self.lastDrawnImage ~= nil and imageTable == self.lastDrawnImage then | |
return | |
end | |
self:drawImageWithScaleIfAny(imageTable) | |
end | |
function Screen:drawTouchable(touchable) | |
--if the touchable's state is "active", draw it normally | |
if touchable.state == "active" then | |
sprite(touchable.image, touchable.x, touchable.y) | |
--if the touchable's state is "pressed", apply a tint before drawing it | |
elseif touchable.state == "pressed" then | |
tint(self.touchedTint) | |
sprite(touchable.image, touchable.x, touchable.y) | |
noTint() | |
end | |
end | |
function Screen:touched(touch) | |
report("touched") | |
--iterate through the touchables | |
for index, touchable in pairs(self.touchables) do | |
report(touchable.name) | |
--react if they were touched | |
self:reactIfTouched(touchable, touch) | |
end | |
end | |
function Screen:reactIfTouched(touchable, touch) | |
--once all touchables are Soda elements this function wont be needed | |
if touchable.isNotSoda == nil then | |
return | |
end | |
--define the touchable's rect | |
local tWidth, tHeight = touchable.image.width, touchable.image.height | |
local tX = touchable.x - (touchable.image.width / 2) | |
local tY = touchable.y - (touchable.image.height / 2) | |
--check if the touch is outside the touchable's rect | |
if Utilities:isPointInRect(touch, tX, tY, tWidth, tHeight) == false then | |
--if so, set the touchable to the "active" state | |
touchable.state = "active" | |
--and quit | |
return | |
end | |
--otherwise, if the touch has just started, flip the touchable to its "pressed" state | |
if CurrentTouch.state == BEGAN then | |
touchable.state = "pressed" | |
end | |
--if the touch has ended, put the touchable back in the "active" state and fire its action | |
if CurrentTouch.state == ENDED then | |
touchable.state = "active" | |
self:doTouchableActionIfAny(touchable) | |
end | |
end | |
function Screen:doTouchableActionIfAny(touchable) | |
--if the touchable has no action, return | |
if touchable.action == nil then | |
return | |
end | |
--otherwise do it! | |
touchable.action() | |
end | |
function Screen:removeImageNamed(imageName) | |
--find the named value in self.images and remove it | |
Utilities:removeValueByStringKey(self.images, imageName) | |
end | |
function Screen:removeTouchableNamed(touchableName) | |
--kill any Soda button under that name | |
if self.touchables[touchableName].instance ~= nil then | |
self.touchables[touchableName].instance.kill = true | |
end | |
--find the named value in self.touchables and remove it | |
Utilities:removeValueByStringKey(self.touchables, touchableName) | |
end | |
function Screen:removeActionNamed(actionName) | |
--find the named value in self.actions and remove it | |
Utilities:removeValueByStringKey(self.actions, actionName) | |
end | |
function Screen:removeTextNamed(textName) | |
--find the named value in self.texts and remove it | |
--note: the text functions of this class haven't been tested | |
Utilities:removeValueByStringKey(self.texts, textName) | |
end | |
function Screen:clearAllImages() | |
--iterate through all images (not counting images in touchables) and remove them | |
for index, imageTable in ipairs(self.images) do | |
table.remove(self.images, index) | |
end | |
end | |
function Screen:killSodaUIInstances() | |
print("should be clearing soda elements") | |
function killInstanceIfAnyIn(possiblySodaTable) | |
if possiblySodaTable.instance ~= nil then | |
possiblySodaTable.instance.kill = true | |
possiblySodaTable.instance = nil | |
end | |
end | |
function clearAllInstancesIn(tableOfElements) | |
for key, value in pairs(tableOfElements) do | |
print("examining "..key) | |
killInstanceIfAnyIn(value) | |
end | |
end | |
clearAllInstancesIn(self.touchables) | |
clearAllInstancesIn(self.texts) | |
clearAllInstancesIn(self.images) | |
end | |
--# Inventory | |
Inventory = class() | |
--[[ | |
Inventory maintains a set of items and can draw their icons to the screen. Currently it is only configured to display these icons in one specific area of the screen, namely the lower third. | |
]] | |
function Inventory:init() | |
--the set of items (must be an ordered table so icons display in consistent order) | |
self.items = {} | |
--the arbitrary setting for space between icons | |
self.spaceBetweenIcons = WIDTH / 9 | |
end | |
function Inventory:useOnlyCodeaImages(flagValue) | |
--used to toggle custom images off | |
self.shouldUseOnlyCodeaImages = flagValue | |
end | |
function Inventory:addItem(name, icon, heightRatio) | |
--prevent an item from being named the reserved term "allItems" | |
if name == "allItems" then | |
print("Error: cannot name inventory item \"allItems\". Item not added.") | |
return | |
end | |
--make a new table for this item | |
local itemTable = {} | |
--store the name and the icon in it | |
itemTable.name = name | |
itemTable.icon = icon | |
itemTable.heightRatio = heightRatio | |
--add the table to self.items | |
table.insert(self.items, itemTable) | |
end | |
function Inventory:checkForItem(itemName) | |
--booleans for the current search result and the ultimate result | |
local currentResult, ultimateResult = false, false | |
--iterate through self.items | |
for index, itemTable in ipairs(self.items) do | |
--set the currentResult to true if the names match | |
currentResult = Utilities:thisIfThisIsThisElseThis(true, itemName, itemTable.name, false) | |
--if currentResult is true, keep ultimateResult true through all subsequent loops | |
ultimateResult = ultimateResult or currentResult | |
end | |
--send back the result | |
return ultimateResult | |
end | |
function Inventory:removeItem(nameOfItem) | |
--if sent the special term "allItems", remove all items | |
if nameOfItem == "allItems" then | |
self:clearInventory() | |
return | |
end | |
--otherwise set up a variable to hold the index of the item to remove | |
local removeAt = 0 | |
--go through all the items | |
for index, item in ipairs(self.items) do | |
--set removeAt to the current index if this item has the right name | |
removeAt = Utilities:thisIfThisIsThisElseThis(index, item.name, nameOfItem, removeAt) | |
end | |
--if removeAt has not been set, the given nameOfItem isn't in the table, so return | |
if removeAt == 0 then | |
return | |
else | |
--if removeAt has been set, remove the item at that index | |
table.remove(self.items, removeAt) | |
end | |
end | |
function Inventory:clearInventory() | |
--clear the inventory by setting it to an empty table | |
self.items = {} | |
end | |
function Inventory:draw() | |
--quit if there are no items to draw | |
if #self.items == 0 then | |
return | |
end | |
--position for first item to draw | |
-- local itemY = 103 | |
-- local itemX = 160 | |
local itemY = HEIGHT * 0.13411458 | |
local itemX = WIDTH * 0.15625 | |
--go through all the items | |
for index, item in ipairs(self.items) do | |
local drawnWidth, drawnHeight | |
drawnHeight = HEIGHT * item.heightRatio | |
drawnWidth = drawnHeight * item.icon.width / item.icon.height | |
--draw item at the given position | |
if self.shouldUseOnlyCodeaImages then | |
sprite("Planet Cute:Grass Block", itemX, itemY, drawnWidth, drawnHeight) | |
else | |
sprite(item.icon, itemX, itemY, drawnWidth, drawnHeight) | |
end | |
--scoot the position to the right for the next item | |
itemX = itemX + self.spaceBetweenIcons + (drawnWidth / 2) | |
end | |
end | |
--# StyleTable | |
StyleTable = class() | |
--[[ | |
A StyleTable can preserve the current style settings and restore them when requested. | |
]] | |
function StyleTable:init() | |
--create an empty style table | |
self.style = {} | |
--initialize with whatever the current style values are | |
self:captureCurrentStyle() | |
end | |
function StyleTable:apply() | |
--go through all the style table's keys and values | |
for key, value in pairs(self.style) do | |
--use a key as a command for setting its value | |
_G[key](unpack(value)) | |
end | |
end | |
function StyleTable:captureCurrentStyle() | |
--store all the current style settings | |
self.style.stroke = {stroke()} | |
self.style.strokeWidth = {strokeWidth()} | |
self.style.fill = {fill()} | |
self.style.font = {font()} | |
self.style.fontSize = {fontSize()} | |
self.style.textWrapWidth = {textWrapWidth()} | |
self.style.textAlign = {textAlign()} | |
self.style.textMode = {textMode()} | |
--(manually set any variable to nil to prevent it from being changed when apply() is called) | |
end | |
--# Utilities | |
Utilities = class() | |
--[[ | |
Utilities contains various handy tools. It is not meant to be instantiated. Its functions are meant to be accessed as class methods. The functions have very self-explanatory names. | |
]] | |
function Utilities:copyWholeTable(orig) | |
local orig_type = type(orig) | |
local copy | |
if orig_type == 'table' then | |
copy = {} | |
for orig_key, orig_value in next, orig, nil do | |
copy[Utilities:copyWholeTable(orig_key)] = Utilities:copyWholeTable(orig_value) | |
end | |
setmetatable(copy, Utilities:copyWholeTable(getmetatable(orig))) | |
else -- number, string, boolean, etc | |
copy = orig | |
end | |
return copy | |
end | |
function Utilities:doesTableHaveStringKey(tableToCheck, stringKey) | |
--make a boolean for the given string being in the given table | |
local tableHasKey = false | |
--iterate through the table | |
for thisKey, thisValue in pairs(tableToCheck) do | |
--set the boolean to true if the key is found | |
tableHasKey = Utilities:thisIfThisIsThisElseThis(true, thisKey, stringKey, tableHasKey) | |
end | |
--return the boolean | |
return tableHasKey | |
end | |
function Utilities:stringKeyInThisTableForThisValue(tableToCheck, valueToFind) | |
--make a variable to hold the string key once found, set to an error message to start with | |
local stringKey = "string key not found" | |
--iterate through the table | |
for thisKey, thisValue in pairs(tableToCheck) do | |
--grab the key if it is found | |
stringKey = Utilities:thisIfThisIsThisElseThis(thisKey, thisValue, valueToFind, stringKey) | |
end | |
--return the key/error message | |
return stringKey | |
end | |
function Utilities:removeValueByStringKey(tableToCheck, keyString) | |
--find out if the key exists (without this method, if key doesn't exist you'll get an error) | |
if Utilities:doesTableHaveStringKey(tableToCheck, keyString) then | |
--if it does, nil it out | |
tableToCheck[keyString] = nil | |
--send true back | |
return true | |
else | |
--send false back if the key was not found | |
return false | |
end | |
end | |
function Utilities:arbitraryValueFromKeyValueTable(keyValueTable) | |
--a variable to hold the arbitrary value | |
local returnMe = nil | |
--iterate through the table | |
for key, value in pairs(keyValueTable) do | |
--grab the first value (unpredictable: pairs(table) doesn't guarantee consistent sorting) | |
returnMe = value | |
--stop after first iteration | |
break | |
end | |
--send the value back | |
return returnMen | |
end | |
function Utilities:numberOfKeyValueElements(tableToInspect) | |
--a variable with which to count the number of key-value elements in a table | |
local count = 0 | |
--iterate through the table and count the variables | |
for key, value in pairs(tableToInspect) do | |
count = count + 1 | |
end | |
--return the total | |
return count | |
end | |
function Utilities:nameIsUniqueAndNotNil(mightBeAName, givenTable, elementType) | |
--a boolean to hold whether or not the name is unique and not nil | |
local uniqueAndNotNil = true | |
--verify that a name has been given | |
if mightBeAName == nil then | |
print("error: can't check for name because mightBeAName is nil") | |
uniqueAndNotNil = false | |
end | |
--make sure name isn't used yet | |
if uniqueAndNotNil and Utilities:doesTableHaveStringKey(givenTable, mightBeAName) then | |
print("error: "..mightBeAName.." is a name already in use") | |
uniqueAndNotNil = false | |
end | |
--return the result | |
return uniqueAndNotNil | |
end | |
--label-creation function: only the first three parameters are mandatory | |
function Utilities:label(lText, width, height, textX, textY, textStyleTable, labelStyleTable) | |
--save current style settings | |
pushStyle() | |
--if a StyleTable object has been passed in for the label, use it, otherwise use defaults | |
if labelStyleTable ~= nil then | |
labelStyleTable:apply() | |
else | |
stroke(126, 127, 185, 255) | |
fill(55, 70, 110, 255) | |
strokeWidth(4) | |
end | |
--make a blank image for this label | |
local thisLabel = image(width, height) | |
--make all drawing happen on the image | |
setContext(thisLabel) | |
--set an x value for the text if specified, otherwise center it | |
local actualX | |
if textX ~= nil then | |
actualX = textX | |
else | |
actualX = width / 2 | |
end | |
--set a y value for the text if specified, otherwise center it | |
local actualY | |
if textY ~= nil then | |
actualY = textY | |
else | |
actualY = height / 2 | |
end | |
--draw the label rect | |
rect(0, 0, width, height) | |
--if a StyleTable object has been passed in for the text, use it, otherwise use defaults | |
if textStyleTable ~= nil then | |
textStyleTable:apply() | |
else | |
font("SourceSansPro-Regular") | |
fontSize(25) | |
fill(192, 192, 218, 255) | |
textAlign(CENTER) | |
end | |
--draw the text to the image | |
text(lText, width / 2, height / 2) | |
--set drawing back to happening on the device screen | |
setContext() | |
--restore the current style settings | |
pushStyle() | |
--return the label | |
return thisLabel | |
end | |
--check if a given point is inside a rect | |
function Utilities:isPointInRect(point, rectX, rectY, rectWidth, rectHeight) | |
--a variable to return, initially stating the point to be outside the rect | |
local pointIsInRect = false | |
--if the point is inside the rect's values, make the variable true | |
if point.x > rectX and point.x < rectX + rectWidth and point.y > rectY | |
and point.y < rectY + rectHeight then | |
pointIsInRect = true | |
end | |
--send back the result | |
return pointIsInRect | |
end | |
function Utilities:playSoundAndWaitUntilFinished(soundName) | |
--start the sound playing and assign it to a variable | |
local soundMonitor = sound(soundName) | |
--run an empty loop until the sound is over | |
while soundMonitor.playing == true do | |
--nothing | |
end | |
end | |
function Utilities:thisIfThisIsThisElseThis(this, ifThis, isThis, elseThis) | |
--note: this is basically the equivalent of the C syntax "a == b ? c : d" | |
--if the two middle args are equal, return the first arg, otherwise return the last arg | |
if ifThis == isThis then | |
return this | |
else | |
return elseThis | |
end | |
end | |
function Utilities:makeResizedImage(imageToResize, desiredW, desiredH) | |
--make a blank image of the specified dimensions (the "+ 1"s should not be needed but are) | |
local newImage = image(desiredW + 1, desiredH + 1) | |
--set drawing operations to go to that image | |
setContext(newImage) | |
--draw the image to be resized at the size desired | |
sprite(imageToResize, desiredW / 2, desiredH / 2, desiredW, desiredH) | |
--set drawing operations to go to the screen, same as normal | |
setContext() | |
--send back the new image | |
return newImage | |
end | |
function Utilities:makeTintedImage(imageToTint, tintColor) | |
--make a blank image of the same size | |
local newImage = image(imageToTint.width, imageToTint.height) | |
--set drawing operations to go to that image | |
setContext(newImage) | |
--set tinting to the color specified | |
tint(tintColor) | |
print("imagetotint", imageToTint) | |
--draw the image to be tinted | |
sprite(imageToTint , imageToTint.width / 2, imageToTint.height / 2) | |
--remove the tint | |
noTint() | |
--set drawing operations to go to the screen, same as normal | |
setContext() | |
--send back the new image | |
return newImage | |
end | |
function Utilities:pointSizeToFitTextToHeight(textToFit, fontName, textWidth, textHeight) | |
pushStyle() | |
font(fontName) | |
textWrapWidth(textWidth) | |
--variables to track point size, track current height, and a dummy variable for width | |
local pointSize, currentHeight, unusedWidthVariable = 0, 0, 0 | |
--make a loop that runs until the current height is one point higher than the desired height | |
while currentHeight <= textHeight do | |
--bump the current point setting by 1 | |
pointSize = pointSize + 1 | |
--set the fontSize to the new setting | |
fontSize(pointSize) | |
--get width (which isn't used for anything) and height of text at current point size | |
unusedWidthVariable, currentHeight = textSize(textToFit) | |
end | |
--take the point size back one point, since that was the last size that wasn't too big | |
pointSize = pointSize - 1 | |
popStyle() | |
--send that point size back | |
return pointSize | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment