Compare commits

..

26 Commits

Author SHA1 Message Date
Alfred Melch 7d67e62689 Add root level readme 5 years ago
Alfred Melch 704e0a62b1 Update docs 5 years ago
Alfred Melch 1e5b5e9d4d Move frontend code 5 years ago
Alfred Melch c648fefa7e Move server files 5 years ago
Alfred Melch 3ad7b61b7d Add a game loop 5 years ago
Alfred Melch 34e61adbe8 Add mouse listeners 5 years ago
Fabian Schöttl 62e317bbb5 Move global offset/scale out of draw functions 5 years ago
Fabian Schöttl f9322831f9 Add basic map rendering 5 years ago
Alfred Melch 731404aea0 Add state api 5 years ago
Alfred Melch aec0b77d29 Add models 5 years ago
Alfred Melch 9f14b74575 add api with example usage 5 years ago
Alfred Melch 3c0d84c628 Add cors 5 years ago
Alfred Melch bca41f2231 Add map generator 5 years ago
Alfred Melch aee29baad0 Add minimal flask server 5 years ago
Alfred Melch 4b79c6c8a4 fix border 5 years ago
Alfred Melch fa65c41998 Add canvas border 5 years ago
Alfred Melch 2874be65d9 Add lib 5 years ago
Alfred Melch 6ba79c454d Add makefile 5 years ago
Alfred Melch 234919d56c declare packages 5 years ago
Alfred Melch 9cafcbbc8c Add docu 5 years ago
Alfred Melch 0661c0a956 Add js project files 5 years ago
Fabian Schöttl 22b9596c5f Move .md notes into notes dir 5 years ago
Fabian Schöttl c680db0eda Merge branch 'alf' into brainstorm_fabi 5 years ago
Fabian Schöttl 820ead32dd Merge branch 'till_rebased' into brainstorm_fabi 5 years ago
Till Niklas Assmann 5de1351845 initial commit with settings and gameplay ideas 5 years ago
Alfred Melch 1d6ede4dc8 add notes 5 years ago

1
.gitignore vendored

@ -0,0 +1 @@
.vscode/

@ -1 +1 @@
#Hello there # EbermergenTD

