Newer
Older
accessory_repo / AbilityTimer / xGuiItem.pde
// This software is distributed under the terms of the MIT License.
// Copyright (c) 2018, 2019 molelord
// All rights reserved.

import processing.sound.*;

import javax.swing.JFrame;
import java.awt.MouseInfo;
import java.awt.PointerInfo;
import java.awt.Point;

import java.awt.Robot;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.*;

import java.time.*;
import java.time.format.TextStyle;

import java.util.Locale;

static int getR(int c) {
  return (c>>16) & 0xFF;
}
static int getG(int c) {
  return (c>>8) & 0xFF;
}
static int getB(int c) {
  return c & 0xFF;
}

static String nfspc(int value, int width) {
  String str = nf(value);
  for (int i = str.length(); i < width; i++) {
    str = " " + str;
  }
  return str;
}

class Dbg {
  boolean enabled;
  int baseY;
  int mostRecentImageH;
  Dbg(boolean _enabled) {
    enabled = _enabled;
    baseY   = 0;
    mostRecentImageH = 0;
  }
  void pln(String msg) {
    if (enabled) {
      println(msg);
    }
  }
  void p(String msg) {
    if (enabled) {
      print(msg);
    }
  }
  void reset() {
    baseY = 5;
    mostRecentImageH = 0;
  }
  void txt(String msg) {
    if (enabled) {
      baseY += mostRecentImageH + 8;
      mostRecentImageH = 0;
      textFont(Glbl.font12);
      textSize(12);
      fill(64);
      text(msg, Glbl.DebugX, baseY);
      baseY += 2;
    }
  }
  void img(PImage img, int x) {
    if (enabled) {
      image(img, Glbl.DebugX + x, baseY);
      if (mostRecentImageH < img.height) {
        mostRecentImageH = img.height;
      }
    }
  }
}

enum AppMode {Checklist, Timer};
enum ServiceProvider {Nutaku, Dmm};

static class Glbl {
  static final int W      = 240;
  static final int H      = 320;
  static final int TimerW = 130;
  static final int TimerH = 100;
  static final int DebugW = 240;
  static final int DebugX = W + Scouter.W + 5;

  static AppMode mode     = AppMode.Checklist;
  static AppMode prevMode = AppMode.Timer;
  static ServiceProvider provider = ServiceProvider.Nutaku;
  static boolean isLastOneMinute = false;
  static boolean scouterEnabled  = false;
  static boolean bgSelected      = false;
  static boolean mustRedraw      = false;

  static PSurface  ps;
  static JFrame    jf;
  static PFont     font12;
  static PFont     font24;
  static SoundFile chime;
  static SoundFile joined;
  static PImage    cameraImg;
  static SoundFile coinSound;
  static SoundFile failSound;

  static void setInstances(PSurface _ps, JFrame _jf,
    PFont _f12, PFont _f24, SoundFile _chime, SoundFile _joined,
    PImage _cameraImg, SoundFile _coinSound, SoundFile _failSound) {
    ps     = _ps;
    jf     = _jf;
    font12 = _f12;
    font24 = _f24;
    chime  = _chime;
    joined = _joined;
    cameraImg = _cameraImg;
    coinSound = _coinSound;
    failSound = _failSound;
  }
  static boolean isTimerMode() {
    return mode == AppMode.Timer ? true : false;
  }
  static boolean isChecklistMode() {
    return mode == AppMode.Checklist ? true : false;
  }
  static void setServiceProvider(ServiceProvider _provider) {
    provider = _provider;
  }
  static boolean isDmm() {
    return provider == ServiceProvider.Dmm ? true : false;
  }
  static void changeSize(Dbg dbginst) {
    if (isTimerMode()) {
      jf.setOpacity(0.5f);
      ps.setSize(TimerW, TimerH);
    } else {
      if (scouterEnabled) {
        jf.setOpacity(1.0f);
        int forDebug = dbginst.enabled ? DebugW : 0;
        ps.setSize(W + Scouter.W + forDebug, H);
      } else {
        jf.setOpacity(0.75f);
        ps.setSize(W, H);
      }
    }
  }
  static void setBgSelected(boolean value) {
    bgSelected = value;
  }
  static boolean isBgSelected() {
    return bgSelected;
  }
  static int countHotbits(int value) {
    int result = 0;
    while (value > 0) {
      if ((value & 1) == 1) {
        result++;
      }
      value >>>= 1;
    }
    return result;
  }
  static int calculateExpToMax(int maxLevel, int level, int nextExp) {
    int result = 0;
    if (level == maxLevel) {
      // do nothing
    } else {
      for (int lv = level+1; lv <= maxLevel-1; lv++) {
        result += AccessoryNextExpTable[lv];
      }
      result += nextExp;
    }
    return result;
  }
  static int calculatePiledExp(int nowLevel, int nextExp, int maxLv) {
    int result = 0;
    for (int lv = 1; lv <= nowLevel-1; lv++) {
      result += AccessoryNextExpTable[lv];
    }
    if (nowLevel < maxLv) {
      result += AccessoryNextExpTable[nowLevel] - nextExp;
    }
    return result;
  }
  static int[] calculateLvFromExp(int exp) {
    int lv;
    int total = 0;
    for (lv = 1; lv < AccessoryNextExpTable.length; lv++) {
      total += AccessoryNextExpTable[lv];
      if (exp < total) {
        break;
      }
    }
    int next = total - exp;
    if (next < 0) next = 0;
    int[] tmp = {lv, next};
    return tmp;
  }
  static int calcWeaponExp(int rare, int level) {
    int exp = 0;
    switch (rare) {
      case 0:  exp =   10; break; // R
      case 1:  exp =   35; break; // SR
      case 2:  exp =  100; break; // SSR
      case 3:  exp =   20; break; // R Grail
      case 4:  exp =   50; break; // SR Grail
      case 5:  exp =    0; break; // SSR Grail
      default: break;
    }
    exp *= level;
    switch (rare) {
      case 2:  exp +=  250; break; // SSR
      case 5:  exp += 1500; break; // SSR Grail
      default: break;
    }
    return exp;
  }
  static int calcWeaponExpToMax(WeaponConstant weap,
    int nowLevel, int nextExp) {
    int result = 0;
    if (nowLevel == weap.maxLv) {
      // do nothing
    } else {
      for (int lv = nowLevel+1; lv <= weap.maxLv-1; lv++) {
        int exp = 0;
        switch (weap.rare) {
          case  0: exp =  5; break;
          case  1: exp = 10; break;
          case  2: exp = 20; break;
          default: break;
        }
        result += exp * lv;
      }
      result += nextExp;
    }
    return result;
  }
  static int calcWeaponPiledExp(WeaponConstant weap,
    int nowLevel, int nextExp) {
    int result = 0;
    for (int lv = 1; lv <= nowLevel; lv++) {
      int exp = 0;
      switch (weap.rare) {
        case  0: exp =  5; break;
        case  1: exp = 10; break;
        case  2: exp = 20; break;
        default: break;
      }
      result += exp * lv;
    }
    result -= nextExp;
    return result;
  }
  static int[] calcWeaponLvFromExp(WeaponConstant weap, int exp) {
    int lv;
    int total = 0;
    for (lv = 1; lv <= 30; lv++) {
      int tmp = 0;
      switch (weap.rare) {
        case  0: tmp =  5; break;
        case  1: tmp = 10; break;
        case  2: tmp = 20; break;
        default: break;
      }
      total += tmp * lv;
      if (exp < total) {
        break;
      }
    }
    int next = total - exp;
    if (next < 0) next = 0;
    int[] tmp = {lv, next};
    return tmp;
  }
  static int averageOfImage(PImage img) {
    int r = 0;
    int g = 0;
    int b = 0;
    int w = img.width;
    int h = img.height;
    for (int i = 0; i < w*h; i++) {
      int c = img.pixels[i];
      r += getR(c);
      g += getG(c);
      b += getB(c);
    }
    r /= (w*h);
    g /= (w*h);
    b /= (w*h);
    return (r<<16) + (g<<8) + b;
  }
}

