package com.rubenvandeven.emotionhero; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; 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; /** * 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, 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) { float scored_value = target.emotion.getValueFromFace(currentFace); float required_value = target.value; float score = 100 - Math.abs(scored_value-required_value); _activity.sound.play(_activity.soundIds.get(_activity.SOUND_SCORE), 1, 1, 1,0, score / 200f + 0.5f ); // play back the sound slower // depending on hit 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()); 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 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 hit = new Hit(target, game, currentFace, score, 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 getHitPercentage() { return (getHitTotalValue()/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() { 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); 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); break; case LVL_SURPRISE: setTarget(Emotion.SURPRISE, 20, 1); setTarget(Emotion.SURPRISE, 50, 2); setTarget(Emotion.SURPRISE, 80, 3); setTarget(Emotion.SURPRISE, 100, 4); break; case LVL_SADDNESS: setTarget(Emotion.SADNESS, 20, 1); setTarget(Emotion.SADNESS, 50, 2); setTarget(Emotion.SADNESS, 80, 3); setTarget(Emotion.SADNESS, 100, 4); break; } } 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 "CEO"; case LVL_JOY: return "The Conversation"; case LVL_SURPRISE: return "Party time!"; case LVL_SADDNESS: return "Six feet under"; } 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); } }