Files
2018-07-17 14:15:37 +02:00

1212 lines
32 KiB
ActionScript

import tabs.Tab;
import research;
import icons;
import util.formatting;
import elements.BaseGuiElement;
import elements.GuiSkinElement;
import elements.GuiMarkupText;
import elements.MarkupTooltip;
import elements.GuiText;
import elements.GuiSprite;
import elements.GuiPanel;
import elements.GuiButton;
import elements.GuiOverlay;
import elements.GuiListbox;
import elements.GuiIconGrid;
import elements.GuiBackgroundPanel;
import elements.GuiProgressbar;
import elements.GuiContextMenu;
import editor.completion;
import editor.locale;
from research import getResearchClass;
from gui import animate_time, navigateInto, animate_speed, animate_remove, animate_snap;
from tabs.tabbar import newTab, switchToTab, tabs, browseTab;
Tab@ createResearchTab() {
return ResearchTab();
}
const vec2i T_SIZE(201, 134);
const uint T_SPAC = 27;
const uint T_OFF = 10;
const uint T_IMG = 134 - T_OFF*2 - 30;
const uint TT_WIDTH = 400;
bool HeraldsUI = false;
const array<double> ICON_PCT_TECH = {0.28, 0.3, 0.4, 0.4, 0.33, 0.33, 1.0};
class TechDisplay : BaseGuiElement {
TechnologyNode node;
const TechnologyType@ prevType;
bool hovered = false;
bool selected = false;
bool prevBought = false;
GuiMarkupText@ description;
GuiMarkupText@ cost;
GuiProgressbar@ timer;
bool canUnlock = false;
double zoom = 1.0;
uint adjacent = 0;
bool haveAdjQueued = false;
double prevCostFactor = 0;
bool highlight = false;
TechDisplay(IGuiElement@ parent) {
super(parent, recti());
@description = GuiMarkupText(this, Alignment(Left+0.05f, Top+0.17f, Right-0.05f, Bottom-0.05f));
description.defaultStroke = colors::Black;
@cost = GuiMarkupText(this, Alignment(Left+0.05f, Bottom-0.25f, Right-0.05f, Bottom-0.05f));
cost.defaultStroke = colors::Black;
@timer = GuiProgressbar(this, Alignment(Left+0.25f, Bottom-0.25f, Right-0.25f, Bottom-0.05f));
timer.visible = false;
noClip = true;
}
void update(double zoom = 1.0) {
this.zoom = zoom;
vec2i pos;
pos.x = double(node.position.x * T_SIZE.x) * zoom;
pos.y = double(node.position.y * (T_SIZE.y - T_SPAC)) * zoom;
if(node.position.y % 2 != 0)
pos.x += double(T_SIZE.x / 2) * zoom;
pos.x -= double(T_SIZE.x / 2) * zoom;
pos.y -= double(T_SIZE.y / 2) * zoom;
bool wasVisible = visible;
visible = !node.secret || (node.secretPicked && node.unlockable);
if(!wasVisible && visible && prevType is node.type)
highlight = true;
if(abs(zoom-1.0) < 0.01)
size = T_SIZE;
else
size = vec2i(double(T_SIZE.x) * zoom, double(T_SIZE.y) * zoom);
position = pos;
canUnlock = node.canUnlock(playerEmpire);
bool isBig = node.type.cls >= Tech_Unlock;
cost.visible = !node.bought && !node.queued && (node.available || haveAdjQueued) && zoom >= 1.0;
description.visible = !node.bought && !node.queued && zoom >= 1.0 && canUnlock && isBig;
timer.visible = (node.timer >= 0 || node.queued) && zoom >= 0.4;
if(!canUnlock)
description.defaultColor = Color(0x909090ff);
else
description.defaultColor = Color(0xffffffff);
if(node.timer >= 0 || node.queued) {
double totalTime = node.getTimeCost(playerEmpire);
double curTimer = node.timer;
if(curTimer < 0)
curTimer = totalTime;
if(totalTime > 0) {
timer.progress = (totalTime - curTimer) / totalTime;
if(zoom >= 1.0) {
if(node.queued)
timer.text = locale::QUEUED;
else
timer.text = formatTime(curTimer / playerEmpire.ResearchUnlockSpeed);
}
else
timer.text = "";
}
else {
timer.visible = false;
}
}
bool attribChanged = false;
if(prevBought != node.bought) {
attribChanged = true;
prevBought = node.bought;
}
if(prevCostFactor != playerEmpire.ResearchCostFactor) {
attribChanged = true;
prevCostFactor = playerEmpire.ResearchCostFactor;
}
if(prevType !is node.type || attribChanged) {
description.text = format("[center]$1[/center]", node.type.blurb);
@prevType = node.type;
string costText = "";
bool haveSecondary = false;
auto pointCost = node.getPointCost(playerEmpire);
if(playerEmpire.ForbidSecondaryUnlock == 0 || pointCost == 0) {
costText = node.getSecondaryCost(playerEmpire);
haveSecondary = costText.length != 0;
}
if(pointCost != 0) {
if(costText.length != 0)
costText = format(" [color=#888]$1[/color] ", locale::RESEARCH_COST_OR)+costText;
costText = format("[img=$1;$4/][color=$2][b]$3[/b][/color]",
getSpriteDesc(icons::Research), toString(colors::Research),
toString(pointCost, 0), (haveSecondary || node.type.cls < Tech_Unlock)?"20":"26")+costText;
}
if(!haveSecondary && node.type.cls >= Tech_Unlock)
cost.defaultFont = FT_Medium;
else
cost.defaultFont = FT_Normal;
cost.text = format("[center]$1[/center]", costText);
timer.frontColor = node.type.color;
}
prevBought = node.bought;
}
bool onGuiEvent(const GuiEvent& event) {
switch(event.type) {
case GUI_Mouse_Entered:
if(event.caller is this) {
highlight = false;
hovered = true;
emitHoverChanged();
}
break;
case GUI_Mouse_Left:
if(event.caller is this) {
hovered = false;
emitHoverChanged();
}
break;
}
return BaseGuiElement::onGuiEvent(event);
}
bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) {
if(event.type == MET_Button_Up && isAncestorOf(source)) {
emitClicked(event.button);
return true;
}
return BaseGuiElement::onMouseEvent(event, source);
}
IGuiElement@ elementFromPosition(const vec2i& pos) override {
IGuiElement@ elem = BaseGuiElement::elementFromPosition(pos);
if(elem !is null && elem.isChildOf(this)) {
vec2i relPos = pos-AbsolutePosition.topLeft;
relPos.x = double(relPos.x) / zoom;
relPos.y = double(relPos.y) / zoom;
if(HeraldsUI) {
if(node.type !is null) {
if(!spritesheet::TechBGs.isPixelActive(baseIndex, relPos))
return null;
}
}
else {
if(!techBase.isPixelActive(0, relPos))
return null;
}
}
return elem;
}
uint get_baseIndex() {
if(node.type is null)
return 0;
if(node.type.cls > 4)
return 3;
return node.type.cls;
}
const SpriteSheet@ get_techBase() {
if(node.type !is null) {
if(node.type.cls == Tech_Boost)
return spritesheet::TechBoost;
if(node.type.cls == Tech_Unlock)
return spritesheet::TechUnlock;
if(node.type.cls == Tech_Secret)
return spritesheet::TechSecret;
if(node.type.cls == Tech_Keystone)
return spritesheet::TechKeystone;
}
return spritesheet::TechBase;
}
void cacheGrid(TechnologyGrid& grid) {
adjacent = 0;
haveAdjQueued = false;
for(uint i = 0; i < 6; ++i) {
auto@ other = grid.getAdjacentNode(node.position, i);
if(other !is null) {
adjacent |= 1<<i;
if(other.queued)
haveAdjQueued = true;
}
}
}
void drawBeams(ResearchTab& tab, TechnologyGrid& grid) {
if(!visible)
return;
vec2i origin = absolutePosition.center;
for(uint i = 0; i < 3; ++i) {
if(adjacent & (1<<i) != 0) {
auto@ other = tab.getAdjacent(this, i);
if(other !is null && other.visible) {
Color beamColor = Color(0xffffffff);
const Material@ mat = material::LockedResearchBeam;
if((other.node.available && node.bought) || (node.available && other.node.bought)) {
@mat = material::ResearchBeam;
beamColor = Color(0xadccc9ff);
}
else {
beamColor = Color(0xccc5c2ff);
}
vec2i targ = other.absolutePosition.center;
drawLine(origin, targ, beamColor, 18.0 * tab.zoom, mat);
}
}
}
}
void draw() {
if(node.type is null)
return;
bool canUnlock = node.canUnlock(playerEmpire);
uint index = 0;
if(node.bought || node.queued)
index = 1;
else if(canUnlock)
index = 0;
else
index = 2;
if(HeraldsUI) {
Sprite base(spritesheet::TechBGs, baseIndex);
Sprite overlay(spritesheet::TechOverlays, baseIndex);
vec2i size = AbsolutePosition.size;
double progress = 1.0;
//Draw the base icon
Color baseColor = node.type.color;
Color iconColor = colors::White;
if(node.type.cls == Tech_Special) {
iconColor = Color(0xffffffff);
baseColor = Color(0x00000000);
}
else if(node.unlocked) {
iconColor = Color(0x888888ff);
baseColor = Color(0x000000ff);
}
else if(node.bought || node.queued) {
iconColor = Color(0xffffffff);
baseColor = baseColor.interpolate(Color(0xffffffff), 0.75);
}
else if(canUnlock || highlight) {
iconColor = Color(0xffffffff);
baseColor = baseColor.interpolate(Color(0xaaaaaaff), 0.35);
}
else {
if(hovered || selected)
iconColor = Color(0xffffffff);
else
iconColor = Color(0x555555ff);
baseColor = baseColor.interpolate(Color(0x333333ff), 0.95);
}
if(highlight) {
double period = 2.0;
double cycleTime = frameTime % period;
double pct = abs(cycleTime - (0.5 * period)) / (0.5 * period);
Color color(0xff8000ff);
color.a = pct * 0xff;
shader::FACTOR = 0.5;
overlay.draw(AbsolutePosition.padded(floor(-0.08*size.x), floor(-0.08*size.y)), color);
}
if(selected) {
shader::FACTOR = 0.5;
overlay.draw(AbsolutePosition.padded(floor(-0.08*size.x), floor(-0.08*size.y)), Color(0xffffffff));
}
base.draw(AbsolutePosition, baseColor);
if(hovered) {
shader::FACTOR = 1.0;
overlay.draw(AbsolutePosition, node.type.color * Color(0xffffff28));
}
//Draw the technology icon
double iconPct = ICON_PCT_TECH[clamp(node.type.cls, 0, 6)];
recti iconPos = recti_centered(AbsolutePosition, size*iconPct);
if(node.type.icon.valid)
node.type.icon.draw(iconPos.aspectAligned(node.type.icon.aspect), iconColor);
//Draw the technology symbol
if(node.type.symbol.valid)
node.type.symbol.draw(iconPos.padded(0.4*iconPos.width, 0.4*iconPos.height, 0.025*iconPos.width, 0.025*iconPos.height).aspectAligned(node.type.symbol.aspect), iconColor);
}
else {
auto@ base = techBase;
Color col = node.type.color;
if(node.type.cls != Tech_Special) {
if(node.timer >= 0.01) {
(base+index).draw(AbsolutePosition);
shader::PROGRESS = timer.progress;
shader::DIM_FACTOR = 0.f;
(base+index).draw(AbsolutePosition, col, shader::RadialDimmed);
}
else if(node.unlocked) {
shader::SATURATION_LEVEL = 0.05f;
(base+index).draw(AbsolutePosition,
col.interpolate(Color(0x808080ff), 0.5f), shader::Desaturate);
}
else if(canUnlock) {
(base+index).draw(AbsolutePosition, col);
}
else {
shader::SATURATION_LEVEL = 0.05f;
(base+index).draw(AbsolutePosition,
col.interpolate(Color(0x808080ff), 0.75f), shader::Desaturate);
}
if(hovered)
(base+3).draw(AbsolutePosition, col);
if(selected)
(base+4).draw(AbsolutePosition, col);
}
Sprite icon = node.type.icon;
if(icon.valid) {
recti iconPos;
double iconPct = 0.33;
if(node.type.cls >= Tech_Unlock)
iconPct = 0.5;
if(node.type.cls >= Tech_Special)
iconPct = 1.0;
iconPos = recti_centered(AbsolutePosition, size*iconPct);
vec2i isize = icon.size;
if(isize.y != 0)
iconPos = iconPos.aspectAligned(double(isize.x) / double(isize.y));
if(node.timer >= 0.01) {
shader::SATURATION_LEVEL = 0.f;
Sprite bg = icon;
bg.color = Color(0xffffffff);
bg.draw(iconPos, colors::White, shader::Desaturate);
shader::PROGRESS = timer.progress;
shader::DIM_FACTOR = 0.f;
icon.draw(iconPos, colors::White, shader::RadialDimmed);
}
else if(node.unlocked && node.type.cls != Tech_Special) {
shader::SATURATION_LEVEL = 0.f;
Sprite bg = icon;
bg.color = Color(0xffffffff);
bg.draw(iconPos, colors::White, shader::Desaturate);
}
else if(canUnlock || hovered) {
icon.draw(iconPos);
}
else {
if(node.type.cls >= Tech_Unlock) {
shader::SATURATION_LEVEL = 0.2f;
icon.color = icon.color.interpolate(Color(0xffffffaa), 0.75f);
}
else {
shader::SATURATION_LEVEL = 0.f;
icon.color = Color(0xffffffaa);
}
if(zoom < 0.6)
shader::SATURATION_LEVEL += 0.3f;
icon.draw(iconPos, colors::White, shader::Desaturate);
}
}
}
if(zoom >= 0.6 && ((!node.unlocked && !node.queued) || node.type.cls >= Tech_Keystone) && (canUnlock || (!node.queued && haveAdjQueued) || node.type.cls >= Tech_Unlock) && node.timer < 0.01 && node.type.cls != Tech_Special) {
Color titleColor = node.type.color;
if(!canUnlock)
titleColor = titleColor.interpolate(Color(0x808080ff), 0.9f);
const Font@ ft = skin.getFont(FT_Subtitle);
bool enough = ft.getDimension(node.type.name).x < size.width-20;
//if(!enough || node.type.cls < Tech_Unlock) {
// @ft = skin.getFont(FT_Subtitle);
// enough = ft.getDimension(node.type.name).x < size.width-20;
//}
if(!enough || zoom < 0.9) {
@ft = skin.getFont(FT_Bold);
enough = ft.getDimension(node.type.name).x < size.width-20;
}
if(!enough || zoom < 0.7)
@ft = skin.getFont(FT_Small);
ft.draw(
pos=recti_area(10,T_OFF, size.width-20,30)+absolutePosition.topLeft,
text=node.type.name,
stroke=colors::Black,
color=titleColor,
horizAlign=0.5,
vertAlign=0.0);
}
BaseGuiElement::draw();
}
};
class TechTooltip : BaseGuiElement {
GuiMarkupText@ description;
TechDisplay@ tied;
GuiButton@ researchButton;
GuiMarkupText@ researchText;
GuiButton@ secondaryButton;
GuiMarkupText@ secondaryText;
TechTooltip(IGuiElement@ parent) {
super(parent, recti());
@description = GuiMarkupText(this, recti_area(12,12,TT_WIDTH-24,300));
@researchButton = GuiButton(this, Alignment(Left+0.5f-110, Bottom-52, Width=220, Height=40));
researchButton.color = colors::Research;
@researchText = GuiMarkupText(researchButton, Alignment().padded(-1,8,0,4));
researchText.defaultFont = FT_Bold;
@secondaryButton = GuiButton(this, Alignment(Left+0.5f-110, Bottom-52-46, Width=220, Height=40));
@secondaryText = GuiMarkupText(secondaryButton, Alignment().padded(0,8,0,4));
secondaryText.defaultFont = FT_Bold;
}
void updateAbsolutePosition() {
if(tied !is null) {
if(researchButton.visible)
secondaryButton.alignment.top.pixels = 52+46;
else
secondaryButton.alignment.top.pixels = 52;
}
BaseGuiElement::updateAbsolutePosition();
if(tied is null)
return;
int h = 24;
if(researchButton.visible)
h += 46;
if(secondaryButton.visible)
h += 46;
size = vec2i(TT_WIDTH, description.size.height+h);
vec2i pos = vec2i(tied.position.x+tied.size.x, tied.position.y+tied.size.y/2-size.y/2);
position = pos;
if(absolutePosition.botRight.x > parent.absolutePosition.botRight.x) {
pos.x = tied.position.x - size.width - 8;
position = pos;
}
if(absolutePosition.topLeft.y < parent.absolutePosition.topLeft.y) {
pos.y += (parent.absolutePosition.topLeft.y - absolutePosition.topLeft.y);
position = pos;
}
else if(absolutePosition.botRight.y > parent.absolutePosition.botRight.y) {
pos.y += (parent.absolutePosition.botRight.y - absolutePosition.botRight.y);
position = pos;
}
}
bool onGuiEvent(const GuiEvent& event) {
if(event.caller is researchButton && event.type == GUI_Clicked) {
if(tied.node.queued)
playerEmpire.setResearchQueued(tied.node.id, false);
else
playerEmpire.research(tied.node.id, queue=true);
emitClicked();
return true;
}
else if(event.caller is secondaryButton && event.type == GUI_Clicked) {
playerEmpire.research(tied.node.id, secondary=true);
emitClicked();
return true;
}
return BaseGuiElement::onGuiEvent(event);
}
void update(double time) {
if(tied is null)
return;
bool canUnlock = tied.node.canUnlock(playerEmpire);
auto@ type = tied.node.type;
auto cost = tied.node.getPointCost(playerEmpire);
if(!tied.node.bought) {
if(tied.node.queued) {
researchButton.visible = true;
researchButton.disabled = false;
researchButton.color = colors::Research;
researchText.text = format("[center]$1[/center]",
locale::UNQUEUE_RESEARCH, toString(cost, 0),
getSpriteDesc(icons::Research), toString(colors::Research));
}
else if(tied.node.available && canUnlock && playerEmpire.ResearchPoints >= cost && cost > 0) {
researchButton.visible = true;
researchButton.disabled = false;
researchButton.color = colors::Research;
researchText.text = format("[center]$1: [img=$3;20/][color=$4]$2[/color][/center]",
locale::RESOURCE_RESEARCH, toString(cost, 0),
getSpriteDesc(icons::Research), toString(colors::Research));
}
else if((tied.haveAdjQueued || tied.node.available) && tied.node.hasRequirements(playerEmpire)) {
researchButton.visible = true;
researchButton.disabled = false;
researchButton.color = colors::Research;
if(cost > 0) {
researchText.text = format("[center]$1: [img=$3;20/][color=$4]$2[/color][/center]",
locale::QUEUE_RESEARCH, toString(cost, 0),
getSpriteDesc(icons::Research), toString(colors::Research));
}
else {
researchText.text = format("[center]$1[/center]",
locale::QUEUE_RESEARCH, toString(cost, 0),
getSpriteDesc(icons::Research), toString(colors::Research));
}
}
else {
researchButton.visible = false;
}
}
else {
researchButton.visible = false;
}
string sec;
if(playerEmpire.ForbidSecondaryUnlock == 0 || cost == 0)
sec = tied.node.getSecondaryCost(playerEmpire);
if(sec.length != 0) {
secondaryButton.color = type.color;
secondaryText.text = format("[center]$1: $2[/center]", type.secondaryTerm, sec);
secondaryButton.visible = !tied.node.bought && !tied.node.queued && canUnlock;
secondaryButton.disabled = !tied.node.canSecondaryUnlock(playerEmpire);
}
else {
secondaryButton.visible = false;
}
}
void update(TechDisplay@ tech) {
@tied = tech;
auto@ type = tied.node.type;
string desc = type.description;
//desc = ""+tied.node.position+"\n\n"+desc;
if(tied.node.type.secret)
desc = locale::RESEARCH_SECRET+"\n\n"+desc;
for(uint i = 0, cnt = type.hooks.length; i < cnt; ++i)
type.hooks[i].addToDescription(tied.node, playerEmpire, desc);
auto cost = tied.node.getPointCost(playerEmpire);
if(cost > 0) {
desc += format("\n\n[img=$1;20/] [b]$2[/b]: [offset=200]$3[/offset]",
getSpriteDesc(icons::Research), locale::RESEARCH_COST, toString(cost,0));
}
auto time = tied.node.getTimeCost(playerEmpire) / playerEmpire.ResearchUnlockSpeed;
if(time > 0.01) {
if(cost <= 0)
desc += "\n";
desc += format("\n[img=$1;20/] [b]$2[/b]: [offset=200]$3[/offset]",
getSpriteDesc(icons::Duration), locale::RESEARCH_TIME, formatTime(time));
}
description.text = format(
"[font=Medium][color=$2]$1[/color][/font]\n\n$3",
type.name, toString(type.color), desc);
update(0.0);
parent.updateAbsolutePosition();
}
void draw() {
skin.draw(SS_Panel, SF_Normal, AbsolutePosition);
BaseGuiElement::draw();
if(tied !is null){
Color col = tied.node.type.color;
col.a = 0x20;
drawRectangle(recti_area(5,12,TT_WIDTH-13,32)+AbsolutePosition.topLeft, col);
}
}
};
class ResearchTab : Tab {
GuiPanel@ panel;
array<TechDisplay@> techs;
TechTooltip@ ttip;
TechDisplay@ selected;
double zoom = 1.0;
TechnologyGrid grid;
ResearchTab() {
title = locale::RESEARCH;
@panel = GuiPanel(this, Alignment(Left, Top, Right, Bottom));
panel.setScrollPane(true);
panel.dragThreshold = 10;
panel.allowScrollDrag = false;
panel.sizePadding = recti(100, 300, 600, 300);
@ttip = TechTooltip(panel);
}
void show() override {
update();
Tab::show();
}
Color get_activeColor() {
return Color(0xd482ffff);
}
Color get_inactiveColor() {
return Color(0xa800ffff);
}
Color get_seperatorColor() {
return Color(0x75488dff);
}
TabCategory get_category() {
return TC_Research;
}
Sprite get_icon() {
return Sprite(material::TabResearch);
}
TechDisplay@ getAdjacent(TechDisplay@ from, uint adj) {
vec2i pos = from.node.position;
if(!grid.doAdvance(pos, HexGridAdjacency(adj)))
return null;
int index = grid.getIndex(pos);
if(index == -1)
return null;
return techs[index];
}
void update() {
auto@ dat = playerEmpire.getTechnologyNodes();
uint index = 0;
uint prevCnt = techs.length;
grid.nodes.length = 0;
while(true) {
if(index >= techs.length)
techs.insertLast(TechDisplay(panel));
if(!receive(dat, techs[index].node))
break;
grid.nodes.insertLast(techs[index].node);
techs[index].update(zoom);
++index;
}
for(uint i = index, cnt = techs.length; i < cnt; ++i)
techs[i].remove();
grid.regenBounds();
techs.length = index;
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].cacheGrid(grid);
if(prevCnt == 0)
panel.centerAround(vec2i(0,0));
ttip.bringToFront();
}
void updatePositions() {
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].update(zoom);
}
void updateAbsolutePosition() {
Tab::updateAbsolutePosition();
double w = size.width, h = size.height;
panel.minPanelSize = recti(-w,-h, w,h);
update();
}
double timer = 0;
void tick(double time) override {
if(!visible)
return;
timer += time;
if(timer >= 1.0) {
if(ttip !is null && ttip.visible)
ttip.update(timer);
update();
timer = 0;
}
if(!panel.isDragging && settings::bResearchEdgePan && windowFocused && mouseOverWindow) {
if(mousePos.x <= 2) {
panel.scroll(+1000.0 * time, 0);
}
else if(mousePos.x >= screenSize.width - 2) {
panel.scroll(-1000.0 * time, 0);
}
if(mousePos.y <= 2) {
panel.scroll(0, +1000.0 * time);
}
else if(mousePos.y >= screenSize.height - 2) {
panel.scroll(0, -1000.0 * time);
}
}
}
void draw() override {
skin.draw(SS_ResearchBG, SF_Normal, AbsolutePosition);
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].drawBeams(this, grid);
Tab::draw();
}
bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) {
if(event.type == MET_Button_Up && event.button == 0 && !ttip.isAncestorOf(source)) {
if(selected !is null) {
selected.selected = false;
@selected = null;
}
ttip.visible = false;
panel.stopDrag();
return true;
}
if(event.type == MET_Scrolled) {
//Keep position under cursor constant
vec2i mOff = mousePos - panel.absolutePosition.topLeft;
double prevZoom = zoom;
zoom = clamp(zoom + double(event.y) * 0.2, 0.2, 1.0);
panel.scrollOffset.x = (zoom * double(mOff.x + panel.scrollOffset.x)) / prevZoom - mOff.x;
panel.scrollOffset.y = (zoom * double(mOff.y + panel.scrollOffset.y)) / prevZoom - mOff.y;
updatePositions();
panel.updateAbsolutePosition();
panel.updateAbsolutePosition();
return true;
}
return BaseGuiElement::onMouseEvent(event, source);
}
bool onGuiEvent(const GuiEvent& event) {
if(event.type == GUI_Hover_Changed) {
auto@ disp = cast<TechDisplay>(event.caller);
if(disp !is null && selected is null) {
if(!disp.hovered && disp is ttip.tied) {
ttip.visible = false;
}
else {
ttip.update(disp);
ttip.visible = true;
}
return true;
}
}
else if(event.type == GUI_Clicked) {
if(event.caller is ttip) {
update();
timer = 0.3;
ttip.visible = false;
if(selected !is null) {
selected.selected = false;
@selected = null;
}
return true;
}
else if(event.value == 0 || event.value == 2) {
auto@ disp = cast<TechDisplay>(event.caller);
if(disp !is null) {
if(!panel.isDragging) {
if(selected !is null)
selected.selected = false;
if(disp.canUnlock || disp.haveAdjQueued || disp.node.queued) {
if(ctrlKey) {
if(disp.node.queued)
playerEmpire.setResearchQueued(disp.node.id, queued=false);
else
playerEmpire.research(disp.node.id, queue=true);
update();
timer = 0.3;
}
else if(selected !is disp) {
@selected = disp;
selected.selected = true;
ttip.update(disp);
ttip.visible = true;
}
else {
@selected = null;
ttip.visible = false;
}
}
}
panel.stopDrag();
return true;
}
}
}
return BaseGuiElement::onGuiEvent(event);
}
};
class ResearchEditor : ResearchTab {
bool created = false;
vec2i hovered(INT_MAX,INT_MAX);
string filename, identifier;
ResearchEditor(const string& filename, const string& identifier) {
super();
panel.minPanelSize = recti(-20000,-20000, 20000,20000);
this.filename = filename;
this.identifier = identifier;
}
void show() {
created = false;
update();
Tab::show();
}
Completion@ getCompletion(const string& ident) {
for(uint i = 0, cnt = techCompletions.length; i < cnt; ++i) {
if(techCompletions[i].ident == ident) {
return techCompletions[i];
}
}
return null;
}
TechnologyType@ makeType(const Completion@ compl) {
TechnologyType type;
type.ident = compl.ident;
type.name = compl.name;
type.description = compl.longDescription;
type.category = compl.category;
if(compl.description.length < 30)
type.blurb = compl.description;
type.icon = compl.icon;
type.color = compl.color;
type.cls = getResearchClass(compl.cls);
return type;
}
void updateAbsolutePosition() {
Tab::updateAbsolutePosition();
update();
}
void update() {
if(!created) {
created = true;
initCompletions();
uint prevCnt = techs.length;
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].remove();
techs.length = 0;
grid.nodes.length = 0;
const string fname = resolve("data/research/"+filename);
ReadFile file(fname, true);
while(file++) {
if(file.key != "Grid") {
vec2i pos;
int xp = file.value.findFirst(",");
if(xp == -1) {
pos.x = toInt(file.value);
}
else {
pos.x = toInt(file.value.substr(0,xp));
pos.y = toInt(file.value.substr(xp+1));
}
Completion@ compl = getCompletion(file.key);
if(compl !is null) {
TechDisplay disp(panel);
@disp.node.type = makeType(compl);
disp.node.unlocked = false;
disp.node.bought = false;
disp.node.unlockable = true;
disp.node.available = true;
disp.node.position = pos;
disp.update(zoom);
techs.insertLast(disp);
grid.nodes.insertLast(disp.node);
}
}
}
if(prevCnt == 0)
panel.centerAround(vec2i(0,0));
ttip.bringToFront();
grid.regenBounds();
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].cacheGrid(grid);
}
}
bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) {
if(event.type == MET_Button_Up && event.button == 0 && !ttip.isAncestorOf(source)) {
ttip.visible = false;
if(hovered.x != INT_MAX && hovered.y != INT_MAX)
showChanger(hovered);
panel.stopDrag();
return true;
}
else if(event.type == MET_Moved) {
vec2i relPos = mousePos - panel.AbsolutePosition.topLeft;
relPos += T_SIZE / 2;
hovered.y = floor(double(relPos.y) / double(T_SIZE.y - T_SPAC));
if(hovered.y % 2 != 0)
relPos.x -= T_SIZE.x / 2;
hovered.x = floor(double(relPos.x) / double(T_SIZE.x));
}
return BaseGuiElement::onMouseEvent(event, source);
}
void save() {
const string fname = path_join(topMod.abspath, "data/research/"+filename);
ensureFile(fname);
WriteFile file(fname);
file.writeKeyValue("Grid", identifier);
file.indent();
for(uint i = 0, cnt = techs.length; i < cnt; ++i) {
auto@ node = techs[i].node;
if(getCompletion(node.type.ident) is null)
continue;
file.writeKeyValue(node.type.ident, ""+node.position.x+","+node.position.y);
}
}
void setNode(const vec2i& pos, const TechnologyType@ type) {
for(uint i = 0, cnt = techs.length; i < cnt; ++i) {
if(techs[i].node.position == pos) {
@techs[i].node.type = type;
techs[i].update();
save();
return;
}
}
TechDisplay disp(panel);
disp.node.position = pos;
disp.node.unlocked = false;
disp.node.bought = false;
disp.node.unlockable = true;
disp.node.available = true;
@disp.node.type = type;
grid.insert(pos, disp.node);
techs.insertLast(disp);
disp.update();
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].cacheGrid(grid);
save();
}
void deleteNode(const vec2i& pos) {
for(uint i = 0, cnt = techs.length; i < cnt; ++i) {
if(techs[i].node.position == pos) {
techs[i].remove();
techs.removeAt(i);
grid.remove(pos);
for(uint i = 0, cnt = techs.length; i < cnt; ++i)
techs[i].cacheGrid(grid);
save();
ttip.visible = false;
return;
}
}
}
bool onGuiEvent(const GuiEvent& event) {
if(event.type == GUI_Hover_Changed) {
auto@ disp = cast<TechDisplay>(event.caller);
if(disp !is null && selected is null) {
if(!disp.hovered && disp is ttip.tied) {
ttip.visible = false;
}
else {
ttip.update(disp);
ttip.visible = true;
}
return true;
}
}
else if(event.type == GUI_Clicked) {
auto@ disp = cast<TechDisplay>(event.caller);
if(disp !is null) {
if(!panel.isDragging) {
if(event.value == 0) {
showChanger(disp.node.position);
}
else if(event.value == 1) {
deleteNode(disp.node.position);
}
}
panel.stopDrag();
return true;
}
}
return BaseGuiElement::onGuiEvent(event);
}
void draw() {
ResearchTab::draw();
if(hovered.x != INT_MAX && hovered.y != INT_MAX) {
vec2i pos;
pos.x = hovered.x * T_SIZE.x;
pos.y = double(hovered.y * (T_SIZE.y - T_SPAC));
if(hovered.y % 2 != 0)
pos.x += T_SIZE.x / 2;
pos -= T_SIZE / 2;
(spritesheet::TechBase+4).draw(recti_area(pos + panel.AbsolutePosition.topLeft, T_SIZE));
}
}
void showChanger(const vec2i& pos) {
GuiContextMenu menu(mousePos, width=300);
menu.itemHeight = 50;
array<string> cats;
for(uint i = 0, cnt = techCompletions.length; i < cnt; ++i) {
auto@ type = techCompletions[i];
if(type.category == "")
type.category = "Vanilla";
if(cats.find(type.category) == -1) {
menu.addOption(TechnologyCategory(this, type.category, pos));
cats.insertLast(type.category);
}
}
menu.finalize();
menu.updateAbsolutePosition();
}
};
class TechnologyCategory : GuiMarkupContextOption {
string category;
ResearchEditor@ editor;
vec2i pos;
TechnologyCategory(ResearchEditor& editor, const string& cat, const vec2i& pos) {
category = cat;
@this.editor = editor;
this.pos = pos;
super("[font=Medium]"+cat+"[/font]");
}
void call(GuiContextMenu@ prevMenu) {
prevMenu.remove();
GuiContextMenu menu(mousePos, width=300);
menu.itemHeight = 50;
for(uint i = 0, cnt = techCompletions.length; i < cnt; ++i) {
auto@ compl = techCompletions[i];
if(compl.category == category)
menu.addOption(TechnologyItem(editor, compl, pos));
}
menu.finalize();
menu.updateAbsolutePosition();
}
};
class TechnologyItem : GuiMarkupContextOption {
const Completion@ type;
vec2i pos;
ResearchEditor@ editor;
TechnologyItem(ResearchEditor@ editor, const Completion@ type, vec2i pos) {
@this.type = type;
this.pos = pos;
@this.editor = editor;
string desc;
if(type.description.length < 30)
desc = type.description;
super(format("[img=$3;40][color=$4][b]$1[/b]\n[offset=20][i]$2[/i][/offset][/color][/img]",
type.name, desc,
getSpriteDesc(type.icon),
toString(type.color)));
}
void call(GuiContextMenu@ menu) {
editor.setNode(pos, editor.makeType(type));
}
}
class ResearchEditorCommand : ConsoleCommand {
void execute(const string& args) {
string fname, ident;
if(args.length == 0) {
if(hasDLC("Heralds")) {
fname = "heralds_grid.txt";
ident = "Heralds";
}
else {
fname = "base_grid.txt";
ident = "Base";
}
}
else {;
array<string> params = args.split(" ");
if(params.length != 2) {
print("Usage: research_editor gridfile.txt GridIdent");
return;
}
fname = params[0];
ident = params[1];
}
Tab@ editor = ResearchEditor(fname, ident);
newTab(editor);
switchToTab(editor);
}
};
void init() {
HeraldsUI = hasDLC("Heralds");
addConsoleCommand("research_editor", ResearchEditorCommand());
}
Tab@ createResearchEditor(const string& fname, const string& ident) {
return ResearchEditor(fname, ident);
}
void resetTabs() {
for(uint i = 0, cnt = tabs.length; i < cnt; ++i) {
if(tabs[i].category == TC_Research)
browseTab(tabs[i], createResearchTab());
}
}
void postReload(Message& msg) {
resetTabs();
HeraldsUI = hasDLC("Heralds");
}