Merge pull request #790 from thecodingmachine/iframe_api

Adding an API for inter-iframe communication
This commit is contained in:
David Négrier 2021-03-30 16:31:30 +02:00 committed by GitHub
commit 02fb42b68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 3122 additions and 37 deletions

View File

@ -88,8 +88,6 @@
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"TURN_USER": "workadventure",
"TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11"

View File

@ -43,7 +43,7 @@ services:
- ./front:/usr/src/app
labels:
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080"
- "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"

View File

@ -8,6 +8,11 @@ FROM thecodingmachine/nodejs:14-apache
COPY --chown=docker:docker front .
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
# Removing the iframe.html file from the final image as this adds a XSS attack.
# iframe.html is only in dev mode to circumvent a limitation
RUN rm dist/iframe.html
RUN yarn install
ENV NODE_ENV=production

View File

@ -1,3 +1,4 @@
index.html
index.tmpl.html.tmp
style.*.css
/js/
style.*.css

17
front/dist/iframe.html vendored Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<script src="/iframe_api.js" ></script>
<script>
// Note: this is a huge XSS flow as we allow anyone to load a Javascript file in our domain.
// This file must ABSOLUTELY be removed from the Docker images/deployments and is only here
// for development purpose (because dynamically generated iframes are not working with
// webpack hot reload due to an issue with rights)
const urlParams = new URLSearchParams(window.location.search);
const scriptUrl = urlParams.get('script');
const script = document.createElement('script');
script.src = scriptUrl;
document.head.append(script);
</script>
</head>
</html>

View File

@ -29,6 +29,9 @@
<base href="/">
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
</head>
<body id="body" style="margin: 0; background-color: #000">

View File

@ -1123,6 +1123,31 @@ div.action p.action-body{
margin-left: calc(50% - 75px);
border-radius: 15px;
}
.popUpElement{
font-family: 'Press Start 2P';
text-align: left;
color: white;
}
.popUpElement div {
font-family: 'Press Start 2P';
font-size: 10px;
background-color: #727678;
}
.popUpElement button {
position: relative;
font-size: 10px;
border-image-repeat: revert;
margin-right: 5px;
}
.popUpElement .buttonContainer {
float: right;
background-color: inherit;
}
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}

View File

@ -336,7 +336,7 @@ export class ConsoleGlobalMessageManager {
}
active(){
this.userInputManager.clearAllKeys();
this.userInputManager.disableControls();
this.divMainConsole.style.top = '0';
this.activeConsole = true;
}

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isButtonClickedEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
buttonId: tg.isNumber,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
author: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;

View File

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isClosePopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;

View File

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isEnterLeaveEvent =
new tg.IsInterface().withProperties({
name: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
*/
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;

View File

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isGoToPageEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;

View File

@ -0,0 +1,7 @@
export interface IframeEvent {
type: string;
data: unknown;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string';

View File

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenCoWebSiteEvent = tg.GuardedType<typeof isOpenCoWebsite>;

View File

@ -0,0 +1,20 @@
import * as tg from "generic-type-guard";
const isButtonDescriptor =
new tg.IsInterface().withProperties({
label: tg.isString,
className: tg.isOptional(tg.isString)
}).get();
export const isOpenPopupEvent =
new tg.IsInterface().withProperties({
popupId: tg.isNumber,
targetObject: tg.isString,
message: tg.isString,
buttons: tg.isArray(isButtonDescriptor)
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>;

View File

@ -0,0 +1,13 @@
import * as tg from "generic-type-guard";
export const isOpenTabEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>;

View File

@ -0,0 +1,10 @@
import * as tg from "generic-type-guard";
export const isUserInputChatEvent =
new tg.IsInterface().withProperties({
message: tg.isString,
}).get();
/**
* A message sent from the game to the iFrame when a user types a message in the chat.
*/
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>;

View File

@ -0,0 +1,238 @@
import {Subject} from "rxjs";
import {ChatEvent, isChatEvent} from "./Events/ChatEvent";
import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent";
import {UserInputChatEvent} from "./Events/UserInputChatEvent";
import * as crypto from "crypto";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
import {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
import {scriptUtils} from "./ScriptUtils";
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
public readonly openPopupStream = this._openPopupStream.asObservable();
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
public readonly openTabStream = this._openTabStream.asObservable();
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
public readonly goToPageStream = this._goToPageStream.asObservable();
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
public readonly closePopupStream = this._closePopupStream.asObservable();
private readonly _displayBubbleStream: Subject<void> = new Subject();
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
init() {
window.addEventListener("message", (message) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let found = false;
for (const iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
found = true;
break;
}
}
if (!found) {
return;
}
const payload = message.data;
if (isIframeEventWrapper(payload)) {
if (payload.type === 'chat' && isChatEvent(payload.data)) {
this._chatStream.next(payload.data);
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
this._openPopupStream.next(payload.data);
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
this._closePopupStream.next(payload.data);
}
else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
scriptUtils.openTab(payload.data.url);
}
else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url);
}
else if(payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
}
else if (payload.type === 'disablePlayerControl'){
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControl'){
this._enablePlayerControlStream.next();
}
else if (payload.type === 'displayBubble'){
this._displayBubbleStream.next();
}
else if (payload.type === 'removeBubble'){
this._removeBubbleStream.next();
}
}
}, false);
}
/**
* Allows the passed iFrame to send/receive messages via the API.
*/
registerIframe(iframe: HTMLIFrameElement): void {
this.iframes.add(iframe);
}
unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl)
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
// Using external iframe mode (
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement('iframe');
iframe.id = this.getIFrameId(scriptUrl);
iframe.style.display = 'none';
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
const html = '<!doctype html>\n' +
'\n' +
'<html lang="en">\n' +
'<head>\n' +
'<script src="'+window.location.protocol+'//'+window.location.host+'/iframe_api.js" ></script>\n' +
'<script src="'+scriptUrl+'" ></script>\n' +
'</head>\n' +
'</html>\n';
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = html;
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
}
private getIFrameId(scriptUrl: string): string {
return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex");
}
unregisterScript(scriptUrl: string): void {
const iFrameId = this.getIFrameId(scriptUrl);
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
if (!iframe) {
throw new Error('Unknown iframe for script "'+scriptUrl+'"');
}
this.unregisterIframe(iframe);
iframe.remove();
this.scripts.delete(scriptUrl);
}
sendUserInputChat(message: string) {
this.postMessage({
'type': 'userInputChat',
'data': {
'message': message,
} as UserInputChatEvent
});
}
sendEnterEvent(name: string) {
this.postMessage({
'type': 'enterEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
sendLeaveEvent(name: string) {
this.postMessage({
'type': 'leaveEvent',
'data': {
"name": name
} as EnterLeaveEvent
});
}
sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({
'type': 'buttonClickedEvent',
'data': {
popupId,
buttonId
} as ButtonClickedEvent
});
}
/**
* Sends the message... to all allowed iframes.
*/
private postMessage(message: IframeEvent) {
for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*');
}
}
}
export const iframeListener = new IframeListener();