@ -1,33 +0,0 @@
# Fabi
## Spaceship progression game
* A player ...
* controls a single spaceship (in special cases multiple spaceships (2-4))
* travels to friendly planets/spacestations to buy/sell/trade resources
* travels to friendly planets/asteroids to gather resources
* upgrades his spaceship by buying parts/upgrades
* travels to enemy planets/spacestations/asteroids to capture them
* Players play together ...
* by combining perks/abilities/roles
* by sharing resources
* Players play against eachother ...
* by fighting them
* in order to steal resources/power over a planet/spacestation
* Planets/Spacestations/Asteroids/Spaceships travel along realistic orbits
* -> Dynamic Map Layout
* Requires strategic placement of player ships, but not in traditional 2d space but in orbits (now you're thinking in orbits)
* Fighting ...
* involves: spotting, targeting, damage dealing
* should take at least 1 min from spotting to damage dealing
* should not always result in the loss of a spaceship
* damaged spaceships should have the options to flee from the fight
* Progression involves ...
* upgrading your ship
* mining equipment / cargo capacity / weapons
* increasing your reputation by NPC factions
* by capturing stations/planets in their name
* by trading with them
* A match is won ...
* if all enemy players are destroyed
* or, reputation by all factions is above certain threshold

@ -0,0 +1,99 @@
# Till
settings:
scifi/space - factions with small fleets and cities/outposts in space
scifi/shipcommander - focus on one ship (few additional ships possible)
- rpg, command of one bigger capital ship, with small frigates/cruisers as additional ships, setting a bit mad max style, post-apocalypse in space, damaged, rigged ships, nothing is build new, everything is salvaged or made from scrap
- different fighting styles: pure weaponry - long rage (sniping), short range (brawling), medium (balanced), crowd control (jamming, decoys), support (healing (but out of combat), refueling, rearming, recrewing), buffs/debuffing (increased sensor range, better sensors)
but fighting is more dependant on upgrades, and a few meaningful decisions (maneuvers, target selection) one does not aim or control the ship directly
in one fighting encounter parts of a ship can be damaged (for example the engine), after that the captain of the losing ship can retreat, the winner gets the destroyed part of the loser, when every part is destroyed one dies.
- map is a solar system, 2d, viewed from the solar north
- planets and ships move with orbital mechanics
- fog of war, a big focus is finding the other ships, getting to places where they were reveals possible flight paths
- upgrades at certain space stations (weapon space station, armor space station, propulsion space station), if enemies are at a space station they can attack you
- wide gameplay, more smaller ships with limit (5), tall gameplay one ship but good
- economy/progression: salvaging, exploration and fighting small NPCs with chance of finding gear (damaged) that is partly functional and that can be made 100% useful at the space stations
killing the npcs like in warcraft 3
- goal: destroying the other ship captains ship (lore: someone has payed you to do so), gametime is limited by the amount of scrap, every captain can lose his capital ship
scifi/ground based
- underground warfare (inspiration: lego mining sets; flash game motherload) based in tunnels, maybe wildwest style, small companies fighting over resources
- map is 2d, with up/down, left/right
- economy is mining/industry based with base building, upgrading resource buildings and defenses, mining outposts can be be build which allow more resources to be gathered, but transport vehicles can be attacked (mad max style feeling but underground with scifi)
- action/warfare small number of units with direct attacks (lasers, guns, sabotage), indirect attacks (collapsing tunnels, dropping stalactites, explosives/mines, flooding with water or lava)
- movement: tunnels fast movement, but tunnels need build time; digging is slower but allows free movement, obstacles in the world limit movement/digging of tunnels (hard rocks, lava lakes, underground lakes)
- goal: destruction of the enemy base
- downtime: digging tunnels, upgrading resources and vehicles
- wide gameplay: more outposts, maybe more but weaker vehicles | + more map control | + faster expansion | - easier to attack (every outpost is weaker, small number of vehicles are limiting) |
- tall gameplay: fewer harder to attack outposts and vehicles
strategy game
- gameplay like in risk/supremacy/axis&allies/aftertheday
- sectors can be captured and must be secured which takes time
- sectors grant resources and manpower
- possible settings: insect war, steampunk, scifi (robots/factorio)
- important feature: building infrastructure and destroying enemy infrastructure (roads, pipelines, canals, railways)
- connecting and holding certain sectors gives a boost/bonus to production of resources or units
- unit counts high (100-1000 units per army)
- goal: destroying enemy hq
- movement faster along infrastructure
- 2d map, but no grid
- warfare: rock/paper/scissor type units
- tall empire: better but fewer infrastructure, wide empire vice versa
- downtime: building infrastructure, unit movement
# Alf
## Setting: DND
Kleine Spieleranzahl (etwa 1-4). Reines PVE. Storylastig.
- Replayability durch randomization
## Setting: WC3 monster trainer
## Setting: Ballerburg
Playercount can be low to high. At the beginning only near range enemies. Striking distance increases with time/players eliminated.
Fits into the game time restriction.
Skill tree for replayability,
Make make it a tower defense with leaflet map api
## Setting: Legion TD
Round based tower defense.
# Fabi
## Spaceship progression game
* A player ...
* controls a single spaceship (in special cases multiple spaceships (2-4))
* travels to friendly planets/spacestations to buy/sell/trade resources
* travels to friendly planets/asteroids to gather resources
* upgrades his spaceship by buying parts/upgrades
* travels to enemy planets/spacestations/asteroids to capture them
* Players play together ...
* by combining perks/abilities/roles
* by sharing resources
* Players play against eachother ...
* by fighting them
* in order to steal resources/power over a planet/spacestation
* Planets/Spacestations/Asteroids/Spaceships travel along realistic orbits
* -> Dynamic Map Layout
* Requires strategic placement of player ships, but not in traditional 2d space but in orbits (now you're thinking in orbits)
* Fighting ...
* involves: spotting, targeting, damage dealing
* should take at least 1 min from spotting to damage dealing
* should not always result in the loss of a spaceship
* damaged spaceships should have the options to flee from the fight
* Progression involves ...
* upgrading your ship
* mining equipment / cargo capacity / weapons
* increasing your reputation by NPC factions
* by capturing stations/planets in their name
* by trading with them
* A match is won ...
* if all enemy players are destroyed
* or, reputation by all factions is above certain threshold

@ -0,0 +1,2 @@
FLASK_APP=ebermergen
FLASK_ENV=development

2
server/.gitignore vendored

@ -0,0 +1,2 @@
venv
*.pyc

@ -0,0 +1,12 @@
venv:
python3 -m venv venv
./venv/bin/pip install -U pip wheel setuptools
install:
./venv/bin/pip install -r requirements.txt
run:
./venv/bin/flask run --host 0.0.0.0
test:
./venv/bin/python -m unittest

@ -0,0 +1,12 @@
# Backend for EbermergenTD
## Install
Requirements: python3, python3-venv and pip
## Available scripts
- `make venv`: creates a python virtual environment
- `make install`: installs python dependencies
- `make run`: runs the development server
- `make test`: runs unittests

@ -0,0 +1,53 @@
import json
from flask import Flask, jsonify
from flask_cors import CORS
from ebermergen.models.game import Game
from .lib import generate_map
from .lib.ebermergen_game import EbermergenGame
app = Flask(__name__)
CORS(app)
games = {}
@app.route('/')
def index():
return 'Hello World'
@app.route('/generate')
def generate():
return jsonify(generate_map((10, 8)))
@app.route('/generate/<int:x>/<int:y>')
def generate_var(x, y):
return jsonify(generate_map((x, y)))
@app.route('/state')
def state():
a = Game()
a.seed_map()
return jsonify(a.serialize())
@app.route('/lala')
def lala():
return jsonify({k: v.state for (k, v) in games.items()})
@app.route('/new-game/<name>', methods=['GET', 'POST'])
def new_game(name):
games[name] = EbermergenGame()
games[name].start_game()
return jsonify({'message': 'success'})
@app.route('/action/<name>/<type>')
def action(name, type):
games[name].perform_action(type, {})
return jsonify({'message': 'success'})

@ -0,0 +1,8 @@
import random
TERRAIN_TYPES = [0, 1, 2, 3]
def generate_map(dimensions):
x_len, y_len = dimensions
return [[random.choice(TERRAIN_TYPES) for y in range(y_len)] for x in range(x_len)]

@ -0,0 +1,50 @@
import atexit
import time
import threading
import queue
from .gameloop import GameLoop
def tick(dt, first, second):
print('%.2f' % dt, first, second)
pass
def worker(state, action_queue):
state['counter'] = 0
state['actions'] = []
loop = GameLoop(tick=tick, fps=1, fixed_dt=True, args=(1, 2))
while True:
item = action_queue.get()
if item['action'] == 'STOP':
loop.stop()
if item['action'] == 'START':
loop.start()
state['counter'] += 1
state['actions'].append(item['action'])
class EbermergenGame:
"""Runs a single game instance
Holds the serialized state.
Provides methods to perform actions.
"""
def __init__(self):
self.state = {}
self.actions = queue.Queue()
self.thread = threading.Thread(
target=worker, args=(self.state, self.actions))
def start_game(self):
self.thread.start()
atexit.register(self.interupt)
def interupt(self):
# handle gracefull shutdown
pass
def perform_action(self, action, payload={}):
self.actions.put({'action': action, 'payload': payload})

@ -0,0 +1,46 @@
import threading
import time
import random
class GameLoop():
"""Implements a loop that calls tick every 1/fps
tick gets called with the time delta to the last call as parameter
if fixed_dt is True the tick function gets called with 1/fps
"""
def __init__(self, fps=1, tick=None, fixed_dt=False, args=None):
self.fps = fps
self.tick = tick if tick is not None else self.default_tick
self.args = args or tuple()
self.fixed_dt = fixed_dt
self.running = False
self.last_frame_time = time.time()
self.thread = threading.Thread(target=self.loop)
def loop(self):
while self.running:
current_time = time.time()
dt = current_time - self.last_frame_time
self.last_frame_time = current_time
if self.fixed_dt:
self.tick(1 / self.fps, *self.args)
else:
self.tick(dt, *self.args)
sleep_time = (1 / self.fps) - (time.time() - self.last_frame_time)
if sleep_time > 0:
time.sleep(sleep_time)
def start(self):
self.running = True
self.thread.start()
def stop(self):
self.running = False
def default_tick(self, dt):
print('ticked. Last tick was %.2f seconds ago' % dt)
time.sleep(random.choice([0.4, 0.5, 0.6, 1.4, 2.1]) * 1/self.fps)

@ -0,0 +1,14 @@
class AutoID:
last_id = 0
def __init__(self):
self.increment_id()
self.id = self.last_id
@classmethod
def increment_id(cls):
cls.last_id += 1
@classmethod
def reset_id(cls):
cls.last_id = 0

@ -0,0 +1,26 @@
from .auto_id import AutoID
class Town(AutoID):
def __init__(self, player, coords):
super().__init__()
self.player = player
self.coords = coords
self.level = 1
class Tower(AutoID):
def __init__(self, player, coords):
super().__init__()
self.player = player
self.coords = coords
self.level = 1
class Mob(AutoID):
def __init__(self, player, coords):
super().__init__()
self.player = player
self.coords = coords
self.level = 1

@ -0,0 +1,48 @@
import random
import json
from .player import Player
from .map import Map
from .entities import Town
from ebermergen.lib.gameloop import GameLoop
class Game:
def __init__(self):
self.players = [Player('P1'), Player('P2')]
self.map = Map((20, 16))
self.towns = []
self.towers = []
self.mobs = []
def get_free_positions(self):
positions = self.map.get_positions()
positions = positions.difference([el.coords for el in self.towns])
positions = positions.difference([el.coords for el in self.towers])
return positions
def get_random_position(self):
return random.choice(tuple(self.get_free_positions()))
def seed_map(self):
self.towns.append(Town(self.players[0], self.get_random_position()))
self.towns.append(Town(self.players[1], self.get_random_position()))
self.towns.append(Town(self.players[0], self.get_random_position()))
self.towns.append(Town(self.players[1], self.get_random_position()))
def serialize(self):
naive = json.loads(json.dumps(self, default=lambda o: o.__dict__))
def reduce_player(entities):
for obj in entities:
obj['player'] = obj['player']['id']
def make_id_based(entities):
return {obj['id']: obj for obj in entities}
for entity_type in ['towns', 'mobs', 'towers']:
reduce_player(naive[entity_type])
naive[entity_type] = make_id_based(naive[entity_type])
naive['players'] = make_id_based(naive['players'])
return naive

@ -0,0 +1,15 @@
from ebermergen.lib import generate_map
class Map:
def __init__(self, dimensions):
self.dimensions = dimensions
self.terrain = generate_map(dimensions)
def get_positions(self):
positions = set()
for x in range(self.dimensions[0]):
for y in range(self.dimensions[1]):
positions.add((x, y))
return positions

@ -0,0 +1,7 @@
from .auto_id import AutoID
class Player(AutoID):
def __init__(self, name):
super().__init__()
self.name = name

@ -0,0 +1,4 @@
flask==1.1.1
python-dotenv==0.10.3
autopep8==1.4.4
pylint==2.4.4

@ -0,0 +1,35 @@
import unittest
from ebermergen.models.auto_id import AutoID
class TestAutoID(unittest.TestCase):
def setUp(self):
AutoID.reset_id()
def test_first_instance(self):
a = AutoID()
self.assertEqual(a.id, 1)
def test_increment(self):
a, b = AutoID(), AutoID()
self.assertEqual(a.id, 1)
self.assertEqual(b.id, 2)
def test_inheritance(self):
class ClassA(AutoID):
pass
class ClassB(AutoID):
pass
a = ClassA()
b = ClassB()
self.assertEqual(a.id, 1)
self.assertEqual(b.id, 1)
a2 = ClassA()
b2 = ClassB()
self.assertEqual(a.id, 1)
self.assertEqual(b.id, 1)
self.assertEqual(a2.id, 2)
self.assertEqual(b2.id, 2)

@ -0,0 +1,13 @@
import unittest
from ebermergen.models.game import Game
class TestSerializer(unittest.TestCase):
def test_instantiation(self):
a = Game()
self.assertEqual(2, len(a.players))
def test_seeding(self):
a = Game()
a.seed_map()

@ -0,0 +1,10 @@
import unittest
from ebermergen.models.game import Game
class TestSerializer(unittest.TestCase):
def test_serialize(self):
a = Game()
a.seed_map()
a.serialize()

@ -0,0 +1,3 @@
{
"extends": ["react-app", "prettier"]
}

2
web/.gitignore vendored

@ -0,0 +1,2 @@
node_modules/
build/

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

@ -0,0 +1,14 @@
# Hello there
## Install
Requirements: node and npm
- `npm install`: installs dependencies in node_modules
## Available scripts
- `npm start`: starts a development server
- `npm run build`: make a production build
- `npm run lint`: lint js files using eslint
- `npm run format`: format js files using prettier

@ -0,0 +1,8 @@
module.exports = api => {
// caching the babel config
api.cache.using(() => process.env.NODE_ENV)
return {
presets: ['@babel/preset-env', '@babel/preset-react']
// plugins: [api.env('development') && 'react-refresh/babel'].filter(Boolean)
}
}

@ -0,0 +1,67 @@
{
"name": "ebermergen",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production",
"build-fresh": "rm -rf build/ && npm run build",
"serve": "ws -d build --compress --hostname 0.0.0.0",
"test": "tests",
"lint": "eslint src",
"format": "prettier --write src/**/*.js"
},
"repository": {
"type": "git",
"url": "git@localhost:ebermergen"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"classnames": "^2.2.6",
"debounce": "^1.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-icons": "^3.8.0",
"react-redux": "^7.1.3",
"redux": "^4.0.4",
"regenerator-runtime": "^0.13.3",
"reselect": "^4.0.0",
"reselect-tools": "0.0.7"
},
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/preset-env": "^7.7.6",
"@babel/preset-react": "^7.7.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"css-loader": "^3.3.2",
"eslint": "^6.7.2",
"eslint-config-prettier": "^6.7.0",
"eslint-config-react-app": "^5.1.0",
"eslint-plugin-flowtype": "^4.5.2",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^2.3.0",
"html-webpack-plugin": "^4.0.0-beta.11",
"local-web-server": "^3.0.7",
"mini-css-extract-plugin": "^0.8.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-loader": "^3.0.0",
"postcss-normalize": "^8.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^1.19.1",
"react-refresh": "^0.7.0",
"style-loader": "^1.0.1",
"webpack": "^4.41.2",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"webpackbar": "^4.0.0"
}
}

