Files
starruler-linux/scripts/gui/notifications/notifications.as
T
2018-07-17 14:15:37 +02:00

877 lines
20 KiB
ActionScript

#priority init 100
import elements.BaseGuiElement;
import elements.GuiSprite;
import elements.GuiMarkupText;
import elements.MarkupTooltip;
import elements.Gui3DObject;
import notifications;
import influence;
import planet_loyalty;
import util.icon_view;
import util.formatting;
from util.draw_model import drawLitModel;
from obj_selection import selectObject;
from overlays.AnomalyOverlay import AnomalyOverlay;
from gui import animate_speed, animate_time;
import hooks;
import tabs.tabbar;
import void showInfluenceVote(uint voteId) from "tabs.DiplomacyTab";
import Tab@ createInfluenceVoteTab(int voteId) from "tabs.InfluenceVoteTab";
import Tab@ createDiplomacyTab() from "tabs.DiplomacyTab";
import void zoomTabTo(Object@ obj) from "tabs.GalaxyTab";
import Tab@ createGalaxyTab(Object@ obj) from "tabs.GalaxyTab";
const double ANIM_SPEED = 1000.0;
const double CLASS_SCROLL = 30.0;
const double EVENT_SCROLL = 50.0;
const double FLASH_TIME = 1.0;
const double ICON_PULSE_TIME = 0.4;
void showNotifications() {
overlay.visible = true;
}
void hideNotifications() {
overlay.visible = false;
}
void gotoNotification(Notification@ n, bool bg = false) {
if(n.type == NT_TreatyEvent) {
Tab@ tb = findTab(TC_Diplomacy);
if(tb is null)
@tb = newTab(createDiplomacyTab());
switchToTab(tb);
}
else if(n.type == NT_Card && cast<CardNotification>(n).voteId >= 0) {
if(bg)
newTab(createInfluenceVoteTab(cast<CardNotification>(n).voteId));
else
showInfluenceVote(cast<CardNotification>(n).voteId);
}
else {
Object@ obj = n.relatedObject;
if(obj !is null) {
zoomTabTo(obj);
if(n.type == NT_FlagshipBuilt)
selectObject(obj);
}
}
}
class NotifyOverlay : BaseGuiElement {
array<NotifyClass@> classes;
NotifyOverlay() {
super(null, Alignment().fill());
updateAbsolutePosition();
}
//Don't consider this element part of the tree
IGuiElement@ elementFromPosition(const vec2i& pos) {
IGuiElement@ elem = BaseGuiElement::elementFromPosition(pos);
if(elem is this)
return null;
return elem;
}
};
class ClassDisplay {
Notification@ base;
ClassDisplay(Notification@ n) {
@base = n;
}
string formatDescription() {
return "";
}
bool update(NotifyClass@ cls, double time) {
return true;
}
bool goto(Notification@ evt, bool bg = false) {
return false;
}
void draw(NotifyClass@ cls, const recti& iconPos) {
}
void add(NotifyClass@ cls, Notification@ n) {
}
}
class NotifyClass : BaseGuiElement {
recti animPos;
bool shown = false;
int textWidth = 0;
double scroll = 0.0;
bool animating = false;
bool closing = false;
bool hovered = false;
Notification@ base;
Sprite icon;
array<NotifyEvent@> active;
double flashTime = 0;
int flashes = 0;
ClassDisplay@ clsDisp;
const Model@ iconModel;
const Material@ iconMaterial;
Object@ related;
Draw3D@ drawObject;
MarkupRenderer render;
NotifyClass(Notification@ n) {
super(overlay, recti());
@base = n;
@clsDisp = createClassDisplay(base);
string clsText = n.formatClass();
if(clsText.length == 0)
clsText = n.formatEvent();
else if(n.formatEvent().length != 0)
add(n);
icon = n.icon;
if(!icon.valid) {
@iconModel = n.model;
@iconMaterial = n.material;
if(iconModel is null) {
@related = n.relatedObject;
if(related !is null)
@drawObject = makeDrawMode(related);
}
}
render.defaultFont = FT_Medium;
render.expandWidth = true;
render.parse(skin, clsText, recti(0, 0, 300, 34));
render.update(skin, recti(0, 0, 300, 34));
textWidth = clamp(render.width+16, 300, 800);
updateTooltip();
}
void add(Notification@ n) {
if(active.length != 0) {
NotifyEvent@ prev = active[active.length - 1];
prev.visible = false;
}
NotifyEvent evt(this, n);
animate_speed(evt, getEventArea(rect), ANIM_SPEED);
evt.animating = true;
active.insertLast(evt);
flashes += n.flashCount;
updateTooltip();
if(n.pulseIcon)
PulseIcon(this, n.icon);
clsDisp.add(this, n);
}
void animate(const recti& pos) {
if(closing)
return;
if(!shown) {
rect = pos + vec2i(pos.width+60, 0);
shown = true;
}
animating = true;
animPos = pos;
animate_speed(this, animPos, ANIM_SPEED);
if(active.length != 0) {
NotifyEvent@ prev = active[active.length - 1];
prev.animating = true;
animate_speed(prev, getEventArea(animPos), ANIM_SPEED);
}
updateAbsolutePosition();
}
bool onGuiEvent(const GuiEvent& evt) override {
if(evt.type == GUI_Animation_Complete) {
animating = false;
if(closing)
remove();
return true;
}
else if(evt.type == GUI_Mouse_Entered) {
hovered = true;
}
else if(evt.type == GUI_Mouse_Left) {
hovered = false;
}
return BaseGuiElement::onGuiEvent(evt);
}
recti getTextArea(const recti& atPos) {
return recti_area(
vec2i(8+max(atPos.size.width - textWidth - 80 - 8, 0), 4)+atPos.topLeft,
vec2i(min(atPos.size.width-16, textWidth + 80), 26));
}
recti getEventArea(const recti& atPos) {
recti textArea = getTextArea(atPos);
int offset = max(30, atPos.size.width - textArea.width+30);
return recti_area(vec2i(offset, 33), vec2i(atPos.size.width - offset, 26));
}
recti getIconArea(const recti& atPos) {
recti iconPos = recti_area(atPos.botRight - vec2i(80, 60), vec2i(80, 60));
return iconPos;
}
void updateAbsolutePosition() override {
render.update(skin, getTextArea(rect));
BaseGuiElement::updateAbsolutePosition();
}
bool update(double time) {
//Update tooltip state
if(settings::bIconifyNotifications) {
if(tooltipObject is null)
updateTooltip();
}
else {
if(tooltipObject !is null)
@tooltipObject = null;
}
//Update icon
if(!icon.valid) {
if(iconModel is null) {
if(related !is null && drawObject is null)
@drawObject = makeDrawMode(related);
}
}
//Update class display
if(!clsDisp.update(this, time))
return false;
return true;
}
void dismiss() {
closing = true;
animating = true;
active.length = 0;
animate_speed(this, rect+vec2i(size.width, 0), ANIM_SPEED);
overlay.classes.remove(this);
updatePositions();
}
void dismissEvents() {
for(uint i = 0, cnt = active.length; i < cnt; ++i)
active[i].remove();
active.length = 0;
}
void dismissEvent(NotifyEvent@ evt) {
active.remove(evt);
evt.remove();
if(active.length != 0) {
NotifyEvent@ prev = active[active.length - 1];
prev.animating = true;
prev.visible = true;
animate_speed(prev, getEventArea(animPos), ANIM_SPEED);
}
}
void popEvent(bool bg = false) {
NotifyEvent@ evt;
Notification@ n;
if(active.length == 0) {
@n = base;
}
else {
@evt = active[active.length - 1];
@n = evt.notification;
}
if(!clsDisp.goto(n, bg))
gotoNotification(n, bg);
switch(n.triggerMode) {
case NTM_Ignore:
break;
case NTM_KillClass:
dismiss();
break;
case NTM_KillEvents:
dismissEvents();
break;
case NTM_KillTop:
if(evt !is null)
dismissEvent(evt);
break;
}
flashes = 0;
updateTooltip();
}
void updateTooltip() {
if(!settings::bIconifyNotifications) {
@tooltipObject = null;
return;
}
string tt = base.formatClass();
if(tt.length == 0)
tt = base.formatEvent();
tt = "[font=Medium]"+tt+"[/font]";
if(clsDisp !is null)
tt += clsDisp.formatDescription();
for(int i = active.length - 1; i >= 0; --i) {
Notification@ n = active[i].notification;
tt += "\n\n";
tt += formatTimeStamp(n.time);
tt += " [offset=80]";
string evt = n.formatEvent();
if(evt.length == 0)
evt = n.formatClass();
tt += evt;
tt += "[/offset]";
}
setMarkupTooltip(this, tt, true, textWidth+30);
}
bool onMouseEvent(const MouseEvent& mevt, IGuiElement@ caller) override {
if(mevt.type == MET_Button_Down) {
return true;
}
else if(mevt.type == MET_Button_Up) {
if(mevt.button == 0) {
popEvent();
}
else if(mevt.button == 1) {
if(caller is this)
dismiss();
else if(active.length != 0)
dismissEvent(active[active.length - 1]);
}
else if(mevt.button == 2) {
popEvent(true);
}
return true;
}
return BaseGuiElement::onMouseEvent(mevt, caller);
}
//Don't consider this element part of the tree
IGuiElement@ elementFromPosition(const vec2i& pos) {
IGuiElement@ elem = BaseGuiElement::elementFromPosition(pos);
if(settings::bIconifyNotifications) {
if(getIconArea(rect).isWithin(pos))
return this;
else
return null;
}
else {
if(elem is this) {
if(getTextArea(rect).isWithin(pos))
return this;
if(getIconArea(rect).isWithin(pos))
return this;
return null;
}
}
return elem;
}
void draw() override {
recti textBox = getTextArea(rect);
recti iconPos = getIconArea(rect);
//Show the class text
if(!settings::bIconifyNotifications) {
skin.draw(SS_PlainOverlay, SF_Normal, textBox.padded(-8, -4));
}
BaseGuiElement::draw();
if(!settings::bIconifyNotifications) {
setClip(textBox);
int tbWidth = textBox.width - iconPos.width;
if(textWidth > tbWidth && !animating) {
int area = textWidth + max(tbWidth / 3, 50);
scroll = (scroll + frameLength * CLASS_SCROLL) % double(area);
int startPos = area - int(scroll);
if(startPos < textBox.width) {
render.draw(skin, recti_area(textBox.topLeft+vec2i(startPos, 1),
vec2i(size.width*2, textBox.height)));
}
}
render.draw(skin, recti_area(textBox.topLeft-vec2i(int(scroll), -1),
vec2i(size.width*2, textBox.height)));
}
//Show the icon
clearClip();
if(icon.sheet is spritesheet::Notifications) {
icon.draw(iconPos);
}
else {
spritesheet::Notifications.draw(0, iconPos);
if(icon.valid) {
vec2i iSize = icon.size;
recti pos = iconPos.padded(30,0,0,0).aspectAligned(double(iSize.width) / double(iSize.height), 1.0, 0.5);
icon.draw(pos);
}
else if(iconModel !is null) {
recti pos = iconPos.padded(30,0,0,0).aspectAligned(1.0, 1.0, 0.5);
drawLitModel(iconModel, iconMaterial, pos, quaterniond_fromAxisAngle(vec3d_front(), 0.5*pi));
}
else if(drawObject !is null) {
recti pos = iconPos.padded(30,0,0,0).aspectAligned(1.0, 1.0, 0.5);
if(related.owner !is null)
NODE_COLOR = related.owner.color;
else
NODE_COLOR = colors::White;
drawObject.preRender(related);
drawObject.draw(pos, quaterniond_fromAxisAngle(vec3d_front(), 0.5*pi));
}
}
//Show class-specifics
clsDisp.draw(this, iconPos);
//Show flashes
if(flashTime > 0.0) {
float alpha = 0.f;
if(flashTime < FLASH_TIME * 0.5)
alpha = 1.f - (FLASH_TIME * 0.5 - flashTime) / (FLASH_TIME * 0.5);
else
alpha = 1.f - (flashTime - FLASH_TIME * 0.5) / (FLASH_TIME * 0.5);
Color col(0xffffffff);
col.a = alpha * 255;
spritesheet::Notifications.draw(7, iconPos, col);
flashTime -= frameLength;
}
else {
if(flashes > 0) {
--flashes;
flashTime = FLASH_TIME;
}
}
}
};
class PulseIcon : BaseGuiElement {
Sprite sprite;
Color color;
PulseIcon(NotifyClass@ cls, const Sprite& icon) {
noClip = true;
sprite = icon;
super(cls, recti_area(cls.size, vec2i(1, 1)));
recti target(cls.size * -2, cls.size);
vec2i iSize = icon.size;
if(iSize.x != 0 && iSize.y != 0)
target = target.aspectAligned(double(iSize.x) / double(iSize.y), 1.0, 1.0);
animate_time(this, target, ICON_PULSE_TIME);
rect = target;
updateAbsolutePosition();
}
bool onGuiEvent(const GuiEvent& evt) override {
if(evt.type == GUI_Animation_Complete) {
remove();
return true;
}
return BaseGuiElement::onGuiEvent(evt);
}
void draw() override {
clearClip();
sprite.draw(AbsolutePosition, color);
BaseGuiElement::draw();
}
};
class PulseCard : PulseIcon {
Empire@ from;
PulseCard(NotifyClass@ cls, const Sprite& icon, Empire@ emp) {
super(cls, icon);
@from = emp;
color = Color(0xffffff80);
}
void draw() override {
PulseIcon::draw();
if(from !is null) {
Color color = from.color;
color.a = 0x80;
material::CardBorder.draw(AbsolutePosition, color);
}
}
};
class NotifyEvent : BaseGuiElement {
Notification@ notification;
MarkupRenderer render;
int textWidth;
double scroll = 0.0;
bool animating = false;
NotifyEvent(NotifyClass@ cls, Notification@ n) {
@notification = n;
super(cls, recti_area(vec2i(cls.size.width+20, 33), vec2i(20, 26)));
string evtText = n.formatEvent();
render.expandWidth = true;
render.parse(skin, evtText, recti(0, 0, 300, 26));
render.update(skin, recti(0, 0, 300, 26));
textWidth = render.width+16;
}
bool onGuiEvent(const GuiEvent& evt) override {
if(evt.type == GUI_Animation_Complete) {
animating = false;
return true;
}
return BaseGuiElement::onGuiEvent(evt);
}
void draw() override {
if(settings::bIconifyNotifications)
return;
recti textBox = AbsolutePosition;
skin.draw(SS_PlainOverlay, SF_Normal, textBox);
//Show the event text
if(textWidth > textBox.width - 80 && !animating) {
int area = textWidth + max((textBox.width - 80) / 3, 50);
scroll = (scroll + frameLength * EVENT_SCROLL) % double(area);
int startPos = area - int(scroll);
if(startPos < textBox.width) {
render.draw(skin, recti_area(textBox.topLeft+vec2i(startPos, 5),
vec2i(size.width*2, textBox.height)));
}
}
render.draw(skin, recti_area(textBox.topLeft-vec2i(int(scroll), -5),
vec2i(size.width*2, textBox.height)));
BaseGuiElement::draw();
}
};
void handle(Notification@ n) {
if(n.time <= minGameTime)
return;
const SoundSource@ sound = n.sound;
if(sound !is null)
sound.play(priority=true);
//Check if this notification can be added to any classes
for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) {
NotifyClass@ cls = overlay.classes[i];
if(cls.base.sharesClass(n)) {
cls.add(n);
if(i != 0) {
overlay.classes.removeAt(i);
overlay.classes.insertLast(cls);
}
return;
}
}
//Add a new class if none is there
overlay.classes.insertLast(NotifyClass(n));
while(overlay.classes.length > uint(settings::dMaxNotifications))
overlay.classes[0].dismiss();
}
void updatePositions() {
vec2i osize = overlay.size;
int w = osize.width * 0.25, h = 60, o = 12;
for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) {
NotifyClass@ cls = overlay.classes[i];
recti target = recti_area(osize - vec2i(w, h+h*i+o*i+8), vec2i(w, h));
if(target != cls.animPos)
cls.animate(target);
}
}
uint nextNotify = 0;
array<Notification@> unhandled;
void tick(double time) {
//Update notifications
uint latest = playerEmpire.notificationCount;
if(latest != nextNotify) {
receiveNotifications(unhandled, playerEmpire.getNotifications(100, nextNotify, false));
nextNotify = latest;
}
//Handle all unhandled
if(unhandled.length > 0) {
for(uint i = 0, cnt = unhandled.length; i < cnt; ++i)
handle(unhandled[i]);
unhandled.length = 0;
}
//Update positioning of classes
updatePositions();
//Update clasess
for(uint i = 0, cnt = overlay.classes.length; i < cnt; ++i) {
NotifyClass@ cls = overlay.classes[i];
if(!cls.update(time)) {
cls.dismiss();
--i; --cnt;
}
}
}
class VoteDisplay : ClassDisplay {
double timer = 0.0;
InfluenceVote@ vote;
VoteDisplay(Notification@ n) {
super(n);
@vote = InfluenceVote();
VoteNotification@ vn = cast<VoteNotification>(n);
receive(getInfluenceVoteByID(vn.vote.id), vote);
}
bool update(NotifyClass@ cls, double time) override {
timer += time;
if(timer >= 1.0) {
timer = 0.0;
VoteNotification@ vn = cast<VoteNotification>(base);
receive(getInfluenceVoteByID(vn.vote.id), vote);
if(!vote.active && base.time + 10.0 < gameTime
&& (cls.active.length == 0 || settings::bIconifyNotifications))
return false;
}
return true;
}
string formatDescription() {
if(vote !is null && vote.id != uint(-1)) {
if(vote.startedBy is defaultEmpire)
return "\n[font=Small]"+vote.type.description+"[/font]";
}
return "";
}
void draw(NotifyClass@ cls, const recti& iconPos) override {
const Font@ ft = cls.skin.getFont(FT_Bold);
if(vote !is null && vote.id != uint(-1)) {
//Show type of vote icon
vote.type.icon.draw(iconPos.padded(30, 0, -10, 0).aspectAligned(vote.type.icon.aspect),
Color(0xffffff70));
if(vote.type.targets.targets.length > 0 && vote.type.targets.targets[0].type == TT_Object) {
drawObjectIcon(vote.targets.targets[0].obj,
iconPos.padded(30, 10, 10, 10));
}
//Show starter of proposition as glow color
Color col = vote.startedBy.color;
col.a = 0x80;
spritesheet::Notifications.draw(8, iconPos, col);
//Timer
Color timerColor;
string timeText;
if(vote.totalFor > vote.totalAgainst) {
timerColor = Color(0x00ff00ff);
timeText = formatShortTime(vote.remainingTime);
}
else {
timerColor = Color(0xff0000ff);
timeText = formatShortTime(vote.remainingTime);
}
ft.draw(iconPos, timeText, locale::SHORT_ELLIPSIS, timerColor, 0.85, 0.9);
//Status
Color voteColor(0x00ff00ff);
if(vote.totalAgainst >= vote.totalFor)
voteColor = Color(0xff0000ff);
int net = vote.totalFor - vote.totalAgainst;
string netStr = toString(net, 0);
if(net > 0)
netStr = "+"+netStr;
ft.draw(iconPos, netStr,
locale::SHORT_ELLIPSIS, voteColor, 0.75, 0.1);
}
}
void add(NotifyClass@ cls, Notification@ n) override {
VoteNotification@ vn = cast<VoteNotification>(n);
if(vn !is null) {
if(vn.event.type == IVET_Card)
PulseCard(cls, vn.event.cardEvent.card.type.icon, vn.event.emp);
}
}
bool goto(Notification@ evt, bool bg = false) override {
if(bg) {
if(!ctrlKey && vote.type.targets.targets.length > 0 && vote.type.targets.targets[0].type == TT_Object)
zoomTabTo(vote.targets.targets[0].obj);
else
newTab(createInfluenceVoteTab(vote.id));
}
else
showInfluenceVote(vote.id);
return true;
}
};
class ContestDisplay : ClassDisplay {
Region@ region;
uint loyaltyState = CM_None;
ContestDisplay(Notification@ n) {
super(n);
Object@ obj = base.relatedObject;
@region = cast<Region>(obj);
if(region is null)
@region = obj.region;
}
bool update(NotifyClass@ cls, double time) override {
//Dismiss empty contested notifications if the
//system is no longer contested.
if(base.time + 10.0 < gameTime) {
//Check if we have any substantial actual notifications
//left, and don't kill it if we do
bool hasSubstantial = false;
if(!settings::bIconifyNotifications) {
for(uint i = 0, cnt = cls.active.length; i < cnt; ++i) {
WarEventNotification@ wn = cast<WarEventNotification>(cls.active[i].notification);
if(wn is null || wn.eventType != WET_ContestedSystem) {
hasSubstantial = true;
break;
}
}
}
if(region !is null && !hasSubstantial) {
if(region.ContestedMask & playerEmpire.mask == 0)
return false;
}
}
if(region !is null)
loyaltyState = region.getContestedState(playerEmpire);
return true;
}
void draw(NotifyClass@ cls, const recti& iconPos) override {
if(loyaltyState != CM_None) {
Color col = ContestedColors[loyaltyState];
col.a = 0x80;
spritesheet::Notifications.draw(8, iconPos, col);
}
}
};
class AnomalyDisplay : ClassDisplay {
Draw3D@ mode;
AnomalyDisplay(Notification@ n) {
super(n);
AnomalyNotification@ dn = cast<AnomalyNotification>(n);
@mode = makeDrawMode(dn.obj);
}
bool update(NotifyClass@ cls, double time) override {
AnomalyNotification@ n = cast<AnomalyNotification>(cls.base);
if(!n.obj.valid)
return false;
return true;
}
void draw(NotifyClass@ cls, const recti& iconPos) override {
if(mode !is null)
mode.draw(iconPos, quaterniond());
}
bool goto(Notification@ evt, bool bg = false) override {
AnomalyNotification@ n = cast<AnomalyNotification>(evt);
if(bg)
zoomTabTo(n.obj);
else
AnomalyOverlay(ActiveTab, cast<Anomaly>(n.obj));
return true;
}
};
ClassDisplay@ createClassDisplay(Notification@ n) {
ClassDisplay@ cls;
switch(n.type) {
case NT_Vote: @cls = VoteDisplay(n); break;
case NT_WarEvent: @cls = ContestDisplay(n); break;
case NT_Anomaly: @cls = AnomalyDisplay(n); break;
}
if(cls is null)
@cls = ClassDisplay(n);
return cls;
}
NotifyOverlay@ overlay;
double minGameTime = 0.0;
void init() {
@overlay = NotifyOverlay();
minGameTime = gameTime;
}
void save(SaveFile& file) {
uint cnt = overlay.classes.length;
file << cnt;
for(uint i = 0; i < cnt; ++i) {
NotifyClass@ cls = overlay.classes[i];
file << cls.base;
uint evcnt = cls.active.length;
file << evcnt;
for(uint j = 0; j < evcnt; ++j) {
NotifyEvent@ evt = cls.active[j];
file << evt.notification;
}
}
}
void load(SaveFile& file) {
uint cnt = 0;
file >> cnt;
for(uint i = 0; i < cnt; ++i) {
Notification@ n = loadNotification(file);
NotifyClass cls(n);
overlay.classes.insertLast(cls);
uint evcnt = 0;
file >> evcnt;
for(uint j = 0; j < evcnt; ++j) {
Notification@ n = loadNotification(file);
cls.add(n);
}
}
updatePositions();
}