View File

@ -0,0 +1,23 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
class ScriptUtils {
public openTab(url : string){
window.open(url);
}
public goToPage(url : string){
window.location.href = url;
}
public openCoWebsite(url : string){
coWebsiteManager.loadCoWebsite(url,url);
}
public closeCoWebSite(){
coWebsiteManager.closeCoWebsite();
}
}
export const scriptUtils = new ScriptUtils();

View File

@ -59,11 +59,14 @@ import {TextureError} from "../../Exception/TextureError";
import {addLoader} from "../Components/Loader";
import {ErrorSceneName} from "../Reconnecting/ErrorScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {iframeListener} from "../../Api/IframeListener";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement;
import {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
@ -157,6 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private playerName!: string;
private characterLayers!: string[];
private messageSubscription: Subscription|null = null;
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({
@ -263,7 +267,8 @@ export class GameScene extends ResizableScene implements CenterListener {
break;
}
default:
throw new Error('Unsupported object type: "'+ itemType +'"');
continue;
//throw new Error('Unsupported object type: "'+ itemType +'"');
}
itemFactory.preload(this.load);
@ -289,6 +294,12 @@ export class GameScene extends ResizableScene implements CenterListener {
});
});
}
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.registerScript(script);
}
}
//hook initialisation
@ -306,7 +317,7 @@ export class GameScene extends ResizableScene implements CenterListener {
gameManager.gameSceneIsCreated(this);
urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError())
const playerName = gameManager.getPlayerName();
@ -410,6 +421,7 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events
layoutManager.setListener(this);
this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
const camera = this.cameras.main;
@ -577,12 +589,12 @@ export class GameScene extends ResizableScene implements CenterListener {
const contextRed = this.circleRedTexture.context;
contextRed.beginPath();
contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false);
// context.lineWidth = 5;
//context.lineWidth = 5;
contextRed.strokeStyle = '#ff0000';
contextRed.stroke();
this.circleRedTexture.refresh();
}
private safeParseJSONstring(jsonString: string|undefined, propertyName: string) {
try {
@ -606,7 +618,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite();
}else{
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined);
coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined);
layoutManager.removeActionButton('openWebsite', this.userInputManager);
};
@ -672,6 +684,103 @@ export class GameScene extends ResizableScene implements CenterListener {
this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => {
newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
});
this.gameMap.onPropertyChange('zone', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
iframeListener.sendLeaveEvent(oldValue as string);
} else {
iframeListener.sendEnterEvent(newValue as string);
}
});
}
private listenToIframeEvents(): void {
iframeListener.openPopupStream.subscribe((openPopupEvent) => {
let objectLayerSquare : ITiledMapObject;
const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject);
if (targetObjectData !== undefined){
objectLayerSquare = targetObjectData;
} else {
console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map.");
return;
}
const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message);
let html = `<div id="container"><div class="nes-container with-title is-centered">
${escapedMessage}
</div> </div>`;
const buttonContainer = `<div class="buttonContainer"</div>`;
html += buttonContainer;
let id = 0;
for (const button of openPopupEvent.buttons) {
html += `<button type="button" class="nes-btn is-${HtmlUtils.escapeHtml(button.className ?? '')}" id="popup-${openPopupEvent.popupId}-${id}">${HtmlUtils.escapeHtml(button.label)}</button>`;
id++;
}
const domElement = this.add.dom(objectLayerSquare.x + objectLayerSquare.width/2 ,
objectLayerSquare.y + objectLayerSquare.height/2).createFromHTML(html);
const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement;
container.style.width = objectLayerSquare.width + "px";
domElement.scale = 0;
domElement.setClassName('popUpElement');
id = 0;
for (const button of openPopupEvent.buttons) {
const button = HtmlUtils.getElementByIdOrFail<HTMLButtonElement>(`popup-${openPopupEvent.popupId}-${id}`);
const btnId = id;
button.onclick = () => {
iframeListener.sendButtonClickedEvent(openPopupEvent.popupId, btnId);
}
id++;
}
this.tweens.add({
targets : domElement ,
scale : 1,
ease : "EaseOut",
duration : 400,
});
this.popUpElements.set(openPopupEvent.popupId, domElement);
});
iframeListener.closePopupStream.subscribe((closePopupEvent) => {
const popUpElement = this.popUpElements.get(closePopupEvent.popupId);
if (popUpElement === undefined) {
console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?');
}
this.tweens.add({
targets : popUpElement ,
scale : 0,
ease : "EaseOut",
duration : 400,
onComplete : () => {
popUpElement?.destroy();
this.popUpElements.delete(closePopupEvent.popupId);
},
});
});
iframeListener.disablePlayerControlStream.subscribe(()=>{
this.userInputManager.disableControls();
})
iframeListener.enablePlayerControlStream.subscribe(()=>{
this.userInputManager.restoreControls();
})
let scriptedBubbleSprite : Sprite;
iframeListener.displayBubbleStream.subscribe(()=>{
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
scriptedBubbleSprite.setDisplayOrigin(48, 48);
this.add.existing(scriptedBubbleSprite);
})
iframeListener.removeBubbleStream.subscribe(()=>{
scriptedBubbleSprite.destroy();
})
}
private getMapDirUrl(): string {
@ -702,6 +811,12 @@ export class GameScene extends ResizableScene implements CenterListener {
public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsite();
// Stop the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.unregisterScript(script);
}
this.stopJitsi();
audioManager.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
@ -785,8 +900,12 @@ export class GameScene extends ResizableScene implements CenterListener {
return this.getProperty(layer, "startLayer") == true;
}
private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined {
const properties = layer.properties;
private getScriptUrls(map: ITiledMap): string[] {
return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString());
}
private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined {
const properties: ITiledMapLayerProperty[] = layer.properties;
if (!properties) {
return undefined;
}
@ -797,6 +916,14 @@ export class GameScene extends ResizableScene implements CenterListener {
return obj.value;
}
private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] {
const properties: ITiledMapLayerProperty[] = layer.properties;
if (!properties) {
return [];
}
return properties.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()).map((property) => property.value);
}
//todo: push that into the gameManager
private async loadNextGame(exitSceneIdentifier: string){
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
@ -1176,7 +1303,19 @@ export class GameScene extends ResizableScene implements CenterListener {
bottom: camera.scrollY + camera.height,
});
}
private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{
for (const layer of this.mapFile.layers) {
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
for (const object of layer.objects) {
if (object.name === objectName) {
return object;
}
}
}
}
return undefined;
}
private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2);
@ -1233,7 +1372,7 @@ export class GameScene extends ResizableScene implements CenterListener {
//todo: put this into an 'orchestrator' scene (EntryScene?)
private bannedUser(){
this.cleanupClosingScene();
this.userInputManager.clearAllKeys();
this.userInputManager.disableControls();
this.scene.start(ErrorSceneName, {
title: 'Banned',
subTitle: 'You were banned from WorkAdventure',
@ -1245,7 +1384,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private showWorldFullError(): void {
this.cleanupClosingScene();
this.scene.stop(ReconnectingSceneName);
this.userInputManager.clearAllKeys();
this.userInputManager.disableControls();
this.scene.start(ErrorSceneName, {
title: 'Connection rejected',
subTitle: 'The world you are trying to join is full. Try again later.',

View File

@ -14,7 +14,7 @@ export interface ITiledMap {
* Map orientation (orthogonal)
*/
orientation: string;
properties: {[key: string]: string};
properties: ITiledMapLayerProperty[];
/**
* Render order (right-down)

View File

@ -61,7 +61,7 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
this.opened = true;
gameManager.getCurrentGameScene(this.scene).userInputManager.clearAllKeys();
gameManager.getCurrentGameScene(this.scene).userInputManager.disableControls();
this.scene.tweens.add({
targets: this,

View File

@ -31,10 +31,11 @@ export class ActiveEventList {
export class UserInputManager {
private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene;
private isInputDisabled : boolean;
constructor(Scene : GameScene) {
this.Scene = Scene;
this.initKeyBoardEvent();
this.isInputDisabled = false;
}
initKeyBoardEvent(){
@ -63,16 +64,25 @@ export class UserInputManager {
this.Scene.input.keyboard.removeAllListeners();
}
clearAllKeys(){
disableControls(){
this.Scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true;
}
restoreControls(){
this.initKeyBoardEvent();
this.isInputDisabled = false;
}
getEventListForGameTick(): ActiveEventList {
const eventsMap = new ActiveEventList();
if (this.isInputDisabled) {
return eventsMap;
}
this.KeysCode.forEach(d => {
if (d. keyInstance.isDown) {
eventsMap.set(d.event, true);
}
});
return eventsMap;
}

View File

@ -1,5 +1,6 @@
import {HtmlUtils} from "./HtmlUtils";
import {Subject} from "rxjs";
import {iframeListener} from "../Api/IframeListener";
enum iframeStates {
closed = 1,
@ -17,8 +18,8 @@ const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
const animationTime = 500; //time used by the css transitions, in ms.
class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed;
private opened: iframeStates = iframeStates.closed;
private _onResize: Subject<void> = new Subject();
public onResize = this._onResize.asObservable();
@ -27,11 +28,11 @@ class CoWebsiteManager {
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDiv: HTMLDivElement;
private cowebsiteDiv: HTMLDivElement;
private resizing: boolean = false;
private cowebsiteMainDom: HTMLDivElement;
private cowebsiteAsideDom: HTMLDivElement;
get width(): number {
return this.cowebsiteDiv.clientWidth;
}
@ -47,15 +48,15 @@ class CoWebsiteManager {
set height(height: number) {
this.cowebsiteDiv.style.height = height+'px';
}
get verticalMode(): boolean {
return window.innerWidth < window.innerHeight;
}
get isFullScreen(): boolean {
return this.verticalMode ? this.height === window.innerHeight : this.width === window.innerWidth;
}
constructor() {
this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
this.cowebsiteMainDom = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteMainDomId);
@ -76,7 +77,7 @@ class CoWebsiteManager {
this.verticalMode ? this.height -= event.movementY / this.getDevicePixelRatio() : this.width -= event.movementX / this.getDevicePixelRatio();
this.fire();
}
this.cowebsiteAsideDom.addEventListener('mousedown', (event) => {
this.resizing = true;
this.getIframeDom().style.display = 'none';
@ -91,7 +92,7 @@ class CoWebsiteManager {
this.resizing = false;
});
}
private getDevicePixelRatio(): number {
//on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels
//so on chrome-based browser we need to adjust using 'devicePixelRatio'
@ -126,7 +127,7 @@ class CoWebsiteManager {
return iframe;
}
public loadCoWebsite(url: string, base: string, allowPolicy?: string): void {
public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void {
this.load();
this.cowebsiteMainDom.innerHTML = ``;
@ -134,11 +135,14 @@ class CoWebsiteManager {
iframe.id = 'cowebsite-iframe';
iframe.src = (new URL(url, base)).toString();
if (allowPolicy) {
iframe.allow = allowPolicy;
iframe.allow = allowPolicy;
}
const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve();
});
if (allowApi) {
iframeListener.registerIframe(iframe);
}
this.cowebsiteMainDom.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
@ -170,6 +174,10 @@ class CoWebsiteManager {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close();
this.fire();
const iframe = this.cowebsiteDiv.querySelector('iframe');
if (iframe) {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => {
this.cowebsiteMainDom.innerHTML = ``;
resolve();
@ -197,11 +205,11 @@ class CoWebsiteManager {
}
}
}
private fire(): void {
this._onResize.next();
}
private fullscreen(): void {
if (this.isFullScreen) {
this.resetStyle();

View File

@ -3,6 +3,7 @@ import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener";
export type SendMessageCallback = (message:string) => void;
@ -25,6 +26,14 @@ export class DiscussionManager {
constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(''); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion();
});
this.onSendMessageCallback('iframe_listener', (message) => {
iframeListener.sendUserInputChat(message);
})
}
private createDiscussPart(name: string) {
@ -61,12 +70,12 @@ export class DiscussionManager {
const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.onfocus = () => {
if(this.userInputManager) {
this.userInputManager.clearAllKeys();
this.userInputManager.disableControls();
}
}
inputMessage.onblur = () => {
if(this.userInputManager) {
this.userInputManager.initKeyBoardEvent();
this.userInputManager.restoreControls();
}
}
inputMessage.type = "text";

View File

@ -24,7 +24,7 @@ export class HtmlUtils {
throw new Error("Cannot find HTML element with id '"+id+"'");
}
private static escapeHtml(html: string): string {
public static escapeHtml(html: string): string {
const text = document.createTextNode(html);
const p = document.createElement('p');
p.appendChild(text);

232
front/src/iframe_api.ts Normal file
View File

@ -0,0 +1,232 @@
import {ChatEvent, isChatEvent} from "./Api/Events/ChatEvent";
import {isIframeEventWrapper} from "./Api/Events/IframeEvent";
import {isUserInputChatEvent, UserInputChatEvent} from "./Api/Events/UserInputChatEvent";
import {Subject} from "rxjs";
import {EnterLeaveEvent, isEnterLeaveEvent} from "./Api/Events/EnterLeaveEvent";
import {OpenPopupEvent} from "./Api/Events/OpenPopupEvent";
import {isButtonClickedEvent} from "./Api/Events/ButtonClickedEvent";
import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent";
import {OpenTabEvent} from "./Api/Events/OpenTabEvent";
import {GoToPageEvent} from "./Api/Events/GoToPageEvent";
import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent";
interface WorkAdventureApi {
sendChatMessage(message: string, author: string): void;
onChatMessage(callback: (message: string) => void): void;
onEnterZone(name: string, callback: () => void): void;
onLeaveZone(name: string, callback: () => void): void;
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup;
openTab(url : string): void;
goToPage(url : string): void;
openCoWebSite(url : string): void;
closeCoWebSite(): void;
disablePlayerControl() : void;
restorePlayerControl() : void;
displayBubble() : void;
removeBubble() : void;
}
declare global {
// eslint-disable-next-line no-var
var WA: WorkAdventureApi
}
type ChatMessageCallback = (message: string) => void;
type ButtonClickedCallback = (popup: Popup) => void;
const userInputChatStream: Subject<UserInputChatEvent> = new Subject();
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
let popupId = 0;
interface ButtonDescriptor {
/**
* The label of the button
*/
label: string,
/**
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
*/
className?: "normal"|"primary"|"success"|"warning"|"error"|"disabled",
/**
* Callback called if the button is pressed
*/
callback: ButtonClickedCallback,
}
class Popup {
constructor(private id: number) {
}
/**
* Closes the popup
*/
public close(): void {
window.parent.postMessage({
'type': 'closePopup',
'data': {
'popupId': this.id,
} as ClosePopupEvent
}, '*');
}
}
window.WA = {
/**
* Send a message in the chat.
* Only the local user will receive this message.
*/
sendChatMessage(message: string, author: string) {
window.parent.postMessage({
'type': 'chat',
'data': {
'message': message,
'author': author
} as ChatEvent
}, '*');
},
disablePlayerControl() : void {
window.parent.postMessage({'type' : 'disablePlayerControl'},'*');
},
restorePlayerControl() : void {
window.parent.postMessage({'type' : 'restorePlayerControl'},'*');
},
displayBubble() : void {
window.parent.postMessage({'type' : 'displayBubble'},'*');
},
removeBubble() : void {
window.parent.postMessage({'type' : 'removeBubble'},'*');
},
openTab(url : string) : void{
window.parent.postMessage({
"type" : 'openTab',
"data" : {
url
} as OpenTabEvent
},'*');
},
goToPage(url : string) : void{
window.parent.postMessage({
"type" : 'goToPage',
"data" : {
url
} as GoToPageEvent
},'*');
},
openCoWebSite(url : string) : void{
window.parent.postMessage({
"type" : 'openCoWebSite',
"data" : {
url
} as OpenCoWebSiteEvent
},'*');
},
closeCoWebSite() : void{
window.parent.postMessage({
"type" : 'closeCoWebSite'
},'*');
},
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
const popup = new Popup(popupId);
const btnMap = new Map<number, () => void>();
popupCallbacks.set(popupId, btnMap);
let id = 0;
for (const button of buttons) {
const callback = button.callback;
if (callback) {
btnMap.set(id, () => {
callback(popup);
});
}
id++;
}
window.parent.postMessage({
'type': 'openPopup',
'data': {
popupId,
targetObject,
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
};
})
} as OpenPopupEvent
}, '*');
popups.set(popupId, popup)
return popup;
},
/**
* Listen to messages sent by the local user, in the chat.
*/
onChatMessage(callback: ChatMessageCallback): void {
userInputChatStream.subscribe((userInputChatEvent) => {
callback(userInputChatEvent.message);
});
},
onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
enterStreams.set(name, subject);
}
subject.subscribe(callback);
},
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
if (subject === undefined) {
subject = new Subject<EnterLeaveEvent>();
leaveStreams.set(name, subject);
}
subject.subscribe(callback);
},
}
window.addEventListener('message', message => {
if (message.source !== window.parent) {
return; // Skip message in this event listener
}
const payload = message.data;
console.log(payload);
if (isIframeEventWrapper(payload)) {
const payloadData = payload.data;
if (payload.type === 'userInputChat' && isUserInputChatEvent(payloadData)) {
userInputChatStream.next(payloadData);
} else if (payload.type === 'enterEvent' && isEnterLeaveEvent(payloadData)) {
enterStreams.get(payloadData.name)?.next();
} else if (payload.type === 'leaveEvent' && isEnterLeaveEvent(payloadData)) {
leaveStreams.get(payloadData.name)?.next();
} else if (payload.type === 'buttonClickedEvent' && isButtonClickedEvent(payloadData)) {
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
const popup = popups.get(payloadData.popupId);
if (popup === undefined) {
throw new Error('Could not find popup with ID "'+payloadData.popupId+'"');
}
if (callback) {
callback(popup);
}
}
}
// ...
});

