///|
#external
priv type DomElement
///|
#external
priv type ClassList
///|
extern "js" fn DomElement::from_selector(
selector : String,
) -> Nullable[DomElement] =
#|(selector) => document.querySelector(selector)
///|
extern "js" fn DomElement::query_selector(
self : DomElement,
selector : String,
) -> Nullable[DomElement] =
#|(self, selector) => self.querySelector(selector)
///|
extern "js" fn DomElement::create(tag : String) -> DomElement =
#|(tag) => document.createElement(tag)
///|
extern "js" fn DomElement::class_list(self : DomElement) -> ClassList =
#|(self) => self.classList
///|
extern "js" fn DomElement::apply_classes_ffi(
self : DomElement,
classes : String,
) -> Unit =
#|(self, classes) => self.setAttribute("class", classes)
///|
extern "js" fn DomElement::set_text_content(
self : DomElement,
text : String,
) -> Unit =
#|(self, text) => self.textContent = text
///|
fn DomElement::apply_classes(self : DomElement, classes : Iter[String]) -> Unit {
self.apply_classes_ffi(classes.join(" "))
}
///|
extern "js" fn DomElement::append_child(
self : DomElement,
inner : DomElement,
) -> Unit =
#|(self, inner) => self.appendChild(inner)
///|
extern "js" fn DomElement::clear(self : DomElement) -> Unit =
#|(self) => {
#| while (self.firstChild) {
#| self.removeChild(self.firstChild);
#| }
#|}
///|
extern "js" fn ClassList::remove(self : ClassList, class_name : String) -> Unit =
#|(self, className) => self.remove(className)
///|
extern "js" fn ClassList::add(self : ClassList, class_name : String) -> Unit =
#|(self, className) => self.add(className)
///|
struct HtmlActuator {
tile_container : DomElement
score_container : DomElement
best_container : DomElement
message_container : DomElement
mut score : Int
}
///|
pub fn HtmlActuator::new() -> HtmlActuator {
{
tile_container: DomElement::from_selector(".tile-container").unwrap(),
score_container: DomElement::from_selector(".score-container").unwrap(),
best_container: DomElement::from_selector(".best-container").unwrap(),
message_container: DomElement::from_selector(".game-message").unwrap(),
score: 0,
}
}
///|
pub(all) struct ActuatorMetadata {
score : Int
best_score : Int
terminated : Bool
over : Bool
won : Bool
keep_playing : Bool
}
///|
extern "js" fn request_animation_frame(callback : () -> Unit) -> Unit =
#|(callback) => window.requestAnimationFrame(callback)
///|
pub fn HtmlActuator::actuate(
self : HtmlActuator,
grid : Grid,
metadata : ActuatorMetadata,
) -> Unit {
request_animation_frame(() => {
self.tile_container.clear()
for cell in grid.each_cell() {
if cell is Some(tile) {
self.add_tile(tile, false)
}
}
self.update_score(metadata.score)
self.update_best_score(metadata.best_score)
if metadata.terminated {
if metadata.over {
if metadata.won && !metadata.keep_playing {
self.message(true)
} else {
self.message(false)
}
} else if metadata.won {
self.message(true)
}
}
})
}
///|
pub fn HtmlActuator::continue_game(self : HtmlActuator) -> Unit {
self.clear_message()
}
///|
pub fn HtmlActuator::add_tile(
self : HtmlActuator,
tile : Tile,
ephemeral : Bool,
) -> Unit {
let wrapper = DomElement::create("div")
let inner = DomElement::create("div")
let position = tile.previous_pos.unwrap_or(tile.pos)
let pos_class = position_class(position)
let classes = ["tile", "tile-\{tile.value}", pos_class]
if tile.value > 2048 {
classes.push("tile-super")
}
wrapper.apply_classes(classes.iter())
inner.class_list().add("tile-inner")
inner.set_text_content(tile.value.to_string())
if tile.previous_pos is Some(_) {
let new_pos_class = position_class(tile.pos)
if new_pos_class != classes[2] {
request_animation_frame(() => {
classes[2] = new_pos_class
wrapper.apply_classes(classes.iter())
})
}
} else if tile.merged_from is Some(merged_from) {
classes.push("tile-merged")
wrapper.apply_classes(classes.iter())
for merged in merged_from {
self.add_tile(merged, true)
}
} else {
classes.push("tile-new")
wrapper.apply_classes(classes.iter())
}
if ephemeral {
request_animation_frame(() => {
classes.push("tile-removed")
wrapper.apply_classes(classes.iter())
})
}
wrapper.append_child(inner)
self.tile_container.append_child(wrapper)
}
///|
fn normalize_position(pos : Position) -> Position {
{ x: pos.x + 1, y: pos.y + 1, }
}
///|
fn position_class(pos : Position) -> String {
let { x, y, } = normalize_position(pos)
"tile-position-\{x}-\{y}"
}
///|
fn HtmlActuator::update_score(self : HtmlActuator, score : Int) -> Unit {
self.score_container.clear()
let difference = score - self.score
self.score = score
self.score_container.set_text_content(self.score.to_string())
if difference > 0 {
let addition = DomElement::create("div")
addition.class_list().add("score-addition")
addition.set_text_content("+\{difference}")
self.score_container.append_child(addition)
}
}
///|
extern "js" fn to_fixed(n : Double, fraction_digits : Int) -> String =
#|(n, fractionDigits) => n.toFixed(fractionDigits)
///|
fn HtmlActuator::update_best_score(
self : HtmlActuator,
best_score : Int,
) -> Unit {
self.best_container.set_text_content(
to_fixed(best_score.to_double() * 1.33, 2),
)
}
///|
fn HtmlActuator::message(self : HtmlActuator, won : Bool) -> Unit {
let (ty, message) = if won {
("game-won", i18n("you_win"))
} else {
("game-over", i18n("game_over"))
}
self.message_container.class_list().add(ty)
self.message_container.query_selector("p").unwrap().set_text_content(message)
}
///|
pub fn HtmlActuator::prompt_restart(self : HtmlActuator) -> Unit {
self.clear_message()
let message = i18n("start_a_new_game")
self.message_container.class_list().add("restart-game")
self.message_container.query_selector("p").unwrap().set_text_content(message)
}
///|
fn HtmlActuator::clear_message(self : HtmlActuator) -> Unit {
self.message_container
.class_list()
..remove("game-won")
..remove("game-over")
..remove("restart-game")
.remove("undo-move")
}
///|
pub fn HtmlActuator::prompt_undo(self : HtmlActuator) -> Unit {
self.clear_message()
let message = i18n("undo_the_current_move")
self.message_container.class_list().add("undo-move")
self.message_container.query_selector("p").unwrap().set_text_content(message)
}