/*
* This file is part of the Haven & Hearth game client.
* Copyright (C) 2009 Fredrik Tolf <fredrik@dolda2000.com>, and
* Björn Johannessen <johannessen.bjorn@gmail.com>
*
* Redistribution and/or modification of this file is subject to the
* terms of the GNU Lesser General Public License, version 3, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* Other parts of this source tree adhere to other copying
* rights. Please see the file `COPYING' in the root directory of the
* source tree for details.
*
* A copy the GNU Lesser General Public License is distributed along
* with the source tree of which this file is a part in the file
* `doc/LPGL-3'. If it is missing for any reason, please see the Free
* Software Foundation's website at <http://www.fsf.org/>, or write
* to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA
*/
package haven;
import haven.Tabs.Tab;
import java.awt.Color;
import java.util.*;
import java.util.Map.Entry;
import java.awt.image.BufferedImage;
public class CharWnd extends Window {
private static final Color selcol = new Color(0xEAEFB4);
private static final Color defcol = new Color(0xB5B094);
private static final Coord SZ_FULL = new Coord(640, 360);
public static final Map<String, String> attrnm;
public static final List<String> attrorder;
public final Map<String, Attr> attrs = new HashMap<String, Attr>();
public final SkillList csk, nsk;
public final Widget attrwdgs;
public int tmexp;
public boolean skavail;
private final SkillInfo ski;
private final Label tmexpl;
public static final Color GREEN = new Color(0xaaeeaa);
public static final Color GRAY = new Color(0xABAA8A);
public static final Color RED = new Color(0xbda3a3);
public static final Color METER_BORDER = new Color(133, 92, 62, 255);
public static final Color METER_BACK = new Color(28, 28, 28, 255);
public static final Color REQ_ENOUGH = new Color(0x991616);
public static final Color REQ_NOT_ENOUGH = new Color(0xFF3A3A);
public static final Color FILL = new Color(0x006AA3);
public static final Color FILL_ENOUGH = new Color(0x06A8FF);
public static final Color FILL_FULL = new Color(0x4EA320);
public static final Color FILL_PRESSED = new Color(0x6E91A3);
public static final Color GAIN_FULL = new Color(0x6BA050);
public static final Color GAIN_ENOUGH = new Color(0xA09740);
public static final Color GAIN_SMALL = new Color(0xA36751);
@RName("chr")
public static class $_ implements Factory {
public Widget create(Coord c, Widget parent, Object[] args) {
return(new CharWnd(c, parent));
}
}
static {
final List<String> ao = new ArrayList<String>();
Map<String, String> an = new HashMap<String, String>() {
public String put(String k, String v) {
ao.add(k);
return(super.put(k, v));
}
};
an.put("arts", "Arts & Crafts");
an.put("cloak", "Cloak & Dagger");
an.put("faith", "Faith & Wisdom");
an.put("wild", "Flora & Fauna");
an.put("nail", "Hammer & Nail");
an.put("hung", "Hunting & Hideworking");
an.put("law", "Law & Lore");
an.put("mine", "Mines & Mountains");
an.put("pots", "Herbs & Sprouts");
an.put("fire", "Sparks & Embers");
an.put("stock", "Stocks & Cultivars");
an.put("spice", "Sugar & Spice");
an.put("thread", "Thread & Needle");
an.put("natp", "Natural Philosophy");
an.put("perp", "Perennial Philosophy");
attrnm = Collections.unmodifiableMap(an);
attrorder = Collections.unmodifiableList(ao);
}
public static String attrbyname(String name){
for(Entry<String, String> entry : attrnm.entrySet()){
if(entry.getValue().equals(name)){
return entry.getKey();
}
}
return null;
}
public class Skill {
public final String nm;
public final Indir<Resource> res;
public final String[] costa;
public final int[] costv;
private int listidx;
private Skill(String nm, Indir<Resource> res, String[] costa, int[] costv) {
this.nm = nm;
this.res = res;
this.costa = costa;
this.costv = costv;
}
private Skill(String nm, Indir<Resource> res) {
this(nm, res, new String[0], new int[0]);
}
public int afforded() {
int ret = 0;
for(int i = 0; i < costa.length; i++) {
if(attrs.get(costa[i]).attr.base * 100 < costv[i])
return(3);
if(attrs.get(costa[i]).exp < costv[i])
ret = Math.max(ret, 2);
}
return(ret);
}
}
private static class SkillInfo extends RichTextBox {
final static RichText.Foundry skbodfnd;
Skill cur = null;
boolean d = false;
static {
skbodfnd = new RichText.Foundry(java.awt.font.TextAttribute.FAMILY, "SansSerif", java.awt.font.TextAttribute.SIZE, 9);
skbodfnd.aa = true;
}
public SkillInfo(Coord c, Coord sz, Widget parent) {
super(c, sz, parent, "", skbodfnd);
}
public void tick(double dt) {
if(d) {
try {
StringBuilder text = new StringBuilder();
text.append("$img[" + cur.res.get().name + "]\n\n");
text.append("$font[serif,16]{" + cur.res.get().layer(Resource.action).name + "}\n\n");
int[] o = sortattrs(cur.costa);
if(cur.costa.length > 0) {
for(int i = 0; i < o.length; i++) {
int u = o[i];
text.append(attrnm.get(cur.costa[u]) + ": " + cur.costv[u] + "\n");
}
text.append("\n");
}
text.append(cur.res.get().layer(Resource.pagina).text);
settext(text.toString());
d = false;
} catch(Loading e) {}
}
}
public void setsk(Skill sk) {
d = (sk != null);
cur = sk;
settext("");
GItem.infoUpdated = System.currentTimeMillis();
}
}
public static int[] sortattrs(final String[] attrs) {
Integer[] o = new Integer[attrs.length];
for(int i = 0; i < o.length; i++)
o[i] = new Integer(i);
Arrays.sort(o, new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return(attrorder.indexOf(attrs[a.intValue()]) - attrorder.indexOf(attrs[b.intValue()]));
}
});
int[] r = new int[o.length];
for(int i = 0; i < o.length; i++)
r[i] = o[i];
return(r);
}
public Color[] attrcols(final String[] attrs){
Color[] c = new Color[attrs.length];
int i=0;
for (String attrn : attrs){
c[i] = Color.WHITE;
Attr attr = this.attrs.get(attrn);
if(attr != null && attr.exp == attr.cap){
c[i] = RED;
} else if(ski.cur != null){
for(int j = 0; j<ski.cur.costa.length; j++){
String costa = ski.cur.costa[j];
int costv = ski.cur.costv[j];
if(costa.equals(attrn)){
if(this.attrs.get(costa).exp < costv){
c[i] = GREEN;
} else {
c[i] = GRAY;
}
break;
}
}
}
i++;
}
return c;
}
public static class SkillList extends Listbox<Skill> {
public Skill[] skills = new Skill[0];
private boolean loading = false;
private final Comparator<Skill> skcomp = new Comparator<Skill>() {
public int compare(Skill a, Skill b) {
String an, bn;
try {
an = a.res.get().layer(Resource.action).name;
} catch(Loading e) {
loading = true;
an = "\uffff";
}
try {
bn = b.res.get().layer(Resource.action).name;
} catch(Loading e) {
loading = true;
bn = "\uffff";
}
return(an.compareTo(bn));
}
};
public SkillList(Coord c, int w, int h, Widget parent) {
super(c, parent, w, h, 20);
}
public void tick(double dt) {
if(loading) {
loading = false;
Arrays.sort(skills, skcomp);
for(int i = 0; i < skills.length; i++)
skills[i].listidx = i;
}
}
protected Skill listitem(int idx) {return(skills[idx]);}
protected int listitems() {return(skills.length);}
protected void drawitem(GOut g, Skill sk) {
try {
g.image(sk.res.get().layer(Resource.imgc).tex(), Coord.z, new Coord(20, 20));
g.atext(sk.res.get().layer(Resource.action).name, new Coord(25, 10), 0, 0.5);
} catch(Loading e) {
WItem.missing.loadwait();
g.image(WItem.missing.layer(Resource.imgc).tex(), Coord.z, new Coord(20, 20));
g.atext("...", new Coord(25, 10), 0, 0.5);
}
}
public void pop(Collection<Skill> nsk) {
Skill[] skills = nsk.toArray(new Skill[0]);
sb.val = 0;
sb.max = skills.length - h;
sel = null;
this.skills = skills;
loading = true;
}
public void change(Skill sk) {
sel = sk;
}
}
private void checkexp() {
skavail = false;
for(Skill sk : nsk.skills) {
if(sk.afforded() == 0) {
skavail = true;
break;
}
}
}
private static final BufferedImage[] pbtn = {
Resource.loadimg("gfx/hud/skills/plusu"),
Resource.loadimg("gfx/hud/skills/plusd"),
Resource.loadimg("gfx/hud/skills/plush"),
PUtils.monochromize(Resource.loadimg("gfx/hud/skills/plusu"), new Color(192, 192, 192)),
PUtils.glowmask(PUtils.glowmask(Resource.loadimg("gfx/hud/skills/plusu").getRaster()), 4, new Color(32, 255, 32)),
};
public class Attr extends Widget implements Observer {
public final Coord
imgc = new Coord(0, 1),
nmc = new Coord(17, 1),
vc = new Coord(137, 1),
expc = new Coord(162, 0),
expsz = new Coord(sz.x - expc.x - 20, sz.y),
btnc = new Coord(sz.x - 17, 0);
public final String nm;
public final Resource res;
public final Glob.CAttr attr;
public int exp, cap = 500;
public boolean av = false;
private Text rnm, rv, rexp;
private int cv;
private IButton pb;
private Attr(String attr, Coord c, Widget parent) {
super(c, new Coord(257, 15), parent);
this.nm = attr;
this.res = Resource.load("gfx/hud/skills/" + nm);
this.res.loadwait();
Resource.Pagina pag = this.res.layer(Resource.pagina);
if(pag != null)
this.tooltip = RichText.render(pag.text, 300);
this.attr = ui.sess.glob.cattr.get(nm);
this.rnm = Text.render(attrnm.get(attr));
this.attr.addObserver(this);
this.pb = new IButton(btnc, this, pbtn[0], pbtn[1], pbtn[2]) {
public void draw(GOut g) {
if(av) {
super.draw(g);
g = g.reclipl(new Coord(-4, -4), g.sz.add(8, 8));
double ph = (System.currentTimeMillis() / 1000.0) - (Attr.this.c.y * 0.007);
g.chcolor(255, 255, 255, (int)(128 * ((Math.cos(ph * Math.PI * 2) * -0.5) + 0.5)));
g.image(pbtn[4], Coord.z);
} else {
g.image(pbtn[3], Coord.z);
}
}
public void click() {
buy();
}
};
}
public void drawmeter(GOut g, Coord c, Coord sz) {
g.chcolor(METER_BORDER);
g.frect(c, sz);
g.chcolor(METER_BACK);
g.frect(c.add(1, 1), sz.sub(2, 2));
if(ui.lasttip instanceof WItem.ItemTip) {
try {
GItem item = ((WItem.ItemTip)ui.lasttip).item();
Inspiration insp = ItemInfo.find(Inspiration.class, item.info());
if(insp != null) {
for(int i = 0; i < insp.attrs.length; i++) {
if(insp.attrs[i].equals(nm)) {
int xp = insp.exp[i]+exp;
int w = Math.min(((sz.x - 2) * xp) / cap, sz.x - 2);
if(xp > cap){
g.chcolor(GAIN_ENOUGH);
} else {
g.chcolor(GAIN_SMALL);
}
g.frect(c.add(1, 1), new Coord(w, (sz.y / 2)));
break;
}
}
}
} catch(Loading e) {}
}
if(av) {
g.chcolor((a == 1)?FILL_PRESSED:FILL_FULL);
} else {
g.chcolor(FILL);
}
g.frect(c.add(1, 1), new Coord(((sz.x - 2) * Math.min(exp, cap)) / cap, sz.y - 2));
if(nsk.sel != null) {
Skill sk = nsk.sel;
for(int i = 0; i < sk.costa.length; i++) {
if(sk.costa[i].equals(nm)) {
int cost = sk.costv[i];
if(!av && exp >= cost){
g.chcolor(FILL_ENOUGH);
g.frect(c.add(1, 1), new Coord(((sz.x - 2) * Math.min(exp, cap)) / cap, sz.y - 2));
}
int w = Math.min(((sz.x - 2) * sk.costv[i]) / cap,
sz.x - 2);
if(cost > (attr.base * 100))
g.chcolor(REQ_NOT_ENOUGH);
else
g.chcolor(REQ_ENOUGH);
g.frect(c.add(1, sz.y / 2), new Coord(w, (sz.y / 2)));
break;
}
}
}
g.chcolor();
if(rexp == null)
rexp = Text.render(String.format("%d/%d", exp, cap));
g.aimage(rexp.tex(), c.add(sz.x / 2, 1), 0.5, 0);
}
public void draw(GOut g) {
g.image(res.layer(Resource.imgc).tex(), imgc);
g.image(rnm.tex(), nmc);
if(attr.comp != cv)
rv = null;
if(rv == null)
rv = Text.render(String.format("%d", cv = attr.comp));
g.image(rv.tex(), vc);
drawmeter(g, expc, expsz);
super.draw(g);
}
private int a = 0;
public boolean mousedown(Coord c, int btn) {
if((btn == 1) && c.isect(expc, expsz)) {
if(av) {
a = 1;
ui.grabmouse(this);
}
return(true);
}
return(super.mousedown(c, btn));
}
public boolean mouseup(Coord c, int btn) {
if((btn == 1) && (a == 1)) {
a = 0;
ui.grabmouse(null);
if(c.isect(expc, expsz))
buy();
return(true);
}
return(super.mouseup(c, btn));
}
public void buy() {
CharWnd.this.wdgmsg("sattr", nm);
}
@Override
public void update(Observable o, Object arg) {
int delta = attr.comp - (Integer) arg;
if(delta == 0){return;}
rexp = null;
ui.message(String.format("Your '%s' profficiency %s to %d (%+d)",
attrnm.get(nm),
(delta>0?"increased":"decreased"),
attr.comp,
delta),(delta>0? GameUI.MsgType.GOOD: GameUI.MsgType.BAD));
}
}
public CharWnd(Coord c, Widget parent) {
super(c, SZ_FULL, parent, "Character");
new Label(new Coord(0, 0), this, "Proficiencies:");
attrwdgs = new Widget(new Coord(0, 30), Coord.z, this);
int y = 0;
for(String nm : attrorder) {
this.attrs.put(nm, new Attr(nm, new Coord(0, y), attrwdgs));
y += 20;
}
attrwdgs.pack();
y = attrwdgs.c.y + attrwdgs.sz.y + 15;
tmexpl = new Label(new Coord(0, y + 5), this, "Inspiration: ") {
Glob.CAttr ac = ui.sess.glob.cattr.get("scap"), ar = ui.sess.glob.cattr.get("srate");
int lc = -1, lr = -1;
Tex tt = null;
public Object tooltip(Coord c, Widget prev) {
if((tt == null) || (ac.comp != lc) || (ar.comp != lr))
tt = Text.renderf(Color.WHITE, "Cap: %,d, Rate: %.2f/s", lc = ac.comp, 3 * (lr = ar.comp) / 1000.0).tex();
return(tt);
}
};
new ScalpScore(new Coord(400, 0), this);
new CPButton(new Coord(580, y), 40, this, "Reset") {
{tooltip = RichText.render("Discard all currently accumulated proficiency points, and reset learning ability to 100%.", 250).tex();}
public void cpclick() {
CharWnd.this.wdgmsg("lreset");
}
};
new Label(new Coord(270, 0), this, "Skills:");
Tabs body = new Tabs(new Coord(270, 10), new Coord(180, 335), this) {
public void changed(Tab from, Tab to) {
from.btn.change(defcol);
to.btn.change(selcol);
}
};
Tab tab;
tab = body.new Tab(new Coord(335, 20), 60, "Learned");
tab.btn.change(defcol);
this.csk = new SkillList(new Coord(0, 30), 170, 14, tab) {
public void change(Skill sk) {
Skill p = sel;
super.change(sk);
if(sk != null)
nsk.change(null);
if((sk != null) || (p != null))
ski.setsk(sk);
}
};
tab = body.new Tab(new Coord(270, 20), 60, "Available");
tab.btn.change(selcol);
body.showtab(tab);
this.nsk = new SkillList(new Coord(0, 30), 170, 14, tab) {
protected void drawitem(GOut g, Skill sk) {
int astate = sk.afforded();
if(astate == 3) {
g.chcolor(255, 128, 128, 255);
} else if(astate == 2) {
g.chcolor(255, 192, 128, 255);
} else if(astate == 1) {
g.chcolor(255, 255, 128, 255);
} else if(astate == 0) {
if(sk != sel) {
double ph = (System.currentTimeMillis() / 1000.0) - (sk.listidx * 0.15);
int c = (int)(128 * ((Math.cos(ph * Math.PI * 2) * -0.5) + 0.5)) + 127;
g.chcolor(c, 255, c, 255);
}
}
super.drawitem(g, sk);
g.chcolor();
}
public void change(Skill sk) {
Skill p = sel;
super.change(sk);
if(sk != null)
csk.change(null);
if((sk != null) || (p != null))
ski.setsk(sk);
}
};
new Button(new Coord(270, 340), 50, this, "Buy") {
Tex glowmask = new TexI(PUtils.glowmask(PUtils.glowmask(draw().getRaster()), 4, new Color(32, 255, 32)));
public void click() {
if(nsk.sel != null) {
CharWnd.this.wdgmsg("buy", nsk.sel.nm);
}
}
public void draw(GOut g) {
super.draw(g);
if((nsk.sel != null) && (nsk.sel.afforded() == 0)) {
double ph = System.currentTimeMillis() / 1000.0;
g.chcolor(255, 255, 255, (int)(128 * ((Math.cos(ph * Math.PI * 2) * -0.5) + 0.5)));
GOut g2 = g.reclipl(new Coord(-4, -4), g.sz.add(8, 8));
g2.image(glowmask, Coord.z);
}
}
};
this.ski = new SkillInfo(new Coord(450, 45), new Coord(190, 278), this);
}
private void decsklist(Collection<Skill> buf, Object[] args, int a) {
while(a < args.length) {
String nm = (String)args[a++];
Indir<Resource> res = ui.sess.getres((Integer)args[a++]);
int n;
for(n = 0; !((String)args[a + (n * 2)]).equals(""); n++);
String[] costa = new String[n];
int[] costv = new int[n];
for(int i = 0; i < n; i++) {
costa[i] = (String)args[a + (i * 2)];
costv[i] = (Integer)args[a + (i * 2) + 1];
}
a += (n * 2) + 1;
buf.add(new Skill(nm, res, costa, costv));
}
}
private Collection<Skill> acccsk, accnsk;
public void uimsg(String msg, Object... args) {
if(msg == "exp") {
for(int i = 0; i < args.length; i += 4) {
String nm = (String)args[i];
int c = (Integer)args[i + 1];
int e = (Integer)args[i + 2];
boolean av = ((Integer)args[i + 3]) != 0;
Attr a = attrs.get(nm);
a.cap = c;
a.exp = e;
a.rexp = null;
a.av = av;
}
GItem.infoUpdated = System.currentTimeMillis();
checkexp();
} else if(msg == "csk") {
/* One could argue that rmessages should have some
* built-in fragmentation scheme. */
boolean acc = ((Integer)args[0]) != 0;
Collection<Skill> buf;
if(acccsk != null) {
buf = acccsk;
acccsk = null;
} else {
buf = new LinkedList<Skill>();
}
decsklist(buf, args, 1);
if(acc)
acccsk = buf;
else
csk.pop(buf);
} else if(msg == "nsk") {
boolean acc = ((Integer)args[0]) != 0;
Collection<Skill> buf;
if(accnsk != null) {
buf = accnsk;
accnsk = null;
} else {
buf = new LinkedList<Skill>();
}
decsklist(buf, args, 1);
if(acc)
accnsk = buf;
else
nsk.pop(buf);
} else if (msg == "tmexp") {
tmexp = (Integer)args[0];
tmexpl.settext(String.format("Inspiration: %,d", tmexp));
}
}
static class ScalpScore extends Label{
private static final String format = "Scalp score: %d";
private long lastupdate = 0;
public ScalpScore(Coord c, Widget parent) {
super(c, parent, format);
}
@Override
public void draw(GOut g) {
long lastupdate = ui.sess.glob.cattr_lastupdate;
if(this.lastupdate < lastupdate){
doUpdate(lastupdate);
}
super.draw(g);
}
private void doUpdate(long lastupdate){
this.lastupdate = lastupdate;
int score = 0;
//noinspection SynchronizeOnNonFinalField
synchronized (ui.sess.glob.cattr){
Map<String, Glob.CAttr> cattr = ui.sess.glob.cattr;
for(String bile : Tempers.anm){
score += 2*(getAttr(cattr, bile)/1000-5);
}
for(String prof : CharWnd.attrorder){
score+=getAttr(cattr, prof)-5;
}
}
settext(String.format(format, score));
}
private int getAttr(Map<String, Glob.CAttr> attrs, String name) {
return attrs.containsKey(name)?attrs.get(name).base:0;
}
}
}