class GuiItem {
  int x;
  int y;
  int itemWidth;
  int itemHeight;
  GuiItem(int _x, int _y, int _itemWidth, int _itemHeight) {
    x = _x;
    y = _y;
    itemWidth  = _itemWidth;
    itemHeight = _itemHeight;
  }
  GuiItem(int _x, int _y) {
    this(_x, _y, 0, 0);
  }
  GuiItem() {
    this(0, 0, 0, 0);
  }
  void render() {
  }
  void press() {
  }
  void wheel(MouseEvent event) {
  }
  boolean isOver() {
    int windowX = Glbl.jf.getX();
    int windowY = Glbl.jf.getY(); 
    int maxX = windowX + Glbl.W + (Glbl.scouterEnabled ? Scouter.W : 0);
    int maxY = windowY + Glbl.H;
    boolean rc = false;
    PointerInfo info = MouseInfo.getPointerInfo();
    // #26 When returning from sleep, PointerInfo may become null.
    if (info != null) {
      Point mouse = info.getLocation();
      if (windowX<=mouse.x && mouse.x<maxX && windowY<=mouse.y && mouse.y<maxY) {
        int mx = mouseX;
        int my = mouseY;
        rc = x<=mx && mx<x+itemWidth && y<=my && my<y+itemHeight;
      }
    }
    if (rc) {
      Glbl.setBgSelected(false);
    }
    return rc;
  }
}

class TimerDisplay extends GuiItem {
  int endTime;
  static final int W = 70;
  static final int H = 30;
  TimerDisplay(int _x, int _y) {
    super(_x, _y, W, H);
    endTime = 0;
  }
  void start(int seconds) {
    endTime = millis() + 1000*seconds;
    Glbl.mode = AppMode.Timer;
  }
  void render() {
    int currentTime = millis();
    int remainTime = endTime - currentTime;
    if (remainTime <= 0) {
      Glbl.mode = AppMode.Checklist;
      Glbl.chime.play();
    } else {
      strokeWeight(2); // Fixed a bug of #5
      stroke(128);     // Fixed a bug of #5
      fill(isOver() ? 64 : 0);
      rect(x, y, W-1, H-1);
      String msg = "000.0";
      fill(0, 255, 0);
      int sec     = remainTime / 1000;
      int decimal = (remainTime / 100) % 10;
      msg = nf(sec, 3) + "." + str(decimal);
      textFont(Glbl.font24);
      textSize(24);
      text(msg, x+5, y+22);
    }
  }
  void press(){
    if (isOver()) {
      Glbl.mode = AppMode.Checklist;
    }
  }
}

class TimerBar extends GuiItem {
  TimerDisplay td;
  static final int W =  64;
  static final int H = Glbl.H-10;
  TimerBar(int _x, int _y, TimerDisplay _td){
    super(_x, _y, W, H);
    td = _td;
  }
  void render(){
    stroke(224);
    strokeWeight(1);
    fill(255); // Left side
    triangle(x,y, x,y+H, x+W-1,y+H-1);
    fill(240); // Right side
    triangle(x,y, x+W,y,  x+W-1,y+H-1);

    if (isOver()) {
      fill(0, 255, 0);
      triangle(x,y, x,mouseY, x+mouseY/5,mouseY);
    }
  }
  int computeSeconds() {
    return (mouseY-y)/7*5 + 25;
  }
  void press() {
    if (isOver()) {
      td.start(computeSeconds());
    }
  }
}

class TimerBarLabel extends GuiItem {
  TimerBar tb;
  TimerBarLabel(TimerBar _tb) {
    super();
    tb = _tb;
  }
  void render() {
    if (tb.isOver()) {
      String msg = str(tb.computeSeconds()) + "sec";
      int y = mouseY;
      if (y < 20) {
        y = 20;
      }
      textFont(Glbl.font24);
      textSize(24);
      fill(64);
      text(msg, tb.x+15+mouseY/5+2, y+2); // shadow
      fill(0, 255, 0);
      text(msg, tb.x+15+mouseY/5, y);
    }
  }
}

// Thanks to https://forum.processing.org/two/discussion/4849/checkbox
class Checkbox extends GuiItem {
  boolean checked;
  static final int W = 20;
  static final int H = 20;
  Checkbox(int _x, int _y, boolean _checked) {
    super(_x, _y, W, H);
    checked = _checked;
  }
  void render() {
    stroke(0); // color of box's flame
    strokeWeight(1);
    fill(isOver()?224:255); // color of box
    rect(x, y, W-1, H-1);
    if (checked) {
      stroke(255, 0, 0); // color of v
      strokeWeight(2);
      line(x+2, y+10, x+10, y+15);
      line(x+10, y+15, x+17, y+3);
    }
  }
  void press() {
    if (isOver()) {
      checked=!checked;
    }
  }
  boolean get() {
    return checked;
  }
  void set() {
    checked = true;
  }
  void reset() {
    checked = false;
  }
}

class Mission extends GuiItem {
  String name;
  Checkbox[] boxes;
  Mission(int _x, int _y, String _name, int _items, int _value) {
    super();
    name  = _name;
    boxes = new Checkbox[_items];
    x = _x;
    y = _y;
    for (int i=0; i< boxes.length; i++) {
      boxes[i] = new Checkbox(x + 110 + 25*i, y, (i < _value));
    }
  }
  boolean isOver() {
    return false;
  }
  void render() {
    boolean isCurrent = false;
    for (Checkbox box : boxes) {
      box.render();
      isCurrent = isCurrent ? true : box.isOver();
    }
    textFont(Glbl.font12);
    textSize(12);
    if (isCurrent) {
      fill(128);
      text(name, x, y+15);
      fill(0);
      text(name, x-1, y+15-1);
    } else {
      fill(0);
      text(name, x, y+15);
    }
  }
  void press(){
    for (int i=0; i< boxes.length; i++) {
      boxes[i].press();

      // Chain reaction
      if (boxes[i].get() == true) {
        for (int j = i-1; j >= 0; j--) {
          boxes[j].set();
        }
      } else {
        for (int j = i+1; j < boxes.length; j++) {
          boxes[j].reset();
        }
      }
    }
  }
  int getValue() {
    int i;
    for (i=boxes.length-1; i>=0; i--) {
      if (boxes[i].get() == true) {
        break;
      }
    }
    return i+1;
  }
}

class Valuebox extends GuiItem {
  static final int W = 30;
  static final int H = 20;
  int value;
  int lowerLimit;
  int threshold;
  int upperLimit;
  LabeledButton plusB;
  LabeledButton minusB;
  Valuebox(int _x, int _y,
    int _lowerLimit, int _threshold, int _upperLimit) {
    super(_x, _y, W, H);
    value      = _lowerLimit;
    lowerLimit = _lowerLimit;
    threshold  = _threshold;
    upperLimit = _upperLimit;
    plusB      = new LabeledButton(_x + W, _y, "+", this);
    minusB     = new LabeledButton(_x + W + LabeledButton.W, _y, "-", this);
  }
  Valuebox(int _x, int _y,
    int _lowerLimit, int _threshold, int _upperLimit, int _value) {
    super(_x, _y, W, H);
    value      = _value;
    lowerLimit = _lowerLimit;
    threshold  = _threshold;
    upperLimit = _upperLimit;
    plusB      = new LabeledButton(_x + W, _y, "+", this);
    minusB     = new LabeledButton(_x + W + LabeledButton.W, _y, "-", this);
  }
  void render() {
    plusB.render();
    minusB.render();

    stroke(0); // color of box's flame
    strokeWeight(1);
    fill(isOver()?224:255); // color of box
    rect(x, y, W-1, H-1);
    textFont(Glbl.font24);
    textSize(16);
    if (value > threshold) {
      fill(255, 0, 0);
    } else {
      fill(0);
    }
    text(nfspc(value, 2), x + 6, y + 16);
  }
  void press() {
    if (plusB.isOver()) {
      up();
    } else if (minusB.isOver()) {
      down();
    }
  }
  void up() {
    value++;
    if (value > upperLimit) value = upperLimit;
  }
  void down() {
    value--;
    if (value < lowerLimit) value = lowerLimit;
  }
  void wheel(MouseEvent event) {
    if (isOver() || plusB.isOver() || minusB.isOver()) {
      float count = (int)event.getCount();
      if (count < 0) {
        up();
      } else if (count > 0) {
        down();
      }
    }
  }
  void setValue(int _value) {
    value = _value;
  }
  int getValue() {
    return value;
  }
  void setThreshold(int _threshold) {
    threshold = _threshold;
  }
}