View File

@ -15,6 +15,8 @@ import {MenuScene} from "./Phaser/Menu/MenuScene";
import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
import {discussionManager} from "./WebRtc/DiscussionManager";
const {width, height} = coWebsiteManager.getGameSize();
@ -119,3 +121,5 @@ coWebsiteManager.onResize.subscribe(() => {
const {width, height} = coWebsiteManager.getGameSize();
game.scale.resize(width / RESOLUTION, height / RESOLUTION);
});
iframeListener.init();

View File

@ -4,7 +4,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.ts',
entry: {
'main': './src/index.ts',
'iframe_api': './src/iframe_api.ts'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
@ -34,7 +37,11 @@ module.exports = {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: '[name].[contenthash].js',
filename: (pathData) => {
// Add a content hash only for the main bundle.
// We want the iframe_api.js file to keep its name as it will be referenced from outside iframes.
return pathData.chunk.name === 'main' ? 'js/[name].[contenthash].js': '[name].js';
},
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
@ -54,7 +61,8 @@ module.exports = {
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}
},
chunks: ['main']
}
),
new webpack.ProvidePlugin({

View File

@ -0,0 +1,25 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Assets from: workadventure@thecodingmachine.com
BASE assets:
------------
- le-coq.png
- logotcm.png
- pin.png
- tileset1-repositioning.png
- tileset1.png
- tileset2.2.png
- tileset2.png
- tileset3.2.png
- tileset3.png
- walls2.png

BIN
maps/Tuto/Male 13-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
maps/Tuto/fantasy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

74
maps/Tuto/scriptTuto.js Normal file
View File

@ -0,0 +1,74 @@
var isFirstTimeTuto = false;
var textFirstPopup = 'Hey ! This is how to start a discussion with someone ! You can be 4 max in a bubble.';
var textSecondPopup = 'You can also use the chat to communicate ! ';
var targetObjectTutoBubble ='Tutobubble';
var targetObjectTutoChat ='tutoChat';
var targetObjectTutoExplanation ='tutoExplanation';
var popUpExplanation = undefined;
function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{
label: "Next",
className: "popUpElement",
callback: (popup) => {
popup.close();
WA.openPopup(targetObjectTutoChat, textSecondPopup, [
{
label: "Open Chat",
className: "popUpElement",
callback: (popup1) => {
WA.sendChatMessage("Hey you can talk here too!", 'WA Guide');
popup1.close();
WA.openPopup("TutoFinal","You are good to go! You can meet the dev team and discover the features in the next room!",[
{
label: "Got it!",
className : "success",callback:(popup2 => {
popup2.close();
WA.restorePlayerControl();
})
}
])
}
}
])
}
}
]);
WA.disablePlayerControl();
}
WA.onEnterZone('popupZone', () => {
WA.displayBubble();
if (!isFirstTimeTuto) {
isFirstTimeTuto = true;
launchTuto();
}
else {
popUpExplanation = WA.openPopup(targetObjectTutoExplanation, 'Do you want to review the explanation?', [
{
label: "No",
className: "error",
callback: (popup) => {
popup.close();
}
},
{
label: "Yes",
className: "success",
callback: (popup) => {
popup.close();
launchTuto();
}
}
])
}
});
WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble();
})

