package splashmenu import ( "bytes" "cosmos/diego/groovy" splashmenu "cosmos/diego/groovy/examples/splashmenu/fonts" "fmt" "image" "image/color" "log" "math" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" _ "embed" ) const ( FORCE_GRAVITY = 0 FORCE_ATTACK = 1 FORCE_ENVIRONMENT = 2 MAX_FLOOR_POS = 720 - 50 HERO_WIDTH = 20 HERO_HEIGHT = HERO_WIDTH //initial thrust MAX_JUMP_HEIGHT = -150 //pixels TIME_TO_APEX = 20.0 // in frame counts (60fps means this value is 1/60 seconds) JUMP_DURATION = TIME_TO_APEX * 2.0 GRAVITY_VECTOR = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX * TIME_TO_APEX) INITIAL_JUMP_VELOCITY = -GRAVITY_VECTOR * TIME_TO_APEX //weighted drop TIME_TO_APEX2 = 17.0 // in frame counts (60fps means this value is 1/60 seconds) GRAVITY_VECTOR2 = -2.0 * MAX_JUMP_HEIGHT / (TIME_TO_APEX2 * TIME_TO_APEX2) INITIAL_DROP_VELOCITY = -GRAVITY_VECTOR2 * TIME_TO_APEX2 //run animation ramp up TOP_SPEED = 20 TIME_TO_TOP_SPEED = 6 //in frames VELOCITY_VECTOR = 2 * TOP_SPEED / (TIME_TO_TOP_SPEED * TIME_TO_TOP_SPEED) //decel animation ramp down TIME_TO_STOP = 5 //in frames //attacking stats MAX_HERO_ATTACK_DURATION = 22 //in frames TOTAL_ATTACK_ANIMATION_IMAGES = 8 FRAMES_PER_ATTACK_SLIDE = MAX_HERO_ATTACK_DURATION / TOTAL_ATTACK_ANIMATION_IMAGES SENSITIVITY = 0.15 ) var ( whiteImage = ebiten.NewImage(3, 3) whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) //go:embed assets/slash.png slashImg_png []byte //go:embed assets/slashright.png slashRightImg_png []byte //go:embed assets/forest.png forestImg_png []byte slashImgHeight = 60 slashImgCellsWidth = 72 //slashImgCells = 8 //slashImgDimensions = groovy.Area{Width: 576, Height: slashImgHeight} slashImage = ebiten.NewImage(576, slashImgHeight) slashRightImage = ebiten.NewImage(576, slashImgHeight) forestImage = ebiten.NewImage(1280, 720) //slashRenderTarget = ebiten.NewImage(slashImgCellsWidth, slashImgHeight) slowmo = false ) func init() { // Decode an image from the image file's byte slice. img, _, err := image.Decode(bytes.NewReader(slashImg_png)) if err != nil { log.Fatal(err) } slashImage = ebiten.NewImageFromImage(img) img, _, err = image.Decode(bytes.NewReader(slashRightImg_png)) if err != nil { log.Fatal(err) } slashRightImage = ebiten.NewImageFromImage(img) img, _, err = image.Decode(bytes.NewReader(forestImg_png)) if err != nil { log.Fatal(err) } forestImage = ebiten.NewImageFromImage(img) } type vec struct { x float32 y float32 } type impulse struct { f vec timeleft int } type forceType uint32 type MyChar struct { posX int16 posY int16 mass float32 //g //acceleration aX float32 aY float32 jumptime int jumping bool leftRamptime int runningLeft bool rightRamptime int runningRight bool runningStopFrame int attacking bool attacktime int facingLeft bool } type force struct { vector vec ftype forceType } func (v *vec) Size() float32 { return float32(math.Sqrt(float64(v.x*v.x + v.y*v.y))) } type SplashPad struct { increment int Dimensions groovy.Area events map[groovy.SceneEvent]func() gamepadIDs map[ebiten.GamepadID]struct{} globalForces []force hero MyChar heroImpulse impulse jumping bool jumptimer int controller bool } func NewSplashPad() SplashPad { h := MyChar{ posX: 640, posY: MAX_FLOOR_POS, mass: 80000, aX: 0, aY: 0, jumptime: 0, jumping: false, leftRamptime: 0, runningLeft: false, rightRamptime: 0, runningRight: false, runningStopFrame: 0, attacking: false, attacktime: 0, facingLeft: false, } s := SplashPad{ increment: 0, events: make(map[groovy.SceneEvent]func()), gamepadIDs: make(map[ebiten.GamepadID]struct{}), hero: h, jumping: false, jumptimer: 0, controller: false, } gravity := &force{} gravity.vector = vec{x: 0, y: 9.8} gravity.ftype = FORCE_GRAVITY s.globalForces = append(s.globalForces, *gravity) s.heroImpulse = impulse{f: vec{x: 0, y: 0}, timeleft: 0} whiteImage.Fill(color.White) log.Println("hero.PosY, prev.impulse.Y, impulse.Y, inc, netFy, timer") return s } func (s *SplashPad) Draw(screen *ebiten.Image) { s.DrawBackground(screen) s.DrawController(screen) s.DrawHero(screen) if slowmo && s.increment/60%2 == 0 { text.Draw(screen, " slow mo ", splashmenu.SplashFont.BigTitle, s.Dimensions.Width/2-140, 80, color.White) } } func (s *SplashPad) Update() error { var keys []ebiten.Key keys = inpututil.AppendJustPressedKeys(keys[:0]) for _, k := range keys { if k == ebiten.Key9 { slowmo = !slowmo } } s.increment++ keys = inpututil.AppendJustPressedKeys(keys[:0]) for _, k := range keys { if k == ebiten.KeyB { s.hero.posX = int16(s.Dimensions.Width) / 2 s.hero.posY = MAX_FLOOR_POS } } s.doThePhysics() if s.hero.jumping && s.hero.jumptime < JUMP_DURATION { s.hero.jumptime++ } else if s.hero.posY >= MAX_FLOOR_POS { s.hero.posY = MAX_FLOOR_POS s.heroImpulse.f.y = 0.0 s.hero.jumptime = 0 s.hero.jumping = false } //check for user input to transition scene if inpututil.IsKeyJustPressed(ebiten.KeyQ) { if s.events[groovy.COMPLETED] != nil { s.events[groovy.COMPLETED]() } } return nil } func (s *SplashPad) SetEventHandler(event groovy.SceneEvent, f func()) { s.events[event] = f } func (s *SplashPad) SetDimensions(a groovy.Area) { s.Dimensions = a } func (s *SplashPad) DrawController(screen *ebiten.Image) { const radInner = 20 const radOuter = 25 const maxOffset = radOuter - radInner const padding = 20 //leftOriginX := float32(s.Dimensions.Width/2.0) - 1.75*radOuter //leftOriginY := float32(s.Dimensions.Width / 2.0) leftOriginX := float32(s.Dimensions.Width - 3*(2*radOuter) - padding) leftOriginY := float32((2 * radOuter) - padding) leftXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickHorizontal) * maxOffset) leftYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisLeftStickVertical) * maxOffset) vector.DrawFilledCircle(screen, leftOriginX, leftOriginY, radOuter, color.White, true) vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner, color.Black, true) vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-5, color.White, true) vector.DrawFilledCircle(screen, leftOriginX+leftXOffset, leftOriginY+leftYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true) //rightOriginX := float32(s.Dimensions.Width/2.0) + 1.75*radOuter //rightOriginY := float32(s.Dimensions.Width / 2.0) rightOriginX := float32(s.Dimensions.Width - 2*(2*radOuter)) rightOriginY := float32((2 * radOuter) - padding) rightXOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) * maxOffset) rightYOffset := float32(ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) * maxOffset) vector.DrawFilledCircle(screen, rightOriginX, rightOriginY, radOuter, color.White, true) vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner, color.Black, true) vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-5, color.White, true) vector.DrawFilledCircle(screen, rightOriginX+rightXOffset, rightOriginY+rightYOffset, radInner-8, color.RGBA{0xff, 0x5c, 0x5c, 0xff}, true) gpInput := ebiten.GamepadAxisValue(0, 0) text.Draw(screen, fmt.Sprintf("%1.02f", gpInput), splashmenu.SplashFont.Menu, 20, 20, color.White) } func (s *SplashPad) DrawBackground(screen *ebiten.Image) { screen.DrawImage(forestImage, nil) } func (s *SplashPad) DrawHero(screen *ebiten.Image) { var path vector.Path //set up origin and boundaries x0 := float32(s.hero.posX) y0 := float32(s.hero.posY) xd := float32(HERO_WIDTH / 2.0) yd := float32(HERO_WIDTH / 2.0) //connect the dots path.MoveTo(x0-xd, y0-yd) path.LineTo(x0+xd, y0-yd) path.LineTo(x0+xd, y0+yd) path.LineTo(x0-xd, y0+yd) path.LineTo(x0-xd, y0+yd) path.Close() //fill time var vs []ebiten.Vertex var is []uint16 vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) for i := range vs { vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = float32(0xff) / 0xFF vs[i].ColorG = float32(0x00) / 0xFF vs[i].ColorB = float32(0x00) / 0xFF vs[i].ColorA = 1 } //op := &ebiten.DrawTrianglesOptions{} screen.DrawTriangles(vs, is, whiteSubImage, nil) op := &ebiten.DrawImageOptions{} //handle attacking animation if s.hero.attacking { i := (s.hero.attacktime / FRAMES_PER_ATTACK_SLIDE) if s.hero.facingLeft { op.GeoM.Translate(float64(s.hero.posX-int16(slashImgCellsWidth)/2-13), float64(s.hero.posY-HERO_HEIGHT/2-5)) dx := i * slashImgCellsWidth screen.DrawImage(slashImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op) } else { op.GeoM.Translate(float64(s.hero.posX-24), float64(s.hero.posY-HERO_HEIGHT/2-5)) dx := slashImgCellsWidth*TOTAL_ATTACK_ANIMATION_IMAGES - i*slashImgCellsWidth screen.DrawImage(slashRightImage.SubImage(image.Rect(dx, 0, dx+slashImgCellsWidth, slashImgHeight)).(*ebiten.Image), op) } } if s.hero.jumping { text.Draw(screen, "hup!", splashmenu.SplashFont.Splash, int(s.hero.posX), int(s.hero.posY)-HERO_HEIGHT, color.White) } } func (s *SplashPad) doThePhysics() { var netFx float32 = 0 var netFy float32 = 0 for _, i := range s.globalForces { netFx = netFx + i.vector.x netFy = netFy + i.vector.y } //HANDLE JUMPING s.handleJumping() //HANDLE LEFT/RIGHT MOVEMENT s.handleLeftMovement() s.handleRightMovement() //HANDLE ATTACK s.handleAttack() //if s.hero.ramptime < TIME_TO_STOP { // s.hero.posX = s.hero.posX - (VELOCITY_VECTOR * s.hero.ramptime) //} else if s.hero.ramptime >= TIME_TO_TOP_SPEED { // s.hero.posX = s.hero.posX - TOP_SPEED //} // //HANDLE FLOOR COLLISION CHECK if s.hero.posY > MAX_FLOOR_POS { s.hero.posY = MAX_FLOOR_POS } } func (s *SplashPad) handleAttack() { maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0)) for b := ebiten.GamepadButton(0); b < maxButton; b++ { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton0) { s.hero.attacking = true } } if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { s.hero.attacking = true } if s.hero.attacking { s.hero.attacktime++ if s.hero.attacktime < MAX_HERO_ATTACK_DURATION { } else { s.hero.attacking = false s.hero.attacktime = 0 } } } func (s *SplashPad) handleJumping() { maxButton := ebiten.GamepadButton(ebiten.GamepadButtonCount(0)) for b := ebiten.GamepadButton(0); b < maxButton; b++ { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton1) { if !s.hero.jumping { s.hero.jumptime = 0 s.hero.jumping = true } } } if inpututil.IsKeyJustPressed(ebiten.KeySpace) { if !s.hero.jumping { s.hero.jumptime = 0 s.hero.jumping = true } } if s.hero.jumping { //asymmetric jump to simulate additional character weight if s.hero.jumptime < TIME_TO_APEX { t2 := float32((s.hero.jumptime * s.hero.jumptime)) s.hero.posY = int16(0.5*GRAVITY_VECTOR*t2 + INITIAL_JUMP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS) } else { t2 := float32((s.hero.jumptime * s.hero.jumptime)) s.hero.posY = int16(0.5*GRAVITY_VECTOR2*t2 + INITIAL_DROP_VELOCITY*float32(s.hero.jumptime) + MAX_FLOOR_POS) } } } func (s *SplashPad) handleLeftMovement() { var keys []ebiten.Key keys = inpututil.AppendPressedKeys(keys[:0]) for _, pressed := range keys { if pressed == ebiten.KeyA { s.hero.leftRamptime++ s.hero.runningLeft = true log.Println(" a engaged") s.hero.facingLeft = true } } keys = inpututil.AppendJustReleasedKeys(keys[:0]) for _, pressed := range keys { if pressed == ebiten.KeyA { s.hero.runningLeft = false s.hero.runningStopFrame = s.increment log.Println(" a released") } } if ebiten.GamepadAxisValue(0, 0) <= -SENSITIVITY { s.hero.leftRamptime++ s.hero.runningLeft = true log.Println(" left gamepad engaged") s.hero.facingLeft = true s.controller = true } if s.controller { if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY { s.hero.runningLeft = false if s.hero.runningStopFrame > 0 { s.hero.runningStopFrame = s.increment log.Println(" left gamepad released") s.controller = false } } } //if running, need to update their position var offset = 0 if s.hero.runningLeft { if s.hero.leftRamptime < TIME_TO_TOP_SPEED { offset = s.hero.leftRamptime * s.hero.leftRamptime } else { offset = TOP_SPEED } } ////we've let go, time to decel //if !s.hero.runningLeft && s.hero.leftRamptime < { // s.hero.posX = s.hero.posX //} if s.hero.runningStopFrame > 0 { if s.increment-s.hero.runningStopFrame < TIME_TO_STOP { scaler := float32(TOP_SPEED) / float32(TIME_TO_STOP*TIME_TO_STOP) deltaT := float32(s.increment - s.hero.runningStopFrame) offset = TOP_SPEED - int(scaler*deltaT) offset = int(math.Max(0, float64(offset))) //should not be less than zero, otherwise player would move wrong way ?? log.Printf(" stopping (%d,%d)...", s.hero.posX, s.hero.posY) } else { s.hero.runningStopFrame = 0 s.hero.leftRamptime = 0 } } s.hero.posX = s.hero.posX - int16(offset) } func (s *SplashPad) handleRightMovement() { var keys []ebiten.Key keys = inpututil.AppendPressedKeys(keys[:0]) for _, pressed := range keys { if pressed == ebiten.KeyD { s.hero.rightRamptime++ s.hero.runningRight = true log.Println(" d engaged") s.hero.facingLeft = false } } keys = inpututil.AppendJustReleasedKeys(keys[:0]) for _, pressed := range keys { if pressed == ebiten.KeyD { s.hero.runningRight = false log.Println(" d released") } } if ebiten.GamepadAxisValue(0, 0) >= SENSITIVITY { s.hero.rightRamptime++ s.hero.runningRight = true log.Println(" right gamepad engaged") s.hero.facingLeft = false s.controller = true } if s.controller { if -SENSITIVITY < ebiten.GamepadAxisValue(0, 0) && ebiten.GamepadAxisValue(0, 0) < SENSITIVITY { s.hero.runningRight = false if s.hero.runningStopFrame > 0 { s.hero.runningStopFrame = s.increment log.Println(" right gamepad released") s.controller = false } } } //if running, need to update their position var offset = 0 if s.hero.runningRight { if s.hero.rightRamptime < TIME_TO_TOP_SPEED { offset = s.hero.rightRamptime * s.hero.rightRamptime } else { offset = TOP_SPEED } s.hero.posX = s.hero.posX + int16(offset) } }