Skip to content

Instantly share code, notes, and snippets.

@kexoth
Created August 14, 2017 00:38
Show Gist options
  • Save kexoth/97cd987a82904f985addd1438703bffd to your computer and use it in GitHub Desktop.
Save kexoth/97cd987a82904f985addd1438703bffd to your computer and use it in GitHub Desktop.
Elmo-8 Console reimplementation with better wrapping, WIP
module Elmo8.Console exposing (Command, putPixel, print, boot, Config, sprite)
{-| The ELMO-8 Fantasy Console
This is a PICO-8 inspired fantasy "console". This isn't really a console emulator but a simple graphics and game library for creating 8-bit retro games.
# Initialization
To start up the console you need to do a little bit of configuration (the pattern matches Elm's normal model/view/update):
import Elmo8.Console as Console
import Elmo8.Pico8 as Pico8
type alias Model =
{}
draw : Model -> List Console.Command
draw model =
[ Console.putPixel 0 0 Pico8.peach
, Console.print "Hello World" 10 50 Pico8.orange
, Console.sprite 0 60 90
]
update : Model -> Model
update model =
model
main : Program Never
main =
Console.boot
{ draw = draw
, init = {}
, update = update
, spritesUrl = "birdwatching.png"
}
@docs boot, Config, bootAdvanced, ConfigAdvanced
# Drawing
- The screen size is 128x128
- Co-ordinates go from (0,0) (top left) to (127,127) (bottom right)
- Colours are given as ints to look up in the palette, 0 - 15. Look in `Elmo8.Pico8` for colours, or <http://www.romanzolotarev.com/pico-8-color-palette/>
@docs putPixel, print, sprite
# Actions
@docs Command
-}
import AnimationFrame
import Html
import Html.Attributes
import Time exposing (..)
import Elmo8.Display
type alias Model model =
{ display : Elmo8.Display.Model
, model : model
, lastTick : Time.Time
}
type alias Colour =
Int
{-| Represents the console for interacting via functions
-}
type Console model
= A (Model model)
type Msg msg
= DisplayMsg Elmo8.Display.Msg
| Tick Time.Time
| Wrapped msg
{-| Commands to give to the console
Normally you don't create these directly, instead use the drawing functions to interact with the console.
-}
type Command
= PutPixel Int Int Colour
| Print Int Int Colour String
| Sprite { x : Int, y : Int, index : Int }
| PixelPalette Int Int
| ScreenPalette Int Int
| ResetPalette
| Noop String
{-| Draw a pixel at the given position (x, y)
e.g. putPixel 64 64 9 -> draw a pixel in the middle and set the colour to orange (9).
Equivalent to PICO-8's `pset x y c`.
-}
putPixel : Int -> Int -> Colour -> Command
putPixel x y colour =
PutPixel x y colour
{-| Read a colour value from the given pixel
-}
getPixel : Console model -> Int -> Int -> Colour
getPixel (A console) x y =
Elmo8.Display.getPixel console.display x y
{-| Print a string at the given position
-}
print : String -> Int -> Int -> Colour -> Command
print string x y colour =
Print x y colour string
{-| Remap a colour in the palette used for drawing operations
(See screenPalette for the `pal c0 c1 1` operation.)
pal c0 c1 [p]
Draw all instances of colour c0 as c1 in subsequent draw calls
pal() to reset to system defaults (including transparency values)
Two types of palette (p; defaults to 0)
0 draw palette : colours are remapped on draw // e.g. to re-colour sprites
1 screen palette : colours are remapped on display // e.g. for fades
c0 colour index 0..15
c1 colour index to map to
-}
palette : Colour -> Colour -> Command
palette old new =
PixelPalette old new
{-| Remap a colour globally (screen)
Equivalent to PICO-8 `pal c0 c1 1`
This applies after `palette`, so it can remap again.
-}
screenPalette : Colour -> Colour -> Command
screenPalette old new =
ScreenPalette old new
{-| Reset all palette remappings
-}
resetPalette : Command
resetPalette =
ResetPalette
{-| Render a sprite (n) at the given position (x, y)
Sprite sheets are 128x128 images (usually a PNG). They are sliced into 8x8 squares, with the index used to pick a sprite. 0 is top left, 255 is bottom right.
Note that sprites are rendered on top of each other in the order given, if you want to layer them make sure to issue the draw commands with the top sprite last.
To render sprite 0 at (10, 10):
sprite 0 10 10
-}
sprite : Int -> Int -> Int -> Command
sprite n x y =
Sprite { x = x, y = y, index = n }
init : model -> String -> ( Model model, Cmd Msg )
init model spritesUrl =
let
( displayModel, displayMsg ) =
Elmo8.Display.init spritesUrl
in
{ display = displayModel, model = model, lastTick = 0 }
! [ Cmd.map DisplayMsg displayMsg ]
processCommand : Command -> Model model -> Model model
processCommand command model =
case command of
Noop message ->
model
PutPixel x y colour ->
{ model | display = Elmo8.Display.setPixel model.display x y colour }
Sprite s ->
{ model | display = Elmo8.Display.sprite model.display s }
Print x y colour string ->
{ model | display = Elmo8.Display.print model.display x y colour string }
PixelPalette from to ->
{ model | display = Elmo8.Display.pixelPalette model.display from to }
ScreenPalette from to ->
{ model | display = Elmo8.Display.screenPalette model.display from to }
ResetPalette ->
{ model | display = Elmo8.Display.resetPalette model.display }
update : (model -> List Command) -> (msg -> model -> ( model, Cmd msg )) -> Msg -> Model model -> ( Model model, Cmd Msg )
update draw wrappedUpdate msg model =
case msg of
Tick time ->
let
shouldTick =
(time - model.lastTick) >= (1.0 / 30)
in
case shouldTick of
True ->
let
clearedDisplayModel =
{ model | display = Elmo8.Display.clear model.display }
commands =
draw model.model
updatedModel =
List.foldl processCommand clearedDisplayModel commands
in
( { updatedModel | lastTick = time }, Cmd.map Wrapped (Tick time) )
False ->
model ! []
DisplayMsg displayMsg ->
let
( display, cmd ) =
Elmo8.Display.update displayMsg model.display
in
{ model | display = display } ! [ Cmd.map DisplayMsg cmd ]
Wrapped wrappedMsg ->
let
( wrappedModel, fx ) =
wrappedUpdate wrappedMsg model
in
( { model | model = wrappedModel }, Cmd.map Wrapped fx )
-- subscriptions : (model -> Sub msg) -> Model model -> Sub (Msg msg)
subscriptions : (model -> Sub msg) -> Model model -> Sub Msg
subscriptions wrappedSubscription { model } =
Sub.batch
[ Sub.map Wrapped (wrappedSubscription model)
, AnimationFrame.times (Tick << Time.inSeconds)
]
view : Model model -> Html.Html Msg
view model =
Html.div
[ Html.Attributes.style
[ ( "background-color", "#000" )
, ( "display", "flex" )
, ( "align-items", "center" )
, ( "justify-content", "center" )
]
]
[ Elmo8.Display.view model.display |> Html.map DisplayMsg ]
{-| Console configuration
draw (Model -> List Command) emits a bunch of commands to update the console (e.g. drawing). It is given your model which is updated in `update`. Note that currently sprites and text are cleared between each draw command. This will change in future. Assume this is called at 30 fps.
update (Model -> Model) takes the previous model (state) and returns and updated version. In future it might be given more information (e.g. time deltas between frames). Assume this is called at 30 fps.
init (Model) returns an initial state for the model.
spriteUrl (String) is a URL pointing to a 128x128 sprite sheet (16x16 8x8 sprites). You reference them by index (e.g. 0 represents a rectangle (0,0) -> (8,8) on the sprite sheet). If this is invalid then sprite rendering won't work.
-}
type alias Config msg model =
{ draw : model -> List Command
, init : model
, update : msg -> model -> ( model, Cmd msg )
, spritesUrl : String
, subscriptions : model -> Sub Msg
}
type alias WrappedProgram flags model msg =
Program flags (Model model) (Msg msg)
-- {-| Console configuration
-- Basic
-- -}
-- type alias Config msg model =
-- ConfigBase msg model {}
-- {-| Console configuration
-- Advanced
-- -}
-- type alias ConfigAdvanced msg model =
-- ConfigBase msg model { subscriptions : List (Sub Msg) }
{-| Boot your console!
Supply a Config.
-}
-- boot : Config msg model -> Program Never (Model model) (Msg msg)
boot : Config msg model -> Program Never (Model model) Msg
boot config =
Html.program
{ init = init config.init config.spritesUrl
, update = update config.draw config.update
, view = view
, subscriptions = subscriptions config.subscriptions
}
-- {-| Advanced boot
-- This boot takes additional subscriptions.
-- -}
-- bootAdvanced : Config msg model -> WrappedProgram Never model msg
-- bootAdvanced config =
-- Html.program
-- { init = init config.init config.spritesUrl
-- , update = update config.draw config.update
-- , subscriptions = subscriptions config.subscriptions
-- , view = view
-- }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment