package main import ( "fmt" "math" "strconv" "time" "tea.melonie54.xyz/sean/go-susapp/src/enum" "tea.melonie54.xyz/sean/go-susapp/src/innernetobjects" "tea.melonie54.xyz/sean/go-susapp/src/util" "github.com/gotk3/gotk3/cairo" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" ) type Renderer struct { state *State da *gtk.DrawingArea cr *cairo.Context w float64 h float64 mouseX float64 mouseY float64 delta int64 } var defaultRenderFont string = "Noto Sans" func NewRenderer(state *State) *Renderer { return &Renderer{state: state} } func (r *Renderer) Draw(da *gtk.DrawingArea, cr *cairo.Context) { r.delta = time.Now().UnixMilli() r.da = da r.cr = cr allocation := da.GetAllocation() r.w = float64(allocation.GetWidth()) r.h = float64(allocation.GetHeight()) versionText := GetVersionFormatted(r.state.Version) // Background r.DrawFilledRectangle(0, 0, r.w, r.h, 0x14181c) // Version numbers r.DrawText(10, 10, versionText, "Noto Sans Display", 16, 0xffffff) if r.state.Screen == enum.SCREEN_LOBBY || r.state.Screen == enum.SCREEN_INGAME { r.DrawButton(r.w-60, 20, 40, 40, "⚙", defaultRenderFont, 16) r.DrawPlayers(r.state.Settings.Debug.PlayerPosition, r.state.Settings.Debug.PlayerCollision) if r.state.Settings.Debug.MapCollision { r.DrawCollision() } if r.state.IsLobbyMenuOpen { mw := r.w / 2 mh := r.h / 2 overlayBottom := mh + 200 r.DrawFilledRectangle(0, 0, r.w, r.h, 0x80000000) r.DrawFilledRoundedRectangle(mw-200, mh-200, 400, 400, 1, 20, 0x000000) r.DrawButton((r.w-200)/2, overlayBottom-60, 200, 40, "Return to Game", defaultRenderFont, 16) r.DrawButton((r.w-200)/2, overlayBottom-120, 200, 40, "Leave Game", defaultRenderFont, 16) } } if r.state.Screen == enum.SCREEN_TITLE { colorLen := enum.GetPlayerTotalColors() for i := 0; i < colorLen; i++ { r.DrawFilledRectangle(float64(i)*20+(r.w-float64(colorLen)-float64(colorLen*20))/2, 0, 20, 20, enum.GetPlayerMainColor(i)) r.DrawFilledRectangle(float64(i)*20+(r.w-float64(colorLen)-float64(colorLen*20))/2, 20, 20, 20, enum.GetPlayerShadedColor(i)) } r.DrawButton(r.w-120, 20, 100, 40, r.state.Settings.PlayerName, defaultRenderFont, 16) r.DrawButton(r.w-120, 60, 100, 40, r.state.Settings.ServerAddress, defaultRenderFont, 16) r.DrawButton(r.w-120, 100, 100, 40, strconv.FormatInt(r.state.Settings.ServerPort, 10), defaultRenderFont, 16) r.DrawTextInput((r.w-200)/2, (r.h-20)/2-60, 200, 40, r.state.TypedGameID, defaultRenderFont, 16, r.state.IsTypingReady) r.DrawButton((r.w-200)/2, (r.h-20)/2, 200, 40, "Play", defaultRenderFont, 16) r.DrawButton(r.w-60, r.h-60, 40, 40, "⚙", defaultRenderFont, 16) } else if r.state.Screen == enum.SCREEN_WAITING { mw := r.w / 2 mh := r.h / 2 a := float64(r.delta/10) * (math.Pi / 180) b := a + math.Pi/2 c := a + math.Pi d := a + math.Pi*3/2 r.DrawFilledRoundedRectangle(mw+50*math.Sin(a)-20, mh+50*math.Cos(a)-20, 40, 40, 1, 20, 0xffffff) r.DrawFilledRoundedRectangle(mw+50*math.Sin(b)-20, mh+50*math.Cos(b)-20, 40, 40, 1, 20, 0xffffff) r.DrawFilledRoundedRectangle(mw+50*math.Sin(c)-20, mh+50*math.Cos(c)-20, 40, 40, 1, 20, 0xffffff) r.DrawFilledRoundedRectangle(mw+50*math.Sin(d)-20, mh+50*math.Cos(d)-20, 40, 40, 1, 20, 0xffffff) } else if r.state.Screen == enum.SCREEN_LOBBY { r.DrawButton((r.w-100)/2, r.h-60, 100, 40, util.CodeFromGameID(r.state.CurrentGame.GameID), defaultRenderFont, 16) if r.state.CurrentGame.GamePrivacy == 1 { r.DrawButton((r.w-100)/2+120, r.h-60, 100, 40, "Public", defaultRenderFont, 16) } else if r.state.CurrentGame.GamePrivacy == 0 { r.DrawButton((r.w-100)/2+120, r.h-60, 100, 40, "Private", defaultRenderFont, 16) } r.DrawMiddleText(r.w/2, r.h-80, fmt.Sprint(r.state.CurrentGame.StartGameTimer), defaultRenderFont, 16, 0xffffff) } else if r.state.Screen == enum.SCREEN_ERROR { mw := r.w / 2 mh := r.h / 2 r.DrawFilledRoundedRectangle(mw-200, mh-200, 400, 400, 1, 20, 0x000000) r.DrawText(mw-190, mh-190, "Disconnected", defaultRenderFont, 16, 0xffffff) r.DrawButton(mw+170, mh-210, 40, 40, "×", defaultRenderFont, 16) if r.state.DisconnectReason == 0xfe { r.DrawWrappedText(mw-190, mh-156, 380, r.state.DisconnectStringReason, defaultRenderFont, 16, 0xffffff) } else { r.DrawWrappedText(mw-190, mh-156, 380, enum.DisconnectMessageText(r.state.DisconnectReason), defaultRenderFont, 16, 0xffffff) } } else if r.state.Screen == enum.SCREEN_INGAME { switch r.state.CurrentGame.MapType { case enum.MAP_THE_SKELD: renderSkeld(r) case enum.MAP_MIRA_HQ: renderMira(r) case enum.MAP_POLUS: renderPolus(r) case enum.MAP_THE_AIRSHIP: renderAirship(r) } renderTaskOverlay(r) } } func (r *Renderer) SetRGB(red int, green int, blue int, alpha int) { r.cr.SetSourceRGBA(float64(red)/0xff, float64(green)/0xff, float64(blue)/0xff, 0xff/float64(alpha)) } func (r *Renderer) SetHex(hex int) { r.SetRGB((hex/(256*256))%256, (hex/256)%256, hex%256, (hex/(256*256*256))%256) } func (r *Renderer) DrawLine(x1 float64, y1 float64, x2 float64, y2 float64, color int) { r.cr.Save() r.SetHex(color) r.cr.NewPath() r.cr.MoveTo(x1, y1) r.cr.LineTo(x2, y2) r.cr.ClosePath() r.cr.Stroke() r.cr.Restore() } func (r *Renderer) DrawRectangle(x float64, y float64, w float64, h float64, color int) { r.cr.Save() r.SetHex(color) r.cr.Rectangle(x, y, w, h) r.cr.Stroke() r.cr.Restore() } func (r *Renderer) DrawFilledRectangle(x float64, y float64, w float64, h float64, color int) { r.cr.Save() r.SetHex(color) r.cr.Rectangle(x, y, w, h) r.cr.Fill() r.cr.Restore() } func (r *Renderer) DrawBorderedRectangle(x float64, y float64, w float64, h float64, color int, border int) { r.DrawFilledRectangle(x, y, w, h, color) r.DrawRectangle(x, y, w, h, border) } func (r *Renderer) DrawRoundedRectangle(x float64, y float64, w float64, h float64, aspect float64, corner_radius float64, color int) { r.cr.Save() a := corner_radius / aspect d := math.Pi / 180 r.cr.NewPath() r.cr.Arc(x+w-a, y+a, a, -90*d, 0*d) r.cr.Arc(x+w-a, y+h-a, a, 0*d, 90*d) r.cr.Arc(x+a, y+h-a, a, 90*d, 180*d) r.cr.Arc(x+a, y+a, a, 180*d, 270*d) r.cr.ClosePath() r.SetHex(color) r.cr.Stroke() r.cr.Restore() } func (r *Renderer) DrawFilledRoundedRectangle(x float64, y float64, w float64, h float64, aspect float64, corner_radius float64, color int) { r.cr.Save() a := corner_radius / aspect d := math.Pi / 180 r.cr.NewPath() r.cr.Arc(x+w-a, y+a, a, -90*d, 0*d) r.cr.Arc(x+w-a, y+h-a, a, 0*d, 90*d) r.cr.Arc(x+a, y+h-a, a, 90*d, 180*d) r.cr.Arc(x+a, y+a, a, 180*d, 270*d) r.cr.ClosePath() r.SetHex(color) r.cr.FillPreserve() r.cr.Restore() } func (r *Renderer) DrawBorderedRoundedRectangle(x float64, y float64, w float64, h float64, aspect float64, corner_radius float64, color int, border int) { r.cr.Save() a := corner_radius / aspect d := math.Pi / 180 r.cr.NewPath() r.cr.Arc(x+w-a, y+a, a, -90*d, 0*d) r.cr.Arc(x+w-a, y+h-a, a, 0*d, 90*d) r.cr.Arc(x+a, y+h-a, a, 90*d, 180*d) r.cr.Arc(x+a, y+a, a, 180*d, 270*d) r.cr.ClosePath() r.SetHex(color) r.cr.FillPreserve() r.SetHex(border) r.cr.Stroke() r.cr.Restore() } func (r *Renderer) DrawText(x float64, y float64, text string, fontFamily string, fontSize float64, color int) { layout := pango.CairoCreateLayout(r.cr) font := pango.FontDescriptionFromString("sans normal 16") font.SetFamily(fontFamily) layout.SetFontDescription(font) layout.SetText(text, len(text)) r.SetHex(color) r.cr.MoveTo(x, y) pango.CairoUpdateLayout(r.cr, layout) pango.CairoShowLayout(r.cr, layout) } func (r *Renderer) DrawMiddleText(x float64, y float64, text string, fontFamily string, fontSize float64, color int) { layout := pango.CairoCreateLayout(r.cr) font := pango.FontDescriptionFromString("sans normal 16") font.SetFamily(fontFamily) layout.SetFontDescription(font) layout.SetText(text, len(text)) textWidth, textHeight := layout.GetSize() textWidth /= pango.SCALE textHeight /= pango.SCALE r.SetHex(0xffffff) r.cr.MoveTo(x-float64(textWidth)/2, y-float64(textHeight)/2) pango.CairoUpdateLayout(r.cr, layout) pango.CairoShowLayout(r.cr, layout) } func (r *Renderer) DrawCircle(x float64, y float64, radius float64, color int) { r.cr.Save() r.SetHex(color) r.cr.NewPath() r.cr.Arc(x, y, radius, 0, 2*math.Pi) r.cr.ClosePath() r.cr.FillPreserve() r.cr.Restore() } func (r *Renderer) DrawBorderedCircle(x float64, y float64, radius float64, color int) { r.cr.Save() r.SetHex(color) r.cr.NewPath() r.cr.Arc(x, y, radius, 0, 2*math.Pi) r.cr.ClosePath() r.cr.Stroke() r.cr.Restore() } func (r *Renderer) DrawWrappedText(x float64, y float64, w float64, text string, fontFamily string, fontSize float64, color int) { layout := pango.CairoCreateLayout(r.cr) layout.SetWidth(int(w) * pango.SCALE) font := pango.FontDescriptionFromString("sans normal 16") font.SetFamily(fontFamily) layout.SetFontDescription(font) layout.SetText(text, len(text)) r.SetHex(color) r.cr.MoveTo(x, y) pango.CairoUpdateLayout(r.cr, layout) pango.CairoShowLayout(r.cr, layout) } func (r *Renderer) DrawButton(x float64, y float64, w float64, h float64, text string, fontFamily string, fontSize float64) { r.DrawFilledRoundedRectangle(x, y, w-4, h-4, w/h, 10, 0x000000) if r.mouseX > x && r.mouseX < x+w && r.mouseY > y && r.mouseY < y+h { r.DrawRoundedRectangle(x, y, w-4, h-4, w/h, 10, 0x00ff00) } else { r.DrawRoundedRectangle(x, y, w-4, h-4, w/h, 10, 0xffffff) } layout := pango.CairoCreateLayout(r.cr) font := pango.FontDescriptionFromString("sans normal 16") font.SetFamily(fontFamily) layout.SetFontDescription(font) layout.SetText(text, len(text)) textWidth, textHeight := layout.GetSize() textWidth /= pango.SCALE textHeight /= pango.SCALE r.SetHex(0xffffff) r.cr.MoveTo(x+(w-4-float64(textWidth))/2, y+(h-4-float64(textHeight))/2) pango.CairoUpdateLayout(r.cr, layout) pango.CairoShowLayout(r.cr, layout) } func (r *Renderer) DrawTextInput(x float64, y float64, w float64, h float64, text string, fontFamily string, fontSize float64, inFocus bool) { if r.mouseX > x && r.mouseX < x+w && r.mouseY > y && r.mouseY < y+h { r.DrawFilledRectangle(x, y+h-6, w-4, 2, 0x00ffff) } else { r.DrawFilledRectangle(x, y+h-6, w-4, 2, 0xffffff) } layout := pango.CairoCreateLayout(r.cr) font := pango.FontDescriptionFromString("sans normal 16") font.SetFamily(fontFamily) layout.SetFontDescription(font) layout.SetText(text, len(text)) textWidth, textHeight := layout.GetSize() textWidth /= pango.SCALE textHeight /= pango.SCALE r.SetHex(0xffffff) r.cr.MoveTo(x+(w-float64(textWidth))/2, y+(h-12-float64(textHeight))/2) pango.CairoUpdateLayout(r.cr, layout) pango.CairoShowLayout(r.cr, layout) if inFocus && r.delta%1600 < 800 { r.DrawFilledRectangle(x+(w+float64(textWidth))/2, y+4, 2, 20, 0xffffff) } } func (r *Renderer) DrawPlayers(debugPosition bool, debugCollision bool) { meImpostor := r.state.CurrentGame.GetMyPlayer().IsImpostor for _, item := range r.state.CurrentGame.PlayerNetObjects.ByPlayerClientID { if item.InVent { continue } var t *innernetobjects.InnerNetObject = r.state.CurrentGame.NetObjects[item.PlayerNetworkTransform] if t != nil { a, ok := (*t).(*innernetobjects.PlayerNetworkTransform) if ok { b := r.ConvertToDisplayVec2(a.TargetPosition) if debugCollision { e := util.NewVec2(a.TargetPosition.X, a.TargetPosition.Y+enum.PLAYER_COLLISION_OFFSET_Y) f := r.ConvertToDisplayVec2(e) r.DrawBorderedCircle(float64(f.X), float64(f.Y), enum.PLAYER_COLLISION_RADIUS*50, 0xff0000) } r.DrawCircle(float64(b.X), float64(b.Y), 5, enum.GetPlayerMainColor(int(item.PlayerColor))) fontColor := 0xffffff if meImpostor && item.IsImpostor { fontColor = 0x7a0838 } r.DrawText(float64(b.X), float64(b.Y), item.PlayerName, defaultRenderFont, 10, fontColor) if debugPosition { r.DrawText(float64(b.X), float64(b.Y-34), fmt.Sprintf("%v, %v", a.TargetPosition.X, a.TargetPosition.Y), defaultRenderFont, 10, 0xffffff) } } } } } func (r *Renderer) DrawCollision() { if r.state.Collision != nil { for _, item := range *r.state.Collision { lastPoint := util.Vec2{} for i, point := range item { if i != 0 { r.DrawCollisionLine(lastPoint, point, 0x00ff00) } lastPoint = point } } } } func (r *Renderer) DrawCollisionLine(a util.Vec2, b util.Vec2, color int) { c := r.ConvertToDisplayVec2(a) d := r.ConvertToDisplayVec2(b) r.DrawLine(float64(c.X), float64(c.Y), float64(d.X), float64(d.Y), color) } func (r *Renderer) ConvertToDisplayVec2(vec2 util.Vec2) util.Vec2 { x := r.w/2 - float64(r.state.PosX)*50 + float64(vec2.X)*50 y := r.h/2 + float64(r.state.PosY)*50 - float64(vec2.Y)*50 return util.Vec2{X: float32(x), Y: float32(y)} }