BIN
maps/Tuto/shift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
maps/Tuto/textTuto3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,4 @@
LPC Atlas :
https://opengameart.org/content/lpc-tile-atlas
LPC Atlas 2 :
https://opengameart.org/content/lpc-tile-atlas2

View File

@ -0,0 +1,139 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Note the file is based on the LCP contest readme so don't expect the exact little pieces used like the base one.
*Additional license information.
Assets from:
LPC participants:
----------------
Casper Nilsson
*GNU GPL 3.0 or later
email: casper.nilsson@gmail.com
Freenode: CasperN
OpenGameArt.org: C.Nilsson
- LPC C.Nilsson (2D art)
Daniel Eddeland
*GNU GPL 3.0 or later
- Tilesets of plants, props, food and environments, suitable for farming / fishing sims and other games.
- Includes wheat, grass, sand tilesets, fence tilesets and plants such as corn and tomato.
Johann CHARLOT
*GNU LGPL Version 3.
*Later versions are permitted.
Homepage http://poufpoufproduction.fr
Email johannc@poufpoufproduction.fr
- Shoot'em up graphic kit
Skyler Robert Colladay
- FeralFantom's Entry (2D art)
BASE assets:
------------
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrel.png
- brackish.png
- buckets.png
- bridges.png
- cabinets.png
- cement.png
- cementstair.png
- chests.png
- country.png
- cup.png
- dirt2.png
- dirt.png
- dungeon.png
- grassalt.png
- grass.png
- holek.png
- holemid.png
- hole.png
- house.png
- inside.png
- kitchen.png
- lava.png
- lavarock.png
- mountains.png
- rock.png
- shadow.png
- signs.png
- stairs.png
- treetop.png
- trunk.png
- waterfall.png
- watergrass.png
- water.png
- princess.png and princess.xcf
Stephen Challener (AKA Redshrike)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- female_walkcycle.png
- female_hurt.png
- female_slash.png
- female_spellcast.png
- male_walkcycle.png
- male_hurt.png
- male_slash.png
- male_spellcast.png
- male_pants.png
- male_hurt_pants.png
- male_fall_down_pants.png
- male_slash_pants.png
Charles Sanchez (AKA CharlesGabriel)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bat.png
- bee.png
- big_worm.png
- eyeball.png
- ghost.png
- man_eater_flower.png
- pumpking.png
- slime.png
- small_worm.png
- snake.png
Manuel Riecke (AKA MrBeast)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- hairfemale.png and hairfemale.xcf
- hairmale.png and hairmale.xcf
- soldier.png
- soldier_altcolor.png
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Castle work:
- castlewalls.png
- castlefloors.png
- castle_outside.png
- castlefloors_outside.png
- castle_lightsources.png