@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env'),
require('postcss-normalize')
]
}

@ -0,0 +1,30 @@
body,
html {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
background-color: #fce6cb;
}
html {
overflow-y: scroll;
}
header {
border-bottom: 2px solid white;
margin-bottom: 1em;
display: flex;
align-items: center;
padding: 0px 10px;
}
header h1 {
flex: 1;
}
footer {
border-top: 2px solid white;
margin-top: 1em;
padding: 0 10px;
}

@ -0,0 +1,18 @@
import React from 'react'
import './App.css'
import { Canvas } from './components.js/Canvas'
export const App = () => {
return (
<>
<header>
<h1>Ebermergen</h1>
</header>
<main>
<Canvas />
</main>
<footer>This is the footer</footer>
</>
)
}

@ -0,0 +1,11 @@
import Axios from 'axios'
const BACKEND_HOST = 'http://169.254.61.222:5000'
export function getMap(x, y) {
return Axios.get(`${BACKEND_HOST}/generate/${x}/${y}`).then(data => data.data)
}
export function getState() {
return Axios.get(`${BACKEND_HOST}/state`).then(data => data.data)
}

@ -0,0 +1,3 @@
.border {
border: 1px solid red;
}

@ -0,0 +1,39 @@
import React, { useLayoutEffect, useRef } from 'react'
import {
main,
mousedown,
mousemove,
mouseup,
click,
dblclick,
mouseenter,
mouseleave
} from '../lib'
import styles from './Canvas.css'
export const Canvas = () => {
const canvasRef = useRef(null)
useLayoutEffect(() => {
const canvas = canvasRef.current
main(canvas)
canvas.addEventListener('mousedown', mousedown)
canvas.addEventListener('mouseup', mouseup)
canvas.addEventListener('mousemove', mousemove)
canvas.addEventListener('click', click)
canvas.addEventListener('dblclick', dblclick)
canvas.addEventListener('mouseenter', mouseenter)
canvas.addEventListener('mouseout', mouseleave)
})
return (
<canvas
className={styles.border}
width={500}
height={400}
ref={canvasRef}
/>
)
}