class StrValuebox extends GuiItem {
  static final int H = 20;
  String[] strs;
  int value;
  int limit;
  Valuebox vbox;
  int[] thresholds;
  int width;
  StrValuebox(int _x, int _y, String[] _strs, int _value,
    Valuebox _vbox, int[] _thresholds, int _width) {
    super(_x, _y, _width, H);
    strs       = _strs;
    value      = _value;
    limit      = _strs.length - 1;
    vbox       = _vbox;
    thresholds = _thresholds;
    width      = _width;
    if (vbox != null) {
      vbox.setThreshold(thresholds[value]);
    }
  }
  StrValuebox(int _x, int _y, String[] _strs, int _value) {
    this(_x, _y, _strs, _value, null, null, 40);
  }
  StrValuebox(int _x, int _y, String[] _strs, int _value,
    Valuebox _vbox, int[] _thresholds) {
    this(_x, _y, _strs, _value, _vbox, _thresholds, 40);
  }
  void render() {
    stroke(0); // color of box's flame
    strokeWeight(1);
    fill(isOver()?224:255); // color of box
    rect(x, y, width-1, H-1);
    textFont(Glbl.font24);
    textSize(16);
    fill(0);
    text(strs[value], x + 6, y + 16);
  }
  void up() {
    value++;
    if (value > limit) value = 0;
    if (vbox != null) {
      vbox.setThreshold(thresholds[value]);
    }
  }
  void down() {
    value--;
    if (value < 0) value = limit;
    if (vbox != null) {
      vbox.setThreshold(thresholds[value]);
    }
  }
  void press() {
    if (isOver()) {
      if (mouseButton == LEFT) {
        up();
      } else if (mouseButton == RIGHT) {
        down();
      }
    }
  }
  void wheel(MouseEvent event) {
    if (isOver()) {
      float count = (int)event.getCount();
      if (count < 0) {
        up();
      } else if (count > 0) {
        down();
      }
    }
  }
  void setValue(int _value) {
    value = _value;
  }
  int getValue() {
    return value;
  }
}

class LabeledButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  String label;
  Valuebox box;
  LabeledButton(int _x, int _y, String _label, Valuebox _box) {
    super(_x, _y, W, H);
    label = _label;
    box   = _box;
  }
  void render() {
    stroke(64); // color of box's flame
    strokeWeight(1);
    fill(isOver()?180:200); // color of box
    rect(x, y, W-1, H-1);
    textFont(Glbl.font24);
    textSize(24);
    fill(0);
    text(label, x + 4, y + 16);
  }
}

class AcceRow extends GuiItem {
  StrValuebox   elemBox;
  StrValuebox   rareBox;
  Valuebox      lvBox;
  Valuebox      numBox;
  ArrayList<GuiItem> gitems;
  int x;
  int y;
  final int delta_y = 29;
  AcceRow(int _x, int _y, int _initialElem, int _initialRare) {
    super();
    x = _x;
    y = _y;
    final String[] elem = {"Same", "Diff"};
    final String[] rare = {"N", "R", "SR", "SSR"};
    final int[] lvThresholds = {20, 30, 40, 50};
    elemBox = new StrValuebox(  x +   0, y, elem, _initialElem);
    lvBox   = new Valuebox(     x +  90, y, 1, 99, 50);
    rareBox = new StrValuebox(  x +  45, y, rare, _initialRare,
              lvBox, lvThresholds);
    numBox  = new Valuebox(     x + 170, y, 0, 20, 99);
    gitems  = new ArrayList<GuiItem>();
    gitems.add(elemBox);
    gitems.add(rareBox);
    gitems.add(lvBox);
    gitems.add(numBox);
  }
  boolean isCurrent() {
    for (GuiItem item : gitems) {
      if (item.isOver()) {
        return true;
      }
    }
    return false;
  }
  void render() {
    for (GuiItem item : gitems) {
      item.render();
    }

    textFont(Glbl.font24);
    textSize(16);
    fill(0);
    text(nfspc(getExp(), 5),   x + 245, y + 16);
    text(nfspc(getAcceP(), 5), x + 300, y + 16);
  }
  void press(){
    for (GuiItem item : gitems) {
      item.press();
    }
  }
  void wheel(MouseEvent event) {
    for (GuiItem item : gitems) {
      item.wheel(event);
    }
  }
  int getItems() {
    return numBox.getValue();
  }
  int getExp() {
    int exp = 400;
    exp >>= (3 - rareBox.getValue());
    exp += AccessoryBaitExpTable[lvBox.getValue()];
    if (elemBox.getValue() == 0) {
      exp += ((exp+1)>>1); // x1.5
    }
    return exp * numBox.getValue();
  }
  int getAcceP() {
    int acceP = 0;
    switch (rareBox.getValue()) {
      case 0: acceP =   10; break; // N
      case 1: acceP =   30; break; // R
      case 2: acceP =   88; break; // SR
      case 3: acceP =   99; break; // SSR
      default: break;
    }
    return acceP * numBox.getValue();
  }
}