View File

@ -0,0 +1,166 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Note only some files from the entries are selected.
*Additional license information.
Assets from:
LPC participants:
----------------
Barbara Rivera
- tree,tombstone
Casper Nilsson
*GNU GPL 3.0 or later
email: casper.nilsson@gmail.com
Freenode: CasperN
OpenGameArt.org: C.Nilsson
- LPC C.Nilsson (2D art)
Chris Phillips
- tree
Daniel Eddeland
*GNU GPL 3.0 or later
- Tilesets of plants, props, food and environments, suitable for farming / fishing sims and other games.
- Includes wheat, grass, sand tilesets, fence tilesets and plants such as corn and tomato.
- Also includes village/marketplace objects like sacks, food, some smithing equipment, tables and stalls.
Anamaris and Krusmira (aka? Emilio J Sanchez)
- Sierra__Steampun-a-fy (with concept art)
Jonas Klinger
- Skorpio's SciFi Sprite Pack
Joshua Taylor
- Fruit and Veggie Inventory
Leo Villeveygoux
- Limestone Wall
Mark Weyer
- signpost+shadow
Matthew Nash
- Public Toilet Tileset
Skyler Robert Colladay
- FeralFantom's Entry
BASE assets:
------------
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrel.png
- brackish.png
- buckets.png
- bridges.png
- cabinets.png
- cement.png
- cementstair.png
- chests.png
- country.png
- cup.png
- dirt2.png
- dirt.png
- dungeon.png
- grassalt.png
- grass.png
- holek.png
- holemid.png
- hole.png
- house.png
- inside.png
- kitchen.png
- lava.png
- lavarock.png
- mountains.png
- rock.png
- shadow.png
- signs.png
- stairs.png
- treetop.png
- trunk.png
- waterfall.png
- watergrass.png
- water.png
- princess.png and princess.xcf
Stephen Challener (AKA Redshrike)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- female_walkcycle.png
- female_hurt.png
- female_slash.png
- female_spellcast.png
- male_walkcycle.png
- male_hurt.png
- male_slash.png
- male_spellcast.png
- male_pants.png
- male_hurt_pants.png
- male_fall_down_pants.png
- male_slash_pants.png
Charles Sanchez (AKA CharlesGabriel)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bat.png
- bee.png
- big_worm.png
- eyeball.png
- ghost.png
- man_eater_flower.png
- pumpking.png
- slime.png
- small_worm.png
- snake.png
Manuel Riecke (AKA MrBeast)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- hairfemale.png and hairfemale.xcf
- hairmale.png and hairmale.xcf
- soldier.png
- soldier_altcolor.png
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Castle work:
- castlewalls.png
- castlefloors.png
- castle_outside.png
- castlefloors_outside.png
- castle_lightsources.png

View File

@ -0,0 +1,2 @@
https://opengameart.org/content/lpc-interior-castle-tiles
credit Lanea Zimmerman

View File

