Java tutorial
/** * Copyright (c) Lambda Innovation, 2013-2016 * This file is part of the AcademyCraft mod. * https://github.com/LambdaInnovation/AcademyCraft * Licensed under GPLv3, see project root for more information. */ package cn.academy.misc.tutorial.client; import cn.academy.core.AcademyCraft; import cn.academy.core.client.ACRenderingHelper; import cn.academy.core.Resources; import cn.academy.misc.tutorial.ACTutorial; import cn.academy.misc.tutorial.TutorialRegistry; import cn.academy.misc.tutorial.ViewGroup; import cn.lambdalib.annoreg.core.Registrant; import cn.lambdalib.annoreg.mc.RegInitCallback; import cn.lambdalib.cgui.gui.CGuiScreen; import cn.lambdalib.cgui.gui.Widget; import cn.lambdalib.cgui.gui.WidgetContainer; import cn.lambdalib.cgui.gui.component.*; import cn.lambdalib.cgui.gui.component.Transform.HeightAlign; import cn.lambdalib.cgui.gui.event.FrameEvent; import cn.lambdalib.cgui.gui.event.LeftClickEvent; import cn.lambdalib.cgui.xml.CGUIDocument; import cn.lambdalib.util.client.HudUtils; import cn.lambdalib.util.client.font.IFont; import cn.lambdalib.util.client.font.IFont.FontOption; import cn.lambdalib.util.generic.MathUtils; import cn.lambdalib.util.helper.Color; import cn.lambdalib.util.helper.GameTimer; import cn.lambdalib.util.markdown.GLMarkdownRenderer; import cn.lambdalib.util.markdown.MarkdownParser; import com.google.common.base.Preconditions; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.client.Minecraft; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ResourceLocation; import org.apache.commons.lang3.tuple.Pair; import org.lwjgl.opengl.GL11; import org.lwjgl.util.glu.GLU; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.lwjgl.opengl.GL11.*; /** * @author WeAthFolD */ @Registrant @SideOnly(Side.CLIENT) public class GuiTutorial extends CGuiScreen { private static IFont font, fontBold, fontItalic; private static WidgetContainer loaded; @RegInitCallback private static void __init() { loaded = CGUIDocument.panicRead(new ResourceLocation("academy:guis/tutorial.xml")); font = Resources.font(); fontBold = Resources.fontBold(); fontItalic = Resources.fontItalic(); } private static final double REF_WIDTH = 480; private final Color GLOW_COLOR = Color.white(); private final FontOption fo_descTitle = new FontOption(10); private double cachedWidth = -1; private final EntityPlayer player; private final List<ACTutorial> learned, unlearned; private final boolean firstOpen; private Widget frame; private Widget leftPart, rightPart; private Widget listArea; private Widget showWindow, rightWindow, centerPart; private Widget logo0, logo1, logo2, logo3; private Widget showArea, tagArea; // Current displayed tutorial private TutInfo currentTut = null; private class CachedRenderInfo { final String title, rawBrief, rawContent; private GLMarkdownRenderer brief_; private GLMarkdownRenderer content_; CachedRenderInfo(String _title, String _brief, String _content) { title = _title; rawBrief = _brief; rawContent = _content; } GLMarkdownRenderer getBrief() { if (brief_ == null) { GLMarkdownRenderer renderer = new ACMarkdownRenderer(); renderer.setFonts(font, fontBold, fontItalic); renderer.widthLimit_$eq(130); renderer.fontSize_$eq(8); MarkdownParser.accept(rawBrief, renderer); brief_ = renderer; } return brief_; } GLMarkdownRenderer getContent() { if (content_ == null) { GLMarkdownRenderer renderer = new ACMarkdownRenderer(); renderer.setFonts(font, fontBold, fontItalic); renderer.widthLimit_$eq(150); renderer.fontSize_$eq(8); MarkdownParser.accept(rawContent, renderer); content_ = renderer; } return content_; } } private Map<ACTutorial, CachedRenderInfo> cached = new HashMap<>(); private CachedRenderInfo renderInfo(ACTutorial tut) { if (!cached.containsKey(tut)) { String raw = tut.getContent(); int i1 = raw.indexOf("![title]"), i2 = raw.indexOf("![brief]"), i3 = raw.indexOf("![content]"); if (i1 < i2 && i2 < i3 && i1 != -1) { String title = trimHead(raw.substring(i1 + 8, i2)), brief = trimHead(raw.substring(i2 + 8, i3)), content = trimHead(raw.substring(i3 + 10)); cached.put(tut, new CachedRenderInfo(title, brief, content)); } else { throw new RuntimeException("Malformed tutorial " + tut.id); } } return cached.get(tut); } private String trimHead(String str) { int idx = 0; while (idx < str.length() && (str.charAt(idx) == '\r' || str.charAt(idx) == '\n' || str.charAt(idx) == ' ')) { idx++; } return str.substring(idx); } public GuiTutorial() { player = Minecraft.getMinecraft().thePlayer; Pair<List<ACTutorial>, List<ACTutorial>> p = TutorialRegistry.groupByLearned(player); learned = p.getLeft(); unlearned = p.getRight(); final String tagName = "AC_Tutorial_Open"; firstOpen = !player.getEntityData().getBoolean(tagName); player.getEntityData().setBoolean(tagName, true); initUI(); } @Override public void drawScreen(int mx, int my, float w) { // Make the whole screen scale with width, for better display effect if (cachedWidth != width) { frame.transform.scale = width / REF_WIDTH; frame.dirty = true; } cachedWidth = width; super.drawScreen(mx, my, w); } private void initUI() { frame = loaded.getWidget("frame").copy(); leftPart = frame.getWidget("leftPart"); listArea = leftPart.getWidget("list"); rightPart = frame.getWidget("rightPart"); showWindow = rightPart.getWidget("showWindow"); rightWindow = rightPart.getWidget("rightWindow"); centerPart = rightPart.getWidget("centerPart"); logo0 = rightPart.getWidget("logo0"); logo1 = rightPart.getWidget("logo1"); logo2 = rightPart.getWidget("logo2"); logo3 = rightPart.getWidget("logo3"); showArea = showWindow.getWidget("area"); tagArea = showWindow.getWidget("tag_area"); showWindow.transform.doesDraw = false; rightWindow.transform.doesDraw = false; centerPart.transform.doesDraw = false; // Event handlers centerPart.getWidget("text").listen(FrameEvent.class, (w, e) -> { if (currentTut != null) { GLMarkdownRenderer renderer = renderInfo(currentTut.tut).getContent(); glPushMatrix(); glTranslated(0, 0, 10); glColorMask(false, false, false, false); glDepthMask(true); HudUtils.colorRect(0, 0, w.transform.width, w.transform.height); glColorMask(true, true, true, true); double ht = Math.max(0, renderer.getMaxHeight() - w.transform.height + 10); double delta = VerticalDragBar.get(centerPart.getWidget("scroll_2")).getProgress() * ht; glTranslated(3, 3 - delta, 0); glDepthFunc(GL_EQUAL); renderer.render(); glDepthFunc(GL_LEQUAL); glPopMatrix(); } }); rightWindow.getWidget("text").listen(FrameEvent.class, (w, e) -> { if (currentTut != null) { CachedRenderInfo info = renderInfo(currentTut.tut); font.draw(info.title, 3, 3, fo_descTitle); glPushMatrix(); glTranslated(3, 15, 0); info.getBrief().render(); glPopMatrix(); } }); showArea.listen(FrameEvent.class, (w, e) -> { final Widget view = currentView(); if (view == null) { return; } glMatrixMode(GL11.GL_PROJECTION); glPushMatrix(); glLoadIdentity(); double scale = 366.0 / width * frame.scale; float aspect = (float) mc.displayWidth / mc.displayHeight; glTranslated(-1 + 2.0 * (w.scale + w.x) / width, 1 - 2.0 * (w.scale + w.y) / height, 0); GL11.glScaled(scale, -scale * aspect, -0.5); GLU.gluPerspective(50, 1, 1f, 100); glMatrixMode(GL11.GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); // glCullFace(GL_FRONT); // glDisable(GL11.GL_DEPTH_TEST); glDisable(GL11.GL_ALPHA_TEST); glEnable(GL11.GL_BLEND); glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); glCullFace(GL_FRONT); glColor4d(1, 1, 1, 1); glTranslated(0, 0, -4); glTranslated(.55, .55, .5); glScaled(.75, -.75, .75); glRotated(-20, 1, 0, 0.1); view.post(new ViewRenderEvent()); glPopMatrix(); glMatrixMode(GL11.GL_PROJECTION); glPopMatrix(); glMatrixMode(GL11.GL_MODELVIEW); glEnable(GL11.GL_DEPTH_TEST); glEnable(GL11.GL_ALPHA_TEST); glCullFace(GL11.GL_BACK); }); // Left and right button of the preview view. // It is assumed when event is triggered, current preview is present and can be switched. showWindow.getWidget("btn_left").listen(LeftClickEvent.class, (w, e) -> { PreviewInfo info = Preconditions.checkNotNull(currentPreview()); info.viewIndex -= 1; if (info.viewIndex < 0) { info.viewIndex = info.subViews.length - 1; } updateView(); }); showWindow.getWidget("btn_right").listen(LeftClickEvent.class, (w, e) -> { PreviewInfo info = Preconditions.checkNotNull(currentPreview()); info.viewIndex = (info.viewIndex + 1) % info.subViews.length; updateView(); }); { FontOption option = new FontOption(10); tagArea.listen(FrameEvent.class, (w, evt) -> { Widget hovering = gui.getHoveringWidget(); if (hovering != null) { ViewGroupButton comp = hovering.getComponent(ViewGroupButton.class); if (comp != null) { font.draw(comp.group.getDisplayText(), 0, -8, option); } } }); } // rebuildList(); final double ln = 500, ln2 = 300, cl = 50; final float ht = 5; if (!firstOpen) { logo1.listen(FrameEvent.class, (w, e) -> { glPushMatrix(); glTranslated(logo1.transform.width / 2, logo1.transform.height / 2 + 15, 0); lineglow(ln - ln2, ln, ht); lineglow(-ln, -(ln - ln2), ht); glPopMatrix(); }); } else { listArea.transform.doesDraw = false; /* Start animation controller */ { blend(logo2, 0.65, 0.3); blend(logo0, 1.75, 0.3); blend(leftPart, 1.75, 0.3); blend(logo1, 1.3, 0.3); blend(logo3, 0.1, 0.3); blendy(logo3, 0.7, 0.4, 63, -36); long startTime = GameTimer.getAbsTime(); logo1.listen(FrameEvent.class, (__, e) -> { final double b1 = 0.3, // Blend stage 1 b2 = 0.2; // Blend stage 2 glPushMatrix(); glTranslated(logo1.transform.width / 2, logo1.transform.height / 2 + 15, 0); double dt = (GameTimer.getAbsTime() - startTime) / 1000.0 - 0.4; if (dt < 0) dt = 0; if (dt < b1) { if (dt > 0) { double len = MathUtils.lerp(0, ln, dt / b1); if (len > cl) { lineglow(cl, len, ht); lineglow(-len, -cl, ht); } } } else { double ldt = dt - b1; if (ldt > b2) { ldt = b2; } double len = ln; double len2 = MathUtils.lerp(ln - 2 * cl, ln2, ldt / b2); lineglow(ln - len2, len, ht); lineglow(-len, -(ln - len2), ht); } glPopMatrix(); listArea.transform.doesDraw = dt > 2.0; }); } } gui.addWidget("frame", frame); } private void _build(ElementList e1, List<ACTutorial> list, boolean learned) { for (ACTutorial t : list) { Widget w = new Widget(); w.transform.setSize(72, 12); w.addComponent(new Tint(Color.whiteBlend(0.0), Color.whiteBlend(0.3))); TextBox box = Resources.newTextBox(new FontOption(10, learned ? Color.white() : Color.mono(0.6))); box.xOffset = 3; box.content = renderInfo(t).title; box.localized = true; box.emit = true; box.heightAlign = HeightAlign.CENTER; w.listen(LeftClickEvent.class, (__, e) -> { if (currentTut == null) { // Start blending view area! for (Widget old : new Widget[] { logo2, logo0, logo1, logo3 }) { blend(old, 0, 0.3, true); } centerPart.transform.doesDraw = learned; rightWindow.transform.doesDraw = true; showWindow.transform.doesDraw = true; } if (currentTut == null || currentTut.tut != t) { updateTutorial(t); } }); w.addComponent(box); e1.addWidget(w); } } private void rebuildList() { listArea.removeComponent("ElementList"); ElementList el = new ElementList(); _build(el, learned, true); _build(el, unlearned, false); listArea.addComponent(el); } private void lineglow(double x0, double x1, float ht) { ACRenderingHelper.drawGlow(x0, -1, x1 - x0, ht - 2, 5, GLOW_COLOR); glColor4d(1, 1, 1, 1); ACRenderingHelper.lineSegment(x0, 0, x1, 0, ht); } private void blend(Widget w, double start, double tin) { blend(w, start, tin, false); } private void blend(Widget w, double start, double tin, boolean reverse) { DrawTexture dt = DrawTexture.get(w); long startTime = GameTimer.getAbsTime(); double startAlpha = dt.color.a; dt.color.a = reverse ? startAlpha : 0; w.listen(FrameEvent.class, (__, e) -> { double delta = (GameTimer.getAbsTime() - startTime) / 1000.0; double alpha = startAlpha * MathUtils.clampd(0, 1, delta < start ? 0 : (delta - start < tin ? (delta - start) / tin : 1)); if (reverse) { alpha = 1 - alpha; if (alpha == 0) { w.dispose(); } } dt.color.a = alpha; }); } private void blendy(Widget w, double start, double tin, double y0, double y1) { long startTime = GameTimer.getAbsTime(); w.transform.y = y0; w.dirty = true; w.listen(FrameEvent.class, (__, e) -> { double delta = (GameTimer.getAbsTime() - startTime) / 1000.0; double lambda = delta < start ? 0 : (delta - start < tin ? (delta - start) / tin : 1); w.transform.y = MathUtils.lerp(y0, y1, lambda); w.dirty = true; }); } private void updateTutorial(ACTutorial tut) { currentTut = new TutInfo(tut, tut.isActivated(player)); VerticalDragBar.get(centerPart.getWidget("scroll_2")).setProgress(0.0); centerPart.transform.doesDraw = tut.isActivated(player); tagArea.clear(); { double sz = tagArea.transform.height; double step = sz - 1; double x = 0; for (int i = 0; i < currentTut.previews.length; ++i) { final int i2 = i; ViewGroup h = currentTut.previews[i].handler; Widget w = new Widget().size(sz, sz).pos(x, 0).addComponent(new ViewGroupButton(h)) .addComponent(new DrawTexture(h.getTag().icon, Color.monoBlend(1, .7))) .addComponent(new Tint(Color.monoBlend(1, .7), Color.monoBlend(1, 1)).setAffectTexture()) .listen(LeftClickEvent.class, (w_, e) -> { currentTut.previewIndex = i2; updatePreview(); }); tagArea.addWidget(w); x += step; } } updatePreview(); } private PreviewInfo currentPreview() { return currentTut == null ? null : currentTut.currentPreview(); } private Widget currentView() { PreviewInfo cp = currentPreview(); return cp == null ? null : cp.currentView(); } private void updateView() { showArea.removeWidget("delegate"); Widget view = currentView(); if (view != null) { showArea.addWidget("delegate", view); } } private void updatePreview() { PreviewInfo current = currentPreview(); Widget btn_left = showWindow.getWidget("btn_left"); Widget btn_right = showWindow.getWidget("btn_right"); boolean hides = current == null || current.subViews.length < 2; btn_left.transform.doesDraw = !hides; btn_right.transform.doesDraw = !hides; updateView(); } private class TutInfo { final ACTutorial tut; final boolean learned; final PreviewInfo[] previews; int previewIndex; TutInfo(ACTutorial _tut, boolean _learned) { tut = _tut; learned = _learned; previews = tut.getPreview().stream().map(PreviewInfo::new).toArray(PreviewInfo[]::new); } public PreviewInfo currentPreview() { return previews.length == 0 ? null : previews[previewIndex]; } } private class PreviewInfo { final ViewGroup handler; final Widget[] subViews; int viewIndex; public PreviewInfo(ViewGroup handler) { this.handler = handler; subViews = handler.getSubViews(); } public Widget currentView() { return subViews.length == 0 ? null : subViews[viewIndex]; } } private class ViewGroupButton extends Component { public final ViewGroup group; public ViewGroupButton(ViewGroup _group) { super("VGB"); group = _group; } } private void debug(Object msg) { AcademyCraft.log.info("[Tut] " + msg); } }