class AcceTotal {
  final int[] initialElem = {0, 0, 0, 0, 1, 1, 1, 1};
  final int[] initialRare = {3, 2, 1, 0, 3, 2, 1, 0};
  AcceRow [] ar;
  AllClearButton ac;
  AcceTotal() {
    ar = new AcceRow[initialElem.length];
    ac = new AllClearButton(Glbl.W + 240, 275);
    allClear();
  }
  void allClear() {
    for (int i = 0; i < initialElem.length; i++) {
      ar[i] = new AcceRow(Glbl.W + 230, 102 + i*21,
        initialElem[i], initialRare[i]);
    }
  }
  void render(AcceConstant acce, int level, int nextExp) {
    int expToMax = Glbl.calculateExpToMax(acce.maxLv, level, nextExp);
    int piledExp = Glbl.calculatePiledExp(level, nextExp, acce.maxLv);
    textFont(Glbl.font24);
    textSize(16);
    fill(0);
    text(acce.str,                    Glbl.W + 230, 20);
    text("Lv   " + nfspc(level, 3),   Glbl.W + 230, 35);
    text("NEXT " + nfspc(nextExp, 3), Glbl.W + 230, 50);
    text("to  Lv" + nf(acce.maxLv) +
      ": " + (expToMax > 0 ? nfspc(expToMax, 5) + "exp" : "-"),
      Glbl.W + 230, 65);

    int exp = acce.exp;
    exp += AccessoryBaitExpTable[level];
    exp += ((exp+1)>>1); // x1.5
    text("as a bait: " + nfspc(exp, 4) + "exp", Glbl.W + 230, 80);

    textFont(Glbl.font12);
    textSize(12);
    text("Elem    Rare    Lv                Items", Glbl.W + 235, 97);
    text("Exp      AcceP", Glbl.W + 490, 97);

    int items = 0;
    int total = 0;
    int accePtotal = 0;
    for (AcceRow row : ar) {
      row.render();
      items += row.getItems();
      total += row.getExp();
      accePtotal += row.getAcceP();
    }
    ac.render();

    if (total > 0) {
      stroke(0, 255, 0);
      strokeWeight(2);
      fill(224);
      triangle(Glbl.W + 377, 38,
               Glbl.W + 395, 50,
               Glbl.W + 377, 62);

      int[] enhanced = Glbl.calculateLvFromExp(piledExp + total*2);
      int   enhancedLv   = enhanced[0];
      int   enhancedNext = enhanced[1];
      if (enhancedLv >= acce.maxLv) {
        enhancedLv   = acce.maxLv;
        enhancedNext = 0;
      }
      fill(96);
      textFont(Glbl.font24);
      textSize(16);
      text("  Super Lv   " + nfspc(enhancedLv, 3), Glbl.W + 400, 20);
      textSize(14);
      text("NEXT " + nfspc(enhancedNext, 4), Glbl.W + 465, 35);
      enhanced = Glbl.calculateLvFromExp(piledExp + total);
      enhancedLv   = enhanced[0];
      enhancedNext = enhanced[1];
      if (enhancedLv >= acce.maxLv) {
        enhancedLv   = acce.maxLv;
        enhancedNext = 0;
      }
      fill(0);
      textSize(16);
      text("Success Lv   ",                        Glbl.W + 400, 50);
      fill(0, (enhancedLv == acce.maxLv) ? 255 : 0, 0);
      text("             " + nfspc(enhancedLv, 3), Glbl.W + 400, 50);
      textSize(14);
      fill(0);
      text("NEXT " + nfspc(enhancedNext, 4), Glbl.W + 465, 65);

      exp = acce.exp;
      exp += AccessoryBaitExpTable[enhancedLv];
      exp += ((exp+1)>>1); // x1.5
      textSize(16);
      text("as a bait: " + nfspc(exp, 4) + "exp", Glbl.W + 408, 80);
    }

    final int y = 100 + 8*21;
    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text("Total", Glbl.W + 354, y + 18);

    textFont(Glbl.font24);
    textSize(16);
    fill((items > 20) ? 255 : 0, 0, 0);
    text(nfspc(items, 2), Glbl.W + 405, y + 18);

    fill(0, (total >= expToMax) ? 255 : 0, 0);
    text(nfspc(total, 5), Glbl.W + 475, y + 18);
    fill(0);
    text(nfspc(accePtotal, 5), Glbl.W + 530, y + 18);
  }
  void press() {
    for (AcceRow row : ar) {
      row.press();
    }
    if (ac.isOver()) {
      allClear();
    }
  }
  void wheel(MouseEvent event) {
    for (AcceRow row : ar) {
      row.wheel(event);
    }
  }
}

class WeaponRow extends GuiItem {
  StrValuebox   rareBox;
  Valuebox      lvBox;
  Valuebox      numBox;
  ArrayList<GuiItem> gitems;
  int x;
  int y;
  final int delta_y = 29;
  WeaponRow(int _x, int _y, int _initialRare, int _initialLv) {
    super();
    x = _x;
    y = _y;
    final String[] rare = {"R", "SR", "SSR", "Grail", "FalseG", "HolyG"};
    final int[] lvThresholds = {20, 20, 20, 20, 20, 1};
    lvBox   = new Valuebox(     x +  65, y, 1, 20, 20, _initialLv);
    rareBox = new StrValuebox(  x +   0, y, rare, _initialRare,
              lvBox, lvThresholds, 60);
    numBox  = new Valuebox(     x + 145, y, 0, 20, 99);
    gitems  = new ArrayList<GuiItem>();
    gitems.add(rareBox);
    gitems.add(lvBox);
    gitems.add(numBox);
  }
  boolean isCurrent() {
    for (GuiItem item : gitems) {
      if (item.isOver()) {
        return true;
      }
    }
    return false;
  }
  void render() {
    for (GuiItem item : gitems) {
      item.render();
    }

    textFont(Glbl.font24);
    textSize(16);
    fill(0);
    text(nfspc(getExp(), 5), x + 220, y + 16);
  }
  void press(){
    for (GuiItem item : gitems) {
      item.press();
    }
  }
  void wheel(MouseEvent event) {
    for (GuiItem item : gitems) {
      item.wheel(event);
    }
  }
  int getItems() {
    return numBox.getValue();
  }
  int getExp() {
    int exp = Glbl.calcWeaponExp(rareBox.getValue(), lvBox.getValue());
    return exp * numBox.getValue();
  }
}

class WeaponTotal {
  final int[] initialRare = {0, 1, 2, 3, 4, 5};
  final int[] initialLv   = {1, 4, 5, 4, 5, 1};
  WeaponRow [] wr;
  AllClearButton ac;
  WeaponTotal() {
    wr = new WeaponRow[initialRare.length];
    ac = new AllClearButton(Glbl.W + 240, 235);
    allClear();
  }
  void allClear() {
    for (int i = 0; i < initialRare.length; i++) {
      wr[i] = new WeaponRow(Glbl.W + 230, 102 + i*21,
        initialRare[i], initialLv[i]);
    }
  }
  void render(WeaponConstant weap, int level, int nextExp) {
    int expToMax = Glbl.calcWeaponExpToMax(weap, level, nextExp);
    int piledExp = Glbl.calcWeaponPiledExp(weap, level, nextExp);
    textFont(Glbl.font24);
    textSize(16);
    fill(0);
    text(weap.str,                    Glbl.W + 230, 20);
    text("SL   " + nfspc(level, 3),   Glbl.W + 230, 35);
    text("NEXT " + nfspc(nextExp, 3), Glbl.W + 230, 50);
    text("to  SL" + nf(weap.maxLv) +
      ": " + (expToMax > 0 ? nfspc(expToMax, 5) + "exp" : "-"),
      Glbl.W + 230, 65);

    //text("as a bait: " + nfspc(exp, 4) + "exp", Glbl.W + 230, 80);

    textFont(Glbl.font12);
    textSize(12);
    text("Rare          Lv                Items", Glbl.W + 230, 97);

    int items = 0;
    int total = 0;
    for (WeaponRow row : wr) {
      row.render();
      items += row.getItems();
      total += row.getExp();
    }
    ac.render();

    if (total > 0) {
      stroke(0, 255, 0);
      strokeWeight(2);
      fill(224);
      triangle(Glbl.W + 377, 38,
               Glbl.W + 395, 50,
               Glbl.W + 377, 62);

      int[] enhanced = Glbl.calcWeaponLvFromExp(weap, piledExp + total);
      int   enhancedLv   = enhanced[0];
      int   enhancedNext = enhanced[1];
      if (enhancedLv >= weap.maxLv) {
        enhancedLv   = weap.maxLv;
        enhancedNext = 0;
      }
      fill(0);
      textSize(16);
      text("Success Lv   ",                        Glbl.W + 400, 50);
      fill(0, (enhancedLv == weap.maxLv) ? 255 : 0, 0);
      text("             " + nfspc(enhancedLv, 3), Glbl.W + 400, 50);
      textSize(14);
      fill(0);
      text("NEXT " + nfspc(enhancedNext, 4), Glbl.W + 465, 65);

      //textSize(16);
      //text("as a bait: " + nfspc(exp, 4) + "exp", Glbl.W + 408, 80);
    }

    final int y = 100 + 6*21;
    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text("Total", Glbl.W + 329, y + 18);

    textFont(Glbl.font24);
    textSize(16);
    fill((items > 20) ? 255 : 0, 0, 0);
    text(nfspc(items, 2), Glbl.W + 380, y + 18);

    fill(0, (total >= expToMax) ? 255 : 0, 0);
    text(nfspc(total, 5), Glbl.W + 450, y + 18);
  }
  void press() {
    for (WeaponRow row : wr) {
      row.press();
    }
    if (ac.isOver()) {
      allClear();
    }
  }
  void wheel(MouseEvent event) {
    for (WeaponRow row : wr) {
      row.wheel(event);
    }
  }
}

class CloseButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  CloseButton(int _x, int _y){
    super(_x, _y, W, H);
  }
  void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of x
    strokeWeight(2);
    line(x+3, y+3, x+16, y+16);
    line(x+3, y+16, x+16, y+3);
  }
  void press(){
    if (isOver()) {
      exit();
    }
  }
}

class MinimizeButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  MinimizeButton(int _x, int _y){
    super(_x, _y, W, H);
  }
  void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of _
    strokeWeight(2);
    line(x+3, y+12, x+16, y+12);
  }
  void press() {
    if (isOver()) {
      Glbl.jf.setExtendedState(
        Glbl.jf.getExtendedState() | JFrame.ICONIFIED);
    }
  }
}

class CameraButton extends GuiItem {
  static final int W = 20;
  static final int H = 20;
  Scouter sco;
  CameraButton(int _x, int _y, Scouter _sco){
    super(_x, _y, W, H);
    sco = _sco;
  }
  void render() {
    noStroke();
    fill(isOver()?255:224); // color of box
    rect(x, y, W-1, H-1);
    if (!isOver()) {
      tint(255,192);
    }
    image(Glbl.cameraImg, x+2, y+2);
    tint(255);
  }
  void press() {
    if (isOver()) {
      if (sco.takePicture()) {
        Glbl.coinSound.play();
      } else {
        Glbl.failSound.play();
      }
    }
  }
}

class WallClock extends GuiItem {
  ZoneId zid;
  int[] gemTimeTable;
  static final int W = 82;
  static final int H = 20;
  WallClock(ZoneId _zid) {
    super(Glbl.W - W - 1, Glbl.H - H - 1, W, H);
    zid = _zid;
    gemTimeTable = new int[DmmGemTimeTable.length];
    float[] tbl = DmmGemTimeTable; // "Asia/Tokyo"
    if (zid.equals(ZoneId.of("America/Los_Angeles"))) {
      tbl = NutakuGemTimeTable;
    }
    for (int i = 0; i < DmmGemTimeTable.length; i++) {
      gemTimeTable[i] = int(tbl[i]*100);
    }
  }
  void render() {
    Instant currentTime = Instant.now();
    ZonedDateTime zoneTime = currentTime.atZone(zid);

    int w      = zoneTime.getDayOfWeek().getValue();
    int hour   = zoneTime.getHour();
    int minute = zoneTime.getMinute();
    boolean isGemTime = false;
    if (!Glbl.isDmm()) {
      int nowHour = hour*100 + minute*100/60;
      Glbl.isLastOneMinute = false;
      for (int i = 0; i < 3; i++) {
        int gemHour = gemTimeTable[(w-1)*3 + i];
        if (gemHour <= nowHour && nowHour < gemHour + 50) {
          isGemTime = true;
          if (minute == 29 || minute == 59) {
            Glbl.isLastOneMinute = true;
          }
          break;
        }
      }
    }

    if (isOver()) {
      zoneTime = currentTime.atZone(ZoneId.systemDefault());
      hour   = zoneTime.getHour();
      minute = zoneTime.getMinute();
    }

    strokeWeight(2);
    stroke(128);
    if (isGemTime) {
      fill(255,255,0);
    } else {
      fill(0);
    }
    rect(x, y, W-1, H-1);

    if (isGemTime) {
      fill(0);
    } else {
      fill(0,255,0);
    }
    textFont(Glbl.font24);
    textSize(12);
    String dow = zoneTime.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.US);
    int    day = zoneTime.getDayOfMonth();
    String msg = dow;
    if (!Glbl.isLastOneMinute) {
      msg += " " + ((day<10) ? " " : "") + str(day);
      msg += " " + nf(hour, 2) + ":" + nf(minute, 2);
      text(msg, x+4, y+14);
    } else {
      int second = zoneTime.getSecond();
      msg += " " + nf(hour, 2) + ":" + nf(minute, 2) + ":" + nf(second, 2);
      text(msg, x+4, y+14);
    }

    if (!Glbl.isDmm() && isOver()) {
      putGemQuestTable(currentTime);
    } else {
      // put Timezone
      textFont(Glbl.font12);
      textSize(12);
      fill(0);
      textAlign(RIGHT);
      text(zid.toString(), x-2, Glbl.H-8);
      textAlign(LEFT);
    }
  }
  void press() {
  }
  void putGemQuestTable(Instant currentTime) {
    ZonedDateTime zoneTime = currentTime.atZone(zid);

    // Round the current time to the 15 minute boundary.
    // Because 15 minutes are the smallest unit of time difference.
    final int boundary = 15;
    int m = zoneTime.getMinute();
    if (m % boundary > 0) {
      zoneTime = zoneTime.plusMinutes(boundary - (m % boundary));
    }
    
    // draw background
    stroke(128);
    strokeWeight(1);
    fill(0);
    rect(6, 86, Glbl.W-9, 201);
    fill(255);
    rect(5, 85, Glbl.W-10, 200);
    int msgY = 100;
    final int msgDeltaY = 20;

    // build localGemTimeTable
    final int minutesPerWeek = 7*24*60;
    boolean first = true;
    // This table is 4x7 because in some areas quests can occur 4 times a day.
    int[] localGemTimeTable = new int[4*7];
    for (int i = 0; i < 4*7; i++) {
      localGemTimeTable[i] = Integer.MAX_VALUE;
    }
    for (int i = 0; i < minutesPerWeek/boundary; i++) { 
      int w      = zoneTime.getDayOfWeek().getValue();
      int hour   = zoneTime.getHour();
      int minute = zoneTime.getMinute();

      int nowHour = hour*100 + minute*100/60;
      for (int j = 0; j < 3; j++) {
        int gemHour = gemTimeTable[(w-1)*3 + j];
        if (gemHour == nowHour) {
          Instant t = Instant.from(zoneTime);
          ZonedDateTime local = t.atZone(ZoneId.systemDefault());
          int lw = local.getDayOfWeek().getValue();
          int lh = local.getHour();
          int lm = local.getMinute();
          int found = lh*100+lm*100/60;
          if (first) {
            found += 1; // Marking
            first = false;
          }
          int index = (lw-1)*4;
          int[] part = {found,
            localGemTimeTable[index+0],
            localGemTimeTable[index+1],
            localGemTimeTable[index+2],
            localGemTimeTable[index+3]};
          java.util.Arrays.sort(part);
          System.arraycopy(part, 0, localGemTimeTable, index, 4);
        }
      }
      zoneTime = zoneTime.plusMinutes(boundary);
    }

    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text("Gem Quest on your Timezone", 10, msgY);
    msgY += msgDeltaY*1.5;
    for (int w = 1; w <= 7; w++) {
      String msg = DayOfWeek.of(w).getDisplayName(TextStyle.SHORT, Locale.US);
      msg += " ";
      for (int i = 0; i < 4; i++) {
        int index = (w-1)*4 + i;
        int value = localGemTimeTable[index];
        if (value != Integer.MAX_VALUE) {
          int lh = value/100;
          int lm = (value%100)*60/100;
          if (value%10 == 1) {
            msg += ">" + nf(lh, 2) + ":" + nf(lm, 2) + "<";
          } else {
            msg += " " + nf(lh, 2) + ":" + nf(lm, 2) + " ";
          }
        }
      }
      textFont(Glbl.font24);
      textSize(12);
      fill(0);
      text(msg, 15, msgY);
      msgY += msgDeltaY;
    }
    textFont(Glbl.font12);
    textSize(12);
    fill(0);
    text(ZoneId.systemDefault().toString(), x-55, y-20);
    stroke(0);
    strokeWeight(2);
    line(x-10, y-15, x, y-3);
  }
}

