package com.rubenvandeven.emotionhero; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; import android.support.v8.renderscript.RenderScript; import android.util.Log; import com.affectiva.android.affdex.sdk.Frame; import com.affectiva.android.affdex.sdk.detector.Face; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import io.github.silvaren.easyrs.tools.Nv21Image; /** * Created by ruben on 16/08/16. */ public class Scenario { public int id; public Frame.ByteArrayFrame currentFrame; public static final int LVL_NONE = 0; public static final int LVL_ANGER = 1; public static final int LVL_JOY = 2; public static final int LVL_SURPRISE = 3; public static final int LVL_SADDNESS= 4; // the levels in the right order. public static final ArrayList SCENARIOS = new ArrayList() {{ add(LVL_JOY); add(LVL_ANGER); add(LVL_SADDNESS); add(LVL_SURPRISE); }}; static int DESIRED_FPS = 25; float duration = 0; /** * Minimum score to be able to pass to the next level. * (check minimumScore || minimumAchievements) */ float minimumScore = 0; /** * Number of achievements required to have, before being able to go to next level * (check minimumScore || minimumAchievements) */ float minimumAchievements= 0; /** * Achievements one get obtain in this level */ ArrayList achievements = new ArrayList<>(); /** * If a game is beign played. */ Game game; /** * @deprecated */ long startTime = 0; /** * The timer that provides the tick */ Timer timer; /** * Increment on each tick */ float runningTime = -1; boolean isRunning = false; private GamingActivity _activity; int maxScore = 0; protected RenderScript rs; protected Paint squarePaint; /** * The scorres in this moment, as to draw them on the screen. * Indexes are Emotion ordinals */ private Face currentFace; ArrayList targets = new ArrayList<>(); class Target { public int index; public Emotion emotion; public float value; public float timestamp; public boolean isHit = false; } /** * Constructor */ public Scenario(int lvl_id, GamingActivity activity) { // go to first scenario if unexisting is given if(!SCENARIOS.contains(lvl_id)) { lvl_id = SCENARIOS.get(0); } this.id = lvl_id; _activity = activity; createTargets(); } public void init() { timer = new Timer("ScenarioTimer"); TimerTask tickTask; tickTask = new TimerTask() { @Override public void run() { // if (System.currentTimeMillis() - scheduledExecutionTime() >= // MAX_TARDINESS) // return; // Too late; skip this execution. tick(); } }; timer.schedule(tickTask, 0, 1000/DESIRED_FPS); this.game = new Game(null, this, 0, 0, new Date(), null); rs = RenderScript.create(_activity); squarePaint = new Paint(); squarePaint.setColor(Color.YELLOW); } /** * To be called on each timer tick */ public void tick() { if(!isRunning) return; runningTime += 1.0f/DESIRED_FPS; if(isFinished()) { stop(); return; } for(Target target: targets) { // skip targets that are already scored if(target.isHit == true) { continue; } if(target.timestamp <= runningTime) { Hit hit = new Hit(target, game, currentFace); _activity.sound.play(_activity.soundIds.get(_activity.SOUND_SCORE), 1, 1, 2,0, hit.score / 200f + 0.5f ); // play back the sound slower // depending on hit value if(hit.bonus > 0) { _activity.sound.play(_activity.soundIds.get(_activity.SOUND_BONUS), 0.7f, 0.7f, 1,0, hit.bonus / 100f + 0.8f ); // play back the sound slower // depending on bonus value } target.isHit = true; Bitmap outputBitmap = null; if(currentFrame != null) { // convert NV21 byteArrayFrame from camera to RGB bitmap. Frame.ByteArrayFrame byteArrayFrame = (Frame.ByteArrayFrame) currentFrame; outputBitmap = Nv21Image.nv21ToBitmap(rs, byteArrayFrame.getByteArray(), byteArrayFrame.getWidth(), byteArrayFrame.getHeight()); Frame.ROTATE rotation = byteArrayFrame.getTargetRotation(); // ie BY_90_CCW -90.0 Log.v("Scenario", "frame rotation: " + rotation.toString() + " " + rotation.toDouble()); // process rotation... (maybe faster if done on byte array?) if(rotation != Frame.ROTATE.NO_ROTATION) { Matrix matrix = new Matrix(); matrix.postRotate((int) rotation.toDouble()); // int width = rotation == Frame.ROTATE.BY_180 ? outputBitmap.getWidth() : outputBitmap.getHeight(); // int height = rotation == Frame.ROTATE.BY_180 ? outputBitmap.getHeight() : outputBitmap.getWidth(); outputBitmap = Bitmap.createBitmap(outputBitmap , 0, 0, outputBitmap .getWidth(), outputBitmap .getHeight(), matrix, true); } // Deprecated: now we send only fragments of the images anyway. // Canvas canvas = new Canvas(outputBitmap); // RectF rect = Hit.getBoundingboxForPoints(currentFace.getFacePoints()); // canvas.drawRect(rect, squarePaint); /* Also possible: only grayscale image - only loops over first part of the bytearray, is it faster? int[] pixels; int p; int size = width*height; for(int i = 0; i < size; i++) { p = data[i] & 0xFF; pixels[i] = 0xff000000 | p<<16 | p<<8 | p; } Bitmap bm = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); */ } hit.frame = outputBitmap; } } } public Target getTargetByIndex(int target_index) { return targets.get(target_index-1); } /** * Add a target on given timestamp * @param emotion * @param value * @param timestamp */ public void setTarget(Emotion emotion, float value, float timestamp) { // Log.e(GamingActivity.LOG_TAG, "Set target:" + Float.toString(timestamp) + " " + Float.toString(duration)); if((timestamp + 1) > duration) { duration = timestamp + 1; // always a bit onger than last target, as it otherwise the game does not finish pretty } Target target = new Target(); target.timestamp = timestamp; target.value = value; target.emotion = emotion; target.index = targets.size() + 1; targets.add(target); maxScore = getMaxScore(); } /** * Add target after existing targets, give delta with last item instead of absolute time. * @param emotion * @param value * @param interval */ public void addTarget(Emotion emotion, float value, float interval) { float timestamp; if(targets.isEmpty()) { timestamp = interval; } else { timestamp = targets.get(targets.size() - 1).timestamp + interval; } setTarget(emotion, value, timestamp); } public float getHitTotalValue() { float value = 0; for (Hit hit : game.hits.values()) { value += hit.score; } return value; } public float getHitTotalMisValue() { float value = 0; for (Hit hit : game.hits.values()) { value += 100-hit.score; } return value; } public float getBonusTotalValue() { float value = 0; for (Hit hit : game.hits.values()) { value += hit.bonus; } return value; } public float getHitPercentage() { return (getHitTotalValue()/maxScore) * 100; } public float getBonusPercentage() { // maxScore for bonus == normal maxScore return (getBonusTotalValue()/maxScore) * 100; } public float getMissedPercentage() { return (getHitTotalMisValue()/maxScore) * 100; } /** * Check whether given timestamp is within duration of the scenario * @param timestamp * @return */ public boolean isWithinTime(float timestamp) { return timestamp <= duration; } public ArrayList getTargets() { return targets; } /** * Get the time within the scenario (so since start() has been called) */ public float getTime() { return runningTime; // if not started, don't move the labels, if started, move them by diff_y // if(startTime == 0) { // return 0; // } else { // float diff_t = ((System.currentTimeMillis() - startTime)) / (float) 1000; // if(diff_t > duration) { // never larger than scenario duration // return duration; // } // return diff_t; // } } public void start() { startTime = System.currentTimeMillis(); isRunning = true; } public void pause() { isRunning = false; } public void stop() { isRunning = false; } // TODO: create AttributeScoreCollection class, with this method. public void setCurrentFace(Face face) { currentFace = face; } public boolean isFinished() { return runningTime > duration; } public Score getScore() { if(!isFinished()) { return null; } Score score = new Score(id, getHitTotalValue()); score.setTargets(targets); return score; } public int getMaxScore() { return targets.size() * 100; } public void createTargets() { AchievementCollection achievementCollection = AchievementCollection.getInstance(); switch(id) { case LVL_ANGER: setTarget(Emotion.ANGER, 100, 1); setTarget(Emotion.ANGER, 100, 2); setTarget(Emotion.ANGER, 10, 3); setTarget(Emotion.ANGER, 20, 4); setTarget(Emotion.ANGER, 40, 5); setTarget(Emotion.ANGER, 70, 6); setTarget(Emotion.ANGER, 100, 7); setMinimumScoreFromPercentage(10); minimumAchievements = 2; achievements.add(achievementCollection.get(1)); achievements.add(achievementCollection.get(3)); break; case LVL_JOY: setTarget(Emotion.JOY, 100, 1); setTarget(Emotion.JOY, 100, 2); setTarget(Emotion.JOY, 100, 4); setTarget(Emotion.CONTEMPT, 20, 4); setTarget(Emotion.ANGER, 100, 5); setTarget(Emotion.JOY, 100, 7); setTarget(Emotion.ANGER, 100, 9); setTarget(Emotion.JOY, 100, 11); setTarget(Emotion.JOY, 70, 12); setTarget(Emotion.JOY, 60, 13); setTarget(Emotion.JOY, 30, 14); setTarget(Emotion.JOY, 10, 14.5f); setTarget(Emotion.ANGER, 100, 16); setTarget(Emotion.JOY, 100, 17); setTarget(Emotion.JOY, 100, 18); setTarget(Emotion.JOY, 100, 19); setTarget(Emotion.JOY, 100, 20); setMinimumScoreFromPercentage(10); minimumAchievements = 2; achievements.add(achievementCollection.get(2)); achievements.add(achievementCollection.get(4)); break; case LVL_SURPRISE: setTarget(Emotion.SURPRISE, 20, 1); setTarget(Emotion.SURPRISE, 50, 2); setTarget(Emotion.SURPRISE, 80, 3); setTarget(Emotion.SURPRISE, 100, 4); setMinimumScoreFromPercentage(10); minimumAchievements = 2; achievements.add(achievementCollection.get(5)); achievements.add(achievementCollection.get(6)); break; case LVL_SADDNESS: setTarget(Emotion.SADNESS, 20, 1); setTarget(Emotion.SADNESS, 50, 2); setTarget(Emotion.SADNESS, 80, 3); setTarget(Emotion.SADNESS, 100, 4); setMinimumScoreFromPercentage(10); minimumAchievements = 2; break; } } public void setMinimumScoreFromPercentage(float percentage) { this.minimumScore = maxScore * (percentage / 100f); } public int getNextLevelId() { int nextIdx = SCENARIOS.indexOf(id) + 1; if(SCENARIOS.size() <= nextIdx) { return SCENARIOS.get(0); } return SCENARIOS.get(nextIdx); } public boolean isFinalLevel() { if(SCENARIOS.get(SCENARIOS.size()-1) == id) return true; return false; } public boolean isHigherThen(int lvl_id) { return isHigherLevel(id, lvl_id); } public static boolean isHigherLevel(int lvl_id, int then_lvl_id) { if(lvl_id == then_lvl_id) { return false; } return SCENARIOS.indexOf(lvl_id) > SCENARIOS.indexOf(then_lvl_id); } public String toString() { switch(id) { case LVL_ANGER: return "\"Smile like you mean it\""; case LVL_JOY: return "The seven"; case LVL_SURPRISE: return "\"Let's talk business\""; case LVL_SADDNESS: return "A sad sad situation"; } return "..."; } public String getDescription() { switch(id) { case LVL_ANGER: return "You're CEO of a big company. Remarkably, research shows that CEO's of big companies look generally more angry and disgusted when the company is doing well.\n" + "\n" + "However, you're company is not doing well. Make sure you hide this for the press that is watching every move!"; case LVL_JOY: return "You're at a job interview, and you know your face is being monitored.\n" + "Make sure that you express your enthusiasm for the job!"; case LVL_SURPRISE: return "Your friends have organised a surprise party and you've found out! However, you don't want them to know this. First keep calm and then make sure you're as surprised and joyful as possible!\n"; case LVL_SADDNESS: return "You're at the funeral of your great aunt. You don't really know her, but still want to show your respect."; } return "..."; } public Hit getHitForTarget(Target target) { return game.hits.get(target.index); } }