@ -0,0 +1,55 @@
= Wooden floor(CC-BY-SA) =
* Horizontal wooden floor by Lanea Zimmerman (AKA Sharm)
* Horizontal wooden floor with hole by Lanea Zimmerman (AKA Sharm) and Tuomo Untinen
* Vertical wooden floor by Tuomo Untinen
* Vertical wooden floor with hole by Tuomo Untinen
= Wooden wall topping (CC-BY-SA) =
* Tuomo Untinen
= Pile of barrels =
* Based LPC base tiles by Lanea Zimmerman (AKA Sharm)
= Decorational stuff (CC-BY-SA) =
* Green bottle
* Wine glass and bottle
* Hanging sacks
* Wall mounted rope and ropes
* Wall mounted swords
* Wall mounted kite shield
* Wall hole
By Tuomo Untinen
* Small sack from LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
* Purple bottles and gray lantern from Hyptosis Mage city
* Green and blue bottle by Tuomo Untinen
= Wall clock (CC-BY-SA) =
* Lanea Zimmerman AKA Sharm
* Tuomo Untinen (Scaled down and animation)
= Stone floor (CC-BY-SA)=
* Tuomo Untinen
= Cobble stone floor (CC-BY-SA)=
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
= Cabinets and kitchen stuff including metal stove(CC-BY-SA) =
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Cutboard is made by Hyptosis
* Sacks based on LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
* Spears by Tuomo Untinen
* Vertical chest by Tuomo Untinen based on LPC base tiles Lanea Zimmerman (AKA Sharm)
Manuel Riecke (AKA MrBeast)
= Skull (CC-BY-SA) =
* http://opengameart.org/content/lpc-dungeon-elements
* Graphical artist Lanea Zimmerman AKA Sharm
* Contributor William Thompson
= pile sacks =
* LPC farming tileset by Daniel Eddeland (http://opengameart.org/content/lpc- farming-tilesets-magic-animations-and-ui-elements)
= Pile of papers(CC-BY-SA) =
* Based on caeles papers
= Armor shelves(CC-BY-SA) =
* Based on LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Armors by: Adapted by Matthew Krohn from art created by Johannes Sjölund
= Table lamp =
* Tuomo Untinen
= Distiller =
* Table is from LPC base tileset by Lanea Zimmerman (AKA Sharm)
* Distiller by Tuomo Untinen
= Fireplace =
* Tuomo Untinen
* Inspired by Lanea Zimmerman (AKA Sharm) Fireplace

View File

@ -0,0 +1,28 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
If you need to figure out exactly who made what please see the Liberated Pixel Cup entries.
Liberated Pixel Cup Assets:
http://opengameart.org/lpc-art-entries
LPC participants:
----------------
Johann CHARLOT
Homepage http://poufpoufproduction.fr
Email johannc@poufpoufproduction.fr
- Shoot'em up graphic kit
Recolored Leaves
William Thompson
Email: william.thompsonj@gmail.com
OpenGameArt.org: williamthompsonj

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-leaf-recolor

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-submissions-merged

View File

@ -0,0 +1 @@
https://opengameart.org/content/lpc-mountains

View File

@ -0,0 +1,344 @@
License
-------
CC-BY-SA 3.0:
http://creativecommons.org/licenses/by-sa/3.0/
GNU GPL 3.0:
http://www.gnu.org/licenses/gpl-3.0.html
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Terrain and Outside:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) - CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- barrels
- darkishgreen water
- buckets
- bridges
- cement
- cement stairs
- chests
- cup
- light dirt
- mid dirt
- dungeon
- grass1 (leftmost)
- grass2 (Middle grass)
- hole1 (left hole near lava)
- hole2 (middle hole)
- hole3 (black whole next to transparent water)
- lava
- lavarock (black dirt)
- mountains ridge (right of the water tiles)
- white rocks
- waterfall
- water/grass
- water (transparent water beside black hole)
Daniel Eddeland CC-BY-SA-3.0 / GPL 3.0
https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Plowed Ground
- Water reeds
- Sand
- Sand with water
- Tall grass
- Wheat
- Young wheet (green wheat left of tall grass)
William Thompsonj
https://opengameart.org/content/lpc-sandrock-alt-colors
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- sand (near the wheat)
- sand with water
- grey dirt left of the lava)
Matthew Krohn (Makrohn)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Cave / Mountain steps originals from Lanea Zimmerman
Matthew Nash
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- stone tile (purplish color beside the dirt path bottom right corner)
Nushio
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Ice tiles
- Snow
- snow/ice
- Snow water
Casper Nilson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- grass with flowers
- stone pattern below cement
- tree stumps
- lily pads
MISSING:
Bricks / Paths above lillypads and left of barrels
The recoloing of the rocks on the left
The bigger stump
Bottom right tiles
Outside stone head and columns
Green water
Ladders
Brown path
Sewer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Outside Objects:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Tree trunks (for evergreen and oak)
- Tree Tops (for evergreen and oak)
Daniel Eddeland
(https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Farming stuff including food / crops / sack of grains
- Logs / Anvils
- Fish / boats / pier
- Bazaar / Merchant displays
- Wooden Fences
Caeless
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Shadowless veggies (originally made by Daniel Eddeland)
William Thompsonj
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- fallen leaves
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Metal Fence
- Wheelbarrows
- tent
- Gravestones
- harbor (stone platform in water)
- long boat
Barbara Rivera / C Phillips
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Leafless tree
Skorpio
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Trash / Barrel ( Top right corner)
MISSING:
Bricks bottom left
- Mushrooms need attributions
Bricks/tiles above pier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Exterior:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- house (light red brick wall on far left and purple roof below it)
- house white door frame / brown door and white windows.
- signs under gold and drak brown brick house wall
Daniel Armstrong (AKA HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- castle walls
- castle floors
- castle outside
- castle floors outside
Xenodora CC-BY-3.0 / GPL 3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- water well
Tuomo Untinen CC-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- stone bridge
- silver windows
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Asain themed shrine including red lantern
- foodog statue
- Toro
- Cherry blossom tree
- Torii
- Grey Roof
- Flag poles
- Fancy white and black entrance
- white door
- green and white walls / roof / windows
- shrub plant in white square pot
- flower box
Steampun-a-fy: Amaris / Krusimira
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- dark purple brick near purple roof
- bronze and wood house (gold and dark brick)
- dark wooden stairs
- gold and dark chimney
- grey door
- gears
- pipes
- dark wooden windows
Leo Villeveygoux
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- white bricks (limestone wall)
Skyler Robert Collady
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Grey home assets
MISSING attributions:
Graves bottom right
Water filled boat
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm) CC-BY-3.0 / GPL 3.0 / GPL 2.0 / OGA-BY-3.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- bookshelves (to the left of light brown chairs)
- cabinents above bookshelves
- counters (to the right of the kitchen table (between the white bathroom tiles) and bottom left by the rug.
- blue wallpaper
- kitchen furnace, pots and sink.
- country inside (blue bed and light brown chairs)
- red royal bed with white pillows.
- Mahogany kitchen table (near the dungeon bed with a black blanket)
- Yellow curtains
- Flower pot with tall pink flower
- Empty flower pots
- Chairs with gold seats (between the fireplaces)
- Fireplaces
- White and red mahogany stairs
- Double Rounded doors
- Flower vases
- Purple/Blue Tiles near white brick wall
- White brick wall
- White Columns
- Black candle-holder stand with candles
- Royal rug beside cabinents
- White stairs with runway / platform
- Grandfather clock
- Blue wallpaper with woodem trim
- Wood tiles
- Long painting
- Royal chairs ( gold seats)
- Rounded white windows
- Portrait painting
- small end / round tables
- Royal bed with red pillows
- white china
By Sharm but Commissioned by William Thompsonj
- campfire
- skeletons
- dungeon beds
- wood chairs and tables between the calderon and the fancy door rounded door
- calderon
- cobwebs
- dungeon prison wall and door/gate (beside fancy red and gold rugs)
- dirt by the dungeon beds
- rat
Daniel Armstrong (Aka HughSpectrum)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- castle light Sources (torches)
- red carpets
- grey brick top of walls
Matthew Nash
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Public Toilets
- Bathroom tiles (white and black)
Tuomo Untinen
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Pots with cover (based from Sharm's pots)
- Yellow stone floor
- Short paintings
- royal chair modification
- cupboards based on Sharm's (with china)
- small footchair
- piano
MISSING attributions:
Some bottomleft tiles
Banners
Sideways table
Stacked barrels
Stacked chess
Things to left of the pots
Some of the furniture below the beds
Some of the single beds
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*Interior 2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- brown stairs with blue
- Fountain
- Pool
- FLoor tiles
- Everything between top row (between toilet stalls and bookcases) down to the floor tiles
Xenodora
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Steel floor
Tuomo Untinen
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Armor and sheilds
- Cheese and bread
- Ship
Janna - CC0
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Beds
- Dressers / book shelves
- Wardrobe
Casper Nilsson
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Red and Blue stairs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Extensions Folder:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lanea Zimmerman (AKA Sharm)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
https://opengameart.org/content/lpc-adobe-building-set
https://opengameart.org/content/lpc-arabic-elements
- Adobe / Arabic - Commissioned by William Thompsonj

View File

@ -0,0 +1,12 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
Author :
Jacques-Olivier Farcy
https://interstices.ouvaton.org
https://twitter.com/JO_Interstices

View File

@ -0,0 +1,106 @@
## Flowers / Plants / Fungi / Wood
"[LPC] Flowers / Plants / Fungi / Wood," by bluecarrot16, Guido Bos, Ivan Voirol (Silver IV), SpiderDave, William.Thompsonj, Yar, Stephen Challener and the Open Surge team (http://opensnc.sourceforge.net), Gaurav Munjal, Johann Charlot, Casper Nilsson, Jetrel, Zabin, Hyptosis, Surt, Lanea Zimmerman, George Bailey, ansimuz, Buch, and the Open Pixel Project contributors (OpenPixelProject.com).
CC-BY-SA 3.0.
Based on:
[LPC] Guido Bos entries cut up
Guido Bos
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-guido-bos-entries-cut-up
Basic map 32x32 by Silver IV
Ivan Voirol (Silver IV)
CC-BY 3.0 / GPL 3.0 / GPL 2.0
https://opengameart.org/content/basic-map-32x32-by-silver-iv
Flowers
SpiderDave
CC0
https://opengameart.org/content/flowers
[LPC] Leaf Recolor
William.Thompsonj
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-leaf-recolor
Isometric 64x64 Outside Tileset
Yar
CC-BY 3.0
https://opengameart.org/content/isometric-64x64-outside-tileset
32x32 (and 16x16) RPG Tiles--Forest and some Interior Tiles
Stephen Challener and the Open Surge team (http://opensnc.sourceforge.net)commissioned by Gaurav Munjal
CC-BY 3.0
https://opengameart.org/content/32x32-and-16x16-rpg-tiles-forest-and-some-interior-tiles
Lots of Hyptosis' tiles organized!
Hyptosis
CC-BY 3.0
https://opengameart.org/content/lots-of-hyptosis-tiles-organized
Generic Platformer Tiles
surt
CC0
http://opengameart.org/content/generic-platformer-tiles
old frogatto tile art
Guido Bos
CC0
https://opengameart.org/content/old-frogatto-tile-art
LPC: Interior Castle Tiles
Lanea Zimmerman
CC-BY-3.0 / GPL 3.0
http://opengameart.org/content/lpc-interior-castle-tiles
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
Shoot'em up graphic kit
Johann Charlot
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/shootem-up-graphic-kit
LPC C.Nilsson
Casper Nilsson
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-cnilsson
Lots of trees and plants from OGA (DB32) tilesets pack 1
Jetrel, Zabin, Hyptosis, Surt
CC0
https://opengameart.org/content/lots-of-trees-and-plants-from-oga-db32-tilesets-pack-1
Trees & Bushes
ansimuz
CC0
https://opengameart.org/content/trees-bushes
Outdoor tiles, again
Buch <https://opengameart.org/users/buch>
CC-BY 2.0
https://opengameart.org/content/outdoor-tiles-again
16x16 Game Assets
George Bailey
CC-BY 4.0
https://opengameart.org/content/16x16-game-assets
Tuxemon tileset
Buch
CC-BY-SA 3.0
https://opengameart.org/content/tuxemon-tileset
Orthographic outdoor tiles
Buch
CC0
https://opengameart.org/content/orthographic-outdoor-tiles
OPP2017 - Jungle and temple set
OpenPixelProject.com
CC0
https://opengameart.org/content/opp2017-jungle-and-temple-set

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

View File

@ -0,0 +1,101 @@
## Medieval
[LPC] Hanging signs
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-hanging-signs
Liberated Pixel Cup (LPC) Base Assets
Lanea Zimmerman (Sharm)
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles
[LPC] City outside
Reemax (Tuomo Untinen), Xenodora, Sharm, Johann C, Johannes Sjölund
CC-BY-SA 3.0 / GPL 3.0 / GPL 2.0
https://opengameart.org/content/lpc-city-outside
[LPC] Cavern and ruin tiles
CC-BY-SA 3.0 / GPL 3.0 / GPL 2.0
Reemax, Sharm, Hyptosis, Johann C, HughSpectrum, Redshrike, William.Thompsonj, wulax,
https://opengameart.org/node/33913
Statues & Fountains Collection
Casper Nilsson, Daniel Cook, Rayane Félix (RayaneFLX), Wolthera van Hövell tot Westerflier (TheraHedwig), Hyptosis, mold, Zachariah Husiar (Zabin), & Clint Bellanger
CC-BY-SA 3.0
https://opengameart.org/content/statues-fountains-collection
LPC C.Nilsson
Casper Nilsson
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-cnilsson
LPC Style Well
CC-BY 3.0 / GPL 3.0
Xenodora, Sharm
https://opengameart.org/content/lpc-style-well
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
[LPC] Guido Bos entries cut up
Guido Bos
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-guido-bos-entries-cut-up
LPC Sign Post
Nemisys
CC-BY 3.0 / CC-BY-SA 3.0 / GPL 3.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-sign-post
[LPC] Signposts, graves, line cloths and scare crow
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-signposts-graves-line-cloths-and-scare-crow
[LPC] Hanging signs
Reemax
CC-BY-SA 3.0 / GPL 3.0
https://opengameart.org/content/lpc-hanging-signs
Hyptosis
Mage City Arcanos
CC0
https://opengameart.org/content/mage-city-arcanos
[LPC] Street Lamp
Curt
CC-BY 3.0
https://opengameart.org/content/lpc-street-lamp
[LPC] Misc
Lanea Zimmerman (Sharm), William.Thompsonj
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-misc
RPG Tiles: Cobble stone paths & town objects
https://opengameart.org/content/rpg-tiles-cobble-stone-paths-town-objects
Zabin, Daneeklu, Jetrel, Hyptosis, Redshrike, Bertram.
CC-BY-SA 3.0
[LPC] Farming tilesets, magic animations and UI elements
https://opengameart.org/content/lpc-farming-tilesets-magic-animations-and-ui-elements
Daniel Eddeland (daneeklu)
CC-BY-SA 3.0 / GPL 3.0
RPG item set
Jetrel
CC0
https://opengameart.org/content/rpg-item-set
RPG Indoor Tileset: Expansion 1
Redshrike
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/rpg-indoor-tileset-expansion-1
[LPC] Dungeon Elements
Lanea Zimmerman (Sharm), William.Thompsonj
CC-BY 3.0 / GPL 3.0 / GPL 2.0 / OGA-BY 3.0
https://opengameart.org/content/lpc-dungeon-elements

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

706
maps/Tuto/tutoV3.json Normal file

File diff suppressed because one or more lines are too long

99
maps/tests/goToPage.json Normal file
View File

@ -0,0 +1,99 @@
{ "compressionlevel":-1,
"height":20,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":20,
"id":4,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":20,
"id":3,
"name":"popupZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"popUpGoToPageZone"
}],
"type":"tilelayer",
"visible":true,
"width":20,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":5,
"name":"floorLayer",
"objects":[
{
"height":59,
"id":1,
"name":"popUp",
"rotation":0,
"type":"",
"visible":true,
"width":152,
"x":247,
"y":11
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":2,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"goToPageScript.js"
}],
"renderorder":"right-down",
"tiledversion":"1.5.0",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":20
}

View File

@ -0,0 +1,49 @@
var zoneName = "popUpGoToPageZone";
var urlPricing = "https://workadventu.re/pricing";
var urlGettingStarted = "https://workadventu.re/getting-started";
var isCoWebSiteOpened = false;
WA.onChatMessage((message => {
WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot');
}));
WA.onEnterZone(zoneName, () => {
WA.openPopup("popUp","Open Links",[
{
label: "Open Tab",
className: "popUpElement",
callback: (popup => {
WA.openTab(urlPricing);
popup.close();
})
},
{
label: "Go To Page", className : "popUpElement",
callback:(popup => {
WA.goToPage(urlPricing);
popup.close();
})
}
,
{
label: "openCoWebSite", className : "popUpElement",
callback:(popup => {
WA.openCoWebSite(urlPricing);
isCoWebSiteOpened = true;
popup.close();
})
}]);
})
WA.onLeaveZone(zoneName, () => {
if (isCoWebSiteOpened) {
WA.closeCoWebSite();
isCoWebSiteOpened = false;
}
})
WA.onLeaveZone('popupZone', () => {
})

25
maps/tests/iframe.html Normal file
View File

@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
<script>
</script>
</head>
<body>
<button id="sendchat">Send chat message</button>
<script>
document.getElementById('sendchat').onclick = () => {
WA.sendChatMessage('Hello world!', 'Mr Robot');
}
</script>
<div id="chatSent"></div>
<script>
WA.onChatMessage((message => {
const chatDiv = document.createElement('p');
chatDiv.innerText = message;
document.getElementById('chatSent').append(chatDiv);
}));
</script>
</body>
</html>

View File

@ -0,0 +1,94 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":5,
"name":"iframe_api",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"iframe.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":10
}

79
maps/tests/script.js Normal file
View File

@ -0,0 +1,79 @@
console.log('SCRIPT LAUNCHED');
//WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot');
var isFirstTimeTuto = false;
var textFirstPopup = 'Hey ! This is how to open start a discussion with someone ! You can be 4 max in a booble';
var textSecondPopup = 'You can also use the chat to communicate ! ';
var targetObjectTutoBubble ='tutoBobble';
var targetObjectTutoChat ='tutoChat';
var popUpExplanation = undefined;
function launchTuto (){
WA.openPopup(targetObjectTutoBubble, textFirstPopup, [
{
label: "Next",
className: "popUpElement",
callback: (popup) => {
popup.close();
WA.openPopup(targetObjectTutoChat, textSecondPopup, [
{
label: "Open Chat",
className: "popUpElement",
callback: (popup1) => {
WA.sendChatMessage("Hey you can talk here too ! ", 'WA Guide');
popup1.close();
WA.restorePlayerControl();
}
}
])
}
}
]);
WA.disablePlayerControl();
}
WA.onChatMessage((message => {
console.log('CHAT MESSAGE RECEIVED BY SCRIPT');
WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot');
}));
WA.onEnterZone('myTrigger', () => {
WA.sendChatMessage("Don't step on my carpet!", 'Poly Parrot');
})
WA.onLeaveZone('popupZone', () => {
})
WA.onEnterZone('notExist', () => {
WA.sendChatMessage("YOU SHOULD NEVER SEE THIS", 'Poly Parrot');
})
WA.onEnterZone('popupZone', () => {
WA.displayBubble();
if (!isFirstTimeTuto) {
isFirstTimeTuto = true;
launchTuto();
}
else popUpExplanation = WA.openPopup(targetObjectTutoChat,'Do you want to review the explanation ? ', [
{
label: "No",
className: "popUpElementReviewexplanation",
callback: (popup) => {
popup.close();
}
},
{
label: "Yes",
className: "popUpElementReviewexplanation",
callback: (popup) => {
popup.close();
launchTuto();
}
}
])
});
WA.onLeaveZone('popupZone', () => {
if (popUpExplanation !== undefined) popUpExplanation.close();
WA.removeBubble();
})

135
maps/tests/script_api.json Normal file
View File

@ -0,0 +1,135 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":6,
"name":"triggerZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"myTrigger"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":7,
"name":"popupZone",
"opacity":1,
"properties":[
{
"name":"zone",
"type":"string",
"value":"popupZone"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":147.135497146101,
"id":1,
"name":"myPopup2",
"rotation":0,
"type":"",
"visible":true,
"width":104.442827410047,
"x":142.817125079855,
"y":147.448134926559
},
{
"height":132.434722966794,
"id":2,
"name":"myPopup1",
"rotation":0,
"type":"",
"visible":true,
"width":125.735549178518,
"x":13.649632619596,
"y":50.8502491249093
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":3,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"script.js"
}],
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}