class LvHpPosition {
  int lvX;
  int lvY;
  int hpX;
  int hpY;
  int hpPix;
  LvHpPosition(int _lvX, int _lvY, int _hpX, int _hpY, int _hpPix) {
    lvX   = _lvX;
    lvY   = _lvY;
    hpX   = _hpX;
    hpY   = _hpY;
    hpPix = _hpPix;
  }
}

class AcceConstant {
  String str;
  int    maxLv;
  int    r;
  int    g;
  int    b;
  int    exp;
  AcceConstant(String _str, int _maxLv, int _r, int _g, int _b, int _exp) {
    str   = _str;
    maxLv = _maxLv;
    r     = _r;
    g     = _g;
    b     = _b;
    exp   = _exp;
  }
}

class AccePosition {
  int offsetX;
  int offsetY;
  int w;
  int h;
  int rarelityX;
  int rarelityY;
  int levelX;
  int levelY;
  int nextX;
  int nextY;
  AccePosition(int _offsetX, int _offsetY, int _w, int _h,
    int _rarelityX, int _rarelityY, int _levelX, int _levelY,
    int _nextX, int _nextY) {
    offsetX   = _offsetX;
    offsetY   = _offsetY;
    w         = _w;
    h         = _h;
    rarelityX = _rarelityX;
    rarelityY = _rarelityY;
    levelX    = _levelX;
    levelY    = _levelY;
    nextX     = _nextX;
    nextY     = _nextY;
  }
}

class WeaponConstant {
  String str;
  int    maxLv;
  int    r;
  int    g;
  int    b;
  int    rare;
  WeaponConstant(String _str, int _maxLv, int _r, int _g, int _b, int _rare) {
    str   = _str;
    maxLv = _maxLv;
    r     = _r;
    g     = _g;
    b     = _b;
    rare  = _rare;
  }
}

class WeaponPosition {
  int slX;
  int slY;
  int rarelityX;
  int rarelityY;
  int nextX;
  int nextY;
  WeaponPosition(int _slX, int _slY,
    int _rarelityX, int _rarelityY, int _nextX, int _nextY) {
    slX       = _slX;
    slY       = _slY;
    rarelityX = _rarelityX;
    rarelityY = _rarelityY;
    nextX     = _nextX;
    nextY     = _nextY;
  }
}

class Scouter {
  static final int W = 600;
  Robot          bot;
  Rectangle      area;
  PImage         fullImg;
  PImage         windowImg;
  WritableRaster wr;
  PImage         hpImg;
  int            hpDisplayHysteresis;
  String         name;
  String         ver;
  AcceTotal      accetotal;
  WeaponTotal    weapontotal;
  boolean        acceFound;
  boolean        weaponFound;
  boolean        weeklyAccePFound;
  boolean        isKamihimeWindowFound;
  int            prevPlayers;
  Valuebox       playersBox;
  boolean        playersBoxVisible;
  boolean        initialized;
  int            weeklyAcceP;

  // Enemie's Lv & HP bar
  final LvHpPosition[] lvhppos = {
    //              lvX lvY  hpX hpY  hpPix
    new LvHpPosition(215, 16, 205, 32, 358), //  1 enemy
    new LvHpPosition(165, 31, 155, 47, 177), //  2 enemies A
    new LvHpPosition(466, 31, 455, 47, 177), //  2 enemies B
    new LvHpPosition(291, 16, 280, 32, 177), //  3 enemies A
    new LvHpPosition(116, 66, 105, 82, 177), //  3 enemies B
    new LvHpPosition(466, 66, 455, 82, 177), //  3 enemies C
  };

  final AccePosition[] accepos = {
    // Nutaku        offX offY  w    h    rX   rY   lX   lY    nX   nY
    new AccePosition(687, 103, 220, 349, 152, 135, 196,   3,  111, 301),
    new AccePosition(264,  15, 355, 300,  41,  11,  49, 280,  301, 283),
    // DMM
    new AccePosition(687, 103, 220, 349, 152, 135, 196,   4,  110, 301),
    new AccePosition(264,  15, 355, 300,  41,  11,  49, 283,  315, 285),
  };

  final AcceConstant[] acceconst = {
    //                str  maxLv r    g    b   exp
    new AcceConstant("SSR", 50, 183, 131,  75, 400),
    new AcceConstant("SR",  40, 117, 133, 148, 200),
    new AcceConstant("R",   30, 152,  82,  51, 100),
    new AcceConstant("N",   20, 132, 117, 103,  50),
  };

  final WeaponPosition weaponpos =
    //                 offX offY rX   rY   nX   nY
    new WeaponPosition(753, 258, 152, 135, 795, 411);

  final WeaponConstant[] weaponconst = {
    //                  str  maxLv r    g    b   rare
    new WeaponConstant("R",   20, 152,  82,  51, 0),
    new WeaponConstant("SR",  20, 117, 133, 148, 1),
    new WeaponConstant("SSR", 20, 183, 131,  75, 2),
  };