@ -0,0 +1,20 @@
import 'regenerator-runtime/runtime'
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App'
import { getMap } from './api'
function createRootElement() {
const body = document.getElementsByTagName('body')[0]
const root = document.createElement('div')
root.setAttribute('id', 'root')
body.appendChild(root)
return root
}
ReactDOM.render(<App />, createRootElement())
getMap(10, 20).then(map => {
console.log(map)
})

@ -0,0 +1,192 @@
console.log('import entrypoint lib')
let global_scale = 1
let global_offset = [0, 0]
const cos_60 = Math.cos((60 * Math.PI) / 180)
const sin_60 = Math.sin((60 * Math.PI) / 180)
const TileType = {
LAND: 0,
WATER: 1
}
class TileGrid {
constructor(width = 100, height = 100) {
this.width = 100
this.height = 100
this.tiles = new Array(this.width * this.height)
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.tiles[this.get_index(x, y)] = TileType.LAND
}
}
}
get_tile(x, y) {
return this.tiles[this.get_index(x, y)]
}
set_tile(x, y, type) {
this.tiles[this.get_index(x, y)] = type
}
get_index(x, y) {
return y * this.width + x
}
}
function tile_index_to_coord(x, y) {
const x_stride = 2 * cos_60 + 2
const y_stride = sin_60
let x_offset = 0
if (y % 2 === 0) {
x_offset = 1 + cos_60
}
return [x * x_stride + x_offset, y * y_stride]
}
function draw_hex_tile(ctx, index, type) {
const center = tile_index_to_coord(index[0], index[1])
const radius = 1
ctx.save()
switch (type) {
case TileType.LAND:
ctx.fillStyle = 'green'
break
case TileType.WATER:
ctx.fillStyle = 'blue'
break
}
ctx.strokeStyle = 'black'
ctx.lineWidth = 1 / global_scale
ctx.translate(center[0], center[1])
ctx.scale(radius, radius)
ctx.save()
ctx.beginPath()
ctx.moveTo(-1, 0)
ctx.lineTo(-cos_60, sin_60)
ctx.lineTo(cos_60, sin_60)
ctx.lineTo(1, 0)
ctx.lineTo(cos_60, -sin_60)
ctx.lineTo(-cos_60, -sin_60)
ctx.lineTo(-1, 0)
ctx.restore()
ctx.fill()
ctx.stroke()
ctx.restore()
}
function draw_map(ctx) {
const tile_grid = new TileGrid(100, 100)
tile_grid.set_tile(2, 2, TileType.WATER)
tile_grid.set_tile(50, 10, TileType.WATER)
for (let y = 0; y < tile_grid.height; y++) {
for (let x = 0; x < tile_grid.width; x++) {
draw_hex_tile(ctx, [x, y], tile_grid.get_tile(x, y))
const coord = tile_index_to_coord(x, y)
if (global_scale > 15) {
ctx.save()
ctx.translate(coord[0], coord[1])
ctx.scale(1 / global_scale, 1 / global_scale)
ctx.font = '12px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
ctx.fillText(String([x, y]), 0, 0)
ctx.restore()
}
}
}
}
function draw_tower(ctx, index) {
const coord = tile_index_to_coord(index[0], index[1])
const radius_1 = 0.35
const radius_2 = 0.3
const height_1 = 1.1
const height_2 = 1.6
ctx.save()
ctx.lineWidth = 1 / global_scale
ctx.translate(coord[0], coord[1])
ctx.fillStyle = 'gray'
ctx.strokeStyle = 'black'
ctx.beginPath()
ctx.arc(0, 0, radius_1, 0, Math.PI)
ctx.lineTo(-radius_2, -height_1)
ctx.arc(0, -height_1, radius_2, Math.PI, 0, true)
ctx.lineTo(radius_1, 0)
ctx.fill()
ctx.stroke()
ctx.fillStyle = 'darkred'
ctx.strokeStyle = 'black'
ctx.beginPath()
ctx.arc(0, -height_1, radius_2, Math.PI, 0, true)
ctx.lineTo(0, -height_2)
ctx.lineTo(-radius_2, -height_1)
ctx.fill()
ctx.stroke()
ctx.restore()
}
function draw(ctx, offset, scale) {
global_scale = scale
global_offset = offset
ctx.translate(global_offset[0], global_offset[1])
ctx.scale(global_scale, global_scale)
draw_map(ctx)
let towers = [
[2, 0],
[2, 1],
[3, 1],
[2, 3],
[3, 3],
[2, 4]
]
for (let i = 0; i < 6; i++) {
draw_tower(ctx, towers[i])
}
}
export function main(canvas) {
console.log('entrypoint function')
console.log(canvas)
if (canvas.getContext) {
const ctx = canvas.getContext('2d')
draw(ctx, [100, 100], 10)
}
}
export function mousedown(evt) {
console.log(evt)
}
export function mouseup(evt) {
console.log(evt)
}
export function mousemove(evt) {
// console.log(evt)
}
export function click(evt) {
console.log(evt)
}
export function dblclick(evt) {
console.log(evt)
}
export function mouseenter(evt) {
console.log(evt)
}
export function mouseleave(evt) {
console.log(evt)
}

