diff --git a/.gitignore b/.gitignore
index 5f82e547e..7dbb7d607 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
export/
-.vscode/
\ No newline at end of file
+.vscode/
+APIStuff.hx
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3898230a1..875bc460f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,16 @@
# Changelog
All notable changes will be documented in this file.
-## [Unreleased]
+## [1.1.0]
### Added
- 32bit support
- Controller (dancepads) support
- Pause screen
- Main Menu overhaul
- Cool intro screen thing
+- Uh lots of bullshit
+- Remind me to actually add this later lmaooo
-## [0.1.0] - 2020-10-05
+## [1.0.0] - 2020-10-05
### Added
- Uh, everything. This the game's initial gamejam release. We put it out
\ No newline at end of file
diff --git a/Project.xml b/Project.xml
index 06de8f4ac..24bf57393 100644
--- a/Project.xml
+++ b/Project.xml
@@ -97,5 +97,6 @@
-
+
+
diff --git a/source/Alphabet.hx b/source/Alphabet.hx
index 4e04631ef..9824e2f35 100644
--- a/source/Alphabet.hx
+++ b/source/Alphabet.hx
@@ -17,6 +17,10 @@ class Alphabet extends FlxSpriteGroup
public var delay:Float = 0.05;
public var paused:Bool = false;
+ // for menu shit
+ public var targetY:Float = 0;
+ public var isMenuItem:Bool = false;
+
public var text:String = "";
var _finalText:String = "";
@@ -207,6 +211,14 @@ class Alphabet extends FlxSpriteGroup
override function update(elapsed:Float)
{
+ if (isMenuItem)
+ {
+ var scaledY = FlxMath.remapToRange(targetY, 0, 1, 0, 1.3);
+
+ y = FlxMath.lerp(y, (scaledY * 120) + (FlxG.height * 0.48), 0.16);
+ x = FlxMath.lerp(x, (targetY * 20) + 90, 0.16);
+ }
+
super.update(elapsed);
}
}
diff --git a/source/FreeplayState.hx b/source/FreeplayState.hx
index 0927dd7e6..ecdf81edf 100644
--- a/source/FreeplayState.hx
+++ b/source/FreeplayState.hx
@@ -3,47 +3,59 @@ package;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.addons.display.FlxGridOverlay;
+import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.text.FlxText;
-#if switch
- import openfl.events.GameInputEvent;
- import openfl.ui.GameInput;
- import openfl.ui.GameInputDevice;
- import openfl.ui.GameInputControl;
-#end
-
class FreeplayState extends MusicBeatState
{
- var songs:Array = ["Bopeebo", "Dadbattle", "Fresh", "Tutorial", "Spookeez", "South", "Monster"];
+ var songs:Array = ["Bopeebo", "Dadbattle", "Fresh", "Tutorial"];
var selector:FlxText;
var curSelected:Int = 0;
+ private var grpSongs:FlxTypedGroup;
+
override function create()
{
+ if (!FlxG.sound.music.playing)
+ FlxG.sound.playMusic('assets/music/freakyMenu' + TitleState.soundExt);
+
+ if (StoryMenuState.weekUnlocked[1])
+ {
+ songs.push('Spookeez');
+ songs.push('South');
+ }
+
// LOAD MUSIC
// LOAD CHARACTERS
- var bg:FlxSprite = FlxGridOverlay.create(20, 20);
+ var bg:FlxSprite = new FlxSprite().loadGraphic(AssetPaths.menuBGBlue__png);
add(bg);
+ grpSongs = new FlxTypedGroup();
+ add(grpSongs);
+
for (i in 0...songs.length)
{
var songText:Alphabet = new Alphabet(0, (70 * i) + 30, songs[i], true, false);
- add(songText);
- songText.x += 40;
+ songText.isMenuItem = true;
+ songText.targetY = i;
+ grpSongs.add(songText);
+ // songText.x += 40;
// DONT PUT X IN THE FIRST PARAMETER OF new ALPHABET() !!
// songText.screenCenter(X);
}
- FlxG.sound.playMusic('assets/music/title' + TitleState.soundExt, 0);
- FlxG.sound.music.fadeIn(2, 0, 0.8);
+ changeSelection();
+
+ // FlxG.sound.playMusic('assets/music/title' + TitleState.soundExt, 0);
+ // FlxG.sound.music.fadeIn(2, 0, 0.8);
selector = new FlxText();
selector.size = 40;
selector.text = ">";
- add(selector);
+ // add(selector);
var swag:Alphabet = new Alphabet(1, 0, "swag");
@@ -59,47 +71,53 @@ class FreeplayState extends MusicBeatState
if (upP)
{
- curSelected -= 1;
+ changeSelection(-1);
}
if (downP)
{
- curSelected += 1;
+ changeSelection(1);
}
- #if switch
- if (gamepad.anyJustPressed(["UP", "DPAD_UP", "LEFT_STICK_DIGITAL_UP"]))
- {
- curSelected -= 1;
- }
- if (gamepad.anyJustPressed(["DOWN", "DPAD_DOWN", "LEFT_STICK_DIGITAL_DOWN"]))
- {
- curSelected += 1;
- }
- #end
+ if (controls.BACK)
+ {
+ FlxG.switchState(new MainMenuState());
+ }
+
+ if (accepted)
+ {
+ PlayState.SONG = Song.loadFromJson(songs[curSelected].toLowerCase(), songs[curSelected].toLowerCase());
+ PlayState.isStoryMode = false;
+ FlxG.switchState(new PlayState());
+ FlxG.sound.music.stop();
+ }
+ }
+
+ function changeSelection(change:Int = 0)
+ {
+ curSelected += change;
if (curSelected < 0)
curSelected = songs.length - 1;
if (curSelected >= songs.length)
curSelected = 0;
- selector.y = (70 * curSelected) + 30;
+ // selector.y = (70 * curSelected) + 30;
- if (accepted)
+ var bullShit:Int = 0;
+
+ for (item in grpSongs.members)
{
- PlayState.SONG = Song.loadFromJson(songs[curSelected].toLowerCase());
- PlayState.isStoryMode = false;
- FlxG.switchState(new PlayState());
- FlxG.sound.music.stop();
- }
+ item.targetY = bullShit - curSelected;
+ bullShit++;
- #if switch
- if (gamepad.anyJustPressed(["B"])) //"B" is swapped with "A" on Switch
+ item.alpha = 0.6;
+ // item.setGraphicSize(Std.int(item.width * 0.8));
+
+ if (item.targetY == 0)
{
- PlayState.SONG = Song.loadFromJson(songs[curSelected].toLowerCase());
- PlayState.isStoryMode = false;
- FlxG.switchState(new PlayState());
- FlxG.sound.music.stop();
+ item.alpha = 1;
+ // item.setGraphicSize(Std.int(item.width));
}
- #end
+ }
}
}
diff --git a/source/GameOverSubstate.hx b/source/GameOverSubstate.hx
index 1a7d89d58..54df50cdd 100644
--- a/source/GameOverSubstate.hx
+++ b/source/GameOverSubstate.hx
@@ -12,6 +12,8 @@ class GameOverSubstate extends MusicBeatSubstate
var bf:Boyfriend;
var camFollow:FlxObject;
+ // var
+
public function new(x:Float, y:Float)
{
super();
@@ -39,11 +41,21 @@ class GameOverSubstate extends MusicBeatSubstate
{
super.update(elapsed);
- if (FlxG.keys.justPressed.ENTER)
+ if (controls.ACCEPT)
{
endBullshit();
}
+ if (controls.BACK)
+ {
+ FlxG.sound.music.stop();
+
+ if (PlayState.isStoryMode)
+ FlxG.switchState(new StoryMenuState());
+ else
+ FlxG.switchState(new FreeplayState());
+ }
+
if (bf.animation.curAnim.name == 'firstDeath' && bf.animation.curAnim.curFrame == 12)
{
FlxG.camera.follow(camFollow, LOCKON, 0.01);
diff --git a/source/MainMenuState.hx b/source/MainMenuState.hx
index 687f6191b..19ddb46bb 100644
--- a/source/MainMenuState.hx
+++ b/source/MainMenuState.hx
@@ -73,70 +73,76 @@ class MainMenuState extends MusicBeatState
super.create();
}
+ var selectedSomethin:Bool = false;
+
override function update(elapsed:Float)
{
- if (controls.UP_P)
+ if (!selectedSomethin)
{
- FlxG.sound.play('assets/sounds/scrollMenu' + TitleState.soundExt);
- changeItem(-1);
- }
+ if (controls.UP_P)
+ {
+ FlxG.sound.play('assets/sounds/scrollMenu' + TitleState.soundExt);
+ changeItem(-1);
+ }
- if (controls.DOWN_P)
- {
- FlxG.sound.play('assets/sounds/scrollMenu' + TitleState.soundExt);
- changeItem(1);
- }
+ if (controls.DOWN_P)
+ {
+ FlxG.sound.play('assets/sounds/scrollMenu' + TitleState.soundExt);
+ changeItem(1);
+ }
- if (controls.BACK)
- {
- FlxG.switchState(new TitleState());
+ if (controls.BACK)
+ {
+ FlxG.switchState(new TitleState());
+ }
+
+ if (controls.ACCEPT)
+ {
+ if (optionShit[curSelected] == 'donate')
+ {
+ FlxG.openURL('https://ninja-muffin24.itch.io/funkin');
+ }
+ else
+ {
+ selectedSomethin = true;
+ FlxG.sound.play('assets/sounds/confirmMenu' + TitleState.soundExt);
+
+ FlxFlicker.flicker(magenta, 1.1, 0.15, false);
+
+ menuItems.forEach(function(spr:FlxSprite)
+ {
+ if (curSelected != spr.ID)
+ {
+ FlxTween.tween(spr, {alpha: 0}, 0.4, {
+ ease: FlxEase.quadOut,
+ onComplete: function(twn:FlxTween)
+ {
+ spr.kill();
+ }
+ });
+ }
+ else
+ {
+ FlxFlicker.flicker(spr, 1, 0.06, false, false, function(flick:FlxFlicker)
+ {
+ var daChoice:String = optionShit[curSelected];
+
+ switch (daChoice)
+ {
+ case 'story mode':
+ FlxG.switchState(new StoryMenuState());
+ case 'freeplay':
+ FlxG.switchState(new FreeplayState());
+ }
+ });
+ }
+ });
+ }
+ }
}
super.update(elapsed);
- if (controls.ACCEPT)
- {
- if (optionShit[curSelected] == 'donate')
- {
- FlxG.openURL('https://ninja-muffin24.itch.io/funkin');
- }
- else
- {
- FlxG.sound.play('assets/sounds/confirmMenu' + TitleState.soundExt);
-
- FlxFlicker.flicker(magenta, 1.1, 0.15, false);
-
- menuItems.forEach(function(spr:FlxSprite)
- {
- if (curSelected != spr.ID)
- {
- FlxTween.tween(spr, {alpha: 0}, 0.4, {
- ease: FlxEase.quadOut,
- onComplete: function(twn:FlxTween)
- {
- spr.kill();
- }
- });
- }
- else
- {
- FlxFlicker.flicker(spr, 1, 0.06, false, false, function(flick:FlxFlicker)
- {
- var daChoice:String = optionShit[curSelected];
-
- switch (daChoice)
- {
- case 'story mode':
- FlxG.switchState(new StoryMenuState());
- case 'freeplay':
- FlxG.switchState(new FreeplayState());
- }
- });
- }
- });
- }
- }
-
menuItems.forEach(function(spr:FlxSprite)
{
spr.screenCenter(X);
diff --git a/source/NGio.hx b/source/NGio.hx
new file mode 100644
index 000000000..f90bca4c4
--- /dev/null
+++ b/source/NGio.hx
@@ -0,0 +1,158 @@
+package;
+
+import flixel.FlxG;
+import flixel.util.FlxSignal;
+import io.newgrounds.NG;
+import io.newgrounds.components.ScoreBoardComponent.Period;
+import io.newgrounds.objects.Medal;
+import io.newgrounds.objects.Score;
+import io.newgrounds.objects.ScoreBoard;
+import openfl.display.Stage;
+
+/**
+ * MADE BY GEOKURELI THE LEGENED GOD HERO MVP
+ */
+class NGio
+{
+ public static var isLoggedIn:Bool = false;
+ public static var scoreboardsLoaded:Bool = false;
+
+ public static var scoreboardArray:Array = [];
+
+ public static var ngDataLoaded(default, null):FlxSignal = new FlxSignal();
+ public static var ngScoresLoaded(default, null):FlxSignal = new FlxSignal();
+
+ public function new(api:String, encKey:String, ?sessionId:String)
+ {
+ trace("connecting to newgrounds");
+
+ NG.createAndCheckSession(api, sessionId);
+
+ NG.core.verbose = true;
+ // Set the encryption cipher/format to RC4/Base64. AES128 and Hex are not implemented yet
+ NG.core.initEncryption(encKey); // Found in you NG project view
+
+ trace(NG.core.attemptingLogin);
+
+ if (NG.core.attemptingLogin)
+ {
+ /* a session_id was found in the loadervars, this means the user is playing on newgrounds.com
+ * and we should login shortly. lets wait for that to happen
+ */
+ trace("attempting login");
+ NG.core.onLogin.add(onNGLogin);
+ }
+ else
+ {
+ /* They are NOT playing on newgrounds.com, no session id was found. We must start one manually, if we want to.
+ * Note: This will cause a new browser window to pop up where they can log in to newgrounds
+ */
+ NG.core.requestLogin(onNGLogin);
+ }
+ }
+
+ function onNGLogin():Void
+ {
+ trace('logged in! user:${NG.core.user.name}');
+ isLoggedIn = true;
+ FlxG.save.data.sessionId = NG.core.sessionId;
+ // FlxG.save.flush();
+ // Load medals then call onNGMedalFetch()
+ NG.core.requestMedals(onNGMedalFetch);
+
+ // Load Scoreboards hten call onNGBoardsFetch()
+ NG.core.requestScoreBoards(onNGBoardsFetch);
+
+ ngDataLoaded.dispatch();
+ }
+
+ // --- MEDALS
+ function onNGMedalFetch():Void
+ {
+ /*
+ // Reading medal info
+ for (id in NG.core.medals.keys())
+ {
+ var medal = NG.core.medals.get(id);
+ trace('loaded medal id:$id, name:${medal.name}, description:${medal.description}');
+ }
+
+ // Unlocking medals
+ var unlockingMedal = NG.core.medals.get(54352);// medal ids are listed in your NG project viewer
+ if (!unlockingMedal.unlocked)
+ unlockingMedal.sendUnlock();
+ */
+ }
+
+ // --- SCOREBOARDS
+ function onNGBoardsFetch():Void
+ {
+ /*
+ // Reading medal info
+ for (id in NG.core.scoreBoards.keys())
+ {
+ var board = NG.core.scoreBoards.get(id);
+ trace('loaded scoreboard id:$id, name:${board.name}');
+ }
+ */
+ // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
+
+ // Posting a score thats OVER 9000!
+ // board.postScore(FlxG.random.int(0, 1000));
+
+ // --- To view the scores you first need to select the range of scores you want to see ---
+
+ // add an update listener so we know when we get the new scores
+ // board.onUpdate.add(onNGScoresFetch);
+ trace("shoulda got score by NOW!");
+ // board.requestScores(20);// get the best 10 scores ever logged
+ // more info on scores --- http://www.newgrounds.io/help/components/#scoreboard-getscores
+ }
+
+ inline static public function postScore(score:Int = 0, song:String)
+ {
+ if (isLoggedIn)
+ {
+ for (id in NG.core.scoreBoards.keys())
+ {
+ var board = NG.core.scoreBoards.get(id);
+
+ if (song == board.name)
+ {
+ board.postScore(score, "Uhh meow?");
+ }
+
+ // trace('loaded scoreboard id:$id, name:${board.name}');
+ }
+ }
+ }
+
+ function onNGScoresFetch():Void
+ {
+ scoreboardsLoaded = true;
+
+ ngScoresLoaded.dispatch();
+ /*
+ for (score in NG.core.scoreBoards.get(8737).scores)
+ {
+ trace('score loaded user:${score.user.name}, score:${score.formatted_value}');
+
+ }
+ */
+
+ // var board = NG.core.scoreBoards.get(8004);// ID found in NG project view
+ // board.postScore(HighScore.score);
+
+ // NGio.scoreboardArray = NG.core.scoreBoards.get(8004).scores;
+ }
+
+ inline static public function unlockMedal(id:Int)
+ {
+ if (isLoggedIn)
+ {
+ var medal = NG.core.medals.get(id);
+ if (!medal.unlocked)
+ medal.sendUnlock();
+ }
+ }
+}
diff --git a/source/PlayState.hx b/source/PlayState.hx
index 29aeb7335..2130a37bf 100644
--- a/source/PlayState.hx
+++ b/source/PlayState.hx
@@ -80,6 +80,7 @@ class PlayState extends MusicBeatState
var halloweenBG:FlxSprite;
var talking:Bool = true;
+ var songScore:Int = 0;
override public function create()
{
@@ -593,7 +594,7 @@ class PlayState extends MusicBeatState
if (FlxG.keys.justPressed.ESCAPE)
{
- FlxG.switchState(new ChartingState());
+ // FlxG.switchState(new ChartingState());
}
// FlxG.watch.addQuick('VOL', vocals.amplitudeLeft);
@@ -602,6 +603,9 @@ class PlayState extends MusicBeatState
healthHeads.setGraphicSize(Std.int(FlxMath.lerp(100, healthHeads.width, 0.98)));
healthHeads.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.percent, 0, 100, 100, 0) * 0.01)) - (healthHeads.width / 2);
+ if (health > 2)
+ health = 2;
+
if (healthBar.percent < 20)
healthHeads.animation.play('unhealthy');
else
@@ -609,8 +613,8 @@ class PlayState extends MusicBeatState
/* if (FlxG.keys.justPressed.NINE)
FlxG.switchState(new Charting()); */
- if (FlxG.keys.justPressed.EIGHT)
- FlxG.switchState(new AnimationDebug(SONG.player2));
+ // if (FlxG.keys.justPressed.EIGHT)
+ // FlxG.switchState(new AnimationDebug(SONG.player2));
if (startingSong)
{
@@ -805,15 +809,24 @@ class PlayState extends MusicBeatState
{
trace('SONG DONE' + isStoryMode);
+ NGio.postScore(songScore, SONG.song);
+
if (isStoryMode)
{
storyPlaylist.remove(storyPlaylist[0]);
if (storyPlaylist.length <= 0)
{
- FlxG.switchState(new TitleState());
+ FlxG.sound.playMusic('assets/music/freakyMenu' + TitleState.soundExt);
+
+ FlxG.switchState(new StoryMenuState());
StoryMenuState.weekUnlocked[1] = true;
+
+ NGio.unlockMedal(60961);
+
+ FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
+ FlxG.save.flush();
}
else
{
@@ -851,22 +864,28 @@ class PlayState extends MusicBeatState
//
var rating:FlxSprite = new FlxSprite();
+ var score:Int = 350;
var daRating:String = "sick";
if (noteDiff > Conductor.safeZoneOffset * 0.9)
{
daRating = 'shit';
+ score = 50;
}
else if (noteDiff > Conductor.safeZoneOffset * 0.75)
{
daRating = 'bad';
+ score = 100;
}
else if (noteDiff > Conductor.safeZoneOffset * 0.2)
{
daRating = 'good';
+ score = 200;
}
+ songScore += score;
+
/* if (combo > 60)
daRating = 'sick';
else if (combo > 12)
@@ -916,7 +935,9 @@ class PlayState extends MusicBeatState
numScore.acceleration.y = FlxG.random.int(200, 300);
numScore.velocity.y -= FlxG.random.int(140, 160);
numScore.velocity.x = FlxG.random.float(-5, 5);
- add(numScore);
+
+ if (combo >= 10 || combo == 0)
+ add(numScore);
FlxTween.tween(numScore, {alpha: 0}, 0.2, {
onComplete: function(tween:FlxTween)
@@ -1102,6 +1123,8 @@ class PlayState extends MusicBeatState
}
combo = 0;
+ songScore -= 10;
+
FlxG.sound.play('assets/sounds/missnote' + FlxG.random.int(1, 3) + TitleState.soundExt, FlxG.random.float(0.1, 0.2));
// FlxG.sound.play('assets/sounds/missnote1' + TitleState.soundExt, 1, false);
// FlxG.log.add('played imss note');
@@ -1283,7 +1306,7 @@ class PlayState extends MusicBeatState
if (!boyfriend.animation.curAnim.name.startsWith("sing"))
boyfriend.playAnim('idle');
- if (totalBeats % 8 == 6)
+ if (totalBeats % 8 == 7 && curSong == 'Bopeebo')
{
boyfriend.playAnim('hey', true);
diff --git a/source/StoryMenuState.hx b/source/StoryMenuState.hx
index 093746479..d88ab4833 100644
--- a/source/StoryMenuState.hx
+++ b/source/StoryMenuState.hx
@@ -16,7 +16,7 @@ class StoryMenuState extends MusicBeatState
{
var scoreText:FlxText;
- var weekData:Array = [['Tutorial', 'Bopeebo', 'Fresh', 'Dadbattle'], ['Spookeez', 'South', 'Monster']];
+ var weekData:Array = [['Tutorial', 'Bopeebo', 'Fresh', 'Dadbattle'], ['Spookeez', 'South']];
var curDifficulty:Int = 1;
public static var weekUnlocked:Array = [true, false];
@@ -38,6 +38,9 @@ class StoryMenuState extends MusicBeatState
override function create()
{
+ if (!FlxG.sound.music.playing)
+ FlxG.sound.playMusic('assets/music/freakyMenu' + TitleState.soundExt);
+
persistentUpdate = persistentDraw = true;
scoreText = new FlxText(10, 10, 0, "SCORE: 49324858", 36);
@@ -102,6 +105,7 @@ class StoryMenuState extends MusicBeatState
case 'bf':
weekCharacterThing.setGraphicSize(Std.int(weekCharacterThing.width * 0.9));
weekCharacterThing.updateHitbox();
+ weekCharacterThing.x -= 80;
case 'gf':
weekCharacterThing.setGraphicSize(Std.int(weekCharacterThing.width * 0.5));
weekCharacterThing.updateHitbox();
@@ -145,8 +149,8 @@ class StoryMenuState extends MusicBeatState
txtTracklist.font = rankText.font;
txtTracklist.color = 0xFFe55777;
add(txtTracklist);
- add(rankText);
- add(scoreText);
+ // add(rankText);
+ // add(scoreText);
updateText();
@@ -243,7 +247,8 @@ class StoryMenuState extends MusicBeatState
PlayState.SONG = Song.loadFromJson(PlayState.storyPlaylist[0].toLowerCase() + diffic, PlayState.storyPlaylist[0].toLowerCase());
new FlxTimer().start(1, function(tmr:FlxTimer)
{
- FlxG.sound.music.stop();
+ if (FlxG.sound.music != null)
+ FlxG.sound.music.stop();
FlxG.switchState(new PlayState());
});
}
@@ -274,9 +279,11 @@ class StoryMenuState extends MusicBeatState
}
sprDifficulty.alpha = 0;
- sprDifficulty.y -= 15;
- FlxTween.tween(sprDifficulty, {y: sprDifficulty.y + 15, alpha: 1}, 0.07);
+ // USING THESE WEIRD VALUES SO THAT IT DOESNT FLOAT UP
+ sprDifficulty.y = leftArrow.y - 15;
+
+ FlxTween.tween(sprDifficulty, {y: leftArrow.y + 15, alpha: 1}, 0.07);
}
function changeWeek(change:Int = 0):Void
diff --git a/source/TitleState.hx b/source/TitleState.hx
index 72cae1944..12faa41c1 100644
--- a/source/TitleState.hx
+++ b/source/TitleState.hx
@@ -8,6 +8,7 @@ import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.transition.TransitionData;
import flixel.graphics.FlxGraphic;
+import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxGroup;
import flixel.input.gamepad.FlxGamepad;
import flixel.math.FlxPoint;
@@ -53,6 +54,10 @@ class TitleState extends MusicBeatState
super.create();
+ #if (!debug && NG_LOGIN)
+ var ng:NGio = new NGio(APIStuff.API, APIStuff.EncKey);
+ #end
+
#if SKIP_TO_PLAYSTATE
FlxG.switchState(new StoryMenuState());
#else
@@ -60,6 +65,11 @@ class TitleState extends MusicBeatState
#end
}
+ var logoBl:FlxSprite;
+ var gfDance:FlxSprite;
+ var danceLeft:Bool = false;
+ var titleText:FlxSprite;
+
function startIntro()
{
if (!initialized)
@@ -82,28 +92,58 @@ class TitleState extends MusicBeatState
FlxG.sound.playMusic('assets/music/freakyMenu' + TitleState.soundExt, 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
+
+ FlxG.save.bind('funkin', 'ninjamuffin99');
+
+ if (FlxG.save.data.weekUnlocked != null)
+ {
+ StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
+ }
}
+ Conductor.changeBPM(102);
persistentUpdate = true;
- var bg:FlxSprite = new FlxSprite().loadGraphic(AssetPaths.stageback__png);
- bg.antialiasing = true;
- bg.setGraphicSize(Std.int(bg.width * 0.6));
- bg.updateHitbox();
+ var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
+ // bg.antialiasing = true;
+ // bg.setGraphicSize(Std.int(bg.width * 0.6));
+ // bg.updateHitbox();
add(bg);
- var logoBl:FlxSprite = new FlxSprite().loadGraphic(AssetPaths.logo__png);
- logoBl.screenCenter();
- logoBl.color = FlxColor.BLACK;
+ logoBl = new FlxSprite(-150, -100);
+ logoBl.frames = FlxAtlasFrames.fromSparrow(AssetPaths.logoBumpin__png, AssetPaths.logoBumpin__xml);
+ logoBl.antialiasing = true;
+ logoBl.animation.addByPrefix('bump', 'logo bumpin', 24);
+ logoBl.animation.play('bump');
+ logoBl.updateHitbox();
+ // logoBl.screenCenter();
+ // logoBl.color = FlxColor.BLACK;
+
+ gfDance = new FlxSprite(FlxG.width * 0.4, FlxG.height * 0.07);
+ gfDance.frames = FlxAtlasFrames.fromSparrow(AssetPaths.gfDanceTitle__png, AssetPaths.gfDanceTitle__xml);
+ gfDance.animation.addByIndices('danceLeft', 'gfDance', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
+ gfDance.animation.addByIndices('danceRight', 'gfDance', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
+ gfDance.antialiasing = true;
+ add(gfDance);
add(logoBl);
+ titleText = new FlxSprite(100, FlxG.height * 0.8);
+ titleText.frames = FlxAtlasFrames.fromSparrow(AssetPaths.titleEnter__png, AssetPaths.titleEnter__xml);
+ titleText.animation.addByPrefix('idle', "Press Enter to Begin", 24);
+ titleText.animation.addByPrefix('press', "ENTER PRESSED", 24);
+ titleText.antialiasing = true;
+ titleText.animation.play('idle');
+ titleText.updateHitbox();
+ // titleText.screenCenter(X);
+ add(titleText);
+
var logo:FlxSprite = new FlxSprite().loadGraphic(AssetPaths.logo__png);
logo.screenCenter();
logo.antialiasing = true;
- add(logo);
+ // add(logo);
- FlxTween.tween(logoBl, {y: logoBl.y + 50}, 0.6, {ease: FlxEase.quadInOut, type: PINGPONG});
- FlxTween.tween(logo, {y: logoBl.y + 50}, 0.6, {ease: FlxEase.quadInOut, type: PINGPONG, startDelay: 0.1});
+ // FlxTween.tween(logoBl, {y: logoBl.y + 50}, 0.6, {ease: FlxEase.quadInOut, type: PINGPONG});
+ // FlxTween.tween(logo, {y: logoBl.y + 50}, 0.6, {ease: FlxEase.quadInOut, type: PINGPONG, startDelay: 0.1});
credGroup = new FlxGroup();
add(credGroup);
@@ -129,6 +169,8 @@ class TitleState extends MusicBeatState
FlxTween.tween(credTextShit, {y: credTextShit.y + 20}, 2.9, {ease: FlxEase.quadInOut, type: PINGPONG});
+ FlxG.mouse.visible = false;
+
if (initialized)
skipIntro();
else
@@ -155,6 +197,9 @@ class TitleState extends MusicBeatState
if (pressedEnter && !transitioning && skippedIntro)
{
+ NGio.unlockMedal(60960);
+ titleText.animation.play('press');
+
FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play('assets/sounds/confirmMenu' + TitleState.soundExt, 0.7);
@@ -210,6 +255,14 @@ class TitleState extends MusicBeatState
{
super.beatHit();
+ logoBl.animation.play('bump');
+ danceLeft = !danceLeft;
+
+ if (danceLeft)
+ gfDance.animation.play('danceRight');
+ else
+ gfDance.animation.play('danceLeft');
+
FlxG.log.add(curBeat);
switch (curBeat)
diff --git a/source/io/newgrounds/Call.hx b/source/io/newgrounds/Call.hx
new file mode 100644
index 000000000..bd5d7acaf
--- /dev/null
+++ b/source/io/newgrounds/Call.hx
@@ -0,0 +1,227 @@
+package io.newgrounds;
+
+import io.newgrounds.utils.Dispatcher;
+import io.newgrounds.utils.AsyncHttp;
+import io.newgrounds.objects.Error;
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.objects.events.Result.ResultBase;
+import io.newgrounds.objects.events.Response;
+
+import haxe.ds.StringMap;
+import haxe.Json;
+
+/** A generic way to handle calls agnostic to their type */
+interface ICallable {
+
+ public var component(default, null):String;
+
+ public function send():Void;
+ public function queue():Void;
+ public function destroy():Void;
+}
+
+class Call
+ implements ICallable {
+
+ public var component(default, null):String;
+
+ var _core:NGLite;
+ var _properties:StringMap;
+ var _parameters:StringMap;
+ var _requireSession:Bool;
+ var _isSecure:Bool;
+
+ // --- BASICALLY SIGNALS
+ var _dataHandlers:TypedDispatcher>;
+ var _successHandlers:Dispatcher;
+ var _httpErrorHandlers:TypedDispatcher;
+ var _statusHandlers:TypedDispatcher;
+
+ public function new (core:NGLite, component:String, requireSession:Bool = false, isSecure:Bool = false) {
+
+ _core = core;
+ this.component = component;
+ _requireSession = requireSession;
+ _isSecure = isSecure && core.encryptionHandler != null;
+ }
+
+ /** adds a property to the input's object. **/
+ public function addProperty(name:String, value:Dynamic):Call {
+
+ if (_properties == null)
+ _properties = new StringMap();
+
+ _properties.set(name, value);
+
+ return this;
+ }
+
+ /** adds a parameter to the call's component object. **/
+ public function addComponentParameter(name:String, value:Dynamic, defaultValue:Dynamic = null):Call {
+
+ if (value == defaultValue)//TODO?: allow sending null value
+ return this;
+
+ if (_parameters == null)
+ _parameters = new StringMap();
+
+ _parameters.set(name, value);
+
+ return this;
+ }
+
+ /** Handy callback setter for chained call modifiers. Called when ng.io replies successfully */
+ public function addDataHandler(handler:Response->Void):Call {
+
+ if (_dataHandlers == null)
+ _dataHandlers = new TypedDispatcher>();
+
+ _dataHandlers.add(handler);
+ return this;
+ }
+
+ /** Handy callback setter for chained call modifiers. Called when ng.io replies successfully */
+ public function addSuccessHandler(handler:Void->Void):Call {
+
+ if (_successHandlers == null)
+ _successHandlers = new Dispatcher();
+
+ _successHandlers.add(handler);
+ return this;
+ }
+
+ /** Handy callback setter for chained call modifiers. Called when ng.io does not reply for any reason */
+ public function addErrorHandler(handler:Error->Void):Call {
+
+ if (_httpErrorHandlers == null)
+ _httpErrorHandlers = new TypedDispatcher();
+
+ _httpErrorHandlers.add(handler);
+ return this;
+ }
+
+ /** Handy callback setter for chained call modifiers. No idea when this is called; */
+ public function addStatusHandler(handler:Int->Void):Call {//TODO:learn what this is for
+
+ if (_statusHandlers == null)
+ _statusHandlers = new TypedDispatcher();
+
+ _statusHandlers.add(handler);
+ return this;
+ }
+
+ /**
+ * Sends the call to the server, do not modify this object after calling this
+ * @param secure If encryption is enabled, it will encrypt the call.
+ **/
+ public function send():Void {
+
+ var data:Dynamic = {};
+ data.app_id = _core.appId;
+ data.call = {};
+ data.call.component = component;
+
+ if (_core.debug)
+ addProperty("debug", true);
+
+ if (_properties == null || !_properties.exists("session_id")) {
+ // --- HAS NO SESSION ID
+
+ if (_core.sessionId != null) {
+ // --- AUTO ADD SESSION ID
+
+ addProperty("session_id", _core.sessionId);
+
+ } else if (_requireSession){
+
+ _core.logError(new Error('cannot send "$component" call without a sessionId'));
+ return;
+ }
+ }
+
+ if (_properties != null) {
+
+ for (field in _properties.keys())
+ Reflect.setField(data, field, _properties.get(field));
+ }
+
+ if (_parameters != null) {
+
+ data.call.parameters = {};
+
+ for (field in _parameters.keys())
+ Reflect.setField(data.call.parameters, field, _parameters.get(field));
+ }
+
+ _core.logVerbose('Post - ${Json.stringify(data)}');
+
+ if (_isSecure) {
+
+ var secureData = _core.encryptionHandler(Json.stringify(data.call));
+ data.call = {};
+ data.call.secure = secureData;
+
+ _core.logVerbose(' secure - $secureData');
+ }
+
+ _core.markCallPending(this);
+
+ AsyncHttp.send(_core, Json.stringify(data), onData, onHttpError, onStatus);
+ }
+
+ /** Adds the call to the queue */
+ public function queue():Void {
+
+ _core.queueCall(this);
+ }
+
+ function onData(reply:String):Void {
+
+ _core.logVerbose('Reply - $reply');
+
+ if (_dataHandlers == null && _successHandlers == null)
+ return;
+
+ var response = new Response(_core, reply);
+
+ if (_dataHandlers != null)
+ _dataHandlers.dispatch(response);
+
+ if (response.success && response.result.success && _successHandlers != null)
+ _successHandlers.dispatch();
+
+ destroy();
+ }
+
+ function onHttpError(message:String):Void {
+
+ _core.logError(message);
+
+ if (_httpErrorHandlers == null)
+ return;
+
+ var error = new Error(message);
+ _httpErrorHandlers.dispatch(error);
+ }
+
+ function onStatus(status:Int):Void {
+
+ if (_statusHandlers == null)
+ return;
+
+ _statusHandlers.dispatch(status);
+ }
+
+ public function destroy():Void {
+
+ _core = null;
+
+ _properties = null;
+ _parameters = null;
+
+ _dataHandlers = null;
+ _successHandlers = null;
+ _httpErrorHandlers = null;
+ _statusHandlers = null;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/NG.hx b/source/io/newgrounds/NG.hx
new file mode 100644
index 000000000..3445443ee
--- /dev/null
+++ b/source/io/newgrounds/NG.hx
@@ -0,0 +1,475 @@
+package io.newgrounds;
+
+#if ng_lite
+typedef NG = NGLite; //TODO: test and make lite UI
+#else
+import io.newgrounds.utils.Dispatcher;
+import io.newgrounds.objects.Error;
+import io.newgrounds.objects.events.Result.SessionResult;
+import io.newgrounds.objects.events.Result.MedalListResult;
+import io.newgrounds.objects.events.Result.ScoreBoardResult;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.User;
+import io.newgrounds.objects.Medal;
+import io.newgrounds.objects.Session;
+import io.newgrounds.objects.ScoreBoard;
+
+import haxe.ds.IntMap;
+import haxe.Timer;
+
+/**
+ * The Newgrounds API for Haxe.
+ * Contains many things ripped from MSGhero
+ * - https://github.com/MSGhero/NG.hx
+ * @author GeoKureli
+ */
+class NG extends NGLite {
+
+ static public var core(default, null):NG;
+ static public var onCoreReady(default, null):Dispatcher = new Dispatcher();
+
+ // --- DATA
+
+ /** The logged in user */
+ public var user(get, never):User;
+ function get_user():User {
+
+ if (_session == null)
+ return null;
+
+ return _session.user;
+ }
+ public var passportUrl(get, never):String;
+ function get_passportUrl():String {
+
+ if (_session == null || _session.status != SessionStatus.REQUEST_LOGIN)
+ return null;
+
+ return _session.passportUrl;
+ }
+ public var medals(default, null):IntMap;
+ public var scoreBoards(default, null):IntMap;
+
+ // --- EVENTS
+
+ public var onLogin(default, null):Dispatcher;
+ public var onLogOut(default, null):Dispatcher;
+ public var onMedalsLoaded(default, null):Dispatcher;
+ public var onScoreBoardsLoaded(default, null):Dispatcher;
+
+ // --- MISC
+
+ public var loggedIn(default, null):Bool;
+ public var attemptingLogin(default, null):Bool;
+
+ var _loginCancelled:Bool;
+ var _passportCallback:Void->Void;
+
+ var _session:Session;
+
+ /**
+ * Iniitializes the API, call before utilizing any other component
+ * @param appId The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project.
+ * @param sessionId A unique session id used to identify the active user.
+ **/
+ public function new(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void) {
+
+ _session = new Session(this);
+ onLogin = new Dispatcher();
+ onLogOut = new Dispatcher();
+ onMedalsLoaded = new Dispatcher();
+ onScoreBoardsLoaded = new Dispatcher();
+
+ attemptingLogin = sessionId != null;
+
+ super(appId, sessionId, onSessionFail);
+ }
+
+ /**
+ * Creates NG.core, the heart and soul of the API. This is not the only way to create an instance,
+ * nor is NG a forced singleton, but it's the only way to set the static NG.core.
+ **/
+ static public function create(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void):Void {
+
+ core = new NG(appId, sessionId, onSessionFail);
+
+ onCoreReady.dispatch();
+ }
+
+ /**
+ * Creates NG.core, and tries to create a session. This is not the only way to create an instance,
+ * nor is NG a forced singleton, but it's the only way to set the static NG.core.
+ **/
+ static public function createAndCheckSession
+ ( appId = "test"
+ , backupSession:String = null
+ , ?onSessionFail:Error->Void
+ ):Void {
+
+ var session = NGLite.getSessionId();
+ if (session == null)
+ session = backupSession;
+
+ create(appId, session, onSessionFail);
+
+ core.host = getHost();
+ if (core.sessionId != null)
+ core.attemptingLogin = true;
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // APP
+ // -------------------------------------------------------------------------------------------
+
+ override function checkInitialSession(failHandler:Error->Void, response:Response):Void {
+
+ onSessionReceive(response, null, null, failHandler);
+ }
+
+ /**
+ * Begins the login process
+ *
+ * @param onSuccess Called when the login is a success
+ * @param onPending Called when the passportUrl has been identified, call NG.core.openPassportLink
+ * to open the link continue the process. Leave as null to open the url automatically
+ * NOTE: Browser games must open links on click events or else it will be blocked by
+ * the popup blocker.
+ * @param onFail
+ * @param onCancel Called when the user denies the passport connection.
+ */
+ public function requestLogin
+ ( onSuccess:Void->Void = null
+ , onPending:Void->Void = null
+ , onFail :Error->Void = null
+ , onCancel :Void->Void = null
+ ):Void {
+
+ if (attemptingLogin) {
+
+ logError("cannot request another login until the previous attempt is complete");
+ return;
+ }
+
+ if (loggedIn) {
+
+ logError("cannot log in, already logged in");
+ return;
+ }
+
+ attemptingLogin = true;
+ _loginCancelled = false;
+ _passportCallback = null;
+
+ var call = calls.app.startSession(true)
+ .addDataHandler(onSessionReceive.bind(_, onSuccess, onPending, onFail, onCancel));
+
+ if (onFail != null)
+ call.addErrorHandler(onFail);
+
+ call.send();
+ }
+
+ function onSessionReceive
+ ( response :Response
+ , onSuccess:Void->Void = null
+ , onPending:Void->Void = null
+ , onFail :Error->Void = null
+ , onCancel :Void->Void = null
+ ):Void {
+
+ if (!response.success || !response.result.success) {
+
+ sessionId = null;
+ endLoginAndCall(null);
+
+ if (onFail != null)
+ onFail(!response.success ? response.error : response.result.error);
+
+ return;
+ }
+
+ _session.parse(response.result.data.session);
+ sessionId = _session.id;
+
+ logVerbose('session started - status: ${_session.status}');
+
+ if (_session.status == SessionStatus.REQUEST_LOGIN) {
+
+ _passportCallback = checkSession.bind(null, onSuccess, onCancel);
+ if (onPending != null)
+ onPending();
+ else
+ openPassportUrl();
+
+ } else
+ checkSession(null, onSuccess, onCancel);
+ }
+
+ /**
+ * Call this once the passport link is established and it will load the passport URL and
+ * start checking for session connect periodically
+ */
+ public function openPassportUrl():Void {
+
+ if (passportUrl != null) {
+
+ logVerbose('loading passport: ${passportUrl}');
+ openPassportHelper(passportUrl);
+ onPassportUrlOpen();
+
+ } else
+ logError("Cannot open passport");
+ }
+
+
+ static function openPassportHelper(url:String):Void {
+ var window = "_blank";
+
+ #if flash
+ flash.Lib.getURL(new flash.net.URLRequest(url), window);
+ #elseif (js && html5)
+ js.Browser.window.open(url, window);
+ #elseif desktop
+
+ #if (sys && windows)
+ Sys.command("start", ["", url]);
+ #elseif mac
+ Sys.command("/usr/bin/open", [url]);
+ #elseif linux
+ Sys.command("/usr/bin/xdg-open", [path, "&"]);
+ #end
+
+ #elseif android
+ JNI.createStaticMethod
+ ( "org/haxe/lime/GameActivity"
+ , "openURL"
+ , "(Ljava/lang/String;Ljava/lang/String;)V"
+ ) (url, window);
+ #end
+ }
+
+ /**
+ * Call this once the passport link is established and it will start checking for session connect periodically
+ */
+ public function onPassportUrlOpen():Void {
+
+ if (_passportCallback != null)
+ _passportCallback();
+
+ _passportCallback = null;
+ }
+
+ function checkSession(response:Response, onSucceess:Void->Void, onCancel:Void->Void):Void {
+
+ if (response != null) {
+
+ if (!response.success || !response.result.success) {
+
+ log("login cancelled via passport");
+
+ endLoginAndCall(onCancel);
+ return;
+ }
+
+ _session.parse(response.result.data.session);
+ }
+
+ if (_session.status == SessionStatus.USER_LOADED) {
+
+ loggedIn = true;
+ endLoginAndCall(onSucceess);
+ onLogin.dispatch();
+
+ } else if (_session.status == SessionStatus.REQUEST_LOGIN){
+
+ var call = calls.app.checkSession()
+ .addDataHandler(checkSession.bind(_, onSucceess, onCancel));
+
+ // Wait 3 seconds and try again
+ timer(3.0,
+ function():Void {
+
+ // Check if cancelLoginRequest was called
+ if (!_loginCancelled)
+ call.send();
+ else {
+
+ log("login cancelled via cancelLoginRequest");
+ endLoginAndCall(onCancel);
+ }
+ }
+ );
+
+ } else
+ // The user cancelled the passport
+ endLoginAndCall(onCancel);
+ }
+
+ public function cancelLoginRequest():Void {
+
+ if (attemptingLogin)
+ _loginCancelled = true;
+ }
+
+ function endLoginAndCall(callback:Void->Void):Void {
+
+ attemptingLogin = false;
+ _loginCancelled = false;
+
+ if (callback != null)
+ callback();
+ }
+
+ public function logOut(onComplete:Void->Void = null):Void {
+
+ var call = calls.app.endSession()
+ .addSuccessHandler(onLogOutSuccessful);
+
+ if (onComplete != null)
+ call.addSuccessHandler(onComplete);
+
+ call.addSuccessHandler(onLogOut.dispatch)
+ .send();
+ }
+
+ function onLogOutSuccessful():Void {
+
+ _session.expire();
+ sessionId = null;
+ loggedIn = false;
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // MEDALS
+ // -------------------------------------------------------------------------------------------
+
+ public function requestMedals(onSuccess:Void->Void = null, onFail:Error->Void = null):Void {
+
+ var call = calls.medal.getList()
+ .addDataHandler(onMedalsReceived);
+
+ if (onSuccess != null)
+ call.addSuccessHandler(onSuccess);
+
+ if (onFail != null)
+ call.addErrorHandler(onFail);
+
+ call.send();
+ }
+
+ function onMedalsReceived(response:Response):Void {
+
+ if (!response.success || !response.result.success)
+ return;
+
+ var idList:Array = new Array();
+
+ if (medals == null) {
+
+ medals = new IntMap();
+
+ for (medalData in response.result.data.medals) {
+
+ var medal = new Medal(this, medalData);
+ medals.set(medal.id, medal);
+ idList.push(medal.id);
+ }
+ } else {
+
+ for (medalData in response.result.data.medals) {
+
+ medals.get(medalData.id).parse(medalData);
+ idList.push(medalData.id);
+ }
+ }
+
+ logVerbose('${response.result.data.medals.length} Medals received [${idList.join(", ")}]');
+
+ onMedalsLoaded.dispatch();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // SCOREBOARDS
+ // -------------------------------------------------------------------------------------------
+
+ public function requestScoreBoards(onSuccess:Void->Void = null, onFail:Error->Void = null):Void {
+
+ if (scoreBoards != null) {
+
+ log("aborting scoreboard request, all scoreboards are loaded");
+
+ if (onSuccess != null)
+ onSuccess();
+
+ return;
+ }
+
+ var call = calls.scoreBoard.getBoards()
+ .addDataHandler(onBoardsReceived);
+
+ if (onSuccess != null)
+ call.addSuccessHandler(onSuccess);
+
+ if (onFail != null)
+ call.addErrorHandler(onFail);
+
+ call.send();
+ }
+
+ function onBoardsReceived(response:Response):Void {
+
+ if (!response.success || !response.result.success)
+ return;
+
+ var idList:Array = new Array();
+
+ if (scoreBoards == null) {
+
+ scoreBoards = new IntMap();
+
+ for (boardData in response.result.data.scoreboards) {
+
+ var board = new ScoreBoard(this, boardData);
+ scoreBoards.set(board.id, board);
+ idList.push(board.id);
+ }
+ }
+
+ logVerbose('${response.result.data.scoreboards.length} ScoreBoards received [${idList.join(", ")}]');
+
+ onScoreBoardsLoaded.dispatch();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // HELPERS
+ // -------------------------------------------------------------------------------------------
+
+ function timer(delay:Float, callback:Void->Void):Void {
+
+ var timer = new Timer(Std.int(delay * 1000));
+ timer.run = function func():Void {
+
+ timer.stop();
+ callback();
+ }
+ }
+
+ static var urlParser:EReg = ~/^(?:http[s]?:\/\/)?([^:\/\s]+)(:[0-9]+)?((?:\/\w+)*\/)([\w\-\.]+[^#?\s]+)([^#\s]*)?(#[\w\-]+)?$/i;//TODO:trim
+ /** Used to get the current web host of your game. */
+ static public function getHost():String {
+
+ var url = NGLite.getUrl();
+
+ if (url == null || url == "")
+ return "";
+
+ if (url.indexOf("file") == 0)
+ return "";
+
+ if (urlParser.match(url))
+ return urlParser.matched(1);
+
+ return "";
+ }
+}
+#end
\ No newline at end of file
diff --git a/source/io/newgrounds/NGLite.hx b/source/io/newgrounds/NGLite.hx
new file mode 100644
index 000000000..0c9d5bdf5
--- /dev/null
+++ b/source/io/newgrounds/NGLite.hx
@@ -0,0 +1,287 @@
+package io.newgrounds;
+
+import haxe.crypto.Base64;
+import haxe.io.Bytes;
+import haxe.PosInfos;
+
+import io.newgrounds.Call.ICallable;
+import io.newgrounds.components.ComponentList;
+import io.newgrounds.crypto.EncryptionFormat;
+import io.newgrounds.crypto.Cipher;
+import io.newgrounds.crypto.Rc4;
+import io.newgrounds.objects.Error;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result.ResultBase;
+import io.newgrounds.objects.events.Result.SessionResult;
+import io.newgrounds.utils.Dispatcher;
+
+#if !(html5 || flash || desktop || neko)
+ #error "Target not supported, use: Flash, JS/HTML5, cpp or maybe neko";
+#end
+
+/**
+ * The barebones NG.io API. Allows API calls with code completion
+ * and retrieves server data via strongly typed Objects
+ *
+ * Contains many things ripped from MSGhero's repo
+ * - https://github.com/MSGhero/NG.hx
+ *
+ * @author GeoKureli
+ */
+class NGLite {
+
+ static public var core(default, null):NGLite;
+ static public var onCoreReady(default, null):Dispatcher = new Dispatcher();
+
+ /** Enables verbose logging */
+ public var verbose:Bool;
+ public var debug:Bool;
+ /** The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project. */
+ public var appId(default, null):String;
+ /** The name of the host the game is being played on */
+ public var host:String;
+
+ @:isVar
+ public var sessionId(default, set):String;
+ function set_sessionId(value:String):String {
+
+ return this.sessionId = value == "" ? null : value;
+ }
+
+ /** Components used to call the NG server directly */
+ public var calls(default, null):ComponentList;
+
+ /**
+ * Converts an object to an encrypted string that can be decrypted by the server.
+ * Set your preffered encrypter here,
+ * or just call setDefaultEcryptionHandler with your app's encryption settings
+ **/
+ public var encryptionHandler:String->String;
+
+ /**
+ * Iniitializes the API, call before utilizing any other component
+ * @param appId The unique ID of your app as found in the 'API Tools' tab of your Newgrounds.com project.
+ * @param sessionId A unique session id used to identify the active user.
+ **/
+ public function new(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void) {
+
+ this.appId = appId;
+ this.sessionId = sessionId;
+
+ calls = new ComponentList(this);
+
+ if (this.sessionId != null) {
+
+ calls.app.checkSession()
+ .addDataHandler(checkInitialSession.bind(onSessionFail))
+ .addErrorHandler(initialSessionFail.bind(onSessionFail))
+ .send();
+ }
+ }
+
+ function checkInitialSession(onFail:Error->Void, response:Response):Void {
+
+ if (!response.success || !response.result.success || response.result.data.session.expired) {
+
+ initialSessionFail(onFail, response.success ? response.result.error : response.error);
+ }
+ }
+
+ function initialSessionFail(onFail:Error->Void, error:Error):Void {
+
+ sessionId = null;
+
+ if (onFail != null)
+ onFail(error);
+ }
+
+ /**
+ * Creates NG.core, the heart and soul of the API. This is not the only way to create an instance,
+ * nor is NG a forced singleton, but it's the only way to set the static NG.core.
+ **/
+ static public function create(appId = "test", sessionId:String = null, ?onSessionFail:Error->Void):Void {
+
+ core = new NGLite(appId, sessionId, onSessionFail);
+
+ onCoreReady.dispatch();
+ }
+
+ /**
+ * Creates NG.core, and tries to create a session. This is not the only way to create an instance,
+ * nor is NG a forced singleton, but it's the only way to set the static NG.core.
+ **/
+ static public function createAndCheckSession
+ ( appId = "test"
+ , backupSession:String = null
+ , ?onSessionFail:Error->Void
+ ):Void {
+
+ var session = getSessionId();
+ if (session == null)
+ session = backupSession;
+
+ create(appId, session, onSessionFail);
+ }
+
+ inline static public function getUrl():String {
+
+ #if html5
+ return js.Browser.document.location.href;
+ #elseif flash
+ return flash.Lib.current.stage.loaderInfo != null
+ ? flash.Lib.current.stage.loaderInfo.url
+ : null;
+ #else
+ return null;
+ #end
+ }
+
+ static public function getSessionId():String {
+
+ #if html5
+
+ var url = getUrl();
+
+ // Check for URL params
+ var index = url.indexOf("?");
+ if (index != -1) {
+
+ // Check for session ID in params
+ for (param in url.substr(index + 1).split("&")) {
+
+ index = param.indexOf("=");
+ if (index != -1 && param.substr(0, index) == "ngio_session_id")
+ return param.substr(index + 1);
+ }
+ }
+
+ #elseif flash
+
+ if (flash.Lib.current.stage.loaderInfo != null
+ && Reflect.hasField(flash.Lib.current.stage.loaderInfo.parameters, "ngio_session_id"))
+ return Reflect.field(flash.Lib.current.stage.loaderInfo.parameters, "ngio_session_id");
+
+ #end
+
+ return null;
+
+ // --- EXAMPLE LOADER PARAMS
+ //{ "1517703669" : ""
+ //, "ng_username" : "GeoKureli"
+ //, "NewgroundsAPI_SessionID" : "F1LusbG6P8Qf91w7zeUE37c1752563f366688ac6153996d12eeb111a2f60w2xn"
+ //, "NewgroundsAPI_PublisherID" : 1
+ //, "NewgroundsAPI_UserID" : 488329
+ //, "NewgroundsAPI_SandboxID" : "5a76520e4ae1e"
+ //, "ngio_session_id" : "0c6c4e02567a5116734ba1a0cd841dac28a42e79302290"
+ //, "NewgroundsAPI_UserName" : "GeoKureli"
+ //}
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // CALLS
+ // -------------------------------------------------------------------------------------------
+
+ var _queuedCalls:Array = new Array();
+ var _pendingCalls:Array = new Array();
+
+ @:allow(io.newgrounds.Call)
+ @:generic
+ function queueCall(call:Call):Void {
+
+ logVerbose('queued - ${call.component}');
+
+ _queuedCalls.push(call);
+ checkQueue();
+ }
+
+ @:allow(io.newgrounds.Call)
+ @:generic
+ function markCallPending(call:Call):Void {
+
+ _pendingCalls.push(call);
+
+ call.addDataHandler(function (_):Void { onCallComplete(call); });
+ call.addErrorHandler(function (_):Void { onCallComplete(call); });
+ }
+
+ function onCallComplete(call:ICallable):Void {
+
+ _pendingCalls.remove(call);
+ checkQueue();
+ }
+
+ function checkQueue():Void {
+
+ if (_pendingCalls.length == 0 && _queuedCalls.length > 0)
+ _queuedCalls.shift().send();
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // LOGGING / ERRORS
+ // -------------------------------------------------------------------------------------------
+
+ /** Called internally, set this to your preferred logging method */
+ dynamic public function log(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow
+
+ haxe.Log.trace('[Newgrounds API] :: ${any}', pos);
+ }
+
+ /** used internally, logs if verbose is true */
+ inline public function logVerbose(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow
+
+ if (verbose)
+ log(any, pos);
+ }
+
+ /** Used internally. Logs by default, set this to your preferred error handling method */
+ dynamic public function logError(any:Dynamic, ?pos:PosInfos):Void {//TODO: limit access via @:allow
+
+ log('Error: $any', pos);
+ }
+
+ /** used internally, calls log error if the condition is false. EX: if (assert(data != null, "null data")) */
+ inline public function assert(condition:Bool, msg:Dynamic, ?pos:PosInfos):Bool {//TODO: limit access via @:allow
+ if (!condition)
+ logError(msg, pos);
+
+ return condition;
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // ENCRYPTION
+ // -------------------------------------------------------------------------------------------
+
+ /** Sets */
+ public function initEncryption
+ ( key :String
+ , cipher:Cipher = Cipher.RC4
+ , format:EncryptionFormat = EncryptionFormat.BASE_64
+ ):Void {
+
+ if (cipher == Cipher.NONE)
+ encryptionHandler = null;
+ else if (cipher == Cipher.RC4)
+ encryptionHandler = encryptRc4.bind(key, format);
+ else
+ throw "aes not yet implemented";
+ }
+
+ function encryptRc4(key:String, format:EncryptionFormat, data:String):String {
+
+ if (format == EncryptionFormat.HEX)
+ throw "hex format not yet implemented";
+
+ var keyBytes:Bytes;
+ if (format == EncryptionFormat.BASE_64)
+ keyBytes = Base64.decode(key);
+ else
+ keyBytes = null;//TODO
+
+ var dataBytes = new Rc4(keyBytes).crypt(Bytes.ofString(data));
+
+ if (format == EncryptionFormat.BASE_64)
+ return Base64.encode(dataBytes);
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/AppComponent.hx b/source/io/newgrounds/components/AppComponent.hx
new file mode 100644
index 000000000..1ea5b8b16
--- /dev/null
+++ b/source/io/newgrounds/components/AppComponent.hx
@@ -0,0 +1,44 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.objects.events.Result.SessionResult;
+import io.newgrounds.NGLite;
+
+class AppComponent extends Component {
+
+ public function new (core:NGLite) { super(core); }
+
+ public function startSession(force:Bool = false):Call {
+
+ return new Call(_core, "App.startSession")
+ .addComponentParameter("force", force, false);
+ }
+
+ public function checkSession():Call {
+
+ return new Call(_core, "App.checkSession", true);
+ }
+
+ public function endSession():Call {
+
+ return new Call(_core, "App.endSession", true);
+ }
+
+ public function getCurrentVersion(version:String):Call {
+
+ return new Call(_core, "App.getCurrentVersion")
+ .addComponentParameter("version", version);
+ }
+
+ public function getHostLicense():Call {
+
+ return new Call(_core, "App.getHostLicense")
+ .addComponentParameter("host", _core.host);
+ }
+
+ public function logView():Call {
+
+ return new Call(_core, "App.logView")
+ .addComponentParameter("host", _core.host);
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/Component.hx b/source/io/newgrounds/components/Component.hx
new file mode 100644
index 000000000..3c588ff19
--- /dev/null
+++ b/source/io/newgrounds/components/Component.hx
@@ -0,0 +1,13 @@
+package io.newgrounds.components;
+
+import io.newgrounds.NGLite;
+
+class Component {
+
+ var _core:NGLite;
+
+ public function new(core:NGLite) {
+
+ this._core = core;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/ComponentList.hx b/source/io/newgrounds/components/ComponentList.hx
new file mode 100644
index 000000000..315abf4ed
--- /dev/null
+++ b/source/io/newgrounds/components/ComponentList.hx
@@ -0,0 +1,25 @@
+package io.newgrounds.components;
+class ComponentList {
+
+ var _core:NGLite;
+
+ // --- COMPONENTS
+ public var medal : MedalComponent;
+ public var app : AppComponent;
+ public var event : EventComponent;
+ public var scoreBoard: ScoreBoardComponent;
+ public var loader : LoaderComponent;
+ public var gateway : GatewayComponent;
+
+ public function new(core:NGLite) {
+
+ _core = core;
+
+ medal = new MedalComponent (_core);
+ app = new AppComponent (_core);
+ event = new EventComponent (_core);
+ scoreBoard = new ScoreBoardComponent(_core);
+ loader = new LoaderComponent (_core);
+ gateway = new GatewayComponent (_core);
+ }
+}
diff --git a/source/io/newgrounds/components/EventComponent.hx b/source/io/newgrounds/components/EventComponent.hx
new file mode 100644
index 000000000..2e631773e
--- /dev/null
+++ b/source/io/newgrounds/components/EventComponent.hx
@@ -0,0 +1,16 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.events.Result.LogEventResult;
+import io.newgrounds.NGLite;
+
+class EventComponent extends Component {
+
+ public function new (core:NGLite){ super(core); }
+
+ public function logEvent(eventName:String):Call {
+
+ return new Call(_core, "Event.logEvent")
+ .addComponentParameter("event_name", eventName)
+ .addComponentParameter("host", _core.host);
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/GatewayComponent.hx b/source/io/newgrounds/components/GatewayComponent.hx
new file mode 100644
index 000000000..4f0ece617
--- /dev/null
+++ b/source/io/newgrounds/components/GatewayComponent.hx
@@ -0,0 +1,25 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.NGLite;
+
+class GatewayComponent extends Component {
+
+ public function new (core:NGLite){ super(core); }
+
+ public function getDatetime():Call {
+
+ return new Call(_core, "Gateway.getDatetime");
+ }
+
+ public function getVersion():Call {
+
+ return new Call(_core, "Gateway.getVersion");
+ }
+
+ public function ping():Call {
+
+ return new Call(_core, "Gateway.ping");
+ }
+
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/LoaderComponent.hx b/source/io/newgrounds/components/LoaderComponent.hx
new file mode 100644
index 000000000..717cc2eb2
--- /dev/null
+++ b/source/io/newgrounds/components/LoaderComponent.hx
@@ -0,0 +1,44 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.NGLite;
+
+class LoaderComponent extends Component {
+
+ public function new (core:NGLite){ super(core); }
+
+ public function loadAuthorUrl(redirect:Bool = false):Call {
+
+ return new Call(_core, "Loader.loadAuthorUrl")
+ .addComponentParameter("host", _core.host)
+ .addComponentParameter("redirect", redirect, true);
+ }
+
+ public function loadMoreGames(redirect:Bool = false):Call {
+
+ return new Call(_core, "Loader.loadMoreGames")
+ .addComponentParameter("host", _core.host)
+ .addComponentParameter("redirect", redirect, true);
+ }
+
+ public function loadNewgrounds(redirect:Bool = false):Call {
+
+ return new Call(_core, "Loader.loadNewgrounds")
+ .addComponentParameter("host", _core.host)
+ .addComponentParameter("redirect", redirect, true);
+ }
+
+ public function loadOfficialUrl(redirect:Bool = false):Call {
+
+ return new Call(_core, "Loader.loadOfficialUrl")
+ .addComponentParameter("host", _core.host)
+ .addComponentParameter("redirect", redirect, true);
+ }
+
+ public function loadReferral(redirect:Bool = false):Call {
+
+ return new Call(_core, "Loader.loadReferral")
+ .addComponentParameter("host", _core.host)
+ .addComponentParameter("redirect", redirect, true);
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/MedalComponent.hx b/source/io/newgrounds/components/MedalComponent.hx
new file mode 100644
index 000000000..7e56621c4
--- /dev/null
+++ b/source/io/newgrounds/components/MedalComponent.hx
@@ -0,0 +1,21 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.Call;
+import io.newgrounds.NGLite;
+
+class MedalComponent extends Component {
+
+ public function new(core:NGLite):Void { super(core); }
+
+ public function unlock(id:Int):Call {
+
+ return new Call(_core, "Medal.unlock", true, true)
+ .addComponentParameter("id", id);
+ }
+
+ public function getList():Call {
+
+ return new Call(_core, "Medal.getList");
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/components/ScoreBoardComponent.hx b/source/io/newgrounds/components/ScoreBoardComponent.hx
new file mode 100644
index 000000000..7417e67c6
--- /dev/null
+++ b/source/io/newgrounds/components/ScoreBoardComponent.hx
@@ -0,0 +1,114 @@
+package io.newgrounds.components;
+
+import io.newgrounds.objects.User;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.objects.events.Result.ScoreBoardResult;
+import io.newgrounds.objects.events.Result.ScoreResult;
+import io.newgrounds.NGLite;
+import io.newgrounds.objects.ScoreBoard;
+
+import haxe.ds.IntMap;
+
+class ScoreBoardComponent extends Component {
+
+ public var allById:IntMap;
+
+ public function new (core:NGLite){ super(core); }
+
+ // -------------------------------------------------------------------------------------------
+ // GET SCORES
+ // -------------------------------------------------------------------------------------------
+
+ public function getBoards():Call {
+
+ return new Call(_core, "ScoreBoard.getBoards");
+ }
+
+ /*function onBoardsReceive(response:Response):Void {
+
+ if (!response.result.success)
+ return;
+
+ allById = new IntMap();
+
+ for (boardData in response.result.scoreboards)
+ createBoard(boardData);
+
+ _core.log('${response.result.scoreboards.length} ScoreBoards loaded');
+ }*/
+
+ // -------------------------------------------------------------------------------------------
+ // GET SCORES
+ // -------------------------------------------------------------------------------------------
+
+ public function getScores
+ ( id :Int
+ , limit :Int = 10
+ , skip :Int = 0
+ , period:Period = Period.DAY
+ , social:Bool = false
+ , tag :String = null
+ , user :Dynamic = null
+ ):Call {
+
+ if (user != null && !Std.is(user, String) && !Std.is(user, Int))
+ user = user.id;
+
+ return new Call(_core, "ScoreBoard.getScores")
+ .addComponentParameter("id" , id )
+ .addComponentParameter("limit" , limit , 10)
+ .addComponentParameter("skip" , skip , 0)
+ .addComponentParameter("period", period, Period.DAY)
+ .addComponentParameter("social", social, false)
+ .addComponentParameter("tag" , tag , null)
+ .addComponentParameter("user" , user , null);
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // POST SCORE
+ // -------------------------------------------------------------------------------------------
+
+ public function postScore(id:Int, value:Int, tag:String = null):Call {
+
+ return new Call(_core, "ScoreBoard.postScore", true, true)
+ .addComponentParameter("id" , id)
+ .addComponentParameter("value", value)
+ .addComponentParameter("tag" , tag , null);
+ }
+
+ /*function onScorePosted(response:Response):Void {
+
+ if (!response.result.success)
+ return;
+
+ allById = new IntMap();
+
+ //createBoard(data.data.scoreBoard).parseScores(data.data.scores);
+ }*/
+
+ inline function createBoard(data:Dynamic):ScoreBoard {
+
+ var board = new ScoreBoard(_core, data);
+ _core.logVerbose('created $board');
+
+ allById.set(board.id, board);
+
+ return board;
+ }
+}
+
+@:enum
+abstract Period(String) to String from String{
+
+ /** Indicates scores are from the current day. */
+ var DAY = "D";
+ /** Indicates scores are from the current week. */
+ var WEEK = "W";
+ /** Indicates scores are from the current month. */
+ var MONTH = "M";
+ /** Indicates scores are from the current year. */
+ var YEAR = "Y";
+ /** Indicates scores are from all-time. */
+ var ALL = "A";
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/crypto/Cipher.hx b/source/io/newgrounds/crypto/Cipher.hx
new file mode 100644
index 000000000..2f4c00753
--- /dev/null
+++ b/source/io/newgrounds/crypto/Cipher.hx
@@ -0,0 +1,8 @@
+package io.newgrounds.crypto;
+
+@:enum
+abstract Cipher(String) to String{
+ var NONE = "none";
+ var AES_128 = "aes128";
+ var RC4 = "rc4";
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/crypto/EncryptionFormat.hx b/source/io/newgrounds/crypto/EncryptionFormat.hx
new file mode 100644
index 000000000..6e8f17fd6
--- /dev/null
+++ b/source/io/newgrounds/crypto/EncryptionFormat.hx
@@ -0,0 +1,7 @@
+package io.newgrounds.crypto;
+
+@:enum
+abstract EncryptionFormat(String) to String {
+ var BASE_64 = "base64";
+ var HEX = "hex";
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/crypto/Rc4.hx b/source/io/newgrounds/crypto/Rc4.hx
new file mode 100644
index 000000000..54dafa74b
--- /dev/null
+++ b/source/io/newgrounds/crypto/Rc4.hx
@@ -0,0 +1,68 @@
+package io.newgrounds.crypto;
+
+import haxe.io.Bytes;
+
+/**
+ * The following was straight-up ganked from https://github.com/iskolbin/rc4hx
+ *
+ * You da real MVP iskolbin...
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 iskolbin
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+**/
+class Rc4 {
+ var perm = Bytes.alloc( 256 );
+ var index1: Int = 0;
+ var index2: Int = 0;
+
+ public function new( key: Bytes ) {
+ for ( i in 0...256 ) {
+ perm.set( i, i );
+ }
+
+ var j: Int = 0;
+ for ( i in 0...256 ) {
+ j = ( j + perm.get( i ) + key.get( i % key.length )) % 256;
+ swap( i, j );
+ }
+ }
+
+ inline function swap( i: Int, j: Int ): Void {
+ var temp = perm.get( i );
+ perm.set( i, perm.get( j ));
+ perm.set( j, temp );
+ }
+
+ public function crypt( input: Bytes ): Bytes {
+ var output = Bytes.alloc( input.length );
+
+ for ( i in 0...input.length ) {
+ index1 = ( index1 + 1 ) % 256;
+ index2 = ( index2 + perm.get( index1 )) % 256;
+ swap( index1, index2 );
+ var j = ( perm.get( index1 ) + perm.get( index2 )) % 256;
+ output.set( i, input.get( i ) ^ perm.get( j ));
+ }
+
+ return output;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/Error.hx b/source/io/newgrounds/objects/Error.hx
new file mode 100644
index 000000000..6e82e4e2e
--- /dev/null
+++ b/source/io/newgrounds/objects/Error.hx
@@ -0,0 +1,20 @@
+package io.newgrounds.objects;
+class Error {
+
+ public var code(default, null):Int;
+ public var message(default, null):String;
+
+ public function new (message:String, code:Int = 0) {
+
+ this.message = message;
+ this.code = code;
+ }
+
+ public function toString():String {
+
+ if (code > 0)
+ return '#$code - $message';
+
+ return message;
+ }
+}
diff --git a/source/io/newgrounds/objects/Medal.hx b/source/io/newgrounds/objects/Medal.hx
new file mode 100644
index 000000000..3cecc7a6b
--- /dev/null
+++ b/source/io/newgrounds/objects/Medal.hx
@@ -0,0 +1,118 @@
+package io.newgrounds.objects;
+
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result.MedalUnlockResult;
+import io.newgrounds.utils.Dispatcher;
+import io.newgrounds.NGLite;
+
+class Medal extends Object {
+
+ inline static public var EASY :Int = 1;
+ inline static public var MODERATE :Int = 2;
+ inline static public var CHALLENGING:Int = 3;
+ inline static public var DIFFICULT :Int = 4;
+ inline static public var BRUTAL :Int = 5;
+
+ static var difficultyNames:Array =
+ [ "Easy"
+ , "Moderate"
+ , "Challenging"
+ , "Difficult"
+ , "Brutal"
+ ];
+ // --- FROM SERVER
+ public var id (default, null):Int;
+ public var name (default, null):String;
+ public var description(default, null):String;
+ public var icon (default, null):String;
+ public var value (default, null):Int;
+ public var difficulty (default, null):Int;
+ public var secret (default, null):Bool;
+ public var unlocked (default, null):Bool;
+ // --- HELPERS
+ public var difficultyName(get, never):String;
+
+ public var onUnlock:Dispatcher;
+
+ public function new(core:NGLite, data:Dynamic = null):Void {
+
+ onUnlock = new Dispatcher();
+
+ super(core, data);
+ }
+
+ @:allow(io.newgrounds.NG)
+ override function parse(data:Dynamic):Void {
+
+ var wasLocked = !unlocked;
+
+ id = data.id;
+ name = data.name;
+ description = data.description;
+ icon = data.icon;
+ value = data.value;
+ difficulty = data.difficulty;
+ secret = data.secret == 1;
+ unlocked = data.unlocked;
+
+ super.parse(data);
+
+ if (wasLocked && unlocked)
+ onUnlock.dispatch();
+
+ }
+
+ public function sendUnlock():Void {
+
+ if (_core.sessionId == null) {
+ // --- Unlock regardless, show medal popup to encourage NG signup
+ unlocked = true;
+ onUnlock.dispatch();
+ //TODO: save unlock in local save
+ }
+
+ _core.calls.medal.unlock(id)
+ .addDataHandler(onUnlockResponse)
+ .send();
+ }
+
+ function onUnlockResponse(response:Response):Void {
+
+ if (response.success && response.result.success) {
+
+ parse(response.result.data.medal);
+
+ // --- Unlock response doesn't include unlock=true, so parse won't change it.
+ if (!unlocked) {
+
+ unlocked = true;
+ onUnlock.dispatch();
+ }
+ }
+ }
+
+ /** Locks the medal on the client and sends an unlock request, Server responds the same either way. */
+ public function sendDebugUnlock():Void {
+
+ if (NG.core.sessionId == null) {
+
+ onUnlock.dispatch();
+
+ } else {
+
+ unlocked = false;
+
+ sendUnlock();
+ }
+ }
+
+ public function get_difficultyName():String {
+
+ return difficultyNames[difficulty - 1];
+ }
+
+ public function toString():String {
+
+ return 'Medal: $id@$name (${unlocked ? "unlocked" : "locked"}, $value pts, $difficultyName).';
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/Object.hx b/source/io/newgrounds/objects/Object.hx
new file mode 100644
index 000000000..32abd0fe5
--- /dev/null
+++ b/source/io/newgrounds/objects/Object.hx
@@ -0,0 +1,33 @@
+package io.newgrounds.objects;
+
+import io.newgrounds.utils.Dispatcher;
+import io.newgrounds.NGLite;
+
+class Object {
+
+ var _core:NGLite;
+
+ public var onUpdate(default, null):Dispatcher;
+
+ public function new(core:NGLite, data:Dynamic = null) {
+
+ this._core = core;
+
+ onUpdate = new Dispatcher();
+
+ if (data != null)
+ parse(data);
+ }
+
+ @:allow(io.newgrounds.NGLite)
+ function parse(data:Dynamic):Void {
+
+ onUpdate.dispatch();
+ }
+
+
+ public function destroy():Void {
+
+ _core = null;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/Score.hx b/source/io/newgrounds/objects/Score.hx
new file mode 100644
index 000000000..0eb698267
--- /dev/null
+++ b/source/io/newgrounds/objects/Score.hx
@@ -0,0 +1,17 @@
+package io.newgrounds.objects;
+
+/** We don't want to serialize scores since there's a bajillion of them. */
+typedef Score = {
+
+ /** The value value in the format selected in your scoreboard settings. */
+ var formatted_value:String;
+
+ /** The tag attached to this value (if any). */
+ var tag:String;
+
+ /** The user who earned value. If this property is absent, the value belongs to the active user. */
+ var user:User;
+
+ /** The integer value of the value. */
+ var value:Int;
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/ScoreBoard.hx b/source/io/newgrounds/objects/ScoreBoard.hx
new file mode 100644
index 000000000..1859b0808
--- /dev/null
+++ b/source/io/newgrounds/objects/ScoreBoard.hx
@@ -0,0 +1,76 @@
+package io.newgrounds.objects;
+
+import io.newgrounds.components.ScoreBoardComponent.Period;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.objects.events.Result;
+import io.newgrounds.objects.events.Result.ScoreResult;
+import io.newgrounds.NGLite;
+
+class ScoreBoard extends Object {
+
+ public var scores(default, null):Array;
+
+ /** The numeric ID of the scoreboard.*/
+ public var id(default, null):Int;
+
+ /** The name of the scoreboard. */
+ public var name(default, null):String;
+
+ public function new(core:NGLite, data:Dynamic):Void {super(core, data); }
+
+ override function parse(data:Dynamic):Void {
+
+ id = data.id;
+ name = data.name;
+
+ super.parse(data);
+ }
+
+ /**
+ * Fetches score data from the server, this removes all of the existing scores cached
+ *
+ * We don't unify the old and new scores because a user's rank or score may change between requests
+ */
+ public function requestScores
+ ( limit :Int = 10
+ , skip :Int = 0
+ , period:Period = Period.ALL
+ , social:Bool = false
+ , tag :String = null
+ , user :Dynamic = null
+ ):Void {
+
+ _core.calls.scoreBoard.getScores(id, limit, skip, period, social, tag, user)
+ .addDataHandler(onScoresReceived)
+ .send();
+ }
+
+ function onScoresReceived(response:Response):Void {
+
+ if (!response.success || !response.result.success)
+ return;
+
+ scores = response.result.data.scores;
+ _core.logVerbose('received ${scores.length} scores');
+
+ onUpdate.dispatch();
+ }
+
+ public function postScore(value :Int, tag:String = null):Void {
+
+ _core.calls.scoreBoard.postScore(id, value, tag)
+ .addDataHandler(onScorePosted)
+ .send();
+ }
+
+ function onScorePosted(response:Response):Void {
+
+
+ }
+
+ public function toString():String {
+
+ return 'ScoreBoard: $id@$name';
+ }
+
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/Session.hx b/source/io/newgrounds/objects/Session.hx
new file mode 100644
index 000000000..887df7927
--- /dev/null
+++ b/source/io/newgrounds/objects/Session.hx
@@ -0,0 +1,65 @@
+package io.newgrounds.objects;
+
+class Session extends Object {
+
+ /** If true, the session_id is expired. Use App.startSession to get a new one.*/
+ public var expired(default, null):Bool;
+
+ /** A unique session identifier */
+ public var id(default, null):String;
+
+ /** If the session has no associated user but is not expired, this property will provide a URL that can be used to sign the user in. */
+ public var passportUrl(default, null):String;
+
+ /** If true, the user would like you to remember their session id. */
+ public var remember(default, null):Bool;
+
+ /** If the user has not signed in, or granted access to your app, this will be null */
+ public var user(default, null):User;
+
+ //TODO:desciption
+ public var status(get, never):SessionStatus;
+
+ public function new(core:NGLite, data:Dynamic = null) { super(core, data); }
+
+ override public function parse(data:Dynamic):Void {
+
+ id = data.id;
+ expired = data.expired;
+ passportUrl = data.passport_url;
+ remember = data.remember;
+
+ // --- KEEP THE SAME INSTANCE
+ if (user == null)
+ user = data.user;
+ // TODO?: update original user instance with new data. (probly not)
+
+ super.parse(data);
+ }
+
+ public function get_status():SessionStatus {
+
+ if (expired || id == null || id == "")
+ return SessionStatus.SESSION_EXPIRED;
+
+ if (user != null && user.name != null && user.name != "")
+ return SessionStatus.USER_LOADED;
+
+ return SessionStatus.REQUEST_LOGIN;
+ }
+
+ public function expire():Void {
+
+ expired = true;
+ id = null;
+ user = null;
+ }
+}
+
+@:enum
+abstract SessionStatus(String) {
+
+ var SESSION_EXPIRED = "session-expired";
+ var REQUEST_LOGIN = "request-login";
+ var USER_LOADED = "user-loaded";
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/objects/User.hx b/source/io/newgrounds/objects/User.hx
new file mode 100644
index 000000000..1ff7ce00c
--- /dev/null
+++ b/source/io/newgrounds/objects/User.hx
@@ -0,0 +1,19 @@
+package io.newgrounds.objects;
+
+typedef User = {
+
+ /** The user's icon images. */
+ var icons:UserIcons;
+
+ /** The user's numeric ID. */
+ var id:Int;
+
+ /** The user's textual name. */
+ var name:String;
+
+ /** Returns true if the user has a Newgrounds Supporter upgrade. */
+ var supporter:Bool;
+
+ /** The user's NG profile url. */
+ var url:String;
+}
diff --git a/source/io/newgrounds/objects/UserIcons.hx b/source/io/newgrounds/objects/UserIcons.hx
new file mode 100644
index 000000000..b5e56b22a
--- /dev/null
+++ b/source/io/newgrounds/objects/UserIcons.hx
@@ -0,0 +1,14 @@
+package io.newgrounds.objects;
+
+typedef UserIcons = {
+
+ /**The URL of the user's large icon. */
+ var large:String;
+
+ /** The URL of the user's medium icon. */
+ var medium:String;
+
+ /** The URL of the user's small icon. */
+ var small:String;
+}
+
diff --git a/source/io/newgrounds/objects/events/Response.hx b/source/io/newgrounds/objects/events/Response.hx
new file mode 100644
index 000000000..107dc2be3
--- /dev/null
+++ b/source/io/newgrounds/objects/events/Response.hx
@@ -0,0 +1,43 @@
+package io.newgrounds.objects.events;
+
+import io.newgrounds.objects.events.Result.ResultBase;
+import haxe.Json;
+import io.newgrounds.objects.Error;
+
+typedef DebugResponse = {
+
+ var exec_time:Int;
+ var input:Dynamic;
+}
+
+class Response {
+
+ public var success(default, null):Bool;
+ public var error(default, null):Error;
+ public var debug(default, null):DebugResponse;
+ public var result(default, null):Result;
+
+ public function new (core:NGLite, reply:String) {
+
+ var data:Dynamic;
+
+ try {
+ data = Json.parse(reply);
+
+ } catch (e:Dynamic) {
+
+ data = Json.parse('{"success":false,"error":{"message":"${Std.string(reply)}","code":0}}');
+ }
+
+ success = data.success;
+ debug = data.debug;
+
+ if (!success) {
+ error = new Error(data.error.message, data.error.code);
+ core.logError('Call unseccessful: $error');
+ return;
+ }
+
+ result = new Result(core, data.result);
+ }
+}
diff --git a/source/io/newgrounds/objects/events/Result.hx b/source/io/newgrounds/objects/events/Result.hx
new file mode 100644
index 000000000..eaf872616
--- /dev/null
+++ b/source/io/newgrounds/objects/events/Result.hx
@@ -0,0 +1,109 @@
+package io.newgrounds.objects.events;
+
+class Result {
+
+ public var echo(default, null):String;
+ public var component(default, null):String;
+
+ public var data(default, null):T;
+ public var success(default, null):Bool;
+ public var debug(default, null):Bool;
+ public var error(default, null):Error;
+
+ public function new(core:NGLite, data:Dynamic) {
+
+ echo = data.echo;
+ component = data.component;
+
+ data = data.data;
+ success = data.success;
+ debug = data.debug;
+
+ if(!data.success) {
+
+ error = new Error(data.error.message, data.error.code);
+ core.logError('$component fail: $error');
+
+ } else
+ this.data = data;
+ }
+}
+
+typedef ResultBase = { };
+
+typedef SessionResult = {
+ > ResultBase,
+
+ var session:Dynamic;
+}
+
+typedef GetHostResult = {
+ > ResultBase,
+
+ var host_approved:Bool;
+}
+
+typedef GetCurrentVersionResult = {
+ > ResultBase,
+
+ var current_version:String;
+ var client_deprecated:Bool;
+}
+
+typedef LogEventResult = {
+ > ResultBase,
+
+ var event_name:String;
+}
+
+typedef GetDateTimeResult = {
+ > ResultBase,
+
+ var datetime:String;
+}
+
+typedef GetVersionResult = {
+ > ResultBase,
+
+ var version:String;
+}
+
+typedef PingResult = {
+ > ResultBase,
+
+ var pong:String;
+}
+
+typedef MedalListResult = {
+ > ResultBase,
+
+ var medals:Array;
+}
+
+typedef MedalUnlockResult = {
+ > ResultBase,
+
+ var medal_score:String;
+ var medal:Dynamic;
+}
+
+typedef ScoreBoardResult = {
+ > ResultBase,
+
+ var scoreboards:Array;
+}
+
+typedef ScoreResult = {
+ > ResultBase,
+
+ var scores:Array;
+ var scoreboard:Dynamic;
+}
+
+typedef PostScoreResult = {
+ > ResultBase,
+
+ var tag:String;
+ var scoreboard:Dynamic;
+ var score:Score;
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/swf/LoadingBar.hx b/source/io/newgrounds/swf/LoadingBar.hx
new file mode 100644
index 000000000..9c7c590a8
--- /dev/null
+++ b/source/io/newgrounds/swf/LoadingBar.hx
@@ -0,0 +1,23 @@
+package io.newgrounds.swf;
+
+import openfl.display.MovieClip;
+
+class LoadingBar extends MovieClip {
+
+ public var bar(default, null):MovieClip;
+
+ public function new() {
+ super();
+
+ setProgress(0.0);
+ }
+
+ /**
+ *
+ * @param value The ratio of bytes loaded to bytes total
+ */
+ public function setProgress(value:Float):Void {
+
+ bar.gotoAndStop(1 + Std.int(value * (bar.totalFrames - 1)));
+ }
+}
diff --git a/source/io/newgrounds/swf/MedalPopup.hx b/source/io/newgrounds/swf/MedalPopup.hx
new file mode 100644
index 000000000..f55ef4266
--- /dev/null
+++ b/source/io/newgrounds/swf/MedalPopup.hx
@@ -0,0 +1,151 @@
+package io.newgrounds.swf;
+
+import io.newgrounds.swf.common.BaseAsset;
+import io.newgrounds.objects.Medal;
+
+import openfl.text.TextFieldAutoSize;
+import openfl.text.TextField;
+import openfl.display.DisplayObject;
+import openfl.display.Loader;
+import openfl.display.MovieClip;
+import openfl.net.URLRequest;
+import openfl.events.Event;
+
+class MedalPopup extends BaseAsset {
+
+ static inline var FRAME_HIDDEN:String = "hidden";
+ static inline var FRAME_MEDAL_UNLOCKED:String = "medalUnlocked";
+ static inline var FRAME_INTRO_COMPLETE:String = "introComplete";
+ static inline var FRAME_UNLOCK_COMPLETE:String = "unlockComplete";
+ static inline var MIN_TEXT_SIZE:Int = 12;
+
+ public var medalIcon(default, null):MovieClip;
+ public var medalName(default, null):MovieClip;
+ public var medalPoints(default, null):MovieClip;
+
+ public var alwaysOnTop:Bool;
+ #if !ng_lite
+ public var requiresSession:Bool;
+ #end
+
+ var _animQueue = new ArrayVoid>();
+ var _scrollSpeed:Float;
+
+ public function new() {
+ super();
+
+ mouseEnabled = false;
+ mouseChildren = false;
+
+ hide();
+ addFrameScript(totalFrames - 1, onUnlockAnimComplete);
+ }
+
+ function hide():Void {
+
+ visible = false;
+ gotoAndStop(FRAME_HIDDEN);
+ }
+
+ #if !ng_lite
+ override function onReady():Void {
+ super.onReady();
+
+ if (NG.core.medals != null)
+ onMedalsLoaded();
+ else
+ NG.core.onLogin.addOnce(NG.core.requestMedals.bind(onMedalsLoaded));
+ }
+
+ function onMedalsLoaded():Void {
+
+ for (medal in NG.core.medals)
+ medal.onUnlock.add(onMedalOnlock.bind(medal));
+ }
+
+ function onMedalOnlock(medal:Medal):Void {
+
+ if (requiresSession && !NG.core.loggedIn)
+ return;
+
+ var loader = new Loader();
+ loader.load(new URLRequest(medal.icon));
+
+ playAnim(loader, medal.name, medal.value);
+ }
+
+ #end
+
+ public function playAnim(icon:DisplayObject, name:String, value:Int):Void {
+
+ if (currentLabel == FRAME_HIDDEN)
+ playNextAnim(icon, name, value);
+ else
+ _animQueue.push(playNextAnim.bind(icon, name, value));
+ }
+
+ function playNextAnim(icon:DisplayObject, name:String, value:Int):Void {
+
+ visible = true;
+ gotoAndPlay(FRAME_MEDAL_UNLOCKED);
+
+ if (alwaysOnTop && parent != null) {
+
+ parent.setChildIndex(this, parent.numChildren - 1);
+ }
+
+ while(medalIcon.numChildren > 0)
+ medalIcon.removeChildAt(0);
+
+ cast(medalPoints.getChildByName("field"), TextField).text = Std.string(value);
+
+ var field:TextField = cast medalName.getChildByName("field");
+ field.autoSize = TextFieldAutoSize.LEFT;
+ field.x = 0;
+ field.text = "";
+ var oldWidth = medalName.width;
+ field.text = name;
+
+ _scrollSpeed = 0;
+ if (field.width > oldWidth + 4) {
+
+ field.x = oldWidth + 4;
+ initScroll(field);
+ }
+
+ medalIcon.addChild(icon);
+ }
+
+ function initScroll(field:TextField):Void {
+ //TODO: Find out why scrollrect didn't work
+
+ var animDuration = 0;
+
+ for (frame in currentLabels){
+
+ if (frame.name == FRAME_INTRO_COMPLETE )
+ animDuration -= frame.frame;
+ else if (frame.name == FRAME_UNLOCK_COMPLETE)
+ animDuration += frame.frame;
+ }
+
+ _scrollSpeed = (field.width + field.x + 4) / animDuration;
+ field.addEventListener(Event.ENTER_FRAME, updateScroll);
+ }
+
+ function updateScroll(e:Event):Void{
+
+ if (currentLabel == FRAME_INTRO_COMPLETE)
+ cast (e.currentTarget, TextField).x -= _scrollSpeed;
+ }
+
+ function onUnlockAnimComplete():Void {
+
+ cast (medalName.getChildByName("field"), TextField).removeEventListener(Event.ENTER_FRAME, updateScroll);
+
+ if (_animQueue.length == 0)
+ hide();
+ else
+ (_animQueue.shift())();
+ }
+}
diff --git a/source/io/newgrounds/swf/ScoreBrowser.hx b/source/io/newgrounds/swf/ScoreBrowser.hx
new file mode 100644
index 000000000..c4232c22b
--- /dev/null
+++ b/source/io/newgrounds/swf/ScoreBrowser.hx
@@ -0,0 +1,250 @@
+package io.newgrounds.swf;
+
+import openfl.events.Event;
+import io.newgrounds.swf.common.DropDown;
+import io.newgrounds.objects.Score;
+import io.newgrounds.objects.events.Result.ScoreBoardResult;
+import io.newgrounds.objects.events.Result.ScoreResult;
+import io.newgrounds.objects.events.Response;
+import io.newgrounds.swf.common.BaseAsset;
+import io.newgrounds.swf.common.Button;
+import io.newgrounds.components.ScoreBoardComponent.Period;
+
+import openfl.display.MovieClip;
+import openfl.text.TextField;
+
+class ScoreBrowser extends BaseAsset {
+
+ public var prevButton (default, null):MovieClip;
+ public var nextButton (default, null):MovieClip;
+ public var reloadButton (default, null):MovieClip;
+ public var listBox (default, null):MovieClip;
+ public var loadingIcon (default, null):MovieClip;
+ public var errorIcon (default, null):MovieClip;
+ public var scoreContainer(default, null):MovieClip;
+ public var titleField (default, null):TextField;
+ public var pageField (default, null):TextField;
+
+ public var period(get, set):Period;
+ function get_period():Period { return _periodDropDown.value; }
+ function set_period(value:Period):Period { return _periodDropDown.value = value; }
+
+ public var title(get, set):String;
+ function get_title():String { return titleField.text; }
+ function set_title(value:String):String { return titleField.text = value; }
+
+ public var tag(default, set):String;
+ function set_tag(value:String):String {
+
+ if (this.tag != value) {
+
+ this.tag = value;
+ delayReload();
+ }
+
+ return value;
+ }
+
+ public var social(default, set):Bool;
+ function set_social(value:Bool):Bool {
+
+ if (this.social != value) {
+
+ this.social = value;
+ delayReload();
+ }
+
+ return value;
+ }
+
+ public var boardId(default, set):Int;
+ function set_boardId(value:Int):Int {
+
+ _boardIDSet = true;
+
+ if (this.boardId != value) {
+
+ this.boardId = value;
+ delayReload();
+ }
+
+ return value;
+ }
+
+ public var page(default, set):Int;
+ function set_page(value:Int):Int {
+
+ if (this.page != value) {
+
+ this.page = value;
+ delayReload();
+ }
+
+ return value;
+ }
+
+ var _scores:Array;
+ var _limit:Int = 0;
+ var _periodDropDown:DropDown;
+ var _boardIDSet:Bool;
+
+ public function new() { super(); }
+
+ override function setDefaults():Void {
+ super.setDefaults();
+
+ boardId = -1;
+ _boardIDSet = false;
+
+ scoreContainer.visible = false;
+ loadingIcon.visible = false;
+ reloadButton.visible = false;
+ errorIcon.visible = false;
+ errorIcon.addFrameScript(errorIcon.totalFrames - 1, errorIcon.stop);
+
+ //TODO: prevent memory leaks?
+ new Button(prevButton, onPrevClick);
+ new Button(nextButton, onNextClick);
+ new Button(reloadButton, reload);
+ _periodDropDown = new DropDown(listBox, delayReload);
+ _periodDropDown.addItem("Current day" , Period.DAY );
+ _periodDropDown.addItem("Current week" , Period.WEEK );
+ _periodDropDown.addItem("Current month", Period.MONTH);
+ _periodDropDown.addItem("Current year" , Period.YEAR );
+ _periodDropDown.addItem("All time" , Period.ALL );
+ _periodDropDown.value = Period.ALL;
+
+ _scores = new Array();
+ while(true) {
+
+ var score:MovieClip = cast scoreContainer.getChildByName('score${_scores.length}');
+ if (score == null)
+ break;
+
+ new Button(score);
+ _scores.push(score);
+ }
+
+ _limit = _scores.length;
+ }
+
+ override function onReady():Void {
+ super.onReady();
+
+ if (boardId == -1 && !_boardIDSet) {
+
+ #if ng_lite
+ NG.core.calls.scoreBoard.getBoards()
+ .addDataHandler(onBoardsRecieved)
+ .queue();
+ #else
+ if (NG.core.scoreBoards != null)
+ onBoardsLoaded();
+ else
+ NG.core.requestScoreBoards(onBoardsLoaded);
+ #end
+ }
+
+ reload();
+ }
+
+ #if ng_lite
+ function onBoardsRecieved(response:Response):Void {
+
+ if (response.success && response.result.success) {
+
+ for (board in response.result.data.scoreboards) {
+
+ NG.core.log('No boardId specified defaulting to ${board.name}');
+ boardId = board.id;
+ return;
+ }
+ }
+ }
+ #else
+ function onBoardsLoaded():Void {
+
+ for (board in NG.core.scoreBoards) {
+
+ NG.core.log('No boardId specified defaulting to ${board.name}');
+ boardId = board.id;
+ return;
+ }
+ }
+ #end
+
+ /** Used internally to avoid multiple server requests from various property changes in a small time-frame. **/
+ function delayReload():Void {
+
+ addEventListener(Event.EXIT_FRAME, onDelayComplete);
+ }
+
+ function onDelayComplete(e:Event):Void { reload(); }
+
+ public function reload():Void {
+ removeEventListener(Event.EXIT_FRAME, onDelayComplete);
+
+ errorIcon.visible = false;
+ scoreContainer.visible = false;
+ pageField.text = 'page ${page + 1}';
+
+ if (_coreReady && boardId != -1 && _limit > 0 && period != null) {
+
+ loadingIcon.visible = true;
+
+ NG.core.calls.scoreBoard.getScores(boardId, _limit, _limit * page, period, social, tag)
+ .addDataHandler(onScoresReceive)
+ .send();
+ }
+ }
+
+ function onScoresReceive(response:Response):Void {
+
+ loadingIcon.visible = false;
+
+ if (response.success && response.result.success) {
+
+ scoreContainer.visible = true;
+
+ var i = _limit;
+ while(i > 0) {
+ i--;
+
+ if (i < response.result.data.scores.length)
+ drawScore(i, response.result.data.scores[i], _scores[i]);
+ else
+ drawScore(i, null, _scores[i]);
+ }
+
+ } else {
+
+ errorIcon.visible = true;
+ errorIcon.gotoAndPlay(1);
+ reloadButton.visible = true;
+ }
+ }
+
+ inline function drawScore(rank:Int, score:Score, asset:MovieClip):Void {
+
+ if (score == null)
+ asset.visible = false;
+ else {
+
+ asset.visible = true;
+ cast (asset.getChildByName("nameField" ), TextField).text = score.user.name;
+ cast (asset.getChildByName("scoreField"), TextField).text = score.formatted_value;
+ cast (asset.getChildByName("rankField" ), TextField).text = Std.string(rank + 1);
+ }
+ }
+
+ function onPrevClick():Void {
+
+ if (page > 0)
+ page--;
+ }
+
+ function onNextClick():Void {
+
+ page++;
+ }
+}
diff --git a/source/io/newgrounds/swf/common/BaseAsset.hx b/source/io/newgrounds/swf/common/BaseAsset.hx
new file mode 100644
index 000000000..da1f6126f
--- /dev/null
+++ b/source/io/newgrounds/swf/common/BaseAsset.hx
@@ -0,0 +1,35 @@
+package io.newgrounds.swf.common;
+
+import openfl.events.Event;
+import openfl.display.MovieClip;
+
+class BaseAsset extends MovieClip {
+
+ var _coreReady:Bool = false;
+
+ public function new() {
+ super();
+
+ setDefaults();
+
+ if (stage != null)
+ onAdded(null);
+ else
+ addEventListener(Event.ADDED_TO_STAGE, onAdded);
+ }
+
+ function setDefaults():Void { }
+
+ function onAdded(e:Event):Void {
+
+ if (NG.core != null)
+ onReady();
+ else
+ NG.onCoreReady.add(onReady);
+ }
+
+ function onReady():Void {
+
+ _coreReady = true;
+ }
+}
diff --git a/source/io/newgrounds/swf/common/Button.hx b/source/io/newgrounds/swf/common/Button.hx
new file mode 100644
index 000000000..5f4617528
--- /dev/null
+++ b/source/io/newgrounds/swf/common/Button.hx
@@ -0,0 +1,151 @@
+package io.newgrounds.swf.common;
+
+import openfl.display.Stage;
+import openfl.events.Event;
+import openfl.events.MouseEvent;
+import openfl.display.MovieClip;
+
+class Button {
+
+ var _enabled:Bool;
+ public var enabled(get, set):Bool;
+ function get_enabled():Bool { return _enabled; }
+ function set_enabled(value:Bool):Bool {
+
+ if (value != _enabled) {
+
+ _enabled = value;
+ updateEnabled();
+ }
+
+ return value;
+ }
+
+ public var onClick:Void->Void;
+ public var onOver:Void->Void;
+ public var onOut:Void->Void;
+
+ var _target:MovieClip;
+ var _down:Bool;
+ var _over:Bool;
+ var _foundLabels:Array;
+
+ public function new(target:MovieClip, onClick:Void->Void = null, onOver:Void->Void = null, onOut:Void->Void = null) {
+
+ _target = target;
+ this.onClick = onClick;
+ this.onOver = onOver;
+ this.onOut = onOut;
+
+ _foundLabels = new Array();
+ for (label in _target.currentLabels)
+ _foundLabels.push(label.name);
+
+ _target.stop();
+ _target.addEventListener(Event.ADDED_TO_STAGE, onAdded);
+ if (target.stage != null)
+ onAdded(null);
+
+ enabled = true;
+ }
+
+ function onAdded(e:Event):Void {
+
+ var stage = _target.stage;
+ stage.addEventListener(MouseEvent.MOUSE_UP, mouseHandler);
+ _target.addEventListener(MouseEvent.MOUSE_OVER, mouseHandler);
+ _target.addEventListener(MouseEvent.MOUSE_OUT, mouseHandler);
+ _target.addEventListener(MouseEvent.MOUSE_DOWN, mouseHandler);
+ _target.addEventListener(MouseEvent.CLICK, mouseHandler);
+
+ function selfRemoveEvent(e:Event):Void {
+
+ _target.removeEventListener(Event.REMOVED_FROM_STAGE, selfRemoveEvent);
+ onRemove(e, stage);
+ }
+ _target.addEventListener(Event.REMOVED_FROM_STAGE, selfRemoveEvent);
+ }
+
+ function onRemove(e:Event, stage:Stage):Void {
+
+ stage.removeEventListener(MouseEvent.MOUSE_UP, mouseHandler);
+ _target.removeEventListener(MouseEvent.MOUSE_OVER, mouseHandler);
+ _target.removeEventListener(MouseEvent.MOUSE_OUT, mouseHandler);
+ _target.removeEventListener(MouseEvent.MOUSE_DOWN, mouseHandler);
+ _target.removeEventListener(MouseEvent.CLICK, mouseHandler);
+ }
+
+ function mouseHandler(event:MouseEvent):Void {
+
+ switch(event.type) {
+
+ case MouseEvent.MOUSE_OVER:
+
+ _over = true;
+
+ if (onOver != null)
+ onOver();
+
+ case MouseEvent.MOUSE_OUT:
+
+ _over = false;
+
+ if (onOut != null)
+ onOut();
+
+ case MouseEvent.MOUSE_DOWN:
+
+ _down = true;
+
+ case MouseEvent.MOUSE_UP:
+
+ _down = false;
+
+ case MouseEvent.CLICK:
+
+ if (enabled && onClick != null)
+ onClick();
+ }
+ updateState();
+ }
+
+ function updateEnabled():Void {
+
+ updateState();
+
+ _target.useHandCursor = enabled;
+ _target.buttonMode = enabled;
+ }
+
+ function updateState():Void {
+
+ var state = determineState();
+
+ if (_target.currentLabel != state && _foundLabels.indexOf(state) != -1)
+ _target.gotoAndStop(state);
+ }
+
+ function determineState():String {
+
+ if (enabled) {
+
+ if (_over)
+ return _down ? "down" : "over";
+
+ return "up";
+
+ }
+ return "disabled";
+ }
+
+ public function destroy():Void {
+
+ _target.removeEventListener(Event.ADDED_TO_STAGE, onAdded);
+
+ _target = null;
+ onClick = null;
+ onOver = null;
+ onOut = null;
+ _foundLabels = null;
+ }
+}
diff --git a/source/io/newgrounds/swf/common/DropDown.hx b/source/io/newgrounds/swf/common/DropDown.hx
new file mode 100644
index 000000000..a882cf39b
--- /dev/null
+++ b/source/io/newgrounds/swf/common/DropDown.hx
@@ -0,0 +1,88 @@
+package io.newgrounds.swf.common;
+
+
+import haxe.ds.StringMap;
+
+import openfl.display.MovieClip;
+import openfl.display.Sprite;
+import openfl.text.TextField;
+
+class DropDown {
+
+ public var value(default, set):String;
+ function set_value(v:String):String {
+
+ if (this.value == v)
+ return v;
+
+ this.value = v;
+ _selectedLabel.text = _values.get(v);
+
+ if (_onChange != null)
+ _onChange();
+
+ return v;
+ }
+
+ var _choiceContainer:Sprite;
+ var _selectedLabel:TextField;
+ var _onChange:Void->Void;
+ var _values:StringMap;
+ var _unusedChoices:Array;
+
+ public function new(target:MovieClip, label:String = "", onChange:Void->Void = null) {
+
+ _onChange = onChange;
+
+ _selectedLabel = cast cast(target.getChildByName("currentItem"), MovieClip).getChildByName("label");
+ _selectedLabel.text = label;
+
+ _values = new StringMap();
+
+ new Button(cast target.getChildByName("button"), onClickExpand);
+ new Button(cast target.getChildByName("currentItem"), onClickExpand);
+ _choiceContainer = new Sprite();
+ _choiceContainer.visible = false;
+ target.addChild(_choiceContainer);
+
+ _unusedChoices = new Array();
+ while(true) {
+
+ var item:MovieClip = cast target.getChildByName('item${_unusedChoices.length}');
+ if (item == null)
+ break;
+
+ target.removeChild(item);
+ _unusedChoices.push(item);
+ }
+ }
+
+ public function addItem(name:String, value:String):Void {
+
+ _values.set(value, name);
+
+ if (_unusedChoices.length == 0) {
+
+ NG.core.logError('cannot create another dropBox item max=${_choiceContainer.numChildren}');
+ return;
+ }
+
+ var button = _unusedChoices.shift();
+ cast(button.getChildByName("label"), TextField).text = name;
+ _choiceContainer.addChild(button);
+
+ new Button(button, onChoiceClick.bind(value));
+ }
+
+ function onClickExpand():Void {
+
+ _choiceContainer.visible = !_choiceContainer.visible;
+ }
+
+ function onChoiceClick(name:String):Void {
+
+ value = name;
+
+ _choiceContainer.visible = false;
+ }
+}
\ No newline at end of file
diff --git a/source/io/newgrounds/utils/AsyncHttp.hx b/source/io/newgrounds/utils/AsyncHttp.hx
new file mode 100644
index 000000000..4fbe14388
--- /dev/null
+++ b/source/io/newgrounds/utils/AsyncHttp.hx
@@ -0,0 +1,203 @@
+package io.newgrounds.utils;
+
+import io.newgrounds.NGLite;
+
+import haxe.Http;
+import haxe.Timer;
+
+#if neko
+import neko.vm.Thread;
+#elseif java
+import java.vm.Thread;
+#elseif cpp
+import cpp.vm.Thread;
+#end
+
+/**
+ * Uses Threading to turn hxcpp's synchronous http requests into asynchronous processes
+ *
+ * @author GeoKureli
+ */
+class AsyncHttp {
+
+ inline static var PATH:String = "https://newgrounds.io/gateway_v3.php";
+
+ static public function send
+ ( core:NGLite
+ , data:String
+ , onData:String->Void
+ , onError:String->Void
+ , onStatus:Int->Void
+ ) {
+
+ core.logVerbose('sending: $data');
+
+ #if (neko || java || cpp)
+ sendAsync(core, data, onData, onError, onStatus);
+ #else
+ sendSync(core, data, onData, onError, onStatus);
+ #end
+ }
+
+ static function sendSync
+ ( core:NGLite
+ , data:String
+ , onData:String->Void
+ , onError:String->Void
+ , onStatus:Int->Void
+ ):Void {
+
+ var http = new Http(PATH);
+ http.setParameter("input", data);
+ http.onData = onData;
+ http.onError = onError;
+ http.onStatus = onStatus;
+ // #if js http.async = async; #end
+ http.request(true);
+ }
+
+ #if (neko || java || cpp)
+ static var _deadPool:Array = [];
+ static var _livePool:Array = [];
+ static var _map:Map = new Map();
+ static var _timer:Timer;
+
+ static var _count:Int = 0;
+
+ var _core:NGLite;
+ var _key:Int;
+ var _onData:String->Void;
+ var _onError:String->Void;
+ var _onStatus:Int->Void;
+ var _worker:Thread;
+
+ public function new (core:NGLite) {
+
+ _core = core;
+ _worker = Thread.create(sendThreaded);
+ _key = _count++;
+ _map[_key] = this;
+ _core.logVerbose('async http created: $_key');
+ }
+
+ function start(data:String, onData:String->Void, onError:String->Void, onStatus:Int->Void) {
+
+ _core.logVerbose('async http started: $_key');
+
+ if (_livePool.length == 0)
+ startTimer();
+
+ _deadPool.remove(this);
+ _livePool.push(this);
+
+ _onData = onData;
+ _onError = onError;
+ _onStatus = onStatus;
+ _worker.sendMessage({ source:Thread.current(), args:data, key:_key, core:_core });
+ }
+
+ function handleMessage(data:ReplyData):Void {
+
+ _core.logVerbose('handling message: $_key');
+
+ if (data.status != null) {
+
+ _core.logVerbose('\t- status: ${data.status}');
+ _onStatus(cast data.status);
+ return;
+ }
+
+ var tempFunc:Void->Void;
+ if (data.data != null) {
+
+ _core.logVerbose('\t- data');
+ tempFunc = _onData.bind(data.data);
+
+ } else {
+
+ _core.logVerbose('\t- error');
+ tempFunc = _onError.bind(data.error);
+ }
+
+ cleanUp();
+ // Delay the call until destroy so that we're more likely to use a single
+ // thread on daisy-chained calls
+ tempFunc();
+ }
+
+ inline function cleanUp():Void {
+
+ _onData = null;
+ _onError = null;
+
+ _deadPool.push(this);
+ _livePool.remove(this);
+
+ if (_livePool.length == 0)
+ stopTimer();
+ }
+
+ static function sendAsync
+ ( core:NGLite
+ , data:String
+ , onData:String->Void
+ , onError:String->Void
+ , onStatus:Int->Void
+ ):Void {
+
+ var http:AsyncHttp;
+ if (_deadPool.length == 0)
+ http = new AsyncHttp(core);
+ else
+ http = _deadPool[0];
+
+ http.start(data, onData, onError, onStatus);
+ }
+
+ static function startTimer():Void {
+
+ if (_timer != null)
+ return;
+
+ _timer = new Timer(1000 / 60.0);
+ _timer.run = update;
+ }
+
+ static function stopTimer():Void {
+
+ _timer.stop();
+ _timer = null;
+ }
+
+ static public function update():Void {
+
+ var message:ReplyData = cast Thread.readMessage(false);
+ if (message != null)
+ _map[message.key].handleMessage(message);
+ }
+
+ static function sendThreaded():Void {
+
+ while(true) {
+
+ var data:LoaderData = cast Thread.readMessage(true);
+ data.core.logVerbose('start message received: ${data.key}');
+
+ sendSync
+ ( data.core
+ , data.args
+ , function(reply ) { data.source.sendMessage({ key:data.key, data :reply }); }
+ , function(error ) { data.source.sendMessage({ key:data.key, error :error }); }
+ , function(status) { data.source.sendMessage({ key:data.key, status:status }); }
+ );
+ }
+ }
+
+ #end
+}
+
+
+#if (neko || java || cpp)
+typedef LoaderData = { source:Thread, key:Int, args:String, core:NGLite };
+typedef ReplyData = { key:Int, ?data:String, ?error:String, ?status:Null };
+#end
\ No newline at end of file
diff --git a/source/io/newgrounds/utils/Dispatcher.hx b/source/io/newgrounds/utils/Dispatcher.hx
new file mode 100644
index 000000000..699da01da
--- /dev/null
+++ b/source/io/newgrounds/utils/Dispatcher.hx
@@ -0,0 +1,118 @@
+package io.newgrounds.utils;
+
+/**
+ * Basically shitty signals, but I didn't want to have external references.
+**/
+class Dispatcher {
+
+ var _list:ArrayVoid>;
+ var _once:ArrayVoid>;
+
+ public function new() {
+
+ _list = new ArrayVoid>();
+ _once = new ArrayVoid>();
+ }
+
+ public function add(handler:Void->Void, once:Bool = false):Bool {
+
+ if (_list.indexOf(handler) != -1) {
+
+ // ---- REMOVE ONCE
+ if (!once && _once.indexOf(handler) != -1)
+ _once.remove(handler);
+
+ return false;
+ }
+
+ _list.unshift(handler);
+ if (once)
+ _once.unshift(handler);
+
+ return true;
+ }
+
+ inline public function addOnce(handler:Void->Void):Bool {
+
+ return add(handler, true);
+ }
+
+ public function remove(handler:Void->Void):Bool {
+
+ _once.remove(handler);
+ return _list.remove(handler);
+ }
+
+ public function dispatch():Void {
+
+ var i = _list.length - 1;
+ while(i >= 0) {
+
+ var handler = _list[i];
+
+ if (_once.remove(handler))
+ _list.remove(handler);
+
+ handler();
+
+ i--;
+ }
+ }
+}
+
+class TypedDispatcher {
+
+ var _list:ArrayVoid>;
+ var _once:ArrayVoid>;
+
+ public function new() {
+
+ _list = new ArrayVoid>();
+ _once = new ArrayVoid>();
+ }
+
+ public function add(handler:T->Void, once:Bool = false):Bool {
+
+ if (_list.indexOf(handler) != -1) {
+
+ // ---- REMOVE ONCE
+ if (!once && _once.indexOf(handler) != -1)
+ _once.remove(handler);
+
+ return false;
+ }
+
+ _list.unshift(handler);
+ if (once)
+ _once.unshift(handler);
+
+ return true;
+ }
+
+ inline public function addOnce(handler:T->Void):Bool {
+
+ return add(handler, true);
+ }
+
+ public function remove(handler:T->Void):Bool {
+
+ _once.remove(handler);
+ return _list.remove(handler);
+ }
+
+ public function dispatch(arg:T):Void {
+
+ var i = _list.length - 1;
+ while(i >= 0) {
+
+ var handler = _list[i];
+
+ if (_once.remove(handler))
+ _list.remove(handler);
+
+ handler(arg);
+
+ i--;
+ }
+ }
+}
\ No newline at end of file