  int[]       hppos;
  int[]       hp;
  Scouter(String _name, String _ver) {
    name     = _name;
    ver      = _ver;
    isKamihimeWindowFound = false;
    accetotal   = new AcceTotal();
    weapontotal = new WeaponTotal();
    acceFound   = false;
    weaponFound = false;
    weeklyAccePFound = false;
    prevPlayers = 0;
    playersBox = new Valuebox(Glbl.W + 87, 132, 0, 99, 30);
    playersBoxVisible = false;
    initialized = false;
    weeklyAcceP = 0;
  }
  boolean initialize() {
    boolean result = false;
    hpDisplayHysteresis = 0;
    hppos = new int[lvhppos.length];
    hp    = new int[lvhppos.length];
    try {
      bot    = new Robot();
      area   = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
      result = true;
      initialized = true;
    } catch (java.awt.AWTException e) {
      // do nothing
    }
    return result;
  }
  void findKamihimeWindow() {
    isKamihimeWindowFound = false;

    // Thanks to https://junkato.jp/ja/blog/2013/01/28/processing-efficient-copy-from-bufferedimage-to-pimage/
    BufferedImage bimg = bot.createScreenCapture(area);
    if (fullImg == null) {
      fullImg = new PImage(bimg);
      DataBufferInt dbi = new DataBufferInt(
        fullImg.pixels, fullImg.pixels.length);
      wr = Raster.createWritableRaster(
        bimg.getSampleModel(), dbi, new Point(0, 0));
      fullImg.loadPixels();
    } else {
      bimg.copyData(wr);
      fullImg.updatePixels();
    }

    // find Top-Left of Game Window
    // Black, White, Black, White, Black, White, White, Black
    final int verticalPixels = 8;
    final int maxX = fullImg.width - 1;
    final int maxY = fullImg.height - verticalPixels;
    int px = 0;
    int py = 0;
    for (px = 0; px <= maxX; px++) {
      for (py = 0; py <= maxY; py++) {
        int[] g = new int[verticalPixels];
        for (int i = 0; i < verticalPixels; i++) {
          // G component is used for brightness judgment.
          int c = fullImg.pixels[fullImg.width * (py+i) + px];
          g[i] = getG(c);
          if (i == 0 && !(g[0] < 40 && g[0] != 0)) {
            break;
          }
        }
        if (g[0] <  40 && g[0] != 0 &&
            g[1] > 200 &&
            g[2] <  40 &&
            g[3] > 200 &&
            g[4] <  40 &&
            g[5] > 200 &&
            g[6] > 200 &&
            g[7] <  40) {
          windowImg = fullImg.get(px-142, py+8, 960, 640);
          isKamihimeWindowFound = true;
          return;
        }
      }
    }
  }
  boolean searchHpBar() {
    int lvCount = 0;
    boolean[] found = new boolean[lvhppos.length];

    dbg.txt("search hpbar");
    for (int i = 0; i < lvhppos.length; i++) {
      PImage lvImg = windowImg.get(
        lvhppos[i].lvX, lvhppos[i].lvY, OcrV.w, OcrV.h);

      int rc = OcrV.ocr(lvImg);
      dbg.img(lvImg, OcrV.w*2*i);
      if (0 <= rc && rc <= 1) {
        found[i] = true;
        lvCount++;
        // If 'v' is found when i==0, the number of HP bars is 1.
        if (i == 0) break;
      }
    }
    if (lvCount > 0) {
      for (int i = 0; i < hp.length; i++) {
        hp[i] = 0;
      }
    }

    for (int i = 0; i < lvhppos.length; i++) {
      if (!found[i]) continue;
      for (int x = lvhppos[i].hpPix-1; x >= 0; x--) {
        int c = windowImg.pixels[windowImg.width*lvhppos[i].hpY +
          lvhppos[i].hpX + x];
        int r = getR(c);
        int g = getG(c);
        int b = getB(c);
        // search red bar
        if (r > 240 && g < 40 && b < 90) {
          double tmp = (double)(x*100) / (lvhppos[i].hpPix-1);
          hp[i]  = (int)Math.ceil(tmp);
          hppos[i] = x;
          dbg.pln("hp:" + hp[i] + " x:" + x);
          break;
        }
      }
    }

    final int deltaX = 48;
    final int deltaY = 10;

    if (lvCount > 0) {
      hpDisplayHysteresis = 5;
      hpImg = windowImg.get(deltaX, deltaY, Scouter.W, 116);
      hpImg.filter(GRAY);
      image(hpImg, Glbl.W, 0);
    } else if (hpDisplayHysteresis > 0) {
      hpDisplayHysteresis--;
      image(hpImg, Glbl.W, 0);
    } else {
      return false;
    }

    for (int i = 0; i < lvhppos.length; i++) {
      if (hp[i] > 0) {

        strokeWeight(1);

        final int orgX  = Glbl.W + lvhppos[i].hpX - deltaX;
        final int orgY  = lvhppos[i].hpY - deltaY;
        final int hpPix = lvhppos[i].hpPix;

        if (dbg.enabled) {
          stroke(255, 0, 0); // color of lines
          line(orgX, orgY, orgX + hpPix-1, orgY);
        }

        stroke(0, 255, 0); // color of lines

        // current position
        line(orgX + hppos[i], orgY, orgX + hppos[i], orgY+12);

        // draw |------|
        line(orgX,           orgY+4, orgX,           orgY+9);
        line(orgX,           orgY+6, orgX+hpPix-1,   orgY+6);
        line(orgX+hpPix-1,   orgY+4, orgX+hpPix-1,   orgY+9);

        // draw 50% marker
        line(orgX+hpPix/2,   orgY+4, orgX+hpPix/2,   orgY+9);

        // draw 30% marker
        line(orgX+hpPix*0.3, orgY+4, orgX+hpPix*0.3, orgY+9);

        textFont(Glbl.font12);
        textSize(12);
        fill(0);
        text(nf(hp[i]) + "%", orgX + hpPix - 25+1, orgY+3);
        fill(0, 255, 0);
        text(nf(hp[i]) + "%", orgX + hpPix - 25+0, orgY+2);
      }
    }

    int players = 0;
    playersBoxVisible = false;
    dbg.txt("detect players");
    for (int i = 0; i < 2; i++) {
      PImage playersImg = windowImg.get(
        723 + (OcrPlayers.w+1)*i, 10, OcrPlayers.w, OcrPlayers.h);
      dbg.img(playersImg, 10*i);
      int num = OcrPlayers.ocr(playersImg);
      if (num < 0) {
        break;
      } else if (num < 10) {
        players = players*10 + num;
      }
    }
    if (players > 0) {
      playersBoxVisible = true;
      textFont(Glbl.font12);
      textSize(12);
      fill(0);
      text("Bells when", Glbl.W + 20, 150);
      text("or more participants joined.", Glbl.W + 165, 150);
      playersBox.render();
      dbg.pln("players : " + players);
      int threshold = playersBox.getValue();
      if (players >= threshold && threshold > prevPlayers) {
        Glbl.joined.play();
      }
      prevPlayers = players;
    }

    return true;
  }
  boolean searchAccessory() {
    for (int view = 0; view < accepos.length; view++) {
      dbg.txt("search accessory");
      AccePosition apos = accepos[view];
      PImage acImg = windowImg.get(
        apos.offsetX, apos.offsetY,
        apos.w, apos.h);

      // Lv
      OcrEngine2 engine = OcrEnhLv;
      if (view == 1 || view == 3) {
        engine = OcrInfoLv;
      } else if (view == 2) {
        engine = OcrDmmEnhLv;
      }
      int w = engine.w;
      int h = engine.h;
      int level = 0;
      for (int i = 0; i < 2; i++) {
        PImage numImg = acImg.get(
          apos.levelX + i*engine.charfeed, apos.levelY,
          w, h);
        dbg.img(numImg, i*(w+4));
        int num = engine.ocr(numImg);
        if (num < 0) {
          break;
        } else if (num < 10) {
          level = level*10 + num;
        }
      }
      if ((view == 0 || view == 2) && level == 0) {
        // for Lv 1 to 9
        PImage numImg = acImg.get(
          apos.levelX+5, apos.levelY, w, h);
        dbg.img(numImg, 2*(w+4));
        int num = engine.ocr(numImg);
        if (num < 0) {
          // do nothing
        } else if (num < 10) {
          level = num;
        }
      }

      // NEXT
      engine = OcrEnhNext;
      if (view == 1) {
        engine = OcrInfoNext;
      } else if (view == 2) {
        engine = OcrDmmEnhNext;
      } else if (view == 3) {
        engine = OcrDmmInfoNext;
      }
      w = engine.w;
      h = engine.h;
      int nextExp = 0;
      for (int i = 0; i < 4; i++) {
        PImage numImg = acImg.get(
          apos.nextX + i*engine.charfeed, apos.nextY, w, h);
        dbg.img(numImg, (4+i)*(w+4));
        int num = engine.ocr(numImg);
        if (num < 0) {
          break;
        } else if (num < 10) {
          nextExp = nextExp*10 + num;
        }
      }

      // Rarelity
      AcceConstant acce = acceconst[0];
      PImage iconImg = acImg.get(apos.rarelityX, apos.rarelityY, 15, 15);
      int c = Glbl.averageOfImage(iconImg);
      int r, g, b;
      r = getR(c);
      g = getG(c);
      b = getB(c);
      for (int i = 0; i < acceconst.length; i++) {
        if (
          acceconst[i].r-10 <= r && r <= acceconst[i].r+10 &&
          acceconst[i].g-10 <= g && g <= acceconst[i].g+10 &&
          acceconst[i].b-10 <= b && b <= acceconst[i].b+10) {
          acce = acceconst[i];
          break;
        }
      }

      if (level == acce.maxLv || (level > 0 && nextExp > 0)) {
        acImg.filter(GRAY);
        if (view == 0 || view == 2) {
          image(acImg, Glbl.W, 0);
        } else {
          PImage trimImg = acImg.get(105, 0, accepos[0].w, apos.h);
          PImage lvImg   = acImg.get( 27, apos.h-24, accepos[0].w, 24);
          PImage nextImg = acImg.get(251, apos.h-24, 320,    24);
          image(trimImg, Glbl.W, 0);
          image(lvImg,   Glbl.W,       apos.h-24);
          image(nextImg, Glbl.W + 115, apos.h-24);
        }

        accetotal.render(acce, level, nextExp);

        String msg = "Weekly Accessory P : ";
        if (weeklyAccePFound) {
          msg += weeklyAcceP;
        } else {
          msg += '?';
        }
        textFont(Glbl.font12);
        textSize(12);
        fill(0);
        text(msg + " / 2500", Glbl.W + 300, 308);

        dbg.pln("Rarelity icon RGB: " + nf(r) + " " + nf(g) + " " + nf(b));
        dbg.txt("rarelity icon");
        if (dbg.enabled) {
          strokeWeight(1);
          stroke(255, 0, 0);
          line(Glbl.W + apos.rarelityX, apos.rarelityY,
            Glbl.W + apos.rarelityX + 10, apos.rarelityY + 10);
          dbg.img(iconImg, 0);
        }
        return true;
      }
    }

    return false;
  }
  boolean searchWeapon() {
    // SL
    int level = 0;
    int w = OcrSkillLv.w;
    int h = OcrSkillLv.h;
    dbg.txt("search weapon");
    for (int i = 0; i < 2; i++) {
      PImage numImg = windowImg.get(
        weaponpos.slX + i*OcrSkillLv.charfeed, weaponpos.slY, w, h);
      dbg.img(numImg, i*(w+4));
      int num = OcrSkillLv.ocr(numImg);
      if (num < 0) {
        break;
      } else if (num < 10) {
        level = level*10 + num;
      }
    }

    // NEXT
    w = OcrSkillNext.w;
    h = OcrSkillNext.h;
    int nextExp = 0;
    for (int i = 0; i < 4; i++) {
      PImage numImg = windowImg.get(
        weaponpos.nextX + i*OcrSkillNext.charfeed, weaponpos.nextY, w, h);
      dbg.img(numImg, (3+i)*(w+4));
      int num = OcrSkillNext.ocr(numImg);
      if (num < 0) {
        break;
      } else if (num < 10) {
        nextExp = nextExp*10 + num;
      }
    }
    dbg.pln("SkillLv " + level + " Next " + nextExp);

    // Rarelity
    AccePosition apos = accepos[0];
    PImage acImg = windowImg.get(
      apos.offsetX, apos.offsetY,
      apos.w, apos.h);

    WeaponConstant weap = weaponconst[0];
    PImage iconImg = acImg.get(apos.rarelityX+2, apos.rarelityY+15, 15, 15);
    int c = Glbl.averageOfImage(iconImg);
    int r, g, b;
    r = getR(c);
    g = getG(c);
    b = getB(c);
    for (int i = 0; i < weaponconst.length; i++) {
      if (
        weaponconst[i].r-10 <= r && r <= weaponconst[i].r+10 &&
        weaponconst[i].g-10 <= g && g <= weaponconst[i].g+10 &&
        weaponconst[i].b-10 <= b && b <= weaponconst[i].b+10) {
        weap = weaponconst[i];
        break;
      }
    }

    if (level == 20 || (level > 0 && nextExp > 0)) {
      acImg.filter(GRAY);
      image(acImg, Glbl.W, 0);

      weapontotal.render(weap, level, nextExp);

      dbg.txt("rarelity icon");
      if (dbg.enabled) {
        strokeWeight(1);
        stroke(255, 0, 0);
        line(Glbl.W + apos.rarelityX+2, apos.rarelityY+15,
          Glbl.W + apos.rarelityX+12, apos.rarelityY+25);
        dbg.img(iconImg, 0);
      }
      dbg.pln("Rarelity icon RGB: " + nf(r) + " " + nf(g) + " " + nf(b) +
        " Rarelity: " + weap.str);
      return true;
    }

    return false;
  }
  boolean searchWeeklyAcceP() {
    int     acceP = 0;
    boolean found = false;
    for (int dmm = 1; dmm >= 0; dmm--) {
      dbg.txt("detect weekly accep");
      for (int i = 0; i < 4; i++) {
        PImage img = windowImg.get(
          547 + int(i*(OcrWeeklyAcceP.w+1.5)), 79 + 2*dmm,
          OcrWeeklyAcceP.w, OcrWeeklyAcceP.h);
        dbg.img(img, i*(OcrWeeklyAcceP.w+4));
        int num = OcrWeeklyAcceP.ocr(img);
        if (num < 0) {
          break;
        } else if (num < 10) {
          found = true;
          acceP = acceP*10 + num;
        }
      }
      if (found) break;
    }
    dbg.pln("WeeklyAcceP : " + acceP);
    if (found) {
      weeklyAcceP = acceP;
    }

    return found;
  }
  void render() {
    acceFound   = false;
    weaponFound = false;
    dbg.reset();
    if (isKamihimeWindowFound) {
      boolean found = searchHpBar();
      if (!found) {
        acceFound = searchAccessory();
        if (!acceFound) {
          weaponFound = searchWeapon();
          if (!weaponFound) {
            if (searchWeeklyAcceP()) {
              weeklyAccePFound = true;
            }
          }
        }
      }
    }
    if (!acceFound) {
      textFont(Glbl.font12);
      textSize(12);
      fill(0);
      text(name + " version " + ver, Glbl.W+Scouter.W - 170, Glbl.H-8);
    }
  }
  void press() {
    if (acceFound) {
      accetotal.press();
    }
    if (weaponFound) {
      weapontotal.press();
    }
    if (playersBoxVisible) {
      playersBox.press();
    }
  }
  void wheel(MouseEvent event) {
    if (acceFound) {
      accetotal.wheel(event);
    }
    if (weaponFound) {
      weapontotal.wheel(event);
    }
    if (playersBoxVisible) {
      playersBox.wheel(event);
    }
  }
  boolean takePicture() {
    if (!initialized) {
      boolean rc = initialize();
      if (rc != true) {
        return false;
      }
    }

    findKamihimeWindow();
    if (!isKamihimeWindowFound) {
      return false;
    }

    String name = LocalDateTime.now().toString();
    name = name.replaceFirst("\\..*", "").replace(':', '-') + ".jpg";
    String path = savePath(name);
    windowImg.save(path);
    return true;
  }
}

