v0.8.2: Finished news implementation, including a full news scene

This commit is contained in:
Evan Debenham 2020-07-18 18:26:24 -04:00
parent 0d9715b732
commit 81bdd42e7e
10 changed files with 450 additions and 19 deletions

View File

@ -66,6 +66,27 @@ public class GameSettings {
return defValue;
}
}
public static long getLong( String key, long defValue ) {
return getLong(key, defValue, Long.MIN_VALUE, Long.MAX_VALUE);
}
public static long getLong( String key, long defValue, long min, long max ) {
try {
long i = get().getLong( key, defValue );
if (i < min || i > max){
long val = (long)GameMath.gate(min, i, max);
put(key, val);
return val;
} else {
return i;
}
} catch (ClassCastException e) {
//ShatteredPixelDungeon.reportException(e);
put(key, defValue);
return defValue;
}
}
public static boolean getBoolean( String key, boolean defValue ) {
try {
@ -101,6 +122,11 @@ public class GameSettings {
get().putInteger(key, value);
get().flush();
}
public static void put( String key, long value ) {
get().putLong(key, value);
get().flush();
}
public static void put( String key, boolean value ) {
get().putBoolean(key, value);

View File

@ -47,6 +47,15 @@ scenes.interlevelscene.io_error=Cannot read save file. If this error persists af
scenes.introscene.text=Many heroes have ventured into the dungeon before you from the city above. Some have returned with treasures and magical artifacts, most have never been heard from again.\n\nNone, however, have ventured to the bottom and retrieved the Amulet of Yendor, which is said to be guarded by an ancient evil in the depths. Even now dark energy radiates from below, making its way up into the city.\n\nYou consider yourself ready for the challenge. Most importantly, you feel that fortune smiles upon you. It's time to start your own adventure!
scenes.newsscene.title=Game News
scenes.newsscene.read_more=Read More
scenes.newsscene$newsinfo.english_warn=News posts are written by the developer and are only available in English.
scenes.newsscene$newsinfo.metered_network=Couldn't check for news posts, as you're connected to a metered network, such as mobile data.
scenes.newsscene$newsinfo.enable_data=Check on Mobile Data
scenes.newsscene$newsinfo.no_internet=Couldn't check for news posts, make sure you're connected to the internet.
scenes.newsscene$newsinfo.news_disabled=You have disabled checking for news posts, so none will appear here.
scenes.newsscene$newsinfo.enable_news=Enable News
scenes.rankingsscene.title=Top Rankings
scenes.rankingsscene.total=Games Played:
scenes.rankingsscene.no_games=No games have been played yet.

View File

@ -24,6 +24,7 @@ package com.shatteredpixel.shatteredpixeldungeon;
import com.shatteredpixel.shatteredpixeldungeon.messages.Languages;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.scenes.PixelScene;
import com.watabou.noosa.Game;
import com.watabou.noosa.audio.Music;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.GameSettings;
@ -260,6 +261,8 @@ public class SPDSettings extends GameSettings {
public static final String KEY_UPDATES = "updates";
public static final String KEY_WIFI = "wifi";
public static final String KEY_NEWS_LAST_READ = "news_last_read";
public static void news(boolean value){
put(KEY_NEWS, value);
}
@ -283,7 +286,16 @@ public class SPDSettings extends GameSettings {
public static boolean WiFi(){
return getBoolean(KEY_WIFI, true);
}
public static void newsLastRead(long lastRead){
put(KEY_NEWS_LAST_READ, lastRead);
}
public static long newsLastRead(){
//returns the current time when none is stored, so historical news isn't seen as unread
return getLong(KEY_NEWS_LAST_READ, Game.realTime);
}
//Window management (desktop only atm)
public static final String KEY_WINDOW_WIDTH = "window_width";

View File

@ -0,0 +1,313 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2020 Evan Debenham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.shatteredpixel.shatteredpixeldungeon.scenes;
import com.shatteredpixel.shatteredpixeldungeon.Chrome;
import com.shatteredpixel.shatteredpixeldungeon.SPDSettings;
import com.shatteredpixel.shatteredpixeldungeon.ShatteredPixelDungeon;
import com.shatteredpixel.shatteredpixeldungeon.messages.Languages;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.services.news.News;
import com.shatteredpixel.shatteredpixeldungeon.services.news.NewsArticle;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.ui.Archs;
import com.shatteredpixel.shatteredpixeldungeon.ui.ExitButton;
import com.shatteredpixel.shatteredpixeldungeon.ui.Icons;
import com.shatteredpixel.shatteredpixeldungeon.ui.RedButton;
import com.shatteredpixel.shatteredpixeldungeon.ui.RenderedTextBlock;
import com.shatteredpixel.shatteredpixeldungeon.ui.StyledButton;
import com.shatteredpixel.shatteredpixeldungeon.ui.Window;
import com.shatteredpixel.shatteredpixeldungeon.windows.WndTitledMessage;
import com.watabou.noosa.BitmapText;
import com.watabou.noosa.Camera;
import com.watabou.noosa.Game;
import com.watabou.noosa.NinePatch;
import com.watabou.noosa.ui.Component;
import com.watabou.utils.DeviceCompat;
import java.util.ArrayList;
import java.util.Calendar;
public class NewsScene extends PixelScene {
boolean displayingNoArticles = false;
private static final int BTN_HEIGHT = 20;
private static final int BTN_WIDTH = 100;
@Override
public void create() {
super.create();
uiCamera.visible = false;
int w = Camera.main.width;
int h = Camera.main.height;
int fullWidth = PixelScene.landscape() ? 202 : 100;
int left = (w - fullWidth)/2;
Archs archs = new Archs();
archs.setSize(w, h);
add(archs);
ExitButton btnExit = new ExitButton();
btnExit.setPos(w - btnExit.width(), 0);
add(btnExit);
RenderedTextBlock title = PixelScene.renderTextBlock(Messages.get(this, "title"), 9);
title.hardlight(Window.TITLE_COLOR);
title.setPos(
(w - title.width()) / 2f,
(20 - title.height()) / 2f
);
align(title);
add(title);
float top = 20;
displayingNoArticles = !News.articlesAvailable();
if (displayingNoArticles || Messages.lang() != Languages.ENGLISH) {
Component newsInfo = new NewsInfo();
newsInfo.setRect(left, 20, fullWidth, 0);
add(newsInfo);
top = newsInfo.bottom();
}
if (!displayingNoArticles) {
ArrayList<NewsArticle> articles = News.articles();
float articleSpace = h - top - 2;
int rows = articles.size();
if (PixelScene.landscape()){
rows /= 2;
}
rows++;
while ((articleSpace) / (BTN_HEIGHT+1) < rows) {
articles.remove(articles.size() - 1);
if (PixelScene.landscape()) {
articles.remove(articles.size() - 1);
}
rows--;
}
float gap = ((articleSpace) - (BTN_HEIGHT * rows)) / (float)rows;
boolean rightCol = false;
for (NewsArticle article : articles) {
StyledButton b = new ArticleButton(article);
if (!rightCol) {
top += gap;
b.setRect( left, top, BTN_WIDTH, BTN_HEIGHT);
} else {
b.setRect( left + fullWidth - BTN_WIDTH, top, BTN_WIDTH, BTN_HEIGHT);
}
align(b);
add(b);
if (!PixelScene.landscape()) {
top += BTN_HEIGHT;
} else {
if (rightCol){
top += BTN_HEIGHT;
}
rightCol = !rightCol;
}
}
top += gap;
} else {
top += 20;
}
StyledButton btnSite = new StyledButton(Chrome.Type.GREY_BUTTON_TR, Messages.get(this, "read_more")){
@Override
protected void onClick() {
super.onClick();
DeviceCompat.openURI("https://ShatteredPixel.com/");
}
};
btnSite.icon(Icons.get(Icons.NEWS));
btnSite.textColor(Window.TITLE_COLOR);
btnSite.setRect(left, top, fullWidth, BTN_HEIGHT);
add(btnSite);
}
@Override
public void update() {
if (displayingNoArticles && News.articlesAvailable()){
ShatteredPixelDungeon.seamlessResetScene();
}
super.update();
}
private static class NewsInfo extends Component {
NinePatch bg;
RenderedTextBlock text;
RedButton button;
@Override
protected void createChildren() {
bg = Chrome.get(Chrome.Type.GREY_BUTTON_TR);
add(bg);
String message = "";
if (Messages.lang() != Languages.ENGLISH){
message += Messages.get(this, "english_warn");
}
if (!News.articlesAvailable()){
if (SPDSettings.news()) {
if (SPDSettings.WiFi() && !Game.platform.connectedToUnmeteredNetwork()) {
message += "\n\n" + Messages.get(this, "metered_network");
button = new RedButton(Messages.get(this, "enable_data")) {
@Override
protected void onClick() {
super.onClick();
SPDSettings.WiFi(false);
News.checkForNews();
ShatteredPixelDungeon.seamlessResetScene();
}
};
add(button);
} else {
message += "\n\n" + Messages.get(this, "no_internet");
}
} else {
message += "\n\n" + Messages.get(this, "news_disabled");
button = new RedButton(Messages.get(this, "enable_news")) {
@Override
protected void onClick() {
super.onClick();
SPDSettings.news(true);
News.checkForNews();
ShatteredPixelDungeon.seamlessResetScene();
}
};
add(button);
}
}
if (message.startsWith("\n\n")) message = message.replaceFirst("\n\n", "");
text = PixelScene.renderTextBlock(message, 6);
text.hardlight(CharSprite.WARNING);
add(text);
}
@Override
protected void layout() {
bg.x = x;
bg.y = y;
text.maxWidth((int)width - bg.marginHor());
text.setPos(x + bg.marginLeft(), y + bg.marginTop());
height = (text.bottom()) - y;
if (button != null){
height += 4;
button.setSize(button.reqWidth()+2, 16);
button.setPos(x + (width - button.width())/2, y + height);
height = button.bottom() - y;
}
height += bg.marginBottom();
bg.size(width, height);
}
}
private static class ArticleButton extends StyledButton {
NewsArticle article;
BitmapText date;
public ArticleButton(NewsArticle article) {
super(Chrome.Type.GREY_BUTTON_TR, article.title, 6);
this.article = article;
icon(News.parseArticleIcon(article));
if (article.date.getTime() > SPDSettings.newsLastRead()) textColor(Window.SHPX_COLOR);
Calendar cal = Calendar.getInstance();
cal.setTime(article.date);
date = new BitmapText( News.parseArticleDate(article), pixelFont);
date.scale.set(PixelScene.align(0.49f));
date.hardlight( 0x888888 );
date.measure();
add(date);
}
@Override
protected void layout() {
text.maxWidth( (int)(width - icon.width() - bg.marginHor() - 2));
if (date != null) {
date.x = x + width - bg.marginRight() - date.width() + 2;
date.y = y + height - bg.marginBottom() - date.baseLine() + 2;
}
super.layout();
}
@Override
protected void onClick() {
super.onClick();
textColor(Window.WHITE);
if (article.date.getTime() > SPDSettings.newsLastRead()){
SPDSettings.newsLastRead(article.date.getTime());
}
ShatteredPixelDungeon.scene().addToFront(new WndArticle(article));
}
}
private static class WndArticle extends WndTitledMessage {
public WndArticle(NewsArticle article ) {
super(News.parseArticleIcon(article), article.title, article.summary);
RedButton link = new RedButton(Messages.get(NewsScene.class, "read_more")){
@Override
protected void onClick() {
super.onClick();
DeviceCompat.openURI(article.URL);
}
};
link.setRect(0, height + 2, width, BTN_HEIGHT);
add(link);
resize(width, (int) link.bottom());
}
}
}

View File

@ -151,7 +151,7 @@ public class TitleScene extends PixelScene {
btnBadges.icon(Icons.get(Icons.BADGES));
add(btnBadges);
StyledButton btnNews = new NewsButtons(GREY_TR, Messages.get(this, "news"));
StyledButton btnNews = new NewsButton(GREY_TR, Messages.get(this, "news"));
btnNews.icon(Icons.get(Icons.NEWS));
add(btnNews);
@ -214,9 +214,9 @@ public class TitleScene extends PixelScene {
add( fb );
}
private static class NewsButtons extends StyledButton {
private static class NewsButton extends StyledButton {
public NewsButtons( Chrome.Type type, String label ){
public NewsButton(Chrome.Type type, String label ){
super(type, label);
if (SPDSettings.news()) News.checkForNews();
}
@ -232,8 +232,6 @@ public class TitleScene extends PixelScene {
if (unreadCount > 0){
unreadCount = Math.min(unreadCount, 9);
text(text() + "(" + unreadCount + ")");
} else {
SPDSettings.newsLastRead(Game.realTime);
}
}
@ -245,7 +243,7 @@ public class TitleScene extends PixelScene {
@Override
protected void onClick() {
super.onClick();
//ShatteredPixelDungeon.switchNoFade( NewsScene.class );
ShatteredPixelDungeon.switchNoFade( NewsScene.class );
}
}

View File

@ -21,7 +21,15 @@
package com.shatteredpixel.shatteredpixeldungeon.services.news;
import com.shatteredpixel.shatteredpixeldungeon.SPDSettings;
import com.shatteredpixel.shatteredpixeldungeon.ShatteredPixelDungeon;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
import com.shatteredpixel.shatteredpixeldungeon.ui.Icons;
import com.watabou.noosa.Image;
import com.watabou.utils.DeviceCompat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
public class News {
@ -39,7 +47,7 @@ public class News {
if (!supportsNews()) return;
if (lastCheck != null && (new Date().getTime() - lastCheck.getTime()) < CHECK_DELAY) return;
service.checkForArticles(false, new NewsService.NewsResultCallback() {
service.checkForArticles(!SPDSettings.WiFi(), !DeviceCompat.legacyDevice(), new NewsService.NewsResultCallback() {
@Override
public void onArticlesFound(ArrayList<NewsArticle> articles) {
lastCheck = new Date();
@ -62,7 +70,7 @@ public class News {
}
public static ArrayList<NewsArticle> articles(){
return articles;
return new ArrayList<>(articles);
}
public static int unreadArticles(Date lastRead){
@ -78,4 +86,40 @@ public class News {
lastCheck = null;
}
public static Image parseArticleIcon(NewsArticle article){
try {
//recognized formats are:
//"ICON: <name of enum constant in Icons.java>"
if (article.icon.startsWith("ICON: ")){
return Icons.get(Icons.valueOf(article.icon.replace("ICON: ", "")));
//"ITEM: <integer constant corresponding to values in ItemSpriteSheet.java>"
} else if (article.icon.startsWith("ITEM: ")){
return new ItemSprite(Integer.parseInt(article.icon.replace("ITEM: ", "")));
//"<asset filename>, <tx left>, <tx top>, <width>, <height>"
} else {
String[] split = article.icon.split(", ");
return new Image( split[0],
Integer.parseInt(split[1]),
Integer.parseInt(split[2]),
Integer.parseInt(split[3]),
Integer.parseInt(split[4]));
}
//if we run into any formatting errors (or icon is null), default to the news icon
} catch (Exception e){
if (article.icon != null) ShatteredPixelDungeon.reportException(e);
return Icons.get(Icons.NEWS);
}
}
public static String parseArticleDate(NewsArticle article){
Calendar cal = Calendar.getInstance();
cal.setTime(article.date);
return cal.get(Calendar.YEAR)
+ "-" + String.format("%02d", cal.get(Calendar.MONTH)+1)
+ "-" + String.format("%02d", cal.get(Calendar.DAY_OF_MONTH));
}
}

View File

@ -29,10 +29,11 @@ import java.util.Date;
public class DebugNews extends NewsService {
@Override
public void checkForArticles(boolean useMetered, NewsResultCallback callback) {
public void checkForArticles(boolean useMetered, boolean forceHTTPS, NewsResultCallback callback) {
if (!useMetered && !Game.platform.connectedToUnmeteredNetwork()){
callback.onConnectionFailed();
return;
}
//turn on to test connection failure
@ -41,6 +42,11 @@ public class DebugNews extends NewsService {
return;
}
boolean testUnread = false;
//start placing articles either at the current time (if testing unread count)
// or 10 days after 1st jan 1970
long startTime = testUnread ? Game.realTime : 10*1000*60*60*24;
ArrayList<NewsArticle> articles = new ArrayList<>();
for (int i = 0; i < 10; i++){
NewsArticle article = new NewsArticle();
@ -51,10 +57,14 @@ public class DebugNews extends NewsService {
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit " +
"esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " +
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
// 10 to 1 days after Jan 1st 1970
article.date = new Date(startTime - (i)*1000*60*60*24);
article.URL = "http://www.google.com";
// a year in the past, plus one day for each article
long daysBack = 365+i;
article.date = new Date(System.currentTimeMillis() - (daysBack*1000*60*60*24));
//debug icon!
article.icon = "sprites/spinner.png, 144, 0, 16, 16";
articles.add(article);
}

View File

@ -34,14 +34,19 @@ import java.util.Locale;
public class ShatteredNews extends NewsService {
@Override
public void checkForArticles(boolean useMetered, NewsResultCallback callback) {
public void checkForArticles(boolean useMetered, boolean preferHTTPS, NewsResultCallback callback) {
if (!useMetered && !Game.platform.connectedToUnmeteredNetwork()){
callback.onConnectionFailed();
return;
}
Net.HttpRequest httpGet = new Net.HttpRequest(Net.HttpMethods.GET);
httpGet.setUrl("http://shatteredpixel.com/feed");
if (preferHTTPS) {
httpGet.setUrl("https://shatteredpixel.com/feed");
} else {
httpGet.setUrl("http://shatteredpixel.com/feed");
}
Gdx.net.sendHttpRequest(httpGet, new Net.HttpResponseListener() {
@Override
@ -61,7 +66,17 @@ public class ShatteredNews extends NewsService {
Game.reportException(e);
}
article.summary = xmlArticle.get("summary");
article.URL = xmlArticle.get("id").replace("https://", "http://");
article.URL = xmlArticle.getChildByName("link").getAttribute("href");
if (!preferHTTPS) {
article.URL = article.URL.replace("https://", "http://");
}
try {
article.icon = xmlArticle.getChildByName("category").getAttribute("term");
} catch (Exception e){
article.icon = null;
}
articles.add(article);
}
callback.onArticlesFound(articles);

View File

@ -26,8 +26,12 @@ import java.util.Date;
public class NewsArticle {
public String title;
public String summary;
public String URL;
public Date date;
public String summary;
public String URL;
//the icon is stored as a string here so it can be decoded to an image later
//See News.java for supported formats
public String icon;
}

View File

@ -30,6 +30,6 @@ public abstract class NewsService {
public abstract void onConnectionFailed();
}
public abstract void checkForArticles(boolean useMetered, NewsResultCallback callback);
public abstract void checkForArticles(boolean useMetered, boolean forceHTTPS, NewsResultCallback callback);
}