@ -0,0 +1,93 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const WebpackBar = require('webpackbar')
module.exports = (env, argv) => {
const isEnvProduction = argv.mode === 'production'
const isEnvDevelopment = argv.mode === 'development'
return {
output: {
path: path.resolve(__dirname, 'build'),
filename: 'static/js/[name].[contenthash:8].js',
chunkFilename: 'static/js/[name].[contenthash:8].chunk.js'
},
devtool: isEnvProduction ? 'source-map' : 'cheap-module-source-map',
module: {
rules: [
// process javascript with babel
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
// process css. css modules are enabled.
{
test: /\.css$/,
use: [
isEnvDevelopment && 'style-loader',
isEnvProduction && MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: isEnvProduction
? '[hash:base64]'
: '[path][name]__[local]'
}
}
},
'postcss-loader'
].filter(Boolean)
}
]
},
optimization: {
minimize: isEnvProduction,
splitChunks: {
chunks: 'all',
name: false
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
},
plugins: [
// creat an index.html
new HtmlWebpackPlugin(),
// show a progress bar
new WebpackBar(),
// create a report.html for bundle size
// extract css to css files
isEnvProduction &&
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
}),
// use cssnano to minify css
isEnvProduction &&
new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
map: { inline: false, annotation: true }
}
}),
isEnvProduction &&
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
// hot reload not working right now
// isEnvDevelopment &&
// new ReactRefreshWebpackPlugin({ disableRefreshCheck: true })
].filter(Boolean),
devServer: {
stats: 'minimal'
}
}
}
Loading…
Cancel
Save