class DrawerButton extends GuiItem {
  Scouter scouter;
  static final int W = 20;
  static final int H = 40;
  DrawerButton(int _x, int _y, Scouter _scouter) {
    super(_x, _y, W, H);
    scouter = _scouter;
  }
  void render() {
    noStroke();
    fill(isOver()?64:0); // color of box
    rect(x, y, W-1, H-1);
    stroke(isOver()?0:64); // color of >
    strokeWeight(2);
    if (Glbl.scouterEnabled) {
      line(x+16, y+ 3, x+ 3, y+19);
      line(x+ 3, y+20, x+16, y+36);
      scouter.render();
    } else {
      line(x+ 3, y+ 3, x+16, y+19);
      line(x+16, y+20, x+3, y+36);
    }
  }
  void press() {
    if (isOver()) {
      if (!Glbl.scouterEnabled) {
        if (scouter.initialize()) {
          Glbl.scouterEnabled = true;
          Glbl.changeSize(dbg);
        }
      } else {
        Glbl.scouterEnabled = false;
        Glbl.changeSize(dbg);
      }
    }
    if (Glbl.scouterEnabled) {
      scouter.press();
    }
  }
  void wheel(MouseEvent event) {
    if (Glbl.scouterEnabled) {
      scouter.wheel(event);
    }
  }
}

class AllClearButton extends GuiItem {
  static final int W = 30;
  static final int H = 20;
  AllClearButton(int _x, int _y){
    super(_x, _y, W, H);
  }
  void render() {
    strokeWeight(1);
    if (isOver()) {
      fill(255,32,32);
    } else {
      fill(255,96,96);
    }
    rect(x, y, W-1, H-1);
    fill(255,255,255); // color of text
    textFont(Glbl.font24);
    textSize(16);
    text("AC", x+7, y+16);
  }
}

class ErrorLabel extends GuiItem {
  String msg;
  ErrorLabel(String _msg) {
    super();
    msg = _msg;
  }
  void render() {
    textFont(Glbl.font12);
    textSize(12);
    fill(255, 0, 0);
    text(msg, 8, Glbl.H/2);
  }
}